HotPDF renderer accenterede bogstaver fra indlejrede fonte korrekt ved at håndtere to font-mekanismer, der bygger én glyf ud af andre: TrueType composite glyffer, hvis komponent-forskydninger skal anvendes i glyf-enheder før glyf-til-device-matricen, og CFF seac-formen af endchar, som syer et basisbogstav og en accent sammen via StandardEncoding-kode. Får man én af dem forkert, kommer é, Å, ü eller et krøllet dobbelt anførselstegn ud med en manglende del, mens de almindelige bogstaver omkring ser perfekte ud
Netop den sidste detalje er, der gør disse bugs dyre. En rapport renderer, brødteksten er fin, og kun "München" har et hul i sig, eller et citat åbner med et enkelt anførselstegn og et hul. Ingen mistænker rendereren. Den første teori er altid en beskadiget font eller en dårlig producer, og ticketen hopper rundt i en uge. Denne artikel gennemgår, hvad der rent faktisk går galt i hver mekanisme, hvordan HotPDF Delphi Component håndterer det siden v2.768.10 og v2.768.11, og det relaterede subsetting-fix i v2.770.83, der holder composite glyffer intakte, når HotPDF skriver fonte i stedet for at læse dem
Hvorfor forsvinder kun accenten fra en PDF-glyf?
Accenten forsvinder, fordi den er den eneste del af glyffen, der bærer en offset forskellig fra nul. Både TrueType- og CFF-fonte sparer plads ved at genbruge outlines: é tegnes ikke som en ny form, men som "e-glyffen plus acute-glyffen forskubbet op og til højre". Basisbogstavet ligger næsten altid på offset (0, 0), så enhver fejl i, hvordan forskydninger anvendes, rører det ikke. Accenten er den komponent, der flytter sig, så det er den komponent, der lander det forkerte sted eller aldrig bliver tegnet
Symptommønsteret er en diagnostik i sig selv. Renderer basisbogstaverne i accenterede tegn, men diakritikaerne ikke, eller viser parvis tegnsætning som " og " sig som ét mærke i stedet for to, så hold op med at kigge på fontfilen. En ødelagt font bryder som regel hele glyffer, og den bryder dem i enhver viser. Manglende dele af sammensatte glyffer i én renderer, mens Acrobat viser siden fint, peger på koden, der samler komponenterne
Hvordan placerer TrueType composite glyffer deres komponenter?
En TrueType composite glyf placerer hver komponent med sin egen lille affine transform, udtrykt i font-enheder, og den transform skal virke på komponent-outlinen, før glyf-til-device-matricen gør det. I OpenType glyf-tabellen har en composite glyf et negativt contour-antal efterfulgt af en liste over komponent-poster. Hver post holder et flags-word, komponentens glyf-indeks, to argumenter, der enten er bytes eller words (ARG_1_AND_2_ARE_WORDS, 0x0001), og en valgfri skala: én F2Dot14-værdi for WE_HAVE_A_SCALE (0x0008), to for WE_HAVE_AN_X_AND_Y_SCALE (0x0040) eller en fuld 2x2-matriks for WE_HAVE_A_TWO_BY_TWO (0x0080). Er ARGS_ARE_XY_VALUES (0x0002) sat, er de to argumenter en dx/dy-offset i font-enheder. MORE_COMPONENTS (0x0020) siger, at der følger endnu en post
Til hver komponent bygger man en komponentmatrix CM ud fra skala-delen og offseten og kombinerer den så med M, matricen, der mapper font-enheder til device-pixels. Rækkefølgen af den kombination er hele bug'en. HotPDFs interne matrice-multiplikation bruger kolonnevektor-konventionen, så produktet af A og B anvender B først og så A. Den korrekte sammensætning anvender CM først, i glyf-space, og M så. Skrevet den anden vej rundt kører M først, og CMs offset lægges til bagefter, allerede i device-pixels og aldrig skaleret
// Kun illustration: en 2D affine-record med kolonnevektor-konventionen,
// hvor Mul(A, B) mapper et punkt gennem B først og derefter gennem A
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-enheder -> device-pixels (lille skala, y vendt)
// CM : komponent-transform; e/f holder dx/dy i font-enheder
Sub := Mul(M, CM); // korrekt: offset skaleret ned med outlinen
// Sub := Mul(CM, M); // forkert: dx/dy lander som rå device-pixels
Tallene gør fejlen indlysende, når man ser dem. Arial bruger en 2048-enheds em, og accent-komponenten af é bærer sin offset i de font-enheder. Med den korrekte rækkefølge skrumper offseten sammen med outlinen til et par pixels ved normal zoom. Med den omvendte rækkefølge lægges den rå font-enheds-værdi til som device-pixels, og i HotPDFs test-render endte accenten af Arials é 243 pixels fra sit bogstav, langt uden for glyffen og som regel uden for alt synligt. Det venstre dobbelte anførselstegn i Arial er to kopier af enkelt-anførselstegns-formen, den anden forskubbet, så det renderede som et ensomt ' efterfulgt af tomrum
Den lineære del afslører sjældent bug'en. Accent-komponenter bærer som regel slet ingen skala, og en ensartet skala commuter med M alligevel, så kun translationen afslører det omvendte produkt. Det er også derfor, en hurtig test med almindelig latinsk tekst består: omvendingen havde siddet i koden gennem mange udgivelser, før v2.768.10 korrigerede den, og kun komponenter med offsets viste den nogensinde
Rekursion og cykler
Composite glyffer kan referere til andre composite glyffer, så en renderer går dem rekursivt igennem og må forsvare sig. HotPDF begrænser nesting-dybden til 5 niveauer og det samlede komponentantal pr. glyf til 1024, springer en komponent over, der nævner sit eget glyf-indeks, og behandler den sjældent brugte point-matching-mode (argumenter, der er punktnumre frem for offsets) som en nul-offset. En fjendtlig eller korrupt font kan derfor miste en komponent, men den kan ikke hænge side-renderingen op
Hvad er seac-formen af CFF endchar?
Seac-formen af endchar er en deprecieret måde for en CFF Type 2 charstring at sige "denne glyf er et basisbogstav plus en accent" på: har endchar fire argumenter, er de adx ady bchar achar. Adobe Technical Note #5177 beskriver den i sit appendiks om kompatibilitet med Type 1. Navnet kommer fra Type 1 seac-operatoren (standard encoding accented character), og CFF-subsets konverteret fra Type 1-fonte bruger den stadig flittigt til accenterede latinske bogstaver
Tre detaljer vælter implementatorer. For det første er bchar og achar ikke glyf-ID'er. De er koder i Adobe StandardEncoding, så rendereren mapper hver kode til et glyf-navn og slår så navnet op gennem fontens charset for at finde GID'en. For det andet har Type 2 intet asb-argument, i modsætning til Type 1 seac, som tager asb adx ady bchar achar. I Type 2 går accentens origo direkte til (adx, ady) relativt til basisglyfens origo, uden side-bearing-korrektion. For det tredje må CID-keyed CFF-fonte slet ikke bruge seac, for der er intet glyf-navne-charset at opløse StandardEncoding-koder gennem
Så er der bredden. Hver Type 2 charstring må starte sin første stack-clearing-operator med et valgfrit advance width-argument, og endchar er en af de operatorer. En almindelig endchar med ét argument bærer en bredde; en seac endchar med fem argumenter bærer en bredde efterfulgt af de fire seac-værdier. Reglen er at tælle argumenter op imod det forventede faste antal, som er fire for seac. Før v2.768.11 behandlede HotPDF enhver endchar som "tag bredden og stop", så seac-glyffer frembragte en tom outline: ingen basis, ingen accent, blot et korrekt størrelsesbestemt hul i linjen
// Illustration af endchar-beslutningen i en Type 2-fortolker.
// Stack holder de operander, der går forud for endchar; Count er hvor mange.
if (Count >= 4) and not IsCIDFont then
begin
// 5 operander: den første er advance-bredden, resten er seac
if Count = 5 then
TakeWidthAndShift; // drop bredden, behold 4 operander
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;
// ellers: almindelig endchar, valgfri bredde, luk path'en
I den rigtige fortolker eksekveres begge komponent-glyffer med samme charstring-engine, accentens matrix er den aktuelle glyf-matrix sammensat med en translation til (adx, ady), og matricen gendannes bagefter, så senere glyffer på linjen påvirkes ikke. Opløser ét af navnene sig ikke til en GID, springes den komponent over i stedet for at fejle hele glyffen, hvilket er den tilgivende adfærd, man vil have, når en subset er blevet beskåret lidt for aggressivt
At tjekke fixet fra egen Delphi-kode
Den hurtigste måde at verificere composite glyf- og seac-håndtering på er at rendere en side med accenteret tekst fra indlejrede fonte gennem begge offentlige output-veje: THotPDF.RenderLoadedPageToBitmap til raster-output og THotPDF.BuildLoadedPageSVG med EmbedFonts sat til True til vektor-output. Begge deler samme glyf-outline-kode, så begge modtog v2.768.10- og v2.768.11-fixene. Pipelinen som helhed er dækket i hvordan HotPDF renderer indlejrede TrueType-, CFF- og Type 3-glyffer, og bitmap-options i rendering af PDF-sider til bitmapmer i 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;
// Raster-vej: 144 DPI gør forskubbet accenter lette at spotte
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('accent-probe.bmp');
finally
Bmp.Free;
end;
// Vektor-vej: EmbedFonts = True skriver glyf-outlines, så
// composite- og seac-samlingen er synlig i SVG-path'erne selv
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;
Vælg testdokumenter med omtanke. Man vil have én PDF med et indlejret TrueType-subset indeholdende é, Å og krøllede dobbelte anførselstegn, og én med et indlejret CFF-subset (FontFile3, Type1C) produceret fra en Type 1-font, som er der, hvor seac stadig lever, typisk i ældre sætteriarbejde og nogle print-workflows. Tekst sat i fonte, producenten ikke indlejrede, fortæller ingenting her, for de glyffer kommer fra installerede Windows-fonte frem for det indlejrede fontprogram beskrevet i ISO 32000-1 §9.9
Hvorfor brød composite glyffer sammen under font-subsetting?
Composite glyffer brød sammen under subsetting, fordi subsetteren gik gennem komponent-poster med forkert stride, når en komponent bar en skala eller 2x2-transform, og så læste næste komponents glyf-indeks forkert. HotPDF fixede dette i v2.770.83. Subsetting er rendering-bug'ens tvilling på skrivesiden: når HotPDF indlejrer et subset af en installeret TrueType-font, skal den beholde hver glyf, en composite refererer til, og omnummerere de referencer til de nye, kompakte glyf-ID'er
Den gamle walker kiggede kun på argument-størrelses-flagget og more-components-flagget, så den rykkede 6 eller 8 bytes pr. post. Den ignorerede de 2, 4 eller 8 bytes transform-data, som skala-flaggene tilføjer. Efter en skaleret komponent var næste "glyf-indeks", den læste, i virkeligheden transform-bytes, så den rigtige komponent kunne blive udeladt af subsettet, og omnummereringstrinnet kunne skrive et nyt glyf-ID direkte ind i transformen. Resultatet var et accenteret bogstav, der renderede i nogle visere med den forkerte accent, en forvreden accent eller slet ingen, og kun for fonte, der skalerer deres komponenter, hvilket er derfor, de fleste tests aldrig fangede det
To flere problemer lå ved siden af stride-bug'en. At omnummerere et glyf-indeks er ikke idempotent, og indlejrede composites blev omnummereret to gange: én gang af gennemløbet over hver markeret composite og igen af et rekursivt dyk ned i den. HotPDF omnummererer nu hver composite præcis én gang og driver "hvilke glyffer behøver denne composite"-trinnet fra en arbejdskø, så en cyklus af composites, der refererer til hinanden, terminerer, glyf-indekser ud over fontens maxp-glyfantal afvises, og tomme glyffer aldrig parses som composites
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'accents-subset.pdf';
Pdf.FontEmbedding := True; // indlejre de glyffer, der bruges
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 14);
// é, Å, ü og krøllede anførselstegn er alle composite glyffer i Arial
Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
'Café, Ångström, München, “quoted”');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Send den fil igennem prooven ovenfor og igennem en anden viser. Siden v2.770.83 overlever hver komponent af hver composite glyf subsetting, inklusive komponenter bag en skala eller 2x2-transform, så accenterne renderer ens i HotPDF, Acrobat og browser-visere
Hvad composite glyf-håndteringen ikke dækker
Composite glyf-fixet dækker offset-baseret placering, hvilket er, hvad næsten hver rigtig font bruger, og approksimerer resten konservativt. TrueType point-matching-composites, hvor de to argumenter navngiver punkter at justere efter i stedet for at give en offset, tegnes med nul-offset. Seac i CID-keyed CFF-fonte ignoreres med vilje, fordi specifikationen forbyder den der. En seac, hvis basis- eller accentkode ikke har nogen glyf i et beskåret subset, tegner den komponent, den kan finde. Ingen af tilfældene crasher rendereren, men producerer man PDF'er til arkivering eller PDF/A-brug, er det sikrere træk at indlejre fonte, der bruger offset-composites og Type 2 charstrings uden seac, så enhver forbruger, ikke bare én renderer, ser de samme bogstaver
FAQ
Hvorfor renderer accenterede bogstaver uden deres accenter i min PDF-renderer?
Accenterede bogstaver er som regel composite glyffer eller CFF seac-glyffer, og accenten er den eneste komponent med en offset. En renderer, der anvender komponent-offseten efter device-matricen, eller som ignorerer seac i endchar, dropper eller forskubber accenten, mens basisbogstavet renderer normalt
Understøtter HotPDF CFF seac-formen af endchar?
Ja. Siden v2.768.11 tegner HotPDF endchar med fire argumenter, eller fem inklusive bredden, som en basisglyf plus en accent forskubbet til (adx, ady), hvor begge opløses gennem StandardEncoding-navne og font-charseten, til både bitmap-rendering og SVG-eksport med indlejrede fonte
Bevares composite glyffer, når HotPDF subsets TrueType-fonte?
Ja. Siden v2.770.83 går HotPDF korrekt over komponent-transforms på 2, 4 eller 8 bytes, beholder hver refereret komponent i subsettet, omnummererer indlejrede composites én gang og stopper sikkert ved cykliske eller uden for rækkevidde liggende glyf-referencer
Composite glyf-rendering, CFF seac-understøttelse og sikker TrueType-subsetting følger med som en del af HotPDF Delphi PDF Component, et native VCL-bibliotek til Delphi og C++Builder, der opretter, redigerer, udtrækker og renderer PDF-dokumenter uden eksterne DLL'er