Artikel Teknis

Tesseract OCR ke PDF Searchable di Delphi dengan HotPDF

HotPDF mengubah halaman PDF hasil pindai menjadi PDF searchable dengan Tesseract lewat HPDFCreateTesseractOCREngine, sebuah factory yang membungkus executable Tesseract yang terpasang lokal sebagai IHPDFOCREngine. Anda menyerahkan engine itu ke ApplyLoadedOCRTextLayer, yang merender setiap halaman, menjalankan Tesseract sekali per halaman, mem-parse output TSV level kata-nya, dan meng-commit text layer Unicode tak terlihat untuk semua halaman yang diminta dalam satu transaksi, atau untuk tidak sama sekali

Pipeline OCR HotPDF per halaman: render halaman pada DPI yang dikonfigurasi, simpan input.bmp di direktori HotPDF-OCR privat, luncurkan proses anak Tesseract dengan tessedit_create_tsv, parse TSV dua belas kolom, saring kata menurut confidence, dan commit text layer tak terlihat untuk semua halaman yang diminta atau tidak sama sekali
Adapter hanya menggantikan pengenalan: rendering, parsing, validasi, dan commit semua-atau-tidak sama sekali tetap berada di pipeline text layer yang sudah ada, jadi kode hilir tak pernah berubah

Alasan adapter ini ada adalah cakupan. OCR engine bawaan berbasis template matching sengaja dibuat sempit: huruf dan angka ASCII hasil cetakan mesin, tidak lebih. Faktur dengan nama beraksen, kontrak bahasa Mandarin, dan arsip multi-bahasa butuh recognizer sungguhan dengan language model terlatih, dan Tesseract adalah kandidat yang paling jelas karena ia program command-line yang bisa Anda sediakan di samping aplikasi Anda. Memanggil program eksternal dari sebuah document library terdengar sepele. Tidak sepele, dan sebagian besar kode menarik di adapter ini justru soal apa yang terjadi ketika program itu berperilaku buruk, menggantung, dibatalkan, atau mewarisi hal-hal yang tak seharusnya dilihatnya

Bagaimana HotPDF menggerakkan Tesseract dari aplikasi Delphi?

HotPDF menjalankan Tesseract sebagai proses anak tersembunyi per halaman, memberinya bitmap hasil render dan membaca kembali file TSV, dan mengekspos hasilnya lewat seam IHPDFOCREngine yang sama dengan yang dipakai engine bawaan. Tak ada yang berubah di hilir: pemetaan koordinat, penanganan rotasi, validasi Unicode, penyaringan confidence, dan commit atomik adalah pipeline text layer yang sudah Anda miliki. Factory-nya tinggal di unit HPDFTesseractRecognition dan memvalidasi dengan cerewet: executable-nya harus ada, direktori tessdata harus ada, timeout harus di antara 1 dan 3.600.000 milidetik, dan identifier bahasa hanya boleh memuat huruf ASCII, angka, _, dan +. Pemeriksaan terakhir itu penting karena string bahasa berakhir di sebuah command line, dan eng+chi_sim adalah nilai Tesseract yang sah sementara apa pun dengan kutip atau spasi bukan

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // melempar EArgumentException untuk executable yang hilang, tessdata yang hilang,
  // identifier bahasa yang buruk, atau timeout di luar 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // beberapa model dirangkai dengan '+'
    120000);            // batas per halaman, bawaannya 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;
    // daftar halaman kosong berarti semua halaman; halaman berteks dilewati secara bawaan
    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;

Untuk setiap halaman, Recognize membuat direktori privat di bawah temp path bernama HotPDF-OCR-{GUID}, menyimpan bitmap hasil render sebagai input.bmp, dan meluncurkan tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, dengan setiap argumen path dikutip memakai aturan escaping command-line Windows untuk backslash dan kutip terselip. Nilai --dpi adalah DPI render dari THPDFOCRTextLayerOptions.DPI, jadi Tesseract tak perlu menduga resolusi dari metadata gambar, dan --psm 3 meminta segmentasi halaman yang sepenuhnya otomatis. Engine ini melaporkan dirinya sebagai Tesseract (local CLI), dan itulah yang mendarat di Info.EngineName. Tesseract dan language model-nya tidak dibundel dengan HotPDF; memasangnya adalah pekerjaan aplikasi Anda

