Artículo técnico

Ciclo completo de XLSX sin pérdidas en Delphi: Temas, extLst, calcChain

HotXLS, la biblioteca nativa de Excel para Delphi y C++Builder, está diseñada para ciclos completos de XLSX sin pérdidas: abra un libro de trabajo, modique 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 esto funcione — el almacenamiento en caché literal de xl/theme/theme1.xml, la nueva serialización basada en eventos de bloques <ext> desconocidos y un archivo xl/calcChain.xml nuevo y válido según la especificación en cada guardado de un libro con fórmulas

El escenario que motiva estos tres aspectos es lamentablemente 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 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 formato después de las ediciones de una biblioteca?

Los archivos de Excel pierden formato después de las ediciones de una biblioteca porque la mayoría de las bibliotecas no editan el archivo; lo reconstruyen. Un paquete .xlsx es un archivo 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 bxque de extensión de un Excel más nuevo — no tiene dónde residir 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 características 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, segmentadores de datos y los tipos de formato condicional más nuevos viajan de esta manera. Por lo tanto, una biblioteca que descarta bloques <ext> desconocidos no solo causa pérdidas, sino que viola el contrato de compatibilidad hacia adelante sobre el cual se diseñó el formato. La pregunta que se debe hacer a cualquier biblioteca de hojas de cálculo que esté evaluando es directa: si cambio una celda, ¿qué más cambia?

¿Cómo conserva 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 manera idéntica al guardar. 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 regeneraban un tema de Office fijo en cada guardado, que es exactamente la falla de "los colores de la marca volvieron al valor estándar" descrita 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 integrado de Office se genera solo para libros creados desde cero. Los bytes sin procesar son la mayor garantía de fidelidad posible: sin análisis, sin nueva serialización, sin posibilidad de desviación

La copia literal supera deliberadamente al acceso al tema mediante programación. TXLSXWorkbook expone ThemeMajorFont y ThemeMinorFont para que pueda elegir tipos de letra para encabezados y cuerpo en libros de trabajo nuevos, pero cuando se capturó un tema idéntico al abrir, esos métodos de escritura no tienen efecto en el archivo guardado — el ciclo completo tiene prioridad. Si realmente necesita alterar el tema de un libro de trabajo existente, eso 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 el todo:

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 sección extLst de la hoja guardada, por lo que las características escritas por versiones de Excel más nuevas sobreviven intactas al ciclo completo. Desde la versión v2.131.0, los fragmentos creados son visibles a través de la propiedad de solo lectura RawWorksheetExts, una lista TStringList en cada hoja 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 nueva serialización a nivel de evento, no una copia de bytes sin formato. El lector XML de flujo de HotXLS no expone desplazamientos de origen, por lo que el subárbol desconocido se reconstruye a partir de los eventos Element, Text y EndElement a medida que pasan por el flujo. 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. Si se maneja, el fragmento reconstruido es 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 autocontenido en cuanto a su espacio de nombres, y esa misma propiedad es la razón por la que al duplicar una hoja de trabajo dentro o a través de libros de trabajo se pueden trasladar 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) cada vez que el libro de trabajo guardado contiene fórmulas, y elige entre dos ordenamientos. Si el grafo de dependencias de fórmulas ya se ha construido y está actualizado — si 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. Ambos son correctos: las notas de implementación de Microsoft para el formato, [MS-XLSX], tratan a la cadena de cálculo como una sugerencia que Excel verifica y reordena durante la carga, por lo que cualquier listado completo es legal, y HotXLS se niega deliberadamente a forzar la construcción de un grafo dentro de SaveAs — la construcción de aristas es cuadrática en el recuento de celdas, un costo 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');

¿Por qué carecer de una parte que Excel trata como recomendación? Porque su ausencia es una señal. Algunos consumidores — heurísticas de reparación, visores de terceros, herramientas de comparación — esperan que un libro de trabajo con fórmulas contenga una cadena de cálculo, y una biblioteca que descarta silenciosamente esa parte al guardar produce archivos que son sutilmente diferentes de lo que Excel escribe. Emitir una cadena válida mantiene la salida dentro del comportamiento normal del ecosistema para el que ha sido probado, lo que constituye la parte silenciosa y menos llamativa de la ingeniería de ciclo completo

Dónde termina el ciclo completo sin pérdidas

La honestidad importa más que una casilla de verificación de mercadotecnia aquí, por lo que los límites merecen la misma atención. 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 idéntica a nivel binario — solo los encabezados locales de ZIP llevan marcas de tiempo DOS recientes. Los fragmentos <ext> capturados se devuelven normalizados, como se describió anteriormente. Las anulaciones programáticas de fuentes de temas 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 reescriben en lugar de copiarse a ciegas) más el contenido extLst externo más las partes almacenadas en caché de forma idéntica. 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, por lo que debe probar sus propias plantillas en lugar de asumir

El trabajo de preservación adyacente completa la imagen. Los proyectos VBA y las referencias a libros de trabajo externos continúan a través del guardado bajo la misma filosofía de conservar lo que no se modela, cubierta en el artículo complementario sobre preservación de VBA y enlaces externos, y las propiedades del documento en docProps tienen su propia API de lectura y 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 con muchas características, cambie un solo valor, guarde y compare 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 ciclo completo descritos aquí — la retención literal de temas desde la versión v2.89.46, la captura externa de extLst y la emisión de calcChain.xml desde la versión v2.131.0 — se distribuyen en el componente actual HotXLS Delphi Excel Component, cuya página del producto documenta el conjunto completo de características de lectura y escritura de XLSX para Delphi y C++Builder