Teknisk artikel

Rendera inbäddade PDF-typsnitt i Delphi med HotPDF

HotPDF renderar inbäddade PDF-typsnitt i Delphi utan att installera något på maskinen: pipelinen för inbäddad glyfrendering i HPDFGlyphRender.pas tolkar de typsnittsprogram som lagras inuti själva PDF:en — TrueType glyf-konturer från FontFile2, CFF Type 2-charstrings från FontFile3, och innehållsströmmar för Type 3-glyfer — och spelar upp dem som fyllda GDI-vektorbanor. Den här artikeln är fördjupningen om typsnittstrohet bakom rendering av PDF-sidor till en TBitmap med HotPDF: den texten täcker renderaren som helhet, den här täcker hur texten på de sidorna får sina exakta former

Varför renderas en PDF med rutor istället för text?

Rutor, tomrum eller subtilt felaktiga tecken i renderad PDF-utdata betyder nästan alltid att renderaren bad operativsystemet om ett typsnitt istället för att använda det som är inbäddat i filen. Klagomålet kommer på samma sätt varje gång: dokumentet ser perfekt ut på maskinen som framställde det, sedan öppnar en kund det på en ren server eller ett låst skrivbord och den japanska fakturan visar tofurutor, eller ett utbytt liknande typsnitt förskjuter varje radbrytning. Typsnitten fanns aldrig på den maskinen — bara inuti PDF:en — och en renderare som stannar vid utbyte mot systemtypsnitt kan inte se dem. Delmängdstypsnitt gör det värre: en delmängd kan bära fyrtio glyfer under teckenkoder som tilldelats privat för just den filen, en tilldelning inget installerat typsnitt delar

ISO 32000-1 §9.9 definierar tre bärare för ett inbäddat typsnittsprogram i typsnittsbeskrivaren: FontFile håller ett ursprungligt Type 1-program, FontFile2 ett TrueType-program, och FontFile3 en naken CFF (Type1C eller CIDFontType0C) eller ett OpenType-omslag. En fjärde variant, Type 3-typsnittet i ISO 32000-1 §9.6.5, bäddar inte in något binärt alls — varje glyf är en liten PDF-innehållsström som körs på plats. De tre bärarna skiljer sig i konturmatematik (kvadratiska B-splines kontra kubiska charstrings kontra godtyckliga sidoperatorer), så en trogen renderare behöver en separat tolk för var och en, plus ett kodningslager som förvandlar teckenkoder till rätt glyfindex innan någon kontur rörs

HotPDF: Bärare av inbäddade PDF-typsnitt: typsnittsbeskrivaren håller programmen FontFile, FontFile2 eller FontFile3 och innehållsströmmar för Type 3, och varje bärare följer sin egen renderingsväg
Varje inbäddad bärare får sin egen tolk, från TrueType glyf-konturer till Type 3-innehållsströmmar

Hur omvandlar HotPDF TrueType glyf-konturer till GDI-banor?

THPDFEmbeddedTTF i HPDFGlyphRender.pas läser loca-tabellen för att lokalisera varje glyfpost, går igenom glyf-konturerna punkt för punkt, och avger en GDI-bana. Två TrueType-konventioner behöver uttrycklig hantering. För det första innebär två på varandra följande punkter utanför kurvan en punkt på kurvan i deras mittpunkt, och en kontur vars punkter alla ligger utanför kurvan börjar i mittpunkten mellan sin sista och sin första punkt — hoppa över endera regeln och rundade glyfer får platta fasetter eller kollapsar. För det andra är TrueType-kurvor kvadratiska Bézier-kurvor medan GDI:s PolyBezierTo tar kubiska, så varje kvadratiskt segment gradhöjs exakt snarare än plattas ut till linjesegment

Renderingspipeline för TrueType glyf i Delphi: loca-tabellen lokaliserar glyfposter, underförstådda mittpunkter på kurvan rekonstrueras, kvadratiska kurvor gradhöjs exakt till kubiska, och GDI fyller banan
Underförstådda mittpunkter och exakt gradhöjning håller den uppspelade konturen identisk med de författade glyf-kurvorna
// Exakt gradhöjning: kvadratisk (P0, Q, P2) -> kubisk (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// sedan PolyBezierTo med C1, C2, P2 — geometriskt identisk kurva

