Formede glyffer rendres som .notdef-bokse, når en fontundersætter kun beholder de glyffer, der kan nås fra de udsendte kodepunkter. HotPDF, den native VCL PDF-komponent til Delphi og C++Builder, bar netop den defekt frem til version 2.435.0: OpenType GSUB-output blev registreret i et internt usage-bitmap, som undersætteren erklærede den ville respektere, men som den aldrig faktisk læste
Dette er en anden fejl end den, der beskrives i EndDoc-fejlen, der stille deaktiverede fontundersætning. Den fejl handlede om hvornår undersætningen kørte i forhold til serialiseringen, og den deaktiverede undersætning fuldstændigt. Denne handler om hvad undersættet indeholder, når undersætningen kører helt efter planen. Pipelinen affyres på det rette tidspunkt, det seks-bogstavs undersætsprefiks vises på /BaseFont nøjagtigt som ISO 32000-1 §9.6.4 kræver, filen bliver mindre, hver latinsk side korrekturlæses ren, og en arabisk side kommer ud som en række tomme rektangler. Rækkefølgefejl er højlydte, når man kigger efter dem. Closure-fejl forbliver tavse for evigt, fordi undersættet er strukturelt gyldigt og kun forkert om sin egen medlemsliste
Hvorfor rendres formede glyffer som .notdef?
Fordi det sæt af kodepunkter et dokument udsender, ikke er det sæt af glyffer dokumentet tegner, og en undersætter der sammenblander de to, dropper hver glyf produceret af shaping. Text shaping omdanner en logisk tegnsekvens til en positioneret glyf-sekvens, og hele dens formål er at producere glyffer, som ikke et eneste inputtegn mapper til: et arabisk medialt heh, en fi-ligatur, en devanagari-konjunkt, et kontekstuelt alternativ valgt af rclt-funktionen. Hver af disse er et glyf-id, som et GSUB-lookup har fremstillet, ikke et som cmap-tabellen giver dig for noget tegn i din streng. En undersætter drevet udelukkende af cmap går derfor det forkerte indeks igennem. Den bevarer trofast hver glyf, teksten kunne have brugt før shaping, og kasserer netop de glyffer, teksten faktisk bruger efter shaping. Rendereren beder derefter den indlejrede font om GID 1847, undersættet har nulstillet den post i loca, og glyf-indeks 0 kommer tilbage i stedet. Glyf-indeks 0 er .notdef per OpenType-definition, hvilket er grunden til at fejlsignaturen er en tom boks frem for et forkert bogstav eller et nedbrud. Intet i PDF'en er malformet; fonten indeholder blot ikke den glyf, indholdsstrømmen bad om
Kodepunkter er ikke glyffer: de tre kilder til et undersæt
En korrekt subset closure skal forene tre uafhængige kilder, hver med sin egen akkumulator. Den første er den kodepunkt-afledte mængde: HotPDF akkumulerer FUnicodeUsedCps efterhånden som BMP-tegn udsendes, og FUnicodeSmpUsed for supplerende-plan-tegn nået gennem surrogatpar, og mapper derefter hver gennem FUnicodeCpToGid til et glyf-id. Den anden er den shaping-afledte mængde, de glyf-id'er som en GSUB-substitution producerede, registreret via MarkUnicodeGlyphUsed og EnableShapingFeatureForSubset i FUnicodeExtraUsedGlyphs. Den tredje er composite closure: en glyf hvis numberOfContours er -1 i glyf, er samlet fra komponent-glyf-id'er, og at beholde det sammensatte glyf mens dets komponenter droppes, giver en tom kontur frem for et .notdef, hvilket nok er værre, fordi det læses som en afstandsfejl
HotPDF har altid håndteret den første og tredje. BuildAndApplyUnicodeFontSubset, undersætningens indgangspunkt som EndDoc kalder før serialisering, seeder used-glyph-arrayet med GID 0, gennemgår BMP-kodepunkterne, gennemgår SMP-brugslisten og overgiver arrayet til en subset-builder, som løser komponentdele internt. Den anden kilde var skrevet men aldrig konsumeret, og fordi de tre kilder fejler på forskelligt indhold, kan hullet gemme sig i årevis i en kodebase, hvis regressionskorpus mest er latinsk
Arrayet der blev skrevet og aldrig læst
Kontrakten var dokumenteret tre steder og overholdt ingen af dem. Deklarationen af FUnicodeExtraUsedGlyphs angav, at EndDoc-undersætteren forener den med den kodepunkt-afledte brug; header-kommentaren på ApplyArabicGSUBRefinement lovede, at hvert udsendt erstatnings-GID sendes gennem MarkUnicodeGlyphUsed, så undersætteren trækker glyffen ind i den indlejrede font; det samme løfte optræder ordret på ApplyArabicGSUBContextualRefinement for rclt-stien. Begge kaldere holdt deres halvdel. Et grep over hver reference til feltet afgjorde den anden halvdel på omkring halvfems sekunder: én deklaration, én SetLength-allokering inde i RegisterUnicodeTTF, og skrivninger i de to markeringsrutiner. Ikke en eneste læsning. Det er den diagnose, det er værd at indarbejde, fordi den generaliserer langt ud over fonte. Når et felt skrives af flere kaldesteder og læses af ingen, eksisterer den funktion, det repræsenterer, ikke, uanset hvor grundigt den er kommenteret. Trin 1 i undersætteren er lille nok til at læse på én skærm, og hullet er åbenlyst, når man ved man skal kigge efter det
// Step 1: derive the used-glyph set (as it stood before 2.435.0)
SetLength(UsedGlyphs, FUnicodeNumGlyphs);
for I := 0 to FUnicodeNumGlyphs - 1 do
UsedGlyphs[I] := False;
UsedGlyphs[0] := True; // .notdef is always present
for Cp := 0 to $FFFF do // source 1a: BMP code points
if (Cp < Length(FUnicodeUsedCps)) and FUnicodeUsedCps[Cp]
and (Cp < Length(FUnicodeCpToGid)) then
begin
GID := FUnicodeCpToGid[Cp];
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
for I := 0 to High(FUnicodeSmpUsed) do // source 1b: SMP code points
begin
GID := FUnicodeSmpUsed[I].GID;
if (GID > 0) and (GID < FUnicodeNumGlyphs) then
UsedGlyphs[GID] := True;
end;
// source 2 was missing here: nothing ever consulted FUnicodeExtraUsedGlyphs
Rettelsen med den ene løkke, og at markere glyffer selv
Rettelsen er en forening, og dens sikkerhedsargument kommer fra operationens retning: den sætter kun bit, aldrig rydder dem, så ingen glyf, der plejede at overleve undersætningen, kan begynde at blive droppet
// v2.435.0: pull GSUB-derived extra glyphs into the subset.
// MarkUnicodeGlyphUsed / EnableShapingFeatureForSubset record GIDs that
// shaping produced but that no emitted code point maps to directly.
for I := 0 to FUnicodeNumGlyphs - 1 do
if (I < Length(FUnicodeExtraUsedGlyphs)) and FUnicodeExtraUsedGlyphs[I] then
UsedGlyphs[I] := True;
Tre egenskaber gør dette til en lavrisiko-ændring frem for en omskrivning af fontmotoren. Den er monoton, som ovenfor. Den er en no-op på fonte, der aldrig har formet noget, da FUnicodeExtraUsedGlyphs forbliver helt False, og byteoutputtet for et rent latinsk dokument er uændret. Og den lander før Trin 2, så begge subset-buildere arver den: den sparsomme builder der bevarer den oprindelige GID-nummerering, og den kompakte builder _BuildCompactSubsetTTF, som HotPDF vælger under PDF/A for at omnummerere bevarede glyffer til et tæt interval, formindske maxp.numGlyphs, og udsende den gamle-til-nye mapning som den /CIDToGIDMap-strøm, ISO 32000-1 §9.7.4.2 kræver. Begge kalder _TTFWalkCompositeClosure internt, så en formet glyf, der tilfældigvis er sammensat, nu også trækker sine komponenter med. Composite closure var aldrig i stykker; den blev blot aldrig nået for disse glyf-id'er, fordi glyf-id'erne ikke var i den mængde, den gennemgår. Hvis du styrer GSUB-motoren direkte i stedet for at stole på de indbyggede raffineringspas, bliver closure dit ansvar, og hvert erstatnings-glyf-id du udsender, skal markeres, før EndDoc fryser den brugte glyf-mængde
var
Pdf: THotPDF;
GIDs: array[0..1] of Word;
LigGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'shaped.pdf';
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoNaskhArabic-Regular.ttf');
Pdf.ShapingFeatures := [sfArabicGSUB, sfStandardLigatures,
sfContextualAlternates];
GIDs[0] := Pdf.GetUnicodeGlyphForCodepoint($0644); // lam
GIDs[1] := Pdf.GetUnicodeGlyphForCodepoint($0627); // alef
if Pdf.ApplyLigatureSubstitution(GIDs, 0, 'liga', LigGID) then
Pdf.MarkUnicodeGlyphUsed(LigGID); // omit this and you get .notdef
Pdf.EnableShapingFeatureForSubset('rclt');
Pdf.CurrentPage.RtLTextOut(50, 700, 0, WideString(ArabicText));
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
EnableShapingFeatureForSubset er batch-modstykket til det enkelte GID-kald, og det er bevidst konservativt. Det gennemgår GSUB-lookup-listen for de lookups, der er koblet til ét fire-byte-funktionstag under den aktuelt valgte script- og sprogsti, og markerer de erstatnings-glyf-id'er, disse lookups kan producere. Det er en defensiv no-op, når fonten ikke bærer nogen GSUB-tabel, eller når funktionen mangler på den sti, så at kalde det betingelsesløst er sikkert. Det er også en overapproksimation med vilje: det kan bevare glyffer, et givent dokument aldrig tegner. For undersætning koster overinklusion bytes, og underinklusion koster korrekthed, hvilket gør den afvejning let. Strukturen af disse lookups, og de coverage-tabeller der afgør hvilke glyffer deltager, dækkes i gennemgangen af GSUB-stilistiske alternativer i ren Delphi
Hvordan beviser man, at glyffen faktisk er i undersættet?
Ved at læse den udsendte font, ikke ved at kigge på siden i en fremviser, der måske substituerer en systemfont bag din ryg. Kontrollen, der fanger hele denne fejlklasse, er mekanisk: udtræk /FontFile2-strømmen fra output-PDF'en, parse loca, og bekræft at det glyf-id, du forventer, bærer en ikke-tom post, hvilket betyder at dens start- og slutoffsets adskiller sig. En tom post er undersætteren, der har besluttet, at glyffen er ubrugt. To vaner gør det derefter meget sværere at levere fejlen igen. Behold en formet-script-side i det automatiserede smoke-korpus frem for kun i det manuelle korrekturlæsnings-sæt, fordi arabisk, devanagari og khmer udøver closure-stier, som ingen mængde latinsk dækning vil røre. Og hver gang en akkumulator findes, hævd at noget konsumerer den, siden et write-only-felt er en funktion, der kompilerer, tester grønt på det forkerte korpus, og ikke gør noget
Hvor rettelsen stopper
Subset closure er nødvendig for at en formet glyf kan rendres, og den er ikke tilstrækkelig. Glyffen skal også kunne adresseres fra indholdsstrømmen, hvilket er et separat problem med sin egen grænse. HotPDF's indbyggede arabiske raffineringspas committer kun en substitution, når hvert erstatnings-glyf-id kan nås gennem et Unicode-præsentationsform-kodepunkt via et omvendt cmap-scan over cirka 690 kodepunkter i U+FB50 til U+FDFF og U+FE70 til U+FEFF. Når en erstatning lander på et glyf-id uden for det interval, passerer inputvinduet uændret igennem frem for at udsende noget, læseren ikke kan adressere; fontspecifikke alternativer på vilkårlige glyf-id'er kræver et syntetisk private-use-kodepunkt allokeret i U+E000 til U+F8FF for at bære dem gennem emit-stien. Så det ærlige resumé er, at 2.435.0-rettelsen fjernede en hård blokering frem for at fuldføre historien. Før den kunne en glyf formes korrekt, udsendes korrekt, og alligevel forsvinde ved undersætningstidspunktet, hvilket betød at shaping-motoren ikke kunne stoles på fra ende til anden, uanset hvor gode dens lookups var. Det, der er tilbage, er adresserbarhed, og den begrænsning fejler i det mindste synligt på udsendelsestidspunktet frem for stille i et build-trin, der kører efter alt det, du holdt øje med. For emit-siden af den samme pipeline, se guiden til arabisk og RTL-tekstshaping i Delphi-PDF'er
Fontundersætningen, GSUB-motoren og den komplekse-script-shaping beskrevet her leveres i standard-HotPDF Component til Delphi og C++Builder; produktsiden bærer den fulde API-reference for de Unicode-font- og shaping-kald, der er navngivet ovenfor