☰
Java中使用iText将PNG转高清PDF:DPI换算与避坑指南
2026/10/19 3:18:15 网站建设 项目流程

简介:将图片转为高清PDF是Java开发中常见需求,iText库可高效完成这一任务。这份资源面向有基础Java编程经验的开发者,提供一份基于iText的完整可运行Demo,重点解决默认转换清晰度不足的问题,通过设置DPI与选择合适缩放方式,确保PNG等图片输出为高清PDF。压缩包共11个文件,约1MB,包含两个iText jar包、一个Java源文件、JSP页面及XML/classpath等工程配置,并附带readme.txt说明,目录结构清晰,包含src、WebRoot等目录,便于直接导入Eclipse等IDE运行。已有5467人学习使用,适合需要快速实现图片转PDF功能或希望掌握iText核心API的开发者。通过这份资源,读者可了解从创建Document、加载图片到设置300 DPI并缩放输出的完整流程,同时熟悉iText中按宽度、高度或绝对尺寸缩放图片的灵活处理方式,最终获得一个可直接复用的无坑版项目模板,节省自行排查环境与依赖的时间。

1. 把PNG塞进PDF不等于高清:先想清楚你在“打印”还是“看图”

你八成和我遇到过的某同事一样,手里有一批PNG图片要转成PDF,第一反应是拿itext直接写个循环,把图片add进Document,一运行还挺顺利,客户却说打印出来糊成一片。这里有个反直觉的结论:PDF里没有“像素密度”这个属性,只有物理尺寸。一张8000px的PNG被直接放进去,默认按72DPI解释,页面上一张图就可能超过整张A4纸,而当你把它缩到页面大小时,缩放反而把有效像素浪费掉了。真正的高清PDF,核心不是图片有多清晰,而是图片像素与PDF页面物理尺寸的换算关系是否合理。这篇文章就以itext 5为基础,从选型讲到DPI换算,给出可以直接照抄的生成demo,再把高清方向上的内存、旋转、授权、验证这些坑逐个填一遍,适合正在做图片转PDF功能但被“不模糊但也不够清晰”卡住的Java开发者。

2. itext 5还是itext 7:清晰度的起点在选型这一步就定了

很多人上来就搜“itext图片生成PDF”,结果资料一半是5.x写法,一半是7.x写法,Maven坐标都对不上,最后卡在ClassNotFoundException上。这里先把库本身说清楚:itext 5和itext 7不是小版本升级关系,它们的包名、核心API和Document模型完全不兼容,官方也明确表示5.x进入维护期。对于“把图片转成高清PDF”这种相对固定的批处理场景,我建议直接用itext 5,理由下面详细说。

2.1 两个版本差异:连坐标和包名都是两套体系

itext 5的Maven坐标是这样:

<dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13.3</version> </dependency>

核心包名是com.itextpdf.text,入口类是com.itextpdf.text.Document配合com.itextpdf.text.pdf.PdfWriter。itext 7的坐标则是com.itextpdf的itext7-core系列(比如kernel、layout、io三个模块要分开引入),包名是com.itextpdf.kernel和com.itextpdf.layout,入口类变成了com.itextpdf.kernel.pdf.PdfDocument配合com.itextpdf.layout.Document。

如果你照着5.x的博客用7.x依赖跑,第一行PdfWriter.getInstance就会直接报ClassNotFoundException,因为7.x里PdfWriter的构造方式是new PdfWriter(dest),设计思路完全变了。7.x把底层读写与高层布局拆开,理论上能做更精细的排版控制,但对于“图片铺满一页生成PDF”这种需求,这些能力并不需要,反而多了一层概念负担。

2.2 itext 5最简Demo:九行代码出一张图片的PDF

选完版本就看核心用法。itext 5里一张PNG生成PDF,最简代码是这样的:

