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

การแยกรูปภาพจาก PDF ด้วย PDFium Component ใน Delphi

PDF จัดเก็บรูปภาพเป็นอ็อบเจ็กต์ระดับเฟิร์สคลาสภายในสตรีมเนื้อหา เมื่อหน้าเอกสารอ้างอิงถึงภาพถ่าย ภาพสแกน หรือแผนภาพ ข้อมูลพิกเซลจะอยู่ในพจนานุกรม XObject ควบคู่ไปกับรูปทรงเรขาคณิตของหน้าเอกสาร PDFium Component นำเสนอสิ่งนั้นผ่านคุณสมบัติสองประการใน TPdf ได้แก่ BitmapCount ซึ่งส่งคืนจำนวนบิตแมปที่ฝังอยู่ในหน้าปัจจุบัน และ Bitmap[Index] ซึ่งจะถอดรหัสหนึ่งในนั้นเป็น TBitmap ที่คุณเป็นเจ้าของและต้องปลดปล่อย (free) เอง นั่นคือรูปแบบการแยกข้อมูลทั้งหมด ลูปการทำงานมีเพียงสี่บรรทัด สิ่งที่ต้องพิจารณาคือองค์ประกอบแวดล้อมต่างๆ

การเปิดเอกสาร

สิ่งแรกที่ควรรู้เกี่ยวกับ TPdf คือ Active := True จะไม่ทำให้เกิดข้อผิดพลาด (exception) เสมอไป ความล้มเหลวในการโหลด รหัสผ่านไม่ถูกต้อง ไฟล์เสียหาย: ทั้งหมดนี้จะถูกกลืนหายไปภายใน และคอมโพเนนต์จะยังคงไม่ออกฤทธิ์ (inactive) คุณต้องตรวจสอบแฟล็กด้วยตัวเองหลังจากกำหนดค่า มิฉะนั้นคุณจะเข้าสู่ลูปของหน้าเอกสารโดยที่ PageCount ส่งคืนค่าเป็นศูนย์ และสงสัยว่าเหตุใดจึงไม่มีการแยกข้อมูลใดๆ ออกมา

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // ดำเนินการ extraction ต่อ
  finally
    Pdf.Free;
  end;
end;

ไฟล์ที่ป้องกันด้วยรหัสผ่านก็ใช้รูปแบบเดียวกัน: กำหนด Pdf.Password ก่อนตั้งค่า Active := True หากรหัสผ่านผิด Active จะยังคงเป็น False และคุณจะไม่ได้รับ exception ใดๆ ให้ดักจับ ในเครื่องมือประมวลผลแบบแบตช์ที่จัดการไฟล์หลายร้อยไฟล์ พฤติกรรมที่เงียบเช่นนี้เป็นประโยชน์อย่างยิ่ง: คุณรวบรวมความล้มเหลวไว้ในรายการแทนที่จะต้องคลาย call stack สำหรับแต่ละไฟล์

การวนซ้ำหน้าเอกสารและการดึงบิตแมป

BitmapCount เป็นค่าต่อหน้า ดังนั้นคุณต้องตั้งค่า Pdf.PageNumber ก่อนอ่านค่า หมายเลขหน้าจะเริ่มต้นที่ 1; ค่าเริ่มต้นคือ 0 ซึ่งหมายความว่าไม่มีการโหลดหน้าเอกสาร คุณสมบัติ Bitmap[Index] จะเริ่มต้นที่ 0 และส่งคืน TBitmap ที่เป็นของผู้เรียก (caller-owned) คุณต้องปลดปล่อยมัน หากละเลยการปลดปล่อยหน่วยความจำภายในลูปยาวๆ ที่ทำงานกับเอกสารขนาดใหญ่ หน่วยความจำจะเพิ่มขึ้นอย่างรวดเร็ว เนื่องจากบิตแมปแต่ละภาพอาจมีข้อมูลพิกเซลดิบหลายเมกะไบต์ก่อนที่จะมีการบีบอัดใดๆ

ลูปสกัดรูปภาพ PDF ใน Delphi ด้วย PDFium ที่ตรวจ flag Active แบบเงียบ, ตั้ง PageNumber ทุกหน้า, อ่าน BitmapCount, ข้ามบิตแมป nil และคืน TBitmap แต่ละตัวหลังบันทึก
ลูปอ่านจำนวนเฉพาะหลังเคอร์เซอร์หน้าขยับ ข้ามบิตแมป nil จาก XObject ที่ผิดรูป และคืน TBitmap ที่ผู้เรียกเป็นเจ้าของแต่ละตัวก่อนดึงตัวถัดไป
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

