Articolo tecnico

RapidOCR in-process di HotPDF: OCR da DLL nativa in Delphi

HotPDF rende ricercabili le pagine PDF scansionate con RapidOCR in-process attraverso HPDFCreateRapidOCRDLLOCREngine, una factory aggiunta nella v2.774.0 che carica HotPDFRapidOCR.dll, tiene residenti in memoria i modelli ONNX di rilevamento, classificazione dell'angolo e riconoscimento, e restituisce un IHPDFOCREngine. Passi quel motore a THotPDF.ApplyLoadedOCRTextLayer, che renderizza ogni pagina, esegue inferenza su CPU senza Python né un processo figlio, e scrive un livello di testo Unicode invisibile

La motivazione è il costo per pagina. L'adapter di processo RapidOCR spedito in precedenza, HPDFCreateRapidOCREngine, avvia un worker Python per ogni chiamata Recognize, e quel worker importa il proprio runtime e carica i propri modelli ONNX prima di leggere un solo pixel. Su un archivio da 500 pagine quella tassa di avvio si ripete 500 volte, e il deployment significa spedire un ambiente Python accanto a un eseguibile Delphi. La DLL nativa carica i modelli una volta sola, quando crei il motore, e il deployment si riduce alla DLL, ai suoi file di modello e a un dizionario di caratteri. Quello che cedi in cambio è la possibilità di uccidere un riconoscitore bloccato, e gran parte dell'ingegneria di questo adapter riguarda il conviverci onestamente

Come rendi ricercabile un PDF scansionato con la DLL RapidOCR?

Creare un PDF ricercabile con la DLL nativa RapidOCR richiede una chiamata alla factory e la stessa chiamata ApplyLoadedOCRTextLayer che usa ogni motore OCR di HotPDF. La factory vive nell'unità HPDFRapidOCRRecognition e valida con eager: la DLL e la directory dei modelli devono esistere, ogni file di modello e dizionario deve risolversi, la versione ABI deve essere 1, e tutti gli export richiesti devono essere presenti prima che qualsiasi modello venga inizializzato. Gli errori di configurazione sollevano EArgumentException; un modello che non carica solleva EInvalidOperation con il testo diagnostico che la DLL ha scritto

Sequenza di validazione della factory DLL RapidOCR di HotPDF per HPDFCreateRapidOCRDLLOCREngine: percorsi e file di modello devono esistere, HPDFRapidOCRAbiVersion deve restituire 1, gli export richiesti devono risolversi, e HPDFRapidOCRCreate deve inizializzare i modelli, con EArgumentException o EInvalidOperation sollevate con eager prima che qualsiasi riconoscimento giri, la seconda con il testo diagnostico nativo
la validazione è eager di proposito: i problemi di configurazione si sollevano prima che qualsiasi modello si inizializzi, così un percorso o un ABI sbagliato non arriva mai a una scadenza di riconoscimento
uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // I modelli si caricano qui, fuori da qualsiasi scadenza di riconoscimento.
  // I nomi di modello relativi in THPDFRapidOCRDLLOptions.Default si risolvono
  // contro la directory dei modelli.
  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
    // una lista pagine vuota significa tutte le pagine; le pagine con testo vengono saltate
    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 nomina ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx, e ppocr_keys_v1.txt, con un thread CPU, un limite di ingresso di 16.777.216 pixel, e una scadenza di riconoscimento di 60.000 ms. Dalla v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage scambia in un modello di riconoscimento e un dizionario corrispondenti per cinese tradizionale, russo, giapponese, arabo e altri profili; perché modello e dizionario devono cambiare insieme è coperto in modelli multilingua RapidOCR e dizionari CTC in HotPDF. Il motore si dichiara come RapidOCR (native DLL) in Info.EngineName, il che tiene i log non ambigui accanto all'adapter del processo OCR Tesseract esterno e al motore OCR integrato a corrispondenza di template

Perché il C ABI parla solo int32_t e byte UTF-8?

Il ABI di HotPDFRapidOCR.dll usa solo interi a larghezza fissa, puntatori grezzi e lunghezze di byte esplicite perché Delphi, C++Builder e Free Pascal non condividono nulla con MSVC oltre la convenzione di chiamata C. Una std::string, una std::vector o un'eccezione C++ hanno un layout e un modello di unwinding che appartengono a un compilatore e a una libreria di runtime. Fai attraversare il confine a uno qualsiasi di questi e il fallimento è uno stack corrotto o un blocco heap liberato dall'allocatore sbagliato, non un errore pulito

