jquick-pdf 图片嵌入:本地图片与网络图片的接入与安全边界
引入
把图片放进 PDF,传统方案要自己管理字节流、尺寸和资源生命周期。jquick-pdf 用 <image src="..." alt="..."> 在模板里表达图片位置,渲染器负责绘制,模板更接近业务页面。真正的风险不在标签怎么写,而在图片来源是否受控:本地路径越界、把任意用户 URL 交给渲染器、下载失败后生成一份“看似成功”却没有照片的 PDF。本文聚焦图片资源的接入方式与安全边界。
环境基线为 io.github.paohaijiao:jquick-pdfx:4.1.0 与 JDK 8+。
核心讲解
图片元素与本地路径
位图使用 <image src="..." alt="...">;src 指向本地文件路径(如 D:/pdf/image.png)。源码 sample 已验证写法:
<image src="src/main/resources/image/img.png" style="width:200px;height:150px"></image>
完整示例:ImageDemo
import com.github.paohaijiao.executor.JQuickPdfFactory; // 导入工厂
import java.nio.file.*; // 导入文件工具
public class ImageDemo {
// 声明类
public static void main(String[] args) throws Exception {
// 声明入口
String template = "<pdf><body><h1>'产品图片'</h1>" // 创建模板
+ "<image src=\"src/main/resources/image/img.png\" style=\"width:200px;height:150px\"></image>" // 嵌入受控本地图片
+ "</body></pdf>"; // 结束模板
byte[] pdf = new JQuickPdfFactory().executeContent(template); // 执行渲染
Files.write(Paths.get("image-report.pdf"), pdf); // 保存 PDF
}
}
本地图片的路径与尺寸配置
src 可以写相对路径,也可以写绝对路径;相对路径相对于进程工作目录,而不是源码目录,这一点在 IDE 与生产环境之间最容易踩坑。尺寸由 width、height 控制,单位支持 px、pt、mm、cm、in;建议两个维度都写,避免不同原图比例导致版面高度跳动。alt 是位图元素的替代文本属性,用来标注业务含义(如“设备正面照”),便于模板审阅与后续定位。是否可替换为其他图片格式,取决于运行环境对位图的解码支持。
网络图片先下载为受控本地文件
仓库没有验证渲染器直接读取 http/https 图片,因此 src不 可以直接写网络地址。保守且可落地的方案是:由后端用 HTTP 客户端把图片下载到受控本地目录,模板只接收该目录内的本地路径。下面是一个最小化实现,只用 JDK 自带能力(java.net、java.nio.file、java.util),把白名单、超时、类型与大小校验全部挡在进入模板之前:
private static Path downloadToLocal(String url, Set<String> allowHosts) throws Exception {
// 下载并落地
URL u = new URL(url); // 解析地址
if (!"https".equals(u.getProtocol()) || !allowHosts.contains(u.getHost())) {
// 协议与域名白名单
throw new IllegalArgumentException("host not allowed"); // 非白名单直接拒绝
}
Path dir = Files.createTempDirectory("jquick-img-"); // 隔离的受控临时目录
Path file = dir.resolve(UUID.randomUUID().toString() + ".png"); // 文件名由服务端生成
HttpURLConnection conn = (HttpURLConnection) u.openConnection(); // 打开连接
conn.setConnectTimeout(3000); // 连接超时 3s
conn.setReadTimeout(5000); // 读取超时 5s
String type = conn.getContentType(); // 响应类型
if (type == null || !type.startsWith("image/")) {
// 类型校验
throw new IllegalArgumentException("not an image"); // 拒绝非图片响应
}
long limit = 5L * 1024 * 1024; // 字节上限 5MB
try (InputStream in = conn.getInputStream(); // 输入流
OutputStream out = Files.newOutputStream(file)) {
// 输出流
byte[] buf = new byte[8192]; // 拷贝缓冲
long total = 0; int n; // 累计字节数
while ((n = in.read(buf)) > 0) {
// 边读边计
total += n; // 累加
if (total > limit) {
throw new IllegalArgumentException("image too large"); } // 超限立即中断
out.write(buf, 0, n); // 写入受控目录
}
}
return file; // 返回受控本地路径
}
调用方拿到的绝对路径拼进模板后,渲染侧与本地图片完全一致。注意字节数并不能约束解码内存,像素上限(宽×高)需要在读取图片元数据后单独判断。
关键细节
下载环节的安全校验(SSRF 防护)
- 域名白名单:只允许预先登记的域名或对象存储桶域名,拒绝 IP 直连、内网网段与非标准端口。
- 协议与响应码:只接受 HTTPS,并校验响应码。
- 重定向:限制跳转次数,每一跳都要重新校验目标主机是否仍在白名单内。
- Content-Type:只接受
image/png、image/jpeg等预期类型,不依赖文件扩展名判断。 - 大小与超时:设置连接超时与读取超时;下载时按字节累计,超过上限立即中断,避免超大文件打满磁盘或内存。
- 落地路径:写入隔离的受控临时目录,文件名由服务端生成,不使用 URL 中的原始文件名,避免路径穿越。
缺失图片与下载失败的兜底
- 占位图:在受控目录准备一张固定的 placeholder.png,失败时改用它,并在正文标注“图片获取失败”。
- 明确失败:涉及合规或审计的场景不做静默降级,直接抛错并记录失败原因。
- 分类与日志:区分超时、类型不符、超限、非白名单等原因,至少记录目标域名、失败分类与耗时,便于判断是网络问题还是配置问题。
- 重试边界:对连接超时等可重试错误做有限次退避重试,对类型不符、超限这类不可重试错误直接失败。
实战说明
验证步骤
- 准备一张受支持格式的图片,放到受控目录,记下路径。
- 用上方模板执行
executeContent,写入image-report.pdf。 - 打开 PDF,确认出现 200×150 的图片区域,且位置在标题之后。
- 把
src改成一个不存在的路径,观察异常与输出,确认失败路径不会产出无照片的“成功”文件。 - 网络图片:先手工下载到受控目录,再复用同一段
<image>模板渲染,确认“下载”与“渲染”两段边界清晰。
结果预期
- 本地图片存在时,PDF 中出现指定尺寸的图片。
- 路径错误时,渲染阶段抛出可定位的错误,而不是静默产出空白页。
- 网络图片下载成功后,模板侧与本地图片无差别。
常见问题排查
- 图片不显示:先确认路径是否存在、是否是相对工作目录,再确认模板里
src的双引号是否被错误转义。 - 尺寸与预期不符:检查是否只写了
width或height,以及单位是否在 px、pt、mm、cm、in 之列。 - 网络图片失败:按“白名单 → 协议与响应码 → Content-Type → 字节与像素上限 → 超时”的顺序逐项检查。
- PDF 体积异常:多半来自原图像素过大,而不是
width、height写大了。
生产注意事项
- 不要把任意用户 URL 直接交给渲染器;网络访问属于应用层能力,不承诺
http://直链可用。 - 及时清理临时文件,避免磁盘堆积。
- 注意 PNG 透明背景在不同阅读器中的显示差异,以及高 DPI 原图带来的体积增长,建议统一缩放策略。
- 适用于商品报告、证书、巡检照片、合同附件等场景。
- 图片数量多时,控制单次导出的图片数量与单页图片数量,避免 PDF 体积与内存峰值同时失控。
总结
- 本文的关键结论:模板侧只负责“把受控图片放进 PDF”,网络下载、安全校验、缓存与临时文件清理都应由后端承担。
- 常见误区:认为
<image src>可以直接吃网络 URL、把下载失败当成功、跳过 Content-Type 与大小校验、用 URL 原始文件名直接落地。 - 把资源安全与版面渲染分层,图片类 PDF 才能稳定上线。版本基线:jquick-pdfx 4.0.0、JDK 8+,本文示例基于仓库现有 image sample,升级前请核对 README_zh.md 的版本对照表;更多示例见 GitHub 仓库。