Sugeneruokite ataskaitą, įterpkite TrueType šriftą, ir gautas rezultatas bus teisingai atidaromas bet kurioje peržiūros programoje. Glifai rodomi teisingai, tekstą galima pažymėti, o failas yra galiojantis. Vienintelė problema yra failo dydis. Dokumentas, kuriame panaudota vos pora dešimčių lotyniškų simbolių, savyje neša visą 350 KB šriftą. Dokumentas, kuriame atspausdinta kinų kalbos pastraipa, nešiojasi 14 MB CJK šriftą vietoj pusės megabaito dalies, kurios jam turėtų pakakti. Nebuvo pateikta jokia išimtis, nebuvo užregistruotas joks įspėjimas, o failas sėkmingai praėjo patikrą. Taip iš išorės atrodo neteisinga baigiamojo žingsnio tvarka: niekas nesugenda, o vienintelis įrodymas yra per didelis skaičius.
Ši klaida, sukūrusi tokią situaciją, viena programos versijų serija egzistavo HotPDF komponente ir nuo to laiko jau buvo ištaisyta. Apie ją verta parašyti ne kaip apie pranešimą apie defektą, o kaip apie pamoką, nes tokios klaidos pobūdis yra bendras. Bet kuris dokumentų generavimo variklis turi baigiamąjį etapą, kuris keičia objektus prieš pat juos įrašant, o šio etapo teisingumas visiškai priklauso nuo jo žingsnių tvarkos serializavimo atžvilgiu. Jei vienas žingsnis atsidurs neteisingoje įrašymo pusėje, jis tyliai nieko neatliks.
Ką turėtų atlikti šriftų poaibių kūrimas
Šrifto poaibis yra ta TrueType failo dalis, kurią dokumentas faktiškai naudoja. ISO 32000-1 §9.9 aprašo, kaip įterpta šrifto programa veikia sraute, kurį nurodo šrifto deskriptorius, o TrueType programai šis srautas yra /FontFile2 su /Length1, nurodančiu nesuspaustų baitų skaičių. Poaibių kūrimas perrašo glyf ir loca lenteles taip, kad jose būtų tik tie glifai, kuriuos naudoja dokumentas, pakeičia glifų identifikatorių numeraciją ir prideda šešių raidžių prefiksą prie /BaseFont pavadinimo, pavyzdžiui, ABCDEF+, kad pažymėtų šriftą kaip poaibį, tiksliai taip, kaip reikalauja specifikacija. Lotynų kalbos šrifto poaibis, sumažintas iki dešimties ar penkiolikos kilobaitų, yra skirtumas tarp lengvo PDF failo ir tokio, kuris siunčia visą šriftą dėl vienos antraštės.
Momentas, kada tai įvyksta, yra labai svarbus. Poaibių kūrimas nėra transformacija, taikoma diske jau esantiems baitams. Jis redaguoja objektų grafiką atmintyje: sumažina /FontFile2 srauto turinį, pataiso /Length1 ir perrašo /BaseFont eilutę. Visa tai turi būti paruošta, kai serializatorius eina per grafiką ir generuoja baitus. Jei redagavimai atliekami po to, kai baitai jau įrašyti, jie atnaujina objektus, kurių niekas niekada neperskaitys.
Simptomas ir kodėl nekilo jokių klaidų
Pastebėtas elgesys buvo pilnų šriftų pateikimas rezultate be jokios diagnostikos. Naudotojas, kuris užregistravo Unicode TrueType šriftą ir sukūrė įprastą dokumentą, nustatė, kad įterptasis šrifto objektas buvo tokio paties ilgio kaip ir šaltinio .ttf failas, o /BaseFont pavadinime nebuvo šešių raidžių poaibio prefikso. Rezultato failo dydis niekada nesumažėjo, nepriklausomai nuo to, ar buvo naudojama dešimt glifų, ar dešimt tūkstančių.
Klaidų nebuvimas yra būtent ta dalis, dėl kurios ši klaidų klasė yra brangi. Poaibių kūrimo rutina, kuri paleidžiama netinkamu laiku, vis tiek veikia sėkmingai. Ji pereina per sukauptą naudotų kodų rinkinį, sukuria visiškai teisingą poaibį ir pritaiko jį objektų grafikui atmintyje. Vidinė dalis atlieka darbą ir iškvietimas grąžina sėkmės rezultatą. Vienintelė problema yra ta, kad objektų grafikas, kurį ji redagavo, nebėra tas, kuris yra įrašomas, nes įrašymo programa jau baigė savo darbą. Iškvėpėjo požiūriu, dokumentas buvo sukurtas ir išsaugotas be jokių nesklandumų, o tai yra būtent toks įspūdis, kokį sukuria tylus gedimas.
Pagrindinė priežastis buvo baigiamųjų žingsnių tvarka
HotPDF komponente baigiamasis darbas vyksta EndDoc procedūroje. Poaibių kūrimo žingsnis yra vidinė rutina, vadinama BuildAndApplyUnicodeFontSubset. Ji nuskaito kiekvienam dokumentui naudojamų kodų rinkinį, saugomą bitų lauke, kurį teksto išvesties kelias užpildo atvaizduojant glifus, susieja kiekvieną naudotą kodą per talpykloje esančią kodų-glifų lentelę su tikruoju glifo identifikatoriumi ir perrašo šrifto programą. Kai užregistruojamas Unicode TrueType šriftas, išvesties kelias nustato bitą kiekvienam nupieštam simboliui naudotų kodų rinkinyje, todėl iki dokumento uždarymo variklis tiksliai žino, kuriuos glifus poaibis privalo išsaugoti.
Defektas buvo tas, kad BuildAndApplyUnicodeFontSubset buvo iškviečiama po to, kai SaveToStream arba SaveToFile jau buvo serializavę dokumentą. Poaibių kūrimo programos atlikti /FontFile2 pakeitimai, pataisytas /Length1 ir šešių raidžių /BaseFont prefiksas buvo apskaičiuoti objektų grafikui, kuris jau buvo pavertas baitais. Sprendimas buvo vienos eilutės tvarkos pakeitimas: perkelti poaibio iškvietimą prieš serializavimą, kad įrašymo programa išvestų poaibį, o ne originalų šriftą. Ištaisyta seka pirmiausia paleidžia poaibio kūrimą, o tik po to atlieka serializavimą.
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;
Ištaisius tvarką, niekas iškvietimo kode nesikeičia. Poaibių kūrimas yra įjungtas pagal nutylėjimą, kai tik užregistruojamas Unicode TrueType šriftas. Jūs užregistruojate šriftą, pradedate dokumentą, piešiate ir jį užbaigiate, o poaibis sukuriamas iš glifų, kuriuos panaudojote prieš baitams paliekant atmintį.
Kodėl vienas klaidingas žingsnis yra visa kategorija
Priežastis, kodėl tai verta pamokos, o ne trumpos pastabos, yra ta, kad EndDoc atlieka daugybę baigiamųjų žingsnių, ir kiekvienas iš jų yra jautrus savo pozicijai įrašymo atžvilgiu. Šriftų poaibių kūrimas yra tik vienas iš jų. PDF/A išvestis reikalauja /CIDSet srauto, kuris išvardija tiksliai poaibyje esančius glifų identifikatorius – tai yra ISO 19005 nustatytas apribojimas, kad validatorius galėtų patvirtinti, jog įterpta programa atitinka šrifto deskriptoriaus reikalavimus; šis srautas generuojamas tame pačiame baigiamajame lange ir priklauso nuo to, ar poaibis buvo sukurtas pirmiausia. PDF/UA-1 reikalauja, pagal ISO 14289-1 §7.18.3, kad kiekvienas puslapis su anotacija deklaruotų /Tabs su reikšme /S, o vidinė rutina, vadinama EnsurePDFUATabsOnAnnotatedPages, pažymi šį raktą tame pačiame etape. Ten pat atliekami ir išvesties tikslo (output intent) patikrinimai.
Ta pati tvarkos klaida, kuri išjungė poaibių kūrimą, taip pat praleido PDF/UA tabuliacijos tvarkos raktą anotacijas turinčiuose puslapiuose, nes šis žingsnis buvo toje pačioje neteisingoje įrašymo pusėje. veraPDF ir PAC praneša apie trūkstamą /Tabs /S kaip apie Matterhorn protokolo kontrolinio taško 21-001 pažeidimą. Taigi, vienas netinkamoje vietoje esantis iškvietimas ne tik padidino failo dydį, bet tuo pat metu tyliai pažeidė prieinamumo atitikties reikalavimą be jokių klaidų pranešimų. Tai yra baigiamojo etapo pavojus: jo žingsniai dalijasi ta pačia išankstine sąlyga, o viena tvarkos klaida gali sugadinti kelis iš jų vienu metu, nors kiekvienas iškvietimas vis tiek grąžina sėkmės rezultatą.
Kaip iš tikrųjų nustatomas tylus išvesties gedimas
Klaida, kuri nesukelia išimties, nėra nustatoma tiesiog paleidžiant programą. Ji aptinkama tikrinant išvesties failą ir lyginant jį su tuo, ką turėjo sugeneruoti įvestis. Šrifto poaibių kūrimui patikrinimai yra konkretūs. Palyginkite išvesties failo dydį su apytikriais lūkesčiais: dokumentas, kuriame panaudoti keli glifai, neturėtų būti viso šrifto dydžio. Atidarykite įterptą šrifto objektą ir perskaitykite jo baitų ilgį; lotyniškų simbolių šrifto poaibis /FontFile2 sudaro tik nedidelę šaltinio failo dalį. Perskaitykite /BaseFont pavadinimą ir įsitikinkite, kad yra šešių raidžių prefiksas, nes jo nebuvimas yra tiesioginis signalas, kad joks poaibis nebuvo pritaikytas.
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 išvesties atveju patikra yra dar griežtesnė, nes validatorius atlieka darbą už jus. Nustatykite atitikties lygį ir paleiskite rezultatą per veraPDF: trūkstamas /CIDSet arba poaibis, kuris neatitinka deskriptoriaus, yra pranešamas kaip nepavykusi sąlyga, o ne paliekamas pastebėti plika akimi. Atitikties jungikliai, kurie valdo šį baigiamąjį darbą, yra dokumento savybės. PDFACompliance priima eilutę, pavyzdžiui, '2B', nurodančią PDF/A-2 Level B atitiktį, o PDFUACompliance yra loginis kintamasis, kuris įjungia pažymėto (tagged) PDF ir tabuliacijos tvarkos reikalavimus.
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žinerinė pamoka
Iš to seka dvi taisyklės. Pirmoji ir svarbiausia yra ta, kad bet koks baigiamasis žingsnis, kuris keičia objektus, tani būti paleistas prieš šių objektų serializavimą, o dokumentų variklio uždarymo etapas turėtų būti traktuojamas kaip nuoseklus konvejeris, kuriame serializavimas yra paskutinis veiksmas, o ne vienas iš daugelio. Antroji taisyklė yra ta, kuri čia pareikalavo daugiausiai laiko: išvesties žingsniui klaidos nebuvimas nėra sėkmės įrodymas. Rutina, kuri sukuria teisingą poaibį ir pritaiko jį neteisingam, jau įrašytam grafikui, nepraneša apie jokias problemas, nes jos pačios požiūriu viskas buvo gerai. Verifikavimas turi tikrinti patį rezultatą (artefaktą), o ne grąžinamą kodą. Patikrinkite išvesties dydį, perskaitykite įterpto šrifto baitų ilgį bei jo /BaseFont prefiksą ir leiskite veraPDF įvertinti PDF/A išvestį, kur trūkstamas /CIDSet paverčia tylų trūkumą konkrečiu klaidos pranešimu.
Šriftų vaizdo kūrimo pusė, kaip šriftai registruojami ir įterpiami ataskaitų išvestyje, yra aprašyta mūsų straipsnyje apie šriftus ir vaizdus ataskaitų išvestyje. Validavimo pusė, kurioje šie baigiamieji žingsniai tikrinami pagal standartus, yra aprašyta PDF/A ir PDF/UA validavimo apžvalgoje. Abu šie aspektai derinami su poaibių kūrimo ir atitikties darbais, aprašytais čia, kurie yra platinami kaip HotPDF Component, skirto Delphi ir C++Builder, dalis kartu su dokumentų įkėlimo, redagavimo, šifravimo ir pasirašymo API, aprašytais kitose šio tinklaraščio dalyse.