HotPDF leverer THPDFBuiltInOCREngine, en avgrenset malbasert OCR-motor skrevet helt i Object Pascal: Den binariserer en rendret side med Otsu-terskling, henter ut glypher som sammenhengende komponenter og scorer hver glyph etter gråtone-dekning mot hurtigbufrede maler fra flere fonter, slik at en Delphi-applikasjon kan bygge et søkbart tekstlag uten en ekstern OCR-avhengighet. Motoren måtte bygges helt på nytt i v2.731.0, og årsaken var ikke matcheren. Det var pikslene
Den gamle motoren besto testene sine. Den gjenkjente store ASCII-bokstaver på syntetiske bitmap-er, og på Win32 fortsatte den med det i flere måneder. Så ble den samme koden kjørt under Win64 og produserte ingenting: ingen ord, ingen diagnose utover "found no high-contrast foreground", ingen krasj. Feilen viste seg å være to uavhengige feil i piksel-lesestien som hadde opphevet hverandre, og det å nøste dem opp er en god illustrasjon av hvorfor OCR-kode feiler stille i stedet for høylytt
Hvorfor virket den gamle OCR-motoren bare ved en tilfeldighet?
Den gamle motoren virket fordi mal-bitmap-ene og mål-bitmap-ene ble snudd på samme måte, så en vertikal invertering i piksel-leseren var usynlig for matcheren. TBitmap.ScanLine gir tilbake rader i motsatt rekkefølge av DIB-konvensjonen med positiv biHeight som resten av bildebanen antar. Rendrer du en M opp ned og sammenligner den med en mal som også er opp ned, er L1-forskjellen identisk med den korrekte sammenligningen. Hver glyph matchet. Ingenting var riktig
Den symmetrien er nøyaktig det som gjør denne feilkategorien kostbar. Enhver ensidig retting ødelegger matchingen: Rett mål-lesingen og la malene ligge, så bryter gjenkjenningen sammen i støy; rett malene først, så får du det samme sammenbruddet fra den andre retningen. Det finnes ingen trinnvis reparasjonsvei. Gjenoppbyggingen erstattet derfor hele lesingen med GetDIBits mot en eksplisitt deklarert BITMAPINFOHEADER, der positiv biHeight betyr rader nedenfra og opp som en kontrakt i stedet for som en VCL-konvensjon, og snur én gang, bevisst, når den kopierer inn i gråtonebufferen
Den andre feilen er den som bare Win64 avdekket. HDC-en som sendes til GetDIBits, må ikke være bitmap-ens egen minne-DC, fordi bitmap-en allerede er valgt inn i den, og Windows dokumenterer dette som ugyldig. Å sende Bitmap.Canvas.Handle ble tolerert av Win32-prosessen og feilet konsekvent i Win64-testprosessen. Rettelsen er en midlertidig skjerm-DC fra GetDC(0), frigjort i en finally-blokk, som ikke har noe med noen bitmap å gjø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 => rader nedenfra og opp
Info.bmiHeader.biPlanes := 1;
Info.bmiHeader.biBitCount := 24;
Info.bmiHeader.biCompression := BI_RGB;
DC := GetDC(0); // aldri Work.Canvas.Handle: Work er valgt der
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 bevisst 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 sammenhengende komponenter: fra gråtonepiksler til glyph-bokser
HotPDF binariserer først med Otsu-metoden og faller bare tilbake til en terskel i et lokalt vindu når Otsu ikke kan brukes. Den globale stien krever et reelt bimodalt histogram: Motoren beregner maksimumet for variansen mellom klassene og krever i tillegg at gråtoneområdet spenner over minst 64 nivåer før den stoler på resultatet. En utvasket skanning, en side med gradert bakgrunn eller en bitmap som nesten bare består av blekk, stryker på denne testen. Fallbacken sammenligner deretter hver piksel med gjennomsnittet i et vindu på 31 ganger 31 med en bias på 6 gråtonenivåer, beregnet med løpende kolonnesummer slik at skyvevinduet forblir lineært i pikselantallet
Glyph-uthenting er 8-tilkoblet komponentmerking over den resulterende masken, med en eksplisitt stack i stedet for rekursjon, fordi en maske for en hel side lett kan sprenge Delphi-trådstacken under en dyp flomfylling. To filtre kjører under merkingen: komponenter som er mindre enn 9 piksler, fjernes som spetter, og enhver komponent som dekker mer enn tre femtedeler av både bredden og høyden på bildet, fjernes som en ramme eller strek i stedet for en glyph. Et nytt pass slår sammen vertikalt stablede bokser der den horisontale overlappingen er minst en fjerdedel av den smaleste boksen, og det er dette som gjenforener prikken over en i eller en j med stammen. Alt dette arbeider på en raster, og rasteren kommer fra den samme rendereren som er beskrevet i rendering av en innlastet PDF-side til bitmap i Delphi. Det har en praktisk betydning: OCR-kvaliteten er begrenset av renderingskvaliteten, og standardverdien på 300 DPI for tekstlaget er et bevisst kompromiss, ikke et maksimum
Hva gjør stor I og liten l umulige å skille?
I Arial rasteriseres stor I og liten l til pikselsidentiske stolper, så ingen formfunksjon kan skille dem, og skille mellom store og små bokstaver må komme fra et helt annet sted. Motorens svar er høydeklustering på linjenivå. Glyph-bokser grupperes i tekstlinjer etter vertikal overlapping, hver linje analyseres for cap-høyden og den vanligste grunnlinjen, og høydene i en linje deles i en kort klynge og en høy klynge. En stolpe i den korte klyngen er en l; den samme stolpen i den høye klyngen er en I
Den åpenbare implementasjonen av dette skillet er en terskel med fast forhold, og den virker ikke. Forholdet mellom x-høyde og cap-høyde i Arial er rundt 0,72, som treffer rett på verdiene 0,70 og 0,75 som alle først griper etter. Flytt konstanten et hundredel i hver retning, og et helt korpus skifter bokstavtype. HotPDF gjør i stedet et endimensjonalt variansminimerende k=2-snitt: sorter kandidat-høydene, prøv hvert snittpunkt og behold snittet der summen av kvadrerte avvik innenfor klyngene er minst. Terskelen blir en egenskap ved siden, ikke en konstant i kildekoden
// ClusterHeights er sortert stigende; finn k=2-snittet med minst 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;
// bare forholdet mellom de to klyngegjennomsnittene avgjør hva som er det korte båndet
if SmallMean / TallMean <= 0.80 then
SmallGroup := ggSmall // et ekte x-høydebånd: små bokstavformer
else
SmallGroup := ggTall; // ett høydebånd: alt er versalhøyde
Line.LowercaseContext := (SmallGroup = ggSmall);
Linjer med bare ett høydebånd har ingen intern evidens i det hele tatt. En overskrift med bare versaler og en bildetekst med bare små bokstaver ser like ut isolert. For slike linjer sammenligner HotPDF linjens medianhøyde med median-x-høyden på siden, hentet fra linjer som faktisk ble delt: Et forhold på høyst 1,10 merker linjen som kontekst med små bokstaver, et forhold på minst 1,18 merker den som versalkontekst, og alt mellom dem forblir uten binding. Matchingen gir deretter en liten bonus for bokstavtype på 0,03 til kandidaten som stemmer med denne konteksten, noe som dytter uavgjorte resultater uten noen gang å overstyre en tydelig formforskjell
Hvorfor forvekslet en malrutenett på 12x18 c og o?
Malrutenettet ble utvidet fra 12 ganger 18 celler til 16 ganger 24 fordi gråtone-dekningsmarginen mellom c og o ved den mindre oppløsningen falt under 0,007, godt innenfor motorens tvetydighetsterskel. Hver glyph-boks resamples til rutenettet som dekningsverdier fra 0 til 255 i stedet for som en binær sjablong, så en celle som er én tredel blekk, leses som omtrent 85 i stedet for å rundes til svart eller hvitt. Ved 12 ganger 18 spenner den åpne siden av en c knapt over mer enn én cellekolonne, og det antialiasede gjennomsnittet vasker bort åpningen. Ved 16 ganger 24 overlever åpningen resamplingen, og de fleste parene som lett forveksles, flytter seg tilbake til en trygg avstand
Scoringen er den normaliserte L1-avstanden mellom de to dekningsrutenettene, pluss en straff på 0,30 ganger logaritmen av forskjellen i størrelsesforhold og 0,16 ganger forskjellen i blekkdensitet, med et hardt forfilter som hopper over alle maler der størrelsesforholdet avviker med mer enn en faktor på 2,6. Maler rasteriseres én gang per prosess fra fem systemfonter (Arial, Times New Roman, Courier New, Tahoma og Segoe UI) over et alfabet på 62 tegn, hurtigbufres bak en kritisk seksjon og gjenbrukes av alle påfølgende kall
Den siste konstanten er den interessante. Når kandidaten på andreplass scorer innenfor 0,018 av vinneren, setter HotPDF glyphens konfidens til 0,5, som ligger under akseptgrensen på 0,55, slik at glyphen ganske enkelt ikke sendes ut. Det er en bevisst fail-closed-grense, ikke en tuningrest: En avgrenset motor som gjetter, produserer et søkbart lag der teksten ikke stemmer med bildet, og et feil ord i et tekstlag er verre enn et manglende ord fordi det er usynlig for personen som gjennomgår skanningen
Del ord uten en terskel for fast mellomrom
HotPDF utleder terskelen for ordmellomrom per linje fra fordelingen av avstanden mellom glypher i stedet for fra et fast multiplum av gjennomsnittlig glyphbredde. Den klassiske heuristikken, "et mellomrom som er bredere enn 0,75 av middelsteget", bryter sammen så snart en linje blander sifre med smale bokstaver, fordi middelsteget slutter å beskrive noe reelt. Motoren sorterer i stedet gapene for linjen og ser etter det største spranget mellom to påfølgende sorterte verdier. Dette er grensen mellom klyngen innenfor ord og klyngen mellom ord, hvis en slik grense finnes. Tre vakter hindrer at dette slår ut på støy: Spranget må være minst 0,22 av gjennomsnittlig glyphbredde, det første gapet over skillet må være minst 0,32 av den, og det siste gapet under skillet må ikke overstige 0,65 av den. Hvis en vakt feiler, blir terskelen stående på MaxInt, og hele linjen blir ett ord. Den siste vakten hindrer at ett uvanlig bredt kerningpar deler et ord i to, noe som er en langt mer skadelig feil enn å slå sammen to ord, siden et sammenslått token fortsatt inneholder de riktige tegnene i riktig rekkefølge for et delstrengsøk
Skriv det usynlige tekstlaget over det skannede bildet
ApplyLoadedOCRTextLayer gjør gjenkjente ord om til et søkbart lag ved å tegne dem i tekst-renderingsmodus 3, modusen som verken fyller eller stryker, definert i ISO 32000-1 §9.3.6, og plassere dem over skannebildet de kom fra. Content stream-en åpner med BT fulgt av 3 Tr, og hvert ord plasseres med en tekstmatrise bygget fra den rapporterte grunnlinjen, cap-høyden omregnet fra piksler ved forespurt DPI og en horisontal skalering som strekker det syntetiske glyph-løpet til den målte ordbredden. Resultatet kan kopieres og søkes i som tekst, men tegner ingenting
Det finnes en overload uten motor som oppretter den innebygde gjenkjenneren for deg, og det er denne de fleste callers av den innebygde stien bør bruke. Gjenkjenning, Unicode-validering, budsjettregnskap og innholdskonstruksjon fullføres før copy-on-write-transaksjonen åpnes, så en kansellering, et budsjettoverskridelse eller en motorfeil lar objektgrafen og versjonsnummeret være urørt. Ord filtreres to ganger: Motoren forkaster alt under sin egen glyph-konfidensgrense på 0,55, og deretter forkaster THPDFOCRTextLayerOptions.MinimumConfidence (standard 0,5) hele ord under caller-ens grense
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; // la digitalt fødte sider være i fred
Options.UseOptionalContentGroup := True;
Options.OptionalContentGroupName := 'OCR Text Layer';
// overload uten motor: HotPDF leverer den innebygde avgrensede gjenkjenneren
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 begrensning bør sies tydelig i stedet for å oppdages senere. Det usynlige laget bruker en delt, syntetisk, ikke-innebygd Type0-font, som er nok til søk og kopiering i alle visere, men som ikke oppfyller kravet om fontinnbygging i ISO 19005. Hvis output-en må være PDF/A, må caller-en bygge inn en samsvarende font separat. Og et OCR-tekstlag har geometri, ikke struktur, så leserekkefølgen kommer bare fra glyph-posisjoner. Hvis du trenger logisk rekkefølge fra en side som allerede har ekte tekst, er strukturordnet tekstuthenting drevet av tag-treet et annet verktøy for et annet problem
Hvor stopper den innebygde motoren?
Den innebygde motoren er med vilje smal, og det er kunnskap om kantene som holder den nyttig. Den retter seg mot høykontrast, maskintrykt ASCII fra fonter som ligger nær de fem malfontene, og alt utenfor dette returnerer ingen ord i stedet for en gjetning. De konkrete grensene er:
- Bilder på opptil 4096 ganger 4096 og 4 194 304 piksler, med en gjenkjenningsfrist på 2000 ms og samarbeidende kansellering gjennom
THPDFCancellationToken - Et alfabet på 62 tegn med ASCII-bokstaver og sifre; ingen tegnsetting, aksenttegn eller CJK
- Bare aksejustert tekst, ved siderotasjonen som rendereren allerede normaliserte; skjeve skanninger deskewes ikke
- Tvetydige glyph-par forblir uløste, så en side kan returnere delvise ord eller diagnosen "found no unambiguous ASCII words"
Når denne rammen er for liten, er IHPDFOCREngine skjøten. Implementer Recognize mot din egen motor, gi den til overload-en av ApplyLoadedOCRTextLayer med tre argumenter, og alt nedstrøms (koordinatmapping, rotasjonshåndtering, Unicode-validering, budsjetter og den atomiske commit-en) forblir det samme. Bitmap-en lånes under hele det synkrone kallet og må ikke beholdes. For å bekrefte at laget ble skrevet riktig, last den lagrede filen på nytt og kjør den vanlige tekststien som er beskrevet i uthenting av tekst fra en innlastet PDF i Delphi; hvis ordene kommer tilbake, er laget ekte
Den innebygde malbaserte OCR-en, det usynlige tekstlaget, siderendereren som mater dem og tekstuthentingen fra innlastede dokumenter som verifiserer dem, leveres samlet i den samme native VCL-komponenten, uten ekstern OCR-runtime og uten en DLL som må distribueres sammen med applikasjonen. Hvis du bygger dokumentfangst, arkivering eller søk over skannede PDF-er i Delphi eller C++Builder, gir HotPDF Delphi PDF-komponenten deg hele pipelinen i én avhengighet