Teknisk artikel

Tesseract OCR till sökbar PDF i Delphi med HotPDF

HotPDF gör skannade PDF-sidor till sökbar PDF med Tesseract genom HPDFCreateTesseractOCREngine, en fabrik som slår in en lokalt installerad Tesseract-körbar fil som en IHPDFOCREngine. Du skickar den motorn till ApplyLoadedOCRTextLayer, som renderar varje sida, kör Tesseract en gång per sida, tolkar dess ordsnivå-TSV-utdata och committar ett osynligt Unicode-textlager för alla begärda sidor i en transaktion, eller för ingen av dem

HotPDF OCR-pipeline per sida: rendera sidan vid konfigurerad DPI, spara input.bmp i en privat HotPDF-OCR-katalog, starta Tesseract-barnprocessen med tessedit_create_tsv, tolka tolvkolumns-TSV:en, filtrera ord efter konfidens, och committa det osynliga textlagret för alla begärda sidor eller ingen
Adaptern ersätter bara igenkänningen: rendering, tolkning, validering och allt-eller-inget-committet stannar i den befintliga textlagerpipelinen, så nedströmskod ändras aldrig

Skälet till att den här adaptern finns är omfattning. Den inbyggda mallmatchande OCR-motorn är medvetet smal: maskinskrivna ASCII-bokstäver och siffror, ingenting annat. Fakturor med accenterade namn, kinesiska kontrakt och flerspråkiga arkiv behöver en riktig igenkännare med tränade språkmodeller, och Tesseract är den uppenbara kandidaten eftersom det är ett kommandoradsprogram du kan installera bredvid din applikation. Att anropa ett externt program från ett dokumentbibliotek låter trivialt. Det är det inte, och den mesta intressanta koden i adaptern handlar om vad som händer när programmet missköter sig, hänger sig, avbryts eller ärver saker den aldrig ska se

Hur driver HotPDF Tesseract från en Delphi-applikation?

HotPDF kör Tesseract som en dold barnprocess per sida, matar den med en renderad bitmap och läser tillbaka en TSV-fil, och exponerar resultatet genom samma IHPDFOCREngine-fog som den inbyggda motorn använder. Ingenting nedströms ändras: koordinatmappning, roteringshantering, Unicode-validering, konfidensfiltrering och det atomära committet är textlagerpipelinen du redan har. Fabriken bor i uniten HPDFTesseractRecognition och validerar tidigt: den körbara filen måste finnas, tessdata-katalogen måste finnas, timeouten måste ligga mellan 1 och 3 600 000 millisekunder, och språkidentifieraren får bara innehålla ASCII-bokstäver, siffror, _ och +. Den sista kontrollen spelar roll för att språksträngen hamnar på en kommandorad, och eng+chi_sim är ett legitimt Tesseract-värde medan allt med citattecken eller mellanslag inte är det

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // kastar EArgumentException för en saknad körbar fil, saknad tessdata,
  // en felaktig språkidentifierare eller en timeout utanför 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // flera modeller sammanfogade med '+'
    120000);            // gräns per sida, standard är 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 sidolista betyder alla sidor; sidor med text hoppas över 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;

För varje sida skapar Recognize en privat katalog under temp-sökvägen med namnet HotPDF-OCR-{GUID}, sparar den renderade bitmappen som input.bmp och startar tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, med vart och ett sökvägsargument citerat enligt Windows kommandoradsregler för omvänt snedstreck och inbäddade citattecken. --dpi-värdet är render-DPI:n från THPDFOCRTextLayerOptions.DPI, så Tesseract behöver aldrig gissa upplösningen utifrån bildmetadata, och --psm 3 ber om helt automatisk sidsegmentering. Motorn rapporterar sig själv som Tesseract (local CLI), vilket är det som landar i Info.EngineName. Tesseract och dess språkmodeller levereras inte med HotPDF; att installera dem är applikationens jobb

Varför är TSV-parsern så strikt?

TSV-parsern i HotPDF fallerar hela sidan på varje felformaterad rad, för att en partiellt tolkad ordlista ger ett textlager som tyst inte stämmer med bilden. Tesseracts TSV-utdata har ett fast tolvkolumnshuvud, från level till text, och HotPDF jämför första raden mot exakt det huvudet efter att ha strippat en valfri byteordningsmarkör. Varje följande rad måste delas i exakt tolv fält, och delningen stannar efter elfte tabben så att en tabb inuti den igenkända texten förblir del av ordet i stället för att skapa en trettonde kolumn. Bara level 5-rader är ord; level 1 till 4 beskriver sidor, block, stycken och rader, och de hoppas över. Level 5-rader vars text är tom eller ren whitespace hoppas också över, för att ett tomt ord har en låda men ingenting att lokalisera eller söka. Allt annat kontrolleras hårt: heltalsgeometri, en konfidens tolkad med invariant en-US-format så att en tysk locale inte läser 93.5 som skräp, en låda som ligger helt inuti bitmappen, och en konfidens mellan 0 och 100. Ett enda fel kastar, motorn returnerar False och ordarrayen rensas. Regressionstesterna inkluderar exakt det fallet: ett giltigt ord följt av en bruten rad måste ge noll ord, inte ett

