Les couches de texte OCR, les bornes de codes-barres et les boîtes de suppression de visages dérivent sur les pages PDF rognées quand les pixels du bitmap sont remappés à travers le MediaBox au lieu de la box que le moteur de rendu a réellement rastérisée : le CropBox écrêté par le MediaBox (ISO 32000-1 §14.11.2). HotPDF a corrigé ça pour ApplyLoadedOCRTextLayer en v2.770.153, et pour DecodeLoadedPageBarcodes et DetectLoadedRedactionFindings en v2.770.154
Le rapport de bug qui arrive d’ordinaire ressemble à ça. Une archive de contrats scannés passe à l’OCR, la sortie est cherchable, et le résultat de recherche d’un numéro d’article est surligné un demi-pouce plus bas et à gauche du numéro imprimé. La plupart des fichiers du lot sont bons. Les cassés viennent tous d’une station de scan qui écrit un /CropBox pour rogner la marge de la vitre. Ce seul détail sépare l’image que le moteur OCR a vue du cadre dans lequel la couche de texte a été posée, et le même désaccord déplace les bornes de codes-barres et, plus grave, les boîtes de suppression de visages
Pourquoi la couche de texte OCR dérive-t-elle loin des mots scannés ?
La couche de texte dérive parce que deux moitiés du pipeline étaient en désaccord sur le rectangle que couvre le bitmap. En v2.766.64, HotPDF a modifié le rendu, l’export SVG, la visionneuse et l’impression pour honorer le CropBox : une page est affichée à travers son CropBox écrêté par son MediaBox, ce que prescrit ISO 32000-1 §14.11.2, et GetLoadedPageVisibleBox a été ajoutée pour renvoyer cette box visible. Les fonctions de reconnaissance continuaient de bâtir leur transformation dispositif-vers-page depuis GetLoadedPageBox(PageIndex, pbMediaBox, ...). Le raster couvrait désormais la box visible, la transformation supposait toujours le MediaBox, et chaque position reconnue revenait décalée de l’écart entre les deux
La fenêtre touchée est donc précise. ApplyLoadedOCRTextLayer plaçait mal le texte de v2.766.64 à v2.770.152. Le DecodeLoadedPageBarcodes page entière et la détection de visages dans DetectLoadedRedactionFindings sont restés faux un build de plus, jusqu’à v2.770.153. Avant v2.766.64, le moteur de rendu dessinait tout le MediaBox, donc le remappage et le raster étaient d’accord, au prix de reconnaître du contenu que les visionneuses n’affichent jamais. Les corrections ont changé trois choses ensemble pour chaque fonction : la transformation, l’estimation du budget de pixels, et la box de page remise à un moteur personnalisé dans le record de requête
Plusieurs cas n’ont jamais été touchés :
- Les pages sans
/CropBox, ou dont le CropBox égale le MediaBox, se remappent à l’identique avant et après la correction DecodeLoadedPageBarcodesavecHasRegiondéfini rend exactement la région que vous passez et remappe à travers cette même région, donc le décodage à région explicite a toujours été correct ; le contrôle que la région se trouve dans la page utilise toujours le MediaBox- Les résultats de suppression par motifs (adresses e-mail, numéros de carte et ainsi de suite) viennent de l’extraction de texte en espace utilisateur, pas d’un raster, donc seuls les résultats de détection de visages ont bougé
Trois repères de coordonnées, et quelles API HotPDF utilisent chacun
Le code HotPDF qui touche à la reconnaissance manipule trois repères, et la plupart des bugs de cartographie viennent d’en mélanger deux
- Pixels du bitmap : origine en haut à gauche, Y croît vers le bas, les unités sont des pixels au DPI de la requête.
THPDFOCRWord.Left,Top,RightetBottomsont dans ce repère, tout comme les points de baseline optionnels, les résultats qu’unIHPDFBarcodeDecoderpersonnalisé renvoie, et les boîtes d’unIHPDFFaceDetectorpersonnalisé - Espace utilisateur PDF d’une page chargée : origine en bas à gauche, Y croît vers le haut, les unités sont des points, avec
Bottom < Top.GetLoadedPageBoxetGetLoadedPageVisibleBoxrenvoient Left, Bottom, Right, Top dans ce repère, tout comme les champsPageLeft,PageBottom,PageRightetPageTopdeTHPDFOCRRequest, les bornes dansTHPDFDecodedBarcodeet les rectangles dansTHPDFRedactionFinding - Coordonnées de dessin de page HotPDF : l’API que vous employez pour construire de nouvelles pages (sortie de texte, formes, codes-barres, liens, champs de formulaire) fonctionne avec une origine en haut à gauche et Y croissant vers le bas. Ce repère appartient à la génération de documents et n’a rien à voir avec les API de document chargé ci-dessus, donc ne lui donnez jamais tel quel un rectangle en espace utilisateur de page chargée
Le record de mot OCR est délibérément en pixels : un moteur rapporte ce qu’il a vu dans l’image, et ApplyLoadedOCRTextLayer possède la conversion. Cette division ne marche que quand la conversion utilise la bonne box, ce que v2.770.153 a rétabli
La transformation dispositif-vers-page derrière OCR, codes-barres et visages
HotPDF remappe les pixels du bitmap vers la page avec une seule matrice affine bâtie sur cinq entrées : la rotation, l’échelle DPI / 72, la hauteur du bitmap, et le Left, Bottom, Right et Top de la box rendue. OCR, décodage de codes-barres et détection de visages partagent une seule routine pour ça, voilà pourquoi une seule entrée de box fausse a cassé les trois de la même façon. Pour une page non tournée, la matrice page-vers-dispositif [A B C D E F] est :
A = ScaleetD = -Scale, oùScale = DPI / 72; leDnégatif retourne l’espace utilisateur (Y vers le haut) en espace bitmap (Y vers le bas)B = C = 0, parce qu’une page non tournée n’a ni cisaillement ni échange entre axesE = -Left * Scale, qui amène le bord gauche de la box à la colonne de pixels 0F = BitmapHeight + Bottom * Scale, qui remappe le bord bas de la box à y = BitmapHeight, le bord bas du bitmap, si bien que le bord haut atterrit sur la ligne 0
Les pixels reviennent vers la page par l’inverse de cette matrice. Request.PageRotation porte le /Rotate de la page normalisé à 0, 90, 180 ou 270 (toute valeur qui n’est pas un multiple de 90 est traitée comme 0), et le moteur de rendu tourne la page dans le sens horaire comme l’exige ISO 32000-1 §7.7.3.3. Sous rotation, les axes s’échangent et une autre paire de bords de box est arrimée à l’origine du bitmap. Écrites comme formules inverses, avec S = DPI / 72, x et y en pixels et H la hauteur du bitmap :
| /Rotate | Page X | Page Y | Bords de box dont dépend le remappage |
|---|---|---|---|
| 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 |
La dernière colonne explique pourquoi le bug semblait aléatoire en production. Un CropBox qui ne rogne que le haut de la page laisse Left et Bottom intacts, si bien que les pages droites sortaient parfaitement et que seules les pages portant /Rotate 270 dérivaient. La rotation échange aussi les dimensions du bitmap : à 90 et 270, le bitmap fait (Top - Bottom) * S pixels de large et (Right - Left) * S pixels de haut
Qu’est-ce qui cloche avec MediaBox [0 0 612 792] et CropBox [36 36 576 756] ?
Avec un rogne d’un demi-pouce sur chaque côté, la couche de texte d’une page non tournée atterrit exactement 36 points à gauche et 36 points en dessous des mots scannés quand on utilise le MediaBox. Prenons une page US Letter dont le CropBox rogne 36 points (0,5 pouce) sur chaque bord. La box visible fait 540 par 720 points, donc à la résolution OCR par défaut de 300 DPI, l’échelle vaut 300 / 72 ≈ 4,1667 et le bitmap fait 2250 par 3000 pixels
Supposons que le moteur rapporte un mot avec la boîte en pixels Left 450, Top 600, Right 900, Bottom 660 et pas de baseline. HotPDF place alors la baseline 20 pour cent de la hauteur du mot au-dessus du bord bas, à la ligne de pixels 648, et remappe le point de départ (450, 648) :
- Par la box visible : x = 36 + 450 / 4.1667 = 144.0 et y = 36 + (3000 - 648) / 4.1667 = 600.48, ce qui est l’endroit où le mot est imprimé
- Par le MediaBox : x = 0 + 108.0 = 108.0 et y = 0 + 564.48 = 564.48, un décalage uniforme de (-36, -36) points
Tournez la même page et la direction de l’erreur change, parce que ce sont d’autres bords qui entrent en jeu. À /Rotate 180, le terme X utilise Right, et 612 au lieu de 576 pousse la couche 36 points à droite pendant que Bottom la tire toujours 36 points vers le bas. À /Rotate 270, Right et Top sont tous deux trop grands, donc la couche bouge de 36 points à droite et 36 points vers le haut. Un document aux orientations mixtes peut montrer la dérive dans trois directions, une empreinte fiable pour ce bug. Du code écrit à la main qui déduit l’échelle de la box, comme Bitmap.Width / (Right - Left), étire aussi chaque coordonnée de 612 / 540, environ 13 pour cent, par-dessus le décalage
Quels documents PDF sont touchés chez vous ?
Un document PDF est exposé dès qu’au moins une page a une box visible qui diffère de son MediaBox, et HotPDF peut vous le dire en quelques lignes. Comparez GetLoadedPageBox avec pbMediaBox contre GetLoadedPageVisibleBox pour chaque page, et imprimez GetLoadedPageRotation à côté pour prédire la direction de dérive depuis la table ci-dessus. THPDFPageBoundary propose aussi pbCropBox, pbBleedBox, pbTrimBox et pbArtBox, mais GetLoadedPageBox(pbCropBox) retombe sur le MediaBox quand aucune crop box n’existe et n’écrête pas, donc la box visible est la bonne chose à comparer
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;
// le tableau stocké peut lister ses coins dans n’importe quel ordre
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// déjà normalisée et écrêtée par le 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;
Deux détails de GetLoadedPageVisibleBox comptent pour des scripts comme celui-ci. La fonction laisse ses paramètres out intacts quand elle échoue, donc prérégler une taille de page par défaut avant l’appel est un motif sûr. Et quand un CropBox mal formé n’intersecte pas du tout le MediaBox, la fonction renvoie le MediaBox plutôt qu’un rectangle vide. Si le rapport liste des pages et que votre build déployé est plus ancien que v2.770.153 pour l’OCR, ou v2.770.154 pour les codes-barres et visages, relancez la reconnaissance sur ces pages après la mise à jour. Une couche OCR commitée par un build touché reste dans le fichier sauvegardé, et l’option SkipPagesWithText par défaut sautera ces pages à une seconde passe à moins que vous la désactiviez ou retiriez d’abord l’ancienne couche
Comment un IHPDFOCREngine personnalisé doit-il remapper les pixels vers l’espace PDF ?
Un IHPDFOCREngine personnalisé doit renvoyer des boîtes de mots en pixels du bitmap et laisser HotPDF faire le remappage ; convertissez vers l’espace utilisateur seulement pour vos propres décisions, et alors utilisez la box de la requête, jamais le MediaBox. Depuis v2.770.153, les PageLeft, PageBottom, PageRight et PageTop de la requête décrivent la box visible rendue, si bien qu’ils correspondent exactement à Request.Bitmap. Le helper ci-dessous est l’inverse de la transformation de la bibliothèque, y compris son usage de la hauteur réelle du bitmap pour les pages droites, donc il s’accorde avec HotPDF au pixel près
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Pixel du bitmap (origine haut gauche, Y vers le bas) vers l’espace
// utilisateur PDF (origine bas gauche, Y vers le haut), par la box de rendu du bitmap
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;
Une raison réaliste de vouloir l’espace utilisateur à l’intérieur d’un moteur est une règle de zone : des factures dont l’en-tête ne doit jamais devenir cherchable, ou une zone de tampon qui embrouille le reconnaisseur. Le moteur ci-dessous, écrit avec TInterfacedObject pour que le comptage de références gère sa durée de vie, filtre les mots selon où leurs centres tombent sur la page, puis renvoie les survivants intacts en coordonnées pixels. RunRecognizer tient lieu de votre propre appel de reconnaissance
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // espace utilisateur
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // votre reconnaisseur, boîtes en pixels
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]; // toujours des pixels : HotPDF les remappe lui-même
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Remettez le moteur à ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) comme avec n’importe quel autre moteur. La bibliothèque valide ce qui revient avant de s’y fier : un mot est abandonné et compté dans Info.DroppedWordCount quand sa boîte sort du bitmap, quand Right <= Left ou Bottom <= Top, ou quand Confidence est hors de 0..1 ou en dessous de MinimumConfidence. Renvoyer plus de mots que MaxWordsPerPage, ou pousser le total courant au-delà de MaxTotalWords, fait échouer tout l’appel avec une erreur de budget, donc honorez Request.MaxWords dans le moteur. Ne convertissez pas les boîtes de mots vers l’espace utilisateur avant de les renvoyer ; HotPDF traiterait les valeurs en points comme des pixels et la couche s’effondrerait vers l’origine du bitmap
Cartographier la sortie de votre propre détecteur
Le même helper sert un pipeline maison bâti sur RenderLoadedPageToBitmap, qui rend la box visible et applique /Rotate exactement comme les fonctions de reconnaissance. Lisez la box avec GetLoadedPageVisibleBox, normalisez la rotation comme le fait HotPDF, et remappez deux coins opposés de chaque boîte en pixels. L’axe Y se retourne et, à 90 et 270 degrés, les axes s’échangent, si bien que les coins remappés sortent sans ordre fixe ; prenez le minimum et le maximum des points remappés, ce qui est aussi la façon dont HotPDF bâtit les bornes de codes-barres
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); // votre code, boîte en pixels
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;
Le comportement de rotation est approfondi dans l’aplatissement de la rotation de page sans casser les boxes de page, et le pipeline de décodage de codes-barres qui consomme la même transformation dans le décodage des QR codes tournés depuis des pages PDF. Si votre moteur encapsule un reconnaisseur externe, l’adaptateur OCR Tesseract pour PDF cherchable montre le côté isolation de processus et annulation de la même interface
Aide-mémoire : cartographie des coordonnées sûre vis-à-vis du CropBox
- Le moteur de rendu rastérise la box visible, le CropBox écrêté par le MediaBox (ISO 32000-1 §14.11.2) ; tout remappage pixel-vers-page doit utiliser cette box, lue avec
GetLoadedPageVisibleBox - HotPDF v2.770.153 a corrigé
ApplyLoadedOCRTextLayer; v2.770.154 a corrigé leDecodeLoadedPageBarcodespage entière et les résultats de visages deDetectLoadedRedactionFindings; les builds de v2.766.64 à ces versions sont touchés - Les boîtes
THPDFOCRWordsont en pixels du bitmap à origine haut gauche ;GetLoadedPageBoxetGetLoadedPageVisibleBoxrenvoient l’espace utilisateur PDF à origine bas gauche avecBottom < Top - L’échelle vaut
DPI / 72; déduisez-la du DPI, jamais d’une box de page divisée dans la largeur du bitmap - /Rotate décide des bords qui comptent : Left et Bottom à 0 et 90, Right et Bottom à 180, Right et Top à 270
- Renvoyez les mots OCR en pixels et laissez HotPDF les remapper ; ne convertissez que pour votre propre logique de filtrage
- Relancez l’OCR sur les pages rognées traitées par un build touché, et retenez que
SkipPagesWithTextsaute les pages qui portent déjà l’ancienne couche
Les fonctions de reconnaissance, les requêtes de boxes de page et le rendu de document chargé utilisés ici se livrent tous dans le composant HotPDF pour Delphi et C++Builder ; licences, téléchargements d’essai et liste complète des fonctionnalités sont sur la page du composant HotPDF Delphi PDF