Volver a las guías

Registros de texto NDEF: UTF-8 frente a UTF-16 y códigos de idioma

El primer byte de un registro de texto NDEF indica si el texto es UTF-8 o UTF-16 y cuánto mide el código de idioma. Aquí tienes el formato, con ejemplos en hexadecimal y una forma rápida de revisar una etiqueta.

Publicado

Un registro de texto NDEF guarda su texto en UTF-8 o en UTF-16, y el primer byte de la carga útil indica cuál. Ese mismo byte contiene además la longitud del código de idioma que viene después. Si una etiqueta se lee como basura en un teléfono y bien en otro, ese primer byte es el lugar donde mirar.

Estructura de la carga útil: byte de estado, idioma y texto

Un registro de texto es un registro Well Known de tipo T. Android llama a ese tipo RTD_TEXT y lo usa con TNF_WELL_KNOWN, como muestra el código de ejemplo de la guía de conceptos básicos de NFC de Android. La carga útil tiene tres partes, en este orden:

  1. Un byte de estado.
  2. El código de idioma, como bytes US-ASCII.
  3. El texto, en la codificación que indica el byte de estado.

Toma la palabra "Hello" en inglés, en UTF-8. La carga útil empieza con 02 65 6E, que es el byte de estado seguido de las letras ASCII "en", y después vienen los bytes del texto: 48 65 6C 6C 6F. No hay nada más entre las partes. No existe un terminador tras el código de idioma ni un campo de longitud para el texto, porque la longitud de la propia carga útil del registro ya lo cubre.

El byte de estado: el bit 7 y la longitud del idioma

El ejemplo de Android construye el byte de estado sumando un indicador de codificación a la longitud del idioma. El indicador vale 0 para UTF-8 y 1 << 7 para UTF-16. En términos sencillos:

  • Bit 7: 0 significa UTF-8, 1 significa UTF-16.
  • Bit 6: reservado. El código de referencia de ndeflib, de código abierto, nunca lo activa al escribir y lo ignora al leer.
  • Bits 0 a 5: la longitud del código de idioma. ndeflib los lee con la máscara 0x3F.

Así, un registro en inglés y UTF-8 empieza con 0x02. El mismo registro en UTF-16 empieza con 0x82. Con un código de idioma de dos letras, esos son los dos valores que verás casi siempre. La documentación de ndeflib dice que un registro de texto lleva "a UTF-8 or UTF-16 encoded text string" (una cadena de texto codificada en UTF-8 o UTF-16), y esas son las únicas dos codificaciones que acepta.

No hemos citado aquí la especificación del registro de texto del NFC Forum, así que toma las posiciones de los bits como lo que hacen el ejemplo de Android y ndeflib, no como una cita del estándar.

Códigos de idioma

El idioma es un identificador IANA o ISO, como en o de, escrito en ASCII. El decodificador de ndeflib rechaza una longitud de idioma igual a cero y otra mayor que la carga útil, y su método de asignación acepta de 1 a 63 caracteres. No comprueba el código contra el registro de IANA, así que un error como ne en lugar de en se guarda sin quejas.

En Android puedes omitir el idioma. La referencia de NdefRecord dice que createTextRecord usa "the language code corresponding to the current default locale" (el código de idioma que corresponde a la configuración regional predeterminada) cuando no indicas ninguno. Es cómodo, pero una etiqueta escrita en un teléfono configurado en alemán dirá de aunque el texto esté en inglés. Si tus lectores filtran o muestran por idioma, indícalo tú.

UTF-16 y la marca de orden de bytes

UTF-16 guarda cada carácter en dos bytes, así que hay que saber cuál va primero. La marca de orden de bytes (BOM) lo resuelve: FE FF para big-endian y FF FE para little-endian. Según el artículo de Wikipedia sobre la marca de orden de bytes, UTF-16 "may or may not begin with a BOM" (puede empezar o no con una BOM), y cuando no hay ninguna, Unicode indica suponer big-endian, aunque el artículo añade que esa presunción se ignora "widely" (ampliamente).

Por eso UTF-16 es la opción frágil para una etiqueta. Un escritor puede incluir la BOM, otro puede omitirla y un lector puede suponer el otro orden de bytes. ndeflib, por ejemplo, usa el códec genérico UTF-16 de Python, que escribe una BOM al codificar y depende de ella al decodificar. UTF-8 no tiene nada de esto: los bytes se leen en orden, y el texto ASCII simple son los mismos bytes en UTF-8 que en ASCII.

Por qué Android usa UTF-8 por defecto

La función auxiliar propia de Android, NdefRecord.createTextRecord(languageCode, text), está documentada como creadora de "a new NDEF record containing UTF-8 text data" (un nuevo registro NDEF que contiene texto UTF-8). El ejemplo más largo de la guía de conceptos básicos de NFC permite elegir UTF-16, pero la función de una línea no.

Un problema de UTF-16 en iPhone, según desarrolladores

Dos desarrolladores del foro de Apple, en un hilo llamado "coreNFC write NDEF with utf8", dicen que NFCNDEFPayload.wellKnownTypeTextPayload(string:locale:) escribe UTF-16 y que el resultado era ilegible en una aplicación de Android. El primer mensaje es de 2019 y un segundo desarrollador hace la misma pregunta en 2020. Ningún ingeniero de Apple respondió, y el hilo no ofrece ninguna solución ni mensaje de error.

Ese hilo no nos permite saber qué hacen las versiones actuales de iOS. Tómalo como un motivo para probar, no como una regla confirmada: escribe una etiqueta con tu herramienta de iPhone, léela en un teléfono Android y mira el primer byte.

Cómo comprobar qué contiene tu etiqueta

Lee la carga útil sin procesar del registro y mira el primer byte. 02 seguido de 65 6E es UTF-8 con "en". 82 significa que el texto es UTF-16, y debes esperar una BOM justo después del código de idioma. Si solo quieres pasar el mensaje a otra etiqueta, consulta copiar el mensaje NDEF en lugar de un volcado sin procesar, y si una reescritura falla, comprueba primero si la etiqueta es de solo lectura.

Una aplicación NFC que muestre los registros sin procesar convierte esto en una comprobación de diez segundos. NFCore lee y escribe NDEF en iPhone y Android. Si una etiqueta muestra 82, reescríbela en UTF-8 con una herramienta que te deje elegir la codificación.

Resumen

Mira el bit 7 del primer byte: 0 es UTF-8 y 1 es UTF-16. Prefiere UTF-8, indica el código de idioma a propósito y, cuando una etiqueta se lea mal en un teléfono, revisa la codificación antes de culpar al chip.

Preguntas frecuentes

¿Tiene que ser UTF-8 un registro de texto NDEF?

No. El registro admite UTF-8 o UTF-16, según el bit 7 del byte de estado. UTF-8 es la opción más segura porque es lo que escribe la función auxiliar de registros de texto de Android.

¿Cuánto puede medir el código de idioma?

La longitud está en los bits bajos del byte de estado. ndeflib lee seis bits, así que hasta 63 caracteres, y rechaza el cero. En la práctica, códigos como en o de son unos pocos caracteres ASCII.

¿Por qué un registro de texto se lee bien en un teléfono y no en otro?

Una causa habitual es el bit de codificación. Un lector que supone UTF-8, o uno que gestiona mal la marca de orden de bytes de UTF-16, puede mostrar texto ilegible. Revisa el primer byte de la carga útil: 02 significa UTF-8 con un idioma de dos letras, y 82 significa UTF-16.