No confíe en el valor decodificado de un código QR sin validarlo — que la corrección de errores funcione no garantiza que el valor sea correcto

· Actualizado el: · · Código QR, Código de barras, Corrección de errores, Validación de entradas, Calidad de datos, Sistemas empresariales, C#, Diseño, Operación en planta

En el terminal de inspección del almacén suena un «pip». Es la señal de que se leyó el código QR del comprobante. La cadena leída pasa directamente al sistema de inventario, se reserva el comprobante correspondiente y se confirma la instrucción de envío. Como el código QR tiene corrección de errores, aunque esté algo sucio devolverá el valor correcto — no es en absoluto raro encontrar sistemas construidos sobre esta premisa.

La primera mitad es cierta. El código QR está diseñado para recuperar datos incluso con suciedad o daño, y según el nivel de corrección de errores, de L a H, puede recuperar entre aproximadamente el 7 % y el 30 % de las palabras código.[^denso] El problema está en la segunda mitad: en el razonamiento de que «por lo tanto el valor devuelto es correcto».

En este artículo, a partir de imágenes de muestra de códigos QR generados realmente y de pruebas con dos decodificadores distintos, ordenamos por qué la corrección de errores puede funcionar sin que el valor quede garantizado y qué debe validar el sistema empresarial que lo recibe. Todos los códigos QR que aparecen en el artículo son reales. Los que se pueden leer, puede comprobarlos usted mismo con su propio lector de QR (también se incluyen ejemplos pensados para mostrar «que no se puede leer»; en esos puntos se indica explícitamente). Además hemos preparado una herramienta de comparación de lectura de QR que permite alternar entre jsQR y OpenCV.js directamente en el navegador. Las pruebas de este artículo se pueden reproducir allí.

1. Conclusión de entrada

  • La corrección de errores es «recuperación», no «verificación».El propio estándar dice que un módulo dañado puede «decodificarse incorrectamente hacia una palabra código distinta pero claramente válida».1
  • Con suciedad aleatoria, el resultado casi siempre cae en «no se puede leer».En 9.700 pruebas no hubo ni un solo caso en que se devolviera un valor incorrecto.
  • Pero si el daño se concentra en un punto, el resultado cambia con seguridad a otro valor.Con solo 7 de 26 palabras código alteradas, 004873 se lee como 104873, y dos decodificadores independientes devolvieron exactamente el mismo error.
  • Fuera de la corrección de errores hay trampas todavía más sencillas.El QR dividido, la codificación de caracteres, otro código dentro del mismo encuadre. Todas ellas pueden ocurrir sin que se produzca ningún error. Según el decodificador puede aparecer una advertencia o una excepción, pero eso depende de la implementación, y existen combinaciones reales en las que no aparece nada.
  • Por eso, el valor leído debe tratarse como una entrada sin validar.El esquema básico es recibirlo en tres etapas: comprobación de formato → dígito de control → validación operativa.

2. Muestra: dos códigos QR que parecen casi idénticos

Primero, vea los originales. Los dos códigos siguientes se leen ambos sin ningún error.

Arriba un código QR normal, abajo el mismo código con una franja vertical de daño. Visualmente son casi idénticos

Antes de probar: en algunos iPhone la cámara estándar de iOS puede no reaccionar. Si no lee el código, pruebe con una aplicación lectora de QR (la razón se explica en la nota que sigue).

  • A (arriba)NO:20260725-004873
  • B (abajo)NO:20260725-104873

Son códigos reales, así que puede probarlos directamente con su propio lector. Este número de comprobante sigue el esquema «8 dígitos de fecha de pedido + 5 dígitos de número correlativo + 1 dígito de control», de modo que el cambio se produjo en el correlativo: 00487 pasó a 10487, apuntando a un comprobante distinto, con una diferencia de diez mil unidades.

En algunos iPhone la cámara estándar de iOS puede no reaccionar.La cámara estándar está pensada para priorizar contenido que admite una acción de «abrir», como una URL, y con un QR que solo contiene texto plano como el de este artículo puede no mostrar nada. Con una aplicación lectora de QR sí se lee. Que la misma imagen se comporte de forma distinta según la implementación del lector es exactamente el tema central de este artículo, y ya se puede experimentar con la primera muestra.

Lo único que distingue B de A son 31 módulos dentro de una única franja vertical, entre las columnas 10 y 14 contando desde la izquierda. Eso equivale al 15 % de los 208 módulos de la zona de palabras código.

Los 31 módulos que cambiaron de A a B en B, marcados con un recuadro rojo. Se distribuyen en una franja estrecha y vertical

Y lo esencial es que el decodificador no devuelve ningún error en ninguno de los dos casos. Ni siquiera hay un aviso de «se corrigió». Desde el punto de vista de la aplicación, ambas son lecturas exitosas de igual manera.

Cabe aclarar que este daño se construyó deliberadamente; la probabilidad de que aparezca así por casualidad no es alta. El método de construcción y la distancia con la suciedad real se tratan en la sección 4.

3. Por qué ocurre: el funcionamiento interno de la corrección de errores

La corrección de errores del código QR es un código Reed-Solomon que actúa por palabra código (unidades de 8 bits). El desglose de la versión 1, nivel de corrección M (21×21 módulos), que usamos aquí, es este.1

Total de palabras código Palabras código de datos Palabras código de corrección Capacidad de corrección según el estándar
26 16 10 4 palabras código

Lo llamativo está en la última columna. Con 10 palabras código de corrección, un código Reed-Solomon podría corregir hasta 5. Sin embargo, la capacidad de corrección que fija el estándar es 4. La palabra código de diferencia se reserva deliberadamente como palabra código antierror de lectura p (en la versión 1-M, p = 2). La Tabla 13 del estándar incluye una nota al pie que dice explícitamente que «la capacidad de corrección se fija por debajo de la mitad del número de palabras código de corrección, para reducir la probabilidad de lecturas erróneas».1

