PDFBox-线/矩形提取

时间:2019-03-14 15:58:06

标签: java pdf pdfbox

我正在尝试从PDF中提取文本坐标和线(或矩形)坐标。

TextPosition类具有getXDirAdj()getYDirAdj()方法,它们根据相应的TextPosition对象表示的文本片段的方向来转换坐标(根据@mkl的注释更正) 不论页面旋转如何,最终输出都是一致的。

输出所需的坐标为X0,Y0(页面的左上角)

这是@Tilman Hausherr解决方案的略微修改。将y坐标反转(高度-y)以使其与文本提取过程中的坐标保持一致,并且将输出写入到csv中。

    public class LineCatcher extends PDFGraphicsStreamEngine
{
    private static final GeneralPath linePath = new GeneralPath();
    private static ArrayList<Rectangle2D> rectList= new ArrayList<Rectangle2D>();
    private int clipWindingRule = -1;
    private static String headerRecord = "Text|Page|x|y|width|height|space|font";

    public LineCatcher(PDPage page)
    {
        super(page);
    }

    public static void main(String[] args) throws IOException
    {
        if( args.length != 4 )
        {
            usage();
        }
        else
        {
            PDDocument document = null;
            FileOutputStream fop = null;
            File file;
            Writer osw = null;
            int numPages;
            double page_height;
            try
            {
                document = PDDocument.load( new File(args[0], args[1]) );
                numPages = document.getNumberOfPages();
                file = new File(args[2], args[3]);
                fop = new FileOutputStream(file);

                // if file doesnt exists, then create it
                if (!file.exists()) {
                    file.createNewFile();
                }

                osw = new OutputStreamWriter(fop, "UTF8");
                osw.write(headerRecord + System.lineSeparator());
                System.out.println("Line Processing numPages:" + numPages);
                for (int n = 0; n < numPages; n++) {
                    System.out.println("Line Processing page:" + n);
                    rectList = new ArrayList<Rectangle2D>();
                    PDPage page = document.getPage(n);
                    page_height = page.getCropBox().getUpperRightY();
                    LineCatcher lineCatcher = new LineCatcher(page);
                    lineCatcher.processPage(page);

                    try{
                        for(Rectangle2D rect:rectList) {

                            String pageNum = Integer.toString(n + 1);
                            String x = Double.toString(rect.getX());
                            String y = Double.toString(page_height - rect.getY()) ;
                            String w = Double.toString(rect.getWidth());
                            String h = Double.toString(rect.getHeight());
                            writeToFile(pageNum, x, y, w, h, osw);

                        }
                        rectList = null;
                        page = null;
                        lineCatcher = null;
                    }
                    catch(IOException io){
                        throw new IOException("Failed to Parse document for line processing. Incorrect document format. Page:" + n);
                    }
                };

            }
            catch(IOException io){
                throw new IOException("Failed to Parse document for line processing. Incorrect document format.");
            }
            finally
            {
                if ( osw != null ){
                    osw.close();
                }
                if( document != null )
                {
                    document.close();
                }
            }
        }
    }

    private static void writeToFile(String pageNum, String x, String y, String w, String h, Writer osw) throws IOException {
        String c = "^" + "|" +
                pageNum + "|" +
                x + "|" +
                y + "|" +
                w + "|" +
                h + "|" +
                "999" + "|" +
                "marker-only";
        osw.write(c + System.lineSeparator());
    }

    @Override
    public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException
    {
        // to ensure that the path is created in the right direction, we have to create
        // it by combining single lines instead of creating a simple rectangle
        linePath.moveTo((float) p0.getX(), (float) p0.getY());
        linePath.lineTo((float) p1.getX(), (float) p1.getY());
        linePath.lineTo((float) p2.getX(), (float) p2.getY());
        linePath.lineTo((float) p3.getX(), (float) p3.getY());

        // close the subpath instead of adding the last line so that a possible set line
        // cap style isn't taken into account at the "beginning" of the rectangle
        linePath.closePath();
    }

    @Override
    public void drawImage(PDImage pdi) throws IOException
    {
    }

    @Override
    public void clip(int windingRule) throws IOException
    {
        // the clipping path will not be updated until the succeeding painting operator is called
        clipWindingRule = windingRule;

    }

    @Override
    public void moveTo(float x, float y) throws IOException
    {
        linePath.moveTo(x, y);
    }

    @Override
    public void lineTo(float x, float y) throws IOException
    {
        linePath.lineTo(x, y);
    }

    @Override
    public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException
    {
        linePath.curveTo(x1, y1, x2, y2, x3, y3);
    }

    @Override
    public Point2D getCurrentPoint() throws IOException
    {
        return linePath.getCurrentPoint();
    }

    @Override
    public void closePath() throws IOException
    {
        linePath.closePath();
    }

