Bài viết kỹ thuật

CompressDocument của HotPDF: font subset gọn trong Delphi

HotPDF THotPDF.CompressDocument là một công tắc duy nhất khiến BeginDoc cho ra PDF lossless nhỏ nhất mà component viết được: FlateDecode ở mức tối đa, một cross-reference stream với object stream, font subsetting và các font subset gọn đánh lại số các glyph được giữ phía sau một /CIDToGIDMap tường minh. EndDoc sau đó trả lại đúng các thiết lập của bạn. Một tài liệu test ba trang Arial và SimSun rớt từ 10.2 MB xuống 20 KB với bản render y hệt

CompressDocument thực sự bật những gì?

CompressDocument ghi đè sáu thiết lập writer cùng trần object-stream, cho đúng một tài liệu rồi khôi phục tất cả về sau. Tại BeginDoc, trước khi phiên bản PDF được chốt, HotPDF ghi lại giá trị của bạn rồi đặt Compression thành cmFlateDecode, CompressionLevel thành clMaximum, bật EnableFontSubsetting và CompactFontSubsetting, và kích hoạt UseXRefStream cùng UseObjectStreams (ISO 32000-1 §7.5.7 và §7.5.8). Object stream cần PDF 1.5, nên một Version cũ hơn được nâng lên 1.5 khi nó không bị khóa. PDF/A-1 cấm cả hai cấu trúc đó, nên một tài liệu PDF/A-1 giữ bảng cross-reference cổ điển của mình và chỉ nhận phần Flate cùng phần font. Ảnh thì được giữ nguyên y như bạn đã nhúng

Sơ đồ vòng đời CompressDocument của HotPDF trong Delphi: BeginDoc ghi lại các giá trị riêng của writer, ghi đè sáu thiết lập gồm Compression và UseObjectStreams cho đúng một tài liệu, còn EndDoc trả lại mọi giá trị vay mượn trong finally ngoài cùng trong khi thuộc tính CompressDocument vẫn đứng ở True
Sáu thiết lập writer cùng trần object-stream được vay đúng một tài liệu rồi trả lại khi EndDoc chạy, nên một bản báo cáo hỏng không bao giờ để component kẹt ở nén tối đa
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // BeginDoc áp vào, EndDoc gỡ ra
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Phép khôi phục diễn ra trong finally ngoài cùng của EndDoc, nên một exception giữa chừng một bản báo cáo không để một component sống lâu kẹt ở nén tối đa cho job kế tiếp. Thuộc tính CompressDocument vẫn giữ nguyên True; chỉ sáu thiết lập nó từng vay được trả lại. Phiên bản được xử lý cẩn thận hơn. HotPDF chỉ gỡ chính phép nâng lên 1.5 của mình nếu tài liệu vẫn kết thúc ở 1.5, nên khi một tính năng khác đẩy tệp lên 1.6 trong lúc chạy (chẳng hạn một font OpenType được nhúng), phiên bản cao hơn được giữ lại, đúng như khi không có nén

Vì sao font subset không gộp vẫn còn to?

Một subset TrueType kiểu cổ bỏ các outline bạn không bao giờ vẽ nhưng giữ nguyên mọi glyph ID ở đúng chỗ cũ, và chính cách đánh số ấy khiến nó nặng. Content stream hiển thị các CID trùng với GID gốc, nên subset phải giữ một offset loca và một entry hmtx cho mọi slot tính tới glyph cao nhất nó giữ lại, trống hay không. Với một font Latin, khoản phí đó là thiết thừa. Với một font CJK như SimSun, mà các chữ tượng hình nằm sâu trong một bảng glyph cực lớn, hai chữ Hán kéo theo những bảng được tính cỡ cho cả bộ font. Luật đóng subset font cho các glyph đã shaping quyết định glyph nào sống sót; còn gộp chỉ là chuyện những kẻ sống sót tốn bao nhiêu

