Zum Inhalt springen
getnextpdf.com

Schriften in der Produktion bereitstellen

Ihr PDF wird auf Ihrem Laptop korrekt gerendert, geht dann in einen Container und kommt als Reihe leerer Kästchen heraus – die „Tofu“-Glyphe – oder mit fehlenden Akzenten und nicht-lateinischen Zeichen. Die Ursache ist fast immer dieselbe: Die ausgewählte Schrift ist im bereitgestellten Image nicht vorhanden.

Die native, prozessinterne NextPDF-Engine löst Schriften aus Schriftdateien auf, die die Font-Registry lesen kann. Sie entdeckt OS- oder fontconfig-Schriften nicht automatisch – OS-installierte Schriftdateien helfen nur, wenn Sie diese Dateien explizit registrieren oder ihr enthaltendes Verzeichnis zum Suchpfad der FontRegistry hinzufügen. Ein aus einem schlanken Basis-Image gebauter Container hat keine per apt/apk installierten Schriften, und selbst wenn doch, ignoriert die native Engine sie, sofern Sie die Registry nicht auf deren Dateien verweisen. Die Lösung besteht darin, die tatsächlichen Schriftdateien innerhalb Ihrer Anwendung oder Ihres Images zu bündeln und sie bei der Engine zu registrieren. Die Registry liest TrueType- (.ttf), OpenType- (.otf) und TrueType-Collection-Dateien (.ttc); Legacy-Type1 (.pfb) wird ebenfalls akzeptiert, ist für neue Arbeiten aber selten nötig.

Bestätigen Sie vor dem Start, dass diese Teile vorhanden sind:

  • NextPDF Core ist installiert.
  • Sie verfügen über die tatsächlichen Schriftdateien, die Sie verwenden möchten, und Sie sind berechtigt, sie einzubetten. Die Einbettungsrechte liegen in Ihrer Verantwortung – siehe TrueType-Schriftart einbetten und subsetten.
  • Ihr Build kann diese Dateien in das bereitgestellte Artefakt kopieren.

Dies ist eine betriebliche Anleitung. Der Code ist minimal; die Arbeit steckt im Build und im Dateisystem-Layout. Für die API-Mechanik des Registrierens und Subsettens eines einzelnen Schnitts lesen Sie das oben verlinkte Embed-and-subset-Recipe. Diese Seite behandelt, wie Sie die Dateien auf die Box bekommen und die Engine auf sie verweisen.

Warum die native Engine OS-Schriften nicht automatisch findet

Abschnitt betitelt „Warum die native Engine OS-Schriften nicht automatisch findet“

Es gibt zwei verschiedene Rendering-Pfade, und die Schriftgeschichte unterscheidet sich zwischen ihnen.

  • Native prozessinterne Engine (der Standard, Document / writeHtml): Die Engine ruft für die Erkennung nicht das Schriftsystem oder fontconfig des Betriebssystems auf. Sie löst einen Schnitt über die Font-Registry auf, die eine bestimmte registrierte Schriftdatei liest oder eine innerhalb eines als Suchpfad konfigurierten Verzeichnisses findet. Das Installieren einer Schrift mit apt-get install fonts-noto oder das Ausführen von fc-cache bewirkt für sich genommen nichts – die native Engine sieht diese Dateien nur, wenn Sie sie registrieren oder ihr Verzeichnis zum Suchpfad der Registry hinzufügen.
  • Chrome-Bridge (der HTML-zu-PDF-Renderer, der einen Headless-Browser ansteuert): Dieser Pfad nutzt die installierten Schriften des Hosts über die normale Schriftauffindung des Browsers, sodass apt/apk-Schriftpakete und fontconfig dort von Bedeutung sind.

Wenn Sie allgemeine Anleitungen wie „installieren Sie diese System-Schriftpakete in Ihrem Dockerfile“ lesen, gelten diese für die Chrome-Bridge, nicht für die auf dieser Seite behandelte native Engine. Für die native Generierung bündeln Sie die Dateien und registrieren sie.

Schritt 1 — Die tatsächlichen Schriftdateien bündeln

Abschnitt betitelt „Schritt 1 — Die tatsächlichen Schriftdateien bündeln“

Legen Sie die Schriftdateien in Ihren Anwendungsbaum, sodass sie versioniert sind und mit jedem Build ausgeliefert werden. Ein üblicher Ort ist ein Verzeichnis resources/fonts/.

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

