PDFium Component เพิ่มเลเยอร์ข้อความที่ค้นหาได้ให้กับหน้า PDF ที่สแกนจาก Delphi ผ่าน ApplyOcrSearchLayer มันเรนเดอร์แต่ละหน้าที่เลือกไว้ ส่งพิกเซลให้ผู้ให้บริการ OCR ที่คุณจัดหาให้ และเขียนคำที่รู้จำได้กลับมาเป็นอ็อบเจกต์ข้อความที่มองไม่เห็น วางตำแหน่งทับคำในภาพสแกน ภาพหน้าดั้งเดิมไม่เคยถูกถอดรหัส เข้ารหัสใหม่ หรือแทนที่เลย ดังนั้นผลลัพธ์ทางภาพจึงเป็นหน้าเดียวกันแบบไบต์ต่อไบต์กับที่คุณเริ่มต้นไว้
เอนจินการรู้จำไม่ใช่ส่วนหนึ่งของไลบรารีอย่างจงใจ PDFium เปิดให้ใช้การเรนเดอร์หน้า การแม็ปพิกัด การโหลดฟอนต์ การสร้างอ็อบเจกต์ข้อความ และโหมดเรนเดอร์แบบมองไม่เห็น แต่ไม่มีเอนจิน OCR อยู่ในตัว และการแสร้งทำเป็นอย่างอื่นจะหมายถึงการรวมผลิตภัณฑ์การรู้จำของใครสักคนเข้าไปในคอมโพเนนต์ PDF แทนที่จะเป็นเช่นนั้น การรู้จำอยู่เบื้องหลังอินเทอร์เฟซ IPdfOcrProvider ไลบรารีส่งพิกเซลแบบ BGRA เลย์เอาต์คงที่ต้นกำเนิดบน และผู้ให้บริการคืนค่าข้อความยูนิโค้ด ค่าความเชื่อมั่น และรูปสี่เหลี่ยมของคำ
เลเยอร์ข้อความที่ค้นหาได้คืออะไรกันแน่
PDF ที่สแกนคือภาพถ่ายของเอกสาร เนื้อหาหน้าเป็นภาพขนาดใหญ่ภาพเดียว และไม่มีอะไรให้เลือก ค้นหา คัดลอก หรือทำดัชนีได้เลย เลเยอร์ข้อความที่ค้นหาได้จะเพิ่มอ็อบเจกต์ข้อความจริงทับบนภาพนั้น โดยตั้งโหมดเรนเดอร์เป็นมองไม่เห็น ดังนั้นตัวแสดงผลจึงไม่วาดอะไรเลย แต่การเลือก การค้นหา และการสกัดจะพบคำตรงตำแหน่งที่มันปรากฏพอดี
การจัดตำแหน่งคือหัวใจของทั้งหมด หากข้อความที่มองไม่เห็นเลื่อนไปไม่กี่พอยต์ การไฮไลต์จากการเลือกจะตกอยู่ข้างๆ คำแทนที่จะอยู่บนคำ และการคัดลอกย่อหน้าหนึ่งจะสร้างข้อความที่เรียงลำดับผิด นี่คือเหตุผลที่เรขาคณิตต้องมาจากการแปลงชุดเดียวกับที่ PDFium ใช้เรนเดอร์หน้า แทนที่จะเป็นการเดาแบบสัดส่วน
การนำผู้ให้บริการไปใช้งานจริง
ข้อตกลงของผู้ให้บริการมีเมธอดเดียว มันรับระเบียนภาพหน้าที่พกมิติ สไตรด์ DPI รูปแบบพิกเซล และไบต์พิกเซลเอง บวกกับโทเคนการยกเลิก และคืนค่าคำหรือข้อความแสดงข้อผิดพลาด:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels เก็บแถว BGRA ต้นกำเนิดบนขนาด Image.Stride ไบต์
// ส่งให้เอนจินของคุณ แล้วเติมหนึ่งรายการต่อหนึ่งคำที่รู้จำได้
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // ช่วง 0 ถึง 1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
ใช้รูปสี่เหลี่ยมแทนสี่เหลี่ยมผืนผ้า เพราะภาพสแกนไม่ค่อยตั้งฉากกับหน้ากระดาษ คำบนหน้าที่หมุนเล็กน้อยจะครอบครองรูปสี่เหลี่ยมด้านขนาน และ TPdfOcrQuad พกจุดมุมสี่จุดเพื่อให้คำที่เอียงและหมุนยังคงมีบริเวณการเลือกที่แม่นยำ เอนจินที่รายงานเฉพาะกล่องที่จัดแนวตามแกนสามารถใช้ FromRectangle ซึ่งสร้างรูปสี่เหลี่ยมแบบเสื่อมสภาพขึ้นมาได้
เหตุใดตำแหน่งของคำจึงปรับสัดส่วนแบบง่ายๆ ไม่ได้
เป็นเรื่องน่าดึงดูดใจที่จะแปลงพิกัดพิกเซลเป็นพิกัดหน้าด้วยการหารด้วยความกว้างการเรนเดอร์แล้วคูณด้วยความกว้างหน้า วิธีนั้นได้ผลเฉพาะกับหน้าที่ไม่มีการหมุน มี CropBox เหมือนกับ MediaBox และมีจุดกำเนิดที่ศูนย์ และเอกสารสแกนจำนวนมากไม่ผ่านเงื่อนไขอย่างน้อยหนึ่งข้อนั้น
PDFium Component แม็ปมุมทั้งสี่ของรูปสี่เหลี่ยมทีละมุมผ่าน FPDF_DeviceToPage ซึ่งเป็นการแม็ปเดียวกับที่ตัวเรนเดอร์ใช้สร้างพิกเซล ดังนั้นรายการ /Rotate และกรอบครอบตัดที่มีออฟเซ็ตจึงถูกจัดการได้โดยธรรมชาติของโครงสร้าง เมทริกซ์อฟฟีนสำหรับอ็อบเจกต์ข้อความจะถูกสร้างจากจุดที่แม็ปแล้วสามจุด คือมุมล่างซ้าย ล่างขวา และบนซ้าย ซึ่งเพียงพอพอดีสำหรับแสดงตำแหน่ง ขนาด การหมุน และการเฉือน
อ็อบเจกต์ข้อความเองถูกสร้างขึ้นด้วยขนาดฟอนต์หนึ่งหน่วยเพื่อให้สามารถวัดขอบเขตฟอนต์จริงของมันได้ และขอบเขตอ็อบเจกต์ที่วัดได้ก็จะถูกแม็ปเข้ากับรูปสี่เหลี่ยมเป้าหมาย การกำหนดขนาดด้วยขนาดพอยต์ที่เดาไว้แล้วหวังว่าจะตรงกับคำที่สแกนจะเบี่ยงเบนไปทุกครั้งที่มีการแทนที่ฟอนต์ การวัดก่อนทำให้ความพอดีไม่ขึ้นกับว่าเลเยอร์ใช้ฟอนต์ตัวใด
การรันมันทั่วทั้งเอกสาร
ระเบียนตัวเลือกควบคุมความละเอียด การกรอง และงบประมาณทุกตัว การกรองตามความเชื่อมั่นสำคัญกว่าที่เห็น คำขยะที่มีความเชื่อมั่นต่ำจะปนเปื้อนผลการค้นหาอย่างถาวร และต่างจากการเรนเดอร์ที่ผิดพลาด ไม่มีใครสังเกตเห็นจนกว่าการค้นหาจะให้ผลลัพธ์ที่ไร้สาระ:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // ความละเอียดสำหรับการรู้จำ
Options.MinConfidence := 0.60; // ทิ้งคำที่ไม่แน่ใจ
Options.SkipPagesWithText := True; // ปล่อยหน้าที่เกิดมาเป็นดิจิทัลไว้เฉยๆ
Options.ContinueOnError := True; // หนึ่งหน้าที่แย่ต้องไม่หยุดงานทั้งหมด
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText สมควรได้รับการเน้นย้ำในคลังเก็บแบบผสม PDF ที่มีข้อความจริงอยู่แล้ว ไม่ว่าจะเกิดมาเป็นดิจิทัลหรือผ่านการประมวลผลมาก่อน จะได้เลเยอร์ข้อความชั้นที่สองหากคุณรัน OCR ทับมันแบบไม่ดูตาม้าตาเรือ และการซ้ำนี้ทำให้การสกัดคืนค่าทุกคำเป็นสองเท่า สถานะรายหน้า popsSkippedExistingText จะบอกคุณอย่างแม่นยำว่าหน้าใดถูกปล่อยไว้เฉยๆ
งบประมาณ การยกเลิก และการควบคุมความล้มเหลว
ปริมาณทุกอย่างที่เอกสารประสงค์ร้ายหรือแค่มีขนาดมหึมาสามารถทำให้พองตัวได้มีเพดานกำกับไว้ คือพิกเซลต่อหน้าและรวมทั้งหมด คำต่อหน้าและรวมทั้งหมด และอักขระต่อคำ ทั้งหมดนี้ถูกตรวจสอบก่อนที่หน้าจะถูกเขียน ไม่ใช่หลังจากนั้น และการประมาณพิกเซลถูกคำนวณจากขนาดหน้าและ DPI ก่อนที่จะจัดสรรบิตแมปใดๆ การเพิ่ม DPI จาก 150 เป็น 300 ทำให้หน่วยความจำต่อหน้าเพิ่มเป็นสี่เท่า ดังนั้นเพดานต่อหน้าจึงเป็นพารามิเตอร์ที่ควรปรับก่อนเมื่องานแบบแบตช์เริ่มล้มเหลวบนรูปแบบขนาดใหญ่
โทเคนการยกเลิกร้อยเรียงผ่านทั้งเส้นทาง ตั้งแต่การเรนเดอร์แบบก้าวหน้า การเรียกผู้ให้บริการ และลูปการแทรกต่อคำ นั่นหมายความว่าผู้ใช้ที่ยกเลิกระหว่างการรู้จำไฟล์ 400 หน้าจะหยุดภายในหนึ่งหน้า แทนที่จะหยุดที่ท้ายเอกสาร และรูปแบบโทเคนเดียวกับที่ใช้ในที่อื่นของคอมโพเนนต์ ตามที่อธิบายไว้ในการเรนเดอร์แบบก้าวหน้าที่ยกเลิกได้ ก็ใช้ในที่นี้โดยไม่เปลี่ยนแปลง
การควบคุมความล้มเหลวทำเป็นรายหน้า ไลบรารีเก็บแฮนเดิลอ็อบเจกต์ที่มันแทรกไว้บนหน้าหนึ่ง และเรียก FPDFPage_GenerateContent เพียงครั้งเดียว หลังจากคำทั้งหมดถูกวางแล้ว หากสิ่งใดล้มเหลวระหว่างทาง ไม่ว่าจะเป็นข้อผิดพลาดของผู้ให้บริการหรือปัญหาฟอนต์ อ็อบเจกต์ที่แทรกไว้บนหน้านั้นจะถูกลบออกในทิศทางย้อนกลับ และเนื้อหาหน้าจะถูกสร้างขึ้นใหม่ ดังนั้นหน้าที่ล้มเหลวจะย้อนกลับไปสู่สถานะดั้งเดิม แทนที่จะเก็บเลเยอร์ข้อความไว้เพียงครึ่งเดียว ลูปเอกสารจะดำเนินต่อหรือหยุดตาม ContinueOnError และหน้าที่ใช้งานอยู่จะถูกเรียกคืนเสมอ
การตรวจสอบว่าภาพไม่ถูกแตะต้องจริงๆ
การตรวจสอบที่แข็งแกร่งที่สุดที่มีก็เป็นวิธีที่ง่ายที่สุดด้วย เรนเดอร์หน้าก่อนและหลังการใส่เลเยอร์ในขนาดเดียวกันแล้วเปรียบเทียบบิตแมป ทั้งสองควรเหมือนกันแบบไบต์ต่อไบต์ เพราะข้อความที่มองไม่เห็นไม่วาดอะไรเลย และสตรีมภาพไม่เคยถูกถอดรหัส ความแตกต่างใดๆ หมายความว่ามีสิ่งอื่นที่ไม่ใช่เลเยอร์ข้อความเปลี่ยนแปลงหน้านั้น
หลังจากนั้น ตรวจสอบฝั่งข้อความด้วยการสกัดจากไฟล์ที่ประมวลผลแล้ว และยืนยันว่าตำแหน่งของคำตกอยู่บนภาพสแกน เส้นทางการสกัดเป็นเส้นทางเดียวกับที่อธิบายไว้ในการสกัดข้อความจากเอกสาร PDF และสำหรับการตรวจสอบการจัดตำแหน่งด้วยสายตาอย่างรวดเร็ว การเรนเดอร์หน้าเป็นภาพตามที่อธิบายไว้ในการแปลงหน้า PDF เป็น JPEG จะช่วยให้คุณซ้อนกล่องคำทับบนภาพสแกนได้
การใส่เลเยอร์ OCR การเรนเดอร์ การสกัด และการแก้ไข ล้วนทำงานกับอ็อบเจกต์เอกสารเดียวกันใน Delphi, C++Builder และ Lazarus พื้นผิว API ทั้งหมดอธิบายไว้ที่หน้าคอมโพเนนต์ PDFium สำหรับ Delphi