Teknisk artikel

Tesseract OCR til søgbar PDF i Delphi med HotPDF

HotPDF omdanner scannede PDF-sider til søgbar PDF med Tesseract gennem HPDFCreateTesseractOCREngine, en factory, der pakker en lokalt installeret Tesseract-eksekverbar som en IHPDFOCREngine. Du giver den engine videre til ApplyLoadedOCRTextLayer, som renderer hver side, kører Tesseract én gang pr. side, parser dets word-level TSV-output og committer et usynligt Unicode-tekstlag for alle ønskede sider i én transaktion, eller for ingen af dem

HotPDF OCR-pipeline pr. side: render siden ved den konfigurerede DPI, gem input.bmp i en privat HotPDF-OCR-mappe, start Tesseract-barneprocessen med tessedit_create_tsv, parser den tolv-kolonners TSV, filtrerer ord efter confidence og committer det usynlige tekstlag for alle ønskede sider eller ingen
Adapteren erstatter kun genkendelsen: rendering, parsing, validering og all-or-nothing-committet bliver i den eksisterende tekstlags-pipeline, så downstream-kode ændres aldrig

Grunden til, at adapteren findes, er scope. Den indbyggede template-matching OCR-engine er bevidst snæver: maskinskrevne ASCII-bogstaver og cifre, intet andet. Fakturaer med accenter i navnene, kinesiske kontrakter og flersprogede arkiver behøver en rigtig recognizer med trænede sprogmodeller, og Tesseract er den oplagte kandidat, fordi det er et command-line-program, du kan provisionere ved siden af din applikation. At kalde et eksternt program fra et dokumentbibliotek lyder trivielt. Det er det ikke, og det meste af den interessante kode i adapteren handler om, hvad der sker, når programmet opfører sig dårligt, hænger, bliver annulleret eller arver ting, det aldrig burde se

Hvordan driver HotPDF Tesseract fra en Delphi-applikation?

HotPDF kører Tesseract som en skjult barneproces pr. side, føder den en renderet bitmap og læser en TSV-fil tilbage og eksponerer resultatet gennem samme IHPDFOCREngine-søm, som den indbyggede engine bruger. Intet downstream ændres: koordinat-mapping, rotation-håndtering, Unicode-validering, confidence-filtrering og det atomare commit er den tekstlags-pipeline, du allerede har. Factoryen bor i HPDFTesseractRecognition-uniten og validerer tidligt: den eksekverbare skal findes, tessdata-mappen skal findes, timeouten skal ligge mellem 1 og 3.600.000 millisekunder, og sprog-identifikatoren må kun indeholde ASCII-bogstaver, cifre, _ og +. Sidstnævnte tjek betyder noget, fordi sprogstrengen ender på en command line, og eng+chi_sim er en legitim Tesseract-værdi, mens alt med anførselstegn eller mellemrum ikke er

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // rejser EArgumentException for manglende eksekverbar, manglende tessdata,
  // en dårlig sprog-identifikator eller en timeout uden for 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // flere modeller sat sammen med '+'
    120000);            // grænse pr. side, default er 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;
    // en tom sideliste betyder alle sider; sider med tekst skippes som 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;

For hver side opretter Recognize en privat mappe under temp-stien kaldet HotPDF-OCR-{GUID}, gemmer den renderede bitmap som input.bmp og starter tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, med hvert sti-argument i anførselstegn efter Windows' command-line escaping-regler for backslashes og indlejrede anførselstegn. --dpi-værdien er render-DPI'en fra THPDFOCRTextLayerOptions.DPI, så Tesseract aldrig behøver at gætte opløsningen ud fra billedmetadata, og --psm 3 beder om fuldt automatisk pagesegmentering. Enginen rapporterer sig selv som Tesseract (local CLI), hvilket er det, der lander i Info.EngineName. Tesseract og dets sprogmodeller følger ikke med HotPDF; at installere dem er applikationens job

Hvorfor er TSV-parseren så streng?

TSV-parseren i HotPDF fejler hele siden ved enhver misdannet række, for en delvist parseret ordliste producerer et tekstlag, der lydløst er uenigt med billedet. Tesseracts TSV-output har en fast tolv-kolonners header fra level til text, og HotPDF sammenligner første linje med netop den header efter at have strippet en eventuel byte order mark. Hver efterfølgende række skal splittes i præcis tolv felter, og splittelsen stopper efter den ellevte tab, så en tab inde i den genkendte tekst forbliver en del af ordet i stedet for at skabe en trettende kolonne. Kun level 5-rækker er ord; level 1 til 4 beskriver sider, blokke, afsnit og linjer, og de skippes. Level 5-rækker, hvis tekst er tom eller ren whitespace, skippes også, for et tomt ord har en boks, men intet at lokalisere eller søge i. Alt andet tjekkes hårdt: heltals-geometri, en confidence parset med et invariant en-US-format, så et tysk locale ikke læser 93.5 som skrald, en boks, der ligger fuldt inde i bitmappen, og en confidence mellem 0 og 100. Én enkelt fejl rejser, enginen returnerer False, og ord-arrayet ryddes. Regressionstestene inkluderer netop det tilfælde: ét gyldigt ord efterfulgt af en ødelagt række skal give nul ord, ikke ét