Es decir, el propio estándar está diseñado bajo la premisa de que «la corrección de errores puede producir un valor incorrecto». En el mismo apartado se dice también lo siguiente:

Dado que el código QR es un símbolo matricial, los defectos que cambian un módulo de oscuro a claro (o viceversa) pueden producir que el carácter de símbolo correspondiente se decodifique incorrectamente hacia una palabra código distinta pero claramente válida.1

La razón está en el propio principio de la corrección. Lo que hace la decodificación Reed-Solomon es buscar, a partir del patrón recibido, si existe una palabra código dentro de una distancia determinada (la capacidad de corrección). Si la encuentra, la devuelve como respuesta; si no, termina en «no se puede leer». No es un mecanismo que, por muy dañado que esté el patrón, siempre busque la palabra código más cercana sin más.

De esta propiedad se derivan dos resultados. El daño aleatorio se dispersa lejos de cualquier palabra código, así que en la mayoría de los casos cae en «no se encuentra = no se puede leer». Lo peligroso es cuando el daño cae, por azar, cerca de otra palabra código.En ese caso el decodificador juzga que esa palabra código es la correcta y la devuelve. La palabra código devuelta es, en sí misma, perfectamente coherente, así que no hay forma de saber que es un error.

4. Hasta dónde se corrige y a partir de dónde es peligroso

A partir de aquí van las pruebas reales. Adelantando la conclusión: lo peligroso no es la «cantidad» de suciedad, sino «dónde golpea».

Antes de los números, resumimos las condiciones de este experimento, como referencia para quien quiera reproducirlo.

Elemento Contenido
Símbolo utilizado Versión 1-M / 21×21 (26 palabras código = 16 de datos + 10 de corrección)
Generación segno 1.6.6 / Python 3.11
Decodificador 1 OpenCV 5.0.0 cv2.QRCodeDetector (Python 3.11)
Decodificador 2 jsQR 1.4.0 (Node.js 22)
Forma de dañar (sección 4.1) Selección aleatoria e inversión dentro de los 208 módulos de la zona de palabras código / corrupción aleatoria por palabra código / desenfoque, ruido y reducción de contraste uniformes sobre todo el símbolo
Forma de dañar (sección 4.2) Prueba exhaustiva de combinaciones de palabras código que se acercan al valor objetivo B
Número de pruebas Inversión de módulos: 3.900 (300 por nivel); corrupción de palabras código: 1.800 (200 por nivel) + 4.000 pruebas adicionales superando la capacidad de corrección; degradación de imagen: 1.000 (200 por condición). La sección 4.2 recorre exhaustivamente 792 y 495 combinaciones
Criterio de clasificación El valor devuelto por el decodificador se clasifica como «lectura correcta» si coincide con el valor esperado, «no se puede leer» si no se obtiene ningún valor, y «valor incorrecto» si se devuelve un valor distinto y no vacío

El entorno completo del artículo, incluidas las muestras de la sección 5, está resumido en «Entorno de verificación» al final del artículo.

4.1. El daño aleatorio termina en «no se puede leer»

Sobre un símbolo de versión 1-M, invertimos al azar una cantidad variable de módulos dentro de los 208 de la zona de palabras código y clasificamos el resultado (300 pruebas por nivel, 3.900 en total).

Comparación de un QR con 6 módulos invertidos y otro con 9 módulos invertidos. El de arriba se puede leer, el de abajo no

Arriba se invirtieron 6 módulos, abajo 9. El de arriba se lee correctamente; el de abajo no se lee en absoluto.La diferencia de 3 módulos apenas se percibe a simple vista. El límite no se manifiesta visualmente.

Módulos invertidos Lectura correcta No se pudo leer Valor incorrecto
0–5 1.799 1 0
6 131 169 0
7 32 268 0
8 11 289 0
9–12 0 1.200 0

La posibilidad de leer se derrumba entre 5 y 6 módulos, y a partir de 9 el fracaso es total. Y no se produjo ni un solo valor incorrecto. El daño que no se puede corregir cae en «no se puede leer»: esa es la buena noticia directa. Con jsQR las cifras fueron prácticamente iguales (de 0 a 5 acertó los 1.800 casos; de 6 en adelante, dentro de una diferencia de 1 caso respecto de OpenCV).

Si se mira lo mismo por palabra código en lugar de por módulo, el límite se ve todavía más claro (200 pruebas por nivel, 1.800 en total).

Palabras código corrompidas Lectura correcta No se pudo leer Valor incorrecto
0–5 1.194 6 0
6–8 0 600 0

Se corrigen hasta 5 palabras código.Como se explicó en la sección anterior, la capacidad de corrección del estándar es 4; el resto era, en teoría, un margen «solo para detectar, no para corregir». jsQR también leyó correctamente los 1.200 casos de 0 a 5, así que las dos implementaciones agotan ese margen usándolo para corregir. El margen que el estándar reservó como protección antierror de lectura no se puede dar por seguro a nivel de implementación.

También repetimos por separado 2.000 pruebas cada una de daño que supera claramente la capacidad de corrección (6 y 8 palabras código), y de nuevo hubo cero valores incorrectos: todas terminaron en «no se puede leer».

La degradación derivada de la cámara sigue la misma tendencia. Resultado de aplicar desenfoque, ruido y reducción de contraste uniformes a todo el símbolo (200 pruebas por condición, 1.000 en total).

σ de desenfoque σ de ruido Contraste Lectura correcta No se pudo leer Valor incorrecto
0 0 1,00 200 0 0
1,5 10 0,90 183 17 0
3,0 20 0,70 1 199 0
4,5 30 0,50 0 200 0
6,0 40 0,35 0 200 0

El resultado es siempre «se lee correctamente» o «no se puede leer»: no hay término medio. A medida que avanza la degradación baja la tasa de aciertos, pero todo lo que se pierde se convierte en fallo de lectura.

