Teknisk artikel

Find manglende PDF-glyphs på tegnetidspunktet i Delphi

En manglende glyph i en PDF er ikke en fejl. Producenten beder om et tegn, den valgte font ikke kan mappe, fonten returnerer glyph-indeks nul, og filen, der kommer ud, er strukturelt gyldig, åbner alle steder og viser en tom boks, hvor et navn eller et beløb burde stå. Ingen i den genererende pipeline får det at vide. Modtageren gør. HotPDF lukker den løkke med TrackUnresolvedGlyphs: slå den til, og teksttegningsvejen registrerer hvert kodepunkt, hvis glyph-opslag opløses til indeks nul, og udløser OnUnresolvedGlyph én gang pr. unikt fund med kodepunktet, fonten, det fejlede på, det script, det tilhører, og et forslag til fonte, der ville dække det

Detektion er halvdelen af svaret. Den anden halvdel er SetFontFallbackChain, som registrerer en ordnet fontliste pr. script, så de almindelige tilfælde løser sig selv, og kun ægte huller når din handler. Tilsammen forvandler de en klasse af defekter, der plejede at blive rapporteret af kunder, til et build-tidstjek

Hvorfor udløser en manglende glyph intet?

Fordi ISO 32000 ikke pålægger en producent nogen forpligtelse til at verificere dækning, og glyph-indeks nul er en lovlig glyph. Det er .notdef, hvis omrids fontdesigneren vælger: normalt en tom eller hul rektangel, somme tider slet ingenting. En fremviser, der tegner det, opfører sig korrekt. Tekstudtrækning kan endda returnere de rigtige tegn, for /ToUnicode-mappingen skrives ud fra kildeteksten frem for ud fra omridsene, så et automatiseret round-trip-tjek vil gerne bestå et dokument, hvis synlige tekst har huller i

Diagram over, hvorfor en manglende PDF-glyph forbliver stille, idet glyph nul tegner en tom boks, mens ToUnicode-udtrækning består round-trip-tjek
Glyph nul er et lovligt .notdef-svar, og /ToUnicode skrives fra kildeteksten, så intet i pipelinen får at vide, at hullet findes

Den praktiske konsekvens er, at dækning skal tjekkes i tegneøjeblikket, hvor biblioteket stadig ved, hvilket kodepunkt der blev bedt om, og hvilken glyph fonten reelt tilbød. Bagefter er oplysningen væk

Detektoren skal holde øje med subset-tilstanden, ikke device context

Det er her, den første implementation tog fejl, og årsagen er værd at forstå, for den gælder ethvert dækningstjek, der boltres på en tekstpipeline. HotPDF har to tekstveje. Den ene udsender gennem en registreret Unicode TrueType-font med et in-memory tegnkort bygget på registreringstidspunktet. Den anden er en legacy GDI-vej, der opretter en frisk device context og fonthandle pr. tegnkørsel

At bedømme dækning fra GDI-vejen er håbløst. Dens mapping er ikke den mapping, der ender i den udsendte content stream, og de to er ikke synkroniserede, så en detektor, der læser GDI-resultater, rapporterer hele det udskrivbare ASCII-interval som uopløst. Det autoritative svar bor i den registrerede font: tegnkortet, som RegisterUnicodeTTF parser, forespurgt gennem GetUnicodeGlyphForCodepoint. Detektoren er derfor gatet på subset-ready-tilstanden, ikke på nogen GDI-betingelse, og den kører simpelthen ikke på dokumenter, der aldrig registrerede en Unicode-font, hvilket er korrekt, for de dokumenter er alligevel begrænset til standardkodningerne

En anden fælde ligger ved siden af. En fonts GDI-familienavn og det PostScript-navn, der udtrækkes fra fontbinærfilen på registreringstidspunktet, er forskellige strenge, og ikke på en måde, du kan normalisere: en familie kaldet Arial Unicode MS bærer PostScript-navnet ArialMT. Enhver gate skrevet som "er den aktuelt valgte font den, vi registrerede", sammenlignet efter navn, er død kode, der aldrig udløses. Gate på tilstand, aldrig på fontnavne

HotPDF uopløst glyph-detektionsflow, der viser subset-tilstand-gaten, GetUnicodeGlyphForCodepoint-opslaget og OnUnresolvedGlyph-event-koblingen
Dækning bedømmes ud fra det registrerede Unicode-fontkort frem for GDI, og hvert unikt kodepunkt udløser ét event med et fontforslag

Test ikke en glyph-detektor med emoji

Det oplagte testtilfælde er et smilende ansigt, og det vil overbevise dig om, at detektoren er i stykker. Almindelige emoji-kodepunkter i de astrale planer opløses gennem en private-use-syntesevej, der mapper dem direkte til et glyph-indeks, så de når aldrig den generelle dækningsgren. Detektoren opfører sig korrekt, og testen måler den forkerte vej

