Перейти к содержимому
getnextpdf.com

Шрифты: самая сложная часть

Шрифты — та область, где PDF может выглядеть совершенно правильно и при этом быть незаметно сломанным. Страница может отрисовывать нужные глифы, но не находиться поиском, не копироваться как текст и не соответствовать профилю архивного хранения. Всё это может случиться одновременно, и визуально ничто вас не предупредит. Эта страница о трёх вещах, которые должны быть сделаны правильно: встраивании, создании подмножеств и кодировании, — и о том, что NextPDF делает для каждой из них.

“Выглядит нормально” — самая опасная фраза в работе с PDF, и именно со шрифтами она обходится дороже всего. Должны выполняться три независимых условия:

  1. Встраивание — программа шрифта переносится внутрь файла, поэтому он отрисовывается одинаково даже на машине, где этот шрифт не установлен.
  2. Создание подмножества — переносятся только реально используемые глифы, поэтому CJK-шрифт размером 20 MB не раздувает каждый документ.
  3. Кодирование — есть правильное отображение кодов символов на странице обратно в Unicode, поэтому текст можно искать, копировать, индексировать и читать с помощью вспомогательных технологий.

Визуальная отрисовка лишь частично подтверждает первое условие. Документ может показывать безупречные глифы и при этом полностью проваливать третье: текст оказывается изображением слов, а не словами. Именно такой сбой проходит любую проверку “выглядит нормально”, а затем проваливает аудит соответствия или запрос на раскрытие документов.

  • Шрифт в PDF — это словарь и, как правило, поток со встроенной программой шрифта.
  • Создание подмножества переписывает эту программу так, чтобы в ней остались только используемые глифы. Имя шрифта-подмножества получает тег из шести заглавных букв и +, чтобы средства просмотра воспринимали его как отдельный шрифт.
  • Кодирование — отдельная задача: отобразить коды символов в Unicode. CMap /ToUnicode делает текст пригодным для поиска и копирования и не зависит от того, правильно ли выглядят глифы.
  • Правильно выглядящий текст без /ToUnicode (или с неправильным /ToUnicode) — классический незаметный сбой: на экране всё идеально, на практике поиск не работает.
  • NextPDF создаёт подмножества шрифтов TrueType, сохраняет идентичность глифов для правильной отрисовки и выдаёт CMap /ToUnicode, чтобы извлечение работало, — а также может обеспечивать выполнение правила встраивания PDF 2.0, а не только предупреждать.

Создание подмножества. FontSubsetter (src/Typography/FontSubsetter.php) разбирает каталог таблиц исходного шрифта TrueType и читает cmap, чтобы сопоставить кодовые точки Unicode с идентификаторами глифов. Он обрабатывает и BMP format 4, и format 12 с полным покрытием Unicode, который нужен для CJK. Затем он делает шаг, который наивные средства создания подмножеств пропускают: разрешает зависимости составных глифов по транзитивному замыканию. Глиф с диакритикой, построенный из базовой буквы и комбинируемого знака, ссылается на другие глифы как на компоненты. Если эти компоненты отбросить, глиф отрисуется неправильно. Средство создания подмножеств обходит этот граф, пока не перестанут появляться новые компоненты, с защитой от циклов, чтобы некорректный шрифт не мог зациклить процесс.

В этом файле стоит отметить два инженерных решения. Во-первых, идентификаторы глифов сохраняются, а не перенумеровываются: неиспользуемые слоты заполняются нулями в glyf/loca, поэтому исходные индексы глифов в потоке содержимого остаются действительными при CIDToGIDMap /Identity. Перенумерование было бы компактнее, но потребовало бы переписать каждую ссылку на глиф. Сохранение идентичности корректно по построению. Во-вторых, обход отсортирован (по возрастанию gid), поэтому подмножество побайтово детерминировано: один и тот же шрифт и одни и те же используемые глифы дают одни и те же байты подмножества, что и требуется для воспроизводимых сборок. Если создание подмножества сэкономило бы менее ~10 % файла, исходный шрифт возвращается без изменений. Накладные расходы не стоят такого небольшого выигрыша.

