Bỏ qua để đến nội dung

PDF thật sự là gì

Evidence: Standard-backed

PDF không phải là một bản mô tả trang tình cờ nằm trong một tệp. Nó là một cơ sở dữ liệu đồ thị nhỏ, có gắn kèm một máy in. Trang này mô tả bốn phần mà mọi PDF đều có — header, body, bảng tham chiếu chéo, trailer — và cách NextPDF ghi chúng để trình đọc có thể tìm thấy từng đối tượng mà không cần phỏng đoán.

Phần lớn lỗi PDF không phải là lỗi kết xuất. Chúng là lỗi cấu trúc: một byte offset trỏ lệch một ký tự so với đối tượng mà nó phải trỏ tới, một trailer nêu sai root, một mục tham chiếu chéo mâu thuẫn với vị trí thực tế của đối tượng. Không lỗi nào trong số đó làm thay đổi diện mạo của trang, cho đến khi một trình đọc đi theo một đường dẫn khác qua tệp và đọc quá phần cuối tệp.

Nếu bạn coi một PDF là hộp đen, những sự cố đó trông có vẻ ngẫu nhiên. Nếu bạn hiểu mô hình đối tượng, chúng hiện ra đúng bản chất: một con số không khớp với một vị trí. Đọc được định dạng là ranh giới giữa “PDF bị hỏng” và “offset của đối tượng 14 đã cũ vì bộ ghi đo nó trước khi hoàn tất độ dài luồng.”

Một PDF gồm bốn phần, theo thứ tự trong tệp:

  1. Một header — một dòng nêu phiên bản (%PDF-2.0).
  2. Một body — một chuỗi các đối tượng gián tiếp được đánh số: dictionary, luồng, mảng, số, chuỗi, tên.
  3. Một bảng tham chiếu chéo (hoặc, trong PDF 2.0, một luồng tham chiếu chéo) — một bảng tra từ số đối tượng sang byte offset, để có thể truy cập mọi đối tượng mà không cần quét toàn bộ tệp.
  4. Một trailer — một dictionary nhỏ nêu đối tượng root của tài liệu và trỏ tới vị trí bắt đầu của phần tham chiếu chéo.

Một trình đọc không đọc PDF từ đầu đến cuối. Nó đọc dòng cuối cùng trước, tìm startxref, nhảy tới phần tham chiếu chéo và dùng phần đó như một chỉ mục vào body. Định dạng này được xây dựng để đọc ngược. Chỉ riêng sự thật đó đã giải thích phần lớn thiết kế của nó.

NextPDF dựng một PDF theo đúng cách định dạng được đọc: đối tượng đi trước, offset được ghi lại sau, bảng được ghi cuối cùng.

Mỗi đối tượng gián tiếp được một registry duy nhất (src/Core/ObjectRegistry.php) cấp số. Registry cấp các số tuần tự thông qua allocate() và, sau khi các byte của một đối tượng được ghi vào buffer đầu ra, ghi lại byte offset thông qua register(). Các offset không bao giờ được đoán trước. Chúng được quan sát từ BinaryBuffer::getOffset() đúng lúc header của đối tượng được phát ra. Đây là lý do một mục tham chiếu chéo của NextPDF không thể lệch khỏi đối tượng mà nó mô tả: offset chính là vị trí thực tế của buffer tại thời điểm đó.

Khi body hoàn tất, một chiến lược tuần tự hóa riêng cho từng phiên bản (src/Writer/PdfSerializationStrategy.php) sẽ ghi phần tham chiếu chéo và trailer:

  • Pdf20StreamStrategy phát ra một luồng tham chiếu chéo đã nén (/Type /XRef) — mặc định của PDF 2.0.
  • Pdf17TableStrategyPdf14TableStrategy phát ra một bảng tham chiếu chéo truyền thống 20 byte cùng với một dictionary trailer riêng — bắt buộc với các profile PDF/A yêu cầu cấu trúc tệp cũ hơn.

Chiến lược được chọn theo profile đầu ra, không dựa trên suy đoán. Dù theo cách nào, các byte cuối cùng đều có cùng một hình dạng: phần tham chiếu chéo, rồi startxref, rồi byte offset, rồi %%EOF. Phần đuôi đó là thứ trình đọc tìm thấy đầu tiên.

  1. Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
  2. Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
  3. Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
  4. Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
Cách một trình đọc phân giải một đối tượng trong tệp NextPDF, và điều khoản ISO 32000-2 định nghĩa từng bước: nó bắt đầu từ cuối tệp và đi dần vào trong.

Cấu trúc bốn phần này không phải là quy ước riêng của NextPDF; nó là điều khoản về cấu trúc tệp trong Spec: ISO 32000-2, §7.5 . Tiêu chuẩn này định nghĩa một PDF gồm một header, một body chứa các đối tượng, một bảng tham chiếu chéo và một trailer, đồng thời nêu rằng trình đọc nên phân tích cú pháp từ cuối tệp. Dòng cuối cùng là %%EOF, còn hai dòng trước đó là từ khóa startxref và byte offset tới phần tham chiếu chéo.

Evidence: Standard-backed

Một đối tượng gián tiếp được định nghĩa bằng một số đối tượng và một số thế hệ, cách nhau bởi khoảng trắng, theo sau là giá trị của đối tượng được đặt giữa hai từ khóa objendobj. Tổ hợp số đối tượng và số thế hệ xác định đối tượng một cách duy nhất; một tham chiếu gián tiếp tới nó được viết dưới dạng số đối tượng, số thế hệ và từ khóa R. ObjectRegistry của NextPDF phản ánh chính xác điều này: một số tuần tự, thế hệ 0 cho các đối tượng vừa ghi, và một offset được ghi lại.

