Ga naar inhoud
getnextpdf.com

Lettertypen voorzien in productie

Je PDF rendert correct op je laptop, gaat dan naar een container en komt eruit als een rij lege vakjes — de “tofu”-glyph — of met ontbrekende accenten en niet-Latijnse tekens. De oorzaak is bijna altijd dezelfde: het lettertype dat je hebt geselecteerd is niet aanwezig in de gedeployde image.

De native, in-process NextPDF-engine lost lettertypen op uit lettertypebestanden die het lettertyperegister kan lezen. Het ontdekt OS- of fontconfig-lettertypen niet automatisch — OS-geïnstalleerde lettertypebestanden helpen alleen als je die bestanden expliciet registreert of hun bovenliggende map toevoegt aan het zoekpad van het FontRegistry. Een container gebouwd op een slank base-image heeft geen via apt/apk geïnstalleerde lettertypen, en zelfs wanneer het die wel heeft, negeert de native engine ze tenzij je het register naar hun bestanden wijst. De oplossing is de werkelijke lettertypebestanden mee te leveren binnen je applicatie of image en ze te registreren bij de engine. Het register leest TrueType- (.ttf), OpenType- (.otf) en TrueType Collection-bestanden (.ttc); legacy Type1 (.pfb) wordt ook geaccepteerd maar is zelden nodig voor nieuw werk.

Bevestig voordat je begint dat deze onderdelen op hun plek staan:

  • NextPDF core is geïnstalleerd.
  • Je hebt de werkelijke lettertypebestanden die je wilt gebruiken, en je hebt het recht ze in te sluiten. Insluitingsrechten zijn jouw verantwoordelijkheid — zie Een TrueType-lettertype insluiten en subsetten.
  • Je build kan die bestanden naar het gedeployde artefact kopiëren.

Dit is een operationele how-to. De code is minimaal; het werk zit in de build en de filesystem-indeling. Lees voor de API-niveau-mechanica van het registreren en subsetten van één face het hierboven gelinkte embed-and-subset-recipe. Deze pagina behandelt het op de doos krijgen van de bestanden en het wijzen van de engine naar die bestanden.

Waarom de native engine OS-lettertypen niet automatisch vindt

Sectie met titel “Waarom de native engine OS-lettertypen niet automatisch vindt”

Er zijn twee verschillende renderpaden, en het lettertypeverhaal verschilt tussen beide.

  • Native in-process engine (de standaard, Document / writeHtml): de engine roept het lettertypesysteem of fontconfig van het besturingssysteem niet aan voor ontdekking. Het lost een face op via het lettertyperegister, dat een specifiek lettertypebestand leest dat je hebt geregistreerd of er een vindt binnen een map die je als zoekpad hebt geconfigureerd. Een lettertype installeren met apt-get install fonts-noto of fc-cache draaien doet op zichzelf niets — de native engine ziet die bestanden alleen als je ze registreert of hun map toevoegt aan het zoekpad van het register.
  • Chrome-brug (de HTML-naar-PDF-renderer die een headless browser aanstuurt): dit pad gebruikt de geïnstalleerde lettertypen van de host wel, via de normale lettertypeontdekking van de browser, dus apt/apk-lettertypepakketten en fontconfig zijn daar van belang.

Als je algemene “installeer deze systeemlettertypepakketten in je Dockerfile”- richtlijnen leest, gelden die voor de Chrome-brug, niet voor de native engine die op deze pagina wordt behandeld. Voor native generatie lever je de bestanden mee en registreer je ze.

Stap 1 — De werkelijke lettertypebestanden meeleveren

Sectie met titel “Stap 1 — De werkelijke lettertypebestanden meeleveren”

Zet de lettertypebestanden binnen je applicatieboom zodat ze geversioneerd zijn en met elke build meegaan. Een conventionele locatie is een resources/fonts/-map.

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

