Генерирайте отчет, вградете TrueType шрифт и изходният файл се отваря правилно във всеки четец, който опитате. Глифовете са правилни, текстът може да се избира, файлът е валиден. Единственото нещо, което не е наред, е размерът. Документ, който използва няколко десетки латински знака, носи целия шрифт от 350 KB. Документ, който отпечатва абзац на китайски език, носи 14 MB CJK шрифт вместо полумегабайтовата част, която би трябвало да му е необходима. Не беше генерирано изключение, не беше записано предупреждение и файлът премина валидация. Ето как изглежда отвън една неправилно подредена стъпка на финализиране: нищо не се срива и единственото доказателство е твърде голямото число.
Бъгът, който я причини, съществуваше в 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 поставя този ключ по време на същия етап. Проверките за изходно намерение също се изпълняват там.
Същата грешка в подредбата, която деактивира подмножеството, също така пропусна ключа за реда на табулация в 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 (tagged-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 Component за Delphi и C++Builder, заедно с API за зареждане, редактиране, шифриране и подписване, разгледани на други места в този блог.