Lewati ke konten
getnextpdf.com

Menyediakan font di produksi

PDF Anda dirender dengan benar di laptop Anda, lalu dikirim ke sebuah kontainer dan keluar sebagai deretan kotak kosong — glyph “tofu” — atau dengan aksen dan karakter non-Latin yang hilang. Penyebabnya hampir selalu sama: font yang Anda pilih tidak ada di image yang ter-deploy.

Native engine NextPDF yang berjalan in-process me-resolve font dari berkas font yang dapat dibaca oleh font registry. Ia tidak menemukan font OS atau fontconfig secara otomatis — berkas font yang terpasang di OS hanya membantu jika Anda secara eksplisit mendaftarkan berkas tersebut atau menambahkan direktori yang memuatnya ke search path FontRegistry. Sebuah kontainer yang dibangun dari base image ramping tidak memiliki font yang terpasang via apt/apk, dan bahkan ketika ada, native engine mengabaikannya kecuali Anda mengarahkan registry ke berkasnya. Perbaikannya adalah membundel berkas font yang sebenarnya di dalam aplikasi atau image Anda dan mendaftarkannya ke engine. Registry membaca berkas TrueType (.ttf), OpenType (.otf), dan TrueType Collection (.ttc); Type1 (.pfb) legasi juga diterima tetapi jarang diperlukan untuk pekerjaan baru.

Sebelum Anda mulai, pastikan bagian-bagian ini telah tersedia:

  • NextPDF core terpasang.
  • Anda memiliki berkas font yang sebenarnya yang ingin Anda gunakan, dan Anda berlisensi untuk menyematkannya. Hak penyematan adalah tanggung jawab Anda — lihat Menyematkan dan men-subset font TrueType.
  • Build Anda dapat menyalin berkas-berkas tersebut ke dalam artefak yang ter-deploy.

Ini adalah how-to operasi. Kodenya minimal; pekerjaannya ada pada build dan tata letak filesystem. Untuk mekanik tingkat API dalam mendaftarkan dan men-subset satu face, baca resep embed-and-subset yang ditautkan di atas. Halaman ini membahas cara memasukkan berkas ke dalam server dan mengarahkan engine ke sana.

Mengapa native engine tidak menemukan font OS secara otomatis

Bagian berjudul “Mengapa native engine tidak menemukan font OS secara otomatis”

Ada dua jalur rendering yang berbeda, dan kisah font berbeda di antara keduanya.

  • Native engine in-process (default, Document / writeHtml): engine tidak memanggil sistem font sistem operasi atau fontconfig untuk penemuan. Ia me-resolve sebuah face melalui font registry, yang membaca sebuah berkas font spesifik yang Anda daftarkan atau menemukan satu di dalam direktori yang Anda konfigurasi sebagai search path. Memasang font dengan apt-get install fonts-noto atau menjalankan fc-cache tidak melakukan apa pun dengan sendirinya — native engine hanya melihat berkas-berkas tersebut jika Anda mendaftarkannya atau menambahkan direktorinya ke search path registry.
  • Chrome bridge (perender HTML-ke-PDF yang menggerakkan headless browser): jalur ini memang menggunakan font terpasang dari host melalui penemuan font normal milik browser, sehingga paket font apt/apk dan fontconfig penting di sana.

Jika Anda membaca panduan umum “pasang paket font sistem ini di Dockerfile Anda”, panduan itu berlaku untuk Chrome bridge, bukan untuk native engine yang dibahas di halaman ini. Untuk generasi native, bundel berkasnya dan daftarkan.

Langkah 1 — Bundel berkas font yang sebenarnya

Bagian berjudul “Langkah 1 — Bundel berkas font yang sebenarnya”

Letakkan berkas font di dalam pohon aplikasi Anda sehingga mereka terversioning dan dikirim dengan setiap build. Lokasi konvensional adalah direktori resources/fonts/.

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

