Gộp hay tách một tệp PDF hai gigabyte theo cách hiển nhiên khiến bạn trả giá hai lần cùng lúc: thời gian đồng hồ và không gian địa chỉ. Cách hiển nhiên là nạp từng đầu vào, làm việc, rồi ghi đầu ra. Chính khâu nạp là chỗ vỡ trận. Một kho ảnh quét chuyển từ 300 lên 600 DPI thì độ phân giải theo chiều dài tăng gấp đôi và dung lượng trên đĩa tăng khoảng gấp bốn, nên đúng cái công việc lắp ghép đã xử lý êm các tệp 400 MB suốt cả năm bắt đầu giằng xé bộ nhớ ngay khi một đầu vào vượt một gigabyte, thường là trong lúc chẳng làm gì hơn ngoài đếm số trang. Nhiệm vụ chưa bao giờ khó hơn. Mở, đếm, chọn khoảng, nối lại, chỉ có thế. Chỉ là việc nạp trọn cây đối tượng thôi làm mặc định hợp lý ở cỡ tệp đó. PDF Library for Delphi, thư viện PDF của losLab dành cho Delphi và C++Builder, đáp lại điều này bằng tầng Direct Access: một họ hàm mang tiền tố DA tựa trên bộ đọc streaming duyệt bảng tham chiếu chéo ngay tại chỗ thay vì dựng cả tài liệu trong bộ nhớ
Bộ nhớ đi đâu trong một lượt nạp đầy đủ
Nạp một tệp PDF theo lối "bình thường" nghĩa là phân tích xref, phân giải mọi đối tượng gián tiếp thành một cây trong bộ nhớ, giải mã các object stream, rồi đấu nối cây trang, font và annotation thành những đối tượng bạn thao tác được. Với luồng công việc chỉnh sửa thì đó là đánh đổi đúng đắn. Với việc gộp, tách và soi xét thì phần lớn là lãng phí. Một kho ảnh quét 30.000 trang có thể chứa hàng triệu đối tượng gián tiếp, còn một công việc tách chỉ cần đọc vài trăm cái: các nút trang trong khoảng được yêu cầu, cộng thêm những gì các nút đó tham chiếu tới
Tầng Direct Access lật ngược mô hình. DAOpenFile và DAOpenFileReadOnly phân tích trailer cùng xref, tức vài kilobyte ở đuôi tệp, rồi trả về một handle tệp. Các đối tượng chỉ được lấy về khi có lệnh gọi cần tới. Hệ quả thực tế là mở một tệp nhiều gigabyte mất chừng bằng thời gian mở một tệp nhỏ, và bộ nhớ bám theo phần bạn chạm tới chứ không theo phần tệp chứa
Dò một tệp khổng lồ mà không nạp nó
Mẫu hình dưới đây lấy từ chính bài đo hiệu năng tệp lớn của thư viện: mở ở chế độ chỉ đọc, đặt câu hỏi, đóng lại. Không có cây tài liệu nào từng tồn tại
var
Lib: TPDFlib;
Handle, Pages: Integer;
begin
Lib := TPDFlib.Create;
try
Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
if Handle = 0 then
raise Exception.Create('Direct access open failed');
Pages := Lib.DAGetPageCount(Handle);
Writeln('pages : ', Pages);
Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
Lib.DACloseFile(Handle);
finally
Lib.Free;
end;
end;
Chế độ chỉ đọc đáng được ưu tiên bất cứ khi nào có thể: nó cho phép khâu tiếp nhận chạy trong lúc các tiến trình khác đang giữ tệp, và nó ghi rõ chủ đích. Một khâu dò lỡ gọi phải hàm làm thay đổi dữ liệu sẽ hỏng ngay lập tức thay vì làm hỏng kho lưu trữ
PageRef là handle đối tượng, không phải số trang
Sai lầm phổ biến nhất với API DA là truyền số trang vào chỗ mà hàm mong đợi một PageRef. Gần như mọi lệnh gọi DA theo từng trang đều nhận một handle tham chiếu tới đối tượng trang chứ không phải số trang: DAExtractPageText, DARenderPageToFile, DARotatePage và DACapturePage đều chờ một ref. Bạn lấy được nó bằng cách chuyển đổi con số dành cho con người qua DAFindPage:
PageRef := Lib.DAFindPage(Handle, 250); // số trang -> handle đối tượng
if PageRef <> 0 then
begin
Text := Lib.DAExtractPageText(Handle, PageRef, 0);
Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;
Truyền thẳng con số 250 vào thay thế không hề gây lỗi. Nó địa chỉ hóa bất cứ đối tượng nào tình cờ nằm sau giá trị handle đó, mà ngày lành thì hỏng lộ liễu còn ngày xấu thì trích ra văn bản của sai trang vào một tài liệu gửi tới khách hàng. Nếu bạn bọc tầng DA trong mã dịch vụ của mình, hãy làm cho bước chuyển đổi không thể bỏ qua: nhận số trang ở ranh giới, gọi DAFindPage ngay lập tức, và bên trong chỉ truyền ref
Gộp hàng trăm tệp bằng một danh sách có tên
Với hai tệp thì MergeFiles(First, Second, Output) là đủ. Việc lắp ghép theo lô mở rộng tốt hơn qua danh sách tệp: đăng ký các đầu vào dưới một tên danh sách, rồi gộp cả danh sách trong một lượt
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');
// Kiểm chứng kết quả theo cách rẻ nhất: lại dùng direct access
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);
Họ hàm gộp có ba biến thể, và khác biệt không chỉ nằm ở tốc độ. MergeFileListFast bỏ qua việc giữ lại cây cấu trúc; MergeFileListStrict áp chế độ nghiêm ngặt; bản không hậu tố là mặc định cân bằng. Quy tắc vận hành rút ra: nếu bất kỳ đầu vào nào là Tagged PDF mà cấu trúc hỗ trợ tiếp cận của nó phải sống sót, trường hợp hiển nhiên là mọi thứ làm ra cho PDF/UA, hãy chọn bản mặc định hoặc bản Strict, vì bản Fast lặng lẽ vứt bỏ cây cấu trúc. Với các kho ảnh quét thuần không gắn thẻ, Fast là hiệu năng miễn phí. Hãy quyết theo từng pipeline chứ không theo tâm trạng của lập trình viên, và ghi lại biến thể đã dùng vào nhật ký công việc
Tách mà không nạp: trích theo khoảng
Việc tách theo cùng triết lý không nạp. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) kéo một khoảng trang thẳng từ tệp sang tệp, với danh sách khoảng kiểu '1-500', '501-1000', hoặc các lựa chọn ngăn bằng dấu phẩy, và nguồn không bao giờ trở thành cây tài liệu. Khi một tài liệu vốn đã được nạp vì lý do khác, ExtractPageRanges tạo ra một tài liệu mới trong bộ nhớ từ tài liệu hiện tại, còn CopyPageRanges kéo các khoảng sang từ một tài liệu đã nạp khác theo ID. Để tách từng sao kê ra khỏi các luồng in gộp, dạng tệp sang tệp mới là thứ giữ cho một đầu vào 4 GB không bao giờ phình lên trong RAM
Những tệp nói dối về hình thể của chính mình
Các pipeline tệp lớn gặp tệp hỏng với tần suất mà pipeline tệp nhỏ chẳng bao giờ thấy, đơn giản vì đầu vào đi qua nhiều hệ thống hơn. Hai dạng hỏng đáng được xử lý tường minh
Thứ nhất, header bị dịch. Các cổng thư điện tử và bộ xếp hàng in đôi khi chèn thêm byte vào đầu tệp PDF, khiến dấu %PDF không còn nằm ở offset 0 và mọi offset xref trong tệp đều sai đi cùng một lượng. Bộ đọc streaming phát hiện điều này và phơi nó ra (DAShiftedHeader ở tầng phẳng, ShiftedHeader trên TSmartPDFReader), rồi bù trừ trong lúc đọc. Phép tính offset tự chế thường thì không, và đó là lý do "chạy tốt với mọi tệp chúng tôi tạo ra, hỏng với tệp từ khách hàng X" là triệu chứng kinh điển
Thứ hai, bảng tham chiếu chéo hỏng. DACopyFile(InputFileName, OutputFileName, PageCount) truyền toàn bộ tệp sang một bản sao mới trong khi dựng lại xref, và trả về số trang như sản phẩm phụ. Chạy nó như một khâu chuẩn hóa đặt trước một bên tiêu thụ khó tính ở hạ nguồn sẽ biến cả một nhóm lỗi phân tích chập chờn thành một bước sửa chữa đoán trước được. Còn khi chính các chỉnh sửa của bạn cần lưu lại, DAAppendFile ghi chúng dưới dạng incremental update, nối thêm một revision mới thay vì ghi lại hàng gigabyte, nhờ đó chi phí lưu tỉ lệ với phần thay đổi chứ không với cả tệp
Chi tiết khi giao hàng: linearization và bố cục ghép
Hai năng lực liền kề khép lại một pipeline tệp lớn. Khi đầu ra đã lắp ghép được phục vụ qua HTTP để xem ngay trong trình duyệt, LinearizeFile tổ chức lại tệp cho việc truyền theo byte-range để trang đầu hiện ra trước khi phần còn lại của gói 500 MB tải xong. Hãy chạy nó ở khâu cuối cùng, sau mọi thao tác gộp, vì bất kỳ chỉnh sửa nào về sau cũng phá vỡ trạng thái linearized. Và khi các gói cần được ghép bố cục chứ không chỉ nối đuôi nhau, chẳng hạn một trang bìa đóng dấu phía sau mỗi bản sao kê hoặc hai trang nguồn dồn lên một tờ đầu ra, DACapturePage biến một trang bất kỳ thành khuôn mẫu tái dùng được để DADrawCapturedPage đặt lên trang đích tại một hình chữ nhật tùy ý, vẫn không cần nạp toàn bộ tài liệu nguồn nhiều gigabyte
Giới hạn và những gì vẫn chỉ đọc
Bản thân định dạng hết chỗ từ rất lâu trước khi Direct Access hết chỗ. Offset là Int64 xuyên suốt tầng DA, nên trần thật sự là dung lượng đĩa còn trống và trường offset xref 10 chữ số của bảng tham chiếu chéo cổ điển (loại không phải stream). Các kho ảnh quét nhiều gigabyte trên thực tế chẳng có gì đặc biệt, và bộ nhớ vẫn bị chặn trên bất kể cỡ tệp, vì đối tượng chỉ được đọc khi có lệnh gọi hỏi tới
Hai câu hỏi xuất hiện đủ thường xuyên để trả lời thẳng. Gộp theo đường mặc định mang cấu trúc tài liệu đi cùng, nên bookmark và liên kết sống sót; biến thể Fast mới là bản đánh đổi cây cấu trúc lấy tốc độ, và đó chính là toàn bộ lý do để dành riêng nó cho các đầu vào không gắn thẻ. Thói quen an toàn là mở đầu ra đã gộp, duyệt phần outline của nó, và kiểm tra ngẫu nhiên vài liên kết nội bộ trước khi giao. Còn về chuyện chỉnh sửa: có một vùng trung dung hữu ích giữa việc dò chỉ đọc và một lượt nạp đầy đủ. Các thao tác ở mức trang làm việc trực tiếp trên handle, trong đó có DARotatePage, DAMovePage và DAHidePage, cùng với việc đọc trường form, và DAAppendFile lưu bền những chỉnh sửa ấy dưới dạng một revision nối thêm. Việc chỉnh sửa ở mức nội dung, tức bất cứ thao tác nào ghi lại các toán tử vẽ bên trong một trang, thì vẫn thuộc về tầng tài liệu đầy đủ
Bài viết liên quan
Nếu đầu ra đã gộp của bạn phải giữ được khả năng tiếp cận, phần nền về cây cấu trúc được bàn trong bài viết về khả năng tiếp cận của Tagged PDF, nơi giải thích chính xác thứ mà biến thể gộp Fast sẽ vứt bỏ. Để moi nội dung ra khỏi những khoảng bạn đã tách, xem hướng dẫn trích xuất văn bản, ảnh và font
Danh sách hàm Direct Access đầy đủ đi kèm thư viện; các phiên bản và bản dùng thử nằm trên trang sản phẩm PDF Library for Delphi