Benennen Sie die Dateien so, dass die Verzeichnissuche der Engine sie nach Familie und Stil finden kann. Wenn Sie ein Verzeichnis registrieren (statt einer bestimmten Datei) und später setFont('DejaVuSans', 'B', 12) aufrufen, sucht die Engine in jedem konfigurierten Verzeichnis nach Dateien wie DejaVuSans-B.ttf, DejaVuSansB.ttf oder DejaVuSans.ttf. Die Verzeichnissuche bildet diese Kandidatennamen aus demselben einbuchstabigen Stilcode, den Sie an setFont übergeben (B für fett, I für kursiv, BI für fett-kursiv), nicht aus einem ausgeschriebenen Wort – die zuverlässige Form ist also Family-<StyleCode>.ttf (zum Beispiel DejaVuSans-B.ttf oder DejaVuSans-BI.ttf), nicht Family-Bold.ttf. Eine Datei namens DejaVuSans-Bold.ttf wird von der Verzeichnissuche niemals gefunden; um eine solche Datei zu verwenden, registrieren Sie sie explizit mit register() – was die Schrift parst und sie unter der aus den eigenen name-Tabellen der Datei gelesenen Familie und dem dort gelesenen Stil indexiert, sodass der ausgeschriebene Dateiname keine Rolle mehr spielt (siehe Schritt 2).

Schritt 2 — Die Schriften bei der Engine registrieren

Abschnitt betitelt „Schritt 2 — Die Schriften bei der Engine registrieren“

Sie haben zwei gleichwertige Wege, die Dateien sichtbar zu machen. Beide laufen über NextPDF\Typography\FontRegistry, das NextPDF\Contracts\FontRegistryInterface implementiert.

Registrieren Sie eine bestimmte Datei unter einem Alias, wenn Sie den exakten Schnitt kontrollieren:

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

register(string $fontFile, string $alias = '', int $fontIndex = 0) akzeptiert .ttf-, .otf- und .ttc-Dateien sowie Legacy-Type1-.pfb (das seine begleitende .afm-Metrik aus demselben Pfad lädt); $fontIndex wählt eine Unterschrift innerhalb einer TrueType-Collection (.ttc) aus. register() parst die Datei und indexiert den Schnitt nach der aus den eigenen name-Tabellen gelesenen Familie und dem dort gelesenen Stil, sodass der physische Dateiname nach der Registrierung irrelevant ist. Der optionale $alias ist lediglich ein zusätzlicher Nachschlagename für den Schnitt – er ist kein Stilcode und ändert nicht, welchen Stil die Datei bereitstellt; übergeben Sie ihn, wenn Sie setFont() mit einem anderen Namen als dem eingebetteten Familiennamen der Schrift aufrufen möchten. Er gibt das geparste FontInfo zurück.

Registrieren Sie ein Verzeichnis, wenn die Engine Schnitte nach Namen aus einem von Ihnen kontrollierten Ordner auflösen soll:

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

Der FontRegistry-Konstruktor nimmt dieses Verzeichnis als sein erstes Argument, und addFontDirectory() fügt weitere Suchpfade hinzu. Ein nacktes Document stellt für den eigenständigen Fall ebenfalls addFontDirectory() bereit.

Um eine selbst befüllte Registry zu verwenden, bauen Sie Dokumente über DocumentFactory, die genau diese Registry in jedes von ihr erzeugte Dokument verdrahtet:

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() baut seine eigene interne Registry auf, sodass ein auf einer separaten FontRegistry registrierter Schnitt für es unsichtbar ist. Gehen Sie in der Produktion über DocumentFactory (oder die Factory Ihres Frameworks), damit die befüllte Registry diejenige ist, die verwendet wird.

Jede Framework-Integration stellt dieselben beiden Konzepte als Konfiguration bereit, sodass Sie die Registry selten direkt anfassen. In der nextpdf.php des Laravel-Pakets ist fonts_path (Standard NEXTPDF_FONTS_PATH, mit Rückfall auf resource_path('fonts')) das Suchverzeichnis, und preload_fonts ist eine Liste absoluter Schriftdatei-Pfade, die beim Worker-Boot geparst werden. Verweisen Sie fonts_path auf das gebündelte Verzeichnis, und Ihre registrierten Schnitte lösen sich automatisch auf.

Schritt 3 — Schriften in einem Docker-Image bereitstellen

Abschnitt betitelt „Schritt 3 — Schriften in einem Docker-Image bereitstellen“

In einem Container müssen die Schriftdateien Teil der Image-Schicht sein, zur Build-Zeit hineinkopiert. Da der Anwendungscode und die Schriften zusammen ausgeliefert werden, wenn Sie sie unter resources/fonts/ bündeln, trägt ein normales COPY . . sie bereits. Wenn Sie Schriften außerhalb des Build-Kontexts halten, kopieren Sie sie explizit und stellen Sie sicher, dass der von Ihnen registrierte Pfad mit dem Pfad innerhalb des Images übereinstimmt.

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