Document document = new Document(new Rectangle(595, 842), 0, 0, 0, 0); PdfWriter.getInstance(document, new FileOutputStream("output.pdf")); document.open(); Image image = Image.getInstance("input.png"); image.setAbsolutePosition(0, 0); image.scaleToFit(595, 842); document.add(image); document.close();

这段代码只做了四件事:创建A4尺寸的页面、创建writer、把图片定位到页面左下角并缩放至A4内、写盘。new Rectangle(595, 842)是A4页面尺寸,单位是PDF的点(point),1 point等于1/72英寸,所以A4宽是595点、高是842点。scaleToFit的意思是等比缩放,目标矩形是页面宽高,图片不会变形。

这里有个容易误会的点:setAbsolutePosition(0, 0)设置的是图片左下角的坐标,不是居中。图片会从页面左下角开始摆放,方向朝右和朝上,整个页面是笛卡尔坐标系。如果你想居中放置,需要自己计算偏移:

float x = (595 - image.getScaledWidth()) / 2; float y = (842 - image.getScaledHeight()) / 2; image.setAbsolutePosition(x, y);

getScaledWidth()返回的是scaleToFit之后的物理宽度,这个值以点为单位的,和PNG原始像素数没有直接关系。这是理解后面“高清”逻辑的关键分水岭。

2.3 为什么我建议新项目也从itext 5起步

itext 7的布局模型更适合做多段落、多元素的复杂排版,比如合同、报表这类文字和图片混排的文档。但纯图片转PDF的场景,通常是一张图一页,或者批量图片顺序导出,需要的功能非常有限,itext 5足足够用,而且网上搜“itext Image PDF”能搜到的大多是5.x的代码,照着排错也方便。

另外有个现实因素要考虑:itext 5是AGPL开源协议,itext 7同样有AGPL版本和商业版本之分。AGPL要求如果通过网络提供服务的软件用了它,需要开源对应代码。对于公司内部工具或者离线脚本,这个问题不大;如果是对外提供下载或SaaS服务,务必让法务确认一下授权边界。这不是说5比7更宽松,而是它便宜好查、资料稳定,维护成本低。

3. 高清生成的完整实现:像素、页面与DPI的换算公式和代码落地

高清版和能跑的版本之间,差的不是图片质量,而是三个数字:目标DPI、页面物理尺寸(point)、图片有效像素。这三个数的换算关系搞清楚了,代码怎么改都是顺的。

3.1 高清的本质:先算DPI再决定要不要缩放

PDF的页面是由物理点组成的,一页A4宽595pt。图片进入PDF后,PDF并不关心图片原始像素是多少,只关心它在页面上占多大物理矩形。假设一张图原图是3000px宽,你想让它占满整行(大约595pt),那么在PDF里图片的“有效DPI”就等于3000除以页面物理宽度(单位英寸)。595pt等于8.26英寸,3000除以8.26约等于363DPI,这是一个比较高的清晰度。同样的图,如果你只让它显示半页宽(约297pt),那有效DPI就是3000除以4.13约等于726DPI,肉眼很难分辨区别。

反过来,如果你拿一张1000px宽的手机截图,强行scaleToFit(595, 842)铺满整页,有效DPI只有1000除以8.26约等于121DPI,打印出来一放大就是马赛克。很多所谓“高清版”其实只是原图分辨率高,放进PDF时用了一个错误的物理尺寸,把高分辨率浪费掉了。

所以高清生成的正确路径是:先确定你要达到多少有效DPI,然后算图片在页面上应该显示的物理尺寸。打印场景一般要求300DPI,屏幕浏览150DPI足够。公式是:

图片在PDF中应显示的宽度(pt) = 图片像素宽度 × 72 / 目标DPI 图片在PDF中应显示的高度(pt) = 图片像素高度 × 72 / 目标DPI

3.2 一张图生成高清PDF:连图片属性一起算

下面这段代码是完整的单图转PDF版本,它先读图宽高,再按目标300DPI计算图片应该占的物理尺寸,最后按这个尺寸放入A4页面并居中:

