OCR-textskikt, streckkodsgränser och ansiktssvartningsboxar driver på beskurna PDF-sidor när bitmappspixlar mappas tillbaka genom MediaBox i stället för genom boxen renderaren faktiskt rastrerade: CropBox klippt mot MediaBox (ISO 32000-1 §14.11.2). HotPDF fixade detta för ApplyLoadedOCRTextLayer i v2.770.153, och för DecodeLoadedPageBarcodes och DetectLoadedRedactionFindings i v2.770.154
Buggrapporten som vanligen anländer ser ut så här. Ett inskannat kontraktsarkiv går genom OCR, utdatat är sökbart, och sökträffen för ett klausulnummer markeras en halv tum under och till vänster om det tryckta numret. De flesta filer i batchen är fina. De trasiga kom alla från en skanningsstation som skriver en /CropBox för att trimna platanmarginalen. Den där ena detaljen skiljer bilden OCR-motorn såg från ramen textskiktet placerades i, och samma missmatchning flyttar streckkodsgränser och, allvarligare, ansiktssvartningsboxar
Varför driver OCR-textskiktet iväg från de inskannade orden?
Textskiktet driver för att två halvor av pipelinen var oense om vilken rektangel bitmappen täcker. I v2.766.64 ändrade HotPDF rendering, SVG-export, visaren och utskrift till att hedra CropBox: en sida visas genom sin CropBox klippt mot sin MediaBox, vilket är vad ISO 32000-1 §14.11.2 föreskriver, och GetLoadedPageVisibleBox lades till för att returnera den synliga boxen. Igenkänningsfunktionerna fortsatte bygga sin device-till-sida-transform från GetLoadedPageBox(PageIndex, pbMediaBox, ...). Rastreringen täckte nu den synliga boxen, transformen antog fortfarande MediaBox, och varje igenkänd position kom tillbaka förskjuten med gapet mellan de två
Det drabbade fönstret är därför precist. ApplyLoadedOCRTextLayer felpacerade text från v2.766.64 till v2.770.152. Hela sidans DecodeLoadedPageBarcodes och ansiktsdetekteringen inuti DetectLoadedRedactionFindings förblev fel ett bygge längre, till v2.770.153. Före v2.766.64 ritade renderaren hela MediaBox, så mappning och rastrering överensstämde, på bekostnad av att igenkänna innehåll visare aldrig visar. Fixarna ändrade tre saker tillsammans för varje funktion: transformen, pixelbudgetuppskattningen, och sidboxen som lämnas till en egen motor i begäranposten
Flera fall drabbades aldrig:
- Sidor utan
/CropBox, eller vars CropBox är lika med MediaBox, mappas identiskt före och efter fixen DecodeLoadedPageBarcodesmedHasRegionsatt renderar exakt den region du skickar och mappar genom samma region, så explicit regionsavkodning var korrekt hela tiden; kontrollen att regionen ligger inuti sidan använder fortfarande MediaBox- Mönsterbaserade svartningsfynd (mejl, kortnummer och så vidare) kommer från textextraktion i user space, inte från en rastrering, så bara ansiktsdetekteringsfynden flyttades
Tre koordinatramar, och vilka HotPDF-API:er som använder vilken
HotPDF-kod som rör igenkänning har att göra med tre ramar, och de flesta mappningsbuggar kommer från att blanda två av dem
- Bitmappspixlar: ursprung uppe vänster, Y växer nedåt, enheten är pixlar vid begärandets DPI.
THPDFOCRWord.Left,Top,RightochBottomligger i den ramen, liksom de valfria baslinjepunkterna, resultaten en egenIHPDFBarcodeDecoderreturnerar, och boxarna från en egenIHPDFFaceDetector - PDF user space för en laddad sida: ursprung nere vänster, Y växer uppåt, enheten är punkter, med
Bottom < Top.GetLoadedPageBoxochGetLoadedPageVisibleBoxreturnerar Left, Bottom, Right, Top i den ramen, och det gör också fältenPageLeft,PageBottom,PageRightochPageTophosTHPDFOCRRequest, gränserna iTHPDFDecodedBarcodeoch rektanglarna iTHPDFRedactionFinding - HotPDF sidritningskoordinater: API:et du använder för att bygga nya sidor (textutdata, former, streckkoder, länkar, formulärfält) arbetar med ursprung uppe vänster och Y växande nedåt. Den ramen tillhör dokumentgenerering och har ingenting med de laddade dokumentens API:er ovan att göra, så mata aldrig en laddad sidas user space-rektangel in i den oförändrad
OCR-ordposten är med flit pixelbaserad: en motor rapporterar vad den såg i bilden, och ApplyLoadedOCRTextLayer äger konverteringen. Den divisionen fungerar bara när konverteringen använder rätt box, vilket är vad v2.770.153 återställde
Device-till-sida-transformen bakom OCR, streckkoder och ansikten
HotPDF mappar bitmappspixlar till sidan med en enda affin matris byggd från fem inmatningar: rotationen, skalan DPI / 72, bitmappshöjden, och Left, Bottom, Right och Top hos den renderade boxen. OCR, streckkodsavkodning och ansiktsdetektering delar alla en rutin för det, vilket är varför en felaktig boxinmatning slogo sönder alla tre på samma sätt. För en oroterad sida är sid-till-device-matrisen [A B C D E F]:
A = ScaleochD = -Scale, därScale = DPI / 72; den negativaDvänder user space (Y uppåt) till bitmappsutrymme (Y nedåt)B = C = 0, för en oroterad sida har ingen skjuvning eller byte mellan axlarE = -Left * Scale, som flyttar boxens vänsterkant till pixelkolumn 0F = BitmapHeight + Bottom * Scale, som mappar boxens nederkant till y = BitmapHeight, bitmappens undre kant, så att toppkanten landar på rad 0
Pixlar går tillbaka till sidan genom den matrisens invers. Request.PageRotation bär sidans /Rotate normaliserad till 0, 90, 180 eller 270 (varje värde som inte är en multipel av 90 behandlas som 0), och renderaren vrider sidan medurs som ISO 32000-1 §7.7.3.3 kräver. Under rotation byter axlarna plats och ett annat par boxkanter fästs vid bitmappsorigo. Utskrivet som inversa formler, med S = DPI / 72, x och y i pixlar och H bitmappshöjden:
| /Rotate | Sid-X | Sid-Y | Boxkanter mappningen beror på |
|---|---|---|---|
| 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 |
Sista kolumnen förklarar varför buggen verkade slumpmässig i produktion. En CropBox som trimnar bara sidans topp lämnar Left och Bottom orörda, så upprätta sidor kom ut perfekt och bara sidor bärande /Rotate 270 drev. Rotation byter också bitmappsdimensioner: vid 90 och 270 är bitmappen (Top - Bottom) * S pixlar bred och (Right - Left) * S pixlar hög
Vad går fel med MediaBox [0 0 612 792] och CropBox [36 36 576 756]?
Med en halvtumsbeskärning på varje sida landar en oroterad sidas textskikt exakt 36 punkter vänster om och 36 punkter under de inskannade orden när MediaBox används. Ta en US Letter-sida vars CropBox trimnar 36 punkter (0,5 tum) från varje kant. Den synliga boxen är 540 gånger 720 punkter, så vid standard-OCR-upplösningen 300 DPI är skalan 300 / 72 ≈ 4,1667 och bitmappen är 2250 gånger 3000 pixlar
Anta att motorn rapporterar ett ord med pixelbox Left 450, Top 600, Right 900, Bottom 660 och ingen baslinje. HotPDF placerar sedan baslinjen 20 procent av ordhöjden över nederkanten, på pixelrad 648, och mappar startpunkten (450, 648):
- Genom den synliga boxen: x = 36 + 450 / 4,1667 = 144,0 och y = 36 + (3000 - 648) / 4,1667 = 600,48, vilket är där ordet är tryckt
- Genom MediaBox: x = 0 + 108,0 = 108,0 och y = 0 + 564,48 = 564,48, en enhetlig förskjutning på (-36, -36) punkter
Rotera samma sida och felets riktning ändras, för olika kanter är inblandade. Vid /Rotate 180 använder X-termet Right, och 612 i stället för 576 knuffar skiktet 36 punkter åt höger medan Bottom fortfarande drar det 36 punkter ner. Vid /Rotate 270 är både Right och Top för stora, så skiktet flyttar 36 punkter höger och 36 punkter upp. Ett dokument med blandade orienteringar kan visa driften i tre riktningar, ett pålitligt fingeravtryck för denna bugg. Handskriven kod som härleder skalan ur boxen, som Bitmap.Width / (Right - Left), drar dessutom ut varje koordinat med 612 / 540, ungefär 13 procent, utöver offseten
Vilka av dina PDF-dokument är drabbade?
Ett PDF-dokument är utsatt när minst en sida har en synlig box som avviker från sin MediaBox, och HotPDF kan säga dig det på några rader. Jämför GetLoadedPageBox med pbMediaBox mot GetLoadedPageVisibleBox för varje sida, och skriv ut GetLoadedPageRotation bredvid så att du kan förutsäga driftriktningen från tabellen ovan. THPDFPageBoundary erbjuder också pbCropBox, pbBleedBox, pbTrimBox och pbArtBox, men GetLoadedPageBox(pbCropBox) faller tillbaka på MediaBox när ingen crop box finns och klipper inte, så den synliga boxen är det rätta att jämföra mot
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;
// den lagrade arrayen kan lista sina hörn i vilken ordning som helst
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// redan normaliserad och klippt mot 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;
Två detaljer hos GetLoadedPageVisibleBox spelar roll för skript som detta. Funktionen lämnar sina out-parametrar orörda när den misslyckas, så att förinställa en standardsidstorlek före anropet är ett säkert mönster. Och när en felformad CropBox inte skär MediaBox alls returnerar funktionen MediaBox i stället för en tom rektangel. Listar rapporten sidor och ditt utrullade bygge är äldre än v2.770.153 för OCR, eller v2.770.154 för streckkoder och ansikten, kör igenkänningen igen på de sidorna efter uppgradering. Ett textskikt committat av ett drabbat bygge stannar i den sparade filen, och standardalternativet SkipPagesWithText hoppar över de sidorna vid andra passet om du inte stänger av det eller tar bort gamla skiktet först
Hur ska en egen IHPDFOCREngine mappa pixlar tillbaka till PDF-utrymmet?
En egen IHPDFOCREngine ska returnera ordboxar i bitmappspixlar och låta HotPDF göra mappningen; konvertera till user space bara för dina egna beslut, och använd då boxen från begärandet, aldrig MediaBox. Sedan v2.770.153 beskriver begärandets PageLeft, PageBottom, PageRight och PageTop den renderade synliga boxen, så de matchar Request.Bitmap exakt. Hjälparen nedan är inversen till bibliotekets transform, inklusive dess användning av den faktiska bitmappshöjden för upprätta sidor, så den överensstämmer med HotPDF till pixeln
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Bitmappspixel (ursprung uppe vänster, Y nedåt) till PDF user space
// (ursprung nere vänster, Y uppåt), genom boxen bitmappen renderades från
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 anledning att behöva user space inuti en motor är en zonregel: fakturor vars brevhuvud du aldrig vill ha sökbart, eller ett stämpelområde som förvirrar igenkännaren. Motorn nedan, skriven med TInterfacedObject så att referensräkning hanterar dess livstid, filtrerar ord efter var deras centrum landar på sidan, och returnerar sedan de överlevande orörda i pixelkoordinater. RunRecognizer står in för ditt eget igenkännaranrop
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // din igenkännare, pixelboxar
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]; // fortfarande pixlar: HotPDF mappar dem själv
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Lämna motorn till ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) som med vilken annan motor som helst. Biblioteket validerar det som kommer tillbaka innan det litar på det: ett ord kastas och räknas i Info.DroppedWordCount när dess box lämnar bitmappen, när Right <= Left eller Bottom <= Top, eller när Confidence är utanför 0..1 eller under MinimumConfidence. Att returnera fler ord än MaxWordsPerPage, eller knuffa löpande totalen förbi MaxTotalWords, fäller hela anropet med ett budgetfel, så hedra Request.MaxWords i motorn. Konvertera inte ordboxar till user space innan du returnerar dem; HotPDF skulle behandla punktvärdena som pixlar och skiktet skulle kollapsa mot bitmappsorigo
Mappa din egen detektorutdata
Samma hjälpare tjänar en egenpipel byggd på RenderLoadedPageToBitmap, som renderar den synliga boxen och tillämpar /Rotate precis som igenkänningsfunktionerna gör. Läs boxen med GetLoadedPageVisibleBox, normalisera rotationen på samma sätt HotPDF gör, och mappa två motstående hörn av varje pixelbox. Y-axeln vänder och, vid 90 och 270 grader, byter axlarna plats, så de mappade hörnen kommer ut i ingen fast ordning; ta minimum och maximum av de mappade punkterna, vilket också är hur HotPDF bygger streckkodsgrä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 kod, pixelbox
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;
Rotationsbeteendet tas upp mer i djupet i att flattena sidrotation utan att bryta sidboxarna, och streckkodsavkodningspipelinen som konsumerar samma transform i att avkoda roterade QR-koder från PDF-sidor. Wrappar din motor en extern igenkännare visar Tesseract OCR-adaptorn för sökbar PDF processisolering- och annulleringssidan av samma gränssnitt
Snabbreferens: CropBox-säker koordinatmappning
- Renderaren rastrerar den synliga boxen, CropBox klippt mot MediaBox (ISO 32000-1 §14.11.2); varje pixel-till-sida-mappning måste använda den boxen, läst med
GetLoadedPageVisibleBox - HotPDF v2.770.153 fixade
ApplyLoadedOCRTextLayer; v2.770.154 fixade hela sidansDecodeLoadedPageBarcodesoch ansiktsfynd frånDetectLoadedRedactionFindings; byggen från v2.766.64 upp till de versionerna är drabbade THPDFOCRWord-boxar är bitmappspixlar med ursprung uppe vänster;GetLoadedPageBoxochGetLoadedPageVisibleBoxreturnerar PDF user space med ursprung nere vänster ochBottom < Top- Skalan är
DPI / 72; härled den från DPI:n, aldrig från en sidbox delad i bitmappsbredden - /Rotate avgör vilka kanter som spelar roll: Left och Bottom vid 0 och 90, Right och Bottom vid 180, Right och Top vid 270
- Returnera OCR-ord i pixlar och låt HotPDF mappa dem; konvertera bara för din egen filtreringslogik
- Kör OCR igen på beskurna sidor processade av ett drabbat bygge, och kom ihåg att
SkipPagesWithTexthoppar över sidor som redan bär gamla skiktet
Igenkänningsfunktionerna, sidboxfrågorna och renderingen av laddade dokument som används här skeppas alla i HotPDF-komponenten för Delphi och C++Builder; licensiering, testnedladdningar och den fullständiga funktionslistan finns på HotPDF Delphi PDF component-sidan