Articolo tecnico

Rendering dei font PDF incorporati in Delphi con HotPDF

HotPDF disegna in Delphi i font PDF incorporati senza installare nulla sulla macchina: la catena di rendering dei glifi incorporati in HPDFGlyphRender.pas analizza i programmi di font conservati dentro il PDF stesso — contorni TrueType glyf da FontFile2, charstring CFF Type 2 da FontFile3 e content stream di glifo Type 3 — e li riesegue come percorsi vettoriali GDI riempiti. Questo articolo è l'approfondimento sulla fedeltà dei font che sta dietro a disegnare pagine PDF su una TBitmap con HotPDF: quel pezzo tratta il renderer nel suo insieme, questo tratta come il testo di quelle pagine ottiene le sue forme esatte

Perché un PDF viene disegnato con dei quadratini al posto del testo?

Quadratini, spazi vuoti o caratteri leggermente sbagliati nell'uscita PDF disegnata quasi sempre significano che il renderer ha chiesto un font al sistema operativo invece di usare quello incorporato nel file. La segnalazione arriva sempre allo stesso modo: il documento sembra perfetto sulla macchina che lo ha prodotto, poi un cliente lo apre su un server appena installato o su una postazione bloccata e la fattura in giapponese mostra quadratini vuoti, oppure un carattere sostitutivo somigliante sposta ogni ritorno a capo. I font non erano mai stati su quella macchina, esistevano solo dentro il PDF, e un renderer che si ferma alla sostituzione con i font di sistema non può vederli. I font in sottoinsieme peggiorano la cosa: un sottoinsieme può portare quaranta glifi sotto codici di carattere assegnati privatamente a quel solo file, una assegnazione che nessun font installato condivide

La ISO 32000-1 §9.9 definisce tre contenitori per un programma di font incorporato nel descrittore di font: FontFile contiene un programma Type 1 originale, FontFile2 un programma TrueType e FontFile3 un CFF nudo (Type1C o CIDFontType0C) oppure un involucro OpenType. Una quarta variante, il font Type 3 della ISO 32000-1 §9.6.5, non incorpora alcun binario: ogni glifo è un piccolo content stream PDF eseguito sul posto. I tre contenitori differiscono nella matematica dei contorni (B-spline quadratiche contro charstring cubici contro operatori di pagina arbitrari), quindi un renderer fedele ha bisogno di un interprete separato per ciascuno, più uno strato di codifica che trasformi i codici di carattere nel giusto indice di glifo prima che si tocchi un solo contorno

HotPDF: contenitori dei font PDF incorporati: il descrittore di font contiene programmi FontFile, FontFile2 o FontFile3 e content stream Type 3, e ogni contenitore segue un proprio percorso di rendering
Ogni contenitore incorporato ha il proprio interprete, dai contorni TrueType glyf ai content stream Type 3

Come HotPDF converte i contorni glyf TrueType in percorsi GDI?

THPDFEmbeddedTTF in HPDFGlyphRender.pas legge la tabella loca per individuare ogni record di glifo, percorre i contorni glyf punto per punto ed emette un percorso GDI. Due convenzioni TrueType richiedono una gestione esplicita. Primo, punti fuori curva consecutivi implicano un punto sulla curva nel loro punto medio, e un contorno i cui punti sono tutti fuori curva inizia nel punto medio fra il suo ultimo e il suo primo punto: saltate una delle due regole e i glifi arrotondati sviluppano sfaccettature piatte o collassano. Secondo, le curve TrueType sono Bezier quadratiche mentre PolyBezierTo di GDI accetta cubiche, quindi ogni segmento quadratico viene elevato di grado in modo esatto anziché appiattito in segmenti di retta

Catena di rendering glyf TrueType in Delphi: la tabella loca individua i record di glifo, i punti medi impliciti sulla curva vengono ricostruiti, le quadratiche sono elevate esattamente a cubiche e GDI riempie il percorso
Punti medi impliciti ed elevazione di grado esatta mantengono il contorno rieseguito identico alle curve glyf create dal disegnatore
// Elevazione di grado esatta: quadratica (P0, Q, P2) -> cubica (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;
// poi PolyBezierTo con C1, C2, P2 — curva geometricamente identica

