Java+iText扫描PDF压缩难题:仅可压缩1MB以上文件求优化方案
针对你遇到的小扫描PDF压缩无效的问题,我来分享一些实用的优化思路、技术手段和最佳实践,帮你搞定这些1MB以下的文件:
为什么小扫描PDF压缩没效果?
扫描生成的PDF本质是内嵌图像集合,1MB以下的文件可能已经做了基础压缩,但还有不少潜在冗余可以挖:
- 图像色彩空间可能是RGB(扫描件其实转灰度甚至黑白二值就足够)
- 分辨率过高(扫描件300DPI完全满足打印/识别需求,有些扫描默认会设到600DPI)
- PDF结构里藏着冗余元数据、未使用的资源
- 压缩算法没选对(比如给黑白文字图用了JPEG,而不是专门的CCITT压缩)
具体优化思路与技术手段
1. 图像层面深度优化(核心!)
扫描PDF的体积几乎全由内嵌图像决定,这部分是优化重点:
- 转色彩空间:文字类扫描件直接转黑白二值,图文混合转灰度,能砍一半以上体积
- 降分辨率:把超过300DPI的图像按比例缩小,画质几乎无损失
- 选对压缩算法:
- 黑白文字图:用CCITT Group 4压缩(专门针对黑白图像的高效压缩)
- 灰度/彩色图:用JPEG2000(比传统JPEG压缩率更高,画质损失更小)或调整JPEG质量参数
2. PDF结构与资源优化
- 移除PDF里的冗余元数据、注释、表单域(扫描件根本不需要这些)
- 合并重复出现的图像资源(比如多页重复的页眉logo)
- 启用PDF对象压缩和交叉引用表压缩
- 清理未使用的字体、XObject等资源
3. iText代码改进方案
你现有的代码只做了简单的图像缩放和压缩级别设置,针对小文件需要更精细化处理,给你改了一版:
import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.plugins.jpeg.JPEGImageWriteParam; import javax.imageio.stream.ImageOutputStream; import com.itextpdf.text.DocumentException; import com.itextpdf.text.pdf.*; import com.itextpdf.text.pdf.parser.PdfImageObject; public class SmallPdfCompressor { // 扫描件目标分辨率(300DPI足够) private static final int TARGET_DPI = 300; // JPEG质量参数(0-1,越小压缩率越高,0.6兼顾画质和体积) private static final float JPEG_QUALITY = 0.6f; // 文字类扫描件建议开启灰度转换 private static final boolean CONVERT_TO_GRAY = true; public void compressScannedPdf(String src, String dest) throws IOException, DocumentException { PdfReader reader = new PdfReader(src); reader.setFullCompression(); int pageCount = reader.getNumberOfPages(); for (int i = 1; i <= pageCount; i++) { PdfDictionary pageDict = reader.getPageN(i); PdfDictionary resources = pageDict.getAsDict(PdfName.RESOURCES); if (resources == null) continue; PdfDictionary xObjects = resources.getAsDict(PdfName.XOBJECT); if (xObjects == null) continue; for (PdfName key : xObjects.getKeys()) { PdfObject obj = xObjects.get(key); if (obj.isIndirect()) { obj = reader.getPdfObject(obj.getIndirectReference().getNumber()); } if (obj instanceof PRStream stream) { PdfName subtype = stream.getAsName(PdfName.SUBTYPE); if (PdfName.IMAGE.equals(subtype)) { processImageStream(reader, stream); } } } } PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest), PdfWriter.VERSION_1_7); stamper.getWriter().setCompressionLevel(9); stamper.setFullCompression(); stamper.close(); reader.close(); } private void processImageStream(PdfReader reader, PRStream stream) throws IOException { PdfImageObject imageObj = new PdfImageObject(stream); BufferedImage originalImage = imageObj.getBufferedImage(); if (originalImage == null) return; // 1. 转换为灰度 BufferedImage processedImage = originalImage; if (CONVERT_TO_GRAY) { processedImage = convertToGrayscale(originalImage); } // 2. 调整分辨率 float originalDpi = getImageDpi(stream); if (originalDpi > TARGET_DPI) { float scaleFactor = TARGET_DPI / originalDpi; processedImage = resizeImage(processedImage, scaleFactor); } // 3. 选择最优压缩算法 ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (CONVERT_TO_GRAY && isBlackWhiteCandidate(processedImage)) { // 黑白图像用CCITT Group4压缩 byte[] ccittData = convertToCcitt(processedImage); stream.setData(ccittData, true, 9); stream.put(PdfName.FILTER, PdfName.CCITTFAXDECODE); PdfDictionary params = new PdfDictionary(); params.put(PdfName.K, new PdfNumber(-1)); // Group4压缩标识 params.put(PdfName.COLUMNS, new PdfNumber(processedImage.getWidth())); params.put(PdfName.ROWS, new PdfNumber(processedImage.getHeight())); stream.put(PdfName.PARAMS, params); } else { // 灰度/彩色图用指定质量的JPEG压缩 saveJpegWithQuality(processedImage, baos, JPEG_QUALITY); stream.setData(baos.toByteArray(), true, 9); stream.put(PdfName.FILTER, PdfName.DCTDECODE); stream.put(PdfName.BITSPERCOMPONENT, new PdfNumber(8)); stream.put(PdfName.COLORSPACE, CONVERT_TO_GRAY ? PdfName.DEVICEGRAY : PdfName.DEVICERGB); } // 更新图像尺寸参数 stream.put(PdfName.WIDTH, new PdfNumber(processedImage.getWidth())); stream.put(PdfName.HEIGHT, new PdfNumber(processedImage.getHeight())); stream.remove(new PdfName("ITXT_SpecialId")); } private BufferedImage convertToGrayscale(BufferedImage original) { BufferedImage grayImage = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_BYTE_GRAY); Graphics2D g = grayImage.createGraphics(); g.drawImage(original, 0, 0, null); g.dispose(); return grayImage; } private BufferedImage resizeImage(BufferedImage original, float scaleFactor) { int newWidth = Math.round(original.getWidth() * scaleFactor); int newHeight = Math.round(original.getHeight() * scaleFactor); BufferedImage resized = new BufferedImage(newWidth, newHeight, original.getType()); Graphics2D g = resized.createGraphics(); g.setTransform(AffineTransform.getScaleInstance(scaleFactor, scaleFactor)); g.drawImage(original, 0, 0, null); g.dispose(); return resized; } private float getImageDpi(PRStream stream) { PdfDictionary decodeParams = stream.getAsDict(PdfName.DECODEPARMS); if (decodeParams != null) { PdfNumber hDpi = decodeParams.getAsNumber(new PdfName("HWResolution")); if (hDpi != null) return hDpi.floatValue(); } return 300f; // 默认300DPI } private boolean isBlackWhiteCandidate(BufferedImage image) { // 判断是否为文字类黑白图(可根据实际调整阈值) DataBufferByte buffer = (DataBufferByte) image.getRaster().getDataBuffer(); byte[] pixels = buffer.getData(); int whiteThreshold = 240; int blackCount = 0; for (byte pixel : pixels) { int val = pixel & 0xFF; if (val < whiteThreshold) blackCount++; } return (blackCount / (float)pixels.length) < 0.5f; } private byte[] convertToCcitt(BufferedImage image) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter writer = PdfWriter.getInstance(null, baos); writer.open(); PdfContentByte cb = writer.getDirectContent(); cb.addImage(com.itextpdf.text.Image.getInstance(image), image.getWidth(), 0, 0, image.getHeight(), 0, 0); writer.close(); return baos.toByteArray(); } private void saveJpegWithQuality(BufferedImage image, ByteArrayOutputStream baos, float quality) throws IOException { ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next(); JPEGImageWriteParam param = (JPEGImageWriteParam) writer.getDefaultWriteParam(); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionQuality(quality); try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) { writer.setOutput(ios); writer.write(null, new javax.imageio.IIOImage(image, null, null), param); } writer.dispose(); } public static void main(String[] args) throws IOException, DocumentException { new SmallPdfCompressor().compressScannedPdf("input.pdf", "compressed_output.pdf"); } }
4. PdfBox替代方案
如果iText的调整效果不够,试试PdfBox的专门扫描PDF压缩逻辑:
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.util.ImageIOUtil; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.List; public class PdfBoxCompressor { public static void compressScannedPdf(String src, String dest) throws IOException { try (PDDocument document = PDDocument.load(new File(src))) { for (PDPage page : document.getPages()) { List<PDImageXObject> images = page.getResources().getImages(); for (PDImageXObject image : images) { BufferedImage original = image.getImage(); // 转灰度 BufferedImage grayImage = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_BYTE_GRAY); Graphics2D g = grayImage.createGraphics(); g.drawImage(original, 0, 0, null); g.dispose(); // 调整分辨率到300DPI float scale = 300f / image.getDPI(); int newWidth = Math.round(original.getWidth() * scale); int newHeight = Math.round(original.getHeight() * scale); BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY); g = resized.createGraphics(); g.drawImage(grayImage, 0, 0, newWidth, newHeight, null); g.dispose(); // 替换为压缩后的图像 PDImageXObject compressedImage = PDImageXObject.createFromByteArray(document, ImageIOUtil.writeImageToBytes(resized, "JPEG", 60), "compressed"); page.getResources().put(image.getName(), compressedImage); } } document.save(dest); } } public static void main(String[] args) throws IOException { compressScannedPdf("input.pdf", "compressed_output.pdf"); } }
推荐书籍
- 《iText in Action: Creating and Manipulating PDF》:深入讲解iText的PDF处理细节,包含大量图像压缩优化案例
- 《PDF Explained: The Definitive Guide to PDF》:帮你理解PDF底层结构,找到更多隐藏的优化点
- 《Apache PDFBox Cookbook》:针对PdfBox的实用操作指南,专门有扫描PDF优化的章节
最佳实践
- 分场景优化:纯文字扫描件优先转黑白+CCITT压缩,图文混合用灰度+JPEG2000
- 批量处理:写个脚本批量处理所有文件,省得手动操作
- 质量校验:压缩后一定要检查清晰度,确保不影响后续打印或OCR识别
- 对比基准:用Adobe Acrobat的"优化PDF"功能做参考,对比自己实现的压缩效果
- 更新依赖:用最新版的iText/PdfBox,新版本通常会优化压缩算法和性能
内容的提问来源于stack exchange,提问作者Ion Gatman
相关产品推荐
相关产品推荐

