Создайте отчет, внедрите шрифт TrueType, и результат корректно откроется в любой программе для просмотра. Глифы отображаются правильно, текст можно выделить, файл валиден. Единственной проблемой является размер. Документ, использующий несколько десятков латинских символов, содержит весь шрифт размером 350 КБ. Документ, в котором напечатан абзац на китайском языке, содержит шрифт CJK объемом 14 МБ вместо половины мегабайта, которая ему действительно нужна. Никаких исключений не возникло, никаких предупреждений не было записано в лог, и файл успешно прошел валидацию. Так со стороны выглядит неправильный порядок шагов финализации: ничего не ломается, а единственным доказательством является слишком большой размер
Стоит записать это как урок, потому что форма ошибки универсальна. У любого документного движка есть стадия финализации, которая изменяет объекты непосредственно перед их записью, и корректность этой стадии целиком зависит от порядка её шагов относительно сериализации. Окажись один шаг не на той стороне записи — и он ничего не делает, тихо
Что должно делать создание подмножества шрифта
Подмножество шрифта представляет собой часть файла 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, перед записью
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;
// Несколько глифов из начертания размером ~700 KB не должны давать поток размером в сотни KB
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, включает вывод /CIDSet
Pdf.PDFUACompliance := True; // ставит /Tabs /S на страницах с аннотациями
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, а вывод PDF/A отдайте на суд veraPDF, где отсутствующий /CIDSet превращает тихий недобор в именованный отказ
Сторона создания при работе со шрифтами, их регистрация и внедрение для вывода отчетов подробно описаны в нашей статье о шрифтах и изображениях в выводе отчетов. Сторона валидации, проверяющая шаги финализации на соответствие стандартам, рассматривается в руководстве по валидации PDF/A и PDF/UA. Оба аспекта дополняют работу по созданию подмножеств и проверке соответствия, описанную здесь, которая поставляется в составе HotPDF Delphi Component для Delphi и C++Builder вместе с API для загрузки, редактирования, шифрования и подписи документов, рассмотренными в других статьях этого блога