HotXLS Delphi Component predvaja nespremenjen grafikon Excel bajt za bajtom samo, kadar veljata dve stvari: do grafikona se je prišlo prek relacije risbe delovnega lista in ne prek ugibanega imena dela ter je bil 64-bitni prstni odtis modela zajet po tem, ko je razčlenjevanje modela grafikona že končano. Različica 2.382.0 je odpravila prvi pogoj, različica 2.382.3 pa drugega in začela voziti v obe smeri neničelna odmika sidra xdr:colOff in xdr:rowOff, ki jih je pisec risbe dotlej trdo nastavljal na nič. Oba defekta sta prišla iz enega samega primera lokalnega korpusa, two-charts.xlsx: najprej je strukturna trditev opazila, da sta iz dveh delov grafikona nastali trije, nato pa je primerjava bajtov vsake datoteke xl/charts/chartN.xml pokazala, da se grafikoni, ki se jih ni nihče dotaknil, še vedno prepisujejo — in noben od obeh problemov ni sprožil izjeme niti povzročil pritožbe Excela, zato sta tako dolgo preživela
Zakaj se je delovni zvezek z dvema grafikonoma vrnil s tremi deli grafikona?
Ker je imel nalagalnik rezervno pot, ki je ugibala. Ko delovni list v svojem delu .rels ni imel relacije risbe, je stara koda domnevala, da risba živi pod običajnim imenom xl/drawings/drawing{i+1}.xml, kjer je i položaj lista, in je ta del pripela, če je v arhivu obstajal. V datoteki two-charts.xlsx prvi list nima ne risbe ne dela .rels, medtem ko xl/drawings/drawing1.xml obstaja — pripada drugemu listu, ki do njega pride prek Target="../drawings/drawing1.xml". List 1 je zato podedoval grafikon, na katerega se ni nikoli skliceval, chart1.xml se je razčlenil dvakrat, shranjevanje pa je delovni zvezek zapisalo s tremi deli grafikona namesto z dvema
Popravek v HotXLS 2.382.0 je ugibanje v celoti odstranil. Risba delovnega lista se zdaj naloži le prek ParPartTargets[i].Values[XlsxRtDrawing], cilja, zabeleženega za tip relacije risbe na tem listu, list brez takšne relacije pa ne dobi nobene risbe. To je vedenje, ki ga format zahteva: element <drawing r:id="…"/> v delovnem listu (ECMA-376 1. del §18.3.1.36) je edina povezava med listom in njegovo risbo, imena delov v paketu OPC pa ne nosijo nobenega pomena onkraj tistega, ki jim ga dodeli graf relacij. Arhivi, ki jih zapiše Excel, slučajno uporabljajo običajna imena, zato je bližnjica tako dolgo šla skozi; pregled razreševanja relacij OPC v HotXLS pokriva, zakaj ugibanje imena dela ni nikoli varno, tudi kadar je ugibanje običajno pravilno
// Pred v2.382.0: manjkajoča relacija risbe je padla na ugibanje
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // lahko pripada drugemu listu
// Od v2.382.0: relacija ali nič
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Kaj zagotavlja prstni odtis grafikona?
Prstni odtis se za vsak grafikon odloči, ali lahko shranjevanje kopira izvirni del ali ga mora znova zgraditi. Ob uvozu, ko je PreserveUnsupportedParts omogočen pred Open, HotXLS obdrži surove bajte UTF-8 vsakega dela grafikona v FRawChartXml, zgradi lastno serializacijo tipiziranega modela z BuildChartKnownXml ter shrani dolžino te serializacije v FRawChartModelLength in njen hash v FRawChartModelHash. Hash je FNV-1a čez kodne enote UTF-16 generiranega XML, z običajno 64-bitno osnovo odmika 14695981039346656037 in praštevilom 1099511628211. Ob shranjevanju XlsxChartRawModelUnchanged znova zgradi znani XML ter primerja dolžino in hash; ujemanje pomeni, da je tipizirani model natanko tak, kot je bil ob uvozu, torej se ni spremenilo nič, kar bi aplikacija lahko spremenila
function XlsxChartRawModelUnchanged(Chart: TXLSXChart;
const KnownXml: WideString): Boolean;
begin
Result := (Chart <> nil) and (Chart.FRawChartXml <> '') and
(Length(KnownXml) = Chart.FRawChartModelLength) and
(XlsxChartModelHash(KnownXml) = Chart.FRawChartModelHash);
end;
function BuildChartXmlFromKnown(Chart: TXLSXChart;
const KnownXml: WideString): WideString;
begin
if Chart.FRawChartXml = '' then
Result := KnownXml // nič ohranjenega
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // dobesedno predvajanje
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // strukturno združevanje
end;
Pisec XLSX gre korak dlje od BuildChartXmlFromKnown. Ko je model nespremenjen in je StrictOOXML izključen, najprej poskusi kopirati stisnjeni vnos naravnost iz izvornega arhiva v izhod pod novim imenom dela grafikona, tako da se bajti sploh ne dekodirajo in znova stisnejo. Šele če ta kopija ni mogoča, pade na pot dekodiraj-ali-združi. Mehanizem sam — dolžina plus hash, predvajaj, ko sta enaka, združi, ko nista — je tisti, ki je opisan v zapisku o urejanju grafikonov Excel brez izgube ChartML. Ta članek pa je o tem, kako je tiho nehal delovati
Zakaj je vsak grafikon vseeno šel po poti združevanja?
Ker je bil prstni odtis zajet en klic prezgodaj. Razčlenjevanje grafikonov v HotXLS je prehod SAX čez del grafikona, ki mu sledi niz obnovitvenih prehodov, ki iz surovega besedila potegnejo podrobnosti, ki jih obdelovalci SAX ne modelirajo neposredno: XlsxChartParseSeriesFlags prebere vsak blok <c:ser> zaradi njegove zastavice <c:smooth> in vrednosti srgbClr polnila in obrobe označevalca, nato pa obnovi načine prečkanja osi ter sloge glavnih in stranskih oznak za kategorijsko in vrednostno os. Pred različico 2.382.3 je bil vrstni red na koncu ParseChartXml tak: razvrsti skupine osi, zgradi znani XML, zajemi dolžino in hash ter šele nato poženi XlsxChartParseSeriesFlags. Prstni odtis je zato opisoval model, ki mu še manjkajo zastavice smooth, barve označevalcev in oznake. Ob shranjevanju se je BuildChartKnownXml izvedel nad dokončanim modelom, ki je zdaj oddajal <c:smooth val="1"/> in obnovljene barve označevalcev. Daljši XML, drugačen hash, XlsxChartRawModelUnchanged vrne False in grafikon gre skozi XlsxMergeChartXml. Združevanje je pravilna operacija za grafikon, ki ga je nekdo urejal, ni pa operacija, ki bi ohranjala bajte: drevo znova serializira, pravilo lastništva, ki za serije, osi in skupine risbe pusti zmagati tipiziranemu modelu, pa pomeni, da regenerirana vozlišča nadomestijo izvirnike. Viden rezultat v zagonu korpusa so bile barve serij, ki so se premaknile na grafikonih, ki jih nihče ni urejal — vsak grafikon v vsakem ohranjenem delovnem zvezku, ob vsakem shranjevanju, brez kakršne koli diagnostike
Popravek je ena sama prerazporeditev: XlsxChartParseSeriesFlags se zdaj izvede, preden se zgradi znani XML, tako da prstni odtis opisuje model, kakršen bo obstajal, ko ga aplikacija prvič vidi. Lekcija se posploši onkraj grafikonov. Prstni odtis za zaznavanje sprememb je toliko dober, kolikor je dober trenutek njegovega zajema, varen trenutek pa je po tem, ko so končani vsi prehodi, ki lahko spremenijo model. HotXLS ima za isti vrednosti drugo mesto zajema, izhodišče, ki ga po uspešnem shranjevanju znova vzpostavi glede na izhodno datoteko, in to mesto se je vedno izvajalo nad v celoti razčlenjenim modelom; mesto ob uvozu je bilo tisto, ki je izstopalo
Kam so izginili odmiki sidra?
V dobesedno ničlo. twoCellAnchor v delu risbe pripne grafikon med dve celici, vsak vogal pa nosi indeks celice in odmik znotraj te celice: from (ECMA-376 1. del §20.5.2.5) in to (§20.5.2.32) vsebujeta col, colOff (§20.5.2.4), row in rowOff. Odmiki so v angleških metričnih enotah, 914400 na palec, Excel pa neničelne vrednosti zapiše vedno, ko je bil grafikon postavljen ali spremenjen z miško, kar velja za večino grafikonov. Prvi grafikon v datoteki two-charts.xlsx se začne v vrstici 0 z rowOff 19049 in konča v stolpcu 8, vrstici 15, s colOff 247650 in rowOff 66674 — približno četrt palca v zadnji stolpec. Razčlenjevalnik risbe v HotXLS je te štiri vrednosti vedno bral — koda za slike jih je uporabljala — pisec grafikona pa je za vsak vogal oddal <xdr:colOff>0</xdr:colOff> in <xdr:rowOff>0</xdr:rowOff>, s čimer je ob shranjevanju vsak grafikon prilepil na mrežo celic
// Od v2.382.3 pisec sidra predvaja uvožene odmike EMU
Result := '<xdr:twoCellAnchor' + EditAsAttr + '><xdr:from><xdr:col>' +
IntToStr(Chart.FromCol - 1) + '</xdr:col><xdr:colOff>' +
IntToStr(Chart.FFromColOff) + '</xdr:colOff>' +
'<xdr:row>' + IntToStr(Chart.FromRow - 1) + '</xdr:row>' +
'<xdr:rowOff>' + IntToStr(Chart.FFromRowOff) + '</xdr:rowOff></xdr:from>' +
'<xdr:to><xdr:col>' + IntToStr(Chart.ToCol - 1) + '</xdr:col><xdr:colOff>' +
IntToStr(Chart.FToColOff) + '</xdr:colOff>' +
'<xdr:row>' + IntToStr(Chart.ToRow - 1) + '</xdr:row>' +
'<xdr:rowOff>' + IntToStr(Chart.FToRowOff) + '</xdr:rowOff></xdr:to>' + ...
TXLSXChart zdaj nosi FFromColOff, FFromRowOff, FToColOff in FToRowOff, napolnjene iz razčlenjevalnika risbe in kopirane skupaj z drugim stanjem sidra, ko je grafikon dodeljen. Namenoma so zasebne: javna površina sidra so še naprej štiri koordinate celic FromRow, FromCol, ToRow in ToCol, grafikon, ustvarjen iz kode Delphi, pa pristane na mejah celic kot prej. Odmiki obstajajo zato, da je obhod zvest, ne zato, da bi podcelično pozicioniranje izpostavili kot funkcijo. Ta popravek je neodvisen od prstnega odtisa: sidro živi v delu risbe in ne v delu grafikona, zato bi grafikon, katerega ChartML se je predvajal brezhibno, brez njega še vedno skočil na mrežo. Pretvorbe enot za temi vrednostmi EMU so obdelane v zapisku o geometriji slik HotXLS in skaliranju EMU
Kako dokažete, da se grafikon vrne nespremenjen?
S primerjanjem bajtov, ne z odpiranjem rezultata v Excelu. Excel ob nalaganju toliko popravi in normalizira, da je premaknjen grafikon videti v redu vse do trenutka, ko analitik opazi, da se je barva označevalca spremenila. Test korpusa, ki je ujel oba defekta, po odpiranju in shranjevanju brez urejanj naredi tri stvari: prehodi relacije delovnega lista, risbe in grafikona ter odpove ob vsaki podvojeni, osiroteli ali viseči referenci grafikona; primerja podpis tipa grafikona, formul serij in geometrije sidra med izvirnikom in izhodom; za two-charts.xlsx pa prebere vsak xl/charts/chartN.xml iz obeh arhivov in zahteva identične bajte. Isti preizkus je v Delphiju preprosto napisati z RTL TZipFile
uses System.Zip, System.SysUtils;
function ChartPartsIdentical(const Original, Resaved: string): Boolean;
var
Src, Dst: TZipFile;
Name: string;
A, B: TBytes;
begin
Result := True;
Src := TZipFile.Create;
Dst := TZipFile.Create;
try
Src.Open(Original, zmRead);
Dst.Open(Resaved, zmRead);
for Name in Src.FileNames do
if Name.StartsWith('xl/charts/chart') and Name.EndsWith('.xml') then
begin
Src.Read(Name, A);
Dst.Read(Name, B); // sproži izjemo, če je del izginil
if (Length(A) <> Length(B)) or
((Length(A) > 0) and not CompareMem(@A[0], @B[0], Length(A))) then
begin
Writeln('changed: ', Name);
Result := False;
end;
end;
finally
Dst.Free;
Src.Free;
end;
end;
Trije pogoji naredijo to primerjavo smiselno in vsak od njih tiho odpove, če ga pozabite. PreserveUnsupportedParts mora biti True pred Open, sicer se ne zajame noben surov bajt in vsak grafikon se znova zgradi iz modela. StrictOOXML mora biti False, ker strogi način po zasnovi vsili regeneracijo. In aplikacija se med odpiranjem in shranjevanjem ne sme dotakniti grafikona — branje lastnosti je v redu, vsak setter, ki spremeni tipizirani model, pa obrne prstni odtis in pošlje grafikon po poti združevanja, kar je pravilno vedenje in ne tisto, za kar je ta test. Deli grafikona se ob shranjevanju tudi znova oštevilčijo iz števca na ravni celotnega delovnega zvezka, zato bo delovni zvezek, pri katerem se je spremenil vrstni red listov ali grafikonov, enake bajte postavil pod drugo ime chartN.xml; preverjevalnik korpusa zato sledi relacijam in ne imenom
Oba popravka sta izšla v HotXLS 2.382.0 in 2.382.3 ter sta preverjena na Win32 in Win64 proti lokalnemu korpusu, pri čemer so bili znova shranjeni vzorci grafikonov tudi upodobljeni prek neodvisne pisarniške zbirke v PDF in primerjani stran za stran z izvirniki. HotXLS bere, ureja in piše grafikone XLSX iz izvorne kode Delphi in C++Builder brez nameščenega Excela, zato je ta raven zvestobe odgovornost knjižnice — stran izdelka komponente HotXLS Delphi za preglednice ima seznam funkcij in preskusno različico