Типографика: реестр шрифтов, подмножества, CMap, кодирование, BiDi
Модуль типографики преобразует файл шрифта и строку Unicode в байты, необходимые для потока содержимого Portable Document Format (PDF). Он отвечает за разбор шрифтов, реестр на уровне процесса, создание подмножеств глифов, вывод CMap ToUnicode, стратегии кодирования с учётом cmap и движок двунаправленного текста Unicode.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Концептуальный обзор
Заголовок раздела «Концептуальный обзор»FontRegistry хранит шрифты на протяжении жизни процесса и реализует FontRegistryInterface. Он один раз разбирает файл TrueType, OpenType, TrueType Collection (TTC) или Type 1 (Printer Font Binary (PFB) и Adobe Font Metrics (AFM)) и возвращает неизменяемый FontInfo. Используйте его в долгоживущих обработчиках: прогрейте набор шрифтов при запуске, затем вызовите lock(). После этого реестр отклоняет любые изменения, а поиск продолжает обслуживать запросы. В нём остаются только чистые данные PHP: разобранные метаданные и необработанные байты шрифта. Пул обработчиков может совместно использовать один экземпляр. registerFromBinary() принимает необработанные байты шрифта, которые мост HyperText Markup Language (HTML) @font-face использует для шрифта, загруженного из удалённого источника или из URI данных.
Движок встраивает каждый используемый шрифт и создаёт для него подмножество. Встроенная программа шрифта находится внутри PDF, поэтому документ отображается одинаково в любом средстве просмотра и не зависит от установленных системных шрифтов — ISO 32000-2 §9. Подмножество содержит только те глифы, на которые ссылается документ, что важно для содержимого на китайском, японском и корейском языках (CJK) или для содержимого с большим количеством символов Unicode — ISO 32000-2 §9. FontSubsetter разбирает исходный каталог таблиц, извлекает cmap, разрешает зависимости составных глифов до транзитивного замыкания и перестраивает таблицы head, hhea, maxp, cmap, loca, glyf и hmtx. Он сохраняет исходную нумерацию идентификаторов глифов и заполняет нулями неиспользуемые позиции, поэтому CIDToGIDMap со значением /Identity остаётся допустимым. Если подмножество сэкономило бы менее десяти процентов, он возвращает исходный шрифт без изменений и не выполняет работу, которая не окупается. CffSubsetter выполняет ту же операцию для шрифтов OpenType, которые содержат таблицу контуров Compact Font Format.
Вывод текста проходит три преобразования: кодовая точка Unicode, код символа в потоке содержимого и идентификатор глифа внутри шрифта. Модуль делает этот путь явным. FontInfo::encodeText() — это фасад; FontEncodingStrategyResolver направляет вызовы к стратегии для конкретного шрифта. Встроенный шрифт TrueType или OpenType с cmap Unicode передаётся в TrueTypeCmapStrategy, который формирует двухбайтовый шестнадцатеричный поток Identity-H. Именно такая форма требуется для шрифта Type 0 с CMap Identity-H и потомком CIDFontType2 (ISO 32000-2 §9.7.4; соответствующий дайджест фрагмента retrieval-augmented generation (RAG) был возвращён усечённым из-за лицензионного ограничения, что зафиксировано в _downgraded-claims-o3.md). Любой другой шрифт — стандартные шрифты Base 14, Type 1 PFB и AFM — передаётся в Base14EncodingStrategy, который формирует однобайтовую литеральную строку WinAnsi. Этот поток охватывает полный репертуар WinAnsiEncoding (Windows code page 1252): латиницу с диакритикой, знак евро и распространённую типографскую пунктуацию. Кодовые точки за его пределами отбрасываются из однобайтового потока и обрабатываются покластерной заменой шрифта, когда зарегистрирован покрывающий их шрифт (ISO 32000-2 Annex D.2). Резолвер охватывает всё пространство значений FontInfo; пути, допускающего null, нет. ToUnicodeCMapBuilder строит ресурс /ToUnicode, который позволяет средству чтения восстановить исходный Unicode из шрифта Identity-H. Он применяет жадное объединение bfrange и ограничение блока в 100 записей.
BidiEngine — граничная служба для алгоритма двунаправленного текста Unicode, определённого в Unicode Standard Annex #9 (UAX #9), Unicode 16. Когда поддержка изоляции выключена, он делегирует работу прежнему резолверу, чтобы существующий вызывающий код видел то же поведение. Когда поддержка изоляции включена, он выполняет конвейер с учётом изоляции: стек явных изоляций с максимальной глубиной 125, проходы слабых типов, проходы нейтральных типов, включая разрешение парных скобок, а также проходы неявных уровней и переупорядочения строки. Покрытие глифов CJK для шрифта-кандидата проверяется отдельно: CjkFontValidator выбирает требуемые блоки Unicode по каждому письму и сообщает процент покрытия.
Состав API
Заголовок раздела «Состав API»| Тип | Вид | Ключевые члены | Стабильность | С версии |
|---|---|---|---|---|
FontRegistry | final class | register(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage() | stable | 1.7.0 |
FontInfo | final readonly class | $family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText() | stable | 1.0.0 |
FontSubsetter | final class | subset(string, array<int>, int): string | stable | 1.0.0 |
CffSubsetter | final class | Создание подмножеств контуров OpenType/CFF | stable | 1.0.0 |
FontEncodingStrategyResolver | final class | resolve(FontInfo): FontEncodingStrategy | stable | 2.7.0 |
ToUnicodeCMapBuilder | final class | buildFromRun(), buildFromMap(), encodeUnicodeUtf16Be() | stable | 2.7.0 |
BidiEngine | final class | Разрешение с учётом изоляции по UAX #9 | stable | 3.1.0 |
CjkFontValidator | final class | validateCoverage(), detectScript(), isCjkCodepoint() | stable | 1.0.0 |
FontInfo неизменяемый: сигнатура его конструктора и открытые свойства зафиксированы. Стратегии кодирования — это чистые функции от (FontInfo, UTF-8 text): при одинаковых входных данных каждый вызов возвращает один и тот же EncodedGlyphRun.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.assert($encoded->mode === EncodingMode::TwoByteCid);register() один раз разбирает шрифт и возвращает неизменяемый FontInfo. encodeText() проходит через резолвер и возвращает EncodedGlyphRun с байтовым потоком, строковым операндом PDF, шириной продвижения для каждого глифа и картой соответствия между идентификатором глифа (GID) и Unicode, которую использует CMap /ToUnicode.
Пример кода — промышленная среда
Заголовок раздела «Пример кода — промышленная среда»<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;use NextPDF\Typography\FontRegistry;use Psr\Log\LoggerInterface;
final readonly class FontBootstrap{ public function __construct( private FontRegistry $registry, private LoggerInterface $logger, ) {}
/** * Warm a font set at worker boot, then lock the registry for the * lifetime of the process. * * @param list<string> $fontFiles Absolute paths to font files. */ public function boot(array $fontFiles): void { try { $this->registry->warmup($fontFiles); $this->registry->lock(); } catch (NextPdfException $e) { $this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e; }
$report = $this->registry->memoryUsage(); $this->logger->info('Font cache primed', [ 'fonts' => $report->entryCount, 'bytes' => $report->currentBytes, ]); }}warmup() с последующим lock() — это последовательность загрузки обработчика. После lock() любое изменение вызывает исключение, а поиск продолжает обслуживать запросы. memoryUsage() возвращает MemoryReport, поэтому обработчик может отслеживать кэш шрифтов в пределах своего бюджета.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Когда реестр заблокирован, он отклоняет
register(),registerFromBinary(),addFontDirectory()иwarmup(). Прогревайте и блокируйте реестр при запуске; никогда не регистрируйте шрифты во время обработки запроса. FontSubsetter::subset()возвращает исходные байты без изменений, если экономия составила бы менее десяти процентов или если отсутствует необходимая таблица. Возвращённый шрифт, совпадающий со входными данными, — это документированный сценарий без выигрыша, а не сбой.- Механизм создания подмножеств сохраняет исходную нумерацию идентификаторов глифов и заполняет нулями неиспользуемые глифы. Это сохраняет
CIDToGIDMap /Identityдопустимым; не предполагайте, что идентификаторы глифов перенумерованы в непрерывный диапазон. registerFromBinary()записывает байты во временный файл для разбора и удаляет и файл с расширением, и базовый файлtempnam()в блокеfinally. Недоверенные данные шрифта представляют собой поверхность атаки через разбор; ограничьте их до передачи парсеру (см. примечания по безопасности).BidiEngineпри выключенной поддержке изоляции дословно делегирует работу прежнему резолверу. Управляющие символы изоляции тогда проходят без изменений как граничные нейтральные. Включите поддержку изоляции через политику соответствия для полного поведения по UAX #9.CjkFontValidatorвыбирает кодовые точки с шагом, а не проверяет каждую из них, поэтому его показатель покрытия — статистически достаточная оценка, а не исчерпывающий подсчёт.
Производительность
Заголовок раздела «Производительность»При первом использовании основная стоимость — разбор шрифта; реестр платит её один раз на процесс. После прогрева get() и has() — это поиск в карте за O(1). Стоимость создания подмножества растёт пропорционально количеству глифов, которые использует документ, а не полной таблице глифов шрифта. Именно поэтому создание подмножеств улучшает и размер, и скорость для содержимого CJK: механизм создания подмножеств обрабатывает шрифты с более чем 20,000 глифами с помощью двоичного поиска, предварительно выделенных буферов и пакетных строковых операций. Разрешение составных глифов ограничено 100 итерациями замыкания для защиты от циклических ссылок на компоненты. Парсер cmap формата 12 ограничивает количество групп и записей, чтобы сдерживать использование памяти при враждебных входных данных шрифта. performance_budget в 1500 мс по времени и 64 МБ пикового объёма памяти покрывает типичный прогрев шрифтов и отрисовку документа.
Примечания по безопасности
Заголовок раздела «Примечания по безопасности»С точки зрения безопасности важны две поверхности. Первая — входные данные шрифта. register() и registerFromBinary() разбирают произвольные байты. registerFromBinary() создаёт временный файл. На границе отклоняются обёртки потоков и нулевые байты в путях. Недоверенные данные шрифта должны пройти политику внешних ресурсов, которая ограничивает размер файла и количество глифов до передачи парсеру. Двоичные считыватели механизма создания подмножеств проверяют границы каждого смещения. Парсеры cmap ограничивают количество групп, записей и таблиц (numGroups > 31000 и ограничение записей в 200,000 в формате 12), поэтому специально созданный шрифт не может вызвать неограниченное выделение памяти. Вторая поверхность — восстановление текста: ToUnicodeCMapBuilder проверяет, что каждый код символа находится внутри 16-битного кодового пространства, а каждое значение Unicode является допустимым скаляром. Он отклоняет суррогатные половины, поэтому некорректная карта не может создать повреждённый ресурс извлечения. Считайте любой шрифт или текст, полученный извне, недоверенным.
Соответствие
Заголовок раздела «Соответствие»| Утверждение | Стандарт | Пункт | Подтверждение |
|---|---|---|---|
| Каждый шрифт, используемый документом, встроен, поэтому документ отображается без опоры на системные шрифты. | ISO 32000-2 | §9 | |
| Встроенный шрифт сведён к подмножеству глифов, на которые ссылается документ. | ISO 32000-2 | §9 | |
Встроенное начертание CJK TrueType формируется как шрифт Type 0 с CMap Identity-H и потомком CIDFontType2. | ISO 32000-2 | §9.7.4 | Дайджест RAG усечён из-за лицензионного ограничения; префикс 7a5258772f508e3b, см. _downgraded-claims-o3.md |
Первые два утверждения перефразированы и привязаны к дайджесту. Полный дайджест RAG для третьего пункта не был возвращён (усечение из-за лицензионного ограничения); ADR-013 и обзор разработчика по кодировщику cmap подтверждают его, и он зафиксирован как пониженный. NextPDF не воспроизводит нормативный текст. Соответствие PDF/A-4 и PDF/UA-2 для содержимого CJK зависит от создания подмножеств на стороне записи и связывания /ToUnicode, которые отслеживаются там.
Коммерческий контекст
Заголовок раздела «Коммерческий контекст»Коммерческий пакет функций OpenType и премиальные цепочки запасных шрифтов построены на реестре Core и уровне кодирования. Модуль типографики Core встраивает, создаёт подмножества и кодирует каждый шрифт без лицензии; платный пакет добавляет курируемое разрешение запасных шрифтов. Конверсионная ссылка пропущена намеренно: эта страница — документация, а не путь к продаже.
См. также
Заголовок раздела «См. также»- Шрифт: TrueType, OpenType и реестр CID — типы значений шрифта, встраивание и использование запасных шрифтов.
- Текст: формирование, разбиение, BiDi — обработка прогонов и формирование, которое потребляет закодированные глифы.
- Контракты / Типографика — контракты
FontRegistryInterfaceи препроцессора текста. - Движок отрисовки HTML — мост
@font-face, который вызываетregisterFromBinary().