Ahora bien, esto vale solo para una degradación uniforme. El desenfoque de mano real tiene dirección, y una toma en ángulo o una iluminación desigual solo deforma una parte de la imagen. Como se verá a continuación, lo peligroso es que el daño se concentre, así que no generalice diciendo que «si es un problema de calidad de imagen, no habrá lecturas erróneas».

4.2. Si el daño golpea en el lugar equivocado, el resultado es, sin excepción, otro valor

La muestra B de la sección 2 se construyó deliberadamente para representar ese «daño concentrado».

Al comparar las 26 palabras código de NO:20260725-004873 (A) y NO:20260725-104873 (B), hay diferencias en 12 posiciones: 2 palabras código de datos y 10 palabras código de corrección arrastradas por ese cambio.

Si de esas 12 posiciones se acercan 7 al valor de B, el patrón resultante queda a 7 palabras código de distancia de A y a 5 de B. Con una capacidad de corrección de 5, el decodificador interpreta esto como «B con 5 posiciones dañadas» y corrige hacia B.

Condición Combinaciones probadas Leídas erróneamente como B
7 palabras código acercadas a B (distancia 5 respecto de B) 792 combinaciones 792 combinaciones (100 %)
8 palabras código acercadas a B (distancia 4 respecto de B) 495 combinaciones 495 combinaciones (100 %)

En todas las combinaciones, sin excepción, se produjo una lectura errónea.La fila inferior corresponde a una distancia de 4 respecto de B, es decir, dentro de la capacidad de corrección que fija el estándar. Incluso una implementación que respete la palabra código antierror de lectura p llega al mismo resultado en cuanto el daño aumenta en una palabra código más. p reduce la probabilidad; no la impide.

La muestra B presentada en la sección 2 es, dentro de este conjunto, la combinación en la que el daño se concentra en una única franja vertical (31 módulos). Minimizándola se pudo reducir hasta 23 módulos. OpenCV 5.0.0 y jsQR 1.4.0, dos implementaciones sin relación entre sí, devuelven ambas NO:20260725-104873.

4.3. Cómo interpretar esta diferencia

Para ser honestos, este tipo de daño no ocurre fácilmente por azar. Con suciedad aleatoria probamos 9.700 veces y no hubo ni una sola lectura errónea. No es un caso de «puede pasar mañana mismo».

Aun así, hay tres razones por las que no conviene ignorarlo.

  • El daño real no es aleatorio.Los pliegues siguen líneas rectas, el roce del transporte se concentra en el mismo borde, y un cabezal de impresión obstruido genera vetas verticales. La franja de daño de este artículo es un ejemplo de este tipo de «daño concentrado en una posición». Ahora bien, B incluye cambios en ambas direcciones —17 módulos de blanco a negro y 14 de negro a blanco—, así que un simple defecto de falta de tinta no lo reproduce. Que ambas direcciones ocurran a la vez suele deberse a que la sombra de un pliegue desplaza el umbral de binarización, a que suciedad y desgaste se superponen, o a que se pega parcialmente otra etiqueta encima.
  • El número de escaneos es de otro orden de magnitud.Una probabilidad despreciable en un solo intento deja de serlo cuando el mismo punto se lee decenas de miles de veces al día. Además, como la lectura errónea no genera ningún error, no queda registrada en ninguna parte, y termina tratada como una diferencia de inventario de origen desconocido.
  • Que se pueda construir deliberadamente significa que otra persona también puede hacerlo.En este caso, el patrón se construyó de forma mecánica a partir de un valor objetivo elegido de antemano. En usos con incentivo para la manipulación, como etiquetas de precio o cupones, esto se convierte en un vector de ataque.

5. «Se leyó, pero es otro valor»: casos ajenos a la corrección de errores

En la práctica, esto es lo que se topa con más frecuencia. Ocurre incluso cuando la corrección de errores funciona a la perfección, y ni siquiera es cuestión de probabilidad.

5.1. Leer solo la primera de las partes de un QR dividido

El código QR tiene un mecanismo (Structured Append) para repartir datos largos entre varios símbolos y que el lector los concatene. A continuación se muestra la primera de las tres partes en que se dividió NO:20260725-004873/LOT:AB-77/QTY:120/EXP:20270131.

La primera de tres partes de un QR dividido. Leída sola, devuelve un número de comprobante con el final recortado

A simple vista es un código QR normal, sin ninguna pista de que sea una de tres partes. Si se lee sola, OpenCV devuelve esto:

NO:20260725-00487

Sin error, sin advertencia. Un número de comprobante perfectamente plausible, al que solo le falta el dígito de control 3 del final.La segunda y la tercera parte darían, respectivamente, 3/LOT:AB-77/QTY: y 120/EXP:20270131. Al pasar la misma imagen a jsQR se obtuvo una cadena vacía. Lo que ocurre cuando una aplicación que no contempla el QR dividido escanea por casualidad la primera parte depende del decodificador.

En la práctica, esto aparece así.En una pantalla que busca el número de comprobante por coincidencia inicial o con LIKE, el NO:20260725-00487 con el último dígito recortado coincide igualmente con el comprobante original, y como «se leyó y además apareció el comprobante», nadie nota nada anómalo. Mientras no se verifique la longitud de forma estricta, este fragmento sigue circulando hasta el final como una lectura perfectamente normal.

5.2. Codificación de caracteres y ECI

A continuación, un código QR con 部品番号 東-004873 (número de pieza, con un carácter japonés) codificado en Shift_JIS, sin especificar ECI.

Código QR generado en Shift_JIS sin especificar ECI. Al leerlo se producen caracteres corrompidos

Este también es un código real. Al leerlo con su propio lector, puede aparecer 部品番号 東-004873, una cadena con caracteres corrompidos, o nada en absoluto: eso le indica qué interpretación está usando su lector.