CompactFontSubsetting đánh lại số các glyph được giữ thành một dải dày đặc bắt đầu từ zero và ghi một stream /CIDToGIDMap trên CIDFont, thứ ISO 32000-1 §9.7.4.2 định nghĩa là một bảng các GID hai byte được đánh chỉ số theo CID. Cái bảng ấy là toàn bộ mẹo của chuyện này. Content stream, mảng bề rộng /W và ToUnicode CMap đều giữ nguyên CID gốc, nên chẳng gì đã ghi ra phải đổi; chỉ phép tra từ CID sang glyph dời vào trong map. Trong ca test thúc đẩy tính năng này, SimSun với hai chữ Hán từ 24.8 KB dữ liệu font rớt xuống 3.1 KB

So sánh một font subset thưa của HotPDF, giữ các entry loca và hmtx cho mọi glyph ID gốc tính tới GID giữ lại cao nhất, với output CompactFontSubsetting đánh lại số các glyph giữ lại dày đặc từ zero và ánh xạ CID qua một stream CIDToGIDMap trong khi content stream, /W và ToUnicode đứng yên
Đánh lại số dời khoản chi phí ra khỏi font program và nhét vào một map stream nhỏ xíu — hai chữ SimSun từ 24.8 KB rớt xuống 3.1 KB mà chẳng đụng lấy một byte content đã ghi

Việc gộp có giới hạn cứng, và nó thoái hóa một cách thầm lặng thay vì thất bại. HotPDF chỉ dựng subset gọn cho các face Type 0 TrueType, cả những face đặt qua SetFont có bật subsetting lẫn face đăng ký qua RegisterUnicodeTTF. Một font TrueType đơn giản tìm glyph của mình qua cmap bên trong font program, thứ mà đánh lại số sẽ làm gãy, nên nó giữ subset thưa. Các face OpenType-CFF cũng chẳng có đường gộp nào. Một lần dựng gọn thất bại sẽ rơi về subset thưa thay vì văng lỗi. Thuộc tính mặc định tắt, nên output hiện có giữ nguyên từng byte, còn dưới PDF/A thì face Unicode đăng ký luôn được một subset gọn

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // dùng được mà không cần CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Writer đóng gói bóp chặt cấu trúc tệp ra sao?

Khi font cùng stream đã nhỏ, các dictionary và dữ liệu cross-reference thành khoản chi lớn nhất còn lại, nên writer object-stream đứng sau CompressDocument bóp luôn cả những thứ đó. Hướng dẫn object stream và cập nhật tăng dần đã bàn định dạng container; riêng đường nén thêm bốn bước tinh chỉnh lên trên:

  • Cú pháp gộp theo ISO 32000-1 §7.2.2: một dấu cách chỉ được ghi giữa hai token mà nếu không sẽ dính liền nhau thành ký tự thường, nên /Type /Page thành /Type/Page
  • Các trường cross-reference stream nhận bất kỳ bề rộng nào §7.5.8.2 cho phép, nên một tệp dưới 16 MB lưu mỗi offset trong 3 byte thay vì 4
  • Tối đa 250 object nằm trong mỗi object stream thay cho mức 100 thường thấy, trừ khi bạn tự đặt trần riêng qua ConfigureAdaptiveObjectStreamPacking
  • Khi tệp không bị mã hóa, Catalog cùng dictionary Info cũng bị nhét vào object stream; output mã hóa giữ chúng ở tầng trên cùng

