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

Cùng những byte như nhau mỗi lần: PDF tái lập được

Spec: ISO 32000-2, §14.4Spec: ISO 32000-2, §14.3.3

Xây một tệp PDF từ cùng đầu vào hai lần và bạn sẽ mong đợi cùng một tệp. Hầu hết các thư viện PDF không thể hứa điều đó — xây lại và diff, và các byte trôi dạt. NextPDF có thể ghim hai thứ di chuyển, để cùng đầu vào tạo ra cùng các byte, mỗi lần.

Đầu ra giống-hệt-từng-byte không phải một chỉ số phù phiếm. Nó là nền tảng nằm dưới ba thứ mà các nhóm thực sự muốn.

Thứ nhất là cache. Nếu một lần build là một hàm thuần túy của các đầu vào của nó, thì hash đầu ra của nó là một cache key. Cùng đầu vào, cùng hash, bỏ qua công việc và phục vụ tệp đã lưu. Khi các byte lang thang, hash lang thang, và cache không bao giờ trúng.

Thứ hai là bằng chứng chống can thiệp. Một đường ống có thể tái tạo đúng cái tệp nó đã ship có thể chứng minh, về sau, rằng một tài liệu được lưu trữ đã không bị thay đổi: xây lại nó, hash cả hai, so sánh. Nếu thậm chí một byte khác đi vì một đồng hồ được nhúng, bằng chứng biến mất và bạn quay lại với “hãy tin tôi”.

Thứ ba là CI đáng tin cậy. Một golden-file test ghi lại một đầu ra biết-là-tốt và thất bại khi một thay đổi làm biến đổi nó. Tín hiệu đó chỉ có ý nghĩa nếu một engine không-đổi tái tạo một tệp không-đổi. Nếu mỗi lần chạy khác nhau ở một timestamp, golden file là nhiễu, và nhóm học được cách phớt lờ một build đỏ — thói quen đắt giá nhất trong kiểm thử.

Trong profile tất định của NextPDF, hai trường do-engine-kiểm-soát mà nếu không sẽ trôi dạt giữa các lần build giống hệt nhau là các mốc ngày và /ID. Điều này giả định rằng phần còn lại của đường ống đã ổn định sẵn — cùng đầu vào, và một cách serialize không tự biến đổi (thêm về điều đó bên dưới):

  • Các mốc ngày được nhúng. Document information dictionary mang theo CreationDateModDate (Spec: ISO 32000-2, §14.3.3), và metadata XMP phản chiếu chúng. Bắt lấy “bây giờ” tại thời điểm build và mỗi lần xây lại khác đi.
  • Định danh tệp. Mảng /ID là một cặp chuỗi byte định danh tệp (Spec: ISO 32000-2, §14.4), được lưu trong trailer dictionary (Spec: ISO 32000-2, §7.5.5). Các thư viện thường suy ra nó từ thời gian hiện tại cộng với các byte ngẫu nhiên, nên nó khác đi trên mỗi lần chạy theo thiết kế.

Ghim cả hai — một timestamp cố định và một seed cố định cho /ID — và đầu ra trở thành một hàm tất định của nội dung của nó. Để nguyên nội dung và tệp giống-hệt-từng-byte. Đây là cùng kỷ luật mà dự án Reproducible Builds thiết lập cho phần mềm được biên dịch, áp dụng cho tầng tài liệu.

Tính tất định trong NextPDF là một đối tượng cấu hình, không phải một mẹo kiểm thử. Engine phơi ra một value object DeterministicSettings trong namespace NextPDF\Core. Nó là final readonly, bất biến, và nó ghim đúng hai nguồn trôi dạt phái-sinh-từ-đồng-hồ và phái-sinh-từ-ngẫu-nhiên được nêu trên: các mốc ngày và /ID. Ghim chúng loại bỏ hai nguồn trôi dạt phổ biến nhất, nhưng tự thân nó không bảo đảm đầu ra giống-hệt-từng-byte. Hành vi serialize khác của engine — thứ tự đối tượng, font subsetting, và các cài đặt nén — cũng phải tất định thì đầu ra mới tái lập được, và NextPDF giữ những thứ đó ổn định theo thiết kế.

Constructor của nó nhận hai đối số:

public function __construct(
public DateTimeImmutable $timestamp,
public string $fileIdSeed,
) {
// ...
}

$timestamp là khoảnh khắc cố định duy nhất được ghi vào mọi trường ngày — CreationDate, ModDate, và bản phản chiếu XMP của chúng. Truyền một DateTimeImmutable và tài liệu thôi không còn hỏi đồng hồ treo tường mấy giờ rồi nữa. $fileIdSeed là đầu vào ghim /ID của trailer: một chuỗi thập-lục-phân 32 ký tự. Đưa cùng seed và engine suy ra cùng định danh tệp thay vì lấy mẫu từ đồng hồ và một nguồn ngẫu nhiên.

Đối tượng tự xác nhận đầu vào của chính nó. Seed phải đúng 32 ký tự thập-lục-phân; bất cứ thứ gì khác bị từ chối tại thời điểm khởi tạo với một InvalidConfigException thay vì lặng lẽ tạo ra một /ID trông-khác-đi. Đây là cùng lập trường từ-chối-đoán mà phần còn lại của engine áp dụng — một đầu vào mơ hồ thất bại ầm ĩ thay vì lặng lẽ thay đổi các byte.

Với cả hai được ghim, công thức là cái mà dự án Reproducible Builds đã làm cho quen thuộc: xây lại nó, diff nó, và bản diff rỗng.

  1. Fix the inputsThe same content, fonts, and settings that produced the original document.
  2. Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
  3. Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
  4. BuildThe output is now a pure function of content; the two moving parts are held still.
  5. Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
