Artículo técnico

Abrir y guardar archivos ODS en Delphi con HotXLS

Un backend de informes en Delphi que lleva años emitiendo .xlsx recibe un requisito nuevo: las reglas de contratación de un cliente del sector público exigen salida en OpenDocument Spreadsheet, y los analistas de esa cuenta devuelven sus ediciones como archivos .ods guardados desde LibreOffice. Así que ahora el mismo código tiene que escribir ODS y leerlo. HotXLS, la biblioteca nativa de hojas de cálculo en Object Pascal de losLab para Delphi y C++Builder, maneja ambas direcciones sin Excel ni LibreOffice instalados en ningún lado. Lo que no hace es volver simétricas las dos direcciones. La exportación lleva mucho más de lo que la importación recupera, y un equipo que suponga lo contrario verá evaporarse fórmulas y formato en algún punto entre la revisión del cliente y el siguiente informe, sin ningún error al que señalar

El soporte de ODS vive en la fachada XLSX, no en la XLS

HotXLS incluye dos jerarquías de clases independientes en un solo paquete: TXLSWorkbook en la unidad lxHandle para archivos .xls binarios BIFF8, y TXLSXWorkbook en la unidad lxHandleX para paquetes .xlsx OOXML. Cada punto de entrada de OpenDocument (OpenODS, SaveAsODS, GetODSSheetNames) cuelga de TXLSXWorkbook. La ubicación no es arbitraria. Un paquete ODS, tal como lo especifica OASIS ODF 1.3, es un archivo zip que lleva un miembro mimetype, un manifiesto y un cuerpo content.xml, lo que lo convierte en primo estructural del zip OOXML; BIFF8 es un flujo binario de registros de los años noventa sin nada en común

Esa ubicación tiene una consecuencia práctica: un libro de trabajo .xls heredado no puede convertirse en .ods en una sola llamada. Primero tiende un puente del contenido BIFF al modelo XLSX con SaveXLSWorkbookAsXLSX de la unidad lxXlsxExport, reabre el resultado mediante TXLSXWorkbook y luego exporta desde ahí. El puente no es sin pérdidas, y conviene conocer los huecos antes de construir sobre él. Copia valores, fórmulas, formatos numéricos, fuentes, rellenos y anchos de columna. Descarta bordes, rangos combinados, comentarios, gráficos y formato condicional. Un origen .xls con formato intenso llegará a ODS más sencillo de lo que salió, y esa es una propiedad del puente, no del escritor de ODS

La detección del lado de la importación es automática. El método Open simple reconoce un paquete ODS por su miembro mimetype, recurriendo a una comprobación de content.xml de nivel superior cuando ese miembro falta, así que una ruta de código genérica de "abrir lo que sea que subió el usuario" no necesita su propia detección por extensión. Después de abrir, la propiedad SourceFormat informa qué rama se activó

Diagrama de la distribución de clases de HotXLS en Delphi donde cada punto de entrada de ODS vive en TXLSXWorkbook y un puente SaveXLSWorkbookAsXLSX traslada el contenido .xls BIFF8
Cada punto de entrada de OpenDocument cuelga de TXLSXWorkbook, y un .xls heredado llega a ODS solo a través del puente con pérdidas de BIFF a XLSX

Exportar a ODS con TODSExportOptions

La llamada de exportación en sí es una sola línea; el objeto de opciones que la rodea lleva las decisiones por las que un revisor preguntará más tarde:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // el llamador es dueño de esto y lo libera
    try
      Opts.Generator := 'ReportService 4.2'; // sobrescribe meta:generator
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

El objeto de opciones es propiedad del llamador. HotXLS no lo liberará, y por eso el try..finally interno está ahí y no es opcional. Las dos propiedades que cambian la salida, en lugar de solo etiquetarla, merecen una mirada más de cerca. Fijar IncludeCharts := False hace más que ocultar gráficos: elimina del paquete los subdocumentos de gráfico y sus entradas de manifiesto, que es exactamente lo que quiere cuando el consumidor es un pipeline de datos que tropezaría con ellos. Generator sobrescribe la cadena meta:generator de ODF, que de otro modo dice HotXLS/<version>; sobrescríbala cuando las herramientas posteriores identifiquen a los productores de archivos para enrutar el soporte. Si nada de eso aplica, omita por completo el objeto de opciones. Llamar a SaveAs(FileName, xlsxOpenDocumentSpreadsheet) es lo mismo que SaveAsODS con valores predeterminados, y las sobrecargas de stream de ambos le permiten escribir el paquete directamente en una respuesta HTTP sin archivo temporal

Qué lee la ruta de importación, y qué omite deliberadamente

Lea esta parte con cuidado antes de prometerle a alguien fidelidad de ida y vuelta. La importación de ODS en HotXLS es deliberadamente una ruta ligera. Conserva los valores escalares de las celdas y el resultado en caché que cada fórmula llevaba al momento de guardar, y expande las filas y columnas repetidas hacia la cuadrícula. No trae estilos, expresiones de fórmula ODS ni dibujos