การตรวจสอบ Assigned เป็นสิ่งสำคัญ เครื่องมือสร้าง PDF จำนวนน้อยเขียนภาพ XObjects ด้วยขนาดพิกเซลที่เป็นศูนย์ หรือมีข้อมูลที่ผิดรูปแบบ; ในกรณีเหล่านั้น คอมโพเนนต์จะส่งคืน nil แทนบิตแมปที่ว่างเปล่า การพิจารณาว่าผลลัพธ์ nil เป็นข้อผิดพลาดและหยุดการแยกข้อมูลเป็นปฏิกิริยาที่ผิด: ให้ข้ามไป บันทึกหน้าและดัชนีหากคุณต้องการร่องรอยการตรวจสอบ และดำเนินการต่อ ส่วนที่เหลือของหน้าอาจยังคงให้รูปภาพที่ถูกต้อง

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

การเลือกรูปแบบผลลัพธ์

BMP ไม่สูญเสียความละเอียด (lossless) และใช้งานได้เสมอโดยไม่ต้องใช้ยูนิตเพิ่มเติม ซึ่งทำให้มันเป็นค่าเริ่มต้นที่เหมาะสมเมื่อคุณยังไม่ทราบว่ารูปภาพประกอบด้วยอะไร เมื่อขนาดไฟล์เป็นสิ่งสำคัญ รูปแบบพิกเซลของ TBitmap ที่ส่งคืนจะบอกคุณว่าตัวแปลงสัญญาณ (codec) ใดเหมาะสม บิตแมป 32 บิตจะมีช่องอัลฟา (alpha channel); PNG จะรักษาสิ่งนั้นไว้โดยไม่สูญเสียความละเอียด ภาพถ่าย 24 บิตขนาดใหญ่ที่มีโทนสีต่อเนื่องเหมาะสมกับการเป็น JPEG ภาพขนาดเล็กหรือภาพที่วาดด้วยจานสีที่จำกัดโดยทั่วไปควรปล่อยไว้เป็น BMP ดีกว่าการแปลงเป็น JPEG ซึ่งจะเพิ่มสิ่งแปลกปลอมแบบบล็อก (blocking artifacts) ในการตั้งค่าคุณภาพต่ำ และประหยัดพื้นที่ได้น้อยมากในการตั้งค่าคุณภาพสูง

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: lossless, no extra units
  end;
end;

ในทางปฏิบัติ การเลือกรูปแบบจะพิจารณาจาก Bmp.PixelFormat และขนาด หาก PixelFormat = pf32bit คุณต้องมีรูปแบบที่รองรับอัลฟา; PNG เป็นทางเลือกที่ชัดเจน แม้ว่าจะต้องใช้ยูนิต PNGImage ใน Delphi เวอร์ชันเก่าก็ตาม สำหรับภาพถ่าย 24 บิตที่มีความกว้างมากกว่า 300 พิกเซลโดยประมาณ การใช้ JPEG ที่คุณภาพ 85 จะลดขนาดลงแบบสามต่อหนึ่งเมื่อเทียบกับ BMP โดยไม่สูญเสียความคมชัดอย่างเห็นได้ชัดในเนื้อหาภาพถ่ายส่วนใหญ่ หากต่ำกว่าเกณฑ์นั้น BMP จะมีขนาดใกล้เคียงกัน และหลีกเลี่ยงการตัดสินใจเรื่องคุณภาพไปได้โดยสิ้นเชิง

สิ่งที่ BitmapCount นับและไม่นับ

PDF แยกความแตกต่างระหว่างภาพ XObjects และกราฟิกเวกเตอร์ที่วาดด้วยตัวดำเนินการเส้นทาง (path operators) หน้าที่ดูมีความซับซ้อนทางสายตาอาจส่งคืนค่า BitmapCount เป็นศูนย์ หากทุกองค์ประกอบเป็นเวกเตอร์ หน้าที่สแกนเกือบจะส่งคืนค่าเป็นหนึ่งเสมอ: เครื่องสแกนจะเขียนการสแกนทั้งหมดเป็นภาพ XObject แบบเต็มหน้าภาพเดียว ตามความละเอียดที่ตั้งค่าไว้ในเครื่องสแกน หน้าที่ผสมระหว่างข้อความที่จัดพิมพ์และภาพถ่ายที่ฝังไว้จะส่งคืนหนึ่งรายการต่อหนึ่งภาพถ่าย เส้นประดับ พื้นหลังที่มีการแรเงา และเส้นขอบตารางมักจะไม่ปรากฏในการนับบิตแมปเลย

BitmapCount ครอบคลุมอะไรบนหน้า PDF ใน PDFium โดยนับ image XObject เช่น ภาพสแกนกับภาพถ่าย ขณะที่ข้อความเวกเตอร์, เส้นคั่น, การระบายแบบมีเฉด และ inline image อยู่นอกการนับ
หน้าที่มาจากการสแกนคืน image XObject เต็มหน้าหนึ่งตัวพอดี ในขณะที่หน้าเวกเตอร์ล้วนอาจดูพลุกพล่านแต่คืนจำนวนเป็นศูนย์