Si hace leer esta imagen con la herramienta de comparación de lectura de QR, se muestran juntos la secuencia de bytes en bruto que extrae jsQR y el resultado de reinterpretarla como UTF-8, Shift_JIS o EUC-JP, entre otros. Ahí mismo puede comprobar cómo la misma secuencia de bytes se convierte en algo distinto según la codificación de caracteres.

El mismo contenido, generado variando la codificación de caracteres y la especificación de ECI, dio estos resultados con los dos decodificadores. Las cuatro condiciones de esta tabla están incluidas en las muestras de la herramienta.Sin embargo, los valores de la tabla se midieron con cv2 de Python, y solo la tercera fila difiere de la versión de navegador (esto se detalla justo después de la tabla). Como lo que se puede comprobar con la herramienta es el comportamiento de la versión de navegador, para reproducir el «falla con una excepción» de la tercera fila hace falta el cv2 de Python.

Condición de generación OpenCV 5.0.0 jsQR 1.4.0
Shift_JIS / sin ECI Devuelve una cadena corrompida como éxito Cadena vacía
UTF-8 / sin ECI 部品番号 東-004873 部品番号 東-004873
Shift_JIS / con ECI Falla al decodificar, con advertencia Cadena vacía
UTF-8 / con ECI 部品番号 東-004873 部品番号 東-004873

La primera fila es la peor. OpenCV interpretó la secuencia de bytes como Latin-1 y devolvió, sin ningún error, la cadena corrompida \x95\x94\x95i.... Desde el punto de vista de la aplicación es una lectura normal, y si se guarda directamente en la base de datos se crea un registro con texto corrompido.

En la práctica, esto aparece así.En el campo de descripción del artículo de un registro de entrada de mercancía queda guardado un texto corrompido, y al día siguiente llega la consulta de «al buscar por ese número de pieza no aparece el movimiento». Al volver a leer la misma etiqueta se obtiene el mismo valor, así que el área operativa reporta que «la búsqueda del sistema falla», y las idas y vueltas continúan hasta que se descubre que el origen es la codificación de caracteres del lector.

No se trata de un error de OpenCV, sino de un comportamiento que sigue la interpretación por defecto que fija el estándar vigente (ISO/IEC 8859-1).2 Lo que se aparta del estándar es, en todo caso, el lado que generó el Shift_JIS sin ECI.

La tercera fila tampoco se puede pasar por alto. Aun especificando correctamente el ECI (el mecanismo que declara la codificación de caracteres), OpenCV mostró la advertencia QR: ECI is not supported properly y falló con una excepción al no poder interpretar el valor devuelto como UTF-8. Se produce una inversión real: el QR construido con fidelidad al estándar es el que no se puede leer.

Además, esta misma tercera fila cambia de resultado incluso dentro de OpenCV según el binding de lenguaje.La tabla anterior corresponde al resultado con cv2 de Python, pero al pasar la misma imagen a la versión de navegador (opencv.js 5.0.0) no aparece ninguna excepción: devuelve, como éxito, la cadena ���i��� ��-004873, llena de caracteres de reemplazo. Esto ocurre porque, al convertir un std::string a UTF-8, Emscripten sustituye los bytes inválidos por U+FFFD en lugar de lanzar una excepción. Misma versión, misma imagen; lo único que cambió fue el lenguaje de la llamada. Python, donde al menos se nota con una excepción, sale mejor parado; la versión de navegador dice «se leyó» mientras devuelve un valor corrompido. Puede comprobarlo directamente con las muestras de la herramienta.

5.3. Varios códigos QR dentro del encuadre

Un comprobante con varios QR impresos, la etiqueta de la caja de al lado que entra en el encuadre: son situaciones habituales. Probamos con tres códigos QR colocados uno junto a otro.

Imagen con tres códigos QR colocados en fila. De izquierda a derecha: NO:, ITEM: y LOT:

Primero, con esta imagen la API de lectura única de OpenCV no devolvió nada.Pero esto no es garantía de que «si hay varios, los rechaza». La API de lectura única solo está documentada como algo que detecta y decodifica un único QR; no dice que rechace la presencia de varios, y según la disposición podría devolver cualquiera de ellos. No sustituya la detección de múltiples códigos por la ausencia de valor de la API de lectura única.

¿Y con la API de lectura múltiple? Aquí es donde se complica.

Usamos la misma imagen de arriba (de izquierda a derecha, NO: / ITEM: / LOT:) y la leímos variando únicamente el número de píxeles, lo que equivale, en un escáner real, a cambiar la distancia al objetivo o la resolución de la cámara.

Ancho de la imagen Orden devuelto
1.001 px NO: / LOT: / ITEM:
1.502 px No devuelve nada
2.002 px NO: / LOT: / ITEM:
3.003 px NO: / ITEM: / LOT:
4.004 px LOT: / NO: / ITEM:

Con la misma imagen, el orden cambia solo por variar la resolución.No es de izquierda a derecha ni de mayor a menor. Incluso hay una resolución en la que no se puede leer nada. El orden depende de la lógica interna del algoritmo de detección, y como no está normalizado, esto es lo que ocurre.

Es decir, un código que asume que «el primero debe ser el número de comprobante» y usa el índice 0 puede estar funcionando hoy por casualidad, y mañana, con solo acercar la cámara, tomar otro código distinto.Es el tipo de fallo difícil de reproducir y difícil de rastrear.

En la práctica, esto aparece así.En la mesa de inspección, el comprobante objetivo y la etiqueta de la caja de al lado entran a la vez en el encuadre. Una aplicación que usa el índice 0 toma el comprobante equivocado, y la instrucción de envío se confirma con ese. El operario tenía la intención de apuntar la cámara a la etiqueta correcta, así que nadie se da cuenta hasta que, después del envío, alguien reporta que «llegó un producto distinto».

