Technisch artikel

Herhaalde ODS-rijen als rijhoogte-runs in Delphi

Een spreadsheet met veertien kolommen en 45 rijen data eindigt, als ODS opgeslagen, meestal met één element meer dan u verwacht: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Die ene regel beschrijft elke rij van 46 tot aan de onderkant van het raster. Hem letterlijk uitklappen, als die rij 1.048.530 keer uitgeschreven, en een kleine content.xml verandert in een miljoen rijrecords, een miljoen hoogte-items en, als de kolommen een standaardcelstijl dragen, bijna vijftien miljoen gestijlde lege cellen

Hoe HotXLS één herhaalde ODS-rij naar compacte toestand omzet: het content.xml-element met table:number-rows-repeated 1048530 en stijl ro1 verwijst naar één record TXLSXRowHeightRun dat rijen 46 tot 1048575 omspant op 12.81 pt plus één StyleOverlays-item per kolomspanne met dezelfde stijl, in plaats van hetzelfde element uit te klappen naar een miljoen SetRowHeight-items en celobjecten
De herhalingsteller, de ro1-rijhoogte en de kolomstandaardstijlen beschrijven elke lege rij onder rij 45, dus de importer kan één runrecord en een paar kolomspanne-overlays bouwen zonder een miljoen coördinaten aan te raken

Waarom één herhaalde rij duur is om te importeren

De kosten komen van het uitklappen, en uitklappen is de voor de hand liggende manier om een importer te schrijven. Stijl ro1 zet style:row-height="0.452cm", wat 12.81 punten is. Een rij-afronder die dat toepast met één aanroep SetRowHeight(RowIndex + i, RowHeight) per herhaalde rij schrijft 1.048.530 items weg in welke hoogtetabel per rij het objectmodel ook bijhoudt. Is die tabel een stringlijst met sleutels waarin elke schrijfactie eerst naar een bestaande sleutel zoekt, dan is de import kwadratisch in de herhalingsteller, en een herhalingsteller van een miljoen is precies de invoer waarbij kwadratisch ophoudt theoretisch te zijn

Lege cellen maken het erger. Elke <table:table-column> in zo'n bestand noemt meestal een table:default-cell-style-name, en elke lege cel in de herhaalde rij erft die. Een gestijlde leegte is voor de meeste objectmodellen nog steeds een cel. Per kolom per herhaalde rij één materialiseren en de geheugenrekening overtreft de hoogtetabel verre

De opslagkant heeft het spiegelbeeldprobleem. Zegt het model eenmaal dat rij 1.048.575 een hoogte heeft, dan schrijft een writer die van rij 1 tot de laatste rij met iets erop loopt een miljoen <table:table-row>-elementen. Een goedkope import gevolgd door een dure opslag verplaatst de kosten alleen naar een andere knop

Hoe HotXLS een herhaalde rijhoogte opslaat

HotXLS Delphi Excel Component slaat een herhaalde ODS-rijhoogte op als een run: een record TXLSXRowHeightRun met FirstRow, LastRow en één Height. Rondt TXLSXWorkbook.OpenODS een rij af waarvan de herhalingsteller 1 is, dan roept hij SetRowHeight aan zoals elke andere code dat zou doen. Bij elke grotere teller legt hij de hele spanne als één run vast, dus de staart in het voorbeeld wordt rijen 46 tot 1.048.575 op 12.81 pt. Een herhalingsteller die voorbij het einde van het raster loopt, wordt afgekapt op rij 1.048.576 in plaats van geweigerd

Eén regel in de importer verrast wie het resultaat tegen het bronbestand difft. Spreadsheetapplicaties die ODS schrijven geven vrijwel elke rijstijl een style:row-height, ook rijen die niemand ooit heeft verschaald, en markeren die met style:use-optimal-row-height="true". Voor een rij die helemaal geen cellen opslaat is een automatische hoogte simpelweg de defaulthoogte, dus HotXLS legt voor die rij geen hoogte vast. Rijen die wel iets opslaan houden hun hoogte, ook als die als optimaal is gemarkeerd, want een rij met omlopende tekst die was meegegroeid met zijn inhoud zou anders inkrimpen zodra de workbook opnieuw als XLSX wordt geopend. Die afweging is de juiste: een echte hoogte laten vallen levert een zichtbare lay-outwijziging op, terwijl een redundante houden één run kost

