Generați un raport, încorporați un font TrueType, iar rezultatul se deschide corect în orice vizualizator încercați. Glifele sunt corecte, textul poate fi selectat, fișierul este valid. Singura problemă este dimensiunea. Un document care a folosit câteva zeci de caractere latine conține întregul font de 350 KB. Un document care a tipărit un paragraf în chineză conține un font CJK de 14 MB în loc de segmentul de jumătate de megabyte de care ar fi avut nevoie. Nu a fost generată nicio excepție, nu a fost înregistrat niciun avertisment, iar fișierul a trecut validarea. Așa arată din exterior o etapă de finalizare dezordonată: nimic nu eșuează, iar singura dovadă este un număr prea mare.
Bugul care l-a produs a existat în HotPDF pentru o singură linie de versiune și de atunci a fost remediat. Merită descris nu ca o notificare de defect, ci ca o lecție, deoarece tipul greșelii este general. Orice motor de documente are o etapă de finalizare care modifică obiectele chiar înainte de a le scrie, iar corectitudinea acelei etape depinde în întregime de ordinea pașilor săi în raport cu serializarea. Puneți un singur pas pe partea greșită a scrierii și acesta nu va face nimic, în mod silențios.
Ce ar trebui să facă subsetarea fonturilor
Un font subsetat este partea dintr-un fișier TrueType pe care un document o folosește efectiv. ISO 32000-1 §9.9 descrie modul în care un program de font încorporat rulează într-un flux referit de descriptorul de font, iar pentru un program TrueType acel flux este /FontFile2 cu un /Length1 care oferă numărul de octeți necomprimați. Subsetarea rescrie tabelele glyf și loca astfel încât să conțină doar glifele la care face referire documentul, renumerotează identificatorii de glife și adaugă la numele /BaseFont un prefix de șase litere, cum ar fi ABCDEF+, pentru a marca fontul ca fiind un subset, exact așa cum cere specificația. O familie de caractere latină care se subsetează la zece sau cincisprezece kiloocteți reprezintă diferența dintre un PDF optimizat și unul care livrează o întreagă familie de caractere de dragul unui singur titlu.
Momentul în care se întâmplă acest lucru contează. Subsetarea nu este o transformare pe care o aplicați octeților deja aflați pe disc. Aceasta modifică graful de obiecte din memorie: reduce conținutul fluxului /FontFile2, corectează /Length1 și rescrie șirul /BaseFont. Toate acestea trebuie să fie la locul lor atunci când serializatorul parcurge graful și emite octeți. Dacă modificările ajung după ce octeții au fost scriși, acestea actualizează obiecte pe care nimeni nu le va citi vreodată.
Simptomul și de ce nu a existat nicio alertă
Comportamentul raportat a fost reprezentat de fonturi complete în fișierul de ieșire, fără niciun diagnostic. Un utilizator care a înregistrat un font Unicode TrueType și a generat un document normal a constatat că obiectul de font încorporat avea aceeași lungime ca fișierul sursă .ttf și că numele /BaseFont nu conținea prefixul de subset de șase litere. Rezultatul nu s-a micșorat niciodată între execuțiile care foloseau zece glife și cele care foloseau zece mii.
Absența oricărei erori este partea care face ca această clasă de buguri să fie costisitoare. O rutină de subsetare care rulează la momentul nepotrivit tot rulează. Aceasta parcurge utilizarea cumulată a punctelor de cod, construiește un subset perfect corect și îl aplică grafului de obiecte din memorie. Intern, treaba este făcută, iar apelul se termină cu succes. Singurul lucru greșit este că graful de obiecte pe care l-a editat nu mai este cel care este scris, deoarece scriitorul a terminat deja. Din punctul de vedere al apelantului, documentul a fost generat și salvat fără probleme, ceea ce este exact impresia pe care o oferă o eroare silențioasă.
Cauza principală a fost ordinea de finalizare
În HotPDF, operațiunea de închidere are loc în interiorul EndDoc. Pasul de subsetare este o rutină internă numită BuildAndApplyUnicodeFontSubset. Aceasta citește setul de puncte de cod utilizate pe document, păstrat într-o hartă de biți pe care calea de emitere a textului o completează pe măsură ce sunt afișate glifele, mapează fiecare punct de cod utilizat prin tabelul stocat în cache de la puncte de cod la glife la un identificator de glifă real și rescrie programul de font în jurul acelei închideri. Atunci când un font Unicode TrueType este înregistrat, calea de emitere setează un bit în setul de puncte de cod utilizate pentru fiecare caracter pe care îl desenează, astfel încât, în momentul în care documentul se închide, motorul știe exact ce glife trebuie să păstreze subsetul.
Defectul a fost că BuildAndApplyUnicodeFontSubset era invocat după ce SaveToStream sau SaveToFile serializase deja documentul. Modificările aduse de subsetator la /FontFile2, valoarea corectată pentru /Length1 și prefixul de șase litere pentru /BaseFont au fost toate calculate în raport cu un graf de obiecte care fusese deja transformat în octeți. Remedierea a fost o reordonare de o singură linie: mutarea apelului de subsetare înainte de serializare, astfel încât scriitorul să emită fontul subsetat în locul celui original. Secvența corectată rulează mai întâi subsetatorul și apoi serializarea.
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;
Cu ordinea corectată, nimic din codul de apelare nu se schimbă. Subsetarea jest activată implicit odată ce un font Unicode TrueType a fost înregistrat. Înregistrați fontul, începeți documentul, desenați și îl finalizați, iar subsetul este construit din glifele pe care le-ați folosit înainte ca octeții să părăsească memoria.
De ce un singur pas greșit reprezintă o întreagă categorie
Motivul pentru care acest lucru merită o lecție, mai degrabă decât o simplă notă de subsol, este că EndDoc emite o listă de pași de închidere și fiecare dintre aceștia este sensibil la poziția sa în raport cu scrierea. Subsetarea fonturilor este unul dintre ei. Fișierul de ieșire PDF/A necesită un flux /CIDSet care enumeră exact identificatorii de glife prezenți în subset, o constrângere pe care ISO 19005 o impune pentru ca un validator să poată confirma că programul încorporat se potrivește cu ceea ce pretinde descriptorul de font; acel flux este emis în aceeași fereastră de finalizare și depinde de construirea prealabilă a subsetului. PDF/UA-1 cere, prin ISO 14289-1 §7.18.3, ca fiecare pagină care conține o adnotare să declare /Tabs cu valoarea /S, iar o rutină internă numită EnsurePDFUATabsOnAnnotatedPages aplică acea cheie în timpul aceleiași etape. Verificările de intenție de ieșire rulează tot acolo.
Aceeași eroare de ordonare care a dezactivat subsetarea a eliminat și cheia de ordine a tabulatoarelor PDF/UA de pe paginile adnotate, deoarece acel pas se afla pe aceeași parte greșită a scrierii. veraPDF și PAC raportează o cheie /Tabs /S lipsă ca o încălcare a punctului de control 21-001 din protocolul Matterhorn. Astfel, un singur apel rătăcit nu doar că a mărit dimensiunea fișierului; a încălcat în mod silențios o cerință de conformitate cu accesibilitatea în același timp, cu aceeași lipsă a oricărei erori. Acesta este riscul unei etape de finalizare: pașii săi partajează o precondiție, iar o singură greșeală de ordonare poate scoate din funcțiune mai mulți pași deodată, în timp ce fiecare apel returnează în continuare succes.
Cum este detectată de fapt o eroare de emitere silențioasă
Un bug care nu generează nicio excepție nu este detectat prin rularea programului. Este detectat prin inspectarea fișierului de ieșire și compararea acestuia cu ceea ce ar fi trebuit să producă datele de intrare. Pentru subsetarea fonturilor, verificările sunt concrete. Comparați dimensiunea fișierului de ieșire cu o așteptare aproximativă: un document care a folosit doar câteva glife nu ar trebui să aibă dimensiunea unei familii de caractere complete. Deschideți obiectul de font încorporat și citiți lungimea sa în octeți; un flux /FontFile2 subsetat pentru un aspect latin reprezintă o mică fracțiune din fișierul sursă. Citiți numele /BaseFont și confirmați că prefixul de șase litere este prezent, deoarece absența sa este un semnal direct că nu a fost aplicat niciun 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;
Pentru fișierul de ieșire PDF/A, verificarea este și mai precisă, deoarece un validator face treaba în locul dvs. Setați nivelul de conformitate și treceți rezultatul prin veraPDF: o cheie /CIDSet lipsă sau un subset care nu se potrivește cu descriptorul este raportat ca o clauză eșuată, în loc să fie lăsat să fie observat cu ochiul liber. Comutatoarele de conformitate care conduc această activitate de finalizare sunt proprietăți ale documentului. PDFACompliance acceptă un șir de caractere precum '2B' pentru PDF/A-2 Nivelul B, iar PDFUACompliance este o valoare booleană care activează cerințele pentru PDF etichetat și ordinea tabulatoarelor.
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;
Lecția de inginerie software
Două reguli reies din aceasta. Prima este că orice pas de finalizare care modifică obiecte trebuie să ruleze înainte ca acele obiecte să fie serializate, iar etapa de închidere a unui motor de documente ar trebui interpretată ca o conductă ordonată în care serializarea este ultima acțiune, nu o acțiune printre altele. A doua este cea care a costat cel mai mult timp aici: pentru un pas de emitere, absența unei erori nu este o dovadă de succes. O rutină care construiește subsetul corect și îl aplică grafului greșit, deja scris, nu raportează nimic în neregulă, deoarece din propria perspectivă totul a fost în regulă. Verificarea trebuie să analizeze artefactul, nu codul de returnare. Verificați dimensiunea de ieșire, citiți lungimea în octeți a fontului încorporat și prefixul său /BaseFont și lăsați veraPDF să evalueze rezultatul PDF/A, unde o cheie /CIDSet lipsă transformă un deficit silențios într-un eșec nominal.
Partea de generare a gestionării fonturilor, modul în care familiile de caractere sunt înregistrate și încorporate pentru rezultatul raportului, este acoperită în articolul nostru despre fonturi și imagini în rezultatul raportului. Partea de validare, în care acești pași de finalizare sunt verificați în raport cu standardele, este acoperită în ghidul despre validarea PDF/A și PDF/UA. Ambele se corelează cu activitatea de subsetare și conformitate descrisă aici, care este livrată ca parte a HotPDF Component pentru Delphi și C++Builder, alături de API-urile de încărcare, editare, criptare și semnare acoperite în alte părți ale acestui blog.