Brug et ikke-tildelt kodepunkt i stedet. U+0378 er permanent ikke-allokeret i Unicode, så ingen font kan lovligt mappe det, og det afprøver præcis den gren, du vil verificere. Den skelnen mellem "funktionen er i stykker" og "testen valgte et input, der omgår funktionen" koster rigtige timer, og ikke-tildelte kodepunkter er den billigste måde at undgå det på

type
  TCoverageAudit = class
  private
    FFindings: TStringList;
  public
    procedure Handle(Sender: TObject;
      const Info: THPDFUnresolvedGlyphInfo);
    property Findings: TStringList read FFindings;
  end;

procedure TCoverageAudit.Handle(Sender: TObject;
  const Info: THPDFUnresolvedGlyphInfo);
begin
  // Udløses én gang pr. unikt kodepunkt, ikke én gang pr. forekomst
  FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
    [Info.CodePoint, String(Info.FontName), Ord(Info.Script),
     String(Info.SuggestedFonts)]));
end;

// Kobling ind i et genererende job
Pdf := THotPDF.Create(nil);
try
  Pdf.TrackUnresolvedGlyphs := True;
  Pdf.OnUnresolvedGlyph := Audit.Handle;
  Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
  Pdf.EndDoc;
  if Audit.Findings.Count > 0 then
    // Få jobbet til at fejle i stedet for at udsende en side med bokse på
    raise Exception.Create(Audit.Findings.Text);
finally
  Pdf.Free;
end;

Fallback-kæder er pr. script, ikke pr. font

Grunden til, at fallback afgrænses efter script frem for efter kilde-font, er, at dækningshuller klumper sig sammen efter skrivesystem. En latinsk tekstfont mangler devanagari, thai, Han og emoji alt på én gang, og erstatningen for hver er en forskellig font. At erklære én kæde pr. script beskriver derfor den reelle udrulning: én latinsk font til brødtekst, én CJK-font, én emoji-font, én catch-all

Pr. script font-fallback-diagram, der mapper hfsCJK-, hfsArabic-, hfsEmoji- og hfsOther-scripts til ordnede erstatningsfontkæder i HotPDF
Hvert script får sin egen ordnede kæde, så en latinsk brødtekstfont, der mangler Han, arabisk eller emoji, falder igennem til en font, der dækker det
// THPDFFontScript dækker hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji og hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
  ['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);

Fallback og detektion er komplementære snarere end alternativer. Kæder håndterer den dækning, du forudså; detektoren rapporterer den dækning, du ikke gjorde, hvilket på et system, der behandler vilkårlige kundedata, er den interessante halvdel. Bemærk, at at udskifte en font ændrer metrics, så et afsnit, der falder tilbage, kan reflowe; hvis layoutet tæller, er det værd at læse om den erstattede fonts closure- og subsetting-adfærd i artiklen om font subset closure, og scripts, der behøver reordering eller joining, håndteres af shaping-trinnet beskrevet i complex script text shaping

Sådan eftermonteres adfærd uden at risikere den eksisterende vej

Samme udgivelse tilføjede en legacy kern-tabel-fallback til parafstand, og måden den blev afgrænset på er et mønster værd at kopiere. I stedet for at tilføje et nyt beslutningspunkt til kerning-logikken hænger fallbacken på den early-exit-gren, der allerede fandtes for fonte uden GPOS-tabel. En moderne font med GPOS når den aldrig, så dens adfærd er uændret per konstruktion frem for per test. Veje, der ikke registrerer en Unicode-font, producerer to nul-offsets, så de er uændrede ligeledes

Det er den generelle form for en lavrisiko-eftermontering i et modent rendingsbibliotek: find den gren, der i øjeblikket ikke producerer noget, og læg den nye adfærd der. Det omdanner "vi tror, dette ikke reducerede noget" til "dette kan ikke have reduceret noget", hvilket er en langt bedre ting at sige om en tekstmotor, som andres fakturaer passerer gennem

Gør det til en gate, ikke en log

Dækningsfund er kun nyttige, hvis noget fejler på dem. I en dokumentgenererende tjeneste er den produktive ordning at holde tracking til i det natlige regression job mod et korpus af rigtige kundenavne, adresser og produktbeskrivelser og at få jobbet til at fejle ved ethvert fund. Fordi eventen udløses én gang pr. unikt kodepunkt frem for én gang pr. forekomst, forbliver outputtet lille nok til at læse, selv når et helt script mangler

I produktion bruges samme handler bedre som telemetri: registrer kodepunktet og fonten, fortsæt med at levere dokumentet, og lad aggregatet fortælle dig, hvilket script der skal tilføjes udrulningsfontsættet næste gang. Renderingsadfærd for indlejrede og erstattede fonte er dækket yderligere i rendering af indlejrede font-glyphs, og den fulde egenskabsliste inklusive TrackUnresolvedGlyphs er dokumenteret på produktsiden HotPDF Delphi PDF component