Шрифты: самая сложная часть
Шрифты — та область, где PDF может выглядеть совершенно правильно и при этом быть незаметно сломанным. Страница может отрисовывать нужные глифы, но не находиться поиском, не копироваться как текст и не соответствовать профилю архивного хранения. Всё это может случиться одновременно, и визуально ничто вас не предупредит. Эта страница о трёх вещах, которые должны быть сделаны правильно: встраивании, создании подмножеств и кодировании, — и о том, что NextPDF делает для каждой из них.
Почему это важно
Заголовок раздела «Почему это важно»“Выглядит нормально” — самая опасная фраза в работе с PDF, и именно со шрифтами она обходится дороже всего. Должны выполняться три независимых условия:
- Встраивание — программа шрифта переносится внутрь файла, поэтому он отрисовывается одинаково даже на машине, где этот шрифт не установлен.
- Создание подмножества — переносятся только реально используемые глифы, поэтому CJK-шрифт размером 20 MB не раздувает каждый документ.
- Кодирование — есть правильное отображение кодов символов на странице обратно в Unicode, поэтому текст можно искать, копировать, индексировать и читать с помощью вспомогательных технологий.
Визуальная отрисовка лишь частично подтверждает первое условие. Документ может показывать безупречные глифы и при этом полностью проваливать третье: текст оказывается изображением слов, а не словами. Именно такой сбой проходит любую проверку “выглядит нормально”, а затем проваливает аудит соответствия или запрос на раскрытие документов.
Если коротко
Заголовок раздела «Если коротко»- Шрифт в PDF — это словарь и, как правило, поток со встроенной программой шрифта.
- Создание подмножества переписывает эту программу так, чтобы в ней остались только используемые глифы. Имя шрифта-подмножества получает тег из шести заглавных букв и
+, чтобы средства просмотра воспринимали его как отдельный шрифт. - Кодирование — отдельная задача: отобразить коды символов в Unicode. CMap
/ToUnicodeделает текст пригодным для поиска и копирования и не зависит от того, правильно ли выглядят глифы. - Правильно выглядящий текст без
/ToUnicode(или с неправильным/ToUnicode) — классический незаметный сбой: на экране всё идеально, на практике поиск не работает. - NextPDF создаёт подмножества шрифтов TrueType, сохраняет идентичность глифов для правильной отрисовки и выдаёт CMap
/ToUnicode, чтобы извлечение работало, — а также может обеспечивать выполнение правила встраивания PDF 2.0, а не только предупреждать.
Как к этому подходит NextPDF
Заголовок раздела «Как к этому подходит NextPDF»Создание подмножества. 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 dictionary20 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 searchableendobj
% 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. И выдача соответствующего подмножества и кодирования сама по себе не сертифицирует документ по полному профилю архивного хранения — это отдельная, более широкая проверка.
Мини-FAQ
Заголовок раздела «Мини-FAQ»Почему тег из шести букв — почему не имя шрифта? Чтобы средство просмотра могло различать два разных подмножества одного и того же шрифта и объединять документы, не смешивая их наборы глифов. Разные подмножества — разные теги, по правилу.
Когда допустимо не иметь /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 ожидает, что шрифты будут встроены, а не указаны по имени.