    @Override
    public void endPath() throws IOException
    {
        if (clipWindingRule != -1)
        {
            linePath.setWindingRule(clipWindingRule);
            getGraphicsState().intersectClippingPath(linePath);
            clipWindingRule = -1;
        }
        linePath.reset();

    }

    @Override
    public void strokePath() throws IOException
    {
        rectList.add(linePath.getBounds2D());
        linePath.reset();
    }

    @Override
    public void fillPath(int windingRule) throws IOException
    {
        linePath.reset();
    }

    @Override
    public void fillAndStrokePath(int windingRule) throws IOException
    {
        linePath.reset();
    }

    @Override
    public void shadingFill(COSName cosn) throws IOException
    {
    }

    /**
     * This will print the usage for this document.
     */
    private static void usage()
    {
        System.err.println( "Usage: java " + LineCatcher.class.getName() + " <input-pdf>"  + " <output-file>");
    }
}

使用PDFGraphicsStreamEngine类提取Line和Rectangle坐标。线和矩形的坐标与文本的坐标不对齐

PLOT of the text and line extract

绿色:文字 红色:原样获得的线坐标 黑色:预期坐标(在输出上应用转换后获得)

在运行行提取之前,尝试过setRotation()方法来校正旋转。但是结果不一致。

使用PDFBox获得旋转并获得一致的Line / Rectangle坐标的可能选项是什么?

1 个答案:

答案 0 :(得分:2)

据我了解的要求,OP在坐标系中工作,其原点位于可见页面的左上角(考虑页面旋转), x 坐标增加在右侧, y 坐标向下增加,并且这些单位是PDF默认用户空间单位(通常为 1 / 72 英寸)。

在这个坐标系中,他需要提取(水平或垂直)线形式为

  • 左/上端点的坐标和
  • 宽度/高度。

转换LineCatcher个结果

另一方面,他从蒂尔曼(Tilman)获得的帮助程序类LineCatcher并没有考虑到页面旋转。此外,它返回垂直线的底部终点,而不是顶部终点。因此,必须对LineCatcher结果进行坐标变换。

为此,只需替换

for(Rectangle2D rect:rectList) {
    String pageNum = Integer.toString(n + 1);
    String x = Double.toString(rect.getX());
    String y = Double.toString(page_height - rect.getY()) ;
    String w = Double.toString(rect.getWidth());
    String h = Double.toString(rect.getHeight());
    writeToFile(pageNum, x, y, w, h, osw);
}

作者

int pageRotation = page.getRotation();
PDRectangle pageCropBox = page.getCropBox();

for(Rectangle2D rect:rectList) {
    String pageNum = Integer.toString(n + 1);
    String x, y, w, h;
    switch(pageRotation) {
    case 0:
        x = Double.toString(rect.getX() - pageCropBox.getLowerLeftX());
        y = Double.toString(pageCropBox.getUpperRightY() - rect.getY() + rect.getHeight());
        w = Double.toString(rect.getWidth());
        h = Double.toString(rect.getHeight());
        break;
    case 90:
        x = Double.toString(rect.getY() - pageCropBox.getLowerLeftY());
        y = Double.toString(rect.getX() - pageCropBox.getLowerLeftX());
        w = Double.toString(rect.getHeight());
        h = Double.toString(rect.getWidth());
        break;
    case 180:
        x = Double.toString(pageCropBox.getUpperRightX() - rect.getX() - rect.getWidth());
        y = Double.toString(rect.getY() - pageCropBox.getLowerLeftY());
        w = Double.toString(rect.getWidth());
        h = Double.toString(rect.getHeight());
        break;
    case 270:
        x = Double.toString(pageCropBox.getUpperRightY() - rect.getY() + rect.getHeight());
        y = Double.toString(pageCropBox.getUpperRightX() - rect.getX() - rect.getWidth());
        w = Double.toString(rect.getHeight());
        h = Double.toString(rect.getWidth());
        break;
    default:
        throw new IOException(String.format("Unsupported page rotation %d on page %d.", pageRotation, page));
    }
    writeToFile(pageNum, x, y, w, h, osw);
}

ExtractLinesWithDir测试testExtractLineRotationTestWithDir

TextPosition.get?DirAdj()坐标的关系

OP通过引用TextPosition类方法getXDirAdj()getYDirAdj()描述坐标。实际上,这些方法返回的坐标系是坐标,其原点位于页面左上角,并且 y 坐标使页面旋转后向下向下递增,从而将文本向上绘制

在示例文档的情况下,所有文本都被绘制为使得在应用页面旋转之后它是直立的。由此得出了我对顶部要求的理解。

但是,在全局范围内使用TextPosition.get?DirAdj()值作为坐标的问题是,在具有以不同方向绘制文本的页面的文档中,收集的文本坐标突然相对于不同的坐标系。因此,一般解决方案不应像这样疯狂地收集坐标。相反,它应该首先确定页面方向(例如,由页面旋转指定的方向或大多数文本共享的方向),并使用该方向指定的固定坐标系中的坐标以及文本的书写方向的指示有问题的