Mở một tệp PDF do Microsoft Word hoặc Excel tạo ra, cuộn qua các trang và không thấy gì bất thường. Tải nó vào chương trình Delphi, đọc lại số trang và con số hiển thị chính xác. Nhưng khi lưu lại tài liệu với tính năng mã hóa được bật, tác vụ sẽ thất bại với lỗi EListError, hoặc kết quả đầu ra sẽ mở ra kèm cảnh báo bảng tham chiếu chéo (cross-reference) bị hỏng. Tệp tin chưa bao giờ bị lỗi cấu trúc. Đây là một tệp tham chiếu hỗn hợp (hybrid-reference), và chính cấu trúc cho phép một trình xem mười lăm năm tuổi mở được tệp lại là cấu trúc đánh bại bộ tải dừng đọc quá sớm.
Đây là một trong những cách phổ biến nhất khiến một quy trình xử lý PDF đã vượt qua mọi bài kiểm tra nội bộ gặp phải một tệp mà nó không thể xử lý khứ hồi (round-trip). Tất cả các đầu vào trước đó đều được tạo nội bộ, vì vậy chúng không bao giờ là hỗn hợp. Tệp hỗn hợp đầu tiên xuất hiện vào ngày khách hàng chuyển tiếp một hóa đơn được xuất từ một bảng tính.
Word và Excel thực sự ghi những gì
ISO 32000-1 mô tả định dạng tham chiếu hỗn hợp trong mục §7.5.8.4. Một ứng dụng muốn sử dụng các tính năng của PDF 1.5 như luồng đối tượng (object streams), trong khi vẫn cho phép trình đọc PDF 1.4 mở được tệp, sẽ ghi thông tin tham chiếu chéo hai lần. Có một bảng tham chiếu chéo cổ điển, chứa các hàng ASCII có độ rộng cố định vốn kết thúc mọi tệp PDF lên đến phiên bản 1.4, và có một luồng tham chiếu chéo lập chỉ mục phần còn lại. Phần đuôi (trailer) của phần cổ điển mang một mục nhập /XRefStm có giá trị là độ lệch byte (byte offset) của luồng đó.
Sự phân chia nhiệm vụ này là có chủ đích. Các đối tượng mà một trình đọc cũ cần truy cập, bao gồm danh mục (catalog) và cây trang (page tree), có thể được định vị từ bảng cổ điển. Các đối tượng được đưa vào luồng đối tượng nén được đánh dấu là tự do trong bảng cổ điển, với một mục nhập kiểu f, để trình đọc 1.4 bỏ qua thẳng qua chúng và không bao giờ gặp phải cấu trúc mà nó không thể phân tích cú pháp. Vị trí thực của chúng chỉ nằm trong luồng tham chiếu chéo. Dấu hiệu nhận biết của một tệp như vậy là phần đuôi của nó: một phần cổ điển ngắn, thường không có gì hơn ngoài xref theo sau là tiêu đề phần phụ 0 0, có phần đuôi chỉ vào /XRefStm nơi chứa dữ liệu phục hồi thực tế.
Tại sao số lượng trang chính xác không chứng minh được gì
Bởi vì danh mục và cây trang có thể truy cập được từ bảng cổ điển một cách có chủ ý, một bộ tải chỉ đọc bảng đó sẽ tìm thấy /Root, duyệt qua cây trang và báo cáo đúng số lượng trang. Mọi thứ trình đọc cũ cần đều hiển thị đầy đủ, nên tệp có vẻ bình thường. Các đối tượng bị thiếu là các đối tượng được đóng gói vào các luồng đối tượng: các từ điển trường AcroForm, các phần tử cấu trúc tagged-PDF, và phần đuôi dài của các từ điển nhỏ vốn không cần hiển thị với trình xem cũ.
Bạn sẽ không nhận ra khoảng trống đó cho đến khi có thứ gì đó chạm vào các đối tượng đó, và một thao tác lưu lại đầy đủ (full resave) sẽ chạm vào tất cả chúng. Việc duyệt qua tài liệu để mã hóa lại hoặc ghi lại chính xác là thao tác yêu cầu kiểm tra tuần tự từng số hiệu đối tượng, đó là lý do tại sao triệu chứng lại xuất hiện vào thời điểm lưu tệp thay vì thời điểm tải tệp, cách xa nguyên nhân thực sự gây ra lỗi.
Bẫy nằm ở bộ phát hiện nhìn thấy xref là dừng
Cách đơn giản nhất để xác định cách một tệp được lập chỉ mục là đi theo startxref và kiểm tra các byte đầu tiên mà nó trỏ tới. Từ khóa xref có nghĩa là bảng cổ điển; một đối tượng luồng (stream object) có nghĩa là một luồng tham chiếu chéo. Cách kiểm tra đó là chính xác đối với bất kỳ tệp nào chỉ tuân theo một cơ chế. Nhưng nó sai đối với tệp hỗn hợp, có startxref hướng tới phần cổ điển với mục đích duy nhất là đáp ứng các trình đọc cũ, trong khi /XRefStm ở phần đuôi của phần đó mới là nơi phần lớn tài liệu thực sự được lập chỉ mục. Một bộ phát hiện trả về "classic" khi gặp xref đầu tiên sẽ không bao giờ đọc /XRefStm, và mọi đối tượng chỉ tồn tại trong luồng đó sẽ trở nên vô hình.
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
Với bộ phát hiện thoát sớm được áp dụng, việc tải tệp có vẻ ổn nhưng việc lưu lại mới là lúc các đối tượng bị thiếu hiển thị. Giải pháp không phải là đọc thêm nhiều byte ở phần đầu; mà là nhận diện phần đuôi hỗn hợp và đi theo /XRefStm trước khi quyết định tệp đã được xử lý xong.
Thứ tự hợp nhất là điều bắt buộc
Sau khi cả hai chỉ mục đã được đọc, chúng chỉ có thể được kết hợp theo một chiều duy nhất. Luồng tham chiếu chéo phải được hợp nhất trước, với các mục nhập cổ điển được điền xung quanh nó. Lý do là sự tinh ranh có chủ ý ở trung tâm định dạng này. Một tệp hỗn hợp đánh dấu các đối tượng nén của nó là tự do trong bảng cổ điển để các trình đọc cũ bỏ qua chúng. Một bộ tải tuân theo chính sách "ưu tiên cái nhìn thấy đầu tiên" và đọc bảng cổ điển trước sẽ ghi nhận các số hiệu đối tượng đó là tự do, sau đó loại bỏ các mục nhập luồng thực tế xác định vị trí của chúng, vì các vị trí đó đã bị chiếm. Đảo ngược thứ tự sẽ giúp các mục nhập kiểu 2 từ luồng, mỗi mục đại diện cho một số hiệu luồng đối tượng kèm theo một chỉ mục, giành được các vị trí mà chúng vốn thuộc về, và các mục nhập cổ điển sẽ được xếp xung quanh chúng.
Quy tắc tương tự giúp ngăn ngừa việc một phiên bản cũ hơn khôi phục lại một đối tượng đã bị xóa. Các bản cập nhật lũy tiến (incremental updates) liên kết ngược thông qua /Prev, và một mục nhập tự do kiểu 0 là một lính gác (sentinel) báo hiệu rằng một phần mới hơn đã thu hồi một số hiệu đối tượng. Một phần xuất hiện sau nhưng cũ hơn trong chuỗi liên kết không được phép ghi đè lên lính gác đó bằng một vị trí đã lỗi thời. Việc coi mục nhập đầu tiên nhìn thấy là có thẩm quyền đối với các điểm đánh dấu tự do sẽ giúp đối tượng đã xóa giữ nguyên trạng thái bị xóa; nếu xử lý thiếu cẩn thận, chính lịch sử của tệp sẽ khôi phục lại nội dung mà phiên bản mới nhất đã loại bỏ.
Điều này có ý nghĩa gì đối với HotPDF
Trình xử lý tự động giải quyết các tệp tham chiếu hỗn hợp cho bạn, và nó thực hiện việc này trên mọi đường dẫn cần phân tích cú pháp dữ liệu tham chiếu chéo. Tải một tài liệu bằng LoadFromFile hoặc LoadFromStream, thực hiện các thay đổi và gọi SaveLoadedDocument; hoặc chạy một thao tác xử lý một lần (one-shot) như EncryptFile để đọc tệp đầu vào và ghi tệp đầu ra. Trong cả hai trường hợp, quá trình phục hồi đều đọc /XRefStm, hợp nhất phần luồng trước các mục nhập cổ điển và giải quyết các đối tượng nằm trong luồng trước khi lệnh ghi liệt kê chúng. Đường dẫn mã hóa AES-256 là nơi vấn đề xuất hiện đầu tiên, vì việc mã hóa tài liệu yêu cầu ghi lại mọi đối tượng và do đó đòi hỏi mọi đối tượng phải được xác định vị trí sẵn trước đó.
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Chi tiết quan trọng cần lưu ý nằm ở phía trước API. Các tệp tin đến từ Word, Excel, PowerPoint và một danh sách dài các quy trình chuyển đổi "Save as PDF" thường ở định dạng hỗn hợp, vì vậy một bộ tải bạn chỉ thử nghiệm với kết quả đầu ra của chính công cụ tạo lập của bạn có thể không bao giờ gặp lỗi này trong quá trình kiểm thử. Hãy bổ sung vào bộ dữ liệu kiểm thử của bạn các tài liệu được xuất từ các ứng dụng Office thực tế, chứ không chỉ các tệp do mã nguồn của chính bạn tạo ra.
Kiểm tra tệp tin mà bạn nghi ngờ
Hai phương pháp kiểm tra có thể nhanh chóng làm rõ vấn đề. Hãy mở tệp bằng trình xem mã Hex và đọc các byte sau mục startxref cuối cùng; một tệp hỗn hợp sẽ hiển thị một phần cổ điển ngắn với từ điển phần đuôi (trailer dictionary) chứa /XRefStm. Or so sánh số lượng đối tượng mà một phân tích cú pháp đầy đủ báo cáo với số hiệu đối tượng cao nhất mà /Size khai báo trong phần đuôi. Một khoảng cách lớn có nghĩa là các đối tượng đang ẩn trong các luồng mà bộ tải chưa mở, đây chính là sự thiếu hụt dẫn đến lỗi ở bước lưu tệp sau đó.
Quy trình phía bộ ghi, cách các luồng đối tượng và tham chiếu chéo nén được tạo ra từ đầu, được trình bày trong bài viết của chúng tôi về luồng đối tượng và cập nhật lũy tiến. Khi tệp hỗn hợp được nói đến có dung lượng rất lớn, các kỹ thuật tải trong hướng dẫn Direct File API cho luồng công việc PDF lớn sẽ cho phép bạn kiểm tra tệp mà không cần tải toàn bộ vào bộ nhớ. Cả hai kỹ thuật đều kết hợp tự nhiên với tính năng phục hồi được mô tả ở đây, vốn được phát hành như một phần của HotPDF Component dành cho Delphi và C++Builder bên cạnh các API tải, chỉnh sửa, mã hóa và ký số được đề cập ở những bài viết khác trên blog này.