Una hoja de catorce columnas con 45 filas de datos, guardada como ODS, suele acabar con un elemento más de lo que espera: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Esa única línea describe todas las filas desde la 46 hasta el fondo de la cuadrícula. Expándala literalmente, como esa fila escrita 1.048.530 veces, y un content.xml pequeño se convierte en un millón de registros de fila, un millón de entradas de altura y, si las columnas llevan un estilo de celda por defecto, casi quince millones de celdas vacías con estilo
Por qué una sola fila repetida es cara de importar
El coste viene de la expansión, y la expansión es la forma obvia de escribir un importador. El estilo ro1 fija style:row-height="0.452cm", que son 12,81 puntos. Un finalizador de filas que la aplica con una llamada SetRowHeight(RowIndex + i, RowHeight) por fila repetida escribe 1.048.530 entradas en la tabla de alturas por fila que el modelo de objetos mantenga. Si esa tabla es una lista de cadenas con clave donde cada escritura busca primero una clave existente, la importación es cuadrática respecto al recuento de repeticiones, y un recuento de un millón es justo la entrada donde lo cuadrático deja de ser teórico
Las celdas vacías lo empeoran. Cada <table:table-column> de un archivo así suele nombrar un table:default-cell-style-name, y cada celda vacía de la fila repetida lo hereda. Un vacío con estilo sigue siendo una celda para la mayoría de los modelos de objetos. Materializar una por columna por fila repetida y la factura de memoria eclipsa la tabla de alturas
El lado del guardado tiene el problema en espejo. Una vez que el modelo dice que la fila 1.048.575 tiene una altura, un escritor que recorre desde la fila 1 hasta la última fila con algo emite un millón de elementos <table:table-row>. Una importación barata seguida de un guardado caro solo mueve el coste a otro botón
Cómo almacena HotXLS una altura de fila repetida
HotXLS Delphi Excel Component almacena una altura de fila ODS repetida como un tramo: un registro TXLSXRowHeightRun con FirstRow, LastRow y una Height. Cuando TXLSXWorkbook.OpenODS termina una fila cuyo recuento de repeticiones es 1, llama a SetRowHeight como cualquier otro código. Para cualquier recuento mayor registra todo el intervalo como un solo tramo, así que la cola del ejemplo pasa a ser las filas 46 a 1.048.575 a 12,81 pt. Un recuento de repeticiones que se pase del final de la cuadrícula se recorta en la fila 1.048.576 en vez de rechazarse
Una regla del importador sorprende a quienes comparan el resultado con el archivo de origen. Las aplicaciones de hoja de cálculo que escriben ODS tienden a dar a cada estilo de fila un style:row-height, incluidas filas que nadie redimensionó nunca, y a marcarlas con style:use-optimal-row-height="true". Para una fila que no almacena ninguna celda, una altura automática es sencillamente la altura por defecto, así que HotXLS no registra altura para ella. Las filas que sí almacenan algo conservan su altura aunque esté marcada como óptima, porque una fila con texto ajustado que creció para encajar su contenido volvería a encoger al reabrir el libro como XLSX. El intercambio es el correcto: soltar una altura real produce un cambio visible de maquetación, mientras que conservar una redundante cuesta un solo tramo
La lectura de alturas pasa por la propiedad RowHeight de siempre. Consulta primero la lista por fila y recurre a los tramos solo cuando la fila no tiene entrada explícita; HasRowHeight sigue el mismo orden. Los tramos nunca se solapan, así que la consulta para en el primer tramo que contiene la fila
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Spans: TXLSXAxisSpans;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
// Una hoja cuyo último elemento de fila se repite 1.048.530 veces
Workbook.OpenODS('tail-repeat.ods');
Sheet := Workbook.Sheets[1]; // Sheets es 1-based
// Metadatos de fila almacenados: primero entradas de una fila, luego tramos
Spans := Sheet.GetStoredAxisSpans(True);
for I := 0 to High(Spans) do
Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
// Ambas lecturas salen del mismo tramo
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Una entrada por fila se consulta antes que el tramo, así que gana
Sheet.RowHeight[500000] := 36;
// Limpiar una fila dentro del tramo le abre un agujero de una fila
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // sigue siendo la altura del tramo
finally
Workbook.Free;
end;
end;
Las dos ediciones de ese ejemplo se comportan distinto a propósito. Sheet.RowHeight[500000] := 36 deja el tramo tranquilo y añade una entrada a la lista por fila, que ensombrece al tramo porque se consulta antes. ClearRowHeight no puede funcionar así, porque una fila limpiada tiene que reportar que no tiene altura aunque un tramo siga cubriéndola. Así que borra cualquier entrada por fila y luego reconstruye la lista de tramos: cada tramo fuera de la fila limpiada se queda como está, el tramo que la contenía se parte en la pieza anterior y la posterior, y no se almacena nada para la fila 500.001. GetStoredAxisSpans(True) lista ahora 46..500000 y 500002..1048575 como dos tramos separados, junto a la entrada de una sola fila para 500.000. ClearRowHeights vacía la lista por fila y los tramos en una sola llamada
¿Dónde van a parar los estilos de celda vacía heredados?
Los estilos vacíos heredados se convierten en overlays de estilo: rectángulos sobre la hoja que llevan un estilo, sin objetos de celda detrás. Mientras lee una fila, el importador recorre las celdas repetidas en intervalos de columnas adyacentes que resuelven al mismo estilo. Un intervalo cuyo estilo se ve exactamente como Default no almacena nada. Un intervalo con formato real, digamos un estilo por defecto de columna centrado o en negrita, se convierte en un solo valor de fila que cubre todas sus columnas
Cuando ese valor de fila se confirma y la fila se repite, HotXLS materializa una única celda en la esquina superior izquierda del intervalo, le aplica el estilo heredado y registra los seis índices de estilo (fuente, relleno, borde, formato de número, alineación, protección) mediante Sheet.StyleOverlays.Add para el rectángulo completo: todas las columnas del intervalo, desde la primera fila repetida hasta la última. No se crea nada más para las 1.048.529 filas restantes. Sheet.Cells.Count se queda cerca del número de celdas que de verdad contienen datos
Una celda dentro del rectángulo recoge el overlay en el momento en que su código la toca. Tras abrir una hoja cuya cola tiene formato por un estilo por defecto de columna en negrita, Sheet.Cells[700000, 1].FontIndex resuelve a la fuente en negrita aunque no existiera ninguna celda en esa coordenada una línea antes. Los archivos XLSX con una columna con formato pero vacía pasan por la misma maquinaria, y el artículo sobre almacenamiento de celdas por bloques de fila y overlays de estilo por intervalo cubre cómo se apilan y resuelven varios overlays. El importador de ODS solo se diferencia en de dónde salen los overlays: los deriva él mismo a partir de los recuentos de repeticiones
Cómo escribe SaveAsODS el recuento de repeticiones de vuelta
TXLSXWorkbook.SaveAsODS escribe cada tramo de filas idénticas como un solo <table:table-row> con table:number-rows-repeated, y cierra un tramo solo donde algo de las filas cambia. La tabla se extiende hasta la fila más baja que lleve algo: un valor, una celda vacía solo con estilo, una altura de una sola fila, el final de un tramo de alturas, el borde inferior de un overlay, una entrada oculta o de esquema, o un área de validación o de formato condicional
Desde la fila 1, el escritor mira adelante en busca del siguiente límite. Los candidatos son la siguiente celda materializada, el principio o el final de un tramo de alturas, cualquier entrada de una sola fila (altura, flag de oculta, nivel de esquema, estado contraído, salto de página manual), bordes de overlays, rangos combinados, hipervínculos, comentarios, imágenes, y los bordes de cada rectángulo de validación y de formato condicional. Todo lo que llega hasta ese límite es un elemento. Dentro de cada elemento la misma lógica corre por columnas, así que un tramo vacío se convierte en un solo <table:table-cell> con table:number-columns-repeated y el nombre de estilo al que resuelve el overlay
Los estilos de fila se comparten. La altura de fila se formatea como texto, 12.81pt para el ejemplo, y ese texto junto con el flag de oculta y el flag de salto de página forma la clave del estilo automático de fila. Cada fila con los mismos tres valores apunta al mismo estilo ro<N>, que lleva una única propiedad style:row-height. Una fila sin altura, visible y sin salto no recibe estilo de fila alguno
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('tail-repeat.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// La cola se escribe como unos pocos elementos de fila repetidos
Workbook.SaveAsODS(Saved);
Writeln('ODS stream: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.OpenODS(Saved);
// Esperado: override, agujero y ambas piezas del tramo sobreviven al viaje
Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2); // 36.00
Writeln(Reopened.Sheets[1].HasRowHeight(500001)); // FALSE
Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2); // la altura del tramo
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Para la hoja de arriba, la cola se parte en cuatro piezas: filas 46 a 499.999 a la altura del tramo, fila 500.000 a 36 pt, fila 500.001 sin estilo de fila, y filas 500.002 a 1.048.575 otra vez a la altura del tramo. Cualquier columna cuyo estilo por defecto dejó un overlay puede añadir un corte donde acaba ese overlay. Las 45 filas de datos sobre la cola se escriben un elemento por fila porque cada una contiene celdas
XLSX es otra historia. SpreadsheetML no tiene atributo de repetición, y un elemento <row> describe exactamente una fila, así que guardar una hoja respaldada por tramos como .xlsx enumera las filas que cubre cada tramo y escribe ht y customHeight="1" en cada una. El modelo sigue compacto en memoria; el formato de destino decide lo grande que se hace el archivo. Si un libro llega como ODS y solo tiene que salir como ODS, nunca paga ese coste por fila
Ediciones que renumeran filas
Una segunda representación de las alturas de fila solo es correcta si toda operación que mueve filas la mueve también. TXLSXWorksheet.InsertRows y DeleteRows desplazan los tramos junto con las listas por fila: la parte de un tramo anterior al punto de edición se queda donde está, las filas dentro de una ventana borrada se descartan, y el resto se mueve según el número de filas insertadas o borradas. Un tramo que atraviesa una inserción se convierte por tanto en dos tramos con un hueco de filas sin altura entre medias, y un tramo que atraviesa un borrado se acorta
procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Dos filas nuevas en 500000: el override pasa a 500002, el agujero a 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // FALSE
Writeln(Sheet.HasRowHeight(500000)); // FALSE: las filas insertadas no tienen altura
// Elimínelas otra vez: el override y el agujero vuelven a sus números de fila
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.UsedRange.Row2); // incluye el final del tramo desplazado
end;
El viaje de ida y vuelta no deja la cola exactamente donde estaba. La inserción empuja el tramo de la cola de 500.002..1.048.575 a 500.004..1.048.577, y la cuadrícula acaba en la fila 1.048.576, así que la parte que pasa de esa fila se recorta. El borrado mueve luego lo que queda dos filas hacia arriba, y el tramo acaba en la fila 1.048.574: la fila 1.048.575 ya no tiene altura almacenada. Si las últimas filas de la cuadrícula le importan, vuelva a fijar su altura tras un par de inserción y borrado. Las filas insertadas en medio de un tramo tampoco heredan su altura: abren un hueco de filas sin altura y no toman la altura del tramo, que es por lo que HasRowHeight(500000) imprimió FALSE arriba
Las demás operaciones que mueven filas siguen el mismo patrón. TileRangeAxisMetadata limpia los tramos de todo el intervalo tileado y luego registra cada tramo de origen una vez por copia en su desplazamiento. TXLSXWorksheet.CopyFrom y TXLSXSheets.AddCopy dan a la hoja nueva su propia copia de la lista de tramos, así que un ClearRowHeights sobre un clon deja las alturas del original en su sitio. Del lado de la lectura, UsedRange se extiende hasta la primera y la última fila de cada tramo, y el recorrido de filas del escritor XLSX se extiende por los tramos para que las filas que solo tienen altura se sigan escribiendo
Las filas ocultas y los niveles de esquema se guardan por fila
Las alturas de fila y los estilos vacíos heredados son las dos cosas que se pliegan en tramos y overlays. Los flags de oculta, los niveles de esquema y el estado contraído se registran una vez por fila. Una fila repetida que está oculta, o que está dentro de un <table:table-row-group>, produce una entrada por fila repetida para cada uno de esos atributos, y al guardar cada una de esas entradas es un límite propio, así que el tramo oculto se escribe un <table:table-row> por fila
Para las plantillas que usted controla, mantenga la parte oculta y agrupada de una hoja tan ajustada como los datos necesiten. Ocultar veinte filas bajo un informe es gratis. Ocultar todo desde la fila 46 hasta el fondo de la cuadrícula significa un millón de entradas al cargar y un millón de elementos de fila al guardar, y la única diferencia visible es que nadie puede hacer scroll más allá del informe
El lado del formato a nivel de libro, incluido qué características de ODS se mapean a qué partes del modelo de objetos, está en el recorrido de abrir y guardar archivos ODS, y otras palancas para archivos grandes se recogen en rendimiento con libros grandes. El elemento de fila en sí, con sus atributos de repetición y estilo, está definido en el capítulo de tablas de ODF 1.3 Part 3
Una repetición de un millón de filas es un detalle del formato que un reader nativo tiene que modelar bien, porque no hay ninguna suite ofimática de fondo que lo absorba. La página del componente de hoja de cálculo HotXLS para Delphi lista las características XLS, XLSX y ODS que lee y escribe, junto con las versiones de Delphi y C++Builder soportadas