HotPDF rendert letters met accenten uit ingebedde fonts correct door twee fontmechanismen te behandelen die één glyph opbouwen uit andere: TrueType composite glyphs, waarvan de component-offsets in glyph units toegepast moeten worden vóór de glyph-naar-device-matrix, en de CFF-seac-vorm van endchar, die een basisletter en een accent aan elkaar naait via StandardEncoding-codes. Zit er een van beide naast, dan komt é, Å, ü of een curly double quote een onderdeel tekort, terwijl de gewone letters eromheen er perfect uitzien
Dat laatste detail maakt deze bugs duur. Een rapport rendert, de bodytekst is in orde, en alleen "München" heeft een gat, of een citaat opent met een single quote en een leegte. Niemand verdenkt de renderer. De eerste theorie is altijd een beschadigd font of een slechte producent, en de ticket stuitert een week rond. Dit artikel loopt langs wat er in elk mechanisme werkelijk misgaat, hoe de HotPDF Delphi component het sinds v2.768.10 en v2.768.11 afhandelt, en de verwante subsetting-fix in v2.770.83 die composite glyphs heel houdt wanneer HotPDF fonts schrijft in plaats van leest
Waarom verdwijnt alleen het accent uit een PDF-glyph?
Het accent verdwijnt omdat het het enige deel van de glyph is met een niet-nul offset. Zowel TrueType- als CFF-fonts besparen ruimte door outlines te hergebruiken: é wordt niet als nieuwe vorm getekend maar als "de e-glyph, plus de acute-glyph omhoog en naar rechts geschoven". De basisletter zit vrijwel altijd op offset (0, 0), dus elke fout in hoe offsets toegepast worden laat haar onaangeroerd. Het accent is de component die beweegt, en dus de component die op de verkeerde plek belandt of nooit getekend wordt
Het symptoomverband is op zichzelf al een diagnostiek. Rendert de basisletter van tekens met accenten wel en de diacrieten niet, of toont gepaarde interpunctie zoals “ en ” zich als één teken in plaats van twee, hou dan op met naar het fontbestand te kijken. Een kapot font breekt doorgaans hele glyphs, en dat voor elke viewer. Ontbrekende onderdelen van samengestelde glyphs in één renderer, terwijl Acrobat de pagina prima toont, wijzen op de code die de componenten assembleert
Hoe plaatsen TrueType composite glyphs hun componenten?
Een TrueType composite glyph plaatst elke component met zijn eigen kleine affine transform, uitgedrukt in font units, en die transform moet op de componentoutline werken vóór de glyph-naar-device-matrix dat doet. In de OpenType-tabel glyf heeft een composite glyph een negatief contouraantal, gevolgd door een lijst componentrecords. Elk record houdt een flags-woord vast, de component-glyph-index, twee argumenten die bytes of words zijn (ARG_1_AND_2_ARE_WORDS, 0x0001) en een optionele schaal: één F2Dot14-waarde voor WE_HAVE_A_SCALE (0x0008), twee voor WE_HAVE_AN_X_AND_Y_SCALE (0x0040), of een volledige 2x2-matrix voor WE_HAVE_A_TWO_BY_TWO (0x0080). Staat ARGS_ARE_XY_VALUES (0x0002) aan, dan zijn de twee argumenten een dx/dy-offset in font units. MORE_COMPONENTS (0x0020) zegt dat er nog een record volgt
Per component bouwt u dus een componentmatrix CM op uit het schaaldeel en de offset, en combineert die met M, de matrix die font units naar device pixels mapt. De volgorde van die combinatie is de hele bug. De interne matrixvermenigvuldiging van HotPDF gebruikt de kolomvectorconventie, dus het product van A en B past eerst B toe en daarna A. De juiste compositie past eerst CM toe, in glyph space, en daarna M. Andersom geschreven loopt M eerst en wordt de offset van CM erachteraan opgeteld, al in device pixels en nooit geschaald
// Alleen illustratie: een 2D-affine record met de kolomvectorconventie,
// waar Mul(A, B) een punt eerst door B en daarna door A stuurt
type
TAffine = record
a, b, c, d, e, f: Double;
end;
function Mul(const A, B: TAffine): TAffine;
begin
Result.a := A.a * B.a + A.c * B.b;
Result.b := A.b * B.a + A.d * B.b;
Result.c := A.a * B.c + A.c * B.d;
Result.d := A.b * B.c + A.d * B.d;
Result.e := A.a * B.e + A.c * B.f + A.e;
Result.f := A.b * B.e + A.d * B.f + A.f;
end;
// M : font units -> device pixels (kleine schaal, y gespiegeld)
// CM : componenttransform; e/f houdt dx/dy in font units
Sub := Mul(M, CM); // correct: offset meegeschaald met de outline
// Sub := Mul(CM, M); // fout: dx/dy belanden als rauwe device pixels
De cijfers maken de mislukking evident zodra u ze ziet. Arial gebruikt een em van 2048 units, en de accentcomponent van é voert zijn offset uit in die font units. Met de juiste volgorde krimpt die offset mee met de outline tot enkele pixels bij normale zoom. Met de omgekeerde volgorde wordt de rauwe font-unit-waarde opgeteld als device pixels, en in de testrender van HotPDF belandde het accent van Arial's é 243 pixels van zijn letter, ver buiten de glyph en meestal buiten alles wat zichtbaar is. De linker dubbele quote in Arial is twee kopieën van de single-quote-vorm, waarvan de tweede verschoven, dus hij rendereed als een eenzame ‘ gevolgd door lege ruimte
Het lineaire deel legt de bug zelden bloot. Accentcomponenten voeren meestal helemaal geen schaal mee, en een uniforme schaal commuteert sowieso met M, dus alleen de translatie verraadt het omgekeerde product. Daarom slaagt ook een snelle test met gewone Latijnse tekst: de omkering zat door vele releases heen in de code voordat v2.768.10 hem corrigeerde, en alleen componenten met offsets lieten hem ooit zien
Recursie en cycli
Composite glyphs mogen naar andere composite glyphs verwijzen, dus een renderer loopt ze recursief door en moet zich verdedigen. HotPDF begrenst de nestdiepte op 5 niveaus en het totale aantal componenten per glyph op 1024, slaat een component over die zijn eigen glyph-index noemt, en behandelt de zelden gebruikte point-matching-modus (argumenten die puntnummers zijn in plaats van offsets) als nul-offset. Een vijandig of gecorrumpeerd font kan daardoor een component verliezen, maar het kan de paginarender niet laten hangen
Wat is de seac-vorm van CFF endchar?
De seac-vorm van endchar is een verouderde manier voor een CFF Type 2-charstring om te zeggen "deze glyph is een basisletter plus een accent": heeft endchar vier argumenten, dan zijn het adx ady bchar achar. Adobe Technical Note #5177 beschrijft hem in zijn appendix over compatibiliteit met Type 1. De naam komt van de Type 1-operator seac (standard encoding accented character), en CFF-subsets geconverteerd uit Type 1-fonts gebruiken hem nog volop voor Latijnse letters met accenten
Drie details laten implementateurs struikelen. Ten eerste zijn bchar en achar geen glyph-ID's. Het zijn codes in Adobe StandardEncoding, dus de renderer mapt elke code naar een glyphnaam en zoekt die naam vervolgens via de charset van het font om de GID te vinden. Ten tweede heeft Type 2 geen asb-argument, anders dan Type 1 seac, dat asb adx ady bchar achar neemt. In Type 2 gaat de oorsprong van het accent rechtstreeks naar (adx, ady) ten opzichte van de oorsprong van de basisglyph, zonder side-bearing-correctie. Ten derde mogen CID-keyed CFF-fonts seac helemaal niet gebruiken, want er is geen glyph-name-charset om StandardEncoding-codes doorheen te herleiden
Dan is er nog de width. Elke Type 2-charstring mag zijn eerste stack-clearing-operator openen met een optioneel advance-width-argument, en endchar is zo'n operator. Een gewone endchar met één argument voert een width mee; een seac-endchar met vijf argumenten voert een width mee gevolgd door de vier seac-waarden. De regel is argumenten te tellen tegen het verwachte vaste aantal, dat voor seac vier is. Vóór v2.768.11 behandelde HotPDF elke endchar als "pak de width en stop", dus seac-glyphs produceerden een lege outline: geen basis, geen accent, alleen een correct gedimensioneerd gat in de regel
// Illustratie van de endchar-beslissing in een Type 2-interpreter.
// Stack houdt de operands vast die endchar voorafgaan; Count is hoeveel.
if (Count >= 4) and not IsCIDFont then
begin
// 5 operands: de eerste is de advance width, de rest is seac
if Count = 5 then
TakeWidthAndShift; // val de width, houd 4 operands over
if StackDepth = 4 then
begin
BaseGID := GIDForName(StdEncName(Round(Stack[2]))); // bchar
AccentGID := GIDForName(StdEncName(Round(Stack[3]))); // achar
DrawGlyph(BaseGID, Identity);
DrawGlyph(AccentGID, Translate(Stack[0], Stack[1])); // adx, ady
Exit;
end;
end;
// anders: gewone endchar, optionele width, sluit het pad
In de echte interpreter worden beide componentglyphs uitgevoerd met dezelfde charstring-engine, is de matrix van het accent de huidige glyphmatrix gecomponeerd met een translatie naar (adx, ady), en wordt de matrix daarna hersteld zodat latere glyphs op de regel onaangeroerd blijven. Resolueert een van beide namen niet naar een GID, dan wordt die component overgeslagen in plaats van dat de hele glyph faalt — het vergevingsgezinde gedrag dat u wilt wanneer een subset net iets te agressief gesnoeid is
De fix controleren vanuit uw eigen Delphi-code
De snelste manier om de composite-glyph- en seac-afhandeling te verifiëren, is een pagina met geaccentueerde tekst uit ingebedde fonts renderen via beide publieke uitvoerpaden: THotPDF.RenderLoadedPageToBitmap voor rasteruitvoer en THotPDF.BuildLoadedPageSVG met EmbedFonts op True voor vectoruitvoer. Ze delen dezelfde glyph-outline-code, dus beide kregen de fixes uit v2.768.10 en v2.768.11. De pijplijn als geheel wordt behandeld in hoe HotPDF ingebedde TrueType-, CFF- en Type 3-glyphs rendert, en de bitmapopties in PDF-pagina's renderen naar bitmaps in Delphi
uses
System.Classes, Vcl.Graphics, HPDFDoc;
procedure RenderAccentProbe(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
Svg: AnsiString;
Lines: TStringList;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
// Rasterpad: 144 DPI maakt verkeerd geplaatste accenten makkelijk zichtbaar
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('accent-probe.bmp');
finally
Bmp.Free;
end;
// Vectorpad: EmbedFonts = True schrijft glyph-outlines, dus de
// composite- en seac-assemblage is zichtbaar in de SVG-paden zelf
Svg := Pdf.BuildLoadedPageSVG(0, True);
if Svg <> '' then
begin
Lines := TStringList.Create;
try
Lines.Text := string(Svg);
Lines.SaveToFile('accent-probe.svg');
finally
Lines.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Kies testdocumenten bewust. U wilt één PDF met een ingebedde TrueType-subset met é, Å en curly double quotes, en één met een ingebedde CFF-subset (FontFile3, Type1C) geproduceerd uit een Type 1-font, waar seac nog leeft — doorgaans in oudere zetwerkuitvoer en sommige drukworkflows. Tekst gezet in fonts die de producent niet inbedt, zegt u hier niets, want die glyphs komen uit geïnstalleerde Windows-fonts in plaats van uit het ingebedde fontprogramma dat ISO 32000-1 §9.9 beschrijft
Waarom gingen composite glyphs stuk bij font-subsetting?
Composite glyphs gingen stuk bij subsetting omdat de subsetter door de componentrecords liep met een verkeerde stride zodra een component een schaal of 2x2-transform voerde, en daarna de glyph-index van de volgende component verkeerd las. HotPDF heeft dit gefixt in v2.770.83. Subsetting is de schrijfkant-tweeling van de renderingbug: bedt HotPDF een subset van een geïnstalleerd TrueType-font in, dan moet hij elke glyph die een composite aanhaalt behouden en die verwijzingen hernummeren naar de nieuwe, compacte glyph-ID's
De oude walker keek alleen naar de argument-size-vlag en de more-components-vlag, dus hij schoof 6 of 8 bytes per record op. Hij negeerde de 2, 4 of 8 bytes aan transformdata die de scale-vlaggen toevoegen. Na een geschaalde component was de volgende "glyph-index" die hij las in werkelijkheid transformdata, dus de echte component kon buiten de subset blijven, en de hernummeringsstap kon een nieuwe glyph-ID rechtstreeks in de transform wegschrijven. Het resultaat was een letter met accent die in sommige viewers het verkeerde accent, een vervormd accent of helemaal geen accent toonde, en alleen bij fonts die hun componenten schalen — daarom vatten de meeste tests het nooit
Nog twee problemen zaten naast de stride-bug. Een glyph-index hernummeren is niet idempotent, en geneste composites werden dubbel hernummerd: een keer door de passage over elke gemarkeerde composite en nog een keer door een recursieve afdaling erin. HotPDF hernummert elke composite nu precies één keer en stuurt de stap "welke glyphs heeft deze composite nodig" vanuit een werkwachtrij, zodat een cyclus van composites die naar elkaar verwijzen afloopt, glyph-indexen voorbij het glyphaantal uit maxp van het font worden geweigerd en lege glyphs nooit als composites geparsed worden
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'accents-subset.pdf';
Pdf.FontEmbedding := True; // bed de gebruikte glyphs in
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 14);
// é, Å, ü en curly quotes zijn in Arial allemaal composite glyphs
Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
'Café, Ångström, München, “quoted”');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Stuur dat bestand rond via de probe hierboven en via een tweede viewer. Sinds v2.770.83 overleeft elke component van elke composite glyph de subsetting, inclusief componenten achter een schaal of 2x2-transform, dus de accenten renderen hetzelfde in HotPDF, Acrobat en browserviewers
Wat de composite-glyph-afhandeling niet dekt
De composite-glyph-fix dekt placement op basis van offsets — wat vrijwel elk echt font gebruikt — en benadert de rest conservatief. TrueType point-matching-composites, waar de twee argumenten punten noemen om uit te lijnen in plaats van een offset te geven, worden met nul-offset getekend. Seac in CID-keyed CFF-fonts wordt bij ontwerp genegeerd, omdat de specificatie het daar verbiedt. Een seac waarvan de basis- of accentcode geen glyph heeft in een gesnoeide subset, tekent de component die hij maar kan vinden. Geen van deze gevallen laat de renderer crashen, maar produceert u PDF's voor archivering of PDF/A, dan is de veiligere keuze fonts inbedden die offset-composites en Type 2-charstrings zonder seac gebruiken, zodat elke consument — niet slechts één renderer — dezelfde letters ziet
FAQ
Waarom renderen letters met accenten zonder hun accenten in mijn PDF-renderer?
Letters met accenten zijn meestal composite glyphs of CFF-seac-glyphs, en het accent is de enige component met een offset. Een renderer die de component-offset ná de device-matrix toepast, of seac in endchar negeert, laat het accent vallen of plaatst hem verkeerd terwijl de basisletter normaal rendert
Ondersteunt HotPDF de CFF-seac-vorm van endchar?
Ja. Sinds v2.768.11 tekent HotPDF endchar met vier argumenten, of vijf inclusief de width, als basisglyph plus een accent verschoven naar (adx, ady), waarbij beide via StandardEncoding-namen en de font-charset worden herleid, voor zowel bitmaprendering als SVG-export met ingebedde fonts
Blijven composite glyphs behouden wanneer HotPDF TrueType-fonts subset?
Ja. Sinds v2.770.83 stapt HotPDF correct over componenttransformaties van 2, 4 of 8 bytes heen, houdt elke gerefereerde component in de subset aan, hernummert geneste composites één keer en stopt veilig bij cyclische of buiten bereik vallende glyphverwijzingen
Composite-glyph-rendering, CFF-seac-ondersteuning en veilige TrueType-subsetting worden geleverd als onderdeel van de HotPDF Delphi PDF Component, een native VCL-bibliotheek voor Delphi en C++Builder die PDF-documenten creëert, bewerkt, extraheert en rendert zonder externe DLL's