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

Java PDFBox提取PDF图片遇反转拆分(非工具Bug),求修复方案及代码

解决方案

这类PDF的图片被拆分为多个小图块,并通过页面变换矩阵(CTM)施加了翻转操作,可通过以下步骤恢复完整图片:

核心思路

  • 提取每页中所有图像块及其对应的变换矩阵(包含位置、缩放、翻转信息)
  • 根据所有图块的位置计算完整图片的实际尺寸
  • 将图块按坐标拼接成完整图像
  • 根据变换矩阵修正图像方向

Java代码实现

首先引入PDFBox核心依赖(Maven示例):

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

完整实现代码:

import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class PDFImageRestorer {
    static class ImageBlock {
        PDImageXObject image;
        AffineTransform transform;

        ImageBlock(PDImageXObject image, AffineTransform transform) {
            this.image = image;
            this.transform = transform;
        }
    }

    public static void main(String[] args) throws IOException {
        File pdfFile = new File("my.pdf");
        try (PDDocument document = PDDocument.load(pdfFile)) {
            for (int pageIdx = 0; pageIdx < document.getNumberOfPages(); pageIdx++) {
                PDPage page = document.getPage(pageIdx);
                List<ImageBlock> blocks = extractImageBlocks(page);

                if (blocks.isEmpty()) continue;

                // 计算完整图片的边界范围
                Rectangle totalBounds = calculateTotalBounds(blocks);
                BufferedImage fullImage = new BufferedImage(totalBounds.width, totalBounds.height, BufferedImage.TYPE_INT_RGB);
                Graphics2D g2d = fullImage.createGraphics();
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

                // 转换PDF坐标系(左下为原点)到Java图像坐标系(左上为原点)
                for (ImageBlock block : blocks) {
                    AffineTransform adjustedTransform = new AffineTransform(block.transform);
                    adjustedTransform.translate(0, totalBounds.height);
                    adjustedTransform.scale(1, -1);
                    g2d.drawImage(block.image.getImage(), adjustedTransform, null);
                }
                g2d.dispose();

                // 修正图像翻转方向
                BufferedImage corrected = fixOrientation(fullImage, blocks.get(0).transform);
                ImageIO.write(corrected, "PNG", new File("page_" + (pageIdx + 1) + "_fixed.png"));
            }
        }
    }

    // 提取页面内所有图像块及对应的变换矩阵
    private static List<ImageBlock> extractImageBlocks(PDPage page) throws IOException {
        List<ImageBlock> blocks = new ArrayList<>();
        PDFStreamEngine engine = new PDFStreamEngine() {
            @Override
            protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
                if ("Do".equals(operator.getName())) {
                    COSName imgName = (COSName) operands.get(0);
                    PDImageXObject image = (PDImageXObject) getResources().getXObject(imgName);
                    if (image != null) {
                        AffineTransform ctm = getGraphicsState().getCurrentTransformationMatrix();
                        blocks.add(new ImageBlock(image, new AffineTransform(ctm)));
                    }
                }
                super.processOperator(operator, operands);
            }
        };
        engine.processPage(page);
        return blocks;
    }

    // 计算所有图像块的整体边界
    private static Rectangle calculateTotalBounds(List<ImageBlock> blocks) {
        double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
        double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;

        for (ImageBlock block : blocks) {
            PDImageXObject img = block.image;
            AffineTransform transform = block.transform;

            Point2D[] corners = {
                new Point2D.Double(0, 0),
                new Point2D.Double(img.getWidth(), 0),
                new Point2D.Double(img.getWidth(), img.getHeight()),
                new Point2D.Double(0, img.getHeight())
            };

            for (Point2D corner : corners) {
                transform.transform(corner, corner);
                minX = Math.min(minX, corner.getX());
                minY = Math.min(minY, corner.getY());
                maxX = Math.max(maxX, corner.getX());
                maxY = Math.max(maxY, corner.getY());
            }
        }

        return new Rectangle(
            (int) Math.floor(minX),
            (int) Math.floor(minY),
            (int) Math.ceil(maxX - minX),
            (int) Math.ceil(maxY - minY)
        );
    }

    // 根据变换矩阵修正图像翻转方向
    private static BufferedImage fixOrientation(BufferedImage image, AffineTransform transform) {
        boolean flipHoriz = transform.getScaleX() < 0;
        boolean flipVert = transform.getScaleY() < 0;

        BufferedImage corrected = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
        Graphics2D g2d = corrected.createGraphics();

        if (flipHoriz) {
            g2d.drawImage(image, image.getWidth(), 0, -image.getWidth(), image.getHeight(), null);
        } else if (flipVert) {
            g2d.drawImage(image, 0, image.getHeight(), image.getWidth(), -image.getHeight(), null);
        } else {
            g2d.drawImage(image, 0, 0, null);
        }

        g2d.dispose();
        return corrected;
    }
}

关键说明

  • 变换矩阵解析:通过PDFBox的getGraphicsState().getCurrentTransformationMatrix()获取图像的位置、缩放和翻转信息,这是恢复图像的核心依据。
  • 坐标系转换:PDF以左下角为原点,Java图像以左上角为原点,因此需要通过平移和缩放修正坐标。
  • 方向修正:通过变换矩阵的缩放因子符号判断水平/垂直翻转,若PDF存在旋转需求,可扩展代码解析变换矩阵的旋转角度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 22:12:04