Przejdź do głównej zawartości
getnextpdf.com

Udostępnianie czcionek w środowisku produkcyjnym

Twój plik PDF renderuje się poprawnie na laptopie, potem trafia do kontenera i wychodzi jako rząd pustych prostokątów — glif „tofu” — albo z brakującymi akcentami i znakami spoza alfabetu łacińskiego. Przyczyna jest niemal zawsze ta sama: czcionka, którą wybrałeś, nie jest obecna we wdrożonym obrazie.

Natywny silnik NextPDF działający w ramach procesu rozwiązuje czcionki na podstawie plików czcionek, które rejestr czcionek potrafi odczytać. Nie wykrywa automatycznie czcionek systemu operacyjnego ani fontconfig — pliki czcionek zainstalowane w systemie operacyjnym pomagają tylko wtedy, gdy jawnie zarejestrujesz te pliki albo dodasz zawierający je katalog do ścieżki wyszukiwania FontRegistry. Kontener zbudowany z odchudzonego obrazu bazowego nie ma czcionek zainstalowanych przez apt/apk, a nawet gdy je ma, natywny silnik je ignoruje, dopóki nie wskażesz rejestrowi ich plików. Rozwiązaniem jest dołączenie rzeczywistych plików czcionek do aplikacji lub obrazu i zarejestrowanie ich w silniku. Rejestr odczytuje pliki TrueType (.ttf), OpenType (.otf) oraz TrueType Collection (.ttc); przestarzały Type1 (.pfb) również jest akceptowany, ale rzadko potrzebny w nowych projektach.

Zanim zaczniesz, potwierdź, że te elementy są na miejscu:

  • NextPDF core jest zainstalowany.
  • Masz rzeczywiste pliki czcionek, których zamierzasz użyć, i masz prawo je osadzić. Prawa do osadzania są Twoją odpowiedzialnością — zobacz Osadzanie czcionki TrueType i tworzenie jej podzbioru.
  • Twoja kompilacja może skopiować te pliki do wdrażanego artefaktu.

To poradnik operacyjny. Kod jest minimalny; praca tkwi w kompilacji i układzie systemu plików. Mechanikę rejestrowania i tworzenia podzbioru pojedynczego kroju na poziomie API znajdziesz w przepisie o osadzaniu i podzbiorze, do którego odnosi się powyższy odnośnik. Ta strona obejmuje umieszczenie plików na maszynie i wskazanie ich silnikowi.

Dlaczego natywny silnik nie znajduje automatycznie czcionek systemu operacyjnego

Dział zatytułowany „Dlaczego natywny silnik nie znajduje automatycznie czcionek systemu operacyjnego”

Istnieją dwie odrębne ścieżki renderowania, a kwestia czcionek różni się między nimi.

  • Natywny silnik w ramach procesu (domyślny, Document / writeHtml): silnik nie wywołuje systemu czcionek systemu operacyjnego ani fontconfig w celu wykrywania. Rozwiązuje krój przez rejestr czcionek, który odczytuje konkretny plik czcionki, który zarejestrowałeś, albo odnajduje go wewnątrz katalogu skonfigurowanego jako ścieżka wyszukiwania. Zainstalowanie czcionki za pomocą apt-get install fonts-noto lub uruchomienie fc-cache samo w sobie nic nie daje — natywny silnik widzi te pliki tylko wtedy, gdy je zarejestrujesz albo dodasz ich katalog do ścieżki wyszukiwania rejestru.
  • Mostek Chrome (renderer HTML-do-PDF, który napędza przeglądarkę headless): ta ścieżka korzysta z czcionek zainstalowanych na hoście za pośrednictwem normalnego wykrywania czcionek przeglądarki, więc pakiety czcionek apt/apk i fontconfig mają tam znaczenie.

Jeśli czytasz ogólne wytyczne „zainstaluj te pakiety czcionek systemowych w swoim Dockerfile”, dotyczą one mostka Chrome, a nie natywnego silnika opisanego na tej stronie. Dla generowania natywnego dołącz pliki i zarejestruj je.

Umieść pliki czcionek wewnątrz drzewa aplikacji, aby były wersjonowane i wysyłane z każdą kompilacją. Konwencjonalną lokalizacją jest katalog resources/fonts/.

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