Встраивание. Явная политика решает, переносится ли программа шрифта вообще, — без догадок. Pdf20FontEmbeddingPolicy (src/Writer/Pdf20FontEmbeddingPolicy.php) имеет два режима. В профиле PDF 2.0 Strict отклоняет ссылку на невстроенный стандартный Type 1 (“Base14”) с типизированным исключением — это поведение корректно с точки зрения соответствия. AllowBase14 сохраняет исторический рекомендательный путь. На время миграции он выдаёт минимальный дескриптор шрифта, который стандарт всё ещё требует, и отправляет предупреждение, а не выбрасывает исключение. Вызывающая сторона выбирает режим явно на уровне документа; он никогда не выводится из самого шрифта.

Кодирование. Для составных (Type 0) шрифтов EmbeddedTtfFontDictBuilder (src/Writer/EmbeddedTtfFontDictBuilder.php) выдаёт потомка CIDFontType2, родителя Type0 и поток CMap /ToUnicode, чтобы коды символов разрешались обратно в Unicode. Поток /ToUnicode правомерно отсутствует только в одном случае: когда самоописываемая предопределённая CJK CMap уже даёт средству просмотра отображение символов в Unicode. В этом случае CMap и есть кодирование, поэтому простой профиль опускает избыточный поток /ToUnicode, чтобы сэкономить байты. Во всех остальных случаях именно поток /ToUnicode сохраняет текст текстом.

АспектЧто он гарантируетЧего он не гарантируетНезаметный сбой при ошибке
ВстраиваниеОдинаковая отрисовка без установленного шрифтаЧто текст пригоден для поискаПодставленный шрифт; неправильные метрики на другой машине
Создание подмножестваМаленький файл; только используемые глифыЧто-либо о кодированииОтсутствующие компоненты составных глифов → сломанные глифы с диакритикой
Кодирование (/ToUnicode)Текст, пригодный для поиска, копирования и доступный для вспомогательных технологийЧто глифы отрисовываются правильноИдеально выглядящая страница, непригодная для поиска / искажённая при копировании

Три аспекта шрифтов независимы. Встраивание и создание подмножеств отвечают за внешний вид и размер; кодирование отвечает за смысл. Страница может пройти первые два условия и провалить третье, причём визуально это никак не проявится.

Вот две половины правильно встроенного составного шрифта с подмножеством, пригодного для поиска. Тег подмножества следует правилу шести букв из стандарта; ссылка /ToUnicode сохраняет текст извлекаемым.

% The Type 0 (composite) font dictionary
20 0 obj
<< /Type /Font /Subtype /Type0
/BaseFont /ABCDEF+NotoSans % six-letter subset tag + '+'
/Encoding /Identity-H
/DescendantFonts [21 0 R]
/ToUnicode 23 0 R >> % the map that makes text searchable
endobj
% The descendant CIDFontType2 (carries the subsetted program)
21 0 obj
<< /Type /Font /Subtype /CIDFontType2
/BaseFont /ABCDEF+NotoSans
/CIDToGIDMap /Identity % glyph IDs preserved, not remapped
/FontDescriptor 22 0 R >>
endobj

Поле /ToUnicode 23 0 R объекта 20 — это и есть разница между документом, пригодным для поиска, и его изображением. Уберите его (вне случая предопределённой CMap), и каждый глиф по-прежнему будет отрисовываться идеально, но поиск не найдёт на странице ни одного слова.

