Bỏ qua để đến nội dung
getnextpdf.com

Cung cấp phông chữ trong môi trường thực tế

PDF của bạn kết xuất đúng trên máy tính xách tay, rồi được gửi tới một container và ra thành một hàng các ô trống — glyph “tofu” — hoặc với các dấu phụ và ký tự phi-Latin bị thiếu. Nguyên nhân gần như luôn giống nhau: phông chữ bạn chọn không hiện diện trong ảnh đã triển khai.

Engine NextPDF native, trong tiến trình, giải quyết phông chữ từ các tệp phông chữ mà registry phông chữ có thể đọc. Nó không tự động phát hiện phông chữ của hệ điều hành hay fontconfig — các tệp phông chữ do hệ điều hành cài chỉ giúp ích nếu bạn đăng ký tường minh các tệp đó hoặc thêm thư mục chứa chúng vào đường tìm kiếm của FontRegistry. Một container được build từ một ảnh nền tinh giản không có phông chữ do apt/apk cài, và ngay cả khi có, engine native vẫn bỏ qua chúng trừ khi bạn trỏ registry tới các tệp của chúng. Cách khắc phục là đóng gói chính các tệp phông chữ vào bên trong ứng dụng hoặc ảnh của bạn và đăng ký chúng với engine. Registry đọc các tệp TrueType (.ttf), OpenType (.otf), và TrueType Collection (.ttc); Type1 cũ (.pfb) cũng được chấp nhận nhưng hiếm khi cần cho công việc mới.

Trước khi bắt đầu, hãy xác nhận các phần này đã sẵn sàng:

  • NextPDF core đã được cài đặt.
  • Bạn có chính các tệp phông chữ bạn định dùng, và bạn được cấp phép để nhúng chúng. Quyền nhúng là trách nhiệm của bạn — xem Nhúng và subset hóa một phông chữ TrueType.
  • Bản build của bạn có thể sao chép các tệp đó vào hiện vật đã triển khai.

Đây là một hướng dẫn vận hành. Mã là tối thiểu; công việc nằm ở bản build và bố cục hệ thống tệp. Để biết cơ chế ở mức API của việc đăng ký và subset hóa một mặt chữ đơn lẻ, hãy đọc công thức nhúng-và-subset được liên kết ở trên. Trang này bao quát việc đưa các tệp lên hộp và trỏ engine tới chúng.

Vì sao engine native không tự động tìm phông chữ của hệ điều hành

Phần tiêu đề “Vì sao engine native không tự động tìm phông chữ của hệ điều hành”

Có hai đường kết xuất riêng biệt, và câu chuyện phông chữ khác nhau giữa chúng.

  • Engine native trong tiến trình (mặc định, Document / writeHtml): engine không gọi vào hệ thống phông chữ của hệ điều hành hay fontconfig để phát hiện. Nó giải quyết một mặt chữ qua registry phông chữ, vốn đọc một tệp phông chữ cụ thể bạn đã đăng ký hoặc tìm một tệp bên trong một thư mục bạn đã cấu hình làm đường tìm kiếm. Cài một phông chữ bằng apt-get install fonts-noto hoặc chạy fc-cache tự nó không làm gì — engine native chỉ thấy các tệp đó nếu bạn đăng ký chúng hoặc thêm thư mục của chúng vào đường tìm kiếm của registry.
  • Cầu nối Chrome (bộ kết xuất HTML-sang-PDF điều khiển một trình duyệt headless): đường này dùng các phông chữ đã cài của host qua việc phát hiện phông chữ thông thường của trình duyệt, nên các gói phông chữ apt/apkfontconfig mới có ý nghĩa ở đó.

Nếu bạn đọc hướng dẫn chung kiểu “cài các gói phông chữ hệ thống này trong Dockerfile của bạn”, nó áp dụng cho cầu nối Chrome, không cho engine native được bao quát trên trang này. Với việc tạo native, hãy đóng gói các tệp và đăng ký chúng.

Bước 1 — Đóng gói chính các tệp phông chữ

Phần tiêu đề “Bước 1 — Đóng gói chính các tệp phông chữ”

Đặt các tệp phông chữ vào bên trong cây ứng dụng của bạn để chúng được quản lý phiên bản và đi kèm mọi bản build. Một vị trí thông lệ là một thư mục resources/fonts/.

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

