Tehnički članak

XLSX povratno putovanje bez gubitaka u Delphiju: Tema, extLst, calcChain

HotXLS, izvorna Delphi i C++Builder Excel biblioteka, izgrađena je za XLSX povratno putovanje (round-trip) bez gubitaka: otvorite radnu svesku, promenite jednu ćeliju, sačuvajte, i kupčeva prilagođena tema, spoljni extLst prošireni blokovi i lanac izračunavanja (calculation chain) preživljavaju. Tri mehanizma to čine mogućim — doslovno keširanje (verbatim caching) datoteke xl/theme/theme1.xml, ponovna serijalizacija nepoznatih blokova <ext> utemeljena na događajima te sveža i prema specifikaciji valjana datoteka xl/calcChain.xml pri svakom čuvanju radne sveske sa formulama

Scenario koji motiviše sva tri mehanizma deprimirajuće je čest. Usluga naplate učitava šablon koji je kupac dizajnirao u Excelu — korporativnu temu boja, mini-grafikone (sparklines) u KPI koloni, pravilo uslovnog oblikovanja dodato novijom verzijom Excela — upisuje jedan ukupni iznos računa u ćeliju B3 i čuva. Kupac otvara rezultat, a korporativne boje su se vratile na fabrički plavu Office boju, mini-grafikoni su nestali, a Excel nudi 'popravku' datoteke. Ništa u kodu nije dotaklo nijednu od tih funkcija. Biblioteka jeste, jednostavno čuvanjem

Zašto Excel datoteke gube oblikovanje nakon uređivanja pomoću biblioteke?

Excel datoteke gube oblikovanje nakon uređivanja pomoću biblioteke jer većina biblioteka ne uređuje datoteku — one je ponovo grade. Paket .xlsx je ZIP XML delova: xl/workbook.xml, jedna xl/worksheets/sheetN.xml po listu, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml i drugi. Tipična biblioteka analizira te delove u model objekta pri otvaranju i regeneriše svaki deo iz tog modela pri čuvanju. Bilo koja funkcija koju model ne predstavlja — tema koju nikada nije analizirao, blok proširenja iz novijeg Excela — nema gde da živi u memoriji, pa je regenerisani deo tiho izostavlja

Specifikacija ECMA-376 predvidela je polovinu ovog problema. SpreadsheetML definiše extLst (ECMA-376 Deo 1, 'Područje za čuvanje podataka budućih funkcija', §18.2.10 za element na nivou radne sveske) kao označenu tačku proširenja: noviji proizvođači tamo smeštaju funkcije, svaku omotanu u element <ext> koji nosi atribut uri koji identifikuje funkciju, a od starijih se potrošača očekuje da sačuvaju ono što ne razumeju. Mini-grafikoni, rezači (slicers) i novije vrste uslovnog oblikovanja putuju na ovaj način. Biblioteka koja odbacuje nepoznate blokove <ext> stoga nije samo manjkava (lossy) — ona krši ugovor o kompatibilnosti prema napred oko kojeg je format dizajniran. Pitanje koje treba postaviti svakoj biblioteci proračunskih tabela koju procenjujete je direktno: ako promenim jednu ćeliju, šta se još menja

Kako HotXLS čuva prilagođenu temu bajt po bajt?

HotXLS čuva temu radne sveske keširanjem izvornih bajtova xl/theme/theme1.xml pri otvaranju i njihovim doslovnim zapisivanjem nazad pri čuvanju. Deo sa temom (ECMA-376 Deo 1, §14.2.7) je DrawingML, a ne SpreadsheetML — sheme boja, sheme fontova, sheme formata — i mehanizam proračunskih tabela nema razloga duboko da ga modelira. Ranije verzije HotXLS-a regenerisale su fiksnu Office temu pri svakom čuvanju, što je tačno gore opisani neuspeh sa povratkom na fabričke boje; od verzije v2.89.46 tema otvorenog paketa čuva se sirova i ponovno emituje netaknuta, a ugrađena Office tema generiše se samo za radne sveske stvorene ispočetka. Sirovi bajtovi su najjače moguće jemstvo vernosti: nema analiziranja, nema ponovne serijalizacije, nema šanse za odstupanje

Doslovna kopija namerno pobeđuje programski pristup temi. Klasa TXLSXWorkbook izlaže ThemeMajorFont i ThemeMinorFont kako biste mogli da odaberete heading i body fontove za nove radne sveske, no kada se pri otvaranju uhvati doslovna tema, ti postavljači (setters) nemaju uticaja na sačuvanu datoteku — povratno putovanje ima prioritet. Ako uistinu trebate da promenite temu postojeće radne sveske, to je znak da šablon uredite u samom Excelu, a ne putem API-ja usmerenog na podatke. Svakodnevni slučaj uopšte ne treba API:

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;

Šta se događa sa nepoznatim extLst blokovima pri čuvanju?

HotXLS zahvata svaki prošireni blok <ext> na nivou lista koji izvorno ne modelira i ponovo ga reprodukuje u extLst sačuvanog lista, tako da funkcije zapisane novijim verzijama Excela preživljavaju povratno putovanje netaknute. Od verzije v2.131.0, uhvaćeni fragmenti su vidljivi kroz svojstvo samo za čitanje RawWorksheetExts, što je TStringList na svakom XLSX listu, što čini jemstvo proverljivim iz testnog koda umesto činom vere:

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;