Namai berkas sehingga pencarian direktori engine dapat menemukannya berdasarkan family dan style. Ketika Anda mendaftarkan sebuah direktori (alih-alih berkas spesifik) dan nantinya memanggil setFont('DejaVuSans', 'B', 12), engine mencari berkas seperti DejaVuSans-B.ttf, DejaVuSansB.ttf, atau DejaVuSans.ttf di setiap direktori yang dikonfigurasi. Pencarian direktori membangun nama-nama kandidat tersebut dari kode style satu-huruf yang sama yang Anda teruskan ke setFont (B untuk bold, I untuk italic, BI untuk bold-italic), bukan kata yang dieja penuh — sehingga bentuk yang andal adalah Family-<StyleCode>.ttf (misalnya DejaVuSans-B.ttf atau DejaVuSans-BI.ttf), bukan Family-Bold.ttf. Sebuah berkas bernama DejaVuSans-Bold.ttf tidak pernah ditemukan oleh pencarian direktori; untuk menggunakan berkas semacam itu, daftarkan secara eksplisit dengan register() — yang mengurai font dan mengindeksnya di bawah family dan style yang dibaca dari name table berkas itu sendiri, sehingga nama berkas yang dieja penuh tidak lagi penting (lihat Langkah 2).

Anda punya dua cara yang setara untuk membuat berkas terlihat. Keduanya melalui NextPDF\Typography\FontRegistry, yang mengimplementasikan NextPDF\Contracts\FontRegistryInterface.

Daftarkan berkas spesifik di bawah sebuah alias ketika Anda mengendalikan face yang persis:

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

register(string $fontFile, string $alias = '', int $fontIndex = 0) menerima berkas .ttf, .otf, dan .ttc, plus Type1 .pfb legasi (yang memuat metrik .afm pendampingnya dari path yang sama); $fontIndex memilih sub-font di dalam sebuah TrueType Collection (.ttc). register() mengurai berkas dan mengindeks face berdasarkan family dan style yang dibaca dari name table-nya sendiri, sehingga nama berkas fisik tidak relevan setelah terdaftar. $alias opsional hanyalah nama lookup tambahan untuk face — ia bukan kode style dan tidak mengubah style mana yang disediakan berkas; teruskan ketika Anda ingin memanggil setFont() dengan nama selain nama family tersemat font. Ia mengembalikan FontInfo yang telah diurai.

Daftarkan sebuah direktori ketika Anda ingin engine me-resolve face berdasarkan nama dari sebuah folder yang Anda kendalikan:

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

Constructor FontRegistry mengambil direktori tersebut sebagai argumen pertamanya, dan addFontDirectory() menambahkan search path lain. Sebuah Document polos juga mengekspos addFontDirectory() untuk kasus standalone.

Untuk menggunakan registry yang Anda isi sendiri, bangun dokumen melalui DocumentFactory, yang mengaitkan registry persis itu ke setiap dokumen yang dibuatnya:

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() membangun registry internalnya sendiri, sehingga sebuah face yang Anda daftarkan pada FontRegistry terpisah tidak terlihat olehnya. Di produksi, lewati DocumentFactory (atau factory framework Anda) sehingga registry yang terisi adalah yang digunakan.

Setiap integrasi framework mengekspos dua konsep yang sama sebagai konfigurasi, sehingga Anda jarang menyentuh registry secara langsung. Pada nextpdf.php paket Laravel, fonts_path (default NEXTPDF_FONTS_PATH, jatuh kembali ke resource_path('fonts')) adalah direktori pencarian, dan preload_fonts adalah sebuah list path berkas font absolut yang diurai saat boot worker. Arahkan fonts_path ke direktori yang Anda bundel dan face yang Anda daftarkan ter-resolve secara otomatis.

Langkah 3 — Sediakan font dalam image Docker

Bagian berjudul “Langkah 3 — Sediakan font dalam image Docker”

Dalam sebuah kontainer, berkas font harus menjadi bagian dari layer image, disalin saat build time. Karena kode aplikasi dan font dikirim bersama ketika Anda membundelnya di bawah resources/fonts/, sebuah COPY . . biasa sudah membawanya. Jika Anda menyimpan font di luar konteks build, salin secara eksplisit dan pastikan path yang Anda daftarkan cocok dengan path di dalam 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"]

