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

Рендеринг вбудованих шрифтів PDF у Delphi за допомогою HotPDF

HotPDF рендерить вбудовані шрифти PDF у Delphi без необхідності встановлювати будь-що в систему: конвеєр рендерингу вбудованих гліфів у модулі HPDFGlyphRender.pas аналізує програми шрифтів, що зберігаються всередині самого PDF-файлу — контури TrueType glyf із FontFile2, символьні рядки CFF Type 2 із FontFile3, та потоки вмісту гліфів Type 3 — і відтворює їх як заповнені векторні контури GDI. Ця стаття є глибоким дослідженням точності відображення шрифтів, яке доповнює тему рендерингу сторінок PDF у TBitmap за допомогою HotPDF: та стаття описує рендерер загалом, а ця — те, як текст на сторінках набуває своїх точних обрисів

Чому PDF рендериться з квадратиками замість тексту?

Квадратики, порожнечі або неправильні символи при рендерингу PDF майже завжди означають, що рендерер звернувся до операційної системи за шрифтом замість того, щоб використовувати вбудований у файл шрифт. Ця проблема виглядає однаково: документ виглядає ідеально на комп'ютері, де його створили, але коли клієнт відкриває його на чистому сервері чи обмеженому робочому столі, японський інвойс показує порожні квадрати («тофу»), або підібраний схожий шрифт зсуває кожне перенесення рядка. Цих шрифтів ніколи не було на цільовій машині — вони є лише всередині PDF — і рендерер, який обмежується заміною на системні шрифти, не може їх прочитати. Підмножини шрифтів (subset fonts) лише погіршують ситуацію: підмножина може містити сорок гліфів под кодами символів, призначеними приватно для цього конкретного файлу, і жоден встановлений у системі шрифт не має такого самого зіставлення

Стандарт 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 приймає кубічні криві, тому кожен квадратичний сегмент точно підвищується в ступені (degree elevation), а не просто розбивається на відрізки прямих

// 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), і рендерер об'єднує матрицю масштабування, текстову матрицю та поточну матрицю перетворення CTM в одне перетворення гліфа на пристрій перед заповненням контуру. Порядок операцій тут критично важливий: якщо об'єднати ці три матриці у зворотному порядку, кожен гліф зсунеться до початку координат — сторінка виглядатиме зіпсованою через помилку в одному рядку матричної алгебри

Як інтерпретатор символьних рядків Type 2 обробляє шрифти CFF

Клас THPDFEmbeddedCFF забезпечує програмам FontFile3 повноцінний інтерпретатор символьних рядків Type 2: він аналізує структури CFF INDEX, Top DICT та Private DICT, після чого виконує кожен символьний рядок і передає сегменти контуру безпосередньо в GDI. Обгортка OpenType (контейнер OTTO) спочатку очищається для доступу до самої таблиці CFF; чисті потоки CIDFontType0C та Type1C обробляються безпосередньо. Символьні рядки — це компактна стекова мова, і три її особливості визначають, чи залишається інтерпретатор синхронізованим із потоком байтів. Необов'язковий префікс ширини означає, що перший оператор очищення стека може містити один додатковий перший операнд. Оператор hintmask передбачає виклик vstemhm, коли операнди все ще знаходяться в стеку, а кількість байтів маски, які потрібно пропустити, залежить від накопиченої кількості вертикальних штрихів (stems) — якщо помилитися в підрахунку хоча б раз, кожен наступний код операції буде прочитано неправильно. А виклики підпрограм додають зміщення (bias) до свого індексу (107, 1131 або 32768 залежно від кількості підпрограм) перед пошуком, тому виклик без зміщення призведе до виклику зовсім іншої підпрограми

Шрифти CFF із ключем CID додають ще один рівень опосередкованості, на якому часто виникають помилки в простих реалізаціях: код символу вибирає CID, але індексом символьного рядка є GID, а набір символів шрифту зіставляє GID із CID — тому рендерер будує зворотну карту CID-to-GID перед малюванням і вибирає Private DICT для кожного гліфа за допомогою FDSelect для шрифтів, які містять кілька таких словників. Програми Type1C з іменами як ключами (звичайний носій для простих шрифтів Type 1) замість цього розпізнають однобайтові коди через вбудоване кодування програми CFF або через механізм кодування на рівні PDF, описаний далі. Одне застереження: інтерпретатор зчитує оператори підказок (hints) для підтримки синхронізації потоку, але не виконує хинтинг — про це обмеження детальніше написано в кінці

Що є шрифт 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 та їхні аналоги для обведення всередині процедури ігноруються на час виконання цього гліфа. Якщо проігнорувати правило кольору, то шрифт штрих-коду d1, виведений на сторінці синім кольором, виявиться чорним; якщо проігнорувати обмеження області, то некоректний гліф вийде за межі своєї комірки

Як коди символів стають ідентифікаторами гліфів

Інтерпретатори контурів — це лише половина справи, оскільки байти в текстовому рядку PDF є кодами символів, а не індексами гліфів, і стандарт ISO 32000-1 присвячує цьому зіставленню два підрозділи. Для простих шрифтів §9.6.6 встановлює суворий пріоритет: масив /Differences має перевагу над базовим кодуванням (WinAnsiEncoding, MacRomanEncoding або StandardEncoding), яке, у свою чергу, має перевагу над власною картою програми шрифту. HotPDF перетворює цей ланцюжок на таблицю відповідності кодів і GID на 256 елементів, транслюючи імена гліфів в індекси гліфів трьома шляхами: точний збіг набору символів у програмі 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

Коли потрібен пошук у таблиці cmap TrueType, HotPDF перевіряє ланцюжок варіантів замість того, щоб довіряти одній субтаблиці: спочатку йдуть субтаблиці Windows Unicode (формат 4, потім формат 12 для додаткових площин), потім субтаблиця символів (3,0) зі своєю угодою про область приватного використання F000, яка відображається на молодший байт — ось чому символьний шрифт на кшталт Wingdings реагує на звичайні коди ASCII — далі йдуть застарілі формати 6 і 0. Субтаблиці формату 2 навмисно не інтерпретуються: вони зіставляють застарілі багатобайтові кодові сторінки, такі як Shift-JIS та Big5, а не Unicode, а сучасні шрифти CJK у будь-якому разі містять субтаблицю формату 4 або 12. Будь-який код, який не вдалося знайти жодним із цих способів, повертається до малювання засобами GDI для цього конкретного гліфа, тому один нерозпізнаний символ псує один гліф, а не весь текстовий блок

Чого не робить шлях рендерингу вбудованих шрифтів

Ці обмеження варто описати чітко. Хинтинг (hinting) не виконується — контури заповнюються так, як вони описані, що практично не відрізняється від виводу з хинтингом при роздільній здатності від 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