Teknisk artikel

Rendering af indlejrede PDF-skrifttyper i Delphi med HotPDF

HotPDF renderer indlejrede PDF-skrifttyper i Delphi uden at installere noget på maskinen: Den indlejrede renderingspipeline for glyfer i HPDFGlyphRender.pas analyserer de skrifttypeprogrammer, der er gemt inde i selve PDF-filen — TrueType-glyf-konturer fra FontFile2, CFF Type 2-charstrings fra FontFile3 og Type 3-glyfindholdsstrømme — og genafspiller dem som udfyldte GDI-vektorstier. Denne artikel er det dybe dyk i skrifttypepræcision bag rendering af PDF-sider til en TBitmap med HotPDF: Den del dækker rendereren som helhed, mens denne dækker, hvordan teksten på disse sider får sine nøjagtige former

Hvorfor renderer en PDF med bokse i stedet for tekst?

Bokse, tomme felter eller subtilt forkerte tegn i det renderede PDF-output betyder næsten altid, at rendereren spurgte operativsystemet efter en skrifttype i stedet for at bruge den, der er indlejret i filen. Klagen ankommer på samme måde hver gang: Dokumentet ser perfekt ud på den maskine, der producerede det, og derefter åbner en kunde det på en ren server eller et låst skrivebord, og den japanske faktura viser tofu-firkanter, eller en substitueret lignende skrifttype forskyder hvert linjeskift. Skrifttyperne var aldrig på den maskine — kun inde i PDF'en — og en renderer, der stopper ved system-skrifttypesubstitution, kan ikke se dem. Delmængde-skrifttyper gør det værre: En delmængde kan bære fyrre glyfer under tegnkoder, der er tildelt privat til netop den ene fil, en tildeling som ingen installeret skrifttype deler

ISO 32000-1 §9.9 definerer tre bærere for et indlejret skrifttypeprogram i skrifttype-descriptoren: FontFile indeholder et originalt Type 1-program, FontFile2 et TrueType-program og FontFile3 en ren CFF (Type1C eller CIDFontType0C) eller en OpenType-wrapper. En fjerde variant, Type 3-skrifttypen i ISO 32000-1 §9.6.5, indlejrer slet intet binært — hver glyf is en lille PDF-indholdsstrøm, der udføres på stedet. De tre bærere adskiller sig i konturmatematik (kvadratiske B-splines mod kubiske charstrings mod vilkårlige sideoperatorer), så en trofast renderer har brug for en separat fortolker til hver samt et kodningslag, der gør tegnkoder til det rigtige glyf-indeks, før nogen kontur berøres

Hvordan konverterer HotPDF TrueType glyf-konturer til GDI-stier?

