Ustvarite poročilo, vgradite pisavo TrueType in izhod se pravilno odpre v vsakem pregledovalniku, ki ga poskusite. Glifi so pravilni, besedilo je mogoče izbrati, datoteka je veljavna. Edina težava je velikost. Dokument, ki je uporabil le nekaj ducat latiničnih znakov, vsebuje celotno pisavo velikosti 350 KB. Dokument, ki je natisnil odstavek kitajščine, vsebuje 14 MB pisavo CJK namesto polmegabajtnega dela, ki bi bil dejansko potreben. Sprožen ni bil noben izjemni dogodek, zabeleženo ni bilo nobeno opozorilo in datoteka je uspešno prestala preverjanje veljavnosti. Tako je od zunaj videti napačno razvrščen korak zaključevanja: nič ne spodleti, edini dokaz pa je prevelika številka.
Hrošč, ki je to povzročil, je bil prisoten v eni različici knjižnice HotPDF in je bil od takrat odpravljen. Vredno ga je opisati ne kot obvestilo o napaki, temveč kot lekcijo, saj je oblika te napake splošna. Vsak dokumentni pogon ima zaključno fazo, ki spreminja objekte tik pred njihovim zapisovanjem, pravilnost te faze pa je v celoti odvisna od vrstnega reda njenih korakov glede na serializacijo. Če en korak postavite na napačno stran zapisa, ne naredi ničesar, povsem tiho.
Kaj naj bi delal delni nabor pisav
Delni nabor pisave (subset font) je del datoteke TrueType, ki ga dokument dejansko uporablja. Standard ISO 32000-1 §9.9 opisuje, kako vgrajen program pisave deluje v toku, na katerega se nanaša deskriptor pisave, za program TrueType pa je ta tok /FontFile2 z vrednostjo /Length1, ki določa nestisnjeno število bajtov. Ustvarjanje delnega nabora znova zapiše tabeli glyf in loca, tako da vsebujeta samo tiste glife, na katere se dokument nanaša, ponovno oštevilči identifikatorje glifov in pred ime /BaseFont doda predpono s šestimi črkami, kot je ABCDEF+, da označi pisavo kot delni nabor, natanko tako, kot zahteva specifikacija. Latinična pisava, ki se zmanjša na deset ali petnajst kilobajtov, predstavlja razliko med vitko datoteko PDF in tisto, ki zaradi enega naslova prenaša celotno pisavo.
Trenutek, ko se to zgodi, je pomemben. Ustvarjanje delnega nabora ni pretvorba, ki jo uporabite na bajtih, ki so že na disku. Spreminja graf objektov v pomnilniku: skrči vsebino toka /FontFile2, popravi /Length1 in znova zapiše niz /BaseFont. Vse to mora biti pripravljeno, ko serializator prehodi graf in odda bajte. Če urejanja nastopijo po tem, ko so bajti že zapisani, posodobijo objekte, ki jih nihče ne bo nikoli prebral.
Simptom in zakaj se ni nič pritožilo
Poročano obnašanje je bilo prisotnost celotnih pisav v izhodu brez kakršne koli diagnostike. Uporabnik, ki je registriral pisavo Unicode TrueType in ustvaril običajen dokument, je ugotovil, da je bil vgrajeni objekt pisave enako dolg kot izvorna datoteka .ttf in da ime /BaseFont ni vsebovalo predpone s šestimi črkami. Izhod se ni nikoli zmanjšal, ne glede na to, ali je dokument obsojal deset glifov ali deset tisoč.
Odsotnost kakršne koli napake je tisto, zaradi česar je ta razred hroščev drag. Rutina za delni nabor pisav, ki se zažene ob napačnem času, se še vedno izvede. Pregleda zbrano uporabo kodnih točk, zgradi popolnoma pravilen delni nabor in ga uporabi na grafu objektov v pomnilniku. Interno je delo opravljeno in klic se zaključi brez napak. Edina težava je v tem, da graf objektov, ki ga je uredil, ni več tisto, kar se zapisuje, saj je pisec že zaključil delo. Z vidika klicatelja je bil dokument ustvarjen in shranjen brez zapletov, kar je natanko vtis, ki ga daje tihi neuspeh.
Izvorni vzrok je bil vrstni red zaključevanja
V knjižnici HotPDF se zaključno delo zgodi znotraj metode EndDoc. Korak delnega nabora je interna rutina z imenom BuildAndApplyUnicodeFontSubset. Prebere niz uporabljenih kodnih točk za posamezen dokument, ki se hrani v bitni sliki, ki jo pot izpisa besedila polni ob prikazovanju glifov, preslika vsako uporabljeno kodno točko prek predpomnjene tabele kodnih točk v glife v dejanski identifikator glifa ter znova zapiše program pisave okoli tega nabora. Ko je pisava Unicode TrueType registrirana, pot izpisa nastavi bit v nizu uporabljenih kodnih točk za vsak znak, ki ga nariše, tako da ob zapiranju dokumenta pogon natančno ve, katere glife mora delni nabor ohraniti.
Napaka je bila v tem, da je bila rutina BuildAndApplyUnicodeFontSubset priklicana po tem, ko sta SaveToStream ali SaveToFile že serializirala dokument. Urejanja delnega nabora za /FontFile2, njegov popravljeni /Length1 in predpona /BaseFont s šestimi črkami so bili izračunani na grafu objektov, ki je bil že pretvorjen v bajte. Rešitev je bila prerazporeditev v eni vrstici: premik klica delnega nabora pred serializacijo, tako da pisec odda pisavo z delnim naborom namesto izvirne. Popravljeno zaporedje najprej zažene delni nabor in šele nato izvede serializacijo.
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;
Ko je vrstni red popravljen, se klicna koda v ničemer ne spremeni. Ustvarjanje delnega nabora je privzeto vklopljeno takoj, ko je registrirana pisava Unicode TrueType. Registrirate pisavo, začnete dokument, rišete in ga končate, delni nabor pa se zgradi iz glifov, ki ste jih uporabili, preden bajti zapustijo pomnilnik.
Zakaj je en napačno postavljen korak celotna kategorija
Razlog, zakaj je to vredno lekcije in ne le opombe pod črto, je v tem, da EndDoc sproži seznam zaključnih korakov in vsak od njih je občutljiv na svoj položaj glede na zapis. Delni nabor pisav je le eden izmed njih. Izhod PDF/A zahteva tok /CIDSet, ki natančno navaja identifikatorje glifov, prisotne v delnem naboru, kar je omejitev, ki jo določa standard ISO 19005, da lahko validator potrdi, da se vgrajeni program ujema s tistim, kar trdi deskriptor pisave; ta tok se odda v istem zaključnem oknu in je odvisen od tega, da je bil delni nabor zgrajen najprej. Standard PDF/UA-1 po ISO 14289-1 §7.18.3 zahteva, da vsaka stran, ki vsebuje pripombo, deklarira /Tabs z vrednostjo /S, interna rutina z imenom EnsurePDFUATabsOnAnnotatedPages pa ta ključ vtisne v isti fazi. Tudi preverjanja namena izhoda (output intent) se izvajajo tam.
Ista napaka v vrstnem redu, ki je onemogočila delni nabor, je povzročila tudi opustitev ključa vrstnega reda zavihkov PDF/UA na straneh s pripombami, saj se je ta korak nahajal na isti napačni strani zapisa. Orodji veraPDF in PAC poročata o manjkajočem /Tabs /S kot o kršitvi kontrolne točke 21-001 protokola Matterhorn. Tako en sam napačno umeščen klic ni le povečal velikosti datoteke; hkrati je tiho prekršil zahtevo o skladnosti z dostopnostjo, prav tako brez kakršne koli napake. To je nevarnost zaključne faze: njeni koraki si delijo predpogoj in ena sama napaka v vrstnem redu lahko onemogoči več korakov hkrati, medtem ko vsak klic še vedno vrne uspeh.
Kako se dejansko odkrije tihi neuspeh izpisa
Hrošča, ki ne sproži izjeme, ne odkrijemo z zagonom programa. Odkrijemo ga s pregledom izhoda in primerjavo s tistim, kar bi vhod moral ustvariti. Za delni nabor pisav so preverjanja konkretna. Primerjajte velikost izhodne datoteke z grobimi pričakovanji: dokument, ki se je dotaknil le peščice glifov, ne bi smel imeti velikosti celotne pisave. Odprite vgrajeni objekt pisave in preberite njegovo dolžino v bajtih; delni nabor /FontFile2 za latinično pisavo je le majhen del izvorne datoteke. Preberite ime /BaseFont in potrdite prisotnost predpone s šestimi črkami, saj je njena odsotnost neposreden znak, da delni nabor ni bil uporabljen.
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;
Za izhod PDF/A je preverjanje še strožje, saj validator opravi delo namesto vas. Nastavite raven skladnosti in zaženite rezultat skozi veraPDF: manjkajoči /CIDSet ali delni nabor, ki se ne ujema z deskriptorjem, se izpiše kot neuspešno določilo, namesto da bi to opazili sami na oko. Stikala za skladnost, ki vodijo to zaključno delo, so lastnosti dokumenta. PDFACompliance sprejme niz, kot je '2B' za PDF/A-2 stopnje B, PDFUACompliance pa je logična vrednost (boolean), ki vklopi zahteve za označeni PDF in vrstni red zavihkov.
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ženirska lekcija
Iz tega izhajata dve pravili. Prvo je, da se mora vsak zaključni korak, ki spreminja objekte, izvesti pred serializacijo teh objektov, zaključno fazo dokumentnega pogona pa je treba razumeti kot urejen cevovod, kjer je serializacija zadnje dejanje in ne le eno izmed več dejanj. Drugo pravilo je tisto, ki je tukaj vzelo največ časa: za korak izpisa odsotnost napake ni dokaz o uspehu. Rutina, ki zgradi pravilen delni nabor in ga uporabi na napačnem, že zapisanem grafu, ne poroča o ničemer napačnem, saj je bilo z njenega lastnega vidika vse v redu. Preverjanje mora pregledati artefakt, ne le povratne kode. Preverite velikost izhoda, preberite dolžino bajtov vgrajene pisave in njeno predpono /BaseFont ter pustite, da veraPDF oceni izhod PDF/A, kjer manjkajoči /CIDSet spremeni tiho pomanjkljivost v imenovano napako.
Stran ustvarjanja pri delu s pisavami, kako se pisave registrirajo in vgrajujejo za izpise poročil, je obravnavana v našem članku o pisavah in slikah v izpisu poročil. Stran preverjanja veljavnosti, kjer se ti zaključni koraki preverjajo glede na standarde, je zajeta v vodniku o preverjanju veljavnosti PDF/A in PDF/UA. Obe se dopolnjujeta z delom na področju delnih naborov in skladnosti, ki je opisano tukaj in se dostavlja kot del komponente HotPDF Component za Delphi in C++Builder, skupaj z vmesniki API za nalaganje, urejanje, šifriranje in podpisovanje, ki so obravnavani drugje na tem blogu.