简介:面向Java开发者与需要批量出图的办公场景,这份资源聚焦用iText把PNG等图片转成高清PDF这一常见需求,提供可直接运行的示例工程与配套说明,帮助读者绕开DPI设置、图片缩放等易踩的坑。压缩包共11个文件,约1022KB,以2个jar依赖包为核心,辅以java源码、class编译文件、jsp页面、xml配置及readme说明,另有工程元数据文件,结构贴近真实MyEclipse项目,便于导入后对照调试。已有5467人学习下载,说明该示例在同类需求中具备一定参考价值。读者可从中获得完整的图片转PDF实现流程,包括Document与PdfWriter的创建、Image加载与DPI调整、按宽度或高度缩放以及文档关闭保存等关键环节,并借助readme与源码快速定位参数含义,适合作为入门iText图像处理的实践起点。
1. iText 图片转 PDF:为什么“能生成”和“够高清”是两回事
上周帮一个做档案数字化的朋友看问题:他用 iText 把一批扫描件 PNG 拼成 PDF,文件能打开、页数也对,但打印出来边缘发虚,客户直接退回。翻代码发现两处硬伤——图片被scaleToFit压到了 A4 宽度,且输出时用了默认压缩。这类“能生成但不够高清”的翻车,在 iText 图片转 PDF 场景里非常典型。iText 本身不生产像素,它只负责把图片按指定尺寸、指定压缩方式嵌进 PDF 容器;高清与否,取决于你是否守住了原始分辨率、是否选对了压缩路径、是否让 DPI 和页面尺寸对得上。这篇笔记围绕 iText 图片 PNG 转 PDF demo 这条主线,把参数、代码和踩坑一次讲透,适合正在做票据、扫描件、报告附图导出的开发者照着复现。
2. 高清 PDF 的三个前提:分辨率、压缩与页面坐标系
动手写代码之前,得先把“高清”拆成可量化的条件。很多人以为把 PNG 塞进 PDF 就完事,结果打印出来糊成一片,回头怪 iText,其实是三个前提没立住。
2.1 像素密度决定清晰度上限
PDF 是矢量容器,但嵌进去的位图仍是位图。一张 3000×2000 像素的 PNG,如果被塞进 210mm×297mm 的 A4 页面,等效 DPI 约为 3000/(210/25.4)≈363,打印够用;同一张图若被拉伸到 A3,DPI 直接掉到 180 左右,肉眼就能看出软。所以第一步不是写代码,而是算账:目标页面物理尺寸(毫米)× 目标 DPI ÷ 25.4 = 需要的像素宽。常见做法是扫描件按 300 DPI 归档,屏幕预览按 150 DPI 即可。iText 不会替你补像素,它只会按你给的矩形区域绘制,图不够大就是拉伸,够大就是缩放,缩放算法默认是双线性,细节会丢。
2.2 压缩方式选错,高清也白搭
iText 嵌图有两条路:一是把 PNG 原始字节流直接写入(Image.getInstance(byte[])走的是解码后重编码),二是先解码成BufferedImage再交给 iText 处理。前者对 PNG 的无损特性保留较好,后者在 JPEG 质量参数上更可控。关键参数是Image#setCompressionLevel(0~9)和 JPEG 的 quality。压缩级别 9 体积最小但编码耗时上升,对归档场景可接受;quality 低于 0.85 时文字边缘会出现振铃,票据类图片建议 0.9 以上。另一个常被忽略的点是颜色空间:PNG 带 alpha 通道时,iText 默认可能转成 RGB 并填白底,若原图是透明底黑字,转完反而发灰,需要显式指定背景色。
2.3 页面坐标系与图片矩形的对齐
iText 的Document默认边距 36pt,Image.scaleToFit(w,h)是按可用宽度等比缩放,但很多人直接传页面宽高,忘了减边距,导致图片被裁或留白不均。更稳的做法是用PdfCanvas在绝对坐标上绘制,配合setFixedPosition,把图片左下角锚点算准。对于多页拼图,每页新建Document或复用PdfWriter加addNewPage都行,但要注意Document.close()只关一次,重复关会抛异常。下面这张表把三个前提对应的参数列清楚,写代码时对着填。
| 前提 | 关键参数/方法 | 推荐值 | 失控后果 |
|---|---|---|---|
| 像素密度 | 原图像素宽 ÷ 页面物理宽 | ≥300 DPI(打印) | 拉伸发虚 |
| 压缩 | setCompressionLevel / JPEG quality | 6~9 / ≥0.9 | 体积大或边缘振铃 |
| 坐标系 | 页边距、scaleToFit 入参 | 减掉 margin 后的可用区 | 裁切或留白 |
3. 从 PNG 到 PDF:一份可复现的 iText 代码骨架
前提立住后,落到代码。下面这份骨架覆盖单图、多图分页、指定 DPI 三个场景,依赖 iText 7(kernel与layout两个模块),Maven 坐标按你本地仓库实际版本填,不写死版本号避免误导。
3.1 依赖与最小可运行示例
<!-- pom.xml 片段:iText 7 核心与布局模块 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>kernel</artifactId> <version>${itext.version}</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>layout</artifactId> <version>${itext.version}</version> </dependency>// 单张 PNG 生成单页 PDF,按目标 DPI 反算绘制尺寸 public void pngToPdf(Path png, Path out, float targetDpi) throws Exception { BufferedImage src = ImageIO.read(png.toFile()); // 先解码,拿到真实像素 float pxW = src.getWidth(), pxH = src.getHeight(); float ptW = pxW / targetDpi * 72f; // 像素→点:1pt=1/72英寸 float ptH = pxH / targetDpi * 72f; PdfDocument pdf = new PdfDocument(new PdfWriter(out.toFile())); Document doc = new Document(pdf, new PageSize(ptW, ptH)); // 页面尺寸跟随图片 doc.setMargins(0, 0, 0, 0); // 无边距,避免二次缩放 Image img = new Image(ImageDataFactory.create(png.toString())); img.setWidth(ptW); img.setHeight(ptH); // 显式定尺寸,禁用自动缩放 doc.add(img); doc.close(); }逻辑说明:先ImageIO.read拿到像素宽高,再按像素/DPI×72换算成 PDF 点,页面尺寸直接等于图片物理尺寸,边距清零,这样 iText 不会做任何额外缩放,1 像素对应 1/DPI 英寸,打印清晰度由原图 DPI 决定。参数上,targetDpi传 300 适合归档,传 150 适合屏幕;setWidth/setHeight必须同时设,只设一个会触发等比缩放,反而引入插值。
3.2 多图分页与压缩参数
// 多张 PNG 每页一张,统一压缩级别与 JPEG 质量 PdfWriter writer = new PdfWriter(out.toFile()); PdfDocument pdf = new PdfDocument(writer); Document doc = new Document(pdf, PageSize.A4); doc.setMargins(20, 20, 20, 20); for (Path p : pngList) { ImageData data = ImageDataFactory.create(p.toString()); Image img = new Image(data); img.setCompressionLevel(9); // 0~9,9 体积最小 img.scaleToFit(PageSize.A4.getWidth() - 40, PageSize.A4.getHeight() - 40); // 减掉左右/上下边距 doc.add(img); doc.add(new AreaBreak()); // 强制分页,避免两张挤一页 } doc.close();这里setCompressionLevel(9)对 PNG 走的是 Flate 压缩,无损但耗时略增;若源图是照片类,可改用ImageDataFactory.createJpeg路径并设 quality。scaleToFit的入参一定要减掉setMargins的两倍值,否则图片会顶到页边甚至溢出。AreaBreak是分页的关键,漏写会导致多图叠在同一页。
3.3 用 PdfCanvas 做精确定位
当图片需要叠在固定坐标(比如票据模板的指定区域),Document.add的流式布局就不够用了,改用PdfCanvas:
PdfPage page = pdf.addNewPage(PageSize.A4); PdfCanvas canvas = new PdfCanvas(page); ImageData data = ImageDataFactory.create(png.toString()); float x = 50, y = 400, w = 300, h = 200; // 单位均为 pt,左下角为原点 canvas.addImageFittedIntoRectangle(data, new Rectangle(x, y, w, h), false);addImageFittedIntoRectangle的最后一个布尔参数控制是否保持比例,传false表示拉伸填满,传true则等比缩放留白。坐标原点在页面左下角,y 值从下往上算,这点和屏幕坐标相反,第一次用容易把图放反。矩形宽高比最好与原图一致,否则拉伸变形,高清也救不回来。
4. 避坑与排查:五条血泪经验
代码跑通只是开始,真正耗时间的是那些“看起来对、打印出来错”的问题。下面五条按现象、原因、解决拆开写,都是我在实际项目里踩过的。
4.1 图片发虚:现象是打印边缘模糊,原因是 DPI 与页面尺寸不匹配
现象:PDF 在屏幕上看着正常,打印或放大到 200% 后文字边缘出现锯齿。原因:原图 150 DPI 却被铺满 A4,等效 DPI 不足 150,iText 做了上采样插值。解决:先算原图像素宽 ÷ (页面宽mm/25.4),低于 200 就换更高分辨率源图,或缩小页面物理尺寸;代码里用 3.1 的 DPI 反算方式,让页面跟随图片,而不是让图片迁就页面。
4.2 透明底变黑或变灰:现象是 PNG 透明区域在 PDF 里成了色块
现象:带 alpha 通道的 PNG 嵌入后,透明部分显示为黑色或灰色底。原因:PDF 页面默认无背景,透明像素在部分阅读器里按黑色合成;iText 转 RGB 时未指定背景。解决:在Document或PdfCanvas绘制前先填白底,canvas.setFillColor(ColorConstants.WHITE).rectangle(...).fill(),再叠图片;或预处理阶段用BufferedImage把 alpha 合成到白底再交给 iText。
4.3 文件体积暴涨:现象是几十张图生成几百 MB
现象:20 张 300 DPI 扫描件生成 PDF 超过 300MB,传输困难。原因:setCompressionLevel未设或设成 0,PNG 按原始流写入。解决:统一设setCompressionLevel(9);照片类改走 JPEG 路径 quality 0.9;必要时在嵌入前用ImageIO重编码降色深。注意压缩级别对已压缩的 PNG 收益有限,真正的大头是分辨率,先降 DPI 再谈压缩。
4.4 多图叠页或丢页:现象是页数比图片数少
现象:循环添加图片,结果 PDF 页数少于图片数,部分图重叠。原因:Document.add是流式布局,图片高度累计未超页高时不会自动分页。解决:每张图后显式doc.add(new AreaBreak());或改用pdf.addNewPage()逐页PdfCanvas绘制,页数完全可控。
4.5 中文路径或特殊字符报错:现象是ImageDataFactory.create抛异常
现象:图片路径含中文或空格时读取失败。原因:create(String)按 URL 解析,特殊字符未编码。解决:改用create(byte[])先Files.readAllBytes读入字节,或传File对象重载;输出路径同理,用new PdfWriter(File)而非字符串。
提示:以上五条里,4.1 和 4.3 经常同时出现——为了压体积降了 DPI,结果打印发虚,回头又加分辨率,体积再涨。正确顺序是先定目标 DPI,再选压缩,别反过来。
5. 验收与进阶:用 DPI 反算和抽样打印把住最后一关
代码写完不等于交付。我现在的习惯是每次生成完 PDF,先跑一遍自动校验,再抽一页实际打印,两步都过才算完。
自动校验的核心是反算每页图片的等效 DPI。用 iText 的PdfReader遍历页面资源里的XObject,拿到图片像素宽高和绘制矩形,算像素宽 ÷ (矩形宽pt/72),低于阈值就报警。下面这段是校验骨架:
// 遍历 PDF 每页图片,反算等效 DPI,低于阈值打印告警 try (PdfDocument pdf = new PdfDocument(new PdfReader(in.toFile()))) { for (int i = 1; i <= pdf.getNumberOfPages(); i++) { PdfPage page = pdf.getPage(i); PdfDictionary res = page.getResources().getPdfObject(); PdfDictionary xobj = res.getAsDictionary(PdfName.XObject); if (xobj == null) continue; for (PdfName name : xobj.keySet()) { PdfStream s = xobj.getAsStream(name); if (!PdfName.Image.equals(s.getAsName(PdfName.Subtype))) continue; int pxW = s.getAsNumber(PdfName.Width).intValue(); float rectW = /* 从内容流解析绘制宽,简化场景可按页面宽估算 */ page.getPageSize().getWidth(); float dpi = pxW / (rectW / 72f); if (dpi < 200) System.out.println("第" + i + "页 DPI 偏低: " + dpi); } } }参数说明:阈值 200 是打印下限,归档建议 300;rectW精确值需解析内容流里的cm矩阵,简化版按页面宽估算已能拦住大部分问题。抽样打印则固定选含小字号文字的那一页,A4 实际出纸看边缘,屏幕预览会骗人。
进阶一点的做法是把 DPI 反算做成构建流水线的一步,生成即校验,不达标直接失败,避免人工漏看。另一个技巧是给Image设setInterpolation(true),在必须缩放时用双三次插值,比默认双线性锐利一些,但代价是编码变慢,只对少量关键图开启。
从那以后我每次做图片转 PDF,都强制先算目标 DPI 再写页面尺寸,生成后跑一遍反算校验,最后抽一页真打印——这三步少一步,返工概率就翻倍。希望帮到你。
本文还有配套的精品资源,点击获取