Artículo técnico

Leer y escribir workbooks binarios XLSB en Delphi

HotXLS lee y escribe archivos Excel Binary Workbook (XLSB) desde Delphi a través de la misma fachada TXLSXWorkbook que usa para XLSX: Open detecta un workbook binario por el contenido del paquete, y SaveAs o SaveAsChecked con xlsxExcelBinaryWorkbook escribe uno. El backend cubre un subset validado y rechaza todo lo que no pueda representar antes de tocar el destino

Esa última cláusula es la parte que conviene leer despacio. XLSB parece una variación simple de XLSX, y en términos de empaquetado lo es. El problema vive en los detalles que los dos formatos codifican distinto, y en lo que una biblioteca asume en silencio cuando tiende un puente de uno a otro. El bug más instructivo de este backend no fue ni siquiera un registro malformado. Fue una fuente por defecto que desapareció, y una columna de fechas que se volvió #### en Excel aunque el valor, el código de formato y el ancho de columna estaban todos correctos

¿Qué es un archivo XLSB y en qué se diferencia de XLSX?

Un archivo XLSB es un paquete OPC con exactamente la estructura de partes de un workbook XLSX, pero las partes de workbook, worksheet, estilos y cadenas compartidas almacenan registros binarios en lugar de XML. El formato está especificado en [MS-XLSB], y la codificación de registros suele llamarse BIFF12. Sigues teniendo [Content_Types].xml, partes de relationship y un contenedor ZIP; lo que cambia es que xl/workbook.bin sustituye a xl/workbook.xml, xl/worksheets/sheet1.bin sustituye al XML de worksheet, y así

Cada registro BIFF12 arranca con una cabecera compacta de longitud variable. El tipo de registro toma uno o dos bytes, siete bits por byte con el bit alto como flag de continuación, así que un tipo de registro cabe en 14 bits. El tamaño del payload sigue en uno a cuatro bytes con el mismo esquema de siete bits. Las cadenas son un recuento de caracteres de 32 bits seguido de code units UTF-16. Comparado con BIFF8 es una maquetación mucho más limpia: sin partir en CONTINUE de 8224 bytes, sin flags de cadena comprimida contra ancha a mitad de registro. Comparado con XLSX se salta por completo la tokenización de XML, que es por lo que los archivos XLSB grandes tienden a ser más pequeños en disco y más rápidos de abrir para Excel

Un detalle de empaquetado hace tropezar al código de detección. Excel registra la parte workbook XLSB con el content type application/vnd.ms-excel.sheet.binary.macroEnabled.main, incluso para workbooks sin macros. HotXLS comprueba ese registro en [Content_Types].xml primero y solo recurre a la extensión de parte .bin cuando el content type no está, y cualquiera de las dos rutas sigue teniendo que pasar el validador estricto de registros. Las reglas generales sobre nombres de parte, relationships y content types son las mismas que cubre por qué Excel repara un XLSX válido y las reglas de paquete OPC que hay detrás

¿Cuándo elegir XLSB en lugar de XLSX?

Elige XLSB cuando un sistema receptor lo exige o cuando intercambias con usuarios de Excel workbooks grandes cargados de valores a quienes les importa el tiempo de apertura; si no, XLSX sigue siendo el valor por defecto más seguro. XLSX es un estándar ISO (ISO/IEC 29500) con amplio soporte de herramientas, es inspeccionable con una herramienta ZIP y un editor de texto, y todo producto de hojas de cálculo lo lee. XLSB es un formato Microsoft con especificación publicada pero un ecosistema más estrecho

En la práctica las peticiones llegan de dos direcciones. Algunos pipelines de informes y archivos estandarizaron en .xlsb hace años y rechazan cualquier otra cosa. Otros equipos generan workbooks con cientos de miles de celdas numéricas y encontraron que los usuarios abren la versión binaria notablemente más rápido. Ambas son razones legítimas. "Suena más eficiente" no lo es, porque el backend binario de HotXLS monta el paquete completo en memoria, así que no es la herramienta para exportaciones de servidor con memoria acotada. Para esas, el escritor XLSX en streaming es el camino correcto