Hoogtes teruglezen gaat via de gewone eigenschap RowHeight. Hij bekijkt eerst de lijst per rij en valt alleen terug op de runs wanneer de rij geen expliciet item heeft; HasRowHeight volgt dezelfde volgorde. Runs overlappen nooit, dus de fallback stopt bij de eerste run die de rij bevat

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Spans: TXLSXAxisSpans;
  I: Integer;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Een blad waarvan het laatste rijelement 1.048.530 keer herhaalt
    Workbook.OpenODS('tail-repeat.ods');
    Sheet := Workbook.Sheets[1];  // Sheets is 1-based
    // Opgeslagen rijmetadata: eerst items van één rij, daarna runs
    Spans := Sheet.GetStoredAxisSpans(True);
    for I := 0 to High(Spans) do
      Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
    // Beide uitlezingen komen uit dezelfde run
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Een item per rij wordt vóór de run opgezocht, dus dat wint
    Sheet.RowHeight[500000] := 36;
    // Eén rij binnen de run wissen knipt een gat van één rij erin
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001));           // FALSE
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt');   // nog steeds de runhoogte
  finally
    Workbook.Free;
  end;
end;

De twee bewerkingen in dat voorbeeld gedragen zich met opzet verschillend. Sheet.RowHeight[500000] := 36 laat de run met rust en voegt één item toe aan de lijst per rij, dat de run overschaduwt omdat het eerst wordt geraadpleegd. ClearRowHeight kan niet zo werken, want een gewiste rij moet melden dat hij geen hoogte heeft, ook al omspant een run hem nog. Dus hij verwijdert elk item per rij en bouwt daarna de runlijst opnieuw: elke run buiten de gewiste rij blijft zoals hij is, de run die hem bevatte wordt gesplitst in het stuk ervoor en het stuk erna, en voor rij 500.001 zelf wordt niets opgeslagen. GetStoredAxisSpans(True) zet nu 46..500000 en 500002..1048575 als twee aparte runs op de lijst, naast het single-row-item voor 500.000. ClearRowHeights maakt de lijst per rij en de runs in één aanroep leeg

Rijhoogte-run-chirurgie in HotXLS: na OpenODS dekt één run rijen 46 tot 1048575 op 12.81 pt terwijl een item per rij rij 500000 op 36 pt zet en de opzoeking wint omdat RowHeight eerst de lijst per rij bekijkt, en ClearRowHeight van rij 500001 splitst de run in twee disjuncte stukken rond het gat
ClearRowHeight houdt de stukken van de run buiten de gewiste rij bij en slaat voor de rij zelf niets op, dus runs overlappen nooit, een opzoeking mag bij de eerste treffer stoppen en de override op rij 500000 blijft onaangeroerd

Waar blijven de geërfde stijlen van lege cellen?

Geërfde lege stijlen worden stijloverlays: rechthoeken op het werkblad die een stijl dragen, zonder celobjecten erachter. Bij het lezen van een rij loopt de importer de herhaalde cellen af in spannen van aangrenzende kolommen die naar dezelfde stijl oplossen. Een span waarvan de stijl er precies uitziet als Default slaat niets op. Een span met echte opmaak, zeg een gecentreerde of vette kolomstandaard, wordt één rijwaarde die al zijn kolommen dekt

Wordt die rijwaarde vastgelegd en herhaalt de rij, dan materialiseert HotXLS één cel in de linkerbovenhoek van de span, past er de geërfde stijl op toe en registreert de zes stijlindexen (font, vulling, rand, getalnotatie, uitlijning, beveiliging) via Sheet.StyleOverlays.Add voor de hele rechthoek: elke kolom in de span, van de eerste herhaalde rij tot de laatste. Voor de overige 1.048.529 rijen wordt niets anders aangemaakt. Sheet.Cells.Count blijft dicht bij het aantal cellen dat echt data bevat

