HotPDF เปลี่ยนหน้า PDF ที่สแกนมาเป็น PDF ค้นหาได้ด้วย Tesseract ผ่าน HPDFCreateTesseractOCREngine โรงงานที่ห่อ Tesseract executable ที่ติดตั้งในเครื่องไว้เป็น IHPDFOCREngine คุณส่ง engine ตัวนั้นให้ ApplyLoadedOCRTextLayer ซึ่ง render แต่ละหน้า รัน Tesseract หน้าละครั้ง parse output TSV ระดับคำของมัน แล้วกระทำชั้นข้อความ Unicode ที่มองไม่เห็นลงให้ทุกหน้าที่ขอไว้ใน transaction เดียว หรือไม่กระทำเลย
เหตุผลที่ adapter ตัวนี้มีอยู่คือขอบข่าย engine OCR แบบจับคู่เทมเพลตที่ติดมากับตัว ตั้งใจแคบ: ตัวอักษร ASCII กับตัวเลขที่เครื่องพิมพ์ เท่านั้น ใบแจ้งหนี้ที่มีชื่อติดวรรณยุกต์ภาษาละติน สัญญาภาษาจีน และ archive หลายภาษาต้องการ recognizer จริงที่มี language model ที่ได้รับการฝึก Tesseract เป็นตัวเลือกที่ตาเห็นเพราะมันเป็นโปรแกรม command line ที่คุณจัดหาไว้ข้างแอปพลิเคชันได้ การเรียกโปรแกรมภายนอกจาก library เอกสารฟังดูเบา ๆ มันไม่เบา และโค้ดที่น่าสนใจส่วนใหญ่ใน adapter ว่าด้วยสิ่งที่เกิดขึ้นเมื่อโปรแกรมวิกลจริต ค้าง โดนยกเลิก หรือได้รับสิ่งที่ไม่ควรเห็นตกทอดมา
HotPDF ขับ Tesseract จากแอปพลิเคชัน Delphi อย่างไร
HotPDF รัน Tesseract เป็น child process ที่ซ่อนอยู่หน้าละตัว ป้อน bitmap ที่ render แล้วให้และอ่านไฟล์ TSV กลับมา พร้อมเปิดผลลัพธ์ผ่านรอยต่อ IHPDFOCREngine เดียวกับที่ engine ติดตั้งใช้ อะไรปลายน้ำไม่เปลี่ยน: การแมปพิกัด การจัดการการหมุน การ validate Unicode การกรองด้วย confidence และการกระทำแบบ atomic คือ pipeline ชั้นข้อความที่คุณมีอยู่แล้ว โรงงานอยู่ใน unit HPDFTesseractRecognition และ validate ตั้งแต่ต้น: executable ต้องมีอยู่ ไดเรกทอรี tessdata ต้องมีอยู่ timeout ต้องอยู่ระหว่าง 1 ถึง 3,600,000 มิลลิวินาที และ identifier ของภาษาเป็นได้แค่ ASCII ตัวอักษร ตัวเลข _ กับ + การเช็กสุดท้ายสำคัญเพราะสตริงภาษาไปจบลงบน command line eng+chi_sim เป็นค่าที่ Tesseract ยอมรับ ขณะที่อะไรที่มีเครื่องหมายคำพูดหรือช่องว่างยอมไม่ได้
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// โยน EArgumentException เมื่อ executable หาย, tessdata หาย,
// identifier ภาษาผิด หรือ timeout หลุดช่วง 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // หลาย model ต่อกันด้วย '+'
120000); // เพดานต่อหน้า, default คือ 60000
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Options := THPDFOCRTextLayerOptions.Default; // 300 DPI, MinimumConfidence 0.5
Options.CancellationToken := Token;
// list หน้าว่างแปลว่าทุกหน้า; หน้าที่มีข้อความถูกข้ามโดย default
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
สำหรับแต่ละหน้า Recognize สร้างไดเรกทอรีส่วนตัวใต้ temp path ชื่อ HotPDF-OCR-{GUID} เซฟ bitmap ที่ render แล้วเป็น input.bmp และปล่อย tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1 โดยอาร์กิวเมนต์ path ทุกตัวถูก quote ตามกฎ escaping ของ command line Windows สำหรับแบ็กสแลชกับเครื่องหมายคำพูดที่ฝังอยู่ ค่า --dpi คือ DPI ของการ render จาก THPDFOCRTextLayerOptions.DPI Tesseract จึงไม่ต้องเดาความละเอียดจาก metadata ของภาพ และ --psm 3 ขอการแบ่งส่วนหน้าแบบอัตโนมัติเต็มรูปแบบ engine รายงานตัวเองว่า Tesseract (local CLI) ซึ่งเป็นสิ่งที่ตกลงใน Info.EngineName Tesseract กับ language model ของมันไม่ได้มาแถมกับ HotPDF การติดตั้งเป็นหน้าที่ของแอปพลิเคชัน
ทำไม parser ของ TSV ถึงเข้มงวดขนาดนั้น
parser TSV ใน HotPDF พังทั้งหน้ากับแถวที่ผิดรูปแม้แต่แถวเดียว เพราะ list คำที่ parse มาครึ่ง ๆ ผลิตชั้นข้อความที่ขัดแย้งกับภาพอย่างเงียบ ๆ output TSV ของ Tesseract มี header คอลัมน์สิบสองตายตัว จาก level ถึง text และ HotPDF เทียบบรรทัดแรกกับ header เนี่ย ๆ หลังถอด byte order mark ที่ใส่มาให้ก็ได้ ทุกแถวถัดไปต้องแยกได้พอดีสิบสองฟิลด์ และการแยกหยุดหลังแท็บตัวที่สิบเอ็ด เพื่อให้แท็บข้างในข้อความที่รู้จำได้ยังเป็นส่วนของคำแทนที่จะกลายเป็นคอลัมน์ที่สิบสาม แถว level 5 เท่านั้นที่เป็นคำ; level 1 ถึง 4 บรรยายหน้า บล็อก ย่อหน้า กับบรรทัด จึงถูกข้าม แถว level 5 ที่ข้อความว่างหรือเป็น whitespace ล้วนถูกข้ามด้วย เพราะคำเปล่ามีกล่องแต่ไม่มีอะไรให้ระบุตำแหน่งหรือค้นหา อย่างอื่นถูกเช็กหนักทุกอย่าง: เรขาคณิตแบบจำนวนเต็ม, confidence ที่ parse ด้วยรูปแบบ invariant en-US เพื่อไม่ให้ locale เยอรมันอ่าน 93.5 เป็นขยะ, กล่องที่อยู่ข้างใน bitmap ทั้งก้อน และ confidence ระหว่าง 0 กับ 100 พังแม้แต่ครั้งเดียวคือโยน exception, engine คืน False และ array คำถูกล้าง test regression มีเคสนี้พอดี: คำที่ถูกต้องหนึ่งตัวตามด้วยแถวพังต้องให้ศูนย์คำ ไม่ใช่หนึ่ง
// ย่อจาก loop ของ level-5 ใน HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // แถวของ page/block/paragraph/line
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // คำที่เป็น whitespace ไม่มีตำแหน่ง
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // pipeline คาดหวัง 0..1
บรรทัดสุดท้ายนั้นประสานกับค่า default ที่คุณอาจไม่คาดความมั่นใจของ Tesseract วิ่งจาก 0 ถึง 100 ท่อทำงานในช่วง 0 ถึง 1 และ THPDFOCRTextLayerOptions.MinimumConfidence default เป็น 0.5 คำของ Tesseract ใดที่ต่ำกว่า 50 จึงถูกนับใน Info.DroppedWordCount และไม่เคยไปถึงหน้า บนภาพสแกน 300 DPI ที่สะอาดมันเป็นพื้นที่สมเหตุสมผล บนแฟกซ์ที่ noisy มันอาจทิ้งสัดส่วนของหน้าไปจนน่าตกใจ และทางที่ถูกคือดูจำนวนที่ถูกทิ้งก่อนลดเกณฑ์ เพราะความมั่นใจต่ำคือพวกที่ผิดมากที่สุดพอดี
child process ของ Tesseract ได้รับสิ่งใดตกทอด
child process ของ Tesseract ได้รับ handle ตกทอดจาก HotPDF พอดีสองตัว: handle NUL สำหรับ standard input กับ output และ file handle สำหรับ standard error ความเป๊ะนี้แหละคือแก่นเรื่อง CreateProcess ด้วย bInheritHandles = True เป็นวิธีส่ง standard handle ให้ child แต่ตามลำพังมันส่ง handle ที่สืบทอดได้ทุกตัวในโปรเซสเจ้าบ้านไปด้วย รวมถึงไฟล์ ท่อ และ event ที่โค้ดอื่นในแอปพลิเคชันของคุณเปิดไว้ child จึงพวงวัตถุเหล่านั้นไว้ไม่ปล่อยจนกว่ามันจะจบ ไฟล์จึงค้างล็อกหรือท่อไม่เคยเห็นปลายทางขณะ Tesseract บดหน้าหนึ่งอยู่ HotPDF ปิดช่องว่างนี้ด้วย record สตาร์ตอัปแบบขยาย: STARTUPINFOEX, attribute list ที่แบก PROC_THREAD_ATTRIBUTE_HANDLE_LIST และธงสร้าง EXTENDED_STARTUPINFO_PRESENT เมื่อ list ของ handle อยู่ในตำแหน่ง bInheritHandles ยังต้องเป็น True แต่มีแค่ handle ที่ลิสต์ไว้ข้ามเขตแดน ความคิดเรื่องการกักขังแบบเดียวกันเป็นแกนของการแยก codec ภาพของ PDF ไว้ใน worker process ที่ child เป็นโค้ดที่ไม่น่าเชื่อถือ ที่นี่ child น่าเชื่อถือ แต่เจ้าบ้านก็ไม่ใช่เจ้าของตาราง handle ของตัวเองเพียงผู้เดียว
// ค่าคงที่แสดงด้วยชื่อ; ซอร์สส่งค่าตัวเลขของมัน
// handle ทั้งสองถูกสร้างด้วย bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin กับ stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt ในไดเรกทอรีส่วนตัว
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // ที่ list ของ handle ทวง
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
ทำไมการรัน OCR ที่ถูกยกเลิกถึงดูเหมือนความล้มเหลวของ engine
การรัน OCR ที่ถูกยกเลิกดูเหมือนความล้มเหลวของ engine เพราะ IHPDFOCREngine.Recognize คืนค่า Boolean ตัวเดียว และ False หมายถึงทั้ง "Tesseract พัง" กับ "ผู้ใช้กดยกเลิก" adapter poll token การยกเลิกกับ timeout ทุก 25 มิลลิวินาทีขณะ child วิ่ง เมื่อ token ยิงมันโยน exception ข้างใน Recognize คว้า exception ของตัวเอง เก็บกวาด แล้วคืน False พร้อม diagnostic ถ้า pipeline ถือว่าเป็น error ของ engine caller จะเห็น otlsEngineError กับงานที่ผู้ใช้หยุดด้วยเจตนา ApplyLoadedOCRTextLayer จึงเช็ก token ก่อนเป็นอย่างแรกทุกครั้งที่ Recognize คืน False และแปลงผลเป็นความล้มเหลวของ engine เมื่อ token ไม่ได้ถูกตั้งเท่านั้น ลำดับแบบนี้รักษาสัญญาหลายหน้า: การรู้จำ การตรวจ การนับ budget และการประกอบเนื้อหาวิ่งให้ทุกหน้าที่ขอไว้ก่อน transaction ของ graph เปิด การยกเลิกที่หน้า 40 จาก 50 จึงรายงาน otlsCancelled และปล่อยเอกสาร รวมถึงสามสิบเก้าหน้าแรก ให้เหมือนเดิม ไม่มีไฟล์ค้นหาได้ครึ่ง ๆ กลาง ๆ ต้องไปอธิบายทีหลัง และการจัดการความล้มเหลวที่เหลือเดินตามสไตล์ที่มีขอบเขตแบบเดียวกัน:
- timeout อยู่ต่อหนึ่ง call
Recognizeนับจากจุดเริ่มของมัน default 60,000 ms จึงมีผลกับแต่ละหน้า ไม่ใช่ทั้งเอกสาร - child ที่ยังวิ่งอยู่ตอน timeout หรือการยกเลิกถูก terminated รอมันไม่เกิน 5 วินาที และไดเรกทอรีส่วนตัวถูกลบในบล็อก
finally output.tsvเพดาน 64 MiB และstderr.txtเพดาน 1 MiB เช็กระหว่างที่ child วิ่งและหลังมันจบด้วย- จำนวนคำกับ code unit ของ UTF-16 ถูกเพดานต่อหน้าด้วย budget
MaxWordsPerPage,MaxTotalWordsกับMaxTextCodeUnitsที่เหลืออยู่ ล้นพวกมันคือรอบรันพัง ไม่ใช่ตัด list คำทิ้ง - standard output ไหลไป
NULเพราะ Tesseract เขียนoutput.tsvขณะที่ standard error ไหลไปไฟล์ รหัสออกไม่เป็นศูนย์จึงถูกรายงานพร้อมข้อร้องของ engine เองได้ถึง 4,096 ตัวอักษร ซึ่งมักเป็นทางเร็วที่สุดที่จะรู้ว่าไฟล์.traineddataหาย
คำที่รู้จำได้กลายเป็นชั้นข้อความที่มองไม่เห็นอย่างไร
HotPDF เขียนคำของ Tesseract เป็นข้อความที่มองไม่เห็นด้วย text rendering mode 3 โหมดไม่เติมไม่ลากเส้นตามนิยามใน ISO 32000-1 §9.3.6 หน้าจึงยังแสดงภาพที่สแกนมาขณะที่การค้นหากับคัดลอกทำงานบนคำที่รู้จำได้ content stream เปิด BT ด้วย 3 Tr คำแต่ละคำได้ matrix Tm ที่ baseline ของมัน, ขนาดฟอนต์ที่ derive จากความสูงของกล่องเป็นพิกเซลที่ DPI ของการ render และสเกลแนวนอน Tz ที่ยืดชุด glyph ให้เต็มความกว้างกล่องที่วัดได้ ซึ่งเป็นเหตุผลที่ไฮไลต์การค้นหาตกลงบนคำในภาพ แทนที่จะล่องลอยข้ามมันไป
TSV ของ Tesseract มีแต่กล่องไม่มี baseline adapter จึงรายงานทุกคำแบบไม่มีเส้นและ pipeline ประมาณ baseline ไว้หนึ่งในห้าของความสูงกล่องเหนือขอบล่าง ข้อความเองเดินผ่านฟอนต์ Type0 แบบไม่ฝังที่ใช้ร่วมกัน encoding Identity-H พร้อม CMap ToUnicode ที่สร้างขึ้นหนึ่ง CID ต่อ Unicode scalar ที่ต่างกันหนึ่งตัวตลอดทั้งชุดรัน ภาษาจีน ละตินที่ติดวรรณยุกต์ และตัวอักษรบน supplementary plane จึงรอดจากการคัดลอกกับค้นหาทั้งคู่ ดีไซน์นี้มีขีดจำกัดสองข้อที่ควรพูดตั้งแต่ต้น: หนึ่งชุดรันแบก scalar ที่ต่างกันได้ไม่เกิน 65,535 ตัว และฟอนต์แบบไม่ฝังไม่เข้าข้อบังคับการฝังฟอนต์ของ ISO 19005 output PDF/A จึงต้องการฟอนต์ที่เข้าข่ายซึ่งฝังแยกต่างหาก การเช็กผลง่ายและสมควรทำเป็นอัตโนมัติ: เซฟ โหลดใหม่ แล้ววิ่งเส้นทางข้อความของเอกสารที่โหลดธรรมดาจากการดึงข้อความจาก PDF ที่โหลดมาใน Delphi คำกลับมาอยู่ในหน้าที่คาดไว้ ชั้นนั้นก็เป็นของจริง
RapidOCR กับ engine อื่นบนโปรโตคอล TSV เดียวกัน
HotPDF ใช้ runner ของโปรเซสกับ parser TSV ตัวเดิมซ้ำกับ RapidOCR ผ่าน HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds) ซึ่งเป็นตัวเลือกที่ใช้ประโยชน์กว่าสำหรับภาพสแกนจีนตัวย่อ command line เหมือนกันทุกอย่างยกเว้น path ของ bridge script ที่ถูกแทรกหลัง executable ของ Python และภาษาถูกตรึงไว้ที่ chi_sim HotPDF แถม bridge มาเป็น tools/OCR/rapidocr_tsv.py มันทวงแพ็กเกจ rapidocr กับ onnxruntime บวกโมเดล ONNX ท้องถิ่นสามตัว ปิดการดาวน์โหลดโมเดลอัตโนมัติ แล้วเขียน TSV ที่รูปร่างแบบ Tesseract ฝั่ง Delphi จึงไม่ต้องมี parser ที่สอง ชื่อ engine ที่รายงานใน Info.EngineName คือ RapidOCR (local ONNX) รูปร่างแบบนี้บอกสูตรลับทั่วไปได้: recognizer ใดก็ตามที่คุณห่อด้วยสคริปต์เล็ก ๆ ที่รับ argument list สไตล์ Tesseract แล้วปล่อย TSV สิบสองคอลัมน์ จะได้การแยก handle, timeout, การยกเลิก, budget output และการกระทำแบบเอาทั้งหมดหรือไม่เอาเลยมาฟรี ๆ adapter เหล่านี้ทำงานบน Windows เท่านั้น รันหนึ่งหน้าต่อครั้งแบบ sync และไม่ deskew หรือ preprocess ภาพเกินสิ่งที่ renderer ผลิต คุณภาพของภาพตอนเข้าจึงยังกำหนดเพดานของสิ่งที่ออกมา
adapter Tesseract กับ RapidOCR, ตัวเขียนชั้นข้อความที่มองไม่เห็น, renderer หน้าที่ป้อนพวกมัน และการดึงข้อความที่ตรวจยืนยันผล ทั้งหมด ship มาใน component VCL native ตัวเดียวกันสำหรับ Delphi และ C++Builder ถ้าคุณกำลังเติม OCR ให้แอปพลิเคชันจับเอกสารหรือเก็บ archive HotPDF Delphi PDF component ให้ pipeline มาพร้อม เหลือแค่ตัว OCR engine ที่ต้องติดตั้งเอง