Detalj implementacije koji vredi znati jeste da je zahvatanje ponovna serijalizacija na nivou događaja, a ne sirova kopija bajtova. HotXLS-ov čitač XML-a u toku ne izlaže izvorne pomake, pa se nepoznato podstablo ponovo gradi od događaja Element, Text i EndElement dok prolaze kroz tok. Taj pristup skriva jednu klasičnu zamku: samozatvarajući element poput <a/> pokreće samo događaj Element osnačen kao prazan, a nikada EndElement, pa bilo koji brojač dubine koji se smanjuje isključivo pri EndElementu nikada neće videti zatvaranje podstabla. Rešite to, i obnovljeni fragment je semantički ekvivalentan izvorniku — navođenje atributa i samozatvarajući oblici se normalizuju, tako da nije bajt-identičan, ali Excel čita značenje, a ne bajtove. Dva svojstva Excelovog sopstvenog izlaza čine reprodukciju bezbednom: Excel deklarise potrebne atribute xmlns na elementu <ext> ili unutar njega, pa je svaki uhvaćeni fragment prostorno-nazivno (namespace) samostalan. Ta je samostalnost razlog zašto dupliranje lista unutar ili preko radnih sveski može preneti spoljne blokove uz obično dodeljivanje string-liste

Zapisivanje calcChain.xml kako bi Excel verovao vašim formulama

HotXLS zapisuje xl/calcChain.xml (deo lanca izračunavanja, ECMA-376 Deo 1, §12.3.1) kad god sačuvana radna sveska sadrži formule, i bira između dva redosleda. Ako je grafikon zavisnosti formula već izgrađen i trenutan — pozvali ste Recalculate nakon poslednjeg uređivanja — lanac se emituje u potpunom topološkom redosledu, zavisnosti pre zavisnih, sa dodanim članovima kružne reference na kraju. U suprotnom, ćelije se popisuju redosledom u dokumentu. Oba su načina tačna: Microsoftove zabeleške o implementaciji formata, [MS-XLSX], tretiraju lanac izračunavanja kao nagoveštaj koji Excel proverava i preuređuje tokom učitavanja, pa je svaki potpuni spisak legalan, a HotXLS namerno odbija da forsira izgradnju grafikona unutar SaveAs — stvaranje bridova je kvadratno u odnosu na broj ćelija, što je neprihvatljiv skriveni trošak pri čuvanju radne sveske sa milion ćelija

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

Zašto brinuti o delu koji Excel tretira kao savetodavan? Zato što je njegova odsutnost signal. Neki potrošači — heuristika popravke, pregledači trećih strana, alati za usporedbu (diff) — očekuju da radna sveska sa formulama nosi lanac izračunavanja, a biblioteka koja tiho odbacuje taj deo pri čuvanju proizvodi datoteke koje su suptilno različite od svega što Excel piše. Emitovanje valjanog lanca drži izlaz unutar okvira onoga za šta je ostatak ekosistema testiran, što je tiha, neglamurozna jezgra inženjeringa povratnog putovanja

Gde prestaje povratno putovanje bez gubitaka

Poštenje je ovde važnije od marketinškog okvira, pa granice zaslužuju jednaku pažnju. HotXLS ne kopira ceo paket bajt po bajt: XML radnog lista, stilovi, deljeni nizovi znakova (shared strings) i delovi radne sveske regenerišu se iz analiziranog modela, tako da je izlaz semantički veran, ali ne i binarno identičan — sami lokalni ZIP zaglavlja nose sveže DOS vremenske oznake. Uhvaćeni fragmenti <ext> vraćaju se normalizovani, kao što je gore opisano. Programska nadjačavanja fontova teme se zanemaruju kada je prisutna doslovna tema. Mreža očuvanja ima definisanu strukturu: funkcije koje HotXLS izvorno modelira (mini-grafikoni, na primer, analiziraju se i ponovo zapisuju umesto da se slepo kopiraju) plus spoljni sadržaj extLst plus doslovno keširani delovi. Deo koji nije modeliran niti se nalazi unutar tačke proširenja — recimo, prilagođeni deo nekog egzotičnog dodatka — van je tri mehanizma koje ovaj članak pokriva, stoga testirajte svoje stvarne šablone umesto pretpostavljanja

Srodni radovi na očuvanju nadopunjuju sliku. Projekti VBA i reference na vanjske radne sveske prolaze kroz čuvanje prema istoj filozofiji zadržavanja onoga što se ne modelira, što je pokriveno u pratećem članku o očuvanju VBA i spoljnih poveznica, a svojstva dokumenta u docProps imaju sopstveni read-write API umesto da budu tiho odbačena. Kada procenjujete bilo koju biblioteku proračunskih tabela, pokrenite test sa jednom ćelijom: otvorite funkcijama bogatu produkciju radnu svesku, promenite jednu vrednost, sačuvajte i uporedite otpakovane delove sa izvornikom. Ono što se promenilo van lista koji ste dotaknuli govori vam više o biblioteci nego bilo koja tabela funkcija

Mehanizmi povratnog putovanja koji su ovde opisani — doslovno zadržavanje teme od verzije v2.89.46, zahvatanje spoljnog extLst i emitovanje calcChain.xml od verzije v2.131.0 — dolaze u trenutnoj komponenti HotXLS Delphi Excel Component, čija stranica proizvoda dokumentuje celokupni skup funkcija za čitanje i pisanje XLSX datoteka za Delphi i C++Builder