ฟีเจอร์บีบอัดส่งมอบไปเพียงสัปดาห์เดียว คำร้องเรียนก็มาถึงสองเรื่อง: สัญญาฉบับสแกนล่าสุดตัวอักษรกลายเป็นขั้นบันไดและขุ่นมัว ส่วนโลโก้โปร่งใสบนหน้าปกกลับนั่งอยู่ในวงแหวนสีจาง PDFiumPas เก็บสองเรื่องนี้ได้ในจุดเดียว TPdf.OptimizeImages วัดภาพแต่ละภาพก่อนย่อมัน แล้วจึงเลือกเคอร์เนลรีแซมเปิลและสะสมสีในรูปแบบที่รู้เรื่อง alpha
เรื่องนี้ไม่เคยเป็นจริงมาตลอด ก่อน v3.100.0 เมธอดเดียวกันย่อภาพ non-bilevel ทุกภาพด้วยขั้นตอน nearest-neighbour คงที่ ซึ่งเป็นอัลกอริทึมที่ผลิตคำร้องทั้งสองเรื่องพอดี: มัน point-sample พิกเซลต้นทางหนึ่งตัวต่อพิกเซลปลายทางหนึ่งตัว และมันปฏิบัติ RGB ที่ซ่อนอยู่ใต้พิกเซลโปร่งใสเต็มที่ราวกับผู้อ่านจะได้เห็นมันเสมอ การเขียนใหม่ใน v3.100.0 แทนเส้นทางเดียวนั้นด้วยเคอร์เนลห้าตัว กฎคัดเลือกที่คำนวณจากค่าที่วัดได้ และงบหน่วยความจำทำงานที่ประกาศชัดเจน
ทำไมการย่อขนาดจึงทำให้ข้อความสแกนดูขรุขระ?
เพราะ point sampling ตอบคำถามผิดข้อ เมื่อสแกน 300 DPI ถูก retarget เป็น 150 DPI พิกเซลปลายทางแต่ละตัวยืนแทนบล็อกสองคูณสองของพิกเซลต้นทาง และ nearest neighbour เก็บหนึ่งในสี่ทิ้งที่เหลือ ตัวที่รอดขึ้นอยู่กับการปัดเศษ ขอบขีดอักษรที่เคยเรียบเนียนด้วย antialiasing ในต้นฉบับจึงกลายเป็นการเสี่ยงทอยต่อพิกเซล ผลคือบันได aliasing คลาสสิกตามขอบอักษร บวก moire บนบริเวณ halftone ที่ตัวอย่างที่ถูกทิ้งบังเอิญแบกรูปแบบมาด้วย เรื่องนี้สำคัญกว่าใน PDF มากกว่าบนหน้าจอ เพราะความเสียหายเป็นถาวร image XObject แบกข้อมูลตัวอย่างไว้คู่กับ /Width, /Height และ /BitsPerComponent (ISO 32000-1 §8.9.5) และการรีแซมเปิลเขียนทับทั้งสามค่าภายในไฟล์ การซูมแย่ใน viewer เป็นเฟรมที่วาดใหม่ได้ และ PDFiumPas มีเครื่องจักรแยกสำหรับเรื่องนั้นใน render cache และประสิทธิภาพการซูม แต่การย่อที่พังคือเอกสารใหม่ฉบับหนึ่งที่คุณส่งให้ลูกค้า
PDFiumPas วัดรายละเอียดและเลือกเคอร์เนลอย่างไร
PDFiumPas ตัดสินใจต่อภาพ ไม่ใช่ต่อเอกสาร ก่อนเลือกเคอร์เนล มันคำนวณคะแนนรายละเอียดความสว่างแบบ normalized จากกริดสุ่มตัวอย่างที่มีขอบเขต: ก้าวแนวนอนกับแนวตั้งคือ (Width + 63) div 64 กับ (Height + 63) div 64 สแกน 12000 พิกเซลกับ thumbnail 300 พิกเซลจึงเสียค่าการกวาด 64 คูณ 64 ครั้งพอ ๆ กัน ทุกตำแหน่งที่สุ่มตัวอย่าง มันรวมค่าสัมบูรณ์ของผลต่างกับเพื่อนบ้านด้านขวาและด้านล่าง ครอบคลุมสูงสุดสามช่องสัญญาณ แล้วหารด้วยจำนวนตัวอย่างคูณ 255 คะแนนที่ได้อยู่ในช่วง 0 ถึง 1 กราฟิกธุรกิจเรียบ ๆ นั่งใกล้ศูนย์ พื้นผิวภาพถ่ายหนาแน่นไต่ขึ้น
บันไดคัดเลือกจากนั้นวิ่งตามลำดับตายตัว ถ้า ResampleFilter เป็นอย่างอื่นที่ไม่ใช่ pirfAdaptive ฟิลเตอร์นั้นถูกใช้ตามเดิม มิฉะนั้น: เนื้อหา 1-bit ได้ pirfBilevel; ContentClass เป็น piccLineArt ได้ pirfBox; scale factor ตั้งแต่ 4 ขึ้นไปได้ pirfBox เช่นกัน เพราะที่อัตราย่อระดับนั้นค่าเฉลี่ยพื้นที่ทั้งถูกที่สุดและถูกต้องที่สุดพร้อมกัน; piccPhoto, คะแนนรายละเอียด 0.08 ขึ้นไป หรือ PreferredQuality 0.9 ขึ้นไปได้ pirfLanczos พร้อมเคอร์เนลสาม lobe; scale 2 ขึ้นไปหรือคุณภาพ 0.7 ขึ้นไปได้ pirfBicubic รัศมี 2; ที่เหลือทั้งหมดได้ pirfBilinear เนื่องจาก TPdfImageOptimizeOptions.Default ตั้ง PreferredQuality เป็น 0.85 การรันด้วยค่า default จึงไม่ตกไปที่ bilinear เว้นแต่การย่ออ่อนโยนและเนื้อหาเรียบ
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, quality 0.85, งบ 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;
ภาพจะถูกแตะต้องเมื่อ DPI ของการวางที่ใหญ่กว่าระหว่างแนวนอนกับแนวตั้ง หารด้วย TargetDpi แล้วถึง MinDpiRatio กล่องป้องกันนี้มีไว้เพื่อไม่ให้ภาพถ่าย 160 DPI ที่เผชิญเป้าหมาย 150 DPI ถูกเขียนใหม่เพื่อกำไรหกเปอร์เซ็นต์ที่แลกมาด้วยคุณภาพหายหนึ่งเจเนอเรชัน ภาพที่เล็กกว่า MinDimension บนแกนใดแกนหนึ่ง โดย default คือ 8 ถูกข้ามในฐานะไอคอนหรือเส้นขีด
ทำไมโลโก้โปร่งใสจึงได้ขอบขาวติดมา?
เพราะสีใต้พิกเซลโปร่งใสเต็มที่เป็นค่าใดก็ได้ และค่าเฉลี่ยถ่วงน้ำหนักธรรมดายอมให้มันลงคะแนน ส่งออกโลโก้จากเครื่องมือออกแบบแล้วขอบมองไม่เห็นมักเป็นสีขาว หรือดำ หรืออะไรก็ตามที่ canvas เคยเป็น ช่อง alpha ซ่อนมันไว้ และการรวมตรง ๆ ทั่ว footprint ของเคอร์เนลผสมมันกลับเข้าขอบที่มองเห็นทันที PDFiumPas เลี่ยงสิ่งนี้โดยสะสมตัวอย่าง BGRA ในรูป premultiplied และไข premultiplication คืนเฉพาะที่พิกเซลปลายทาง
เจาะจงลงไป ตัวอย่างที่มีส่วนร่วมแต่ละตัวเติม channel * alpha * weight ลงตัวสะสมสี alpha * weight ลงตัวสะสม alpha และ weight ลงผลรวมน้ำหนัก สีปลายทางจากนั้นถูกหารด้วยตัวสะสม alpha มิใช่ผลรวมน้ำหนัก นั่นคือขั้นที่ชี้ขาด: หารด้วยผลรวมน้ำหนักจะลากสีไปหาพิกเซลมองไม่เห็น ขณะที่หารด้วย alpha ที่สะสมไว้สร้างคืนสีที่ตัวอย่างที่มองเห็นตกลงร่วมกันจริง alpha ปลายทางเป็นปริมาณแยกต่างหาก 255 * AlphaSum / WeightSum ฟอร์แมตที่ไม่มี alpha หารด้วยผลรวมน้ำหนักตามปกติ padding byte ของปลายทาง FPDFBitmap_BGRx เขียนเป็นค่าคงที่ 255 และทุกช่องสัญญาณถูก clamp เข้าช่วง 0 ถึง 255 ก่อนเก็บ alpha นั้นตามปกติมาจากรายการ soft mask ใน image dictionary (ISO 32000-1 §11.4) ซึ่ง PDFium composite เข้าบัฟเฟอร์ BGRA ที่รีแซมเปลอร์ได้รับไปแล้ว
// รูปร่างลูปสะสมชั้นใน ต่อตัวอย่างต้นทางที่มีส่วนร่วมหนึ่งตัว
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;
// ... และที่พิกเซลปลายทาง ไข premultiply คืนด้วยผลรวม 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;
กันไลน์อาร์ต 1-bit ออกจากเขตสีเทา
เคอร์เนลต่อเนื่องใด ๆ ที่วางลงบนสแกน bilevel ผลิตสีเทา และสีเทาคือสิ่งที่ภาพแบบแฟกซ์ไม่มีสิทธิมี PDFiumPas จึงปล่อยภาพ 1-bit ให้เป็นธรรมชาติโดย default: PreserveBilevel เป็น True ใน TPdfImageOptimizeOptions.Default และภาพกลุ่มนี้ตกลงใน SkippedCount โดยไม่ถูกแตะ ตั้งเป็น False แล้วเส้นทาง pirfBilevel เข้ามารับช่วงแทนเคอร์เนลทำให้เรียบ มันเดินสี่เหลี่ยมต้นทางเป๊ะ ๆ ที่ครอบพิกเซลปลายทางแต่ละตัว เฉลี่ยความสว่างด้วยน้ำหนัก 0.114, 0.587 กับ 0.299 ตามลำดับหน่วยความจำ BGR และ threshold ผลลัพธ์ที่ 127.5 เป็น 0 หรือ 255 แบน ๆ ไม่มีค่ากลางใดเขียนได้ ขอบจึงยังคมและไม่เกิดวงแหวนเทาล้อมขีดบาง ช่อง alpha ของแหล่ง BGRA ถูกเฉลี่ยตามปกติ และปลายทาง BGRx ได้ค่าคงที่ 255 ถ้าคุณต้องการพิกเซลต้นฉบับมากกว่าเอกสารที่เล็กลง การสกัดภาพจากเอกสาร PDF เป็นเส้นทางแยกต่างหาก
เกิดอะไรขึ้นเมื่อภาพเกินงบหน่วยความจำทำงาน?
มันถูกปล่อยไว้เป๊ะเหมือนเดิม และถูกนับ MaxWorkingBytes default 64 MiB ถูกบังคับสองรอบ ก่อนสร้าง bitmap ปลายทาง PDFiumPas ปฏิเสธภาพถ้ากว้างคูณสูงคูณ byte ต่อพิกเซลเกินงบ หลัง FPDFBitmap_CreateEx สำเร็จ มันตรวจอีกรอบด้วย stride จริงคูณความสูง เพราะ padding แถวผลักการจองข้ามลิมิตที่ผลคูณดิบ ๆ ผ่านได้ การปฏิเสธทั้งสองทำลายปลายทางและไม่คืนอะไรเลย ควรเข้าใจชัดว่าการเสื่อมสภาพแบบนี้หมายถึงอะไร: ภาพเกินงบไม่ถูกรีแซมเปิลที่คุณภาพต่ำลง และไม่ถูกแบ่งเป็น tile ต้นฉบับคงอยู่ในเอกสาร BudgetExceededCount กับ SkippedCount เพิ่มขึ้นทั้งคู่ และการรันหนึ่งครั้งจึงรายงานสำเร็จได้ทั้งที่เอกสารถูกปรับให้เหมาะเพียงบางส่วน นั่นเป็นพฤติกรรม fail-safe ที่ตั้งใจไว้ แต่มันหมายความว่ารายงานไม่ใช่สิ่งที่จะเลือกไม่อ่านก็ได้ รูปแบบล้มเหลวอื่นก็มี: ภาพที่ PDFium ผลิต bitmap ให้ไม่ได้เลย เช่น CMYK, JPX, JBIG2 หรือแหล่งที่มี mask เพิ่ม FailedCount แทนและถูกปล่อยไว้เช่นกัน
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // ใช้การลงคะแนนพื้นที่แบบ bilevel
Options.ContentClass := piccPhoto; // บังคับ Lanczos สำหรับชุดภาพถ่าย
Options.MaxWorkingBytes := 256 * 1024 * 1024; // เผื่อที่สำหรับสแกนใหญ่
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;
อ่านรายงานก่อนส่งไฟล์ออกไป
TPdfImageOptimizeReport ถูกออกแบบให้วินิจฉัยได้ ไม่ใช่แค่เขียนล็อก นอกจาก OptimizedCount, SkippedCount กับ FailedCount มันเปิดตัวนับหนึ่งตัวต่อเคอร์เนล BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount กับ BilevelFilterCount บอกว่ากฎ adaptive สรุปอะไรเกี่ยวกับคลังภาพของคุณจริง ๆ ผลลัพธ์ box ล้วนแปลว่าการย่อชันหรือเนื้อหาถูกจัดเป็นไลน์อาร์ต ผลลัพธ์ Lanczos ล้วนบนเอกสารที่คุณเชื่อว่าเป็นไลน์อาร์ตคือสัญญาณว่าควรตั้ง ContentClass อย่างชัดเจน AverageDetailScore คือตัวเลขที่ใช้เทียบกับ threshold 0.08 ของ Lanczos เวลาปรับ PreferredQuality และ PeakWorkingBytes แสดงว่าการรันต้องใช้ MaxWorkingBytes ไปเท่าไร ออปชันที่ไม่ถูกต้องล้มเหลวอย่างชัดเจนไม่ใช่เงียบ ๆ: TargetDpi ที่ไม่เป็นบวก, MinDpiRatio ต่ำกว่า 1, PreferredQuality นอกช่วง 0 ถึง 1 หรือ MaxWorkingBytes ที่ไม่เป็นบวก raise EPdfError ก่อนแตะหน้าใด ๆ และ OptimizeImages แก้เฉพาะเอกสารในหน่วยความจำ หน้าที่ถูกแก้แต่ละหน้าถูก commit ด้วย FPDFPage_GenerateContent หลังจากนั้นคุณยังต้องเรียก SaveAs เอง จะตรวจตาดูว่าอะไรเปลี่ยน ให้เรนเดอร์เอกสารก่อนกับหลังเป็น bitmap ตามที่อธิบายใน การแปลงหน้า PDF เป็นภาพ JPEG แล้วเทียบกันที่การซูมเต็ม
รีแซมเปิลแบบปรับตัวเป็นฟีเจอร์จำพวกที่มองไม่เห็นเมื่อมันทำงานถูก และผลิตตั๋วซัพพอร์ตเมื่อไม่ถูก นั่นคือเหตุที่การวัด การจัดการ alpha กับงบหน่วยความจำต้องมาถึงพร้อมกัน ไม่ใช่เป็นสามการปรับปรุงแยกกัน ถ้าคุณกำลังพิจารณาฟีเจอร์นี้สำหรับผลิตภัณฑ์ Delphi, C++Builder หรือ Lazarus API ฉบับเต็มกับรายละเอียดการให้สิทธิ์อยู่บน หน้า PDFiumPas Delphi PDFium component