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

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

Evidence: Mixed evidence

Шрифты — та область, где 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)Текст, пригодный для поиска, копирования и доступный для вспомогательных технологийЧто глифы отрисовываются правильноИдеально выглядящая страница, непригодная для поиска / искажённая при копировании

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

Правило именования подмножеств нормативно и сформулировано точно. Spec: ISO 32000-2, §9.9.2 требует, чтобы PostScript-имя подмножества шрифта — BaseFont и FontName дескриптора — начиналось с тега из ровно шести заглавных букв, затем знака плюс, затем PostScript-имени исходного шрифта. Он также требует, чтобы разные подмножества одного и того же шрифта в одном файле использовали разные теги. Именно это правило позволяет средству просмотра различать два подмножества и правильно объединять документы. Evidence: Standard-backed

Кодирование — отдельный пункт стандарта, не связанный с отрисовкой. Spec: ISO 32000-2, §9.10.3 определяет /ToUnicode как поток, содержащий CMap, которая отображает коды символов в значения Unicode, а процедура извлечения текста в Spec: ISO 32000-2, §9.10.2 использует эту CMap для преобразования кодов символов в Unicode при поиске и индексировании. Ничто в механизме отрисовки глифов не затрагивает /ToUnicode — именно поэтому текст может выглядеть правильно, а извлекаться неправильно.

Что касается встраивания, стандарт указывает, что большинство словарей шрифтов содержат дескриптор шрифта, чей поток встроенного файла шрифта необязателен, но настоятельно рекомендуется. PDF 2.0 ужесточает это специально для четырнадцати стандартных шрифтов Type 1. Политика Strict в NextPDF — корректное с точки зрения соответствия прочтение этого ужесточения. AllowBase14 — явный механизм обратной совместимости, включаемый по выбору: движок никогда не понижает требования незаметно.

Strict PDF 2.0 font-embedding enforcement — edition availability
Edition Availability
Core

Доступно. Создание подмножеств, выдача /ToUnicode и явная политика встраивания Strict / AllowBase14 — это поведение основного движка.

Pro

Добавляет более глубокий контроль соответствия и отчётность по встраиванию шрифтов на уровне профиля.

Enterprise

Добавляет такой же контроль соответствия в рамках корпоративного эксплуатационного контура.

Вот две половины правильно встроенного составного шрифта с подмножеством, пригодного для поиска. Тег подмножества следует правилу шести букв из стандарта; ссылка /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 ожидает, что шрифты будут встроены, а не указаны по имени.