Vygenerujte report, vložte TrueType písmo a výstup se správně otevře v každém prohlížeči, který vyzkoušíte. Glyfy jsou správné, text lze vybrat, soubor je platný. Jediná věc, která je špatně, je velikost. Dokument, který použil několik desítek latinských znaků, nese celé 350 KB písmo. Dokument, který vytiskl odstavec v čínštině, nese 14 MB CJK písmo namísto půlmegabajtového výřezu, který by měl stačit. Nebyla vyvolána žádná výjimka, nebylo zaznamenáno žádné varování a soubor prošel validací. Takto zvenčí vypadá chybně seřazený krok finalizace: nic neselže a jediným důkazem je příliš velké číslo.
Chyba, která to způsobila, existovala v HotPDF po dobu jedné verze a od té doby byla opravena. Stojí za to ji popsat nikoli jako oznámení o chybě, ale jako ponaučení, protože charakter této chyby je obecný. Jakýkoli dokumentový engine má finalizační fázi, která mění objekty těsně před jejich zápisem, a správnost této fáze závisí výhradně na pořadí jejích kroků vzhledem k serializaci. Pokud uděláte jeden krok na špatné straně zápisu, neprovede se nic a bez jakéhokoli upozornění.
Co mají podmnožiny písem dělat
Podmnožina písma (subset font) je část TrueType souboru, kterou dokument skutečně používá. ISO 32000-1 §9.9 popisuje, jak je vestavěný program písma uložen v datovém proudu (streamu) odkazovaném deskriptorem písma. Pro TrueType program je tímto streamem /FontFile2 s /Length1 udávajícím nekomprimovaný počet bajtů. Vytvoření podmnožiny přepíše tabulky glyf a loca tak, aby obsahovaly pouze glyfy odkazované v dokumentu, přečísluje identifikátory glyfů a před název /BaseFont vloží Popis šestipísmenného tagu, například ABCDEF+, čímž písmo označí za podmnožinu přesně tak, jak vyžaduje specifikace. Latinské písmo oříznuté na deset nebo patnáct kilobajtů představuje rozdíl mezi štíhlým PDF a souborem, který přenáší celý řez písma kvůli jedinému nadpisu.
Okamžik, kdy k tomu dochází, je zásadní. Vytváření podmnožiny není transformace, kterou použijete na bajty již zapsané na disku. Upravuje objektový graf v paměti: zmenšuje obsah streamu /FontFile2, opravuje /Length1 a přepisuje řetězec /BaseFont. To vše musí být připraveno, když serializátor prochází graf a zapisuje bajty. Pokud k úpravám dojde až po zápisu bajtů, aktualizují se objekty, které už nikdo nebude číst.
Symptom a proč si nikdo nestěžoval
Hlášené chování spočívalo v přítomnosti plných písem ve výstupu bez jakékoli diagnostiky. Uživatel, který zaregistroval Unicode TrueType písmo a vytvořil běžný dokument, zjistil, že objekt vestavěného písma měl stejnou délku jako zdrojový soubor .ttf a název /BaseFont neobsahoval šestipísmenný prefix podmnožiny. Výstup se nikdy nezmenšil, ať už šlo o spuštění s deseti glyfy nebo s deseti tisíci.
Absence jakékoli chyby je tím, co činí tuto třídu chyb nákladnou. Rutina pro vytváření podmnožin, která se spustí v nesprávný čas, stále běží. Prochází nashromážděné využití kódových bodů, vytvoří naprosto správnou podmnožinu a aplikuje ji na objektový graf v paměti. Interně je práce hotová a volání skončí bez chyb. Jediným problémem je, že objektový graf, který rutina upravila, už není tím, co se zapisuje, protože zapisovač již práci dokončil. Z pohledu volajícího byl dokument vytvořen a uložen bez incidentu, což je přesně dojem, který tiché selhání vyvolává.
Hlavní příčinou bylo pořadí finalizace
V HotPDF probíhají ukončovací práce uvnitř EndDoc. Krok vytváření podmnožiny je interní rutina s názvem BuildAndApplyUnicodeFontSubset. Načítá sadu použitých kódových bodů pro daný dokument uchovávanou v bitmapě, kterou cesta pro výstup textu plní při zobrazování glyfů, mapuje každý použitý kódový bod přes kešovanou tabulku kódových bodů na glyfy na skutečný identifikátor glyfu a přepisuje program písma kolem tohoto uzávěru. Při registraci Unicode TrueType písma nastavuje cesta pro výstup bit v sadě použitých kódových bodů pro každý vykreslený znak, takže v okamžiku uzavření dokumentu engine přesně ví, které glyfy musí podmnožina zachovat.
Chyba spočívala v tom, že BuildAndApplyUnicodeFontSubset byla volána až poté, co SaveToStream nebo SaveToFile již dokument serializovaly. Úpravy podmnožiny v /FontFile2, její opravená /Length1 a šestipísmenný prefix /BaseFont byly vypočítány nad objektovým grafem, který již byl převeden na bajty. Nápravou byla změna pořadí na jednom řádku: přesunout volání podmnožiny před serializaci, aby zapisovač zapsal písmo s podmnožinou namísto původního. Opravená sekvence nejprve spustí vytváření podmnožiny a poté provede serializaci.
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;
Po opravě pořadí se na volajícím kódu nic nemění. Vytváření podmnožin je ve výchozím nastavení zapnuté, jakmile je Unicode TrueType písmo zaregistrováno. Zaregistrujete písmo, zahájíte dokument, vykreslíte obsah, ukončíte jej a podmnožina se sestaví z glyfů, které jste použili předtím, než bajty opustí paměť.
Proč je jeden nesprávně umístěný krok celou kategorií
Důvod, proč toto stojí za ponaučení a ne jen za poznámku pod čarou, je ten, že EndDoc provádí řadu ukončovacích kroků a každý z nich je citlivý na svou pozici vůči zápisu. Vytváření podmnožiny písma je jedním z nich. Výstup PDF/A vyžaduje stream /CIDSet, který přesně vyjmenovává identifikátory glyfů přítomné v podmnožině. Toto omezení ukládá norma ISO 19005, aby validátor mohl potvrdit, že vestavěný program odpovídá tomu, co deklaruje deskriptor písma. Tento stream se zapisuje ve stejném finalizačním okně a závisí na tom, že podmnožina byla vytvořena jako první. Norma PDF/UA-1 vyžaduje podle ISO 14289-1 §7.18.3, aby každá stránka obsahující anotaci deklarovala /Tabs s hodnotou /S. Interní rutina s názvem EnsurePDFUATabsOnAnnotatedPages zapisuje tento klíč během stejné fáze. V této fázi probíhají také kontroly záměru výstupu (output intent).
Stejná chyba v pořadí, která zakázala vytváření podmnožin, také vynechala klíč pořadí tabulátorů PDF/UA na stránkách s anotacemi, protože tento krok se nacházel na stejné nesprávné straně zápisu. Nástroje veraPDF a PAC hlásí chybějící /Tabs /S jako porušení bodu 21-001 protokolu Matterhorn. Jediné nesprávně umístěné volání tedy nejen zvětšilo velikost souboru, ale zároveň tiše porušilo požadavek na shodu s přístupností, a to se stejným nedostatkem jakýchkoli chybových hlášení. To je riziko finalizační fáze: její kroky sdílejí společný předpoklad a jediná chyba v pořadí může vyřadit několik z nich najednou, přičemž každé volání stále vrací úspěch.
Jak se tiché selhání výstupu skutečně odhalí
Chyba, která nevyvolá žádnou výjimku, se spuštěním programu neodhalí. Odhalí se kontrolou výstupu a jeho porovnáním s tím, co měl vstup vytvořit. Pro podmnožiny písem jsou kontroly konkrétní. Porovnejte velikost výstupního souboru s hrubým očekáváním: dokument, který použil jen hrstku glyfů, by neměl mít velikost celého písma. Otevřete objekt vestavěného písma a přečtěte jeho délku v bajtech. Podmnožina /FontFile2 pro latinské písmo tvoří jen malý zlomek zdrojového souboru. Přečtěte název /BaseFont a potvrďte přítomnost šestipísmenného prefixu, protože jeho absence je přímým signálem, že nebyla použita žádná podmnožina.
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;
Pro výstup PDF/A je kontrola ještě přesnější, protože validátor udělá práci za vás. Nastavte úroveň shody a spusťte výsledek přes veraPDF: chybějící /CIDSet nebo podmnožina, která neodpovídá deskriptoru, se nahlásí jako nesplněná klauzule, namísto toho, abyste si toho museli všimnout sami pohledem. Přepínače shody, které řídí tuto finalizační práci, jsou vlastnostmi dokumentu. PDFACompliance přijímá řetězec, například '2B' pro PDF/A-2 Level B, a PDFUACompliance is a boolean, který zapíná požadavky na tagované PDF a pořadí tabulátorů.
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;
Vývojářské ponaučení
Z toho vyplývají dvě pravidla. Prvním je, že jakýkoli finalizační krok, který mění objekty, musí proběhnout před serializací těchto objektů. Ukončovací fáze dokumentového enginu by měla být chápána jako seřazená pipeline, kde serializace je poslední akcí, nikoli jednou z mnoha. Druhým je pravidlo, které zde stálo nejvíce času: u kroků generujících výstup není absence chyby důkazem úspěchu. Rutina, která sestaví správnou podmnožinu a aplikuje ji na špatný, již zapsaný graf, nenahlásí žádnou chybu, protože z jejího pohledu bylo vše v pořádku. Ověření se musí zaměřit na samotný artefakt, nikoli na návratový kód. Zkontrolujte velikost výstupu, přečtěte délku vestavěného písma v bajtech a jeho prefix /BaseFont, a nechte veraPDF posoudit výstup PDF/A, kde chybějící /CIDSet promění tiché selhání v jasně pojmenovanou chybu.
Strana generování a zpracování písem, tedy jak se řezy písem registrují a vkládají pro výstupy reportů, je popsána v našem článku o písmech a obrázcích ve výstupech reportů. Strana validace, kde se tyto finalizační kroky kontrolují vůči standardům, je popsána v průvodci validací PDF/A a PDF/UA. Oba témy doplňují práci s podmnožinami a shodou popsanou zde, která je dodávána jako součást HotPDF Component pro Delphi a C++Builder společně s rozhraními API pro načítání, úpravy, šifrování a podepisování popsanými jinde na tomto blogu.