PDF Чтение выделенного текста (выделение аннотаций) с использованием C#
Я написал инструмент извлечения с использованием 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;
Результат, возвращаемый PdfTextExtractor
не совсем правильно. Например, он возвращает "собирался устранить бумажную погоню", хотя было выделено только "устранить".
Интересно, что весь текст для TJ, содержащий выделенное "исключить", "собирался устранить погоню за бумагой" (TJ - это инструкция PDF, которая записывает текст на страницу).
Я хотел бы услышать любой вклад относительно этой проблемы - также решения, которые не вовлекают iTextSharp.
2 ответа
Причина
Достаточно интересно, что весь текст для 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
Таким образом, все, что вам нужно сделать, это создать и использовать RenderListener
/ IRenderListener
реализация, которая перенаправляет все вызовы, которые он получает, к другому слушателю (вашему фильтрованному слушателю в вашем случае) с поворотом, который renderText
/ RenderText
разбивает его TextRenderInfo
аргумент и направляет осколки один за другим индивидуально.
Образец Java
Как ОП попросил более подробную информацию, здесь еще немного кода. Поскольку я преимущественно работаю с Java, я предоставляю его на Java для iText. Но это легко портировать на C# для iTextSharp.
Как упомянуто выше, необходим этап предварительной обработки, который разбивает фрагменты текста на отдельные символы и перенаправляет их по отдельности вашему отфильтрованному слушателю рендеринга.
Для этого шага вы можете использовать этот класс TextRenderInfoSplitter
:
package stackru.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;
}
Если у тебя есть TextExtractionStrategy strategy
(как ваш new FilteredTextRenderListener(new LocationTextExtractionStrategy(), filter)
), теперь вы можете кормить его односимвольным TextRenderInfo
такие случаи:
String textInsideRect = PdfTextExtractor.getTextFromPage(reader, pageNo, new TextRenderInfoSplitter(strategy));
Я проверил это с PDF, созданным в этом ответе для области
Rectangle rect = new Rectangle(200, 600, 200, 135);
Для справки я отметил область в PDF:
Извлечение текста отфильтровано по области без TextRenderInfoSplitter
результаты в:
I am trying to create a PDF file with a lot
of text contents in the document. I am
using PDFBox
Извлечение текста отфильтровано по области с TextRenderInfoSplitter
результаты в:
to create a PDF f
ntents in the docu
n g P D F
Кстати, здесь вы видите недостаток раннего разбиения текста на отдельные символы: последняя строка текста набирается с использованием очень большого интервала символов. Если вы сохраните текстовые сегменты в PDF как они есть, стратегии извлечения текста все равно легко увидят, что строка состоит из двух слов using и PDFBox. Как только вы вводите текстовые сегменты символ за символом в стратегии извлечения текста, они, вероятно, будут интерпретировать такие широко распространенные слова как многие однобуквенные слова.
Улучшение
Выделенное слово "устранить", например, извлекается как "о ликвидации т". Это было выделено двойным щелчком по слову и выделено в 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);
}
Учитывая, что вы сначала разбиваете текст на символы, вы можете проверить, полностью ли содержится соответствующая базовая строка в рассматриваемой области, то есть реализовать собственную RenderFilter
копируя RegionTextRenderFilter
а затем заменить линию
return filterRect.intersectsLine(x1, y1, x2, y2);
от
return filterRect.contains(x1, y1) && filterRect.contains(x2, y2);
Однако, в зависимости от того, насколько точно текст выделен в Adobe Acrobat Reader, вы можете изменить его полностью по-своему.
Выделенные аннотации представляют собой набор четырехугольников, представляющих области на странице, окруженные аннотацией в /QuadPoints
запись в словаре.
Почему они так?
Это моя вина, на самом деле. В Acrobat 1.0 я работал над кодом "найти текст", который изначально использовал только прямоугольник для представления выбранной области на странице. Работая над кодом, я был очень недоволен результатами, особенно с картами, где текст следовал за деталями земли.
В результате я заставил инструмент find создать набор четырехугольников на странице и отжечь их, когда это возможно, для построения слов.
В Acrobat 2.0 инженер, отвечающий за полное обобщенное извлечение текста, создал алгоритм под названием Wordy, который был лучше, чем мой первый вырез, но он сохранил четырехсторонний код, поскольку он был наиболее точным представлением того, что было на странице.
Почти весь текстовый код был реорганизован для использования этого кода.
Тогда мы получим основные аннотации. Когда аннотации разметки были добавлены в Acrobat, они использовались для оформления текста, который уже находился на странице. Когда пользователь нажимает на страницу вниз, Wordy извлекает текст в соответствующие структуры данных, а затем инструмент выбора текста отображает движение мыши на четырехугольные наборы. Когда текстовая аннотация подсветки текста создается, подмножество четырехугольников из Wordy помещается в новую аннотацию подсветки текста.
Как вы получаете слова на странице, которые выделены. Tricky. Вы должны извлечь текст на странице (у вас нет Wordy, извините), а затем найти все квадраты, содержащиеся в наборе, из аннотации.