如何使用iText解析PDF路径(如文档线条、文本框等图形元素)
实现方案
你当前使用的基础RenderListener只能接收到文本渲染事件,要提取线条、矩形这类矢量图形内容,需要把自定义监听器改成实现ExtRenderListener接口——它是RenderListener的子接口,额外暴露了路径绘制、裁剪区域的回调方法,刚好可以满足两类内容的提取需求。
1. 提取文档中的水平线条
水平线条属于PDF描边路径的一种,在renderPath(PathRenderInfo)回调中按以下逻辑处理即可:
- 先过滤操作类型:只处理带
STROKE(描边)标记的路径,填充类路径是色块不需要处理 - 遍历路径下的所有段,筛选出
Line类型的直线段 - 取线段两个端点的Y坐标,差值小于设定阈值(一般取0.5f即可,兼容PDF坐标浮点误差)就判定为水平线,记录它的X轴起止范围、Y轴坐标即可
注意:PDF默认坐标系原点在页面左下角,如果需要匹配视觉上从上到下的位置,用页面高度减去Y坐标做转换即可。
2. 提取方框内的文本内容
方框本质是闭合的描边矩形路径,处理分两步:
- 同样在
renderPath回调里识别矩形:筛选由4条直线段首尾闭合组成的路径,校验对边平行等长、邻边垂直后,计算并记录矩形的左、右、上、下边界坐标 - 你在
renderText回调中拿到的每个文本块,可以通过TextRenderInfo获取自身的包围矩形,等整页内容渲染完成后,统一判断文本块的包围矩形是否落在对应方框的边界范围内(留1单位左右的容差兼容误差),匹配到的文本就属于对应方框内的内容。
不建议边渲染边匹配:PDF内容流按绘制顺序执行,存在先画文本后画方框的场景,整页内容收集完成后再匹配准确率更高。
核心实现代码
public class CustomRenderListener implements ExtRenderListener { private Rectangle currentPageSize; // 存储当前页识别到的方框 private final List<Rectangle> pageBoxList = new ArrayList<>(); // 存储当前页识别到的水平线,数组结构为[X起始坐标, X结束坐标, Y坐标] private final List<float[]> pageHorizontalLineList = new ArrayList<>(); // 存储当前页所有文本块 private final List<TextRenderInfo> pageTextList = new ArrayList<>(); /** * 每页开始处理前调用,重置临时存储 */ public void startPage(int pageNum, Rectangle pageSize) { this.currentPageSize = pageSize; pageBoxList.clear(); pageHorizontalLineList.clear(); pageTextList.clear(); } @Override public void renderText(TextRenderInfo renderInfo) { // 保留原有文本获取逻辑,先暂存所有文本块,后续统一匹配 pageTextList.add(renderInfo); } @Override public void renderPath(PathRenderInfo renderInfo) { int operation = renderInfo.getOperation(); // 跳过非描边路径 if ((operation & PathRenderInfo.STROKE) == 0) { return; } Path renderPath = renderInfo.getPath(); List<Shape> pathSegments = renderPath.getSegments(); // 识别水平线 for (Shape segment : pathSegments) { if (segment instanceof Line) { List<Point2D> points = segment.getBasePoints(); Point2D p1 = points.get(0); Point2D p2 = points.get(1); // Y坐标差小于0.5判定为水平线,阈值可根据文档精度调整 if (Math.abs(p1.getY() - p2.getY()) < 0.5f) { float lineY = (float) p1.getY(); float xStart = (float) Math.min(p1.getX(), p2.getX()); float xEnd = (float) Math.max(p1.getX(), p2.getX()); pageHorizontalLineList.add(new float[]{xStart, xEnd, lineY}); } } } // 识别矩形方框 if (pathSegments.size() == 4 && pathSegments.stream().allMatch(seg -> seg instanceof Line)) { List<Point2D> allPoints = pathSegments.stream() .flatMap(seg -> seg.getBasePoints().stream()) .distinct() .toList(); // 矩形有4个不重复顶点 if (allPoints.size() == 4) { float minX = Float.MAX_VALUE, maxX = Float.MIN_VALUE; float minY = Float.MAX_VALUE, maxY = Float.MIN_VALUE; for (Point2D point : allPoints) { minX = (float) Math.min(minX, point.getX()); maxX = (float) Math.max(maxX, point.getX()); minY = (float) Math.min(minY, point.getY()); maxY = (float) Math.max(maxY, point.getY()); } pageBoxList.add(new Rectangle(minX, minY, maxX - minX, maxY - minY)); } } } /** * 整页内容处理完后调用,匹配方框和对应文本 * @return key为方框矩形,value为方框内的文本列表 */ public Map<Rectangle, List<String>> matchBoxText() { Map<Rectangle, List<String>> result = new HashMap<>(); for (TextRenderInfo textInfo : pageTextList) { Rectangle textRect = textInfo.getBaseline().getBoundingRectangle(); for (Rectangle box : pageBoxList) { // 留1单位容差,兼容坐标误差 if (textRect.getLeft() >= box.getLeft() - 1 && textRect.getRight() <= box.getRight() + 1 && textRect.getBottom() >= box.getBottom() - 1 && textRect.getTop() <= box.getTop() + 1) { result.computeIfAbsent(box, k -> new ArrayList<>()).add(textInfo.getText()); break; } } } return result; } // 其余接口方法无需求可留空 @Override public void beginTextBlock() {} @Override public void endTextBlock() {} @Override public void clipPath(int rule) {} }
注意事项
- 代码里的误差阈值(水平线判定的0.5f、文本匹配的1单位)可以根据你实际解析的PDF调整,不同生成工具导出的PDF浮点误差范围有区别
- 表格单元格边框本质也是矩形,该逻辑可以直接识别表格单元格,你可以根据矩形的大小、位置过滤不需要的方框
- 原有的
PdfContentStreamProcessor初始化、页面遍历逻辑不需要改动,只需要把原来的监听器替换成这个实现ExtRenderListener的新监听器即可。
内容的提问来源于stack exchange,提问作者Hervé Girod
相关产品推荐
相关产品推荐

