Generujte report, vložte TrueType písmo a výstup sa otvorí správne v každom prehliadači, ktorý vyskúšate. Glyfy sú správne, text sa dá vybrať, súbor je platný. Jediné, čo je nesprávne, je veľkosť. Dokument, ktorý použil niekoľko desiatok latinských znakov, nesie celé 350 KB písmo. Dokument, ktorý vytlačil odsek v čínštine, nesie 14 MB CJK písmo namiesto polmegabajtového výseku, ktorý by mal potrebovať. Nebola vyvolaná žiadna výnimka, nezaznamenalo sa žiadne varovanie a súbor prešiel validáciou. Takto vyzerá nesprávne zoradený krok finalizácie zvonku: nič nezlyhá a jediným dôkazom je príliš veľké číslo.
Chyba, ktorá to spôsobila, žila v HotPDF počas jednej verzie a odvtedy bola opravená. Stojí za to o nej napísať nie ako o oznámení o chybe, ale ako o ponaučení, pretože tvar tejto chyby je všeobecný. Každý dokumentový engine má finalizačnú fázu, ktorá mení objekty tesne pred ich zápisom, a správnosť tejto fázy závisí výlučne od poradia jej krokov vo vzťahu k serializácii. Ak urobíte jeden krok na nesprávnej strane zápisu, neurobí nič, a to potichu.
Čo má subsetting písiem vlastne robiť
Podskupina písma (subset) je tá časť súboru TrueType, ktorú dokument skutočne používa. Norma ISO 32000-1 §9.9 popisuje, ako je program vloženého písma umiestnený v streame odkazovanom deskriptorom písma, a pre program TrueType je týmto streamom /FontFile2 s /Length1 udávajúcim nekomprimovaný počet bajtov. Subsetting prepisuje tabuľky glyf a loca tak, aby obsahovali iba glyfy, na ktoré dokument odkazuje, prečísluje identifikátory glyfov a pred názov /BaseFont pridá šesťpísmenový tag, napríklad ABCDEF+, na označenie písma ako podskupiny, presne tak, ako to vyžaduje špecifikácia. Latinské písmo, ktorého subset má desať alebo pätnásť kilobajtov, predstavuje rozdiel medzi štíhlym PDF a takým, ktoré prenáša celú rodinu písiem kvôli jednému nadpisu.
Moment, kedy k tomu dochádza, je dôležitý. Subsetting nie je transformácia, ktorú aplikujete na bajty už zapísané na disku. Upravuje graf objektov v pamäti: zmenšuje obsah streamu /FontFile2, opravuje /Length1 a prepisuje reťazec /BaseFont. To všetko musí byť na svojom mieste, keď serializátor prechádza graf a generuje bajty. Ak sa úpravy vykonajú až po zápise bajtov, aktualizujú objekty, ktoré už nikto nikdy nebude čítať.
Príznak a prečo nič nehlásilo chybu
Nahlásené správanie bolo, že vo výstupe boli úplné písma bez akejkoľvek diagnostiky. Používateľ, ktorý zaregistroval Unicode TrueType písmo a vytvoril bežný dokument, zistil, že vložený objekt písma mal rovnakú dĺžku ako zdrojový súbor .ttf a že názov /BaseFont neniesol žiadny šesťpísmenový subset prefix. Výstup sa nikdy nezmenšil medzi spusteniami, ktoré použili desať glyfov, a tými, ktoré ich použili desaťtisíc.
Absencia akejkoľvek chyby je to, čo robí túto triedu chýb nákladnou. Rutina pre subsetting, ktorá beží v nesprávnom čase, stále beží. Prejde akumulované použitie kódových bodov, vytvorí dokonale správny subset a aplikuje ho na graf objektov v pamäti. Vnútorne je práca vykonaná a volanie sa vráti čisto. Jediné, čo je nesprávne, je, že graf objektov, ktorý upravila, už nie je tým, čo sa zapisuje, pretože zapisovač už skončil. Z pohľadu volajúceho bol dokument vytvorený a uložený bez incidentu, čo je presne dojem, ktorý vyvoláva tiché zlyhanie.
Hlavnou príčinou bolo poradie finalizácie
V HotPDF sa uzatváracia práca deje vnútri EndDoc. Krok subsettingu je interná rutina s názvom BuildAndApplyUnicodeFontSubset. Číta sadu použitých kódových bodov pre daný dokument, ktorá sa uchováva v bitmape vypĺňanej cestou generovania textu pri zobrazovaní glyfov, mapuje každý použitý kódový bod cez kešovanú tabuľku kódových bodov na glyfy na skutočný identifikátor glyfu a prepisuje program písma okolo tohto uzáveru. Keď je zaregistrované Unicode TrueType písmo, cesta generovania nastaví bit v sade použitých kódových bodov pre každý znak, ktorý vykreslí, takže v čase zatvorenia dokumentu engine presne vie, ktoré glyfy musí subset zachovať.
Chybou bolo, že BuildAndApplyUnicodeFontSubset sa volalo až po tom, čo SaveToStream alebo SaveToFile už dokument serializovali. Úpravy subsettera v /FontFile2, jeho opravená hodnota /Length1 a šesťpísmenový prefix /BaseFont boli vypočítané voči grafu objektov, ktorý už bol premenený na bajty. Nápravou bolo zmena poradia o jeden riadok: presunúť volanie subsetu pred serializáciu, aby zapisovač generoval subsetované písmo namiesto originálu. Opravená sekvencia spúšťa subsetter ako prvý a serializuje až potom.
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;
S opraveným poradím sa na volajúcom kóde nič nemení. Subsetting je predvolene zapnutý, akonáhle bolo zaregistrované Unicode TrueType písmo. Zaregistrujete písmo, začnete dokument, kreslíte, ukončíte ho a subset sa vytvorí z glyfov, ktoré ste použili pred tým, ako bajty opustia pamäť.
Prečo je jeden nesprávne umiestnený krok celou kategóriou
Dôvod, prečo to stojí za lekciu a nie iba za poznámku pod čiarou, je ten, že EndDoc vykonáva zoznam uzatváracích krokov a každý z nich je citlivý na svoju pozíciu voči zápisu. Jedným z nich je subsetting písiem. Výstup PDF/A vyžaduje stream /CIDSet, ktorý presne vymenúva identifikátory glyfov prítomné v subsete, čo je obmedzenie, ktoré ukladá norma ISO 19005, aby validátor mohol potvrdiť, že vložený program zodpovedá tomu, čo deklaruje deskriptor písma; tento stream sa generuje v rovnakom finalizačnom okne a závisí od toho, že subset bol postavený ako prvý. PDF/UA-1 vyžaduje podľa normy ISO 14289-1 §7.18.3, aby každá stránka nesúca anotáciu deklarovala /Tabs s hodnotou /S, pričom interná rutina s názvom EnsurePDFUATabsOnAnnotatedPages zapisuje tento kľúč počas rovnakej fázy. Prebiehajú tu aj kontroly zámeru výstupu (output-intent).
Rovnaká chyba v poradí, ktorá zakázala subsetting, tiež vynechala kľúč tabulátora PDF/UA na anotovaných stránkach, pretože tento krok ležal na rovnakej nesprávnej strane zápisu. Programy veraPDF a PAC hlásia chýbajúce /Tabs /S ako porušenie kontrolného bodu 21-001 protokolu Matterhorn. Jediné nesprávne umiestnené volanie teda nielen zväčšilo veľkosť súboru; zároveň ticho porušilo požiadavku na zhodu s prístupnosťou pri rovnakej absencii akejkoľvek chyby. To je nebezpečenstvo finalizačnej fázy: jej kroky zdieľajú predpoklad a jediná chyba v poradí môže vyradit niekoľko z nich naraz, pričom každé volanie stále vracia úspech.
Ako sa tiché zlyhanie generovania skutočne odhalí
Chyba, ktorá nevyvolá žiadnu výnimku, sa neodhalí spustením programu. Odhalí sa kontrolou výstupu a jeho porovnaním s tým, čo mal vstup vyprodukovať. Pre subsetting písiem sú kontroly konkrétne. Porovnajte veľkosť výstupného súboru s hrubým očakávaním: dokument, ktorý sa dotkol len niekoľkých glyfov, by nemal mať veľkosť celej rodiny písma. Otvorte vložený objekt písma a prečítajte jeho bajtovú dĺžku; subsetovaný /FontFile2 pre latinské písmo je malým zlomkom zdrojového súboru. Prečítajte názov /BaseFont a potvrďte, že je prítomný šesťpísmenový prefix, pretože jeho absencia je priamym signálom, že nebol aplikovaný žiadny subset.
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;
Pre výstup PDF/A je kontrola ešte ostrejšia, pretože validátor urobí prácu za vás. Nastavte úroveň zhody a prežeňte výsledok cez veraPDF: chýbajúci /CIDSet alebo subset, ktorý nezodpovedá deskriptoru, sa nahlási ako neúspešné pravidlo, namiesto toho, aby ste si to museli všimnúť voľným okom. Prepínače zhody, ktoré riadia túto finalizačnú prácu, sú vlastnosťami dokumentu. PDFACompliance prijíma reťazec ako napríklad '2B' pre PDF/A-2 Level B a PDFUACompliance je boolean, ktorý zapína požiadavky na tagované PDF a poradie tabulátora.
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;
Inžinierske ponaučenie
Z tohto vyplývajú dve pravidlá. Prvým je, že akýkoľvek finalizačný krok, ktorý mení objekty, musí prebehnúť pred tým, ako sú tieto objekty serializované, a záverečná fáza dokumentového enginu by sa mala vnímať ako zoradená pipeline, kde serializácia je poslednou akciou, nie jednou z niekoľkých. Druhé pravidlo je to, ktoré tu stálo najviac času: pri generovaní nie je absencia chyby dôkazom úspechu. Rutina, ktorá vytvorí správny subset a aplikuje ho na nesprávny, už zapísaný graf, nehlási nič zlé, pretože z jej vlastného pohľadu bolo všetko v poriadku. Overenie sa musí pozrieť na výsledný produkt, nie na návratový kód. Skontrolujte veľkosť výstupu, prečítajte si bajtovú dĺžku vloženého písma a jeho prefix /BaseFont a nechajte veraPDF posúdiť výstup PDF/A, kde chýbajúci /CIDSet zmení tiché zlyhanie na pomenovanú chybu.
Strana tvorcu pri práci s písmami, ako sa rodiny písiem registrujú a vkladajú do výstupov reportov, je popísaná v našom článku o písmach a obrázkoch vo výstupe reportov. Strana validácie, kde sa tieto finalizačné kroky kontrolujú voči štandardom, je pokrytá v návode na validáciu PDF/A a PDF/UA. Obe témy sa spájajú so subsettingom a prácou na zhode popísanou tu, ktorá sa dodáva ako súčasť HotPDF Component pre Delphi a C++Builder spolu s API na načítanie, úpravu, šifrovanie a podpisovanie, ktoré sú popísané inde na tomto blogu.