Teknisk artikkel

Tegning av innebygde PDF-skrifter i Delphi med HotPDF

HotPDF tegner innebygde PDF-skrifter i Delphi uten å installere noe på maskinen: den innebygde tegningsrørledningen for glyfer i HPDFGlyphRender.pas analyserer skriftprogrammene som er lagret inne i selve PDF-en — TrueType glyf-konturer fra FontFile2, CFF Type 2-charstrings fra FontFile3 og innholdsstrømmer for Type 3-glyfer — og spiller dem av som fylte GDI-vektorbaner. Denne artikkelen er dypdykket i skrifttroskap bak tegning av PDF-sider til en TBitmap med HotPDF: den delen dekker tegneprosessen som helhet, mens denne dekker hvordan teksten på disse sidene får sine nøyaktige former

Hvorfor tegnes en PDF med firkanter i stedet for tekst?

Firkanter, blanke tegn eller subtilt feilaktige tegn i tegnede PDF-utdata betyr nesten alltid at tegneprosessen ba operativsystemet om en skrifttype i stedet for å bruke den som var bygd inn i filen. Klagen er den samme hver gang: dokumentet ser perfekt ut på maskinen der det ble laget, men så åpner en kunde det på en ren server eller en låst arbeidsstasjon, og den japanske fakturaen viser "tofu-firkanter", eller en erstatningsskrifttype forskyver hvert linjeskift. Skrifttypene var aldri på den maskinen — bare inne i PDF-en — og en tegneprosess som stopper ved systemskrifterstatning kan ikke se dem. Skrift-delsett gjør det verre: et delsett kan bære førti glyfer under tegnkoder tildelt privat for akkurat den ene filen, en tildeling ingen installert skrifttype deler

ISO 32000-1 §9.9 definerer tre bærere for et innebygd skriftprogram i skrift-descriptoren: FontFile inneholder et originalt Type 1-program, FontFile2 et TrueType-program og FontFile3 en ren CFF (Type1C eller CIDFontType0C) eller en OpenType-innpakning. En fjerde variant, Type 3-skrifttypen i ISO 32000-1 §9.6.5, bygger ikke inn noe binært i det hele tatt — hver glyf er en liten PDF-innholdsstrøm som kjøres på stedet. De tre bærerne er forskjellige i konturmatematikk (kvadratiske B-splines kontra kubiske charstrings kontra vilkårlige sideoperatorer), så en nøyaktig tegneprosess trenger en egen tolker for hver av dem, pluss et kodingslag som gjør tegnkoder om til riktig glyfindeks før noen kontur berøres

Hvordan konverterer HotPDF TrueType glyf-konturer til GDI baner?

THPDFEmbeddedTTF i HPDFGlyphRender.pas leser tabellen loca for å finne hver glyf-post, går gjennom glyf-konturene punkt for punkt og sender ut en GDI-bane. To TrueType-konvensjoner trenger eksplisitt håndtering. For det første innebærer påfølgende punkter utenfor kurven et punkt på kurven midt mellom dem, og en kontur der alle punkter er utenfor kurven starter på midtpunktet av det siste og det første punktet — hopper du over en av reglene, får runde glyfer flate fasetter eller kollapser. For det andre er TrueType-kurver kvadratiske Bézier-kurver mens GDI-funksjonen PolyBezierTo tar kubiske, så hvert kvadratiske segment heves nøyaktig i grad i stedet for å flates ut 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

Gradsheving er tapsfri: den kubiske kurven følger den identiske kurven, så den tegnede konturen samsvarer med det en kompatibel viser tegner fra samme tabell, ved enhver zoom. Resten av arbeidet er plassering. Hver glyf er skrevet i skriftenheter (vanligvis et rutenett på 1000 eller 2048 enheter per em), og tegneprosessen setter sammen skaleringsmatrisen, tekstmatrisen og den gjeldende transformasjonsmatrisen til én enkelt glyf-til-enhet-transformasjon før banen fylles. Rekkefølgen har mer betydning enn det ser ut til: setter du sammen de samme tre matrisene baklengs, kollapser hver glyf mot opprinnelsen — en side som ser feil ut, der den faktiske feilen er en enkelt linje med matrisealgebra

Hvordan tolken for Type 2-charstring håndterer CFF-skrifter

THPDFEmbeddedCFF gir FontFile3-programmer en ekte Type 2-charstring-tolk: den analyserer CFF INDEX-strukturene, Top DICT og Private DICT, kjører deretter hver charstring og sender banesegmenter direkte til GDI. En OpenType-innpakning (beholderen OTTO) strippes først for å nå den bare CFF-tabellen; nakne CIDFontType0C- og Type1C-strømmer forbrukes direkte. Charstrings er et kompakt stakkspråk, og tre av dets konvensjoner avgjør om tolken forblir synkronisert med bytestrømmen. Det valgfrie breddeprefikset betyr at den første stakktømmings-operatoren kan bære én ekstra ledende operand. Operatoren hintmask innebærer en vstemhm når operander fremdeles er på stakken, og antallet maskebytter som skal hoppes over avhenger av det akkumulerte stammeantallet — teller du feil her én gang, blir hver påfølgende instruksjonskode feillest. Og subrutinekall legger til en skjevhet (bias) på indeksen sin (107, 1131 eller 32768 avhengig av subrutineantallet) før oppslag, slik at et kall uten denne justeringen lander på en helt feil subrutine