La decisión sobre las fórmulas es la que tiene más probabilidades de morder, y se tomó a propósito. Una celda ODF almacena dos cosas lado a lado: la expresión de fórmula, escrita en el dialecto OpenFormula definido en ODF 1.3 Parte 4, y el último valor que la aplicación productora calculó para ella. Traducir OpenFormula a sintaxis de fórmula de Excel es un problema de conversión de dialectos por derecho propio, con casos límite reales en torno a vocabularios de funciones, sintaxis de referencias y modelos de error. Leer el valor en caché en su lugar evita toda esa clase de errores de traducción silenciosos, así que los números que importa son exactamente los números que el remitente vio por última vez. El costo es que llegan como números, no como las fórmulas vivas que los produjeron

El modo de fallo alrededor del cual diseñar se deduce directamente: una hoja de cálculo cuyos totales eran correctos cuando LibreOffice la guardó por última vez se importa con números correctos, pero esos números ahora son constantes. Edite una celda de entrada, recalcule, y nada se mueve: la fórmula desapareció, solo queda su resultado final. Si el flujo de trabajo necesita fórmulas vivas después de la importación, restablézcalas programáticamente a partir de sus propias reglas de negocio mediante Cell.Formula, que en la fachada XLSX recibe la expresión sin el signo igual inicial

Diseñar en torno a la ida y vuelta asimétrica

La exportación se renderiza desde el modelo completo del libro de trabajo en memoria: valores, estilos y, si los pide, gráficos e imágenes. La importación devuelve solo valores. Así que el tramo de .xlsx a .ods es de alta fidelidad, y el tramo de .ods a .xlsx trae de vuelta valores y resultados en caché pero sin estilo y sin fórmulas vivas. Encadene los dos y la asimetría se acumula. Un ciclo completo de .xlsx a .ods a .xlsx escribe todo fielmente a la salida y pierde los estilos y las fórmulas en el camino de regreso, aunque nada haya salido mal en ninguno de los dos pasos

Diagrama de la ida y vuelta asimétrica de ODS con HotXLS desde Delphi: exportación de fidelidad completa desde el modelo de libro de trabajo en memoria y una importación de solo valores que deja las fórmulas como constantes
La exportación renderiza el modelo completo en memoria mientras que la importación devuelve valores y resultados en caché, así que un ciclo completo de .xlsx a .ods a .xlsx descarta silenciosamente estilos y fórmulas vivas
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // formato detectado automáticamente
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Los valores y los resultados de fórmula en caché están presentes
    // tras una importación ODS; los estilos y las fórmulas vivas no.
    // Reconstruya lo que necesite el pipeline posterior antes de guardar.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

El patrón arquitectónico que se desprende de esto: trate los archivos .ods entrantes como fuentes de datos, no como documentos para editar en el lugar. Mantenga el libro de trabajo canónico en .xlsx, lea los valores de las revisiones del cliente y emita ODS nuevo bajo demanda desde la copia canónica. La verificación corresponde a ambos bandos: abra los archivos exportados en LibreOffice Calc, el consumidor ODF de referencia, y en Excel, que lee ODS desde hace años pero discrepa de LibreOffice en los bordes del soporte de gráficos y estilos. El conteo de hojas, un puñado de celdas clave y la presencia de gráficos constituyen una prueba de humo suficiente por perfil de exportación

Clasificar un archivo ODS antes de comprometerse con una importación

Cuando un endpoint acepta cargas, listar los nombres de las hojas es mucho más barato que un análisis completo y atrapa temprano las sorpresas estructurales:

Diagrama de la puerta de clasificación de cargas de HotXLS en Delphi donde GetODSSheetNames rechaza paquetes ODS ilegibles y hojas faltantes antes de que se ejecute una importación completa
Una sonda GetODSSheetNames cuesta mucho menos que un análisis completo y atrapa el fallo de la hoja renombrada mientras el error todavía puede nombrar el archivo
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
    raise Exception.Create('not a readable ODS package');
  if Names.IndexOf('Data') < 0 then
    raise Exception.Create('revision is missing the Data sheet');
finally
  Book.Free;
  Names.Free;
end;

La convención de retorno hace tropezar a la gente: las llamadas de HotXLS generalmente devuelven un conteo positivo o 1 en caso de éxito y -1 en caso de fallo, vaciando la lista al fallar, así que compruebe <= 0 en lugar de comparar contra un valor positivo específico. GetODSSheetNames ni reinicia ni puebla la instancia del libro de trabajo, así que un solo objeto de sondeo puede examinar todo un directorio de archivos entrantes. Comprobaciones estructurales como esta atrapan el fallo más común del mundo real (un analista que renombra o elimina una hoja antes de devolver la revisión) en la puerta, donde el mensaje de error todavía puede nombrar el archivo y la hoja faltante en lugar de aflorar como una referencia nil tres capas más abajo

Si está construyendo un pipeline de conversión más amplio en torno a esto, el patrón de banco de trabajo de auditoría y conversión de libros muestra cómo inventariar las funciones de un archivo antes de elegir un formato de destino, y la guía de rendimiento con libros de trabajo grandes mantiene las exportaciones por lotes dentro de límites de memoria razonables

HotXLS es una biblioteca nativa de hojas de cálculo para Delphi y C++Builder con código fuente completo; la lista completa de funciones y los detalles de licencia están en la página de producto de HotXLS Delphi Component