Створіть звіт, вбудуйте шрифт TrueType, і вихідний файл правильно відкриється в будь-якому переглядачі. Гліфи правильні, текст можна виділити, а файл є валідним. Єдине, що не так, - це розмір. Документ, у якому використовувалося кілька десятків латинських символів, містить весь шрифт розміром 350 КБ. Документ, у якому надруковано абзац китайського тексту, містить CJK-шрифт розміром 14 МБ замість потрібної частини розміром у пів мегабайта. Жодного винятку не виникло, жодного попередження не записано, а файл пройшов валідацію. Ось так ззовні виглядає неправильний порядок кроків фіналізації: нічого не ламається, а єдиним доказом є занадто великий розмір.
Помилка, яка призводила до цього, існувала в одній із версій HotPDF і з того часу була виправлена. Про неї варто розповісти не як про повідомлення про дефект, а як про урок, оскільки характер цієї помилки є типовим. Будь-який рушій документів має етап фіналізації, який змінює об'єкти безпосередньо перед їх записом, і правильність цього етапу повністю залежить від порядку його кроків відносно серіалізації. Якщо виконати один крок не з того боку операції запису, він нічого не зробить, причому без жодних повідомлень.
Що має робити субсетинг шрифтів
Субсет шрифту - це частина файлу TrueType, яку фактично використовує документ. Стандарт ISO 32000-1 §9.9 описує, як вбудована програма шрифту міститься в потоці, на який посилається дескриптор шрифту. Для програми TrueType цим потоком є /FontFile2 із параметром /Length1, що вказує кількість нестиснутих байтів. Субсетинг перезаписує таблиці glyf та loca, щоб вони містили лише ті гліфи, на які посилається документ, перенумеровує ідентифікатори гліфів і додає до назви /BaseFont префікс із шести літер, наприклад ABCDEF+, для позначення шрифту як субсету, як того вимагає специфікація. Латинський шрифт, який завдяки субсетингу зменшується до десяти або п'ятнадцяти кілобайтів, створює різницю між компактним PDF-файлом і файлом, який містить цілу гарнітуру заради одного заголовка.
Момент, коли це відбувається, має значення. Субсетинг - це не перетворення, яке ви застосовуєте до байтів, що вже знаходяться на диску. Він редагує граф об'єктів у пам'яті: зменшує вміст потоку /FontFile2, коригує /Length1 і перезаписує рядок /BaseFont. Усе це має бути готове, коли серіалізатор обходитиме граф об'єктів і записуватиме байти. Якщо зміни вносяться після запису байтів, вони оновлять об'єкти, які ніхто ніколи не прочитає.
Симптом і чому не виникало помилок
Повідомлена поведінка полягала у вбудовуванні повних шрифтів у вихідний файл без жодної діагностики. Користувач, який зареєстрував шрифт Unicode TrueType і створив звичайний документ, виявив, що вбудований об'єкт шрифту мав ту саму довжину, що й вихідний файл .ttf, а назва /BaseFont не містила шестилітерного префікса субсету. Розмір вихідного файлу ніколи не зменшувався між запусками з використанням десяти гліфів і запусками з використанням десяти тисяч гліфів.
Відсутність помилки - це саме те, що робить цей клас помилок дорогим. Процедура субсетингу, яка запускається в невідповідний час, все одно виконується. Вона обходить накопичений набір використаних кодових точок, створює абсолютно правильний субсет і застосовує його до графу об'єктів у пам'яті. Внутрішньо робота виконана, і виклик успішно повертає керування. Єдина проблема полягає в тому, що відредагований граф об'єктів більше не є тим, що записується, оскільки запис уже завершено. З погляду викликача, документ був створений і збережений без жодних інцидентів, що є класичним прикладом тихої помилки.
Першопричиною був порядок фіналізації
У HotPDF завершальна робота відбувається всередині EndDoc. Крок субсетингу - це внутрішня процедура під назвою BuildAndApplyUnicodeFontSubset. Вона зчитує набір використаних кодових точок для конкретного документа, який зберігається в бітовій карті (її шлях виведення тексту заповнює під час відображення гліфів), відображає кожну використану кодову точку через кешовану таблицю відповідності кодових точок гліфам на реальний ідентифікатор гліфа та перезаписує програму шрифту навколо цього набору. При реєстрації шрифту Unicode TrueType шлях виведення встановлює біт у наборі використаних кодових точок для кожного символу, який він малює, тому до моменту закриття документа рушій точно знає, які саме гліфи має містити субсет.
Дефект полягав у тому, що BuildAndApplyUnicodeFontSubset викликався вже після того, як SaveToStream або SaveToFile серіалізували документ. Зміни субсетера в /FontFile2, його скоригований /Length1 та шестилітерний префікс /BaseFont обчислювалися для графу об'єктів, який уже був перетворений на байти. Виправлення полягало в зміні порядку в один рядок: перемістити виклик субсетингу перед серіалізацією, щоб записувач виводив субсет шрифту, а не оригінал. Виправлена послідовність спочатку запускає субсетер, а потім виконує серіалізацію.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Після виправлення порядку викликів код користувача не змінюється. Субсетинг увімкнено за замовчуванням після реєстрації шрифту Unicode TrueType. Ви реєструєте шрифт, починаєте документ, малюєте і завершуєте його, а субсет створюється з використаних гліфів перед тим, як байти залишать пам'ять.
Чому один неправильний крок створює цілу категорію помилок
Причина, чому це варте окремого уроку, а не просто примітки, полягає в тому, що EndDoc виконує список завершальних кроків, і кожен із них чутливий до свого положення відносно запису файлу. Субсетинг шрифтів - один із них. Виведення PDF/A вимагає потоку /CIDSet, який перелічує саме ті ідентифікатори гліфів, які присутні в субсеті; це обмеження накладає стандарт ISO 19005, щоб валідатор міг підтвердити, що вбудована програма відповідає вимогам дескриптора шрифту. Цей потік виводиться в тому самому вікні фіналізації та залежить від того, чи був спочатку створений субсет. Стандарт PDF/UA-1 вимагає відповідно до ISO 14289-1 §7.18.3, щоб кожна сторінка з анотаціями містила ключ /Tabs із значенням /S, і внутрішня процедура під назвою EnsurePDFUATabsOnAnnotatedPages записує цей ключ на тому самому етапі. Перевірки наміру виведення (output intent) також виконуються там.
Та сама помилка впорядкування, яка вимикала субсетинг, також призводила до втрати ключа порядку табуляції PDF/UA на сторінках з анотаціями, оскільки цей крок знаходився по той самий неправильний бік запису. Програми veraPDF та PAC повідомляють про відсутність /Tabs /S як про порушення пункту 21-001 протоколу Matterhorn. Таким чином, один неправильно розміщений виклик не просто збільшив розмір файлу, він одночасно непомітно порушив вимогу відповідності доступності, і так само без жодних повідомлень про помилки. У цьому й полягає небезпека етапу фіналізації: його кроки мають спільну передумову, і одна помилка впорядкування може вивести з ладу відразу кілька з них, хоча кожен виклик все одно повертатиме успішний результат.
Як насправді виявляється тиха помилка виведення
Помилка, яка не викликає винятку, не виявляється під час виконання програми. Вона виявляється шляхом перевірки вихідного файлу та порівняння його з тим, що мало бути створено. Для субсетингу шрифтів перевірки є конкретними. Порівняйте розмір вихідного файлу з очікуваним: документ, який містить лише кілька гліфів, не повинен мати розмір повного шрифту. Відкрийте вбудований об'єкт шрифту та зчитайте його довжину в байтах; субсетований потік /FontFile2 для латинського шрифту становить невелику частину вихідного файлу. Прочитайте назву /BaseFont і переконайтеся, що шестилітерний префікс присутній, оскільки його відсутність є прямим сигналом того, що субсетинг не було застосовано.
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Для виведення PDF/A перевірка є ще точнішою, оскільки валідатор робить роботу за вас. Встановіть рівень відповідності та запустіть перевірку результату через veraPDF: відсутній /CIDSet або субсет, який не відповідає дескриптору, буде повідомлено як невідповідність стандарту, а не залишено для вашого візуального контролю. Перемикачі відповідності, які керують цією роботою з фіналізації, є властивостями документа. Властивість PDFACompliance приймає рядок, наприклад '2B' для PDF/A-2 Level B, а PDFUACompliance - це логічне значення, яке вмикає вимоги до тегованого PDF та порядку табуляції.
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
Інженерний урок
З цього випливають два правила. Перше полягає в тому, що будь-який крок фіналізації, який змінює об'єкти, повинен виконуватися до серіалізації цих об'єктів, а кінцевий етап рушія документів слід розглядати як упорядкований конвеєр, де серіалізація є останньою дією, а не однією з кількох. Друге правило - це те, що забрало найбільше часу в даному випадку: для кроку виведення відсутність помилки не є доказом успіху. Процедура, яка будує правильний субсет і застосовує його до неправильного, уже записаного графу об'єктів, не повідомляє про проблеми, бо з її власної перспективи все було гаразд. Верифікація має досліджувати сам артефакт, а не код повернення. Перевіряйте розмір вихідного файлу, зчитуйте довжину вбудованого шрифту в байтах та його префікс /BaseFont, а також дозвольте veraPDF оцінити вихідний файл PDF/A, де відсутній /CIDSet перетворює тиху помилку на цілком конкретний звіт про збій.
Сторона створення при роботі зі шрифтами, тобто те, як гарнітури реєструються та вбудовуються для виведення звітів, описана в нашій статті про шрифти та зображення у виведенні звітів. Сторона валідації, де ці кроки фіналізації перевіряються на відповідність стандартам, розглядається в інструкції з валідації PDF/A та PDF/UA. Обидва аспекти поєднуються з роботою над субсетингом та відповідністю стандартам, описаною тут, яка постачається як частина компонента HotPDF для Delphi та C++Builder разом з API для завантаження, редагування, шифрування та підписання, описаними в інших статтях цього блогу.