Pada filesystem imutabel atau read-only (kontainer readOnlyRootFilesystem, image serverless, atau host yang dikeraskan), berkas font dibaca saat generation time dan tidak pernah ditulis, sehingga mount read-only tidak masalah. Satu-satunya penulisan yang mungkin diinginkan engine adalah cache font-terurai-nya: berikan direktori itu sebuah volume tulis yang kecil, atau panaskan dan kunci registry saat boot (bagian berikutnya) sehingga tidak ada penulisan atau pendaftaran runtime yang dicoba.

Pada worker yang berjalan lama, urai setiap face sekali saat boot, lalu kunci registry sehingga tidak ada pendaftaran per-permintaan yang terjadi dan sebuah salah-konfigurasi gagal dengan lantang alih-alih diam-diam jatuh kembali:

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

Setelah lock(), register(), addFontDirectory(), dan warmup() melempar, yang mengubah kesalahan “path salah di image” menjadi kegagalan boot yang keras alih-alih halaman tofu di produksi.

Tambahkan pemeriksaan smoke deployment yang merender satu halaman dengan setiap face yang diperlukan. Pemeriksaan header di bawah hanya memverifikasi bahwa dokumen menghasilkan keluaran — ia tidak membuktikan bahwa font terurai, tersemat, atau bahkan ter-resolve. Sebuah face yang tidak dapat ditemukan engine mungkin jatuh kembali ke base font standar (dan, di bawah perilaku non-strict saat ini, sebuah profil konformitas mungkin justru menyediakan pengganti yang dibundel) sambil tetap memancarkan PDF yang valid dan tidak kosong — sehingga bahkan ketika fallback itu terjadi, pemeriksaan ini saja tidak akan menangkap degradasi senyap. Jangan mengandalkan fallback dijamin atau senyap pada setiap jalur; verifikasi program tersemat secara langsung, seperti ditunjukkan di bawah:

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

Untuk benar-benar menggagalkan deploy ketika sebuah face hilang, periksa PDF yang dipancarkan untuk program font tersemat. Sebuah face terdaftar yang ter-resolve membawa font dictionary-nya sendiri dengan program tersemat, sehingga menegaskan keberadaannya menangkap kasus di mana face yang diminta tidak pernah ter-resolve (apa pun yang dijadikan fallback oleh engine) yang dilewatkan oleh pemeriksaan header. Kunci mana yang menampung program bergantung pada format outline: outline TrueType (.ttf, .ttc) menggunakan /FontFile2, outline CFF/OpenType (.otf dengan outline PostScript) menggunakan /FontFile3, dan Type1 (.pfb) legasi menggunakan /FontFile.

Jika yang Anda butuhkan hanyalah sinyal “ada program font tersemat” yang agnostik terhadap format, uji /FontFile saja — karena /FontFile adalah substring dari baik /FontFile2 maupun /FontFile3, sebuah pemeriksaan substring polos sudah mencocokkan setiap tipe outline, dan menambahkan /FontFile2//FontFile3 sebagai cabang || ekstra adalah redundan:

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

Namun, sebuah substring /FontFile polos tidak dapat membedakan tipe outline. Untuk membedakannya, cocokkan token persisnya dengan word boundary sehingga /FontFile tidak juga terpicu pada /FontFile2 atau /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.');
}

Bagaimanapun, perlakukan ini sebagai heuristik kasar saja, bukan gerbang deploy yang andal. Pencarian byte mentah atas PDF yang diserialkan tidak akurat karena beberapa alasan: program font dapat berada di dalam object stream terkompresi (di mana /FontFile* tidak pernah muncul sebagai byte polos), incremental update dapat menambahkan atau menggantikan object, font non-tersemat atau standard-14 secara sah tidak membawa program font sama sekali, dan perbedaan serialisasi (urutan object, whitespace, pengkodean nama) dapat memindahkan atau menyembunyikan token. Paling banter ia mengonfirmasi suatu face menyematkan sebuah program — tidak pernah bahwa face spesifik yang Anda inginkan ter-resolve.

