PDFiumPas คืนค่าข้อความของหน้าในรูปแบบโครงสร้าง แทนที่จะเป็นสตริงธรรมดา GetStructuredText สร้าง TPdfStructuredTextPage ที่บรรจุบล็อก แต่ละบล็อกเก็บบรรทัด แต่ละบรรทัดเก็บช่วงข้อความมีสไตล์ พร้อมขอบเขตในพื้นที่หน้าในทุกระดับ และดัชนีตัวอักษรต้นทางถูกเก็บรักษาไว้ ทำให้ชิ้นส่วนใดก็ตามสามารถแมปกลับไปยังหน้าข้อความต้นทางได้
การดึงข้อความแบบสตริงแบนที่โค้ดส่วนใหญ่เริ่มต้นด้วยยังคงมีอยู่และยังคงถูกต้องสำหรับจุดประสงค์ของมัน มันจะหยุดเพียงพอทันทีที่คุณต้องรู้ว่าคำใดเป็นหัวข้อ คำใดอยู่ในคอลัมน์ซ้าย หรือผลลัพธ์ที่ค้นพบอยู่ตรงไหนบนหน้าจริง ๆ
เหตุใดสตริงแบนจึงเป็นผลลัพธ์ที่ผิดสำหรับงานส่วนใหญ่
เพราะคำถามที่คนถามข้อความที่ดึงออกมาแทบไม่เคยเป็น “ตัวอักษรอะไรอยู่บนหน้านี้” เลย มันมักเป็น “อะไรคือหัวข้อเรื่อง” “นี่เป็นตารางหรือไม่” “ย่อหน้านี้อยู่ในส่วนที่ 4 หรือไม่” “ฉันควรวาดไฮไลต์ตรงไหน” สตริงเดียวตอบคำถามเหล่านี้ไม่ได้เลยสักข้อ และทุกคำตอบที่คุณสร้างขึ้นใหม่จากมันก็คือฮิวริสติกที่คุณต้องรับผิดชอบเอง
เลย์เอาต์แบบสองคอลัมน์ทำให้ประเด็นนี้เป็นรูปธรรม ดึงบทความสองคอลัมน์ออกมาเป็นสตริง แล้วขึ้นอยู่กับว่าผู้สร้างไฟล์เขียนคอนเทนต์สตรีมอย่างไร คุณอาจได้คอลัมน์หนึ่งตามด้วยคอลัมน์สอง หรือคุณอาจได้บรรทัดที่หนึ่งของคอลัมน์หนึ่ง บรรทัดที่หนึ่งของคอลัมน์สอง บรรทัดที่สองของคอลัมน์หนึ่ง สลับกันไปเรื่อย ๆ ตลอดทั้งหน้า ทั้งสองแบบออกมาจาก PDF ที่ถูกต้องตามข้อกำหนดทั้งคู่ ไม่มีแบบไหนผิดในระดับรูปแบบไฟล์ เพราะ PDF บรรยายรอยที่ปรากฏบนหน้า ไม่ใช่โครงร่างของเอกสาร โมเดลแบบบล็อกให้ตัวดึงข้อมูลตัดสินใจเรื่องลำดับได้อย่างชัดเจน และบอกคุณได้ว่ามันตัดสินใจแบบไหน
ลำดับตามเนื้อหา หรือเลย์เอาต์ทางกายภาพ
TPdfStructuredTextOptions.ReadingOrder เลือกระหว่าง roContentOrder และ roPhysicalLayout และคำตอบที่ถูกต้องขึ้นอยู่กับว่าคุณเชื่ออะไรมากกว่ากัน ระหว่างผู้สร้างไฟล์กับรูปทรงเรขาคณิต
ลำดับตามเนื้อหาคืนค่าข้อความตามลำดับที่คอนเทนต์สตรีมวาดมัน วิธีนี้เร็ว และสำหรับเอกสารที่สร้างขึ้นโดยระบบที่มีมารยาทดี มักเป็นลำดับการอ่านที่ตั้งใจไว้อยู่แล้ว เลย์เอาต์ทางกายภาพจะละเลยลำดับของสตรีมและสร้างลำดับขึ้นใหม่จากตำแหน่งที่ตัวอักษรอยู่จริง โดยจัดกลุ่มเป็นบรรทัดแล้วจึงเป็นคอลัมน์ นี่คือสิ่งที่คุณต้องการสำหรับหน้าที่สแกนแล้วผ่าน OCR สำหรับผลลัพธ์จากเครื่องมือที่ส่งข้อความออกมาตามลำดับฟอนต์แทนที่จะเป็นลำดับการอ่าน และสำหรับอะไรก็ตามที่ผลลัพธ์ทางสายตาเป็นสิ่งเดียวที่คุณพึ่งพาได้
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfStructuredTextOptions;
Page: TPdfStructuredTextPage;
B, L: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'article.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // เริ่มนับที่ 1
Options := TPdfStructuredTextOptions.Default;
Options.ReadingOrder := roPhysicalLayout;
Options.IncludeFontInfo := True;
Options.IncludeSemantics := True;
Options.MaxCharacters := 200000; // งบประมาณแบบล้มเหลวปิดกั้น
Page := Pdf.GetStructuredText(Options);
for B := 0 to High(Page.Blocks) do
begin
if Page.Blocks[B].Kind = cfHeading then
Emit(Format('H%d: %s',
[Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
else
for L := 0 to High(Page.Blocks[B].Lines) do
Emit(Page.Blocks[B].Lines[L].Text);
end;
finally
Pdf.Free;
end;
end;
การติดแท็กเพิ่มอะไรที่รูปทรงเรขาคณิตให้ไม่ได้
เจตนา เมื่อเปิดใช้ IncludeSemantics บล็อกจาก PDF แบบมีแท็กจะพก Kind ที่ดึงมาจากโครงสร้างต้นไม้ ดังนั้นหัวข้อจึงเป็นหัวข้อเพราะผู้สร้างไฟล์บอกไว้เช่นนั้น ไม่ใช่เพราะฟอนต์ของมันใหญ่กว่าค่าเฉลี่ย ชนิดที่ครอบคลุมคือรูปทรงที่สำคัญต่อการใช้ซ้ำ: cfParagraph, cfHeading พร้อม HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure และค่าสำรอง cfPlain สำหรับที่ไม่มีแท็ก
ฟิลด์ Source บันทึกไว้ว่าการจัดประเภทแต่ละครั้งมาจากไหน คือ rosStructure สำหรับโครงสร้างต้นไม้ และ rosHeuristic สำหรับการอนุมาน ซึ่งเป็นฟิลด์ที่ควรบันทึกไว้เมื่อคุณกำลังตัดสินใจว่าจะเชื่อไปป์ไลน์การดึงข้อมูลได้มากแค่ไหนทั่วทั้งชุดเอกสาร รูปภาพเป็นกรณีพิเศษที่ควรรู้ไว้: สำหรับบล็อก cfFigure ข้อความมาจากคำบรรยายทางเลือกแทนที่จะมาจากกลิฟใด ๆ เพราะรูปภาพไม่มีตัวอักษรเป็นของตัวเอง ข้อความทางเลือกที่ไม่ตรงกับอะไรก็ยังคงถูกแสดงไว้แทนที่จะถูกทิ้ง ซึ่งทำให้การตรวจสอบการเข้าถึงเห็นได้ว่ามีคำบรรยายอยู่ แม้ไม่มีอะไรบนหน้าวาดมันออกมา โมเดลการติดแท็กเองอธิบายไว้ใน การตรวจสอบโครงสร้างต้นไม้ PDF/UA
ช่วงข้อความพกทั้งสไตล์และที่มา
TPdfStructuredTextSpan แต่ละตัวเก็บข้อความของตัวเอง ขอบเขตในพื้นที่หน้า FontName, FontSize, FontWeight และ Angle พร้อม SourceStartIndex และ SourceCharacterCount ช่วงข้อความจะแตกออกตรงจุดที่สไตล์เปลี่ยน ดังนั้นประโยคที่มีคำตัวหนาสามคำจะกลายเป็นสามช่วง และการสร้างการเน้นข้อความขึ้นใหม่ใน HTML หรือ Markdown ก็เป็นเพียงเรื่องของการอ่านคุณสมบัติ ไม่ใช่การเดาจากชื่อฟอนต์
ฟิลด์ดัชนีต้นทางทั้งสองคือสิ่งที่เปลี่ยนการดึงข้อมูลให้กลายเป็นฟีเจอร์ ไม่ใช่แค่รายงาน มันชี้กลับไปยังลำดับตัวอักษรของหน้า หมายความว่าบล็อกที่คุณจับคู่ได้ในการค้นหาสามารถแปลงเป็นรูปทรงเรขาคณิตของการเลือกระดับตัวอักษร หรือสี่เหลี่ยมไฮไลต์ได้โดยไม่ต้องผ่านข้อความอีกรอบด้วยลำดับที่ต่างออกไป กลไกนี้อธิบายไว้ใน การเลือกบรรทัดข้อความด้วยกล่องตัวอักษร ฟิลด์ Angle สำคัญกว่าที่เห็น: ข้อความที่หมุนในตราประทับหรือลายน้ำจะอยู่ในพื้นที่พิกัดเดียวกับข้อความเนื้อหา และไปป์ไลน์ที่ละเลยมุมจะรวม “DRAFT” ที่เอียงเข้าไปกลางย่อหน้าโดยไม่รู้ตัว
งบประมาณ และตัวนับคุณภาพสองตัว
MaxCharacters คืองบประมาณแบบล้มเหลวปิดกั้น ไม่ใช่การตั้งค่าตัดทอน: หน้าที่เกินขีดจำกัดจะหยุดทำงาน แทนที่จะคืนค่าเนื้อหาบางส่วนอย่างเงียบ ๆ บนเส้นทางรับไฟล์ที่ไม่น่าเชื่อถือ นี่คือพฤติกรรมที่คุณต้องการ เพราะหน้าที่มีตัวอักษรนับล้านตัวเป็นได้ทั้งไฟล์ประหลาดที่สร้างจากเครื่องจักร หรือความพยายามทำให้ตัวดึงข้อมูลของคุณกลายเป็นส่วนที่ช้าที่สุดในระบบ
ตัวนับสองตัวบนหน้าที่คืนค่ามาบรรยายคุณภาพการดึงข้อมูลโดยตรง UnmappedCharacterCount นับตัวอักษรที่ไม่มีการแมป Unicode ที่ใช้งานได้ ซึ่งเป็นอาการคลาสสิกของฟอนต์แบบ subset ที่ฝังไว้โดยไม่มี CMap /ToUnicode ข้อความแบบนั้นเรนเดอร์ได้สมบูรณ์แบบแต่ดึงออกมาแล้วไม่มีประโยชน์อะไรเลย GeometryFailureCount นับตัวอักษรที่หากรอบล้อมไม่ได้ ซึ่งลดคุณภาพการเรียงลำดับแบบเลย์เอาต์ทางกายภาพ บันทึกทั้งสองค่าไว้ ชุดเอกสารที่ตัวเลขเหล่านี้อยู่ใกล้ศูนย์อย่างสม่ำเสมอสามารถทำดัชนีได้อย่างมั่นใจ ส่วนชุดที่ไม่เป็นเช่นนั้นกำลังบอกคุณว่าระบบสร้างไฟล์บางตัวในไปป์ไลน์ของคุณต้องการความใส่ใจ ก่อนที่ผลลัพธ์ปลายทางใด ๆ จะเชื่อถือได้
var
Page: TPdfStructuredTextPage;
B, S, L: Integer;
Emphasised: Boolean;
begin
Page := Pdf.GetStructuredText(Options);
if Page.UnmappedCharacterCount > 0 then
Log(Format('page %d: %d characters without a Unicode mapping',
[Page.PageNumber, Page.UnmappedCharacterCount]));
if Page.GeometryFailureCount > 0 then
Log(Format('page %d: %d characters without geometry',
[Page.PageNumber, Page.GeometryFailureCount]));
for B := 0 to High(Page.Blocks) do
for L := 0 to High(Page.Blocks[B].Lines) do
for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
begin
Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
end;
end;
ประสิทธิภาพบนหน้าจริง
การดึงข้อมูลแบบเลย์เอาต์ทางกายภาพเป็นโหมดที่มีต้นทุนสูง และการใช้งานนี้ถูกสร้างขึ้นมาสำหรับหน้าที่ใหญ่จริง ๆ: การเรียงลำดับตัวอักษรทำงานที่ O(n log n) แทนที่จะสแกนซ้ำ บัฟเฟอร์บรรทัดและช่วงข้อความเติบโตแบบเรขาคณิตแทนที่จะจัดสรรใหม่ทีละตัวอักษร ข้อความ Unicode ถูกสร้างในบัฟเฟอร์แทนที่จะต่อสตริง และการค้นหาฟอนต์สำหรับอ็อบเจกต์ข้อความที่อยู่ติดกันถูกแคชไว้ การผสมผสานนี้เองที่ทำให้หน้าที่มีตัวอักษรหนาแน่นถึง 5,000 ตัวคาดเดาได้ แทนที่จะเป็นแบบกำลังสอง
สำหรับงานที่มีจำนวนหน้ามาก ก็ยังคุ้มค่าที่จะเลือกโหมดที่ประหยัดกว่าเมื่อทำได้ ใช้ roContentOrder พร้อมเปิด semantics สำหรับเอกสารแบบมีแท็กที่คุณเชื่อถือ และสงวน roPhysicalLayout ไว้สำหรับวัสดุที่สแกนและไฟล์รุ่นเก่าที่รูปทรงเรขาคณิตเป็นสัญญาณเดียวที่มี ถ้าสิ่งที่คุณต้องการคือสตริงธรรมดา API ที่ง่ายกว่าตามที่อธิบายไว้ใน การดึงข้อความจากเอกสาร PDF ยังคงเป็นเส้นทางที่เร็วกว่า และเมื่อคุณต้องการสืบข้อความกลับไปยังตัวระบุเนื้อหาที่ทำเครื่องหมายไว้ การอ่านและเขียนเนื้อหาที่ทำเครื่องหมาย BDC และ MCID ครอบคลุมชั้นนั้นไว้
โมเดลแบบบล็อกยังแมปเข้ากับสิ่งที่ไปป์ไลน์การสืบค้นต้องการได้อย่างลงตัว: หัวข้อพร้อมย่อหน้าของมันคือชิ้นข้อมูลที่มีชื่อเรื่อง และขอบเขตทำให้การอ้างอิงชี้ไปยังตำแหน่งบนหน้าได้ แทนที่จะชี้ไปแค่ทั้งเอกสาร PDFiumPas เป็นคอมโพเนนต์สำหรับ Delphi และ Lazarus ที่ห่อหุ้มเอนจิน PDFium ไว้ พร้อมเอกสารและตัวอย่างที่ หน้าคอมโพเนนต์ PDFium สำหรับ Delphi