Benoem de bestanden zo dat de mapzoekactie van de engine ze kan vinden op familie en stijl. Wanneer je een map registreert (in plaats van een specifiek bestand) en later setFont('DejaVuSans', 'B', 12) aanroept, zoekt de engine in elke geconfigureerde map naar bestanden zoals DejaVuSans-B.ttf, DejaVuSansB.ttf of DejaVuSans.ttf. De mapzoekactie bouwt die kandidaatnamen op uit dezelfde enkele-letter-stijlcode die je aan setFont doorgeeft (B voor bold, I voor italic, BI voor bold-italic), niet een uitgeschreven woord — dus de betrouwbare vorm is Family-<StyleCode>.ttf (bijvoorbeeld DejaVuSans-B.ttf of DejaVuSans-BI.ttf), niet Family-Bold.ttf. Een bestand met de naam DejaVuSans-Bold.ttf wordt nooit gevonden door de mapzoekactie; om zo’n bestand te gebruiken, registreer je het expliciet met register() — wat het lettertype parset en indexeert onder de familie en stijl die uit de eigen name-tabellen van het bestand worden gelezen, zodat de uitgeschreven bestandsnaam er niet meer toe doet (zie Stap 2).

Stap 2 — De lettertypen registreren bij de engine

Sectie met titel “Stap 2 — De lettertypen registreren bij de engine”

Je hebt twee gelijkwaardige manieren om de bestanden zichtbaar te maken. Beide gaan via NextPDF\Typography\FontRegistry, dat NextPDF\Contracts\FontRegistryInterface implementeert.

Registreer een specifiek bestand onder een alias wanneer je de exacte face beheert:

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

register(string $fontFile, string $alias = '', int $fontIndex = 0) accepteert .ttf-, .otf- en .ttc-bestanden, plus legacy Type1 .pfb (dat zijn bijhorende .afm-metrieken vanaf hetzelfde pad laadt); $fontIndex selecteert een sub-font binnen een TrueType Collection (.ttc). register() parset het bestand en indexeert de face op de familie en stijl die uit de eigen name-tabellen worden gelezen, dus de fysieke bestandsnaam is irrelevant zodra het is geregistreerd. De optionele $alias is slechts een extra opzoeknaam voor de face — het is geen stijlcode en verandert niet welke stijl het bestand levert; geef het door wanneer je setFont() wilt aanroepen met een andere naam dan de ingesloten familienaam van het lettertype. Het retourneert de geparste FontInfo.

Registreer een map wanneer je wilt dat de engine faces op naam oplost vanuit een map die je beheert:

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

De FontRegistry-constructor neemt die map als zijn eerste argument, en addFontDirectory() voegt meer zoekpaden toe. Een kaal Document stelt addFontDirectory() ook beschikbaar voor het standalone-geval.

Om een register te gebruiken dat je zelf hebt gevuld, bouw je documenten via DocumentFactory, dat precies dat register bedraadt in elk document dat het maakt:

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() bouwt zijn eigen interne register, dus een face die je op een apart FontRegistry hebt geregistreerd, is daarvoor onzichtbaar. Ga in productie via DocumentFactory (of de factory van je framework) zodat het gevulde register het register is dat in gebruik is.

Elke framework-integratie stelt dezelfde twee concepten beschikbaar als configuratie, zodat je het register zelden direct aanraakt. In de nextpdf.php van het Laravel-pakket is fonts_path (standaard NEXTPDF_FONTS_PATH, terugvallend op resource_path('fonts')) de zoekmap, en preload_fonts is een lijst met absolute lettertypebestandspaden die tijdens het opstarten van de worker worden geparst. Wijs fonts_path naar de map die je hebt meegeleverd en je geregistreerde faces worden automatisch opgelost.

Stap 3 — Lettertypen voorzien in een Docker-image

Sectie met titel “Stap 3 — Lettertypen voorzien in een Docker-image”