Reproducible build: identical inputs plus a pinned timestamp and a pinned /ID seed produce the same bytes, which a rebuild-and-diff step confirms.

Một hình mẫu nhỏ, hoàn chỉnh. Các settings được khởi tạo một lần và tái sử dụng, nên hai lần chạy cùng một chương trình phát ra cùng một tệp.

<?php
declare(strict_types=1);
use NextPDF\Core\DeterministicSettings;
use NextPDF\Exception\InvalidConfigException;
// One fixed instant for every date field — never the wall clock.
$timestamp = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
// A 32-character hex seed pins the trailer /ID. Same seed, same /ID.
$fileIdSeed = '0123456789abcdef0123456789abcdef';
try {
$deterministic = new DeterministicSettings(
timestamp: $timestamp,
fileIdSeed: $fileIdSeed,
);
} catch (InvalidConfigException $e) {
// A malformed seed (not exactly 32 hex chars) is refused here,
// before any document is built — not silently coerced.
error_log($e->getMessage());
throw $e;
}
// Hand $deterministic to the document configuration. With both moving
// parts pinned, building the same content twice yields identical bytes:
//
// sha256(build_one) === sha256(build_two)

Seed là một đầu vào build mà bạn kiểm soát, không phải một bí mật. Lưu nó cạnh phần còn lại của cấu hình build của bạn. Điểm mấu chốt là nó cố định, nên định danh tệp mà nó tạo ra cũng cố định.

Cái bẫy thứ nhất là “Tôi đã bỏ timestamp, vậy build của tôi giờ tái lập được.” Thường thì không, vì mảng /ID là cái lặng lẽ hơn trong hai nguồn. Các mốc ngày thì thấy được trong một bảng metadata và dễ nhớ; /ID của trailer thì vô hình với hầu hết trình đọc và được tái tạo từ đồng hồ và một nguồn ngẫu nhiên trên mỗi lần chạy. Một build chỉ ghim các mốc ngày vẫn tạo ra một tệp khác nhau mỗi lần. Bạn phải giữ cả hai đứng yên.

Cái bẫy thứ hai là coi tính tất định như một tính năng bảo mật tự thân. Một /ID được ghim làm cho một tệp tái lập được; nó không làm cho tệp được , và tự thân nó không chứng minh rằng hai build khớp nhau. Một phép so sánh từng-byte hay một hash chứng minh các build khớp; ghim /ID chỉ loại bỏ một nguồn khác biệt giả tạo. Và không cái nào trong số đó chứng minh rằng một bên thứ ba bảo lãnh cho tệp. Tính tái lập và việc ký là các tầng bổ sung cho nhau, không phải thay thế cho nhau.

Tính tất định ghim các bộ phận di chuyển của chính engine. Nó không ghim đầu vào của bạn. Nếu nội dung của bạn nhúng một timestamp sống, kéo một font đã thay đổi trên đĩa, hoặc hiển thị một giá trị phụ thuộc vào ngày hiện tại, thì đầu ra thay đổi vì đầu vào đã thay đổi — và điều đó là đúng. DeterministicSettings loại bỏ tính không tất định của engine, không phải của bạn. Một build tái lập được vẫn đòi hỏi các đầu vào tái lập được.

Deterministic byte-identical output — edition availability
EditionAvailability
Core

Full support. DeterministicSettings ships in the open-source core: pin the timestamp and the /ID seed and the same content rebuilds to the same bytes — no edition gate.

ProNot in this edition
EnterpriseNot in this edition
  • Golden-file testing — kỹ thuật CI phụ thuộc vào đầu ra giống-hệt-từng-byte, và vì sao một engine tất định là điều kiện tiên quyết của nó.
  • Incremental updates — cách một tệp PDF lớn dần bằng cách nối thêm, nơi mảng /ID lại quan trọng lần nữa để liên hệ một tệp với các phiên bản trước của nó.
  • Metadata and the XMP packet — nơi các mốc ngày được nhúng sống, và cách gói XMP phản chiếu document information dictionary.
  • The anatomy of a PDF file — trailer, cross-reference table, và nơi mảng /ID nằm trong cấu trúc tệp.
  • Giống-hệt-từng-byte (Byte-identical) — hai tệp khớp nhau chính xác, từng byte một. Hình thức mạnh nhất của “giống nhau”, và là hình thức mà một hash hay một diff có thể xác minh.
  • /ID (định danh tệp) — mảng gồm hai chuỗi byte định danh một tệp PDF và các phiên bản của nó (ISO 32000-2 §14.4), được lưu trong trailer dictionary (§7.5.5). Thường được suy ra từ đồng hồ cộng các byte ngẫu nhiên, đó là lý do nó thay đổi trên mỗi build chưa-ghim.
  • Document information dictionary — cấu trúc mang theo CreationDateModDate (ISO 32000-2 §14.3.3). Một trong hai nguồn của tính không tất định mà một build tất định phải ghim.
  • Golden file — một đầu ra biết-là-tốt được ghi lại mà một test so sánh với; chỉ có ý nghĩa khi một engine không-đổi tái tạo một tệp không-đổi.
  • Build tái lập được (Reproducible build) — một build mà đầu ra của nó là một hàm tất định của các đầu vào của nó, nên xây lại từ cùng đầu vào cho ra cùng các byte. Thuật ngữ này đến từ dự án Reproducible Builds cho phần mềm được biên dịch.