Seks gates, som hver Tesseract TSV-række passerer i HotPDF: eksakt tolv-kolonners header, præcis tolv felter, kun level 5, ikke-tom tekst, en boks inde i bitmappen og confidence fra 0 til 100 parset invariant, hvor én ødelagt række fejler hele siden helt ned til nul ord
En delvist parseret ordliste ville lydløst være uenig med billedet, så parseren afviser hele siden ved den første misdannede række i stedet for at beholde de ord, den allerede har læst
// komprimeret fra level-5-løkken i 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-rækker
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // whitespace-ord har ingen position
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;  // pipelinen forventer 0..1

Den sidste linje interagerer med en default, du måske ikke forventer. Tesseract-confidence løber fra 0 til 100, pipelinen arbejder i 0 til 1, og THPDFOCRTextLayerOptions.MinimumConfidence default'er til 0.5, så ethvert Tesseract-ord under 50 tæller med i Info.DroppedWordCount og når aldrig frem til siden. På en ren 300 DPI-scanning er det et rimeligt gulv. På en støjende fax kan det droppe en overraskende andel af siden, og det rigtige træk er at kigge på dropped-antallet, før du sænker tærsklen, for lav-confidence-ord er præcis dem, der med størst sandsynlighed er forkerte

Hvad arver Tesseract-barneprocessen?

Tesseract-barneprocessen arver præcis to handles fra HotPDF: et NUL-handle til standard input og output og et filhandle til standard error. Den præcision er pointen. CreateProcess med bInheritHandles = True er måden at give standard-handles til et child på, men alene giver den videre hvert nedarvbare handle i host-processen, inklusive filer, pipes og events åbnet af urelateret kode i din applikation. Childet holder derefter de objekter i live, til det slutter, så en fil forbliver låst, eller en pipe aldrig ser sin ende, mens Tesseract knokler sig gennem en side. HotPDF lukker det hul med en extended startup record: STARTUPINFOEX, en attributliste, der bærer PROC_THREAD_ATTRIBUTE_HANDLE_LIST, og oprettelsesflagget EXTENDED_STARTUPINFO_PRESENT. Med handle-listen på plads skal bInheritHandles stadig være True, men kun de listede handles krydser grænsen. Samme indhegningstænkning driver isolering af PDF-image-codecs i worker-processer, hvor childet er utroværdig kode; her er childet troværdig, men hosten er ikke den eneste ejer af sit eget handle-table

Tesseract-barneproces handle-arv i HotPDF: en almindelig CreateProcess med bInheritHandles giver hvert nedarvbart fil-, pipe- og event-handle videre til childet, mens STARTUPINFOEX med PROC_THREAD_ATTRIBUTE_HANDLE_LIST begrænser sættet til et NUL-handle til stdin og stdout plus stderr-filhandlen
Uden attributlisten holder childet urelaterede objekter i live, til det slutter, og låser filer og sultner pipes; med den krydser kun de to listede handles grænsen
// konstanter vist ved navn; kilden giver deres numeriske værdier videre
// begge handles oprettes med bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin og stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt i den private mappe
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,                                        // krævet af handle-listen
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Hvorfor kan en annulleret OCR-kørsel ligne en engine-fejl?