import com.itextpdf.text.Document; import com.itextpdf.text.Image; import com.itextpdf.text.PageSize; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.PdfWriter; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; public class PngToPdfHighDpi { public static void main(String[] args) throws Exception { File src = new File("input.png"); BufferedImage bi = ImageIO.read(src); int pxWidth = bi.getWidth(); // 原图像素宽度 int pxHeight = bi.getHeight(); // 原图像素高度 double targetDpi = 300.0; // 打印级清晰度 double widthPt = pxWidth * 72.0 / targetDpi; double heightPt = pxHeight * 72.0 / targetDpi; Rectangle pageRect = new Rectangle(595, 842); Document document = new Document(pageRect, 20, 20, 20, 20); PdfWriter.getInstance(document, new FileOutputStream("output_high.pdf")); document.open(); Image image = Image.getInstance(src.getAbsolutePath()); image.scaleToFit((float) widthPt, (float) heightPt); float x = (595 - image.getScaledWidth()) / 2; float y = (842 - image.getScaledHeight()) / 2; image.setAbsolutePosition(x, y); document.add(image); document.close(); System.out.println("生成完成,图片物理尺寸:" + Math.round(widthPt) + " x " + Math.round(heightPt) + " pt"); } }

这段代码最值得看的是换算那三行:pxWidth * 72 / targetDpi把像素换算成pt。300DPI下,宽度3000px的图片计算结果是720pt,比A4宽度还宽,所以图片会等比缩放后居中铺在A4内,实际有效DIP会等于或略高于300。如果算出来小于页面可用空间,说明原图分辨率不足以支持300DPI,这时候代码不会报错,但输出PDF在打印时清晰度达不到预期——这种情况应该提前提示,而不是默默生成。ImageIO.read(src)同时负责读取图片尺寸和像素数据,图片过大会吃内存,后面第5章会专门处理。

3.3 批量图片生成:多页循环与文件排序

实际使用场景里,单图转PDF很少见,更多是把一个文件夹里几十张PNG按文件名顺序合成一个PDF,每张占一页。批量版本要额外注意两个问题:文件排序不能只依赖listFiles()的默认顺序,不同系统返回顺序不一致;每一页之间要显式newPage(),否则前一张会被后一张留在同一布局流里挤掉。下面这段可以直接用于批量:

import com.itextpdf.text.Document; import com.itextpdf.text.Image; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.PdfWriter; import java.io.File; import java.io.FileOutputStream; import java.util.Arrays; public class BatchPngToPdf { public static void main(String[] args) throws Exception { File dir = new File("images"); File[] files = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".png") || name.toLowerCase().endsWith(".jpg")); if (files == null || files.length == 0) { return; } Arrays.sort(files); Rectangle pageRect = new Rectangle(595, 842); Document document = new Document(pageRect, 0, 0, 0, 0); PdfWriter.getInstance(document, new FileOutputStream("batch_high.pdf")); document.open(); for (File file : files) { Image image = Image.getInstance(file.getAbsolutePath()); image.scaleToFit(595, 842); float x = (595 - image.getScaledWidth()) / 2; float y = (842 - image.getScaledHeight()) / 2; image.setAbsolutePosition(x, y); document.newPage(); document.add(image); } document.close(); System.out.println("共生成 " + files.length + " 页"); } }

document.newPage()放在add之前是为了保证第一页正常创建、从第二张图开始另起新页。scaleToFit(595, 842)这里直接按A4物理尺寸缩放,不按DPI算,因为批量场景下图片尺寸差异很大,统一铺满页面是更实用的策略。如果某张图只有600px宽,放进A4后有效DPI约72,只能适合屏幕浏览,这是原图的物理限制,代码层面无法凭空提升。商业场景下这种低分辨率图片需要在导出前单独统计出来,或者交给后续的人工替换。

