Generer en rapport, bygg inn en TrueType-font, og utdataene åpnes riktig i alle visningsprogrammer du prøver. Glyfene er riktige, teksten kan velges, og filen er gyldig. Det eneste som er feil, er størrelsen. Et dokument som brukte noen få dusin latinske tegn, bærer med seg hele fonten på 350 KB. Et dokument som skrev ut et avsnitt med kinesisk, bærer med seg en CJK-font på 14 MB i stedet for den halve megabyten det burde trenge. Ingen unntak ble utløst, ingen advarsel ble logget, og filen besto valideringen. Dette er hvordan et feilordnet sluttføringstrinn ser ut fra utsiden: ingenting feiler, og det eneste beviset er et tall som er for stort.
Feilen som forårsaket det, fantes i HotPDF i én utgivelseslinje og har siden blitt rettet. Det er verdt å skrive om dette, ikke som en feilmelding, men som en lærdom, fordi formen på feilen er generell. Alle dokumentmotorer har en sluttføringsfase som endrer objekter rett før de skrives, og nøyaktigheten til denne fasen avhenger helt av rekkefølgen på trinnene i forhold til serialiseringen. Hvis et trinn havner på feil side av skrivingen, gjør det ingenting, i det stille.
Hva font-subsetting er ment å gjøre
En subset-font er den delen av en TrueType-fil som et dokument faktisk bruker. ISO 32000-1 §9.9 beskriver hvordan et innebygd fontprogram ligger i en strøm referert til av fontbeskrivelsen, og for et TrueType-program er denne strømmen /FontFile2 med en /Length1 som angir ukomprimert antall byte. Subsetting skriver om tabellene glyf og loca slik at de bare inneholder glyfene dokumentet refererer til, renummererer glyf-identifikatorene, og setter et prefiks på /BaseFont-navnet med en tagg på seks bokstaver som ABCDEF+ for å markere fonten som et subset, nøyaktig slik spesifikasjonen krever. En latinsk fonttype som reduseres to ti eller femten kilobyte er forskjellen mellom en slank PDF og en som sender med en hel skrifttype bare for én enkelt overskrift.
Tidspunktet dette skjer på er viktig. Subsetting er ikke en transformasjon du bruker på byte som allerede ligger på disken. Den redigerer objektgrafen i minnet: den krymper innholdet i /FontFile2-strømmen, retter opp /Length1 og skriver om /BaseFont-strengen. Alt dette må være på plass når serialisereren går gjennom grafen og sender ut byte. Hvis endringene skjer etter at bytene er skrevet, oppdaterer de objekter ingen noen gang vil lese.
Symptomet, og hvorfor ingenting ga beskjed
Den rapporterte oppførselen var fulle fonter i utdataene uten noen diagnosemelding. En bruker som registrerte en Unicode TrueType-font og genererte et vanlig dokument, oppdaget at det innebygde fontobjektet hadde samme lengde som kildens .ttf-fil, og at /BaseFont-navnet ikke hadde noe subset-prefiks med seks bokstaver. Utdataene ble aldri mindre mellom kjøringer som brukte ti glyfer og kjøringer som brukte ti tusen.
Fraværet av feilmeldinger er det som gjør denne typen feil kostbar. En rutine for subsetting som kjører på feil tidspunkt, kjører fortsatt. Den går gjennom den akkumulerte bruken av kodepunkter, bygger et helt riktig subset og bruker det på objektgrafen i minnet. Internt er arbeidet gjort, og kallet returnerer uten problemer. Det eneste som er feil, er at objektgrafen den redigerte ikke lenger er den som skrives, fordi skriveren allerede er ferdig. Fra kallerens ståsted ble dokumentet opprettet og lagret uten problemer, noe som er akkurat det inntrykket en stille feil gir.
Årsaken var rekkefølgen på sluttføringen
I HotPDF skjer avslutningsarbeidet i EndDoc. Subsettingtrinnet er en intern rutine som heter BuildAndApplyUnicodeFontSubset. Den leser dokumentets sett med brukte kodepunkter, som oppbevares i en bitmap som tekstutdatabanen fyller etter hvert som glyfer vises, mapper hvert brukte kodepunkt via den bufrede kodepunkt-til-glyf-tabellen til en faktisk glyf-identifikator, og skriver om fontprogrammet rundt dette. Når en Unicode TrueType-font registreres, setter utdatabanen en bit i settet for brukte kodepunkter for hvert tegn den tegner, slik at motoren vet nøyaktig hvilke glyfer subsetet må beholde når dokumentet lukkes.
Feilen var at BuildAndApplyUnicodeFontSubset ble kalt etter at SaveToStream eller SaveToFile allerede hadde serialisert dokumentet. Endringene subsetteren gjorde i /FontFile2, den korrigerte /Length1 og /BaseFont-prefikset med seks bokstaver ble alle beregnet mot en objektgraf som allerede var gjort om til byte. Løsningen var en omorganisering på én linje: Flytt subset-kallet før serialiseringen, slik at skriveren sender ut den subsettede fonten i stedet for den opprinnelige. Den korrigerte sekvensen kjører subsetteren først og serialiserer etterpå.
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;
Med rekkefølgen korrigert, endres ingenting i den kallende koden. Subsetting er aktivert som standard så snart en Unicode TrueType-font har blitt registrert. Du registrerer fonten, starter dokumentet, tegner og avslutter det, og subsetet bygges fra glyfene du brukte før bytene forlater minnet.
Hvorfor ett feilplassert trinn er en hel kategori
Grunnen til at dette er verdt en leksjon i stedet for en fotnote, er at EndDoc utfører en liste med avslutningstrinn, og hver enkelt av dem er avhengig av plasseringen i forhold til skrivingen. Font-subsetting er ett. PDF/A-utdata krever en /CIDSet-strøm som ramser opp nøyaktig glyf-identifikatorene som finnes i subsetet, en begrensning ISO 19005 pålegger slik at en validator kan bekrefte at det innebygde programmet samsvarer med det fontbeskrivelsen krever; denne strømmen sendes ut i det samme sluttføringsvinduet og avhenger av at subsetet ble bygget først. PDF/UA-1 krever, i henhold til ISO 14289-1 §7.18.3, at alle sider som har en annotering deklarerer /Tabs med verdien /S, og en intern rutine kalt EnsurePDFUATabsOnAnnotatedPages stempler denne nøkkelen i den samme fasen. Kontroller for output-intent kjøres også der.
Den samme rekkefølgefeilen som deaktiverte subsetting, droppet også PDF/UA-tabulaturnøkkelen på annoterte sider, fordi dette trinnet lå på samme feil side av skrivingen. veraPDF og PAC rapporterer en manglende /Tabs /S som et brudd på Matterhorn-protokollens sjekkpunkt 21-001. Så et enkelt feilplassert kall økte ikke bare filstørrelsen; det brøt samtidig et samsvarskrav for universell utforming, uten at det ble oppgitt noen feil. Det er faren med en sluttføringsfase: trinnene deler en forutsetning, og en enkelt rekkefølgefeil kan sette flere av dem ut av spill samtidig, mens alle kall fortsatt rapporterer suksess.
Hvordan en stille utskriftsfeil faktisk fanges opp
En feil som ikke utløser unntak, fanges ikke opp ved å kjøre programmet. Den fanges opp ved å inspisere utdataene og sammenligne dem med hva inndataene burde ha produsert. For font-subsettinger sjekkene konkrete. Sammenlign utdatafilens størrelse med en grov forventning: Et dokument som bare brukte en håndfull glyfer, bør ikke ha størrelsen til en hel skrifttype. Åpne det innebygde fontobjektet og les bytelengden; en subsettet /FontFile2 for en latinsk font er en liten brøkdel av kildefilen. Les /BaseFont-navnet og bekreft at prefikset på seks bokstaver er til stede, da fraværet er et direkte tegn på at intet subset ble tatt i bruk.
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;
For PDF/A-utdata er sjekken enda skarpere, fordi en validator gjør jobben for deg. Angi samsvarsnivået og kjør resultatet gjennom veraPDF: En manglende /CIDSet, eller et subset som ikke samsvarer med beskrivelsen, rapporteres som en feilet klausul i stedet for at du må oppdage det selv. Samsvarsbryterne som styrer dette sluttføringsarbeidet er egenskaper på dokumentet. PDFACompliance tar en streng som '2B' for PDF/A-2 nivå B, og PDFUACompliance er en boolsk verdi som aktiverer kravene til tagget PDF og tabulatorrekkefølge.
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;
Lærdommen for utviklere
To regler kan utledes fra dette. Den første er at alle sluttføringstrinn som endrer objekter må kjøre før disse objektene serialiseres, og avslutningsfasen til en dokumentmotor bør leses som en ordnet rørledning der serialisering er den siste handlingen, ikke én handling blant flere. Den andre er den som kostet mest tid her: For et utskriftstrinn er fraværet av en feil ikke et bevis på suksess. En rutine som bygger det riktige subsetet og bruker det på feil, allerede skrevet graf, rapporterer ingenting feil, fordi alt så bra ut fra dens eget perspektiv. Valideringen må se på selve produktet, ikke returkoden. Sjekk utdatastørrelsen, les den innebygde fontens bytelengde og dens /BaseFont-prefiks, og la veraPDF vurdere PDF/A-utdataene der en manglende /CIDSet forvandler et stille avvik til en konkret feilmelding.
Produsentsiden av fonthåndtering, hvordan skrifttyper registreres og bygges inn for rapportgenerering, er dekket i vår artikkel om fonter og bilder i rapportgenerering. Valideringssiden, der disse sluttføringstrinnene sjekkes mot standardene, is covered in gjennomgangen av PDF/A- og PDF/UA-validering. Begge henger sammen med subsetting- og samsvarsarbeidet som beskrives her, som leveres som en del av HotPDF-komponenten for Delphi og C++Builder sammen med API-ene for lasting, redigering, kryptering og signering som er dekket andre steder på denne bloggen.