HotXLS Delphi Component reprodukuje neizmenjen Excel grafikon bajt za bajt samo ako važe dve stvari: grafikonu se pristupilo preko drawing relacije radnog lista, a ne preko pogođenog imena dela, i fingerprint modela od 64 bita snimljen je pošto je parsiranje modela grafikona završeno. Verzija 2.382.0 ispravila je prvi uslov, verzija 2.382.3 drugi i počela da round-trip-uje nenulte xdr:colOff i xdr:rowOff anchor offsete koje je pisac drawing-a dotad hardkodirao na nulu. Oba defekta izašla su iz jednog lokalnog corpus slučaja, two-charts.xlsx: najpre je strukturna provera videla da dva chart dela postaju tri, a zatim je poređenje bajtova svakog xl/charts/chartN.xml pokazalo da se grafikoni koje niko nije dirao i dalje prepisuju — i nijedan problem nije bacio izuzetak niti je Excel zbog njih protestvovao, što je razlog zašto su tako dugo preživeli
Zašto je radna sveska sa dva grafikona vraćena sa tri chart dela?
Zato što je loader imao fallback koji je pogađao. Kada radni list nije imao drawing relaciju u svom .rels delu, stari kod je pretpostavljao da drawing živi pod konvencionalnim imenom xl/drawings/drawing{i+1}.xml, gde je i pozicija lista, i kačio je taj deo ako postoji u arhivi. U two-charts.xlsx prvi list nema drawing niti .rels deo uopšte, dok xl/drawings/drawing1.xml postoji — pripada drugom listu, koji do njega dolazi preko Target="../drawings/drawing1.xml". List 1 je zato nasledio grafikon na koji se nikad nije pozivao, chart1.xml je parsiran dvaput, a čuvanje je radnu svesku ispisalo sa tri chart dela umesto dva
Ispravka u HotXLS-u v2.382.0 uklonila je pogađanje u potpunosti. Drawing radnog lista sada se učitava isključivo preko ParPartTargets[i].Values[XlsxRtDrawing], targeta zabeleženog za tip drawing relacije na tom listu, a list bez takve relacije ne dobija drawing uopšte. To je ponašanje koje format zahteva: element <drawing r:id="…"/> u radnom listu (ECMA-376 Part 1 §18.3.1.36) jedina je veza između lista i njegovog drawing-a, a imena delova u OPC paketu nemaju nikakvo značenje osim onoga koje im dodeli graf relacija. Arhive koje piše Excel slučajno koriste konvencionalna imena, i to je ono što je prečici omogućilo da tako dugo prolazi; tekst o razrešavanju OPC relacija u HotXLS-u objašnjava zašto pogađanje imena dela nikad nije bezbedno čak i kada je pogodak obično tačan
// Pre v2.382.0: drawing relacija koja nedostaje vraćala se na pogađanje
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // može pripadati drugom listu
// Od v2.382.0: relacija ili ništa
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Šta garantuje fingerprint grafikona?
Fingerprint odlučuje, po grafikonu, da li čuvanje može da kopira originalni deo ili mora da ga regeneriše. Pri uvozu, uz PreserveUnsupportedParts uključen pre Open, HotXLS čuva sirove UTF-8 bajtove svakog chart dela u FRawChartXml, gradi sopstvenu serializaciju tipiziranog modela pomoću BuildChartKnownXml i smešta dužinu te serializacije u FRawChartModelLength a njen hash u FRawChartModelHash. Hash je FNV-1a nad UTF-16 kodnim jedinicama generisanog XML-a, sa standardnom 64-bitnom osnovom 14695981039346656037 i prostim brojem 1099511628211. Pri čuvanju XlsxChartRawModelUnchanged ponovo gradi known XML i poredi dužinu i hash; poklapanje znači da je tipizirani model tačno ono što je bio pri uvozu, pa ništa od onoga što je aplikacija mogla da izmeni nije izmenjeno
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šta nije sačuvano
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // verbatim reprodukcija
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // strukturna merge
end;
XLSX pisac ide i korak dalje od BuildChartXmlFromKnown. Kada je model neizmenjen a StrictOOXML isključen, prvo pokušava da kopira kompresovani unos direktno iz izvorne arhive u izlaz pod novim imenom chart dela, tako da se bajtovi čak ni ne dekodiraju i ponovo deflate-uju. Samo ako ta kopija nije moguća, pada na putanju dekodiranja ili merge-a. Sam mehanizam — dužina plus hash, reprodukcija kada su jednaki, merge kada nisu — onaj je opisan u tekstu o izmeni Excel grafikona bez gubljenja ChartML-a. Ovaj članak govori o načinu na koji je tiho prestao da radi
Zašto je svaki grafikon ipak išao merge putanjom?
Zato što je fingerprint snimljen jedan poziv prerano. Parsiranje grafikona u HotXLS-u je SAX prolaz kroz chart deo praćen skupom prolaza oporavka koji iz sirovog teksta vade detalje koje SAX handleri ne modeluju direktno: XlsxChartParseSeriesFlags čita svaki blok <c:ser> zbog njegove <c:smooth> zastavice i srgbClr vrednosti marker fill-a i marker linije, a zatim oporavlja režime presecanja osa i stilove glavnih i pomoćnih tick mark-ova za kategorijsku i vrednosnu osu. Pre v2.382.3 redosled na kraju ParseChartXml bio je: klasifikuj grupe osa, izgradi known XML, snimi dužinu i hash, i tek onda pokreni XlsxChartParseSeriesFlags. Fingerprint je zato opisivao model kojem još nedostaju smooth zastavice, boje markera i tick mark-ovi. Pri čuvanju BuildChartKnownXml se izvršavao nad završenim modelom, koji je sada emitovao <c:smooth val="1"/> i oporavljene boje markera. Duži XML, drugačiji hash, XlsxChartRawModelUnchanged vraća False, i grafikon ide kroz XlsxMergeChartXml. Merge je korektna operacija za grafikon koji je neko menjao, ali nije operacija koja čuva bajtove: reserializuje stablo, a pravilo vlasništva koje tipiziranom modelu daje prednost za serije, ose i plot grupe znači da regenerisani čvorovi zamenjuju originale. Vidljiv rezultat u corpus pokretanju bile su iskrivljene boje serija na grafikonima koje niko nije menjao — svaki grafikon u svakoj sačuvanoj radnoj svesci, pri svakom čuvanju, bez ikakve dijagnostike
Popravka je jedno preuređivanje: XlsxChartParseSeriesFlags sada se izvršava pre nego što se known XML izgradi, pa fingerprint opisuje model onakav kakav će biti kada ga aplikacija prvi put vidi. Pouka važi i izvan grafikona. Fingerprint za detekciju promena vredi onoliko koliko vredi trenutak u kojem je uzet, a bezbedan trenutak je pošto se završe svi prolazi koji mogu da izmene model. HotXLS ima drugo mesto snimanja istih dveju vrednosti, baseline koji ponovo uspostavlja prema izlaznom fajlu posle uspešnog čuvanja, i to mesto se oduvek izvršavalo nad potpuno parsiranim modelom; mesto pri uvozu je bilo izuzetak
Gde su otišli anchor offseti?
U bukvalnu nulu. twoCellAnchor u drawing delu prikiva grafikon između dve ćelije, a svaki ugao nosi indeks ćelije plus offset unutar te ćelije: from (ECMA-376 Part 1 §20.5.2.5) i to (§20.5.2.32) drže svaki po col, colOff (§20.5.2.4), row i rowOff. Offseti su u English Metric Units, 914400 na inč, i Excel upisuje nenulte vrednosti kad god je grafikon postavljen ili menjan mišem, što je većina grafikona. Prvi grafikon u two-charts.xlsx počinje u redu 0 sa rowOff od 19049 i završava se u koloni 8, redu 15 sa colOff od 247650 i rowOff od 66674 — oko četvrt inča u poslednju kolonu. Parser drawing-a u HotXLS-u oduvek je čitao te četiri vrednosti — kod za slike ih je koristio — ali pisac grafikona emitovao je <xdr:colOff>0</xdr:colOff> i <xdr:rowOff>0</xdr:rowOff> za svaki ugao, uklapajući svaki grafikon u mrežu ćelija pri čuvanju
// Od v2.382.3 pisac anchor-a reprodukuje uvezene EMU offsete
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 sada nosi FFromColOff, FFromRowOff, FToColOff i FToRowOff, popunjene iz parsera drawing-a i kopirane zajedno sa ostalim anchor stanjem kada se grafikon dodeljuje. Namerno su privatne: javna anchor površina i dalje su četiri koordinate ćelija FromRow, FromCol, ToRow i ToCol, a grafikon napravljen iz Delphi koda sleće na granice ćelija kao i pre. Offseti postoje da round trip bude veran, a ne da pozicioniranje unutar ćelije izlože kao funkcionalnost. Ova popravka je nezavisna od fingerprint-a: anchor živi u drawing delu, ne u chart delu, pa bi grafikon čiji se ChartML reprodukovao savršeno i bez nje skočio u mrežu. Konverzije jedinica iza tih EMU vrednosti obrađene su u tekstu o geometriji slika i EMU skaliranju u HotXLS-u
Kako dokazati da grafikon prolazi round trip neizmenjen?
Poređenjem bajtova, a ne otvaranjem rezultata u Excel-u. Excel pri učitavanju popravlja i normalizuje toliko toga da iskrivljen grafikon izgleda dobro sve dok analitičar ne primeti da se boja markera promenila. Corpus test koji je uhvatio oba defekta radi tri stvari posle otvaranja i čuvanja bez izmena: prolazi kroz relacije radnog lista, drawing-a i grafikona i pada na svakoj dupliranoj, napuštenoj ili visećoj chart referenci; poredi potpis tipa grafikona, formula serija i anchor geometrije između originala i izlaza; a za two-charts.xlsx čita svaki xl/charts/chartN.xml iz obe arhive i zahteva identične bajtove. Ista provera se lako piše u Delphiju uz 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); // baca izuzetak ako je deo nestao
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;
Tri uslova čine to poređenje smislenim, i svaki od njih tiho pada ako se zaboravi. PreserveUnsupportedParts mora biti True pre Open, inače se sirovi bajtovi ne snimaju i svaki grafikon se ponovo gradi iz modela. StrictOOXML mora biti False, jer strict režim po dizajnu forsira regeneraciju. I aplikacija ne sme da dira grafikon između otvaranja i čuvanja — čitanje svojstava je u redu, ali svaki setter koji menja tipizirani model obrće fingerprint i šalje grafikon na merge putanju, što je korektno ponašanje ali nije ono za šta ovaj test služi. Chart delovi se pri čuvanju takođe prenumerišu iz brojača na nivou cele radne sveske, pa će radna sveska kojoj se promenio redosled listova ili grafikona smestiti identične bajtove pod drugačije ime chartN.xml; corpus proverivač zato prati relacije a ne imena
Obe ispravke isporučene su u HotXLS-u 2.382.0 i 2.382.3 i verifikovane na Win32 i Win64 nad lokalnim corpus-om, a ponovo sačuvani uzorci grafikona renderovani su i kroz nezavisni office paket u PDF i upoređeni stranu po stranu sa originalima. HotXLS čita, menja i piše XLSX grafikone iz nativnog Delphi i C++Builder koda bez ikakve instalacije Excel-a, i upravo zato je ovakav nivo vernosti odgovornost biblioteke — stranica HotXLS Delphi spreadsheet komponente sadrži listu funkcionalnosti i probnu verziju