Teknisk artikel

Rendera sammansatta glyfer och CFF seac-accenter i PDF

HotPDF renderar accentförsedda bokstäver ur inbäddade typsnitt korrekt genom att hantera två typsnittsmekanismer som bygger en glyph av andra: TrueType composite glyphs, vars komponentförskjutningar måste tillämpas i glyphenheter före glyph-till-enhet-matrisen, och CFF seac-formen av endchar, som syr ihop en basbokstav och en accent via StandardEncoding-koder. Får du fel på endera blir é, Å, ü eller ett krulligt citattecken ut utan en del, medan de vanliga bokstäverna runt omkring ser perfekta ut

Den sista detaljen är det som gör de här buggarna dyra. En rapport renderas, brödtexten är fin, och bara "München" har ett hål i sig, eller så öppnas ett citat med ett enkelt citattecken och ett gap. Ingen misstänker renderaren. Den första teorin är alltid ett skadat typsnitt eller en dålig producent, och ärendet studsat runt i en vecka. Den här artikeln går igenom vad som faktiskt går fel i varje mekanism, hur HotPDF Delphi-komponenten hanterar det sedan v2.768.10 och v2.768.11, och den relaterade subsetting-fixen i v2.770.83 som håller composite glyphs intakta när HotPDF skriver typsnitt i stället för att läsa dem

Varför försvinner bara accenten från en PDF-glyph?

Accenten försvinner eftersom den är den enda delen av glyphen som bär en förskjutning skild från noll. Både TrueType- och CFF-typsnitt sparar utrymme genom att återanvända konturer: é ritas inte som en ny form utan som "e-glyphen, plus acute-glyphen flyttad upp och åt höger". Basbokstaven ligger nästan alltid vid förskjutningen (0, 0), så ett fel i hur förskjutningar tillämpas lämnar den orörd. Accenten är komponenten som flyttas, så det är komponenten som landar på fel ställe eller aldrig ritas alls

Symptommönstret är en diagnostik i sig. Renderas basbokstäverna i accentförsedda tecken men inte diakritikerna, eller visas parat skiljetecken som “ och ” som ett enda märke i stället för två, sluta titta på typsnittsfilen. Ett trasigt typsnitt brukar bryta hela glyfer, och det gör det i varje visare. Saknade bitar av sammansatta glyfer i en renderare, medan Acrobat visar sidan fint, pekar på koden som monterar ihop komponenterna

Hur placerar TrueType composite glyphs sina komponenter?

En TrueType composite glyph placerar varje komponent med sin egen lilla affina transform, uttryckt i typsnittsenheter, och den transformen måste verka på komponentkonturen innan glyph-till-enhet-matrisen gör det. I OpenType:s glyf-tabell har en composite glyph ett negativt konturantal, följt av en lista av komponentposter. Varje post håller ett flaggord, komponentens glyph-index, två argument som är antingen bytes eller words (ARG_1_AND_2_ARE_WORDS, 0x0001), och en valfri skalning: ett F2Dot14-värde för WE_HAVE_A_SCALE (0x0008), två för WE_HAVE_AN_X_AND_Y_SCALE (0x0040), eller en full 2x2-matris för WE_HAVE_A_TWO_BY_TWO (0x0080). När ARGS_ARE_XY_VALUES (0x0002) är satt är de två argumenten en dx/dy-förskjutning i typsnittsenheter. MORE_COMPONENTS (0x0020) säger att en post till följer

Så för varje komponent bygger du en komponentmatris CM ur skalningsdelen och förskjutningen, och kombinerar den sedan med M, matrisen som mappar typsnittsenheter till enhetspixlar. Ordningen i den kombinationen är hela buggen. HotPDF:s interna matrismultiplikation använder kolumnvektorkonventionen, så produkten av A och B applicerar B först och sedan A. Den korrekta sammansättningen applicerar CM först, i glyphutrymmet, och M sedan. Skrivs det andra hållet löper M först och CM:s förskjutning läggs till efteråt, redan i enhetspixlar och aldrig skalad

// Endast illustration: en 2D-affin post med kolumnvektorkonventionen,
// där Mul(A, B) mappar en punkt genom B först och sedan genom 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  : typsnittsenheter -> enhetspixlar (liten skala, y vänt)
// CM : komponenttransform; e/f håller dx/dy i typsnittsenheter
Sub := Mul(M, CM);   // korrekt: förskjutningen skalas ned med konturen
// Sub := Mul(CM, M); // fel: dx/dy landar som råa enhetspixlar

Siffrorna gör felet uppenbart när du ser dem. Arial använder en em på 2048 enheter, och é:s accentkomponent bär sin förskjutning i de typsnittsenheterna. Med korrekt ordning krymper förskjutningen med konturen till några pixlar vid normal zoom. Med omvänd ordning läggs det råa typsnittsenhetsvärdet till som enhetspixlar, och i HotPDF testrendering hamnade Arial:s é-accent 243 pixlar från sin bokstav, långt utanför glyphen och oftast utanför allt synligt. Det vänstra dubbla citattecknet i Arial är två kopior av enkla-citattecknets form, den andra förskjuten, så det renderades som ett ensamt ‘ följt av tomt utrymme

