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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 13:03:19