Teknisk artikkel

Tesseract OCR til søkbar PDF i Delphi med HotPDF

HotPDF gjør skannede PDF-sider om til søkbar PDF med Tesseract gjennom HPDFCreateTesseractOCREngine, en fabrikk som pakker en lokalt installert Tesseract-kjørbar inn som en IHPDFOCREngine. Du sender den motoren til ApplyLoadedOCRTextLayer, som renderer hver side, kjører Tesseract én gang per side, parser dens ordnivå TSV-resultat, og committer et usynlig Unicode-tekstlag for alle forespurte sider i én transaksjon, eller for ingen av dem

HotPDF OCR-pipeline per side: render siden ved konfigurert DPI, lagre input.bmp i en privat HotPDF-OCR-katalog, start Tesseract-barneprosessen med tessedit_create_tsv, parser den tolvkolonne TSV-en, filtrerer ord etter konfidens, og committer det usynlige tekstlaget for alle forespurte sider eller ingen
Adapteren erstatter bare gjenkjenningen: rendering, parsing, validering og alt-eller-ingen-commiten forblir i den eksisterende tekstlag-pipelinen, så nedstrømskode endres aldri

Grunnen til at denne adapteren finnes, er omfang. Den innebygde template-matching OCR-motoren er bevisst smal: maskinskrevne ASCII-bokstaver og siffer, ikke annet. Fakturaer med aksenterte navn, kinesiske kontrakter og flerspråklige arkiver trenger en ekte gjenkjenner med trente språkmodeller, og Tesseract er den opplagte kandidaten fordi det er et kommandolinjeprogram du kan provisjonere ved siden av applikasjonen din. Å kalle et eksternt program fra et dokumentbibliotek høres trivielt ut. Det er det ikke, og det meste av den interessante koden i adapteren handler om hva som skjer når programmet oppfører seg galt, henger, blir kansellert, eller arver ting det aldri burde se

Hvordan driver HotPDF Tesseract fra en Delphi-applikasjon?

HotPDF kjører Tesseract som en skjult barneprosess per side, mater den et rendret bitmap og leser tilbake en TSV-fil, og eksponerer resultatet gjennom den samme IHPDFOCREngine-sømmen den innebygde motoren bruker. Ingenting nedstrøms endres: koordinatmapping, rotasjonshåndtering, Unicode-validering, konfidensfiltrering og atomcommitten er tekstlag-pipelinen du allerede har. Fabrikken bor i HPDFTesseractRecognition-uniten og validerer ivrig: den kjørbare filen må finnes, tessdata-katalogen må finnes, tidsavbruddet må ligge mellom 1 og 3 600 000 millisekunder, og språkidentifikatoren kan bare inneholde ASCII-bokstaver, siffer, _ og +. Den siste sjekken betyr noe fordi språkstrengen ender opp på en kommandolinje, og eng+chi_sim er en legitim Tesseract-verdi mens alt med anførselstegn eller mellomrom ikke er det

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // reiser EArgumentException for en manglende kjørbar, manglende tessdata,
  // en dårlig språkidentifikator, eller et tidsavbrudd utenfor 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // flere modeller slått sammen med '+'
    120000);            // grense per side, standard 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 betyr alle sider; sider med tekst hoppes over som standard
    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 oppretter Recognize en privat katalog under temp-stien kalt HotPDF-OCR-{GUID}, lagrer det rendrete bitmapet som input.bmp, og starter tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, med hvert stisargument anført etter Windows' kommandolinje-escaping-regler for omvendte skråstreker og innebygde anførselstegn. --dpi-verdien er render-DPI-en fra THPDFOCRTextLayerOptions.DPI, så Tesseract trenger aldri å gjette oppløsningen fra bilde-metadata, og --psm 3 ber om fullautomatisk sidesegmentering. Motoren rapporterer seg selv som Tesseract (local CLI), noe som er det som lander i Info.EngineName. Tesseract og dens språkmodeller følger ikke med HotPDF; å installere dem er applikasjonens jobb

Hvorfor er TSV-parseren så streng?

TSV-parseren i HotPDF feiler hele siden på enhver misdannet rad, fordi en delvis parset ordliste produserer et tekstlag som stille er uenig med bildet. Tesseracts TSV-resultat har en fast tolvkolonne-header, fra level til text, og HotPDF sammenligner første linje med den nøyaktige headeren etter å ha strippet en valgfri byteordensmarkør. Hver følgende rad må splitte i nøyaktig tolv felt, og splitten stopper etter den ellevte tabben slik at en tabb inne i den gjenkjente teksten forblir del av ordet i stedet for å danne en trettende kolonne. Bare nivå 5-rader er ord; nivåene 1 til 4 beskriver sider, blokker, avsnitt og linjer, og de hoppes over. Nivå 5-rader hvis tekst er tom eller ren whitespace hoppes også over, fordi et tomt ord har en boks men ingenting å lokalisere eller søke i. Alt annet sjekkes hardt: heltallsgeometri, en konfidens parsert med et invariant en-US-format så en tysk locale ikke leser 93.5 som søppel, en boks som ligger fullt inne i bitmapet, og en konfidens mellom 0 og 100. Én enkelt feil reiser, motoren returnerer False, og ordarrayen tømmes. Regresjonstestene inkluderer nøyaktig det tilfellet: ett gyldig ord fulgt av en ødelagt rad må gi null ord, ikke ett

