บทความเทคนิค

ลดขนาดไฟล์ PDF ใน Delphi: ฟอนต์, รูปภาพ, LZW

สำหรับการลดขนาดไฟล์ PDF ใน Delphi ไลบรารี losLab PDF Library ได้เตรียม API สามตัวเพื่อจัดการกับแหล่งข้อมูลที่ทำให้ขนาดไฟล์บวมมากที่สุดสามจุด: SubsetEmbeddedFonts จะเขียนโปรแกรมฟอนต์ TrueType ที่ฝังไว้ใหม่ทั้งหมดให้เหลือเฉพาะ glyph ที่เอกสารใช้แสดงผลจริง, DownsampleImages จะลดขนาดรูปภาพราสเตอร์ที่มีความละเอียดเกินเป้าหมาย DPI และ NormalizeLZWStreams จะเปลี่ยนการบีบอัด LZWDecode แบบดั้งเดิมเป็น FlateDecode ฟังก์ชันแต่ละตัวจะส่งกลับจำนวนวัตถุที่มันได้เปลี่ยนแปลง ดังนั้นการส่งกลับค่าศูนย์จะเป็นการบอกคุณว่าขั้นตอนนี้ไม่มีความจำเป็นต้องทำงาน แทนที่จะเป็นการล้มเหลวอย่างเงียบๆ

ทำไมไฟล์ PDF ที่รวมกันจึงมีขนาดใหญ่กว่าไฟล์ต้นฉบับ?

ไฟล์ PDF ที่นำมารวมกันหรือที่สร้างขึ้นผ่านโปรแกรมมักจะมีขนาดใหญ่เกินความจำเป็นด้วยเหตุผลหลักสามประการ: การฝังฟอนต์เต็มรูปแบบ, รูปภาพที่สุ่มตัวอย่างความละเอียดสูงกว่าขนาดการแสดงผลจริงอย่างมาก และสตรีมข้อมูลที่ยังคงบีบอัดด้วยตัวกรอง LZW แบบดั้งเดิม ข้อกำหนด ISO 32000-1 §9.9 อนุญาตให้ผู้สร้างไฟล์ฝังโปรแกรมฟอนต์ฉบับสมบูรณ์ได้ และผู้ผลิตส่วนใหญ่ก็เลือกที่จะทำเช่นนั้นเพราะเป็นค่าเริ่มต้นที่ปลอดภัย ไฟล์ FontFile2 ของ Arial ที่สมบูรณ์จะมีขนาดหลายร้อยกิโลไบต์ หากคุณฝังมันลงในไฟล์ต้นฉบับหนึ่งโหลแล้วนำมารวมกัน คุณก็จะต้องแบกรับสำเนารูปร่าง glyph ถึงสิบสองชุดสำหรับอักขระที่ไม่มีใครพิมพ์เลย การรวมไฟล์ด้วยตัวเองไม่ได้สร้างข้อมูลที่เสียเปล่าขึ้นมา แต่มันทำหน้าที่รวบรวมของเสียเหล่านั้นเข้ามาไว้ในไฟล์เดียวจนกระทั่งยอดรวมมีขนาดใหญ่จนสังเกตเห็นได้ชัดเจน

รูปภาพคือตัวการตัวที่สอง ภาพสแกนที่มีความกว้าง 4800 พิกเซลซึ่งวางอยู่ในกรอบขนาดหนึ่งส่วนสี่หน้ากระดาษจะเก็บข้อมูลพิกเซลมากกว่าที่กระบวนการพิมพ์ขนาด 300 DPI ต้องการใช้ถึงประมาณ 40 เท่า ตัวการตัวที่สามจะเงียบกว่า นั่นคือสตรีมข้อมูลที่กรองด้วย LZWDecode โดย ISO 32000-1 §7.4.4 กำหนดให้มีทั้ง LZWDecode และ FlateDecode และระบุว่า Flate มักจะบีบอัดข้อมูลได้ดีกว่าหรือเท่ากัน ในทางปฏิบัติ ผลลัพธ์จาก Flate มักจะมีขนาดเล็กกว่าอย่างสม่ำเสมอในข้อมูลชุดเดียวกัน และการบีบอัดแบบ LZW มักจะหลงเหลืออยู่ในไฟล์ที่ผ่านการสร้างด้วยเครื่องมือในยุคทศวรรษ 1990 ส่วนที่เหลือของบทความนี้จะอธิบายขั้นตอนการซ่อมแซมปัญหาแต่ละจุดด้วย losLab PDF Library จากนั้นจะรวมขั้นตอนทั้งหมดเข้าเป็นไพป์ไลน์เดียวกัน

