Tehnični članak

Brezizgubno krožno potovanje XLSX v Delphiju: Theme, extLst, calcChain

HotXLS, izvorna knjižnica Excel za Delphi in C++Builder, je zasnovana za brezizgubno krožno potovanje (round-trip) XLSX: odprite delovni zvezek, spremenite eno celico, shranite in lastna tema stranke, tuji razširitveni bloki extLst ter veriga izračunov bodo preživeli. Trije mehanizmi skrbijo za to — dobesedno predpomnjenje datoteke xl/theme/theme1.xml, na dogodkih temelječa ponovna serializacija neznanih blokov <ext> in sveža, specifikacijam ustrezna datoteka xl/calcChain.xml ob vsakem shranjevanju delovnega zvezka s formulami

Scenarij, ki motivira vse tri mehanizme, je pogost. Storitev zaračunavanja naloži predlogo, ki jo je stranka oblikovala v Excelu — barvna tema podjetja, grafikoni v vrsticah (sparklines) v stolpcu KPI, pravilo pogojnega oblikovanja, dodano z novejšo različico Excela —, zapiše en znesek računa v celico B3 in shrani. Stranka odpre rezultat, barve blagovne znamke pa so se vrnile na privzeto modro barvo Officea, sparklines so izginili, Excel pa ponudi, da datoteko "popravi". Nič v kodi se ni dotaknilo teh funkcij. Knjižnica se jih je, preprosto s shranjevanjem

Zakaj datoteke Excel po urejanju s knjižnico izgubijo oblikovanje?

Datoteke Excel po urejanju s knjižnico izgubijo oblikovanje, ker večina knjižnic datoteke ne uredi — znova jo zgradijo. Paket .xlsx je ZIP arhiv delov XML: xl/workbook.xml, ena datoteka xl/worksheets/sheetN.xml na list, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml in drugi. Tipična knjižnica analizira te dele v objektni model ob odpiranju in ob shranjevanju znova zgradi vsak del iz tega modela. Katera koli funkcija, ki je model ne predstavlja — tema, ki je nikoli ni analiziral, ali razširitveni blok iz novejšega Excela —, nima mesta v pomnilniku, zato jo znova zgrajeni del tiho izpusti

ECMA-376 je predvidela polovico te težave. Specifikacija SpreadsheetML opredeljuje extLst (ECMA-376 1. del, "Future Feature Data Storage Area", §18.2.10 za element na ravni delovnega zvezka) kot določeno razširitveno točko: novejši proizvajalci tam parkirajo funkcije, od katerih je vsaka ovita v element <ext> z atributom uri, ki identificira funkcijo, od starejših bralnikov pa se pričakuje, da ohranijo tisto, česar ne razumejo. Sparklines, rezalniki (slicers) in novejše vrste pogojnega oblikovanja potujejo na ta način. Knjižnica, ki izpusti neznane bloke <ext>, zato ni le izgubna — krši pogodbo o združljivosti za nazaj, okoli katere je bil format zasnovan. Vprašanje za katero koli knjižnico preglednic, ki jo ocenjujete, je preprosto: če spremenim eno celico, kaj se še spremeni

Kako HotXLS ohrani lastno temo bajt za bajtom?

HotXLS ohrani temo delovnega zvezka tako, da predpomni izvirne bajte datoteke xl/theme/theme1.xml ob odpiranju in jih ob shranjevanju zapiše nazaj dobesedno. Del teme (ECMA-376 1. del, §14.2.7) je DrawingML in ne SpreadsheetML — barvne sheme, sheme pisav, sheme formatov — in mehanizem preglednic nima razloga, da bi ga globoko modeliral. Prejšnje različice HotXLS so ob vsakem shranjevanju znova ustvarile fiksno temo Office, kar je natanko zgoraj opisana napaka z vrnitvijo barv blagovne znamke na privzete. Od različice v2.89.46 dalje se tema odprtega paketa shrani v surovem stanju in ponovno odda nedotaknjena, vgrajena tema Office pa se ustvari le za delovne zvezke, ustvarjene iz nič. Surovi bajti so najmočnejše možno zagotovilo zanesljivosti: brez analize, brez ponovne serializacije, brez možnosti odstopanja

Dobesedna kopija namerno premaga programski dostop do teme. Razred TXLSXWorkbook izpostavlja lastnosti ThemeMajorFont in ThemeMinorFont, tako da lahko izberete pisave naslovov in telesa za nove delovne zvezke, ko pa je bila ob odpiranju zajeta dobesedna tema, ti nastavitelji nimajo vpliva na shranjeno datoteko — krožno potovanje ima prednost. Če resnično želite spremeniti temo obstoječega delovnega zvezka, je to znak, da predlogo uredite v samem Excelu in ne prek podatkovno usmerjenega API-ja. Vsakdanji primer sploh ne potrebuje API-ja:

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;

Kaj se ob shranjevanju zgodi z neznanimi bloki extLst?