Den linjära delen avslöjar sällan buggen. Accentkomponenter bär oftast ingen skalning alls, och en uniform skala kommuterar med M ändå, så bara translationen avslöjar den omvända produkten. Det är också varför ett snabbtest med vanlig latinsk text går igenom: omvändningen hade suttit i koden över många utgåvor innan v2.768.10 korrigerade den, och bara komponenter med förskjutningar visade den någonsin

Rekursion och cykler

Composite glyphs får referera andra composite glyphs, så en renderare vandrar genom dem rekursivt och måste försvara sig. HotPDF takar nästlingsdjupet vid 5 nivåer och det totala komponentantalet per glyph vid 1024, hoppar över en komponent som namnger sitt eget glyph-index, och behandlar det sällan använda punktmatchningsläget (argument som är punktnummer snarare än förskjutningar) som en nollförskjutning. Ett fientligt eller korrupt typsnitt kan därför tappa en komponent, men det kan inte låsa sidrenderingen

Vad är seac-formen av CFF endchar?

Seac-formen av endchar är ett föråldrat sätt för en CFF Type 2-charstring att säga "den här glyphen är en basbokstav plus en accent": när endchar har fyra argument är de adx ady bchar achar. Adobe Technical Note #5177 beskriver den i sin bilaga om kompatibilitet med Type 1. Namnet kommer från Type 1-operatorn seac (standard encoding accented character), och CFF-delmängder konverterade från Type 1-typsnitt använder den fortfarande flitigt för accentförsedda latinska bokstäver

Tre detaljer snubblar implementerare. För det första är bchar och achar inte glyph-ID:n. De är koder i Adobe StandardEncoding, så renderaren mappar varje kod till ett glyphnamn och slår sedan upp det namnet via typsnittets charset för att hitta GID:n. För det andra saknar Type 2 ett asb-argument, till skillnad från Type 1:s seac, som tar asb adx ady bchar achar. I Type 2 går accentens ursprung direkt till (adx, ady) relativt basglyphens ursprung, utan någon sidbärskorrektion. För det tredje får CID-nycklade CFF-typsnitt inte använda seac alls, eftersom det inte finns något glyphnamn-charset att lösa StandardEncoding-koder genom

Sedan finns bredden. Varje Type 2-charstring får inleda sin första stacktömmande operator med ett valfritt förflyttningsbreddsargument, och endchar är en av de operatorerna. En vanlig endchar med ett argument bär en bredd; en seac-endchar med fem argument bär en bredd följt av de fyra seac-värdena. Regeln är att räkna argument mot det förväntade fasta antalet, som är fyra för seac. Före v2.768.11 behandlade HotPDF varje endchar som "ta bredden och stanna", så seac-glyfer producerade en tom kontur: ingen bas, ingen accent, bara ett korrekt storlekat gap i raden

// Illustration av endchar-beslutet i en Type 2-tolk.
// Stacken håller operanderna som föregår endchar; Count är antalet.
if (Count >= 4) and not IsCIDFont then
begin
  // 5 operander: den första är förflyttningsbredden, resten är seac
  if Count = 5 then
    TakeWidthAndShift;                 // kasta bredden, behåll 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;
// annars: vanlig endchar, valfri bredd, stäng banan

I den riktiga tolken exekveras båda komponentglypherna med samma charstring-motor, accentens matris är den aktuella glyphmatrisen komponerad med en translation till (adx, ady), och matrisen återställs efteråt så att senare glyfer på raden påverkas inte. Löser inget av namnen till en GID hoppas den komponenten över i stället för att fälla hela glyphen, vilket är det förlåtande beteende du vill ha när en delmängd trimnats lite för aggressivt

Kontrollera fixen från din egen Delphi-kod

Det snabbaste sättet att verifiera hanteringen av composite glyphs och seac är att rendera en sida med accentförsedd text ur inbäddade typsnitt genom båda publika utdatavägarna: THotPDF.RenderLoadedPageToBitmap för rasterutdata och THotPDF.BuildLoadedPageSVG med EmbedFonts satt till True för vektorutdata. Båda delar samma glyphkonturkod, så båda fick v2.768.10- och v2.768.11-fixarna. Pipelinen som helhet täcks i hur HotPDF renderar inbäddade TrueType-, CFF- och Type 3-glyfer, och bitmap-alternativen i att rendera PDF-sidor till bitmappar 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;

    // Rastervägen: 144 DPI gör felplacerade accenter lätta att upptäcka
    Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bmp <> nil then
    try
      Bmp.SaveToFile('accent-probe.bmp');
    finally
      Bmp.Free;
    end;

    // Vektorvägen: EmbedFonts = True skriver glyphkonturer, så
    // composite- och seac-montaget syns i SVG-path:na själva
    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älj testdokument med omsorg. Du vill ha en PDF med en inbäddad TrueType-delmängd som innehåller é, Å och krulliga dubbla citattecken, och en med en inbäddad CFF-delmängd (FontFile3, Type1C) producerad ur ett Type 1-typsnitt, vilket är där seac fortfarande bor, typiskt i äldre typsättningsutdata och vissa tryckarbetsflöden. Text satt i typsnitt producenten inte bäddade in säger dig ingenting här, eftersom de glyferna kommer från installerade Windows-typsnitt i stället för från det inbäddade typsnittsprogram som ISO 32000-1 §9.9 beskriver