การทำ subset ฟอนต์ด้วย SubsetEmbeddedFonts

ฟังก์ชัน SubsetEmbeddedFonts จะลดขนาดฟอนต์ TrueType ที่ฝังไว้ทุกตัวในเอกสารที่โหลดเข้ามาให้เหลือเฉพาะอักขระที่เอกสารใช้งานจริง โดยไม่จำเป็นต้องส่งอาร์กิวเมนต์ใดๆ เนื่องจากมันจะสร้างรายการอักขระที่ต้องเก็บไว้จากสตรีมเนื้อหาโดยตรง การทำงานภายในจะไล่ดูสตรีมเนื้อหาของทุกหน้าด้วย GetTextRuns เพื่อรวบรวมรหัสอักขระที่อ้างอิงภายใต้แต่ละทรัพยากรฟอนต์ สร้างรายการเก็บรักษา และส่งโปรแกรมฟอนต์ดั้งเดิมไปยังเอนจิน FontSub ของ Windows (CreateFontPackage) เพื่อสร้างชุดย่อย (subset) โปรแกรมที่เขียนขึ้นใหม่จะเข้าไปแทนที่สตรีม FontFile2 ในตำแหน่งเดิม และชื่อ BaseFont จะได้รับแท็ก LOSABC+ ซึ่งเป็นข้อตกลงในการใช้ตัวอักษรภาษาอังกฤษตัวพิมพ์ใหญ่หกตัวตามด้วยเครื่องหมายบวกตามที่ ISO 32000-1 §9.6.4 กำหนดไว้สำหรับฟอนต์ชุดย่อย คำนำหน้านี้ยังช่วยให้การเรียกใช้งานมีสมบัติเปรียบเสมือนเดิม (idempotent): แม้คุณจะรันคำสั่งนี้สองครั้ง ฟอนต์ที่ผ่านการทำ subset ไปแล้วจะถูกตรวจพบและข้ามไป ดังนั้นจึงปลอดภัยในการนำไปใช้ในงานแบบแบทช์ที่อาจต้องดำเนินการกับไฟล์ซ้ำอีกครั้ง

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = จำนวนโปรแกรม FontFile2 ที่เขียนใหม่;
      // 0 หมายถึงไม่มีการฝังฟอนต์ หรือทั้งหมดทำ subset ไปแล้ว
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

มีรายละเอียดการใช้งานสองประการที่น่าสนใจเพื่อช่วยให้เข้าใจขอบเขตของ API นี้ ประการแรก ขั้นตอนนี้จะมุ่งเป้าไปที่ FontFile2 ซึ่งครอบคลุมโปรแกรม TrueType ที่ฝังไว้ ส่วนฟอนต์ที่ฝังในรูปแบบ Type 1 หรือ CFF ดิบจะไม่มีการแตะต้องเพื่อความปลอดภัย ประการที่สอง ฟังก์ชันนี้ต้องพึ่งพา FontSub ซึ่งทำให้ SubsetEmbeddedFonts ใช้งานได้เฉพาะบนระบบปฏิบัติการ Windows เท่านั้น จุดที่ละเอียดอ่อนอีกประการคือ การตัดสินใจว่าฟอนต์ใดมีคุณสมบัติที่เหมาะสมจะพิจารณาจากการไล่ตรวจสอบสายโซ่อ้างอิงของ FontDescriptorFontFile2 จริงๆ แทนที่จะเชื่อถือเกณฑ์การประเมินจากแฟล็กการฝังตัว เนื่องจากฟอนต์ในเอกสารที่โหลดขึ้นมาไม่เคยผ่านขั้นตอนการบันทึกฝั่งผู้เขียนที่จะเป็นตัวกำหนดแฟล็กเหล่านั้น หากสตรีมที่ตรวจสอบมีอยู่จริง ฟอนต์นั้นก็จะถือเป็นผู้เข้าเกณฑ์ แต่หากไม่มีก็จะถูกข้ามไปโดยไม่มีข้อผิดพลาดใดๆ