Cú pháp gộp đi kèm một cái bẫy đáng biết nếu bạn mở rộng writer. Ký số điền chữ ký sau khi tệp đã ghi bằng cách tìm trong các byte các placeholder literal /ByteRange ( và /Contents <, mà chính tả gộp sẽ biến chúng thành /ByteRange( và /Contents<, thứ phép tìm kiếm không bao giờ gặp. Dictionary chữ ký (Type Sig hay DocTimeStamp, FT Sig) và dictionary mã hóa vì thế giữ bố cục có dấu cách. Một khuyết tật liên quan ảnh hưởng các bản trước v2.766.41: mọi lần lưu object-stream, kể cả CompressDocument, đều bắt đầu bằng hai dòng header %PDF-, nên hãy nâng cấp nếu một validator nghiêm ngặt gắn cờ output của bạn

Có nén được một PDF đã nạp sẵn không?

Được, qua overload theo options CompressLoadedDocument(Options, Info), thứ chạy cùng các bước lossless đó trên một tệp có sẵn. Với THPDFLoadedDocumentCompressionOptions.Default, nó dọn các tài nguyên trang không dùng, gộp các font và form giống hệt nhau, subset các font nhúng với subset gọn bật sẵn, nén lại các stream không lọc, Flate, LZW, ASCII và RunLength bằng Flate khi kết quả nhỏ hơn, và bảo lần lưu kế tiếp dùng object stream. HighRatioFlate mặc định tắt, còn object stream bị bỏ qua với PDF/A-1 và các lần lưu tăng dần. Overload CompressLoadedDocument không tham số là lệnh gọi cũ hơn, hẹp hơn, chỉ nén Flate các stream chưa nén

Luồng chạy của HotPDF CompressLoadedDocument trong Delphi: lệnh gọi dọn tài nguyên trang không dùng, gộp font và form giống nhau, subset font nhúng với subset gọn, nén lại stream bằng Flate chỉ khi kết quả nhỏ hơn, và bật object stream cho lần lưu kế, trong khi các trường chữ ký khiến RefusedBySignaturePolicy và tệp được để nguyên
Từng bước đều viết lại những byte mà chữ ký phủ, nên cả tài liệu bị từ chối trừ khi bạn tường minh cho phép mất hiệu lực — Info.BytesSaved khi đó chỉ cộng phần tài nguyên, font và stream
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Hai biên giới đáng nhớ trên đường loaded. Từng bước đều viết lại những byte mà chữ ký phủ, nên một tài liệu có trường chữ ký bị từ chối trọn vẹn: lệnh gọi trả về 0, đặt RefusedBySignaturePolicy và không đổi gì cả, trừ khi bạn đặt AllowSignatureInvalidation, sau đó Info.SignaturesInvalidated sẽ kể bạn đã đánh đổi gì. Việc gộp ở đây cũng dè dặt hơn trên đường tạo: HotPDF chỉ gộp các font program chỉ được dùng bởi font CIDFontType2 với /CIDToGIDMap Identity, nơi CID bằng GID, và bỏ qua các program đã có map stream, có /CIDSet, hay các bảng màu glyph như COLR, sbix, CBDT hay SVG, vì việc dựng gọn lại sẽ làm rơi các lớp màu. Để ý thêm rằng Info.BytesSaved chỉ cộng ba bước tài nguyên, font và stream; khoản lời của object stream lộ diện khi tệp được ghi

Thực tế bạn nên kỳ vọng kết quả ra sao?

Khoản lời đi theo phần cấu trúc chưa nén và dữ liệu font phình to của tệp, chứ không theo số trang. Mẫu ba trang Arial và SimSun co từ 10.2 MB xuống 20 KB khi sinh với CompressDocument, và từ 10.2 MB xuống 19.8 KB khi bản gốc chưa nén được nạp rồi chạy qua CompressLoadedDocument, với bản render y hệt cả hai đường. Một PDF vốn đã gọn hầu như chẳng nhúc nhích: trong bộ hồi quy, các tệp như vậy chỉ tiết kiệm trong khoảng -0.07% tới +0.06% kích thước gốc. Các tệp ngập ảnh lời ít, vì chẳng đường nào đụng vào dữ liệu ảnh

Nếu bạn sinh cùng các báo cáo CJK mỗi đêm, hãy ghép subset gọn với bộ nhớ đệm subset font bền trên đĩa để việc subsetting không phải lặp lại mỗi lần chạy, và diff các output nén theo nội dung object thay vì theo byte, vì một trường thay đổi là cả một object stream bị Flate lại. Tài liệu tham chiếu đầy đủ về thuộc tính và record nằm trên trang sản phẩm HotPDF Delphi PDF component