In een container moeten de lettertypebestanden deel uitmaken van de image-laag, tijdens de build erin gekopieerd. Omdat de applicatiecode en de lettertypen samen worden verscheept wanneer je ze onder resources/fonts/ meelevert, draagt een normale COPY . . ze al mee. Als je lettertypen buiten de buildcontext bewaart, kopieer ze dan expliciet en zorg dat het pad dat je registreert overeenkomt met het pad binnen de image.

# 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"]

Op een immutable of read-only filesystem (een readOnlyRootFilesystem- container, een serverless image of een gehardende host) worden de lettertypebestanden gelezen tijdens de generatie en nooit geschreven, dus een read-only mount is prima. De enige schrijfactie die de engine mogelijk wil, is zijn cache met geparste lettertypen: geef die map ofwel een klein schrijfbaar volume, of warm en lock het register tijdens het opstarten (volgende sectie) zodat er tijdens runtime geen schrijfactie of registratie wordt geprobeerd.

Parse in een langlevende worker elke face eenmaal tijdens het opstarten, en lock dan het register zodat er geen per-request-registratie plaatsvindt en een misconfiguratie luid faalt in plaats van stilzwijgend terug te vallen:

$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();

Na lock() werpen register(), addFontDirectory() en warmup() op, wat een “verkeerd pad in de image”-fout omzet in een harde opstartfout in plaats van een tofu-pagina in productie.

Voeg een deployment-smoke-check toe die één pagina rendert met elke vereiste face. De header-check hieronder verifieert alleen dat het document uitvoer produceerde — het bewijst niet dat het lettertype parste, insloot of zelfs werd opgelost. Een face die de engine niet kan vinden, kan terugvallen op een standaard base-font (en, onder het huidige niet-strikte gedrag, kan een conformiteitsprofiel in plaats daarvan een meegeleverde vervanger leveren) terwijl het toch een geldige, niet-lege PDF uitzendt — dus zelfs waar die fallback plaatsvindt, vangt deze check alleen de stille degradatie niet. Vertrouw er niet op dat de fallback op elk pad gegarandeerd of stil is; verifieer het ingesloten programma direct, zoals hieronder getoond:

$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.');
}

Om de deploy daadwerkelijk te laten falen wanneer een face ontbreekt, controleer je de uitgezonden PDF op het ingesloten lettertypeprogramma. Een geregistreerde face die oplost, draagt zijn eigen lettertype-dictionary met een ingesloten programma, dus het asserteren van zijn aanwezigheid vangt het geval waarin de aangevraagde face nooit oploste (waar de engine ook op terugviel) dat de header-check mist. Welke key het programma bevat, hangt af van het outline-formaat: TrueType-outlines (.ttf, .ttc) gebruiken /FontFile2, CFF/OpenType-outlines (.otf met PostScript-outlines) gebruiken /FontFile3, en legacy Type1 (.pfb) gebruikt /FontFile.

Als je alleen een formaat-agnostisch “een of ander lettertypeprogramma ingesloten”-signaal nodig hebt, test dan op /FontFile alleen — omdat /FontFile een substring is van zowel /FontFile2 als /FontFile3, matcht een kale substringcheck al elk outline-type, en het toevoegen van /FontFile2//FontFile3 als extra ||-takken is overbodig:

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

Een kale /FontFile-substring kan de outline-types echter niet uit elkaar houden. Om ze te onderscheiden, match je op het exacte token met een woordgrens zodat /FontFile niet ook afgaat op /FontFile2 of /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.');
}

Behandel dit hoe dan ook als een grove heuristiek alleen, geen betrouwbare deploy-gate. Een ruwe byte-zoekactie over de geserialiseerde PDF is om verschillende redenen onnauwkeurig: lettertypeprogramma’s kunnen binnen gecomprimeerde object-streams leven (waar /FontFile* nooit als platte bytes verschijnt), incrementele updates kunnen objecten toevoegen of vervangen, niet-ingesloten of standard-14-lettertypen dragen legitiem helemaal geen lettertypeprogramma, en serialisatieverschillen (objectvolgorde, witruimte, naamcodering) kunnen het token verplaatsen of verbergen. In het beste geval bevestigt het dat een of andere face een programma insloot — nooit dat de specifieke face die je wilde, oploste.

