HotPDF ghi các file PDF tuyến tính hóa, bố cục mà Acrobat gọi là Fast Web View, qua thuộc tính LinearizeOutput trên THotPDF. Đặt nó trước BeginDoc khiến HotPDF sắp lại đồ thị object hoàn chỉnh sao cho một trình đọc nhận biết dải byte có thể hiển thị trang một sau khi chỉ tải phần đầu của file, thay vì phải tải toàn bộ tài liệu trước. Cơ chế này là ISO 32000-1 Phụ lục F
Lý do điều này quan trọng khá đời thường. Một PDF bình thường đặt bảng cross-reference của nó ở cuối, nên trình xem phải đọc tới byte cuối cùng trước khi biết bất cứ thứ gì nằm ở đâu. Đưa cho trình duyệt một báo cáo scan 200 trang và người dùng nhìn vào biểu tượng xoay tròn suốt cả quá trình tải, dù thứ duy nhất họ muốn chỉ là trang 1. Tuyến tính hóa khắc phục điều đó bằng cách trả giá tại thời điểm ghi. Bài này nói cụ thể về đường ghi đó, việc phân chia, vòng lặp đo lường và các giới hạn cứng; để có nền tảng khái niệm về những gì Fast Web View mang lại, bài giải thích về tuyến tính hóa PDF và Fast Web View trước đó đã phủ phần nền tảng đó
Bố cục tuyến tính hóa thực sự đảm bảo điều gì?
Một file tuyến tính hóa là một PDF bình thường với một thứ tự vật lý cực kỳ cụ thể, và mọi đảm bảo nó mang lại đến từ thứ tự đó chứ không phải từ bất kỳ kiểu object mới nào. HotPDF phát ra các phần theo thứ tự Phụ lục F quy định: dictionary tham số tuyến tính hóa nằm trong 1024 byte đầu tiên, một bảng cross-reference sớm, các object mức tài liệu, hint stream chính, trang đầu tiên và các object riêng của nó, rồi tới các trang còn lại, rồi tới các object dùng chung, rồi tới mọi thứ khác, và cuối cùng là bảng cross-reference chính
Việc phân chia được suy ra, không phải khai báo. HotPDF duyệt đồ thị tham chiếu từ mỗi object trang và ghi lại, với mỗi object gián tiếp, có bao nhiêu trang với tới nó và trang nào với tới trước tiên. Một object chỉ được dùng bởi đúng một trang trở thành riêng của trang đó. Một object được nhiều trang với tới trở thành dùng chung. Catalog, cộng với bất cứ thứ gì nó tham chiếu dưới /ViewerPreferences, /OpenAction, /Threads và /AcroForm, cộng với dictionary mã hóa khi bảo vệ đang bật, tạo thành nhóm mức tài liệu phải đứng trước mọi thứ khác. Các node cây trang được giữ lại có chủ đích để chúng không làm ô nhiễm phần trang đầu tiên
Dictionary tham số mang những con số mà một trình đọc cần trước khi nó đọc bất cứ thứ gì khác: /L cho tổng độ dài file, /H cho offset và độ dài của hint stream, /O cho số hiệu object của trang đầu tiên, /E cho byte nơi phần trang đầu tiên kết thúc, /N cho số trang và /T cho offset của mục bảng cross-reference chính. Mỗi con số trong số đó đều là một offset byte vào một file chưa tồn tại tại thời điểm bạn cần ghi chúng
Vì sao các offset trong bảng hint phải hội tụ?
Vì các con số trong dictionary tham số mô tả chính file chứa chúng, và thay đổi bất kỳ con số nào cũng thay đổi file. Đó là khó khăn cốt lõi của một trình ghi tuyến tính hóa, và đó là lý do HotPDF đo lường lặp đi lặp lại thay vì ghi một lần. Mở rộng /T từ 6 chữ số lên 7 chữ số thì dictionary tham số lớn thêm một byte; header lớn thêm; mọi object dịch chuyển; bảng cross-reference chính di chuyển; /T giờ cần một giá trị khác. Bố cục phải đạt tới một điểm cố định trước khi bất kỳ byte thực sự nào của đầu ra được ghi chính thức
HotPDF xử lý điều này bằng một vòng lặp giới hạn số lần. Đầu tiên nó serialize mọi object vào một stream đếm ghi lại độ dài mà không giữ byte, để mỗi object có một kích thước đã serialize đã biết. Sau đó nó chạy một lượt bố cục gán offset cho nhóm mức tài liệu, hint stream, nhóm trang đầu tiên, các nhóm trang sau, nhóm dùng chung và phần còn lại, và báo cáo nơi bảng cross-reference chính sẽ nằm. Kết quả đó được đưa trở lại làm đầu vào cho lượt tiếp theo. Vòng lặp bị giới hạn ở tám lần thử, và việc không hội tụ sẽ raise một exception thay vì tạo ra một file với các offset sai trông có vẻ hợp lý
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
Hai chi tiết giữ cho vòng lặp không quay cuồng. Dictionary tham số được ghi vào một khe cố định 384 byte, đệm bằng khoảng trắng, nên sự lớn lên của chính nó không bao giờ có thể làm mất ổn định bố cục; nếu văn bản dictionary từng vượt quá khoản dự trù đó, HotPDF raise thay vì âm thầm dịch chuyển mọi thứ. Và sau khi hội tụ, HotPDF chạy thêm một lượt bố cục xác nhận và kiểm tra lại độ dài hint stream, vì bản thân hint stream mã hóa các offset chỉ được biết đến sau khi bố cục đã ổn định. Cái được từ toàn bộ việc đo lường này là HotPDF không bao giờ đệm một bản sao thứ hai của tài liệu: một khi các offset đã cố định, các object được serialize thẳng vào stream đích, với một assertion tại mỗi ranh giới đoạn để đảm bảo byte đã ghi khớp với offset đã hứa
Bật tính năng này từ Delphi
Bề mặt API là một Boolean duy nhất, và yêu cầu duy nhất của nó là bạn phải đặt nó trước khi việc sinh bắt đầu. LinearizeOutput mặc định là False, và lượt bố cục chạy khi tài liệu được ghi, nên gán nó sau EndDoc không có tác dụng gì
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
Một điều cần lưu ý khi triển khai quan trọng hơn mọi thứ ở phía code. Tuyến tính hóa chỉ có lợi khi lớp truyền tải hỗ trợ yêu cầu HTTP range. Phục vụ cùng một file từ một endpoint stream toàn bộ, hoặc từ một cấu hình CDN bỏ qua Range, và bạn chỉ mua được một đường ghi chậm hơn cùng một file lớn hơn mà không có lợi ích nào người dùng nhìn thấy. Hãy kiểm tra máy chủ trước khi kiểm tra code
Vì sao tuyến tính hóa ghi đè UseXRefStream và UseObjectStreams?
Vì trình ghi tuyến tính hóa cần mỗi object có offset byte riêng có thể định vị trực tiếp, và cả hai tính năng đó đều lấy đi điều đó. Vì vậy HotPDF phát ra các bảng cross-reference dạng văn bản truyền thống và các object gián tiếp không đóng gói bất cứ khi nào LinearizeOutput được bật, ngay cả khi bên gọi cũng đặt UseXRefStream hoặc UseObjectStreams. Đây là một sự ghi đè có chủ đích, không phải một xung đột bạn phải tự giải quyết
Lý do bắt nguồn từ các bảng hint. Một bảng hint mô tả nơi một phần trang bắt đầu và nó dài bao nhiêu, để một trình đọc có thể yêu cầu chính xác dải đó. Một object được đóng gói vào một container /ObjStm không có offset độc lập nào cả; nó chỉ tồn tại như một lát cắt bên trong một stream nén khác phải được tải và giải nén như một đơn vị. Nếu bạn đang trông cậy vào object stream để giảm kích thước file, hãy hiểu rằng tuyến tính hóa và nén đang kéo về hai hướng ngược nhau ở đây, và đọc phần đánh đổi trong bài đồng hành về object stream và cập nhật gia tăng trong HotPDF. Cùng căng thẳng đó định hình các file hybrid-reference, vốn tồn tại chính xác để giữ các trình đọc cũ hơn hoạt động song song với các bảng dựa trên stream, như được trình bày trong bài về hybrid cross-reference stream trong PDF do Office tạo
Cũng có một sàn phiên bản. Tuyến tính hóa yêu cầu PDF 1.2 trở lên. Nếu phiên bản đã chọn cũ hơn, HotPDF tự động nâng nó lên, trừ khi StrictVersionLock được đặt, trong trường hợp đó việc ghi raise một exception thay vì âm thầm nâng cấp một tài liệu mà bạn đã cố định có chủ đích
Bức tường 4 GiB, và vì sao HotPDF từ chối thay vì cắt bớt
Bảng hint tuyến tính hóa lưu offset dưới dạng giá trị 32-bit, nên một file tuyến tính hóa không thể định vị bất cứ thứ gì tại hoặc vượt 4 GiB, và HotPDF từ chối đầu ra như vậy với một exception tường minh thay vì ghi một file với offset bị cuộn vòng. Giới hạn này không phải một lựa chọn triển khai của HotPDF; nó là độ rộng của các trường mà Phụ lục F định nghĩa
Việc kiểm tra được áp dụng ở ba nơi, và cả ba đều quan trọng. HotPDF xác thực mỗi object khi độ dài đã serialize của nó đã biết, xác thực độ dài mỗi phần trang khi xây các mục hint, và xác thực độ dài file cuối cùng sau khi bảng cross-reference chính đã được định kích thước. Thất bại sớm chính là toàn bộ mục đích: một bảng hint với một offset bị cắt âm thầm tạo ra một file mở đúng trong một trình xem tải toàn bộ nó nhưng chỉ thất bại với client dùng byte-range mà tuyến tính hóa tồn tại để phục vụ, đó là kiểu thất bại tệ nhất vì trình xem thử nghiệm của bạn không bao giờ tái hiện được nó. Nếu bạn đang tạo đầu ra nhiều gigabyte, tuyến tính hóa không phải công cụ phù hợp, và cách tiếp cận streaming mô tả trong bài về Direct File API cho quy trình PDF lớn mới là hướng cần xem
Phát hiện tuyến tính hóa trên một file đã tải
THotPDF.IsLoadedLinearized báo cáo liệu tài liệu đang được tải hiện tại đã được ghi ở dạng tuyến tính hóa hay chưa, và nó trả lời dựa trên một snapshot chụp trước khi phân tích, không phải từ stream đang sống. HotPDF đọc 1024 byte đầu tiên từ vị trí không của stream nguồn, quét chúng tìm từ khóa obj đầu tiên rồi tìm mục /Linearized với giá trị 1, và lưu cache kết quả boolean
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
Hai ràng buộc trong mô tả đó có tính quyết định. Việc phát hiện không thể dựa vào vị trí stream, vì đến lúc code ứng dụng hỏi câu hỏi này, trình phân tích đã dịch chuyển nó, và nó không thể đọc lại theo yêu cầu vì LoadFromFile giải phóng stream nguồn nội bộ ngay khi việc tải hoàn tất. Vì vậy mới có thiết kế chụp-trước-khi-phân-tích-và-lưu-cache. Lượt quét cũng cố tình rất chặt chẽ về giá trị: chỉ /Linearized 1 hoặc một dạng tương đương về số học với phần thập phân toàn số không mới được chấp nhận, vì một file mà dictionary tham số của nó nói điều gì khác thì không đưa ra lời hứa Phụ lục F
Một cái bẫy record Delphi đáng học lại
Các record cục bộ chứa mảng động khởi tạo các trường được quản lý của chúng và không gì khác, và nếu bạn giữ một trường Count đơn giản bên cạnh mảng, bạn phải tự xóa nó. Điều này đã cắn phần phân chia tuyến tính hóa trong quá trình phát triển, và đó là kiểu lỗi tốn cả một ngày chính vì một nền tảng che giấu nó
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
Trường mảng động được đếm tham chiếu, nên trình biên dịch tự xóa nó về không. Count bên cạnh nó là một số nguyên thường không có đảm bảo như vậy, và một Count chưa khởi tạo gửi lần thêm phần tử đầu tiên tới một chỉ số bất kỳ. Trên Win32, ô ngăn xếp tình cờ giữ giá trị không, lần thêm phần tử rơi vào chỉ số 0, và mọi test đều pass. Trên Win64, cùng đoạn code đó ghi vượt quá cuối mảng. Bài học vượt xa phạm vi tuyến tính hóa: khi một record trộn lẫn trường được quản lý và không được quản lý, hãy gán Default(TRecord) và ngừng suy luận xem trường nào trình biên dịch bao phủ, và đừng bao giờ coi một lần chạy Win32 xanh là bằng chứng rằng việc khởi tạo là đúng
Các thành viên LinearizeOutput và IsLoadedLinearized mô tả ở đây đi kèm trong HotPDF Component tiêu chuẩn cho Delphi và C++Builder; trang sản phẩm mang tài liệu tham chiếu thuộc tính đầy đủ, bao gồm các quy tắc tương tác với cross-reference stream, object stream và khóa phiên bản