Artículo técnico

Decodificar códigos QR rotados en páginas PDF con HotPDF

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

Cuatro renderizados de la misma matriz de módulos QR de HotPDF bajo las rotaciones del grupo D4 a 0, 90, 180 y 270 grados, mostrando los tres finder patterns migrando de esquina mientras la esquina vacía se mueve con ellos, de modo que solo la orientación canónica presenta finders en superior izquierda, superior derecha e inferior izquierda al decoder
Rotar la máscara de píxeles no puede eliminar la asimetría de finders del QR, así que HotPDF enumera las orientaciones D4 sobre la matriz de módulos muestreada y conserva la primera candidata cuyos finders caen en superior izquierda, superior derecha e inferior izquierda

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

El recorrido de detección de versión de HotPDF para un símbolo QR sobre una máscara muestreada de 126 píxeles, probando cada conteo de módulos candidato 4v más 17 desde la versión 40 hasta la versión 1 por divisibilidad exacta y un piso de tres píxeles por módulo antes de que gane la versión sobreviviente más pequeña
Un conteo de módulos QR sale de una búsqueda de divisibilidad sobre versiones candidatas, no de dividir el ancho de la máscara entre un tamaño de módulo supuesto, y la versión sobreviviente más pequeña resuelve la ambigüedad
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

El pipeline de barcodes de HotPDF desde el bitmap de página renderizado a través del muestreo hacia una matriz booleana de módulos, normalización D4, detección de versión por divisibilidad y decodificación Reed-Solomon, y luego la conversión de coordenadas en dos etapas que deshace el cuarto de giro del reintento y la transformación de render antes de que THPDFDecodedBarcode publique Left, Bottom, Right, Top y OrientationDegrees en user space
La normalización QR dentro del decoder deja que las páginas se resuelvan al primer intento, mientras la conversión de coordenadas en dos etapas convierte resultados del bitmap de intento en cajas alineadas a ejes en user space

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