Việc đếm số trang trong một kho lưu trữ đã quét 1,4 GB lẽ ra phải rẻ. Gọi LoadFromFile trên tệp đó và nó không còn rẻ nữa: HotPDF phân tích cú pháp dữ liệu tham chiếu chéo và xây dựng một đối tượng trong bộ nhớ cho mỗi đối tượng gián tiếp trong số vài trăm nghìn đối tượng của tài liệu, và một worker 32-bit chạm trần không gian địa chỉ 2 GB đâu đó giữa quá trình phân tích cú pháp đó. Thao tác bạn muốn, số trang, chưa bao giờ cần bất kỳ đối tượng nào trong số đó. Nó chỉ cần cây trang (page tree) và không gì khác. Khoảng cách đó, giữa những gì một tác vụ yêu cầu và những gì một lần tải đầy đủ mang lại, chính là toàn bộ lý do Direct File API tồn tại
Direct File API cho Delphi và C++Builder quyền truy cập ở cấp độ tệp vào một PDF: số trang, sao chép, giải mã, nối thêm gia tăng, tất cả đều đọc từ đĩa những gì chúng thực sự cần thay vì tái tạo toàn bộ mô hình tài liệu trong RAM. Kỹ năng ở đây là khớp mỗi tác vụ với tầng nhẹ nhất có thể trả lời nó. Khớp đúng và một dịch vụ giữ bộ nhớ phẳng bất kể kích thước đầu vào nào. Khớp sai và tệp quá khổ đầu tiên sẽ hạ gục worker
Một lần tải đầy đủ tốn kém những gì
LoadFromFile không phải là kẻ thù. Nó xứng đáng với bộ nhớ nó dùng: một khi cây đã ở trong RAM, bạn có quyền truy cập ngẫu nhiên vào mọi trang và mọi đối tượng, đây chính xác là điều mà InsertPagesFromDocument, MovePage, và tuần tự hóa lại thông qua SaveLoadedDocument yêu cầu. Không có đường tắt nào cho việc tái cấu trúc thực sự; bạn phải giữ toàn bộ tài liệu để sắp xếp lại nó
Rắc rối bắt đầu khi kích thước đầu vào không nằm trong tầm kiểm soát của bạn. Các tệp khách hàng tải lên, đầu ra từ máy quét, và các kho lưu trữ từ một thập kỷ trước bỏ qua bất cứ điều gì bộ dữ liệu kiểm thử của bạn đã giả định. Tải mọi đầu vào một cách vô điều kiện và trần bộ nhớ của bạn sẽ được thiết lập bởi tệp lớn nhất mà bất kỳ ai từng gửi lên. Thời gian phân tích cú pháp theo dõi số lượng đối tượng, và bộ nhớ thường trú ổn định ở mức gấp nhiều lần kích thước tệp sau khi các cấu trúc đối tượng và luồng đã giải mã được tính vào, nên một gigabyte trên đĩa có thể có nghĩa là nhiều gigabyte thường trú
Biên dịch lại cho 64-bit nâng trần không gian địa chỉ nhưng để nguyên hóa đơn. Worker vẫn đốt hàng giây CPU và một bội số của kích thước tệp trong RAM để trả lời một câu hỏi mà cấu trúc riêng của tệp lẽ ra có thể trả lời trong vài mili giây. Dưới đồng thời, phép toán trở nên bất lợi: bốn lần tải lớn chạy cùng lúc chia sẻ một ngân sách bộ nhớ, và thông lượng sụp đổ đúng vào lúc hàng đợi sâu nhất và bạn ít có khả năng chịu đựng nó nhất
Đọc một tệp thông qua một handle
Tầng chỉ-đọc mở một tệp như một handle, trả lời các câu hỏi cấu trúc về nó, và đóng nó lại. Không có cây đối tượng, không có kết xuất trang, không có bộ nhớ nào tăng theo đầu vào
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
if Handle > 0 then
try
PageCount := Pdf.DAGetPageCount(Handle);
RouteByPageCount('archive-2026-06.pdf', PageCount);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Ba thói quen giữ cho tầng này trung thực. Thứ nhất, kiểm tra giá trị trả về. Một handle không dương nghĩa là việc mở đã thất bại, và gọi DAGetPageCount trên một handle đã chết là kiểu lỗi ẩn mình cho đến ngày một khách hàng gửi một tệp bị lỗi định dạng. Thứ hai, ghép mỗi lần mở thành công với DACloseFile bên trong một khối finally; một dịch vụ rò rỉ handle không sập, nó chỉ mục ruỗng dần, điều này còn tệ hơn. Thứ ba, tôn trọng những gì tham số mật khẩu thực sự làm. DAOpenFileReadOnly chấp nhận một tham số như vậy, nhưng đối với các đầu vào đã mã hóa, nó âm thầm hạ xuống một lần phân tích cú pháp đầy đủ để đọc số trang, vì vậy đảm bảo bộ nhớ phẳng biến mất. Hãy định tuyến các tệp được bảo vệ qua DecryptFile trước, và phần còn lại của pipeline vẫn rẻ
Cùng một phép dò cũng đóng vai trò như một cổng phân loại. Các tệp xuất hiện với nhãn sai, tải lên một nửa, hoặc được đổi tên từ một định dạng hoàn toàn khác, và một lần kiểm tra DAOpenFileReadOnly từ chối tất cả những thứ đó ngay tại cửa trước trong vài mili giây, với lỗi được ghim vào đúng tệp gây ra. Lựa chọn thay thế là để một tệp rác trôi sâu vào một worker hàng đợi và nổ tung ở đó, nơi việc gỡ rối xem đầu vào nào gây ra nó có thể tốn cả một buổi chiều
Sao chép, giải mã, và mã hóa toàn bộ tệp
Tầng thứ hai di chuyển và biến đổi các tệp hoàn chỉnh mà không bao giờ phơi bày nội bộ của chúng. Đây là những lệnh gọi mà các pipeline tiếp nhận dựa vào nhiều nhất
// Sao chép cấu trúc: xác thực-và-di chuyển mà không phân tích cây đối tượng
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);
// Giải mã trong khi sao chép: đường vào Direct File cho các đầu vào được bảo vệ
Status := Pdf.DecryptFile('incoming\protected.pdf',
'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);
// Mã hóa trong khi sao chép: bảo vệ đầu ra mà không cần tải đầy đủ
Status := Pdf.EncryptFile('verified\statement.pdf',
'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);
Mỗi lệnh gọi đều xứng đáng với vị trí của nó. DACopyFile là bản sao đã được xác thực từ một thư mục cách ly vào bộ lưu trữ được quản lý: nó mở và lập chỉ mục cấu trúc PDF khi đang tiến hành, nên một đầu vào bị cắt cụt hoặc không phải PDF sẽ thất bại ngay tại đây thay vì ba giai đoạn sau đó ở hạ nguồn. DecryptFile ghi một bản sao đã giải mã theo một đường ghi lại AES-256 trực tiếp bỏ qua cây đối tượng bất cứ khi nào đầu vào cho phép, đối tác dành cho tệp lớn của luồng giải mã tải-rồi-lưu-lại được trình bày trong bài viết về mã hóa AES-256. EncryptFile thực hiện cùng động tác theo chiều ngược lại, áp dụng bảo vệ bằng mật khẩu trong khi sao chép ở cấp độ tệp với các tham số kiểu khóa và quyền hạn mà đường đi trong bộ nhớ vốn đã dùng
Nối thêm thay đổi thay vì ghi lại toàn bộ
Cập nhật gia tăng (incremental update), được định nghĩa trong ISO 32000-1 §7.5.6, là tầng thứ ba. Các byte gốc vẫn nằm nguyên tại chỗ trên đĩa, và bất kỳ đối tượng mới hoặc đã sửa đổi nào cũng được nối thêm vào sau chúng, theo sau là một phần tham chiếu chéo mới xâu chuỗi trở lại vào bản gốc. Đối với một kho lưu trữ 900 MB cần thêm một trang duy nhất, chi phí ghi là phần chênh lệch, không phải toàn bộ tệp
// Nối thêm một trang kiểm toán vào một kho lưu trữ lớn mà không ghi lại nó
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf'); // byte gốc + phần chênh lệch
Có hai điểm kỷ luật quan trọng ở đây. BeginIncrementalUpdate phải trỏ vào tệp gốc, vì dữ liệu tham chiếu chéo được nối thêm xâu chuỗi trở lại các offset byte bên trong nó. Và mô hình này chỉ-nối-thêm theo thiết kế: mỗi lần lưu gia tăng làm tệp lớn thêm, không bao giờ thu nhỏ nó. Một tài liệu được đóng dấu hàng đêm sẽ phình to không giới hạn cho đến khi một lần tuần tự hóa lại định kỳ, tải nó lên và ghi nó trở lại thông qua SaveLoadedDocument, nén gọn nó xuống. Chính bản chất chỉ-nối-thêm đó là điều khiến cập nhật gia tăng trở thành cách an toàn duy nhất để chạm vào một tài liệu đã ký số, một ràng buộc được xem xét trong bài viết về chữ ký số và PAdES. Cơ chế tham chiếu chéo bên dưới được trình bày riêng trong bài viết về object stream và cập nhật gia tăng
Có một cái bẫy trong các lần lưu chỉ-nối-thêm mà lọt qua hầu hết các đánh giá. Các byte gốc vẫn nằm trong tệp, đọc được bởi bất kỳ ai sẵn lòng nhìn vào. Một bản cập nhật gia tăng "thay thế" một trang không xóa trang cũ; nó chỉ thay thế trang đó trong phiên bản hiện tại trong khi phiên bản trước vẫn nằm đó, hoàn toàn có thể khôi phục được. Vì vậy các bản cập nhật gia tăng là công cụ sai để loại bỏ nội dung nhạy cảm. Để thực sự bỏ đi lịch sử mà một người nhận không bao giờ nên thấy, bạn cần một lần tuần tự hóa lại đầy đủ: LoadFromFile theo sau bởi SaveLoadedDocument, cách này chỉ ghi ra trạng thái hiện tại và bỏ lại các phiên bản bị chôn vùi phía sau
Khớp tầng với thao tác
Logic lựa chọn đủ ngắn để ghi nhớ trong đầu, và việc mã hóa nó thành một quyết định định tuyến rõ ràng ở đầu một pipeline thay vì để mỗi tác vụ tự ứng biến con đường riêng của nó là điều đáng làm. Thao tác bạn cần sẽ quyết định tầng:
- Đếm, kiểm tra, hoặc phân loại mở một handle:
DAOpenFileReadOnly,DAGetPageCount,DACloseFile - Di chuyển, giải mã, hoặc mã hóa toàn bộ tệp vẫn ở cấp độ tệp với
DACopyFile,DecryptFile, hoặcEncryptFile - Tái cấu trúc trang hoặc hợp nhất tài liệu cần tải đầy đủ:
LoadFromFile, sau đóInsertPagesFromDocumenthoặcMovePage, rồiSaveLoadedDocument - Thêm một phần chênh lệch nhỏ vào một tệp khổng lồ hoặc đã ký gọi
BeginIncrementalUpdatevà lưu lại
Các pipeline hỗn hợp nên đặt một ngưỡng kích thước trước đường tải đầy đủ. Gửi bất cứ thứ gì vượt quá vài trăm megabyte qua các tầng Direct File, và dành riêng lần tải đầy đủ cho việc tái cấu trúc thực sự trên một worker 64-bit với ngân sách bộ nhớ thực sự. Ngưỡng này biến một sự cố hết bộ nhớ thành một quyết định định tuyến mà bạn có thể thấy và điều chỉnh
Bất kể tầng nào xử lý một tác vụ, hãy ghi đầu ra của nó vào một tên tạm thời và chỉ đổi tên vào vị trí chính thức một khi kết quả đã được xác thực. Một tệp ghi dở nằm dưới tên cuối cùng trông y hệt như một tệp tốt đối với giai đoạn tiếp theo của pipeline, và các lệnh gọi Direct File làm cho việc kiểm tra trở nên rẻ: xác nhận một đầu ra là một phép dò handle một dòng
Direct File API được phát hành như một phần của HotPDF Delphi Component cho Delphi và C++Builder. Trang sản phẩm liên kết đến tài liệu tham khảo hàm đầy đủ, bao gồm cả các lệnh gọi cập nhật gia tăng được trình bày ở đây