资讯动态

Java中使用iText将PNG转高清PDF:DPI换算与避坑指南

发布时间:2026/10/19 3:18:20 来源:尧图企业网站定制
简介将图片转为高清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 groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.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的点point1 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 / 目标DPI3.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 大图解码前先读尺寸避免无谓的OOMImageIO.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); IteratorImageReader 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_RGBJPEG不支持透明通道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 groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.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就告警提示。这也养成一个习惯任何“高清”功能都必须有一个数据层面的验收口径而不是“看起来还行”。希望这个验证脚本能帮你也把这道关口卡住。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