ข้อแลกเปลี่ยนที่แท้จริงคือ ฟอนต์ชุดย่อยจะเก็บเฉพาะ glyph ที่มีอยู่ ณ เวลาทำ subset เท่านั้น หากเครื่องมือถัดไปในระบบหรือรหัสของคุณเองทำการเพิ่มข้อความโดยใช้ฟอนต์เดียวกันนี้ในภายหลัง อักขระใดๆ ที่อยู่นอกเหนือฟอนต์ชุดย่อยจะไม่มีรูปร่างโครงร่างอักขระและจะแสดงผลเป็น glyph ที่ขาดหายไป ดังนั้นการทำ subset ควรเป็นขั้นตอนสุดท้ายหลังจากแก้ไขเนื้อหาเสร็จแล้ว และไม่ควรทำก่อนขั้นตอนการแก้ไขข้อความเด็ดขาด ความระมัดระวังแบบเดียวกันนี้ยังใช้หากคุณวางแผนที่จะดึงฟอนต์กลับมาใช้งานใหม่ในภายหลัง บทความเกี่ยวกับการแยกข้อความ รูปภาพ และฟอนต์ด้วย PDFlibPas จะครอบคลุมถึงสิ่งที่คุณจะได้รับและไม่ได้รับจากโปรแกรมฟอนต์ชุดย่อยที่สกัดออกมา

DownsampleImages ตัดสินใจเลือกรูปภาพที่จะลดขนาดอย่างไร?

ฟังก์ชัน DownsampleImages(MaxDPI, Quality, Filter) จะปรับขนาดเฉพาะรูปภาพที่ระบุได้อย่างมั่นใจว่ามีความละเอียดสูงเกินความจำเป็นเท่านั้น โดยใช้วิธีการประเมิน DPI ที่ค่อนข้างปลอดภัย วัตถุรูปภาพ PDF XObject จะจัดเก็บมิติข้อมูลพิกเซล แต่ไม่มีข้อมูลความละเอียดทางกายภาพที่เชื่อถือได้ และแท็ก DPI จากรูปภาพต้นฉบับมักจะไม่หลงเหลือหลังจากกระบวนการโหลด-แก้ไข-บันทึก ดังนั้นฟังก์ชันนี้จึงใช้วิธีประเมินค่า SrcDPI = PixelWidth / 8.5 ซึ่งมีหลักการประเมินว่า: หากภาพนี้ขยายเต็มหน้ากระดาษขนาด Letter ความละเอียดของมันจะเป็นเท่าใด? มีเพียงภาพที่ประเมินแล้วสูงกว่า MaxDPI เท่านั้นที่จะได้รับการประมวลผล วิธีการนี้เป็นการออกแบบโดยตั้งใจ: รูปภาพที่วางไว้เป็นขนาดเล็กบนหน้ากระดาษจะมีค่า DPI จริงที่สูงกว่าค่าประเมิน ดังนั้นฟังก์ชันนี้จะเลือกทำงานน้อยกว่าปกติ ดีกว่าไปลดคุณภาพของรูปภาพที่เหมาะสมกับการพิมพ์ซึ่งตัวระบบไม่สามารถวัดค่าได้

พารามิเตอร์ Quality ตั้งแต่ 1 ถึง 100 จะเลือกคุณภาพการเข้ารหัส JPEG ใหม่ ในขณะที่ 0 จะคงเอาต์พุตไว้เป็น Flate แบบไม่สูญเสียข้อมูลเช่นเดียวกับ PNG ส่วนพารามิเตอร์ Filter จะเลือกวิธีการปรับขนาด โดย 0 สำหรับการหาค่าเฉลี่ยแบบกล่อง (box average) และ 1 สำหรับไบลิเนียร์ (bilinear) สำหรับเอกสารสำนักงานที่ผ่านการสแกน DownsampleImages(150, 75, 1) ถือเป็นจุดเริ่มต้นที่สมเหตุสมผล แต่สำหรับงานใดๆ ที่อาจต้องนำไปสั่งพิมพ์ใหม่ ให้ปรับ MaxDPI เพิ่มขึ้นเป็น 300 หรือข้ามขั้นตอนนี้ไปเลย การลดขนาดภาพ (downsampling) เป็นเพียงขั้นตอนเดียวในสามขั้นตอนนี้ที่มีการสูญเสียข้อมูล (lossy) ดังนั้นจึงควรจัดทำเป็นตัวเลือกที่ผู้ใช้สามารถปิดการใช้งานได้

การแปลงสตรีม LZW แบบเก่าด้วย NormalizeLZWStreams

