Về bản chất, một tệp PDF là một tập hợp các đối tượng trỏ vào lẫn nhau. Lột bỏ phần nén, phần ghi sổ tham chiếu chéo (cross-reference bookkeeping), và các byte offset, và những gì còn lại là một đồ thị (graph): một tập hợp nhỏ các giá trị có kiểu (typed values), được kết nối với nhau bằng các tham chiếu, có gốc tại một đối tượng duy nhất mà trình đọc biết cách tìm kiếm. Mọi thứ mà PDF có thể thể hiện, từ một đoạn văn bản đến một font chữ được nhúng cho đến một chữ ký số, đều được xây dựng từ tám kiểu đối tượng nguyên thủy (primitive object types) và quy tắc cho phép một đối tượng tham chiếu đến một đối tượng khác. Hãy học những điều đó, và phần còn lại của định dạng này sẽ được đọc giống như một sự lắp ghép chứ không còn là một điều bí ẩn
Đây là lớp logic (logical layer) của PDF, được định nghĩa trong khoản 7.3 của tiêu chuẩn ISO 32000-1, và nó nằm ở một cấp độ ngay phía trên bố cục tệp vật lý (header, phần thân, bảng tham chiếu chéo, và trailer, đây là chủ đề riêng biệt trong tổng quan kỹ thuật về cấu trúc tệp PDF). Mô hình logic chính là ý nghĩa của những byte đó một khi đã được phân tích cú pháp. Một trình xem sẽ đọc tệp từ dưới lên để tìm trailer, đi theo nó đến phần gốc (root), và từ đó tài liệu sẽ mở ra thành các đối tượng tham chiếu tới các đối tượng. Đây là phần mà bạn sẽ lập luận (reason about) khi gỡ lỗi một trang bị lỗi định dạng, khi viết một trình phân tích cú pháp, hoặc khi tin tưởng giao cho một thư viện lắp ráp một tài liệu
Tám kiểu đối tượng, và không có gì khác
PDF định nghĩa chính xác tám kiểu đối tượng cơ bản. Mọi giá trị trong một tài liệu đều là một trong số đó, đây chính là điều giúp cho định dạng này vẫn có thể kiểm soát được (tractable) mặc dù phạm vi bao phủ của nó rất rộng
Kiểu Boolean là các từ khóa true và false. Chúng bật và tắt các cờ (flags), chẳng hạn như liệu một chú thích có được in ra hay không
Kiểu số (Numbers) có hai dạng mà thông số kỹ thuật xử lý như là một kiểu duy nhất: các số nguyên (integers) như 42 và các số thực (reals) như 3.14 hoặc -0.002. PDF không có ký hiệu số mũ, vì vậy bạn sẽ không bao giờ nhìn thấy 1e6 trong một tệp tuân thủ chuẩn. Các tọa độ, kích thước font chữ, và các góc xoay đều là các con số
Chuỗi (Strings) chứa các chuỗi byte, được viết trong ngoặc đơn, (Hello), hoặc trong ngoặc nhọn ở định dạng hệ thập lục phân, <48656C6C6F>. Cả hai ký hiệu này đều mã hóa nội dung giống hệt nhau; mã thập lục phân (hex) là lối thoát (escape hatch) cho những byte có vẻ khó xử khi nằm bên trong ngoặc đơn. Chuỗi mang theo văn bản, nhưng trước hết chúng là các byte, điều này có ý nghĩa quan trọng vào thời điểm bạn xử lý bất cứ thứ gì vượt ra ngoài hệ ASCII
Tên (Names) là các mã thông báo nguyên tử (atomic tokens) được bắt đầu bằng một dấu gạch chéo: /Type, /Pages, /MediaBox. Tên không phải là một chuỗi; nó là một định danh, được sử dụng như là một khóa từ điển (dictionary key) hoặc một giá trị liệt kê (enumerated value), và hai tên chỉ bằng nhau nếu chúng khớp với nhau từng byte một. Dấu gạch chéo là cú pháp, không phải là một phần của tên. Điều này thường làm những người mới học bị vấp váp khi họ coi /Times-Roman và chuỗi (Times-Roman) là có thể hoán đổi cho nhau; bản thân định dạng này thì không nghĩ vậy
Mảng (Arrays) là các danh sách có thứ tự, không đồng nhất nằm trong ngoặc vuông: [0 0 612 792] là một hình chữ nhật trang (page rectangle), và một mảng có thể kết hợp các kiểu dữ liệu một cách tự do, bao gồm cả các tham chiếu đến các đối tượng khác. Từ điển (Dictionaries) là công cụ chuyên chở (workhorse). Được viết giữa << và >>, một từ điển ánh xạ các khóa tên tới các giá trị thuộc bất kỳ kiểu nào, và hầu như mọi cấu trúc có ý nghĩa trong PDF, page, catalog, font, annotation, đều là một từ điển với một khóa /Type khai báo xem nó là gì
Luồng (Streams) là các từ điển với một cái đuôi gồm các byte thô (raw bytes) nằm giữa các từ khóa stream và endstream. Từ điển mô tả các byte này (độ dài của chúng, và bất kỳ bộ lọc nào chẳng hạn như FlateDecode dùng để nén chúng), và các byte mang theo khối lượng dữ liệu cồng kềnh (bulky payload): các hướng dẫn nội dung trang, các chương trình font chữ được nhúng, các hình ảnh. Luồng là nơi PDF đặt bất cứ thứ gì quá lớn hoặc quá nhị phân để có thể nằm thẳng hàng (inline)
Kiểu thứ tám là đối tượng rỗng (null object), từ khóa null. Nó là một giá trị thực sự, khác biệt với việc một khóa bị vắng mặt. Một mục từ điển được đặt thành null sẽ được xử lý như thể nó không hiện diện, và một tham chiếu phân giải tới một đối tượng không tồn tại cũng sinh ra null thay vì một lỗi. Hành vi khoan dung này là có chủ ý: nó cho phép một tệp bị hỏng suy thoái từ từ (degrade) thay vì từ chối mở. Không có kiểu thứ chín; mọi thứ mà PDF thể hiện đều bắt nguồn từ cách tám kiểu này kết hợp với nhau
Các giá trị trực tiếp, các đối tượng gián tiếp, và các tham chiếu
Bất kỳ kiểu nào trong số tám kiểu đó đều có thể xuất hiện theo hai cách. Một đối tượng trực tiếp (direct) được viết tại chỗ (in place), giống như số 612 bên trong mảng MediaBox. Một đối tượng gián tiếp (indirect) được gán một danh tính để các đối tượng khác có thể trỏ vào nó: hai số nguyên, một số thứ tự đối tượng và một số thế hệ, bao bọc phần định nghĩa trong obj và endobj:
12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
Đây là đối tượng 12, thế hệ 0, một từ điển font chữ. Ở bất kỳ nơi nào khác trong tệp, một đối tượng khác sẽ tham chiếu đến nó bằng một tham chiếu gián tiếp (indirect reference): cùng hai con số đó theo sau là từ khóa R, 12 0 R. Tham chiếu là một con trỏ. Khi từ điển tài nguyên của một trang có nội dung là /Font << /F1 12 0 R >>, nó định danh đối tượng 12 là font chữ đằng sau cái tên tài nguyên /F1, mà không sao chép định nghĩa của font chữ vào trang đó
Số thế hệ tồn tại là để phục vụ cho các quá trình xóa bỏ và tái sử dụng. Khi một đối tượng được giải phóng và khe của nó được tái sử dụng, số thế hệ sẽ tăng lên để một tham chiếu cũ 12 0 R không thể phân giải đến đối tượng thuê mới (new tenant) của khe 12. Các tệp mới viết gần như luôn mang thế hệ 0, nhưng một tệp bị chỉnh sửa nhiều có thể mang các con số cao hơn, và một trình phân tích cú pháp phớt lờ số thế hệ cuối cùng sẽ đọc sai đối tượng
Tính gián tiếp (indirection) chính là điều làm cho PDF trở nên hiệu quả và có thể chỉnh sửa được. Một font chữ, hình ảnh, hoặc không gian màu có thể được định nghĩa một lần và được tham chiếu từ một trăm trang. Một thay đổi nhỏ có thể được nối (appended) vào như một bản sửa đổi mới (new revision) để thay thế một đối tượng duy nhất thay vì viết lại toàn bộ tệp. Bảng tham chiếu chéo (cross-reference table) là chỉ mục (index) biến một số đối tượng thành một độ lệch byte (byte offset), nhờ vậy trình đọc sẽ nhảy thẳng đến 12 0 obj mà không cần quét, nhưng đó là một sự tối ưu hóa về mặt vật lý. Về mặt logic, tất cả những gì bạn cần biết là 12 0 R có nghĩa là "đối tượng được nhận dạng là 12 0."
Catalog: nơi mọi tài liệu bắt đầu
Việc phân giải các tham chiếu phải bắt đầu từ đâu đó, và cái "đâu đó" ấy chính là mục /Root của trailer, nó trỏ vào catalog tài liệu (document catalog): phần gốc của đồ thị đối tượng, một từ điển mang /Type /Catalog. Trình đọc chạm đến nó đầu tiên bởi vì trailer được tìm thấy đầu tiên, và từ đó mọi phần khác của tài liệu đều có thể tiếp cận được bằng cách đi theo các tham chiếu
Catalog chỉ mang hai mục bắt buộc một cách nghiêm ngặt: /Type của nó, và /Pages, một tham chiếu gián tiếp đến phần gốc của cây trang. Phần còn lại là tùy chọn và chúng mô tả các hành vi trên toàn tài liệu (document-wide) chứ không phải nội dung: /Outlines trỏ vào cây dấu trang (bookmark tree), /Names chứa các cây tên được gắn khóa (keyed) bằng chuỗi, /Metadata tham chiếu đến một luồng siêu dữ liệu XMP, còn /PageMode và /PageLayout đề xuất cách thức mà một trình xem nên mở tài liệu. Không có cái nào trong số đó là cần thiết để kết xuất một trang; chúng cấu hình trải nghiệm xoay quanh các trang. Cấu trúc dấu trang, siêu dữ liệu, và chú thích (annotation) treo lơ lửng khỏi catalog được trình bày trong bài viết về Siêu dữ liệu, Dấu trang, và Chú thích của PDF
Biểu đồ dưới đây cho thấy phần thân đối tượng nằm ở đâu trong tệp bao quanh nó. Catalog và cây trang trú ngụ bên trong phần thân đó như những đối tượng gián tiếp thông thường; header, bảng tham chiếu chéo, và trailer xung quanh chúng là bộ giàn giáo vật lý giúp cho trình đọc có thể định vị được chúng

