Articolo tecnico

Font variabili CFF2 nel PDF con HotPDF per Delphi

HotPDF incorpora i font variabili OpenType con outline CFF2 convertendoli, ai valori di assi scelti, in un font program CFF1 con chiave CID che i reader PDF già capiscono. Il testo nativo ordinario registrato tramite RegisterUnicodeTTF usa le tabelle cmap e hmtx del font stesso, e lo writer incorpora il programma convertito sotto /FontFile3 con /Subtype /CIDFontType0C, mantenendo ogni CID uguale al glyph ID originale

Pipeline HotPDF per incorporare in Delphi un font variabile CFF2 come CFF1 con chiave CID sotto FontFile3, con la deviazione silenziosa della sostituzione GDI che il percorso dei byte registrati evita
HotPDF congela la variazione ai valori di assi scelti e scrive un programma CFF1 con chiave CID in cui ogni CID eguaglia il glyph ID originale, così nessun livello traduce tra numerazioni

La strada ovvia merita una descrizione, perché fallisce senza dire niente a nessuno. Fate il parsing e mettete in cache le tabelle CFF2, poi lasciate che il percorso testo semplice risolva il font attraverso la GDI di Windows con un match solo TrueType: la GDI non ha idea di cosa farsi degli outline CFF2 e sostituisce in silenzio ArialUnicodeMS. Qualunque cosa a valle legga il sostituto riceve gli outline sbagliati, le larghezze sbagliate e l'estrazione sbagliata per i caratteri dei piani supplementari. Il PDF si apre benissimo. Solo non è composto nel font che avete chiesto, ed è per questo che il percorso testo deve lavorare dai byte registrati invece che da una ricerca GDI

Perché un PDF non può incorporare direttamente un font CFF2?

Il PDF non ha uno slot per un programma CFF2. ISO 32000-2 §9.9 elenca i tipi di font program incorporato che un reader deve saper gestire: Type 1 sotto /FontFile, TrueType sotto /FontFile2, e sotto /FontFile3 un programma CFF nudo (Type1C o CIDFontType0C) o un wrapper OpenType attorno a outline TrueType o CFF. Ogni variante CFF in quella lista significa CFF versione 1, il formato che i reader interpretano da due decenni

I tre slot dei font incorporati del PDF, tutti limitati a CFF versione 1, e lo slot CFF2 mancante che porta HotPDF a convertire prima di incorporare in Delphi
ogni variante CFF che i reader PDF devono gestire è CFF versione 1, quindi un font variabile CFF2 non ha slot finché non viene convertito

CFF2 è un formato diverso che per caso condivide un nome. Butta via l'INDEX di Name e String, butta via il charset, toglie gli advance dei glyph dai CharStrings lasciandoli a hmtx, e aggiunge gli operatori blend così un solo outline può descrivere l'intero design space. Un parser CFF1 che incontra una tabella CFF2 non ha alcuna base per tirare a indovinare, e nulla nella specifica PDF lo obbliga a provarci. Avvolgere il font CFF2 in uno stream OpenType e sperare per il meglio è esattamente il tipo di file che renderizza su una macchina e fallisce sulla successiva, quindi HotPDF non lo fa

La conversione deve anche essere statica. Come spiega l'articolo sull'instancing statico dei font variabili, un font PDF è un insieme fisso di outline con metriche fisse, e nessun reader è tenuto ad applicare coordinate di assi. HotPDF quindi risolve prima la variazione e solo dopo scrive CFF1

Scrivere testo nativo in un font CFF2

Per il codice applicativo, un font CFF2 si comporta come qualunque altro font Unicode registrato: scegliete i valori degli assi, registrate il file, selezionate la famiglia per nome e disegnate. RegisterUnicodeTTF conserva una copia separata della tabella nomi originale, così SetFont riconosce il nome famiglia, il nome completo, il nome PostScript o il nome famiglia tipografico e seleziona i byte statici congelati del font, senza installazioni Windows né ricerche GDI

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure.pdf';
    Pdf.BeginDoc;
    // I valori degli assi valgono per i font registrati dopo questa chiamata
    Pdf.SetVariableFontAxis('wght', 650);
    Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
    // Nome famiglia, completo, PostScript o famiglia tipografica dalla tabella nomi
    Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
    Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

I glyph ID arrivano dalla cmap registrata e gli advance dall'hmtx registrato. Un glyph il cui advance è davvero zero resta zero invece di ripiegare su una misura GDI, e una coppia di surrogate UTF-16 produce esattamente un advance, non due. I caratteri BMP, i caratteri supplementari e i portatori di shaping sintetici tengono mappature Unicode per font, così passare a un altro font incorporato o iniziare una nuova pagina non lascia che l'altro font aggiorni lo stato subset di questo o prestiti i suoi glyph ID

La misura segue gli stessi byte. THPDFPage.GetWideTextWidth e THPDFPage.GetWideCharAdvances leggono le metriche del font selezionato appoggiato ai byte, così il ritorno di riga e l'allineamento a destra concordano con ciò che viene disegnato. Un dettaglio di ordine becca molti: le modifiche via SetVariableFontAxis fatte dopo la registrazione valgono per il prossimo font che registrate, non per l'istanza già congelata. Per avere un secondo peso, impostate il nuovo valore e registrate di nuovo

Come costruisce HPDFConvertCFF2ToCFF il programma con chiave CID?

HPDFConvertCFF2ToCFF produce un programma CFF1 grezzo con chiave CID e un solo Font DICT, in cui il CID eguaglia il glyph ID originale per ogni slot, inclusi quelli vuoti e inutilizzati. Quell'identità è tutto il progetto. Il nome del font, le larghezze /W del PDF, gli advance CFF e la CMap ToUnicode parlano tutti della stessa numerazione, così nessun livello deve tradurre tra glyph ID e CID, e nulla può slittare fuori sincrono

Sul lato PDF il discendente è un font CIDFontType0 con encoding Identity-H o Identity-V e lo stream del programma sta sotto /FontFile3 con /Subtype /CIDFontType0C. Poiché il CID eguaglia già il GID, il discendente non porta alcun /CIDToGIDMap, e lo stream è escluso dall'hook successivo di sostituzione stream usato per i font TrueType registrati. A differenza del wrapper /Subtype /OpenType, lo stream convertito non alza il requisito di versione PDF 1.6

Le larghezze hanno voluto un piccolo trucco. Un CharString Type 2 CFF1 memorizza l'advance come primo operando opzionale relativo a nominalWidthX, e gli operandi interi sono limitati all'intervallo signed a 16 bit. Un advance OpenType è un valore unsigned a 16 bit, fino a 65535. HotPDF imposta nominalWidthX a 32768 e defaultWidthX a 0, poi scrive ogni advance esplicitamente come advance - 32768. Un advance zero diventa -32768 e un advance 65535 diventa 32767, così l'intero intervallo uint16 è rappresentabile senza clamping su nessun glyph

Come HotPDF mappa gli advance unsigned a 16 bit in operandi CFF1 signed con nominalWidthX 32768, tenendo ogni advance del font nell'output PDF Delphi senza clamping
nominalWidthX 32768 sposta la finestra degli operandi così un advance zero viene memorizzato come -32768 e 65535 come 32767 senza clamping su alcun glyph

La geometria riceve la stessa cura. La FontMatrix è [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], così un font da 2048 unità conserva le sue coordinate native. I movimenti e le linee relativi troppo grandi per un solo operando vengono spezzati in passi rappresentabili, e le curve cubiche vengono suddivise in modo da preservarne la forma. I record originali di copyright e licenza, la tabella nomi originale e le flag di embedding viaggiano dentro la stringa Notice del CFF

Chiamare il convertitore fuori da un documento

Il convertitore è una funzione pubblica nell'unità HPDFOpenTypeAdvanced, scritta in Pascal, senza dipendenze da HarfBuzz e senza passi di installazione di font di sistema. Questo lo rende utilizzabile da solo, per esempio per pre-convertire un font in fase di build o per ispezionare cosa lo writer PDF incorporerà

uses
  System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;

procedure ConvertWeight650;
var
  Source, Cff: TBytes;
  Axes: THPDFVariableFontAxisValueArray;
begin
  Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
  SetLength(Axes, 1);
  Axes[0].Tag := 'wght';
  Axes[0].Value := 650;
  Cff := HPDFConvertCFF2ToCFF(Source, Axes);
  if Length(Cff) = 0 then
    raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
  TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;

L'input variabile passa per un'instancing statica di sola geometria alle coordinate date, coprendo outline e advance. Un font CFF2 statico accetta solo una selezione vuota; passargli valori di assi viene trattato come errore invece di essere ignorato. Il risultato è un programma CFF1 nudo, non un contenitore sfnt, quindi non datelo a codice che si aspetta un file OpenType

Il fallimento è un TBytes vuoto, non un'eccezione, per i casi attesi: input che non è CFF2, una selezione di assi che l'instancer non accetta, valori non finiti come NaN, e font il cui fsType OS/2 imposta embedding con licenza restrittiva (0x0002) o embedding solo bitmap (0x0200). Testate sempre la lunghezza. Dentro EndDoc lo writer lo fa per voi e solleva "StoreFont: CFF2 outline conversion failed", che è la chiamata giusta: un font program vuoto in un PDF spedito è molto peggio di un errore visibile nel vostro log

Budget e cancellazione per carichi server

Il terzo parametro opzionale, THPDFVariableFontControl, limita la memoria e permette di cancellare una conversione lunga. Senza di esso il convertitore usa i suoi default: un limite di 128 MiB su input e output del font, un budget di lavoro cumulativo di 1 GiB, e il limite CFF1 di 65535 byte per CharString

type
  TFontJob = class
  private
    FCancelled: Boolean;
    procedure CheckCancel;
  public
    function Convert(const Source: TBytes): TBytes;
    property Cancelled: Boolean read FCancelled write FCancelled;
  end;

procedure TFontJob.CheckCancel;
begin
  if FCancelled then
    Abort;  // propaga fuori da HPDFConvertCFF2ToCFF
end;

function TFontJob.Convert(const Source: TBytes): TBytes;
var
  Control: THPDFVariableFontControl;
begin
  Control := THPDFVariableFontControl.Create(
    32 * 1024 * 1024,           // byte massimi di output
    Int64(256) * 1024 * 1024,   // budget di lavoro cumulativo
    CheckCancel);
  try
    // Selezione nil: istanza default di un font variabile, o un font CFF2 statico
    Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
  finally
    Control.Free;
  end;
end;

Sforamenti di budget e cancellazione vengono riportati come eccezioni, deliberatamente distinti dal percorso del risultato vuoto. Un array vuoto significa che il font non può essere convertito; un'eccezione significa che questo tentativo è stato fermato. Tenete i due separati nella vostra gestione errori, perché riprovare un font non convertibile con un budget più grande non aiuta

Quanto pesa un font CFF2 incorporato?

La cosa principale da pianificare è la dimensione del file: HotPDF incorpora l'intero set di glyph convertito per i font CFF2, che EnableFontSubsetting sia attivo o no. Un font con qualche migliaio di glyph li aggiunge tutti a ogni documento che lo usa, anche se la pagina mostra un solo titolo. Per una brochure è accettabile. Per un report server che scrive migliaia di piccoli PDF è un costo reale da misurare prima di impegnarsi

Le funzionalità di dimensione correlate seguono la stessa regola. I font CFF2 convertiti vengono incorporati interi, quindi né i subset compatti di font abilitati da CompressDocument né la cache persistente di subset di font li riguarda. I font TrueType nello stesso documento continuano a essere subsettati e messi in cache come al solito. L'unico beneficio di tenere ogni glyph è che i font la cui licenza vieta il subsetting vengono incorporati in una forma che lo rispetta

I limiti di risorse sono limiti di conversione finiti, e CFF1 stesso tappa quello che un singolo CharString può contenere. Un glyph il cui CharString convertito supera i 65535 byte, o un font che buca il budget di lavoro, fallisce alla luce del sole invece di essere scritto con un outline vuoto. È il trade-off voluto: un fallimento rumoroso su un font esotico batte un documento con glyph che mancano in silenzio

Il testo nativo CFF2 e il convertitore CFF1 portabile viaggiano con l'HotPDF Delphi PDF component per Delphi e C++Builder, accanto alle funzionalità di instancing di font variabili, color glyph e subsetting di font trattate altrove su questo blog