Đặt tên các tệp sao cho việc tìm kiếm theo thư mục của engine có thể tìm thấy chúng theo họ và kiểu. Khi bạn đăng ký một thư mục (thay vì một tệp cụ thể) và sau đó gọi setFont('DejaVuSans', 'B', 12), engine tìm các tệp như DejaVuSans-B.ttf, DejaVuSansB.ttf, hoặc DejaVuSans.ttf trong mỗi thư mục đã cấu hình. Việc tìm kiếm theo thư mục dựng các tên ứng viên đó từ cùng mã kiểu một-chữ-cái mà bạn truyền cho setFont (B cho đậm, I cho nghiêng, BI cho đậm-nghiêng), không phải một từ viết đủ — nên dạng đáng tin cậy là Family-<StyleCode>.ttf (ví dụ DejaVuSans-B.ttf hoặc DejaVuSans-BI.ttf), không phải Family-Bold.ttf. Một tệp tên DejaVuSans-Bold.ttf không bao giờ được việc tìm kiếm theo thư mục tìm thấy; để dùng một tệp như vậy, hãy đăng ký nó tường minh bằng register() — vốn phân tích phông chữ và lập chỉ mục nó theo họ và kiểu đọc từ chính các bảng tên của tệp, nên tên tệp viết đủ không còn quan trọng (xem Bước 2).

Bước 2 — Đăng ký các phông chữ với engine

Phần tiêu đề “Bước 2 — Đăng ký các phông chữ với engine”

Bạn có hai cách tương đương để làm cho các tệp hiển thị. Cả hai đều đi qua NextPDF\Typography\FontRegistry, vốn hiện thực NextPDF\Contracts\FontRegistryInterface.

Đăng ký một tệp cụ thể dưới một alias khi bạn kiểm soát chính xác mặt chữ:

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

register(string $fontFile, string $alias = '', int $fontIndex = 0) chấp nhận các tệp .ttf, .otf, và .ttc, cùng Type1 cũ .pfb (vốn nạp các số đo .afm đi kèm từ cùng đường dẫn); $fontIndex chọn một phông chữ con bên trong một TrueType Collection (.ttc). register() phân tích tệp và lập chỉ mục mặt chữ theo họ và kiểu đọc từ chính các bảng tên của nó, nên tên tệp vật lý không liên quan một khi đã đăng ký. $alias tùy chọn chỉ là một tên tra cứu bổ sung cho mặt chữ — nó không phải một mã kiểu và không thay đổi mặt chữ cung cấp kiểu nào; hãy truyền nó khi bạn muốn gọi setFont() với một tên khác tên họ nhúng của phông chữ. Nó trả về FontInfo đã phân tích.

Đăng ký một thư mục khi bạn muốn engine giải quyết các mặt chữ theo tên từ một thư mục bạn kiểm soát:

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

Hàm khởi tạo FontRegistry nhận thư mục đó làm đối số đầu tiên, và addFontDirectory() thêm các đường tìm kiếm khác. Một Document trần cũng phơi bày addFontDirectory() cho trường hợp độc lập.

Để dùng một registry bạn tự xây dựng, hãy dựng các tài liệu qua DocumentFactory, vốn nối chính registry đó vào mọi tài liệu nó tạo ra:

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() dựng registry nội bộ của riêng nó, nên một mặt chữ bạn đăng ký trên một FontRegistry riêng biệt là vô hình với nó. Trong môi trường thực tế, hãy đi qua DocumentFactory (hoặc factory của framework của bạn) để registry đã được xây dựng là cái đang được dùng.

Mỗi tích hợp framework phơi bày cùng hai khái niệm dưới dạng cấu hình, nên bạn hiếm khi chạm trực tiếp vào registry. Trong nextpdf.php của gói Laravel, fonts_path (mặc định NEXTPDF_FONTS_PATH, lùi về resource_path('fonts')) là thư mục tìm kiếm, và preload_fonts là một danh sách các đường dẫn tệp phông chữ tuyệt đối được phân tích lúc worker khởi động. Hãy trỏ fonts_path tới thư mục bạn đã đóng gói và các mặt chữ đã đăng ký của bạn sẽ giải quyết tự động.

Bước 3 — Cung cấp phông chữ trong một ảnh Docker

Phần tiêu đề “Bước 3 — Cung cấp phông chữ trong một ảnh Docker”

Trong một container, các tệp phông chữ phải là một phần của lớp ảnh, được sao chép vào tại thời điểm build. Vì mã ứng dụng và các phông chữ đi kèm nhau khi bạn đóng gói chúng dưới resources/fonts/, một lệnh COPY . . thông thường đã mang theo chúng. Nếu bạn giữ phông chữ ngoài ngữ cảnh build, hãy sao chép chúng tường minh và đảm bảo đường dẫn bạn đăng ký khớp với đường dẫn bên trong ảnh.

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

