PDF使用C#读取高亮度文本(突出显示注释)

sda*_*lby 4 c# pdf itext itextsharp

我使用iTextSharp编写了一个提取工具,从PDF文档中提取注释信息.对于高亮注释,我只获得页面上突出显示的区域的矩形.

我的目标是提取已突出显示的文本.为此,我使用`PdfTextExtractor'.

Rectangle rect = new Rectangle(
    pdfArray.GetAsNumber(0).FloatValue, 
    pdfArray.GetAsNumber(1).FloatValue,
    pdfArray.GetAsNumber(2).FloatValue,
    pdfArray.GetAsNumber(3).FloatValue);

RenderFilter[] filter = { new RegionTextRenderFilter(rect) };
ITextExtractionStrategy strategy = new FilteredTextRenderListener(new LocationTextExtractionStrategy(), filter);
string textInsideRect = PdfTextExtractor.GetTextFromPage(pdfReader, pageNo, strategy);
return textInsideRect;
Run Code Online (Sandbox Code Playgroud)

返回的结果PdfTextExtractor并不完全正确.例如,它返回"即将消除纸张追逐",即使只突出显示"消除".

有趣的是,包含突出显示的"消除"的TJ的整个文本是"将要消除纸张追逐"(TJ是将文本写入页面的PDF指令).

我很想听到有关此问题的任何意见 - 也包括不涉及iTextSharp的解决方案.

mkl*_*mkl 8

原因

有趣的是,包含突出显示的"消除"的TJ的整个文本是"将要消除纸张追逐"(TJ是将文本写入页面的PDF指令).

这实际上是你的问题的原因.iText解析器类将文本转发到它们在内容流中作为连续字符串找到的片段中的渲染侦听器.您使用的过滤机制会过滤这些碎片.因此,过滤器接受整个句子.

因此,您需要的是一些预处理步骤,将这些片段分成各自的字符,然后将它们分别转发到过滤后的渲染侦听器.

这实际上很容易实现.转发文本片段的参数类型TextRenderInfo,提供了一种自我分割的方法:

/**
 * Provides detail useful if a listener needs access to the position of each individual glyph in the text render operation
 * @return A list of {@link TextRenderInfo} objects that represent each glyph used in the draw operation. The next effect is if there was a separate Tj opertion for each character in the rendered string
 * @since 5.3.3
 */
public List<TextRenderInfo> getCharacterRenderInfos() // iText / Java
virtual public List<TextRenderInfo> GetCharacterRenderInfos() // iTextSharp / .Net
Run Code Online (Sandbox Code Playgroud)

因此,您所要做的就是创建并使用RenderListener/ IRenderListener实现,它将获取的所有调用转发给另一个侦听器(在您的情况下是您的过滤侦听器),其扭曲renderText/ RenderText拆分其TextRenderInfo参数并逐个转发碎片.

一个Java示例

由于OP要求更多细节,这里有更多代码.但是,由于我主要使用Java,因此我将使用Java为iText提供它.但是对于iTextSharp来说很容易移植到C#.

如上所述,需要一个预处理步骤,将文本片段分成各自的字符,然后将它们单独转发到过滤后的渲染器监听器.

对于此步骤,您可以使用此类TextRenderInfoSplitter:

package stackoverflow.itext.extraction;

import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.TextExtractionStrategy;
import com.itextpdf.text.pdf.parser.TextRenderInfo;

public class TextRenderInfoSplitter implements TextExtractionStrategy
{
    public TextRenderInfoSplitter(TextExtractionStrategy strategy)
    {
        this.strategy = strategy;
    }

    public void renderText(TextRenderInfo renderInfo)
    {
        for (TextRenderInfo info : renderInfo.getCharacterRenderInfos())
        {
            strategy.renderText(info);
        }
    }

    public void beginTextBlock()
    {
        strategy.beginTextBlock();
    }

    public void endTextBlock()
    {
        strategy.endTextBlock();
    }

    public void renderImage(ImageRenderInfo renderInfo)
    {
        strategy.renderImage(renderInfo);
    }

    public String getResultantText()
    {
        return strategy.getResultantText();
    }

    final TextExtractionStrategy strategy;
}
Run Code Online (Sandbox Code Playgroud)

如果你有TextExtractionStrategy strategy(像你的new FilteredTextRenderListener(new LocationTextExtractionStrategy(), filter)),你现在可以用这样的单字符TextRenderInfo实例来提供它:

String textInsideRect = PdfTextExtractor.getTextFromPage(reader, pageNo, new TextRenderInfoSplitter(strategy));
Run Code Online (Sandbox Code Playgroud)

我用这个区域的答案中创建的PDF测试了它

Rectangle rect = new Rectangle(200, 600, 200, 135);
Run Code Online (Sandbox Code Playgroud)

作为参考,我标记了PDF中的区域:

带有标记区域的PDF屏幕截图

文本提取按区域过滤,但TextRenderInfoSplitter结果不是:

I am trying to create a PDF file with a lot
of text contents in the document. I am
using PDFBox
Run Code Online (Sandbox Code Playgroud)

按区域过滤文本提取,TextRenderInfoSplitter结果如下:

 to create a PDF f
ntents in the docu
n g P D F
Run Code Online (Sandbox Code Playgroud)

顺便说一句,你在这里看到了将文本早期拆分为单个字符的缺点:最终文本行使用非常大的字符间距进行排版.如果您保留PDF中的文本片段,文本提取策略仍然可以很容易地看到该行包含两个单词usingPDFBox.只要您逐字地将文本片段提供给文本提取策略,他们就可能会将这些广泛设置的单词解释为多个单字母单词.

一种提升

突出显示的单词"消除"例如被提取为"消除t".通过双击单词并在Adobe Acrobat Reader中突出显示,突出显示了这一点.

在上面的示例中发生了类似的事情,几乎没有触及感兴趣区域的字母使其成为结果.

这是由于RegionTextRenderFilter实施allowText允许所有文字,继续其基线相交问题的矩形,即使交集仅仅由一个单一的点:

public boolean allowText(TextRenderInfo renderInfo){
    LineSegment segment = renderInfo.getBaseline();
    Vector startPoint = segment.getStartPoint();
    Vector endPoint = segment.getEndPoint();

    float x1 = startPoint.get(Vector.I1);
    float y1 = startPoint.get(Vector.I2);
    float x2 = endPoint.get(Vector.I1);
    float y2 = endPoint.get(Vector.I2);

    return filterRect.intersectsLine(x1, y1, x2, y2);
}
Run Code Online (Sandbox Code Playgroud)

鉴于您首先将文本拆分为字符,您可能需要检查它们各自的基线是否完全包含在相关区域中,即RenderFilter通过复制RegionTextRenderFilter然后替换线来实现自己 的基线

return filterRect.intersectsLine(x1, y1, x2, y2);
Run Code Online (Sandbox Code Playgroud)

通过

return filterRect.contains(x1, y1) && filterRect.contains(x2, y2);
Run Code Online (Sandbox Code Playgroud)

但是,根据Adobe Acrobat Reader中突出显示文本的准确程度,您可能希望以完全自定义的方式更改此项.