从 PDF 中过滤掉超过特定字体大小的所有文本

Con*_*r M 2 java pdfbox

正如标题所示,我想过滤掉 PDF 中超过特定​​字体大小的所有文本。目前,我正在使用 PDFBox 库,但我愿意使用任何其他免费的 Java 库。

我的方法是使用 PDFStreamParser 来迭代令牌。当我传递大小大于阈值的 Tf 运算符时,不要添加看到的下一个 Tj/TJ。然而,我已经清楚这种相对简单的方法行不通,因为文本可能会被当前的变换矩阵缩放。

我是否可以采取更好的方法,或者有一种方法可以使我的方法发挥作用而不变得太复杂?

mkl*_*mkl 6

你的方法

当我传递大小大于阈值的 Tf 运算符时,不要添加看到的下一个 Tj/TJ。

太简单了。

一方面,正如您自己所说,

文本可以按当前变换矩阵缩放。

(实际上不仅通过变换矩阵,还通过文本矩阵!)

因此,您必须跟踪这些矩阵。

另一方面,Tf不仅为所看到的下一个文本绘制指令设置基本字体大小,它还设置它直到大小被其他指令显式更改为止。

此外,文本字体大小和当前变换矩阵是图形状态的一部分;因此,它们受保存状态和恢复状态指令的约束。

因此,要根据当前状态编辑内容流,您必须跟踪大量信息。幸运的是,PDFBox 包含一些类来完成这里的繁重工作,类层次结构基于PDFStreamEngine,让您可以专注于您的任务。为了获得尽可能多的信息可供编辑,该类PDFGraphicsStreamEngine似乎是一个不错的选择。

通用内容流编辑器类

因此,让我们派生PdfContentStreamEditorPDFGraphicsStreamEngine添加一些代码来生成替换内容流。

public class PdfContentStreamEditor extends PDFGraphicsStreamEngine {
    public PdfContentStreamEditor(PDDocument document, PDPage page) {
        super(page);
        this.document = document;
    }

    /**
     * <p>
     * This method retrieves the next operation before its registered
     * listener is called. The default does nothing.
     * </p>
     * <p>
     * Override this method to retrieve state information from before the
     * operation execution.
     * </p> 
     */
    protected void nextOperation(Operator operator, List<COSBase> operands) {
        
    }

    /**
     * <p>
     * This method writes content stream operations to the target canvas. The default
     * implementation writes them as they come, so it essentially generates identical
     * copies of the original instructions {@link #processOperator(Operator, List)}
     * forwards to it.
     * </p>
     * <p>
     * Override this method to achieve some fancy editing effect.
     * </p> 
     */
    protected void write(ContentStreamWriter contentStreamWriter, Operator operator, List<COSBase> operands) throws IOException {
        contentStreamWriter.writeTokens(operands);
        contentStreamWriter.writeToken(operator);
    }

    // stub implementation of PDFGraphicsStreamEngine abstract methods
    @Override
    public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { }

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

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

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

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

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

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

    @Override
    public void closePath() throws IOException { }

    @Override
    public void endPath() throws IOException { }

    @Override
    public void strokePath() throws IOException { }

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

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

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

    // PDFStreamEngine overrides to allow editing
    @Override
    public void processPage(PDPage page) throws IOException {
        PDStream stream = new PDStream(document);
        replacement = new ContentStreamWriter(replacementStream = stream.createOutputStream(COSName.FLATE_DECODE));
        super.processPage(page);
        replacementStream.close();
        page.setContents(stream);
        replacement = null;
        replacementStream = null;
    }

    @Override
    public void showForm(PDFormXObject form) throws IOException {
        // DON'T descend into XObjects
        // super.showForm(form);
    }

    @Override
    protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
        nextOperation(operator, operands);
        super.processOperator(operator, operands);
        write(replacement, operator, operands);
    }

    final PDDocument document;
    OutputStream replacementStream = null;
    ContentStreamWriter replacement = null;
}
Run Code Online (Sandbox Code Playgroud)

PdfContentStreamEditor类)

此代码将覆盖processPage以创建新的页面内容流并最终用它替换旧的页面内容流。它会覆盖processOperator以提供经过处理的编辑指令。

