Technisch artikel

Ingebedde PDF-lettertypen renderen in Delphi met HotPDF

HotPDF rendert ingebedde PDF-lettertypen in Delphi zonder dat er iets op de machine geïnstalleerd hoeft te worden: de renderingspijplijn voor ingebedde glyphs in HPDFGlyphRender.pas analyseert de lettertypeprogramma's die in de PDF zelf zijn opgeslagen — TrueType glyf-outlines uit FontFile2, CFF Type 2 charstrings uit FontFile3 en Type 3 glyph-inhoudsstromen — en voert deze uit als gevulde GDI-vectorpaden. Dit artikel is de diepgaande duik in de lettertypegetrouwheid achter het renderen van PDF-pagina's naar een TBitmap met HotPDF. Dat artikel behandelt de renderer als geheel; dit artikel bespreekt hoe de tekst op die pagina's zijn exacte vormen krijgt

Waarom rendert een PDF met vakken in plaats van tekst?

Vakken, lege plekken of subtiel onjuiste tekens in gerenderde PDF-uitvoer betekenen bijna altijd dat de renderer het besturingssysteem om een lettertype heeft gevraagd in plaats van het in het bestand ingebedde lettertype te gebruiken. Dit probleem doet zich altijd op dezelfde manier voor: het document ziet er perfect uit op de machine waarop het is gemaakt, maar wanneer een klant het opent op een schone server of een beveiligde desktop, toont de Japanse factuur 'tofu'-vierkanten of verschuift een vervangen, vergelijkbaar lettertype elke regelafbreking. De lettertypen stonden nooit op die machine — alleen binnen de PDF — en een renderer die stopt bij de vervanging door systeemlettertypen kan ze niet zien. Subsetlettertypen maken het nog erger: een subset kan veertig glyphs bevatten onder karaktercodes die privé aan dat ene bestand zijn toegewezen, een toewijzing die geen enkel geïnstalleerd lettertype deelt

ISO 32000-1 §9.9 definieert drie dragers voor een ingebed lettertypeprogramma in de lettertypedescriptor: FontFile bevat een origineel Type 1-programma, FontFile2 een TrueType-programma en FontFile3 een kale CFF (Type1C of CIDFontType0C) of een OpenType-wrapper. Een vierde variant, het Type 3-lettertype van ISO 32000-1 §9.6.5, sluit helemaal niets binairs in — elke glyph is een kleine PDF-inhoudsstroom die ter plaatse wordt uitgevoerd. De drie dragers verschillen in outline-wiskunde (kwadratische B-splines versus kubische charstrings versus willekeurige pagina-operatoren), dus een getrouwe renderer heeft voor elk een aparte interpreter nodig, plus een coderingslaag die karaktercodes omzet in de juiste glyph-index voordat een outline wordt aangeraakt

Hoe converteert HotPDF TrueType glyf-outlines naar GDI-paden?

THPDFEmbeddedTTF in HPDFGlyphRender.pas leest de loca-tabel om elk glyph-record te lokaliseren, doorloopt de glyf-contouren punt voor punt en genereert een GDI-pad. Twee TrueType-conventies vereisen expliciete afhandeling. Ten eerste impliceren opeenvolgende off-curve punten een on-curve punt in het midden, en een contour waarvan de punten allemaal off-curve zijn, begint in het midden van het laatste en eerste punt — als u een van beide regels overslaat, krijgen afgeronde glyphs platte vlakken of storten ze in. Ten tweede zijn TrueType-curven kwadratische Béziers, terwijl GDI's PolyBezierTo kubische curven accepteert, dus elk kwadratisch segment wordt exact in graad verhoogd in plaats van afgeplat tot lijnsegmenten

// 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

Graadsverhoging is verliesvrij: de kubische curve volgt exact dezelfde curve, dus de gerenderde outline komt overeen met wat een conformerende viewer tekent op basis van dezelfde tabel, bij elk zoomniveau. De rest van het werk is de positionering. Elke glyph is gedefinieerd in lettertype-eenheden (meestal een raster van 1000 of 2048 eenheden per em), en de renderer combineert de schaalmatrix, de tekstmatrix en de actieve transformatiematrix tot één glyph-naar-apparaat-transformatie voordat het pad wordt gevuld. De volgorde is hier belangrijker dan het lijkt: combineer dezelfde drie matrices in omgekeerde volgorde en elke glyph stort in richting de oorsprong — een verkeerd uitziende pagina waarvan de eigenlijke fout één regel matrixalgebra is