Abrir y guardar XLSB con TXLSXWorkbook

TXLSXWorkbook.Open no necesita pista de formato: la misma llamada carga XLSX, XLSM o XLSB, y tras una carga binaria con éxito TXLSXWorkbook.SourceFormat reporta xlsxExcelBinaryWorkbook. La escritura es explícita. SaveAs(FileName) sin formato produce siempre un workbook Open XML, incluso cuando el nombre de archivo acaba en .xlsb, así que la salida binaria siempre nombra el formato

uses
  SysUtils, lxHandleX;

procedure ConvertToXlsb(const Source, Target: string);
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // Open detecta XLSX, XLSM o XLSB desde el propio paquete
    if Book.Open(Source) <> 1 then
      raise Exception.Create('Input is unsupported or invalid');

    // La extensión sola jamás selecciona salida binaria
    if Book.SaveAsChecked(Target, xlsxExcelBinaryWorkbook) <> 1 then
      raise Exception.Create('XLSB output is unsupported or failed');
  finally
    Book.Free;
  end;
end;

La salida a stream va por SaveAs(Stream, xlsxExcelBinaryWorkbook) o la sobrecarga de stream de SaveAsChecked. Internamente ambas enrutan a los métodos privados SaveAsXlsbStream y SaveAsXlsbFile; nunca los llamas directamente. La salida a archivo se monta en un archivo temporal hermano y se compromete con un único reemplazo final. Un destino de stream debe ser legible y posicionable, porque el escritor fotografía sus bytes, tamaño y posición y los restaura si la copia final falla. TXLSXWorkbook.LastSaveUsedStreaming reporta False tras un guardado binario, lo que es exacto: el paquete entero existe en memoria antes de que un solo byte llegue a tu destino

¿Por qué un subset validado en lugar de fidelidad completa?

HotXLS no mantiene un segundo modelo de objetos para XLSB. Al abrir, XlsbToOpenXml convierte el paquete binario en un paquete Open XML en memoria y el parser XML ordinario lo carga; al guardar, el escritor XML construye un paquete y OpenXmlToXlsb lo codifica como registros binarios. Cada función tiene por tanto que sobrevivir a dos traducciones, y una función que el puente no pueda traducir exactamente se rechaza en lugar de aproximarse

El conjunto soportado a partir de HotXLS 2.384.113 es lo bastante amplio para la mayoría de workbooks de datos: valores tipados, cadenas compartidas e inline, fuentes comunes, rellenos, bordes y formatos numéricos, alturas de fila y anchos de columna, fórmulas de celda ordinarias con resultados cacheados, nombres definidos, celdas combinadas, hipervínculos, paneles congelados y divididos, rangos AutoFilter con criterios personalizados simples y tablas de worksheet ordinarias. Fuera quedan las fórmulas compartidas y de array, las funciones futuras y definidas por el usuario, las referencias a workbooks externos, los gráficos y drawings, las PivotTables y slicers, el VBA, el formato condicional, la validación de datos y los ajustes de cálculo no por defecto como cálculo manual o iterativo

El rechazo es obligatorio, no consultivo. TXLSXWorkbook.GetConversionReport(xlsConversionExcelBinaryWorkbook) lista las funciones no soportadas antes de guardar, y esas entradas hacen fallar el guardado incluso con AAllowLossy = True o un SaveAs sin comprobación. Eso es deliberado. Soltar en silencio una PivotTable de una copia binaria es de ese tipo de pérdida que nadie nota hasta que el workbook está delante de un cliente

uses
  Classes, lxHandleX, lxDiagnostics;

function CanSaveAsXlsb(Book: TXLSXWorkbook; Log: TStrings): Boolean;
var
  Report: TXLSConversionReport;
  I: Integer;