L'elevazione di grado è senza perdite: la cubica traccia la curva identica, quindi il contorno disegnato corrisponde a ciò che un visualizzatore conforme disegna dalla stessa tabella, a qualunque ingrandimento. Ciò che resta è il posizionamento. Ogni glifo è disegnato in unità di font (tipicamente una griglia da 1000 o 2048 unità per em), e il renderer compone la matrice di scala, la matrice del testo e la matrice di trasformazione corrente in una sola trasformazione da glifo a dispositivo prima che il percorso venga riempito. Qui l'ordine conta più di quanto sembri: componete le stesse tre matrici al contrario e ogni glifo collassa verso l'origine, una pagina dall'aspetto sbagliato il cui difetto reale è una riga di algebra matriciale

Come l'interprete di charstring Type 2 gestisce i font CFF

THPDFEmbeddedCFF dà ai programmi FontFile3 un vero interprete di charstring Type 2: analizza le strutture INDEX del CFF, il Top DICT e il Private DICT, poi esegue ogni charstring ed emette segmenti di percorso direttamente verso GDI. Un involucro OpenType (il contenitore OTTO) viene prima spogliato per raggiungere la tabella CFF nuda; gli stream CIDFontType0C e Type1C nudi vengono consumati direttamente. I charstring sono un compatto linguaggio a pila, e tre delle sue convenzioni decidono se l'interprete resti sincronizzato con il flusso di byte. Il prefisso opzionale di larghezza fa sì che il primo operatore che svuota la pila possa portare un operando iniziale in più. L'operatore hintmask implica un vstemhm quando ci sono ancora operandi sulla pila, e il numero di byte di maschera da saltare dipende dal conteggio accumulato degli stem: sbagliate il conteggio una volta e ogni opcode successivo viene letto male. E le chiamate a subroutine aggiungono un bias al proprio indice (107, 1131 o 32768 a seconda del numero di subroutine) prima della ricerca, quindi una chiamata senza bias atterra su una subroutine completamente diversa

Il CFF a chiave CID aggiunge una indirezione che fa inciampare le implementazioni ingenue: il codice di carattere seleziona un CID, ma l'indice del charstring è un GID, e il charset del font mappa GID su CID, quindi il renderer costruisce la mappatura inversa da CID a GID prima di disegnare, e seleziona il Private DICT per glifo tramite FDSelect nei font che ne portano più di uno. I programmi Type1C a chiave di nome, il contenitore consueto per i font Type 1 semplici, risolvono invece i codici a un byte tramite la codifica interna del programma CFF o tramite la macchina di codifica a livello di PDF descritta più avanti. Una avvertenza onesta: l'interprete legge gli operatori di hinting per tenere sincronizzato il flusso ma non esegue l'hinting, un confine discusso in chiusura

Che cosa è un font Type 3 e come viene disegnato?

Un glifo Type 3 non è affatto un contorno: la ISO 32000-1 §9.6.5 lo definisce come un content stream, quindi HotPDF lo disegna salvando lo stato grafico, componendo sulla CTM la matrice del font, il corpo e la matrice del testo, ed eseguendo la procedura del glifo con lo stesso interprete di operatori che disegna le pagine, con le /Resources proprie del font nel contesto. Due dettagli della specifica contano per la correttezza. I /Widths Type 3 sono espressi nello spazio del glifo anziché nel millesimo di spazio testo che ogni altro tipo di font usa, quindi gli avanzamenti devono passare per /FontMatrix: altrimenti i font per codici a barre con matrice 0.01 avanzano sbagliati di un ordine di grandezza. E una procedura di glifo che si apre con l'operatore d1 fa due promesse che il renderer fa rispettare: il disegno è ritagliato al riquadro di delimitazione dichiarato, e secondo la ISO 32000-1 §9.6.5.2 il glifo ignora i propri operatori di colore e dipinge con il colore di riempimento corrente del chiamante, quindi rg, g, k e i loro gemelli per il tratto dentro la procedura sono soppressi per la durata di quel glifo. Saltate la regola sul colore e un font per codici a barre d1 timbrato in blu dalla pagina esce nero; saltate il ritaglio e un glifo malformato dipinge fuori dalla propria cella

Come i codici di carattere diventano identificativi di glifo