THPDFEmbeddedTTF i HPDFGlyphRender.pas læser loca-tabellen for at lokalisere hver glyf-post, gennemgår glyf-konturerne punkt for punkt og udsender en GDI-sti. To TrueType-konventioner kræver eksplicit håndtering. For det første indebærer på hinanden følgende punkter uden for kurven et punkt på kurven ved deres midtpunkt, og en kontur, hvis punkter alle er uden for kurven, starter ved midtpunktet af dens sidste og første punkt — springes en af reglerne over, får afrundede glyfer flade facetter eller kollapser. For det andet er TrueType-kurver kvadratiske Bézier-kurver, mens GDI's PolyBezierTo tager kubiske, så hvert kvadratiske segment gradhæves nøjagtigt frem for at blive fladet ud til linjesegmenter

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (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;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

Gradhævning er tabsfri: Den kubiske kurve tegner den identiske kurve, så den renderede kontur matcher det, som en konform fremviser tegner fra samme tabel, ved enhver zoom. Det resterende arbejde er placering. Hver glyf er oprettet i skrifttype-enheder (typisk et net på 1000 eller 2048 enheder pr. em), og rendereren sammensætter skaleringsmatrixen, tekstmatrixen og den aktuelle transformationsmatrix til én glyf-til-enhedstransformation, før stien udfyldes. Rækkefølgen betyder mere her, end det ser ud til: Sammensæt de samme tre matrixer baglæns, og hver glyf kollapser mod oprindelsen — en side, der ser forkert ud, og hvis egentlige fejl er en enkelt linje med matrixalgebra

Hvordan Type 2-charstring-fortolkeren håndterer CFF-skrifttyper

THPDFEmbeddedCFF giver FontFile3-programmer en ægte Type 2-charstring-fortolker: Den analyserer CFF-INDEX-strukturerne, Top DICT og Private DICT, udfører derefter hver charstring og sender stisegmenter direkte til GDI. En OpenType-wrapper (OTTO-containeren) strippes først for at nå den rå CFF-tabel; nøgne CIDFontType0C- og Type1C-strømme forbruges direkte. Charstrings er et kompakt staksprog, og tre af dets konventioner afgør, om fortolkeren forbliver i synkronisering med bytestrømmen. Det valgfrie breddepræfiks betyder, at den første stakryddende operator kan bære én ekstra ledende operand. Operatoren hintmask indebærer en vstemhm, når operander stadig er på stakken, og antallet af maskebytes, der skal springes over, afhænger af det akkumulerede antal stammer — regnes antallet forkert én gang, misforstås hver efterfølgende opcode. Og underrutinekald tilføjer en bias til deres indeks (107, 1131 eller 32768 afhængigt af underrutineantallet) før opslag, så et uforudindtaget kald lander på en helt forkert underrutine

CID-nøglet CFF tilføjer en indirektion, som spænder ben for naive implementeringer: Tegnkoden vælger et CID, men charstring-indekset er et GID, og skrifttypens tegnsæt kortlægger GID til CID — så rendereren bygger den omvendte CID-til-GID-kortlægning før tegning og vælger Private DICT pr. glyf via FDSelect for skrifttyper, der bærer flere. Navnenøglede Type1C-programmer, den sædvanlige bærer for simple Type 1-skrifttyper, i stedet løser en-byte-koder via CFF-programmets indbyggede kodning eller via kodningsmaskineriet på PDF-niveau, der beskrives herefter. Et ærligt forbehold: Fortolkeren læser hint-operatorer for at holde strømmen synkroniseret, men udfører ikke hinting, en grænse der diskuteres til sidst

Hvad er en Type 3-skrifttype, og hvordan tegnes den?

En Type 3-glyf er slet ikke en kontur — ISO 32000-1 §9.6.5 definerer den som en indholdsstrøm, så HotPDF renderer den ved at skubbe grafiktilstanden, sammensætte skrifttypematrixen, skrifttypestørrelsen og tekstmatrixen på CTM og udføre glyfproceduren via den samme operatorfortolker, som tegner sider, med skrifttypens egne /Resources i spil. To specifikationsdetaljer har betydning for korrektheden. Type 3 /Widths udtrykkes i glyfområdet frem for det 1/1000 tekstområde, som alle andre skrifttypetyper bruger, så fremrykninger skal passere gennem /FontMatrix — stregkodeskrifttyper med en 0.01-matrix træder ellers forkert med en størrelsesorden. Og en glyfprocedure, der åbner med operatoren d1, giver to løfter, som rendereren håndhæver: Tegning er klippet til den erklærede afgrænsningsboks, og i henhold til ISO 32000-1 §9.6.5.2 ignorerer glyfen sine egne farveoperatorer og tegner med kalderens aktuelle udfyldningsfarve, så rg, g, k og deres stregtegnings-tvillinger inde i proceduren undertrykkes under udførelsen af den pågældende glyf. Springes farvereglen over, bliver en d1-stregkodeskrifttype stemplet i blå af siden sort; springes klippet over, tegner en misdannet glyf uden for sin celle

Hvordan tegnkoder bliver til glyf-id'er

Konturfortolkere er kun det halve arbejde, fordi bytene i en PDF-tekststreng er tegnkoder, ikke glyf-indekser, og ISO 32000-1 dedikerer to underafsnit til denne kortlægning. For simple skrifttyper, §9.6.6 foreskriver en streng prioritet: Et /Differences-array tilsidesætter basiskodningen (WinAnsiEncoding, MacRomanEncoding eller StandardEncoding), som igen tilsidesætter skrifttypeprogrammets eget kort. HotPDF opløser denne kæde til en tabel med 256 poster fra kode til GID, hvilket oversætter glyfnavne til glyfindekser ad tre veje: Nøjagtigt tegnsætsmatch inde i et CFF-program, numeriske gNN/glyphNN-navne taget som bogstavelige indekser og Adobe Glyph List navn-til-Unicode-oversættelse efterfulgt af et cmap-opslag for TrueType-programmer. For sammensatte skrifttyper, §9.7 sætter CIDToGIDMap i spidsen: Det almindelige tilfælde er /Identity, men indgangen kan være en strøm af big-endian-par indekseret efter CID — og HotPDF's eget Unicode-output bruger netop denne strømform til kompakte delmængder, så strømstien er ikke et eksotisk hjørne

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

Når der er brug for et TrueType cmap-opslag, HotPDF går gennem en fallback-kæde frem for at stole på én undertabel: Windows Unicode-undertabeller (format 4, derefter format 12 for supplerende planer) kommer først, derefter (3,0) symbolundertabellen med dens F000-konvention for privat brugsområde spejlet ned til den lave byte — årsagen til at en symbolskrifttype som Wingdings svarer for almindelige ASCII-koder — derefter ældre formater 6 og 0. Format 2-undertabeller fortolkes bevidst ikke: De kortlægger ældre multibyte-kodesider som f.eks. Shift-JIS og Big5, ikke Unicode, og moderne CJK-skrifttyper bærer under alle omstændigheder en format 4- eller format 12-undertabel. Enhver kode, der ikke overlever nogen af disse veje, falder tilbage på GDI-tegning for den pågældende enkeltglyf, så et tegn, der ikke kan kortlægges, forringer én glyf, ikke hele tekstforløbet

Hvad den indlejrede sti ikke gør

Hinting udføres ikke — konturer udfyldes som oprettet, hvilket ikke kan skelnes fra hintet output ved 150 DPI og derover, men kan afvige med en pixel fra en hintet rasterizer ved meget små størrelser. Originale Type 1-programmer i FontFile (eexec-krypterede charstrings) fortolkes ikke, og OpenType variable-skrifttypeakser anvendes ikke; begge tilfælde, ligesom et beskadiget skrifttypeprogram eller en glyf-tabel uden nogen brugbar cmap, falder tilbage på system-skrifttypetegning frem for at fejle siden. Den samme præcisions-først-tilgang strækker sig til andre steder i rendereren — aksiale og radiale skyggemønstre får den samme behandling, som gradienter fortjener — og generationssiden har sin egen skrifttypesubtilitetshistorie i, hvordan EndDoc ordner skrifttypedelmængder

Brug af pipelinen kræver slet ingen skrifttypespecifik kode — hver mekanisme ovenfor aktiveres automatisk i kaldet til siderendering

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      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 praktiske konsekvens er den, din support-indbakke bekymrer sig om: En PDF, der bærer sine skrifttyper, renderer med disse skrifttyper, på en build-server, i en Windows-container eller på et kundeskrivebord, der aldrig har set skrifttypen. Den indlejrede renderingspipeline for glyfer leveres som en del af HotPDF Component til Delphi og C++Builder — et indfødt VCL-bibliotek, der dækker PDF-oprettelse, redigering, tekstudtrækning og siderendering uden eksterne DLL-afhængigheder