对于编辑,只需覆盖write此处即可。现有的实现只是在指令出现时写入指令,而您可以更改要写入的指令。覆盖允许您在应用当前指令之前nextOperation查看图形状态。

按原样应用编辑器,

PDDocument document = PDDocument.load(SOURCE);
for (PDPage page : document.getDocumentCatalog().getPages()) {
    PdfContentStreamEditor identity = new PdfContentStreamEditor(document, page);
    identity.processPage(page);
}
document.save(RESULT);
Run Code Online (Sandbox Code Playgroud)

编辑页面内容测试testIdentityInput

因此,将创建具有等效内容流的结果 PDF。

根据您的用例自定义内容流编辑器

你想要

过滤掉 PDF 中超过特定​​字体大小的所有文本。

因此,我们必须检查write当前指令是否是文本绘制指令,如果是,我们必须检查当前的有效字体大小,即文本矩阵和当前变换矩阵变换后的基本字体大小。如果有效字体太大,我们就必须删除该指令。

这可以按如下方式完成:

PDDocument document = PDDocument.load(SOURCE);
for (PDPage page : document.getDocumentCatalog().getPages()) {
    PdfContentStreamEditor identity = new PdfContentStreamEditor(document, page) {
        @Override
        protected void write(ContentStreamWriter contentStreamWriter, Operator operator, List<COSBase> operands) throws IOException {
            String operatorString = operator.getName();

            if (TEXT_SHOWING_OPERATORS.contains(operatorString))
            {
                float fs = getGraphicsState().getTextState().getFontSize();
                Matrix matrix = getTextMatrix().multiply(getGraphicsState().getCurrentTransformationMatrix());
                Point2D.Float transformedFsVector = matrix.transformPoint(0, fs);
                Point2D.Float transformedOrigin = matrix.transformPoint(0, 0);
                double transformedFs = transformedFsVector.distance(transformedOrigin);
                if (transformedFs > 100)
                    return;
            }

            super.write(contentStreamWriter, operator, operands);
        }

        final List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
    };
    identity.processPage(page);
}
document.save(RESULT);
Run Code Online (Sandbox Code Playgroud)

编辑页面内容测试testRemoveBigTextDocument

严格来说,完全放弃相关指令可能还不够;相反,我们必须将其替换为更改文本矩阵的指令,就像删除的文本绘制指令所做的那样。否则,以下未删除的文本可能会被移动。不过,通常情况下,这确实可以按原样工作,因为文本矩阵是为以下不同文本新设置的。所以让我们在这里保持简单。

限制条件及备注

PdfContentStreamEditor仅编辑页面内容流。从那里可以使用当前未被编辑器编辑的 XObject 和模式。不过,在编辑页面内容流之后,递归地迭代 XObject 和模式并以类似的方式编辑它们应该很容易。

这本质上是此答案中的 for iText 5 (.Net/Java)和此答案中的 iText 7PdfContentStreamEditor的端口。使用这些编辑器类的示例可能会给出一些有关如何将其用于PDFBox 的提示。PdfContentStreamEditorPdfCanvasEditorPdfContentStreamEditor

之前在此答案的HelloSignManipulator类中使用了类似(但不太通用)的方法。

修复错误

在这个问题的上下文中,发现了一个错误PdfContentStreamEditor,导致示例 PDF 中焦点所在的一些文本行被移动。

背景:有些PDF指令是通过其他指令定义的,例如t x t y TD被指定为与-t y TL t x t y Td具有相同的效果。为了简单起见,相应的 PDFBoxOperatorProcessor实现通过将等效指令反馈回流引擎来工作。

在这种情况PdfContentStreamEditor下,上面实现的方法检索替换指令和原始指令的信号并将它们全部写回到结果流中。因此,这些指令的效果加倍。例如,在TD指令的情况下,文本插入点被转发两行而不是一行......

因此,我们必须忽略替换指令。为此,将上面的方法替换processOperator

@Override
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
    if (inOperator) {
        super.processOperator(operator, operands);
    } else {
        inOperator = true;
        nextOperation(operator, operands);
        super.processOperator(operator, operands);
        write(replacement, operator, operands);
        inOperator = false;
    }
}

boolean inOperator = false;
Run Code Online (Sandbox Code Playgroud)