begin
  Report := Book.GetConversionReport(xlsConversionExcelBinaryWorkbook);
  try
    Result := True;
    for I := 0 to Report.Count - 1 do
      if Report[I].Fidelity = xlsConversionUnsupported then
      begin
        Result := False;
        Log.Add(Report[I].SheetName + ' ' + Report[I].Location + ': ' +
          Report[I].Feature + ' - ' + Report[I].Message);
      end;
  finally
    Report.Free;   // el report es propiedad del caller
  end;
end;

El informe cubre el modelo vivo. La validación final del paquete sigue siendo la última compuerta para lo que el modelo nunca ve, como registros malformados o atributos desconocidos en el archivo fuente, así que un informe limpio seguido de un guardado fallido es posible y el resultado del guardado es lo que te debe guiar

¿Por qué desaparecieron la fuente por defecto y el formato 0.00?

La fuente por defecto y los formatos numéricos integrados desaparecieron porque el lector de estilos XML hizo dos suposiciones que se cumplen en los archivos que HotXLS escribe él mismo pero no en los workbooks que escribe Excel. El puente conservó ambos recursos perfectamente; la pérdida ocurrió una capa más arriba, en la ruta pública Open. Ese hueco es la lección principal de este backend: un puente de formato correcto no garantiza un round trip público fiel

La primera suposición concernía a fonts[0]. Cuando HotXLS guarda XLSX, escribe una fuente por defecto implícita en el slot cero, y su lector XML se salta ese slot para que la colección Fonts orientada al usuario empiece en el índice cero. Un XLSB de autoría Excel lleva su propio valor por defecto explícito en ese slot, y saltárselo tiraba esa elección. La segunda suposición concernía a los formatos numéricos. El lector solo mapeaba valores numFmtId personalizados (164 en adelante) a NumberFormats; los ids integrados se quedaban como cero. Una celda en blanco con estilo que llevaba el formato integrado 2, que es 0.00, volvía como General

El arreglo vive por entero en la ruta de importación binaria. Tras pasar el paquete completo el preflight, la apertura XLSB fija un modo de estilos privado solo para el parseo XML síncrono. En ese modo el lector carga toda fuente incluido el slot cero y desplaza cada fontId en uno, convierte cada id integrado soportado en un código de formato explícito en el modelo, y sigue usando el numFmtId original para la detección de fechas, así que un formato de fecha integrado conserva su semántica de fecha. Un bloque finally limpia siempre el modo, y una apertura posterior XLSX u ODS corriente con Open limpia también el estado de fuente por defecto retenida

¿Cómo afecta fonts[0] a los anchos de columna de Excel?

Excel calcula los anchos de columna a partir de la fuente por defecto del workbook en fonts[0], no de las fuentes aplicadas a las celdas de esa columna. El width de columna se almacena en unidades de carácter del ancho máximo de dígito de la fuente por defecto, así que cambiar fonts[0] cambia el ancho en píxeles de todas las columnas, incluidas las columnas cuyas celdas usan otra fuente

Eso produjo el síntoma más desconcertante de toda la investigación. El workbook re-guardado tenía valores de celda idénticos, códigos de formato idénticos y atributos width idénticos, la fuente de la celda y el estilo Normal apuntaban ambos a la fuente conservada, y Excel seguía renderizando una columna de fechas como ####. El fonts[0] escrito había vuelto a Calibri 11, el ancho máximo de dígito se encogió, y las fechas ya no cabían. HotXLS ahora conserva la fuente por defecto del origen y la escribe de vuelta como fonts[0], recurriendo a Calibri 11 solo para workbooks que nunca tuvieron origen binario. La aritmética completa de anchos está en ancho de columna de Excel y max digit width (MDW) en Delphi

Los formatos integrados dependientes de locale se rechazan, no se adivinan