Seks porter hver Tesseract TSV-rad passerer i HotPDF: nøyaktig tolvkolonne-header, nøyaktig tolv felt, kun nivå 5, ikke-tom tekst, en boks inne i bitmapet, og konfidens fra 0 til 100 parsert invariant, der én ødelagt rad feiler hele siden ned til null ord
En delvis parset ordliste ville stille vært uenig med bildet, så parseren nekter hele siden ved den første misdannede raden i stedet for å beholde ordene den allerede har lest
// kondensert fra nivå-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;                 // side/blokk/avsnitt/linje-rader
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // whitespace-ord har ingen posisjon
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 siste linjen vekselvirker med en standardverdi du kanskje ikke forventer. Tesseract-konfidensen løper fra 0 til 100, pipelinen jobber i 0 til 1, og THPDFOCRTextLayerOptions.MinimumConfidence har standardverdi 0,5, så ethvert Tesseract-ord under 50 telles i Info.DroppedWordCount og når aldri siden. På en ren skann ved 300 DPI er det en fornuftig bunn. På en støyete faks kan det droppe en overraskende andel av siden, og det riktige trekket er å se på antallet droppede ord før du senker terskelen, for lavkonfidens-ord er nøyaktig de som mest sannsynlig er feil

Hva arver Tesseract-barneprosessen?

Tesseract-barneprosessen arver nøyaktig to handles fra HotPDF: et NUL-handle for standard input og output, og et filhandle for standard error. Den presisjonen er poenget. CreateProcess med bInheritHandles = True er måten du sender standardhandles til et barn, men på egen hånd sender den hver arvbare handle i vertsprosessen, inkludert filer, pipes og events åpnet av urelatert kode i applikasjonen din. Barnet holder så de objektene i live til det avslutter, så en fil forblir låst eller en pipe ser aldri sin ende mens Tesseract sliter seg gjennom en side. HotPDF lukker det gapet med en utvidet oppstartsrecord: STARTUPINFOEX, en attributtliste som bærer PROC_THREAD_ATTRIBUTE_HANDLE_LIST, og EXTENDED_STARTUPINFO_PRESENT-opprettingsflagget. Med handle-listen på plass må bInheritHandles fortsatt være True, men bare de oppførte handle-ne krysser grensen. Samme inneslutningstenkning driver å isolere PDF-bilde-codecs i arbeiderprosesser, der barnet er utruelig kode; her er barnet betrodd, men verten er ikke den eneste eieren av sin egen handle-tabell

Tesseract-barneprosessens handle-arv i HotPDF: en ren CreateProcess med bInheritHandles sender hver arvbare fil-, pipe- og event-handle til barnet, mens STARTUPINFOEX med PROC_THREAD_ATTRIBUTE_HANDLE_LIST begrenser settet til et NUL-handle for stdin og stdout pluss stderr-filhandleet
Uten attributtlisten holder barnet urelaterte objekter i live til det avslutter, låser filer og sulter pipes; med den krysser bare de to oppførte handle-ne grensen
// konstanter vist ved navn; kilden sender deres numeriske verdier
// begge handle-ne opprettes med bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin og stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt i den private katalogen
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,                                        // krevd av handle-listen
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Hvorfor kan en kansellert OCR-kjøring se ut som en motorfeil?