Vertrouw voor een echte deploy-gate niet op de byte-zoekactie. Parse de uitgezonden PDF met een echte PDF-parser of object-inspector en asserteer dat het lettertype-object voor je doel-face een ingesloten /FontFile//FontFile2//FontFile3-programma draagt, of gebruik een door het product geleverde assertie voor lettertype-oplossing als er een beschikbaar is voor je integratie. De token-bewuste regexes hierboven zijn nuttig voor een snelle lokale sanity-check, maar een structurele inspectie is wat de deploy zou moeten laten falen. De insluitings- en lettertype-dictionarystructuur worden beschreven in Een TrueType-lettertype insluiten en subsetten.

  • createStandalone() heeft zijn eigen register. Een face geregistreerd op een apart FontRegistry is niet zichtbaar voor een standalone document. Gebruik DocumentFactory (of de framework-factory) zodat jouw register het actieve is.
  • Stijlbestanden moeten als bestanden bestaan. De engine synthetiseert geen bold of italic uit een gewone face. Als je setFont('DejaVuSans', 'B') aanroept, zoekt de mapzoekactie naar DejaVuSans-B.ttf, DejaVuSansB.ttf of DejaVuSans.ttf (ook kleine letters en .otf-varianten) — het vormt de kandidaat uit de letterlijke B-stijlcode, dus het zoekt nooit naar DejaVuSans-Bold.ttf. Een bestand met een uitgeschreven naam zoals DejaVuSans-Bold.ttf lost alleen op wanneer je het expliciet registreert met register(), dat het indexeert op de familie en stijl die uit de eigen name-tabellen van het bestand worden gelezen, ongeacht de bestandsnaam; vertrouwen op de mapzoekactie om het te vinden, levert een miss op, waarna de engine kan terugvallen op een base-font (geen gegarandeerd of altijd-stil pad) — de degradatie waarvoor deze pagina waarschuwt.
  • Stream-wrapper- en remote-paden worden geweigerd. Het register weigert paden die een URI-scheme of een null-byte bevatten. Registreer alleen lokale bestanden; gebruik voor lettertypen die tijdens runtime worden opgehaald registerFromBinary() met de ruwe bytes.
  • Een gelockt register is immutable. Zodra je lock() aanroept, werpt elke latere register(), addFontDirectory() of warmup() op. Opzoekmethoden blijven beschikbaar. Registreer en warm alles op vóór het locken.
  • CJK-collecties zijn groot. Registreer de juiste sub-font van een .ttc met $fontIndex, en houd budget voor een grotere ingesloten subset. Zie de CJK- notities in het embed-and-subset-recipe.
  • Een lettertypebestand is niet-vertrouwde binaire invoer. Lever alleen lettertypen mee uit bronnen die je vertrouwt, en valideer de herkomst van elke face die je van eindgebruikers accepteert.
  • Het register locken na het opwarmen verwijdert een runtime-mutatie-oppervlak en maakt dat een padfout faalt tijdens het opstarten in plaats van de uitvoer stilzwijgend te degraderen.
  • Interpoleer geen gebruikersinvoer in een geregistreerd bestandspad. Registreer een vaste set meegeleverde faces; laat een request geen willekeurig filesystem-pad kiezen.

Deze gids doet geen normatieve standaardenclaim. Elk getoond symbool is geverifieerd publiek oppervlak: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), het map-constructorargument), zijn NextPDF\Contracts\FontRegistryInterface-contract, NextPDF\Core\DocumentFactory::create() en NextPDF\Core\Document::setFont() / addFontDirectory(). De Laravel-keys fonts_path en preload_fonts zijn de gedocumenteerde configuratie van het nextpdf/laravel-pakket. Het insluitings- en subset-tag-gedrag, met zijn ISO 32000-2-citaties, is gedocumenteerd in het embed-and-subset-recipe dat onder Zie ook is gelinkt.