HotPDF decodifica símbolos QR rotados en una página PDF cargada normalizando la matriz de módulos muestreada a través de las ocho orientaciones D4 dentro del propio decoder. El reintento por rotación externa que funciona para simbologías lineales no puede funcionar para QR, y entender por qué le ahorra un día persiguiendo un decoder que parece roto pero no lo está
El escenario es de lo más común. Notas de entrega escaneadas llegan como PDFs, cada página carga una etiqueta QR, y el operador del escáner metió una pila de hojas en la dirección que la bandeja aceptó. Algunas etiquetas están derechas, otras tienen un cuarto de giro, y unas cuantas están patas arriba. Usted llama al decoder de barcodes, la mitad de las páginas se resuelve, y la otra mitad vuelve vacía sin error alguno
¿Por qué rotar la máscara de escaneo nunca arregla un QR rotado?
Porque el layout del finder pattern de un QR es deliberadamente asimétrico, y una rotación de la imagen completa preserva esa asimetría en lugar de eliminarla. QR Code coloca tres cuadrados finder en las esquinas superior izquierda, superior derecha e inferior izquierda, y deja vacía la esquina inferior derecha (ISO/IEC 18004:2015 §6.3.3). Esa esquina faltante es la pista de orientación. Rote el bitmap de la página noventa grados y el hueco simplemente se muda a otra esquina. No existe una rotación no trivial del plano que mapee un layout de tres esquinas sobre sí mismo, así que un decoder que solo acepta el arreglo canónico rechaza cada intento por turnos
Esto importa porque la solución obvia es la equivocada. El instinto natural es colgar el reintento por fuera: renderizar la página, entregarle la máscara al decoder, y si eso falla, rotar la máscara y reintentar a 90, 180 y 270 grados. Para Code 39 esa política es exactamente correcta, porque una simbología lineal tiene un patrón de start y stop que el escáner encuentra en cuanto las barras corren horizontales. Para QR son cuatro fallas garantizadas seguidas de un reporte de nada encontrado
El grupo D4, aplicado a la matriz de módulos
El lugar correcto para la normalización es después del muestreo, sobre la grilla booleana de módulos y no sobre la máscara de píxeles. Una vez que el decoder resolvió el símbolo en una matriz de n por n de módulos oscuros y claros, puede enumerar el grupo dihedral del cuadrado: cuatro rotaciones por dos reflexiones, ocho orientaciones candidatas en total. Para cada candidata revisa el triángulo de finders, y la primera candidata cuyos tres finders caen en las posiciones superior izquierda, superior derecha e inferior izquierda es la orientación verdadera. Desde ahí el pipeline existente corre sin cambios, porque los bits de format information, la colocación zigzag de datos y la corrección Reed-Solomon todos asumen una matriz canónica y ahora reciben una
Dos propiedades lo vuelven barato. La matriz es pequeña comparada con el bitmap renderizado, así que ocho transpuestas cuestan mucho menos que ocho renderizados de página. Y la matriz es un arreglo booleano limpio construido por el muestreador, así que ninguna transformación en el camino puede introducir valores que nunca se muestrearon
La detección de versión es una búsqueda de divisibilidad, no una división
El conteo de módulos no se puede derivar dividiendo el ancho muestreado entre un tamaño de módulo supuesto, y equivocarse aquí es una fuente sutil de fallas de decodificación en renderizados de alta resolución. Un símbolo QR de versión v mide 4v + 17 módulos de lado, así que la versión 1 son 21 módulos y la versión 40 son 177. Una máscara que mide 126 píxeles de ancho es igualmente consistente con la versión 1 a seis píxeles por módulo y con varias versiones superiores a tamaños de módulo menores. La división lineal elige una y normalmente se equivoca
Lo que funciona es una búsqueda de divisibilidad sobre las versiones candidatas. Recorra desde la versión 40 hasta la 1, conserve las candidatas cuyo conteo de módulos divide el ancho muestreado de forma exacta y deja al menos tres píxeles por módulo, y tome la versión sobreviviente más pequeña. El piso de tres píxeles es lo que impide que la búsqueda acepte una lectura absurdamente densa de un símbolo grueso, y la regla de la versión más pequeña resuelve la ambigüedad restante a favor de la lectura que un escáner realmente produciría
var
Pdf: THotPDF;
Options: THPDFBarcodeDecodeOptions;
Codes: THPDFDecodedBarcodes;
Info: THPDFBarcodeDecodeInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('delivery-notes.pdf');
Options := THPDFBarcodeDecodeOptions.Default;
Options.DPI := 300;
Options.RotationPolicy := bdrpFallback;
Options.MinimumConfidence := 0.5;
Options.MaxResults := 16;
if Pdf.DecodeLoadedPageBarcodes(0, Options, Codes, Info) then
for I := 0 to High(Codes) do
if Codes[I].Symbology = bsyQRCode then
Writeln(Codes[I].Text, ' at ',
Format('%.0f', [Codes[I].OrientationDegrees]), ' degrees');
finally
Pdf.Free;
end;
end;
THPDFBarcodeDecodeOptions.Default devuelve un record poblado en lugar de uno en ceros, lo que importa porque un DPI de cero o un tope de resultados de cero es una forma con apariencia válida de no recibir nada. RotationPolicy controla solo el reintento externo: bdrpNone renderiza una vez, bdrpFallback reintenta las otras orientaciones después de una primera pasada fallida, y bdrpAll renderiza todas las orientaciones incondicionalmente. Como la normalización QR ocurre dentro del decoder, las páginas QR se resuelven al primer intento bajo cualquiera de las tres políticas. La política existe para las simbologías lineales que genuinamente la necesitan
¿Cómo demuestra que una transformación de bitmap no inventa píxeles?
Cuente la tinta en ambos lados y exija que los totales coincidan. Una rotación es una permutación de píxeles, nada más, así que la cantidad de celdas no nulas en la salida debe igualar la de la entrada. Cuando una rotación de máscara en la ruta de reintento externo reportó 4800 celdas activas al entrar y 7439 al salir, esa única comparación bastó para condenar a la transformación sin leer una sola línea de su geometría
La causa fue mundana y vale llevársela como regla. Un arreglo dinámico dimensionado con SetLength no tiene garantía de llegar en ceros cuando es el resultado de una función que viaja por una ruta que el runtime no limpia, y las celdas que la rotación nunca escribe cargan entonces los bytes que estaban ahí antes. Algunos de esos bytes viejos son no nulos, y no nulo significa tinta. El arreglo es una línea, FillChar(Result[0], N, 0) antes de que corra el loop de permutación, y la disciplina que implica es más amplia: cualquier función que devuelva una máscara o un buffer de bitmap debería limpiar su salida explícitamente en lugar de confiar en la semántica de asignación
Lo que hizo que el defecto sobreviviera a tres releases es más interesante que el defecto. Cuando QR movió su manejo de orientación al decoder, QR dejó de ejercitar la rotación externa de máscara por completo, y el único consumidor restante de esa ruta de código era Code 39. La infraestructura compartida esconde bugs como este todo el tiempo: la cobertura de una función hace que una ruta parezca testeada mientras la función que realmente depende de ella no tiene la propia. Toda ruta que una función nueva deja de usar necesita un test que la siga usando
Leer los resultados de vuelta en coordenadas de página
Cada valor geométrico que el decoder produce se expresa en el frame de coordenadas del bitmap del intento, y quien llama lo necesita en user space de PDF. Esa conversión corre en dos etapas: deshacer el cuarto de giro que aplicó el reintento, y luego deshacer la transformación de render que mapeó el user space sobre el bitmap. Lo que llega en THPDFDecodedBarcode es un bounding box alineado a ejes en user space, con Left, Bottom, Right y Top siguiendo la convención PDF de que la Y crece hacia arriba, más un OrientationDegrees en sentido antihorario
Equivoque la dirección de esa segunda conversión y el síntoma es feo: el texto decodifica perfecto, pero la caja que usted dibuja para un overlay de revisión aterriza en la imagen espejo de la posición correcta. Quien construya una interfaz de revisión sobre el decoder debería afirmar contra un fixture conocido, con un símbolo colocado deliberadamente cerca de una esquina de la página para que un eje Y volteado se vea de un vistazo. El mismo razonamiento aplica a cualquier coordenada que cruce la frontera de renderizado, que es por lo que vale la pena entender renderizar una página PDF a un bitmap en Delphi antes de construir sobre el decoder
Lo que el decoder incorporado hace y no hace
El decoder incorporado es una implementación acotada y sin dependencias, y es honesta con sus límites en lugar de degradarse en silencio. Reconoce Code 39 y QR, valida los bits de formato protegidos con BCH y el patrón de máscara antes de publicar cualquier dato, y no intenta recuperación de errores sobre símbolos dañados. Si su entrada es una fotografía de una etiqueta curvada bajo luz despareja, esa es otra clase de problema y pide un engine especializado
// Cambie por su propio engine: implemente IHPDFBarcodeDecoder y páseselo
// a la sobrecarga que acepta decoders. HotPDF sigue dueño del render de
// páginas, presupuestos, mapeo de coordenadas y de-duplicación
if not Pdf.DecodeLoadedPageBarcodes(PageIndex, MyDecoder, Options,
Codes, Info) then
case Info.Status of
bdsBudgetExceeded:
Log('raise MaxPixels or lower DPI: ' + string(Info.Diagnostic));
bdsRenderError:
Log('page did not render: ' + string(Info.Diagnostic));
bdsDecoderError:
Log(string(Info.DecoderName) + ' failed: ' + string(Info.Diagnostic));
end;
THPDFBarcodeDecodeInfo es donde un pipeline de producción gana sueldo. RotationAttemptCount y DecoderCallCount le dicen si el reintento externo corrió siquiera, ReceivedResultCount contra AcceptedResultCount separa un decoder que no encontró nada de un umbral de confianza que rechazó todo lo que encontró, y RenderedPixels con PeakWorkingBytes es lo que usted grafica cuando un trabajo por lotes empieza a forcejear. Un set de resultados vacío más bdsSucceeded significa que la página de verdad no tiene símbolo legible, que es un hecho operativo distinto de bdsBudgetExceeded
Los campos de presupuesto merecen una decisión deliberada en lugar de un default. MaxPixels y MaxWorkingBytes existen porque el DPI multiplica cuadráticamente: pasar de 300 a 600 DPI en una página A4 cuadruplica tanto el costo de render como la asignación pico, y una entrada no confiable que declare una caja de página enorme puede convertir un trabajo de escaneo en un incidente de out-of-memory. Fije los topes a lo que su peor documento legítimo necesita, y luego deje que bdsBudgetExceeded enrute los atípicos a una ruta más lenta y aislada
Si sus documentos mezclan etiquetas legibles por máquina con texto impreso que piensa indexar, el decoder de barcodes se empareja de forma natural con el motor de reconocimiento cubierto en OCR por template matching dentro de HotPDF, y el lado de generación de la misma historia está en dibujar barcodes en un PDF con HotPDF. Ambos corren sobre la misma infraestructura de render y presupuestos, así que un pipeline que ya fija límites sensatos para uno consigue el otro casi gratis
La tolerancia a rotación es de esas funciones que son invisibles cuando funcionan e infuriantes cuando no, y la lección de ingeniería se generaliza más allá de QR: normalice lo más cerca posible de la representación semántica, no en la capa de píxeles donde los datos todavía cargan cada accidente de cómo fueron capturados. HotPDF distribuye esto como parte del HotPDF Delphi PDF component, junto con las piezas de renderizado, OCR y análisis de página que los mismos pipelines de entrada suelen necesitar