Quien recibe el valor debe elegir por contenido, no por orden: recoger todo con la API de lectura múltiple, quedarse solo con lo que coincide en prefijo y formato, y devolver error si el resultado es cero o dos o más coincidencias. Así se escribe de forma segura.

5.4. Ni siquiera está garantizado que el contenido sea correcto

Hay una capa que el procesamiento de imagen no puede detectar por principio. Que los datos de origen en la impresión estén equivocados, que quede en la caja una etiqueta antigua sin reemplazar, que se haya mezclado la etiqueta de otro cliente, que la etiqueta esté fotocopiada.

El código QR solo dice «qué está escrito ahí».Si «eso es correcto» o «lo emitió realmente nuestra empresa» solo lo puede confirmar quien lo recibe.

En la práctica, esto aparece así.En el costado de una caja reutilizada queda la etiqueta anterior, y se lee esa en lugar de la etiqueta nueva de la cara superior. Como valor, el QR es perfectamente correcto, así que pasa la comprobación de formato, el dígito de control y el cotejo con la base maestra sin problema, y el paquete se dirige al destinatario del envío anterior. Esta capa, en particular, no la detecta ninguna verificación del valor leído.

6. Cómo tratar el valor recibido

La medida consiste, en definitiva, en añadir una validación propia fuera de la corrección de errores.

Nivel Contenido de la validación Qué detecta
1. Comprobación de formato Coincidencia exacta de longitud, tipo de carácter, separadores y prefijo Lectura de otro código, fragmento de QR dividido, texto corrompido
2. Autovalidación Dígito de control Detecta con certeza el cambio de un carácter. Puede pasar por alto cambios en varios caracteres
3. Validación operativa Cotejo con la base maestra y coincidencia con lo que corresponde procesar ahora mismo Etiqueta antigua, etiqueta de otra empresa, confusión con otro comprobante

El número de comprobante de la muestra es NO: + 8 dígitos de fecha de pedido + 5 dígitos de correlativo + 1 dígito de control, y el dígito final usa el mismo esquema módulo 10 con peso 3 que GS1. La lectura errónea NO:20260725-104873 de la sección 2 se detiene aquí: el dígito de control correcto de 2026072510487 es 0, que no coincide con el 3 de la etiqueta.

Antes de escribir el código, decida primero la premisa sobre la codificación de caracteres.La implementación siguiente asume que el número de comprobante está compuesto solo por dígitos ASCII, y calcula el dígito de control como «carácter − '0'». Si a este cálculo llegan dígitos de ancho completo o una secuencia de bytes corrompida como la de la sección 5.2, el resultado deja de tener sentido. Por eso la expresión regular de la comprobación de formato se escribe con [0-9] en lugar de \d, en un orden que evita que ningún dígito fuera de ASCII llegue al cálculo del dígito de control. Primero se fija la premisa, después se garantiza con la comprobación de formato, y solo entonces se calcula. Si este orden se rompe, todas las validaciones posteriores giran en el vacío.

using System.Linq;
using System.Text.RegularExpressions;

public sealed record ScanOutcome(bool Accepted, string? SlipNo, string Reason);

public static class SlipScanValidator
{
    // NO: + 8 dígitos de fecha de pedido + '-' + 5 dígitos de correlativo + 1 dígito de control
    // El final se ancla con \z, no con $. En .NET, $ también puede coincidir
    // justo antes de un salto de línea final, así que dejaría pasar
    // "NO:20260725-004873\n".
    // Los dígitos se escriben como [0-9], no como \d. En .NET, \d coincide con
    // cualquier dígito Unicode, incluidos los de ancho completo, pero el
    // cálculo del dígito de control que sigue asume ASCII
    private static readonly Regex Format =
        new(@"\ANO:(?<date>[0-9]{8})-(?<seq>[0-9]{5})(?<cd>[0-9])\z", RegexOptions.Compiled);

    public static ScanOutcome Validate(string? raw, ISlipRepository repo)
    {
        // 1. No convertir "no se pudo leer" en "éxito vacío".
        //    Cada decodificador devuelve el fallo de forma distinta: null, cadena vacía o excepción
        if (string.IsNullOrEmpty(raw))
            return new(false, null, "No se pudo leer. Vuelva a escanear el código");

        // 2. Comprobación de formato. No coincidencia inicial: coincidencia
        //    exacta que incluye la longitud.
        //    El fragmento de un QR dividido "NO:20260725-00487" se descarta aquí
        var m = Format.Match(raw);
        if (!m.Success)
            return new(false, null, $"No tiene el formato de un QR de comprobante ({Describe(raw)})");

        // 3. Autovalidación. Si la corrección de errores alteró algún dígito, se detecta aquí
        var body = m.Groups["date"].Value + m.Groups["seq"].Value;
        if (Modulus10Weight3(body) != m.Groups["cd"].Value[0] - '0')
            return new(false, null, "El dígito de control no coincide. Verifique si la etiqueta está dañada");

        // 4. Validación operativa. Que exista, y que esté en un estado que se pueda procesar ahora
        var slip = repo.Find(raw);
        if (slip is null)
            return new(false, null, "No existe un comprobante con ese número");
        if (slip.Status != SlipStatus.WaitingForShipment)
            return new(false, null, $"Este comprobante está en estado «{slip.Status}». No es apto para envío");

        return new(true, raw, "OK");
    }

    // Módulo 10 con peso 3, igual que GS1. Se aplican pesos 3,1,3,1... desde el extremo derecho
    private static int Modulus10Weight3(string body)
    {
        var sum = 0;
        for (var i = 0; i < body.Length; i++)
        {
            var weight = (body.Length - i) % 2 == 1 ? 3 : 1;
            sum += (body[i] - '0') * weight;
        }
        return (10 - sum % 10) % 10;
    }

