Genereer een rapport, sluit een TrueType-lettertype in, en de uitvoer opent correct in elke viewer die u probeert. De glyphs kloppen, de tekst is selecteerbaar, het bestand is geldig. Het enige wat niet klopt, is de grootte. Een document dat een paar dozijn Latijnse tekens gebruikte, bevat het volledige lettertype van 350 KB. Een document dat een alinea Chinees afdrukte, bevat een CJK-lettertype van 14 MB in plaats van het deel van een halve megabyte dat het eigenlijk nodig zou moeten hebben. Er is geen uitzondering gegenereerd, er is geen waarschuwing gelogd en het bestand is goedgekeurd bij de validatie. Dit is hoe een verkeerd geordende afrondingsstap er van buitenaf uitziet: er mislukt niets en het enige bewijs is een getal dat te groot is
Het is het opschrijven waard als les, want de vorm van de fout is algemeen. Elke document-engine heeft een finalisatiefase die objecten muteert vlak voordat hij ze wegschrijft, en de correctheid van die fase hangt volledig af van de volgorde van de stappen ten opzichte van de serialisatie. Zet je één stap aan de verkeerde kant van het schrijven, dan doet hij niets, stilletjes
Wat font-subsetting hoort te doen
Een subset-lettertype is het deel van een TrueType-bestand dat daadwerkelijk door een document wordt gebruikt. ISO 32000-1 §9.9 beschrijft hoe een ingesloten lettertypeprogramma meeloopt in een stream waarnaar wordt verwezen door de lettertypedescriptor, en voor een TrueType-programma is die stream /FontFile2 met een /Length1 die het ongecomprimeerde aantal bytes aangeeft. Subsetting herschrijft de glyf- en loca-tabellen zodat ze alleen de glyphs bevatten waarnaar het document verwijst, hernummert de glyph-identificaties en voorziet de /BaseFont-naam van een voorvoegsel van zes letters zoals ABCDEF+ om het lettertype als een subset te markeren, precies zoals de specificatie vereist. Een Latijns lettertype dat wordt verkleind tot tien of vijftien kilobyte is het verschil tussen een slanke PDF and een PDF die een volledig lettertype meestuurt voor één enkele kop
Het moment waarop dit gebeurt is belangrijk. Subsetting is geen transformatie die u toepast op bytes die al op de schijf staan. Het bewerkt de objectgrafiek in het geheugen: het verkleint de inhoud van de /FontFile2-stream, corrigeert /Length1 en herschrijft de /BaseFont-string. Dit moet allemaal op zijn plaats zijn wanneer de serializer door de grafiek loopt en bytes genereert. Als de bewerkingen plaatsvinden nadat de bytes zijn geschreven, worden objecten bijgewerkt die niemand ooit zal lezen
Het symptoom, en waarom er geen meldingen waren
Valt subsetting aan de verkeerde kant van het schrijven, dan is het symptoom: volledige lettertypen in de uitvoer zonder enige diagnose. Registreer een Unicode TrueType-lettertype en produceer een normaal document, en het ingesloten lettertype-object komt eruit met dezelfde lengte als het bron-.ttf-bestand, zonder subset-voorvoegsel van zes letters bij de /BaseFont-naam. De uitvoer wordt nooit kleiner tussen runs die tien glyphs gebruiken en runs die er tienduizend gebruiken
De afwezigheid van fouten is het deel dat dit type fout duur maakt. Een subsettingroutine die op het verkeerde moment draait, draait toch. Hij loopt over het verzamelde codepointgebruik heen, bouwt een perfect correcte subset en past die toe op de objectgrafiek in het geheugen. Intern is het werk gedaan en keert de aanroep schoon terug. Het enige dat fout is: de objectgrafiek die hij bewerkte is niet langer hetgeen dat wordt weggeschreven, want de schrijver was al klaar. Vanuit het perspectief van de aanroeper is het document zonder incidenten geproduceerd en opgeslagen, en dat is precies de indruk die een stille fout geeft
Waar subsetting in de finalisatievolgorde zit
In HotPDF vindt het afsluitende werk binnen EndDoc. De subsetting-stap is een interne routine genaamd BuildAndApplyUnicodeFontSubset. Deze leest de per-document verzameling van gebruikte codepunten, bijgehouden in een bitmap die door het tekstuitvoerpad wordt gevuld naarmate glyphs worden weergegeven, koppelt elk gebruikt codepunt via de gecachte codepunt-naar-glyph-tabel aan een echte glyph-identificatie en herschrijft het lettertypeprogramma rond die selectie. Wanneer een Unicode TrueType-lettertype is geregistreerd, het uitvoerpad zet een bit in de verzameling gebruikte codepunten voor elk getekend teken, zodat de engine tegen de tijd dat het document sluit precies weet welke glyphs de subset moet behouden
De valkuil is de positie van die aanroep. Wordt BuildAndApplyUnicodeFontSubset aangeroepen nadat SaveToStream of SaveToFile het document al heeft geserialiseerd, dan worden de bewerkingen van de subsetter aan /FontFile2, de gecorrigeerde /Length1 en het /BaseFont-voorvoegsel van zes letters allemaal berekend tegen een objectgrafiek die al in bytes is omgezet. De correcte volgorde voert eerst de subsetter uit en serialiseert daarna, zodat de schrijver het subset-lettertype uitzendt in plaats van het origineel. Het goed doen is een kwestie van de positie van één aanroep, niets meer
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 draait hier, vóór het schrijven
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Van dit alles is niets zichtbaar in de aanroepende code. Subsetting staat standaard aan zodra een Unicode TrueType-lettertype is geregistreerd. Je registreert het lettertype, begint het document, tekent en beëindigt het, en de subset wordt gebouwd uit de glyphs die je gebruikte voordat de bytes het geheugen verlaten
Waarom één verkeerde stap een hele categorie is
De reden waarom dit een les waard is in plaats van een voetnoot, is dat EndDoc een lijst met afsluitende stappen doorloopt, en elk daarvan is gevoelig voor de positie ten opzichte van het schrijven. Font-subsetting is er één. PDF/A-uitvoer vereist een /CIDSet-stream die exact de glyph-identificaties in de subset opsomt, een voorwaarde die ISO 19005 stelt zodat een validator kan bevestigen dat het ingesloten programma overeenkomt met wat de lettertypedescriptor claimt. Die stream wordt in hetzelfde afrondingsvenster gegenereerd en is afhankelijk van het feit dat de subset eerst is gebouwd. PDF/UA-1 vereist, conform ISO 14289-1 §7.18.3, dat elke pagina met een annotatie /Tabs declareert met de waarde /S, en een interne routine genaamd EnsurePDFUATabsOnAnnotatedPages schrijft die sleutel weg tijdens dezelfde fase. Controles op de output-intent worden daar ook uitgevoerd
Dezelfde volgordefout die subsetting uitschakelt, zou ook de PDF/UA-tabvolgordesleutel op geannoteerde pagina's laten vervallen, want die stap zit in hetzelfde venster. veraPDF en PAC rapporteren een ontbrekende /Tabs /S als een schending van Matterhorn-protocol checkpoint 21-001. Een enkele verkeerd geplaatste aanroep vergroot dus niet alleen het bestand; hij schendt tegelijk geruisloos een toegankelijkheidseis, eveneens zonder foutmeldingen. Dat is het risico van een finalisatiefase: de stappen delen een randvoorwaarde, en een enkele volgordefout kan er meerdere tegelijk uitschakelen terwijl elke aanroep toch een successtatus retourneert
Hoe een stille uitvoerfout daadwerkelijk wordt gedetecteerd
Een fout die geen uitzondering genereert, wordt niet ontdekt door het programma uit te voeren. Deze wordt ontdekt door de uitvoer te inspecteren en te vergelijken met wat de invoer had moeten produceren. Voor font-subsetting zijn de controles concreet. Vergelijk de grootte van het uitvoerbestand met een ruwe verwachting: een document dat slechts een handvol glyphs gebruikt, mag niet de grootte hebben van een volledig lettertype. Open het ingesloten lettertype-object en lees de lengte in bytes; een gesubsetteerde /FontFile2 voor een Latijns lettertype is slechts een fractie van het bronbestand. Lees de /BaseFont-naam en controleer of het voorvoegsel van zes letters aanwezig is, aangezien de afwezigheid daarvan een direct signaal is dat er geen subset is toegepast
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;
// Een paar glyphs uit een face van ~700 KB mag geen stream van honderden KB's opleveren
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Voor PDF/A-uitvoer is de controle nog scherper, omdat een validator het werk voor u doet. Stel het conformiteitsniveau in en controleer het resultaat met veraPDF: een ontbrekende /CIDSet, of een subset die niet overeenkomt met de descriptor, wordt gerapporteerd als een mislukte clausule in plaats van dat u dit handmatig moet ontdekken. De conformiteitsschakelaars die dit afrondingswerk aansturen zijn eigenschappen van het document. PDFACompliance accepteert een string zoals '2B' voor PDF/A-2 Level B, en PDFUACompliance is een boolean die de tagged-PDF- en tabvolgordevereisten inschakelt
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;
De technische les
Hieruit vloeien twee regels voort. De eerste is dat elke finalisatiestap die objecten muteert, vóór de serialisatie van die objecten moet worden uitgevoerd; de afsluitfase van een document-engine moet worden gelezen als een geordende pijplijn waarin serialisatie de allerlaatste actie is, niet zomaar een actie tussen de andere. De tweede regel is degene die het makkelijkst wordt vergeten: voor een uitvoerstap is de afwezigheid van een fout geen bewijs van succes. Een routine die de juiste subset bouwt en toepast op de verkeerde, al geschreven grafiek rapporteert niets, omdat er vanuit zijn eigen perspectief niets mis was. Validatie moet naar het artefact kijken, niet naar de retourcode. Controleer de uitvoergrootte, lees de bytelengte van het ingesloten lettertype en het /BaseFont-voorvoegsel, en laat veraPDF de PDF/A-uitvoer beoordelen, waar een ontbrekende /CIDSet een stille tekortkoming in een benoemde fout verandert
De producentenkant van lettertypeverwerking, hoe lettertypen worden geregistreerd en ingesloten voor rapportuitvoer, wordt behandeld in ons artikel over lettertypen en afbeeldingen in rapportuitvoer. De validatiekant, waar deze afrondingsstappen worden getoetst aan de standaarden, wordt beschreven in de handleiding over PDF/A- en PDF/UA-validatie. Beide sluiten aan bij het subsetting- en conformiteitswerk dat hier wordt beschreven en dat wordt geleverd als onderdeel van de HotPDF Delphi Component voor Delphi en C++Builder, naast de laad-, bewerkings-, versleutelings- en ondertekenings-API's die elders op deze blog worden behandeld