Salta ai contenuti
getnextpdf.com

Fornire i font in produzione

Il proprio PDF viene reso correttamente sul portatile, poi viene distribuito in un container e ne esce come una fila di riquadri vuoti — il glifo “tofu” — oppure con accenti e caratteri non latini mancanti. La causa è quasi sempre la stessa: il font selezionato non è presente nell’immagine distribuita.

Il motore nativo, in-process, di NextPDF risolve i font a partire da file di font che il registro dei font è in grado di leggere. Non individua automaticamente i font del sistema operativo o di fontconfig — i file di font installati a livello di sistema operativo sono utili solo se si registrano esplicitamente tali file o si aggiunge la directory che li contiene al percorso di ricerca del FontRegistry. Un container costruito a partire da un’immagine di base ridotta non ha font installati con apt/apk, e anche quando li ha, il motore nativo li ignora a meno che non si indirizzi il registro verso i loro file. La soluzione è includere i file di font effettivi all’interno della propria applicazione o immagine e registrarli con il motore. Il registro legge i file TrueType (.ttf), OpenType (.otf) e TrueType Collection (.ttc); è accettato anche il Type1 legacy (.pfb), ma raramente serve per lavori nuovi.

Prima di iniziare, confermare che questi elementi siano a posto:

  • NextPDF core è installato.
  • Si dispone dei file di font effettivi che si intende usare e si è autorizzati a incorporarli. I diritti di incorporamento sono una responsabilità di chi li usa — si veda Incorporare e creare il subset di un font TrueType.
  • La propria build è in grado di copiare tali file nell’artefatto distribuito.

Questa è una guida pratica di tipo operativo. Il codice è minimo; il lavoro sta nella build e nel layout del filesystem. Per i meccanismi a livello di API della registrazione e della creazione del subset di un singolo carattere, leggere la ricetta di incorporamento e subset linkata sopra. Questa pagina tratta di come portare i file sulla macchina e indirizzare il motore verso di essi.

Perché il motore nativo non trova automaticamente i font del sistema operativo

Sezione intitolata “Perché il motore nativo non trova automaticamente i font del sistema operativo”

Esistono due percorsi di rendering distinti, e la questione dei font differisce tra i due.

  • Motore nativo in-process (l’impostazione predefinita, Document / writeHtml): il motore non chiama il sistema dei font del sistema operativo né fontconfig per l’individuazione. Risolve un carattere attraverso il registro dei font, che legge uno specifico file di font che è stato registrato oppure ne trova uno all’interno di una directory configurata come percorso di ricerca. Installare un font con apt-get install fonts-noto o eseguire fc-cache non produce alcun effetto di per sé — il motore nativo vede quei file solo se li si registra o se ne aggiunge la directory al percorso di ricerca del registro.
  • Bridge Chrome (il renderer HTML-to-PDF che pilota un browser headless): questo percorso usa i font installati sull’host attraverso la normale individuazione dei font del browser, perciò i pacchetti di font apt/apk e fontconfig contano lì.

Se si legge una generica indicazione del tipo “installa questi pacchetti di font di sistema nel tuo Dockerfile”, essa si applica al bridge Chrome, non al motore nativo trattato in questa pagina. Per la generazione nativa, includere i file e registrarli.

Passaggio 1 — Includere i file di font effettivi

Sezione intitolata “Passaggio 1 — Includere i file di font effettivi”

Collocare i file di font all’interno dell’albero della propria applicazione in modo che siano versionati e vengano distribuiti con ogni build. Una posizione convenzionale è una directory resources/fonts/.

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

Nominare i file in modo che la ricerca per directory del motore possa trovarli per famiglia e stile. Quando si registra una directory (anziché un file specifico) e in seguito si chiama setFont('DejaVuSans', 'B', 12), il motore cerca file come DejaVuSans-B.ttf, DejaVuSansB.ttf o DejaVuSans.ttf in ciascuna directory configurata. La ricerca per directory costruisce questi nomi candidati a partire dallo stesso codice di stile a una sola lettera che si passa a setFont (B per grassetto, I per corsivo, BI per grassetto-corsivo), non da una parola scritta per esteso — perciò la forma affidabile è Family-<StyleCode>.ttf (ad esempio DejaVuSans-B.ttf o DejaVuSans-BI.ttf), non Family-Bold.ttf. Un file chiamato DejaVuSans-Bold.ttf non viene mai trovato dalla ricerca per directory; per usare un file simile, registrarlo esplicitamente con register() — che analizza il font e lo indicizza per la famiglia e lo stile letti dalle tabelle dei nomi del file stesso, così che il nome di file scritto per esteso non conti più (si veda il Passaggio 2).

Esistono due modi equivalenti per rendere visibili i file. Entrambi passano attraverso NextPDF\Typography\FontRegistry, che implementa NextPDF\Contracts\FontRegistryInterface.

Registrare un file specifico sotto un alias quando si controlla il carattere esatto:

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