Gli interpreti di contorni sono solo metà del lavoro, perché i byte in una stringa di testo PDF sono codici di carattere, non indici di glifo, e la ISO 32000-1 dedica due sottoclausole alla mappatura. Per i font semplici, la §9.6.6 prescrive una priorità rigida: un array /Differences scavalca la codifica di base (WinAnsiEncoding, MacRomanEncoding o StandardEncoding), che a sua volta scavalca la mappa interna del programma di font. HotPDF risolve quella catena in una tabella da 256 voci da codice a GID, traducendo i nomi dei glifi in indici per tre vie: corrispondenza esatta nel charset dentro un programma CFF, nomi numerici gNN/glyphNN presi come indici letterali, e traduzione da nome a Unicode con l'Adobe Glyph List seguita da una ricerca nella cmap per i programmi TrueType. Per i font compositi comanda la §9.7 con CIDToGIDMap: il caso comune è /Identity, ma la voce può essere uno stream di coppie big-endian indicizzate per CID, e l'uscita Unicode di HotPDF usa esattamente quella forma a stream per i sottoinsiemi compatti, quindi il percorso a stream non è un angolo esotico

HotPDF: mappatura dei codici di carattere PDF su identificativi di glifo: i font semplici si risolvono tramite Differences, la codifica di base e la mappa del programma di font, i font compositi tramite CIDToGIDMap, poi una catena di ripiego sulla cmap TrueType con un ripiego GDI per singolo glifo
Entrambe le famiglie di font terminano in una tabella da codice a GID, e ogni codice non mappabile degrada un singolo glifo anziché l'intera sequenza di testo
// /CIDToGIDMap come stream: coppie Word big-endian indicizzate per CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // fuori intervallo mappa su .notdef

Quando serve una ricerca nella cmap TrueType, HotPDF percorre una catena di ripieghi invece di fidarsi di una sola sottotabella: vengono prima le sottotabelle Unicode di Windows (formato 4, poi formato 12 per i piani supplementari), poi la sottotabella simbolo (3,0) con la sua convenzione dell'area a uso privato F000 rispecchiata sul byte basso — il motivo per cui un font simbolico come Wingdings risponde a semplici codici ASCII — poi i formati legacy 6 e 0. Le sottotabelle di formato 2 non vengono interpretate di proposito: mappano vecchie tabelle codici multibyte come Shift-JIS e Big5, non Unicode, e i font CJK moderni portano comunque sempre una sottotabella di formato 4 o 12. Ogni codice che non sopravvive a nessuna di queste vie ripiega sul disegno GDI per quel singolo glifo, così un carattere non mappabile degrada un glifo, non l'intera sequenza di testo

Che cosa il percorso incorporato non fa

Vale la pena enunciare i confini con chiarezza. L'hinting non viene eseguito: i contorni sono riempiti così come sono stati disegnati, il che è indistinguibile da una uscita con hinting a 150 DPI e oltre ma può differire di un pixel da un rasterizzatore con hinting ai corpi molto piccoli. I programmi Type 1 originali in FontFile (charstring cifrati con eexec) non vengono interpretati, e gli assi dei font variabili OpenType non vengono applicati; entrambi i casi, come un programma di font danneggiato o una tabella glyf senza alcuna cmap utilizzabile, ripiegano sul disegno con i font di sistema invece di far fallire la pagina. Lo stesso approccio che mette la fedeltà al primo posto si estende ad altre parti del renderer — i motivi di sfumatura assiali e radiali ricevono il trattamento che le sfumature meritano — e il lato della generazione ha una propria storia di sottigliezze sui font in come EndDoc ordina i sottoinsiemi di font

Usare questa catena non richiede alcun codice specifico per i font: ogni meccanismo descritto sopra entra in azione automaticamente dentro la chiamata di rendering della pagina

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // I font TrueType, CFF e Type 3 incorporati vengono disegnati dal
      // file stesso — non serve installare nulla su questa macchina
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

La conseguenza pratica è quella che interessa alla vostra casella dell'assistenza: un PDF che porta con sé i propri font viene disegnato con quei font, su un server di build, in un container Windows o su una postazione cliente che non ha mai visto quel carattere tipografico. La catena di rendering dei glifi incorporati è distribuita come parte dello HotPDF Delphi Component per Delphi e C++Builder, una libreria VCL nativa che copre creazione, modifica, estrazione del testo e rendering di pagina dei PDF senza dipendenze da DLL esterne