    // El valor leído es una entrada externa. Antes de mostrarlo en pantalla o
    // en el registro, conservar solo caracteres ASCII imprimibles.
    // char.IsControl solo descarta la categoría Unicode Control (Cc) y deja
    // pasar caracteres Format (Cf) como U+202E (sobrescritura a derecha a
    // izquierda). Escribir con lista de permitidos, no de exclusión
    private static string Describe(string raw)
    {
        var kept = raw.Where(c => c >= ' ' && c <= '~').Take(40).ToArray();
        if (kept.Length == 0) return "(cadena no representable)";
        var safe = new string(kept);
        return kept.Length < raw.Length ? safe + "…(se eliminaron algunos caracteres)" : safe;
    }
}

El diseño del dígito de control en sí se explica en Diseño de códigos de sistemas empresariales y dígito de control, con las fórmulas y criterios de elección. El valor de esa comprobación en el contexto de este artículo está en que funciona tanto contra errores de tecleo humano como contra lecturas erróneas de la máquina.

Lo que estos tres niveles no pueden proteger

Aunque se reúnan los tres niveles, quedan tres huecos abiertos. Todos se pueden cerrar «profundizando un nivel más el diseño de la validación», así que conviene tenerlos presentes también.

El dígito de control pasa por alto cambios en varios caracteres.Lo que el módulo 10 con peso 3 detecta con certeza es un error de un solo carácter. De hecho, 2026072500487 y 2026072517487 tienen ambos el dígito de control 3, así que NO:20260725-174873 pasaría el segundo nivel sin problema. Como una corrección errónea no siempre cambia un solo carácter, no se puede prescindir del tercer nivel.

No deje que el cotejo con la base maestra se limite a «comprobar que existe».El repo.Find() del código anterior, junto con la comprobación de estado, solo verifican que «en alguna parte existe un comprobante utilizable». Si un operario lee la etiqueta de la caja de al lado, esa etiqueta también cumple el formato, el dígito de control y el estado de espera de envío, y pasa igualmente. El valor leído se debe cotejar con el objeto que en efecto corresponde procesar en ese momento: si coincide con el siguiente elemento de la lista de picking, si está vinculado al identificador de contenedor ya escaneado, si el destino coincide con el reparto en curso. Con qué cotejar depende de cada operación, así que este único punto no admite código genérico.

El procesamiento duplicado no lo evita la validación.Si dos terminales leen la misma etiqueta casi al mismo tiempo, ambos confirman el estado «pendiente de envío» antes de actualizarlo, y los dos pasan. Evitar esto es tarea de la capa de ejecución: convertir una transición de estado condicional como UPDATE ... WHERE status = 'pendiente de envío' en una única operación atómica, o absorber las repeticiones con una clave de idempotencia. La validación es el filtro de entrada; no sustituye al control de concurrencia.

«Accidente» y «ataque» son problemas distintos

Lo que protegen estos tres niveles hasta aquí son accidentes: lectura errónea por suciedad, pérdida de una parte de un QR dividido, texto corrompido, mezcla de una etiqueta antigua. Los errores sin intención maliciosa quedan detenidos aquí.

En cambio, en usos con incentivo para la manipulación (etiquetas de precio, cupones, entradas, pagos) esto no ofrece ninguna defensa. Un atacante puede cumplir el formato, recalcular el dígito de control y construir libremente un QR que apunte a otro número que sí existe. El cotejo con la base maestra solo verifica existencia, así que lo deja pasar.

Si la mera posesión del QR implica valor o autorización, hace falta dotar de autenticidad al propio valor: usar un token impredecible emitido por el servidor (un número aleatorio suficientemente largo) para que no se pueda deducir un número a partir de otro, o añadir al contenido un MAC con clave o una firma digital que el receptor verifique con su propia clave. En ambos casos, el estado de «ya utilizado» se debe gestionar en el servidor para impedir el uso duplicado de una copia.

Ahora bien, la autenticidad por sí sola no impide la «sustitución».Si se despega el QR legítimo de un producto barato y se pega en uno caro, el token y la firma siguen siendo auténticos. En escenarios donde el soporte se reutiliza, como una etiqueta de precio que se puede despegar, tampoco funciona la comprobación de «ya utilizado». Aquí también sirve la misma idea que en el tercer nivel: confirmar, por una vía distinta del propio valor, si ese QR corresponde realmente al objeto que tiene delante. Algunas maneras de hacerlo son obtener por otro medio la identificación del producto y cotejarla, verificar contra el contexto de la transacción (el detalle de caja, la franja horaria de entrada), o vincular físicamente con una etiqueta que se rompe al despegarla.

Ni el dígito de control ni el cotejo con la base maestra garantizan nada sobre la autenticidad. Y la autenticidad en sí tampoco garantiza que ese valor corresponda al objeto que tiene delante. Lo importante es no intentar cubrir con un mismo mecanismo la «protección contra lecturas erróneas», la «protección contra falsificación» y la «protección contra sustitución».

7. Decisiones que corresponden a la operación

