Bài viết kỹ thuật

PDF không có Từ điển Trang (Pages Dictionary): Những ý nghĩa đối với việc phân tích cú pháp

Từ điển Catalog của PDF có đúng một khóa điều hướng bắt buộc: /Pages. Khóa đó phải trỏ tới một đối tượng gián tiếp (indirect object) có kiểu là /Pages, và đối tượng này đến lượt nó lại chứa mảng /Kids cùng với tổng số trang /Count. Lấy đi con trỏ đó và không một trình đọc tuân thủ chuẩn nào có thể định vị được một trang duy nhất trong tệp. ISO 32000-1 §7.7.2 cực kỳ rõ ràng về điểm này: Catalog phải có một mục /Pages, và đối tượng được tham chiếu phải có kiểu /Pages. Các tệp vi phạm yêu cầu này không chỉ đơn thuần là không tuân thủ (non-conforming); chúng bị hỏng về mặt cấu trúc theo một cách mà hầu hết các trình phân tích cú pháp (parsers) đều xử lý rất kém

Thông số kỹ thuật thực sự nói gì

Một tệp PDF tuân thủ tối thiểu có ít nhất ba đối tượng. Đối tượng 1 là Catalog, đối tượng 2 là gốc Pages, và đối tượng 3 trở đi là các từ điển Page riêng lẻ. Catalog trỏ tới gốc Pages; gốc Pages liệt kê các phần tử con của nó trong /Kids; mỗi Page lại mang theo một tham chiếu ngược /Parent. Toàn bộ chuỗi được thiết kế theo dạng hai chiều, nhờ vậy một trình phân tích cú pháp có thể bắt đầu từ bất kỳ đầu nào và duyệt qua (traverse) đến bất kỳ trang nào trong thời gian O(log n) đối với các cây cân bằng (balanced trees)

% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj

3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj

4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

Cây Pages có thể được lồng vào nhau (nested). Một tài liệu với hàng nghìn trang thường nhóm các trang thành các đối tượng nút trung gian (intermediate node objects) cũng mang kiểu /Pages, mỗi đối tượng có /Kids riêng của mình và một /Count phản ánh cây con (subtree) bên dưới nó. /Count của nút gốc luôn luôn bằng với tổng số trang. Số lượng đó chính là thứ mà các trình xem hiển thị trong trường số trang trước khi chúng phân tích cú pháp một trang nào đó, bởi vì việc đọc một số nguyên từ đối tượng 2 thì rẻ hơn rất nhiều so với việc đi dọc (walking) toàn bộ cây

Một tệp thiếu-Pages trông như thế nào

Các tệp thiếu từ điển Pages thường có nguồn gốc từ các trình tạo PDF (PDF generators) viết trực tiếp các đối tượng trang mà không lắp ráp chúng thành một cái cây, hoặc từ sự cố hỏng hóc làm xóa đi nút gốc nhưng vẫn để lại nguyên vẹn các đối tượng Page dạng lá (leaf Page objects). Catalog trong một tệp như vậy hoặc là thiếu hoàn toàn khóa /Pages, hoặc là giữ một tham chiếu đến một đối tượng không còn tồn tại trong bảng tham chiếu chéo

% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj

% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj

25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj

Một trình phân tích cú pháp tuân theo thông số kỹ thuật sẽ đọc Catalog, cố gắng phân giải /Pages, không tìm thấy gì (hoặc một tham chiếu chết), và báo lỗi hoặc báo cáo là không có trang nào. Điều mà nó không được phép làm là tiếp tục xử lý như thể tệp có không trang và âm thầm báo thành công (silently succeed); điều đó tạo ra một đầu ra trống rỗng, trông có vẻ đúng đối với các công cụ tự động nhưng lại sai hoàn toàn đối với bất kỳ con người nào mở nó ra

Tại sao các trình phân tích cú pháp bị sập

Hầu hết các trình phân tích cú pháp PDF phân bổ bảng trang nội bộ của chúng tại thời điểm tải dựa trên giá trị /Count từ gốc Pages. Khi gốc đó vắng mặt, trình phân tích cú pháp hoặc là đọc ra giá trị 0, không phân bổ gì cả, và sau đó tham chiếu ngược một con trỏ null (dereferences a null pointer) ở lần đầu tiên bất kỳ đoạn mã nào yêu cầu trang 1, hoặc nó sẽ đọc ra dữ liệu rác và phân bổ một bộ đệm sai lệch một cách khủng khiếp. Không có kết quả nào là nhẹ nhàng cả. Lỗi vi phạm truy cập tại 0x008E5D78 hiển thị trong nhật ký sập (crash logs) khi xử lý một tệp như vậy chính xác là do điều này: một hành động tham chiếu ngược con trỏ null bên trong đường dẫn truy cập-trang, được kích hoạt bởi sự vắng mặt của cấu trúc mà trình phân tích cú pháp giả định là sẽ luôn ở đó

