Технічна стаття

Автоматичний резервний шрифт PDFlibPas для CJK і емодзі

PDFlibPas вирішує проблему символів, які вибраний шрифт не може намалювати, шукаючи їх у ланцюжку резервних встановлених гарнітур, кластер за кластером, зберігаючи при цьому шейпінг і порядок двонапрямлених послідовностей. Ви вмикаєте цю функцію через SetAutomaticFontFallback, розширюєте ланцюжок через AddFontFallback, і у файл вбудовуються лише ті резервні шрифти, які справді використані для виведення

Це проблема, з якою рано чи пізно стикається кожен генератор документів — коли ім'я клієнта надходить у писемності, яку шрифт шаблону ніколи не передбачав. Збій відбувається тихо, і саме це робить його дорогим

Чому непідтримуваний текст зникає, а не викликає помилку?

Тому що в PDF немає поняття шрифту, який не може намалювати символ. Простий шрифт зіставляє байтові коди з назвами гліфів через кодування; складений шрифт зіставляє коди через CMap з індексами гліфів. Якщо запитати гліф, якого немає в гарнітурі, ви отримаєте індекс гліфа нуль, .notdef, який більшість гарнітур малюють як нічого або як порожній прямокутник. Файл структурно коректний, оператор тексту сформований правильно, і сторінка рендериться. Просто там, де мало бути ім'я, залишається порожньо

Ніщо в ISO 32000-1 не вимагає, щоб виробник документа це помітив. Генератор, який записує текст без перевірки покриття, створює технічно відповідний PDF, що тихо втратив частину вмісту, і ця втрата випливає на екрані клієнта через кілька тижнів. Саме тому функція резервного шрифту та звіт про відсутні гліфи постачаються разом: вирішення того, що можна вирішити, — лише половина роботи, а звіт про те, що вирішити не вдалося, — друга половина

Резервний шрифт застосовується для кластера, а не для кодової точки

Гранулярність — це та деталь, яка відрізняє робочу реалізацію від правдоподібної на вигляд. Текст — це не послідовність незалежних символів. Склад деванагарі, емодзі з модифікатором тону шкіри, базова літера з діакритичними знаками — кожен із них є одним кластером, який має відображатися одним шрифтом, оскільки рішення щодо шейпінгу всередині нього залежать від таблиць саме цієї гарнітури

PDFlibPas обробляє саме кластери, тож кластер, який покриває резервна гарнітура, повністю малюється цією гарнітурою. Розділення кластера навпіл, коли одна частина малюється основним шрифтом, а інша — резервним, дало б технічно наявний, але візуально зламаний результат, і це, мабуть, гірше за порожнє місце, з якого все починалося. Порядок послідовності теж зберігається, тож резервний шрифт усередині послідовності справа наліво не змінює порядок навколишнього тексту; той самий механізм лежить в основі вертикального макета, описаного в статті про вертикальне письмо для японської та китайської мов

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutomaticFontFallback(1);

    // Порядок пошуку: перший збіг перемагає, тому найширші гарнітури розмістіть останніми
    Lib.AddFontFallback('Microsoft YaHei');   // спрощена китайська
    Lib.AddFontFallback('Meiryo');            // японська
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = вбудувати гарнітуру
    Lib.SetTextSize(11);
    Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
    Lib.DrawText(72, 700, 'Delivery status: on time');

    Lib.SaveToFile('invoice.pdf');
  finally
    Lib.Free;
  end;
end;

Впорядковуйте ланцюжок свідомо. Розв'язання бере першу гарнітуру, яка покриває кластер, тож широкий пан-юнікодний шрифт, поставлений першим, перехопить майже все, і ретельно підібрані специфічні для писемності гарнітури просто ніколи не будуть задіяні. Спершу розмістіть специфічні гарнітури, а універсальну — останньою

Звіт чи переривання: який тип збою вам потрібен?

SetMissingGlyphPolicy приймає PDF_MISSING_GLYPH_REPORT — сумісне значення за замовчуванням — або PDF_MISSING_GLYPH_ABORT. За політики звітування текстова операція виконується далі, невирішені кодові точки відкидаються, як і раніше, але кожна з них фіксується. За політики переривання текстова операція відхиляється ще до запису будь-якого вмісту, а LastErrorCode встановлюється в 521