register(string $fontFile, string $alias = '', int $fontIndex = 0) accetta file .ttf, .otf e .ttc, oltre al Type1 legacy .pfb (che carica le sue metriche .afm compagne dallo stesso percorso); $fontIndex seleziona un sotto-font all’interno di una TrueType Collection (.ttc). register() analizza il file e indicizza il carattere per la famiglia e lo stile letti dalle sue tabelle dei nomi, così che il nome fisico del file sia irrilevante una volta registrato. L’$alias facoltativo è semplicemente un nome di ricerca aggiuntivo per il carattere — non è un codice di stile e non cambia quale stile fornisce il file; passarlo quando si desidera chiamare setFont() con un nome diverso dal nome di famiglia incorporato nel font. Restituisce il FontInfo analizzato.

Registrare una directory quando si desidera che il motore risolva i caratteri per nome a partire da una cartella che si controlla:

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

Il costruttore di FontRegistry accetta quella directory come primo argomento, e addFontDirectory() aggiunge ulteriori percorsi di ricerca. Anche un Document nudo espone addFontDirectory() per il caso standalone.

Per usare un registro popolato autonomamente, costruire i documenti tramite DocumentFactory, che collega esattamente quel registro a ogni documento che crea:

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

Document::createStandalone() costruisce il proprio registro interno, perciò un carattere registrato su un FontRegistry separato è invisibile per esso. In produzione, passare per DocumentFactory (o per la factory del proprio framework) in modo che il registro popolato sia quello effettivamente in uso.

Ogni integrazione di framework espone gli stessi due concetti come configurazione, così che raramente si tocchi direttamente il registro. Nel nextpdf.php del pacchetto Laravel, fonts_path (predefinito NEXTPDF_FONTS_PATH, con ripiego su resource_path('fonts')) è la directory di ricerca, e preload_fonts è un elenco di percorsi assoluti di file di font analizzati all’avvio del worker. Indirizzare fonts_path verso la directory che si è inclusa e i caratteri registrati si risolvono automaticamente.

Passaggio 3 — Fornire i font in un’immagine Docker

Sezione intitolata “Passaggio 3 — Fornire i font in un’immagine Docker”

In un container, i file di font devono far parte del layer dell’immagine, copiati in fase di build. Poiché il codice dell’applicazione e i font vengono distribuiti insieme quando li si include sotto resources/fonts/, un normale COPY . . li porta già con sé. Se si tengono i font fuori dal contesto di build, copiarli esplicitamente e assicurarsi che il percorso che si registra corrisponda al percorso all’interno dell’immagine.

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

Su un filesystem immutabile o di sola lettura (un container con readOnlyRootFilesystem, un’immagine serverless o un host irrobustito), i file di font vengono letti al momento della generazione e non vengono mai scritti, perciò un mount di sola lettura va bene. L’unica scrittura che il motore potrebbe desiderare è la sua cache dei font analizzati: o si assegna a quella directory un piccolo volume scrivibile, oppure si scalda e si blocca il registro all’avvio (sezione successiva) in modo che non venga tentata alcuna scrittura o registrazione a runtime.

In un worker a lunga durata, analizzare ogni carattere una volta all’avvio, poi bloccare il registro in modo che non avvenga alcuna registrazione per richiesta e una configurazione errata fallisca in modo rumoroso anziché ripiegare silenziosamente:

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

Dopo lock(), register(), addFontDirectory() e warmup() sollevano un’eccezione, il che trasforma un errore di “percorso sbagliato nell’immagine” in un fallimento di avvio duro anziché in una pagina con tofu in produzione.

Aggiungere un controllo smoke di deployment che renda una pagina con ciascun carattere richiesto. Il controllo dell’intestazione qui sotto verifica soltanto che il documento abbia prodotto output — non dimostra che il font sia stato analizzato, incorporato o nemmeno risolto. Un carattere che il motore non riesce a trovare può ripiegare su un font di base standard (e, con il comportamento non-strict attuale, un profilo di conformità potrebbe invece fornire un sostituto incluso) pur continuando a emettere un PDF valido e non vuoto — perciò, anche dove quel ripiego avviene, questo controllo da solo non coglie il degrado silenzioso. Non fare affidamento sul fatto che il ripiego sia garantito o silenzioso su ogni percorso; verificare direttamente il programma incorporato, come mostrato qui sotto:

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

Per far effettivamente fallire il deploy quando un carattere è mancante, controllare nel PDF emesso la presenza del programma di font incorporato. Un carattere registrato che si risolve porta con sé il proprio dizionario di font con un programma incorporato, perciò asserirne la presenza coglie il caso in cui il carattere richiesto non si è mai risolto (qualunque sia stato il ripiego del motore) che il controllo dell’intestazione manca. Quale chiave contenga il programma dipende dal formato dei contorni: i contorni TrueType (.ttf, .ttc) usano /FontFile2, i contorni CFF/OpenType (.otf con contorni PostScript) usano /FontFile3, e il Type1 legacy (.pfb) usa /FontFile.