Hay partes que el código por sí solo no puede cubrir.

  • Incluya siempre, debajo del QR, una cadena legible por personas.Es la misma idea que la HRI (Human Readable Interpretation) definida por GS1.3 Si se sospecha de una lectura errónea, queda un medio para que una persona compare manualmente. En la operación real, no es raro que esta sea la única vía de detección. La operación general de códigos de barras está resumida en Fundamentos de los estándares de códigos de barras GS1 y notas para la operación en planta.
  • Defina de antemano el procedimiento para cuando «no se puede leer».El límite de reintentos de escaneo, el respaldo a entrada manual, quién debe aprobarla. Si esto queda ambiguo, el personal tiende a «probar una y otra vez cambiando el ángulo hasta que se lea», lo que precisamente aumenta la probabilidad de una lectura errónea.
  • Registre en el log los valores rechazados.Si se concentran en una etiqueta o un terminal en particular, permite detectar temprano una avería de la impresora o del escáner. Ahora bien, no escriba la cadena en bruto directamente en un log orientado a líneas: un valor con saltos de línea o caracteres de control puede falsificar una línea del log o romper su visualización. Guarde el original con la longitud delimitada y escapado, en un campo de log estructurado o en una columna de base de datos, y en las líneas que lee una persona muestre solo una representación saneada: introduzca esta separación desde el principio. Cuando sea posible, conserve también la imagen leída.
  • No use el QR dividido.Si los datos no caben, aumente la versión o ponga en el QR solo un identificador y obtenga el resto de la base maestra. Esta segunda opción, además, permite etiquetas más pequeñas y corregir el contenido sin reimprimir la etiqueta.
  • Resuelva la codificación de caracteres «no metiéndola» en el QR.Mantenga los QR de uso empresarial dentro del rango ASCII. El modo de bytes, sin especificación de ECI, no declara ninguna codificación de caracteres, y la interpretación por defecto ha cambiado entre versiones del estándar.2 Escribir en UTF-8 sin más no garantiza interoperabilidad: un escáner con otra interpretación producirá texto corrompido. Si de todas formas hace falta incluir caracteres no ASCII, lo correcto según el estándar es declarar UTF-8 con especificación de ECI, pero como se vio en la sección 5.2 existen implementaciones reales con un manejo dudoso de ECI, así que no hay forma de evitar una verificación en el equipo real que se vaya a usar.
  • Antes de una operación irreversible, intercale una confirmación.En operaciones costosas de deshacer, como confirmar un envío, descontar inventario o conciliar un cobro, muestre en pantalla, para que lo vea una persona, el nombre del producto o el importe derivados del valor leído. Una lectura errónea suele parecer válida como valor, pero suele resultar antinatural en el contexto operativo.

Hasta dónde llevar la validación depende del daño que produzca un error.

Uso Comprobación de formato Dígito de control Cotejo con base maestra Confirmación humana
Ubicación o número de estante interno Obligatoria Opcional Recomendada No necesaria
Entrada/salida de almacén, inventario Obligatoria Recomendada Obligatoria No necesaria
Confirmación de envío, descuento de inventario Obligatoria Obligatoria Obligatoria Recomendada
Facturación, conciliación de cobros Obligatoria Obligatoria Obligatoria Obligatoria
Prevención de confusión de medicamentos o materiales peligrosos Obligatoria Obligatoria Obligatoria Obligatoria

8. Resumen

La corrección de errores del código QR es un mecanismo para recuperar la palabra código original a partir del patrón impreso. Ese trabajo lo cumple bien. También en las pruebas reales, frente a suciedad aleatoria y degradación uniforme de imagen, leyó correctamente dentro del rango que se podía corregir y, con honestidad, renunció a leer fuera de ese rango.

Pero eso es una cuestión distinta de que la cadena que recibe la aplicación sea correcta desde el punto de vista operativo. Según dónde caiga el daño, la corrección puede funcionar y producir, como resultado, otro valor igualmente válido. El QR dividido, la codificación de caracteres o la presencia de otro código en el mismo encuadre son, además, problemas fuera de la corrección de errores, y ni siquiera son cuestión de probabilidad.

El hecho de que el propio estándar escriba que puede «decodificarse incorrectamente hacia una palabra código distinta pero claramente válida», y que reserve deliberadamente palabras código antierror de lectura, resume bien esta estructura. Incluso haciendo todo eso, solo se reduce la probabilidad — ese es el punto al que llega el estándar. Solo la aplicación que recibe el valor puede cubrir el resto.

El valor leído es una entrada externa sin validar. Tratarlo igual que una cadena tecleada desde el teclado: esa es, en nuestra opinión, la forma correcta de relacionarse con el código QR.

Tres pasos para probar su propio QR

Todo lo anterior se puede comprobar directamente con las etiquetas de su propia empresa.

  1. Genere.Convierta en QR, con su herramienta de generación habitual, un valor con el formato que realmente usa en operación (las muestras de este artículo se generaron con segno 1.6.6). Primero confirme que se lee sin problema en su estado intacto.
  2. Dañe.Con un editor de imágenes, trace una única franja vertical que simule un pliegue o un cabezal de impresión obstruido. Lo importante no es la cantidad, sino la concentración: en lugar de esparcir ruido tenue por toda la imagen, concéntrelo en una zona estrecha.
  3. Compare con dos decodificadores.Hágalo leer con la herramienta de comparación de lectura de QR y compare los resultados de jsQR y OpenCV.js. Ahí mismo puede comprobar comportamientos como que solo uno de los dos devuelva un valor, o que ambos devuelvan un valor pero distinto del original.

Si el resultado se divide entre los dos decodificadores, esa condición es una que su propio diseño de validación debe atender sin falta. Vale la pena probar una vez, sobre la mesa, si el escáner de su planta está a salvo.


Entorno de verificación

Las secciones 2 a 4, que observan el comportamiento de la corrección de errores, se unifican en la versión 1-M (21×21 módulos). Las muestras de la sección 5 cambian de versión según la cantidad de datos, así que se indican por apartado.