Если сказать прямо, ловушка вот в чём: правильная отрисовка глифов ничего не говорит о том, является ли текст текстом. Отрисовка идёт по пути “кодирование → глиф”. Поиск и копирование идут по пути “код → Unicode” (/ToUnicode). Это разные механизмы, которые читают разные части словаря шрифта. Поэтому документ может иметь безупречный визуальный вывод и отсутствующий или неправильный /ToUnicode. В результате получается страница, которая выглядит убедительно, но функционально непригодна для поиска, — сбой, который проходит любую визуальную проверку, потому что по определению смотреть не на что.

Сопутствующая ловушка — предположение “шрифт встроен, значит, с архивным хранением всё в порядке”. Встраивание необходимо, но недостаточно. Профиль вроде PDF/A также ожидает подмножества, названные по правилу шести букв, и правильное кодирование. Встроенный, но непригодный для поиска шрифт всё равно приводит к сбою.

Средство создания подмножеств в NextPDF — это именно средство создания подмножеств TrueType. Оно требует обязательных таблиц TrueType и возвращает исходный шрифт без изменений, если они отсутствуют или выигрыш ниже порога ~10 %. Создание подмножества и CMap /ToUnicode делают текст извлекаемым, но не могут спасти исходный шрифт, в котором нет информации для отображения глифа обратно в осмысленный символ. Там, где значение Unicode определить нельзя, никакая выдача CMap его не придумает.

Эта страница о создании правильной структуры шрифтов в документах, которые NextPDF записывает. Это не инструмент восстановления шрифтов в произвольных входящих PDF. И выдача соответствующего подмножества и кодирования сама по себе не сертифицирует документ по полному профилю архивного хранения — это отдельная, более широкая проверка.

Почему тег из шести букв — почему не имя шрифта? Чтобы средство просмотра могло различать два разных подмножества одного и того же шрифта и объединять документы, не смешивая их наборы глифов. Разные подмножества — разные теги, по правилу.

Когда допустимо не иметь /ToUnicode? Когда самоописываемая предопределённая CJK CMap уже предоставляет отображение символов в Unicode. В этом случае CMap и есть кодирование. Отдельная /ToUnicode была бы избыточной. В остальных случаях её отсутствие — дефект.

Может ли создание подмножества когда-либо навредить? Только если сделать его неправильно. Отбрасывание компонентов составных глифов ломает глифы с диакритикой. Перенумерование идентификаторов глифов без переписывания ссылок ломает отрисовку. NextPDF избегает обоих сценариев: разрешает замыкание по компонентам и сохраняет идентичность глифов.

  • Потоки и фильтры — встроенные программы шрифтов — это отфильтрованные потоковые объекты со своим собственным контрактом декодирования.
  • Что такое PDF на самом деле — объектная модель, в которой живут словари шрифтов и потоки программ.
  • PDF 2.0: что изменилось — в том числе ужесточённые ожидания по встраиванию шрифтов в базовом стандарте 2.0.
  • Встроенная программа шрифта — собственно файл шрифта (TrueType/CFF/Type 1), переносимый внутрь PDF в виде потока, поэтому отрисовка не зависит от шрифтов, установленных у средства просмотра.
  • Создание подмножества — переписывание программы шрифта так, чтобы для уменьшения размера она содержала только используемые документом глифы.
  • Тег подмножества — обязательный префикс из шести заглавных букв плюс + в имени шрифта-подмножества (например, ABCDEF+NotoSans).
  • /ToUnicode — поток CMap, отображающий коды символов в значения Unicode; то, что делает текст PDF пригодным для поиска, копирования и доступным.
  • Составной глиф — глиф, построенный путём ссылок на другие глифы как на компоненты; его компоненты должны сохраняться при создании подмножества.
  • CIDToGIDMap /Identity — режим, в котором индексы глифов в потоке содержимого являются неизменёнными собственными идентификаторами глифов шрифта; NextPDF сохраняет идентичность глифов, чтобы это оставалось действительным.
  • Base14 — четырнадцать стандартных шрифтов Type 1; PDF 2.0 ожидает, что шрифты будут встроены, а не указаны по имени.