En kansellert OCR-kjøring ser ut som en motorfeil fordi IHPDFOCREngine.Recognize returnerer én enkelt Boolean, og False betyr både «Tesseract feilet» og «brukeren trykket Avbryt». Adapteren poller kanselleringstokenen og tidsavbruddet hver 25. millisekund mens barnet kjører, og når tokenen utløser, reiser den inne i Recognize, fanger sitt eget unntak, rydder opp, og returnerer False med en diagnostikk. Hvis pipelinen behandlet det som en motorfeil, ville kalleren sett otlsEngineError for en jobb brukeren bevisst stoppet. ApplyLoadedOCRTextLayer sjekker derfor tokenen først når som helst Recognize returnerer False, og konverterer bare resultatet til en motorfeil hvis tokenen ikke var satt. Den rekkefølgen bevarer flerpages-kontrakten: gjenkjenning, validering, budsjettregnskap og innholdskonstruksjon kjører for hver forespurte side før graftransaksjonen åpnes, så en kansellering på side 40 av 50 rapporterer otlsCancelled og lar dokumentet, inkludert de første 39 sidene, være urørt. Det finnes ingen delvis søkbar fil å forklare senere, og resten av feilhåndteringen følger samme avgrensede stil:

  • Tidsavbruddet er per Recognize-kall, målt fra dets start, så standardverdien 60 000 ms gjelder hver side snarere enn hele dokumentet
  • Et barn som fortsatt kjører ved tidsavbrudd eller kansellering, termineres, ventes på i opptil 5 sekunder, og dets private katalog slettes i en finally-blokk
  • output.tsv er taksert til 64 MiB og stderr.txt til 1 MiB, sjekket mens barnet kjører så vel som etter det avslutter
  • Ordantall og UTF-16 kodeenheter takseres per side av de gjenværende MaxWordsPerPage-, MaxTotalWords- og MaxTextCodeUnits-budsjettene, og å overskride dem feiler kjøringen i stedet for å avkorte ordlisten
  • Standard output går til NUL fordi Tesseract skriver output.tsv, mens standard error går til en fil slik at en exitkode ulik null rapporteres med opptil 4 096 tegn av motorens egen klage, vanligvis den raskeste måten å lære at en .traineddata-fil mangler

Hvordan de gjenkjente ordene blir et usynlig tekstlag

HotPDF skriver Tesseract-ord som usynlig tekst ved bruk av tekstvisningsmodus 3, verken-fyll-eller-strek-modusen definert i ISO 32000-1 §9.3.6, så siden viser fortsatt det skannede bildet mens søk og kopiering virker på de gjenkjente ordene. Innholdsstrømmen åpner BT med 3 Tr, og hvert ord får en Tm-matrise ved sin grunnlinje, en fontstørrelse utledet av bokshøyden i piksler ved render-DPI-en, og en Tz horisontal skalering som strekker glyffløpet til den målte boksbredden, noe som er grunnen til at et søkehøydepunkt lander på ordet i bildet i stedet for å drive over det

Tesseracts TSV har bokser men ingen grunnlinjer, så adapteren rapporterer hvert ord uten én, og pipelinen estimerer grunnlinjen til en femtedel av bokshøyden over nedre kant. Teksten selv går gjennom en delt uinnebygget Type0-font med Identity-H-enkoding og en generert ToUnicode-CMap, én CID per distinkt Unicode-skalar på tvers av hele løpet, noe som er hvordan kinesisk, aksentert latin og supplementary-plane-tegn alle overlever kopiering og søk. Det designet har to grenser verdt å si på forhånd: ett løp kan bære høyst 65 535 distinkte skalarer, og den uinnebygde fonten tilfredsstiller ikke kravet om fontinnebygging i ISO 19005, så PDF/A-resultat trenger en separat innbygd konform font. Å sjekke resultatet er enkelt og verdt å automatisere: lagre, last på nytt, og kjør den ordinære lastet-dokument tekststien fra å trekke ut tekst fra en lastet PDF i Delphi; hvis ordene kommer tilbake på de forventede sidene, er laget ekte

RapidOCR og andre motorer på samme TSV-protokoll

HotPDF gjenbruker den samme prosesskjøreren og TSV-parseren for RapidOCR gjennom HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), som er det mer nyttige valget for forenklet kinesisk skann. Kommandolinjen er identisk bortsett fra at bridge-script-stien settes inn etter Python-kjørbarfilen, og språket er fast til chi_sim. HotPDF leverer broen som tools/OCR/rapidocr_tsv.py; den forventer rapidocr- og onnxruntime-pakkene pluss tre lokale ONNX-modeller, deaktiverer automatiske modellnedlastinger, og skriver Tesseract-formet TSV slik at Delphi-siden ikke trenger en andre parser. Motornavnet rapportert i Info.EngineName er RapidOCR (local ONNX). Den formen antyder den generelle oppskriften: Enhver gjenkjenner du kan pakke inn i et lite skript som godtar argumentlisten i Tesseract-stil og sender ut den tolvkolonne TSV-en, arver handle-isolasjon, tidsavbruddet, kansellering, utdatabudsjetter og alt-eller-ingen-commiten gratis. Adapterene er kun Windows, kjører én side om gangen synkront, og retter ikke opp skråstilling eller forhåndsbehandler bildet utover det rendereren produserer, så bildekvaliteten som går inn, setter fortsatt taket på hva som kommer ut

Tesseract- og RapidOCR-adapterne, den usynlige tekstlag-skriveren, siderenderen som mater dem, og tekstuttrekket som verifiserer resultatet, leveres alle i den samme native VCL-komponenten for Delphi og C++Builder. Hvis du legger til OCR i en dokumentinnhentings- eller arkiveringsapplikasjon, gir HotPDF Delphi PDF-komponenten deg pipelinen med bare selve OCR-motoren igjen å installere