OCR-Textebenen, Barcode-Grenzen und Schwärzungsboxen für Gesichter driften auf beschnittenen PDF-Seiten, wenn Bitmap-Pixel durch die MediaBox zurückgemappt wurden statt durch die Box, die der Renderer tatsächlich gerastert hat: die auf die MediaBox zugeschnittene CropBox (ISO 32000-1 §14.11.2). HotPDF hat das für ApplyLoadedOCRTextLayer in v2.770.153 gefixt und für DecodeLoadedPageBarcodes und DetectLoadedRedactionFindings in v2.770.154
Der Bugreport, der üblicherweise ankommt, sieht so aus. Ein gescannter Vertragsarchiv läuft durch OCR, die Ausgabe ist durchsuchbar, und der Suchtreffer auf eine Klauselnummer wird einen halben Zoll unterhalb und links der gedruckten Nummer hervorgehoben. Die meisten Dateien im Batch sind fein. Die kaputten stammen alle von einer Scanstation, die eine /CropBox schreibt, um den Auflagerand wegzuschneiden. Dieses eine Detail trennt das Bild, das die OCR-Engine sah, von dem Rahmen, in den die Textebene platziert wurde, und dieselbe Diskrepanz verschiebt Barcode-Grenzen und, ernster, Schwärzungsboxen für Gesichter
Warum driftet die OCR-Textebene von den gescannten Wörtern weg?
Die Textebene driftet, weil zwei Hälften der Pipeline sich uneinig waren, welches Rechteck die Bitmap abdeckt. In v2.766.64 stellte HotPDF Rendering, SVG-Export, Viewer und Druck auf die CropBox um: Eine Seite wird durch ihre auf die MediaBox zugeschnittene CropBox angezeigt, was ISO 32000-1 §14.11.2 vorschreibt, und GetLoadedPageVisibleBox wurde hinzugefügt, um diese sichtbare Box zurückzugeben. Die Erkennungs-Features bauten ihre Device-zu-Seite-Transformation weiterhin aus GetLoadedPageBox(PageIndex, pbMediaBox, ...). Das Raster deckte jetzt die sichtbare Box ab, die Transformation nahm weiterhin die MediaBox an, und jede erkannte Position kam um die Lücke zwischen beiden versetzt zurück
Das betroffene Fenster ist daher präzise. ApplyLoadedOCRTextLayer platzierte Text von v2.766.64 bis v2.770.152 falsch. Ganzseitiges DecodeLoadedPageBarcodes und die Gesichtserkennung innerhalb von DetectLoadedRedactionFindings blieben einen Build länger falsch, bis v2.770.153. Vor v2.766.64 zeichnete der Renderer die ganze MediaBox, Mapping und Raster stimmten also überein, um den Preis, Inhalte zu erkennen, die Viewer nie zeigen. Die Fixes änderten für jedes Feature drei Dinge zusammen: die Transformation, die Pixelbudget-Schätzung und die Seitenbox, die im Request-Record an eine Custom-Engine übergeben wird
Mehrere Fälle waren nie betroffen:
- Seiten ohne
/CropBoxoder deren CropBox gleich der MediaBox ist, mappen vor und nach dem Fix identisch DecodeLoadedPageBarcodesmit gesetztemHasRegionrendert exakt die Region, die Sie übergeben, und mappt durch dieselbe Region, explizites Region-Decoding war also durchgängig korrekt; die Prüfung, dass die Region innerhalb der Seite liegt, nutzt weiterhin die MediaBox- Musterbasierte Redaction-Findings (E-Mails, Kartennummern und so weiter) kommen aus der Textextraktion im User Space, nicht aus einem Raster, nur die Face-Detection-Findings haben sich also bewegt
Drei Koordinatenrahmen — und welche HotPDF-APIs welchen nutzen
HotPDF-Code, der Erkennung anfasst, hat es mit drei Rahmen zu tun, und die meisten Mapping-Bugs kommen daher, zwei davon zu vermischen
- Bitmap-Pixel: Ursprung oben links, Y wächst abwärts, Einheit sind Pixel bei der angefragten DPI.
THPDFOCRWord.Left,Top,RightundBottomliegen in diesem Rahmen, ebenso die optionalen Baseline-Punkte, die Ergebnisse, die ein Custom-IHPDFBarcodeDecoderzurückgibt, und die Boxen eines Custom-IHPDFFaceDetector - PDF-User Space einer geladenen Seite: Ursprung unten links, Y wächst aufwärts, Einheit sind Punkte, mit
Bottom < Top.GetLoadedPageBoxundGetLoadedPageVisibleBoxliefern Left, Bottom, Right, Top in diesem Rahmen, ebenso die FelderPageLeft,PageBottom,PageRightundPageTopvonTHPDFOCRRequest, die Grenzen inTHPDFDecodedBarcodeund die Rechtecke inTHPDFRedactionFinding - HotPDF-Seitenzeichenkoordinaten: Die API, mit der Sie neue Seiten bauen (Textausgabe, Formen, Barcodes, Links, Formfelder), arbeitet mit Ursprung oben links und Y abwärts wachsend. Dieser Rahmen gehört zur Dokumentgenerierung und hat mit den geladenen-Dokument-APIs oben nichts zu tun, füttern Sie ihn also nie unverändert mit einem User-Space-Rechteck einer geladenen Seite
Der OCR-Word-Record ist bewusst pixelbasiert: Eine Engine meldet, was sie im Bild gesehen hat, und ApplyLoadedOCRTextLayer besitzt die Konversion. Diese Arbeitsteilung funktioniert nur, wenn die Konversion die richtige Box nutzt, was v2.770.153 wiederhergestellt hat
Die Device-zu-Seite-Transformation hinter OCR, Barcodes und Gesichtern
HotPDF mappt Bitmap-Pixel auf die Seite mit einer einzigen affinen Matrix, gebaut aus fünf Eingaben: der Rotation, dem Maßstab DPI / 72, der Bitmap-Höhe und Left, Bottom, Right und Top der gerenderten Box. OCR, Barcode-Decoding und Gesichtserkennung teilen sich dafür eine Routine — deshalb brach eine falsche Box-Eingabe alle drei auf dieselbe Weise. Für eine unrotierte Seite ist die Seite-zu-Device-Matrix [A B C D E F]:
A = ScaleundD = -Scale, wobeiScale = DPI / 72; das negativeDkippt den User Space (Y aufwärts) in den Bitmap-Raum (Y abwärts)B = C = 0, denn eine unrotierte Seite hat keine Scherung und keinen AchstauschE = -Left * Scale, was die linke Kante der Box auf Pixelspalte 0 schiebtF = BitmapHeight + Bottom * Scale, was die untere Kante der Box auf y = BitmapHeight mappt, die Unterkante der Bitmap, die Oberkante landet also auf Zeile 0
Zurück auf die Seite gehen Pixel über die Inverse dieser Matrix. Request.PageRotation trägt das /Rotate der Seite, normalisiert auf 0, 90, 180 oder 270 (jeder Wert, der kein Vielfaches von 90 ist, gilt als 0), und der Renderer dreht die Seite im Uhrzeigersinn, wie ISO 32000-1 §7.7.3.3 es verlangt. Unter Rotation tauschen die Achsen, und ein anderes Paar Box-Kanten wird an den Bitmap-Ursprung genagelt. Als inverse Formeln ausgeschrieben, mit S = DPI / 72, x und y in Pixeln und H der Bitmap-Höhe:
| /Rotate | Page X | Page Y | Box-Kanten, von denen das Mapping abhängt |
|---|---|---|---|
| 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 |
Die letzte Spalte erklärt, warum der Bug in der Produktion zufällig wirkte. Eine CropBox, die nur oben an der Seite abschneidet, lässt Left und Bottom unberührt, aufrechte Seiten kamen also perfekt heraus, und nur Seiten mit /Rotate 270 drifteten. Rotation tauscht zudem die Bitmap-Dimensionen: Bei 90 und 270 ist die Bitmap (Top - Bottom) * S Pixel breit und (Right - Left) * S Pixel hoch
Was läuft schief mit MediaBox [0 0 612 792] und CropBox [36 36 576 756]?
Mit einem halben Zoll Beschnitt an jeder Seite landet die Textebene einer unrotierten Seite exakt 36 Punkte links von und 36 Punkte unter den gescannten Wörtern, wenn die MediaBox benutzt wird. Nehmen Sie eine US-Letter-Seite, deren CropBox an jeder Kante 36 Punkte (0,5 Zoll) abschneidet. Die sichtbare Box ist 540 mal 720 Punkte, bei der Default-OCR-Auflösung von 300 DPI ist der Maßstab also 300 / 72 ≈ 4,1667, und die Bitmap ist 2250 mal 3000 Pixel
Angenommen, die Engine meldet ein Wort mit Pixelbox Left 450, Top 600, Right 900, Bottom 660 und ohne Baseline. HotPDF legt die Baseline dann 20 Prozent der Worthöhe über die Unterkante, auf Pixelzeile 648, und mappt den Startpunkt (450, 648):
- Durch die sichtbare Box: x = 36 + 450 / 4,1667 = 144,0 und y = 36 + (3000 - 648) / 4,1667 = 600,48 — genau dort, wo das Wort gedruckt ist
- Durch die MediaBox: x = 0 + 108,0 = 108,0 und y = 0 + 564,48 = 564,48 — eine gleichmäßige Verschiebung um (-36, -36) Punkte
Dieselbe Seite zu rotieren ändert die Fehlrichtung, weil andere Kanten im Spiel sind. Bei /Rotate 180 nutzt der X-Term Right, und 612 statt 576 schiebt die Ebene 36 Punkte nach rechts, während Bottom sie weiterhin 36 Punkte nach unten zieht. Bei /Rotate 270 sind sowohl Right als auch Top zu groß, die Ebene rückt also 36 Punkte nach rechts und 36 Punkte nach oben. Ein Dokument mit gemischten Ausrichtungen kann das Driften in drei Richtungen zeigen — ein verlässlicher Fingerabdruck dieses Bugs. Handgeschriebener Code, der den Maßstab aus der Box ableitet, etwa Bitmap.Width / (Right - Left), streckt zudem jede Koordinate um 612 / 540, rund 13 Prozent, obendrauf
Welche Ihrer PDF-Dokumente sind betroffen?
Ein PDF-Dokument ist exponiert, wenn mindestens eine Seite eine sichtbare Box hat, die von ihrer MediaBox abweicht, und HotPDF sagt Ihnen das in ein paar Zeilen. Vergleichen Sie GetLoadedPageBox mit pbMediaBox gegen GetLoadedPageVisibleBox für jede Seite und drucken Sie GetLoadedPageRotation daneben, sodass Sie die Driftrichtung aus der Tabelle oben vorhersagen können. THPDFPageBoundary bietet auch pbCropBox, pbBleedBox, pbTrimBox und pbArtBox, aber GetLoadedPageBox(pbCropBox) fällt ohne vorhandene CropBox auf die MediaBox zurück und beschneidet nicht, die sichtbare Box ist also das richtige Vergleichsobjekt
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;
// das gespeicherte Array darf seine Ecken in beliebiger Reihenfolge listen
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// bereits normalisiert und auf die MediaBox zugeschnitten
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;
Zwei Details von GetLoadedPageVisibleBox zählen für Skripte wie dieses. Die Funktion lässt ihre out-Parameter bei Misserfolg unangetastet, sie vorher auf eine Default-Seitengröße zu setzen ist also ein sicheres Muster. Und wenn eine missgebildete CropBox die MediaBox überhaupt nicht schneidet, liefert die Funktion die MediaBox statt eines leeren Rechtecks. Listet der Report Seiten auf und Ihr ausgelieferter Build ist älter als v2.770.153 für OCR bzw. v2.770.154 für Barcodes und Gesichter, fahren Sie die Erkennung auf diesen Seiten nach dem Upgrade erneut. Eine von einem betroffenen Build eingebrachte OCR-Ebene bleibt in der gespeicherten Datei, und die Default-Option SkipPagesWithText überspringt diese Seiten in einem zweiten Durchlauf, es sei denn, Sie schalten sie aus oder entfernen die alte Ebene zuerst
Wie soll eine Custom-IHPDFOCREngine Pixel zurück in den PDF-Raum mappen?
Eine Custom-IHPDFOCREngine sollte Wortboxen in Bitmap-Pixeln zurückgeben und HotPDF das Mappen überlassen; konvertieren Sie nur für Ihre eigenen Entscheidungen in den User Space, und dann mit der Box aus dem Request, nie mit der MediaBox. Seit v2.770.153 beschreiben PageLeft, PageBottom, PageRight und PageTop des Requests die gerenderte sichtbare Box, sie passen also exakt zu Request.Bitmap. Der Helfer unten ist die Inverse der Bibliotheks-Transformation, eingeschlossen ihrer Benutzung der tatsächlichen Bitmap-Höhe für aufrechte Seiten, er stimmt also pixelgenau mit HotPDF überein
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Bitmap-Pixel (Ursprung oben links, Y abwärts) in den PDF-User Space
// (Ursprung unten links, Y aufwärts), durch die Box, aus der die Bitmap gerendert wurde
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;
Ein realistischer Grund, innerhalb einer Engine den User Space zu brauchen, ist eine Zonenregel: Rechnungen, deren Briefkopf Sie nie durchsuchbar wollen, oder eine Stempelfläche, die den Recognizer verwirrt. Die Engine unten, geschrieben mit TInterfacedObject, sodass die Referenzzählung ihre Lebensdauer übernimmt, filtert Wörter danach, wo ihre Zentren auf der Seite fallen, und liefert die Überlebenden unverändert in Pixelkoordinaten zurück. RunRecognizer steht stellvertretend für Ihren eigenen Recognizer-Aufruf
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // User Space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // Ihr Recognizer, Pixelboxen
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]; // weiterhin Pixel: HotPDF mappt sie selbst
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Reichen Sie die Engine an ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) weiter wie jede andere Engine auch. Die Bibliothek validiert, was zurückkommt, bevor sie ihm vertraut: Ein Wort wird verworfen und in Info.DroppedWordCount gezählt, wenn seine Box die Bitmap verlässt, wenn Right <= Left oder Bottom <= Top gilt oder wenn Confidence außerhalb 0..1 oder unter MinimumConfidence liegt. Mehr Wörter zurückzugeben als MaxWordsPerPage oder die laufende Summe über MaxTotalWords zu schieben, lässt den ganzen Aufruf mit einem Budgetfehler scheitern, ehren Sie also Request.MaxWords in der Engine. Konvertieren Sie Wortboxen nicht in den User Space, bevor Sie sie zurückgeben; HotPDF würde die Punktwerte als Pixel behandeln, und die Ebene würde zum Bitmap-Ursprung kollabieren
Die Ausgabe Ihres eigenen Detectors mappen
Derselbe Helfer bedient eine selbstgebaute Pipeline auf RenderLoadedPageToBitmap, die die sichtbare Box rendert und /Rotate anwendet, genau wie die Erkennungs-Features. Lesen Sie die Box mit GetLoadedPageVisibleBox, normalisieren Sie die Rotation so wie HotPDF es tut, und mappen Sie zwei gegenüberliegende Ecken jeder Pixelbox. Die Y-Achse kippt, und bei 90 und 270 Grad tauschen die Achsen, die gemappten Ecken kommen also in keiner festen Reihenfolge heraus; nehmen Sie das Minimum und Maximum der gemappten Punkte, so baut auch HotPDF seine Barcode-Grenzen
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); // Ihr Code, 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;
Das Rotationsverhalten ist tiefer behandelt in Seitenrotation glätten, ohne die Seitenboxen zu brechen, und die Barcode-Decoding-Pipeline, die dieselbe Transformation konsumiert, in Rotierte QR-Codes aus PDF-Seiten dekodieren. Wenn Ihre Engine einen externen Recognizer einwickelt, zeigt der Tesseract-OCR-Adapter für durchsuchbare PDFs die Prozessisolation und Abbruchseite derselben Schnittstelle
Kurzreferenz: CropBox-sicheres Koordinaten-Mapping
- Der Renderer rastert die sichtbare Box, die auf die MediaBox zugeschnittene CropBox (ISO 32000-1 §14.11.2); jedes Pixel-zu-Seite-Mapping muss diese Box nutzen, gelesen mit
GetLoadedPageVisibleBox - HotPDF v2.770.153 fixte
ApplyLoadedOCRTextLayer; v2.770.154 fixte ganzseitigesDecodeLoadedPageBarcodesund Face-Findings ausDetectLoadedRedactionFindings; Builds von v2.766.64 bis zu diesen Versionen sind betroffen THPDFOCRWord-Boxen sind Bitmap-Pixel mit Ursprung oben links;GetLoadedPageBoxundGetLoadedPageVisibleBoxliefern PDF-User Space mit Ursprung unten links undBottom < Top- Der Maßstab ist
DPI / 72; leiten Sie ihn aus der DPI ab, nie aus einer in die Bitmap-Breite geteilten Seitenbox - /Rotate entscheidet, welche Kanten zählen: Left und Bottom bei 0 und 90, Right und Bottom bei 180, Right und Top bei 270
- Liefern Sie OCR-Wörter in Pixeln und lassen Sie HotPDF sie mappen; konvertieren Sie nur für Ihre eigene Filterlogik
- Fahren Sie OCR auf beschnittenen Seiten erneut, die ein betroffener Build verarbeitet hat, und denken Sie daran, dass
SkipPagesWithTextSeiten überspringt, die die alte Ebene bereits tragen
Die hier genutzten Erkennungs-Features, Seitenbox-Abfragen und das Rendern geladener Dokumente kommen alle in der HotPDF-Komponente für Delphi und C++Builder; Lizenzierung, Testdownloads und die vollständige Feature-Liste finden Sie auf der HotPDF Delphi PDF component page