Shapede glyfer rendres som .notdef-bokser når en font-subsetter bare beholder glyfene som er nåbare fra utstedte kodepunkter. HotPDF, den native VCL PDF-komponenten for Delphi og C++Builder, bar nøyaktig den defekten frem til versjon 2.435.0: OpenType GSUB-utdata ble registrert i et internt bruksbitkart som subsetteren erklærte at den ville respektere, og deretter aldri faktisk leste
Dette er en annen feil enn den beskrevet i EndDoc-feilen som stille deaktiverte font-subsetting. Den feilen handlet om når subsetting kjørte i forhold til serialisering, og den deaktiverte subsetting fullstendig. Denne handler om hva subsettet inneholder når subsetting kjører perfekt etter planen. Pipelinen utløses på riktig tidspunkt, det seks-bokstavers subset-prefikset dukker opp på /BaseFont nøyaktig slik ISO 32000-1 §9.6.4 krever, filen blir mindre, hver latinsk side korrekturleser rent, og en arabisk side kommer ut som en rad med tomme rektangler. Rekkefølgefeil er høylytte så snart du ser etter dem. Lukkingsfeil forblir stille for alltid, fordi subsettet er strukturelt gyldig og bare feil om sin egen medlemsliste
Hvorfor rendres shapede glyfer som .notdef?
Fordi settet med kodepunkter et dokument utsteder ikke er settet med glyfer dokumentet tegner, og en subsetter som blander sammen de to, dropper hver glyf shaping produserte. Tekst-shaping omgjør en logisk tegnsekvens til en posisjonert glyfsekvens, og hele hensikten er å produsere glyfer som ikke noe enkelt inputtegn mapper til: en arabisk medial heh, en fi-ligatur, en devanagari-konjunkt, et kontekstuelt alternativ valgt av rclt-funksjonen. Hver av disse er en glyf-ID en GSUB-oppslag produserte, ikke en cmap-tabellen gir deg for noe tegn i strengen din. En subsetter drevet utelukkende av cmap går derfor gjennom feil indeks. Den beholder trofast hver glyf teksten kunne ha brukt før shaping og forkaster nøyaktig de glyfene teksten faktisk bruker etter shaping. Rendereren spør deretter den innebygde fonten om GID 1847, subsettet har nullstilt den oppføringen i loca, og glyfindeks 0 kommer tilbake i stedet. Glyfindeks 0 er .notdef per OpenType-definisjon, og det er derfor feilsignaturen er en tom boks fremfor en feil bokstav eller en krasj. Ingenting i PDF-en er feilformet; fonten inneholder ganske enkelt ikke glyfen innholdsstrømmen ba om
Kodepunkter er ikke glyfer: de tre kildene til et subset
En korrekt subset-lukking må forene tre uavhengige kilder, hver med sin egen akkumulator. Den første er det kodepunkt-utledede settet: HotPDF akkumulerer FUnicodeUsedCps etter hvert som BMP-tegn utstedes og FUnicodeSmpUsed for supplerende-plan-tegn nådd gjennom surrogatpar, og mapper deretter hver gjennom FUnicodeCpToGid til en glyf-ID. Den andre er det shaping-utledede settet, glyf-ID-ene en GSUB-substitusjon produserte, registrert gjennom MarkUnicodeGlyphUsed og EnableShapingFeatureForSubset inn i FUnicodeExtraUsedGlyphs. Den tredje er sammensatt lukking: en glyf hvis numberOfContours er -1 i glyf settes sammen fra komponent-glyf-ID-er, og å beholde den sammensatte glyfen mens komponentene droppes gir en tom kontur fremfor en .notdef, noe som argumentativt er verre fordi det leses som en avstandsfeil
HotPDF har alltid håndtert den første og tredje. BuildAndApplyUnicodeFontSubset, subsetting-inngangspunktet EndDoc kaller før serialisering, sår used-glyph-arrayen med GID 0, går gjennom BMP-kodepunktene, går gjennom SMP-brukslisten, og gir arrayen til en subset-bygger som løser sammensatte komponenter internt. Den andre kilden var skrevet men aldri konsumert, og fordi de tre kildene feiler på forskjellig innhold, kan gapet gjemme seg i årevis i en kodebase hvis regresjonskorpus stort sett er latinsk
Arrayen som ble skrevet og aldri lest
Kontrakten var dokumentert tre steder og respektert i ingen av dem. Deklarasjonen av FUnicodeExtraUsedGlyphs uttalte at EndDoc-subsetteren forener den med det kodepunkt-utledede bruket; headerkommentaren på ApplyArabicGSUBRefinement lovet at hver utstedte erstatnings-GID sendes gjennom MarkUnicodeGlyphUsed slik at subsetteren trekker glyfen inn i den innebygde fonten; det samme løftet gjentas ordrett på ApplyArabicGSUBContextualRefinement for rclt-veien. Begge kallerne oppfylte sin halvdel. Et grep over hver referanse til feltet avgjorde den andre halvdelen på rundt nitti sekunder: én deklarasjon, én SetLength-allokering inne i RegisterUnicodeTTF, og skrivinger i de to markeringsrutinene. Ikke en eneste lesing. Det er diagnosen verdt å internalisere, fordi den generaliserer godt langt utover fonter. Når et felt skrives av flere kallsteder og leses av ingen, eksisterer ikke funksjonen det representerer, uansett hvor grundig den er kommentert. Steg 1 i subsetteren er lite nok til å leses på én skjerm, og gapet er åpenbart så snart du vet å lete etter 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
Den ene løkkefiksen, og å markere glyfer selv
Reparasjonen er en forening, og sikkerhetsargumentet dens kommer fra operasjonens retning: den setter bare bit, tømmer dem aldri, så ingen glyf som pleide å overleve subsettet kan begynne å droppes
// 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 egenskaper gjør dette til en lavrisiko-endring fremfor en font-motor-omskriving. Den er monoton, som over. Den er en no-op på fonter som aldri shapet noe, siden FUnicodeExtraUsedGlyphs forblir helt False og byteutdata for et rent latinsk dokument er uendret. Og den kommer inn før Steg 2, så begge subset-byggerne arver den: den sparsomme byggeren som bevarer original GID-nummerering, og den kompakte byggeren _BuildCompactSubsetTTF som HotPDF velger under PDF/A for å renummerere beholdte glyfer inn i et tett intervall, krympe maxp.numGlyphs, og utstede den gamle-til-nye mappingen som /CIDToGIDMap-strømmen som kreves av ISO 32000-1 §9.7.4.2. Begge kaller _TTFWalkCompositeClosure internt, så en shapet glyf som tilfeldigvis er sammensatt, drar nå også komponentene sine med. Sammensatt lukking var aldri ødelagt; den ble ganske enkelt aldri nådd for disse glyf-ID-ene, fordi glyf-ID-ene ikke var i settet den traverserer. Hvis du driver GSUB-motoren direkte i stedet for å stole på de innebygde forbedringspassene, blir lukking ditt ansvar, og hver erstatnings-glyf-ID du utsteder må markeres før EndDoc fryser used-glyph-settet
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-motstykket til enkelt-GID-kallet, og den er bevisst konservativ. Den går gjennom GSUB-oppslagslisten for oppslagene koblet til én firebokstavs funksjonstag under den nåværende valgte skrift- og språkveien, og markerer erstatnings-glyf-ID-ene disse oppslagene kan produsere. Den er en defensiv no-op når fonten ikke bærer noen GSUB-tabell eller når funksjonen mangler fra den veien, så å kalle den ubetinget er trygt. Den er også en overapproksimasjon by design: den kan beholde glyfer et gitt dokument aldri tegner. For subsetting koster overinklusjon byte og underinklusjon koster korrekthet, noe som gjør den avveiningen enkel. Strukturen til disse oppslagene, og dekningstabellene som avgjør hvilke glyfer som deltar, er dekket i gjennomgangen av GSUB stilistiske alternativer i ren Delphi
Hvordan beviser du at glyfen faktisk er i subsettet?
Ved å lese den utstedte fonten, ikke ved å kikke på siden i en visning som kanskje erstatter en systemfont bak ryggen din. Sjekken som fanger denne hele klassen av feil er mekanisk: trekk ut /FontFile2-strømmen fra utdata-PDF-en, parse loca, og bekreft at glyf-ID-en du forventer bærer en ikke-tom oppføring, altså at start- og sluttoffsetene dens er forskjellige. En tom oppføring er subsetteren som har bestemt at glyfen er ubrukt. To vaner gjør det deretter mye vanskeligere å levere feilen igjen. Hold en shapet-skrift-side i det automatiserte røyktest-korpuset fremfor bare i det manuelle korrekturlesings-settet, fordi arabisk, devanagari og khmer utøver lukkingsveier ingen mengde latinsk dekning vil røre. Og når en akkumulator finnes, assert at noe konsumerer den, siden et skriv-bare felt er en funksjon som kompilerer, tester grønt på feil korpus, og gjør ingenting
Hvor fiksen stopper
Subset-lukking er nødvendig for at en shapet glyf skal rendres, og er ikke tilstrekkelig. Glyfen må også kunne adresseres fra innholdsstrømmen, som er et separat problem med sin egen grense. HotPDFs innebygde arabiske forbedringspass forplikter en substitusjon bare når hver erstatnings-glyf-ID er nåbar gjennom et Unicode-presentasjonsform-kodepunkt via en omvendt cmap-skanning over omtrent 690 kodepunkter i U+FB50 til U+FDFF og U+FE70 til U+FEFF. Når en erstatning lander på en glyf-ID utenfor det intervallet, passerer inputvinduet uendret gjennom fremfor å utstede noe leseren ikke kan adressere; font-spesifikke alternativer ved vilkårlige glyf-ID-er trenger et syntetisk privat-bruk-kodepunkt allokert i U+E000 til U+F8FF for å bære dem gjennom utstedelsesveien. Så det ærlige sammendraget er at 2.435.0-fiksen fjernet en hard blokkering fremfor å fullføre historien. Før den kunne en glyf shapes korrekt, utstedes korrekt, og fortsatt forsvinne ved subsett-tidspunktet, noe som betydde at shaping-motoren ikke kunne stoles på fra ende til ende uansett hvor gode oppslagene dens var. Det som gjenstår er adresserbarhet, og den begrensningen feiler i det minste synlig ved utstedelsespunktet fremfor stille i et byggetrinn som kjører etter alt du overvåket. For utstedelsessiden av samme pipeline, se veiledningen om arabisk og RTL-tekst-shaping i Delphi-PDF-er
Font-subsetting, GSUB-motoren og shaping av komplekse skriftsystemer beskrevet her leveres i standard HotPDF Component for Delphi og C++Builder; produktsiden inneholder den fullstendige API-referansen for Unicode-font- og shaping-kallene nevnt over