Een cel binnen de rechthoek pakt de overlay op zodra uw code hem aanraakt. Na het openen van een blad waarvan de staart door een vette kolomstandaard is opgemaakt, lost Sheet.Cells[700000, 1].FontIndex op naar het vette font, ook al bestond op die coördinaat een regel eerder geen cel. XLSX-bestanden met een opgemaakte maar lege kolom gaan door dezelfde machinerie, en het artikel over rijblokcelopslag en interval-stijloverlays behandelt hoe meerdere overlays stapelen en oplossen. De ODS-importer verschilt alleen in waar de overlays vandaan komen: hij leidt ze zelf af uit de herhalingstellers

Hoe SaveAsODS de herhalingsteller terugschrijft

TXLSXWorkbook.SaveAsODS schrijft elke strekking identieke rijen als één <table:table-row> met table:number-rows-repeated, en beëindigt een strekking alleen waar er iets aan de rijen verandert. De tabel loopt door tot de laagste rij die iets draagt: een waarde, een lege cel met alleen stijl, een hoogte voor één rij, het einde van een hoogterun, de onderrand van een overlay, een verborgen of overzichtsitem, of een validatie- of voorwaardelijk-opmaakgebied

Vanaf rij 1 kijkt de writer vooruit naar de volgende grens. Kandidaten zijn de volgende gematerialiseerde cel, het begin of einde van een hoogterun, elk item voor één rij (hoogte, verborgen-vlag, overzichtsniveau, samengevouwen toestand, handmatig pagina-einde), overlayranden, samengevoegde bereiken, hyperlinks, commentaren, afbeeldingen, en de randen van elk validatie- en voorwaardelijk-opmaakrechthoek. Alles tot die grens is één element. Binnen elk element draait dezelfde logica over de kolommen, dus een lege strekking wordt één <table:table-cell> met table:number-columns-repeated en de stijlnaam waar de overlay op uitkomt

Rijstijlen worden gedeeld. De rijhoogte wordt als tekst opgemaakt, 12.81pt voor het voorbeeld, en die tekst vormt samen met de verborgen-vlag en de pagina-einde-vlag de sleutel voor de automatische rijstijl. Elke rij met dezelfde drie waarden wijst naar dezelfde stijl ro<N>, die één eigenschap style:row-height bevat. Een rij zonder hoogte, zichtbaar en zonder pagina-einde krijgt helemaal geen rijstijl

Wat SaveAsODS schrijft voor een blad dat op runs rust: rijen 1 tot 45 bevatten waarden en worden per rij één element geschreven, de staart tot rij 1048575 gaat eruit als herhaalde table-row-elementen die splitten op runranden, overrides per rij en overlayranden, rijen met dezelfde hoogte delen één automatische rijstijl, en celstijlen lossen via de overlays op
Elk herhaald element omspant één uniforme strekking en stopt bij de volgende runrand, het volgende hoogte-item, de volgende overlayrand, het volgende regelgebied of de volgende gematerialiseerde cel, dus een lege staart slaat op als een handvol elementen terwijl een XLSX-export per gedekte rij één rijelement schrijft
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);
    // De staart wordt als een paar herhaalde rijelementen geschreven
    Workbook.SaveAsODS(Saved);
    Writeln('ODS stream: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.OpenODS(Saved);
    // Verwacht: override, gat en beide runstukken overleven de round trip
    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);   // de runhoogte
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Voor het werkblad hierboven splitst de staart in vier stukken: rijen 46 tot 499.999 op de runhoogte, rij 500.000 op 36 pt, rij 500.001 zonder rijstijl, en rijen 500.002 tot 1.048.575 weer op de runhoogte. Elke kolom waarvan de standaardstijl een overlay achterliet, kan een splitsing toevoegen waar die overlay eindigt. De 45 datarijen boven de staart worden per rij één element geschreven, omdat elk ervan cellen bevat

XLSX is een ander verhaal. SpreadsheetML kent geen herhalingsattribuut, en een element <row> beschrijft precies één rij, dus het opslaan van een blad dat op runs rust als .xlsx somt de rijen op die een run dekt en schrijft ht en customHeight="1" op elk ervan. Het model blijft compact in het geheugen; het doelformaat bepaalt hoe groot het bestand wordt. Komt een workbook als ODS binnen en hoeft hij alleen als ODS te vertrekken, dan betaalt hij die kost per rij nooit

Bewerkingen die rijnummers verschuiven