Se tutto ciò che serve è un segnale “un qualche programma di font incorporato” indipendente dal formato, verificare la presenza di /FontFile da solo — poiché /FontFile è una sottostringa sia di /FontFile2 sia di /FontFile3, una semplice verifica di sottostringa corrisponde già a ogni tipo di contorno, e aggiungere /FontFile2//FontFile3 come rami || aggiuntivi è ridondante:

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

Una semplice sottostringa /FontFile, tuttavia, non è in grado di distinguere i tipi di contorno. Per distinguerli, abbinare il token esatto con un confine di parola in modo che /FontFile non scatti anche su /FontFile2 o /FontFile3:

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

In ogni caso, trattare questo come una mera euristica grossolana, non come un gate di deploy affidabile. Una ricerca grezza di byte sul PDF serializzato è inaccurata per diversi motivi: i programmi di font possono risiedere all’interno di stream di oggetti compressi (dove /FontFile* non compare mai come byte in chiaro), gli aggiornamenti incrementali possono aggiungere o soppiantare oggetti, i font non incorporati o standard-14 legittimamente non portano alcun programma di font, e le differenze di serializzazione (ordine degli oggetti, spaziatura, codifica dei nomi) possono spostare o nascondere il token. Nel migliore dei casi conferma che un qualche carattere ha incorporato un programma — mai che si sia risolto il carattere specifico desiderato.

Per un vero gate di deploy, non fare affidamento sulla ricerca di byte. Analizzare il PDF emesso con un vero parser di PDF o un inspector di oggetti e asserire che l’oggetto font per il carattere di destinazione porti un programma incorporato /FontFile//FontFile2//FontFile3, oppure usare un’asserzione di risoluzione dei font fornita dal prodotto se disponibile per la propria integrazione. Le regex consapevoli dei token mostrate sopra sono utili per un rapido controllo di integrità locale, ma è un’ispezione strutturale ciò che dovrebbe far fallire il deploy. L’incorporamento e la struttura del dizionario di font sono descritti in Incorporare e creare il subset di un font TrueType.

  • createStandalone() ha il proprio registro. Un carattere registrato su un FontRegistry separato non è visibile a un documento standalone. Usare DocumentFactory (o la factory del framework) in modo che il proprio registro sia quello attivo.
  • I file di stile devono esistere come file. Il motore non sintetizza il grassetto o il corsivo a partire da un carattere regolare. Se si chiama setFont('DejaVuSans', 'B'), la ricerca per directory cerca DejaVuSans-B.ttf, DejaVuSansB.ttf o DejaVuSans.ttf (anche le varianti minuscole e .otf) — forma il candidato a partire dal codice di stile letterale B, perciò non cerca mai DejaVuSans-Bold.ttf. Un file con un nome scritto per esteso come DejaVuSans-Bold.ttf si risolve solo quando lo si registra esplicitamente con register(), che lo indicizza per la famiglia e lo stile letti dalle tabelle dei nomi del file stesso a prescindere dal nome di file; affidarsi alla ricerca per directory per trovarlo produce un mancato riscontro, dopo il quale il motore potrebbe ripiegare su un font di base (non un percorso garantito né sempre silenzioso) — il degrado di cui questa pagina avverte.
  • I percorsi con stream-wrapper e remoti vengono rifiutati. Il registro rifiuta i percorsi contenenti uno schema URI o un byte nullo. Registrare solo file locali; per i font recuperati a runtime usare registerFromBinary() con i byte non elaborati.
  • Un registro bloccato è immutabile. Una volta chiamato lock(), qualsiasi register(), addFontDirectory() o warmup() successivo solleva un’eccezione. I metodi di ricerca restano disponibili. Registrare e scaldare ogni cosa prima di bloccare.
  • Le collection CJK sono grandi. Registrare il sotto-font corretto di un .ttc con $fontIndex e prevedere un budget per un subset incorporato più grande. Si vedano le note CJK nella ricetta di incorporamento e subset.
  • Un file di font è input binario non attendibile. Includere solo font provenienti da fonti attendibili e convalidare la provenienza di qualsiasi carattere accettato da utenti finali.
  • Bloccare il registro dopo lo scaldamento rimuove una superficie di mutazione a runtime e fa sì che un errore di percorso fallisca all’avvio anziché degradare silenziosamente l’output.
  • Non interpolare input dell’utente in un percorso di file registrato. Registrare un insieme fisso di caratteri inclusi; non lasciare che una richiesta scelga un percorso di filesystem arbitrario.

Questa guida non avanza alcuna affermazione normativa di conformità a standard. Ogni simbolo mostrato è superficie pubblica verificata: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), l’argomento del costruttore per la directory), il suo contratto NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() e NextPDF\Core\Document::setFont() / addFontDirectory(). Le chiavi Laravel fonts_path e preload_fonts sono la configurazione documentata del pacchetto nextpdf/laravel. Il comportamento di incorporamento e del tag di subset, con le sue citazioni ISO 32000-2, è documentato nella ricetta di incorporamento e subset linkata sotto Vedere anche.