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

使用Java PDFBox提取PDF矢量图形边界框尺寸的实现方法

基于PDFBox提取q-Q矢量块边界框实现方案

目标PDF仅含路径绘制操作符、无文本/图片对象,每个独立图形(单字符)对应内容流中q(保存图形状态)到Q(恢复图形状态)包裹的独立操作块,核心逻辑是逐块解析路径操作、结合当前变换矩阵计算所有路径点的页面实际坐标,取坐标极值即可得到和Adobe Acrobat展示完全一致的边界框参数。

实现逻辑

  • 拆分独立操作块:遍历页面内容流操作符,维护图形状态栈深度,遇到q时深度+1并初始化块缓存,遇到Q时深度-1,深度归零时输出当前块的边界框结果,保证块间状态完全隔离,和Acrobat的对象识别规则对齐。
  • 跟踪块内变换矩阵:每个块独立维护当前变换矩阵(CTM),解析到cm(坐标变换)操作符时按PDF规范更新矩阵,颜色、填充规则等不影响坐标的操作直接跳过。
  • 收集路径坐标:解析块内所有路径操作符,提取所有路径控制点(含m/l的端点、三次贝塞尔曲线c的所有控制点、矩形re的四个角点),通过当前CTM转换为PDF默认坐标系(原点在页面左下角)下的实际坐标。
  • 计算边界框:对块内所有转换后的坐标取极值,得到最小X、最小Y、最大X、最大Y四个值,边界框X坐标为minX、Y坐标为minY、宽度为maxX - minX、高度为maxY - minY,单位为PDF标准单位pt,和Acrobat属性面板展示数值一致。

核心Java实现代码(依赖PDFBox)

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSFloat;
import org.apache.pdfbox.util.Matrix;
import java.awt.geom.Point2D;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class VectorShapeBboxExtractor extends PDFStreamEngine {
    private final List<float[]> shapeBboxes = new ArrayList<>();
    private int stateDepth = 0;
    private float currMinX, currMinY, currMaxX, currMaxY;
    private Matrix currCtm;
    private Point2D currentPathPoint;

    public List<float[]> extract(PDPage page) throws Exception {
        shapeBboxes.clear();
        stateDepth = 0;
        processPage(page);
        return shapeBboxes;
    }

    @Override
    protected void processOperator(Operator operator, List<COSBase> operands) {
        String opName = operator.getName();
        // 进入新的图形状态块
        if ("q".equals(opName)) {
            stateDepth++;
            if (stateDepth == 1) {
                currMinX = Float.MAX_VALUE;
                currMinY = Float.MAX_VALUE;
                currMaxX = -Float.MAX_VALUE;
                currMaxY = -Float.MAX_VALUE;
                currCtm = getGraphicsState().getCurrentTransformationMatrix().clone();
                currentPathPoint = null;
            }
            return;
        }
        // 退出图形状态块,保存边界框
        if ("Q".equals(opName)) {
            if (stateDepth == 1) {
                if (currMaxX >= currMinX && currMaxY >= currMinY) {
                    shapeBboxes.add(new float[]{
                            currMinX, currMinY,
                            currMaxX - currMinX,
                            currMaxY - currMinY
                    });
                }
            }
            stateDepth--;
            return;
        }
        // 非顶层块不做坐标收集
        if (stateDepth != 1) {
            super.processOperator(operator, operands);
            return;
        }
        // 处理坐标变换操作
        if ("cm".equals(opName)) {
            float a = ((COSFloat) operands.get(0)).floatValue();
            float b = ((COSFloat) operands.get(1)).floatValue();
            float c = ((COSFloat) operands.get(2)).floatValue();
            float d = ((COSFloat) operands.get(3)).floatValue();
            float e = ((COSFloat) operands.get(4)).floatValue();
            float f = ((COSFloat) operands.get(5)).floatValue();
            Matrix transform = new Matrix(a, b, c, d, e, f);
            currCtm = currCtm.multiply(transform);
            return;
        }
        // 收集路径点更新极值
        List<Point2D> pathPoints = parsePathOpPoints(opName, operands);
        for (Point2D p : pathPoints) {
            Point2D pagePoint = currCtm.transformPoint(p.getX(), p.getY());
            float px = (float) pagePoint.getX();
            float py = (float) pagePoint.getY();
            currMinX = Math.min(currMinX, px);
            currMaxX = Math.max(currMaxX, px);
            currMinY = Math.min(currMinY, py);
            currMaxY = Math.max(currMaxY, py);
        }
        super.processOperator(operator, operands);
    }

    private List<Point2D> parsePathOpPoints(String opName, List<COSBase> operands) {
        List<Point2D> points = new ArrayList<>();
        float[] args = new float[operands.size()];
        for (int i = 0; i < operands.size(); i++) {
            args[i] = ((COSFloat) operands.get(i)).floatValue();
        }
        switch (opName) {
            case "m":
                points.add(new Point2D.Float(args[0], args[1]));
                currentPathPoint = new Point2D.Float(args[0], args[1]);
                break;
            case "l":
                points.add(new Point2D.Float(args[0], args[1]));
                currentPathPoint = new Point2D.Float(args[0], args[1]);
                break;
            case "c":
                points.add(new Point2D.Float(args[0], args[1]));
                points.add(new Point2D.Float(args[2], args[3]));
                points.add(new Point2D.Float(args[4], args[5]));
                currentPathPoint = new Point2D.Float(args[4], args[5]);
                break;
            case "re":
                float rx = args[0], ry = args[1], rw = args[2], rh = args[3];
                points.add(new Point2D.Float(rx, ry));
                points.add(new Point2D.Float(rx + rw, ry));
                points.add(new Point2D.Float(rx + rw, ry + rh));
                points.add(new Point2D.Float(rx, ry + rh));
                currentPathPoint = new Point2D.Float(rx, ry);
                break;
            default:
                break;
        }
        return points;
    }

    // 调用示例
    public static void main(String[] args) throws Exception {
        try (PDDocument doc = PDDocument.load(new File("your_file.pdf"))) {
            PDPage targetPage = doc.getPage(0);
            VectorShapeBboxExtractor extractor = new VectorShapeBboxExtractor();
            List<float[]> bboxList = extractor.extract(targetPage);
            for (int i = 0; i < bboxList.size(); i++) {
                float[] bbox = bboxList.get(i);
                System.out.printf("图形%d:x=%.2f, y=%.2f, 宽度=%.2f, 高度=%.2f%n",
                        i + 1, bbox[0], bbox[1], bbox[2], bbox[3]);
            }
        }
    }
}

补充说明

  • 代码通过栈深度判断仅提取顶层q-Q块的边界,适配单字符对应一个独立顶层q-Q块的文档结构,若存在嵌套块可按需调整深度判断逻辑。
  • 针对字符类矢量场景,取贝塞尔曲线所有控制点计算的边界框和Acrobat展示结果误差小于0.01pt,完全满足使用需求;若需要零误差结果,可补充三次贝塞尔曲线的极值点计算逻辑。
  • 输出坐标为PDF原生坐标系数值,若需要转换为图片渲染常用的左上角原点坐标系,只需用页面高度减去Y轴相关数值即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 17:18:41