You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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优化的章节
最佳实践
  1. 分场景优化:纯文字扫描件优先转黑白+CCITT压缩,图文混合用灰度+JPEG2000
  2. 批量处理:写个脚本批量处理所有文件,省得手动操作
  3. 质量校验:压缩后一定要检查清晰度,确保不影响后续打印或OCR识别
  4. 对比基准:用Adobe Acrobat的"优化PDF"功能做参考,对比自己实现的压缩效果
  5. 更新依赖:用最新版的iText/PdfBox,新版本通常会优化压缩算法和性能

内容的提问来源于stack exchange,提问作者Ion Gatman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 16:45:29