Nazwij pliki tak, aby wyszukiwanie katalogowe silnika mogło odnaleźć je po rodzinie i stylu. Gdy zarejestrujesz katalog (zamiast konkretnego pliku) i później wywołasz setFont('DejaVuSans', 'B', 12), silnik szuka plików takich jak DejaVuSans-B.ttf, DejaVuSansB.ttf lub DejaVuSans.ttf w każdym skonfigurowanym katalogu. Wyszukiwanie katalogowe buduje te kandydujące nazwy z tego samego jednoliterowego kodu stylu, który przekazujesz do setFont (B dla pogrubienia, I dla kursywy, BI dla pogrubionej kursywy), a nie z rozpisanego słowa — więc niezawodną postacią jest Family-<StyleCode>.ttf (na przykład DejaVuSans-B.ttf lub DejaVuSans-BI.ttf), a nie Family-Bold.ttf. Plik o nazwie DejaVuSans-Bold.ttf nigdy nie zostanie odnaleziony przez wyszukiwanie katalogowe; aby użyć takiego pliku, zarejestruj go jawnie za pomocą register() — co parsuje czcionkę i indeksuje ją pod rodziną i stylem odczytanymi z własnych tablic nazw pliku, dzięki czemu rozpisana nazwa pliku przestaje mieć znaczenie (zobacz Krok 2).

Masz dwa równoważne sposoby uwidocznienia plików. Oba przebiegają przez NextPDF\Typography\FontRegistry, który implementuje NextPDF\Contracts\FontRegistryInterface.

Zarejestruj konkretny plik pod aliasem, gdy kontrolujesz dokładny krój:

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

register(string $fontFile, string $alias = '', int $fontIndex = 0) akceptuje pliki .ttf, .otf oraz .ttc, a także przestarzały Type1 .pfb (który ładuje towarzyszące metryki .afm z tej samej ścieżki); $fontIndex wybiera podczcionkę wewnątrz TrueType Collection (.ttc). register() parsuje plik i indeksuje krój po rodzinie i stylu odczytanych z własnych tablic nazw, więc fizyczna nazwa pliku jest nieistotna po zarejestrowaniu. Opcjonalny $alias to po prostu dodatkowa nazwa wyszukiwania kroju — nie jest kodem stylu i nie zmienia tego, który styl dostarcza plik; przekaż go, gdy chcesz wywołać setFont() z nazwą inną niż osadzona nazwa rodziny czcionki. Zwraca sparsowany FontInfo.

Zarejestruj katalog, gdy chcesz, aby silnik rozwiązywał kroje po nazwie z folderu, który kontrolujesz:

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

Konstruktor FontRegistry przyjmuje ten katalog jako swój pierwszy argument, a addFontDirectory() dodaje kolejne ścieżki wyszukiwania. Goły Document także udostępnia addFontDirectory() dla przypadku samodzielnego.

Aby użyć rejestru, który samodzielnie wypełniłeś, buduj dokumenty przez DocumentFactory, który wpina ten dokładnie rejestr do każdego tworzonego przez siebie dokumentu:

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() buduje własny wewnętrzny rejestr, więc krój, który zarejestrowałeś w osobnym FontRegistry, jest dla niego niewidoczny. W środowisku produkcyjnym korzystaj z DocumentFactory (lub fabryki Twojego frameworka), aby w użyciu był wypełniony rejestr.

Każda integracja frameworka udostępnia te same dwie koncepcje jako konfigurację, więc rzadko sięgasz bezpośrednio do rejestru. W pliku nextpdf.php pakietu Laravel fonts_path (domyślnie NEXTPDF_FONTS_PATH, z odwołaniem awaryjnym do resource_path('fonts')) jest katalogiem wyszukiwania, a preload_fonts to lista bezwzględnych ścieżek plików czcionek parsowanych przy starcie procesu roboczego. Skieruj fonts_path na katalog, który dołączyłeś, a Twoje zarejestrowane kroje rozwiążą się automatycznie.

W kontenerze pliki czcionek muszą być częścią warstwy obrazu, skopiowaną w czasie kompilacji. Ponieważ kod aplikacji i czcionki wysyłane są razem, gdy dołączysz je pod resources/fonts/, normalne COPY . . już je przenosi. Jeśli trzymasz czcionki poza kontekstem kompilacji, skopiuj je jawnie i upewnij się, że ścieżka, którą rejestrujesz, odpowiada ścieżce wewnątrz obrazu.

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

