Artículo técnico

HotXLS Delphi Component: CSV, TSV, HTML, and RTF export in Delphi

Imagine un job nocturno que construye un libro de trabajo de facturas en código y lo escribe como CSV para que un sistema posterior lo importe. Los números se ven correctos en Excel. El CSV se abre limpiamente en un editor de texto. Entonces el importador se atraganta con la columna de totales, porque el campo de importe de la fila 42 dice =SUM(D2:D41), la fórmula como texto literal, no la cifra que debería calcular. Nada está roto. Este es un comportamiento documentado, y es lo primero que hay que entender sobre exportar desde HotXLS: el escritor serializa el modelo de celdas exactamente tal como está, y una celda de fórmula cuyo valor nunca se calculó solo tiene su texto de fórmula para entregar

Por qué su CSV contiene fórmulas en lugar de números

HotXLS almacena el texto de la fórmula y el valor calculado como dos cosas separadas. SaveAsCSV no ejecuta el motor de cálculo al exportar, por diseño: una exportación no debería mutar el libro de trabajo, y no debería arriesgarse a quedar bloqueada en una cadena de fórmulas patológica. Los archivos que el propio Excel guardó llevan resultados en caché junto a las fórmulas, de modo que volver a exportarlos se comporta como cabría esperar. La trampa es específica de los libros de trabajo que generó su propio código, donde las fórmulas se escribieron pero nunca se evaluaron. La solución es hacer que los valores existan antes de exportar, usando el mismo motor Calculate que resuelve las referencias entre hojas y las funciones personalizadas:

Diagrama que muestra una celda de libro HotXLS de Delphi conteniendo solo texto de fórmula hasta que Book.Calculate calcula el valor, de modo que la exportación CSV emite un número en lugar de texto =SUM
SaveAsCSV serializa el modelo de celdas tal como está — sin Calculate, el campo de importe lleva texto de fórmula literal y el importador lo rechaza
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('invoice-run.xlsx');
    Sheet := Book.Sheets[0];

    // Materializa los resultados de las fórmulas para que el CSV lleve números, no texto '=...'
    for R := 2 to 41 do
      if Sheet.Cells[R, 4].Formula <> '' then
        Sheet.Cells[R, 4].Value := Book.Calculate(Sheet.Cells[R, 4].Formula);

    Book.SaveAsCSV('feed.csv', 0, ',');    // hoja 0, coma
    Book.SaveAsCSV('feed.tsv', 0, #9);     // misma hoja como TSV
  finally
    Book.Free;
  end;
end;

Observe lo que realmente hace el bucle: sobrescribe las celdas de fórmula con sus valores calculados. Eso es exactamente correcto para un paso de exportación desechable, y erróneo si pretende volver a guardar el libro de trabajo como .xlsx después, porque acaba de sustituir fórmulas vivas por números congelados. Exporte desde una copia, o acote la reescritura para que solo afecte a la ejecución de exportación. El motor detrás de Calculate va más allá de esto, incluyendo el registro de sus propias funciones, que es el tema de el motor de fórmulas de HotXLS y las funciones personalizadas

Qué garantiza el escritor delimitado

La ruta CSV produce UTF-8 con marca de orden de bytes, finales de línea CRLF, y comillado según RFC 4180. Cualquier campo que contenga el delimitador, una comilla, o un salto de línea se envuelve entre comillas, y las comillas incrustadas se duplican. Las fechas se representan como yyyy-mm-dd hh:nn:ss independientemente del formato de visualización de la celda. Esa es la decisión correcta para un consumidor automático, aunque sorprende a cualquiera que esperase que el formato en pantalla se trasladase. Las celdas de texto enriquecido se aplanan concatenando sus fragmentos

Diagrama del escritor delimitado único de HotXLS en Delphi produciendo CSV con coma y TSV con #9 mientras ambas salidas comparten BOM UTF-8, finales CRLF y entrecomillado RFC 4180
CSV y TSV salen del mismo escritor, de modo que el BOM UTF-8, los finales CRLF y el entrecomillado RFC 4180 se aplican a ambos sin cambios

Esos valores por defecto zanjan la mayoría de las discusiones con un importador antes de que empiecen, pero dos de ellos pertenecen de todos modos a su contrato de interfaz. El primero es el BOM. Es lo que permite a Excel abrir el archivo con los caracteres acentuados intactos, aunque un puñado de analizadores estrictos tratan esos tres bytes como datos; si el suyo es uno de ellos, elimínelos en la entrega. El segundo es TSV. No es en absoluto una función independiente, sino simplemente el mismo escritor llamado con #9 como delimitador, de modo que todo lo anterior se aplica sin cambios. La hoja que se exporta se elige mediante un índice de base 0 en la sobrecarga de múltiples argumentos, mientras que la forma abreviada de un solo argumento SaveAsCSV(FileName) toma la hoja activa

La exportación a HTML es una instantánea, no un formato de intercambio

Donde CSV descarta todo excepto los valores, SaveAsHTML intenta conservar la apariencia: un <table> por hoja, regiones combinadas expresadas como colspan y rowspan, estilo básico de celda incrustado como CSS. Los colores relativos al tema se omiten en lugar de resolverse, de modo que una plantilla que se apoya en ranuras de tema sale más simple de lo que se ve en Excel. Establezca colores RGB explícitos en todo lo que tenga que sobrevivir al viaje. El objeto de opciones controla el envoltorio:

var
  Opts: TXLSXHtmlExportOptions;
begin
  Opts := TXLSXHtmlExportOptions.Create;
  try
    Opts.Title := 'Weekly settlement';
    Opts.TableClass := 'report-grid';     // gancho para la hoja de estilos de la página anfitriona
    Opts.WriteDocument := True;           // página completa, no un fragmento
    if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
      raise Exception.Create('Sheet index out of range');
  finally
    Opts.Free;
  end;
end;

Dos detalles de ese fragmento merecen atención. Cambie WriteDocument a False y la salida se convierte en un fragmento de tabla desnudo en lugar de una página completa, que es lo que quiere cuando inyecta una vista previa en un diseño ya existente: establezca TableClass y deje que la hoja de estilos anfitriona se encargue del tema. La convención de retorno también es la inversa de la mayoría de las llamadas de HotXLS. SaveAsHTML devuelve 0 en caso de éxito y -1 para un índice de hoja incorrecto, de modo que una comprobación hecha por costumbre de = 1 reportará como fallo cada exportación exitosa. Cuando necesite una región en lugar de una hoja completa, quizá para enviar por correo o incrustar un único bloque, TXLSXRange.SaveAsHTML exporta cualquier rango rectangular bajo las mismas reglas de renderizado

La salida RTF y dónde todavía se gana su lugar

El cuarto destino escribe tablas RTF 1.6, una hoja por llamada mediante SaveAsRTF. Los anchos de columna se aproximan a razón de unos 96 twips por carácter de ancho de columna. La limitación estructural que conviene conocer es que las celdas combinadas no se extienden en la salida: solo la celda ancla lleva su contenido, y las celdas cubiertas se emiten en blanco. Eso descarta RTF para plantillas cargadas de maquetación. Aun así se gana su lugar como el camino de menor resistencia para volcar resultados tabulares en un procesador de textos o en un sistema heredado de gestión documental anterior a la ingesta de HTML

Ida y vuelta: importar CSV es destructivo por diseño

Volver a leer un CSV tiene su propio contrato. OpenCSV borra todo el libro de trabajo y lo reconstruye como una única hoja llamada Sheet1. Es un constructor en espíritu, no una fusión, así que nunca lo llame sobre un libro de trabajo que todavía contenga contenido sin guardar. Pasar #0 como separador activa la detección automática de delimitador. El indicador ADetectTypes controla la promoción de tipos: con él activado, las cadenas numéricas se convierten en números, las cadenas ISO-8601 se convierten en fechas, y true/false se convierten en booleanos. Desactívelo cuando el feed lleve identificadores con ceros a la izquierda, códigos postales, o códigos de producto, todos los cuales la promoción destroza silenciosamente convirtiéndolos en números (un cero a la izquierda simplemente desaparece en el momento en que 00123 se convierte en 123). Ambas fachadas exponen la misma importación. Combínela con las llamadas de exportación anteriores y tendrá un puente de formatos que no necesita Excel instalado en ningún punto del proceso, el escenario que cubre la generación de informes de base de datos a Excel con HotXLS

Exportar directamente a un stream

Cada escritor aquí tiene una sobrecarga de stream justo al lado de la versión con nombre de archivo: CSV, HTML, RTF, y los propios formatos de libro de trabajo. En código de servidor, esas sobrecargas son las que hay que usar. Un endpoint web que sirve una descarga CSV puede escribir en un TMemoryStream y entregarlo directamente al objeto de respuesta, sin archivo temporal, sin job de limpieza, y sin colisión entre dos solicitudes que resultaron elegir el mismo nombre generado. Lo mismo vale para enviar exportaciones a almacenamiento de blobs o adjuntarlas a correo saliente. El sistema de archivos desaparece por completo de la ecuación

Ese patrón se combina bien con la forma en que se despliega la biblioteca. Ambas fachadas son lectores y escritores nativos de Object Pascal, así que no hay instalación de Excel, ni automatización COM, ni cuello de botella por proceso que serialice las solicitudes en el servidor. Cada solicitud puede tener su propio objeto de libro de trabajo, ejecutar la reescritura de cálculo de la primera sección, y transmitir su exportación en paralelo con sus vecinas. La memoria es el único recurso que hay que vigilar. El modelo del libro de trabajo vive en RAM durante toda la exportación, así que un servicio que abre archivos muy grandes solo para reemitirlos como CSV debería limitar los jobs concurrentes, o poner en cola los que sean excesivamente grandes, en lugar de dejar que un pico de tráfico decida el conjunto de trabajo

Un ajuste más pequeño: active IncludeBOM en las opciones de HTML cuando el fragmento vaya a guardarse como un archivo independiente que alguna herramienta posterior analiza para detectar la codificación. Cuando sirva HTML directamente por HTTP, deje en cambio la declaración de charset a las cabeceras de la respuesta

Cuando los bytes siguen saliendo mal

La pregunta de soporte más habitual sobre la exportación a CSV es el problema inicial con otro disfraz: Excel muestra mojibake en lugar de caracteres acentuados. El instinto es culpar al escritor, pero este emite un BOM UTF-8 precisamente por esta razón, y el archivo casi siempre es correcto cuando sale de su código. Algo entre ese punto y Excel se comió el BOM. Una transferencia FTP en modo texto, una copia de stream que se salta los tres primeros bytes, un proxy que recodifica al pasar: cualquiera de ellos eliminará la marca y dejará que Excel adivine la codificación, cosa que hace mal. Diagnostique esto en el límite, no en la llamada de exportación. Abra el archivo entregado en un visor hexadecimal y confirme que EF BB BF sigue siendo lo primero que contiene

Diagrama que rastrea cómo un BOM UTF-8 correcto escrito por la exportación CSV de HotXLS en Delphi se elimina por una transferencia FTP en modo texto o un proxy de recodificación, dejando que Excel muestre mojibake
El escritor emite EF BB BF correctamente — el mojibake aparece solo cuando un transporte elimina el marcador, de modo que diagnostique los bytes entregados en un visor hexadecimal

Ese es el hilo conductor de los cuatro formatos. La llamada de exportación es la parte fácil, y HotXLS toma una decisión defendible en cada disyuntiva que enfrenta el escritor. Los fallos viven en las costuras, donde el texto de una fórmula se encuentra con un analizador que esperaba un número, donde un BOM se encuentra con un transporte que no lo conserva, donde una celda combinada se encuentra con el modelo de tabla plano de RTF. Cada uno de esos es un hecho que hay que escribir en el contrato entre su exportador y lo que sea que lo consuma, porque el consumidor no puede leer sus intenciones a partir de los bytes. Para la lista completa de métodos en ambas fachadas de libro de trabajo, la página de producto de HotXLS Delphi Component lleva la referencia completa