Technical Article

EndDoc-fejlen, der lydløst deaktiverede undersæt af skrifttyper

Generer en rapport, integrer en TrueType-skrifttype, og outputtet åbnes korrekt i alle fremvisere, du prøver. Glyfferne er rigtige, teksten kan markeres, og filen er gyldig. Det eneste, der er galt, er størrelsen. Et dokument, der brugte et par dusin latinske tegn, bærer hele skrifttypen på 350 KB. Et dokument, der udskrev et afsnit på kinesisk, bærer en 14 MB CJK-skrifttype i stedet for den del på en halv megabyte, som det burde have brug for. Der blev ikke udløst nogen undtagelse, der blev ikke logget nogen advarsel, og filen bestod valideringen. Dette er, hvordan et fejlordnet afslutningstrin ser ud udefra: intet fejler, og det eneste bevis er et tal, der er for stort.

Fejlen, der forårsagede det, eksisterede i HotPDF i en enkelt versionslinje og er siden blevet rettet. Det er værd at skrive om, ikke som en fejlmeddelelse, men som en lektie, fordi fejlens form er generel. Enhver dokumentmotor har en afslutningsfase, der ændrer objekter, lige før de skrives, og korrektheden af denne fase afhænger fuldstændigt af rækkefølgen af dens trin i forhold til serialiseringen. Hvis et trin lander på den forkerte side af skrivningen, gør det ingenting, lydløst.

Hvad undersæt af skrifttyper er beregnet til at gøre

En skrifttype i undersæt (subset) er den del af en TrueType-fil, som et dokument faktisk bruger. ISO 32000-1 §9.9 beskriver, hvordan et indlejret skrifttypeprogram ligger i en strøm, der refereres til af skrifttypedescriptoren, og for et TrueType-program er denne strøm /FontFile2 med en /Length1, der angiver det ukomprimerede byteantal. Undersæt (subsetting) genskriver tabellerne glyf og loca, så de kun indeholder de glyffer, som dokumentet refererer til, renummererer glyf-identifikatorerne og indsætter et præfiks foran /BaseFont-navnet med et mærke på seks bogstaver, f.eks. ABCDEF+, for at markere skrifttypen som et undersæt, præcis som specifikationen kræver. En latinsk skrifttype, der reduceres til ti eller femten kilobytes, is forskellen mellem en slank PDF og en, der leverer en hel skrifttype for en enkelt overskrifts skyld.

Det tidspunkt, hvor dette sker, er afgørende. Undersæt er ikke en transformation, du anvender på bytes, der allerede er på disken. Det redigerer objektgrafen i hukommelsen: det reducerer indholdet af /FontFile2-strømmen, retter /Length1 og genskriver /BaseFont-strengen. Alt dette skal være på plads, når serialiseringen gennemgår grafen og afgiver bytes. Hvis redigeringerne lander, efter at bytes er skrevet, opdaterer de objekter, som ingen nogensinde vil læse.

Symptomet, og hvorfor intet klagede

Den rapporterede adfærd var fulde skrifttyper i outputtet uden nogen diagnosticering. En bruger, der registrerede en Unicode TrueType-skrifttype og oprettede et normalt dokument, opdagede, at det indlejrede skrifttypeobjekt havde samme længde som kilde-.ttf-filen, og at /BaseFont-navnet ikke bar noget undersætspræfiks på seks bogstaver. Outputtet blev aldrig mindre mellem kørsler, der brugte ti glyffer, og kørsler, der brugte ti tusinde.

Fraværet af fejl er den del, der gør denne type fejl dyr. En undersætsrutine, der kører på det forkerte tidspunkt, kører stadig. Den gennemgår det akkumulerede brug af kodepunkter, bygger et helt korrekt undersæt og anvender det på objektgrafen i hukommelsen. Internt er arbejdet udført, og kaldet returnerer fejlfrit. Det eneste, der er galt, er, at den objektgraf, den redigerede, ikke længere er den ting, der skrives, fordi skriveren allerede er færdig. Set fra kalderens synspunkt blev dokumentet oprettet og gemt uden problemer, hvilket er præcis det indtryk, et lydløst svigt giver.

Årsagen var rækkefølgen af afslutningen

I HotPDF sker afslutningsarbejdet inde i EndDoc. Undersætstrinnet is en intern rutine ved navn BuildAndApplyUnicodeFontSubset. Den læser det dokumentspecifikke sæt af brugte kodepunkter, som gemmes i en bitmap, som tekstafgivelsesstien udfylder, efterhånden som glyffer vises, kortlægger hvert brugt kodepunkt via den cachede tabel over kodepunkter-til-glyffer til en reel glyf-identifikator og genskriver skrifttypeprogrammet omkring denne lukning. Når en Unicode TrueType-skrifttype registreres, sætter afgivelsesstien en bit i sættet for brugte kodepunkter for hvert tegn, den tegner, så når dokumentet lukkes, motoren ved præcis, hvilke glyffer undersættet skal beholde.

Fejlen var, at BuildAndApplyUnicodeFontSubset blev kaldt, efter at SaveToStream eller SaveToFile allerede havde serialiseret dokumentet. Undersætsfunktionens redigeringer af /FontFile2, dens rettede /Length1 og præfikset på seks bogstaver til /BaseFont blev alle beregnet mod en objektgraf, der allerede var blevet omdannet til bytes. Løsningen var en omorganisering på én linje: Flyt undersætskaldet før serialiseringen, så skriveren afgiver den undersatte skrifttype i stedet for den oprindelige. Den rettede sekvens kører undersætsfunktionen først og serialiserer bagefter.

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;

