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');
// proceed to 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) คุณต้องปลดปล่อยมัน หากละเลยการปลดปล่อยหน่วยความจำภายในลูปยาวๆ ที่ทำงานกับเอกสารขนาดใหญ่ หน่วยความจำจะเพิ่มขึ้นอย่างรวดเร็ว เนื่องจากบิตแมปแต่ละภาพอาจมีข้อมูลพิกเซลดิบหลายเมกะไบต์ก่อนที่จะมีการบีบอัดใดๆ
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 แบบเต็มหน้าภาพเดียว ตามความละเอียดที่ตั้งค่าไว้ในเครื่องสแกน หน้าที่ผสมระหว่างข้อความที่จัดพิมพ์และภาพถ่ายที่ฝังไว้จะส่งคืนหนึ่งรายการต่อหนึ่งภาพถ่าย เส้นประดับ พื้นหลังที่มีการแรเงา และเส้นขอบตารางมักจะไม่ปรากฏในการนับบิตแมปเลย
การนับนี้ยังไม่รวมภาพแบบอินไลน์ (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