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
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
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
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