Teknisk artikel

Upptäcka saknade PDF-glyfer vid ritning i Delphi

En saknad glyf i en PDF är inte ett fel. Producenten begär ett tecken det valda teckensnittet inte kan mappa, teckensnittet returnerar glyfindex noll, och filen som kommer ut är strukturellt giltig, öppnas överallt och visar en tom ruta där ett namn eller ett belopp borde vara. Ingen i den genererande pipelinen får reda på det. Mottagaren gör det. HotPDF stänger den loopen med TrackUnresolvedGlyphs: slå på den och ritvägen för text spelar in varje kodpunkt vars glyfuppslag resulterar i index noll, och avfyrar OnUnresolvedGlyph en gång per unikt fynd med kodpunkten, teckensnittet den misslyckades på, skriptet den tillhör och ett förslag på teckensnitt som skulle täcka den

Detektering är hälften av svaret. Den andra hälften är SetFontFallbackChain, som registrerar en ordnad lista av teckensnitt per skript, så att de vanliga fallen löser sig själva och bara äkta gap når din hanterare. Tillsammans gör de en klass av defekt som tidigare rapporterades av kunder till en kontroll vid byggtid

Varför kastar en saknad glyf ingenting?

För ISO 32000 ålägger ingen producent skyldigheten att verifiera täckning, och glyfindex noll är en legitim glyf. Det är .notdef, vars kontur teckensnittsdesignern väljer: vanligtvis en tom eller ihålig rektangel, ibland ingenting alls. En visare som ritar den beter sig korrekt. Textextraktion kan till och med returnera de rätta tecknen, eftersom /ToUnicode-mappningen skrivs ur källtexten i stället för ur konturerna, så en automatisk rundturkskontroll passerar glatt ett dokument vars synliga text har hål i

Diagram över varför en saknad PDF-glyf förblir tyst när glyf noll ritar en tom ruta medan ToUnicode-extraktion passerar rundturkskontroller
Glyf noll är ett legitimt .notdef-svar och /ToUnicode skrivs ur källtexten, så inget i pipelinen berättas om gapet

Den praktiska konsekvensen är att täckning måste kontrolleras i stunden av ritningen, när biblioteket fortfarande vet vilken kodpunkt som begärdes och vilken glyf teckensnittet faktiskt erbjöd. Efteråt är informationen borta

Detektorn måste bevaka delmängdstillståndet, inte device context

Här gick den första implementeringen fel, och orsaken är värd att förstå eftersom den gäller varje täckningskontroll som skruvas fast på en textpipeline. HotPDF har två textvägar. En avgår genom ett registrerat Unicode-TrueType-teckensnitt med en teckenkarta i minnet byggd vid registreringstiden. Den andra är ett äldre GDI-spår som skapar en färsk device context och ett teckensnittshandle per teckenkörning

Att bedöma täckning från GDI-spåret är hopplöst. Dess mappning är inte den mappning som hamnar i den avgivna innehållsströmmen, och de två är inte synkroniserade, så en detektor som läser GDI-resultat rapporterar hela det utskrivbara ASCII-intervallet som olöst. Det auktoritativa svaret bor i det registrerade teckensnittet: teckenkartan som RegisterUnicodeTTF parsar, frågad via GetUnicodeGlyphForCodepoint. Detektorn är därför spärrad på delmängdsredo-tillståndet, inte på något GDI-villkor, och den körs helt enkelt inte på dokument som aldrig registrerade ett Unicode-teckensnitt, vilket är korrekt eftersom de dokumenten ändå är begränsade till standardkodningarna

En andra fälla sitter bredvid. GDI-familjenamnet hos ett teckensnitt och PostScript-namnet extraherat ur teckensnittsbinären vid registrering är olika strängar, och inte på ett sätt du kan normalisera: en familj som heter Arial Unicode MS bär PostScript-namnet ArialMT. Varje spärr skriven som ”är det för tillfället valda teckensnittet det vi registrerade”, jämförd per namn, är död kod som aldrig avfyras. Spärra på tillstånd, aldrig på teckensnittsnamn

Flöde för olöst glyf-detektering i HotPDF som visar delmängdstillståndsspärren, GetUnicodeGlyphForCodepoint-uppslag och koppling av OnUnresolvedGlyph-händelsen
Täckning bedöms ur den registrerade Unicode-teckensnittskartan i stället för GDI, och varje unik kodpunkt avfyrar en händelse med ett teckensnittsförslag

Testa inte en glyfdetektor med emoji