La versione ABI 1 quindi segue una breve lista di regole. Ogni export è cdecl e restituisce uno stato int32_t, dove 1 significa successo e 0 fallimento. Ogni funzione che può fallire prende un buffer diagnostico di proprietà del chiamante e la sua capacità in byte; la DLL scrive un messaggio UTF-8 terminato da NUL troncato per stare nello spazio, e l'adapter lo decodifica con un terminatore duro nell'ultimo byte del proprio buffer da 4.096 byte. Il corpo di ogni export è avvolto in try con sia catch (const std::exception &) sia catch (...), così un errore di ONNX Runtime, un'asserzione OpenCV o un dizionario non valido diventa stato 0 più testo, mai un'eccezione che sfugge nel codice Pascal

ExportRuoloQuando l'adapter lo risolve
HPDFRapidOCRAbiVersionRestituisce 1; qualsiasi altro valore viene rifiutatoPer primo, prima di ogni altra cosa
HPDFRapidOCRCreateCarica rilevamento, classificazione opzionale, modelli di riconoscimento e il dizionarioNella factory
HPDFRapidOCRRecognizeEsegue una bitmap ed emette un callback per riga di testoNella factory
HPDFRapidOCRDestroyLibera l'istanza del modelloNella factory
HPDFRapidOCRSetReadingDirectionOrdine delle righe da destra a sinistra opzionale, aggiunto nella v2.775.0Solo quando RightToLeft è impostato

L'export opzionale viene risolto pigramente di proposito: una DLL v2.774.0 che ne è priva serve comunque le richieste da sinistra a destra. La DLL viene caricata con LoadLibraryEx con flag di ricerca che coprono la cartella della DLL stessa più le directory sicure di default, così le dipendenze ONNX Runtime o OpenCV appoggiate accanto a HotPDFRapidOCR.dll vengono trovate senza toccare PATH. I percorsi di modello e dizionario viaggiano come UTF-8 e la DLL li converte con MultiByteToWideChar in modalità strict prima di aprire i file attraverso le API wide-character, così una directory di modelli sotto un nome utente cinese o cirillico funziona invece di venire allargata byte per byte in nonsenso

Una regola vive nella build anziché nell'header. La DLL collega staticamente ONNX Runtime e OpenCV, e la configurazione CMake di default usa la CRT release statica (/MT). Librerie statiche compilate contro /MD mescolate in una DLL /MT producono al meglio errori di link e al peggio due heap indipendenti, quindi le librerie fornite devono corrispondere alla modalità CRT che la DLL usa

Che cosa succede tra una TBitmap e una riga di testo?

HotPDF passa alla DLL uno snapshot BGR top-down indipendente della pagina renderizzata, e la DLL restituisce un callback per riga di testo riconosciuta con testo UTF-8 in prestito che l'adapter deve copiare prima di restituire

Su Delphi l'adapter assegna la bitmap della pagina a una TBitmap privata, forza pf24bit, e legge le righe con GetDIBits usando una biHeight negativa, che dà righe top-down con padding all'allineamento a quattro byte; quello stride viene passato esplicitamente. Su FPC legge attraverso CreateIntfImage, perché le scritture scanline della LCL possono aggiornare l'immagine grezza senza rinfrescare l'handle GDI. La bitmap del chiamante non viene mai modificata, e il budget di pixel (MaxPixels, 16.777.216 per default e configurabile fino a 67.108.864) e il limite di 32.767 pixel per dimensione vengono controllati prima che il buffer dello snapshot venga allocato

Pipeline della DLL RapidOCR di HotPDF da bitmap a livello di testo: l'adapter scatta lo snapshot della pagina come BGR pf24bit top-down, la DLL aggiunge padding, rileva, ordina e riconosce i ritagli, consegna un callback per riga con testo UTF-8 in prestito, box e confidenza, e l'adapter valida ogni riga prima della scrittura del livello di testo
i pixel attraversano l'ABI una volta sola come snapshot, le righe tornano un callback alla volta, e nulla raggiunge il livello ricercabile finché ogni controllo non passa

Dentro la DLL lo snapshot viene riempito con 50 pixel bianchi, le regioni di testo vengono rilevate con un lato massimo di 1.024 pixel, le box vengono ordinate in righe orizzontali, e ogni ritaglio viene facoltativamente ruotato dal classificatore d'angolo prima del riconoscimento. Ogni riga di testo poi passa per un callback che riceve una const char*, un conteggio di byte, una box intera in pixel dell'immagine originale, e la confidenza media dei caratteri. Il puntatore al testo è valido solo durante il callback, quindi l'adapter lo copia immediatamente, ed è severo su ciò che accetta:

  • L'UTF-8 viene decodificato con MB_ERR_INVALID_CHARS; una sequenza malformata fa fallire la pagina invece di produrre caratteri sostitutivi in un livello ricercabile
  • I caratteri di controllo C0 e C1 vengono rifiutati, e le righe di soli spazi vengono saltate
  • La box deve giacere dentro la bitmap e la confidenza deve essere un valore finito da 0 a 1
  • Il testo viene contato contro il MaxTextCodeUnits della richiesta con un tetto duro di 1.048.576 unità UTF-16 per chiamata, e i caratteri del piano supplementare costano due unità
  • Qualsiasi eccezione Pascal dentro il callback viene colta lì, conservata, e trasformata in un ritorno 0, il che fa fermare la DLL e riportare fallimento; il messaggio conservato diventa poi la diagnostica

