Калина Алексей блог программиста

Дружим с PDF

Текстовый формат PDF становится стандартом электронного документооборота во всем мире. Поэтому у программистов регулярно возникает задача извлечения текста из таких файлов. Вместе с ней появляются и более сложные задачи, например, получение текстовой структуры документа. Чтобы подружиться с PDF и решать эти задачи, изучим формат данных. После этого познакомимся с C# библиотекой iTextSharp — инструментом для работы с PDF-файлами.

Структура файла

PDF — текстовый формат, но имеет достаточно сложную структуру. Он может содержать мультимедиа, ссылки и многое другое. Для примера возьмем простой тестовый документ. документ Изучать формат начнем со структуры файла. Он включает четыре раздела.

Заголовок. Заголовком называется первая строка файла. Она содержит информацию о версии PDF. В нашем тестовом документе заголовок выглядит так:

%PDF-1.5

PDF начал свою историю с версии 1.0. Последняя на текущий момент версия — 1.7. Символ % обозначает комментарий в языке PostScript, который лег в основу формата.

Тело. Все содержимое документа находится в теле файла. Типы данных, которые встречаются в теле, рассмотрим в следующем разделе.

Таблица xref. Эта таблица содержит ссылки на все объекты документа. Благодаря ей, не нужно читать весь документ, чтобы найти нужный объект. Таблица xref состоит из секций. Каждая секция соответствует новой версии документа.

xref
0 44
0000000000 65535 f 
0000000361 00000 n 
0000000257 00000 n 
0000000015 00000 n 
…

Таблица ссылок начинается со слова xref. Каждая секция начинается с двух чисел: идентификатора первого объекта и количества объектов в секции. Объекты представляются двумя последовательностями байтов. Первая соответствует позиции первого байта объекта в файле, а вторая — номеру поколения. О поколениях и метках f и n будет немного позже.

Хвост. Этот раздел дает информацию о расположении ключевых объектов в файле. Например, в нем прописано смещение таблицы xref от начала файла. Поэтому любое программное чтение PDF-документа начинается с хвоста. В нашем примере:

trailer
<< 
/Size 44
/Root 42 0 R
/Info 43 0 R
/ID [<7298F57CACD45F4041F17029C0BBF710> <7298F57CACD45F4041F17029C0BBF710>] >>
startxref
11085
%%EOF

Хвост начинается с ключевого слова trailer. Он включает в себя словарь с мета-информацией о документе и позицию начала таблицы ссылок. Конец файла отмечается строкой %%EOF. В словарь хвоста входят данные о количестве объектов (/Size), ссылки на каталог документа (/Root — об этом немного позже) и информационный словарь (/Info), а также идентификатор файла (/ID).

Типы данных

В теле документа содержится вся информация, которая отображается пользователю. Она может быть представлена восемью типами данных:

Любой PDF-объект может быть помечен уникальным идентификатором и использоваться как ссылка. Такие объекты называются косвенными. Они начинаются с идентификатора, номера поколения и ключевого слова obj. Заканчивается косвенный объект словом endobj. На эти объекты можно ссылаться в таблице xref и любом другом объекте (для этого используется символ R). Так как они содержатся в таблице ссылок, доступ к ним осуществляется очень быстро.

Инкрементальные обновления

Формат PDF спроектирован с идеей инкрементальных обновлений документа. То есть, при изменении содержимого, файл не перезаписывается заново. В его конец добавляются новый заголовок, xref таблица и хвост. В новых таблицах ссылок будут записаны только те объекты, которые были добавлены, изменены или удалены. Удаленные объекты помечаются символом f, а новые символом n. Каждый хвост содержит запись /Prev, которая указывает на предыдущую таблицу xref.

Представление документа

Содержимое документа составляют объекты из тела файла. Как мы уже выяснили, они могут содержать ссылки друг на друга. На деле ссылочная структура объектов представляет собой дерево. В корне находится объект, который называется каталог документа. Его потомки — важные структурные элементы, одним из которых является дерево страниц. Рассмотрим подробнее, что оно из себя представляет.

Каталог документа ссылается на корень дерева страниц. Листья дерева являются страницами. Каждый узел дерева содержит информацию о родителе, детях и количестве листьев среди потомков. Каждую страницу документа можно найти по записи /Page, корень дерева страниц — по записи /Pages, а каталог документа — по /Catalog.

Извлечение текста и iTextSharp

Теперь, когда мы получили общее представление о формате данных PDF, мы можем перейти к связанным с ним задачам. Структура PDF-документов сложна, поэтому даже задача извлечения текста не является тривиальной. Текст содержится в объектах, где есть потоки. Обычно он сжат, возможно, несколькими способами. Об этом скажут те самые фильтры из мета-информации потока. Кроме того, каждый символ в потоке зашифрован в соответствии с таблицей символов, которая также хранится в PDF-документе.

Мы не будем самостоятельно реализовывать алгоритм извлечения текста. Если вы используете язык C# для решения своих задач, подходящим инструментом для работы с PDF-документами будет библиотека iTextSharp. Она предоставляет интерфейс к той структуре, что мы обсудили, и реализует решения стандартных задач.

Для работы с существующим PDF-документом создадим экземпляр класса PdfReader.

var reader = new PdfReader("paper.pdf");

PdfReader содержит информацию о всех тех объектах, которые были рассмотрены. Например, мы можем просмотреть мета-информацию каталога документа:

foreach (var key in reader.Catalog.Keys)
{
    Console.WriteLine($"key: {key}   value: {reader.Catalog.Get(key)}");
}

Результат:

key: /Type   value: /Catalog
key: /Pages   value: 6 0 R

Для извлечения текста из документа используется статический класс PdfTextExtractor. Помимо PdfReader’а он принимает на вход номер страницы.

var text = PdfTextExtractor.GetTextFromPage(reader, 1);

Задачи, связанные со структурой текста, такие как выделение параграфов, заголовков и тому подобные вообще не имеют общего решения. Это связано с тем, что в PDF-файле не хранится этой информации, только лишь потоки текста. Но PdfTextExtractor может принимать третий параметр – реализацию интерфейса ITextExtractionStrategy. Вы можете написать свою стратегию обработки текста, которая будет учитывать специфичные для вашего случая параметры (позицию на странице, шрифт и т.д.). В результате вы получите не только текст, но и структурные элементы.

Заключение

Формат PDF был тщательно спроектирован и имеет достаточно замысловатую структуру. Он имеет механизмы для инкрементального обновления, что позволяет рационально сохранять новые версии файла и хранить историю документа. Кроме того, PDF-файл может включать сложные мультимедийные элементы. Однако работа с текстом документа тоже усложняется. iTextSharp – вариант C# разработчиков для взаимодействия с PDF-документами.