Генерирате отчет, вграждате TrueType шрифт и полученият резултат се отваря правилно във всеки четец, който опитате. Глифовете са точни, текстът може да се маркира, файлът е валиден. Единственото нещо, което не е наред, е размерът. Документ, който използва няколко десетки латински знака, носи целия шрифт от 350 KB. Документ, който отпечатва абзац на китайски, носи 14 MB CJK шрифт вместо необходимия отрязък от половин мегабайт. Не е възникнало изключение, не е регистрирано предупреждение и файлът е преминал успешно валидацията. Ето как изглежда отстрани една стъпка на финализиране с грешна подредба: нищо не се срива и единственото доказателство е твърде големият размер на файла
Бъгът, който го причиняваше, съществуваше в HotPDF за една серия версии и оттогава е коригиран. Заслужава си да бъде описан не като уведомление за дефект, а като урок, тъй като моделът на грешката е общ. Всеки двигател за документи има етап на финализиране, който променя обектите точно преди да ги запише, и коректността на този етап зависи изцяло от подредбата на неговите стъпки спрямо сериализацията. Поставете дори една стъпка от грешната страна на записа и тя няма да направи нищо, съвсем тихо
Какво всъщност трябва да прави подмножеството от шрифтове (Font Subsetting)
Подмножеството от шрифтове (subset font) е частта от TrueType файла, която документът действително използва. Стандартът ISO 32000-1 §9.9 описва как вградената програма за шрифт се намира в поток, рефериран от дескриптора на шрифта, и за TrueType програма този поток е /FontFile2 с /Length1, указващ некомпресирания брой байтове. Процесът на подмножество (subsetting) пренаписва таблиците glyf и loca, така че те да съдържат само глифовете, реферирани от документа, преномерира идентификаторите на глифовете и добавя префикс към името на /BaseFont с шестбуквен маркер като ABCDEF+, за да отбележи шрифта като подмножество, точно както изисква спецификацията. Латински шрифт, чийто размер се свива до десет или петнадесет килобайта при подмножество, прави разликата между олекотен PDF и такъв, който пренася цял шрифт заради едно-единствено заглавие
Моментът, в който това се случва, е от решаващо значение. Създаването на подмножество не е трансформация, която прилагате върху байтове, които вече са на диска. То редактира граф от обекти в паметта: свива съдържанието на потока /FontFile2, коригира /Length1 и пренаписва низа /BaseFont. Всичко това трябва да е готово, когато сериализаторът обхожда графа и излъчва байтове. Ако промените се извършат след като байтовете са записани, те просто актуализират обекти, които никой никога няма да прочете
Симптомът и защо нищо не сигнализира за грешка
Докладваното поведение беше пълни шрифтове в изходния файл без никаква диагностика. Потребител, който регистрира Unicode TrueType шрифт и генерира нормален документ, открива, че вграденият обект на шрифта е със същата дължина като изходния .ttf файл и че името на /BaseFont не носи шестбуквения префикс за подмножество. Размерът на изходния файл никога не се свиваше, независимо дали в сесиите се използваха десет глифа или десет хиляди
Липсата на каквато и да е грешка е частта, която прави този клас бъгове изключително скъпи. Рутината за подмножество, която се изпълнява в грешното време, все пак се изпълнява успешно. Тя обхожда натрупаното използване на кодови точки (codepoints), изгражда перфектно коректно подмножество и го прилага към графа от обекти в паметта. Вътрешно работата е свършена и извикването завършва чисто. Единственото грешно нещо е, че графтът от обекти, който е редактиран, вече не е този, който се записва, тъй като записващият модул вече е приключил. От гледна точка на извикващия код, документът е създаден и записан без проблеми – което е точно усещането, което дава един тих срив
Коренът на проблема беше в подредбата на финализирането
В HotPDF финализиращата работа се случва вътре в EndDoc. Стъпката по създаване на подмножество е вътрешна рутина с име BuildAndApplyUnicodeFontSubset. Тя чете набора от използвани кодови точки за съответния документ, съхранявани в растерна карта (bitmap), която пътят за излъчване на текст попълва при извеждане на глифове, картографира всяка използвана кодова точка през кешираната таблица за кодови точки към глифове до реален идентификатор на глиф и пренаписва програмата на шрифта около тези данни. Когато се регистрира 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 и потвърдете, че шестбуквеният префикс е налице, тъй като неговата липса е пряк сигнал, че не е приложено подмножество
За PDF/A изход проверката е още по-точна, тъй като валидаторът върши работата вместо вас. Задайте нивото на съответствие и пуснете резултата през veraPDF: липсващ /CIDSet или подмножество, което не съвпада с дескриптора, се съобщава като несъответствие, вместо да разчитате на око. Превключвателите за съответствие, които управляват тази финализираща работа, са свойства на документа. PDFACompliance приема низ като '2B' за PDF/A-2 ниво B, а PDFUACompliance е булева стойност, която активира изискванията за маркиран PDF и подредба на разделителите (tab-order)
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 ниво B, а PDFUACompliance е булева стойност, която активира изискванията за маркиран PDF и подредба на разделителите (tab-order)
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 Component за Delphi and C++Builder, заедно с програмните интерфейси (API) за зареждане, редактиране, шифриране и подписване, разгледани на други места в този блог