Từ PDF 1.5 trở đi, các đối tượng cũng có thể nằm bên trong một luồng đối tượng, nơi chúng được lưu trữ mà không có hai từ khóa obj/endobj và phải có thế hệ bằng không. Luồng tham chiếu chéo (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) là cơ chế PDF 2.0 lập chỉ mục cho cả các đối tượng thông thường lẫn các đối tượng đã nén này. NextPDF có CrossReferenceStream dựng nó bằng một mảng độ rộng trường /W và nén FlateDecode.

Đây là hình dạng của một body PDF tối giản và trailer của nó. Các con số trong phần tham chiếu chéo là byte offset. Chúng phải chính xác tuyệt đối, đó là lý do NextPDF ghi lại chúng từ buffer thay vì tự tính ra.

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000122 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
196
%%EOF

Một trình đọc mở tệp này từ dưới lên: %%EOF, rồi startxref 196, rồi nhảy tới byte 196 nơi xref bắt đầu, đọc được rằng đối tượng 1 nằm ở byte 9, đi theo /Root 1 0 R tới catalog, và duyệt cây trang từ đó. Đối tượng 0 luôn là phần đầu của danh sách trống với thế hệ 65535 — một đặc điểm kỳ lạ kế thừa từ thiết kế sớm nhất của định dạng, được tái tạo trung thực vì các trình đọc trông đợi nó.

Cái bẫy là tin rằng một PDF được đọc từ trên xuống dưới như mã nguồn. Không phải vậy. Body có thể chứa các đối tượng theo bất kỳ thứ tự nào. Các số đối tượng không cần tuần tự trong tệp, và trình đọc không bao giờ dựa vào việc chúng tuần tự. Chỉ mục có thẩm quyền duy nhất là phần tham chiếu chéo, và cách duy nhất để tìm thấy nó là trailer ở cuối tệp. Một PDF có body hoàn toàn hợp lệ nhưng chỉ một con số sai trong startxref là không đọc được. Một PDF có các đối tượng được ghi theo thứ tự lộn xộn nhưng có bảng tham chiếu chéo đúng thì vẫn ổn. Bản thân vị trí không có ý nghĩa; vị trí được ghi lại mới là tất cả.

Trang này mô tả cấu trúc tệp, không phải nội dung trang. Cách các dấu vết hiện lên trên một trang — luồng nội dung, toán tử đồ họa, hiển thị văn bản — là một chủ đề riêng. Trang này cũng không đề cập đến những gì xảy ra khi một tệp bị thay đổi sau khi được ghi. Đó là phần việc của các bản cập nhật tăng dần, trong đó bộ ghi nối thêm một phần tham chiếu chéo thứ hai và trailer liên kết ngược lại.

NextPDF là một bộ ghi. Hành vi được mô tả ở đây là cách nó tuần tự hóa một tài liệu do chính nó dựng nên. Nó không phải là trình phân tích cú pháp PDF hay công cụ sửa chữa đa năng. Nó không hứa hẹn đọc, tái dựng hay cứu vãn một tệp tùy ý của bên thứ ba có bảng tham chiếu chéo bị hỏng. Sự bảo đảm này hẹp và có chủ ý. Các tệp mà NextPDF ghi ra có các offset khớp nhau, vì chúng được đo, không phải được dự đoán.

Tại sao cần số thế hệ nếu tệp mới luôn dùng 0? Số thế hệ tồn tại để tái sử dụng đối tượng qua các lần cập nhật. Một tệp vừa được ghi có mọi đối tượng ở thế hệ 0. Các thế hệ khác chỉ xuất hiện khi một tệp đã được cập nhật tăng dần và một số đối tượng được tái sử dụng.

Hai đối tượng có thể có cùng một số không? Trong một phần tham chiếu chéo đơn lẻ thì không. Qua các lần cập nhật tăng dần, một tệp trên thực tế có thể chứa nhiều bản sao của cùng một số đối tượng. Mục tham chiếu chéo gần đây nhất sẽ thắng. Đó là chủ đề của trang tiếp theo.

Thứ tự đối tượng trong tệp có quan trọng với đầu ra không? Không. NextPDF ghi các đối tượng theo một thứ tự xác định để có các bản dựng tái lập được, nhưng một trình đọc phân giải mọi thứ thông qua phần tham chiếu chéo, nên thứ tự vật lý không mang ý nghĩa về mặt ngữ nghĩa.

  • Đối tượng gián tiếp — một đối tượng được đánh số trong body, viết dưới dạng N G obj … endobj, trong đó N là số đối tượng và G là số thế hệ.
  • Tham chiếu gián tiếp — một con trỏ tới một đối tượng gián tiếp, viết là N G R.
  • Bảng tham chiếu chéo (xref) — chỉ mục từ số đối tượng sang byte offset. Trong PDF 2.0, đây thường là một luồng tham chiếu chéo (/Type /XRef) thay vì bảng văn bản cổ điển 20 byte mỗi mục.
  • Trailer — dictionary ở cuối một phần tham chiếu chéo, nêu /Root (document catalog) và /Size, được tìm thấy thông qua offset startxref.
  • Luồng đối tượng — một đối tượng luồng mà bản thân nó chứa các đối tượng gián tiếp khác (được nén chung); các thành viên không có obj/endobj và có thế hệ bằng không.
  • Document catalog — đối tượng được nêu bởi /Root; điểm vào của cây trang và mọi thứ khác trong tài liệu.