Gradhöjning är förlustfri: den kubiska kurvan följer den identiska kurvan, så den renderade konturen matchar vad en regelrätt visare ritar från samma tabell, vid vilken zoom som helst. Det som återstår är placering. Varje glyf är författad i typsnittsenheter (typiskt ett rutnät med 1000 eller 2048 enheter per em), och renderaren komponerar skalmatrisen, textmatrisen och den aktuella transformationsmatrisen till en enda glyf-till-enhet-transform innan banan fylls. Ordningen spelar större roll här än det ser ut: komponera samma tre matriser baklänges och varje glyf kollapsar mot origo — en sida som ser fel ut men vars faktiska bugg är en rad matrisalgebra

Hur Type 2-charstring-tolken hanterar CFF-typsnitt

THPDFEmbeddedCFF ger FontFile3-program en riktig Type 2-charstring-tolk: den tolkar CFF:ens INDEX-strukturer, Top DICT och Private DICT, kör sedan varje charstring och avger banssegment rakt till GDI. Ett OpenType-omslag (behållaren OTTO) skalas först av för att nå den nakna CFF-tabellen; nakna CIDFontType0C- och Type1C-strömmar konsumeras direkt. Charstrings är ett kompakt stackspråk, och tre av dess konventioner avgör om tolken håller sig i takt med byteströmmen. Det valfria breddprefixet innebär att den första stacktömmande operatorn kan bära en extra inledande operand. Operatorn hintmask innebär ett underförstått vstemhm när operander fortfarande ligger på stacken, och antalet maskbyte att hoppa över beror på det ackumulerade stamantalet — få antalet fel en gång och varje efterföljande opkod läses fel. Och subrutinanrop lägger till en bias till sitt index (107, 1131 eller 32768 beroende på antalet subrutiner) före uppslagning, så ett anrop utan bias landar på helt fel subrutin

CID-nycklad CFF lägger till en indirektion som fäller naiva implementationer: teckenkoden väljer ett CID, men charstring-indexet är ett GID, och typsnittets charset mappar GID till CID — så renderaren bygger den omvända mappningen från CID till GID före ritning, och väljer den glyfvisa Private DICT genom FDSelect för typsnitt som bär flera. Namnnycklade Type1C-program, den vanliga bäraren för enkla Type 1-typsnitt, löser istället upp enbytekoder genom CFF-programmets inbyggda kodning eller genom det kodningsmaskineri på PDF-nivå som beskrivs härnäst. Ett ärligt förbehåll: tolken läser hint-operatorer för att hålla strömmen synkroniserad men kör inte hinting, en gräns som diskuteras i slutet

Vad är ett Type 3-typsnitt och hur ritas det?

En Type 3-glyf är ingen kontur alls — ISO 32000-1 §9.6.5 definierar den som en innehållsström, så HotPDF renderar den genom att lägga undan grafiktillståndet, komponera typsnittsmatrisen, typsnittsstorleken och textmatrisen på CTM:en, och köra glyfproceduren genom samma operatortolk som ritar sidor, med typsnittets egna /Resources i räckvidd. Två detaljer i specifikationen spelar roll för korrekthet. Type 3:s /Widths uttrycks i glyfrymd snarare än den 1/1000 textrymd varje annan typsnittstyp använder, så framflyttningar måste passera genom /FontMatrix — annars stegar streckkodstypsnitt med en 0,01-matris fel med en storleksordning. Och en glyfprocedur som öppnar med operatorn d1 ger två löften som renderaren upprätthåller: målningen klipps till den deklarerade omslutande rutan, och enligt ISO 32000-1 §9.6.5.2 ignorerar glyfen sina egna färgoperatorer och målar med anroparens aktuella fyllfärg, så rg, g, k och deras streckande tvillingar inuti proceduren undertrycks under den glyfens varaktighet. Hoppa över färgregeln och ett d1-streckkodstypsnitt som sidan stämplat i blått kommer ut svart; hoppa över klippningen och en felformad glyf målar utanför sin cell

Hur teckenkoder blir glyf-ID

