当前位置: 首页 > 知识库问答 >
问题:

如何使用PDFBox确定实际PDF内容的位置?

梁德馨
2023-03-14

我们正在使用PDFBox从Java桌面应用程序打印一些PDF,并且PDF包含太多空格(不幸的是,修复PDF生成器不是一个选项)。

我的问题是确定页面上的实际内容在哪里,因为裁剪/媒体/修剪/艺术/出血框是无用的。有没有比将页面呈现为图像并检查哪些像素保持白色更好/更快的简单有效的方法?

共有1个答案

邓建柏
2023-03-14

正如你在评论中提到的

可以假设没有需要特殊处理的背景或其他元素,

我将展示一个基本的解决方案,没有任何这样的特殊处理。

要在不实际渲染位图和检查位图像素的情况下找到边界框,必须扫描页面内容流的所有指令以及从中引用的任何XObject。确定每个指令绘制的内容的边界框,并最终将它们组合到一个框中。

这里介绍的简单框查找器通过简单地返回其并集的边界框来组合它们。

为了扫描内容流的指令,PDFBox提供了许多基于PDFStreamEngine的类。简单的盒子查找器源自PDFGraphicsStreamEngine,它通过与矢量图形相关的一些方法扩展PDFStreamEngine

public class BoundingBoxFinder extends PDFGraphicsStreamEngine {
    public BoundingBoxFinder(PDPage page) {
        super(page);
    }

    public Rectangle2D getBoundingBox() {
        return rectangle;
    }

    //
    // Text
    //
    @Override
    protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode, Vector displacement)
            throws IOException {
        super.showGlyph(textRenderingMatrix, font, code, unicode, displacement);
        Shape shape = calculateGlyphBounds(textRenderingMatrix, font, code);
        if (shape != null) {
            Rectangle2D rect = shape.getBounds2D();
            add(rect);
        }
    }

    /**
     * Copy of <code>org.apache.pdfbox.examples.util.DrawPrintTextLocations.calculateGlyphBounds(Matrix, PDFont, int)</code>.
     */
    private Shape calculateGlyphBounds(Matrix textRenderingMatrix, PDFont font, int code) throws IOException
    {
        GeneralPath path = null;
        AffineTransform at = textRenderingMatrix.createAffineTransform();
        at.concatenate(font.getFontMatrix().createAffineTransform());
        if (font instanceof PDType3Font)
        {
            // It is difficult to calculate the real individual glyph bounds for type 3 fonts
            // because these are not vector fonts, the content stream could contain almost anything
            // that is found in page content streams.
            PDType3Font t3Font = (PDType3Font) font;
            PDType3CharProc charProc = t3Font.getCharProc(code);
            if (charProc != null)
            {
                BoundingBox fontBBox = t3Font.getBoundingBox();
                PDRectangle glyphBBox = charProc.getGlyphBBox();
                if (glyphBBox != null)
                {
                    // PDFBOX-3850: glyph bbox could be larger than the font bbox
                    glyphBBox.setLowerLeftX(Math.max(fontBBox.getLowerLeftX(), glyphBBox.getLowerLeftX()));
                    glyphBBox.setLowerLeftY(Math.max(fontBBox.getLowerLeftY(), glyphBBox.getLowerLeftY()));
                    glyphBBox.setUpperRightX(Math.min(fontBBox.getUpperRightX(), glyphBBox.getUpperRightX()));
                    glyphBBox.setUpperRightY(Math.min(fontBBox.getUpperRightY(), glyphBBox.getUpperRightY()));
                    path = glyphBBox.toGeneralPath();
                }
            }
        }
        else if (font instanceof PDVectorFont)
        {
            PDVectorFont vectorFont = (PDVectorFont) font;
            path = vectorFont.getPath(code);

            if (font instanceof PDTrueTypeFont)
            {
                PDTrueTypeFont ttFont = (PDTrueTypeFont) font;
                int unitsPerEm = ttFont.getTrueTypeFont().getHeader().getUnitsPerEm();
                at.scale(1000d / unitsPerEm, 1000d / unitsPerEm);
            }
            if (font instanceof PDType0Font)
            {
                PDType0Font t0font = (PDType0Font) font;
                if (t0font.getDescendantFont() instanceof PDCIDFontType2)
                {
                    int unitsPerEm = ((PDCIDFontType2) t0font.getDescendantFont()).getTrueTypeFont().getHeader().getUnitsPerEm();
                    at.scale(1000d / unitsPerEm, 1000d / unitsPerEm);
                }
            }
        }
        else if (font instanceof PDSimpleFont)
        {
            PDSimpleFont simpleFont = (PDSimpleFont) font;

            // these two lines do not always work, e.g. for the TT fonts in file 032431.pdf
            // which is why PDVectorFont is tried first.
            String name = simpleFont.getEncoding().getName(code);
            path = simpleFont.getPath(name);
        }
        else
        {
            // shouldn't happen, please open issue in JIRA
            System.out.println("Unknown font class: " + font.getClass());
        }
        if (path == null)
        {
            return null;
        }
        return at.createTransformedShape(path.getBounds2D());
    }

    //
    // Bitmaps
    //
    @Override
    public void drawImage(PDImage pdImage) throws IOException {
        Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
        for (int x = 0; x < 2; x++) {
            for (int y = 0; y < 2; y++) {
                add(ctm.transformPoint(x, y));
            }
        }
    }

    //
    // Paths
    //
    @Override
    public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException {
        addToPath(p0, p1, p2, p3);
    }

    @Override
    public void clip(int windingRule) throws IOException {
    }

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

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

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

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

    @Override
    public void closePath() throws IOException {
    }

    @Override
    public void endPath() throws IOException {
        rectanglePath = null;
    }

    @Override
    public void strokePath() throws IOException {
        addPath();
    }

    @Override
    public void fillPath(int windingRule) throws IOException {
        addPath();
    }

    @Override
    public void fillAndStrokePath(int windingRule) throws IOException {
        addPath();
    }

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

    void addToPath(Point2D... points) {
        Arrays.asList(points).forEach(p -> addToPath(p.getX(), p.getY()));
    }

    void addToPath(double newx, double newy) {
        if (rectanglePath == null) {
            rectanglePath = new Rectangle2D.Double(newx, newy, 0, 0);
        } else {
            rectanglePath.add(newx, newy);
        }
    }

    void addPath() {
        if (rectanglePath != null) {
            add(rectanglePath);
            rectanglePath = null;
        }
    }

    void add(Rectangle2D rect) {
        if (rectangle == null) {
            rectangle = new Rectangle2D.Double();
            rectangle.setRect(rect);
        } else {
            rectangle.add(rect);
        }
    }

    void add(Point2D... points) {
        for (Point2D point : points) {
            add(point.getX(), point.getY());
        }
    }

    void add(double newx, double newy) {
        if (rectangle == null) {
            rectangle = new Rectangle2D.Double(newx, newy, 0, 0);
        } else {
            rectangle.add(newx, newy);
        }
    }

    Rectangle2D rectanglePath = null;
    Rectangle2D rectangle = null;
}