Een tweede representatie van rijhoogtes is alleen correct als elke bewerking die rijen verplaatst hem ook meeverplaatst. TXLSXWorksheet.InsertRows en DeleteRows schuiven de runs mee met de lijsten per rij: het deel van een run vóór het bewerkingspunt blijft waar het is, rijen binnen een verwijderd venster vallen weg, en de rest schuift met het ingevoegde of verwijderde aantal mee. Een run die een invoeging overspant wordt daardoor twee runs met een gat van rijen zonder hoogte ertussen, en een run die een verwijdering overspant wordt korter

procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Twee nieuwe rijen op 500000: de override schuift naar 500002, het gat naar 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);  // 36.00
  Writeln(Sheet.HasRowHeight(500003));   // FALSE
  Writeln(Sheet.HasRowHeight(500000));   // FALSE: ingevoegde rijen hebben geen hoogte
  // Verwijder ze weer: de override en het gat keren terug naar hun oude rijnummers
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);  // 36.00
  Writeln(Sheet.HasRowHeight(500001));   // FALSE
  Writeln(Sheet.UsedRange.Row2);         // omvat het einde van de verschoven run
end;

De round trip laat de staart niet precies waar hij was. De invoeging schuift de staartrun van 500.002..1.048.575 door naar 500.004..1.048.577, en het raster eindigt op rij 1.048.576, dus het deel voorbij die rij wordt afgekapt. De verwijdering schuift daarna wat er over is twee omhoog, en de run eindigt op rij 1.048.574: rij 1.048.575 heeft geen opgeslagen hoogte meer. Doen de laatste rijen van het raster er voor u toe, zet dan hun hoogte opnieuw na een paar van invoegen en verwijderen. Rijen die midden in een run worden ingevoegd pakken zijn hoogte ook niet op: ze openen een gat van rijen zonder hoogte en erven de runhoogte niet, wat de reden is dat HasRowHeight(500000) hierboven FALSE printte

De andere rij-verplaatsende bewerkingen volgen hetzelfde patroon. TileRangeAxisMetadata wist de runs over de hele getegelde spanne en registreert daarna elke bronrun één keer per kopie op zijn offset. TXLSXWorksheet.CopyFrom en TXLSXSheets.AddCopy geven het nieuwe werkblad zijn eigen kopie van de runlijst, dus ClearRowHeights op een kloon laat de hoogtes van het origineel staan. Aan de leeskant reikt UsedRange tot de eerste en laatste rij van elke run, en rekt de rijronde van de XLSX-writer zich uit over de runs, zodat rijen met alleen hoogte toch worden geschreven

Verborgen rijen en overzichtsniveaus blijven per rij bewaard

Rijhoogtes en geërfde lege stijlen zijn de twee dingen die samenvouwen tot runs en overlays. Verborgen-vlaggen, overzichtsniveaus en samengevouwen toestand worden één keer per rij vastgelegd. Een herhaalde rij die verborgen is, of die in een <table:table-row-group> zit, levert één item per herhaalde rij op voor elk van die attributen, en bij het opslaan is elk zo'n item een grens op zichzelf, dus de verborgen strekking wordt per rij één <table:table-row> geschreven

Voor sjablonen die u zelf beheert: houd het verborgen en gegroepeerde deel van een werkblad zo strak als de data nodig heeft. Twintig rijen onder een rapport verbergen is gratis. Alles vanaf rij 46 tot de onderkant van het raster verbergen betekent een miljoen items bij het laden en een miljoen rijelementen bij het opslaan, en het enige zichtbare verschil is dat niemand nog voorbij het rapport kan scrollen

De kant van het formaat op werkmaphoog, inclusief welke ODS-functies op welke onderdelen van het objectmodel afbeelden, staat in de walkthrough van ODS-bestanden openen en opslaan, en andere hendels voor grote bestanden zijn verzameld onder prestaties van grote werkmappen. Het rijelement zelf, met zijn herhalings- en stijlattributen, is gedefinieerd in het tabelhoofdstuk van ODF 1.3 Part 3

Een herhaling van een miljoen rijen is een formaatdetail dat een native reader goed moet modelleren, want er is geen officesuite op de achtergrond die het opvangt. De HotXLS Delphi spreadsheet component-pagina somt de XLS-, XLSX- en ODS-functies op die hij leest en schrijft, samen met de ondersteunde Delphi- en C++Builder-versies