Når rækkefølgen er rettet, ændres intet i den kaldende kode. Undersæt er aktiveret som standard, så snart en Unicode TrueType-skrifttype er blevet registreret. Du registrerer skrifttypen, påbegynder dokumentet, tegner og afslutter det, og undersættet bygges ud fra de glyffer, du har brugt, før bytes forlader hukommelsen.

Hvorfor ét forkert placeret trin er en hel kategori

Grunden til, at dette er værd at lære af frem for en fodnote, er, at EndDoc afgiver en liste over afslutningstrin, og hvert enkelt af dem er følsomt over for sin placering i forhold til skrivningen. Undersæt af skrifttyper er ét. PDF/A-output kræver en /CIDSet-strøm, der opregner præcis de glyf-identifikatorer, der er til stede i undersættet, en begrænsning, som ISO 19005 pålægger, så en validator kan bekræfte, at det indlejrede program matcher det, som skrifttypedescriptoren hævder; denne strøm afgives i det samme afslutningsvindue og afhænger af, at undersættet er bygget først. PDF/UA-1 kræver, i henhold til ISO 14289-1 §7.18.3, at hver side, der bærer en annotering, erklærer /Tabs med værdien /S, og en intern rutine ved navn EnsurePDFUATabsOnAnnotatedPages stempler denne nøgle i den samme fase. Kontroller af output-intent kører også her.

Den samme rækkefølgefejl, som deaktiverede undersæt, fjernede også PDF/UA-tabulatorrækkefølge-nøglen på annoterede sider, fordi dette trin lå på den samme forkerte side af skrivningen. veraPDF og PAC rapporterer en manglende /Tabs /S som en overtrædelse af Matterhorn-protokollens kontrolpunkt 21-001. Så et enkelt forkert placeret kald oppustede ikke blot filstørrelsen; det brød samtidig lydløst et krav om overensstemmelse med tilgængelighed, med den samme mangel på fejl. Det er faren ved en afslutningsfase: dens trin deler en forudsætning, og en enkelt rækkefølgefejl kan tage flere af dem ud på én gang, mens hvert kald stadig returnerer succes.

Hvordan et lydløst afgivelsessvigt faktisk opdages

En fejl, der ikke udløser nogen undtagelse, opdages ikke ved at køre programmet. Den opdages ved at inspicere outputtet og sammenligne det med, hvad inputtet burde have produceret. For undersæt af skrifttyper er kontrollerne konkrete. Sammenlign outputfilens størrelse med en grov forventning: Et dokument, der berørte en håndfuld glyffer, bør ikke have størrelsen af en hel skrifttype. Åbn det indlejrede skrifttypeobjekt og læs dets byte-længde; en undersat /FontFile2 til en latinsk skrifttype er en lille brøkdel af kildefilen. Læs /BaseFont-navnet og bekræft, at præfikset på seks bogstaver er til stede, fordi dets fravær er et direkte signal om, at der ikke blev anvendt noget undersæt.

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-output er kontrollen endnu skarpere, fordi en validator gør arbejdet for dig. Indstil overensstemmelsesniveauet og kør resultatet gennem veraPDF: en manglende /CIDSet, eller et undersæt, der ikke matcher descriptoren, rapporteres som en mislykket klausul frem for at blive overladt til dig at bemærke med det blotte øje. De overensstemmelseskontakter, der driver dette afslutningsarbejde, er egenskaber på dokumentet. PDFACompliance tager een streng som f.eks. '2B' for PDF/A-2 Level B, og PDFUACompliance er en boolean, der aktiverer kravene til tagget PDF og tabulatorrækkefø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;

Den ingeniørmæssige lektie

To regler udspringer af dette. Den første er, at ethvert afslutningstrin, der ændrer objekter, skal køre, før disse objekter serialseres, og afslutningsfasen i en dokumentmotor bør læses som en ordnet pipeline, hvor serialisering er den sidste handling, ikke én handling blandt flere. Den anden er den, der kostede mest tid her: for et afgivelsestrin er fraværet af en fejl ikke et bevis på succes. En rutine, der bygger det rigtige undersæt og anvender det på den forkerte, allerede skrevne graf, rapporterer intet forkert, fordi intet var forkert fra dens eget perspektiv. Verificeringen skal se på artefakten, ikke på returkoden. Kontroller outputstørrelsen, læs den indlejrede skrifttypes byte-længde og dens /BaseFont-præfiks, og lad veraPDF dømme PDF/A-outputtet, hvor en manglende /CIDSet gør en lydløs mangel til en navngiven fejl.

Producentsiden af skrifttypehåndtering, hvordan skrifttyper registreres og indlejres til rapportoutput, er dækket i vores artikel om skrifttyper og billeder i rapportoutput. Valideringssiden, hvor disse afslutningstrin kontrolleres mod standarderne, er dækket i gennemgangen af PDF/A- og PDF/UA-validering. Begge parres med undersæts- og overensstemmelsesarbejdet beskrevet her, som leveres som en del af HotPDF Component til Delphi og C++Builder sammen med API'erne til indlæsning, redigering, kryptering og signering, der er dækket andre steder på denne blog.