Technical Article

Exportación de hojas de cálculo segura para Unicode en Delphi: RTF y HTML

Una hoja de cálculo contiene una columna de nombres de clientes. Algunos están en chino, otros en cirílico, unos pocos llevan diéresis alemanas o un acento francés. Si la exporta a CSV y abre el resultado, cada carácter está intacto. Exporta el mismo libro de trabajo a RTF para una plantilla de combinación de correspondencia, lo abre en un procesador de textos y los nombres que no son ASCII se han convertido en filas de signos de interrogación. Los datos nunca cambiaron; lo que cambió es el contrato de codificación del formato en el que escribió, y cada ruta de exportación conlleva uno diferente.

Esta es la trampa que atrapa a una biblioteca que parece totalmente compatible con Unicode en la superficie. El texto de la celda se mantiene internamente como WideString, por lo que el modelo nunca pierde ningún carácter. La pérdida ocurre en el límite, en el escritor que debe serializar ese texto en un formato con sus propias reglas sobre qué bytes son legales y cómo debe codificarse cualquier elemento fuera del rango legal. Desarrollar un escritor correcto no evita que distribuya otro que altere el mismo texto. La solución no es un interruptor global; es una decisión separada y correcta en cada ruta.

RTF es un formato seguro para 7 bits por diseño

Rich Text Format (RTF) es anterior a Unicode y se especificó para sobrevivir a transportes que solo transmiten caracteres ASCII imprimibles. Un documento RTF declara una página de códigos en su encabezado, y cualquier carácter que el escritor no pueda representar en esa página de códigos debe emitirse como un escape en lugar de un byte sin procesar. El escape correspondiente es \\u, que lleva una unidad de código de 16 bits con signo seguida de un carácter ASCII de respaldo para lectores demasiado antiguos para entender el escape.

HotXLS escribe RTF de esta manera. El encabezado del documento se inicia declarando la página de códigos, en la forma \\ansi\\ansicpg1252\\uc1, y el escritor en la unidad lxRTF recorre cada cadena emitiendo cualquier carácter por encima del ASCII simple como un escape \\u para que el flujo de bytes se mantenga limpio en 7 bits, independientemente de lo que pueda contener la página de códigos declarada. Un punto de código como U+4E2D se convierte en la secuencia literal \u20013?, no en un byte sin procesar que un visor intentaría interpretar a través de cualquier página de códigos que decidiera asumir. Sin esa disciplina, cualquier elemento fuera de la página de códigos declarada carece de representación de bytes legal, y un escritor que emita el valor bruto produce los signos de interrogación que abrieron este artículo.

El detalle que se debe tener en cuenta es que la página de códigos declarada y los escapes son dos partes de un único contrato. Declarar solo la página de códigos no ayuda al texto que se encuentra fuera de ella. Emitir escapes sin una página de códigos declarada deja los caracteres de respaldo ambiguos. Ambos deben ser correctos en conjunto, razón por la cual un escritor que solo maneja uno de ellos sigue fallando en el primer libro de trabajo multilingüe.

El escape HTML es más que corchetes angulares

La exportación a HTML produce un documento de múltiples hojas cuyos marcos de navegación contienen los nombres de las hojas como texto visible. Esos nombres son cadenas controladas por el autor que pueden contener cualquier carácter, incluidos aquellos con significado especial en el marcado. Una hoja llamada literalmente Q1 & Q2 <draft> debe llegar a la página como entidades de escape; de lo contrario, los corchetes angulares abren una etiqueta fantasma y el ampersand inicia una referencia de entidad que nunca se planeó. Este es el escape HTML ordinario, y omitirlo en una etiqueta de marco es la clase de descuido que supera cualquier prueba realizada con nombres de hojas en ASCII puro.

La cuestión de la codificación se sitúa un nivel más abajo. Cuando los caracteres que no son ASCII llegan a un contexto en el que no se garantiza que se sirvan como UTF-8, la representación segura es una referencia de carácter numérico, de modo que U+00E9 se escribe como &#233; en lugar de como un byte sin procesar cuyo significado dependa del conjunto de caracteres de respuesta. La imagen especular de esta regla se aplica en la entrada. Un libro de trabajo que se lee de vuelta desde XLSX contiene cadenas compartidas en las que un carácter ya puede estar almacenado como una entidad XML numérica, y esa entidad debe decodificarse en un carácter completo antes de ingresar al modelo de celda. Si se decodifica descuidadamente, dividiendo un punto de código en bytes separados, un solo carácter vuelve a surgir como dos fragmentos de mojibake que ninguna exportación posterior podrá reparar.

El contenedor XLSX es un archivo ZIP, y ZIP tiene su propia codificación de nombres

Un archivo XLSX es un archivo ZIP, y el archivo almacena un nombre para cada miembro que contiene. ZIP es lo suficientemente antiguo como para que su especificación original no dijera nada sobre la codificación de esos nombres, por lo que un lector que no encuentra ninguna señal asume la página de códigos del archivo. Esa suposición es incorrecta en el momento en que el nombre de un miembro contiene un carácter no ASCII, lo que ocurre con nombres de secciones de hojas de trabajo localizados y con archivos multimedia incrustados cuyos nombres llevan acentos o caracteres no latinos.