Trên một hệ thống tệp bất biến hoặc chỉ-đọc (một container readOnlyRootFilesystem, một ảnh serverless, hoặc một host được làm cứng), các tệp phông chữ được đọc tại thời điểm tạo và không bao giờ được ghi, nên một mount chỉ-đọc là ổn. Lần ghi duy nhất mà engine có thể muốn là bộ đệm phông chữ đã phân tích của nó: hoặc cấp cho thư mục đó một volume nhỏ ghi được, hoặc làm nóng và khóa registry lúc khởi động (mục tiếp theo) để không có lần ghi hay đăng ký nào lúc chạy bị thử.

Trong một worker chạy lâu, hãy phân tích mọi mặt chữ một lần lúc khởi động, rồi khóa registry để không có đăng ký theo từng-yêu-cầu xảy ra và một cấu hình sai thất bại lớn tiếng thay vì âm thầm lùi về dự phòng:

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

Sau lock(), register(), addFontDirectory(), và warmup() ném, điều này biến một lỗi “đường dẫn sai trong ảnh” thành một thất bại khởi động cứng thay vì một trang tofu trong môi trường thực tế.

Hãy thêm một kiểm tra khói triển khai kết xuất một trang với mỗi mặt chữ bắt buộc. Kiểm tra header bên dưới chỉ xác minh rằng tài liệu đã tạo ra đầu ra — nó không chứng minh rằng phông chữ đã phân tích, đã nhúng, hay thậm chí đã giải quyết. Một mặt chữ mà engine không tìm thấy có thể lùi về một phông chữ nền chuẩn (và, dưới hành vi không-nghiêm-ngặt hiện tại, một hồ sơ tuân thủ có thể thay vào đó cấp một bản thay thế đi kèm) trong khi vẫn phát ra một PDF hợp lệ, không rỗng — nên ngay cả ở nơi sự lùi về đó xảy ra, kiểm tra này một mình sẽ không bắt được sự suy giảm âm thầm. Đừng dựa vào việc sự lùi về được đảm bảo hay âm thầm trên mọi đường; hãy xác minh chương trình đã nhúng trực tiếp, như được trình bày bên dưới:

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

Để thực sự làm thất bại quá trình triển khai khi một mặt chữ bị thiếu, hãy kiểm tra PDF đã phát ra để tìm chương trình phông chữ đã nhúng. Một mặt chữ đã đăng ký mà giải quyết được mang theo từ điển phông chữ của riêng nó với một chương trình đã nhúng, nên khẳng định sự hiện diện của nó bắt được trường hợp mặt chữ được yêu cầu không bao giờ giải quyết (bất kể engine lùi về cái gì) mà kiểm tra header bỏ lỡ. Khóa nào giữ chương trình phụ thuộc vào định dạng đường nét: các đường nét TrueType (.ttf, .ttc) dùng /FontFile2, các đường nét CFF/OpenType (.otf với các đường nét PostScript) dùng /FontFile3, và Type1 cũ (.pfb) dùng /FontFile.

Nếu tất cả những gì bạn cần là một tín hiệu “có chương trình phông chữ nào đó đã nhúng” không phụ thuộc định dạng, hãy kiểm tra /FontFile một mình — vì /FontFile là một chuỗi con của cả /FontFile2 lẫn /FontFile3, một kiểm tra chuỗi con trần đã khớp với mọi loại đường nét, và việc thêm /FontFile2//FontFile3 làm các nhánh || bổ sung là dư thừa:

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

Tuy nhiên, một chuỗi con /FontFile trần không thể phân biệt các loại đường nét. Để phân biệt chúng, hãy khớp trên token chính xác với một ranh giới từ để /FontFile không cũng kích hoạt trên /FontFile2 hay /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.');
}

Dù bằng cách nào, hãy coi đây chỉ là một heuristic thô, không phải một cổng triển khai đáng tin. Một lần tìm byte thô trên PDF đã tuần tự hóa không chính xác vì vài lý do: các chương trình phông chữ có thể nằm bên trong các luồng đối tượng đã nén (nơi /FontFile* không bao giờ xuất hiện dưới dạng byte thuần), các cập nhật gia tăng có thể nối thêm hoặc thay thế các đối tượng, các phông chữ không-nhúng hoặc standard-14 một cách hợp lệ không mang chương trình phông chữ nào cả, và các khác biệt tuần tự hóa (thứ tự đối tượng, khoảng trắng, mã hóa tên) có thể di chuyển hoặc ẩn token. Tốt nhất nó xác nhận một số mặt chữ đã nhúng một chương trình — không bao giờ rằng mặt chữ cụ thể bạn muốn đã giải quyết.