Sex grindar varje Tesseract-TSV-rad passerar i HotPDF: exakt tolvkolumnshuvud, exakt tolv fält, bara level 5, icke-tom text, en låda inuti bitmappen och konfidens från 0 till 100 tolkad invariant, där en enda bruten rad fallerar hela sidan ned till noll ord
En partiellt tolkad ordlista skulle tyst inte stämma med bilden, så parsern vägrar hela sidan vid den första felformaterade raden i stället för att behålla orden den redan läst
// förkortat från level-5-loopen 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;                 // rader för page/block/paragraph/line
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 förväntar 0..1

Sista raden interagerar med en default du kanske inte förväntar dig. Tesseract-konfidens löper från 0 till 100, pipelinen arbetar i 0 till 1, och THPDFOCRTextLayerOptions.MinimumConfidence är 0,5 som standard, så varje Tesseract-ord under 50 räknas i Info.DroppedWordCount och når aldrig sidan. På en ren 300 DPI-skanning är det ett rimligt golv. På ett brusigt fax kan det tappa en förvånansvärd andel av sidan, och den rätta åtgärden är att titta på antalet tappade innan du sänker tröskeln, för att lågkonfidensord är precis de som mest sannolikt är fel

Vad ärver Tesseract-barnprocessen?

Tesseract-barnprocessen ärver exakt två handtag från HotPDF: ett NUL-handtag för standard in och ut, och ett filhandtag för standard error. Den precisionen är poängen. CreateProcess med bInheritHandles = True är hur du skickar standardhandtag till ett barn, men på egen hand skickar den varje ärftligt handtag i värdprocessen, inklusive filer, rör och event öppnade av orelaterad kod i din applikation. Barnet håller sedan de objekten vid liv tills det avslutas, så en fil förblir låst eller ett rör ser aldrig sitt slut medan Tesseract gnager igenom en sida. HotPDF täpper till den luckan med en utökad startup-post: STARTUPINFOEX, en attributlista som bär PROC_THREAD_ATTRIBUTE_HANDLE_LIST, och skapflaggan EXTENDED_STARTUPINFO_PRESENT. Med handtagslistan på plats måste bInheritHandles fortfarande vara True, men bara de listade handtagen korsar gränsen. Samma omslutande tänkande driver också att isolera PDF-bildcodecs i arbetsprocesser, där barnet är otillförlitlig kod; här är barnet betrodd, men värden är inte den enda ägaren till sin egen handtagstabell

Tesseract-barnprocessens handtagsarv i HotPDF: en vanlig CreateProcess med bInheritHandles skickar varje ärftligt fil-, rör- och eventhandtag till barnet, medan STARTUPINFOEX med PROC_THREAD_ATTRIBUTE_HANDLE_LIST begränsar mängden till ett NUL-handtag för stdin och stdout plus stderr-filhandtaget
Utan attributlistan håller barnet orelaterade objekt vid liv tills det avslutas, låser filer och svälter rör; med den korsar bara de två listade handtagen gränsen
// konstanter visade med namn; källkoden skickar deras numeriska värden
// båda handtagen skapas med bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin och stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt i den privata 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,                                        // krävs av handtagslistan
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Varför kan en avbruten OCR-körning se ut som ett motorfel?

