Articolo tecnico

Renderizzare glifi compositi e accenti seac CFF nei PDF

HotPDF renderizza correttamente le lettere accentate dei font incorporati gestendo due meccanismi di font che costruiscono un glifo a partire da altri: i glifi compositi TrueType, i cui offset di componente vanno applicati in unità glifo prima della matrice glifo-dispositivo, e la forma seac di endchar CFF, che cucisce insieme una lettera base e un accento tramite codice StandardEncoding. Sbagliate uno dei due ed é, Å, ü o una virgoletta doppia riccia esce a cui manca un pezzo, mentre le lettere semplici intorno sembrano perfette

Quel dettaglio finale è ciò che rende costosi questi bug. Un rapporto si renderizza, il testo è a posto, e solo «München» ha un buco, o una citazione si apre con un apice singolo e un vuoto. Nessuno sospetta il renderer. La prima teoria è sempre un font danneggiato o un produttore cattivo, e il ticket rimbalza in giro per una settimana. Questo articolo percorre ciò che in realtà va storto in ciascun meccanismo, come lo gestisce il componente HotPDF Delphi dalle v2.768.10 e v2.768.11, e la correzione subsetting correlata nella v2.770.83 che tiene intatti i glifi compositi quando HotPDF scrive font invece di leggerli

Perché scompare solo l'accento da un glifo PDF?

L'accento scompare perché è l'unica parte del glifo che porta un offset diverso da zero. Sia i font TrueType che i CFF risparmiano spazio riutilizzando i contorni: é non viene disegnata come forma nuova ma come «il glifo della e, più il glifo dell'acuto spostato in alto e a destra». La lettera base quasi sempre sta all'offset (0, 0), quindi qualunque errore in come vengono applicati gli offset la lascia intatta. L'accento è il componente che si muove, quindi è il componente che atterra nel posto sbagliato o non viene mai disegnato

Il pattern dei sintomi è una diagnostica a sé. Se le lettere base dei caratteri accentati si renderizzano e i diacritici no, o se la punteggiatura abbinata come “ e ” compare come un solo segno invece che due, smettete di guardare il file font. Un font rotto di solito rompe glifi interi, e li rompe per ogni visualizzatore. Pezzi mancanti di glifi composti in un solo renderer, mentre Acrobat mostra la pagina bene, puntano al codice che assembla i componenti

Come collocano i loro componenti i glifi compositi TrueType?

Un glifo composito TrueType colloca ogni componente con la propria piccola trasformazione affine, espressa in unità font, e quella trasformazione deve agire sul contorno del componente prima della matrice glifo-dispositivo. Nella tabella glyf OpenType un glifo composito ha un conteggio di contorni negativo, seguito da un elenco di record di componente. Ogni record contiene una parola di flag, l'indice del glifo componente, due argomenti che sono byte o parole (ARG_1_AND_2_ARE_WORDS, 0x0001), e una scala opzionale: un valore F2Dot14 per WE_HAVE_A_SCALE (0x0008), due per WE_HAVE_AN_X_AND_Y_SCALE (0x0040), o una matrice 2x2 completa per WE_HAVE_A_TWO_BY_TWO (0x0080). Quando ARGS_ARE_XY_VALUES (0x0002) è impostato, i due argomenti sono un offset dx/dy in unità font. MORE_COMPONENTS (0x0020) dice che segue un altro record

Quindi per ogni componente costruite una matrice di componente CM dalla parte di scala e dall'offset, poi la combinate con M, la matrice che mappa le unità font in pixel dispositivo. L'ordine di quella combinazione è tutto il bug. La moltiplicazione di matrici interna di HotPDF usa la convenzione dei vettori colonna, così il prodotto di A e B applica prima B e poi A. La composizione corretta applica prima CM, nello spazio glifo, e poi M. Scritta al contrario, M gira per prima e l'offset di CM viene aggiunto dopo, già in pixel dispositivo e mai scalato

// Solo illustrazione: un record affine 2D con la convenzione dei vettori colonna,
// dove Mul(A, B) mappa un punto attraverso prima B e poi 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  : unità font -> pixel dispositivo (scala piccola, y capovolta)
// CM : trasformazione di componente; e/f contengono dx/dy in unità font
Sub := Mul(M, CM);   // corretto: offset scalato giù col contorno
// Sub := Mul(CM, M); // sbagliato: dx/dy atterrano come pixel dispositivo grezzi

I numeri rendono il fallimento evidente una volta che li vedete. Arial usa un em da 2048 unità, e il componente accento di é porta il suo offset in quelle unità font. Con l'ordine corretto quell'offset si restringe insieme al contorno a pochi pixel allo zoom normale. Con l'ordine invertito il valore grezzo in unità font viene aggiunto come pixel dispositivo, e nel render di prova di HotPDF l'accento della é di Arial finì a 243 pixel dalla sua lettera, ben fuori dal glifo e di solito fuori da qualunque cosa visibile. La virgoletta doppia sinistra in Arial è due copie della forma dell'apostrofo singolo, la seconda spostata, così si renderizzava come una ‘ singola seguita da spazio vuoto

