OCR-tekstlag, barcode-grænser og face redaction-boxes driver på beskårne PDF-sider, når bitmappixels mappes tilbage gennem MediaBox i stedet for den box, rendereren faktisk rasteriserede: CropBox'en beskåret til MediaBox (ISO 32000-1 §14.11.2). HotPDF rettede det for ApplyLoadedOCRTextLayer i v2.770.153 og for DecodeLoadedPageBarcodes og DetectLoadedRedactionFindings i v2.770.154
Bug-rapporten, der normalt ankommer, ser sådan ud. Et scannet kontraktarkiv køres gennem OCR, outputtet er søgbart, og søgerammen for et paragrafnummer fremhæves en halv tomme under og til venstre for det printede nummer. De fleste filer i batchen er fine. De ødelagte kom alle fra én scanningsstation, der skriver en /CropBox for at trimme kanten fra glaspladen. Den ene detalje adskiller det billede, OCR-enginen så, fra den ramme, tekstlaget blev placeret i, og samme mismatch flytter barcode-grænser og, mere alvorligt, face redaction-boxes
Hvorfor driver OCR-tekstlaget væk fra de scannede ord?
Tekstlaget driver, fordi pipelineens to halvdele var uenige om, hvilket rektangel bitmap'en dækker. I v2.766.64 ændrede HotPDF rendering, SVG-eksport, viewer og print til at følge CropBox'en: en side vises gennem dens CropBox beskåret til dens MediaBox, hvilket er det, ISO 32000-1 §14.11.2 forskriver, og GetLoadedPageVisibleBox blev tilføjet for at returnere den synlige box. Genkendelsesfunktionerne byggede stadig deres device-to-page-transform af GetLoadedPageBox(PageIndex, pbMediaBox, ...). Rasteren dækkede nu den synlige box, transformen antog stadig MediaBox, og hver genkendt position kom tilbage forskudt med afstanden mellem de to
Det berørte vindue er derfor præcist. ApplyLoadedOCRTextLayer placerede tekst forkert fra v2.766.64 til og med v2.770.152. Helsides DecodeLoadedPageBarcodes og face detectionen inde i DetectLoadedRedactionFindings forblev forkerte én build længere, til og med v2.770.153. Før v2.766.64 tegnede rendereren hele MediaBox, så mapping og raster var enige, på bekostning af at genkende indhold, som viewere aldrig viser. Fixene ændrede tre ting sammen for hver funktion: transformen, pixel-budget-estimatet og den side-box, der gives til en tilpasset engine i request-recorden
Flere tilfælde var aldrig berørt:
- Sider uden en
/CropBox, eller hvis CropBox er lig MediaBox, mapper identisk før og efter fixet DecodeLoadedPageBarcodesmedHasRegionsat renderer præcis den region, du giver med, og mapper gennem samme region, så explicit-region-dekodning var korrekt hele vejen; tjekket på, at regionen ligger inde i siden, bruger stadig MediaBox- Mønsterbaserede redaction-fund (emails, kortnumre og så videre) kommer fra tekst-ekstraktion i user space, ikke fra en raster, så kun face-detection-funderne flyttede sig
Tre koordinatrammer, og hvilke HotPDF-API'er der bruger hver
HotPDF-kode, der rører genkendelse, arbejder med tre rammer, og de fleste mapping-bugs kommer fra at blande to af dem
- Bitmap-pixels: origo øverst til venstre, Y vokser nedad, enheden er pixels ved anmodningens DPI.
THPDFOCRWord.Left,Top,RightogBottomer i denne ramme, ligesom de valgfrie baseline-punkter, resultaterne en tilpassetIHPDFBarcodeDecoderreturnerer og boxene fra en tilpassetIHPDFFaceDetector - PDF user space for en indlæst side: origo nederst til venstre, Y vokser opad, enheden er punkter, med
Bottom < Top.GetLoadedPageBoxogGetLoadedPageVisibleBoxreturnerer Left, Bottom, Right, Top i denne ramme, og det gørPageLeft-,PageBottom-,PageRight- ogPageTop-felterne iTHPDFOCRRequest, grænserne iTHPDFDecodedBarcodeog rektanglerne iTHPDFRedactionFindingogså - HotPDF side-tegnekoordinater: API'et, du bruger til at bygge nye sider (tekstoutput, figurer, barcodes, links, formfelter), arbejder med origo øverst til venstre og Y voksende nedad. Den ramme tilhører dokumentgenerering og har intet at gøre med de indlæste dokumenters API'er ovenfor, så giv aldrig et indlæst sides user-space-rektangel videre til den uændret
OCR-ord-recorden er bevidst pixel-baseret: en engine melder, hvad den så i billedet, og ApplyLoadedOCRTextLayer ejer konverteringen. Den opdeling virker kun, når konverteringen bruger den rigtige box, hvilket er det, v2.770.153 genoprettede
Device-to-page-transformen bag OCR, barcodes og faces
HotPDF mapper bitmap-pixels til siden med én enkelt affin matrix bygget af fem inputs: rotationen, skalaen DPI / 72, bitmap-højden og Left, Bottom, Right og Top for den renderede box. OCR, barcode-dekodning og face detection deler alle én rutine til det, hvilket er hvorfor ét forkert box-input brød alle tre på samme måde. For en uroteret side er page-to-device-matrixen [A B C D E F]:
A = ScaleogD = -Scale, hvorScale = DPI / 72; den negativeDvender user space (Y opad) til bitmap space (Y nedad)B = C = 0, for en uroteret side har ingen shear eller bytning mellem akserneE = -Left * Scale, som flytter boxens venstre kant til pixelkolonne 0F = BitmapHeight + Bottom * Scale, som mapper boxens bundkant til y = BitmapHeight, bitmap'ens nederste kant, så topkanten lander på række 0
Pixels går tilbage til siden gennem den matrices inverse. Request.PageRotation bærer sidens /Rotate normaliseret til 0, 90, 180 eller 270 (enhver værdi, der ikke er multiplum af 90, behandles som 0), og rendereren drejer siden med uret, som ISO 32000-1 §7.7.3.3 kræver. Under rotation bytter akserne plads, og et andet par box-kanter fastgøres til bitmap-origo. Skrevet ud som inverse formler, med S = DPI / 72, x og y i pixels og H bitmap-højden:
| /Rotate | Side-X | Side-Y | Box-kanter, mappingen afhænger af |
|---|---|---|---|
| 0 | Left + x / S | Bottom + (H - y) / S | Left, Bottom |
| 90 | Left + y / S | Bottom + x / S | Left, Bottom |
| 180 | Right - x / S | Bottom + y / S | Right, Bottom |
| 270 | Right - y / S | Top - x / S | Right, Top |
Sidste kolonne forklarer, hvorfor buggen så tilfældig ud i produktion. En CropBox, der kun trimmer sidens top, lader Left og Bottom stå urørt, så oprejste sider kom perfekt ud, og kun sider med /Rotate 270 drev. Rotation bytter også bitmap-dimensionerne: ved 90 og 270 er bitmap'en (Top - Bottom) * S pixels bred og (Right - Left) * S pixels høj
Hvad går galt med MediaBox [0 0 612 792] og CropBox [36 36 576 756]?
Med en halv tomme crop på hver side lander en uroteret sides tekstlag præcis 36 punkter venstre for og 36 punkter under de scannede ord, når MediaBox bruges. Tag en US Letter-side, hvis CropBox trimmer 36 punkter (0,5 tomme) fra hver kant. Den synlige box er 540 gange 720 punkter, så ved default OCR-opløsning på 300 DPI er skalaen 300 / 72 ≈ 4,1667, og bitmap'en er 2250 gange 3000 pixels
Antag, at enginen melder et ord med pixel-box Left 450, Top 600, Right 900, Bottom 660 og ingen baseline. HotPDF placerer så baseline 20 procent af ordhøjden over bundkanten, ved pixelrække 648, og mapper startpunktet (450, 648):
- Gennem den synlige box: x = 36 + 450 / 4,1667 = 144,0 og y = 36 + (3000 - 648) / 4,1667 = 600,48, hvilket er dér, hvor ordet er printet
- Gennem MediaBox: x = 0 + 108,0 = 108,0 og y = 0 + 564,48 = 564,48, en ensartet forskydning på (-36, -36) punkter
Rotér samme side, og fejlens retning ændrer sig, for forskellige kanter er involveret. Ved /Rotate 180 bruger X-ledet Right, og 612 i stedet for 576 skubber laget 36 punkter til højre, mens Bottom stadig trækker det 36 punkter ned. Ved /Rotate 270 er både Right og Top for store, så laget flytter 36 punkter til højre og 36 punkter op. Et dokument med blandede orienteringer kan vise drift i tre retninger, et sikkert fingeraftryk for denne bug. Håndskrevet kode, der udleder skalaen fra boxen, som Bitmap.Width / (Right - Left), strækker desuden hver koordinat med 612 / 540, cirka 13 procent, oven på offseten
Hvilke af dine PDF-dokumenter er berørt?
Et PDF-dokument er udsat, når mindst én side har en synlig box, der afviger fra dens MediaBox, og HotPDF kan fortælle dig det på få linjer. Sammenlign GetLoadedPageBox med pbMediaBox mod GetLoadedPageVisibleBox for hver side, og print GetLoadedPageRotation ved siden af, så du kan forudsige drift-retningen ud fra tabellen ovenfor. THPDFPageBoundary tilbyder også pbCropBox, pbBleedBox, pbTrimBox og pbArtBox, men GetLoadedPageBox(pbCropBox) falder tilbage til MediaBox, når ingen crop box findes, og beskærer ikke, så den synlige box er den rigtige at sammenligne med
uses
System.SysUtils, HPDFDoc;
procedure ReportCroppedPages(const FileName: string);
var
Pdf: THotPDF;
I: Integer;
ML, MB, MR, MT, VL, VB, VR, VT, Tmp: Single;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) < 1 then
raise Exception.Create('Cannot load ' + FileName);
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
if not Pdf.GetLoadedPageBox(I, pbMediaBox, ML, MB, MR, MT) then
Continue;
// det gemte array kan liste sine hjørner i vilkårlig rækkefølge
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// allerede normaliseret og beskåret til MediaBox
if not Pdf.GetLoadedPageVisibleBox(I, VL, VB, VR, VT) then
Continue;
if (Abs(VL - ML) > 0.01) or (Abs(VB - MB) > 0.01) or
(Abs(VR - MR) > 0.01) or (Abs(VT - MT) > 0.01) then
Writeln(Format('Page %d MediaBox [%g %g %g %g] visible [%g %g %g %g] /Rotate %d',
[I + 1, ML, MB, MR, MT, VL, VB, VR, VT,
Pdf.GetLoadedPageRotation(I)]));
end;
finally
Pdf.Free;
end;
end;
To detaljer ved GetLoadedPageVisibleBox betyder noget for scripts som dette. Funktionen lader sine out-parametre stå urørt, når den fejler, så at forudindstille en default sidestørrelse før kaldet er et sikkert mønster. Og når en misdannet CropBox slet ikke skærer MediaBox, returnerer funktionen MediaBox frem for et tomt rektangel. Lister rapporten sider, og din deployerede build er ældre end v2.770.153 for OCR, eller v2.770.154 for barcodes og faces, så genkør genkendelsen på de sider efter opgradering. Et OCR-lag committeret af en berørt build bliver i den gemte fil, og default-optionen SkipPagesWithText springer de sider over i anden gennemkørsel, medmindre du slår den fra eller fjerner det gamle lag først
Hvordan bør en tilpasset IHPDFOCREngine mappe pixels tilbage til PDF space?
En tilpasset IHPDFOCREngine bør returnere ord-boxes i bitmap-pixels og lade HotPDF gøre mappingen; konvertér kun til user space til dine egne beslutninger, og brug så boxen fra requesten, aldrig MediaBox. Siden v2.770.153 beskriver requestens PageLeft, PageBottom, PageRight og PageTop den renderede synlige box, så de matcher Request.Bitmap præcist. Hjælperen nedenfor er inversen af bibliotekets transform, inklusive dets brug af den faktiske bitmap-højde for oprejste sider, så den er enig med HotPDF til pixel
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Bitmap-pixel (top-left origo, Y nedad) til PDF user space
// (bottom-left origo, Y opad), gennem den box, bitmap'en blev renderet fra
procedure HotPixelToPage(Rotation, DPI, BitmapHeight: Integer;
Left, Bottom, Right, Top: Single; X, Y: Double;
out PageX, PageY: Double);
var
S: Double;
begin
S := DPI / 72.0;
case Rotation of
90: begin PageX := Left + Y / S; PageY := Bottom + X / S; end;
180: begin PageX := Right - X / S; PageY := Bottom + Y / S; end;
270: begin PageX := Right - Y / S; PageY := Top - X / S; end;
else
PageX := Left + X / S;
PageY := Bottom + (BitmapHeight - Y) / S;
end;
end;
En realistisk grund til at behøve user space inde i en engine er en zoneregel: fakturaer, hvis brevhoved du aldrig vil have søgbart, eller et stempeleje, der forvirrer recognizeren. Enginen nedenfor, skrevet med TInterfacedObject, så reference counting håndterer dens levetid, filtrerer ord efter, hvor deres centre lander på siden, og returnerer derefter de overlevende urørt i pixelkoordinater. RunRecognizer står i stedet for dit eget recognizer-kald
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // din recognizer, pixel-boxes
public
constructor Create(SkipLeft, SkipBottom, SkipRight, SkipTop: Single);
function GetName: AnsiString;
function Recognize(const Request: THPDFOCRRequest;
out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
end;
function TZoneFilterOCREngine.Recognize(const Request: THPDFOCRRequest;
out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
var
Raw: THPDFOCRWords;
I, Count: Integer;
CX, CY: Double;
begin
Diagnostic := '';
SetLength(Words, 0);
if not RunRecognizer(Request.Bitmap, Request.MaxWords, Raw) then
begin
Diagnostic := 'recognizer failed';
Exit(False);
end;
SetLength(Words, Length(Raw));
Count := 0;
for I := 0 to High(Raw) do
begin
HotPixelToPage(Request.PageRotation, Request.DPI,
Request.Bitmap.Height, Request.PageLeft, Request.PageBottom,
Request.PageRight, Request.PageTop,
(Raw[I].Left + Raw[I].Right) / 2, (Raw[I].Top + Raw[I].Bottom) / 2,
CX, CY);
if (CX >= FSkipLeft) and (CX <= FSkipRight) and
(CY >= FSkipBottom) and (CY <= FSkipTop) then
Continue;
Words[Count] := Raw[I]; // stadig pixels: HotPDF mapper dem selv
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Giv enginen videre til ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) som med enhver anden engine. Biblioteket validerer, hvad der kommer tilbage, før det stoler på det: et ord droppes og tælles i Info.DroppedWordCount, når dets box forlader bitmap'en, når Right <= Left eller Bottom <= Top, eller når Confidence er uden for 0..1 eller under MinimumConfidence. At returnere flere ord end MaxWordsPerPage, eller skubbe det løbende total forbi MaxTotalWords, fejler hele kaldet med en budgetfejl, så hold Request.MaxWords i enginen. Konvertér ikke ord-boxes til user space, før du returnerer dem; HotPDF ville behandle punktværdierne som pixels, og laget ville kollapse mod bitmap-origo
At mappe din egen detector-output
Samme hjælper tjener en egen pipeline bygget på RenderLoadedPageToBitmap, som renderer den synlige box og anvender /Rotate ligesom genkendelsesfunktionerne. Læs boxen med GetLoadedPageVisibleBox, normalisér rotationen på samme måde som HotPDF, og map to modstående hjørner af hver pixel-box. Y-aksen vender, og ved 90 og 270 grader bytter akserne plads, så de mappede hjørner kommer ud i ingen fast rækkefølge; tag minimum og maksimum af de mappede punkter, hvilket også er sådan HotPDF bygger barcode-grænser
const
DPI = 200;
var
Pdf: THotPDF;
Bmp: TBitmap;
VL, VB, VR, VT: Single;
Rotation: Integer;
PxL, PxT, PxR, PxB, X1, Y1, X2, Y2: Double;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-ids.pdf');
if not Pdf.GetLoadedPageVisibleBox(0, VL, VB, VR, VT) then Exit;
Rotation := Pdf.GetLoadedPageRotation(0) mod 360;
if Rotation < 0 then Inc(Rotation, 360);
if (Rotation <> 90) and (Rotation <> 180) and (Rotation <> 270) then
Rotation := 0;
Bmp := Pdf.RenderLoadedPageToBitmap(0, DPI);
if Bmp = nil then Exit;
try
MyDetector(Bmp, PxL, PxT, PxR, PxB); // din kode, pixel-box
HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
PxL, PxT, X1, Y1);
HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
PxR, PxB, X2, Y2);
Writeln(Format('User-space box [%.2f %.2f %.2f %.2f]',
[Min(X1, X2), Min(Y1, Y2), Max(X1, X2), Max(Y1, Y2)]));
finally
Bmp.Free;
end;
finally
Pdf.Free;
end;
end;
Rotationsadfærden er dækket mere i dybden i at fladtrykke siderotation uden at ødelægge sideboxes, og barcode-dekodningspipelinen, der bruger samme transform, i dekodning af roterede QR-koder fra PDF-sider. Wrapper din engine en ekstern recognizer, viser Tesseract OCR-adapteren til søgbar PDF procesisolering og cancellation-siden af samme interface
Hurtig reference: CropBox-sikker koordinatmapping
- Rendereren rasteriserer den synlige box, CropBox'en beskåret til MediaBox (ISO 32000-1 §14.11.2); enhver pixel-til-side-mapping skal bruge den box, læst med
GetLoadedPageVisibleBox - HotPDF v2.770.153 rettede
ApplyLoadedOCRTextLayer; v2.770.154 rettede helsidesDecodeLoadedPageBarcodesog face-fund fraDetectLoadedRedactionFindings; builds fra v2.766.64 op til de versioner er berørt THPDFOCRWord-boxes er bitmap-pixels med origo øverst til venstre;GetLoadedPageBoxogGetLoadedPageVisibleBoxreturnerer PDF user space med origo nederst til venstre ogBottom < Top- Skalaen er
DPI / 72; udled den fra DPI'en, aldrig fra en side-box divideret ind i bitmap-bredden - /Rotate afgør, hvilke kanter der betyder noget: Left og Bottom ved 0 og 90, Right og Bottom ved 180, Right og Top ved 270
- Returnér OCR-ord i pixels, og lad HotPDF mappe dem; konvertér kun til din egen filtreringslogik
- Genkør OCR på beskårne sider behandlet af en berørt build, og husk, at
SkipPagesWithTextspringer sider over, der allerede bærer det gamle lag
Genkendelsesfunktionerne, side-box-forespørgslerne og indlæst dokument-rendering, der bruges her, skiber alle i HotPDF-komponenten til Delphi og C++Builder; licensering, trial-downloads og den fulde funktionsliste er på HotPDF Delphi PDF-komponentsiden