W niezmiennym systemie plików lub systemie plików tylko do odczytu (kontener readOnlyRootFilesystem, obraz bezserwerowy lub utwardzony host) pliki czcionek są odczytywane w czasie generowania i nigdy zapisywane, więc montowanie tylko do odczytu jest w porządku. Jedynym zapisem, którego silnik może chcieć, jest jego pamięć podręczna sparsowanych czcionek: albo daj temu katalogowi mały zapisywalny wolumen, albo rozgrzej i zablokuj rejestr przy starcie (następna sekcja), aby nie podjęto próby żadnego zapisu ani rejestracji w czasie działania.

W długo działającym procesie roboczym sparsuj każdy krój raz przy starcie, a potem zablokuj rejestr, aby nie zachodziła rejestracja per żądanie, a błędna konfiguracja zawodziła głośno, zamiast po cichu wracać do wartości awaryjnej:

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

Po lock() register(), addFontDirectory() oraz warmup() zgłaszają wyjątek, co zamienia pomyłkę „błędna ścieżka w obrazie” w twardy błąd startu, zamiast w stronę z tofu na produkcji.

Dodaj wdrożeniową kontrolę dymną, która renderuje jedną stronę każdym wymaganym krojem. Poniższa kontrola nagłówka weryfikuje jedynie, że dokument wytworzył wyjście — nie dowodzi, że czcionka została sparsowana, osadzona ani nawet rozwiązana. Krój, którego silnik nie potrafi odnaleźć, może wrócić do standardowej czcionki bazowej (a przy obecnym, nieścisłym zachowaniu profil zgodności może zamiast tego dostarczyć dołączony zamiennik), wciąż emitując prawidłowy, niepusty plik PDF — więc nawet tam, gdzie ta wartość awaryjna ma miejsce, sama ta kontrola nie wychwyci cichej degradacji. Nie polegaj na tym, że wartość awaryjna jest gwarantowana lub cicha na każdej ścieżce; zweryfikuj osadzony program bezpośrednio, jak pokazano poniżej:

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

Aby faktycznie obalić wdrożenie, gdy brakuje kroju, sprawdź wyemitowany plik PDF pod kątem osadzonego programu czcionki. Zarejestrowany krój, który się rozwiązuje, niesie własny słownik czcionki z osadzonym programem, więc potwierdzenie jego obecności wychwytuje przypadek, w którym żądany krój nigdy się nie rozwiązał (cokolwiek silnik przyjął jako wartość awaryjną), a którego kontrola nagłówka nie wyłapuje. To, który klucz przechowuje program, zależy od formatu konturów: kontury TrueType (.ttf, .ttc) używają /FontFile2, kontury CFF/OpenType (.otf z konturami PostScript) używają /FontFile3, a przestarzały Type1 (.pfb) używa /FontFile.

Jeśli wszystko, czego potrzebujesz, to sygnał niezależny od formatu „jakiś program czcionki osadzony”, testuj pod kątem /FontFile samego w sobie — ponieważ /FontFile jest podłańcuchem zarówno /FontFile2, jak i /FontFile3, goła kontrola podłańcucha pasuje już do każdego typu konturów, a dodanie /FontFile2//FontFile3 jako dodatkowych gałęzi || jest zbędne:

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

Goły podłańcuch /FontFile nie potrafi jednak odróżnić typów konturów. Aby je rozróżnić, dopasuj dokładny token z granicą słowa, tak aby /FontFile nie zadziałał także na /FontFile2 ani /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.');
}

Tak czy inaczej, traktuj to wyłącznie jako zgrubną heurystykę, a nie niezawodną bramkę wdrożeniową. Surowe wyszukiwanie bajtów w zserializowanym pliku PDF jest niedokładne z kilku powodów: programy czcionek mogą żyć wewnątrz skompresowanych strumieni obiektów (gdzie /FontFile* nigdy nie pojawia się jako zwykłe bajty), aktualizacje przyrostowe mogą dołączać lub zastępować obiekty, nieosadzone lub standardowe czcionki z grupy standard-14 legalnie nie niosą żadnego programu czcionki, a różnice serializacji (kolejność obiektów, białe znaki, kodowanie nazw) mogą przesunąć lub ukryć token. W najlepszym razie potwierdza, że jakiś krój osadził program — nigdy że rozwiązał się konkretny krój, którego chciałeś.

