HotXLS Delphi Component redează octet cu octet o diagramă Excel nemodificată doar când se respectă două condiții: diagrama a fost atinsă prin relația de desen a foii de calcul, nu printr-un nume de parte ghicit, iar amprenta de 64 de biți a modelului a fost capturată după ce modelul diagramei a terminat de parsat. Versiunea 2.382.0 a reparat prima condiție, versiunea 2.382.3 a reparat-o pe a doua și a început să redea la salvare offset-urile de ancoră xdr:colOff și xdr:rowOff diferite de zero, pe care scriitorul de desen le fixa la zero. Ambele defecte au ieșit dintr-un singur caz din corpusul local, two-charts.xlsx: mai întâi o aserțiune structurală a văzut două părți de diagramă devenind trei, apoi o comparație octet cu octet a fiecărui xl/charts/chartN.xml a arătat diagrame pe care nimeni nu le atinsese fiind totuși rescrise — și niciuna dintre probleme nu a ridicat o excepție sau nu l-a făcut pe Excel să se plângă, motiv pentru care au supraviețuit atât de mult
De ce s-a întors un registru cu două diagrame având trei părți de diagramă?
Pentru că încărcătorul avea o cale de rezervă care ghicea. Când o foaie de calcul nu avea nicio relație de desen în partea ei .rels, codul vechi presupunea că desenul se află la numele convențional xl/drawings/drawing{i+1}.xml, unde i este poziția foii, și atașa acea parte dacă exista în arhivă. În two-charts.xlsx prima foaie nu are niciun desen și nicio parte .rels, în timp ce xl/drawings/drawing1.xml există — aparține celei de-a doua foi, care ajunge la el prin Target="../drawings/drawing1.xml". Foaia 1 a moștenit astfel o diagramă la care nu făcuse niciodată referire, chart1.xml a fost parsat de două ori, iar salvarea a scris registrul cu trei părți de diagramă în loc de două
Reparația din HotXLS v2.382.0 a eliminat complet ghicitul. Un desen de foaie de calcul se încarcă acum doar prin ParPartTargets[i].Values[XlsxRtDrawing], ținta înregistrată pentru tipul de relație de desen pe acea foaie, iar o foaie fără o astfel de relație nu primește niciun desen. Acesta este comportamentul pe care îl cere formatul: elementul <drawing r:id="…"/> din foaia de calcul (ECMA-376 Part 1 §18.3.1.36) este singura legătură dintre o foaie și desenul ei, iar numele de părți dintr-un pachet OPC nu poartă nicio semnificație dincolo de ce le atribuie graful de relații. Arhivele scrise de Excel se întâmplă să folosească numele convenționale, ceea ce a lăsat scurtătura să treacă atât de mult timp; ghidul despre rezolvarea relațiilor OPC în HotXLS explică de ce ghicirea unui nume de parte nu este niciodată sigură, chiar și atunci când ghicitul este de obicei corect
// Înainte de v2.382.0: o relație de desen lipsă cădea pe un ghicit
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // poate aparține altei foi
// Din v2.382.0: relație sau nimic
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Ce garantează amprenta diagramei?
Amprenta decide, pentru fiecare diagramă, dacă salvarea poate copia partea originală sau trebuie să o regenereze. La import, cu PreserveUnsupportedParts activat înainte de Open, HotXLS păstrează octeții UTF-8 bruti ai fiecărei părți de diagramă în FRawChartXml, construiește propria serializare a modelului tipizat cu BuildChartKnownXml și stochează lungimea acelei serializări în FRawChartModelLength, iar hash-ul ei în FRawChartModelHash. Hash-ul este FNV-1a peste unitățile de cod UTF-16 ale XML-ului generat, cu baza standard de offset pe 64 de biți 14695981039346656037 și primul 1099511628211. La salvare, XlsxChartRawModelUnchanged reconstruiește XML-ul cunoscut și compară lungimea și hash-ul; o potrivire înseamnă că modelul tipizat este exact cel de la import, deci nimic din ce aplicația ar fi putut schimba nu s-a schimbat
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 // nimic păstrat
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // redare identică
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // îmbinare structurală
end;
Scriitorul XLSX merge cu un pas mai departe decât BuildChartXmlFromKnown. Când modelul este neschimbat și StrictOOXML este oprit, încearcă mai întâi să copieze intrarea comprimată direct din arhiva sursă în cea de ieșire, sub noul nume de parte al diagramei, așa că octeții nu sunt nici măcar decodați și deflatați din nou. Doar dacă acea copiere nu este posibilă cade pe calea de decodare sau de îmbinare. Mecanismul în sine — lungime plus hash, redare când sunt egale, îmbinare când nu — este cel descris în nota despre editarea diagramelor Excel fără a pierde ChartML. Acest articol este despre felul în care a încetat să funcționeze, în tăcere
De ce a luat totuși fiecare diagramă calea de îmbinare?
Pentru că amprenta era capturată cu un apel prea devreme. Parsarea diagramelor în HotXLS este o trecere SAX peste partea de diagramă, urmată de un set de treceri de recuperare care scot din textul brut detalii pe care handler-ele SAX nu le modelează direct: XlsxChartParseSeriesFlags citește fiecare bloc <c:ser> pentru flag-ul lui <c:smooth> și pentru valorile srgbClr ale umplerii și liniei marker-ului, apoi recuperează modurile de intersectare a axelor și stilurile de bifă majore și minore pentru axele de categorii și de valori. Înainte de v2.382.3, ordinea la finalul lui ParseChartXml era: clasifică grupurile de axe, construiește XML-ul cunoscut, capturează lungimea și hash-ul și abia apoi rulează XlsxChartParseSeriesFlags. Amprenta descria astfel un model căruia îi lipseau încă flag-urile smooth, culorile de marker și bifele. La salvare, BuildChartKnownXml rula pe modelul complet, care emitea acum <c:smooth val="1"/> și culorile de marker recuperate. XML mai lung, hash diferit, XlsxChartRawModelUnchanged întorcea False, iar diagrama trecea prin XlsxMergeChartXml. Îmbinarea este o operație corectă pentru o diagramă editată de cineva, dar nu este una care păstrează octeții: reserializa arborele, iar regula de proprietate care lasă modelul tipizat să câștige pentru serii, axe și grupuri de plot face ca nodurile regenerate să înlocuiască originalele. Rezultatul vizibil în rularea pe corpus au fost culori de serie deplasate pe diagrame pe care nimeni nu le editase — fiecare diagramă din fiecare registru păstrat, la fiecare salvare, fără niciun diagnostic nicăieri
Reparația este o singură reordonare: XlsxChartParseSeriesFlags rulează acum înainte de construirea XML-ului cunoscut, așa că amprenta descrie modelul așa cum va exista atunci când aplicația îl vede prima dată. Lecția se generalizează dincolo de diagrame. O amprentă de detectare a schimbărilor valorează exact cât momentul în care este luată, iar momentul sigur este după ce s-a terminat fiecare trecere care poate muta modelul. HotXLS are un al doilea loc de captură pentru aceleași două valori, linia de referință pe care o restabilește față de fișierul de ieșire după o salvare reușită, iar acel loc rula întotdeauna pe un model parsat complet; locul de la import era cel atipic
Unde au dispărut offset-urile de ancoră?
Într-un zero literal. Un twoCellAnchor din partea de desen fixează o diagramă între două celule, iar fiecare colț poartă un index de celulă plus un offset în interiorul acelei celule: from (ECMA-376 Part 1 §20.5.2.5) și to (§20.5.2.32) conțin fiecare col, colOff (§20.5.2.4), row și rowOff. Offset-urile sunt în English Metric Units, 914400 la un inch, iar Excel scrie valori diferite de zero ori de câte ori o diagramă a fost plasată sau redimensionată cu mouse-ul, adică pentru majoritatea diagramelor. Prima diagramă din two-charts.xlsx începe la rândul 0 cu un rowOff de 19049 și se termină la coloana 8, rândul 15, cu un colOff de 247650 și un rowOff de 66674 — cam un sfert de inch în ultima coloană. Parserul de desene din HotXLS citise întotdeauna acele patru valori — codul de imagini le folosea — dar scriitorul de diagrame emitea <xdr:colOff>0</xdr:colOff> și <xdr:rowOff>0</xdr:rowOff> pentru fiecare colț, lipind fiecare diagramă la grila de celule la salvare
// Din v2.382.3 scriitorul de ancore redea offset-urile EMU importate
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 poartă acum FFromColOff, FFromRowOff, FToColOff și FToRowOff, completate din parserul de desene și copiate împreună cu restul stării de ancoră atunci când o diagramă este atribuită. Sunt deliberat private: suprafața publică de ancorare rămâne cele patru coordonate de celulă FromRow, FromCol, ToRow și ToCol, iar o diagramă creată din cod Delphi aterizează pe limitele de celulă ca înainte. Offset-urile există pentru ca un round-trip să fie fidel, nu pentru a expune poziționarea sub celulă ca funcționalitate. Rețineți că această reparație este independentă de amprentă: ancora trăiește în partea de desen, nu în partea de diagramă, așa că o diagramă al cărei ChartML s-ar reda perfect ar fi totuși sărit la grilă fără ea. Conversiile de unități din spatele acelor valori EMU sunt acoperite în nota despre geometria imaginilor și scalarea EMU în HotXLS
Cum demonstrați că o diagramă face round-trip neschimbată?
Comparând octeții, nu deschizând rezultatul în Excel. Excel repară și normalizează atât de mult la încărcare, încât o diagramă deplasată arată bine până în momentul în care un analist observă că s-a schimbat culoarea marker-ului. Testul pe corpus care a prins ambele defecte face trei lucruri după o deschidere și o salvare fără nicio editare: parcurge relațiile de foaie de calcul, desen și diagramă și eșuează la orice referință de diagramă duplicată, orfană sau atârnată; compară o semnătură a tipului de diagramă, a formulelor de serie și a geometriei ancorei între original și rezultat; iar pentru two-charts.xlsx citește fiecare xl/charts/chartN.xml din ambele arhive și cere octeți identici. Aceeași verificare este ușor de scris în Delphi cu TZipFile din RTL
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); // ridică excepție dacă partea a dispărut
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;
Trei condiții fac acea comparație semnificativă și fiecare eșuează în tăcere dacă este uitată. PreserveUnsupportedParts trebuie să fie True înainte de Open, altfel nu se capturează niciun octet brut și fiecare diagramă este reconstruită din model. StrictOOXML trebuie să fie False, pentru că modul strict forțează regenerarea prin construcție. Iar aplicația nu trebuie să atingă diagrama între deschidere și salvare — citirea de proprietăți este în regulă, dar orice setter care schimbă modelul tipizat întoarce amprenta și trimite diagrama pe calea de îmbinare, ceea ce este un comportament corect și nu scopul acestui test. Părțile de diagramă sunt de asemenea renumerotate la salvare dintr-un contor la nivel de registru, așa că un registru a cărui ordine a foilor sau a diagramelor s-a schimbat va pune octeți identici sub un alt nume chartN.xml; verificatorul de corpus urmărește relațiile, nu numele, exact din acest motiv
Ambele reparații au fost livrate în HotXLS 2.382.0 și 2.382.3 și sunt verificate pe Win32 și Win64 pe corpusul local, iar eșantioanele de diagrame resalvate au fost randate și printr-o suită de birou independentă în PDF și comparate pagină cu pagină cu originalele. HotXLS citește, editează și scrie diagrame XLSX din cod nativ Delphi și C++Builder fără nicio instalare de Excel, ceea ce face ca acest nivel de fidelitate să fie o responsabilitate a librăriei — pagina componentei HotXLS de foaie de calcul pentru Delphi are lista de funcționalități și un trial de descărcare