Техническая статья

Вариативные шрифты в PDF: статическое инстансирование в Delphi

В PDF нет понятия вариативного шрифта. Шрифт, встроенный в файл PDF, — это фиксированный набор контуров с фиксированными метриками, поэтому вариативный шрифт перед помещением в документ должен быть сведён к одному статическому экземпляру. HotPDF выполняет это инстансирование внутренне: вы изучаете оси вариативного шрифта, выбираете координаты — например, насыщенность 620 или ширину 87,5, — а библиотека запекает эти значения в полную, самодостаточную программу шрифта, которую отрисует любой соответствующий стандарту просмотрщик PDF

Значимость этого — вопрос практики, а не теории. Шрифтовые студии всё чаще поставляют один вариативный файл вместо десятка статических начертаний, а дизайнерские команды выбирают значения, которых нет ни в одном именованном экземпляре. Без инстансирования генератор отчётов либо откатывается к экземпляру по умолчанию, отбрасывая тем самым дизайнерское решение, либо встраивает весь вариативный шрифт и надеется, что просмотрщик учтёт координаты осей, о которых он в принципе не может знать, — а этого не обязан делать ни один просмотрщик

Что на самом деле приходится пересобирать при инстансировании?

Вариативный шрифт OpenType хранит по одному контуру по умолчанию на глиф плюс набор дельт, индексированных по позиции в пространстве дизайна. Применение координаты оси — не вопрос записи числа в заголовок; это означает обход таблицы gvar, интерполяцию дельт для запрошенной позиции, перемещение точек и последующий пересчёт всего, что было выведено из этих точек. HotPDF пересобирает контуры глифов, длинную таблицу loca, полные горизонтальные и вертикальные метрики, общий ограничивающий прямоугольник шрифта и корректировку контрольной суммы sfnt

Не менее важно то, что удаляется. Статический экземпляр не должен сохранять fvar, avar, gvar, HVAR, VVAR, MVAR, STAT или cvar, а устаревшая DSIG тоже должна быть удалена, поскольку подписанных байтов больше не существует. Если оставить что-либо из этого, получится шрифт, который заявляет себя вариативным, но несёт контуры, уже перемещённые, — и просмотрщики, действительно применяющие вариации, применят их повторно

Фантомные точки и ловушка двойного применения

Самое тонкое правило во всём процессе касается метрик. В gvar количество точек глифа охватывает точки контура — или точки компонентов для составного глифа — плюс четыре фантомные точки, кодирующие левый боковой интервал, ширину продвижения и их вертикальные эквиваленты. Эти фантомные точки сами подвержены дельтам

Поэтому, когда у шрифта есть таблица gvar, HotPDF выводит горизонтальные и вертикальные метрики из интерполированных фантомных точек и дополнительно не применяет HVAR или VVAR. Применение обоих — классическая ошибка: одна и та же вариация применяется дважды, и каждая ширина продвижения выходит чуть шире положенного, что проявляется как текст, постепенно сдвигающийся вправо на выключенной по формату строке. Только когда у шрифта нет gvar, библиотека запекает хранилище вариаций метрик прямо в hmtx или vmtx

Ещё две детали удерживают геометрию честной. Фантомные точки никогда не участвуют в интерполяции контура, поэтому точки, явно не перечисленные для простого глифа, выводятся с помощью IUP по каждому контуру, исключая фантомные точки. А составные глифы получают свои дельты, применённые к смещениям компонентов, использующим параметры XY, после чего границы дочерних элементов пересчитываются рекурсивно. Эта рекурсия ограничена по глубине и проверяется на циклы, поскольку враждебный или просто испорченный граф компонентов иначе может рекурсировать бесконечно

Изучение пространства дизайна перед выбором

Первый вызов в любом рабочем процессе инстансирования — это InspectVariableFont, который сообщает оси и именованные экземпляры, определённые шрифтовой студией. Записи осей несут четырёхбайтовый тег, минимальное, значение по умолчанию и максимальное значения, флаги и идентификатор имени; именованные экземпляры несут идентификатор имени подсемейства, флаги, необязательный идентификатор имени PostScript и по одной координате на ось:

var
  Pdf: THotPDF;
  Axes: THPDFVariableFontAxisArray;
  Instances: THPDFVariableFontNamedInstanceArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
    begin
      for I := 0 to High(Axes) do
        Writeln(Format('%s  min=%.1f default=%.1f max=%.1f',
          [string(Axes[I].Tag), Axes[I].MinimumValue,
           Axes[I].DefaultValue, Axes[I].MaximumValue]));
      Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
    end
    else
      Writeln('not a variable font - embed it as an ordinary TrueType face');
  finally
    Pdf.Free;
  end;
end;

Отчёт о диапазоне оси важен, потому что значения осей ограничиваются диапазоном, объявленным шрифтом, а не диапазоном, который предлагает ваш интерфейс. Ползунок, позволяющий пользователю запросить насыщенность 1000 для шрифта, чья ось wght останавливается на 900, должен корректироваться на уровне интерфейса, а не молча на уровне шрифта — иначе напечатанный результат разойдётся с предпросмотром

Выбор координат и формирование документа

Выбор осей имеет состояние и применяется к шрифтам, зарегистрированным после этого. SetVariableFontAxis принимает четырёхбайтовый печатаемый ASCII-тег и конечное значение и отклоняет всё остальное исключением, а не молчаливым игнорированием. ClearVariableFontAxes сбрасывает выбор, а GetVariableFontAxisSelections сообщает, что сейчас ожидает применения, — это стоит логировать в движках отчётов, где разные пути кода могли затронуть один и тот же объект документа. Само семейство шрифтов выбирается по имени через SetFont, точно так же, как и любое другое встроенное начертание TrueType:

begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;

    Pdf.SetVariableFontAxis('wght', 620);   // полужирный, но не именованный экземпляр
    Pdf.SetVariableFontAxis('wdth', 87.5);  // слегка уплотнённый
    Pdf.CurrentPage.SetFont('Inter', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');

    Pdf.ClearVariableFontAxes;              // назад к экземпляру по умолчанию
    Pdf.CurrentPage.SetFont('Inter', [], 10);
    Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Событие OnVariableFontInstance срабатывает при создании каждого экземпляра и сообщает использованные значения осей — это самый дешёвый способ доказать в журнале, что реально содержится в конкретном PDF. Поскольку каждый отдельный набор координат даёт отдельную программу шрифта, относитесь к выбору осей как к части ключа кеша шрифтов; механика кеширования описана в статье постоянный кеш подмножеств шрифтов

Как инстансирование взаимодействует с подмножествами и шейпингом

Инстансирование выполняется до создания подмножества, и этот порядок правильный. Инстансированный шрифт — это обычное статическое начертание TrueType, поэтому обычный сборщик подмножества обрабатывает его как любое другое: вычисляет замыкание глифов, оставляет глифы, реально используемые документом, и отбрасывает остальные. Взаимодействие, о котором стоит помнить: два разных выбора осей одного семейства — это две разные программы шрифта, поэтому документ, смешивающий насыщенность 400 и 620, встраивает два подмножества, а не одно общее начертание с двумя экземплярами

Шейпинг в принципе не затрагивается, но на практике стоит проверить. Функции разметки живут в GSUB и GPOS, которые инстансирование сохраняет, поэтому лигатуры и стилистические альтернативы продолжают работать, как описано в статье стилистические альтернативы OpenType GSUB. Что меняется — так это позиционирование: уплотнённый экземпляр имеет более узкие продвижения, чем экземпляр по умолчанию, поэтому любая разметка, измерившая текст до инстансирования, измерила неверные ширины. Измеряйте с тем же выбором осей, с которым будете рендерить, и расхождение исчезает

Последнее замечание из реализации, полезное для тех, кто расширяет этот путь. Шрифты без вертикальных метрик всё равно вычисляют аргументы динамических массивов на месте вызова в Delphi, поэтому массивы времени разбора всегда выделяются, а не полагаются на проверку HasVerticalMetrics, которая должна была бы обойти пустой индекс. Это как раз та языковая деталь, что превращает, казалось бы, защищённую ветку в нарушение доступа именно на тех шрифтах, которые вы не тестировали

Поддержка вариативных шрифтов встроена в тот же шрифтовой конвейер, что и встраивание, создание подмножеств и замыкание глифов, подробнее описанное в статье замыкание подмножества шрифта и шейпированные глифы. Полный набор возможностей типографики для Delphi и C++Builder перечислен на странице компонента HotPDF для Delphi PDF