Due conseguenze contano per il tuning. Primo, l'unità di output è una riga, non una parola: ogni riga consuma uno slot MaxWords, Info.AcceptedWordCount e Info.DroppedWordCount contano righe, e l'evidenziazione della ricerca copre la box della riga. Secondo, MinimumConfidence (0.5 per default) viene confrontato con la confidenza media dei caratteri della riga, così una riga con un carattere illeggibile su venti puliti di solito sopravvive. La DLL non fornisce alcuna baseline, quindi la pipeline del livello di testo ne stima una dalla box. Una pagina vuota riesce con zero righe, e qualsiasi fallimento azzera i risultati parziali così la scrittura multipagina resta tutto-o-nulla

Proprietà dei modelli e thread safety

Ogni motore della DLL RapidOCR possiede esattamente una istanza di modello per l'intera propria vita, e le chiamate a Recognize su quel motore vengono serializzate da una sezione critica. Tenere l'interfaccia IHPDFOCREngine è ciò che tiene i modelli caldi, quindi il pattern giusto per il lavoro a lotti è creare il motore una volta e riutilizzarlo tra i documenti

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;    // scansioni dritte: nessun modello classificatore caricato
  Models.Threads := 4;                   // 1..64, limitato al numero di processori logici
  Models.TimeoutMilliseconds := 120000;  // per chiamata Recognize, cooperativo
  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;  // ultimo riferimento rilasciato: modelli distrutti, poi la DLL viene scaricata

Il valore Threads stabilisce sia i conteggi di thread intra-op sia inter-op di ogni sessione ONNX, e la DLL lo limita al numero di processori attivi. Due thread che condividono un motore non girano in parallelo; il secondo aspetta il lock. Quell'attesa non è una EnterCriticalSection alla cieca: l'adapter chiama TryEnterCriticalSection ogni 25 ms e controlla il token di cancellazione e la scadenza tra i tentativi, così una richiesta in coda può ancora essere cancellata o scadere. Se ti serve vero parallelismo, crea un motore per worker e accetta che ogni motore tenga la propria copia dei modelli in memoria

L'ordine di smontaggio è fissato dal distruttore del motore: HPDFRapidOCRDestroy libera prima l'istanza del modello, poi FreeLibrary scarica la DLL. Sul lato nativo, l'inizializzazione del modello è altrettanto attenta; quando il modello di riconoscimento fallisce dopo che le sessioni di rilevatore e classificatore erano già state costruite, quelle sessioni vengono rilasciate prima di riportare l'errore, e il conteggio delle classi del dizionario viene controllato contro l'output del modello durante l'inizializzazione anziché alla prima pagina

Perché una chiamata OCR nativa non può essere uccisa a metà inferenza?

Una chiamata RapidOCR nativa non può essere uccisa a metà inferenza perché gira sul tuo thread, dentro il tuo processo, in mezzo a una sessione ONNX Runtime che non accetta interruzioni. La cancellazione nell'adapter DLL di HotPDF è quindi cooperativa: la DLL chiama un callback di aborto prima e dopo il rilevamento, dopo la classificazione, e dopo ogni riga riconosciuta, e si ferma al primo checkpoint dove il callback restituisce 0. Una singola Run di ONNX che è partita finirà prima

Le alternative sono peggio dell'aspettare. TerminateThread lascerebbe il lock dell'heap della CRT, il thread pool di ONNX Runtime e qualsiasi stato OpenCV nella condizione in cui si trovavano, avvelenando il resto del processo. FreeLibrary mentre una chiamata è ancora in esecuzione scarica codice che sta sullo stack. Nessuna delle due può essere resa sicura, quindi l'adapter non ci prova mai. La scadenza in TimeoutMilliseconds è di conseguenza una scadenza cooperativa, e una scadenza scaduta emerge come errore del motore con una diagnostica di timeout, mentre un token cancellato emerge come otlsCancelled:

// Il token è creato dal chiamante e condiviso con il thread UI,
// che chiama Token.Cancel quando l'utente preme Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  case Info.Status of
    otlsCancelled:
      // restituito al prossimo confine di stadio o riga; documento invariato
      Writeln('Cancelled');
    otlsEngineError:
      // include una scadenza cooperativa e diagnostiche native
      Writeln('Engine: ', string(Info.Diagnostic));
    otlsBudgetExceeded:
      Writeln('Budget: ', string(Info.Diagnostic));
  else
    Writeln(string(Info.Diagnostic));
  end;

