HotPDF выполняет рендеринг встроенных шрифтов PDF в Delphi без установки чего-либо на компьютер: конвейер рендеринга встроенных глифов в HPDFGlyphRender.pas анализирует программы шрифтов, хранящиеся внутри самого PDF (контуры TrueType glyf из FontFile2, чарстринги CFF Type 2 из FontFile3 и потоки содержимого глифов Type 3), и воспроизводит их как заполненные векторные пути GDI. Эта статья посвящена детальному разбору точности воспроизведения шрифтов при рендеринге страниц PDF в TBitmap с помощью HotPDF: та статья описывает рендерер в целом, а эта — то, как текст на этих страницах приобретает свою точную форму
Почему PDF отображается с квадратами вместо текста?
Квадраты, пробелы или слегка искаженные символы при отображении PDF почти всегда означают, что рендерер запросил шрифт у операционной системы вместо использования шрифта, встроенного в сам файл. Проблема проявляется одинаково: документ выглядит идеально на компьютере, где он был создан, но когда клиент открывает его на чистом сервере или на рабочем столе с ограниченными правами, в японском счете отображаются квадраты ("тофу"), либо замененный похожий шрифт сдвигает границы строк. Этих шрифтов никогда не было на целевом компьютере — они были только внутри PDF, и рендерер, ограничивающийся подстановкой системных шрифтов, не может их прочитать. Подмножества шрифтов (subset) усугубляют ситуацию: подмножество может содержать сорок глифов с кодами символов, назначенными специально для этого конкретного файла, и ни один установленный шрифт не разделяет эти назначения
Стандарт 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-сплайны против кубических чарстрингов и произвольных страничных операторов), поэтому для точного рендеринга требуются отдельные интерпретаторы для каждого из них, а также слой кодирования, преобразующий коды символов в правильные индексы глифов перед началом работы с контуром
Как HotPDF преобразует контуры TrueType glyf в пути GDI?
Класс THPDFEmbeddedTTF в HPDFGlyphRender.pas считывает таблицу loca для поиска записи каждого глифа, обходит контуры glyf точка за точкой и генерирует путь GDI. Две особенности TrueType требуют явной обработки. Во-первых, идущие подряд точки вне кривой подразумевают точку на кривой в их средней точке, а контур, все точки которого лежат вне кривой, начинается в средней точке между его последней и первой точками. Если пропустить любое из этих правил, округлые глифы получат плоские грани или исказятся. Во-вторых, кривые TrueType являются квадратичными кривыми Безье, тогда как функция GDI PolyBezierTo принимает кубические кривые, поэтому для каждого квадратичного сегмента степень повышается точно, а не преобразуется в ломаную линию
uses HPDFGlyphRender;
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (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;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
Повышение степени происходит без потерь: кубическая кривая в точности повторяет исходную, поэтому отрисованный контур соответствует тому, что стандартная программа просмотра рисует из той же таблицы при любом масштабе. Остальная работа заключается в позиционировании. Каждый глиф создается в единицах шрифта (обычно это сетка 1000 или 2048 единиц на em), и перед заполнением пути рендерер объединяет матрицу масштабирования, матрицу текста и текущую матрицу преобразования в одно преобразование координат глифа в экранные координаты. Порядок здесь имеет критическое значение: если скомпоновать эти три матрицы в обратном порядке, каждый глиф сожмется к началу координат — страница будет выглядеть неверно из-за ошибки в одной строке матричной алгебры
Как интерпретатор чарстрингов Type 2 обрабатывает шрифты CFF
Класс THPDFEmbeddedCFF предоставляет программам из FontFile3 полноценный интерпретатор чарстрингов Type 2: он разбирает структуры CFF INDEX, Top DICT и Private DICT, затем выполняет каждый чарстринг и передает сегменты пути напрямую в GDI. Обертка OpenType (контейнер OTTO) сначала удаляется для доступа к чистой таблице CFF; потоки CIDFontType0C и Type1C используются напрямую. Чарстринги представляют собой компактный стековый язык, и три его особенности определяют, останется ли интерпретатор синхронизированным с байтовым потоком. Необязательный префикс ширины означает, что первый оператор очистки стека может содержать один дополнительный первый операнд. Оператор hintmask подразумевает vstemhm, когда операнды все еще находятся в стеке, а количество пропускаемых байтов маски зависит от накопленного количества стемов — ошибитесь в подсчете один раз, и все последующие коды операций будут прочитаны неверно. А вызовы подпрограмм добавляют смещение (bias) к своему индексу (107, 1131 или 32768 в зависимости от количества подпрограмм) перед поиском, поэтому вызов без учета смещения приведет к выполнению совершенно другой подпрограммы
CFF с ключами CID добавляет уровень косвенной адресации, на котором спотыкаются простые реализации: код символа выбирает CID, но индексом чарстринга является 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, дает два обязательства, которые контролирует рендерер: отрисовка ограничивается объявленной рамкой (bounding box), и, согласно ISO 32000-1 §9.6.5.2, глиф игнорирует собственные операторы цвета и рисуется текущим цветом заливки вызывающей стороны, поэтому операторы rg, g, k и их обводящие аналоги внутри процедуры игнорируются на время отрисовки этого глифа. Если пропустить правило цвета, штрихкод, наложенный страницей синим цветом, выйдет черным; если пропустить ограничение, некорректный глиф выйдет за пределы своей ячейки
Как коды символов преобразуются в идентификаторы глифов (glyph ID)
Интерпретаторы контуров — это лишь половина дела, поскольку байты в текстовой строке 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 as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
Когда требуется поиск в таблице TrueType cmap, 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 (чарстринги с шифрованием 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
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
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 Component для Delphi и C++Builder — нативной библиотеки VCL, охватывающей создание, редактирование PDF, извлечение текста и рендеринг страниц без зависимостей от внешних DLL