การนับนี้ยังไม่รวมภาพแบบอินไลน์ (inline images) ซึ่งเป็นโครงสร้าง PDF ที่ไม่ค่อยมีการใช้งาน ซึ่งข้อมูลภาพจะฝังอยู่ในสตรีมเนื้อหาของหน้าโดยตรง แทนที่จะเป็น XObject ที่มีชื่อ สิ่งเหล่านี้อยู่นอกเหนือจากที่ API นี้นำเสนอ; มันเป็นสิ่งที่ไม่พบบ่อยในเอกสารจริงจนทำให้เครื่องมือดึงข้อมูลส่วนใหญ่ไม่ได้จัดการกับมัน

รายละเอียดหนึ่งที่ควรจดจำไว้: BitmapCount ที่คุณอ่านคือของหน้าปัจจุบัน ณ ตอนที่มีการกำหนด PageNumber ครั้งล่าสุด หากโค้ดของคุณแยกสาขาหรือเรียกใช้ฟังก์ชันใดๆ ที่เปลี่ยนแปลง PageNumber ระหว่างการนับและการดึงข้อมูล คุณอาจอ่านจำนวนรูปภาพได้น้อยกว่าพื้นที่ที่คุณจัดสรรไว้ หรือมีดัชนีที่เกินขอบเขต ควรให้อ่านการนับและวนลูป Bitmap[] ให้อยู่ในหน้าเดียวกันโดยไม่ต้องเปลี่ยน PageNumber ในระหว่างนั้น

การใช้ TPdfView ในแอปพลิเคชันฟอร์ม

คอมโพเนนต์ TPdfView นำเสนอคุณสมบัติ BitmapCount และ Bitmap[] เดียวกัน แต่หน้าที่อ่านค่ามานั้นคือหน้าที่แสดงผลอยู่ในปัจจุบันของวิวเวอร์ ไม่ใช่ TPdf.PageNumber พอยน์เตอร์ของหน้าทั้งสองนั้นเป็นอิสระต่อกัน; การตั้งค่าตัวใดตัวหนึ่งจะไม่ไปขยับอีกตัวหนึ่ง ในแอปพลิเคชัน VCL ฟอร์มที่มีไลฟ์วิวเวอร์ คุณสามารถเรียก Pdf.PageNumber := N เพื่อสั่งให้แยกข้อมูลผ่าน TPdf ในขณะที่วิวเวอร์ยังคงอยู่ในตำแหน่งล่าสุดที่ผู้ใช้เลื่อนหน้าจอ การแยกส่วนนี้มีความตั้งใจและช่วยรักษาสถานะการแสดงผลของวิวเวอร์ให้สะอาดในขณะที่การดึงข้อมูลทำงานอยู่ในเบื้องหลัง

หน่วยความจำและประสิทธิภาพในงานแบตช์ (batch jobs)

ในคลังข้อมูลขนาดใหญ่ สิ่งสำคัญที่ต้องระวังคืองบประมาณหน่วยความจำ การเรียกใช้ Bitmap[] แต่ละครั้งจะจัดสรร TBitmap ใหม่บน heap และในหน้าที่สแกนที่ 300 DPI ซึ่งสามารถมีข้อมูลพิกเซลดิบได้ถึง 25 MB ก่อนการเข้ารหัสใดๆ ได้อย่างง่ายดาย หากคุณประมวลผลหน้าในลูปที่แคบโดยไม่ปลดปล่อยหน่วยความจำในระหว่างการวนซ้ำ ขนาดหน่วยความจำที่ใช้งานจะเพิ่มขึ้นแบบเชิงเส้นตามจำนวนรูปภาพ รูปแบบที่ถูกต้องควรเป็นเสมอ: ดึงบิตแมปมาหนึ่งรูป, ทำในสิ่งที่คุณต้องการ, ปลดปล่อยมัน, ดึงรูปถัดไป หากคุณจำเป็นต้องเก็บตัวอ้างอิงถึงหลายบิตแมปพร้อมกันสำหรับขั้นตอนการเปรียบเทียบ ให้นับด้วย BitmapCount ก่อน และจัดสรรคอนเทนเนอร์ของคุณตามลำดับ จากนั้นปลดปล่อยแต่ละบิตแมปทันทีที่คุณใช้งานเสร็จ แทนที่จะเลื่อนไปทำความสะอาดเมื่อสิ้นสุดเอกสาร ในเอกสารที่สแกน 500 หน้า ความแตกต่างนั้นอาจหมายถึงความแตกต่างระหว่างการใช้หน่วยความจำสูงสุด 25 MB และ 12 GB RSS

คุณสมบัติ BitmapCount และ Bitmap[] ที่แสดงไว้ที่นี่เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder