Las capas de texto OCR, los límites de códigos de barras y las cajas de redacción de rostros se desvían en páginas PDF recortadas cuando los píxeles del bitmap se mapean de vuelta por el MediaBox en vez de por la caja que el renderer de verdad rasterizó: el CropBox recortado al MediaBox (ISO 32000-1 §14.11.2). HotPDF lo corrigió para ApplyLoadedOCRTextLayer en v2.770.153, y para DecodeLoadedPageBarcodes y DetectLoadedRedactionFindings en v2.770.154
El reporte de bug que suele llegar se ve así. Un archivo de contratos escaneados pasa por OCR, la salida es buscable, y el hit de búsqueda de un número de cláusula se resalta media pulgada más abajo y a la izquierda del número impreso. La mayoría de los archivos del lote están bien. Los rotos salieron todos de una estación de escaneo que escribe un /CropBox para recortar el margen de la platina. Ese detalle separa la imagen que vio el engine de OCR del marco donde se colocó la capa de texto, y el mismo desajuste mueve los límites de códigos de barras y, más grave, las cajas de redacción de rostros
¿Por qué la capa de texto OCR se despega de las palabras escaneadas?
La capa de texto se despega porque dos mitades del pipeline discrepan sobre qué rectángulo cubre el bitmap. En v2.766.64, HotPDF cambió el renderizado, el export SVG, el visor y la impresión para honrar el CropBox: una página se muestra a través de su CropBox recortado a su MediaBox, que es lo que prescribe ISO 32000-1 §14.11.2, y se agregó GetLoadedPageVisibleBox para devolver esa caja visible. Las funciones de reconocimiento seguían armando su transformada device-to-page desde GetLoadedPageBox(PageIndex, pbMediaBox, ...). El raster ahora cubría la caja visible, la transformada todavía asumía el MediaBox, y cada posición reconocida volvía desplazada por la brecha entre las dos
La ventana afectada es por eso precisa. ApplyLoadedOCRTextLayer colocó texto mal desde v2.766.64 hasta v2.770.152. El DecodeLoadedPageBarcodes de página completa y la detección de rostros dentro de DetectLoadedRedactionFindings siguieron rotos un build más, hasta v2.770.153. Antes de v2.766.64 el renderer dibujaba el MediaBox entero, así que mapeo y raster concordaban, al costo de reconocer contenido que los visores nunca muestran. Las correcciones cambiaron tres cosas juntas para cada función: la transformada, la estimación del presupuesto de píxeles, y la caja de página entregada a un engine custom en el record del request
Varios casos jamás se vieron afectados:
- Páginas sin
/CropBox, o cuyo CropBox iguala al MediaBox, se mapean idéntico antes y después de la corrección DecodeLoadedPageBarcodesconHasRegionactivo renderiza exactamente la región que usted pasa y mapea por esa misma región, así que el decodificado por región explícita estuvo correcto todo el tiempo; el chequeo de que la región quede dentro de la página sigue usando el MediaBox- Los findings de redacción por patrones (correos, números de tarjeta y demás) salen de extracción de texto en user space, no de un raster, así que solo los findings de detección de rostros se movieron
Tres marcos de coordenadas, y qué APIs de HotPDF usan cada uno
El código de HotPDF que toca reconocimiento lidiará con tres marcos, y la mayoría de los bugs de mapeo salen de mezclar dos de ellos
- Píxeles de bitmap: origen arriba a la izquierda, Y crece hacia abajo, las unidades son píxeles al DPI del request.
THPDFOCRWord.Left,Top,RightyBottomestán en este marco, igual que los puntos de baseline opcionales, los resultados que devuelve unIHPDFBarcodeDecodercustom, y las cajas de unIHPDFFaceDetectorcustom - User space de PDF de una página cargada: origen abajo a la izquierda, Y crece hacia arriba, las unidades son puntos, con
Bottom < Top.GetLoadedPageBoxyGetLoadedPageVisibleBoxdevuelven Left, Bottom, Right, Top en este marco, y también los camposPageLeft,PageBottom,PageRightyPageTopdeTHPDFOCRRequest, los límites enTHPDFDecodedBarcodey los rectángulos enTHPDFRedactionFinding - Coordenadas de dibujo de página de HotPDF: la API con la que usted arma páginas nuevas (salida de texto, formas, códigos de barras, links, form fields) trabaja con origen arriba a la izquierda y Y creciendo hacia abajo. Ese marco pertenece a la generación de documentos y no tiene nada que ver con las APIs de documentos cargados de arriba, así que jamás le meta sin cambios un rectángulo user space de página cargada
El record de palabra OCR es deliberadamente basado en píxeles: un engine reporta lo que vio en la imagen, y ApplyLoadedOCRTextLayer es dueño de la conversión. Esa división solo funciona cuando la conversión usa la caja correcta, que es lo que v2.770.153 restauró
La transformada device-to-page detrás de OCR, códigos de barras y rostros
HotPDF mapea píxeles de bitmap a la página con una única matriz afín armada de cinco inputs: la rotación, la escala DPI / 72, la altura del bitmap, y el Left, Bottom, Right y Top de la caja renderizada. OCR, decodificado de códigos de barras y detección de rostros comparten una sola rutina para esto, razón por la cual un input de caja equivocado rompió a los tres igual. Para una página sin rotar, la matriz page-to-device [A B C D E F] es:
A = ScaleyD = -Scale, dondeScale = DPI / 72; laDnegativa voltea el user space (Y arriba) a espacio de bitmap (Y abajo)B = C = 0, porque una página sin rotar no tiene shear ni intercambio entre ejesE = -Left * Scale, que mueve el borde izquierdo de la caja a la columna de píxel 0F = BitmapHeight + Bottom * Scale, que mapea el borde inferior de la caja a y = BitmapHeight, el borde inferior del bitmap, así el borde superior aterriza en la fila 0
Los píxeles vuelven a la página por la inversa de esa matriz. Request.PageRotation lleva el /Rotate de la página normalizado a 0, 90, 180 o 270 (cualquier valor que no sea múltiplo de 90 se trata como 0), y el renderer gira la página en sentido horario como exige ISO 32000-1 §7.7.3.3. Bajo rotación los ejes se intercambian y un par distinto de bordes de la caja queda anclado al origen del bitmap. Escritas como fórmulas inversas, con S = DPI / 72, x e y en píxeles y H la altura del bitmap:
| /Rotate | X de página | Y de página | Bordes de caja de los que depende el mapeo |
|---|---|---|---|
| 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 última columna explica por qué el bug parecía aleatorio en producción. Un CropBox que recorta solo el tope de la página deja Left y Bottom intactos, así que las páginas derechas salían perfectas y solo las páginas con /Rotate 270 se desviaban. La rotación además intercambia las dimensiones del bitmap: a 90 y 270 el bitmap es de (Top - Bottom) * S píxeles de ancho y (Right - Left) * S píxeles de alto
¿Qué sale mal con MediaBox [0 0 612 792] y CropBox [36 36 576 756]?
Con un recorte de media pulgada por cada lado, la capa de texto de una página sin rotar aterriza exactamente 36 puntos a la izquierda y 36 puntos abajo de las palabras escaneadas cuando se usa el MediaBox. Tome una página US Letter cuyo CropBox recorta 36 puntos (0.5 pulgada) de cada borde. La caja visible es de 540 por 720 puntos, así que a la resolución OCR por defecto de 300 DPI la escala es 300 / 72 ≈ 4.1667 y el bitmap es de 2250 por 3000 píxeles
Suponga que el engine reporta una palabra con caja de píxeles Left 450, Top 600, Right 900, Bottom 660 y sin baseline. HotPDF coloca entonces la baseline un 20 por ciento de la altura de la palabra sobre el borde inferior, en la fila de píxel 648, y mapea el punto de arranque (450, 648):
- Por la caja visible: x = 36 + 450 / 4.1667 = 144.0 y y = 36 + (3000 - 648) / 4.1667 = 600.48, que es donde la palabra está impresa
- Por el MediaBox: x = 0 + 108.0 = 108.0 y y = 0 + 564.48 = 564.48, un desplazamiento uniforme de (-36, -36) puntos
Rothe la misma página y la dirección del error cambia, porque se involucran bordes distintos. A /Rotate 180 el término X usa Right, y 612 en vez de 576 empuja la capa 36 puntos a la derecha mientras Bottom todavía la jala 36 puntos abajo. A /Rotate 270 tanto Right como Top son demasiado grandes, así que la capa se mueve 36 puntos a la derecha y 36 puntos arriba. Un documento con orientaciones mezcladas puede mostrar la deriva en tres direcciones, una huella confiable de este bug. Código escrito a mano que derive la escala desde la caja, como Bitmap.Width / (Right - Left), además estira cada coordenada por 612 / 540, cerca del 13 por ciento, encima del desplazamiento
¿Cuáles de sus documentos PDF están expuestos?
Un documento PDF está expuesto cuando al menos una página tiene una caja visible que difiere de su MediaBox, y HotPDF puede decírselo en pocas líneas. Compare GetLoadedPageBox con pbMediaBox contra GetLoadedPageVisibleBox para cada página, e imprima GetLoadedPageRotation al lado así puede predecir la dirección de la deriva desde la tabla de arriba. THPDFPageBoundary además ofrece pbCropBox, pbBleedBox, pbTrimBox y pbArtBox, pero GetLoadedPageBox(pbCropBox) recurre al MediaBox cuando no existe crop box y no recorta, así que la caja visible es lo correcto contra lo que comparar
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;
// el array guardado puede listar sus esquinas en cualquier orden
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// ya normalizado y recortado al 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;
Dos detalles de GetLoadedPageVisibleBox importan para scripts como este. La función deja sus parámetros out intactos cuando falla, así que preasignar un tamaño de página por defecto antes de la llamada es un patrón seguro. Y cuando un CropBox mal formado no intersecta para nada al MediaBox, la función devuelve el MediaBox en vez de un rectángulo vacío. Si el reporte lista páginas y su build desplegado es anterior a v2.770.153 para OCR, o a v2.770.154 para códigos de barras y rostros, vuelva a correr el reconocimiento sobre esas páginas después de actualizar. Una capa OCR confirmada por un build afectado queda en el archivo guardado, y la opción SkipPagesWithText por defecto se saltará esas páginas en una segunda pasada salvo que la apague o elimine la capa vieja primero
¿Cómo debería mapear píxeles de vuelta a espacio PDF un IHPDFOCREngine custom?
Un IHPDFOCREngine custom debería devolver cajas de palabras en píxeles de bitmap y dejar que HotPDF haga el mapeo; convierta a user space solo para sus propias decisiones, y entonces use la caja del request, jamás el MediaBox. Desde v2.770.153 el PageLeft, PageBottom, PageRight y PageTop del request describen la caja visible renderizada, así que coinciden exactamente con Request.Bitmap. El helper de abajo es la inversa de la transformada de la librería, incluido su uso de la altura real del bitmap para páginas derechas, así que concuerda con HotPDF al píxel
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Píxel de bitmap (origen arriba a la izquierda, Y hacia abajo) a user space
// de PDF (origen abajo a la izquierda, Y hacia arriba), por la caja renderizada
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;
Una razón realista para necesitar user space dentro de un engine es una regla de zonas: facturas cuyo membrete usted jamás quiere buscable, o un área de sello que confunde al reconocedor. El engine de abajo, escrito con TInterfacedObject para que el reference counting maneje su vida, filtra palabras por dónde caen sus centros en la página, y devuelve los sobrevivientes intactos en coordenadas de píxel. RunRecognizer hace de su propia llamada al reconocedor
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // su reconocedor, cajas en píxeles
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]; // siguen siendo píxeles: HotPDF los mapea él mismo
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Entréguele el engine a ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) como a cualquier otro engine. La librería valida lo que vuelve antes de confiar en ello: una palabra se descarta y cuenta en Info.DroppedWordCount cuando su caja sale del bitmap, cuando Right <= Left o Bottom <= Top, o cuando Confidence está fuera de 0..1 o bajo MinimumConfidence. Devolver más palabras que MaxWordsPerPage, o empujar el total acumulado más allá de MaxTotalWords, hace fallar la llamada completa con un error de presupuesto, así que honre el Request.MaxWords en el engine. No convierta las cajas de palabras a user space antes de devolverlas; HotPDF trataría los valores de punto como píxeles y la capa colapsaría hacia el origen del bitmap
Mapear la salida de su propio detector
El mismo helper sirve a un pipeline propio armado sobre RenderLoadedPageToBitmap, que renderiza la caja visible y aplica el /Rotate tal como lo hacen las funciones de reconocimiento. Lea la caja con GetLoadedPageVisibleBox, normalice la rotación igual que HotPDF, y mapee dos esquinas opuestas de cada caja de píxeles. El eje Y se voltea y, a 90 y 270 grados, los ejes se intercambian, así que las esquinas mapeadas salen en ningún orden fijo; tome el mínimo y el máximo de los puntos mapeados, que también es cómo HotPDF arma los límites de los códigos de barras
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); // su código, caja en píxeles
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;
El comportamiento de rotación queda cubierto con más profundidad en aplanar la rotación de página sin romper las cajas de página, y el pipeline de decodificado de códigos de barras que consume la misma transformada en decodificar códigos QR rotados desde páginas PDF. Si su engine envuelve un reconocedor externo, el adapter OCR de Tesseract para PDF buscable muestra el lado de aislamiento de proceso y cancelación de la misma interfaz
Referencia rápida: mapeo de coordenadas seguro ante CropBox
- El renderer rasteriza la caja visible, el CropBox recortado al MediaBox (ISO 32000-1 §14.11.2); todo mapeo píxel-a-página debe usar esa caja, leída con
GetLoadedPageVisibleBox - HotPDF v2.770.153 corrigió
ApplyLoadedOCRTextLayer; v2.770.154 corrigió elDecodeLoadedPageBarcodesde página completa y los findings de rostros deDetectLoadedRedactionFindings; los builds desde v2.766.64 hasta esas versiones están afectados - Las cajas de
THPDFOCRWordson píxeles de bitmap con origen arriba a la izquierda;GetLoadedPageBoxyGetLoadedPageVisibleBoxdevuelven user space de PDF con origen abajo a la izquierda yBottom < Top - La escala es
DPI / 72; derívela del DPI, jamás de una caja de página dividida entre el ancho del bitmap - /Rotate decide qué bordes importan: Left y Bottom a 0 y 90, Right y Bottom a 180, Right y Top a 270
- Devuelva las palabras OCR en píxeles y deje que HotPDF las mapee; convierta solo para su propia lógica de filtrado
- Vuelva a correr el OCR en las páginas recortadas procesadas por un build afectado, y recuerde que
SkipPagesWithTextse salta las páginas que ya traen la capa vieja
Las funciones de reconocimiento, las consultas de cajas de página y el renderizado de documentos cargados usados aquí vienen todos en el componente HotPDF para Delphi y C++Builder; licencias, descargas de prueba y la lista completa de funciones están en la página del componente HotPDF Delphi PDF