Auf einem unveränderlichen oder schreibgeschützten Dateisystem (einem readOnlyRootFilesystem-Container, einem Serverless-Image oder einem gehärteten Host) werden die Schriftdateien zur Generierungszeit gelesen und niemals geschrieben, sodass ein schreibgeschützter Mount in Ordnung ist. Der einzige Schreibvorgang, den die Engine eventuell möchte, ist ihr Cache geparster Schriften: Geben Sie diesem Verzeichnis entweder ein kleines beschreibbares Volume oder wärmen und sperren Sie die Registry beim Boot (nächster Abschnitt), sodass kein Schreibvorgang oder keine Registrierung zur Laufzeit versucht wird.

Parsen Sie in einem langlaufenden Worker jeden Schnitt einmal beim Boot und sperren Sie dann die Registry, sodass keine Registrierung pro Anfrage stattfindet und eine Fehlkonfiguration lautstark fehlschlägt, statt stillschweigend zurückzufallen:

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

Nach lock() werfen register(), addFontDirectory() und warmup() eine Exception, was einen Fehler „falscher Pfad im Image“ in einen harten Boot-Fehler verwandelt statt in eine Tofu-Seite in der Produktion.

Fügen Sie eine Deployment-Smoke-Prüfung hinzu, die eine Seite mit jedem benötigten Schnitt rendert. Die Header-Prüfung unten verifiziert nur, dass das Dokument Ausgabe erzeugt hat – sie beweist nicht, dass die Schrift geparst, eingebettet oder auch nur aufgelöst wurde. Ein Schnitt, den die Engine nicht finden kann, kann auf eine Standard-Basisschrift zurückfallen (und unter dem aktuellen, nicht-strikten Verhalten kann ein Konformitätsprofil stattdessen einen gebündelten Ersatz liefern), während dennoch ein gültiges, nicht-leeres PDF ausgegeben wird – selbst wo dieser Fallback geschieht, fängt diese Prüfung allein also die stille Verschlechterung nicht ab. Verlassen Sie sich nicht darauf, dass der Fallback auf jedem Pfad garantiert oder still ist; verifizieren Sie das eingebettete Programm direkt, wie unten gezeigt:

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

Um das Deployment tatsächlich zum Scheitern zu bringen, wenn ein Schnitt fehlt, prüfen Sie das ausgegebene PDF auf das eingebettete Schriftprogramm. Ein registrierter Schnitt, der sich auflöst, trägt sein eigenes Font-Dictionary mit einem eingebetteten Programm, sodass das Sicherstellen seiner Anwesenheit den Fall abfängt, in dem sich der angeforderte Schnitt nie aufgelöst hat (worauf auch immer die Engine zurückgefallen ist), den die Header-Prüfung verpasst. Welcher Schlüssel das Programm hält, hängt vom Outline-Format ab: TrueType-Outlines (.ttf, .ttc) verwenden /FontFile2, CFF/OpenType-Outlines (.otf mit PostScript-Outlines) verwenden /FontFile3, und Legacy-Type1 (.pfb) verwendet /FontFile.

Wenn Sie nur ein formatunabhängiges Signal „irgendein Schriftprogramm eingebettet“ benötigen, testen Sie auf /FontFile allein – weil /FontFile ein Teilstring sowohl von /FontFile2 als auch von /FontFile3 ist, passt eine nackte Teilstring-Prüfung bereits auf jeden Outline-Typ, und das Hinzufügen von /FontFile2//FontFile3 als zusätzliche ||-Zweige ist redundant:

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

Ein nackter /FontFile-Teilstring kann die Outline-Typen allerdings nicht auseinanderhalten. Um sie zu unterscheiden, passen Sie auf das exakte Token mit einer Wortgrenze, sodass /FontFile nicht auch bei /FontFile2 oder /FontFile3 auslöst:

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

So oder so behandeln Sie dies nur als grobe Heuristik, nicht als zuverlässiges Deploy-Gate. Eine rohe Byte-Suche über das serialisierte PDF ist aus mehreren Gründen ungenau: Schriftprogramme können innerhalb von komprimierten Objekt-Streams liegen (wo /FontFile* nie als Klartext-Bytes erscheint), inkrementelle Updates können Objekte anhängen oder ersetzen, nicht eingebettete oder Standard-14-Schriften tragen rechtmäßig überhaupt kein Schriftprogramm, und Serialisierungsunterschiede (Objektreihenfolge, Whitespace, Namenscodierung) können das Token verschieben oder verbergen. Bestenfalls bestätigt sie, dass irgendein Schnitt ein Programm eingebettet hat – niemals, dass sich der spezifische gewünschte Schnitt aufgelöst hat.

