HotPDF ทำให้หน้า PDF ที่สแกนค้นหาได้ด้วย RapidOCR แบบ in-process ผ่าน HPDFCreateRapidOCRDLLOCREngine factory ที่เพิ่มมาใน v2.774.0 ซึ่งโหลด HotPDFRapidOCR.dll เก็บ model ฝั่ง detection, angle classification กับ recognition ไว้ในหน่วยความจำตลอด แล้วคืน IHPDFOCREngine คุณส่ง engine ตัวนั้นเข้า THotPDF.ApplyLoadedOCRTextLayer มันจะ render ทีละหน้า รัน inference บน CPU โดยไม่มี Python และไม่มี child process แล้ว commit text layer Unicode มองไม่เห็นลงไป
แรงจูงใจคือต้นทุนต่อหน้า adapter ฝั่ง process ของ RapidOCR ที่ส่งมาก่อนหน้าคือ HPDFCreateRapidOCREngine ปลุก Python worker ใหม่ทุกครั้งที่เรียก Recognize และ worker ตัวนั้นต้อง import runtime กับโหลด model ONNX ก่อนจะได้อ่าน pixel สักตัว กับคลังเอกสาร 500 หน้า ภาษีตอนเริ่มงานนี้จ่ายซ้ำ 500 รอบ และการ deploy ก็แปลว่าต้องหอบสภาพแวดล้อม Python ไปวางข้าง executable ของ Delphi ฝั่ง DLL native โหลด model ครั้งเดียวตอนสร้าง engine และสัมภาระการ deploy เหลือแค่ DLL, ไฟล์ model กับ character dictionary สิ่งที่แลกไปคือความสามารถในการยิงตัว recognizer ที่ค้างทิ้ง และงานวิศวกรรมส่วนใหญ่ใน adapter ตัวนี้ก็คือการอยู่ร่วมกับข้อจำกัดนี้อย่างซื่อตรง
ทำให้ PDF ที่สแกนค้นหาได้ด้วย RapidOCR DLL อย่างไร
การสร้าง searchable PDF ด้วย RapidOCR DLL native ใช้ factory call ครั้งเดียวกับ call ApplyLoadedOCRTextLayer ชุดเดิมที่ OCR engine ทุกตัวของ HotPDF ใช้ factory อยู่ในยูนิต HPDFRapidOCRRecognition และตรวจแบบเร่งรัด: ต้องมี DLL กับไดเรกทอรี model จริง ไฟล์ model กับ dictionary ทุกตัวต้อง resolve ได้ ABI version ต้องเป็น 1 และ export ที่จำเป็นต้องครบก่อน model ตัวใดจะถูก initialize ข้อผิดพลาดด้านการตั้งค่า raise EArgumentException ส่วน model ที่โหลดไม่ขึ้น raise EInvalidOperation พร้อมข้อความ diagnostic ที่ DLL เขียนไว้
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// model ถูกโหลดตรงนี้ นอก deadline ของ recognition ใด ๆ
// ชื่อ model แบบ relative ใน THPDFRapidOCRDLLOptions.Default ถูก resolve
// เทียบกับไดเรกทอรีของ model
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
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,
' lines accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
THPDFRapidOCRDLLOptions.Default ระบุ ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx และ ppocr_keys_v1.txt พร้อม thread CPU หนึ่งเส้น ขีดจำกัด input 16,777,216 pixel และ deadline ของ recognition 60,000 ms ตั้งแต่ v2.775.0 THPDFRapidOCRDLLOptions.ForLanguage สลับ model กับ dictionary ของ recognition ให้ตรงกับจีนตัวเต็ม รัสเซีย ญี่ปุ่น อาหรับ และโปรไฟล์อื่น ๆ เหตุผลที่ model กับ dictionary ต้องเปลี่ยนพร้อมกันเล่าไว้ในmodel หลายภาษาของ RapidOCR กับ CTC dictionary ใน HotPDF engine รายงานตัวเองเป็น RapidOCR (native DLL) ใน Info.EngineName log จึงไม่ปนกับadapter ฝั่ง process ของ Tesseract OCR ภายนอกและengine OCR จับคู่เทมเพลตในตัว
ทำไม C ABI คุยได้แค่ int32_t กับไบต์ UTF-8
ABI ของ HotPDFRapidOCR.dll ใช้แต่จำนวนเต็มความกว้างตายตัว pointer ดิบ ๆ และความยาวไบต์แบบแจ้งชัด เพราะ Delphi, C++Builder และ Free Pascal ไม่มีอะไรแชร์กันกับ MSVC นอกจาก C calling convention std::string, std::vector หรือ exception ของ C++ มี layout กับโมเดลการคลี่ stack ที่เป็นของ compiler หนึ่งตัวกับ runtime library หนึ่งตัว ปล่อยให้สิ่งเหล่านี้ข้ามเขตไปเมื่อไร ความพังคือ stack เละหรือ heap block ถูก free ด้วย allocator คนละตัว ไม่ใช่ error สะอาด ๆ
ABI version 1 จึงยึดกฎสั้น ๆ ชุดหนึ่ง export ทุกตัวเป็น cdecl และคืน status เป็น int32_t โดย 1 หมายถึงสำเร็จ 0 หมายถึงล้ม ฟังก์ชันที่ล้มได้ทุกตัวรับ diagnostic buffer ที่ caller เป็นเจ้าของพร้อมความจุเป็นไบต์ DLL เขียนข้อความ UTF-8 ปิดท้าย NUL ที่ตัดให้พอดี และ adapter ถอดมันโดยวาง terminator ตายตัวไว้ในไบต์สุดท้ายของ buffer 4,096 ไบต์ของตัวเอง ตัว body ของ export แต่ละตัวถูกห่อด้วย try พร้อมทั้ง catch (const std::exception &) และ catch (...) error ของ ONNX Runtime, assertion ของ OpenCV หรือ dictionary ที่ไม่ถูกต้องจึงกลายเป็น status 0 บวกข้อความ ไม่ใช่ exception หลุดข้ามเขตไปเข้าโค้ด Pascal
| Export | หน้าที่ | adapter resolve เมื่อไร |
|---|---|---|
HPDFRapidOCRAbiVersion | คืน 1 ค่าอื่นใดถูกปฏิเสธ | ก่อนอย่างอื่นทั้งหมด |
HPDFRapidOCRCreate | โหลด model detection, classification แบบเลือกได้, recognition และ dictionary | ข้างใน factory |
HPDFRapidOCRRecognize | รัน bitmap หนึ่งก้อนและปล่อย callback หนึ่งครั้งต่อบรรทัดข้อความ | ข้างใน factory |
HPDFRapidOCRDestroy | free instance ของ model | ข้างใน factory |
HPDFRapidOCRSetReadingDirection | ลำดับแถวขวาไปซ้ายแบบเลือกได้ เพิ่มใน v2.775.0 | เฉพาะเมื่อเซ็ต RightToLeft |
export ตัวเสริมถูก resolve แบบ lazy ตั้งใจ: DLL ของ v2.774.0 ที่ไม่มีมันยังเสิร์ฟงานซ้ายไปขวาได้ปกติ DLL ถูกโหลดด้วย LoadLibraryEx พร้อม search flag ที่ครอบคลุมโฟลเดอร์ของ DLL เองบวก safe directories ค่าเริ่มต้น dependency ของ ONNX Runtime หรือ OpenCV ที่วางเคียง HotPDFRapidOCR.dll จึงถูกเจอโดยไม่ต้องแตะ PATH path ของ model กับ dictionary เดินทางเป็น UTF-8 และ DLL แปลงด้วย MultiByteToWideChar โหมด strict ก่อนเปิดไฟล์ผ่าน wide-character API ไดเรกทอรี model ใต้ชื่อผู้ใช้จีนหรือซีริลลิกจึงใช้ได้ ไม่ใช่ถูกขยายทีละไบต์จนกลายเป็นตัวอักษรมั่ว
กฎหนึ่งอยู่ที่การ build ไม่ใช่ใน header DLL ลิงก์ ONNX Runtime กับ OpenCV แบบ static และค่าเริ่มต้นของ CMake ใช้ static release CRT (/MT) static library ที่ compile บน /MD ปนเข้าไปใน DLL แบบ /MT ให้ link error ในกรณีดีที่สุด และ heap อิสระกันสองชุดในกรณีเลวที่สุด library ที่จัดหาให้จึงต้องตรงกับโหมด CRT ที่ DLL ใช้
ระหว่าง TBitmap กับบรรทัดข้อความมีอะไรเกิดขึ้น
HotPDF ส่ง snapshot ของหน้าที่ render แล้วให้ DLL ในรูป BGR หัวลงแบบเป็นอิสระ และ DLL ส่งกลับ callback หนึ่งครั้งต่อบรรทัดข้อความที่จดจำได้ พร้อม text UTF-8 แบบยืมใช้ที่ adapter ต้อง copy ก่อน return
บน Delphi adapter assign bitmap ของหน้าเข้า TBitmap ส่วนตัว บังคับเป็น pf24bit แล้วอ่านแถวด้วย GetDIBits ด้วย biHeight ค่าลบ ซึ่งให้แถวแบบหัวลงที่ pad ตามการจัดแนวสี่ไบต์ stride ถูกส่งแบบแจ้งชัด บน FPC มันอ่านผ่าน CreateIntfImage เพราะการเขียน scanline ของ LCL อัปเดตภาพดิบได้โดยไม่ refresh GDI handle bitmap ของ caller ไม่เคยถูกแก้ และงบ pixel (MaxPixels ค่าเริ่มต้น 16,777,216 ปรับได้ถึง 67,108,864) กับขีดจำกัด 32,767 pixel ต่อ dimension ถูกเช็คก่อน buffer ของ snapshot จะถูกจอง
ข้างใน DLL snapshot ถูก pad ด้วย pixel ขาว 50 ตัวรอบทิศ region ของ text ถูก detect ด้วยด้านยาวสุด 1,024 pixel box ถูกเรียงเป็นแถวแนวนอน และแต่ละ crop ถูกหมุนตาม angle classifier แบบเลือกได้ก่อน recognition จากนั้นบรรทัดข้อความแต่ละบรรทัดไหลผ่าน callback ที่รับ const char*, จำนวนไบต์, box จำนวนเต็มหน่วย pixel ของภาพต้นฉบับ และความเชื่อมั่นเฉลี่ยต่ออักขระ pointer ของ text มีผลเฉพาะระหว่าง callback adapter จึง copy ทันที และมันเข้มงวดกับสิ่งที่ยอมรับ:
- UTF-8 ถูกถอดด้วย
MB_ERR_INVALID_CHARSลำดับที่เสียรูปทำให้หน้า fail ทั้งหน้า ไม่ใช่ผลิต replacement character ลงใน searchable layer - อักขระควบคุม C0 กับ C1 ถูกปฏิเสธ และบรรทัดที่มีแต่ช่องว่างถูกข้าม
- box ต้องอยู่ข้างใน bitmap และ confidence ต้องเป็นค่าจำกัดระหว่าง 0 ถึง 1
- text ถูกนับเข้า
MaxTextCodeUnitsของคำขอด้วยเพดานตายตัว 1,048,576 หน่วย UTF-16 ต่อ call อักขระ supplementary plane กินสองหน่วย - exception ของ Pascal ใน callback ถูกจับไว้ตรงนั้น เก็บไว้ แล้วแปลงเป็นการ return 0 ซึ่งทำให้ DLL หยุดและรายงานความล้มเหลว ข้อความที่เก็บไว้กลายเป็น diagnostic
ผลพวงสองข้อสำคัญกับการปรับแต่ง ประการแรกหน่วยของ output คือบรรทัด ไม่ใช่คำ: แต่ละบรรทัดกิน slot MaxWords หนึ่งช่อง Info.AcceptedWordCount กับ Info.DroppedWordCount นับบรรทัด และการ highlight ค้นหาครอบ box ของบรรทัด ประการที่สอง MinimumConfidence (ค่าเริ่มต้น 0.5) ถูกเทียบกับความเชื่อมั่นเฉลี่ยของบรรทัด บรรทัดที่มีอักขระอ่านไม่ออกหนึ่งตัวท่ามกลางอีกยี่สิบตัวที่สะอาดจึงมักรอด DLL ไม่ให้ baseline มา pipeline ของ text layer จึงประมาณเองจาก box หน้าว่าง ๆ สำเร็จด้วยศูนย์บรรทัด และความล้มเหลวใด ๆ จะล้างผลบางส่วนทิ้ง การ commit หลายหน้าจึงเป็น all-or-nothing
ความเป็นเจ้าของ model และ thread safety
engine ของ RapidOCR DLL แต่ละตัวเป็นเจ้าของ instance ของ model เป๊ะหนึ่งตัวตลอดอายุ และการเรียก Recognize บน engine ตัวนั้นถูกเรียงลำดับด้วย critical section การถือ interface IHPDFOCREngine ไว้คือสิ่งที่ทำให้ model ยังอุ่น pattern ที่ถูกต้องของงานแบบ batch คือสร้าง engine ครั้งเดียวแล้วใช้ซ้ำข้ามเอกสาร
procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
Models: THPDFRapidOCRDLLOptions;
Engine: IHPDFOCREngine;
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
I: Integer;
begin
Models := THPDFRapidOCRDLLOptions.Default;
Models.UseAngleClassifier := False; // scan ตั้งตรง: ไม่โหลด model classifier
Models.Threads := 4; // 1..64, หนีบที่จำนวน logical processor
Models.TimeoutMilliseconds := 120000; // ต่อ call Recognize หนึ่งครั้ง แบบ cooperative
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Options := THPDFOCRTextLayerOptions.Default;
for I := 0 to Files.Count - 1 do
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if (Doc.LoadFromFile(Files[I]) > 0) and
Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
ExtractFileName(Files[I]))
else
Writeln(Files[I], ': ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
end; // reference สุดท้ายถูกปล่อย: model ถูกทำลาย แล้ว DLL จึงถูก unload
ค่า Threads เซ็ตจำนวน thread ทั้ง intra-op และ inter-op ของ ONNX session แต่ละตัว และ DLL หนีบไว้ที่จำนวน processor ที่ใช้งานอยู่ สอง thread ที่แชร์ engine เดียวไม่ได้รันขนานกัน ตัวที่สองรอ lock การรอนั้นไม่ใช่ EnterCriticalSection แบบหลับตา: adapter เรียก TryEnterCriticalSection ทุก 25 ms แล้วเช็ค cancellation token กับ deadline ระหว่างรอบ คำขอที่จองคิวไว้จึงยังถูกยกเลิกหรือหมดเวลาได้ ถ้าต้องการ parallelism จริง ๆ ให้สร้าง engine หนึ่งตัวต่อ worker แล้วรับว่า engine แต่ละตัวถือสำเนา model ของตัวเองไว้ในหน่วยความจำ
ลำดับ teardown ถูก fix โดย destructor ของ engine: HPDFRapidOCRDestroy free instance ของ model ก่อน แล้ว FreeLibrary ค่อย unload DLL ฝั่ง native การ initialize model ก็ระวังเท่ากัน เมื่อ model ของ recognition ล้มหลัง detector กับ classifier session ถูกสร้างไปแล้ว session พวกนั้นถูก release ก่อนรายงาน error และจำนวนคลาสของ dictionary ถูกเทียบกับ output ของ model ตอน initialize ไม่ใช่รอหน้าแรก
ทำไม kill call ของ OCR native กลาง inference ไม่ได้
call ของ RapidOCR ฝั่ง native kill กลาง inference ไม่ได้ เพราะมันรันบน thread ของคุณ ข้างใน process ของคุณ กลาง ONNX Runtime session ที่ไม่รับการขัดจังหวะ การ cancellation ใน adapter DLL ของ HotPDF จึงเป็นแบบ cooperative: DLL เรียก abort callback ก่อนและหลัง detection, หลัง classification และหลังแต่ละบรรทัดที่จดจำได้ แล้วหยุดที่ checkpoint แรกที่ callback คืน 0 ONNX Run ที่เริ่มไปแล้วต้องรันจนจบก่อน
ทางเลือกแย่กว่าการรอ TerminateThread จะทิ้ง CRT heap lock, thread pool ของ ONNX Runtime และสถานะของ OpenCV ทุกชิ้นไว้ในสภาพที่มันบังเอิญกำลังเป็น พิษตกที่ process ส่วนที่เหลือทั้งหมด FreeLibrary ขณะ call ยังกำลังรันคือการ unload โค้ดที่นั่งอยู่บน stack ทั้งคู่ทำให้ปลอดภัยไม่ได้ adapter จึงไม่เคยพยายาม deadline ใน TimeoutMilliseconds จึงเป็น deadline แบบ cooperative และ deadline ที่หมดอายุจะปรากฏเป็น error ของ engine พร้อม diagnostic บอกว่า timeout ส่วน token ที่ถูกยกเลิกปรากฏเป็น otlsCancelled:
// Token ถูกสร้างโดย caller และแชร์กับ UI thread
// ซึ่งเรียก Token.Cancel เมื่อผู้ใช้กด Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// คืนค่าที่ขอบของขั้นหรือบรรทัดถัดไป เอกสารไม่เปลี่ยน
Writeln('Cancelled');
otlsEngineError:
// รวม deadline หมดอายุแบบ cooperative กับ diagnostic ฝั่ง native
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
นี่คือ trade-off แกนหลักระหว่าง adapter ฝั่ง process ของ HotPDF กับ DLL แบบ in-process และไม่มีฝั่งไหนชนะทุกแถว:
- ต้นทุนตอนเริ่ม: adapter ของ Tesseract กับ Python RapidOCR ปลุก process และโหลด model ทุกหน้า DLL โหลด model ครั้งเดียวต่อ engine
- การหยุด: child process ถูก terminate ตรง ๆ ได้ และ Python worker รันอยู่ข้างใน Job Object แบบ kill-on-close ต้นไม้ process ทั้งกิ่งจึงไปพร้อมกัน ส่วน DLL หยุดได้แค่ขอบของขั้นกับบรรทัด
- การกักความพัง: crash ใน
tesseract.exeทำหน้าหนึ่งล้ม access violation ข้างใน DLL พา process ของคุณลงทั้งตัว - การ deploy: adapter ฝั่ง process ต้องมีโปรแกรมติดตั้งหรือสภาพแวดล้อม Python DLL ต้องการแค่ตัวมันเอง, model และ dictionary ที่ตรงกับ bitness ของแอปพลิเคชัน
- หน่วยความจำ: adapter ฝั่ง process คืนทุกอย่างเมื่อ child ออก engine ของ DLL คง model ไว้ในหน่วยความจำจน reference สุดท้ายของ interface ถูกปล่อย
สำหรับแอปเดสก์ท็อปเชิงโต้ตอบที่ OCR ทีละหน้า ความตอบสนองของ DLL มักชนะ สำหรับเซิร์ฟเวอร์ที่กลืนสแกนที่ไม่น่าเชื่อถือรอบนาฬิกา กำแพง process คุ้มค่ากับต้นทุนตอนเริ่มงาน
การ build และ deploy HotPDFRapidOCR.dll
HotPDFRapidOCR.dll ถูก build จากซอร์ส C++ ใน Native/RapidOCR ด้วย MSVC, C++17, Windows SDK และ CMake 3.20 ขึ้นไป ผ่าน helper script ที่รับ directory ของ source จากเครือข่ายแบบ native, ONNX Runtime กับ OpenCV บวก platform Win32 หรือ Win64 ถ้าส่งมอบทั้งสองแบบก็ build ทั้งคู่ เพราะแอป Delphi 32 บิตโหลด DLL 64 บิตไม่ได้ และ static library ที่จัดหาต้องตรงทั้งสถาปัตยกรรมเป้าหมายและโหมด CRT
ฝั่ง model มีขีดจำกัดความเข้ากันได้ของตัวเอง detector เป็น DB text detector recognizer รับ model แบบ CTC ใน layout NCHW ที่ความสูง input ตายตัว 32 หรือ 48 และใช้ 48 กับ model ที่ความสูงแบบ dynamic ONNX Runtime แบบ static ที่แถมมาไม่โหลด model ที่ save ด้วย IR version ใหม่กว่า model export ของ PP-OCRv5 ล่าสุดจึง fail ตอน initialize พร้อม diagnostic แทนที่จะโหลดได้ครึ่ง ๆ กลาง ๆ dictionary ต้องเป็น UTF-8 ไม่มี BOM เรียงอักขระตรงกับของ model เป๊ะ ๆ และจำนวนคลาสต้องตรงกับ output ของ model การจบบรรทัดแบบ CRLF ยอมรับ การ recognition เป็นแบบ offline DLL ไม่เคยดาวน์โหลด model ที่หายไป
สรุปด่วน
- factory:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])ในHPDFRapidOCRRecognitionใช้ได้ตั้งแต่ v2.774.0 บน Delphi, C++Builder และ build แบบ FPC/Lazarus บน Windows - คง
IHPDFOCREngineที่คืนมาให้อยู่ข้ามหน้ากับเอกสาร การปล่อยมันทำลาย model และ unload DLL - engine หนึ่งตัวรัน recognition หนึ่งงานต่อครั้ง สร้างหลาย engine สำหรับ worker ขนานกัน และจองหน่วยความจำให้สำเนา model ของแต่ละตัวด้วย
- output เป็น entry หนึ่งรายการต่อบรรทัดข้อความพร้อมความเชื่อมั่นเฉลี่ยของอักขระ กรองด้วย
THPDFOCRTextLayerOptions.MinimumConfidence - cancellation กับ
TimeoutMillisecondsเป็นแบบ cooperative ONNX run ที่กำลังรันต้องจบก่อนเสมอ - จับ bitness ของ DLL ให้ตรงแอปพลิเคชัน และจับโหมด CRT ของ static library ONNX Runtime กับ OpenCV ให้ตรง DLL
- เลือกโปรไฟล์ภาษาต่อ engine ด้วย
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0) engine เดียวไม่ได้ detect ภาษาเอง
adapter RapidOCR ฝั่ง native, adapter OCR ฝั่ง process, page renderer ที่หล่องานให้ทั้งคู่ และตัวเขียน text layer Unicode มองไม่เห็น มาพร้อมกันใน HotPDF ซึ่งเป็น VCL PDF component native สำหรับ Delphi กับ C++Builder ถ้าแอป capture เอกสารหรือ archive ของคุณต้องการ output ที่ค้นหาได้โดยไม่มี runtime Python บนเครื่องปลายทาง HotPDF Delphi PDF component ให้ pipeline ทั้งชุดเหลือให้ deploy แค่ DLL กับ model ของมัน