【问题标题】:Filter out all text above a certain font size from PDF从 PDF 中过滤掉超过特定字体大小的所有文本
【发布时间】:2019-10-20 16:40:37
【问题描述】:

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

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

有没有更好的方法我可以采用,或者有一种方法可以让我的方法工作而不至于变得太复杂?

【问题讨论】:

    标签: java pdfbox


    【解决方案1】:

    你的方法

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

    太简单了。

    一方面,正如你自己所说的,

    文本可以通过当前的变换矩阵进行缩放。

    (其实不仅可以通过变换矩阵,还可以通过文本矩阵!)

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

    另一方面,Tf 不仅为下一个看到的文本绘图指令设置基本字体大小,它还会设置它直到大小被某些人明确更改其他说明。

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

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

    一个通用的内容流编辑器类

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

    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;
    }
    

    (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);
    

    (EditPageContent 测试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);
    

    (EditPageContent 测试testRemoveBigTextDocument)

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

    约束和备注

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

    这个PdfContentStreamEditor 本质上是this answer 的iText 5 (.Net/Java) 的PdfContentStreamEditorthis answer 的iText 7 的PdfCanvasEditor 的一个端口。使用这些编辑器类的示例可能会提供一些关于如何将 PdfContentStreamEditor 用于 PDFBox 的提示。

    以前在this answerHelloSignManipulator 类中使用了类似(但不太通用)的方法。

    修复错误

    this question 的上下文中,发现PdfContentStreamEditor 中的一个错误导致示例PDF 中的一些文本行被移动。

    背景:一些 PDF 指令是通过其他指令定义的,例如txtyTD被指定为和-tyTL txt yTd。为简单起见,相应的 PDFBox OperatorProcessor 实现通过将等效指令反馈回流引擎来工作。

    在这种情况下,上面实现的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;
    

    【讨论】:

    • 继续这个讨论,我们能不能用上面的逻辑改变Pdf中特定文本的颜色。我的意思是,我想更改其中包含特定子字符串的文本的颜色。或者我们可以说,我想将蓝色文本(网站链接、超链接或字符串包含 http、https、www)更改为黑色。
    • 我注意到,使用此方法通常会移动已删除文本之后的文本,正如您警告的那样。我可以提供什么样的指令来改变文本矩阵,就像通常的文本一样?
    • @ConnorM 这取决于您删除的文本指令。对于(text) Tj,请使用[-width] TJ,其中widthtext 的宽度,以文本空间的千分之一单位表示。对于[...] TJ,将每个字符串数组元素替换为其负宽度;您可以将数组中的所有数字相加。对于(text) 'a b (text) ",请记住它们分别定义为T* (text) Tja Tw b Tc T* (text) Tj,并替换上面的 Tj 操作。
    • @AsadRao 在回答您之前的问题时,我今天解释了您的任务的一些障碍并概述了基于 iText 的可能解决方案,请参阅here。该解决方案大纲本质上也适用于 PDFBox,您只需从 PdfTextStripper 而不是 LocationTextExtractionStrategy 借用代码。
    • 是否可以扩充上面的代码以控制调用 super.write() 时生成的最不需要的 WARNING 日志消息?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    • 2022-01-26
    • 2021-07-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多