简介:面向Java开发人员的iText图片转PDF实操资源,解决将PNG等常见图片格式转为高清PDF文档的典型需求。实现关键在于设置合理DPI值保持图片原始清晰度,并配合缩放方式适配A4等页面尺寸,适合开发批量图片入册功能的Java后端工程师参考。压缩包内共11个文件,整体约1022KB,包含iText核心jar包、可直接运行的Java源码、JSP页面以及Eclipse工程配置,导入IDE即可运行demo并观察转换效果;源码完整演示了从创建Document、初始化PdfWriter、加载图片、设置高DPI、缩放图片到输出保存的调用链,也覆盖了页面尺寸与内存管理等常见注意事项。目前已有5467人学习使用,印证了无坑版demo的实用性。对想快速掌握iText图片转PDF、避开清晰度与布局坑位的开发者,可以复用其中的核心代码与参数配置,也能在此基础上继续扩展水印、页眉页脚等进阶功能。
1. 图片转 PDF 这件事,为什么需要单独做高清版
把一张 300 DPI 的 PNG 直接丢给 iText 生成 PDF,出来的文件打开常常是糊的——这跟阅读器无关,是 iText 默认按 72 DPI 渲染,物理尺寸被放大了 4 倍多,像素密度全乱了。这个反直觉的坑,就是“高清版”要专门解决的事。
这个 demo 聚焦一个点:读取图片元数据里的真实 DPI,按像素宽高精确计算 PDF 页面尺寸,让图片以 1:1 物理尺寸落进 PDF 页面。顺带处理透明 PNG 变黑、大图内存溢出这些真实场景里躲不掉的周边问题。核心逻辑只有一层,但这层换算搞明白了,普遍遇到的“图片塞进 PDF 糊一半”就能一次根治。
适合要批量把产品图、扫描件、截图合成 PDF 的 Java 开发者。核心是 iText 5 的 Document + Image 用法,以及“像素 / DPI × 72”的尺寸换算链路,看懂了可以直接平移到自己的导出模块里。
2. 技术选型与项目结构:iText 5 依赖、目录与运行方式
2.1 为什么选 iText 5,而不是 iText 7 或 PDFBox
先说结论:这个场景选 iText 5 不是因为新,而是因为 API 足够短。图片转 PDF 本质是“读图片尺寸 + 算页面大小 + 把图片丢上去”三个动作,iText 5 的 Document、Image、PdfWriter 三件套直接对应这三个动作,没有多余抽象。一个转换方法几十行就能写完,读代码的人不用在一堆模块间跳来跳去。
iText 7 是官方继任者,把功能拆成了 kernel、layout、io 等多个模块,支持复杂排版和表格流布局,但代价是 API 变化很大,很多老写法要重学。对于一个只做图片落页面的 demo,iText 7 的复杂度属于大炮打蚊子。PDFBox 则更底层,页面内容流、坐标变换都要自己控制,适合要精细操作 PDF 内部结构的场景,纯图片合成时用它反而要写更多代码。
还要提一句许可上的考虑:iText 5 走 AGPL,个人学习和内部工具没压力,商用闭源项目要留意授权问题。我见过一个同事的项目因为线上服务引入了 iText 7 社区版,合规评审折腾了半个多月。选库先把许可问清楚,能省很多事。
从长期维护角度看,判断标准很简单:如果你的需求停留在“图片进 PDF、页面尺寸跟随图片”,iText 5 完全够用;如果后面要加表格、水印、表单填写、数字签名这类高级功能,早点迁到 iText 7 反而平滑。我通常把这个判断直接写进项目 README,避免后人接手时对着依赖列表纠结。
2.2 项目目录结构与模块职责
整个项目按“入口—转换—元数据读取”拆成三个包,结构很直白:
image-to-pdf ├── pom.xml └── src/main/java/com/example/imagetopdf ├── App.java # 命令行入口,解析参数 ├── converter/ │ └── PdfConverter.java # 转换核心,DPI兜底、透明图处理、PDF写出 └── util/ ├── ImageMetadataUtil.java # 读取像素宽高、DPI、是否带透明通道 └── PageSizeUtil.java # 像素尺寸 + DPI → PDF 点数尺寸三个类各干各的,没有互相绕。PdfConverter只认图片路径和最终 PDF 路径,需要图片信息时问ImageMetadataUtil要,需要页面大小时问PageSizeUtil要,自己只负责组合流程。改起来位置很好找:想调整页面边距改PdfConverter,想换 DPI 读取逻辑改ImageMetadataUtil,互不牵连。
这种拆法还有一个好处:单元测试好写。PageSizeUtil是纯函数,喂像素宽高和 DPI 就能断言输出;ImageMetadataUtil只需要准备一张带 pHYs 信息的 PNG 就能验证解析结果。真正带 IO 的就是PdfConverter那一层,测试时用临时文件隔离就够了。
2.3 Maven 依赖与运行方式
依赖只有一个核心库:
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13.3</version> </dependency> </dependencies>选择 5.5.13.3 是因为它是 5.x 里用得最广的版本,API 稳定,网上资料也最多。JDK 8 以上都能跑,不需要额外配置模块访问权限,免去在 module-info 里折腾的麻烦。需要注意这个版本比较老,如果公司有统一的安全扫描策略,跑之前先让依赖清单过一遍合规。
运行分两步。先打包:
mvn clean package再执行:
java -jar target/image-to-pdf.jar ./sample.png ./output.pdf 300第三个参数是兜底 DPI,当图片元数据里读不到 DPI 信息时用它替代。命令行入口在 App.java 里,参数少于两个会打印用法。IDE 里跑更简单,直接在 App 的 Run Configuration 里填 Program Arguments,比如./sample.png ./output.pdf 300,省去每次打 jar 的时间。Windows 下注意路径分隔符,用正斜杠或转义过的反斜杠都行。
3. 核心代码拆解:从 PNG 到 PDF 的完整转换链路
3.1 读取图片像素尺寸与 DPI:绕过像素加载拿元数据
第一步最关键:只读元数据,不读像素。很多人一上来就ImageIO.read(file),这个调用会把整张图解码进内存,几千乘几千像素的 PNG 直接翻车。换个思路,用ImageReader拿到宽度、高度、DPI,全程不碰像素数组,内存占用可以忽略。
package com.example.imagetopdf.util; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.ImageInputStream; import java.io.File; import java.io.IOException; import java.util.Iterator; public class ImageMetadataUtil { public static class ImageInfo { public final int pixelWidth; public final int pixelHeight; public final int dpi; public final boolean hasAlpha; public ImageInfo(int pixelWidth, int pixelHeight, int dpi, boolean hasAlpha) { this.pixelWidth = pixelWidth; this.pixelHeight = pixelHeight; this.dpi = dpi; this.hasAlpha = hasAlpha; } } public static ImageInfo read(File file) throws IOException { // 用 ImageInputStream 包一层,方便自动关闭底层文件句柄 try (ImageInputStream iis = ImageIO.createImageInputStream(file)) { Iterator<ImageReader> readers = ImageIO.getImageReaders(iis); if (!readers.hasNext()) { throw new IOException("没有找到能解析图片格式的Reader: " + file.getName()); } ImageReader reader = readers.next(); reader.setInput(iis, true, true); int width = reader.getWidth(0); int height = reader.getHeight(0); int dpi = extractDpi(reader); boolean hasAlpha = hasAlpha(reader); return new ImageInfo(width, height, dpi, hasAlpha); } } private static boolean hasAlpha(ImageReader reader) { try { ImageTypeSpecifier type = reader.getRawImageType(0); return type != null && type.getColorModel().hasAlpha(); } catch (IOException e) { // 个别格式取不到原始类型,直接当成不透明,后面铺底逻辑跳过 return false; } } private static int extractDpi(ImageReader reader) throws IOException { IIOMetadata metadata = reader.getImageMetadata(0); if (metadata == null) { return -1; } // 标准 metadata 树里,Dimension 节点带 HorizontalPixelSize 属性,单位是“毫米/像素” IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree("javax_imageio_1.0"); double mmPerPixel = -1; for (int i = 0; i < root.getLength(); i++) { if ("Dimension".equals(root.item(i).getNodeName())) { String value = root.item(i).getAttributes().getNamedItem("HorizontalPixelSize") != null ? root.item(i).getAttributes().getNamedItem("HorizontalPixelSize").getNodeValue() : null; if (value != null) { mmPerPixel = Double.parseDouble(value); } break; } } if (mmPerPixel <= 0) { return -1; } // 1 英寸 = 25.4 毫米,每英寸像素数 = 25.4 / 每像素毫米数 return (int) Math.round(25.4 / mmPerPixel); } }逻辑说明:reader.getWidth(0)和getHeight(0)直接从文件头解析宽高,不解码像素,所以一张 8000 × 6000 的图也能瞬间返回。getRawImageType(0)用来判断图片是否带透明通道,PNG 常见,JPEG 返回的就是不透明。DPI 藏在标准 metadata 的Dimension节点里,属性HorizontalPixelSize的单位是毫米/像素,比如常见扫描图这个值是 0.08467,换算出来就是25.4 / 0.08467 ≈ 300 DPI。
参数说明:read返回的dpi在读取失败时为 -1,调用方要用兜底值替代。hasAlpha为 true 时,后续转换流程要额外走一次白底重绘,否则生成出来的 PDF 在部分阅读器里背景是黑色的。这套逻辑是格式无关的,PNG、JPEG、BMP 都走同一套,因为 ImageIO 的 Reader 机制已经按格式分流了。
提示:不要试图用
BufferedImage.getWidth()代替这套读取,那会先把整张图解码进堆内存,对“转换工具”这类常跑大图的场景来说是巨大隐患。
3.2 计算 PDF 页面尺寸:像素 / DPI 乘 72 是怎么来的
PDF 的长度单位是 pt(点),1 英寸 = 72 pt。一张 300 DPI 的图片,宽 2550 像素,物理宽度是 2550 / 300 = 8.5 英寸,换算成 pt 是 8.5 × 72 = 612 pt,正好是 A4 纸宽度的一半多一点。这个换算本身不复杂,但容易错在忘记除以 DPI,直接把像素当 pt 用,结果页面比实际大了几倍。
package com.example.imagetopdf.util; import com.itextpdf.text.Rectangle; public class PageSizeUtil { /** * 像素尺寸 + DPI → PDF 页面点数尺寸 * * @param pixelWidth 图片像素宽 * @param pixelHeight 图片像素高 * @param dpi 图片实际 DPI */ public static Rectangle createFromPixels(int pixelWidth, int pixelHeight, int dpi) { float widthPt = pixelWidth * 72f / dpi; float heightPt = pixelHeight * 72f / dpi; return new Rectangle(widthPt, heightPt); } }逻辑说明:Rectangle是 iText 5 里表示页面尺寸的类,构造函数接收宽高两个 float,单位是 pt。createFromPixels只做一件事,把像素按 DPI 换算成 pt。细节容易踩:72f的f不能省,写成72会触发整型除法,像素值算出来全是 0,页面瞬间变成长条。
参数说明:dpi不要传 0,否则出现除零错误,调用方要先判断图片自带 DPI 是否有效。如果后续要做 A4 适配,可以在这里改成返回固定 A4 尺寸,再用scalePercent缩放图片,但那样会牺牲部分清晰度,不属于“高清版”的默认路径。我一般把createFromPixels的结果直接当最终页面大小,只有需要统一打印尺寸时才额外写适配方法。
举两个数感受一下:一张 2550 × 3300、300 DPI 的扫描图,算出来页面是 612 × 792 pt,恰好是 A4 纵向;同一张图如果忽略 DPI 直接用 2550 × 3300 当页面尺寸,生成的 PDF 打开后图片会大到只显示一个角落。这个对比能直观解释为什么“高清版”必须从元数据读取开始。
3.3 写入 PDF:透明 PNG 铺底,图片按 1:1 落位
核心转换逻辑在 PdfConverter 里:先拿图片信息,算出页面尺寸,再决定要不要处理透明通道,最后把图片写进 PDF。
package com.example.imagetopdf.converter; import com.example.imagetopdf.util.ImageMetadataUtil; import com.example.imagetopdf.util.PageSizeUtil; import com.itextpdf.text.Document; import com.itextpdf.text.Image; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.PdfWriter; import javax.imageio.ImageIO; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; public class PdfConverter { /** * @param imagePath 输入图片路径 * @param pdfPath 输出 PDF 路径 * @param fallbackDpi 图片没有 DPI 信息时的兜底值 */ public static void convert(String imagePath, String pdfPath, int fallbackDpi) throws Exception { File imageFile = new File(imagePath); // 1. 只读元数据,拿到像素宽高、DPI、透明通道标记 ImageMetadataUtil.ImageInfo info = ImageMetadataUtil.read(imageFile); int dpi = info.dpi > 0 ? info.dpi : fallbackDpi; // 2. 按物理尺寸创建 PDF 页面,四个边距全部为 0 Rectangle pageSize = PageSizeUtil.createFromPixels(info.pixelWidth, info.pixelHeight, dpi); Document document = new Document(pageSize, 0, 0, 0, 0); PdfWriter.getInstance(document, new FileOutputStream(pdfPath)); document.open(); // 3. 透明 PNG 先铺白底,避免部分阅读器把透明渲染成黑色 File readyFile = imageFile; if (info.hasAlpha) { readyFile = flattenTransparency(imageFile, info.pixelWidth, info.pixelHeight); } // 4. 加载图片,设置 DPI 并撑满整个页面 Image pdfImage = Image.getInstance(readyFile.getAbsolutePath()); pdfImage.setDpi(dpi, dpi); pdfImage.setAbsolutePosition(0, 0); pdfImage.scaleAbsolute(pageSize.getWidth(), pageSize.getHeight()); document.add(pdfImage); document.close(); } /** * 把透明 PNG 重绘到白色背景上,生成临时文件 */ private static File flattenTransparency(File src, int width, int height) throws Exception { BufferedImage rgba = ImageIO.read(src); BufferedImage rgb = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2d = rgb.createGraphics(); g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, width, height); g2d.drawImage(rgba, 0, 0, null); g2d.dispose(); File temp = File.createTempFile("flatten_", ".png"); ImageIO.write(rgb, "png", temp); return temp; } }逻辑说明:第 4 步是高清的关键,setDpi(dpi, dpi)把 DPI 信息写进图片数据流,scaleAbsolute(pageSize.getWidth(), pageSize.getHeight())让图片以第 2 步算出的页面尺寸 1:1 铺满。这里不用scalePercent(100)的原因是scalePercent按图片在默认 72 DPI 下的大小缩放,而我们自己定义了页面尺寸,直接用绝对尺寸更可控。透明图片处理放在写入之前,用 RGB 模式的 BufferedImage 重画一遍,背景填充白色,规避 PDF 不支持透明通道的兼容问题。
参数说明:PdfWriter.getInstance的第二个参数是输出流,这里直接包了FileOutputStream,生产环境一般会在外层套BufferedOutputStream减少 IO 次数。flattenTransparency里仍有一次完整解码,这是透明通道处理逃不掉的代价,非透明 JPEG 不会走到这一步。如果原图片文件本身的宽高元数据和实际像素不一致,输出仍会按页面尺寸拉伸,这是 scaleAbsolute 的特性,需要注意。
3.4 命令行入口:参数解析与兜底 DPI
App.java 提供最简入口,方便在 IDE 里直接跑,也方便后续扩展成批量工具:
package com.example.imagetopdf; import com.example.imagetopdf.converter.PdfConverter; public class App { public static void main(String[] args) { if (args.length < 2) { System.out.println("用法: java -jar image-to-pdf.jar <输入图片> <输出PDF> [回退DPI]"); return; } String src = args[0]; String dest = args[1]; int fallbackDpi = args.length >= 3 ? Integer.parseInt(args[2]) : 300; try { PdfConverter.convert(src, dest, fallbackDpi); System.out.println("转换完成: " + dest); } catch (Exception e) { System.err.println("转换失败: " + e.getMessage()); } } }逻辑说明:第三个参数是兜底 DPI,正常路径下用不到。很多截图工具生成的 PNG 不写 pHYs 信息,元数据里读不到 DPI,这时要么默认 300,要么让用户显式传。默认取 300 符合“图片转 PDF 高清”的预期,如果业务想严格按 72 DPI 生成传统 PDF,把默认值改成 72 即可。
参数说明:输出 PDF 的父目录要提前存在,FileOutputStream不会自动建目录,程序会抛 FileNotFoundException,这个细节在批量脚本里很容易遇到。异常只打印了消息,接入真实项目时建议把堆栈打出来,否则排查问题时只能看到一个干巴巴的失败原因。
4. 高清转换避坑指南:5 个真实踩过的坑
4.1 生成的 PDF 打开后图片超大或超小
现象:同一张图片转换,有的 PDF 打开后图片撑满好几屏,有的只占一个角,怎么调整缩放都不对。
原因:没读取图片 DPI 信息,iText 默认按 72 DPI 处理像素。一张 300 DPI 的 2550 像素宽图片,按 72 DPI 算出来页面宽度是 2550 × 72 / 72 = 2550 pt,比实际应有的 612 pt 大了 4 倍多,打开自然像看放大镜。
解决:先通过 ImageMetadataUtil 读 DPI,再用“像素 / DPI × 72”计算页面尺寸。读不到 DPI 时不要硬算,用 fallback 参数兜底,并打日志提醒。这个坑是高清版的核心价值,也是绝大多数“图片转 PDF 模糊”骂名的真实来源。
4.2 透明 PNG 转出来背景变成黑色
现象:带透明通道的 PNG 转成 PDF 后,在预览工具里正常,换一个阅读器打开背景就黑了。
原因:PDF 本身不支持 alpha 通道,透明区域落在不同渲染引擎里行为不一致,有的当白色处理,有的当成黑色。透明信息在转 PDF 时没有被固化成明确颜色,换个阅读器就暴露,属于典型的玄学问题。
解决:写入 PDF 前先把 RGBA 图像重绘到 RGB 画布上,背景填白色。PdfConverter 里的flattenTransparency就是这个作用。如果不想要白色,可以填其它颜色,但必须在写 PDF 之前固定下来,不要指望 PDF 渲染引擎去猜。
4.3 图片被拉伸变形
现象:图片转出来后宽高比例不对,圆形变成椭圆,直角变斜角。
原因:页面尺寸和图片像素比例不一致,scaleAbsolute按宽高分别缩放,强拉硬拽。常见触发点是 A4 模板适配:先固定了 A4 页面,再往里塞任意比例的图片,比例对不上必然变形。
解决:要么页面完全跟随图片尺寸,也就是本 demo 的做法;要么按图片长边等比缩放,剩余区域留白。等比缩放别用scaleAbsolute,改用scaleToFit(pageWidth, pageHeight),这个方法会在保持比例的前提下自动缩放到恰好放得下,参数是页面可用宽高,注意要预留边距。
4.4 大图转换时内存溢出
现象:一张几千乘几千像素的 PNG,转着转着直接 OOM,日志里是 OutOfMemoryError。
原因:ImageIO.read会把整张图解码进内存。8000 × 6000 的 ARGB 图,一个像素 4 字节,光像素数据就近 200 MB,再叠加 PDF 缓存,几张并发就爆了。
解决:能只读元数据就不要整图解码,读取尺寸和 DPI 一律走 ImageReader。只有需要透明铺底或压缩重编码时才解码,并且解码后立即写入临时文件,释放引用。真要处理超大图,把降采样开关放在命令行参数里,默认不做,需要时再打开。
4.5 生成的 PDF 文件特别大
现象:同样一张图片,原文件只有几 MB,转成 PDF 后体积翻倍甚至更多。
原因:PNG 是无损格式,iText 嵌入时保留了全部像素数据。高清图再叠加多页,文件体积自然失控,等于用存储空间换质量。
解决:对不需要透明通道的图片,先转成 JPEG 压缩再嵌入是常见做法。可以在flattenTransparency之后加一个 JPEG 编码分支,用 ImageIO 的 JPEG writer 设置 quality 0.85,再把临时文件路径交给Image.getInstance。注意 JPEG 不支持透明,必须先铺底再压缩,顺序反了会得到一张黑色背景图。
5. 二次开发:批量转换、压缩优化与质量验证
5.1 批量转换目录下所有 PNG
把单文件入口换成目录遍历,改造成本很低。在 App 里加一个分支,用 Files.list 过滤后缀,逐张调用 PdfConverter.convert:
import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Stream; Path dir = Paths.get(args[0]); try (Stream<Path> stream = Files.list(dir)) { stream.filter(p -> p.toString().toLowerCase().endsWith(".png")) .sorted() .forEach(p -> { String out = p.toString().replace(".png", ".pdf"); PdfConverter.convert(p.toString(), out, 300); }); }逻辑说明:Files.list返回目录下的惰性流,配合 filter 过滤非 PNG 文件,输出路径直接替换扩展名,同一目录下不会产生冲突。try-with-resources保证流最终关闭,否则在 Windows 上可能因为句柄泄漏导致后续文件删除失败。
参数说明:如果目录里混有 JPEG,在 filter 里再加一个.jpg判断即可,PdfConverter 不关心格式,ImageIO Reader 会自动识别。批量转换是内存瓶颈点,逐张转很稳但偏慢,换成并行流(.parallel())能提速,但大图并发时 OOM 概率升高,建议先单线程跑通再优化。
如果想把整个目录合成一个多页 PDF,需要把 PdfConverter 拆一下:循环外创建 Document,循环内对每张图调用一个 addImagePage 方法。常见做法是每张图用各自的Rectangle设一次setPageSize再newPage(),这样每页尺寸可以跟随各自的图片,适合设计稿交付。
5.2 三种输出模式怎么选
转换核心不用改动,页面尺寸的构造方式决定输出效果。三种模式对比如下:
| 模式 | 页面尺寸 | 适用场景 | 清晰度 |
|---|---|---|---|
| 原图 1:1 | 跟随图片物理尺寸 | 电子存档、设计稿交付 | 最高 |
| 每图一页 A4 适配 | A4,图片等比缩放 | 打印、纸质归档 | 中,取决于原图 |
| 多图合一页 | 自定义网格 | 拼接长图、目录预览 | 受排版影响 |
原图 1:1 是高清默认项。A4 适配时图片缩小,如果原图分辨率不足,打印出来就是锯齿,这是物理限制。多图合一页适合做缩略图拼版,每张小图用scaleToFit塞进固定格子,间距通过 setMargins 控制。A4 适配时建议明确边距,iText 的 Document margins 会影响可用绘图区域,scaleToFit要和 margin 配合计算,不然图片可能溢出到页面外。
5.3 质量验证:用 PDFBox 读回页面尺寸做断言
转换完不能只靠人眼,我用 PDFBox 把生成的 PDF 读回来,验证页面尺寸和图片渲染尺寸是否一致:
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; try (PDDocument doc = PDDocument.load(new File("output.pdf"))) { PDPage page = doc.getPage(0); float widthPt = page.getMediaBox().getWidth(); float heightPt = page.getMediaBox().getHeight(); System.out.println("PDF页面: " + widthPt + " x " + heightPt); }逻辑说明:PDFBox 的getMediaBox返回页面尺寸,单位也是 pt,和 iText 写入的值可以直接对照。如果页面宽度明显大于原图物理尺寸,说明 DPI 读取链路有问题,回到第 4 章的 4.1 排查。这个断言我习惯写进构建脚本里,批量转换后自动抽查,比打开 PDF 一个一个看省事。
参数说明:PDFBox 在 pom 里要单独引org.apache.pdfbox:pdfbox,版本用 2.0 系列即可。验证只读页面尺寸,不加载图片像素,内存压力很小。这类验证代码用来做回归监控足够,不需要出现在生产链路里。
做这个项目之前,我转过一批设计稿,当时图省事直接把图片塞进 A4,交付后对方打印出来全是锯齿。从那以后我每次改图片转 PDF 的代码,都会强制走一遍“读 DPI → 算尺寸 → 写 PDF → PDFBox 回读断言”的完整流程,宁可多写几行验证代码,也不再靠肉眼赌输出。希望帮到你,踩坑的路我已经替你走了一遍。
本文还有配套的精品资源,点击获取