Kenapa parser TSV-nya seketat itu?

Parser TSV di HotPDF menggagalkan seluruh halaman pada baris malformed mana pun, karena daftar kata yang ter-parse sebagian menghasilkan text layer yang diam-diam berseberangan dengan gambarnya. Output TSV milik Tesseract punya header dua belas kolom yang tetap, dari level sampai text, dan HotPDF membandingkan baris pertama terhadap header persis itu setelah mencabut byte order mark opsional. Setiap baris berikutnya harus terbelah menjadi tepat dua belas field, dan pembelahan berhenti setelah tab kesebelas supaya tab di dalam teks hasil pengenalan tetap bagian dari kata alih-alih menciptakan kolom ketiga belas. Hanya baris level 5 yang berupa kata; level 1 sampai 4 menggambarkan halaman, blok, paragraf, dan baris, dan mereka dilewati. Baris level 5 yang teksnya kosong atau whitespace murni juga dilewati, karena kata kosong punya kotak tapi tak ada yang bisa dilokasi atau dicari. Sisanya diperiksa keras: geometri integer, confidence yang di-parse dengan format en-US invarian supaya locale Jerman tak membaca 93.5 sebagai sampah, kotak yang sepenuhnya berada di dalam bitmap, dan confidence antara 0 dan 100. Satu kegagalan melempar exception, engine mengembalikan False, dan array katanya dibersihkan. Regresinya mencakup persis kasus itu: satu kata valid disusul satu baris rusak harus menghasilkan nol kata, bukan satu

Enam gate yang dilalui setiap baris TSV Tesseract di HotPDF: header dua belas kolom persis, tepat dua belas field, hanya level 5, teks tak kosong, kotak di dalam bitmap, dan confidence 0 sampai 100 yang di-parse secara invarian, di mana satu baris rusak menggagalkan seluruh halaman hingga nol kata
Daftar kata yang ter-parse sebagian akan diam-diam berseberangan dengan gambar, jadi parser menolak seluruh halaman pada baris malformed pertama alih-alih mempertahankan kata-kata yang sudah dibacanya
// dipadatkan dari loop level-5 di 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;                 // baris halaman/blok/paragraf/baris
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // kata whitespace tak punya posisi
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 mengharapkan 0..1

Baris terakhir itu berinteraksi dengan satu bawaan yang mungkin tak Anda duga. Confidence Tesseract berjalan dari 0 sampai 100, pipeline bekerja di 0 sampai 1, dan THPDFOCRTextLayerOptions.MinimumConfidence bawaannya 0,5, jadi setiap kata Tesseract di bawah 50 terhitung di Info.DroppedWordCount dan tak pernah sampai ke halaman. Pada pindai 300 DPI yang bersih itu lantai yang wajar. Pada fax yang bising, itu bisa membuang porsi halaman yang mengejutkan, dan langkah yang benar adalah melihat jumlah yang dibuang sebelum menurunkan ambangnya, karena kata berconfidence rendah persislah yang paling mungkin salah

Apa yang diwarisi proses anak Tesseract?