En annulleret OCR-kørsel ligner en engine-fejl, fordi IHPDFOCREngine.Recognize returnerer én enkelt Boolean, og False betyder både "Tesseract fejlede" og "brugeren trykkede Annuller". Adapteren poller cancellation-token og timeouten hver 25. millisekund, mens childet kører, og når tokenet fyres, rejser den inde i Recognize, fanger sin egen exception, rydder op og returnerer False med en diagnose. Behandlede pipelinen det som en engine-fejl, ville calleren se otlsEngineError for et job, brugeren bevidst stoppede. ApplyLoadedOCRTextLayer tjekker derfor tokenet først, når som helst Recognize returnerer False, og konverterer kun resultatet til en engine-fejl, hvis tokenet ikke var sat. Den rækkefølge bevarer multi-side-kontrakten: genkendelse, validering, budget-regnskab og indholdsbygning kører for hver ønsket side, før graf-transaktionen åbnes, så en annullering på side 40 af 50 rapporterer otlsCancelled og efterlader dokumentet, inklusive de første 39 sider, urørt. Der er ingen delvist søgbar fil at forklare bagefter, og resten af fejlhåndteringen følger samme afgrænsede stil:

  • Timeouten er pr. Recognize-kald, målt fra dets start, så default 60.000 ms gælder hver side snarere end hele dokumentet
  • Et child, der stadig kører ved timeout eller annullering, termineres, ventes på i op til 5 sekunder, og dets private mappe slettes i en finally-blok
  • output.tsv er begrænset til 64 MiB og stderr.txt til 1 MiB, tjekket mens childet kører samt efter det slutter
  • Ordantal og UTF-16 code units begrænses pr. side af de resterende MaxWordsPerPage-, MaxTotalWords- og MaxTextCodeUnits-budgetter, og at overskride dem fejler kørslen i stedet for at afkorte ordlisten
  • Standard output går til NUL, fordi Tesseract skriver output.tsv, mens standard error går til en fil, så en ikke-nul exitkode rapporteres med op til 4.096 tegn af enginens egen klage, normalt den hurtigste måde at lære, at en .traineddata-fil mangler

Hvordan de genkendte ord bliver til et usynligt tekstlag

HotPDF skriver Tesseract-ord som usynlig tekst med text rendering mode 3, den hverken-fyld-eller-streg-mode, der er defineret i ISO 32000-1 §9.3.6, så siden stadig viser det scannede billede, mens søgning og kopiering arbejder på de genkendte ord. Content streamen åbner BT med 3 Tr, og hvert ord får en Tm-matrix ved sin baseline, en fontstørrelse afledt af boksens højde i pixels ved render-DPI'en og en Tz horisontal skalering, der strækker glyph-run'en til den målte boksbredde, hvilket er grunden til, at et søge-highlight lander på ordet i billedet i stedet for at drive hen over det

Tesseracts TSV har bokse men ingen baselines, så adapteren rapporterer hvert ord uden én, og pipelinen estimerer baselinjen til en femtedel af boksens højde over den nederste kant. Teksten selv går gennem en delt unembedded Type0-font med Identity-H-encoding og en genereret ToUnicode-CMap, ét CID pr. distinkt Unicode-scalar på tværs af hele run'en, hvilket er hvordan kinesisk, accentueret latin og supplementary-plane-tegn alle overlever kopiering og søgning. Det design har to grænser, der er værd at nævne med det samme: ét run kan bære højst 65.535 distinkte scalars, og den unembeddede font opfylder ikke font-embedding-kravet i ISO 19005, så PDF/A-output behøver en separat embedded conforming font. At tjekke resultatet er simpelt og værd at automatisere: gem, reload, og kør den almindelige loaded-document tekst-sti fra at udtrække tekst fra en loadet PDF i Delphi; kommer ordene tilbage på de forventede sider, er laget ægte

RapidOCR og andre enginer på samme TSV-protokol

HotPDF genbruger samme process-runner og TSV-parser til RapidOCR gennem HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), som er det mere nyttige valg til forenklet kinesiske scanninger. Command line er identisk bortset fra, at bridge-script-stien indsættes efter Python-eksekverbaren, og sproget er fastlåst til chi_sim. HotPDF leverer broen som tools/OCR/rapidocr_tsv.py; den forventer rapidocr- og onnxruntime-pakkerne plus tre lokale ONNX-modeller, deaktiverer automatiske model-downloads og skriver Tesseract-formet TSV, så Delphi-siden ikke behøver en anden parser. Enginenavnet rapporteret i Info.EngineName er RapidOCR (local ONNX). Den form antyder den generelle opskrift: Enhver recognizer, du kan pakke ind i et lille script, der accepterer argumentlisten i Tesseract-stil og udsender den tolv-kolonners TSV, arver handle-isolation, timeouten, cancellation, output-budgetterne og all-or-nothing-committet gratis. Adapterne er Windows-only, kører én side ad gangen synkront og deskewer eller preprocesser ikke billedet ud over, hvad rendereren producerer, så billedkvaliteten ind stadig sætter loftet over, hvad der kommer ud

Tesseract- og RapidOCR-adapterne, den usynlige tekstlags-writer, side-rendereren, der føder dem, og tekst-udtrækningen, der verificerer resultatet, følger alle med i samme native VCL-komponent til Delphi og C++Builder. Tilføjer du OCR til en document capture- eller arkiveringsapplikation, giver HotPDF Delphi PDF-komponenten dig pipelinen med kun selve OCR-enginen tilbage at installere