ฟังก์ชัน NormalizeLZWStreams เป็นความสามารถที่ให้ผลลัพธ์คุ้มค่าอย่างยิ่ง: มันจะคลายการบีบอัดข้อมูลสตรีม LZWDecode ทุกตัวโดยไม่มีการสูญเสียข้อมูล แล้วบีบอัดใหม่ด้วย FlateDecode ในตำแหน่งเดิม พร้อมส่งกลับจำนวนสตรีมที่ถูกแปลง ฟังก์ชันนี้รองรับทั้งรายการ /Filter /LZWDecode รายการเดียว และกรณีที่ LZW ปรากฏอยู่ภายในอาร์เรย์ของห่วงโซ่ตัวกรอง (filter chain) โดยจะเปลี่ยนเฉพาะจุดที่เป็น LZW และคงส่วนอื่นๆ ของห่วงโซ่ไว้ ค่าพารามิเตอร์ของตัวคาดการณ์ (เช่น Predictor, Columns, Colors, BitsPerComponent) จะถูกอ่านจาก DecodeParms ของสตรีมและส่งผ่านไปยังตัวคลายการบีบอัดข้อมูล เพื่อให้ข้อมูลรูปภาพที่เข้ารหัสด้วยตัวคาดการณ์ยังคงทำงานได้อย่างถูกต้อง เนื่องจากตัวกรองทั้งสองเป็นตัวแปลงแบบตรงกันทุกบิต (bit-exact) ไบต์ที่คลายข้อมูลออกมาจึงมีความเหมือนกันทุกประการทั้งก่อนและหลังทำกระบวนการ มีเพียงการบีบอัดข้อมูลของคอนเทนเนอร์เท่านั้นที่เปลี่ยนไป นี่เป็นเหตุผลว่าทำไมขั้นตอนนี้จึงมีความปลอดภัยสำหรับการรันโดยไม่มีเงื่อนไขกับไฟล์ทุกตัว

สำหรับเอกสารที่ไม่มีสตรีม LZW การเรียกใช้งานนี้จะส่งกลับค่า 0 และไม่ทำอะไรเลย ซึ่งชุดตรวจสอบการทำงานถดถอยของไลบรารีได้ทำการทดสอบเรื่องนี้ไว้อย่างชัดเจน: ไฟล์ที่เพิ่งสร้างขึ้นใหม่ซึ่งใช้เฉพาะ Flate จะต้องรายงานจำนวนการแปลงเป็นศูนย์ การรับประกันว่าไม่มีการแก้ไขใดๆ เมื่อไม่มีเงื่อนไขที่ตรงกันมีความสำคัญมากเมื่อกระบวนการนี้ทำงานอยู่ในไพป์ไลน์ที่ต้องประมวลผลไฟล์หลากหลายรูปแบบเป็นจำนวนมาก ซึ่งบางไฟล์อาจมาจากปี 2024 และบางไฟล์มาจากปี 1998

ไพป์ไลน์การเพิ่มประสิทธิภาพขนาดไฟล์ที่สมบูรณ์ใน Delphi

ฟังก์ชันการเพิ่มประสิทธิภาพทั้งสามตัวจะรวมเข้ากับฟังก์ชันการโหลด-เพิ่มประสิทธิภาพ-บันทึก เพียงฟังก์ชันเดียว และลำดับขั้นตอนนี้มีความสำคัญน้อยกว่าที่คุณคิด เนื่องจากแต่ละฟังก์ชันจะทำงานกับชนิดออบเจกต์ที่แยกจากกันโดยเด็ดขาด: ฟอนต์ ออบเจกต์รูปภาพ และฟิลเตอร์สตรีม การรันการทำ subset ก่อนเป็นทางเลือกที่เรียบร้อยกว่า เนื่องจากเป็นขั้นตอนที่มีข้อจำกัดเรื่องลำดับการแก้ไข

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;
    // บันทึก Fonts/Images/Streams: ค่าศูนย์สามตัวหมายถึงไฟล์มีขนาดเล็กอยู่แล้ว
  finally
    Lib.Free;
  end;
end;

ตรวจสอบความถูกต้องของไพป์ไลน์ในแบบที่ตัวไลบรารีตรวจสอบความถูกต้องของตัวเอง: แบบไป-กลับ ชุดการตรวจสอบถดถอยเวอร์ชัน v3.130 จะทำหน้าที่สร้างเอกสาร บันทึก โหลดซ้ำ รันการเพิ่มประสิทธิภาพ บันทึกอีกครั้ง แล้วจึงยืนยันคุณสมบัติสามประการ: ไฟล์ผลลัพธ์มีขนาดเล็กลง ค่าจำนวนการแปลงเป็นไปตามความคาดหมาย และการโหลดไฟล์เพิ่มประสิทธิภาพใหม่ยังสามารถแยกวิเคราะห์และเรนเดอร์ได้ดี การทำลูปสร้าง-เพิ่มประสิทธิภาพ-โหลดซ้ำซ้ำๆ แบบนั้นกับตัวอย่างไฟล์จริงของคุณ และเปรียบเทียบข้อความที่คัดแยกออกมาก่อนและหลังทำกระบวนการ ถือเป็นการลงทุนเพียงหนึ่งชั่วโมงที่จะป้องกันความผิดพลาดจากการรวมระบบได้ก่อนที่ลูกค้าจะพบกับปัญหาใบแจ้งหนี้ชำรุด

