Кто интересовался вопросом, то знают, что PDF состоит из заголовка, таблицы перекрестных ссылок (XRef), тела и трайлера (прицепа). Все элементы кроме заголовка могут быть разбросаны частями и в нескольких экземплярах по всему документу. Для начала надо прочитать таблицу XRef. Рекомендую оформить её классом. Для поиска адреса таблицы читаем файл с конца, пока не встретим тег %%EOF. Продолжаем читать задом наперед до тега startxref. теперь можно считать число, которое следует за этим тегом.
Вот пример конца файла:
startxref
173
%%EOF
число 173 — это смещение от начала данных файла к началу первой таблицы XRef. Переместившись в эту точку, мы видим что-то вроде этого:
xref
7628 42
0000000016 00000 n
0000001195 00000 f
и тд.
На 7628 пока не будем обращать внимание (это имя первого объекта, где записана информация о количестве страниц, например, а так же много чего другого). А 42 — это количество записей в данной части таблицы. Далее совсем просто: считываем в 10 байтный буфер первое слово, пропускаем пробел и считываем 5 байтный буфер, читаем отдельный символ. И так 42 раза. Преобразованные к целым строки имеют следующее значение — смещение от начала данных к ссылочному объекту, номер генерации. Последний символ интерпретируется так: n — объект используется, f — объект не используется, но как я говорил, у таблицы XRef могут быть продолжения в потоке файла. Как их найти? после таблицы всегда следует тег trailer. Когда он встретится надо искать строку /Prev — если она есть, то следом идет смещение к следующей таблице.
/Prev 4025745
Таким образом прочитываем все таблицы, если их больше одной. Закончить чтение можно, если в следующем трайлере будет отсутствовать ключ /Prev. Признаком последней таблицы может служить и то, что она начинается с записи 0000000000 65535 f. Надо сказать, что мы читаем таблицы задом наперед, последняя при чтении является первой, которая появилась при создании самого документа, а первая при чтении возникла после последнего редактирования.
Используя полученные данные мы можем перемещаться к любому ссылочному объекту документа. Правда есть еще прямые объекты, адреса которых не внесены в XRef, но об этом позже. Теперь мы можем перебирать объекты документа, проверяя их тип и делая с ними, что душе угодно. Объект начинается так:
7626 0 obj
содержимое объекта
endobj
7626 — номер (имя) объекта, а 0 — номер генерации, который должен совпадать с подобным значением в таблице ссылок для этого объекта. Как я понял, если объект меняется, редактируется, то и номер генерации увеличивается. Мы собрались искать шрифты, для этого надо прочитать словарь объекта, который представляет собой лексему, заключенную в теги <<… >>. Если элементы словаря имеют такую структуру, например:
/FirstChar 32
где слово после слеша — ключ, а необязательное значение после пробела — значение. При парсинге надо помнить, что значение может содержать любые данные, любой вложенности, в том числе и другие словари. Так что рекурсию вам в руки, впрочем, можно и без рекурсии, если мы работаем над конкретной задачей извлечения шрифтов. Указанное значение может также включать вложенные или не вложенные элементы следующих типов:
(… ) -текстовые строки
<… > — hex-строки
[… ] — массивы
Строка значения продолжается до следующего слеша или до перевода строки. Чтобы идентифицировать объект шрифта надо найти в словаре комбинацию:
/Type /Font
Теперь фильтруем Truetype шрифты по содержанию в словаре последовательности:
/Subtype /TrueType
Остальные ключи игнорируем, потому что мы просто хотим извлечь шрифты. Но самого шрифта мы в этом объекте, скорее всего не найдем. Только набор ненужных нам ключей. Читаем один из них:
/FontDescriptor 1675 0 R
Если такой ключ отсутствует, то шрифт внешний и не встроен в документ. Далее номер генерации этого объекта, а символ R обозначает, что это ссылка. Таблицу XRef мы уже прочитали и теперь можем переместиться к данным шрифта, через поиск смещения для объекта с номером 1675. Правда, возможен такой вариант:
/FontDescriptor << словарь и (или) данные шрифта >>
Будем считать, что мы переместились по ссылке к прямому объекту. В его словаре должны быть такие ключи:
/Type /FontDescriptor
В этом объекте тоже много полезных сведений о шрифте, но самого шрифта опять нет. Не моя вина — все претензии к компании Adobe. Нам нужен такой ключ
/FontFile2 1676 0 R
Знакомая конструкция. Переходим к следующему объекту. Если мы все сделали правильно, то это потоковый объект. Он состоит из словаря потока и из бинарных данных, заключенных между тегами stream… endstream. Вот тут надо сказать, что наличие бинарных данных не дает использовать готовые текстовые парсеры. Перепробовал много и пришлось написать свой с нуля. Бинарные данные можно считывать разом, так как в словаре потока имеется ключ /Length с длиной потока. Если попробовать сохранить извлеченный поток в файл с расширением TTF, то система объявит, что это никакой не шрифт. Все правильно, надо его разжать.
Шрифт чаще сжат с помощью zip, но для верности можно это проверить по наличию ключа /FlateDecode. Если работаем в Delphi, то используем стандартный ZLib. Мы можем получить размер буфера для разжатых данных из словаря потока по ключу /Length1. Ну и нужно знать, что встроенный в документ шрифт содержит только те глифы, которые в документе используются.
Думаю, что после этих наметок можно брать в одну руку hex-вьвер, в другую — PDFReference и стоить собственный АкробатРидер.

