Artículo técnico

Viaje de ida y vuelta de XLSX sin pérdidas en Delphi: Theme, extLst, calcChain

HotXLS, la biblioteca nativa de Excel para Delphi y C++Builder, está construida para viajes de ida y vuelta de XLSX sin pérdidas: abra un libro de trabajo, cambie una celda, guarde, y el tema personalizado del cliente, los bloques de extensión extLst externos y la cadena de cálculo sobrevivirán. Tres mecanismos hacen que eso funcione: el almacenamiento en caché literal de xl/theme/theme1.xml, la reserialización basada en eventos de bloques <ext> desconocidos y un xl/calcChain.xml nuevo y válido según la especificación en cada guardado de un libro de trabajo con fórmulas

El escenario que motiva los tres es alarmantemente común. Un servicio de facturación carga una plantilla que el cliente diseñó en Excel (tema de color corporativo, minigráficos en una columna de KPI, una regla de formato condicional añadida por una versión más nueva de Excel), escribe el total de una factura en la celda B3 y la guarda. El cliente abre el resultado y los colores de la marca han vuelto al azul estándar de Office, los minigráficos han desaparecido y Excel ofrece "reparar" el archivo. Nada en el código tocó ninguna de esas características. La biblioteca lo hizo, simplemente al guardar

¿Por qué los archivos de Excel pierden el formato después de las ediciones de la biblioteca?

Los archivos de Excel pierden el formato después de las ediciones de la biblioteca porque la mayoría de las bibliotecas no editan el archivo, lo reconstruyen. Un paquete .xlsx es un ZIP de partes XML: xl/workbook.xml, un xl/worksheets/sheetN.xml por hoja, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml, y más. Una biblioteca típica analiza esas partes en un modelo de objetos al abrir y regenera cada parte a partir de ese modelo al guardar. Cualquier característica que el modelo no represente (un tema que nunca analizó, un bloque de extensión de un Excel más nuevo) no tiene dónde vivir en la memoria, por lo que la parte regenerada la omite silenciosamente

La norma ECMA-376 anticipó la mitad de este problema. SpreadsheetML define extLst (ECMA-376 Parte 1, el "Área de almacenamiento de datos de funciones futuras", §18.2.10 para el elemento a nivel de libro de trabajo) como un punto de extensión designado: los productores más nuevos colocan características allí, cada una envuelta en un elemento <ext> que lleva un atributo uri que identifica la característica, y se espera que los consumidores más antiguos preserven lo que no entienden. Los minigráficos, los segmentadores y los tipos de formato condicional más nuevos viajan de esta manera. Una biblioteca que descarta bloques <ext> desconocidos no es simplemente defectuosa, sino que viola el contrato de compatibilidad hacia adelante en torno al cual se diseñó el formato. La pregunta que debe plantearse a cualquier biblioteca de hojas de cálculo que esté evaluando es directa: si cambio una celda, qué más cambia

¿Cómo mantiene HotXLS un tema personalizado byte por byte?

HotXLS conserva el tema de un libro de trabajo almacenando en caché los bytes originales de xl/theme/theme1.xml en el momento de la apertura y escribiéndolos de nuevo literalmente en el momento del guardado. La parte del tema (ECMA-376 Parte 1, §14.2.7) es DrawingML, no SpreadsheetML (esquemas de color, esquemas de fuentes, esquemas de formato) y un motor de hojas de cálculo no tiene motivos para modelarlo profundamente. Las versiones anteriores de HotXLS vergonzosamente regeneraban un tema de Office fijo en cada guardado, que es exactamente el fallo de "colores de marca devueltos" mencionado anteriormente; desde la versión v2.89.46, el tema del paquete abierto se almacena sin procesar y se vuelve a emitir intacto, y el tema de Office integrado se genera solo para los libros de trabajo creados desde cero. Los bytes sin procesar son la mayor garantía de fidelidad posible: sin análisis, sin reserialización, sin posibilidad de desviación

La copia literal prevalece deliberadamente sobre el acceso al tema mediante programación. TXLSXWorkbook expone ThemeMajorFont y ThemeMinorFont para que pueda elegir tipos de letra para encabezados y cuerpo en nuevos libros de trabajo, pero cuando se capturó un tema literal al abrir, esos establecedores no tienen ningún efecto en el archivo guardado: el viaje de ida y vuelta tiene prioridad. Si realmente necesita alterar el tema de un libro de trabajo existente, esa es una señal para editar la plantilla en el propio Excel en lugar de hacerlo a través de una API orientada a datos. El caso cotidiano no necesita ninguna API en absoluto:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // the one edit
    Book.SaveAs('branded-invoice-out.xlsx');
    // theme1.xml in the output is byte-identical to the input
  finally
    Book.Free;
  end;
end;

¿Qué sucede con los bloques extLst desconocidos al guardar?

