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

Giải phẫu một tệp PDF

Mở bất kỳ PDF nào trong một trình soạn thảo văn bản thuần và thứ đầu tiên bạn thấy khá yên tâm: một header %PDF-1.x hoặc %PDF-2.0. Thứ cuối cùng bạn thấy là %%EOF. Mọi thứ nằm giữa hai dòng đó là một cỗ máy nhỏ gọn gàng để tìm các đối tượng theo số. Trang này là một cuộc mổ xẻ. Chúng ta mở tệp ra, gọi tên từng bộ phận, và cho thấy chúng kết nối với nhau như thế nào.

Đây là người bạn đồng hành về cấu trúc của hai trang lân cận. PDF thật sự là gì coi tệp như một đồ thị đối tượng; cập nhật tăng dần bàn về cách nó lớn lên theo thời gian. Trang này bám sát các byte — những vùng vật lý mà một trình phân tích cú pháp đi qua, theo đúng thứ tự chúng nằm trên đĩa.

Bạn gần như không bao giờ cần điều này để dùng một PDF. Bạn cần nó vào ngày một tệp gặp trục trặc. Một tệp mở được trong trình đọc này nhưng không mở được trong trình đọc khác; một trình kiểm định báo “bảng tham chiếu chéo hỏng”; một tài liệu đã ký bỗng nhiên không xác minh được. Không cái nào trong số đó còn là bí ẩn một khi bạn đọc được bản giải phẫu. Chúng là một con số không còn khớp với một vị trí, một vùng nằm sai chỗ, hay một phần đuôi trỏ vào hư không.

Biết bố cục biến “PDF bị hỏng” thành một chẩn đoán mà bạn có thể hành động. Đó là khác biệt giữa nhún vai trước một hộp đen và chỉ đúng vào byte đang nói dối.

Một PDF hợp quy có bốn phần vật lý, theo thứ tự trong tệp này (Spec: ISO 32000-2, §7.5.1):

  1. Một header — một dòng, %PDF-2.0, nêu phiên bản.
  2. Một body — phần lớn của tệp: một chuỗi các đối tượng gián tiếp được đánh số.
  3. Một phần tham chiếu chéo — một chỉ mục từ số đối tượng tới byte offset nơi đối tượng đó nằm. PDF cổ điển dùng một bảng văn bản; PDF 2.0 dùng một luồng xref đã nén.
  4. Một trailer — một dictionary nhỏ nêu điểm vào, theo sau là startxref, một offset, và %%EOF.

Điều bất ngờ: một trình đọc không bắt đầu từ trên cùng. Nó bắt đầu từ dưới cùng, đọc startxref để tìm chỉ mục, và dùng chỉ mục đó để truy cập trực tiếp bất kỳ đối tượng nào. Tệp được ghi từ đầu tới cuối nhưng được đọc từ cuối lên đầu.

Hãy đi qua bốn vùng theo thứ tự, với các byte ngay trước mặt.

Header là một dòng. NextPDF ghi %PDF-2.0, và theo quy ước thêm một dòng chú thích thứ hai gồm các byte có bit cao để các công cụ truyền tệp ngây thơ coi tệp là nhị phân, không phải văn bản. Dòng thứ hai đó là lý do một PDF mở dưới dạng văn bản thuần hiển thị một chút nhiễu ngay sau phiên bản.

Body là nơi tài liệu sống. Mỗi đối tượng gián tiếp là một con số, một thế hệ, từ khóa obj, một giá trị, và endobj (Spec: ISO 32000-2, §7.3.10). Giá trị là một trong vài hình dạng — nhưng hai hình dạng gánh gần như toàn bộ trọng lượng:

  • Một dictionary, << /Key value … >>, là một ánh xạ từ tên sang giá trị. Cây trang, catalog, các bộ mô tả font: tất cả đều là dictionary.
  • Một luồng là một dictionary theo sau bởi stream, một khối byte tùy ý, và endstream. Nội dung trang, font nhúng, và hình ảnh đều là luồng, gần như luôn được nén. (Các bộ lọc của chúng là một câu chuyện riêng, được kể trong luồng và bộ lọc.)

Các đối tượng trỏ tới nhau bằng tham chiếu gián tiếp2 0 R nghĩa là “đối tượng 2, thế hệ 0.” Đó là phần dây nối biến một danh sách phẳng các đối tượng thành một đồ thị.

