Để giảm dung lượng tệp PDF trong Delphi, losLab PDF Library cung cấp ba API nhắm trực tiếp vào ba nguyên nhân gây phình to dung lượng tệp lớn nhất: SubsetEmbeddedFonts ghi lại mọi chương trình phông chữ TrueType được nhúng chỉ giữ lại các ký tự mà tài liệu thực sự hiển thị, DownsampleImages giảm độ phân giải của các hình ảnh raster vượt quá giá trị DPI mục tiêu, và NormalizeLZWStreams thay thế phương thức nén LZWDecode cũ bằng FlateDecode. Mỗi hàm trả về số lượng đối tượng mà nó đã thay đổi, vì vậy giá trị trả về bằng không cho biết tác vụ đó không thực hiện thay đổi nào (no-op) chứ không phải là lỗi ngầm
Tại sao tệp PDF sau khi ghép lại lớn hơn các tệp nguồn?
Một tệp PDF được ghép hoặc tạo bằng lập trình thường có dung lượng quá lớn vì một trong ba lý do: phông chữ được nhúng đầy đủ, hình ảnh có độ phân giải cao hơn nhiều so với độ phân giải hiển thị thực tế, và các luồng dữ liệu vẫn đang được nén bằng bộ lọc LZW cũ. Tiêu chuẩn ISO 32000-1 §9.9 cho phép trình tạo PDF nhúng toàn bộ chương trình phông chữ, và hầu hết các trình tạo đều làm chính xác điều đó vì đây là lựa chọn mặc định an toàn. Một chương trình phông chữ Arial FontFile2 đầy đủ có thể lên tới hàng trăm kilobyte; nếu nhúng nó vào mười tệp nguồn, rồi ghép chúng lại, bạn sẽ phải mang theo mười bản sao của các đường nét ký tự cho những ký tự mà không ai sử dụng. Bản thân việc ghép nối không tạo ra dung lượng thừa này, nó chỉ gom chúng lại vào một tệp duy nhất và khiến dung lượng phình to đó lộ diện rõ ràng
Hình ảnh là nguyên nhân thứ hai. Một bản quét rộng 4800 pixel được đặt vào một khung hình chỉ chiếm một phần tư trang giấy sẽ truyền tải lượng dữ liệu pixel lớn gấp khoảng 40 lần so với mức mà một quy trình in ấn 300 DPI cần dùng. Nguyên nhân thứ ba ít được chú ý hơn: các luồng dữ liệu được lọc bằng LZWDecode. Tiêu chuẩn ISO 32000-1 §7.4.4 chỉ rõ cả LZWDecode và FlateDecode, và lưu ý rằng Flate thường nén tốt ít nhất là bằng LZW; trong thực tế, kết quả nén Flate luôn có kích thước nhỏ hơn trên cùng một dữ liệu, và LZW chủ yếu chỉ tồn tại trong các tệp tin được xử lý qua các công cụ từ những năm 1990. Phần còn lại của bài viết này sẽ hướng dẫn chi tiết ba lượt xử lý của losLab PDF Library để khắc phục từng vấn đề, sau đó kết hợp chúng vào một quy trình duy nhất
Rút gọn phông chữ với SubsetEmbeddedFonts
SubsetEmbeddedFonts thu nhỏ mọi phông chữ TrueType được nhúng trong tài liệu đã tải về đúng các ký tự mà tài liệu thực sự sử dụng, và nó không cần đối số nào vì nó tự tìm danh sách ký tự cần giữ lại từ chính các luồng nội dung tài liệu. Về mặt nội bộ, lượt xử lý duyệt qua luồng nội dung của từng trang bằng GetTextRuns, thu thập các mã ký tự được tham chiếu dưới mỗi tài nguyên phông chữ, xây dựng danh sách cần giữ lại, và chuyển chương trình phông chữ gốc cho bộ công cụ FontSub của Windows (CreateFontPackage) để tạo ra một phần phông chữ rút gọn (subset). Chương trình phông chữ được ghi lại sẽ thay thế luồng FontFile2 tại chỗ, và tên BaseFont sẽ nhận thêm một thẻ tiền tố LOSABC+, đây là quy ước gồm 6 chữ cái viết hoa và một dấu cộng được định nghĩa trong ISO 32000-1 §9.6.4 cho phông chữ rút gọn. Tiền tố này cũng giúp cho lệnh gọi này có tính lũy đẳng (idempotent): chạy lượt xử lý này hai lần thì các phông chữ đã được rút gọn sẽ được nhận diện và bỏ qua, vì vậy việc đưa nó vào một tác vụ xử lý hàng loạt có thể lặp lại trên các tệp là hoàn toàn an toàn
Có hai chi tiết triển khai cần lưu ý vì chúng giải thích các giới hạn của API này. Thứ nhất, lượt xử lý chỉ nhắm vào FontFile2, vì vậy nó chỉ áp dụng cho các chương trình TrueType được nhúng; các phông chữ được nhúng dưới dạng Type 1 hoặc CFF thô sẽ không được chạm đến để tránh rủi ro. Thứ hai, nó phụ thuộc vào FontSub, điều này làm cho SubsetEmbeddedFonts chỉ chạy được trên hệ điều hành Windows. Một điểm tinh tế khác từ khía cạnh triển khai: liệu một phông chữ có đủ điều kiện hay không được quyết định bằng cách giải phân giải thực tế chuỗi tham chiếu FontDescriptor → FontFile2, chứ không phải dựa vào suy đoán từ cờ nhúng (embedded-flag), vì các phông chữ trong tài liệu được tải không đi qua quá trình ghi chép sổ sách ở phía khởi tạo để thiết lập các cờ đó. Nếu luồng phân giải có tồn tại, phông chữ đó là một ứng viên; nếu không, nó sẽ bị bỏ qua mà không báo lỗi
Sự đánh đổi thực tế: một phông chữ rút gọn chỉ chứa các nét ký tự hiện diện tại thời điểm thực hiện rút gọn. Nếu một công cụ ở hạ nguồn, hoặc chính mã của bạn, sau đó thêm văn bản sử dụng cùng phông chữ đó, bất kỳ ký tự nào nằm ngoài tập hợp rút gọn sẽ không có đường nét hiển thị và sẽ hiển thị dưới dạng một ký tự bị thiếu (missing glyph). Hãy thực hiện rút gọn phông chữ như là bước thay đổi nội dung cuối cùng, tuyệt đối không thực hiện trước giai đoạn chỉnh sửa. Sự thận trọng tương tự cũng áp dụng nếu bạn có kế hoạch trích xuất phông chữ đó ra sau này để tái sử dụng; bài viết về trích xuất văn bản, hình ảnh và phông chữ với PDFlibPas trình bày những gì một chương trình phông chữ rút gọn được trích xuất có thể và không thể cung cấp cho bạn
DownsampleImages quyết định những hình ảnh nào cần thu nhỏ như thế nào?
Hàm DownsampleImages(MaxDPI, Quality, Filter) chỉ lấy mẫu lại các hình ảnh mà nó có thể tự tin xác định là bị quá độ phân giải, sử dụng ước tính DPI thận trọng có chủ ý. Một đối tượng hình ảnh PDF XObject lưu trữ kích thước pixel chứ không có thông tin độ phân giải vật lý đáng tin cậy nào, và bất kỳ thẻ DPI nào từ hình ảnh nguồn hiếm khi tồn tại qua một chu kỳ tải-sửa-lưu. Vì vậy, lượt xử lý ước tính SrcDPI = PixelWidth / 8.5, thực chất là đặt câu hỏi: nếu hình ảnh này trải rộng toàn bộ chiều rộng của một trang giấy cỡ Letter, độ phân giải của nó sẽ là bao nhiêu? Chỉ những hình ảnh có giá trị ước tính vượt quá MaxDPI mới bị xử lý. Sự thiên vị này là có chủ ý: một hình ảnh được đặt nhỏ trên trang sẽ có DPI thực tế cao hơn so với ước tính, do đó lượt xử lý thà bỏ sót còn hơn làm giảm chất lượng của một tài liệu chất lượng in ấn mà nó không thể đo lường chính xác
Tham số Quality từ 1 đến 100 lựa chọn chất lượng nén lại của JPEG, trong khi giá trị 0 giữ kết quả ở dạng nén Flate không mất mát kiểu PNG; tham số Filter chọn nhân lấy mẫu lại, 0 cho trung bình hộp (box average) và 1 cho song tuyến tính (bilinear). Đối với các giấy tờ văn phòng được quét, cấu hình DownsampleImages(150, 75, 1) là một điểm khởi đầu hợp lý; đối với bất kỳ tài liệu nào có thể cần in lại, hãy tăng MaxDPI lên 300 hoặc bỏ qua hoàn toàn lượt xử lý này. Giảm độ phân giải là bước duy nhất gây mất mát chất lượng trong số ba bước, vì vậy nó nên được đặt sau một cấu hình cho phép người dùng của bạn tắt đi
Chuyển đổi các luồng LZW cũ với NormalizeLZWStreams
NormalizeLZWStreams mang lại lợi ích hiển nhiên mà không mất mát gì: nó giải nén không mất mát mọi luồng dữ liệu LZWDecode và nén lại chúng bằng FlateDecode trực tiếp tại chỗ, trả về số lượng luồng đã được chuyển đổi. Nó xử lý cả trường hợp mục nhập /Filter /LZWDecode đơn lẻ và trường hợp LZW xuất hiện bên trong một mảng chuỗi bộ lọc, trong đó chỉ có liên kết LZW được thay thế và phần còn lại của chuỗi được bảo toàn. Các tham số dự đoán (Predictor, Columns, Colors, BitsPerComponent) được đọc từ mục DecodeParms của luồng và được chuyển qua bộ giải nén, để dữ liệu hình ảnh được mã hóa dự đoán được chuyển đổi khứ hồi chính xác. Bởi vì cả hai bộ lọc đều là các codec chính xác đến từng bit, các byte được giải nén là giống hệt nhau trước và sau khi chuyển đổi; chỉ có phương thức nén của thùng chứa thay đổi, đó là lý do tại sao lượt xử lý này cực kỳ an toàn để chạy vô điều kiện trên mọi tệp tin
Trên một tài liệu không có luồng LZW nào, lệnh gọi chỉ đơn giản trả về 0 và không chạm vào bất cứ thứ gì, điều này được bộ kiểm thử hồi quy của thư viện thực thi rõ ràng: một tệp mới được tạo chỉ sử dụng Flate phải báo cáo không có chuyển đổi nào. Đảm bảo no-op này rất quan trọng khi lượt xử lý nằm trong một đường ống xử lý hàng ngàn tệp không đồng nhất, một số từ năm 2024 và một số từ năm 1998
Quy trình tối ưu hóa kích thước hoàn chỉnh trong Delphi
Ba lượt xử lý kết hợp thành một hàm tải-tối ưu-lưu duy nhất, và thứ tự thực hiện ít quan trọng hơn bạn nghĩ bởi vì chúng hoạt động trên các loại đối tượng tách biệt hoàn toàn: phông chữ, đối tượng hình ảnh XObject, và bộ lọc luồng. Chạy rút gọn phông chữ đầu tiên vẫn là lựa chọn gọn gàng nhất, vì nó là lượt xử lý có ràng buộc về thứ tự chỉnh sửa
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> subset
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Log Fonts/Images/Streams: three zeros mean the file was already lean
finally
Lib.Free;
end;
end;
Xác thực đường ống tối ưu hóa theo cách thư viện tự kiểm tra: kiểm tra khứ hồi. Các bài kiểm thử hồi quy v3.130 tạo một tài liệu, lưu nó, tải lại, chạy tối ưu hóa, lưu lại một lần nữa, và sau đó khẳng định ba điều: đầu ra có dung lượng nhỏ hơn, số lượng đối tượng chuyển đổi khớp với kỳ vọng, và việc tải lại tệp tối ưu hóa vẫn phân tích và kết xuất được bình thường. Việc tái hiện vòng lặp tạo-tối ưu-tải lại này trên một mẫu tệp sản xuất của riêng bạn và so sánh văn bản trích xuất trước và sau là một sự đầu tư một giờ giúp phát hiện các lỗi tích hợp trước khi khách hàng mở một hóa đơn bị hỏng
// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
Quy trình này khớp ở đâu trong luồng công việc ghép tài liệu? Hãy thực hiện sau khi ghép, chứ không phải trong khi ghép. Ghép trước rồi mới tối ưu hóa kết quả duy nhất đồng nghĩa với việc mỗi phông chữ nhúng được rút gọn một lần duy nhất dựa trên tập hợp của tất cả các ký tự được sử dụng, thay vì xử lý trên từng tệp nguồn. Nếu băng thông ghép tệp là nút thắt cổ chai, PDFlibPas cung cấp một đường dẫn nhanh cấp độ byte giúp tránh phân tích cú pháp đối tượng đầy đủ, được mô tả trong bài viết về ghép nhanh PDF bằng cách dịch chuyển tham chiếu byte; và đối với các đầu vào quá lớn để lưu hoàn toàn trong bộ nhớ, ghép và chia truy cập trực tiếp cho tệp PDF lớn trình bày con đường xử lý dạng luồng. Cả hai đều kết hợp tự nhiên với một lượt xử lý tối ưu hóa cuối cùng trên đầu ra đã được ghép
Những điều mà ba lượt xử lý không thực hiện
Bộ ba tối ưu hóa của losLab PDF Library cố ý loại trừ bất kỳ thứ gì làm thay đổi ngữ nghĩa tài liệu. SubsetEmbeddedFonts không thống nhất các phông chữ trùng lặp trên các nguồn được ghép thành một chương trình phông chữ duy nhất, nó thu nhỏ từng phông chữ một cách độc lập; việc loại bỏ phông chữ trùng lặp (deduplication) là một phép chuyển đổi khác và nhiều rủi ro hơn. DownsampleImages sẽ bỏ qua một hình ảnh có ước tính DPI thận trọng nằm dưới ngưỡng cấu hình ngay cả khi mắt người có thể nhận thấy nó quá lớn so với khung hình của nó. Và không có lượt xử lý nào chạm vào cấu trúc tài liệu, do đó một tệp phình to do hàng ngàn đối tượng mồ côi (orphaned objects) sẽ cần một thao tác lưu kiểu ghi lại toàn bộ thay vì các lượt xử lý ở cấp độ luồng này. Trong các giới hạn đó, sự kết hợp giữa rút gọn phông chữ, giảm độ phân giải hình ảnh, và chuẩn hóa LZW-sang-Flate giúp loại bỏ ba nguồn phình to kinh điển của tệp PDF bằng cách chỉ sử dụng một lệnh gọi API có thể dự đoán được cho mỗi tác vụ. Ba hàm này đi kèm như một phần của losLab PDF Library dành cho Delphi, C# và VB.NET, bên cạnh các API ghép, trích xuất và kết xuất được thảo luận ở trên