4. 高清PDF避坑实录:五条血泪经验

图片转PDF看着简单,实际上坑都藏在细节里。下面五条是几乎每次都会踩一遍的问题,每条都按“现象→原因→解决”写清楚,直接对照排查。

4.1 高清原图放进PDF反而变小了

现象:原图明明有6000px宽,生成的PDF里图片只有一点点大,旁边大片空白。原因:这就是第3章说到的72DPI默认解释。itext 5读取图片后,如果没有调用scaleToFit或scaleAbsolute,会按1像素对应1pt放置,6000px图片物理宽度就是6000pt,远大于页面595pt。你以为的“原图放进去”,实际被当成了“6000点宽的贴纸”,显示时自然超出页面,且四周空白。解决:先做DPI换算,再用scaleToFit把图片缩放到页面的可用范围内,不要直接add。在生成后打印image.getScaledWidth()和页面宽度做对比,肉眼确认图片物理尺寸是不是接近页面。

4.2 图片旋转后超出页面边界

现象:手机竖拍的照片,PNG本身是竖构图,但PDF生成后图片横了过来,或者旋转后边缘被裁掉一块。原因:EXIF方向信息没有被itext 5读取,图片被当作未旋转的原始像素矩阵处理。如果原始图像方向是横向存储、预览需要旋转90度显示,PDF里直接放置就会横置。解决:在缩放和定位之前先调用image.setRotationDegrees(90)或image.setRotation(Math.PI / 2),注意itext 5的旋转是围绕图片左下角进行的,旋转之后图片的显示宽高会互换,所以scaleToFit的参数要按旋转后的物理方向传,即之前按宽高的计算,旋转后要交换宽高的目标值再缩放,否则一张长图还是会超出页面。

4.3 大图直接OOM,报Java heap space

现象:一张几万像素的扫描件PNG,代码跑到ImageIO.read或者Image.getInstance时直接抛出OutOfMemoryError: Java heap space。原因:这两个方法都会把整张图片的像素数据加载进JVM内存,以RGBA格式算,一张15000×12000的图大约需要700MB内存,默认堆大小根本扛不住。解决:不要直接读全量像素。先用ImageReader读取图片宽高,判断是否需要降采样或缩放后再解码,这个写法在第5章详细给出。顺带说,这类问题如果持续出现,可以给JVM加-Dcom.sun.management.jmxremote这类监控参数观察堆占用,但治本还是要在数据进内存之前控制像素总量。

4.4 不同Maven依赖同时引入两个itext版本

现象:项目里本来有一个老模块用itext 5,新模块引入itext 7后,原本正常的图片导出偶尔出现NoClassDefFoundError或ClassCastException,而且没有固定的出错位置。原因:itext 5和itext 7的包名不同,但类名有重叠,比如都叫Document、Image,如果两个版本的jar都在classpath里,Java类加载顺序随机,某些时候5.x的类被当成7.x的类使用,直接类型转换失败。解决:全项目统一itext大版本,或者用Maven的dependencyManagement锁定版本,彻底排除另一个版本的依赖。只有图片转PDF需求就只保留一个,别图新鲜混用。

4.5 AGPL协议像定时炸弹

现象:生成的PDF在内部跑没问题,一旦你做的软件被客户以SaaS方式使用,突然收到法务邮件要求检查开源依赖。原因:itext 5和itext 7的AGPL协议要求凡是把库集成到通过网络提供服务的软件中,需要将整个应用的源码以AGPL方式开放,很多开发者在引入依赖时完全忽略了这一点。解决:商用前查清楚公司是否有itext商业授权,没有的话建议替换方案,比如用org.apache.pdfbox的PDFBox直接绘制图片,或者只用itext做本地离线导出工具。这条不是代码问题,但翻车代价最大,一定放在最前面想。

5. 批量大图的参数调优:内存阈值、超长图分页与方向自适应