En avbruten OCR-körning ser ut som ett motorfel för att IHPDFOCREngine.Recognize returnerar en enda Boolean, och False betyder både "Tesseract misslyckades" och "användaren tryckte på Avbryt". Adaptern pollar avbrottstoken och timeouten var 25:e millisekund medan barnet kör, och när tokenen utlöses kastar den inuti Recognize, fångar sitt eget undantag, städar upp och returnerar False med en diagnostik. Om pipelinen behandlade det som ett motorfel skulle anroparen se otlsEngineError för ett jobb användaren medvetet stoppade. ApplyLoadedOCRTextLayer kontrollerar därför tokenen först närhelst Recognize returnerar False, och omvandlar resultatet till ett motorfel bara om tokenen inte var satt. Den ordningen bevarar flersidokontraktet: igenkänning, validering, budgetbokföring och innehållskonstruktion körs för varje begärd sida innan graftransaktionen öppnas, så ett avbrott på sida 40 av 50 rapporterar otlsCancelled och lämnar dokumentet, inklusive de första 39 sidorna, orört. Det finns ingen delvis sökbar fil att förklara senare, och resten av felhanteringen följer samma avgränsade stil:

  • Timeouten gäller per Recognize-anrop, mätt från dess start, så standarden 60 000 ms gäller varje sida snarare än hela dokumentet
  • Ett barn som fortfarande körs vid timeout eller avbrott termineras, väntas på i upp till 5 sekunder, och dess privata katalog raderas i ett finally-block
  • output.tsv är takad vid 64 MiB och stderr.txt vid 1 MiB, kontrollerat medan barnet kör liksom efter att det avslutats
  • Ordantal och UTF-16-kodenheter takas per sida av de återstående budgeterna MaxWordsPerPage, MaxTotalWords och MaxTextCodeUnits, och att överskrida dem fallerar körningen i stället för att avkorta ordlistan
  • Standardutmatning går till NUL eftersom Tesseract skriver output.tsv, medan standardfel går till en fil så att en icke-noll exitkod rapporteras med upp till 4 096 tecken av motorns egen klagan, vanligen det snabbaste sättet att få veta att en .traineddata-fil saknas

Hur de igenkända orden blir ett osynligt textlager

HotPDF skriver Tesseract-ord som osynlig text med text rendering mode 3, det varken-fyll-eller-streck-läge som definieras i ISO 32000-1 §9.3.6, så sidan visar fortfarande den skannade bilden medan sökning och kopiering arbetar på de igenkända orden. Innehållsströmmen öppnar BT med 3 Tr, och varje ord får en Tm-matris vid sin baslinje, en teckenstorlek härledd ur lådhöjden i pixlar vid render-DPI:n, och en Tz-horisontalskalning som drar ut glyfloppet till den uppmätta lådbredden, vilket är varför en sökmarkering landar på ordet i bilden i stället för att driva över den

Tesseracts TSV har lådor men inga baslinjer, så adaptern rapporterar varje ord utan en och pipelinen uppskattar baslinjen till en femtedel av lådhöjden över den nedre kanten. Texten i sig går genom en gemensam icke-inbäddad Type0-font med Identity-H-kodning och en genererad ToUnicode-CMap, ett CID per distinkt Unicode-skalär över hela loppet, vilket är hur kinesiska, accenterade latinska och supplementary-plane-tecken alla överlever kopiering och sökning. Den designen har två begränsningar värda att nämna på förhand: ett lopp kan bära högst 65 535 distinkta skalärer, och den icke-inbäddade fonten uppfyller inte kravet på fontinbäddning i ISO 19005, så PDF/A-utdata behöver en separat inbäddad konform font. Att kontrollera resultatet är enkelt och värt att automatisera: spara, läs in igen och kör den vanliga inläsna-dokument-textvägen från att extrahera text ur en inläst PDF i Delphi; om orden kommer tillbaka på de förväntade sidorna är lagret på riktigt

RapidOCR och andra motorer på samma TSV-protokoll

HotPDF återanvänder samma processkörare och TSV-parser för RapidOCR genom HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), vilket är det mer användbara valet för skanningar på förenklad kinesiska. Kommandoraden är identisk för att bryggskriptets sökväg skjuts in efter den Python-körbara filen, och språket är låst till chi_sim. HotPDF levererar bron som tools/OCR/rapidocr_tsv.py; den förväntar sig paketen rapidocr och onnxruntime plus tre lokala ONNX-modeller, stänger av automatiska modellnedladdningar och skriver Tesseract-formad TSV så att Delphi-sidan inte behöver en andra parser. Motornamnet som rapporteras i Info.EngineName är RapidOCR (local ONNX). Den formen antyder det allmänna receptet: vilken igenkännare som helst som du kan slå in i ett litet skript som accepterar Tesseract-stilens argumentlista och skickar ut tolvkolumns-TSV ärver handtagsisolering, timeouten, avbrott, utdatabudgeter och allt-eller-inget-committet gratis. Adapterna är Windows-only, kör en sida i taget synkront och gör ingen rätskiftning eller förbehandling av bilden utöver vad renderaren producerar, så bildkvaliteten på ingången sätter fortfarande taket på det som kommer ut

Tesseract- och RapidOCR-adapterna, skrivaren av det osynliga textlagret, sidrenderaren som matar dem och textextraktionen som verifierar resultatet kommer alla i samma nativa VCL-komponent för Delphi och C++Builder. Om du lägger till OCR i en dokumentinsamlings- eller arkiveringsapplikation ger HotPDF Delphi PDF-komponenten dig pipelinen med bara OCR-motorn själv kvar att installera