La parte lineare raramente espone il bug. I componenti accento di solito non portano alcuna scala, e una scala uniforme commuta comunque con M, quindi solo la traslazione rivela il prodotto invertito. È anche per questo che un test rapido con testo latino semplice passa: l'inversione era rimasta nel codice attraverso molte release prima che la v2.768.10 la correggesse, e la mostravano solo i componenti con offset

Ricorsione e cicli

I glifi compositi possono referenziare altri glifi compositi, quindi un renderer li percorre ricorsivamente e deve difendersi. HotPDF tappa la profondità di annidamento a 5 livelli e il totale di componenti per glifo a 1024, salta un componente che nomina il proprio indice di glifo, e tratta la modalità di accoppiamento a punti raramente usata (argomenti che sono numeri di punto invece che offset) come un offset zero. Un font ostile o corrotto può quindi perdere un componente, ma non può appendere il render della pagina

Che cos'è la forma seac di endchar CFF?

La forma seac di endchar è un modo deprecato per una charstring CFF Type 2 di dire «questo glifo è una lettera base più un accento»: quando endchar ha quattro argomenti, sono adx ady bchar achar. L'Adobe Technical Note #5177 la descrive nell'appendice sulla compatibilità con Type 1. Il nome viene dall'operatore Type 1 seac (standard encoding accented character), e i subset CFF convertiti da font Type 1 la usano ancora pesantemente per le lettere latine accentate

Tre dettagli fanno inciampare chi implementa. Primo, bchar e achar non sono ID di glifo. Sono codici nell'Adobe StandardEncoding, quindi il renderer mappa ogni codice in un nome di glifo e poi cerca quel nome attraverso il charset del font per trovare il GID. Secondo, Type 2 non ha un argomento asb, a differenza del seac Type 1, che prende asb adx ady bchar achar. In Type 2 l'origine dell'accento va direttamente a (adx, ady) relativo all'origine del glifo base, senza correzione del side bearing. Terzo, i font CFF CID-keyed non possono usare affatto seac, perché non c'è un charset di nomi di glifo attraverso cui risolvere i codici StandardEncoding

Poi c'è la larghezza. Ogni charstring Type 2 può iniziare il suo primo operatore che svuota la stack con un argomento opzionale di advance width, e endchar è uno di quegli operatori. Un endchar semplice con un argomento porta una larghezza; un endchar seac con cinque argomenti porta una larghezza seguita dai quattro valori seac. La regola è contare gli argomenti contro il numero fisso atteso, che per seac è quattro. Prima della v2.768.11, HotPDF trattava qualunque endchar come «prendi la larghezza e fermati», così i glifi seac producevano un contorno vuoto: niente base, niente accento, solo un vuoto della misura giusta nella riga

// Illustrazione della decisione di endchar in un interprete Type 2.
// La Stack contiene gli operandi che precedono endchar; Count è quanti sono.
if (Count >= 4) and not IsCIDFont then
begin
  // 5 operandi: il primo è l'advance width, gli altri sono seac
  if Count = 5 then
    TakeWidthAndShift;                 // scarta la larghezza, tieni 4 operandi
  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;
// altrimenti: endchar ordinario, larghezza opzionale, chiudi il percorso

Nell'interprete reale entrambi i glifi componente sono eseguiti con lo stesso motore di charstring, la matrice dell'accento è la matrice glifo corrente composizionata con una traslazione a (adx, ady), e la matrice viene ripristinata dopo così i glifi successivi sulla riga non sono toccati. Se uno dei due nomi non si risolve in un GID, quel componente viene saltato invece di far fallire l'intero glifo, che è il comportamento indulgente che volete quando un subset è stato potato un po' troppo aggressivamente

Verificare la correzione dal vostro codice Delphi

Il modo più rapido di verificare la gestione dei glifi compositi e di seac è renderizzare una pagina con testo accentato da font incorporati attraverso entrambi i percorsi di output pubblici: THotPDF.RenderLoadedPageToBitmap per l'output raster e THotPDF.BuildLoadedPageSVG con EmbedFonts impostato a True per l'output vettoriale. Entrambi condividono lo stesso codice dei contorni di glifo, quindi entrambi hanno ricevuto le correzioni v2.768.10 e v2.768.11. La pipeline nel suo complesso è trattata in come HotPDF renderizza i glifi TrueType, CFF e Type 3 incorporati, e le opzioni bitmap in rendering di pagine PDF in bitmap 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;

    // Percorso raster: 144 DPI rende gli accenti fuori posto facili da vedere
    Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bmp <> nil then
    try
      Bmp.SaveToFile('accent-probe.bmp');
    finally
      Bmp.Free;
    end;

    // Percorso vettoriale: EmbedFonts = True scrive i contorni dei glifi, così
    // l'assemblaggio composito e seac è visibile nei path SVG stessi
    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;