Proses anak Tesseract mewarisi tepat dua handle dari HotPDF: handle NUL untuk input dan output standar, dan handle file untuk standard error. Presisi itulah intinya. CreateProcess dengan bInheritHandles = True adalah cara Anda memberi standard handle ke anak, tapi begitu saja ia meneruskan setiap handle yang bisa diwarisi di proses host, termasuk file, pipe, dan event yang dibuka kode tak berkaitan di aplikasi Anda. Anaknya lalu mempertahankan objek-objek itu hidup sampai ia keluar, jadi sebuah file tetap terkunci atau sebuah pipe tak pernah melihat ujungnya selagi Tesseract menggerus satu halaman. HotPDF menutup celah itu dengan startup record yang diperluas: STARTUPINFOEX, attribute list yang membawa PROC_THREAD_ATTRIBUTE_HANDLE_LIST, dan flag pembuatan EXTENDED_STARTUPINFO_PRESENT. Dengan handle list terpasang, bInheritHandles tetap harus True, tapi hanya handle yang terdaftar yang melintasi batasnya. Pola pikir penahanan yang sama menggerakkan mengisolasi codec gambar PDF di worker process, di mana anaknya kode tak terpercaya; di sini anaknya terpercaya, tapi host bukan pemilik tunggal tabel handle miliknya sendiri

Pewarisan handle proses anak Tesseract di HotPDF: CreateProcess polos dengan bInheritHandles meneruskan setiap handle file, pipe dan event yang bisa diwarisi ke anaknya, sementara STARTUPINFOEX dengan PROC_THREAD_ATTRIBUTE_HANDLE_LIST membatasi set-nya menjadi satu handle NUL untuk stdin dan stdout plus handle file stderr
Tanpa attribute list, anaknya mempertahankan objek tak berkaitan hidup sampai ia keluar, mengunci file dan menghabisi pipe; dengan itu, hanya dua handle terdaftar yang melintasi batasnya
// konstanta ditampilkan dengan namanya; sumbernya mengirim nilai numeriknya
// kedua handle dibuat dengan bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin dan stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt di direktori privat
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,                                        // disyaratkan handle list
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Kenapa run OCR yang dibatalkan bisa tampak seperti kegagalan engine?

Run OCR yang dibatalkan tampak seperti kegagalan engine karena IHPDFOCREngine.Recognize mengembalikan satu Boolean, dan False berarti "Tesseract gagal" sekaligus "pengguna menekan Cancel". Adapter mem-polling cancellation token dan timeout setiap 25 milidetik selama anaknya berjalan, dan ketika token menyala ia melempar exception di dalam Recognize, menangkap exception miliknya sendiri, membersihkan, dan mengembalikan False dengan diagnostik. Andai pipeline memperlakukan itu sebagai error engine, pemanggil akan melihat otlsEngineError untuk pekerjaan yang sengaja dihentikan penggunanya. ApplyLoadedOCRTextLayer karena itu memeriksa token lebih dulu setiap kali Recognize mengembalikan False, dan hanya mengubah hasilnya menjadi kegagalan engine jika token tak disetel. Urutan itu menjaga kontrak multi-halaman: pengenalan, validasi, pembukuan budget, dan pembangunan konten berjalan untuk setiap halaman yang diminta sebelum transaksi graph-nya terbuka, jadi pembatalan di halaman 40 dari 50 melaporkan otlsCancelled dan membiarkan dokumennya, termasuk 39 halaman pertama, tak tersentuh. Tak ada file yang semi-searchable untuk dijelaskan belakangan, dan sisa penanganan kegagalannya mengikuti gaya berbatas yang sama:

  • Timeout-nya per panggilan Recognize, diukur dari mulainya, jadi bawaan 60.000 ms berlaku untuk setiap halaman alih-alih untuk seluruh dokumen
  • Anak yang masih berjalan saat timeout atau pembatalan di-terminate, ditunggu sampai 5 detik, dan direktori privatnya dihapus dalam blok finally
  • output.tsv dipatok 64 MiB dan stderr.txt 1 MiB, diperiksa selama anaknya berjalan maupun setelah ia keluar
  • Jumlah kata dan code unit UTF-16 dipatok per halaman oleh sisa budget MaxWordsPerPage, MaxTotalWords, dan MaxTextCodeUnits, dan melampauinya menggagalkan run alih-alih memotong daftar katanya
  • Standard output menuju NUL karena Tesseract menulis output.tsv, sementara standard error menuju file supaya exit code bukan nol dilaporkan dengan sampai 4.096 karakter keluhan milik engine sendiri, biasanya cara tercepat mengetahui bahwa file .traineddata hilang