Phần tham chiếu chéo là phần mà hầu hết mọi người chưa bao giờ hình dung đúng. Ở dạng cổ điển nó là văn bản thuần: từ khóa xref, rồi các phân đoạn gồm các dòng 20 byte cố định chiều rộng (Spec: ISO 32000-2, §7.5.4). Mỗi dòng là một byte offset mười chữ số, một thế hệ năm chữ số, và một cờ duy nhất — n cho đang dùng, f cho trống — được đệm cho đúng hai mươi byte để một trình đọc có thể nhảy tới bất kỳ mục nào chỉ bằng phép số học. PDF 2.0 thay thế phần này bằng một luồng tham chiếu chéo: cùng chỉ mục đó, nhưng nhị phân và được nén bên trong một đối tượng luồng đánh dấu /Type /XRef (Spec: ISO 32000-2, §7.5.8). Nhỏ hơn, và có khả năng mô tả các đối tượng đóng gói bên trong các luồng đối tượng.

Trailer là mục lục của tệp (Spec: ISO 32000-2, §7.5.5). Nó nêu /Root — document catalog, đối tượng duy nhất mà mọi thứ khác treo vào — và /Size, số lượng đối tượng. Rồi đến cái bắt tay khiến việc đọc ngược trở nên khả thi: startxref, một byte offset trên dòng riêng, và %%EOF. Một trình đọc nhảy tới cuối, đọc offset đó, nhảy thẳng tới phần tham chiếu chéo, và lên đường.

  1. HeaderOne line, %PDF-2.0, naming the version. A binary-marker comment usually follows.
  2. BodyNumbered indirect objects — dictionaries and streams — referenced by N G R.
  3. Cross-reference sectionA text table of 20-byte entries, or a compressed /Type /XRef stream in PDF 2.0.
  4. TrailerNames /Root and /Size, then startxref + offset + %%EOF.
  5. Read orderA reader starts at %%EOF, follows startxref to the index, then reaches each object directly.
The four physical regions of a PDF in file order, and the path a reader actually takes through them — starting at the trailer and working inward via the cross-reference section.

Có một vùng thứ năm mà một tệp tồn tại lâu sẽ phát triển thêm: một bản cập nhật tăng dần. Một thay đổi không được ghi tại chỗ. Các đối tượng bị thay đổi, một phần tham chiếu chéo mới, và một trailer mới được nối thêm sau %%EOF đầu tiên, và trailer mới đó mang /Prev — offset của phần tham chiếu chéo trước đó (Spec: ISO 32000-2, §7.5.6). Các phần tạo thành một chuỗi lùi về sau; với bất kỳ số đối tượng nào, mục mới nhất sẽ thắng. Vì các byte gốc không bao giờ di chuyển, phép kiểm tra mật mã trên dải byte mà một chữ ký thực sự bao phủ vẫn còn đúng sau một bản cập nhật. Một bản cập nhật tăng dần về sau vẫn có thể thay đổi những gì một trình kiểm định báo cáo về toàn bộ tài liệu — liệu các thay đổi sau khi ký có được phép hay không, và chữ ký được hiểu là chứng nhận điều gì — nhưng nó không thể làm thay đổi chính các byte đã được ký. Tính chất đó là cả chủ đề của cập nhật tăng dần.

Đây là toàn bộ bản giải phẫu trong một tệp tối giản. Các con số dưới xref là byte offset, và chúng phải chính xác — trỏ lệch một ký tự so với nơi một đối tượng bắt đầu là một trình đọc nghiêm khắc sẽ bỏ cuộc.

%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
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
186
%%EOF

Đọc nó theo cách một trình phân tích cú pháp đọc. Dòng cuối: %%EOF. Phía trên nó: startxref 186, nên nhảy tới byte 186, nơi xref bắt đầu. Bảng nói đối tượng 1 nằm ở byte 9. /Root 1 0 R của trailer trỏ tới đó — catalog — và từ catalog bạn đi theo /Pages tới cây trang và tìm thấy trang duy nhất. Đối tượng 0 luôn là đầu của danh sách trống với thế hệ 65535, một di tích từ thiết kế đầu tiên của định dạng mà mọi trình đọc vẫn còn trông đợi nhìn thấy.