Für ein echtes Deploy-Gate verlassen Sie sich nicht auf die Byte-Suche. Parsen Sie das ausgegebene PDF mit einem ordentlichen PDF-Parser oder Objekt-Inspektor und stellen Sie sicher, dass das Font-Objekt für Ihren Zielschnitt ein eingebettetes /FontFile//FontFile2//FontFile3-Programm trägt, oder verwenden Sie eine vom Produkt bereitgestellte Schriftauflösungs-Zusicherung, falls eine für Ihre Integration verfügbar ist. Die tokenbewussten Regexes oben sind für eine schnelle lokale Plausibilitätsprüfung nützlich, aber eine strukturelle Inspektion ist das, was das Deployment zum Scheitern bringen sollte. Die Einbettung und die Font-Dictionary-Struktur sind in TrueType-Schriftart einbetten und subsetten beschrieben.

  • createStandalone() hat seine eigene Registry. Ein auf einer separaten FontRegistry registrierter Schnitt ist für ein eigenständiges Dokument nicht sichtbar. Verwenden Sie DocumentFactory (oder die Framework-Factory), damit Ihre Registry die aktive ist.
  • Stildateien müssen als Dateien existieren. Die Engine synthetisiert weder fett noch kursiv aus einem regulären Schnitt. Wenn Sie setFont('DejaVuSans', 'B') aufrufen, sucht die Verzeichnissuche nach DejaVuSans-B.ttf, DejaVuSansB.ttf oder DejaVuSans.ttf (auch in Kleinschreibung und in .otf-Varianten) – sie bildet den Kandidaten aus dem buchstäblichen Stilcode B, sodass sie niemals nach DejaVuSans-Bold.ttf sucht. Eine Datei mit einem ausgeschriebenen Namen wie DejaVuSans-Bold.ttf löst sich nur auf, wenn Sie sie explizit mit register() registrieren, was sie unabhängig vom Dateinamen nach der aus den eigenen name-Tabellen der Datei gelesenen Familie und dem dort gelesenen Stil indexiert; sich auf die Verzeichnissuche zu verlassen, um sie zu finden, erzeugt einen Fehlschlag, woraufhin die Engine auf eine Basisschrift zurückfallen kann (kein garantierter oder stets stiller Pfad) – die Verschlechterung, vor der diese Seite warnt.
  • Stream-Wrapper- und Remote-Pfade werden abgelehnt. Die Registry verweigert Pfade, die ein URI-Schema oder ein Null-Byte enthalten. Registrieren Sie nur lokale Dateien; für zur Laufzeit abgerufene Schriften verwenden Sie registerFromBinary() mit den Rohbytes.
  • Eine gesperrte Registry ist unveränderlich. Sobald Sie lock() aufrufen, wirft jedes spätere register(), addFontDirectory() oder warmup() eine Exception. Nachschlagemethoden bleiben verfügbar. Registrieren und wärmen Sie alles vor dem Sperren auf.
  • CJK-Collections sind groß. Registrieren Sie die richtige Unterschrift einer .ttc mit $fontIndex und planen Sie für ein größeres eingebettetes Subset. Siehe die CJK-Hinweise im Embed-and-subset-Recipe.
  • Eine Schriftdatei ist nicht vertrauenswürdige Binäreingabe. Bündeln Sie nur Schriften aus Quellen, denen Sie vertrauen, und validieren Sie die Herkunft jedes von Endbenutzern angenommenen Schnitts.
  • Das Sperren der Registry nach dem Aufwärmen entfernt eine Laufzeit-Mutations- Oberfläche und lässt einen Pfadfehler beim Boot fehlschlagen, statt die Ausgabe stillschweigend zu verschlechtern.
  • Interpolieren Sie keine Benutzereingaben in einen registrierten Dateipfad. Registrieren Sie eine feste Menge gebündelter Schnitte; lassen Sie eine Anfrage keinen beliebigen Dateisystempfad wählen.

Diese Anleitung erhebt keinen normativen Standardanspruch. Jedes gezeigte Symbol ist verifizierte öffentliche Oberfläche: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), das Verzeichnis-Konstruktorargument), ihr Vertrag NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() und NextPDF\Core\Document::setFont() / addFontDirectory(). Die Laravel-Schlüssel fonts_path und preload_fonts sind die dokumentierte Konfiguration des Pakets nextpdf/laravel. Das Einbettungs- und Subset-Tag-Verhalten mit seinen ISO-32000-2-Zitaten ist im unter „Siehe auch“ verlinkten Embed-and-subset-Recipe dokumentiert.