Hoe de Type 2 charstring-interpreter CFF-lettertypen afhandelt

THPDFEmbeddedCFF geeft FontFile3-programma's een echte Type 2 charstring-interpreter: het analyseert de CFF INDEX-structuren, de Top DICT en Private DICT, voert vervolgens elke charstring uit en verzendt de padsegmenten rechtstreeks naar GDI. Een OpenType-wrapper (de OTTO-container) wordt eerst verwijderd om de kale CFF-tabel te bereiken; kale CIDFontType0C- en Type1C-stromen worden rechtstreeks verwerkt. Charstrings vormen een compacte stapeltaal, en drie van de conventies bepalen of de interpreter synchroon blijft met de byte-stroom. Het optionele breedtevoorvoegsel betekent dat de eerste operator die de stapel leegmaakt één extra leidende operand kan bevatten. De operator hintmask impliceert een vstemhm wanneer er nog operanden op de stapel liggen, en het aantal mask-bytes dat moet worden overgeslagen hangt af van het opgebouwde aantal stems — als u deze telling één keer verkeerd inschat, wordt elke volgende opcode verkeerd gelezen. En subroutines voegen een bias toe aan hun index (107, 1131 of 32768, afhankelijk van het aantal subroutines) vóór het zoeken, dus een aanroep zonder bias verwijst naar een compleet verkeerde subroutine

CFF met CID-sleutels voegt een indirectie toe waar naïeve implementaties over struikelen: de karaktercode selecteert een CID, maar de charstring-index is een GID, en de charset van het lettertype koppelt GID aan CID — de renderer bouwt dus de omgekeerde CID-naar-GID-koppeling op voordat hij gaat tekenen, en selecteert de Private DICT per glyph via FDSelect voor lettertypen die er meerdere bevatten. Programma's van het type Type1C met naamsleutels, de gebruikelijke drager voor eenvoudige Type 1-lettertypen, lossen daarentegen een-byte-codes op via de ingebouwde codering van het CFF-programma of via de hierna beschreven codering op PDF-niveau. Eén eerlijk voorbehoud: de interpreter leest hint-operatoren om de stroom gesynchroniseerd te houden, maar voert geen hinting uit, een grens die aan het einde wordt besproken

Wat is een Type 3-lettertype en hoe wordt het getekend?

Een Type 3-glyph is helemaal geen outline — ISO 32000-1 §9.6.5 definieert het als een inhoudsstroom, dus HotPDF rendert het door de grafische status te pushen, de lettertypematrix, lettertypegrootte en tekstmatrix op de CTM te combineren en de glyphprocedure uit te voeren via dezelfde operator-interpreter die pagina's tekent, met de eigen /Resources van het lettertype binnen het bereik. Twee specificatiedetails zijn belangrijk voor de correctheid. Type 3-/Widths worden uitgedrukt in de glyph-ruimte in plaats van de 1/1000 tekstruimte die elk ander lettertypetype gebruikt, dus stappen moeten door de /FontMatrix worden geleid — barcodelettertypen met een 0.01-matrix stappen anders een orde van grootte verkeerd. En een glyphprocedure die opent met de operator d1 doet twee beloften die de renderer afdwingt: het tekenen is beperkt tot het gedeclareerde begrenzingsvak, en volgens ISO 32000-1 §9.6.5.2 negeert de glyph zijn eigen kleuroperatoren en tekent met de actieve vulkleur van de aanroeper, dus rg, g, k en hun omlijningstegenhangers binnen de procedure worden onderdrukt tijdens die glyph. Sla de kleurregel over en een d1-barcodelettertype dat in het blauw door de pagina is gestempeld, komt er zwart uit; sla het begrenzingsvak over en een misvormde glyph tekent buiten zijn cel

Hoe karaktercodes glyph-ID's worden