Với một cổng triển khai thực sự, đừng dựa vào lần tìm byte. Hãy phân tích PDF đã phát ra bằng một bộ phân tích PDF hoặc bộ kiểm tra đối tượng đúng đắn và khẳng định rằng đối tượng phông chữ cho mặt chữ mục tiêu của bạn mang một chương trình /FontFile//FontFile2//FontFile3 đã nhúng, hoặc dùng một khẳng định giải quyết phông chữ do sản phẩm cung cấp nếu tích hợp của bạn có. Các regex nhận biết token ở trên hữu ích cho một kiểm tra hợp lý cục bộ nhanh, nhưng một lần kiểm tra cấu trúc mới là thứ nên làm thất bại quá trình triển khai. Cấu trúc nhúng và từ điển phông chữ được mô tả trong Nhúng và subset hóa một phông chữ TrueType.

  • createStandalone() có registry riêng của nó. Một mặt chữ đã đăng ký trên một FontRegistry riêng biệt không hiển thị với một tài liệu độc lập. Hãy dùng DocumentFactory (hoặc factory của framework) để registry của bạn là cái đang hoạt động.
  • Các tệp kiểu phải tồn tại dưới dạng tệp. Engine không tổng hợp đậm hay nghiêng từ một mặt chữ thường. Nếu bạn gọi setFont('DejaVuSans', 'B'), việc tìm kiếm theo thư mục tìm DejaVuSans-B.ttf, DejaVuSansB.ttf, hoặc DejaVuSans.ttf (cả các biến thể chữ thường và .otf) — nó tạo ứng viên từ mã kiểu B theo nghĩa đen, nên nó không bao giờ tìm DejaVuSans-Bold.ttf. Một tệp với tên viết đủ như DejaVuSans-Bold.ttf chỉ giải quyết khi bạn đăng ký nó tường minh bằng register(), vốn lập chỉ mục nó theo họ và kiểu đọc từ chính các bảng tên của tệp bất kể tên tệp; dựa vào việc tìm kiếm theo thư mục để tìm nó tạo ra một lần trượt, sau đó engine có thể lùi về một phông chữ nền (không phải một đường được đảm bảo hay luôn-âm-thầm) — sự suy giảm mà trang này cảnh báo.
  • Các đường stream-wrapper và từ xa bị từ chối. Registry từ chối các đường dẫn chứa một scheme URI hoặc một byte null. Hãy đăng ký chỉ các tệp cục bộ; với các phông chữ được lấy lúc chạy hãy dùng registerFromBinary() với các byte thô.
  • Registry đã khóa là bất biến. Một khi bạn gọi lock(), bất kỳ register(), addFontDirectory(), hay warmup() nào sau đó ném. Các phương thức tra cứu vẫn khả dụng. Hãy đăng ký và làm nóng mọi thứ trước khi khóa.
  • Các collection CJK lớn. Hãy đăng ký phông chữ con đúng của một .ttc bằng $fontIndex, và dự trù cho một subset nhúng lớn hơn. Xem các ghi chú CJK trong công thức nhúng-và-subset.
  • Một tệp phông chữ là đầu vào nhị phân không đáng tin. Chỉ đóng gói phông chữ từ các nguồn bạn tin tưởng, và xác thực nguồn gốc của bất kỳ mặt chữ nào được chấp nhận từ người dùng cuối.
  • Khóa registry sau khi làm nóng loại bỏ một bề mặt biến đổi lúc chạy và làm cho một lỗi đường dẫn thất bại lúc khởi động thay vì âm thầm làm suy giảm đầu ra.
  • Đừng chèn đầu vào người dùng vào một đường dẫn tệp đã đăng ký. Hãy đăng ký một tập cố định các mặt chữ đã đóng gói; đừng để một yêu cầu chọn một đường dẫn hệ thống tệp tùy ý.

Hướng dẫn này không đưa ra tuyên bố tiêu chuẩn quy phạm nào. Mọi ký hiệu được trình bày là bề mặt công khai đã được xác minh: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), đối số hàm khởi tạo thư mục), hợp đồng NextPDF\Contracts\FontRegistryInterface của nó, NextPDF\Core\DocumentFactory::create(), và NextPDF\Core\Document::setFont() / addFontDirectory(). Các khóa Laravel fonts_pathpreload_fonts là cấu hình được tài liệu hóa của gói nextpdf/laravel. Hành vi nhúng và thẻ subset, với các trích dẫn ISO 32000-2 của nó, được tài liệu hóa trên công thức nhúng-và-subset được liên kết dưới Xem thêm.