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 de rotación externo 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 bastante ordinario. Albaranes escaneados llegan como PDFs, cada página lleva una etiqueta QR, y el operador del escáner alimentó una pila de hojas en la dirección que la bandeja aceptó. Algunas etiquetas están derechas, otras desviadas un cuarto de vuelta, y unas cuantas boca abajo. Llama al decoder de códigos de barras, la mitad de las páginas se resuelve, y la otra mitad vuelve vacía sin ningún error

¿Por qué rotar la máscara de escaneo nunca arregla un QR rotado?

Porque la disposición de los patrones finder de un QR es deliberadamente asimétrica, 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 la esquina inferior derecha vacía (ISO/IEC 18004:2015 §6.3.3). Esa esquina ausente es la pista de orientación. Rote el bitmap de página noventa grados y el hueco simplemente se mueve a otra esquina. No existe una rotación no trivial del plano que lleve una disposición de tres esquinas de vuelta sobre sí misma, así que un decoder que solo acepta la disposición canónica rechazará cada intento por turnos

Esto importa porque el arreglo obvio es el equivocado. El instinto natural es colgar el reintento por fuera: renderice la página, entregue la máscara al decoder, y si eso falla, rote la máscara y pruebe de nuevo 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 inicio y parada que el escáner puede encontrar en cuanto las barras corren horizontales. Para QR son cuatro fallos garantizados seguidos de un informe 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 rejilla booleana de módulos y no sobre la máscara de píxeles. Una vez que el decoder ha resuelto el símbolo en una matriz de n por n de módulos oscuros y claros, puede enumerar el grupo diédrico del cuadrado: cuatro rotaciones por dos reflexiones, ocho orientaciones candidatas en total. Para cada candidata comprueba el triángulo finder, 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 información de formato, la colocación zigzag de datos y la corrección Reed-Solomon todas asumen una matriz canónica y ahora obtienen una

Cuatro representaciones 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 patrones finder 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 finder del QR, así que HotPDF enumera las orientaciones D4 sobre la matriz de módulos muestreada y se queda con la primera candidata cuyos finders caen en superior izquierda, superior derecha e inferior izquierda

Dos propiedades hacen esto 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 array booleano limpio construido por el muestreador, así que ninguna transformación del 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 recuento de módulos no puede deducirse dividiendo el ancho muestreado entre un tamaño de módulo asumido, y equivocarse aquí es una fuente sutil de fallos de decodificación en renders 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 igual de consistente con la versión 1 a seis píxeles por módulo que con varias versiones superiores a tamaños de módulo menores. La división lineal escoge una de ellas y suele equivocarse

Lo que funciona es una búsqueda de divisibilidad sobre las versiones candidatas. Recorra desde la versión 40 hacia abajo hasta la 1, quédese con las candidatas cuyo recuento 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 superviviente más pequeña. El suelo 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 produciría de verdad

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 recuento de módulos candidato 4v más 17 desde la versión 40 hacia abajo hasta la 1 por divisibilidad exacta y un suelo de módulo de tres píxeles antes de que gane la versión superviviente más pequeña
Un recuento de módulos QR viene 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 asumido, y la versión superviviente 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 registro poblado en lugar de uno a cero, lo que importa porque un DPI de cero o un tope de resultados de cero es una forma con aspecto válido 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 está ahí para las simbologías lineales que de verdad la necesitan

¿Cómo demuestra que una transformación de bitmap no inventa píxeles?

Cuente la tinta a ambos lados y exija que los totales coincidan. Una rotación es una permutación de píxeles, nada más, así que el número de celdas no nulas en la salida debe ser igual al de la entrada. Cuando una rotación de máscara en la ruta de reintento externo reportó 4800 celdas activas entrando y 7439 saliendo, esa única comparación bastó para condenar la transformación sin leer una línea de su geometría