Scegliete i documenti di prova con criterio. Vi serve un PDF con un subset TrueType incorporato che contenga é, Å e virgolette doppie ricce, e uno con un subset CFF incorporato (FontFile3, Type1C) prodotto da un font Type 1, che è dove seac vive ancora, tipicamente in output di impaginazione più vecchi e in alcuni workflow di stampa. Il testo composto in font che il produttore non ha incorporato non vi dice nulla qui, perché quei glifi vengono dai font Windows installati invece che dal programma font incorporato descritto in ISO 32000-1 §9.9

Perché i glifi compositi si rompevano durante il subsetting dei font?

I glifi compositi si rompevano durante il subsetting perché il subsetter percorreva i record di componente con lo stride sbagliato ogni volta che un componente portava una scala o una trasformazione 2x2, poi leggeva male l'indice di glifo del componente successivo. HotPDF l'ha corretto nella v2.770.83. Il subsetting è il gemello lato scrittura del bug di rendering: quando HotPDF incorpora un subset di un font TrueType installato, deve tenere ogni glifo che un composito referenzia e rinumerare quei riferimenti ai nuovi ID di glifo compatti

Il vecchio walker guardava solo il flag di dimensione degli argomenti e il flag more-components, quindi avanzava di 6 o 8 byte per record. Ignorava i 2, 4 o 8 byte di dati di trasformazione che i flag di scala aggiungono. Dopo un componente con scala, il prossimo «indice di glifo» che leggeva erano davvero byte di trasformazione, così il componente vero poteva restare fuori dal subset, e il passo di rinumerazione poteva scrivere un nuovo ID di glifo dritto dentro la trasformazione. Il risultato era una lettera accentata che si renderizzava in alcuni visualizzatori con l'accento sbagliato, un accento distorto o nessun accento, e solo per i font che scalano i loro componenti, che è perché la maggior parte dei test non lo beccava mai

Altri due problemi sedevano accanto al bug di stride. Rinumerare un indice di glifo non è idempotente, e i compositi annidati venivano rinumerati due volte: una dal passaggio su ogni composito marcato e di nuovo da una discesa ricorsiva dentro di esso. HotPDF ora rinumera ogni composito esattamente una volta e guida il passo «quali glifi servono a questo composito» da una coda di lavoro, così un ciclo di compositi che si referenziano a vicenda termina, gli indici di glifo oltre il conteggio di glifi maxp del font vengono rifiutati, e i glifi vuoti non vengono mai parsati come compositi

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'accents-subset.pdf';
    Pdf.FontEmbedding := True;          // incorpora i glifi che sono usati
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    // é, Å, ü e le virgolette ricce sono tutti glifi compositi in Arial
    Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
      'Café, Ångström, München, “quoted”');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Fate girare quel file nel round trip attraverso la sonda sopra e attraverso un secondo visualizzatore. Dalla v2.770.83 ogni componente di ogni glifo composito sopravvive al subsetting, inclusi i componenti dietro una scala o una trasformazione 2x2, così gli accenti si renderizzano allo stesso modo in HotPDF, Acrobat e nei visualizzatori dei browser

Cosa non copre la gestione dei glifi compositi

La correzione dei glifi compositi copre il posizionamento basato su offset, che è ciò che usa quasi ogni font reale, e approssima il resto in modo conservativo. I compositi TrueType con accoppiamento a punti, dove i due argomenti nominano punti da allineare invece di dare un offset, vengono disegnati con offset zero. Il seac nei font CFF CID-keyed è ignorato per progetto perché la specifica lo vieta lì. Un seac il cui codice di base o di accento non ha un glifo in un subset potato disegna qualunque componente riesce a trovare. Nessuno di questi casi crasha il renderer, ma se producete PDF per uso archivistico o PDF/A, la mossa più sicura è incorporare font che usino compositi a offset e charstring Type 2 senza seac, così ogni consumatore, non un solo renderer, vede le stesse lettere

FAQ

Perché le lettere accentate si renderizzano senza accenti nel mio renderer PDF?

Le lettere accentate sono di solito glifi compositi o glifi seac CFF, e l'accento è l'unico componente con un offset. Un renderer che applica l'offset del componente dopo la matrice dispositivo, o che ignora seac in endchar, scarta o mette nel posto sbagliato l'accento mentre la lettera base si renderizza normalmente

HotPDF supporta la forma seac di endchar CFF?

Sì. Dalla v2.768.11 HotPDF disegna endchar con quattro argomenti, o cinque includendo la larghezza, come un glifo base più un accento spostato a (adx, ady), risolvendo entrambi attraverso i nomi StandardEncoding e il charset del font, sia per il rendering bitmap sia per l'esportazione SVG con font incorporati

I glifi compositi si conservano quando HotPDF fa il subsetting dei font TrueType?

Sì. Dalla v2.770.83 HotPDF scavalca correttamente le trasformazioni di componente da 2, 4 o 8 byte, tiene ogni componente referenziato nel subset, rinumera i compositi annidati una volta sola e si ferma in sicurezza su riferimenti a glifo ciclici o fuori intervallo

Il rendering dei glifi compositi, il supporto seac CFF e il subsetting TrueType sicuro vengono spediti come parte del HotPDF Delphi PDF Component, una libreria VCL nativa per Delphi e C++Builder che crea, modifica, estrae e renderizza documenti PDF senza DLL esterne