La solución consiste en un único bit. El bit 11 de propósito general en cada encabezado de archivo local declara que el nombre del miembro está codificado en UTF-8. HotXLS verifica exactamente ese bit cuando lee un archivo, evaluando las banderas de propósito general contra la máscara $0800, y un lector o escritor que lo ignore interpretará de forma incorrecta un nombre que una implementación correcta almacenó como UTF-8. El bit es fácil de establecer y de respetar, y representa toda la diferencia entre un nombre de miembro que sobrevive al viaje de ida y vuelta y uno que llega dañado antes de que se analice el contenido de la hoja de cálculo.

El plegado de casos (case folding) y el escaneo de números ocultan el mismo peligro

La evaluación de fórmulas es el punto donde la seguridad de Unicode deja de relacionarse con la serialización y pasa a tratarse de comparación. La función SEARCH no distingue entre mayúsculas y minúsculas, lo que significa que debe realizar la conversión correspondiente antes de buscar una subcadena. La forma incorrecta de realizar esta conversión es mediante la página de códigos ANSI, porque convertir texto no ASCII a mayúsculas de esa manera dirige los caracteres a través de una página de códigos estrecha y daña cualquier elemento fuera de ella. La forma correcta es la conversión a mayúsculas de cadena ancha (wide-string), que conserva todo el rango UTF-16. HotXLS realiza la conversión con WideUpperCase por esta razón exacta, de modo que una búsqueda de texto con acentos o caracteres no latinos coincide con los mismos caracteres recibidos en lugar de una aproximación dañada por la página de códigos.

El analizador de tokens de fórmulas conlleva una obligación relacionada que no tiene nada que ver con las letras y todo que ver con dónde termina un token. La notación científica como 1E3 o 2.5E-3 representa un único literal numérico, y el escáner debe reconocer la E, un signo opcional y los dígitos siguientes como parte del número en lugar de dividir la entrada en un nombre seguido de un número separado. Un escáner que maneje esto de forma incorrecta convierte una constante perfectamente válida en un error de análisis o, peor aún, en una expresión silenciosamente errónea. Pertenece a la misma discusión porque ambos casos tratan sobre un lector que toma una decisión correcta a nivel de caracteres: uno sobre cómo convertir un carácter para comparación y el otro sobre si un carácter continúa el token actual.

Construir y exportar un libro de trabajo multilingüe

La API pública no le pide que piense en nada de esto. Usted construye el libro de trabajo a partir de valores de celda WideString y llama al punto de entrada de exportación que desee. Las decisiones de codificación ocurren dentro de cada escritor. El ejemplo a continuación inicializa una hoja con texto en varias escrituras y luego escribe un archivo RTF y un archivo HTML a partir del mismo libro de trabajo, de modo que ambas rutas se ejecutan contra la misma entrada.

uses
  lxHandle;

procedure ExportMultilingualWorkbook;
var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Customers');

    Sheet.Cells[1, 1].Value := 'Name';
    Sheet.Cells[1, 2].Value := 'City';

    // Cell text is held as WideString, so every script survives the model.
    Sheet.Cells[2, 1].Value := '王伟';          // Chinese
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // German umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Cyrillic
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // French accents
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: the lxRTF writer declares the code page and emits every
    // non-ASCII character as a \u escape, keeping the file 7-bit clean.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: sheet names are HTML-escaped and non-ASCII text is written
    // so it does not depend on a guessed response charset.
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Ambas llamadas devuelven un estado Integer y ambas consumen el mismo texto en memoria. Nada en el código de llamada declara una página de códigos o escapa un carácter, porque la responsabilidad recae en el escritor que conoce su propio formato. El método SaveAsCSV a nivel de libro de trabajo sigue la misma estructura si necesita una exportación delimitada desde la misma fuente.

// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');

La seguridad de Unicode es por ruta, no por biblioteca

La lección que vale la pena asimilar es que no existe un único lugar para garantizar la seguridad de Unicode. RTF necesita una página de códigos declarada más escapes \\u. HTML necesita escape de entidades para caracteres significativos en el marcado y referencias numéricas donde el conjunto de caracteres no esté garantizado, además de una decodificación correcta de las entidades que llegan en cadenas compartidas. El contenedor ZIP necesita que el bit 11 de propósito general esté establecido para que un nombre de miembro en UTF-8 se lea correctamente. La evaluación de fórmulas necesita plegado de mayúsculas/minúsculas de cadenas anchas y un analizador de tokens que mantenga la notación científica en una sola pieza. Cada uno de estos es un contrato diferente, y una biblioteca puede cumplir uno mientras viola silenciosamente otro. Esa es la razón por la cual una herramienta que maneja correctamente CSV aún puede entregarle un archivo RTF lleno de signos de interrogación.

Si sus exportaciones se inclinan por los formatos delimitados, los compromisos entre ellos se cubren en nuestro recorrido sobre la exportación a CSV, TSV y HTML, y cuando el origen es un conjunto de resultados en lugar de una hoja construida a mano, los patrones en la exportación de bases de datos para informes de Delphi se complementan de forma natural con las reglas de codificación descritas aquí. Todo esto se incluye como parte del HotXLS Component para Delphi y C++Builder, junto con las API de lectura, fórmulas y formato tratadas en otras secciones de este blog.