Java借助iText实现将图片转换成高清PDF
作者:菩提风
简介:面向需要将图片高质量转为PDF的Java开发者,这是一份基于iText的无坑实操示例包。资源以完整的demo工程呈现,重点演示如何通过setDpi设置300 DPI、使用scaleToFit控制图片缩放,从而避免生成模糊PDF;覆盖PNG、JPEG等常见格式,可直接迁移到批量报表、电子书合成等办公自动化场景。压缩包共11个文件,主要包含iText.jar依赖、Java示例源码、JSP测试页面、编译后的class以及工程配置文件与readme说明,整体大小约1022KB,结构简短清晰。已有5467人学习下载,适合正在学习iText或想快速获得可运行参考实现的开发者,对照源码即可理解高清转换的关键参数与配置步骤。
1. 用 iText 把图片压成高清 PDF:这次把 DPI 和缩放的坑一次填平
做导出功能时,总会被问「能不能把图片导成 PDF,最好清楚一点」。直接用 iText 的 Image 往文档里一塞,出来要么图被拉伸得没法看,要么在屏幕上正常、打印或放大后糊成一团。这个「清晰」的坑,其实不只在压缩质量,而在 DPI 与页面尺寸的换算关系。本文要拆的这套 iText 图片生成 PDF(高清版)方案,核心就三件事:把 PNG/JPG 按原始像素塞进自定义尺寸的 PDF 页面、用缩放与 DPI 参数保证视觉清晰度、以及处理 iText 5.x 里图片对象的各种暗坑。适合正在做订单回执、证书导出、图片存档这类功能的开发者,也适合想弄清 iText 里图片为啥「不清晰」的熟手。
2. 高清 PDF 的底层逻辑:像素、DPI 与页面尺寸的三角关系
2.1 为什么直接塞图片会糊
iText 里的 Image 对象本身不带「清晰度」属性,它记录的只是像素宽高和缩放比例。PDF 渲染时,图片会按页面坐标系统中的单位去绘制。默认情况下,iText 会把图片按原始像素的 72 DPI 换算成 PDF 里的物理尺寸。也就是说,一张 3000x2000 像素的图,插入后会被当成 3000/72 x 2000/72 英寸,即约 105.8 x 70.5 厘米。
这带来两个典型问题:一是当你把图片缩得很小放在 A4 页面里,PDF 阅读器缩放时图片会被重新采样,看起来没问题,但一旦用户放大或拿去印刷,就暴露像素不足;二是当你用 setScalePercent(50) 强行缩小,图片的实际像素并没有变,只是显示尺寸缩了,存储体积和渲染开销一点没省。真正的「高清」应该是 让图片在 PDF 里的物理尺寸与它的像素密度匹配 ,也就是让 DPI 落在合理范围,而不是靠缩放百分比去糊弄。
2.2 这套方案的选型:iText 5.x 全家桶
我用的依赖是 iText 5.5.13.3 的 com.itextpdf 包,不是 iText 7。原因很简单:iText 7 的 API 变化太大,网上的老教程大多基于 5.x,而且 5.x 的 Image、PdfWriter、Document 这套组合处理「图片插入 PDF」这个单一场景,代码量最少。如果你已经在用 iText 7,这套思路也能平移,只是类的命名空间从 com.itextpdf.text 换成了 com.itextpdf.layout,细节略有差异。
在 pom.xml 里引入:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>逻辑说明:iText 5.x 的核心入口是 Document 和 PdfWriter,它们配合实现了「添加内容 → 写入文件」的模型。相比 7.x 的布局系统,5.x 更贴近底层,适合我们手动控制页面尺寸。
参数说明:itextpdf 5.5.13.3 是 5.x 最后的稳定版本,支持 JDK 8 及以上。如果项目还在用 JDK 7,需要降到 5.5.12 或更早版本。注意别误引入 itext-asian,这个场景用不上。
2.3 预先算好页面尺寸:按像素定 PDF 大小
高清的关键一步,不是插入图片后去缩放,而是 在创建 Document 之前,就把 PDF 页面尺寸设置为图片的物理尺寸 。这样图片插入后是 1:1 放进页面的,无需缩放,渲染时也不会有重新采样带来的模糊。实现方式是用图片的像素宽高乘以一个目标 DPI 换算系数。
BufferedImage img = ImageIO.read(new File("input.png"));
int pixelWidth = img.getWidth();
int pixelHeight = img.getHeight();
// 目标 DPI,比如 150,打印和屏幕显示都比较清晰
float dpi = 150f;
float scaleFactor = 72f / dpi;
float pageWidth = pixelWidth * scaleFactor;
float pageHeight = pixelHeight * scaleFactor;
Document doc = new Document(new Rectangle(pageWidth, pageHeight), 0, 0, 0, 0);
PdfWriter.getInstance(doc, new FileOutputStream("output.pdf"));
doc.open();
逻辑说明: pixelWidth * 72 / dpi 把像素点转换成 PDF 的点单位。PDF 内部以点(point)为单位,1 英寸 = 72 点。当 DPI 设为 150 时,一张 1500 像素宽的图片,在 PDF 里就是 1500/150*72 = 720 点宽,即 10 英寸。图片插入后不缩放,物理尺寸和像素密度正好匹配。
参数说明: new Rectangle(pageWidth, pageHeight) 是自定义页面大小。四个 0 是页边距,图片场景务必设为 0,否则 iText 默认会有 36 点左右的边距,导致图片被挤小。DPI 建议取 96(屏幕)到 300(印刷)之间,超过 300 对大多数场景无意义,反而让文件变大。
3. 实操:把 PNG / JPG 批量写入 PDF 并控制清晰度
3.1 拿到图片后先做预处理
高清除了 DPI,还跟源图本身有关。如果拿到的图本身只有 500px 宽,那无论 DPI 怎么设都不可能清晰。我一般会在插入前检查图片的像素密度,如果低于预期,可以选择放大图片(插值)或提示调用方更换原图。放大图片用 AffineTransform 插值是个常规方案,但不建议放太大,超过 1.5 倍就属于聊胜于无。
BufferedImage src = ImageIO.read(new File("raw.png"));
int targetWidth = 1200;
double scale = targetWidth / (double) src.getWidth();
int targetHeight = (int) (src.getHeight() * scale);
BufferedImage scaled = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.drawImage(src, 0, 0, targetWidth, targetHeight, null);
g.dispose();
ImageIO.write(scaled, "png", new File("scaled.png"));
逻辑说明:这段是做质量控制的前置步骤。先把源图读进来,按目标宽度等比放大,并重新绘制到新画布。缩放后的图片保存到临时文件,后续插入 PDF 时直接用这个文件。
参数说明:VALUE_INTERPOLATION_BILINEAR 是双线性插值,适合放大图片;如果放大倍数较大,可以换 VALUE_INTERPOLATION_BICUBIC,效果更好但耗时增加。TYPE_INT_RGB 会丢弃透明通道,如果原图是 PNG 带透明背景,这个处理会造成黑底,要注意保留 TYPE_INT_ARGB。
3.2 图片插入与页面自适应:两种策略
处理单张图片时,建议让 PDF 页面跟随图片尺寸。如果是一批不同尺寸的图片,则有两种策略:一是为每张图都单独建一页,页面大小跟随图片;二是统一页面大小,图片缩放填充。前者更清晰,后者适合归档打印。这里给出每张图独立成页的写法。
import com.itextpdf.text.Document;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfWriter;
File[] files = new File("images").listFiles((dir, name) ->
name.toLowerCase().endsWith(".png") || name.toLowerCase().endsWith(".jpg"));
Document doc = new Document();
PdfWriter writer = PdfWriter.getInstance(doc, new FileOutputStream("all.pdf"));
doc.open();
for (File f : files) {
Image img = Image.getInstance(f.getAbsolutePath());
float imgWidth = img.getWidth();
float imgHeight = img.getHeight();
// 按目标 DPI 计算页面尺寸,页面跟随图片
float dpi = 150f;
float scale = 72f / dpi;
Rectangle pageSize = new Rectangle(imgWidth * scale, imgHeight * scale);
doc.setPageSize(pageSize);
doc.newPage();
// 图片位置原点在左下角,无需额外偏移
img.setAbsolutePosition(0, 0);
doc.add(img);
}
doc.close();
逻辑说明: Image.getInstance 是 iText 5.x 加载图片的核心方法,支持路径、URL 和字节数组。 getWidth() 返回的是图片的像素宽度,不等同于 PDF 里的物理宽度。通过 imgWidth * scale 计算页面大小,配合 doc.setPageSize 实现「一图一页、页随图走」。 newPage() 在循环里每次开启新页,避免图片堆叠在同一页上。
参数说明: img.setAbsolutePosition(0, 0) 把图片起点放在页面左下角。由于页边距为 0,页面尺寸就是图片尺寸,图片就能无偏移铺满整页。如果发现图片上下或左右留白,检查是不是 Document 构造时没有传入零边距的 Rectangle。
3.3 用压缩与 DPI 组合实现「高清且不臃肿」
高清不等于文件大。PDF 内嵌图片时,iText 默认不重新编码图片,只是原样嵌入。原样嵌入 PNG 有时反而比 JPG 更占体积。通过 PdfWriter 的压缩级别和图片质量参数,可以做到清晰度不变、体积下降。
PdfWriter writer = PdfWriter.getInstance(doc, new FileOutputStream("hd.pdf"));
writer.setFullCompression(true);
writer.setCompressionLevel(9);
Image img = Image.getInstance("photo.jpg");
img.setDpi(150, 150);
doc.add(img);
逻辑说明: setFullCompression(true) 是对整个 PDF 内容流启用 Flate 压缩,而不仅是图片流。 setCompressionLevel(9) 是 zlib 的压缩等级,0 不压缩,9 最高压缩。注意这两个设置影响的是 PDF 结构数据的体积,对图片本身的编码没有影响。
参数说明: img.setDpi(150, 150) 设置图片的 X/Y DPI。这个 DPI 主要影响 PDF 阅读器「显示尺寸」的计算,不影响像素数据。如果前面已经用像素换算页面尺寸,DPI 的作用被弱化;但如果页面尺寸固定(比如 A4),这个 DPI 就变得重要,因为阅读器会按 DPI 把图片渲染到对应尺寸。
4. 避坑与常见问题:高清导出路上的五个经典翻车现场
4.1 图片怎么插都是糊的,放大后马赛克严重
现象:无论 DPI 怎么设置,生成的 PDF 放大后图片都模糊。
原因:源图本身的像素不够,或者插入时把大图缩小显示。PDF 里的图片是栅格图,放大超过 100% 必然模糊,程序无法「无中生有」。
解决:先检查源图分辨率,建议至少 1000px 宽再导出。如果源图不行,用 3.1 节的双线性插值先放大到目标尺寸,虽然不能完全还原细节,但至少在普通缩放级别下观感好很多。另外确认代码里没有调用 setScalePercent(50) 之类的方法,这类写法只是缩小显示尺寸,不会增加清晰度。
4.2 PNG 图片插入后背景变成黑色
现象:透明背景的 PNG 导出的 PDF 中,透明区域变成了纯黑色块。
原因:iText 5.x 在处理某些 PNG 透明通道时,会走 Jpeg 或 CCITT 编码路径,导致 alpha 通道丢失或解析异常。尤其是带半透明渐变或 PNG-24 的图,概率较高。
解决:在插入之前把 PNG 统一转换成不带透明通道的 JPG,或者把透明区域替换成白色。转换用 BufferedImage 绘制到白色背景上即可。如果非要保留透明,可以试试 iText 的 Image.getInstance 配合 PngImage 类,但效果不稳定,建议直接绕开。
4.3 页面尺寸设了但图片还是被裁剪
现象:图片插入后四周被切掉了一部分,尤其是大尺寸图片。
原因:当 Document 的页面尺寸小于图片的实际显示尺寸时,iText 会保留图片原尺寸绘制,超出页面的部分被裁剪。我见过同事把页面设成 A4,然后直接插入一张 3000px 的图,结果只显示左上角一块。
解决:按 2.3 节的方式用图片尺寸倒数推出页面大小,再创建 Document。如果页面大小必须固定,就要用 img.scaleToFit(pageWidth, pageHeight) 让图片缩放适应页面,但那样清晰度会受影响。
4.4 多张图生成 PDF 时只有第一张
现象:循环里 add 了多张图片,但 PDF 里只有一张。
原因: Document 默认是从第一页开始的,循环中没有调 newPage() ,导致后续图片都在第一页上,而 iText 遇到页面放不下新内容时会自动翻页,但如果图片用了绝对定位,它不会主动触发翻页,所有图片叠在了同一页上。
解决:每次 add 前调用 doc.newPage() 。注意如果是第一张图, newPage() 会创建一个空白页再插入图片,导致第一页空白。可以用计数器判断,第一次不调 newPage() ,后续调用。
4.5 生成的 PDF 体积巨大,一张图占好几 MB
现象:简单的几张图,生成的 PDF 竟然几十 MB。
原因:PNG 原样嵌入时不经过压缩,24 位深、高分辨率的 PNG 本身就很占空间。如果页面尺寸设得过大,图片在 PDF 里的显示尺寸也大,但实际上没有重新压缩编码。
解决:图片统一转成 JPG 再插入,JPG 质量控制在 0.85 左右(见 5.2 节)。配合 3.3 节的 setFullCompression(true) 和 setCompressionLevel(9) ,通常能压掉 60% 的体积。
5. 让清晰度再进一步:统一图片格式与质量参数的调优习惯
5.1 为什么我坚持先把图片转成 JPG 再插 PDF
用 iText 处理图片导出,最先要考虑的就是「源图格式太难伺候」。PNG 的透明通道、色彩空间、位深度各不相同,iText 5.x 对 PNG 的解析依赖 Java 原生的 ImageIO,遇到 CMYK 色彩空间或 16 位深度的 PNG 甚至会抛异常或生成颜色完全错乱的 PDF。相比之下,JPG 只有一个 8 位 RGB 色彩空间,iText 解析几乎零失败。
所以我的习惯是: 所有图片先统一过一遍预处理管线 ,统一转成 RGB JPG,质量 0.85,然后在插 PDF。这样做牺牲了一点透明通道,但换来的是绝不出错、色彩一致、体积可控。如果你非要保留透明,就单独为 PNG 走一套流程,同时做好 4.2 节的避坑准备。
public static File toJpeg(File pngFile) throws IOException {
BufferedImage original = ImageIO.read(pngFile);
BufferedImage rgb = new BufferedImage(original.getWidth(), original.getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D g = rgb.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, rgb.getWidth(), rgb.getHeight());
g.drawImage(original, 0, 0, null);
g.dispose();
File jpg = new File(pngFile.getParent(), pngFile.getName() + ".jpg");
ImageIO.write(rgb, "jpg", jpg);
return jpg;
}
逻辑说明:先把原图绘制到一个全新白底的 TYPE_INT_RGB 画布上,淡化了透明像素;再以 jpg 格式写出。 g.setColor(Color.WHITE) 设置背景色, fillRect 把画布铺满,这一步把原来的透明区域变成了白色,避免后续插 PDF 时出现黑底。
参数说明: TYPE_INT_RGB 是 24 位 RGB,不带 alpha 通道。ImageIO.write 的第二个参数 "jpg" 控制编码器。JPG 质量默认约 0.75,如果要自定义,得用 JPEGImageWriteParam 单独设置压缩质量,后面参数解读会讲。
5.2 控制 JPG 质量参数:清晰与体积的平衡点
JPG 是有损压缩,质量参数决定了压缩率与失真程度。iText 本身不提供「JPG 质量」这样的接口,质量的控制发生在上游的编码环节。用 ImageIO 的默认 write 方法拿不到质量参数,需要显式设置。
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
ImageWriter writer = writers.next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(0.85f);
FileOutputStream fos = new FileOutputStream("output.jpg");
ImageOutputStream ios = ImageIO.createImageOutputStream(fos);
writer.setOutput(ios);
writer.write(null, new IIOImage(rgb, null, null), param);
ios.close();
fos.close();
writer.dispose();
逻辑说明:这里手动控制 JPEG 编码参数。 setCompressionMode(MODE_EXPLICIT) 打开手动设置压缩模式, setCompressionQuality(0.85f) 设置压缩质量。0.85 是我反复试下来清晰度与体积最平衡的值,肉眼几乎看不出差异,体积却比 0.95 小一半多。
参数说明:质量值范围 0f~1f,0.75 以下会出现明显压缩痕迹,0.9 以上体积明显增大但肉眼收益有限。如果做印刷级输出,可以设到 0.92~0.95,但要注意文件体积成倍增长。
5.3 验证输出:肉眼之外,用像素数据说话
生成 PDF 后,验证「高清」不能光靠眼睛。我一般会用一个小工具脚本把 PDF 页面转成图片,然后对比源图的像素数据和页面渲染出的图片像素数据,看有没有明显差异。这里提供一个简单的思路:用 PDF 渲染库把 PDF 转图片,再比较分辨率。
# 需要安装 pdf2image 之类的渲染组件
python -m pip install pdf2image
python -c "
from pdf2image import convert_from_path
images = convert_from_path('output.pdf', dpi=150)
print(images[0].size)
print(images[0].save('page1.png'))
"
逻辑说明: convert_from_path 指定 DPI 为 150 渲染 PDF,得到的是这个 DPI 下的位图。如果渲染出的图片尺寸接近源图尺寸(按比例换算),说明 PDF 里的图片物理尺寸和像素密度是匹配的,没有发生明显的重新采样或缩放失真。如果渲染出的图明显小于源图按 DPI 换算出的尺寸,那说明图片在 PDF 里被缩小了,阅读器在放大查看时必然模糊。
参数说明: dpi=150 必须和你在 PDF 里预设的 DPI 一致,否则渲染尺寸对不上。这个验证脚本适合本地调试,不建议写进生产逻辑,毕竟渲染 PDF 属于重计算,没法用在每个导出请求里。
从那以后,我每次做 iText 的图片 PDF 导出,都会先算一遍页面尺寸和 DPI 的换算关系,再决定压缩级别,而不是直接把图片往 Document 里一丢。这套流程帮我处理过证书批量导出、单据归档、甚至是某个图像处理 Demo 的结果预览,几乎没有再被「图片不清晰」这种问题折腾过。希望这些参数细节和踩坑记录能帮你绕开我当年走过的弯路。
以上就是Java借助iText实现将图片转换成高清PDF的详细内容,更多关于Java iText图片转PDF的资料请关注脚本之家其它相关文章!