CID-nøklet CFF legger til en indireksjon som skaper problemer for enkle implementasjoner: tegnkoden velger en CID, men charstring-indeksen er en GID, og skriftens tegnsett (charset) kartlegger GID til CID — så tegneprosessen bygger den omvendte CID-til-GID-kartleggingen før tegning, og velger Private DICT per glyf via FDSelect for skrifter som bærer flere. Navnenøklede Type1C-programmer, som er den vanlige bæreren for enkle Type 1-skrifter, løser i stedet opp enbyte-koder via CFF-programmets innebygde koding eller via kodingsmekanismene på PDF-nivå som beskrives under. Et ærlig forbehold: tolken leser hintoperatorer for å holde strømmen synkronisert, men utfører ikke hinting, en begrensning som diskuteres til slutt

Hva er en Type 3-skrifttype og hvordan tegnes den?

En Type 3-glyf er ikke en kontur i det hele tatt — ISO 32000-1 §9.6.5 definerer den som en innholdsstrøm, så HotPDF tegner den ved å dytte grafikk-tilstanden, sette sammen skriftmatrisen, skriftstørrelsen og tekstmatrisen på CTM, og kjøre glyf-prosedyren gjennom den samme operatørtolken som tegner sider, med skriftens egne /Resources i virkeområdet. To detaljer i spesifikasjonen har betydning for korrekthet: Type 3-/Widths uttrykkes i glyf-rommet i stedet for det 1/1000 tekstrommet som alle andre skrifttyper bruker, så fremganger må passere gjennom /FontMatrix — ellers vil strekkodeskrifter med en 0,01-matrise gå feil med en størrelsesorden. Og en glyf-prosedyre som åpner med operatoren d1, gir to løfter som tegneprosessen håndhever: tegningen er klippet til den deklarerte grenseboksen, og i henhold til ISO 32000-1 §9.6.5.2 ignorerer glyfen sine egne fargeoperatorer og tegner med kallerens nåværende fyllfarge, slik at rg, g, k og deres strykende tvillinger inne i prosedyren undertrykkes så lenge den glyfen varer. Hopper du over fargeregelen, vil en d1-strekkodeskrift som er stemplet i blått av siden komme ut som svart; hopper du over klippingen, vil en misdannet glyf tegne utenfor cellen sin

Hvordan tegnkoder blir til glyf-ID-er

Konturtolker er bare halve jobben, fordi bytene i en PDF-tekststreng er tegnkoder, ikke glyf-indekser, og ISO 32000-1 dedikerer to underpunkter til denne kartleggingen. For enkle skrifter, §9.6.6 foreskriver en streng prioritet: en /Differences-tabell overstyrer basiskodingen (WinAnsiEncoding, MacRomanEncoding eller StandardEncoding), som igjen overstyrer skriftprogrammets eget kart. HotPDF løser den kjeden opp to en tabell med 256 oppføringer for kode-til-GID, og oversetter glyfnavn til glyfindekser via tre ruter: nøyaktig treff på tegnsett inne i et CFF-program, numeriske gNN/glyphNN-navn tolket som bokstavelige indekser, og oversettelse fra Adobe Glyph List (navn-til-Unicode) etterfulgt av et cmap-oppslag for TrueType-programmer. For sammensatte skrifter, §9.7 setter CIDToGIDMap i førersetet: det vanlige tilfellet er /Identity, men oppføringen kan være en strøm av big-endian-par indeksert med CID — og HotPDFs egne Unicode-utdata bruker akkurat den strømformen for kompakte delsett, så denne banen 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 et TrueType-cmap-oppslag er nødvendig, HotPDF går gjennom en fallback-kjede i stedet for å stole på én enkelt undertabell: Windows Unicode-undertabeller (format 4, deretter format 12 for supplerende plan) kommer først, deretter (3,0)-symbolundertabellen med sin F000-konvensjon for private bruksområder speilet ned til den lave byten — grunnen til at en symbolskrift som Wingdings svarer på vanlige ASCII-koder — og deretter eldre formater 6 og 0. Format 2-undertabeller blir bevisst ikke tolket: de kartlegger eldre flerspråklige kodetabeller som Shift-JIS og Big5, ikke Unicode, og moderne CJK-skrifter bærer uansett alltid en format 4- eller format 12-undertabell. En hver kode som ikke overlever noen av disse rutene faller tilbake på GDI-tegning for den enkelte glyfen, slik at et tegn som ikke kan kartlegges kun forringer den ene glyfen, ikke hele tekstkjøringen

Hva den innebygde banen ikke gjør

Grensene er verdt å slå fast tydelig. Hinting blir ikke utført — konturer fylles slik de er skrevet, noe som ikke kan skilles fra hintede utdata ved 150 DPI og over, men kan avvike med en piksel fra en hintet rasteriserer ved svært små størrelser. Originale Type 1-programmer i FontFile (eexec-krypterte charstrings) blir ikke tolket, og OpenType-akser for variable skrifter blir ikke brukt; begge tilfellene, i likhet med et skadet skriftprogram eller en glyf-tabell uten en brukbar cmap, faller tilbake på tegning med systemskrift i stedet for at siden feiler. Den samme troskap-først-tilnærmingen gjelder også andre steder i tegneprosessen — aksiale og radielle skyggeleggingsmønstre får den samme behandlingen som gradienter fortjener — og genereringssiden har sin egen subtile skrifthistorie i hvordan EndDoc ordner skrift-delsett

Bruk av rørledningen krever ingen skriftspesifikk kode i det hele tatt — hver mekanisme over trer i kraft automatisk inne i tegningskallet for siden

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 konsekvensen er den som brukerstøtten din bryr seg om: en PDF som bærer skrifttypene sine, tegnes med de skrifttypene, enten det er på en byggeserver, i en Windows-container eller på en kundes skrivebord som aldri har sett skrifttypen. Den innebygde rørledningen for tegning av glyfer leveres som en del av HotPDF Component for Delphi og C++Builder — et opprinnelig VCL-bibliotek som dekker PDF-oppretting, redigering, tekstuttrekk og sidetegning uten eksterne DLL-avhengigheter