批量图片转PDF最容易翻车的不是逻辑,而是资源耗尽。图片数量一多、尺寸一大,只要有一颗“雷”图,整个任务就崩。这一章把大图处理做厚一些,给出一套可以直接塞进现有流程的稳健方案。

5.1 大图解码前先读尺寸,避免无谓的OOM

ImageIO.read是无条件全量解码的。更稳的做法是利用ImageReader先读图片元信息,拿到宽高后再决定怎么处理:

import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.io.File; import java.util.Iterator; public class ImageMetaReader { public static void main(String[] args) throws Exception { File file = new File("large.png"); ImageInputStream iis = ImageIO.createImageInputStream(file); Iterator<ImageReader> readers = ImageIO.getImageReaders(iis); ImageReader reader = readers.next(); reader.setInput(iis, true, true); int width = reader.getWidth(0); int height = reader.getHeight(0); System.out.println("图片尺寸:" + width + " x " + height); reader.dispose(); iis.close(); } }

reader.setInput(iis, true, true)里的第二个参数是seekForwardOnly,置为true时只允许顺序读取,读取速度更快;第三个参数是ignoreMetadata,置为true会忽略源图片的元数据,减少内存占用。这个方法只管读尺寸,不会把像素加载进内存。拿到尺寸后,我一般这样判断:如果单边超过5000px,或者宽高乘积超过2500万像素,就先做缩放再进PDF,目标边长控制在3000px以内,这样既保证300DPI下最大物理尺寸约254mm,又能把内存占用压到可控范围。

5.2 超长图自动分页:按页面高度切片