(github上的BoundingBoxFinder)

如您所见,我从PDFBox示例类中借用了calculateGlyphBounds辅助方法。

您可以使用像这样的BoundingBoxFinderPDDocument pdDocument的给定PDPage pdPage沿着边界框边缘绘制边界线:

void drawBoundingBox(PDDocument pdDocument, PDPage pdPage) throws IOException {
    BoundingBoxFinder boxFinder = new BoundingBoxFinder(pdPage);
    boxFinder.processPage(pdPage);
    Rectangle2D box = boxFinder.getBoundingBox();
    if (box != null) {
        try (   PDPageContentStream canvas = new PDPageContentStream(pdDocument, pdPage, AppendMode.APPEND, true, true)) {
            canvas.setStrokingColor(Color.magenta);
            canvas.addRect((float)box.getMinX(), (float)box.getMinY(), (float)box.getWidth(), (float)box.getHeight());
            canvas.stroke();
        }
    }
}

(DetermineBoundingBox辅助方法)

结果如下所示:

注意,BoundingBoxFinder真的不是很复杂;特别是它不会忽略不可见的内容,如白色背景矩形、以渲染模式绘制的文本“不可见”、由白色填充路径覆盖的任意内容、位图图像的白色部分,...此外,它确实忽略了剪辑路径、奇怪的混合模式、注释、...

扩展类以正确处理这些情况非常简单,但要添加的代码总和将超出堆栈溢出答案的范围。

对于这个答案中的代码,我使用了当前的PDFBox 3.0.0-SNAPSHOT开发分支,但对于当前的2,它也应该是现成的。x版本。

 类似资料:
  • 我已经实现了从pdf中删除图层的功能,但问题是,我在图层上绘制的内容无法删除。下面是我用来删除图层的代码:

  • 我正在尝试使用iText7阅读PDF中使用的所有字体。为此,我创建了一个,它将中的字体读取为: 这似乎很好,让我可以阅读所有1型字体。但是调用

  • 问题内容: log4j具有属性,可以帮助用户指示实际使用哪个配置文件来配置日志记录系统。 我还没有找到与(否则更高级的)Logback日志记录框架等效的东西。有什么方法可以在运行时打印(用于诊断目的)Logback用来引导自身的配置文件? [编辑]澄清一下,理想情况下,我想要一个不需要我修改配置文件本身的解决方案(例如,由于组装错误的第三方JAR可能会被错误地提取,并且在我的注销配置之前) XML

  • 问题内容: 我想使用PDFBox打印 由iText创建的 PDF文件 。我已经使用PDDocument类及其方法print()成功尝试了此操作。您可以在此处找到文档: http //pdfbox.apache.org/apidocs/。 (我正在使用此代码:) 方法print()很好用,但是 有一个问题:当我需要打印多个文件时,该方法要求我为每个文档选择打印机。 有什么办法只能设置一次打印机吗?

  • 问题内容: 我正在使用Apache PDFBox处理Java应用程序中的PDF文件。我想在每个页面上分割一个PDF文档。 是否有可能做到这一点Apache PDFBox?如果是这样,怎么办? 问题答案: 可以使用来实现。 这是一个示例代码,它将在每个页面上拆分文档: 您可以使用来控制每个拆分的PDF的页数。

  • 我正在迁移一些代码(最初使用iText)来使用PdfBox进行PDF合并。除了创建PDF包或文件夹,一切都很好。我不得不承认,直到现在我才意识到它的存在。 这是我的代码片段(使用iText): 我需要这个,但与PdfBox。 我正在研究两者的 API 和文档,但找不到解决方案。任何帮助都会很棒。 附言。如果我给人留下印象,我需要在iText中解决方案,我需要它在PdfBox中,因为迁移是从iTex