HotXLS Delphi Component reproducira neizmijenjeni Excel grafikon bajt po bajt samo kada vrijede dvije stvari: grafikon je dohvaćen preko relacije crteža radnog lista, a ne preko pogađanog imena dijela, i 64-bitni fingerprint modela snimljen je nakon što je parsiranje modela grafikona završilo. Verzija 2.382.0 popravila je prvi uvjet, verzija 2.382.3 drugi i pokrenula round trip nenultih xdr:colOff i xdr:rowOff anchor pomaka koje je pisac crteža dotad tvrdo kodirao na nulu. Oba defekta izašla su iz jednog lokalnog corpus slučaja, two-charts.xlsx: najprije je strukturna provjera vidjela da dva chart dijela postaju tri, a zatim je usporedba bajtova svakog xl/charts/chartN.xml pokazala da se grafikoni koje nitko nije dirao i dalje prepisuju — a nijedan problem nije bacio iznimku niti je Excel prigovorio, zato su tako dugo i preživjeli
Zašto se radna knjiga s dva grafikona vratila s tri chart dijela?
Zato što je učitavač imao fallback koji je pogađao. Kada radni list nije imao relaciju crteža u svom .rels dijelu, stari je kod pretpostavljao da crtež živi pod konvencionalnim imenom xl/drawings/drawing{i+1}.xml, gdje je i pozicija lista, i prikačio taj dio ako postoji u arhivi. U two-charts.xlsx prvi list nema crtež niti .rels dio, dok xl/drawings/drawing1.xml postoji — pripada drugom listu, koji ga dohvaća preko Target="../drawings/drawing1.xml". List 1 je tako naslijedio grafikon koji nikad nije referencirao, chart1.xml je parsiran dvaput, a spremanje je ispisalo radnu knjigu s tri chart dijela umjesto s dva
Popravak u HotXLS-u v2.382.0 uklonio je pogađanje u cijelosti. Crtež radnog lista sada se učitava isključivo preko ParPartTargets[i].Values[XlsxRtDrawing], odredišta zabilježenog za tip relacije crteža na tom listu, a list bez takve relacije ne dobiva nikakav crtež. To je ponašanje koje format zahtijeva: element <drawing r:id="…"/> u radnom listu (ECMA-376 Part 1 §18.3.1.36) jedina je veza između lista i njegovog crteža, a imena dijelova u OPC paketu ne nose nikakvo značenje osim onoga koje im dodijeli graf relacija. Arhive koje piše Excel slučajno koriste konvencionalna imena, što je i pustilo taj prečac tako dugo; pregled OPC razrješavanja relacija u HotXLS-u pokriva zašto pogađanje imena dijela nikad nije sigurno čak i kada je pogodak obično točan
// Prije v2.382.0: relacija crteža koja nedostaje pala je 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žda pripada 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);
Što garantira fingerprint grafikona?
Fingerprint odlučuje, po grafikonu, može li spremanje kopirati izvorni dio ili ga mora regenerirati. Pri uvozu, uz PreserveUnsupportedParts uključen prije Open, HotXLS čuva sirove UTF-8 bajtove svakog chart dijela u FRawChartXml, gradi vlastitu serializaciju tipiziranog modela s BuildChartKnownXml i sprema duljinu te serializacije u FRawChartModelLength, a njen hash u FRawChartModelHash. Hash je FNV-1a nad UTF-16 code unitima generiranog XML-a, sa standardnom 64-bitnom offset bazom 14695981039346656037 i prostim brojem 1099511628211. Pri spremanju XlsxChartRawModelUnchanged ponovno gradi poznati XML i uspoređuje duljinu i hash; podudaranje znači da je tipizirani model točno onakav kakav je bio pri uvozu, pa se ništa što je aplikacija mogla promijeniti nije promijenilo
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) // doslovna reprodukcija
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // strukturno spajanje
end;
XLSX pisac ide korak dalje od BuildChartXmlFromKnown. Kada je model nepromijenjen i StrictOOXML isključen, najprije pokušava kopirati komprimiranu stavku izravno iz izvorne arhive u izlaz pod novim imenom chart dijela, tako da se bajtovi niti ne dekodiraju i ponovno deflataju. Samo ako ta kopija nije moguća, pada na putanju dekodiraj-ili-spoji. Sam mehanizam — duljina plus hash, reprodukcija kada su jednaki, spajanje kada nisu — opisan je u bilješci o uređivanju Excel grafikona bez gubitka ChartML-a. Ovaj je članak o načinu na koji je tiho prestao raditi
Zašto je svaki grafikon ipak završio na putanji spajanja?
Zato što je fingerprint snimljen jedan poziv prerano. Parsiranje grafikona u HotXLS-u je SAX prolaz kroz chart dio nakon kojeg slijedi niz recovery prolaza koji vade detalje iz sirovog teksta koje SAX handleri ne modeliraju izravno: XlsxChartParseSeriesFlags čita svaki <c:ser> blok za njegovu <c:smooth> zastavicu i srgbClr vrijednosti marker ispune i marker linije, a zatim oporavlja načine siječenja osi te stilove glavnih i sporednih oznaka za kategorijsku i vrijednosnu os. Prije v2.382.3 redoslijed na kraju ParseChartXml bio je: klasificiraj grupe osi, izgradi poznati XML, snimi duljinu i hash, i tek onda pokreni XlsxChartParseSeriesFlags. Fingerprint je stoga opisivao model kojemu su još nedostajale smooth zastavice, boje markera i oznake. Pri spremanju se BuildChartKnownXml izvršio nad dovršenim modelom, koji je sada emitirao <c:smooth val="1"/> i oporavljene boje markera. Duži XML, drugačiji hash, XlsxChartRawModelUnchanged vratio je False, i grafikon je otišao kroz XlsxMergeChartXml. Spajanje je ispravna operacija za grafikon koji je netko uređivao, ali nije operacija koja čuva bajtove: ono reserializira stablo, a pravilo vlasništva koje dopušta da tipizirani model pobijedi za serije, osi i plot grupe znači da regenerirani čvorovi zamjenjuju originale. Vidljivi rezultat u corpus pokretanju bile su odlutale boje serija na grafikonima koje nitko nije uređivao — svaki grafikon u svakoj očuvanoj radnoj knjizi, pri svakom spremanju, bez ikakve dijagnostike
Popravak je jedno preslagivanje: XlsxChartParseSeriesFlags sada se izvršava prije nego se izgradi poznati XML, pa fingerprint opisuje model onakav kakav će postojati kad ga aplikacija prvi put vidi. Lekcija se generalizira izvan grafikona. Fingerprint za detekciju promjena dobar je onoliko koliko je dobar trenutak u kojem je snimljen, a siguran trenutak je nakon što završi svaki prolaz koji može mijenjati model. HotXLS ima drugo mjesto snimanja istih dviju vrijednosti, baseline koji ponovno uspostavlja prema izlaznoj datoteci nakon uspješnog spremanja, i to je mjesto oduvijek radilo nad potpuno parsiranim modelom; ono pri uvozu bilo je izuzetak
Kamo su nestali anchor pomaci?
U doslovnu nulu. twoCellAnchor u drawing dijelu prikiva grafikon između dvije ćelije, a svaki kut nosi indeks ćelije plus pomak unutar te ćelije: from (ECMA-376 Part 1 §20.5.2.5) i to (§20.5.2.32) drže col, colOff (§20.5.2.4), row i rowOff. Pomaci su u English Metric Units, 914400 na inč, a Excel zapisuje nenulte vrijednosti kad god je grafikon postavljen ili promijenjene veličine mišem, što je većina grafikona. Prvi grafikon u two-charts.xlsx počinje u retku 0 s rowOff od 19049 i završava u stupcu 8, retku 15 s colOff od 247650 i rowOff od 66674 — otprilike četvrt inča u zadnji stupac. Parser crteža u HotXLS-u oduvijek je čitao te četiri vrijednosti — image kod ih je koristio — ali pisac grafikona emitirao je <xdr:colOff>0</xdr:colOff> i <xdr:rowOff>0</xdr:rowOff> za svaki kut, prikvačivši tako svaki grafikon na mrežu ćelija pri spremanju
// Od v2.382.3 anchor pisac reproducira uvezene EMU pomake
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 crteža i kopirane zajedno s ostalim anchor stanjem kada se grafikon dodjeljuje. Namjerno su privatni: javna anchor površina i dalje su četiri koordinate ćelija FromRow, FromCol, ToRow i ToCol, a grafikon stvoren iz Delphi koda slijeće na granice ćelija kao i prije. Pomaci postoje da round trip bude vjeran, a ne da se pozicioniranje unutar ćelije izloži kao značajka. Napominjemo da je ovaj popravak neovisan o fingerprintu: anchor živi u drawing dijelu, ne u chart dijelu, pa bi grafikon čiji se ChartML reproducirao savršeno ipak skočio na mrežu bez njega. Konverzije jedinica iza tih EMU vrijednosti obrađene su u bilješci o HotXLS geometriji slika i EMU skaliranju
Kako dokazati da grafikon prolazi round trip nepromijenjen?
Usporedbom bajtova, a ne otvaranjem rezultata u Excelu. Excel pri učitavanju toliko popravlja i normalizira da odlutali grafikon izgleda dobro sve dok analitičar ne primijeti da se boja markera promijenila. Corpus test koji je uhvatio oba defekta čini tri stvari nakon otvaranja i spremanja bez ikakvih izmjena: prolazi kroz relacije radnih listova, crteža i grafikona i pada na svakoj dupliciranoj, siročetu ili visećoj referenci grafikona; uspoređuje 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 obje arhive i zahtijeva identične bajtove. Ista provjera lako se 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); // diže iznimku ako je dio 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 uvjeta čine tu usporedbu smislenom, i svaki od njih tiho pada ako se zaboravi. PreserveUnsupportedParts mora biti True prije Open, inače se nikakvi sirovi bajtovi ne snimaju i svaki se grafikon ponovno gradi iz modela. StrictOOXML mora biti False, jer strict način po dizajnu prisiljava regeneraciju. I aplikacija ne smije dirati grafikon između otvaranja i spremanja — čitanje svojstava je u redu, ali svaki setter koji mijenja tipizirani model preokreće fingerprint i šalje grafikon na putanju spajanja, što je ispravno ponašanje i nije ono za što ovaj test služi. Chart dijelovi se pri spremanju također prenumeriraju iz brojača na razini cijele radne knjige, pa će radna knjiga kojoj se promijenio redoslijed listova ili grafikona smjestiti identične bajtove pod drugo ime chartN.xml; corpus provjera zato prati relacije, a ne imena
Oba popravka isporučena su u HotXLS 2.382.0 i 2.382.3 te su provjerena na Win32 i Win64 protiv lokalnog corpusa, pri čemu su ponovno spremljeni uzorci grafikona također renderirani kroz nezavisni uredski paket u PDF i uspoređeni stranicu po stranicu s originalima. HotXLS čita, uređuje i piše XLSX grafikone iz izvornog Delphi i C++Builder koda bez ikakve instalacije Excela, što je ono što ovu razinu vjernosti čini odgovornošću biblioteke — stranica HotXLS Delphi spreadsheet komponente ima popis značajki i probno preuzimanje