使用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
相关产品推荐
相关产品推荐

