การแยกข้อความ PDF ดูเหมือนง่ายจนกระทั่งคุณพบเอกสารที่ไม่มีเลเยอร์ข้อความ เสียหาย หรือแยกส่วนกระจายอยู่ตามลำดับตัวอักษรขนาดเล็กหลายสิบกลุ่มโดยไม่มีลำดับที่มีความหมาย PDFium Component ให้จุดเข้าใช้งานสองจุดแก่คุณ: อาร์เรย์ Character[] สำหรับการเข้าถึงไกลฟ์ (glyph) แต่ละตัวบนหน้าตามดัชนีดิบ และ ReadablePageContent สำหรับมุมมองที่มีโครงสร้างซึ่งสร้างย่อหน้าและหัวเรื่องขึ้นใหม่จากแผนผังแท็กของ PDF หรือการวิเคราะห์ฮิวริสติก ไม่มีตัวเลือกใดที่เป็นคำตอบที่ถูกต้องเสมอไป ดังนั้นการทำความเข้าใจสิ่งที่แต่ละตัวแสดงออกมาจึงเป็นเรื่องสำคัญ
การเปิดเอกสารและกับดักความล้มเหลวแบบเงียบ
TPdf จะเปิดไฟล์โดยกำหนดค่า FileName และเปลี่ยน Active := True รายละเอียดที่สำคัญคือ: Active := True จะไม่มีการส่งข้อยกเว้นออกไป หากไม่มีไฟล์ดังกล่าวอยู่ ได้รับการป้องกันด้วยรหัสผ่าน หรือเกิดความเสียหาย PDFium จะตรวจจับข้อผิดพลาดภายในระบบ และค่า Active จะคงเป็น False เท่านั้น นั่นหมายความว่าทุก ๆ ลูปของการแยกข้อมูลจะต้องป้องกันในเรื่องนี้:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
ไฟล์ที่ได้รับการป้องกันด้วยรหัสผ่านจำเป็นต้องกำหนด Pdf.Password := '...' ก่อนที่จะเปลี่ยน Active := True ไม่มีโอกาสครั้งที่สอง: เมื่อ Active ล้มเหลว คุณต้องปิดและเปิดใหม่อีกครั้งพร้อมด้วยรหัสผ่านที่ถูกต้อง
การแยกข้อมูลทีละหน้าด้วย Character[]
แนวทางในระดับต่ำสุด (lowest-level) คือการไล่ดูอักขระทุกตัวในแต่ละหน้า กำหนด Pdf.PageNumber เพื่อโหลดเลเยอร์ข้อความสำหรับหน้านั้น จากนั้นทำซ้ำรายการ CharacterCount โดยใช้พร็อพเพอร์ตี้ Character[] แฟล็กสองตัวในแต่ละรายการควรตรวจสอบ: CharacterGenerated[i] จะทำเครื่องหมายไกลฟ์สังเคราะห์ที่แทรกโดยตัวเรนเดอร์ (เช่น ยัติภังค์อ่อนที่จุดแบ่งบรรทัด) ซึ่งไม่มีค่า Unicode จริง และ CharacterMapError[i] จะส่งสัญญาณว่า PDFium ไม่สามารถจับคู่ไกลฟ์กับจุดรหัส (code point) ได้ ซึ่งจะเกิดขึ้นกับการเข้ารหัสฟอนต์ที่ไม่มีตาราง ToUnicode
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
ผลลัพธ์ที่ได้คือสตริงแบน ๆ ของจุดรหัส Unicode ตามลำดับที่ PDFium แสดงรายการไว้ ซึ่งเป็นลำดับที่ปรากฏในกระแสเนื้อหา (content stream) ไม่จำเป็นต้องเป็นลำดับการอ่านจากซ้ายไปขวา สำหรับเอกสารอักษรละตินส่วนใหญ่ที่สร้างโดยเครื่องมือสำนักงานมาตรฐานแบบนี้จะใช้ได้ สำหรับ PDF ที่สแกนมาซึ่งผ่านกระบวนการ OCR ด้วยลำดับไกลฟ์ที่ผิดปกติ หรือสำหรับข้อความที่อ่านจากขวาไปซ้าย ลำดับดังกล่าวอาจไม่ถูกต้อง นั่นคือช่วงเวลาที่ ReadablePageContent จะมีประโยชน์มากกว่า
การแยกโครงสร้างด้วย ReadablePageContent
ReadablePageContent จะก้าวขึ้นไปอีกระดับหนึ่ง: มันจะส่งคืนเรคคอร์ด TPdfReadableContent ซึ่งอาร์เรย์ Fragments จะเก็บส่วนย่อยเนื้อหาที่มีแท็กกำกับ แต่ละชิ้นจะมี Kind ที่ระบุย่อหน้า หัวเรื่อง รายการในลิสต์ เซลล์ตาราง และอื่น ๆ เมื่อ PDF มีแผนผังโครงสร้าง (ตรวจสอบด้วย Pdf.IsTagged) แหล่งที่มาจะเป็น rosStructure และลำดับการอ่านจะมีผลผูกพันอย่างเป็นทางการ สำหรับไฟล์ที่ไม่มีแท็ก PDFium จะถอยกลับไปใช้ rosHeuristic ซึ่งจะจัดกลุ่มอักขระตามกล่องขอบเขต (bounding boxes) เป็นหน่วยการอ่านที่น่าจะเป็นไปได้ แต่ไม่สามารถรับประกันความถูกต้องแม่นยำได้
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
หาก Content.Source = rosHeuristic and ผลลัพธ์ของคุณดูสับสน เลเยอร์ข้อความของเอกสารอาจไม่ได้ถูกเขียนขึ้นโดยคำนึงถึงลำดับการอ่าน ณ จุดนั้นการแก้ไขที่น่าเชื่อถือเพียงอย่างเดียวคือการส่งออกใหม่จากแอปพลิเคชันต้นทางพร้อมกับแท็กที่เหมาะสม หรือดำเนินขั้นตอนหลังการประมวลผลที่จัดเรียงต้นกำเนิดของอักขระตาม Y แล้วตามด้วย X
สิ่งที่ CharacterOrigin และ CharacterRectangle มอบให้คุณ
พร็อพเพอร์ตี้ทั้งสองจะส่งคืนตำแหน่งของอักขระในพื้นที่หน้าเว็บ (พอยต์, ต้นกำเนิดที่มุมล่างซ้าย, Y เพิ่มขึ้นด้านบน) CharacterOrigin[i] คือจุดยึดเส้นฐานของไกลฟ์; CharacterRectangle[i] คือกล่องขอบเขตแบบเต็ม สิ่งเหล่านี้คือบล็อกก่อร่างสำหรับสิ่งใด ๆ ที่นอกเหนือจากข้อความธรรมดา: การตรวจจับขอบเขตคอลัมน์ การจัดกลุ่มอักขระเป็นบรรทัดโดยการเปรียบเทียบพิกัด Y ภายในค่าเผื่อ (tolerance) หรือการสร้างแผนที่ทดสอบการตี (hit-test map) สำหรับการเลือกข้อความในโปรแกรมดู หากคุณต้องการค้นหาว่าอักขระตัวใดอยู่ใต้การคลิกเมาส์ ฟังก์ชัน CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) จะดำเนินการค้นหานั้นโดยตรงโดยที่คุณไม่จำเป็นต้องทำซ้ำสี่เหลี่ยม
การจัดวาง DLL ให้เข้าที่
PDFium Component มอบหมายการแยกวิเคราะห์ PDF ทั้งหมดให้กับ DLL ดั้งเดิม ไม่ว่าจะเป็น pdfium32.dll หรือ pdfium64.dll ขึ้นอยู่กับแพลตฟอร์มเป้าหมายของคุณ คอมโพเนนต์จัดส่งสคริปต์ CopyDlls.bat ซึ่งจะคัดลอกไฟล์ที่ถูกต้องไปยังไดเรกทอรีระบบของ Windows การสั่งงานในฐานะผู้ดูแลระบบ (Administrator) เพียงครั้งเดียวบนเครื่องพัฒนาซอฟต์แวร์นั้นเพียงพอแล้ว; สำหรับการปรับใช้จริง คุณต้องคัดลอก DLL ควบคู่ไปกับไฟล์ปฏิบัติการของแอปพลิเคชันแทน รุ่นที่เปิดใช้งาน V8 (pdfium32v8.dll, pdfium64v8.dll) จะมีขนาดใหญ่กว่าอย่างมาก และจำเป็นเฉพาะเมื่อ PDF ของคุณมี JavaScript ที่ต้องประมวลผลเท่านั้น สำหรับการแยกข้อความบริสุทธิ์ รุ่นมาตรฐานคือตัวเลือกที่ถูกต้อง
หากไม่มี DLL อยู่ในขณะรันไทม์ การรัน Active := True จะล้มเหลวอย่างเงียบเชียบเช่นเดียวกับกรณีไฟล์ขาดหาย เนื่องจากคอมโพเนนต์จะตรวจจับข้อผิดพลาดในการโหลดเป็นการภายใน โปรดทดสอบบนเครื่องที่สะอาดก่อนส่งมอบงานเสมอ
การใช้ FontSize[] ควบคู่ไปกับ Character[] สำหรับการวิเคราะห์เค้าโครง
นอกเหนือจากข้อความธรรมดา API ในระดับอักขระยังเปิดเผย FontSize[i] ซึ่งส่งคืนขนาดพอยต์เรนเดอร์ของไกลฟ์แต่ละตัว เมื่อผสมผสานกับ CharacterOrigin[i] และ CharacterRectangle[i] สิ่งนี้จะช่วยให้คุณจำแนกข้อความหลักออกจากหัวเรื่องได้โดยไม่ต้องพึ่งพาแผนผังโครงสร้าง กลุ่มอักขระที่ขนาดฟอนต์กระโดดสูงกว่าเกณฑ์ที่กำหนดนั้นเกือบจะเป็นหัวเรื่องอย่างแน่นอนในเอกสารที่ไม่มีแท็ก เทคนิคเดียวกันนี้ใช้กับการตรวจจับคำบรรยายภาพ (ข้อความขนาดเล็กใต้กล่องขอบเขตภาพ) หรือเชิงอรรถ (ข้อความขนาดเล็กใกล้ด้านล่างของหน้า) ทั้งหมดนี้ไม่จำเป็นต้องมีการเรนเดอร์; พร็อพเพอร์ตี้ทั้งสามตัวอ่านข้อมูลโดยตรงจากเลเยอร์ข้อความที่ PDFium สร้างขึ้นในระหว่าง Active := True
ความละเอียดอ่อนประการหนึ่ง: FontSize[i] สะท้อนขนาดหลังจากใช้ CTM (เมทริกซ์การแปลงปัจจุบัน) ของหน้าเว็บแล้ว ดังนั้นเอกสารที่ผู้เขียนปรับขนาดทั้งหน้าจะรายงานขนาดที่ปรับปรุงตามสัดส่วน หากคุณเปรียบเทียบขนาดระหว่างหน้ากระดาษที่มีขนาดหน้าต่างกัน ให้ปรับให้เป็นมาตรฐานเทียบกับความสูง MediaBox ของแต่ละหน้าก่อนจะตัดสินใจเลือกเกณฑ์ค่า
การเขียนผลลัพธ์ไปยังไฟล์
TStringList ของ Delphi จัดการผลลัพธ์ UTF-8 ได้อย่างถูกต้องตั้งแต่เวอร์ชัน XE กำหนดค่า WriteBOM := False หากคุณต้องการไฟล์ที่ไม่มี BOM (ผู้ใช้ปลายน้ำจำนวนมากมักมีปัญหาขัดข้องกับ BOM นำหน้า):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
สำหรับเอกสารขนาดใหญ่มากซึ่งมีความกังวลเกี่ยวกับหน่วยความจำ ให้เขียนโดยตรงไปยัง TStreamWriter พร้อม TEncoding.UTF8 ภายในลูปหน้าเว็บ แทนที่จะสะสมข้อมูลทุกอย่างลงในรายการลิสต์ก่อนในตอนแรก
API ของ Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent และ CharacterIndexAtPos ที่แสดงที่นี่เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder