巡检照片与工单附件:jquick-pdf 图片嵌入的业务实战
引入
商品合同、巡检单和理赔报告常要把照片放进 PDF。真正的难点不是写一个image标签,而是处理路径、尺寸、失败重试、临时文件和不可信 URL。本文以“巡检报告封面照片”为例,讨论这类业务的资源治理:照片从哪来、怎么落到磁盘、什么时候删、批量导出时内存如何控制。模板侧只使用已验证的本地图片标签,网络图片在进入模板之前就被转换为受控本地文件。
核心讲解
现场照片的业务约束
巡检、工单类照片有三个特点:数量多(一次巡检可能十几张)、单张体积大(手机原图常见数 MB)、来源是用户设备且文件名不可信。它们直接决定三件事:必须设置像素与字节上限;必须支持批量且低内存地导出;必须有失败兜底,不能让一张坏图拖垮整单导出。
依赖与运行基线
JDK 8+、Maven 3.6+,文档层核心依赖如下:
<dependency><groupId>io.github.paohaijiao</groupId><artifactId>jquick-pdfx</artifactId><version>4.0.0</version></dependency>按需追加jquick-pdf-font、jquick-pdf-svg、jquick-pdf-data、jquick-pdf-css。把img.png放到项目工作目录,或改用绝对路径。
完整示例:ImageReportDemo
importcom.github.paohaijiao.executor.JQuickPdfFactory;// 导入工厂importjava.nio.file.Files;// 导入文件工具importjava.nio.file.Paths;// 导入路径工具publicclassImageReportDemo{// 声明类publicstaticvoidmain(String[]args)throwsException{// 声明入口StringimagePath="src/main/resources/image/img.png";// 受控图片路径Stringtemplate="<pdf><body>"// 建立文档骨架+"<h1>'巡检报告'</h1>"// 报告标题+"<p>'设备编号:EQ-1001'</p>"// 业务字段+"<image src=\""+imagePath+"\" style=\"width:200px;height:150px\"></image>"// 嵌入照片+"<p>'现场照片已归档'</p>"// 说明文字+"</body></pdf>";// 结束模板byte[]pdf=newJQuickPdfFactory().executeContent(template);// 执行渲染Files.write(Paths.get("image-report.pdf"),pdf);// 写出 PDF}}模板与步骤拆解
- 先确认文件存在,并使用规范路径分隔符。
- 用
<pdf><body>建立文档骨架。 - 文本节点使用单引号,变量才使用
${name}。 <image>的src指向受控文件,width、height控制版面。- 执行
executeContent得到byte[]并写出。
网络图片的流程是:HTTP 客户端设置连接与读取超时,校验 HTTPS、响应码、Content-Type 与大小,下载至服务端随机命名的临时文件,模板引用该文件,导出完成后删除。安全校验的具体清单见上一篇“资源接入与安全边界”,本篇不重复。
关键细节
Windows 路径写法与转义
- 相对路径相对于进程工作目录,不一定是源码目录;生产环境建议使用绝对路径。
- 反斜杠在 Java 字符串里要写成
\\,在模板属性里也容易出错,统一使用/更省事,例如D:/pdf/image.png。 - 路径拼接不要用字符串相加把用户输入直接拼进去,避免路径穿越;文件名由服务端生成。
临时文件生命周期与清理
- 下载的图片写入隔离的临时目录,文件名使用 UUID 或内容哈希。
- 用 try/finally 或框架的资源钩子在导出结束后删除,异常路径同样要清理。
- 同一批次共用的图片(如同一设备的多份报告)只落地一次,批次内复用,批末统一清理。
图片缓存策略
- 以业务图片 ID 或内容 SHA-256 作为缓存键,命中则跳过下载,直接复用本地文件。
- 缓存需要 TTL 与容量上限,避免无限增长;缓存区与临时区分离,互不干扰。
- 缓存失效要有明确触发条件(图片 ID 变更、TTL 到期、容量淘汰),否则旧图会长期占据磁盘。
- 缓存命中率直接影响批量导出的耗时,巡检类业务通常能命中同一设备的多份报告。
下载超时与像素上限
- 连接超时、读取超时都要设置;批量下载要限制并发,避免连接池与线程被打满。
- 只校验字节大小不够,还要限制像素尺寸。一个 4000×3000 的位图解码为位图数据约 4000×3000×4 字节,接近 46MB;手机原图很常见,所以解码内存必须单独设上限。
- 超限图片降级为缩略图或占位图,保证报告本身能正常产出。
批量图片导出的内存控制
executeContent返回byte[],会整体驻留内存;图片越多越大,峰值越高。批量导出的典型骨架是逐张处理、立即落盘:
PathtempDir=Files.createTempDirectory("jquick-batch-");// 批次级隔离目录try{for(inti=0;i<photoIds.size();i++){// 逐张处理,避免整批驻留内存Pathphoto=resolvePhoto(photoIds.get(i),tempDir);// 命中缓存则复用,否则下载到临时目录Stringtemplate="<pdf><body><h1>'巡检报告'</h1><image src=\""+photo// 单张模板+"\" style=\"width:200px;height:150px\"></image></body></pdf>";// 控制版面尺寸byte[]pdf=newJQuickPdfFactory().executeContent(template);// 生成单份 PDFFiles.write(Paths.get("report-"+i+".pdf"),pdf);// 立即落盘,尽快释放if((i+1)%10==0){cleanupFinished(tempDir);}// 每 10 张清理已用完的临时图片}}finally{deleteRecursively(tempDir);// 正常与异常路径都要清理}resolvePhoto、cleanupFinished、deleteRecursively属于应用层辅助方法,不依赖任何模板能力。要点是分批、及时落盘、控制单页图片数量,必要时用areaBreak让每页图片数可预期。
实战说明
验证步骤
- 把
img.png放到工作目录,或用绝对路径改写imagePath。 - 运行示例,确认输出
image-report.pdf的封面显示 200×150 的照片与上下文字。 - 把
imagePath改成不存在的文件,确认业务层给出明确错误,而不是产出无照片的报告。 - 用 10 张以上照片做一次批量导出,观察峰值内存,并检查临时目录在结束后是否被清空。
结果预期
- 单张:报告中按指定尺寸显示照片,文字与图片顺序与模板一致。
- 批量:各批次文件均生成成功,临时目录在批次结束后为空,峰值内存随批次规模线性可控。
- 失败样本:坏图被替换为占位图或使该批次明确失败,不会生成缺图但报告成功的文件。
- 并发:多任务同时导出时各自使用独立临时目录,互相不会覆盖文件。
生产注意事项
- 原图过大会显著增加 PDF 体积,不能只看 CSS 尺寸定价,要按原图像素与压缩质量设上限。
- 网络图片存在 SSRF、重定向与恶意超大文件风险,先按上一篇的校验清单过滤再进入模板。
- PNG 透明背景在不同阅读器中的视觉表现可能不同。
- 并发导出时限制下载与 PDF 任务队列,避免内存峰值叠加。
- 巡检照片常带 EXIF 方向信息,方向纠正属于应用层处理,不在模板能力范围内。
- 适用于商品主图、证件照、签章前置照片、质检留痕、巡检照片等场景。
- 可进一步增加图片方向纠正、缩略图缓存与失败占位页,这些属于应用层逻辑,不是本文验证的模板能力。
总结
- 本文的关键结论:图片 PDF 的稳定性来自资源治理——受控目录、确定性命名、缓存与清理、超时与像素上限、分批导出;模板只负责版面。
- 常见误区:在模板里拼用户 URL、用 URL 原始文件名落地、只限字节不限像素、一次性把整批图片读进内存。
- 把资源安全与版面渲染分层,巡检、工单类照片报告才能稳定上线。本文只依赖 jquick-pdfx 4.0.0 已验证的 image 语法;网络直读未获源码证实,因此采用“下载后本地渲染”的替代方案。版本基线:jquick-pdfx 4.0.0、JDK 8+,升级前请核对 README_zh.md 的版本对照表;更多示例见 GitHub 仓库。