HotPDF отрисовывает встроенные шрифты PDF в Delphi, ничего не устанавливая на машине: конвейер отрисовки встроенных глифов в HPDFGlyphRender.pas разбирает программы шрифтов, хранящиеся внутри самого PDF, — контуры glyf TrueType из FontFile2, charstring CFF Type 2 из FontFile3 и потоки содержимого глифов Type 3 — и воспроизводит их как залитые векторные пути GDI. Эта статья — углублённый разбор точности шрифтов, стоящий за материалом отрисовка страниц PDF в TBitmap с HotPDF: там речь о рендерере в целом, здесь — о том, как текст на этих страницах получает свои точные формы
Почему PDF отрисовывается квадратиками вместо текста?
Квадратики, пробелы или едва заметно неверные символы в отрисованном выводе PDF почти всегда означают, что рендерер запросил шрифт у операционной системы вместо того, чтобы использовать встроенный в файл. Жалоба всякий раз приходит одинаково: документ выглядит безупречно на машине, которая его произвела, потом заказчик открывает его на чистом сервере или на заблокированном рабочем месте, и японский счёт показывает квадратики тофу, либо подставленное похожее начертание сдвигает каждый перенос строки. Шрифтов на той машине никогда и не было — они были только внутри PDF, — а рендерер, который останавливается на подстановке системного шрифта, их не видит. Урезанные шрифты усугубляют дело: набор может нести сорок глифов под кодами символов, назначенными приватно для этого одного файла, и такого назначения нет ни в одном установленном шрифте
ISO 32000-1 §9.9 определяет три носителя встроенной программы шрифта в дескрипторе шрифта: FontFile держит исходную программу Type 1, FontFile2 — программу TrueType, а FontFile3 — голый CFF (Type1C или CIDFontType0C) либо обёртку OpenType. Четвёртая разновидность, шрифт Type 3 из ISO 32000-1 §9.6.5, не встраивает вообще ничего двоичного — каждый глиф представляет собой небольшой поток содержимого PDF, исполняемый на месте. Три носителя различаются математикой контуров (квадратичные B-сплайны против кубических charstring против произвольных операторов страницы), поэтому точному рендереру нужен отдельный интерпретатор для каждого плюс слой кодировки, превращающий коды символов в правильный индекс глифа ещё до того, как будет затронут хоть один контур
Как HotPDF превращает контуры glyf TrueType в пути GDI?
THPDFEmbeddedTTF в HPDFGlyphRender.pas читает таблицу loca, чтобы найти запись каждого глифа, обходит контуры glyf точка за точкой и выдаёт путь GDI. Два соглашения TrueType требуют явной обработки. Во-первых, подряд идущие точки вне кривой подразумевают точку на кривой в их середине, а контур, все точки которого лежат вне кривой, начинается в середине между своими последней и первой точками — пропустите любое из этих правил, и на округлых глифах появятся плоские грани или они схлопнутся. Во-вторых, кривые TrueType — квадратичные кривые Безье, тогда как PolyBezierTo в GDI принимает кубические, поэтому каждый квадратичный сегмент точно повышается в степени, а не разбивается на отрезки прямых
// Точное повышение степени: квадратичная (P0, Q, P2) -> кубическая (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// затем PolyBezierTo с C1, C2, P2 — геометрически идентичная кривая
Повышение степени происходит без потерь: кубическая кривая проходит по той же самой траектории, поэтому отрисованный контур совпадает с тем, что рисует из той же таблицы соответствующая спецификации программа просмотра, при любом увеличении. Остаётся размещение. Каждый глиф начерчен в единицах шрифта (обычно сетка в 1000 или 2048 единиц на em), и рендерер собирает матрицу масштаба, матрицу текста и текущую матрицу преобразования в одно преобразование из глифа в устройство до заливки пути. Порядок здесь важнее, чем кажется: соберите те же три матрицы в обратном порядке — и каждый глиф схлопнется к началу координат, дав неверно выглядящую страницу, чья настоящая ошибка — одна строка матричной алгебры
Как интерпретатор charstring Type 2 обрабатывает шрифты CFF
THPDFEmbeddedCFF даёт программам FontFile3 настоящий интерпретатор charstring Type 2: он разбирает структуры INDEX формата CFF, Top DICT и Private DICT, затем исполняет каждый charstring и выдаёт сегменты пути прямо в GDI. Обёртка OpenType (контейнер OTTO) сначала снимается, чтобы добраться до голой таблицы CFF; голые потоки CIDFontType0C и Type1C потребляются напрямую. Charstring — это компактный стековый язык, и три его соглашения решают, останется ли интерпретатор в синхронизации с потоком байтов. Необязательный префикс ширины означает, что первый очищающий стек оператор может нести один лишний ведущий операнд. Оператор hintmask подразумевает vstemhm, когда на стеке ещё лежат операнды, а число пропускаемых байтов маски зависит от накопленного количества штрихов — ошибитесь в этом количестве один раз, и каждый последующий опкод будет прочитан неверно. А вызовы подпрограмм добавляют к своему индексу смещение (107, 1131 или 32768 в зависимости от числа подпрограмм) до поиска, поэтому вызов без смещения попадает совсем в другую подпрограмму
CFF с ключом CID добавляет одну косвенность, на которой спотыкаются наивные реализации: код символа выбирает CID, но индекс charstring — это GID, а charset шрифта отображает GID в CID, поэтому рендерер строит обратное отображение CID в GID до отрисовки и выбирает Private DICT для каждого глифа через FDSelect для шрифтов, которые несут их несколько. Программы Type1C с ключом-именем, обычный носитель для простых шрифтов Type 1, вместо этого разрешают однобайтовые коды через встроенную кодировку самой программы CFF либо через машинерию кодировок уровня PDF, описанную далее. Одна честная оговорка: интерпретатор читает операторы хинтинга, чтобы держать поток синхронизированным, но сам хинтинг не исполняет — эта граница обсуждается в конце
Что такое шрифт Type 3 и как он рисуется?
Глиф Type 3 вообще не является контуром — ISO 32000-1 §9.6.5 определяет его как поток содержимого, поэтому HotPDF отрисовывает его, сохраняя состояние графики, накладывая матрицу шрифта, кегль и матрицу текста на CTM и исполняя процедуру глифа тем же интерпретатором операторов, который рисует страницы, с собственным /Resources шрифта в области видимости. Две детали спецификации важны для корректности. Значения /Widths у Type 3 выражены в пространстве глифа, а не в пространстве текста с шагом 1/1000, которое использует любой другой тип шрифта, поэтому смещения обязаны проходить через /FontMatrix — иначе штрихкодовые шрифты с матрицей 0.01 шагают неверно на порядок величины. А процедура глифа, открывающаяся оператором d1, даёт два обещания, которые рендерер соблюдает: отрисовка обрезается объявленным ограничивающим прямоугольником, и, согласно ISO 32000-1 §9.6.5.2, глиф игнорирует собственные операторы цвета и рисует текущим цветом заливки вызывающей стороны, поэтому rg, g, k и их обводочные двойники внутри процедуры подавляются на время работы этого глифа. Пропустите правило цвета — и штрихкодовый шрифт d1, проштампованный страницей синим, выйдет чёрным; пропустите обрезку — и некорректный глиф нарисуется за пределами своей ячейки
Как коды символов становятся идентификаторами глифов
Интерпретаторы контуров — лишь половина работы, потому что байты в текстовой строке PDF — это коды символов, а не индексы глифов, и ISO 32000-1 посвящает отображению два подраздела. Для простых шрифтов §9.6.6 предписывает строгий приоритет: массив /Differences перекрывает базовую кодировку (WinAnsiEncoding, MacRomanEncoding или StandardEncoding), которая перекрывает собственную карту программы шрифта. HotPDF сводит эту цепочку в таблицу «код в GID» на 256 записей, переводя имена глифов в индексы глифов тремя путями: точным совпадением по charset внутри программы CFF, числовыми именами gNN/glyphNN, взятыми как буквальные индексы, и переводом имени в Unicode по Adobe Glyph List с последующим поиском в cmap для программ TrueType. Для составных шрифтов §9.7 отдаёт главенство CIDToGIDMap: обычный случай — /Identity, но запись может быть потоком пар big-endian, индексируемых по CID, — и собственный вывод Unicode в HotPDF использует именно эту потоковую форму для компактных урезанных наборов, так что путь с потоком вовсе не экзотический угол
// /CIDToGIDMap как поток: пары Word в порядке big-endian, индексируемые по CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // выход за диапазон отображается в .notdef
Когда нужен поиск в cmap TrueType, HotPDF проходит цепочку запасных вариантов, а не доверяет одной подтаблице: первыми идут подтаблицы Windows Unicode (формат 4, затем формат 12 для дополнительных плоскостей), затем символьная подтаблица (3,0) со своим соглашением о частной области F000, отражённым вниз до младшего байта, — из-за чего символьный шрифт вроде Wingdings отвечает на обычные коды ASCII, — затем устаревшие форматы 6 и 0. Подтаблицы формата 2 намеренно не интерпретируются: они отображают устаревшие многобайтовые кодовые страницы вроде Shift-JIS и Big5, а не Unicode, и современные шрифты CJK всё равно неизменно несут подтаблицу формата 4 или 12. Любой код, не переживший ни одного из этих путей, откатывается на отрисовку через GDI для этого одного глифа, поэтому один неотображаемый символ ухудшает один глиф, а не весь текстовый фрагмент
Чего встроенный путь не делает
Границы стоит назвать прямо. Хинтинг не исполняется — контуры заливаются как начерчены, что неотличимо от хинтованного вывода при 150 DPI и выше, но может отличаться на пиксель от хинтующего растеризатора на очень мелких кеглях. Исходные программы Type 1 в FontFile (charstring, зашифрованные eexec) не интерпретируются, а оси вариативных шрифтов OpenType не применяются; оба случая, как и повреждённая программа шрифта или таблица glyf без пригодной cmap, откатываются на отрисовку системным шрифтом, а не проваливают страницу. Тот же подход «точность прежде всего» распространяется и на другие части рендерера — осевые и радиальные шейдинг-паттерны получают то обращение, которого градиенты заслуживают, — а у стороны генерации есть своя история про тонкости шрифтов в статье как EndDoc упорядочивает урезанные наборы шрифтов
Использование конвейера не требует вообще никакого кода, специфичного для шрифтов, — каждый описанный выше механизм включается автоматически внутри вызова отрисовки страницы
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Встроенные шрифты TrueType, CFF и Type 3 отрисовываются из
// самого файла — на этой машине ничего устанавливать не нужно
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Практическое следствие — ровно то, что заботит вашу почту поддержки: PDF, несущий свои шрифты, отрисовывается именно этими шрифтами и на сервере сборки, и в контейнере Windows, и на рабочем месте заказчика, никогда не видевшем этой гарнитуры. Конвейер отрисовки встроенных глифов поставляется в составе HotPDF Delphi Component для Delphi и C++Builder — нативной библиотеки VCL, покрывающей создание, правку, извлечение текста и отрисовку страниц PDF без внешних зависимостей от DLL