Обирайте залежно від призначення документа. Пакет внутрішніх звітів має продовжувати рендеринг і фіксувати прогалини в журналі, адже трохи неповний звіт сьогодні кращий за відсутність звіту взагалі. Юридично зобов'язальний договір, рахунок-фактура чи будь-що з іменем у тексті мають перериватися, бо тихо відкинутий символ в імені сторони — це дефект, який краще виявити у власному процесі, ніж під час спору. Політика переривання завершується невдачею до запису, тож жодного напівсформованого потоку вмісту не залишається

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... будуємо документ ...

    if Lib.DrawText(72, 660, CustomerName) <> 1 then
      if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
      begin
        Report := Lib.GetMissingGlyphReportJSON;
        // {"valid":false,"policy":1,"eventCount":1,"events":[
        //   {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
        //    "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
        //    "fontType":"TrueType","operation":"DrawText"}]}
        EscalateToOperator(Report);
      end;
  finally
    Lib.Free;
  end;
end;

Звіт свідомо зроблено машинозчитуваним і обмеженим. Кожна подія містить сторінку, індекс UTF-16 у рядку, кодову точку в числовому вигляді та у формі U+XXXX, обраний шрифт, його тип і операцію, на якій виникла проблема, тож заявка в підтримку може назвати точний символ, а не просто описати симптом. Трекер зберігає останні 256 подій, що достатньо для діагностики документа і водночас достатньо мало, щоб патологічний прогін не перетворив діагностику на проблему з пам'яттю

Вимірювання та малювання мають узгоджуватися

Вимірювання ширини використовує ті самі кластерно-орієнтовані рішення щодо резервного шрифту, що й малювання. Це звучить очевидно, але саме тут найчастіше помиляються саморобні реалізації резервних шрифтів: вони латають шлях малювання, залишаючи вимірювання на основному шрифті, і в результаті кожне текстове поле, вирівнювання по правому краю та стовпець таблиці обчислюються за ширинами, які не відповідають тому, що насправді відображено

Оскільки обидва шляхи використовують те саме розв'язання, рядок, виміряний перед малюванням, займає рівно ту ширину, на яку його було виміряно, включно з резервними ділянками. Саме це робить безпечним увімкнення резервного шрифту глобально, а не лише в тих місцях, які ви перевірили вручну

Вбудовується лише те, що справді використано

Резервні шрифти вбудовуються ліниво: гарнітура в ланцюжку, яка жодного разу не покрила жодного кластера, не додає нічого до результату. Документ, що містить один китайський символ і 5000 латинських, не несе в собі повну CJK-гарнітуру — він несе лише те, що прохід субсетингу створив для цього одного гліфа, і ця поведінка описана в статті про оптимізацію розміру файлу та субсетинг шрифтів

Ця лінивість робить налаштування широкого ланцюжка дешевим. Реєструйте всі гарнітури, які можуть знадобитися вашому набору документів для кожної локалі, яку ви обслуговуєте, і кожен окремий PDF платить лише за те, що справді використав. Для документів, які ви не генерували самі, де відсутні гарнітури вже перебувають усередині наявного файлу, шлях виправлення інший і описаний у статті про вбудовування відсутніх шрифтів у наявний PDF

Варто прямо сказати про одну особливість розгортання: резервний механізм працює з гарнітурами, встановленими на машині, що виконує код. Сервер без встановлених CJK-шрифтів просто не матиме куди відступати, і звіт повідомить про це вже на першому документі, а не після першої скарги. Постачайте шрифти, від яких залежите, і перевірте ліцензування на їх вбудовування

PDFlibPas — це PDF-бібліотека для Delphi, C++Builder і Lazarus з відповідними інтерфейсами DLL та ActiveX, тож API резервного шрифту та звітування про відсутні гліфи доступні і з не-Pascal викликів. Повна документація — на сторінці PDFlibPas Delphi PDF library