Varför gick composite glyphs sönder vid typsnittssubsetting?

Composite glyphs gick sönder vid subsetting eftersom delmängdsgeneratorn stegade genom komponentposter med fel steglängd närhelst en komponent bar en skalning eller 2x2-transform, och sedan feltolkade nästa komponents glyph-index. HotPDF fixade det i v2.770.83. Subsetting är skrivsidans tvilling till renderingsbuggen: när HotPDF bäddar in en delmängd av ett installerat TrueType-typsnitt måste den behålla varje glyph en composite refererar och omnumrera de referenserna till nya, kompakta glyph-ID:n

Den gamla vandraren tittade bara på argumentstorleksflaggan och more-components-flaggan, så den avancerade 6 eller 8 bytes per post. Den ignorerade de 2, 4 eller 8 bytes transformdata som skalningsflaggorna lägger till. Efter en skalad komponent var nästa "glyph-index" den läste i själva verket transformbytes, så den riktiga komponenten kunde utelämnas ur delmängden, och omnumreringssteget kunde skriva ett nytt glyph-ID rakt in i transformen. Resultatet var en accentförsedd bokstav som renderades i vissa visare med fel accent, en förvriden accent, eller ingen alls, och bara för typsnitt som skalar sina komponenter, vilket är varför de flesta tester aldrig fångade det

Två fler problem satt bredvid steglängdsbuggen. Att omnumrera ett glyph-index är inte idempotent, och nästlade composites omnumrerades två gånger: en gång av passet över varje markerad composite och igen av en rekursiv nedstigning i den. HotPDF omnumrerar nu varje composite exakt en gång och driver steget "vilka glyfer behöver den här compositen" från en arbetskö, så en cykel av composites som refererar varandra terminerar, glyph-index bortom typsnittets maxp-glyphantal avvisas, och tomma glyfer parsas aldrig som composites

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'accents-subset.pdf';
    Pdf.FontEmbedding := True;          // bädda in de glyfer som används
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    // é, Å, ü och krulliga citattecken är alla composite glyphs i Arial
    Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
      'Café, Ångström, München, “quoted”');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Skicka filen en rundtur genom proben ovan och genom en andra visare. Sedan v2.770.83 överlever varje komponent i varje composite glyph subsetting, inklusive komponenter bakom en skalning eller 2x2-transform, så accenter renderas likadant i HotPDF, Acrobat och webbläsarvisare

Vad hanteringen av composite glyphs inte täcker

Composite glyph-fixen täcker förskjutningsbaserad placering, vilket är vad nästan alla verkliga typsnitt använder, och approximerar resten konservativt. TrueType-punktmatchande composites, där de två argumenten namnger punkter att rikta in i stället för att ge en förskjutning, ritas med nollförskjutning. Seac i CID-nycklade CFF-typsnitt ignoreras med flit eftersom specifikationen förbjuder det där. En seac vars bas- eller accentkod saknar glyph i en trimmad delmängd ritar den komponent den kan hitta. Inget av dessa fall kraschar renderaren, men producerar du PDF:er för arkiv eller PDF/A är det säkrare draget att bädda in typsnitt som använder förskjutningscomposites och Type 2-charstrings utan seac, så varje konsument, inte bara en renderare, ser samma bokstäver

FAQ

Varför renderas accentförsedda bokstäver utan sina accenter i min PDF-renderare?

Accentförsedda bokstäver är oftast composite glyphs eller CFF seac-glyfer, och accenten är den enda komponenten med en förskjutning. En renderare som tillämpar komponentförskjutningen efter enhetsmatrisen, eller som ignorerar seac i endchar, tappar eller felplacerar accenten medan basbokstaven renderas normalt

Stöder HotPDF CFF seac-formen av endchar?

Ja. Sedan v2.768.11 ritar HotPDF endchar med fyra argument, eller fem inklusive bredden, som en basglyph plus en accent flyttad till (adx, ady), med båda lösta genom StandardEncoding-namn och typsnittets charset, för både bitmap-rendering och SVG-export med inbäddade typsnitt

Bevaras composite glyphs när HotPDF skapar delmängder av TrueType-typsnitt?

Ja. Sedan v2.770.83 stegar HotPDF över komponenttransformer på 2, 4 eller 8 bytes korrekt, behåller varje refererad komponent i delmängden, omnumrerar nästlade composites en gång, och stannar säkert på cykliska eller utanför intervallet liggande glyphreferenser

Rendering av composite glyphs, CFF seac-stöd och säker TrueType-subsetting medföljer som del av HotPDF Delphi PDF Component, ett inbyggt VCL-bibliotek för Delphi och C++Builder som skapar, redigerar, extraherar och renderar PDF-dokument utan externa DLL:er