Un id de formato numérico integrado sin código de formato explícito se acepta solo cuando su significado está fijado; los ids dependientes de locale se rechazan durante el preflight del paquete. HotXLS mapea los ids 0 a 22, 37 a 40 y 45 a 49 a códigos conocidos como 0.00, 0%, mm:ss y @. Los ids restantes por debajo de 164 pertenecen a formatos cuyo render depende de la configuración regional de la instalación de Excel, y la importación los rechaza con el mensaje "A locale-dependent number format requires an explicit format code"

Inventar un código para esos ids sería peor que fallar. Un patrón de fecha de Asia Oriental adivinado parece plausible, hace round trip sin error y muestra lo equivocado a la mitad de tus usuarios. El timing importa también: la comprobación corre en el preflight de paquete completo, antes de limpiar el workbook existente, así que un archivo rechazado deja el modelo, el nombre de archivo y el formato fuente previamente cargados intactos. El artículo sobre serial de fecha y numFmt de Excel cubre cómo los ids de fecha integrados interactúan con los sistemas de fecha 1900 y 1904

procedure LoadBinaryWorkbook(Book: TXLSXWorkbook; const FileName: string);
var
  I: Integer;
begin
  // La entrada binaria exige carga completa eager; los modos selectivos rechazan de entrada
  Book.MetadataOnly := False;
  Book.SelectedSheets.Clear;

  if Book.Open(FileName) <> 1 then
  begin
    for I := 0 to Book.Diagnostics.Count - 1 do
      Writeln(Book.Diagnostics[I].Message);
    Exit;   // el workbook cargado previamente sigue intacto
  end;

  // La salida de fórmulas necesita cache salvo que el guardado recalcule
  Book.RecalcBeforeSave := xrmAlways;
end;

Reglas prácticas para XLSB en código de producción

El backend binario es seguro de usar cuando tratas sus rechazos como datos en lugar de como errores que suprimir. Unas cuantas reglas se derivan del comportamiento de arriba

  • Nombra el formato en cada guardado binario; la extensión .xlsb sola sigue produciendo XLSX
  • Corre GetConversionReport(xlsConversionExcelBinaryWorkbook) antes de guardar workbooks editados por usuarios y expón las funciones no soportadas al usuario
  • Deja MetadataOnly apagado y SelectedSheets vacío para entrada binaria, ya que la carga lazy exigiría retener un archivo convertido
  • Dale a las fórmulas un resultado cacheado o recalcula antes de guardar, porque un cache ausente rechaza bajo xrmManual y bajo un xrmIfDirty limpio
  • Presupuesta memoria para el paquete convertido completo más la salida binaria; usa XLSX en streaming cuando la memoria deba quedarse acotada

Cuando pruebes, compara contra Excel a través de la API pública en lugar de a nivel de puente. Las regresiones de la fuente por defecto y del 0.00 pasaron ambas los tests del puente, y ambas aparecieron de inmediato en un workbook de autoría Excel abierto y re-guardado a través de Open y SaveAsChecked

FAQ

¿Detecta HotXLS los archivos XLSB automáticamente?

Sí. TXLSXWorkbook.Open inspecciona el content type registrado para la parte workbook y valida los registros binarios, así que la misma llamada carga XLSX y XLSB, y SourceFormat reporta cuál se leyó

¿Por qué SaveAs con un nombre .xlsb sigue escribiendo XLSX?

HotXLS nunca infiere el formato de salida de la extensión. Pasa xlsxExcelBinaryWorkbook a SaveAs o SaveAsChecked para escribir un workbook binario

¿Qué pasa cuando un workbook usa funciones fuera del subset XLSB?

El guardado falla antes de que cambie el destino, incluso con guardado con pérdidas permitido. GetConversionReport lista de antemano las funciones bloqueantes y Diagnostics explica el fallo después

El soporte XLSB, el informe de conversión y el resto de la API de workbook vienen en HotXLS, el componente de hojas de cálculo nativo para Delphi y C++Builder