【问题标题】:How to get the text position from the pdf page in iText 7如何从iText 7中的pdf页面获取文本位置
【发布时间】:2017-09-30 12:11:56
【问题描述】:

我正在尝试在 PDF 页面中查找文本位置?

我尝试的是通过 PDF 文本提取器使用简单的文本提取策略获取 PDF 页面中的文本。我正在循环每个单词以检查我的单词是否存在。使用以下方法拆分单词:

var Words = pdftextextractor.Split(new char[] { ' ', '\n' });

我无法找到文本位置。问题是我无法找到文本的位置。我只需要找到 PDF 文件中单词的 y 坐标。

【问题讨论】:

  • 您使用var 和大写的方法名称。你想暗示你想要一个 C# 的解决方案吗?或者这只是伪代码和 Java 解决方案也可以吗? (我问是因为我更了解 Java。)

标签: itext7


【解决方案1】:

我能够使用我以前的 Itext5 版本来操作它。我不知道您是否正在寻找 C#,但这就是下面的代码所写的内容。

using iText.Kernel.Geom;
using iText.Kernel.Pdf.Canvas.Parser;
using iText.Kernel.Pdf.Canvas.Parser.Data;
using iText.Kernel.Pdf.Canvas.Parser.Listener;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

class TextLocationStrategy : LocationTextExtractionStrategy
{
    private List<textChunk> objectResult = new List<textChunk>();

    public override void EventOccurred(IEventData data, EventType type)
    {
        if (!type.Equals(EventType.RENDER_TEXT))
            return;

        TextRenderInfo renderInfo = (TextRenderInfo)data;

        string curFont = renderInfo.GetFont().GetFontProgram().ToString();

        float curFontSize = renderInfo.GetFontSize();

        IList<TextRenderInfo> text = renderInfo.GetCharacterRenderInfos();
        foreach (TextRenderInfo t in text)
        {
            string letter = t.GetText();
            Vector letterStart = t.GetBaseline().GetStartPoint();
            Vector letterEnd = t.GetAscentLine().GetEndPoint();
            Rectangle letterRect = new Rectangle(letterStart.Get(0), letterStart.Get(1), letterEnd.Get(0) - letterStart.Get(0), letterEnd.Get(1) - letterStart.Get(1));

            if (letter != " " && !letter.Contains(' '))
            {
                textChunk chunk = new textChunk();
                chunk.text = letter;
                chunk.rect = letterRect;
                chunk.fontFamily = curFont;
                chunk.fontSize = curFontSize;
                chunk.spaceWidth = t.GetSingleSpaceWidth() / 2f;

                objectResult.Add(chunk);
            }
        }
    }
}
public class textChunk
{
    public string text { get; set; }
    public Rectangle rect { get; set; }
    public string fontFamily { get; set; }
    public int fontSize { get; set; }
    public float spaceWidth { get; set; }
}

我也深入了解每个单独的角色,因为它更适合我的流程。您可以操作名称,当然还有对象,但我创建了 textchunk 来保存我想要的内容,而不是拥有一堆 renderInfo 对象。

您可以通过添加几行来从您的 pdf 中获取数据来实现这一点。

PdfDocument reader = new PdfDocument(new PdfReader(filepath));
FilteredEventListener listener = new FilteredEventListener();
var strat = listener.AttachEventListener(new TextExtractionStrat());
PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
processor.ProcessPageContent(reader.GetPage(1));

一旦你走到这一步,你就可以通过将 objectResult 公开或在你的类中创建一个方法来获取 objectResult 并对其进行处理,从而从 strat 中拉取 objectResult。

【讨论】:

  • 是的,我在看 c#。这是给 iT​​ext7 的吗?
  • 是的,这是 Itext7。
  • 真棒。这真的是一个很好的例子。还有一个问题如何查找字符是否在同一行例如:就像行中的最后一个字符
【解决方案2】:

@Joris' answer 解释了如何为任务实现全新的提取策略/事件监听器。或者,您可以尝试调整现有的文本提取策略来满足您的需求。

此答案演示了如何调整现有的 LocationTextExtractionStrategy 以返回文本及其字符各自的 y 坐标。

请注意,这只是一个概念验证,它特别假设文本是水平书写的,即使用有效的变换矩阵(ctm 和文本矩阵组合),其中 b 和 c 等于 0。 此外TextPlusY的字符和坐标检索方法根本没有优化,可能需要很长时间才能执行。

由于 OP 没有表达语言偏好,这里是 iText7 for Java 的解决方案:

TextPlusY

对于手头的任务,需要能够并排检索字符和 y 坐标。为了使这更容易,我使用一个类来表示两个文本及其字符各自的 y 坐标。它源自CharSequence,是String的泛化,可以在很多String相关功能中使用:

public class TextPlusY implements CharSequence
{
    final List<String> texts = new ArrayList<>();
    final List<Float> yCoords = new ArrayList<>();

    //
    // CharSequence implementation
    //
    @Override
    public int length()
    {
        int length = 0;
        for (String text : texts)
        {
            length += text.length();
        }
        return length;
    }

