Я пытаюсь найти положение текста на странице PDF?

Я попытался получить текст на странице PDF с помощью PDF Text Extractor, используя простую стратегию извлечения текста. Я зацикливаю каждое слово, чтобы проверить, существует ли мое слово. разделить слова с помощью:

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

Я не смог найти текстовую позицию. Проблема в том, что я не смог найти расположение текста. Все, что мне нужно найти, это координаты слова в файле PDF.

2
Speed 2 Май 2017 в 23:41

3 ответа

Лучший ответ

Во-первых, SimpleTextExtractionStrategy не совсем «самая умная» стратегия (как следует из названия.

Во-вторых, если вы хотите получить должность, вам придется выполнять гораздо больше работы. TextExtractionStrategy предполагает, что вас интересует только текст.

Возможная реализация:

  • реализовать IEventListener
  • получать уведомления обо всех событиях, отображающих текст, и сохранять соответствующий объект TextRenderInfo
  • как только вы закончите работу с документом, отсортируйте эти объекты в зависимости от их положения на странице.
  • переберите этот список объектов TextRenderInfo, они предлагают как отображаемый текст, так и координаты

Как:

  1. реализовать ITextExtractionStrategy (или расширить существующую реализацию)
  2. используйте PdfTextExtractor.getTextFromPage (doc.getPage (pageNr), стратегия), где стратегия обозначает стратегию, созданную на шаге 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"
рендер "лор сит амет"

Вам нужно будет выполнить некоторое умное слияние (в зависимости от того, насколько далеко расположены два объекта TextRenderInfo) и сортировку (чтобы получить все объекты TextRenderInfo в правильном порядке чтения.

Как только это будет сделано, это должно быть легко.

3
Joris Schellekens 4 Май 2017 в 07:52

@Joris 'answer объясняет, как реализовать совершенно новую стратегию извлечения / прослушиватель событий для этой задачи. В качестве альтернативы можно попробовать настроить существующую стратегию извлечения текста, чтобы сделать то, что вам нужно.

Этот ответ демонстрирует, как настроить существующий LocationTextExtractionStrategy так, чтобы он возвращал как текст, так и соответствующие ему координаты y символов.

Осторожно, это всего лишь подтверждение концепции, которая, в частности, предполагает, что текст должен быть написан горизонтально, то есть с использованием эффективной матрицы преобразования (ctm и текстовая матрица объединены) с b и c, равными 0. Более того, методы поиска символов и координат TextPlusY совсем не оптимизированы и могут выполняться долго.

Поскольку OP не выражает языковые предпочтения, здесь решение для iText7 для 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)

Для моего тестового документа

enter image description here

Вывод тестового кода выше

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. Но это фокус других вопросов ...

4
Community 23 Май 2017 в 12:26

Я смог манипулировать им с моей предыдущей версией для 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 и что-то с ним сделать.

6
Histerical 5 Май 2017 в 15:39