Hai lời phàn nàn đến trong tuần sau khi một tính năng nén ra mắt: bản hợp đồng quét giờ có các nét chữ bậc thang lởm chởm, và logo trong suốt trên trang bìa ngồi trong một quầng nhạt. PDFiumPas trả lời cả hai ở một nơi. TPdf.OptimizeImages đo từng ảnh trước khi thu nó lại, rồi chọn một kernel resample và cộng dồn màu theo dạng biết alpha
Điều đó chưa từng đúng từ trước. Trước v3.100.0, cùng method đó thu nhỏ mọi ảnh không phải bilevel bằng một bước nearest-neighbour cố định — chính là thuật toán sinh ra cả hai lời phàn nàn: nó point-sample một pixel nguồn cho mỗi pixel đầu ra, và nó coi RGB nằm dưới một pixel trong suốt hoàn toàn như thể người đọc sẽ nhìn thấy nó. Lần viết lại trong v3.100.0 thay đường đi đơn lẻ đó bằng năm kernel, một quy tắc chọn dựa trên phép đo, và một ngân sách bộ nhớ làm việc tường minh
Vì sao thu nhỏ làm văn bản quét trông lởm chởm?
Vì point sampling trả lời một câu hỏi sai. Khi một bản quét 300 DPI được nhắm lại về 150 DPI, mỗi pixel đích đại diện cho một khối hai-nhì của các pixel nguồn, và nearest neighbour giữ lại một trong bốn rồi bỏ phần còn lại. Cái nào sống sót phụ thuộc vào làm tròn, nên một mép nét vốn được antialias mượt mà trong nguồn trở thành một cái tung xu theo từng pixel. Kết quả là chiếc bậc thang aliased kinh điển dọc mép chữ glyph, cộng thêm moire trên các vùng halftone nơi các mẫu bị bỏ tình cờ mang theo hoành tiết. Chuyện này quan trọng hơn trong PDF hơn là trên màn hình vì tổn hại là vĩnh viễn. Một ảnh XObject mang dữ liệu mẫu cùng /Width, /Height và /BitsPerComponent (ISO 32000-1 §8.9.5), và resample viết lại cả ba bên trong tệp. Một cú zoom tệ trong viewer là một khung hình bạn có thể vẽ lại, và PDFiumPas có máy móc riêng cho việc đó trong render cache và hiệu năng zoom. Một cú downsample tệ là một tài liệu mới bạn trao cho khách hàng
PDFiumPas đo chi tiết và chọn kernel thế nào
PDFiumPas quyết định theo từng ảnh, không theo từng tài liệu. Trước khi chọn kernel, nó tính một điểm chi tiết độ sáng đã chuẩn hóa từ một lưới lấy mẫu có chặn: các bước ngang và dọc là (Width + 63) div 64 và (Height + 63) div 64, nên một bản quét 12000 pixel và một thumbnail 300 pixel tốn xấp xỉ cùng một lượt quét 64 trên 64. Tại mỗi vị trí lấy mẫu, nó cộng giá trị tuyệt đối của hiệu tới người hàng xóm bên phải và hàng xóm bên dưới, trên tối đa ba kênh, rồi chia cho số mẫu nhân 255. Điểm nằm trong khoảng 0 đến 1, nơi đồ họa văn phòng phẳng nằm gần 0 và kết cấu ảnh dày đặc leo lên
Chiếc thang chọn sau đó chạy theo một thứ tự cố định. Nếu ResampleFilter là bất cứ thứ gì khác pirfAdaptive, filter đó được dùng nguyên văn. Nếu không: nội dung 1-bit nhận pirfBilevel; một ContentClass là piccLineArt nhận pirfBox; hệ số tỉ lệ từ 4 trở lên cũng nhận pirfBox, vì ở mức thu nhỏ đó trung bình diện tích vừa rẻ nhất vừa đúng nhất; piccPhoto, điểm chi tiết từ 0.08 trở lên, hay PreferredQuality từ 0.9 trở lên nhận pirfLanczos với kernel ba thùy của nó; tỉ lệ từ 2 trở lên hay chất lượng từ 0.7 trở lên nhận pirfBicubic ở bán kính 2; phần còn lại nhận pirfBilinear. Vì TPdfImageOptimizeOptions.Default đặt PreferredQuality ở 0.85, một lượt chạy mặc định không bao giờ rơi về bilinear trừ khi mức thu nhỏ nhẹ và nội dung phẳng
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Default: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, chất lượng 0.85, ngân sách 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Một ảnh chỉ bị đụng tới khi DPI đặt vị trí lớn hơn trong hai chiều ngang và dọc, chia cho TargetDpi, đạt tới MinDpiRatio. Guard đó tồn tại để một ảnh 160 DPI nhắm vào mục tiêu 150 DPI không bị encode lại vì một mức lợi nhuận sáu phần trăm mà trả giá một thế hệ chất lượng. Các ảnh dưới MinDimension trên bất kỳ trục nào, mặc định 8, bị bỏ qua như icon hay kẻ đường
Vì sao logo trong suốt dính một quầng trắng?
Vì màu nằm dưới một pixel trong suốt hoàn toàn là tùy hỉ, và một trung bình có trọng số trơn cho nó bỏ phiếu. Xuất một logo từ công cụ thiết kế và lề vô hình thường là trắng, hay đen, hay bất cứ thứ gì canvas từng là; kênh alpha che nó đi, và một tổng trơn trên footprint kernel lập tức trộn nó ngược lại mép hiển thị. PDFiumPas tránh điều này bằng cách cộng dồn các mẫu BGRA ở dạng premultiplied và chỉ hủy premultiplication tại pixel đích
Cụ thể, mỗi mẫu góp phần cộng channel * alpha * weight vào bộ cộng màu, alpha * weight vào bộ cộng alpha, và weight vào tổng trọng số. Màu đích sau đó bị chia cho bộ cộng alpha thay vì cho tổng trọng số, và đó là bước quan trọng: chia cho tổng trọng số sẽ kéo màu về phía các pixel vô hình, trong khi chia cho alpha đã cộng dồn tái dựng màu mà các mẫu hiển thị thực sự đồng thuận. Alpha đích là một đại lượng riêng, 255 * AlphaSum / WeightSum. Các định dạng không alpha chia cho tổng trọng số như thường lệ, byte đệm của một đích FPDFBitmap_BGRx được ghi là hằng 255, và mọi kênh bị kẹp vào khoảng 0 đến 255 trước khi lưu. Alpha đó bình thường bắt nguồn từ một mục soft mask trong từ điển ảnh (ISO 32000-1 §11.4), thứ mà PDFium đã composite sẵn vào bộ đệm BGRA mà bộ resample nhận được
// Hình dạng của vòng cộng dồn trong, cho mỗi mẫu nguồn góp phần
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... và tại pixel đích, unpremultiply theo tổng alpha
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Giữ line art 1-bit ngoài vùng xám
Bất kỳ kernel liên tục nào áp lên một bản quét bilevel đều sinh ra xám, và xám chính là thứ một ảnh kiểu fax không được phép chứa. Vì vậy PDFiumPas mặc định bỏ qua ảnh 1-bit: PreserveBilevel là True trong TPdfImageOptimizeOptions.Default, và các ảnh đó rơi vào SkippedCount nguyên vẹn. Đặt nó thành False và đường pirfBilevel tiếp quản thay cho một kernel làm mượt. Nó đi qua đúng hình chữ nhật nguồn phủ mỗi pixel đích, lấy trung bình độ sáng với các trọng số 0.114, 0.587 và 0.299 theo thứ tự bộ nhớ BGR, và ngưỡng hóa kết quả tại 127.5 thành phẳng 0 hay 255. Không có gì trung gian có thể được ghi, nên mép giữ sắc và không quầng xám nào hình thành quanh nét mảnh; kênh alpha của nguồn BGRA được trung bình bình thường, và đích BGRx nhận hằng 255. Nếu bạn cần các pixel nền thay vì một tài liệu nhỏ hơn, trích xuất ảnh từ tài liệu PDF là đường riêng
Chuyện gì xảy ra khi một ảnh vượt ngân sách bộ nhớ làm việc?
Nó được để lại đúng nguyên trạng, và được đếm. MaxWorkingBytes mặc định 64 MiB và được thực thi hai lần. Trước khi bitmap đích được tạo, PDFiumPas từ chối ảnh nếu chiều rộng nhân chiều cao nhân byte mỗi pixel vượt ngân sách. Sau khi FPDFBitmap_CreateEx thành công, nó kiểm tra lại bằng stride thật nhân chiều cao, vì padding hàng có thể đẩy một lần cấp phát vượt qua một ngưỡng mà tích thô đã qua mặt. Bất kỳ lần từ chối nào cũng phá hủy đích và không trả lại gì. Hãy nói rõ về kiểu suy giảm điều này ngụ ý: một ảnh vượt ngân sách không được resample ở chất lượng thấp hơn, và không được cắt thành tile. Bản gốc ở lại trong tài liệu, BudgetExceededCount và SkippedCount cùng tăng, và một lượt chạy vì thế có thể báo thành công trong khi tài liệu chỉ được tối ưu một phần. Đó là hành vi fail-safe có chủ đích, nhưng nó có nghĩa report không phải thứ đọc tùy hứng. Một kiểu thất bại khác cũng tồn tại: các ảnh mà PDFium không thể tạo bitmap, như CMYK, JPX, JBIG2 hay nguồn có mask, tăng FailedCount thay thế và cũng được để nguyên vẹn
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // dùng bình chọn diện tích bilevel
Options.ContentClass := piccPhoto; // ép Lanczos cho bộ ảnh chụp
Options.MaxWorkingBytes := 256 * 1024 * 1024; // chỗ dự phòng cho bản quét lớn
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Đọc report trước khi đóng gói tệp
TPdfImageOptimizeReport được dựng để chẩn đoán, không chỉ để ghi log. Bên cạnh OptimizedCount, SkippedCount và FailedCount, nó phơi một bộ đếm mỗi kernel, nên BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount và BilevelFilterCount cho bạn biết quy tắc thích ứng thực sự kết luận gì về kho ảnh của bạn. Một kết quả toàn box nghĩa là các mức thu nhỏ dốc hoặc nội dung bị phân loại là line art; một kết quả toàn Lanczos trên tài liệu bạn tin là line art là dấu hiệu nên đặt ContentClass tường minh. AverageDetailScore là con số để so với ngưỡng Lanczos 0.08 khi tinh chỉnh PreferredQuality, và PeakWorkingBytes cho thấy lượt chạy thực cần bao nhiêu trong MaxWorkingBytes. Các tùy chọn không hợp lệ fail ầm ĩ thay vì im lặng: một TargetDpi không dương, một MinDpiRatio dưới 1, một PreferredQuality ngoài khoảng 0 đến 1, hay một MaxWorkingBytes không dương raise EPdfError trước khi bất kỳ trang nào bị đụng tới. Và OptimizeImages chỉ sửa tài liệu trong bộ nhớ; mỗi trang đã sửa được commit bằng FPDFPage_GenerateContent, sau đó bạn vẫn phải tự gọi SaveAs. Để nhìn bằng mắt những gì đã đổi, render hai tài liệu trước và sau ra bitmap như mô tả trong chuyển trang PDF thành ảnh JPEG rồi so chúng ở mức zoom toàn phần
Resample thích ứng thuộc dạng tính năng vô hình khi nó chạy đúng và sinh ticket hỗ trợ khi nó không, đó là lý do phép đo, xử lý alpha và ngân sách bộ nhớ phải hạ cánh cùng nhau thay vì ba lần tinh chỉnh riêng lẻ. Nếu bạn đang đánh giá nó cho một sản phẩm Delphi, C++Builder hay Lazarus, bề mặt API đầy đủ và chi tiết licensing nằm trên trang PDFiumPas Delphi PDFium component