    @Override
    public char charAt(int index)
    {
        for (String text : texts)
        {
            if (index < text.length())
            {
                return text.charAt(index);
            }
            index -= text.length();
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public CharSequence subSequence(int start, int end)
    {
        TextPlusY result = new TextPlusY();
        int length = end - start;
        for (int i = 0; i < yCoords.size(); i++)
        {
            String text = texts.get(i);
            if (start < text.length())
            {
                float yCoord = yCoords.get(i); 
                if (start > 0)
                {
                    text = text.substring(start);
                    start = 0;
                }
                if (length > text.length())
                {
                    result.add(text, yCoord);
                }
                else
                {
                    result.add(text.substring(0, length), yCoord);
                    break;
                }
            }
            else
            {
                start -= text.length();
            }
        }
        return result;
    }

    //
    // Object overrides
    //
    @Override
    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        for (String text : texts)
        {
            builder.append(text);
        }
        return builder.toString();
    }

    //
    // y coordinate support
    //
    public TextPlusY add(String text, float y)
    {
        if (text != null)
        {
            texts.add(text);
            yCoords.add(y);
        }
        return this;
    }

    public float yCoordAt(int index)
    {
        for (int i = 0; i < yCoords.size(); i++)
        {
            String text = texts.get(i);
            if (index < text.length())
            {
                return yCoords.get(i);
            }
            index -= text.length();
        }
        throw new IndexOutOfBoundsException();
    }
}

(TextPlusY.java)

TextPlusYExtractionStrategy

现在我们扩展LocationTextExtractionStrategy 以提取TextPlusY 而不是String。我们只需要泛化方法getResultantText

不幸的是,LocationTextExtractionStrategy 隐藏了一些需要在这里访问的方法和成员(private 或包保护);因此,需要一些反射魔法。如果您的框架不允许这样做,您将不得不复制整个策略并相应地对其进行操作。

public class TextPlusYExtractionStrategy extends LocationTextExtractionStrategy
{
    static Field locationalResultField;
    static Method sortWithMarksMethod;
    static Method startsWithSpaceMethod;
    static Method endsWithSpaceMethod;

    static Method textChunkSameLineMethod;

    static
    {
        try
        {
            locationalResultField = LocationTextExtractionStrategy.class.getDeclaredField("locationalResult");
            locationalResultField.setAccessible(true);
            sortWithMarksMethod = LocationTextExtractionStrategy.class.getDeclaredMethod("sortWithMarks", List.class);
            sortWithMarksMethod.setAccessible(true);
            startsWithSpaceMethod = LocationTextExtractionStrategy.class.getDeclaredMethod("startsWithSpace", String.class);
            startsWithSpaceMethod.setAccessible(true);
            endsWithSpaceMethod = LocationTextExtractionStrategy.class.getDeclaredMethod("endsWithSpace", String.class);
            endsWithSpaceMethod.setAccessible(true);

            textChunkSameLineMethod = TextChunk.class.getDeclaredMethod("sameLine", TextChunk.class);
            textChunkSameLineMethod.setAccessible(true);
        }
        catch(NoSuchFieldException | NoSuchMethodException | SecurityException e)
        {
            // Reflection failed
        }
    }

    //
    // constructors
    //
    public TextPlusYExtractionStrategy()
    {
        super();
    }

    public TextPlusYExtractionStrategy(ITextChunkLocationStrategy strat)
    {
        super(strat);
    }

    @Override
    public String getResultantText()
    {
        return getResultantTextPlusY().toString();
    }