Bagaimana kata hasil pengenalan menjadi text layer tak terlihat

HotPDF menulis kata-kata Tesseract sebagai teks tak terlihat dengan text rendering mode 3, mode tanpa-fill-dan-tanpa-stroke yang didefinisikan ISO 32000-1 §9.3.6, jadi halaman tetap menampilkan gambar hasil pindai sementara pencarian dan penyalinan bekerja pada kata-kata hasil pengenalan. Content stream membuka BT dengan 3 Tr, dan setiap kata kebagian matriks Tm di baseline-nya, ukuran font yang diturunkan dari tinggi kotak dalam piksel pada DPI render, dan skala horizontal Tz yang meregangkan run glyph ke lebar kotak hasil ukur, dan itulah kenapa highlight pencarian mendarat di kata yang ada di gambar alih-alih melayang melewatinya

TSV milik Tesseract punya kotak tapi tak punya baseline, jadi adapter melaporkan setiap kata tanpa baseline dan pipeline mengestimasi baseline di seperlima tinggi kotak di atas tepi bawahnya. Teksnya sendiri melewati satu font Type0 bersama yang tak tertanam dengan encoding Identity-H dan CMap ToUnicode yang dibuat otomatis, satu CID per Unicode scalar berbeda di sepanjang seluruh run, dan itulah cara karakter Mandarin, Latin beraksen, dan supplementary-plane selamat di penyalinan dan pencarian. Desain itu punya dua batas yang layak disebut di depan: satu run bisa membawa paling banyak 65.535 scalar berbeda, dan font tak tertanamnya tak memenuhi syarat penyematan font dari ISO 19005, jadi output PDF/A butuh font yang patuh yang tertanam terpisah. Memeriksa hasilnya sederhana dan layak diotomasi: simpan, muat ulang, dan jalankan jalur teks dokumen-termuat yang biasa dari mengekstrak teks dari PDF termuat di Delphi; kalau kata-katanya kembali di halaman yang diharapkan, layer-nya sungguhan

RapidOCR dan engine lain di protokol TSV yang sama

HotPDF memakai ulang process runner dan parser TSV yang sama untuk RapidOCR lewat HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), yang merupakan pilihan lebih berguna untuk pindai bahasa Mandarin sederhana. Command line-nya identik kecuali path script bridge disisipkan setelah executable Python, dan bahasanya dipatok chi_sim. HotPDF mengirim bridge itu sebagai tools/OCR/rapidocr_tsv.py; ia mengharapkan paket rapidocr dan onnxruntime plus tiga model ONNX lokal, mematikan unduhan model otomatis, dan menulis TSV berbentuk Tesseract supaya sisi Delphi tak butuh parser kedua. Nama engine yang dilaporkan di Info.EngineName adalah RapidOCR (local ONNX). Bentuk itu menyarankan resep umumnya: recognizer apa pun yang bisa Anda bungkus dalam skrip kecil yang menerima daftar argumen gaya Tesseract dan mengeluarkan TSV dua belas kolom mewarisi isolasi handle, timeout, pembatalan, budget output, dan commit semua-atau-tidak sama sekali secara gratis. Adapter-nya hanya untuk Windows, menjalankan satu halaman satu waktu secara sinkron, dan tak mendekemiringkan atau memproses-ulang gambar melebihi apa yang dihasilkan renderer, jadi kualitas gambar yang masuk tetap menetapkan langit-langit apa yang keluar

Adapter Tesseract dan RapidOCR, penulis text layer tak terlihat, renderer halaman yang memberi makan keduanya, dan ekstraksi teks yang memverifikasi hasilnya semuanya ikut terkirim di komponen VCL native yang sama untuk Delphi dan C++Builder. Kalau Anda sedang menambahkan OCR ke aplikasi document capture atau arsip, HotPDF Delphi PDF component memberi Anda pipeline-nya dengan hanya engine OCR-nya sendiri yang tersisa untuk dipasang