Questo è il compromesso centrale tra gli adapter di processo di HotPDF e la DLL in-process, e nessuno dei due vince su ogni riga:

Compromessi degli adapter OCR di HotPDF: gli adapter di processo avviano un worker e caricano i modelli a ogni pagina ma possono essere uccisi e contengono i crash, mentre la DLL RapidOCR in-process carica i modelli una volta, si ferma solo ai checkpoint cooperativi, condivide lo spazio di indirizzi, e si distribuisce come DLL con i suoi modelli e il suo dizionario
scegli per workload: un'app desktop una-pagina-alla-volta trae beneficio dalla DLL calda, mentre un server che ingurgita scansioni non fidate dovrebbe pagare per il muro del processo
  • Costo di avvio: gli adapter Tesseract e Python RapidOCR lanciano un processo e caricano i modelli per ogni pagina; la DLL carica i modelli una volta per motore
  • Fermata: un processo figlio può essere terminato outright, e il worker Python gira dentro un Job Object kill-on-close così il suo intero albero di processi se ne va con lui; la DLL può fermarsi solo ai confini di stadio e riga
  • Contenimento dei guasti: un crash in tesseract.exe fa fallire una pagina; una access violation dentro la DLL ti porta giù il processo
  • Deployment: gli adapter di processo hanno bisogno di un programma installato o di un ambiente Python; la DLL ha bisogno di sé stessa, dei suoi modelli e del suo dizionario, corrispondenti alla bitness dell'applicazione
  • Memoria: gli adapter di processo rilasciano tutto quando il figlio esce; un motore DLL tiene i modelli residenti finché l'ultimo riferimento all'interfaccia viene rilasciato

Per un'applicazione desktop interattiva che fa OCR di una pagina alla volta, la reattività della DLL di solito vince. Per un server che ingurgita scansioni non fidate giorno e notte, il confine di processo vale il proprio costo di avvio

Costruire e distribuire HotPDFRapidOCR.dll

HotPDFRapidOCR.dll si costruisce dai sorgenti C++ in Native/RapidOCR con MSVC, C++17, una Windows SDK, e CMake 3.20 o successivo, usando uno script ausiliario che prende le directory delle sorgenti native di rete, ONNX Runtime, e OpenCV più una piattaforma Win32 o Win64. Costruisci entrambe se spedisci entrambe, perché un'applicazione Delphi a 32 bit non può caricare una DLL a 64 bit, e le librerie statiche che fornisci devono corrispondere all'architettura bersaglio oltre alla modalità CRT

Il lato modelli ha i propri limiti di compatibilità. Il rilevatore è un DB text detector; il riconoscitore accetta modelli CTC in layout NCHW con un'altezza di ingresso fissa di 32 o 48, e usa 48 per i modelli ad altezza dinamica. La ONNX Runtime statica inclusa non può caricare modelli salvati con una versione IR più recente, quindi i recenti export PP-OCRv5 falliscono l'inizializzazione con una diagnostica invece di caricarsi parzialmente. Il dizionario deve essere UTF-8 senza BOM, nell'esatto ordine di caratteri del modello, e il suo conteggio di classi deve corrispondere all'output del modello; i finali di riga CRLF sono accettati. Il riconoscimento è offline: la DLL non scarica mai un modello mancante

Riferimento rapido

  • Factory: HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]) in HPDFRapidOCRRecognition, disponibile dalla v2.774.0 nelle build Delphi, C++Builder e Windows FPC/Lazarus
  • Tieni vivo il IHPDFOCREngine restituito tra pagine e documenti; rilasciarlo distrugge i modelli e scarica la DLL
  • Un motore esegue un riconoscimento alla volta; crea più motori per worker paralleli e metti in budget la memoria per ogni copia del modello
  • L'output è una voce per riga di testo con confidenza media dei caratteri, filtrata da THPDFOCRTextLayerOptions.MinimumConfidence
  • Cancellazione e TimeoutMilliseconds sono cooperativi; una esecuzione ONNX in corso completa sempre
  • Abbina la bitness della DLL all'applicazione e la modalità CRT delle librerie statiche ONNX Runtime e OpenCV alla DLL
  • Scegli un profilo lingua per motore con THPDFRapidOCRDLLOptions.ForLanguage (v2.775.0); un motore non rileva da solo le lingue

L'adapter RapidOCR nativo, gli adapter OCR basati su processo, il renderer di pagine che li alimenta, e lo scrittore del livello di testo Unicode invisibile viaggiano tutti insieme in HotPDF, un componente PDF VCL nativo per Delphi e C++Builder. Se la tua applicazione di acquisizione o archiviazione documenti ha bisogno di output ricercabile senza un runtime Python sulla macchina bersaglio, il HotPDF Delphi PDF component fornisce l'intera pipeline con solo la DLL e i suoi modelli da distribuire