Отсутствующий глиф в PDF — не ошибка. Производитель запрашивает символ, который выбранный шрифт не может отобразить, шрифт возвращает индекс глифа ноль, и выходящий файл структурно корректен, открывается везде и показывает пустой ящик там, где должно быть имя или сумма. Никто в порождающем конвейере не узнаёт. Получатель узнаёт. HotPDF замыкает этот цикл свойством TrackUnresolvedGlyphs: включите его, и путь отрисовки текста записывает каждую кодовую точку, чей поиск глифа даёт индекс ноль, возбуждая OnUnresolvedGlyph один раз на уникальную находку с кодовой точкой, шрифтом, на котором случился провал, скриптом, к которому она принадлежит, и подсказкой шрифтов, которые её покрыли бы
Обнаружение — половина ответа. Вторая половина — SetFontFallbackChain, регистрирующий упорядоченный список шрифтов на скрипт, так что обычные случаи разрешаются сами и лишь настоящие провалы доходят до вашего обработчика. Вместе они превращают класс дефекта, о котором раньше сообщали клиенты, в проверку времени сборки
Почему отсутствующий глиф не поднимает ничего?
Потому что ISO 32000 не возлагает на производителя обязанности проверять покрытие, а индекс глифа ноль — легитимный глиф. Это .notdef, чей контур выбирает дизайнер шрифта: обычно пустой или полый прямоугольник, иногда ничего вовсе. Просмотрщик, рисующий его, ведёт себя правильно. Извлечение текста может даже вернуть правильные символы, ведь отображение /ToUnicode записывается из исходного текста, а не из контуров, поэтому автоматическая проверка туда-обратно счастливо пропустит документ, чей видимый текст дыряв
Практическое следствие: покрытие должно проверяться в момент отрисовки, когда библиотека ещё знает, какая кодовая точка запрашивалась и какой глиф шрифт действительно предложил. Потом информация исчезает
Детектор должен следить за состоянием сабсета, а не за контекстом устройства
Вот где ошиблась первая реализация, и причину стоит понять, ведь она относится к любой проверке покрытия, прикрученной к текстовому конвейеру. У HotPDF два текстовых пути. Один выдаёт через зарегистрированный Unicode-шрифт TrueType с картой символов в памяти, построенной при регистрации. Другой — унаследованный путь GDI, создающий свежий контекст устройства и дескриптор шрифта на каждый прогон символов
Судить покрытие по пути GDI безнадёжно. Его отображение — не то, что в итоге попадает в выдаваемый поток содержимого, и они не синхронизированы, поэтому детектор, читающий результаты GDI, объявляет весь печатный диапазон ASCII неразрешённым. Авторитетный ответ живёт в зарегистрированном шрифте: карта символов, которую разбирает RegisterUnicodeTTF, опрашивается через GetUnicodeGlyphForCodepoint. Детектор поэтому ставится за заслон состояния готовности сабсета, а не за любое условие GDI, и на документах, никогда не регистрировавших Unicode-шрифт, просто не работает, что верно: те документы и так ограничены стандартными кодировками
Рядом сидит вторая ловушка. Семейное имя шрифта GDI и имя PostScript, извлечённое из двоичного файла шрифта при регистрации, — разные строки, и не такие, которые можно нормализовать: семейство с именем Arial Unicode MS несёт имя PostScript ArialMT. Любой заслон в стиле «выбранный сейчас шрифт — тот ли, что мы зарегистрировали», сравнивающий по имени, — мёртвый код, который никогда не сработает. Ставьте заслон на состояние, никогда на имена шрифтов
Не тестируйте детектор глифов эмодзи
Очевидный тестовый случай — улыбающееся лицо, и оно убедит вас, что детектор сломан. Обычные кодовые точки эмодзи в астральных плоскостях разрешаются через путь синтеза частного использования, отображающий их в индекс глифа напрямую, поэтому они никогда не доходят до общей ветви покрытия. Детектор ведёт себя правильно, а тест измеряет не тот путь
Используйте вместо этого неназначенную кодовую точку. U+0378 навсегда не выделен в Unicode, поэтому ни один шрифт не может легитимно его отобразить, и он упражняет ровно ту ветвь, которую хотите проверить. Это различие между «возможность сломана» и «тест взял вход, обходящий возможность», стоит реальных часов, а неназначенные кодовые точки — дешёвый способ его избежать
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// Срабатывает раз на уникальную кодовую точку, а не раз на вхождение
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// Подключение к порождающему заданию
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// Валим задание вместо отгрузки страницы с ящиками
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Цепочки замены задаются на скрипт, а не на шрифт
Причина, по которой замена ограничивается скриптом, а не исходным шрифтом, в том, что провалы покрытия кучкуются по системе письма. Латинский текстовый шрифт лишён деванагари, тайского, ханьского и эмодзи одновременно, и замена для каждого — другой шрифт. Объявление одной цепочки на скрипт поэтому описывает реальное развёртывание: один латинский шрифт для основного текста, один шрифт CJK, один шрифт эмодзи, один на все случаи
// THPDFFontScript покрывает hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji и hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
Замена и обнаружение дополняют, а не заменяют друг друга. Цепочки обрабатывают покрытие, которое вы предвидели; детектор сообщает о покрытии, которое вы не предвидели, а в системе, обрабатывающей произвольные клиентские данные, это интересная половина. Заметьте: подмена шрифта меняет метрики, поэтому абзац, ушедший в замену, может переверстаться; если макет важен, поведение замкнутости и сабсеттинга подменённого шрифта стоит изучить в статье о замкнутости сабсета шрифта, а скрипты, требующие переупорядочения или сцепления, обрабатывает стадия формовки, описанная в формовке текста сложных скриптов
Как дооснастить поведение, не рискуя существующим путём
Тот же выпуск добавил резерв на основе унаследованной таблицы kern для межпарных интервалов, и способ, которым это было ограничено, — образец, достойный копирования. Вместо добавления новой точки решения в логику кернинга замена подвешена к ветви раннего выхода, уже существовавшей для шрифтов без таблицы GPOS. Современный шрифт с GPOS до неё никогда не доходит, поэтому его поведение неизменно по построению, а не по тестированию. Пути, не регистрирующие Unicode-шрифт, дают два нулевых смещения, поэтому они тоже неизменны
Таков общий образец низкорискового дооснащения в зрелой библиотеке отрисовки: найдите ветвь, которая сейчас не производит ничего, и поместите новое поведение туда. Это превращает «мы верим, что ничего не регрессировало» в «это не могло ничего регрессировать», что куда лучше говорить о текстовом движке, через который проходят чужие счета
Сделайте это заслоном, а не журналом
Находки покрытия полезны, только если на них что-то падает. В службе порождения документов продуктивная схема — держать слежение включённым в ночном регрессионном задании на корпусе настоящих имён клиентов, адресов и описаний продуктов и валить задание на любой находке. Поскольку событие срабатывает раз на уникальную кодовую точку, а не раз на вхождение, вывод остаётся достаточно малым для чтения, даже когда отсутствует целый скрипт
В промышленной эксплуатации тот же обработчик лучше использовать как телеметрию: записывайте кодовую точку и шрифт, продолжайте обслуживать документ, а агрегат пусть говорит, какой скрипт добавить в шрифтовой набор развёртывания следующим. Поведение отрисовки для встроенных и подменённых шрифтов далее рассмотрено в материале об отрисовке глифов встроенных шрифтов, а полный список свойств, включая TrackUnresolvedGlyphs, задокументирован на странице продукта HotPDF Delphi PDF component