    public TextPlusY getResultantTextPlusY()
    {
        try
        {
            List<TextChunk> textChunks = new ArrayList<>((List<TextChunk>)locationalResultField.get(this));
            sortWithMarksMethod.invoke(this, textChunks);

            TextPlusY textPlusY = new TextPlusY();
            TextChunk lastChunk = null;
            for (TextChunk chunk : textChunks)
            {
                float chunkY = chunk.getLocation().getStartLocation().get(Vector.I2);
                if (lastChunk == null)
                {
                    textPlusY.add(chunk.getText(), chunkY);
                }
                else if ((Boolean)textChunkSameLineMethod.invoke(chunk, lastChunk))
                {
                    // we only insert a blank space if the trailing character of the previous string wasn't a space, and the leading character of the current string isn't a space
                    if (isChunkAtWordBoundary(chunk, lastChunk) &&
                            !(Boolean)startsWithSpaceMethod.invoke(this, chunk.getText()) &&
                            !(Boolean)endsWithSpaceMethod.invoke(this, lastChunk.getText()))
                    {
                        textPlusY.add(" ", chunkY);
                    }

                    textPlusY.add(chunk.getText(), chunkY);
                }
                else
                {
                    textPlusY.add("\n", lastChunk.getLocation().getStartLocation().get(Vector.I2));
                    textPlusY.add(chunk.getText(), chunkY);
                }
                lastChunk = chunk;
            }

            return textPlusY;
        }
        catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
        {
            throw new RuntimeException("Reflection failed", e);
        }
    }
}

(TextPlusYExtractionStrategy.java)

用法

使用这两个类,您可以提取带有坐标的文本并在其中进行搜索,如下所示:

try (   PdfReader reader = new PdfReader(YOUR_PDF);
        PdfDocument document = new PdfDocument(reader)  )
{
    TextPlusYExtractionStrategy extractionStrategy = new TextPlusYExtractionStrategy();
    PdfPage page = document.getFirstPage();

    PdfCanvasProcessor parser = new PdfCanvasProcessor(extractionStrategy);
    parser.processPageContent(page);
    TextPlusY textPlusY = extractionStrategy.getResultantTextPlusY();

    System.out.printf("\nText from test.pdf\n=====\n%s\n=====\n", textPlusY);

    System.out.print("\nText with y from test.pdf\n=====\n");
    
    int length = textPlusY.length();
    float lastY = Float.MIN_NORMAL;
    for (int i = 0; i < length; i++)
    {
        float y = textPlusY.yCoordAt(i);
        if (y != lastY)
        {
            System.out.printf("\n(%4.1f) ", y);
            lastY = y;
        }
        System.out.print(textPlusY.charAt(i));
    }
    System.out.print("\n=====\n");

    System.out.print("\nMatches of 'est' with y from test.pdf\n=====\n");
    Matcher matcher = Pattern.compile("est").matcher(textPlusY);
    while (matcher.find())
    {
        System.out.printf("from character %s to %s at y position (%4.1f)\n", matcher.start(), matcher.end(), textPlusY.yCoordAt(matcher.start()));
    }
    System.out.print("\n=====\n");
}

(ExtractTextPlusY测试方法testExtractTextPlusYFromTest)

对于我的测试文档

上面测试代码的输出是

Text from test.pdf
=====
Ein Dokumen t mit einigen
T estdaten
T esttest T est test test
=====

Text with y from test.pdf
=====

(691,8) Ein Dokumen t mit einigen

(666,9) T estdaten

(642,0) T esttest T est test test
=====

Matches of 'est' with y from test.pdf
=====
from character 28 to 31 at y position (666,9)
from character 39 to 42 at y position (642,0)
from character 43 to 46 at y position (642,0)
from character 49 to 52 at y position (642,0)
from character 54 to 57 at y position (642,0)
from character 59 to 62 at y position (642,0)

=====

我的语言环境使用逗号作为小数分隔符,您可能会看到 666.9 而不是 666,9

您看到的多余空格可以通过进一步微调基本LocationTextExtractionStrategy 功能来删除。但这是其他问题的重点......

【讨论】:

    【解决方案3】:

    首先,SimpleTextExtractionStrategy 并不完全是“最聪明”的策略(顾名思义。

    其次,如果你想要这个职位,你将不得不做更多的工作。 TextExtractionStrategy 假定您只对文本感兴趣。

    可能的实现:

    • 实现 IEventListener
    • 获取所有呈现文本的事件的通知,并存储相应的 TextRenderInfo 对象
    • 完成文档后,根据这些对象在页面中的位置对它们进行排序
    • 遍历这个 TextRenderInfo 对象列表,它们提供正在渲染的文本和坐标

    如何:

    1. 实现 ITextExtractionStrategy(或扩展现有的 实施)
    2. 使用 PdfTextExtractor.getTextFromPage(doc.getPage(pageNr), strategy),其中 strategy 表示您在步骤 1 中创建的策略
    3. 您的策略应设置为跟踪其处理的文本的位置

    ITextExtractionStrategy 在其接口中有如下方法:

    @Override
    public void eventOccurred(IEventData data, EventType type) {
    
        // you can first check the type of the event
         if (!type.equals(EventType.RENDER_TEXT))
            return;
    
        // now it is safe to cast
        TextRenderInfo renderInfo = (TextRenderInfo) data;
    }
    

    需要记住的重要一点是,pdf 中的渲染说明不需要按顺序出现。 文本“Lorem Ipsum Dolor Sit Amet”可以使用类似于以下的指令呈现: 渲染“Ipsum Do”
    渲染“Lorem”
    渲染“lor Sit Amet”

    您必须进行一些巧妙的合并(取决于两个 TextRenderInfo 对象相距多远)和排序(以正确的阅读顺序获取所有 TextRenderInfo 对象。

    完成后,应该很容易。

    【讨论】:

    • 我无法在 iText7 中找到很多细节。请问有什么帮助吗?我正在寻找的只是发送文本并在相应的 pdf 页面上找到该文本。
    • 我也在尝试做类似的事情,我已经可以用Itext5做到这一点,但是Itext7变化太大了,很难找到如何实现自定义文本提取策略。
    • 我正在寻找的是在给定 pdf 页面中找到的文本的 Y 坐标。无法从 pdfpage 对象获取详细信息。
    • 我们如何从 pdfpage 对象中获取 TextRenderInfo?
    猜你喜欢
    • 2020-06-27
    • 1970-01-01
    • 2023-04-01
    • 2023-04-06
    • 2012-03-14
    • 1970-01-01
    • 2016-12-28
    • 2015-10-04
    • 1970-01-01
    相关资源
    最近更新 更多