十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

医院OA系统集成UEditor:PDF文献图片高效转存实战

医院OA系统集成UEditor:PDF文献图片高效转存实战 医院OA系统集成百度UMEDITOR后如何高效处理PDF文献中的图片转存做医院OA系统集成时最容易被低估的一类需求就是文档编辑区的内容搬运。我去年在处理一家二甲医院OA系统升级时医生反馈最多的痛点之一就是在百度UMEDITOR以下简称UEditor里编辑病案讨论、科研笔记时需要把PDF文献里的图片、影像截图、解剖图、统计图表转存进在线文档。UEditor本身能处理剪贴板粘贴图片但PDF是个封闭的“包裹”图片无法直接拖拽或粘贴只能先截图再另存再上传。这个流程一旦遇到几十页的文献效率低到让人怀疑人生。后来我直接在OA里加了一条“PDF图片转存”通道医生上传PDF后端自动提取文献中的图片按需压缩、格式转换、存储到附件服务再自动回填到UEditor编辑区。整个过程十几秒比原来人工截图上传省了大把时间。这篇文章会把这套方案的技术选型、实现细节和踩坑记录完整写出来重点覆盖C#服务端的处理链路适合正在做Web系统编辑器集成的开发朋友参考。1. 场景拆解为什么PDF图片转存会成为OA集成的老大难1.1 医院OA环境下的特殊约束医院OA系统和普通企业的OA不太一样它有大量的历史文档、病案资料和科研文献文件格式五花八门。很多文献资料是PDF格式有的是出版社排版好的“电子版PDF”有的是老文献扫描后合成的“扫描版PDF”甚至还有不少医生把手机拍的照片、显微镜图、影像系统导出的图硬塞进PDF里。这些PDF文件里的图片质量参差不齐格式也不固定直接提取会碰到各种问题。更麻烦的是医院OA基本跑在专网或内网环境很多服务器不允许随意装第三方软件也不允许直接访问外链。这意味着我没办法用在线PDF解析API所有处理必须部署在本地服务还要考虑CPU、内存占用因为OA服务器上还跑着审批流、公文收发、患者档案管理等一堆服务。我之前见过有的团队引入了一整套基于Java的大型PDF处理中间件结果部署在内网环境里折腾了两周最后因为依赖冲突放弃了。所以在选型时轻量、可离线部署、依赖少是硬指标。1.2 百度UMEDITOR的上传机制与扩展点先说UEditor本身。UEditor是一个富文本编辑器支持图片本地上传、Base64编码上传、远程图片抓取等功能。默认情况下医生在编辑区内粘贴图片时编辑器会先把图片转成Base64字符串再通过配置好的serverUrl传给后端保存。这种机制对单张图片挺方便但它默认不处理“从PDF里面把图片抠出来”这种操作。UEditor其实留了两个可以扩展的入口第一个是自定义上传接口第二个是编辑器的ready事件后追加自定义按钮或工具条。我当时没有改UEditor的源码只加了一个独立的图片转存工具按钮。点开后弹出上传PDF的面板上传完成就把提取到的图片批量插入编辑区。这样对原有代码侵入最小后续升级UEditor也方便。当然还有更激进的做法就是在前端用JavaScript解析PDF直接调用pdf.js把PDF页面渲染成Canvas再转成图片插入编辑区。这个方案我一开始也试过确实不需要后端参与但问题也很明显得到的只是“整页截图”不是PDF里面嵌的原图放大后清晰度不够而且遇到几百页的PDF时浏览器会卡死。后来我还是把主流程放到了后端前端只保留上传和回显。前端能做到的事不一定适合做尤其医学文献对图像清晰度要求很高原图提取永远优先于页面截图。2. 方案选型三条路线怎么选2.1 解析PDF内嵌图片对象直接提取PDF文件本质上是一种对象集合里面的每张图片通常对应一个Image XObject。如果能直接读取这些图像对象就能拿到PDF内嵌的原图质量最高也能保留图像的原始分辨率。在.NET生态里我测试过好几个库最终主要用了UglyToad.PdfPig。这是一个开源的PDF解析库跨平台能读取PDF里的文本、图像对象性能也不错。它的API设计比较干净提取图片只需要遍历页面里的图像对象。另一个常见选择是iTextSharp它在PDF生成与编辑领域很有名但新一代iText 7在有商业版和AGPL许可问题医院的信息科对许可证通常很敏感一听到AGPL就怕惹上法律风险。所以我建议优先用MIT协议的PdfPig来做读取操作如果只是提取图片它完全够用。直接提取内嵌图片会遇到的第一个难题是PDF里存的图片未必是常见格式。有些PDF会内嵌JPEG2000、JPX、CCITT传真编码、JBIG2等格式这些格式普通System.Drawing无法直接处理需要额外解码。PdfPig在某些版本的API里会帮你转换一部分格式但遇到冷门编码还是要自己写解码或转码逻辑。现实场景里医学期刊的PDF绝大多数用的是DCTDecode也就是JPEG或FlateDecode压缩后的位图主流库都能处理不必过度焦虑。2.2 整页渲染后裁剪适合扫描版PDF如果PDF是扫描版也就是说整页就是一张大图那么“提取内嵌图片”的路线就不适用了因为你会提取到一整张页面图像而不是文献里的某个图表。这时候需要换一种思路把PDF页面渲染成位图再用图像处理方式进行裁剪。渲染PDF页面在.NET里可用方案也不少我试过PDFiumGoogle开源PDF渲染引擎的.NET封装PdfiumViewer渲染速度快对复杂排版还原度高而且它能直接输出位图。扫描版PDF经过这一步后得到一张高分辨率页面图像再按内容区域自动裁剪成若干子图就能满足“按图插入”的需求。不过整页渲染这条路也有代价一是渲染300DPI的A4页面单张内存可能到几十MB批量处理时内存峰值很高二是“自动裁剪”很难做到百分百准确经常把两栏论文的两张图拼在一起或者把标题文字也切了进去。所以这个方案我一般只作为备选用来处理扫描版PDF而不是作为默认路线。后文我会讲到如何先判断PDF属于哪种类型再决定走哪条处理管线。2.3 混合方案才是最优解我的最终选型是混合方案先用PdfPig提取内嵌图片如果提取出来的图片数量为零或者图片尺寸远小于页面的可视尺寸再判定为疑似扫描版触发PDFium整页渲染。这样既保证了电子版PDF的原图质量又兜住了扫描版PDF的极端情况。举一个我自己实测过的例子一份外科手术图谱PDF60页里面嵌入了约90张图片其中大部分是300DPI的彩色JPEG直接提取后单张在200KB到1.2MB之间但其中有5页是80年代老文献的灰度扫描图直接提取出来是整页的TIFF编码数据根本没法直接用。混合方案处理完后前55页走原图提取后5页走整页渲染加适当地白边裁切最终医生在UEditor里看到的图文效果和PDF原版几乎一致。这个方案实现起来也不算复杂关键就是在管道入口加一个“PDF类型预判”的逻辑。我在预处理里会统计每个页面的图像对象数量、图像平均宽度与页面宽度的比例。如果某个页面的图像数量为0且页面内容本身是一张图片就标记为扫描页。对于整页都是图片的页面直接渲染该页即可无需再裁剪细节因为很多扫描文献本身就是整页信息医生可以自己用编辑器里的裁剪工具二次处理。3. 实操实现提取、编码、回填一步到位3.1 用PdfPig提取PDF内嵌图片先说后端提取图片的核心代码。假设我们已经拿到了上传的PDF文件字节数组用PdfPig的PdfDocument.Open打开遍历页面调用GetImages方法提取每个页面的图片对象。我贴一个精简版实现using UglyToad.PdfPig; using UglyToad.PdfPig.Content; public static ListPdfImageData ExtractEmbeddedImages(byte[] pdfBytes) { var result new ListPdfImageData(); using (var document PdfDocument.Open(pdfBytes)) { foreach (var page in document.GetPages()) { foreach (var image in page.GetImages()) { var bytes image.TryGetBytes(); if (!bytes.HasValue) { continue; } var rawBytes bytes.Value.ToArray(); var width image.Width; var height image.Height; result.Add(new PdfImageData { PageIndex page.Number, Width width, Height height, RawBytes rawBytes, OriginalName $page{page.Number}_img{width}x{height}.jpg }); } } } return result; } public class PdfImageData { public int PageIndex { get; set; } public int Width { get; set; } public int Height { get; set; } public byte[] RawBytes { get; set; } public string OriginalName { get; set; } }这里有几个细节要注意。第一TryGetBytes返回的原始字节不一定是标准JPEG或PNG。PdfPig对部分图片格式会原样返回但如果是SMask透明蒙版或者使用了索引颜色直接保存可能得到一张花屏图片。我在处理时会把原始字节保存到一个临时目录尝试用图片库识别真实格式识别不出来再交给转码管线。第二page.GetImages()是一个延迟加载的枚举遍历时会实际去解析PDF流。如果PDF页面数非常多比如几百页的文献建议用document.GetPages()分页处理不要一次性把所有页面的图片都加载进内存否则很容易OOM。我在压测一份350页的骨科文献时一次性加载所有图片导致内存直接飙到1.2GB后来改成逐页处理并即时保存到临时文件内存才降下来。第三文件名不要直接用原始PDF里的资源名因为不同PDF的图片内部命名规则差异很大有的叫/Im1有的叫/XObject0保存时直接用页码和尺寸命名方便后续日志排查。3.2 扫描版PDF的整页渲染兜底当提取结果异常时就需要进入扫描版处理分支。我用PdfiumViewer做页面渲染代码也不复杂using PdfiumViewer; public static byte[] RenderPageAsImage(byte[] pdfBytes, int pageIndex, int dpi 300) { using (var stream new MemoryStream(pdfBytes)) using (var document PdfDocument.Load(stream)) { using (var image document.Render(pageIndex, dpi, dpi, true)) { using (var outStream new MemoryStream()) { image.Save(outStream, System.Drawing.Imaging.ImageFormat.Png); return outStream.ToArray(); } } } }渲染时有个设置容易被忽略Render的第三个参数是dpiY最好和dpiX保持一致否则图像会变形。PdfiumViewer的Render方法还有一个布尔参数表示是否使用forceLinearization我一般传true目的是强制按线性化方式读取页面能减少某些畸形PDF导致渲染异常的概率。渲染的质量和速度受DPI影响很大。经验值是医学文献的图表细节多建议用300DPI如果PDF页面本身就是纯文字或简单图表150DPI可以大幅提升速度。我之前做性能对比一份200页的PDF全部渲染300DPI大约耗时40秒降到150DPI只要12秒输出图片体积也小很多。所以我在代码里会根据页面内容自动判断如果图像比例高、内容复杂用300DPI否则用150DPI。另外PdfiumViewer依赖原生PDFium库部署时需要在服务器上放对应的x86或x64原生DLL。这个很容易被忽略第一次部署在新服务器上时如果报DllNotFoundException大概率就是原生DLL没拷贝到位。医院OA服务器通常有两套环境一套测试一套正式我建议在自动化发布脚本里强制把原生DLL复制到bin目录避免漏掉。3.3 对接UEditor上传接口与图片回填提取完成之后就要把图片交给UEditor。UEditor的图片上传接口通常接收upfile表单字段也支持Base64方式。我这里没有走默认上传接口而是自己写了一个专用的批量转存接口因为在一次请求里批量回传多张图片对医生来说最友好。后端的核心逻辑大概长这样[HttpPost(api/pdf-image-transfer)] public async TaskIActionResult TransferPdfImages(IFormFile pdfFile) { if (pdfFile null || pdfFile.Length 0) { return Json(new { state FAIL, msg 未接收到PDF文件 }); } byte[] pdfBytes; await using (var ms new MemoryStream()) { await pdfFile.CopyToAsync(ms); pdfBytes ms.ToArray(); } var images PdfImagePipeline.Process(pdfBytes); var resultUrls new Liststring(); foreach (var image in images) { var optimizedBytes ImageOptimizer.Compress(image.RawBytes, image.Width, image.Height); var filePath SaveFile(optimizedBytes, image.OriginalName); var relativeUrl UrlHelper.GetRelativeUrl(filePath); resultUrls.Add(relativeUrl); } return Json(new { state SUCCESS, urls resultUrls, count resultUrls.Count }); }前端可以单独放一个上传入口也挂在UEditor的工具栏里。最简单的方式是使用UEditor的registerCommand或者自定义按钮。我在实际项目里是在UEditor工具栏加了一个“PDF转存”按钮点击后打开一个Modal弹窗弹窗里用input typefile acceptapplication/pdf上传PDF文件AJAX提交到上面的接口拿到返回的图片URL数组后遍历调用editor.execCommand(insertimage, {src: url})就能把图片批量插入编辑区。这里有一个交互细节医生使用场景中图片插入顺序必须和PDF页面里的顺序一致。如果后端在多线程处理时并发保存返回的URL列表乱序前端插入就会乱。我的解决办法是后端返回结果前先按PageIndex和图片在页面内的坐标排序保证图序稳定。PdfPig的Page.GetImages()返回顺序通常和页面内容流中的位置相关但为了保险我会额外读取图片在页面上的矩形坐标image.Bounds按坐标从上到下排序效果更稳定。3.4 医学文献的特殊处理让转存结果真正可用医学PDF里的图片和普通设计文档里的插图有个显著区别它往往包含大量需要精确分辨率的影像图、显微镜图、病理切片图。直接提取内嵌图后很可能出现两种情况图片分辨率过低预览能看一放大就糊或者色彩模式异常RGB图显示成紫绿色。所以在保存到OA附件服务之前我会加一个“图片标准化”环节核心做三件事第一统一格式。不管原始图片是JPEG、PNG、BMP还是TIFF最后统一转为JPEG格式质量参数设为90。JPEG在OA内网传输和浏览器显示兼容性最好体积也相对可控。但像显微镜图或病理切片这种需要保留细节的图JPEG的压缩会带来一定信息损失我会额外保留一份PNG原图插入UEditor时用PNG缩略展示时用JPEG。医学图像的信息保真需求永远是第一位的。第二检查颜色空间。PDF内嵌图片的颜色空间可能是DeviceGray、DeviceRGB、DeviceCMYK甚至是CalRGB。直接把CMYK的JPEG保存成JPG往往会得到一张颜色严重偏色的图。遇到CMYK我会先转成RGB。在.NET里用System.Drawing处理CMYK JPG有时会有System.Drawing内部的GDI限制遇到这种情况我会用ImageSharp或Magick.NET来做转换。Magick.NET对颜色空间的支持最完善只是原生依赖较多。第三尺寸过大时做降采样。有的医学影像图直接提取出来是5000×4000像素单张图片超过8MB浏览器加载很吃力。我的策略是超过3000像素的图先等比缩到2400px宽再保存同时把原始大图保存到附件库编辑区只插入压缩后的版本。医生如果需要原图点击图片就能看大图。医学文献里还有一类常见情况就是“整页PDF中包含双栏排版左右各有一个图表”。如果只是整页渲染出来的图会包含两个图表和中间的栏间距并不好用。我用一个简单的启发式裁剪渲染出页面位图后用图像像素行扫描法检测页面的“空白垂直带”如果存在接近页面高度的连续空白竖带且空白带宽度占页面宽度8%以上就把它作为分栏边界把页面切成左右两块区域再分别做边缘裁剪。这个方法虽然粗糙但对单栏、双栏、三栏学术论文的图表切分效果很好代码量也就几十行。4. 踩坑记录与性能调优4.1 常见问题速查表我在开发和上线过程中积累了不少问题。这里列一个速查表遇到对应症状可以直接对照排查。症状可能原因解决办法提取出来的图片是扭曲的色块未处理索引颜色或SMask透明蒙版使用Magick.NET统一转码先识别格式再转换ImageMagick原生库加载失败服务器缺少VC运行库安装对应版本的Visual C RedistributablePDFium报DllNotFoundException原生DLL未复制到bin目录复制x86/x64的pdfium.dll到部署目录大PDF处理时内存溢出一次性加载全部页面改为逐页处理用临时文件保存中间结果提取的图片顺序和PDF不一致不同PDF的内容流顺序不同按图片在页面上的Bounds坐标排序医生反馈图片发虚PDF里本身是72DPI的缩略图对低分辨率图片记录日志提醒医生注意原图质量CMYK图片颜色偏绿未转换色彩空间统一走Magick.NET做CMYK转RGB打印时报图片失真插入的是压缩JPG原图单独存储打印时自动替换为高分辨率原图这中间最值得说的是低分辨率问题。医学期刊PDF中有些图片本身网络版只有72DPI你怎么提取都不可能变成高清图。这时候如果转存回去医生会在OA里看到一张模糊图第一反应是系统有问题。我的做法是在提取管线里记录图片的DPI如果低于150DPI在返回给前端时附带一个lowRes标记前端插入图片时在图片左上角加一个“低清晰度”角标这样医生能一眼看出来这是PDF原图分辨率的问题而不是系统压缩造成的。4.2 性能优化与并发控制医院OA服务器资源有限不能指望它像后台批处理服务器一样狂吃CPU。针对这个约束我做了三个优化。第一个是接口超时策略。PDF转存是一个相对耗时的操作如果完全同步处理一份100页的PDF可能需要好几秒前端AJAX很容易超时。我设计的是PDF小于50页且预估图片数不超过30张时走同步接口直接返回结果超过这个阈值先返回“处理中”状态后台用IHostedService跑一个异步任务处理完后通过SignalR或定时轮询通知前端拉取结果。这个策略既保证了小文件秒出结果又避免了大文件卡死请求。第二个是缓存复用。同一个PDF文件用MD5作为指纹在短时间内重复上传时会直接返回上一次处理结果。医生经常会在试错过程中反复上传同一份文献这个缓存能省掉大量重复计算。处理结果的缓存时间我设置为一周存储路径记录在数据库里缓存文件存放在附件服务的一个独立目录。第三个是限制并发。我用了信号量控制同时处理的文件数默认最大并发为2避免多个医生同时上传大PDF把服务器内存占满。这个设置在上线初期非常重要。有一次医院信息科组织病案质量评比几十个医生同时上传PDF如果没有限流服务器直接假死。后来我做了信号量限流并把超过并发上限的请求排队处理系统就稳定了。private static readonly SemaphoreSlim _gate new SemaphoreSlim(2); public async TaskProcessResult ProcessWithSemaphore(byte[] pdfBytes) { await _gate.WaitAsync(); try { return await Task.Run(() PdfImagePipeline.Process(pdfBytes)); } finally { _gate.Release(); } }4.3 几个容易被忽视的细节代码写完了不代表能顺利上线医院OA系统还涉及几个比较“地域性”的细节我这里单独提一下。第一文件存储路径不能放在Web应用目录下尤其是临时文件。医院OA有等保要求Web目录里不应存放可被直接访问的临时文件否则会有文件越权访问风险。我把转存过程的临时目录统一放在了系统专用的数据盘并把该目录的匿名访问权限设为禁止。最终图片文件则保存到独立的附件存储服务由OA系统的附件服务统一提供访问控制。前端拿到的URL都是经过授权校验的相对地址这样医生才能安全查看图片。第二一定要记录审计日志。医院场景下涉及病案、科研资料的内容流转最好记录“哪个用户、在什么时间、上传了什么PDF、转存了几张图片”这样的日志。这既是合规要求也是排障利器。我见过不少系统上线之后出了图片丢失问题就是因为没有日志根本不知道图片去哪了。第三上传PDF之前先检查文件头。acceptapplication/pdf只是前端筛选后端要自己判断文件头。PDF文件头通常是%PDF-我用前5个字节判断不是就直接拒绝避免有人上传伪装成PDF的可执行文件。虽然OA系统在内网但该做的防护不能省。5. 展望与扩展这套方案还能延伸到哪5.1 从PDF转存到Word、PPT文档图片提取做完PDF图片转存之后顺手就可以把能力扩展到Word和PPT。Word和PPT本质上是ZIP包图片存放在word/media/或ppt/media/目录下用System.IO.Compression.ZipArchive直接解压就能拿到图片代码量极少。医生上传一份Word文档插入图片也是常见需求。我在OA里做了一版“文档转存”按钮支持PDF、Word、PPT三种格式底层共用同一套图片标准化和存储逻辑维护成本很低。5.2 接上OCR让扫描版PDF也能被检索扫描版PDF虽然通过整页渲染能转存出图片但图片里的文字信息在OA系统里是不可检索的。医院科研处经常会问“能不能在系统里搜到某篇文献里的某个术语”这就涉及OCR了。把扫描页渲染出的位图接入PaddleOCR或Tesseract提取文字后写入索引再结合图片坐标建立“文字↔图片”的映射关系就能实现全文检索定位图表。这个扩展我目前只做了最简单的验证但思路是完全可行的。5.3 图片去重与智能筛选文献PDF里往往夹杂着重复的插图、logo、装饰性图片。转存之后医生看到一堆重复图也会困惑。下一步可以考虑用感知哈希算法pHash对提取出的图片做去重同一页或跨页相似的图片只保留清晰度最高的一张这样插入编辑区时更干净。装饰性小图标则通过“面积比例色彩复杂度”过滤掉。我在内部版本里试过用ImageSharp计算pHash识别效果不错误删率约5%还需要人工确认但它足以把图片数量减少20%到30%体验提升明显。写在最后的实操心得回到标题里那个问题医院OA系统集成百度UMEDITOR后如何高效处理PDF文献中的图片转存我的答案一句话总结核心不是UEditor而是后端的PDF图片提取管线和图片标准化流程。UEditor只是一个展示与回填的壳真正决定医生体验的是提取出来的图片是否清晰、顺序是否正确、插入是否及时。我个人的经验是做这类功能不要一上来就堆代码先把PDF文件的“配方”摸清楚。我最初花了整整一天去解析医院OA里真实存在的PDF发现它们的来源五花八门有出版社原版、扫描仪输出的、手机拍照合集的还有从影像系统导出的。只有把PDF的类型分布和图片特征摸透了技术选型才不会跑偏。最后再分享一个部署层面的小技巧在新服务器上第一次跑通转存流程后一定要主动清理一次临时目录并重启应用进程。PDFium和Magick.NET这类带有原生依赖的库在首次加载时会释放很多临时DLL如果服务器权限配置不当偶发访问冲突的情况会让人排查到崩溃。把临时目录清理、DLL拷贝、权限检查写进初始化脚本里后续就能少很多麻烦。希望这篇实操记录能帮你少踩一些坑。
返回列表