Untuk gerbang deploy yang sebenarnya, jangan andalkan pencarian byte. Urai PDF yang dipancarkan dengan parser PDF atau object inspector yang tepat dan tegaskan bahwa font object untuk face target Anda membawa program /FontFile//FontFile2//FontFile3 tersemat, atau gunakan asersi font-resolution yang disediakan produk jika ada yang tersedia untuk integrasi Anda. Regex yang sadar-token di atas berguna untuk pemeriksaan kewarasan lokal yang cepat, tetapi inspeksi struktural-lah yang seharusnya menggagalkan deploy. Struktur penyematan dan font dictionary dijelaskan dalam Menyematkan dan men-subset font TrueType.

  • createStandalone() memiliki registry-nya sendiri. Sebuah face yang terdaftar pada FontRegistry terpisah tidak terlihat oleh dokumen standalone. Gunakan DocumentFactory (atau factory framework) sehingga registry Anda yang menjadi yang aktif.
  • Berkas style harus ada sebagai berkas. Engine tidak mensintesis bold atau italic dari sebuah face reguler. Jika Anda memanggil setFont('DejaVuSans', 'B'), pencarian direktori mencari DejaVuSans-B.ttf, DejaVuSansB.ttf, atau DejaVuSans.ttf (varian huruf kecil dan .otf juga) — ia membentuk kandidat dari kode style literal B, jadi ia tidak pernah mencari DejaVuSans-Bold.ttf. Sebuah berkas dengan nama yang dieja penuh seperti DejaVuSans-Bold.ttf hanya ter-resolve ketika Anda mendaftarkannya secara eksplisit dengan register(), yang mengindeksnya berdasarkan family dan style yang dibaca dari name table berkas itu sendiri terlepas dari nama berkas; mengandalkan pencarian direktori untuk menemukannya menghasilkan miss, setelah itu engine mungkin jatuh kembali ke base font (bukan jalur yang dijamin atau selalu senyap) — degradasi yang diperingatkan halaman ini.
  • Path stream-wrapper dan remote ditolak. Registry menolak path yang memuat skema URI atau null byte. Daftarkan hanya berkas lokal; untuk font yang diambil saat runtime gunakan registerFromBinary() dengan byte mentah.
  • Registry terkunci bersifat imutabel. Setelah Anda memanggil lock(), setiap register(), addFontDirectory(), atau warmup() berikutnya melempar. Metode lookup tetap tersedia. Daftarkan dan panaskan semuanya sebelum mengunci.
  • Collection CJK berukuran besar. Daftarkan sub-font yang tepat dari sebuah .ttc dengan $fontIndex, dan anggarkan untuk subset tersemat yang lebih besar. Lihat catatan CJK pada resep embed-and-subset.
  • Sebuah berkas font adalah input biner yang tidak tepercaya. Hanya bundel font dari sumber yang Anda percayai, dan validasi provenans setiap face yang diterima dari pengguna akhir.
  • Mengunci registry setelah warmup menghilangkan permukaan mutasi runtime dan membuat kesalahan path gagal saat boot alih-alih diam-diam mendegradasi keluaran.
  • Jangan menyisipkan input pengguna ke dalam path berkas yang terdaftar. Daftarkan sekumpulan face terbundel yang tetap; jangan biarkan sebuah permintaan memilih path filesystem yang arbitrer.

Panduan ini tidak membuat klaim standar normatif. Setiap simbol yang ditunjukkan adalah permukaan publik yang terverifikasi: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), argumen constructor direktori), kontrak NextPDF\Contracts\FontRegistryInterface-nya, NextPDF\Core\DocumentFactory::create(), dan NextPDF\Core\Document::setFont() / addFontDirectory(). Kunci fonts_path dan preload_fonts Laravel adalah konfigurasi terdokumentasi dari paket nextpdf/laravel. Perilaku penyematan dan subset-tag, dengan kutipan ISO 32000-2-nya, didokumentasikan pada resep embed-and-subset yang ditautkan di bawah Lihat juga.