HotPDF leverer THPDFBuiltInOCREngine, en afgrænset template-matching-OCR-motor skrevet helt i Object Pascal: Den binariserer en renderet side med Otsu-thresholding, udtrækker glyffer som connected components og scorer hver glyf efter gråtone-dækning mod cachede skabeloner i flere fonte, så en Delphi-applikation kan bygge et søgbart tekstlag uden en ekstern OCR-afhængighed. Motoren måtte genopbygges fra bunden i v2.731.0, og årsagen var ikke matcheren. Det var pixlerne
Den gamle motor bestod sine tests. Den genkendte ASCII med store bogstaver på syntetiske bitmapbilleder, og på Win32 blev den ved med at gøre det i månedsvis. Så blev den samme kode kørt under Win64 og producerede slet ingenting: ingen ord, ingen anden diagnosticering end "found no high-contrast foreground", ingen crash. Det viste sig, at fejlen bestod af to uafhængige fejl i pixel-læsevejen, som havde ophævet hinanden, og at skille dem ad er en god illustration af, hvorfor OCR-kode fejler lydløst i stedet for højlydt
Hvorfor virkede den gamle OCR-motor kun ved et tilfælde?
Den gamle motor virkede, fordi dens skabelonbitmapmer og dens målbitmapmer blev vendt på samme måde, så en lodret inversion i pixel-læseren var usynlig for matcheren. TBitmap.ScanLine giver rækker tilbage i den modsatte rækkefølge af den positive biHeight-DIB-konvention, som resten af billedstien antager. Render et M på hovedet, sammenlign det med en skabelon, der også står på hovedet, og L1-forskellen er identisk med den korrekte sammenligning. Hver glyf matchede. Intet var rigtigt
Den symmetri er præcis det, der gør denne type fejl dyr. Enhver ensidig rettelse ødelægger matching: Ret mållæsningen, og lad skabelonerne være, så kollapser genkendelsen til støj; ret skabelonerne først, og du får det samme kollaps fra den anden retning. Der findes ingen trinvis reparationsvej. Genopbygningen erstattede derfor hele læsningen med GetDIBits mod en eksplicit deklareret BITMAPINFOHEADER, hvor en positiv biHeight kontraktligt betyder bottom-up-rækker i stedet for VCL-konvention, og vender én gang, bevidst, når der kopieres til gråtonebufferen
Den anden fejl er den, som kun Win64 afslørede. Den HDC, der sendes til GetDIBits, må ikke være bitmapets egen memory-DC, fordi bitmapet allerede er valgt ind i den, og Windows dokumenterer det som ugyldigt. At sende Bitmap.Canvas.Handle blev tolereret af Win32-processen og fejlede konsekvent i Win64-testprocessen. Rettelsen er en midlertidig screen-DC fra GetDC(0), som frigives i en finally-blok og ikke har noget med et bitmap at gøre
procedure BitmapToGray(Bitmap: TBitmap; out Gray: TBytes);
var
Work: TBitmap;
Info: TBitmapInfo;
Buffer: TBytes;
DC: HDC;
P: PByte;
Stride, X, Y: Integer;
begin
Work := TBitmap.Create;
try
Work.Assign(Bitmap);
Work.PixelFormat := pf24bit;
Stride := ((Work.Width * 24 + 31) div 32) * 4;
SetLength(Buffer, Stride * Work.Height);
FillChar(Info, SizeOf(Info), 0);
Info.bmiHeader.biSize := SizeOf(BITMAPINFOHEADER);
Info.bmiHeader.biWidth := Work.Width;
Info.bmiHeader.biHeight := Work.Height; // positiv => bottom-up-rækker
Info.bmiHeader.biPlanes := 1;
Info.bmiHeader.biBitCount := 24;
Info.bmiHeader.biCompression := BI_RGB;
DC := GetDC(0); // aldrig Work.Canvas.Handle: Work er valgt ind dér
if DC = 0 then
raise EInvalidOperation.Create('Recognition bitmap pixels could not be read');
try
if GetDIBits(DC, Work.Handle, 0, Work.Height,
@Buffer[0], Info, DIB_RGB_COLORS) <> Work.Height then
raise EInvalidOperation.Create('Recognition bitmap pixels could not be read');
finally
ReleaseDC(0, DC);
end;
SetLength(Gray, Work.Width * Work.Height);
for Y := 0 to Work.Height - 1 do
begin
P := @Buffer[(Work.Height - 1 - Y) * Stride]; // én bevidst vending
for X := 0 to Work.Width - 1 do
Gray[Y * Work.Width + X] :=
(Integer(P[X * 3]) * 29 + Integer(P[X * 3 + 1]) * 150 +
Integer(P[X * 3 + 2]) * 77) shr 8;
end;
finally
Work.Free;
end;
end;
Binarisering og connected components: fra grå pixels til glyfbokse
HotPDF binariserer først med Otsus metode og falder kun tilbage til en lokal vindues-threshold, når Otsu ikke kan bruges. Den globale vej kræver et reelt bimodalt histogram: Motoren beregner maksimum for variansen mellem klasserne og kræver desuden, at gråtoneområdet spænder over mindst 64 niveauer, før den stoler på resultatet. En udvasket scanning, en side med gradientbaggrund eller et bitmap, der næsten kun består af blæk, består ikke denne test. Fallbacken sammenligner derefter hver pixel med gennemsnittet af et vindue på 31 gange 31 med en bias på 6 gråtoneniveauer, beregnet med løbende kolonnesummer, så skydevinduet forbliver lineært i antallet af pixels
Glyfudtrækningen er 8-connected component-labeling over den resulterende maske med en eksplicit stack i stedet for rekursion, fordi en maske over en hel side uden problemer kan sprænge en Delphi-tråds stack under en dyb flood fill. To filtre kører allerede under labelingen: komponenter på under 9 pixels fjernes som speckle-støj, og enhver komponent, der spænder over mere end tre femtedele af både billedets bredde og højde, fjernes som en ramme eller linje i stedet for en glyf. Et andet pass sammenfletter lodret stablede bokse, hvis deres vandrette overlap er mindst en fjerdedel af den smalleste boks, hvilket samler prikken over et i eller et j med stammen igen. Alt dette kører på et raster, og rasteret kommer fra den samme renderer, der er beskrevet i rendering af en indlæst PDF-side til et bitmap i Delphi, hvilket er vigtigt af en praktisk grund: OCR-kvaliteten er begrænset af renderkvaliteten, og standarden på 300 DPI for tekstlaget er en bevidst afvejning og ikke et maksimum
Hvad gør stort I og lille l umulige at skelne?
I Arial rasteriserer stort I og lille l til pixelidentiske bjælker, så ingen formfunktion kan adskille dem, og case må komme et helt andet sted fra. Motorens svar er højdeklustering på linjeniveau. Glyfbokse grupperes i tekstlinjer efter lodret overlap, hver linje analyseres for sin cap-højde og sin modale baseline, og højderne i en linje deles i en kort klynge og en høj klynge. En bjælke, der ligger i den korte klynge, er et l; den samme bjælke i den høje klynge er et I
Den oplagte implementering af opdelingen er en fast ratio-threshold, og den virker ikke. Forholdet mellem x-height og cap-height i Arial er cirka 0.72, hvilket lander lige på værdierne 0.70 og 0.75, som alle griber efter først. Flyt konstanten en hundrededel i hver retning, og et helt korpus skifter case. HotPDF udfører i stedet en endimensional k=2-opdeling, der minimerer variansen: sortér kandidat-højderne, prøv hvert skæringspunkt, og behold det skær, hvis sum af kvadrerede afvigelser inden for klyngerne er mindst. Thresholden bliver en egenskab ved siden i stedet for en konstant i kilden
// ClusterHeights er sorteret stigende; find k=2-opdelingen med mindst varians
BestSplit := 1;
BestVariance := 1E18;
for I := 1 to ClusterCount - 1 do
begin
SumA := 0;
for J := 0 to I - 1 do SumA := SumA + ClusterHeights[J];
SumB := 0;
for J := I to ClusterCount - 1 do SumB := SumB + ClusterHeights[J];
MeanA := SumA / I;
MeanB := SumB / (ClusterCount - I);
Variance := 0;
for J := 0 to I - 1 do
Variance := Variance + Sqr(ClusterHeights[J] - MeanA);
for J := I to ClusterCount - 1 do
Variance := Variance + Sqr(ClusterHeights[J] - MeanB);
if Variance < BestVariance then
begin
BestVariance := Variance;
BestSplit := I;
end;
end;
// kun forholdet mellem de to klynge-gennemsnit afgør, hvad den korte stribe er
if SmallMean / TallMean <= 0.80 then
SmallGroup := ggSmall // en ægte x-height-stribe: små bogstavformer
else
SmallGroup := ggTall; // én højdestribe: alt har cap-height
Line.LowercaseContext := (SmallGroup = ggSmall);
Linjer med én enkelt højdestribe har ingen intern evidens overhovedet. En overskrift med kun store bogstaver og en billedtekst med kun små bogstaver ser ens ud isoleret. For dem sammenligner HotPDF linjens medianhøjde med side-medianens x-height, taget fra linjer, der blev delt: Et forhold på højst 1.10 markerer linjen som lowercase-kontekst, et forhold på mindst 1.18 markerer den som cap-kontekst, og alt imellem forbliver ubundet. Matching tilføjer derefter en lille case-præferencebonus på 0.03 mod kandidaten, der passer til denne kontekst, hvilket skubber ved lighed uden nogensinde at tilsidesætte en tydelig formforskel
Hvorfor forvekslede et skabelonnet på 12x18 c og o?
Skabelonnettet blev udvidet fra 12 gange 18 celler til 16 gange 24, fordi gråtone-dækningsmarginen mellem c og o ved den mindre opløsning faldt til under 0.007, langt inden for motorens tvetydighedstærskel. Hver glyfboks resamples til nettet som dækningsværdier fra 0 til 255 i stedet for som en binær stencil, så en celle med en tredjedel blæk læses som cirka 85 i stedet for at blive afrundet til sort eller hvid. Ved 12 gange 18 spænder den åbne side af et c knap over mere end én cellekolonne, og det antialiasede gennemsnit vasker åbningen væk. Ved 16 gange 24 overlever åbningen resampling, og de fleste af de let forvekslelige par flytter tilbage til en sikker afstand
Scoringen er den normaliserede L1-afstand mellem de to dækningsgitre plus en straf på 0.30 gange forskellen i logaritmen af aspektforholdet og 0.16 gange forskellen i blæktæthed, med et hårdt prefilter, der springer enhver skabelon over, hvis dens aspektforhold afviger med mere end en faktor 2.6. Skabeloner rasteriseres én gang pr. proces fra fem systemfonte (Arial, Times New Roman, Courier New, Tahoma og Segoe UI) over et alfabet på 62 tegn, caches bag en kritisk sektion og genbruges af alle efterfølgende kald
Den sidste konstant er den interessante. Når runner-up-tegnet scorer inden for 0.018 af vinderen, begrænser HotPDF glyffens confidence til 0.5, hvilket er under acceptgrænsen på 0.55, så glyffen udsendes ganske enkelt ikke. Det er et bevidst fail-closed-snit og ikke en tuningartefakt: En afgrænset motor, der gætter, producerer et søgbart lag, hvis tekst ikke matcher billedet, og et forkert ord i et tekstlag er værre end et manglende, fordi det er usynligt for den person, der gennemgår scanningen
Opdeling af ord uden en fast afstandstærskel
HotPDF udleder mellemrumstærsklen pr. linje fra fordelingen af afstande mellem glyffer i stedet for fra et fast multiplum af den gennemsnitlige glyfbredde. Den klassiske heuristik, "en afstand bredere end 0.75 af det gennemsnitlige advance er et mellemrum", bryder sammen, så snart en linje blander cifre med smalle bogstaver, fordi gennemsnitsbredden holder op med at beskrive noget reelt. Motoren sorterer i stedet afstandene på linjen og leder efter det største spring mellem på hinanden følgende sorterede værdier, hvilket er grænsen mellem klyngen inden for ord og klyngen mellem ord, hvis en sådan findes. Tre værn forhindrer, at dette udløses af støj: Springet skal være mindst 0.22 af den gennemsnitlige glyfbredde, den første afstand over opdelingen skal være mindst 0.32 af den, og den sidste afstand under opdelingen må ikke overstige 0.65 af den. Hvis et værn fejler, forbliver tærsklen MaxInt, og hele linjen bliver ét ord. Det sidste værn forhindrer, at ét usædvanligt bredt kerningpar deler et ord i to, hvilket er en langt mere skadelig fejl end at sammenflette to ord, eftersom et sammenflettet token stadig indeholder de rigtige tegn i den rigtige rækkefølge til en substring-søgning
Skrivning af det usynlige tekstlag over det scannede billede
ApplyLoadedOCRTextLayer omdanner genkendte ord til et søgbart lag ved at tegne dem i text rendering mode 3, den hverken-udfyldte-eller-stregede tilstand, der er defineret i ISO 32000-1 §9.3.6, placeret over det scannede billede, de kom fra. Content streamen åbner med BT efterfulgt af 3 Tr, og hvert ord placeres med en tekstmatrix bygget fra den rapporterede baseline, dets cap-height omregnet fra pixels ved den ønskede DPI og en vandret skalering, der strækker det syntetiske glyfrun til den målte ordbredde. Resultatet kan kopieres og søges som tekst og maler ingenting
Der findes en overload uden engine, som opretter den indbyggede genkender for dig, og det er den, de fleste kaldere af den indbyggede vej bør bruge. Genkendelse, Unicode-validering, budgetregnskab og content-konstruktion bliver alle afsluttet, før copy-on-write-transaktionen åbner, så en annullering, et budgetoverskridelse eller en motorfejl lader objektgrafen og versionsnummeret urørt. Ord filtreres to gange: Motoren fjerner alt under sin egen confidence-grænse på 0.55 pr. glyf, og derefter fjerner THPDFOCRTextLayerOptions.MinimumConfidence (standard 0.5) hele ord under kalderens grænse
var
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile('scan.pdf') < 1 then
Exit;
Options := THPDFOCRTextLayerOptions.Default; // DPI 300, MinimumConfidence 0.5
Options.SkipPagesWithText := True; // lad sider med digital tekst være
Options.UseOptionalContentGroup := True;
Options.OptionalContentGroupName := 'OCR Text Layer';
// overload uden engine: HotPDF leverer den indbyggede, afgrænsede genkender
if Doc.ApplyLoadedOCRTextLayer([0], Options, Info) then
begin
Writeln(Info.AcceptedWordCount, ' words accepted by ',
string(Info.EngineName));
Doc.SaveLoadedDocument('scan-searchable.pdf');
end
else
Writeln('No text layer written: ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
Én begrænsning bør siges klart i stedet for at blive opdaget senere. Det usynlige lag bruger en delt, syntetisk, ikke-indlejret Type0-font, hvilket er nok til søgning og kopiering i alle viewere, men ikke opfylder kravet om font-embedding i ISO 19005. Hvis outputtet skal være PDF/A, skal kaldere indlejre en kompatibel font separat. Og et OCR-tekstlag bærer geometri, ikke struktur, så læserækkefølgen kommer alene fra glyfpositionerne; hvis du har brug for logisk rækkefølge fra en side, der allerede har rigtig tekst, er strukturordnet tekstekstraktion drevet af tag-træet et andet værktøj til et andet problem
Hvor stopper den indbyggede motor?
Den indbyggede motor er bevidst snæver, og det er kendskabet til dens kanter, der holder den nyttig. Den retter sig mod højkontrast, maskintrykt ASCII fra fonte tæt på dens fem skabelonansigter, og alt uden for dette returnerer intet ord i stedet for et gæt. De konkrete grænser er:
- Billeder op til 4096 gange 4096 og 4.194.304 pixels, med en genkendelsesfrist på 2000 ms og kooperativ annullering gennem
THPDFCancellationToken - Et alfabet på 62 ASCII-bogstaver og cifre; ingen tegnsætning, ingen accenterede tegn, ingen CJK
- Kun aksejusteret tekst ved den siderotation, som rendereren allerede normaliserede; skæve scanninger deskewes ikke
- Tvetydige glyfpar forbliver uafklarede, så en side kan returnere delvise ord eller diagnosticeringen "found no unambiguous ASCII words"
Når denne ramme er for lille, er IHPDFOCREngine samlingspunktet. Implementér Recognize mod din egen motor, giv den til overloaden med tre argumenter af ApplyLoadedOCRTextLayer, og alt downstream (koordinatmapping, rotationshåndtering, Unicode-validering, budgetter, den atomare commit) forbliver det samme. Bitmapet lånes under det synkrone kalds varighed og må ikke beholdes. For at bekræfte, at laget landede korrekt, skal du genindlæse den gemte fil og køre den almindelige tekstvej, der er beskrevet i ekstraktion af tekst fra en indlæst PDF i Delphi; hvis ordene kommer tilbage, er laget ægte
Den indbyggede template-matching-OCR, det usynlige tekstlag, siderendereren, der leverer dem, og tekstekstraktionen fra indlæste dokumenter, der verificerer dem, leveres alle i den samme native VCL-komponent uden ekstern OCR-runtime og uden en DLL, der skal distribueres sammen med applikationen. Hvis du bygger dokumentoptagelse, arkivering eller søgning over scannede PDF-filer i Delphi eller C++Builder, giver HotPDF Delphi PDF-komponenten dig hele pipelinen i én afhængighed