HotXLS zajame vsak blok <ext> na ravni delovnega lista, ki ga izvorno ne modelira, in ga predvaja v extLst shranjenega delovnega lista, tako da funkcije, ki jih napišejo novejši Exceli, preživijo krožno potovanje nedotaknjene. Od različice v2.131.0 dalje so zajeti fragmenti vidni prek lastnosti RawWorksheetExts, ki je samo za branje in predstavlja TStringList na vsakem delovnem listu XLSX, kar omogoča preverjanje zanesljivosti iz testne kode namesto slepega verovanja:

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;

Podrobnost implementacije, ki jo je vredno poznati, je ta, da je zajem ponovna serializacija na ravni dogodkov in ne surovo kopiranje bajtov. HotXLS-ov pretočni bralnik XML ne izpostavlja izvornih odmikov, zato se neznano poddrevo znova zgradi iz dogodkov Element, Text in EndElement, ko se ti pretakajo mimo. Ta pristop skriva eno klasično past: samozapiralni element, kot je <a/>, sproži le dogodek Element, označen kot prazen, in nikoli EndElement, zato kateri koli števec globine, ki se zmanjšuje izključno ob EndElement, nikoli ne bo videl zapiranja poddrevesa. Če to rešite, je znova zgrajeni fragment semantično enakovreden izvirniku — narekovaji atributov in samozapiralne oblike so normalizirani, tako da ni bajtno identičen, vendar Excel bere pomen in ne bajtov. Dve lastnosti lastnega izhoda Excela omogočata varno predvajanje: Excel deklarira potrebne atribute xmlns na elementu <ext> ali znotraj njega, tako da je vsak zajeti fragment samostojen glede imenskih prostorov, ta samostojnost pa je razlog, zakaj lahko podvajanje delovnega lista znotraj ali med delovnimi zvezki prenese tuje bloke skupaj s preprosto dodelitvijo seznama nizov

Zapisovanje calcChain.xml, da Excel zaupa vašim formulam

HotXLS zapiše xl/calcChain.xml (del verige izračunov, ECMA-376 1. del, §12.3.1) vsakič, ko shranjeni delovni zvezek vsebuje formule, pri čemer izbira med dvema vrstnima redoma. Če je bil graf odvisnosti formul že zgrajen in je posodobljen — ko ste poklicali Recalculate po zadnjem urejanju —, se veriga odda v polnem topološkem vrstnem redu (odvisnosti pred odvisnimi celicami), člani krožnih sklicev pa se dodajo na koncu. V nasprotnem primeru so celice navedene v vrstnem redu dokumenta. Oba pristopa sta pravilna: Microsoftove opombe o implementaciji formata, [MS-XLSX], obravnavajo verigo izračunov kot namig, ki ga Excel preveri in znova razvrsti med nalaganjem, tako da je kateri koli celoten seznam legalen, HotXLS pa namerno zavrača prisilno gradnjo grafa znotraj SaveAs — gradnja povezav je namreč kvadratna glede na število celic, kar je nesprejemljiv skriti strošek pri shranjevanju milijona celic

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');

Kje se brezizgubno krožno potovanje konča

Iskrenost je tukaj pomembnejša od marketinške kljukice, zato si meje zaslužijo enako pozornost. HotXLS ne kopira celotnega paketa bajt za bajtom: XML delovnih listov, slogi, skupni nizi in deli delovnega zvezka se znova zgradijo iz analiziranega modela, zato je izhod semantično zvest, a ne binarno identičen — že lokalne glave ZIP nosijo sveže časovne žige DOS. Zajeti fragmenti <ext> se vrnejo normalizirani, kot je opisano zgoraj. Programski prepisi tematskih pisav se prezrejo, ko je prisotna dobesedna tema. Ohranitvena mreža pa ima določeno velikost zank: funkcije, ki jih HotXLS modelira izvorno (sparklines, na primer, se analizirajo in znova zapišejo ter ne le slepo kopirajo), tuji deli extLst in dobesedno predpomnjeni deli. Del, ki ni niti modeliran niti znotraj razširitvene točke — na primer del po meri eksotičnega dodatka —, pade izven treh mehanizmov, ki jih pokriva ta članek, zato raje preizkusite svoje dejanske predloge, kot pa da ugibate

Sorodno ohranitveno delo zaokrožuje sliko. Projekti VBA in zunanji sklici na delovne zvezke preživijo shranjevanje po enaki filozofiji ohranjanja tistega, česar ne modeliramo, kar je opisano v spremljevalnem članku o ohranjanju projektov VBA in zunanjih povezav, lastnosti dokumenta v docProps pa imajo svoj lasten API za branje in pisanje in se ne izpustijo tiho. Ko ocenjujete katero koli knjižnico preglednic, izvedite test z eno celico: odprite funkcijsko bogat produkcijski delovni zvezek, spremenite eno vrednost, shranite in primerjajte (diff) razpakirane dele z izvirnikom. Kaj se je spremenilo poleg lista, ki ste se ga dotaknili, vam pove več o knjižnici kot katera koli tabela funkcij

Mehanizmi krožnega potovanja, opisani tukaj — dobesedno ohranjanje teme od različice v2.89.46 dalje, zajem tujih elementov extLst in oddajanje calcChain.xml od različice v2.131.0 dalje —, so na voljo v trenutni različici komponente HotXLS Delphi Excel Component, katere stran izdelka dokumentira celoten nabor funkcij za branje in pisanje XLSX za Delphi in C++Builder