Outline-interpreters vormen slechts de helft van het werk, omdat de bytes in een PDF-teksttekenreeks karaktercodes zijn en geen glyph-indices, en ISO 32000-1 besteedt twee paragrafen aan deze koppeling. Voor eenvoudige lettertypen schrijft §9.6.6 een strikte prioriteit voor: een array /Differences overschrijft de basiscodering (WinAnsiEncoding, MacRomanEncoding of StandardEncoding), die op haar beurt de eigen tabel van het lettertypeprogramma overschrijft. HotPDF zet die keten om in een tabel van 256 vermeldingen voor de omzetting van codes naar GID, en vertaalt glyph-namen op drie manieren naar glyph-indices: een exacte overeenkomst met de charset binnen een CFF-programma, numerieke namen van het type gNN/glyphNN die als letterlijke indices worden beschouwd, en de omzetting van namen uit de Adobe Glyph List naar Unicode, gevolgd door het zoeken in de cmap voor TrueType-programma's. Voor samengestelde lettertypen bepaalt §9.7 dat de CIDToGIDMap de leiding heeft: het meest voorkomende geval is /Identity, maar de vermelding kan een stroom van big-endian paren zijn die door CID worden geïndexeerd — en HotPDF's eigen Unicode-uitvoer gebruikt precies die stroomvorm voor compacte subsets, dus het stroompad is geen uitzonderlijk scenario

// /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

Wanneer het zoeken in een TrueType-cmap vereist is, doorloopt HotPDF een terugvalketen in plaats van één enkele subtabel te vertrouwen: de Windows Unicode-subtabellen (indeling 4, daarna indeling 12 voor aanvullende vlakken) komen eerst, daarna de (3,0) symboolsubtabel met haar F000 private-use-area-conventie die is gespiegeld naar de lage byte — de reden waarom een symboollettertype zoals Wingdings reageert op gewone ASCII-codes — en daarna de oudere indelingen 6 en 0. Subtabellen van indeling 2 worden bewust niet geïnterpreteerd: deze koppelen oudere multi-byte codepagina's zoals Shift-JIS en Big5, geen Unicode, en moderne CJK-lettertypen bevatten hoe dan ook altijd een subtabel van indeling 4 of indeling 12. Elke code die geen van deze paden overleeft, valt terug op de systeemlettertypetekening van GDI voor die specifieke glyph, zodat één niet-koppelbaar karakter één enkele glyph degradeert en niet de hele tekstreeks

Wat het ingebedde pad niet doet

De grenzen zijn het vermelden waard. Hinting wordt niet uitgevoerd — outlines worden gevuld zoals ze zijn gedefinieerd, wat niet te onderscheiden is van gerenderde uitvoer met hinting bij 150 DPI en hoger, maar een pixel kan verschillen van een gerasterde uitvoer met hinting op zeer kleine formaten. Originele Type 1-programma's in FontFile (eexec-gecodeerde charstrings) worden niet geïnterpreteerd en de assen van variabele OpenType-lettertypen worden niet toegepast; in beide gevallen valt de rendering, net als bij een beschadigd lettertypeprogramma of een glyf-tabel zonder bruikbare cmap, terug op de systeemlettertypetekening in plaats van dat de pagina mislukt. Dezelfde aanpak van 'getrouwheid eerst' geldt elders in de renderer — axiale en radiale verloop-sjablonen krijgen dezelfde behandeling die verlopen verdienen — en de generatiezijde heeft haar eigen lettertypedetail in hoe EndDoc lettertypesubsets ordent

Het gebruik van de pijplijn vereist helemaal geen lettertypespecifieke code — elk bovengenoemd mechanisme wordt automatisch geactiveerd binnen de aanroep voor paginarendering

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;

Het praktische gevolg is wat uw ondersteuningsinbox belangrijk vindt: een PDF die zijn lettertypen bevat, wordt gerenderd met die lettertypen, op een buildserver, in een Windows-container of op de desktop van een klant waarop het lettertype nooit is geïnstalleerd. De ingebedde glyph-renderingspijplijn wordt geleverd als onderdeel van het HotPDF Component voor Delphi en C++Builder — een systeemeigen VCL-bibliotheek die het maken, bewerken, de tekstextractie en de paginarendering van PDF's dekt zonder externe DLL-afhankelijkheden