Det uppenbara testfallet är ett leende ansikte, och det kommer övertyga dig om att detektorn är trasig. Vanliga emoji-kodpunkter i astralplanen löses via en private-use-syntesväg som mappar dem direkt till ett glyfindex, så de når aldrig den allmänna täckningsgrenen. Detektorn beter sig korrekt och testet mäter fel väg

Använd en otilldelad kodpunkt i stället. U+0378 är permanent oallokerad i Unicode, så inget teckensnitt kan legitimt mappa den, och den trär precis grenen du vill verifiera. Den distinktionen mellan ”funktionen är trasig” och ”testet plockade en indata som kringgår funktionen” kostar riktiga timmar, och otilldelade kodpunkter är det billigaste sättet att undvika det

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
  // Avfyras en gång per unik kodpunkt, inte en gång per förekomst
  FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
    [Info.CodePoint, String(Info.FontName), Ord(Info.Script),
     String(Info.SuggestedFonts)]));
end;

// Koppla in den i ett genererande jobb
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
    // Låt jobbet misslyckas i stället för att leverera en sida med rutor på
    raise Exception.Create(Audit.Findings.Text);
finally
  Pdf.Free;
end;

Reservkedjor är per skript, inte per teckensnitt

Orsaken till att reserven omfattas per skript i stället för per källteckensnitt är att täckningsgap kluster sig efter skriftsystem. Ett latinskt textteckensnitt saknar devanagari, thai, han och emoji, allt på en gång, och ersättaren för vart och ett är ett annat teckensnitt. Att deklarera en kedja per skript beskriver därför den verkliga distributionen: ett latinskt teckensnitt för brödtext, ett CJK-teckensnitt, ett emoji-teckensnitt, en allt-i-ett

Diagram över teckensnittsreserv per skript som mappar skripten hfsCJK, hfsArabic, hfsEmoji och hfsOther till ordnade ersättarteckensnittskedjor i HotPDF
Vart och ett skript får sin egen ordnade kedja, så ett latinskt brödtextteckensnitt som saknar han, arabiska eller emoji faller vidare till ett teckensnitt som täcker det
// THPDFFontScript täcker hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji och 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']);

Reserv och detektering är komplementära i stället för alternativ. Kedjor hanterar den täckning du förutsåg; detektorn rapporterar den täckning du inte förutsåg, vilket på ett system som bearbetar godtyckliga kunddata är den intressanta hälften. Observera att ett teckensnittsbyte ändrar mått, så ett stycke som faller tillbaka kan flöda om; om layouten betyder något är stängnings- och delmängdsbeteendet hos ersättarteckensnittet värt att läsa på i artikeln om teckensnittsdelmängdsstängning, och skript som behöver omordning eller sammanfogning hanteras av formningssteget som beskrivs i textformning av komplexa skript

Hur man eftermonterar beteende utan att riskera befintlig väg

Samma release lade till en reserv för den äldre kern-tabellen för paravstånd, och sättet den omfattades på är ett mönster värt att kopiera. I stället för att lägga till en ny beslutspunkt till kerninglogiken hänger reserven på den tidiga utgångsgren som redan fanns för teckensnitt utan GPOS-tabell. Ett modernt teckensnitt med GPOS når den aldrig, så dess beteende är oförändrat genom konstruktion i stället för genom testning. Vägar som inte registrerar ett Unicode-teckensnitt producerar två noll-offset, så de är oförändrade också

Det är den allmänna formen av en lågrisk-eftermontering i ett moget renderingsbibliotek: hitta grenen som för tillfället producerar ingenting och lägg det nya beteendet där. Det omvandlar ”vi tror att detta inte regredierade något” till ”detta kan inte ha regredierat något”, vilket är en mycket bättre sak att säga om en textmotor som andras fakturor passerar genom

Gör det till en port, inte en logg

Täckningsfynd är bara användbara om något misslyckas på dem. I en dokumentgenererande tjänst är det produktiva upplägget att hålla spårningen på i det nattliga regressionjobbet mot en corpus av riktiga kundnamn, adresser och produktbeskrivningar, och att låta jobbet misslyckas på vartdera fynd. Eftersom händelsen avfyras en gång per unik kodpunkt i stället för en gång per förekomst förblir utmatningen liten nog att läsa även när ett helt skript saknas

I produktion är samma hanterare bättre använd som telemetri: spela in kodpunkten och teckensnittet, fortsätt servera dokumentet och låt det aggregerade tala om vilket skript som ska läggas till distributionsfontsetet härnäst. Renderingsbeteende för inbäddade och ersatta teckensnitt tas upp vidare i rendering av inbäddade teckensnittsglyfer, och den fullständiga egenskapslistan inklusive TrackUnresolvedGlyphs är dokumenterad på produktsidan för HotPDF Delphi PDF component