Dla prawdziwej bramki wdrożeniowej nie polegaj na wyszukiwaniu bajtów. Sparsuj wyemitowany plik PDF właściwym parserem PDF lub inspektorem obiektów i potwierdź, że obiekt czcionki dla docelowego kroju niesie osadzony program /FontFile//FontFile2//FontFile3, albo użyj dostarczonego przez produkt potwierdzenia rozwiązania czcionki, jeśli takie jest dostępne dla Twojej integracji. Powyższe wyrażenia regularne świadome tokenów są przydatne do szybkiej lokalnej kontroli poprawności, ale to inspekcja strukturalna powinna obalić wdrożenie. Struktura osadzania i słownika czcionek jest opisana w Osadzanie czcionki TrueType i tworzenie jej podzbioru.

  • createStandalone() ma własny rejestr. Krój zarejestrowany w osobnym FontRegistry nie jest widoczny dla dokumentu samodzielnego. Użyj DocumentFactory (lub fabryki frameworka), aby aktywnym był Twój rejestr.
  • Pliki stylów muszą istnieć jako pliki. Silnik nie syntezuje pogrubienia ani kursywy z kroju zwykłego. Jeśli wywołasz setFont('DejaVuSans', 'B'), wyszukiwanie katalogowe szuka DejaVuSans-B.ttf, DejaVuSansB.ttf lub DejaVuSans.ttf (także warianty z małymi literami i .otf) — tworzy kandydata z dosłownego kodu stylu B, więc nigdy nie szuka DejaVuSans-Bold.ttf. Plik z rozpisaną nazwą taką jak DejaVuSans-Bold.ttf rozwiązuje się tylko wtedy, gdy zarejestrujesz go jawnie za pomocą register(), który indeksuje go po rodzinie i stylu odczytanych z własnych tablic nazw pliku niezależnie od nazwy pliku; poleganie na wyszukiwaniu katalogowym w celu jego odnalezienia daje pudło, po którym silnik może wrócić do czcionki bazowej (nie jest to gwarantowana ani zawsze cicha ścieżka) — degradacja, przed którą ostrzega ta strona.
  • Ścieżki ze strumieniowymi opakowaniami i ścieżki zdalne są odrzucane. Rejestr odmawia ścieżek zawierających schemat URI lub bajt null. Rejestruj wyłącznie pliki lokalne; dla czcionek pobieranych w czasie działania użyj registerFromBinary() z surowymi bajtami.
  • Zablokowany rejestr jest niezmienny. Po wywołaniu lock() każde późniejsze register(), addFontDirectory() lub warmup() zgłasza wyjątek. Metody wyszukiwania pozostają dostępne. Zarejestruj i rozgrzej wszystko przed zablokowaniem.
  • Kolekcje CJK są duże. Zarejestruj właściwą podczcionkę pliku .ttc za pomocą $fontIndex i zaplanuj budżet na większy osadzony podzbiór. Zobacz uwagi o CJK w przepisie o osadzaniu i podzbiorze.
  • Plik czcionki to niezaufane binarne wejście. Dołączaj wyłącznie czcionki ze źródeł, którym ufasz, i waliduj pochodzenie każdego kroju przyjętego od użytkowników końcowych.
  • Zablokowanie rejestru po rozgrzewce usuwa powierzchnię mutacji w czasie działania i sprawia, że pomyłka w ścieżce zawodzi przy starcie, zamiast po cichu degradować wyjście.
  • Nie wstawiaj danych wejściowych użytkownika do zarejestrowanej ścieżki pliku. Zarejestruj stały zestaw dołączonych krojów; nie pozwalaj, aby żądanie wybierało dowolną ścieżkę w systemie plików.

Ten przewodnik nie zgłasza normatywnego roszczenia co do standardów. Każdy pokazany symbol to zweryfikowana powierzchnia publiczna: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), argument konstruktora katalogu), jego kontrakt NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() oraz NextPDF\Core\Document::setFont() / addFontDirectory(). Klucze Laravel fonts_path oraz preload_fonts to udokumentowana konfiguracja pakietu nextpdf/laravel. Zachowanie osadzania i znacznika podzbioru, z cytatami ISO 32000-2, jest udokumentowane w przepisie o osadzaniu i podzbiorze, do którego odnosi się sekcja Zobacz także.