Cái bẫy là đọc một PDF như một câu chuyện — từ trên xuống dưới, theo thứ tự. Nó không phải là một câu chuyện; nó là một chỉ mục với một con trỏ lùi về sau. Các số đối tượng không nhất thiết phải tuần tự trong tệp, các đối tượng có thể xuất hiện theo bất kỳ thứ tự vật lý nào, và một trình đọc không bao giờ dựa vào vị trí của chúng. Bản đồ có thẩm quyền duy nhất là phần tham chiếu chéo, và cách duy nhất để tìm bản đồ đó là offset startxref ở chính cuối tệp.

Hệ quả khiến nhiều người bất ngờ. Một PDF có body hoàn hảo và một chữ số sai trong startxref thì không đọc được — trình đọc không thể tìm thấy chỉ mục. Một PDF có các đối tượng theo thứ tự lộn xộn nhưng có phần tham chiếu chéo đúng thì hoàn toàn ổn. Vị trí vật lý không mang ý nghĩa nào. Vị trí được ghi lại mới mang tất cả ý nghĩa.

Trang này mô tả cấu trúc vật lý, 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 — các toán tử luồng nội dung, hiển thị văn bản, trạng thái đồ họa — là một chủ đề riêng. Nó cũng mô tả một tệp định dạng đúng. Các PDF ngoài đời thực thường hơi lỗi một chút và sống sót chỉ vì các trình đọc khoan dung dựng lại bảng tham chiếu chéo bằng cách quét tìm các từ khóa obj. Việc cứu vãn đó là một hành vi của trình đọc, không phải điều mà định dạng bảo đảm.

Reading and repairing arbitrary third-party PDFs — edition availability
EditionAvailability
CoreNextPDF là một bộ ghi. Nó ghi lại mọi offset từ buffer đầu ra vào đúng thời điểm mỗi đối tượng được phát ra, nên các tệp nó tạo ra có một phần tham chiếu chéo khớp với body theo cách dựng.
ProPhân tích cú pháp, tái dựng, hay sửa chữa một bảng tham chiếu chéo bị hỏng trong một tệp mà NextPDF không ghi nằm ngoài phạm vi trên mọi phiên bản.
EnterpriseĐể kiểm tra cấu trúc của một tệp đã có, hãy dùng một trình phân tích cú pháp hoặc trình kiểm định chuyên dụng; NextPDF bảo đảm tính đúng đắn cho những gì nó ghi, không phải cho những gì nó đọc.

Tại sao một PDF có một dòng đánh dấu nhị phân sau header? Một số công cụ truyền tệp cũ sẽ làm hỏng một tệp mà chúng tưởng là văn bản thuần. Dòng chú thích có bit cao khiến tệp trông rõ ràng là nhị phân, nên nó sống sót qua chuyến đi mà không bị thay đổi.

Luồng xref có phải chỉ là một bảng nhỏ hơn không? Hầu hết là vậy, với một sức mạnh thêm. Ngoài việc được nén, một luồng xref có thể mô tả các đối tượng được lưu bên trong các luồng đối tượng — những mục mà bảng văn bản 20 byte cổ điển không có cách nào diễn đạt.

Tôi có thể có cả một bảng lẫn một luồng trong một tệp không? Một bản sửa đổi đơn lẻ dùng một trong hai. Nhưng một tệp lai có thể ghép một bảng cổ điển cho các trình đọc cũ với một luồng tham chiếu chéo cho các trình đọc mới, để mỗi loại trình đọc tìm thấy một chỉ mục mà nó hiểu.

  • Header — dòng đầu tiên, %PDF-2.0, nêu phiên bản; thường theo sau bởi một chú thích đánh dấu nhị phân.
  • Đối tượng gián tiếp — một đối tượng được đánh số trong body, viết N G obj … endobj, trong đó N là số đối tượng và G là thế hệ.
  • Dictionary — một ánh xạ << /Key value … >> từ tên sang giá trị; hình dạng đối tượng phổ biến nhất.
  • Luồng — một dictionary cộng với một khối byte giữa streamendstream, dùng cho nội dung, font, và hình ảnh.
  • Bảng tham chiếu chéo (xref) — chỉ mục từ số đối tượng sang byte offset; một bảng văn bản 20 byte mỗi mục theo cách cổ điển, một luồng /Type /XRef trong PDF 2.0.
  • Trailer — dictionary nêu /Root/Size, được tìm thấy qua offset startxref ở cuối tệp.
  • Cập nhật tăng dần — các đối tượng bị thay đổi, một phần tham chiếu chéo mới, và một trailer mới được nối thêm sau %%EOF, với /Prev liên kết chuỗi ngược về phần trước.