Konturtolkar är bara halva jobbet, eftersom byten i en PDF-textsträng är teckenkoder, inte glyfindex, och ISO 32000-1 ägnar två underavsnitt åt mappningen. För enkla typsnitt föreskriver §9.6.6 en strikt prioritet: en /Differences-array åsidosätter baskodningen (WinAnsiEncoding, MacRomanEncoding eller StandardEncoding), som åsidosätter typsnittsprogrammets egen karta. HotPDF löser upp den kedjan till en tabell med 256 poster från kod till GID, och översätter glyfnamn till glyfindex på tre vägar: exakt charset-matchning inuti ett CFF-program, numeriska gNN/glyphNN-namn tagna som literala index, och namn-till-Unicode-översättning via Adobe Glyph List följd av en cmap-uppslagning för TrueType-program. För sammansatta typsnitt lägger §9.7 ansvaret på CIDToGIDMap: det vanliga fallet är /Identity, men posten kan vara en ström av big-endian-par indexerade med CID — och HotPDF:s egen Unicode-utdata använder exakt den strömformen för kompakta delmängder, så strömvägen är inget exotiskt hörn

HotPDF: Mappning av PDF-teckenkoder till glyf-ID: enkla typsnitt löses upp genom Differences, baskodningen och typsnittsprogrammets karta, sammansatta typsnitt genom CIDToGIDMap, sedan en reservkedja med TrueType cmap och en GDI-reserv för enstaka glyfer
Båda typsnittsfamiljerna slutar i en tabell från kod till GID, och varje omappbar kod försämrar en enda glyf istället för hela textkörningen
// /CIDToGIDMap som en ström: big-endian Word-par indexerade med CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // utanför intervallet mappas till .notdef

När en TrueType-cmap-uppslagning behövs går HotPDF igenom en reservkedja snarare än att lita på en enda undertabell: Windows Unicode-undertabellerna (format 4, sedan format 12 för supplementära plan) kommer först, sedan (3,0)-symbolundertabellen med sin F000-konvention för privat användningsområde speglad ned till den låga byten — skälet till att ett symboltypsnitt som Wingdings svarar för vanliga ASCII-koder — sedan de äldre formaten 6 och 0. Undertabeller av format 2 tolkas medvetet inte: de mappar äldre teckentabeller med flera byte såsom Shift-JIS och Big5, inte Unicode, och moderna CJK-typsnitt bär undantagslöst ändå en undertabell av format 4 eller 12. Varje kod som inte överlever någon av de här vägarna faller tillbaka på GDI-ritning för just den glyfen, så ett omappbart tecken försämrar en glyf, inte hela textkörningen

Vad den inbäddade vägen inte gör

Gränserna är värda att uttala rakt ut. Hinting körs inte — konturer fylls som de författats, vilket är oskiljbart från hintad utdata vid 150 DPI och uppåt men kan skilja sig med en bildpunkt från en hintande rastrerare vid mycket små storlekar. Ursprungliga Type 1-program i FontFile (eexec-krypterade charstrings) tolkas inte, och axlar i OpenType variabla typsnitt tillämpas inte; båda fallen faller, liksom ett skadat typsnittsprogram eller en glyf-tabell utan någon användbar cmap, tillbaka på ritning med systemtypsnitt snarare än att fälla sidan. Samma trohet-först-hållning sträcker sig till andra delar av renderaren — axiella och radiella skuggningsmönster får den behandling gradienter förtjänar — och genereringssidan har sin egen historia om typsnittssubtiliteter i hur EndDoc ordnar typsnittsdelmängder

Att använda pipelinen kräver ingen typsnittsspecifik kod alls — varje mekanism ovan kopplas in automatiskt inuti anropet som renderar sidan

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Inbäddade TrueType-, CFF- och Type 3-typsnitt renderas från
      // själva filen — inget behöver installeras på den här maskinen
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Den praktiska följden är den din supportinkorg bryr sig om: en PDF som bär sina typsnitt renderas med de typsnitten, på en byggserver, i en Windows-container, eller på ett kundskrivbord som aldrig sett teckensnittet. Pipelinen för inbäddad glyfrendering levereras som en del av HotPDF Delphi Component för Delphi och C++Builder — ett inbyggt VCL-bibliotek som täcker PDF-skapande, redigering, textextraktion och sidrendering utan externa DLL-beroenden