Giả định thiết kế cơ bản là hợp lý. Đại đa số các tệp PDF tồn tại đều có một từ điển Pages. Các trình phân tích cú pháp bỏ qua bước kiểm tra sự tồn tại để tiết kiệm một vài chỉ thị lệnh không phải là liều lĩnh; chúng đang tối ưu hóa cho trường hợp phổ biến. Những tệp trừng phạt sự tối ưu hóa đó đủ hiếm để mã sản xuất có thể không bao giờ bắt gặp một tệp nào cho đến khi nó thực sự xảy ra, tại thời điểm đó vụ sập vừa có thể tái tạo (reproducible) lại vừa gây hoang mang nếu người kỹ sư chưa đọc §7.7.2

Phục hồi khi không có cây Pages

Nếu một trình phân tích cú pháp phải xử lý những tệp này thay vì từ chối chúng, việc phục hồi diễn ra theo một con đường có thể đoán trước: quét từng đối tượng gián tiếp trong bảng tham chiếu chéo, thu thập những đối tượng có /Type /Page, và sắp xếp chúng theo số thứ tự đối tượng. Thứ tự của số đối tượng không được đảm bảo khớp với thứ tự đọc trong thông số kỹ thuật, nhưng trong thực tế, các trình tạo (generators) bỏ qua cây Pages thường có xu hướng phát sinh (emit) các trang một cách tuần tự, do đó thứ tự số đối tượng thường là đúng đắn

Bản thân việc kiểm tra là rẻ (ít tốn kém). Trước khi đi theo con trỏ /Pages của Catalog, hãy xác nhận xem con trỏ đó có tồn tại hay không, liệu nó có phân giải (resolves) tới một đối tượng thực sự hay không, và /Type của đối tượng được phân giải có bằng /Pages hay không. Nếu bất kỳ một trong ba điều kiện đó thất bại, hãy chuyển sang (fall through) quá trình quét tuyến tính. Quét tuyến tính chậm hơn duyệt cây đối với các tài liệu lớn, bởi vì nó đọc từng phần đầu (header) của đối tượng thay vì đi theo một con đường cân bằng, nhưng nó hoạt động được, và đối với một tệp đã bị lỗi định dạng (malformed) sẵn rồi thì tính chính xác quan trọng hơn tốc độ

Một trường hợp ranh giới (edge case) mà quá trình quét tuyến tính không tự động giải quyết được: thứ tự trang. Không có mảng /Kids để định nghĩa trình tự, thứ tự "đúng" là không xác định theo thông số kỹ thuật. Thứ tự số đối tượng là giá trị mặc định thực dụng; nếu tệp đó đủ quan trọng để được xử lý cẩn thận, thì việc kiểm tra xem các đối tượng Page có mang một tham chiếu /StructParents rõ ràng hoặc các tham chiếu chú thích (annotation references) ám chỉ một chuỗi đọc (reading sequence) hay không là điều đáng để bỏ thêm công sức

Những ý nghĩa đối với trình tạo PDF

Đối với bất kỳ ai đang viết một trình tạo PDF thay vì một trình phân tích cú pháp, bài học này rất hẹp: hãy luôn phát sinh gốc Pages trước khi đóng tệp. Một Catalog không có mục /Pages sẽ không phải là một tệp PDF hợp lệ theo bất kỳ bản sửa đổi (revision) nào của thông số kỹ thuật. Các trình tạo xây dựng các đối tượng trang một cách tức thời (on the fly) và lắp ráp cây ở bước hoàn tất (cách tiếp cận mà hầu hết các trình ghi dạng luồng (streaming writers) sử dụng) đều ổn miễn là quá trình hoàn tất thực sự chạy. Chế độ lỗi (failure mode) phổ biến là một ngoại lệ hoặc lệnh kết thúc sớm làm hủy quá trình ghi trước khi trailer được hoàn thành, để lại một tệp mở được ở một số trình xem (những trình có giải pháp phục hồi (recovery heuristics)) và thất bại ở những trình xem khác (những trình không có)

PDF/A và PDF/UA áp đặt những ràng buộc bổ sung lên cây trang vượt ra ngoài những gì mà thông số kỹ thuật cơ sở yêu cầu, nhưng cả hai đều không nới lỏng yêu cầu /Pages. Một công cụ xác thực (validator) chuyên kiểm tra sự tuân thủ đối với chuẩn ISO 19005 hoặc ISO 14289 sẽ bắt lỗi thiếu từ điển Pages như là một lỗi vi phạm thông số kỹ thuật cơ sở trước cả khi nó kịp chạm tới các quy tắc dành riêng cho hồ sơ (profile-specific rules)