HotXLS captura cada bloque <ext> a nivel de hoja de trabajo que no modela de forma nativa y lo reproduce en la lista extLst de la hoja de trabajo guardada, de modo que las características escritas por versiones más nuevas de Excel sobreviven intactas al viaje de ida y vuelta. Desde la versión v2.131.0, los fragmentos capturados son visibles a través de la propiedad de solo lectura RawWorksheetExts, una lista TStringList en cada hoja de trabajo XLSX, lo que hace que la garantía sea auditable desde el código de prueba en lugar de ser un acto de fe:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('from-newer-excel.xlsx');
    Sheet := Book.Sheets[0];
    WriteLn(Format('%d foreign ext block(s) captured',
      [Sheet.RawWorksheetExts.Count]));
    for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
      WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // peek at each uri
  finally
    Book.Free;
  end;
end;

El detalle de implementación que vale la pena conocer es que la captura es una reserialización a nivel de evento, no una copia de bytes sin procesar. El lector XML de transmisión de HotXLS no expone desplazamientos de origen, por lo que el subárbol desconocido se reconstruye a partir de eventos Element, Text y EndElement a medida que pasan. Ese enfoque oculta una trampa clásica: un elemento de autocierre como <a/> genera solo un evento Element marcado como vacío y nunca un EndElement, por lo que cualquier contador de profundidad que disminuya únicamente en EndElement nunca verá cerrarse el subárbol. Gestiónelo, y el fragmento reconstruido será semánticamente equivalente al original (las comillas de los atributos y las formas de autocierre se normalizan, por lo que no es idéntico a nivel de bytes, pero Excel lee significado, no bytes). Dos propiedades de la propia salida de Excel hacen que la reproducción sea segura: Excel declara los atributos xmlns necesarios en el elemento <ext> o dentro de él, por lo que cada fragmento capturado es autónomo en cuanto a espacios de nombres, y esa misma autonomía es la razón por la que duplicar una hoja de trabajo dentro de un libro de trabajo o entre ellos puede transportar los bloques externos junto con una simple asignación de lista de cadenas

Escribir calcChain.xml para que Excel confíe en sus fórmulas

HotXLS escribe xl/calcChain.xml (la parte de la cadena de cálculo, ECMA-376 Parte 1, §12.3.1) siempre que el libro de trabajo guardado contenga fórmulas, y elige entre dos ordenaciones. Si el gráfico de dependencias de fórmulas ya se ha creado y está actualizado (llamó a Recalculate después de su última edición), la cadena se emite en orden topológico completo, las dependencias antes que los dependientes, con cualquier miembro de referencia circular añadido al final. De lo contrario, las celdas se enumeran en el orden del documento. Ambas son correctas: las notas de implementación de Microsoft para el formato, [MS-XLSX], tratan la cadena de cálculo como una sugerencia que Excel verifica y reordenada durante la carga, por lo que cualquier lista completa es legal, y HotXLS se niega deliberadamente a forzar la creación de un gráfico dentro de SaveAs (la construcción de aristas es cuadrática en el recuento de celdas, un coste oculto inaceptable en un guardado de un millón de celdas)

Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

Dónde termina el viaje de ida y vuelta sin pérdidas

La honestidad importa más que una casilla de verificación de marketing aquí, por lo que los límites merecen la misma importancia. HotXLS no copia todo el paquete byte por byte: el XML de la hoja de trabajo, los estilos, las cadenas compartidas y las partes del libro de trabajo se regeneran a partir del modelo analizado, por lo que la salida es semánticamente fiel pero no binaria-idéntica; solo los encabezados locales de ZIP llevan marcas de tiempo DOS nuevas. Los fragmentos <ext> capturados regresan normalizados, como se describió anteriormente. Las modificaciones de fuentes de tema mediante programación se ignoran cuando hay un tema literal presente. Y la red de preservación tiene una malla definida: características que HotXLS modela de forma nativa (los minigráficos, por ejemplo, se analizan y se vuelven a escribir en lugar de copiarse a ciegas) más el contenido extLst externo más las partes almacenadas en caché literalmente. Una parte que no está modelada ni se encuentra dentro de un punto de extensión (por ejemplo, la parte personalizada de un complemento exótico) queda fuera de los tres mecanismos que cubre este artículo, así que pruebe sus plantillas reales en lugar de asumirlo

El trabajo de preservación adyacente completa el panorama. Los proyectos de VBA y las referencias a libros de trabajo externos superan el guardado con la misma filosofía de mantener lo que no se modela, cubierta en el artículo complementario sobre la preservación de VBA y enlaces externos, y las propiedades del documento en docProps tienen su propia API de lectura-escritura en lugar de descartarse silenciosamente. Cuando evalúe cualquier biblioteca de hojas de cálculo, ejecute la prueba de una sola celda: abra un libro de trabajo de producción rico en características, cambie un único valor, guárdelo y compare (diff) las partes descomprimidas con el original. Lo que haya cambiado más allá de la hoja que tocó le dirá más sobre la biblioteca que cualquier matriz de características

Los mecanismos de viaje de ida y vuelta descritos aquí (retención de temas locales desde v2.89.46, captura externa de extLst y emisión de calcChain.xml desde v2.131.0) se distribuyen en el actual HotXLS Delphi Excel Component, cuya página de producto documenta el conjunto completo de características de lectura y escritura de XLSX para Delphi y C++Builder