Giảm kích thước tệp PDF bằng cách nén và subsetting
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”Bạn muốn tệp PDF nhỏ nhất mà nội dung cho phép, nhưng vẫn giữ nguyên độ trung thực. NextPDF cung cấp hai cách kiểm soát kích thước tệp, và cả hai đều được bật theo mặc định:
- Nén luồng. Trình ghi bọc mọi luồng nội dung trang và mọi chương trình phông chữ được nhúng trong một luồng FlateDecode (zlib). Cờ
NextPDF\Core\Configcompresslưu cài đặt này. Điều chỉnh giá trị đó bằng phương thức witherwithCompress()khi bạn dựng tài liệu theo luồng. - Subsetting phông chữ. Khi bạn nhúng một phông chữ TrueType hoặc CFF, trình ghi dựng lại chương trình phông chữ để nó chỉ mang theo các glyph mà tài liệu sử dụng, rồi nén kết quả bằng FlateDecode. Việc này diễn ra tự động. Không có cờ nào để đặt và không có lệnh gọi nào cần thực hiện. Một phông chữ CJK có
20,000glyph nhưng chỉ dùng vài trăm glyph trong một tài liệu sẽ được nhúng với kích thước chỉ bằng một phần nhỏ so với dung lượng trên đĩa của nó.
Một điều cần nói rõ ngay từ đầu: NextPDF Core không cung cấp tính năng lấy mẫu lại ảnh, nút điều chỉnh chất lượng ảnh, công tắc bật/tắt object stream, hay cài đặt loại bỏ trùng lặp tài nguyên. Hai cách kiểm soát ở trên là những cách kiểm soát kích thước duy nhất. Phần còn lại của bài hướng dẫn này chỉ cho bạn cách dùng chúng đúng cách và những gì mỗi cách không làm được.
Điều kiện tiên quyết: một bản cài đặt Core (composer require nextpdf/core:^3) và, cho hướng subsetting, một tệp phông chữ mà bạn có giấy phép để nhúng.
Cài đặt
Phần tiêu đề “Cài đặt”composer require nextpdf/core:^3Tổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”Một tệp PDF là một cây đối tượng. Các đối tượng lớn nhất thường là các luồng nội dung (các toán tử vẽ cho từng trang) và các chương trình phông chữ (các đường viền glyph được nhúng). Cả hai loại này đều nén tốt, nên cách kiểm soát kích thước hiệu quả nhất là nén chúng bằng FlateDecode. FlateDecode là tên gọi trong PDF 2.0 cho một luồng DEFLATE được bọc bằng zlib (ISO 32000-2:2020 §7.4.4), và đó là bộ lọc mà NextPDF phát ra.
Trình ghi ghim mức nén DEFLATE ở 9, mức tối đa theo RFC 1951, thông qua NextPDF\Writer\PinnedZlibCompressor. Mức 9 đánh đổi thêm một chút CPU để có luồng nhỏ nhất. Việc ghim mức nén cũng giữ cho đầu ra có tính tất định, vì phần đầu zlib mã hóa mức nén và một mức nén thay đổi sẽ làm thay đổi các byte. Bạn không chọn mức nén — engine cố định nó để hai lần chạy trên cùng một đầu vào tạo ra các luồng giống hệt nhau từng byte.
Đòn bẩy thứ hai là subsetting phông chữ. Một tệp phông chữ trên đĩa mang theo mọi glyph mà kiểu chữ định nghĩa, nhưng một tài liệu in “Invoice 2026” chỉ cần một vài glyph trong số đó. NextPDF\Typography\FontSubsetter (cho TrueType) và NextPDF\Typography\CffSubsetter (cho CFF / OpenType) duyệt qua các codepoint mà tài liệu thực sự đã kết xuất, giải quyết các phụ thuộc của composite glyph, và chỉ dựng lại các bảng phông chữ cần thiết. Chúng phát ra một tệp nhị phân phông chữ subset hợp lệ với một thẻ tiền tố subset gồm sáu chữ cái có tính tất định (ISO 32000-2:2020 §9.9). Trình ghi áp dụng việc này mỗi khi đã biết tập glyph được dùng của một phông chữ nhúng, rồi nén subset đó bằng FlateDecode. Nếu việc subset một phông chữ cụ thể tiết kiệm được ít hơn mười phần trăm, trình subset sẽ trả về chương trình gốc thay vì subset, vì chi phí dựng lại không đáng cho phần lợi ích quá nhỏ.
Điều cần rút ra: bạn giữ cho các tệp PDF nhỏ gọn bằng cách luôn bật nén (mặc định) và nhúng các tệp phông chữ thực (để subsetting có thứ có thể thu nhỏ), chứ không phải bằng cách tinh chỉnh một danh sách dài các tùy chọn.
Bề mặt API
Phần tiêu đề “Bề mặt API”Nút điều chỉnh kích thước duy nhất mà bạn cấu hình nằm trên đối tượng cấu hình.
NextPDF\Core\Config là một value object bất biến, final readonly với các phương thức wither được định kiểu. Thành viên liên quan đến kích thước là:
compress(bool, mặc địnhtrue) — bật nén FlateDecode. Điều chỉnh giá trị này bằngwithCompress(bool $compress): self, phương thức trả về mộtConfigmới với cờ đã thay đổi và mọi trường khác được giữ nguyên.
Gắn một Config vào tài liệu khi khởi tạo:
NextPDF\Core\Document::createStandalone(?Config $config = null): selfdựng một tài liệu với các registry tạm thời cho một script CLI hoặc một tiến trình tồn tại ngắn, áp dụngConfigcủa bạn.
Hai thành viên định hình phạm vi mà các đòn bẩy kích thước có thể tác động, nhưng bản thân cả hai đều không phải là cách kiểm soát nén:
imageCacheBytes(int, mặc định52_428_800) giới hạn bộ nhớ đệm ảnh trong bộ nhớ, vàwithImageCacheBytes(int $bytes): selfthay đổi nó. Việc này giới hạn mức bộ nhớ đỉnh trong khi dựng. Nó không lấy mẫu lại, nén lại, hay bằng cách nào khác thu nhỏ các ảnh mà bạn nhúng — đây là một trần bộ nhớ, không phải cách kiểm soát kích thước đầu ra.fontsDirectory(string) vàwithFontsDirectory(string $dir): selfđặt đường dẫn tìm kiếm mặc định cho các tệp phông chữ, làm nguồn cho hướng subsetting.
Các thao tác phông chữ đi qua bề mặt typography của tài liệu:
setFont(string $family, string $style = '', float $size = 12.0): staticchọn một phông chữ. Khi family phân giải thành một tệp phông chữ có thể nhúng, trình ghi ghi lại các codepoint mà bạn kết xuất để có thể subset phông chữ đó tại thời điểm lưu.addFontDirectory(string $directory): staticđăng ký một thư mục bổ sung để tìm kiếm các tệp phông chữ.
Đầu ra là bộ ba tiêu chuẩn: getPdfData(): string trả về các byte, save(string $path): void ghi chúng theo cách nguyên tử, và output(?string $filename, OutputDestination $dest): string xử lý việc gửi qua HTTP.
Subsetting không có phương thức công khai và không có cờ. Đây là hành vi tự sinh ra từ việc nhúng một phông chữ và kết xuất văn bản. Trình ghi điều khiển FontSubsetter / CffSubsetter thay cho bạn bên trong NextPDF\Writer\PdfFontWriter.
Mẫu mã — khởi đầu nhanh
Phần tiêu đề “Mẫu mã — khởi đầu nhanh”Ví dụ này dựng một tài liệu với nén được bật rõ ràng và một phông chữ được nhúng, đã subset, rồi ghi các byte. Nó lược bỏ việc xử lý lỗi để giữ cho dạng lệnh gọi rõ ràng. Mẫu dùng cho môi trường thực bên dưới bổ sung đầy đủ các biện pháp bảo vệ.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);$doc->addFontDirectory(__DIR__ . '/fonts');$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer// subsets this font automatically when the document is built.$doc->setFont('LiberationSans', '', 12);$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));Mẫu mã — môi trường thực
Phần tiêu đề “Mẫu mã — môi trường thực”Đây là một chương trình khép kín. Nó dựng một tài liệu với nén được bật, nhúng một phông chữ từ thư mục mà bạn kiểm soát, kết xuất văn bản để trình subset có tập glyph được dùng, và ghi kết quả theo cách nguyên tử. Nó bắt các ngoại lệ NextPDF cụ thể nhất mà các hướng dựng và lưu phát ra, rồi ném lại từng ngoại lệ kèm ngữ cảnh thay vì nuốt đi. Trỏ NEXTPDF_FONT_DIR tới một thư mục chứa phông chữ TrueType hoặc CFF mà bạn có giấy phép để nhúng; chương trình xác thực đường dẫn trước khi nhúng.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;
/** * Resolve and validate the font directory from a server-controlled source. * * Reading the directory from the environment keeps the path off the request * surface. The function rejects a missing or unreadable directory so the * embedding path never runs against untrusted or absent input. */function resolveFontDirectory(): string{ $configured = getenv('NEXTPDF_FONT_DIR'); $dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir); if ($real === false || !is_dir($real) || !is_readable($real)) { throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir)); }
return $real;}
/** * Build a compressed, font-subsetted document and return its bytes. * * @param non-empty-string $fontDirectory Validated directory of embeddable fonts. * * @return string Raw PDF bytes. */function buildCompactPdf(string $fontDirectory): string{ // compress is true by default; pin it so the intent is explicit and the // streaming writer path honours it regardless of any wrapper defaults. $config = (new Config()) ->withCompress(true) ->withFontsDirectory($fontDirectory) // Bound the image cache so a build cannot exhaust memory. This is a // memory ceiling, not an output-size control. ->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config); $doc->addFontDirectory($fontDirectory); $doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the // writer turns into a font subset at build time. $doc->setFont('LiberationSans', '', 12); $doc->cell(0, 10, 'Invoice 2026', newLine: true); $doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program // are FlateDecode-compressed before the bytes are returned. return $doc->getPdfData();}
try { $fontDirectory = resolveFontDirectory(); $pdf = buildCompactPdf($fontDirectory);} catch (CompressionException $e) { // Raised if the zlib encoder hard-fails while compressing a stream. throw new RuntimeException( sprintf('Compression failed for a %s stream.', $e->getAlgorithm()), previous: $e, );} catch (InvalidConfigException $e) { // Raised by the output path for an invalid destination configuration. throw new RuntimeException( sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()), previous: $e, );}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) { throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);STDOUT dự kiến (số byte phụ thuộc vào phông chữ và lần dựng):
Wrote <n> bytes to <path>.Trường hợp biên & điểm cần lưu ý
Phần tiêu đề “Trường hợp biên & điểm cần lưu ý”- Nén bật theo mặc định. Một
Configmới cócompressđược đặt thànhtrue. Bạn hầu như không cần dùngwithCompress(). Chỉ đặt nó rõ ràng để ghi lại ý định, hoặc để chọn không nén cho một lần dựng nhằm gỡ lỗi khi bạn muốn đọc các luồng thô. - Tắt nén làm cho tệp lớn hơn, chứ không nhỏ hơn.
withCompress(false)là một công cụ hỗ trợ chẩn đoán để xem xét các luồng chưa nén. Nó không bao giờ là một cách tối ưu hóa kích thước. Hãy phát hành với nén bật. - Subsetting cần một phông chữ được nhúng thực sự. Các phông chữ tiêu chuẩn Base14 (Helvetica, Times, Courier, và những phông liên quan) được tham chiếu theo tên và không mang theo chương trình nhúng nào trong một tài liệu thông thường, nên không có gì để subset. Subsetting chỉ thu nhỏ các phông chữ mà bạn nhúng từ một tệp phông chữ.
- Subsetting diễn ra tự động và âm thầm. Không có cờ, không có phương thức, và không có xác nhận. Nếu bạn đã nhúng một phông chữ và kết xuất văn bản bằng nó, trình ghi đã subset nó. Chương trình được nhúng mang theo một thẻ tiền tố subset gồm sáu chữ cái (ví dụ
ABCDEF+LiberationSans) để người đọc có thể phân biệt một subset với một phông nhúng đầy đủ. - Mức tiết kiệm nhỏ thì giữ lại phông chữ đầy đủ. Khi một subset sẽ tiết kiệm được ít hơn mười phần trăm kích thước chương trình, trình subset trả về phông gốc. Đây là một ngưỡng sàn có chủ đích: chi phí dựng lại không đáng cho phần lợi ích quá nhỏ. Việc nhúng một phông chữ vốn đã rất nhỏ, hoặc kết xuất gần như toàn bộ glyph của nó, có thể rơi vào trường hợp này.
imageCacheByteskhông phải là nút điều chỉnh kích thước ảnh. Nó giới hạn bộ nhớ, không phải số byte đầu ra. NextPDF Core nhúng dữ liệu ảnh mà bạn đưa cho nó; không có bước lấy mẫu lại, giảm mẫu, hay mã hóa lại nào. Nếu bạn cần ảnh nhỏ hơn, hãy thay đổi kích thước và mã hóa lại chúng trước khi nhúng.- Không tồn tại cài đặt object stream hay loại bỏ trùng lặp. NextPDF Core không cung cấp công tắc cho object stream của PDF 2.0 hay cho việc loại bỏ trùng lặp tài nguyên. Đừng tìm kiếm một công tắc như vậy — các đòn bẩy kích thước là nén luồng và subsetting phông chữ.
Hiệu năng
Phần tiêu đề “Hiệu năng”Nén ở mức 9 là chi phí CPU chủ yếu của việc ghi một luồng. Nó đánh đổi thêm vài phần trăm thời gian dựng để có đầu ra nhỏ nhất. Chi phí tuyến tính theo số byte chưa nén, nên số trang và lượng dữ liệu phông chữ được nhúng quyết định ngân sách. Subsetting bổ sung một lượt chạy duy nhất cho mỗi phông chữ được nhúng; lượt này phân tích thư mục bảng của phông chữ, giải quyết bao đóng của tập glyph được dùng, và dựng lại các bảng cần thiết. Đối với một phông chữ CJK lớn, đây là đòn bẩy tốn kém hơn trong hai đòn bẩy, nhưng nó chạy một lần cho mỗi phông chữ, chứ không phải một lần cho mỗi trang. Ngưỡng sàn tiết kiệm mười phần trăm tồn tại một phần để giữ cho lượt chạy đó nằm ngoài đường xử lý nóng khi nó sẽ không mang lại lợi ích. Một tài liệu nhỏ với một subset được nhúng nằm thoải mái trong ngân sách 1500 ms thời gian thực và 96 MB bộ nhớ đỉnh. Hãy giới hạn imageCacheBytes theo trần thực tế của bạn để một lần dựng nhúng nhiều ảnh sẽ thất bại nhanh do bộ nhớ thay vì rơi vào swap.
Lưu ý về bảo mật
Phần tiêu đề “Lưu ý về bảo mật”Quá trình dựng chạy trong cùng tiến trình; không có byte tài liệu nào rời khỏi máy chủ và không có lệnh gọi mạng nào được thực hiện. Hãy coi mọi phông chữ hoặc ảnh được cung cấp từ bên ngoài là đầu vào không đáng tin cậy:
- Xác thực thư mục phông chữ. Mẫu dùng cho môi trường thực đọc đường dẫn phông chữ từ một biến môi trường do máy chủ kiểm soát và từ chối một thư mục không tồn tại hoặc không đọc được trước khi nhúng. Đừng bao giờ lấy đường dẫn phông chữ từ một trường của yêu cầu.
- Chỉ nhúng các phông chữ mà bạn có giấy phép phân phối lại. Một subset vẫn là một chương trình phông chữ được nhúng. Hãy xác nhận giấy phép cho phép nhúng trước khi bạn phát hành một tài liệu mang theo phông chữ đó.
- Phông chữ bị lỗi định dạng sẽ phát ngoại lệ, chúng không âm thầm làm hỏng. Một tệp phông chữ phân tích thất bại sẽ phát
NextPDF\Exception\FontParsingException, và một lỗi zlib nghiêm trọng sẽ phátNextPDF\Exception\CompressionException. Hãy bắt ngoại lệ cụ thể nhất và xử lý theo từng loại. Đừng bao giờ bọc quá trình dựng trong mộtcatchrỗng. - Đừng bao giờ chèn đầu vào của người dùng vào đường dẫn đầu ra. Mẫu này ghi vào một đường dẫn cố định hoặc một kênh phụ do máy chủ kiểm soát, và từ chối các stream wrapper cùng byte null thông qua trình ghi nguyên tử trong
save(). Hãy lấy các đường dẫn đầu ra từ các giá trị do máy chủ kiểm soát để tránh path traversal. - Không có bí mật trong tài liệu. Đừng nhúng thông tin xác thực, token, hay định danh nội bộ vào một tài liệu được tạo ra mà bạn trả về cho khách hàng.
Tuân thủ
Phần tiêu đề “Tuân thủ”Bài hướng dẫn này tự nó không đưa ra tuyên bố tiêu chuẩn quy phạm nào. Các cơ chế mà nó sử dụng được định nghĩa bởi đặc tả PDF 2.0: nén luồng FlateDecode (ISO 32000-2:2020 §7.4.4) và việc đặt tên subset phông chữ với một tiền tố subset gồm sáu ký tự (ISO 32000-2:2020 §9.9). NextPDF phát ra cả hai như một phần của hướng ghi tiêu chuẩn; bạn không cấu hình chúng ngoài cờ compress. Hồ sơ khả tái lập structural mà trang này khai báo phản ánh việc trình ghi ghim mức DEFLATE, nên các luồng được nén có tính tất định, trong khi các định danh ở cấp tài liệu vẫn có thể khác nhau giữa các lần chạy trừ khi bạn cũng cấu hình các thiết lập mang tính tất định. Để biết cơ chế nhúng đằng sau subsetting, hãy xem bài hướng dẫn embed-and-subset được liên kết bên dưới.
Xem thêm
Phần tiêu đề “Xem thêm”- Nhúng và subset một phông chữ TrueType — đăng ký một phông chữ, kết xuất với nó, và xem xét thẻ subset được nhúng.
- Soạn văn bản và phông chữ — bề mặt soạn văn bản và phông chữ rộng hơn, làm nguồn cho hướng subsetting.
- Tài liệu tham khảo module Configuration — toàn bộ value object
Config, các wither của nó, và giá trị mặc định của chúng. - Xử lý lỗi có nhận biết ngoại lệ — cây phân cấp ngoại lệ NextPDF đằng sau
CompressionException,FontParsingException, vàInvalidConfigException.