Cây trang: một hệ thống phân cấp trang cân bằng
Từ /Pages, tài liệu phân nhánh thành cây trang, nơi sự lựa chọn của PDF khi dùng một đồ thị (graph) thay cho một danh sách phẳng (flat list) phát huy tác dụng. Các trang không được lưu trữ dưới dạng một chuỗi đơn giản; chúng treo lơ lửng từ một cái cây mà các nút bên trong của nó là các nút của cây trang (page tree nodes) (/Type /Pages) và các lá của nó là các đối tượng trang (page objects) (/Type /Page). Một nút bên trong liệt kê các phần tử con của nó trong một mảng /Kids và ghi lại, trong /Count, có bao nhiêu trang dạng lá đang tồn tại bên dưới nó. Mọi nút ngoại trừ phần gốc đều mang một tham chiếu ngược /Parent hướng lên trên, vì vậy cây này có thể đi theo cả hai hướng
2 0 obj % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj
3 0 obj % a leaf page
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F1 12 0 R >> >>
/Contents 5 0 R >>
endobj
4 0 obj % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj
Ở đây đối tượng 2 là phần gốc, với ba trang bên dưới nó: trang lá 3, cộng thêm hai trang nữa có thể tiếp cận được thông qua nút bên trong 4. /Count của phần gốc là 3 phải bằng tổng số các lá bên dưới nó, và một con số đếm không khớp với cấu trúc thực tế là một kiểu hỏng hóc phổ biến của một tệp được chỉnh sửa bằng tay. Mục đích của cái cây này là tính cục bộ của quyền truy cập (locality of access). Một trình đọc khi mở trang 900 của một tài liệu nghìn trang sẽ không duyệt qua 900 đối tượng; nó chỉ đi xuống một số ít các nút, bởi vì một cái cây được định dạng tốt sẽ giữ được tính nông (shallow) và cân bằng (balanced). Việc xây dựng một cái cây như vậy bằng tay đủ độ phức tạp (fiddly) để đáng cho bạn phải quan sát từ đầu đến cuối, giống như những gì mà bài viết hướng dẫn về xây dựng một tài liệu PDF từ con số không đã thực hiện
Cái cây này giành được phần giữ lại (keep) thứ hai của nó thông qua tính kế thừa (inheritance). Một vài thuộc tính trang (page attributes), /Resources, /MediaBox, /CropBox, và /Rotate, có thể được đặt trên một nút bên trong (interior node) và được bỏ đi ở các trang riêng lẻ, để rồi chúng sẽ kế thừa giá trị từ tổ tiên gần nhất. Đặt /MediaBox một lần ở phần gốc và mọi chiếc lá sẽ nhận được cùng một kích thước trang mà không cần lặp lại điều đó; một trang nào cần phải khác đi thì sẽ khai báo giá trị của riêng nó. Đây là nơi duy nhất trong mô hình đối tượng mà ý nghĩa của một giá trị phụ thuộc vào vị trí của một đối tượng trong cái cây, chứ không chỉ phụ thuộc vào nội dung của chính nó
Một trang lá thực sự chứa những gì
Một đối tượng trang là điểm giao thoa (join point) giữa mô hình cấu trúc và nội dung nhìn thấy được. Mục /Contents của nó tham chiếu đến một hoặc nhiều luồng nội dung, tức các toán tử vẽ (drawing operators) làm nhiệm vụ vẽ (paint) văn bản và đồ họa lên trang. Từ điển /Resources của nó liệt kê tên các font chữ, hình ảnh, và các không gian màu mà những toán tử đó phụ thuộc vào, mỗi mục nhập là một tham chiếu gián tiếp đến một đối tượng được chia sẻ trên khắp các trang. /MediaBox cung cấp một hình chữ nhật cho trang (page rectangle) tính bằng point (1/72 inch), và các mục như /Rotate cùng /CropBox sẽ điều chỉnh cách nó được trình bày
Sự phân chia công việc đó chính là toàn bộ mô hình được thu nhỏ. Từ điển trang (page dictionary) là cấu trúc: các mục và các tham chiếu có kiểu nói lên trang đó là gì và nó vẽ bằng cái gì. Luồng nội dung là những hướng dẫn: một blob tách biệt, có thể nén được sẽ ra lệnh về cách vẽ. Font chữ đằng sau /F1 là một tài nguyên chia sẻ, được định nghĩa một lần và được trỏ vào bất cứ nơi nào nó được sử dụng. Từ điển, luồng, và tham chiếu cùng hợp tác để kết xuất một trang, và các mô hình tương tự này nhân rộng ra toàn bộ tài liệu. Các toán tử luồng nội dung bên trong blob đó được bao hàm riêng biệt đối với phần văn bản và font chữ và phần đồ họa cùng các yếu tố hình ảnh
Tại sao mô hình này lại đáng để biết
Hầu hết các nhà phát triển chỉ gặp gỡ mô hình đối tượng này khi có thứ gì đó bị hỏng: một trang kết xuất ra màn hình trắng trơn bởi vì tham chiếu /Contents của nó bị lơ lửng, văn bản biến thành các hình hộp bởi vì một tài nguyên font chữ chưa bao giờ được nhúng, một công cụ báo cáo /Count không khớp với những trang mà nó có thể tìm thấy. Mỗi vấn đề trong số đó đều là một tuyên bố (statement) về cái đồ thị (graph), và việc đọc thẳng trực tiếp vào cái đồ thị này thì tốt hơn hẳn so với việc phỏng đoán. Tám kiểu cùng với quy tắc tham chiếu là một bộ từ vựng đủ nhỏ để có thể lưu giữ trong đầu bạn, và một khi bạn nhìn thấy một PDF dưới dạng các đối tượng trỏ vào các đối tượng, thì những tệp bị lỗi định dạng (malformed files) sẽ không còn là một màn sương mù mờ đục (opaque) nữa
Mặc dù vậy, viết mô hình bằng tay hiếm khi là một lựa chọn đúng đắn trừ khi dùng cho mục đích học tập. Việc giữ cho các khoảng offset tham chiếu chéo, các số thế hệ, các số đếm trên cây trang, và các độ dài luồng luôn nhất quán xuyên suốt các phiên bản chỉnh sửa chính là loại hình công việc ghi chép mà một thư viện được tạo ra để xử lý. Trong sản xuất, một thư viện phát triển PDF trưởng thành sẽ quản lý đồ thị đối tượng trong khi để lại cho bạn việc tư duy dựa trên các trang và nội dung. Dẫu vậy, việc hiểu rõ về mô hình vẫn mang lại giá trị: bạn sẽ hiểu được những gì mà thư viện đó đang xây dựng ở tầng bên dưới, và tại sao nó lại làm như thế