Xuất một tài liệu từ Microsoft Word hoặc Excel bằng tính năng Save as PDF và tệp trên đĩa, thường xuyên hơn là một tệp tham chiếu lai (hybrid-reference). Nó mang thông tin tham chiếu chéo hai lần: một lần dưới dạng bảng chiều rộng cố định cổ điển từng kết thúc mọi PDF cho đến phiên bản 1.4, và một lần dưới dạng luồng tham chiếu chéo được nén mà hầu hết tài liệu thực sự phụ thuộc vào. Một khóa trailer duy nhất, /XRefStm, khâu hai dạng xem lại với nhau, và việc một công cụ có nhìn thấy toàn bộ tài liệu hay không phụ thuộc vào việc nó có theo dõi khóa đó không
Bài viết này xem xét các tệp lai từ phía tiêu thụ: các byte ở cuối tệp trông như thế nào, hai dạng xem bị lệch nhau như thế nào trong quá trình chỉnh sửa, và cách một pipeline Delphi có thể phát hiện và định tuyến các đầu vào lai. Cách một trình tải hợp nhất các dạng xem, và tại sao thứ tự không thể thương lượng, là chủ đề của bài viết HotPDF của chúng tôi về tải tệp PDF tham chiếu lai; bài viết này là về việc nhận biết bố cục ngay từ đầu
Tại sao xuất từ Office ghi chỉ mục hai lần
PDF 1.5 đã giới thiệu hai tính năng làm thay đổi hình dạng của tệp: các luồng tham chiếu chéo (cross-reference streams), lưu trữ chỉ mục đối tượng dưới dạng dữ liệu nhị phân nén thay vì bảng văn bản thuần túy, và các luồng đối tượng (object streams), đóng gói nhiều đối tượng nhỏ vào một vùng chứa nén Flate. Một trình ghi sử dụng chúng sẽ tạo ra các tệp nhỏ hơn, nhưng trình đọc PDF 1.4 không thể mở kết quả, vì các cấu trúc mà nó làm khóa, từ khóa xref và từ điển trailer, đã biến mất
ISO 32000-1 §7.5.8.4 định nghĩa sự thỏa hiệp. Tệp tham chiếu lai ghi cả hai: một bảng tham chiếu chéo cổ điển định vị các đối tượng mà một trình đọc cũ phải tiếp cận, trong đó có catalog và cây trang, và một luồng tham chiếu chéo lập chỉ mục mọi thứ khác. Các đối tượng được gộp vào luồng đối tượng được đánh dấu là rảnh (free) trong bảng cổ điển, vì vậy trình đọc 1.4 sẽ bỏ qua chúng mà không phàn nàn; vị trí thực của chúng chỉ tồn tại trong luồng. Sau đó, trailer cổ điển mang khóa /XRefStm giữ byte offset (độ lệch byte) của luồng đó. Trình xem cũ không bao giờ đọc khóa này và hiển thị tệp từ dạng xem bảng. Trình xem hiện đại theo dõi khóa và thấy toàn bộ tài liệu. Word và Excel đã phát ra bố cục chính xác này trong nhiều năm, đó là lý do tại sao các tệp lai không phải là trường hợp ngoại lệ kỳ lạ mà chiếm phần lớn những gì các pipeline doanh nghiệp nhận được
Phần đuôi của tệp lai trông như thế nào
Bố cục dễ hiểu nhất từ các byte. Dưới đây là phần đuôi của một tệp lai nhỏ, với các độ lệch được làm ngắn lại; trong bản xuất Office thực tế, giá trị /XRefStm thường là một độ lệch lớn gần cuối tệp. Thứ tự đọc là cách duyệt ngược từ cuối (tail-first) được mô tả trong tổng quan của chúng tôi về cấu trúc tệp PDF: tìm %%EOF, đọc startxref, nhảy đến bảng
% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...
xref % classic section: what startxref points at
0 4
0000000000 65535 f % slot 0: head of the free list, always present
0000000017 00000 n % object 1: the catalog, visible to any reader
0000000000 65535 f % object 2: marked free -- lives in an object stream
0000000000 65535 f % object 3: same; only the stream view locates it
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % byte offset of the cross-reference stream
>>
startxref
7164 % byte offset of the 'xref' keyword above
%%EOF
Hai chi tiết trong bản trích xuất (dump) này mang toàn bộ cơ chế. Thứ nhất, startxref trỏ đến phần cổ điển, có chủ đích: đó là địa chỉ mà một trình đọc cũ phải tìm đến. Luồng tham chiếu chéo chỉ có thể được tiếp cận thông qua khóa /XRefStm bên trong từ điển trailer, vì vậy trình phân tích cú pháp không bao giờ tìm kiếm khóa đó sẽ không bao giờ biết luồng này tồn tại. Thứ hai, đối tượng 2 và 3 là những lời nói dối vô hại. Bảng cổ điển tuyên bố chúng là rảnh, nhưng chúng là những đối tượng thực sự nằm bên trong vùng chứa nén; việc đánh dấu rảnh là điều giữ cho trình đọc 1.4 không vấp phải các mục nhập mà nó không thể sử dụng. Một người tiêu thụ chỉ tin tưởng vào dạng xem cổ điển sẽ kết luận rằng phần lớn tài liệu này không tồn tại
Hai dạng xem bị lệch nhau như thế nào
Một tệp lai vừa xuất từ Word có sự nhất quán nội bộ: cả hai dạng xem đều mô tả cùng một tài liệu, mỗi dạng xem trong phạm vi được khai báo của nó. Vấn đề bắt đầu khi tệp được chỉnh sửa bởi một công cụ chỉ hiểu một trong hai dạng xem. Hãy xem xét một tiện ích đóng dấu thêm vào một bản cập nhật gia tăng (incremental update) kiểu cổ điển: các đối tượng mới, một phần xref mới, một chuỗi /Prev đến phần trước, và một trailer mới. Nếu trailer đó loại bỏ khóa /XRefStm, dạng xem luồng sẽ bị mồ côi; nếu nó sao chép giá trị cũ về phía trước, dạng xem luồng vẫn mô tả tài liệu như trước khi chỉnh sửa. Dù bằng cách nào, hai chỉ mục hiện không thống nhất về những gì tệp chứa
Tệp kết quả có một dấu hiệu lỗi đặc trưng: các đối tượng có thể nhìn thấy trong một dạng xem bị thiếu hoặc cũ kỹ trong dạng xem kia. Trình đọc phân giải qua dạng xem luồng sẽ tìm thấy phiên bản trước chỉnh sửa của đối tượng được cập nhật, hoặc hoàn toàn không có mục nhập nào cho đối tượng được nối thêm. Trình đọc ở dạng xem bảng thấy phần chỉnh sửa nhưng mất dấu các đối tượng được nén mà chỉ có luồng mới xác định được vị trí. Trong thực tế, điều này xuất hiện dưới dạng các trường biểu mẫu tồn tại trong một trình xem và biến mất trong trình xem khác, các chú thích mà một lần đóng dấu dường như đã bị xóa, hoặc việc tra cứu hạ cánh trên một đối tượng hoàn toàn sai
Điều làm cho việc gỡ lỗi các tệp này trở nên tốn kém là Adobe Acrobat thường mở chúng mà không phàn nàn gì: khi chỉ mục không đồng nhất với các byte, nó âm thầm xây dựng lại dữ liệu tham chiếu chéo bằng cách quét các tiêu đề đối tượng, vì vậy người tạo ra tệp bị hỏng không thấy có gì sai. Lỗi chỉ xuất hiện sau đó, khi tệp đến tay một đối tượng tiêu thụ nghiêm ngặt, trình xác thực kiểm tra trước (preflight validator), dịch vụ ký kết, hoặc công việc nhập liệu lưu trữ, nơi tin tưởng vào cấu trúc được khai báo và báo cáo các đối tượng bị thiếu hoặc tham chiếu chéo không khớp. "Nó mở tốt trong Acrobat" là cách mà hầu như mọi phiếu yêu cầu hỗ trợ (ticket) về mất đồng bộ lai đều bắt đầu
Phát hiện tệp lai bằng Delphi thuần túy
Việc phân loại đầu vào không cần thư viện PDF. Khóa /XRefStm chỉ có thể xuất hiện bên trong từ điển trailer cổ điển, và trailer đang hoạt động nằm trong khoảng vài kilobyte cuối cùng của tệp, vì thông số kỹ thuật yêu cầu %%EOF xuất hiện gần cuối vật lý. Việc đọc một cửa sổ phần đuôi có giới hạn và tìm kiếm nó là đủ để phân loại:
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
Ba kết quả tương ứng với ba bố cục. Tệp chỉ dùng bảng cổ điển có trailer nhưng không có /XRefStm: False. Tệp hoàn toàn sử dụng các luồng tham chiếu chéo hoàn toàn không có từ khóa trailer, các khóa trailer của nó nằm trong từ điển luồng: cũng trả về False, một cách chính xác, bởi vì một tệp như vậy là tệp được nén, không phải tệp lai. Chỉ bố cục có chỉ mục kép mới trả về True
Đối với việc sử dụng trong môi trường sản xuất, có hai biện pháp củng cố xứng đáng với những dòng mã bổ sung. Phân tích số nguyên phía sau /XRefStm, tìm kiếm đến độ lệch đó và xác nhận rằng một đối tượng luồng với /Type /XRef thực sự nằm ở đó; tệp bị cắt cụt có thể mang khóa trong khi luồng đã biến mất, trường hợp này thuộc về một vấn đề khác so với tệp lai khỏe mạnh. Và coi kích thước cửa sổ như một tham số: 2 KB bao hàm đầu ra thông thường của Office, nhưng từ điển trailer lớn bất thường có thể đẩy từ khóa ra ngoài phạm vi, và việc mở rộng cửa sổ tốt hơn là tình cờ khai báo tệp là cổ điển
Định tuyến các tệp lai qua một pipeline Delphi
Khám phá mang lại cho bạn quyết định định tuyến. Đối với các tệp chỉ được đọc, hiển thị hoặc xác thực, hãy sử dụng trình tải giải quyết được cả hai dạng xem, sau đó xác minh hành vi thay vì số byte. PDFium Component phân tích cú pháp chuỗi /XRefStm trong khi tải, do đó bảng đối tượng mà mã của bạn nhìn thấy là bảng đã hợp nhất, và các kiểm tra được mô tả trong bài viết của chúng tôi về xác thực đối tượng và luồng tham chiếu chéo vẫn áp dụng mà không thay đổi. Nếu tệp lai bị mất đồng bộ hỏng nặng đến mức từ chối tải, engine báo cáo thông qua tập hợp lỗi của nó, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY và FPDF_ERR_PAGE, với FPDF_ERR_FORMAT là lỗi do hỏng cấu trúc sinh ra. Tuy nhiên, đừng quá dựa dẫm vào tín hiệu đó: PDFium theo thiết kế rất khoan dung và âm thầm xây dựng lại hầu hết các tệp không nhất quán, vì vậy việc tải thành công chứng tỏ tệp có thể phục hồi, chứ không phải hai dạng xem của nó thống nhất. Việc kiểm tra tính nhất quán có ý nghĩa là so sánh những gì quá trình duyệt toàn bộ đối tượng tìm thấy với những gì /Size của trailer khai báo
Đối với các tệp mà pipeline của bạn sửa đổi, chính sách an toàn nhất là không để chúng làm tệp lai nữa. Việc tải tiếp nối bởi quá trình lưu toàn bộ thông qua HotPDF sẽ viết lại tài liệu với tham chiếu chéo tự nhất quán duy nhất dưới một hình thức: không /XRefStm, không có dạng xem thứ hai để mất đồng bộ, mọi đối tượng được sở hữu bởi chính xác một mục chỉ mục. Sự chuẩn hóa đó là điều bạn muốn trước khi nhập liệu vào lưu trữ, trước một trình tạo ảnh raster (RIP) hoặc dịch vụ ký kết nghiêm ngặt ở các bước sau, và sau bất kỳ chỉnh sửa nào áp dụng cho đầu vào lai. Nó hoạt động vì trình tải đã hợp nhất các dạng xem một cách chính xác trong quá trình đưa vào, một cơ chế mà bài viết về tham chiếu lai của HotPDF hướng dẫn chi tiết
Một lớp tệp nên để yên là các tài liệu được ký kỹ thuật số. Việc viết lại toàn bộ sẽ di chuyển mọi byte, làm vô hiệu hóa bất kỳ chữ ký nào được tính toán trên các phạm vi gốc. Thay đổi đối với một tệp lai đã ký phải được đưa vào như một bản cập nhật gia tăng thích hợp để duy trì cả hai dạng xem; một tệp chỉ cần đọc nên được truyền qua mà không chạm tới. Việc chuẩn hóa là dành cho các tệp bạn sở hữu; các tệp đã ký thì bạn chỉ có thể nối thêm
Các tệp PDF tham chiếu lai không phải tệp sai định dạng; chúng là cầu nối tương thích của riêng định dạng, và các ứng dụng Office sẽ tiếp tục tạo ra chúng miễn là trình đọc PDF 1.4 vẫn còn tồn tại trong cơ sở cài đặt. Một pipeline có thể phát hiện khóa /XRefStm, xác thực tài liệu đã hợp nhất bằng PDFium Component, và tạo lại đầu ra chỉ mục đơn sạch sẽ bằng HotPDF Component sẽ xử lý chúng đúng như bản chất: các đầu vào thông thường với một biển chỉ dẫn phụ trong trailer