聊天截图、网页长图这类超长PNG,直接缩放进一页会糊成一团,应该保留原始像素切片,每片铺一页A4。做法是按目标页面物理高度对应的像素高度切分:

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.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; public class SliceLongImageToPdf { public static void main(String[] args) throws Exception { BufferedImage full = ImageIO.read(new File("long.png")); int pageWidthPx = 1754; // 对应595pt在300DPI下的像素宽 int pageHeightPx = 2480; // 对应842pt在300DPI下的像素高 Rectangle pageRect = new Rectangle(595, 842, 0); Document document = new Document(pageRect, 0, 0, 0, 0); PdfWriter.getInstance(document, new FileOutputStream("sliced.pdf")); document.open(); int totalSlices = (full.getHeight() + pageHeightPx - 1) / pageHeightPx; for (int i = 0; i < totalSlices; i++) { int srcY = i * pageHeightPx; int currentH = Math.min(pageHeightPx, full.getHeight() - srcY); BufferedImage slice = full.getSubimage(0, srcY, pageWidthPx, currentH); Image image = Image.getInstance(slice, null); image.scaleToFit(595, 842); document.newPage(); document.add(image); } document.close(); } }

分页逻辑没有采用居中策略,而是从顶部开始顺序裁切,这样可以保持阅读习惯。Rect(595, 842, 0)中第三个参数是旋转角度,传0表示页面不旋转。full.getSubimage的裁剪代价极低,它不复制像素数据,而是共享原始光栅,所以即使图很大,切片本身也不会造成内存翻倍。要注意的是,如果长图宽度超过pageWidthPx,上面的代码会把两侧直接裁掉,更稳妥的做法是先等比例缩放宽度到pageWidthPx,再横向居中裁切。具体分成多少页,可以在循环前先算一次totalSlices,提前写入日志,方便这条任务跑挂时知道大概进度。

5.3 图片超过阈值改用JPEG编码,文件体积显著下降

PNG是保留透明通道的无损格式,扫描件、截图层层叠叠的图片用PNG直接放进PDF,输出文件会被撑到几百MB。我发现一个规律:当单张图片超过2MB时,转换成高质量JPEG再入PDF,视觉差异极小,体积能缩到原来的十分之一以下。下面是转换示例:

import javax.imageio.ImageIO; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class PngToJpg { public static void main(String[] args) throws Exception { BufferedImage src = ImageIO.read(new File("input.png")); int width = src.getWidth(); int height = src.getHeight(); BufferedImage rgb = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = rgb.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.drawImage(src, 0, 0, null); g.dispose(); OutputStream out = new FileOutputStream("input.jpg"); ImageIO.write(rgb, "jpg", out); out.close(); } }

关键是TYPE_INT_RGB:JPEG不支持透明通道,PNG里的透明区域会默认变成黑色,必须先绘制到一张RGB画布上,背景色可以自己用g.setColor(Color.WHITE)配合fillRect先铺白,再画原图。VALUE_INTERPOLATION_BICUBIC是双三次插值,缩放质量比默认的双线性更细腻,但耗时也略高。处理完之后,再用前面的代码把这张JPEG放入PDF。这个方案适合不要求保留透明通道的常规图片,如果图片本身是图标、带透明背景,就不要走JPEG,保持PNG就好。

5.4 目标DPI自适应填充页面

最后补一个细节:有时候原图分辨率不够300DPI,强行按300DPI换算会得到一个远小于页面的图片,四周留白。这时候常见做法是自动降级到150DPI,保证图片尽量铺满页面。判断标准是换算后的pt是否小于页面可用区域的一半。自适应代码可以这样写:

double targetDpi = 300.0; float widthPt = (float) (pxWidth * 72 / targetDpi); float heightPt = (float) (pxHeight * 72 / targetDpi); float usableW = 595 - 40; float usableH = 842 - 40; if (widthPt < usableW * 0.5 || heightPt < usableH * 0.5) { targetDpi = 150.0; widthPt = (float) (pxWidth * 72 / targetDpi); heightPt = (float) (pxHeight * 72 / targetDpi); }

usableW * 0.5这个阈值是允许图片在页面上保留一半以上的空间,低于这个值说明原图像素过低,强行高清只会得到一张“高清但很小”的图,不如退一步保证版面完整。这个策略应该在批量任务里作为配置项暴露出来,不同业务对清晰度和观感的要求不一样,别写死。

6. 验证高清效果:把生成的PDF用解析器打回原形

代码写完,怎么证明这张PDF确实达到了目标DPI?靠肉眼放大是玄学,最靠谱的是用PDFBox把图片从页面里抽出来,逆向算出实际DPI。

先引入PDFBox(这里是org.apache.pdfbox,和itext无关):

<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.28</version> </dependency>

然后读取PDF第一页,拿到页面上的图片对象,输出宽度高度:

import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import java.io.File; public class PdfImageInspector { public static void main(String[] args) throws Exception { try (PDDocument doc = PDDocument.load(new File("output_high.pdf"))) { PDPage page = doc.getPage(0); PDResources resources = page.getResources(); for (COSName name : resources.getXObjectNames()) { if (resources.isImageXObject(name)) { PDImageXObject img = (PDImageXObject) resources.getXObject(name); System.out.println("图片对象: " + name.getName()); System.out.println("像素宽高: " + img.getWidth() + " x " + img.getHeight()); System.out.println("页面尺寸(pt): " + page.getMediaBox().getWidth() + " x " + page.getMediaBox().getHeight()); } } } } }

运行后就能直接做换算:假设页面上图片宽度占整页595pt,抽出的图片对象宽2400px,那么实际DPI是2400 / (595 / 72)约等于290,接近300DIP的目标。我记得第一次拿到这个数值时才发现,自己之前做的功能实际只有140DPI,印刷必然模糊,调整了缩放逻辑后,客户立刻反馈清晰度明显改善。从那以后,我每次导出PDF都强制走一遍这个检查,输出到日志里,低于目标DPI就告警提示。这也养成一个习惯:任何“高清”功能,都必须有一个数据层面的验收口径,而不是“看起来还行”。希望这个验证脚本能帮你也把这道关口卡住。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询