Elemento Contenido
Decodificador 1 OpenCV 5.0.0 cv2.QRCodeDetector
Decodificador 2 jsQR 1.4.0 (Node.js 22)
Generación segno 1.6.6 / Python 3.11
Símbolo de las secciones 2 a 4 Versión 1-M / 21×21 (26 palabras código = 16 de datos + 10 de corrección)
Sección 5.1 (QR dividido) Las tres partes en versión 1-M / 21×21
Sección 5.2 (codificación de caracteres y ECI) Shift_JIS en versión 2-Q, UTF-8 en versión 2-M (ambas 25×25). Los 18 a 23 bytes del texto japonés no caben en las 16 palabras código de datos de la versión 1-M
Sección 5.3 (varios códigos) NO: e ITEM: en versión 1-M; LOT:AB-77, al tener menos datos, sube de nivel de corrección con segno y queda en versión 1-H (todas 21×21)
[^denso]: DENSO WAVE Incorporated, [Sobre la función de corrección de errores QRcode.com](https://www.qrcode.com/about/error_correction.html)
  1. ISO/IEC 18004 Information technology — Automatic identification and data capture techniques — QR code bar code symbology specification. La fórmula de capacidad de corrección de errores e + 2t ≦ d - p, el valor de la palabra código antierror de lectura p y la afirmación de que se puede «decodificar incorrectamente hacia una palabra código distinta pero claramente válida» están en el apartado 8.5.1, Error correction capacity; el valor (26,16,4) de la versión 1-M y la nota al pie de que «la capacidad de corrección se fija por debajo de la mitad del número de palabras código de corrección, para reducir la probabilidad de lecturas erróneas» están en la Tabla 13 (la cita se basa en la edición de 2000). La edición vigente es ISO/IEC 18004:2024 2 3 4

  2. La interpretación por defecto cuando el modo de bytes no especifica ECI ha cambiado entre ediciones del estándar. ISO/IEC 18004:2000, apartado 8.3.1, establecía: «The default interpretation for QR Code is ECI 000020 representing the JIS8 and Shift JIS character sets», pero desde la edición de 2006 (QR Code 2005) en adelante el valor por defecto es ECI 000003, es decir, ISO/IEC 8859-1. Depender de un valor por defecto no declarado significa también que la interpretación puede cambiar con solo cambiar de edición del estándar.  2

  3. GS1, GS1 General Specifications 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Si pongo el nivel de corrección de errores en H se evitan las lecturas erróneas?
No se evitan. Subir el nivel aumenta la cantidad de suciedad que se puede corregir, pero no elimina el fenómeno en sí de que «la corrección funcione y el resultado sea otro valor». La decodificación Reed-Solomon consiste en «buscar, a partir del patrón recibido, si existe una palabra código dentro del alcance de la capacidad de corrección»; si el daño cae cerca de otra palabra código, el decodificador devuelve esa otra como si fuera la correcta (un daño que cae lejos de cualquier palabra código simplemente termina en «no se puede leer»). ISO/IEC 18004 lo indica explícitamente como una «decodificación incorrecta hacia una palabra código distinta pero claramente válida», y reserva un margen de palabras código dedicado a reducir el riesgo de lectura errónea, pero incluso esa reserva solo reduce la probabilidad; no la garantiza. La única capa que puede detectar una lectura errónea es la aplicación que recibe el valor.
En la práctica, ¿con qué frecuencia ocurren las lecturas erróneas de QR?
Con daño aleatorio, prácticamente nunca. En las pruebas de este artículo, en 3.900 intentos con módulos invertidos al azar y 5.800 intentos con palabras código corrompidas al azar, no hubo ni un solo caso en que se devolviera un valor incorrecto. El daño que supera la capacidad de corrección termina en «no se puede leer». Sin embargo, el daño real no es aleatorio: los pliegues, el roce y el cabezal de impresión obstruido generan daño concentrado en posiciones concretas. Además, leer solo una parte de un QR dividido o confundir varios códigos ni siquiera es cuestión de probabilidad: ocurre siempre que se dan las condiciones.
Si el QR ya tiene corrección de errores, ¿también hace falta un dígito de control?
Sí hace falta. Protegen capas distintas. La corrección de errores atiende la coherencia interna del símbolo, y solo garantiza la recuperación dentro del alcance de su capacidad de corrección; más allá de ese límite puede «recuperar» hacia otra palabra código igualmente válida. El dígito de control, en cambio, verifica si la cadena que recibió la aplicación es válida como sistema de codificación. Detecta con certeza el cambio de un solo carácter, pero puede pasar por alto cambios en varios caracteres a la vez: un valor de control módulo 10 solo tiene diez posibilidades, y este mismo artículo muestra un caso en que dos dígitos cambian y el dígito de control sigue coincidiendo. Por eso conviene tratar el dígito de control como la capa que detiene la mayoría de las lecturas erróneas, dejando el juicio final al cotejo con la base maestra y a la verificación operativa. Una ventaja adicional es que esta misma comprobación también protege contra errores de tecleo manual, de transcripción o de importación por otra vía.
Si la cámara del teléfono pudo leerlo, ¿no significa eso que el valor es correcto?
Que se haya «leído» solo significa que el decodificador devolvió una cadena no vacía; no dice nada sobre si ese contenido es correcto. Además, la forma de señalar un fallo varía según la implementación: puede ser una excepción, una cadena vacía o un valor nulo, así que tampoco es seguro usar la ausencia de excepción como criterio de éxito. En las pruebas de este artículo hubo varios casos en que, para la misma imagen, OpenCV y jsQR devolvieron resultados distintos. Con un QR que contenía japonés en Shift_JIS, uno de los dos decodificadores devolvió una cadena con caracteres corrompidos como si fuera un éxito, mientras que el otro devolvió una cadena vacía. Incluso con la primera pieza de un QR dividido, los resultados se dividieron entre ambos decodificadores. Que se haya podido leer no dice nada sobre si el valor es correcto.
¿Conviene evitar el QR dividido (Structured Append) en entornos empresariales?
Salvo que haya una razón específica, lo más prudente es evitarlo. El QR dividido reparte los datos entre varios símbolos que el lector debe reunir y concatenar, pero el comportamiento al hacer leer solo la primera pieza a un decodificador que no admite esta función depende de la implementación. En las pruebas de este artículo, OpenCV devolvió sin error un número de comprobante con el final recortado, mientras que jsQR devolvió una cadena vacía. Dado que existen implementaciones que dejan pasar un fragmento como si fuera un valor plausible, si no se va a usar el QR dividido hace falta una validación que rechace los resultados incompletos. Si los datos no caben, es más seguro aumentar la versión del QR o acortar el código y apoyarse en una referencia a la base maestra.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog