Bài viết kỹ thuật

Gộp và chia PDF dung lượng Gigabyte trong Delphi với truy cập trực tiếp PDF Library for Delphi

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. DAOpenFileDAOpenFileReadOnly 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

PDF Library for Delphi so sánh việc nạp một PDF cỡ gigabyte vào toàn bộ cây đối tượng trong bộ nhớ với việc mở bằng direct access, nơi việc phân tích dừng ở trailer và xref, còn handle phục vụ các lượt đọc từng đối tượng theo kiểu lazy
Một lần load đầy đủ giải mã mọi indirect object trước khi merge kịp bắt đầu, nên RAM và thời gian mở tăng theo kho lưu trữ. Đường direct access trả về handle dùng được sau khi đọc vài kilobyte và cho mỗi lệnh gọi chỉ kéo đúng những object cần

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, DARotatePageDACapturePage đề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:

PDF Library for Delphi: Luồng dịch số trang sang PageRef cho thấy DAFindPage nạp cho các lượt gọi direct access từng trang, đối lập với một ref số nguyên thô rơi vào đối tượng tùy ý và sinh văn bản trang sai một cách âm thầm
Mỗi lệnh gọi direct access theo trang tiêu thụ một PageRef do DAFindPage tạo ra, không bao giờ là số dành cho người dùng. Bỏ qua bước chuyển đổi đó khiến số nguyên giả làm object id, và văn bản trang sai có thể xuất hiện mà không ai hay
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

PDF Library for Delphi: Quy trình danh sách tệp có tên, nơi các sao kê Tháng Một, Tháng Hai và Tháng Ba đăng ký dưới một tên danh sách rồi gộp trong một lượt, với ba biến thể Fast, mặc định và strict đánh đổi việc giữ cây cấu trúc lấy tốc độ
Hàng trăm đầu vào đã đăng ký gộp vào một lượt MergeFileList duy nhất, với kết quả xác minh trong vài mili giây qua một probe chỉ đọc khác. Chọn biến thể nào là quyết định riêng của từng pipeline vì Fast loại bỏ cây cấu trúc Tagged PDF
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, DAMovePageDAHidePage, 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