La causa era mundana y merece llevarse como regla. Un array dinámico dimensionado con SetLength no tiene garantía de llegar a cero cuando es un resultado de función que viaja por una ruta que el runtime no limpia, y las celdas que la rotación nunca escribe llevan entonces los bytes que había 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 bucle de permutación, y la disciplina que implica es más amplia: cualquier función que devuelva un buffer de máscara o bitmap debería limpiar su salida explícitamente en lugar de fiarse de la semántica de asignación

Lo que hizo que el defecto sobreviviera tres releases es más interesante que el defecto. Una vez que QR movió su manejo de orientación al decoder, QR dejó de ejercitar por completo la rotación externa de máscara, y el único consumidor restante de esa ruta de código era Code 39. La infraestructura compartida esconde bugs así constantemente: la cobertura de una función hace que una ruta parezca probada mientras la función que realmente depende de ella no tiene ninguna propia. Cada 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 sistema de coordenadas del bitmap de intento, y quien llama lo necesita en el espacio de usuario PDF. Esa conversión corre en dos etapas: deshacer el cuarto de vuelta que aplicó el reintento, y luego deshacer la transformación de renderizado que mapeó el espacio de usuario sobre el bitmap. Lo que llega en THPDFDecodedBarcode es un bounding box alineado con los ejes en espacio de usuario, 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 códigos de barras de HotPDF desde el bitmap de página renderizado pasando por el 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 vuelta del reintento y la transformación de renderizado antes de que THPDFDecodedBarcode publique Left, Bottom, Right, Top y OrientationDegrees en espacio de usuario
La normalización QR dentro del decoder permite que las páginas se resuelvan al primer intento, mientras que la conversión de coordenadas en dos etapas convierte los resultados del bitmap de intento en cajas alineadas con los ejes en espacio de usuario

Equivocar la dirección de esa segunda conversión da un síntoma desagradable: el texto se decodifica perfectamente, pero la caja que dibuja para una capa de revisión cae sobre la imagen especular de la posición correcta. Cualquiera que 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 página para que un eje Y invertido se vea de un vistazo. El mismo razonamiento aplica a cualquier coordenada que cruce la frontera de renderizado, que es por lo que renderizar una página PDF a un bitmap en Delphi merece entenderse antes de construir sobre el decoder

Qué hará y qué no hará el decoder integrado

El decoder integrado 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 desigual, esa es otra clase de problema y quiere un motor especializado

// Cambie a su propio motor: implemente IHPDFBarcodeDecoder y páseselo a la
// sobrecarga consciente de decoders. HotPDF sigue dueño del renderizado,
// los presupuestos, el mapeo de coordenadas y la deduplicació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 se gana su salario. RotationAttemptCount y DecoderCallCount le dicen si el reintento externo corrió siquiera, ReceivedResultCount frente a 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 grafica cuando un trabajo por lotes empieza a hacer thrashing. Un conjunto de resultados vacío más bdsSucceeded significa que la página realmente no tiene símbolo legible, que es un hecho operativo distinto de bdsBudgetExceeded

Los campos de presupuesto merecen una decisión deliberada y no un valor por defecto. MaxPixels y MaxWorkingBytes existen porque el DPI multiplica de forma cuadrática: pasar de 300 a 600 DPI en una página A4 cuadruplica tanto el coste de renderizado como la asignación pico, y una entrada no confiable que declare un cuadro 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 envíe los valores atípicos a una ruta más lenta y aislada

Si sus documentos mezclan etiquetas legibles por máquina con texto impreso que planea indexar, el decoder de códigos de barras se empareja de forma natural con el motor de reconocimiento cubierto en el OCR de plantilla dentro de HotPDF, y el lado de generación de la misma historia está en dibujar códigos de barras en un PDF con HotPDF. Ambos corren sobre la misma infraestructura de renderizado y presupuestos, así que un pipeline que ya fija límites sensatos para uno obtiene 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á del 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 llevan cada accidente de cómo se capturaron. HotPDF envía esto como parte del componente PDF Delphi HotPDF, junto a las piezas de renderizado, OCR y análisis de página que los mismos pipelines de ingesta suelen necesitar