HotPDF รัน Tesseract ข้างใน process ของ Delphi คุณผ่าน HPDFCreateTesseractDLLOCREngine factory ที่เพิ่มมาใน v2.772.0 ซึ่งโหลด DLL ที่เข้ากันได้กับ Tesseract 5 แบบ dynamic ขับเคลื่อน C API ของมัน (TessBaseAPIInit2, TessBaseAPIRecognize กับ result iterator) แล้วคืน IHPDFOCREngine THotPDF.ApplyLoadedOCRTextLayer ใช้ engine ตัวนั้นเพิ่ม text layer Unicode มองไม่เห็นที่ค้นหาได้ลงหน้า PDF ที่สแกน
recognizer ตัวเดิมเข้าถึงได้แล้วผ่านadapter ของ tesseract.exe ภายนอกที่เขียน BMP แล้ว parse TSV เส้นทางนั้นใช้ได้ แต่ทุกหน้าจ่ายค่าปลุก process ค่าไฟล์ bitmap ชั่วคราว และค่า format ข้อความที่ไม่มี baseline กับไม่มีทางคุม page segmentation การเรียก DLL ตัดทั้งสามอย่างทิ้ง มันยังตัดกำแพง process ด้วย ซึ่งแปลว่า binding ของ Pascal นั่งวางตรง ๆ บนโครงสร้าง C, Boolean แบบ C และ string ที่ฝั่ง C จอง สิ่งที่ควรรู้เกี่ยวกับ adapter ตัวนี้ส่วนใหญ่จึงคือจุดที่ binding พังไปเงียบ ๆ ได้ที่ไหน
รัน Tesseract in-process จาก Delphi ด้วย HotPDF อย่างไร
การรัน Tesseract in-process ด้วย HotPDF ใช้ factory call ครั้งเดียวในยูนิต HPDFTesseractRecognition กับ call ApplyLoadedOCRTextLayer ชุดเดิมที่ OCR engine ทุกตัวของ HotPDF ใช้ factory ตรวจแบบเร่งรัด ไฟล์ DLL กับไดเรกทอรี tessdata ต้องมีอยู่จริง identifier ของภาษามีได้แค่ตัวอักษร ASCII ตัวเลข _ กับ + model ทุกตัวในคู่ผสมอย่าง chi_sim+eng ต้องมีไฟล์ .traineddata ตรงกัน และ export ที่จำเป็นทั้ง 21 ตัวต้อง resolve ได้ก่อน engine จะถูกคืน ข้อผิดพลาดด้านการตั้งค่า raise EArgumentException DLL ที่โหลดไม่ขึ้น raise EOSError พร้อมรหัส error ของ Windows และคำใบ้ให้เช็คสถาปัตยกรรมกับ dependency
uses
SysUtils, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// แอป Win64 ต้องใช้ DLL 64 บิต dependency DLL วางเคียงตัวมันไว้
Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
'C:\OCR\tessdata', 'chi_sim+eng'); // THPDFTesseractOptions.Default
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
// รายการหน้าว่างหมายถึงทุกหน้า หน้าที่มี text อยู่แล้วถูกข้าม
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
THPDFTesseractOptions.Default เซ็ต PageSegMode เป็น tpsAuto, EngineMode เป็น temDefault, TimeoutMilliseconds เป็น 60,000 และ MaxPixels เป็น 16,777,216 งบ pixel สำคัญกว่าที่ดู หน้า US Letter ที่ค่าเริ่มต้น 300 DPI render ได้ 2,550 × 3,300 pixel หรือราว 8.4 ล้าน พอดีกับงบ หน้าเดิมที่ 600 DPI คือ 5,100 × 6,600 หรือราว 33.7 ล้าน และ adapter ปฏิเสธก่อน Tesseract จะได้เห็น pixel สักตัว ให้เพิ่ม MaxPixels (เพดานคือ 67,108,864) หรือปล่อย DPI ไว้เหมือนเดิม แต่ละด้านยังถูกจำกัดที่ 32,767 pixel ด้วย
DLL ถูกโหลดด้วย LoadLibraryEx พร้อม search flag ของโฟลเดอร์ของ DLL เองบวก safe directories ค่าเริ่มต้น image library ที่ Tesseract พึ่งพาจึงวางเคียงมันได้โดยไม่ต้องแตะ PATH หรือ current directory HotPDF ไม่แถมและไม่ดาวน์โหลด runtime หรือ model ของ OCR ใด ๆ ให้คุณจัดหาทั้งคู่
อะไรเปลี่ยนไปเทียบกับ adapter ของ tesseract.exe
adapter ฝั่ง DLL แลก process isolation กับ output ที่อุดมขึ้นและค่าใช้จ่ายต่อหน้าที่ถูกลง ทั้งสอง adapter เสียบเข้า pipeline ของ text layer ตัวเดียวกัน การแมปพิกัด, การกรองด้วย confidence และการ commit แบบ all-or-nothing จึงเหมือนกันเป๊ะ สิ่งที่ต่างคือ pixel ไหลเข้ายังไงและคำไหลออกมายังไง
| ด้าน | adapter ของ tesseract.exe | adapter ของ Tesseract DLL |
|---|---|---|
| factory | HPDFCreateTesseractOCREngine | HPDFCreateTesseractDLLOCREngine |
| pixel เข้า | ไฟล์ BMP ในไดเรกทอรีชั่วคราวส่วนตัว | buffer grayscale 8 บิตในหน่วยความจำ |
| คำออก | TSV ระดับคำ จำกัดที่ 64 MiB | result iterator, UTF-8 ต่อคำ |
| baseline | ไม่มีให้ใช้ | ส่งผ่านจาก TessPageIteratorBaseline |
| page segmentation กับ engine mode | automatic segmentation เท่านั้น | THPDFTesseractPageSegMode, THPDFTesseractEngineMode |
| timeout | แบบเด็ดขาด child process ถูก terminate | แบบ cooperative Tesseract ต้องสังเกตเอง |
| การกั้น crash กับหน่วยความจำ | process แยกต่างหาก | ไม่มี ใช้ address space เดียวกับคุณ |
ต้นทุนหนึ่งอย่างไม่หายไป call Recognize แต่ละครั้งสร้าง instance ของ API ของตัวเองแล้วเรียก TessBaseAPIInit2 language model จึงถูก initialize ต่อหน้า แทนที่จะครั้งเดียวต่อ engine file cache ของระบบปฏิบัติการช่วยลดภาระการโหลดซ้ำ แต่กับชุด model หลายภาษาที่ใหญ่ มันยังเป็นต้นทุนตายตัวต่อหน้าที่ครองเก้าอี้และถูกนับเข้า deadline ของ recognition engine ของ RapidOCR DLL แบบ in-processเลือกดีไซน์สวนทางและเก็บ model ONNX ไว้ตลอดอายุของ engine ส่วนปัญหาเรื่องขอบเขต (C ABI, buffer แบบยืมใช้, งาน native ที่ขัดจังหวะไม่ได้) เป็นตระกูลเดียวกัน
ทำไม Delphi ถึงก๊อปโครงสร้าง monitor ของ Tesseract ไม่ได้
Delphi ก๊อป progress monitor ของ Tesseract แบบปลอดภัยไม่ได้ เพราะ ETEXT_DESC มี field ภายในที่ต่างกันไปตามเวอร์ชัน record ที่ก๊อปมือจึงวาง cancel callback กับ deadline ไว้ผิด offset บนบาง build เมื่อเกิดแบบนั้นไม่มีอะไรบ่นดัง ๆ Tesseract แค่อ่าน pointer ของ callback คุณจาก field ที่ตอนนี้ถือของอื่นอยู่ หรือไม่เคยเห็น deadline เลย
HotPDF จึงถือ monitor เป็น pointer กล่องดำแล้วแตะมันผ่าน exported functions เท่านั้น: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs และ TessMonitorDelete ถ้าคุณ bind C API เองเพื่อจุดประสงค์อื่น pattern เดียวกันนี้ใช้ได้ sketch ด้านล่างคือโค้ด binding ของคุณ ไม่ใช่ API ของ HotPDF และสะท้อน declaration ที่ HotPDF ใช้ภายใน
type
// C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
TTessMonitorCreate = function: Pointer; cdecl; // ETEXT_DESC*, ห้าม dereference
TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;
TOCRJob = record
CancelRequested: Boolean;
DeadlineTick: UInt64;
end;
POCRJob = ^TOCRJob;
function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
// รันบน stack ของ Tesseract: อ่าน flag กับนาฬิกาเท่านั้น ห้าม raise
Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
(GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;
// วิธีใช้ โดย resolve function pointer ด้วย GetProcAddress:
// Monitor := MonitorCreate();
// try
// MonitorSetCancelThis(Monitor, @Job);
// MonitorSetCancelFunc(Monitor, ShouldCancel);
// MonitorSetDeadlineMSecs(Monitor, RemainingMs);
// RC := BaseAPIRecognize(API, Monitor);
// finally
// MonitorDelete(Monitor);
// end;
รายละเอียดสองจุดใน sketch นั้นตั้งใจ callback คืน Boolean ซึ่งเป็นขนาดหนึ่งไบต์ทั้งใน Delphi กับ Free Pascal ตรงกับ bool ของ C ใน TessCancelFunc BOOL สี่ไบต์ของ Windows หรือ LongBool ของ Delphi ดูเหมือนสลับกันได้แต่ไม่ได้ เมื่อฝั่งหนึ่งเขียนหนึ่งไบต์แล้วอีกฝั่งอ่านสี่ไบต์ ไบต์บนของ register ค่าคืนคือสิ่งที่ค้างอยู่ตรงนั้น และ false ก็มาถึงในรูป true ได้ header เดียวกันยังเพิ่มเกมด้วยเรื่องที่ฟังก์ชันอย่าง TessPageIteratorBoundingBox คืน int ซึ่ง HotPDF ประกาศเป็น Integer ให้อ่านชนิด C ของค่าคืนทุกตัว อย่าสมมติ convention เดียวทั้ง API
รายละเอียดที่สองคือ callback ไม่เคย raise exception ของ Delphi ที่คลี่ผ่าน frame ของ C++ ของ Tesseract เป็น undefined behavior callback ของ HotPDF จึงอ่านแค่ cancellation token กับค่า GetTickCount64 แบบ monotonic adapter เปลี่ยนผลเป็น diagnostic ของการยกเลิกหรือ timeout หลัง TessBaseAPIRecognize กลับมา และมันทำเช็คนั้นไม่ว่ารหัสคืนฝั่ง native จะเป็นอะไร
pointer ฝั่ง native ตัวไหนที่ฝั่ง Delphi เป็นเจ้าของ
adapter ของ Tesseract DLL ใน HotPDF เป็นเจ้าของ object ฝั่ง native สามชิ้นต่อ request คือ instance ของ API, monitor และ result iterator ส่วนที่เหลือยืมใช้ call Recognize แต่ละครั้งสร้างชุดของตัวเองแล้ว release ในบล็อก finally: TessResultIteratorDelete ก่อน ตามด้วย TessMonitorDelete แล้วจึง TessBaseAPIDelete การปล่อย interface ของ engine จะ unload library
TessResultIteratorGetPageIteratorคืนมุมมองแบบยืมเข้าไปใน result iterator ไม่ใช่ object ใหม่ HotPDF ใช้มันกับTessPageIteratorBoundingBoxกับTessPageIteratorBaselineแล้วไม่เคย free มัน การลบมันแยกจะ free หน่วยความจำเดียวกันสองรอบTessResultIteratorGetUTF8Textคืน string ที่ runtime ของ DLL เป็นคนจอง HotPDF ก๊อปมันแล้วคืนผ่านTessDeleteTextในบล็อกfinallyFreeMemฝั่ง Pascal จะ release มันบน heap ผิดตัว- text ของคำถูกถอดด้วยการตรวจ UTF-8 โหมด strict และเช็คความยาวก่อนแปลง คำที่มีอักขระควบคุม, UTF-8 เสียรูป, box ที่หลุดออกนอกภาพ, สี่เหลี่ยมกลับหัว หรือ confidence นอกช่วง 0–100 ทำให้ request fail ทั้งชุด ไม่ใช่ถูกปะแก้เงียบ ๆ
- text รวมต่อ request ถูกจำกัดที่ 1,048,576 หน่วย UTF-16 และจำนวนคำต้องพอกับงบที่
ApplyLoadedOCRTextLayerส่งมอบลงมา
confidence มาถึงในรูป 0–100 และถูกปรับเป็น 0–1 THPDFOCRTextLayerOptions.MinimumConfidence จึงมีความหมายเดียวกันกับทุก engine เมื่อ Tesseract รายงาน baseline ปลายทั้งสองถูกส่งผ่านไป ไม่งั้น pipeline ของ text layer จะย้อนไปใช้การประมาณเชิงเรขาคณิต พอดีกับที่มันทำกับ input แบบ TSV
ทำไมต้อง validate enum ก่อนส่งเข้า DLL
HotPDF ก๊อป ordinal ดิบของ PageSegMode กับ EngineMode ลง Integer ก่อนเช็คช่วง เพราะ compiler อาจสมมติว่าตัวแปร enum ถือค่าที่ประกาศไว้เสมอ แล้วยุบ Ord(X) > Ord(High(T)) เป็น false ค่าคงที่ ordinal ไม่ใช่ของตกแต่ง: THPDFTesseractPageSegMode ไล่หมายเลข page segmentation ของ Tesseract จาก 0 ถึง 13 THPDFTesseractEngineMode ไล่หมายเลข engine mode จาก 0 ถึง 3 และทั้งคู่ถูกส่งเข้า DLL เป็นจำนวนเต็มดิบ ๆ record ของ options ที่ปั้นด้วย FillChar เติมมาจาก stream หรือถูกส่งมาจาก C++Builder พร้อม cast integer สามารถพาไบต์ค่า 200 มาด้วย การ validate ordinal ที่ก๊อปแล้วเปลี่ยนมันเป็น EArgumentException ตอน factory แทนที่จะเป็น mode ไม่นิยามข้างในโค้ด native factory ยังปฏิเสธ tpsOSDOnly กับ tpsAutoOnly ซึ่งไม่ผลิตคำให้เลย และบังคับ osd.traineddata สำหรับ tpsAutoOSD กับ tpsSparseTextOSD
timeout ของ recognition การันตีอะไรจริง ๆ
timeout ของ Tesseract DLL เป็นแบบ cooperative: HotPDF หยุดงานของตัวเองและขอให้ Tesseract หยุดได้ แต่บังคับโค้ด native ให้ return ไม่ได้ นาฬิกาเดินเมื่อ Recognize เริ่ม การแปลง bitmap กับการ initialize model จึงกินงบเดียวกับ recognition HotPDF เช็คเวลาที่ผ่านไปกับ cancellation token ระหว่างการแปลง grayscale และระหว่างคำขณะไล่ผลลัพธ์ แล้วส่งมิลลิวินาทีที่เหลือให้ TessMonitorSetDeadlineMSecs ก่อนเรียก TessBaseAPIRecognize
ช่องว่างอยู่ข้างใน call ฝั่ง native monitor ของ Tesseract ถูกปรึกษาระหว่างการจดจำคำ ไม่ใช่ระหว่าง TessBaseAPIInit2 หรือการวิเคราะห์ layout หน้า model ที่โหลดช้าหรือ layout ที่พิศวงจึงวิ่งเลย deadline ได้ก่อน timeout จะถูกรายงาน งบ pixel กับ output ก็ไม่ได้จำกัดการใช้หน่วยความจำของ library ฝั่ง native เองด้วย ถ้าต้องการ worker ที่ kill ได้ ให้ใช้ adapter ฝั่ง process นั่นคือการแลกแบบซื่อ ๆ ไม่ใช่ฟีเจอร์ที่หายไป
page segmentation คือที่ที่ adapter ฝั่ง DLL เอาค่าตอบแทนบน input ยาก ๆ ฟอร์ม, ป้ายกำกับ และตารางที่สแกนพร้อม field กระจัดกระจายมักถูกจดจำได้ดีกว่าด้วย tpsSparseText มากกว่า automatic segmentation ซึ่งพยายามประกอบ column กับย่อหน้าที่ไม่มีอยู่จริง
procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
Engine: IHPDFOCREngine;
TessOptions: THPDFTesseractOptions;
LayerOptions: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
TessOptions := THPDFTesseractOptions.Default;
TessOptions.PageSegMode := tpsSparseText; // field กระจัดกระจาย ไม่ประกอบ column
TessOptions.EngineMode := temLSTMOnly; // ต้องมี model LSTM ใน tessdata
TessOptions.TimeoutMilliseconds := 20000; // รวมการ initialize model ด้วย
Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
'C:\OCR\tessdata', 'eng+deu', TessOptions);
LayerOptions := THPDFOCRTextLayerOptions.Default;
LayerOptions.MinimumConfidence := 0.6;
if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
case Info.Status of
otlsCancelled:
Writeln('OCR cancelled, document unchanged');
otlsEngineError:
Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
end;
timeout ปรากฏเป็น otlsEngineError พร้อม diagnostic Tesseract DLL OCR timed out ส่วน token ที่ถูกยกเลิกปรากฏเป็น otlsCancelled ทั้งสองเคส ApplyLoadedOCRTextLayer จดจำหน้าที่เลือกทุกหน้าจบก่อนเริ่ม transaction ของการ commit ความล้มเหลวที่หน้า 40 จาก 50 จึงปล่อยเอกสารที่โหลดไว้ให้เหมือนเดิมเป๊ะ จดไว้ว่า tpsSingleLine, tpsSingleBlock กับ tpsSparseText เปลี่ยนแค่ segmentation ไม่มีตัวไหนทำให้สแกนที่เอียงตรงขึ้น
Free Pascal กับ Lazarus: pixel ค้างเก่ากับจีนที่หายไป
factory ของ Tesseract ทั้งสองตัวทำงานบน Free Pascal ฝั่ง Windows และ Lazarus build Win32 กับ Win64 ตั้งแต่ v2.772.1 หลังการแก้เฉพาะ FPC สองชิ้น rebuild แพ็กเกจของ Lazarus ให้สถาปัตยกรรมเป้าหมายก่อน ส่วน port ภาพรวมเล่าไว้ในHotPDF บน Free Pascal และ Lazarus Win64
การแก้แรกอยู่ที่ pixel TBitmap ของ LCL ที่เขียนผ่าน scanline อัปเดตภาพดิบได้โดยไม่ refresh Windows bitmap handle GetDIBits บน handle นั้นจึงคืน pixel เก่า อาการที่ว่าพอให้งง: ข้อความที่วาดลง bitmap โดยตรงถูกจดจำได้ ขณะที่หน้าที่ render โดย PDF renderer ของ HotPDF ให้รายการคำว่างเปล่า บน FPC adapter ตอนนี้อ่าน snapshot ที่รู้รูปแบบผ่าน CreateIntfImage ซึ่งเคารพ pixel format กับลำดับแถวของภาพดิบ build ฝั่ง Delphi คงเส้นทาง GetDIBits บนสำเนา 24 บิตส่วนตัว ทั้งสอง build ไม่แตะ bitmap ของ caller เลย
การแก้ที่สองเป็นของ adapter ฝั่ง tesseract.exe TStringList ของ FPC เก็บ string แบบ ANSI การ assign text แบบ TSV ที่ถอดเป็น UTF-8 แล้วลง Lines.Text จึงทิ้งอักขระจีนหรือ supplementary plane ทุกตัวที่ system ANSI code page เขียนไม่ได้แบบเงียบ ๆ เส้นทางของ FPC ตอนนี้เก็บ TSV เป็นไบต์ UTF-8 ตัด BOM ที่ระดับไบต์แล้วถอดคำทีละตัวเป็น UnicodeString adapter ฝั่ง DLL ไม่เคยมีปัญหานี้ เพราะมันถอดแต่ละคำจาก iterator โดยตรง
สรุปด่วน
- factory:
HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options])ในHPDFTesseractRecognitionเพิ่มใน v2.772.0 รองรับ FPC ใน v2.772.1 - ค่าเริ่มต้น:
tpsAuto,temDefault, 60,000 ms, 16,777,216 pixel ช่วง timeout 1–3,600,000 ms เพดาน pixel 67,108,864 - จับ bitness ของ DLL ให้ตรงแอปพลิเคชัน แล้ววาง dependency DLL เคียงกับ Tesseract DLL
- ถือ monitor เป็นกล่องดำ ห้ามก๊อป
ETEXT_DESCลง record ของ Pascal เด็ดขาด - ประกาศ cancel callback เป็น
cdeclพร้อมผลลัพธ์Booleanหนึ่งไบต์ และห้ามปล่อย exception หลุดออกจากมัน - free text ของ iterator ด้วย
TessDeleteTextห้าม free page iterator ที่ได้จาก result iterator - คาดหวังว่า deadline เป็นแบบ cooperative การ initialize model กับการวิเคราะห์ layout วิ่งเลยมันได้
- ใช้ adapter ของ tesseract.exe เมื่อต้องการ termination แบบเด็ดขาดหรือการกั้น crash
adapter ฝั่ง Tesseract DLL, adapter ฝั่ง process และ engine OCR ในตัวมาพร้อมกันใน HotPDF Delphi PDF component สำหรับ Delphi, C++Builder และ Free Pascal ดูหน้า product ของ HotPDF สำหรับ edition และดาวน์โหลด