// ตรวจสอบการทำงานแบบไป-กลับ: ไฟล์ที่เพิ่มประสิทธิภาพแล้วต้องยังโหลดได้ตามปกติ
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

ไพป์ไลน์นี้เหมาะสมที่จะอยู่ตรงส่วนใดของกระบวนการรวมไฟล์? คำตอบคือหลังจากการรวมไฟล์ ไม่ใช่ระหว่างกระบวนการรวม การรวมไฟล์ก่อนแล้วจึงเพิ่มประสิทธิภาพของเอกสารผลลัพธ์เพียงตัวเดียวหมายความว่าฟอนต์ที่ฝังแต่ละตัวจะถูกทำ subset เพียงครั้งเดียวเทียบกับอักขระที่ใช้งานร่วมกันทั้งหมด แทนที่จะทำแยกเป็นรายไฟล์ต้นฉบับ หากความเร็วของกระบวนการรวมไฟล์คือคอขวด PDFlibPas ได้จัดเตรียมช่องทางด่วนระดับไบต์ซึ่งจะหลีกเลี่ยงการแยกวิเคราะห์วัตถุเต็มรูปแบบ ซึ่งอธิบายไว้ในบทความเกี่ยวกับการรวม PDF อย่างรวดเร็วด้วยการเลื่อนตำแหน่งอ้างอิงไบต์ และสำหรับกรณีที่เอกสารนำเข้ามีขนาดใหญ่เกินกว่าจะเก็บในหน่วยความจำได้ การรวมและการแยกไฟล์ขนาดใหญ่ด้วยการเข้าถึงตรง จะครอบคลุมถึงเส้นทางการสตรีมข้อมูล ซึ่งทั้งสองวิธีสามารถนำมาจับคู่กับขั้นตอนสุดท้ายในการเพิ่มประสิทธิภาพขนาดไฟล์เอกสารผลลัพธ์ได้อย่างลงตัว

ข้อจำกัดของกระบวนการทั้งสามตัว

ฟังก์ชันการเพิ่มประสิทธิภาพทั้งสามตัวของ losLab PDF Library จะละเว้นกระบวนการใดๆ ที่ส่งผลให้ตรรกะของเอกสารเปลี่ยนไปอย่างตั้งใจ ฟังก์ชัน SubsetEmbeddedFonts จะไม่มีการรวมฟอนต์ที่ซ้ำกันจากการรวมไฟล์จากแหล่งข้อมูลต่างๆ เข้าเป็นโปรแกรมเดียวกัน แต่มันจะทำหน้าที่ลดขนาดของแต่ละตัวแยกกัน การลดความซ้ำซ้อน (deduplication) เป็นกระบวนการที่แตกต่างและมีความเสี่ยงสูงกว่า ฟังก์ชัน DownsampleImages จะข้ามภาพที่ประเมิน DPI ต่ำกว่าเกณฑ์ที่กำหนดไว้ แม้ว่าผู้ใช้จะสังเกตได้ว่ารูปภาพนั้นใหญ่เกินกรอบแสดงผล และไม่มีขั้นตอนใดเข้าไปยุ่งเกี่ยวกับโครงสร้างของเอกสาร ดังนั้นไฟล์ที่บวมขึ้นเนื่องจากออบเจกต์ที่ไม่มีเจ้าของ (orphaned object) นับพันตัวจึงจำเป็นต้องได้รับการบันทึกแบบเขียนทับใหม่ทั้งหมด แทนที่จะใช้กระบวนการระดับสตรีมเหล่านี้ ภายใต้ข้อจำกัดดังกล่าว การผสมผสานระหว่างการทำ subset ของฟอนต์ การลดขนาดรูปภาพ และการเปลี่ยน LZW เป็น Flate จะช่วยขจัดสามตัวการหลักของปัญหาไฟล์ PDF บวมด้วยการเรียกใช้งาน API ที่คาดเดาผลลัพธ์ได้ ฟังก์ชันทั้งสามนี้มาพร้อมกับ losLab PDF Library สำหรับ Delphi, C# และ VB.NET ควบคู่ไปกับ API ด้านการรวมไฟล์ การแยกข้อมูล และการเรนเดอร์เอกสารตามที่กล่าวถึงข้างต้น