A HotXLS Delphi Component csak akkor játszik vissza egy módosítatlan Excel-diagramot bájtazonosan, ha két feltétel egyszerre teljesül: a diagram a munkalap rajzrelációján keresztül érkezett, nem pedig egy kitalált résznévből, és a 64 bites modell-ujjlenyomat a diagrammodell befejezett feldolgozása után készült. A 2.382.0 javította az első feltételt, a 2.382.3 a másodikat, és ezzel elkezdte körbejáratni a nem nulla xdr:colOff és xdr:rowOff horgonyeltolásokat is, amelyeket a rajzíró addig mereven nullára állított. Mindkét hiba egyetlen helyi korpuszesetből, a two-charts.xlsx-ből került elő: először egy szerkezeti ellenőrzés látta, hogy két diagramrészből három lesz, majd az összes xl/charts/chartN.xml bájtos összehasonlítása mutatta meg, hogy olyan diagramokat is újraírnak, amelyekhez senki hozzá sem nyúlt — és egyik probléma sem dobott kivételt, sem az Excelt nem zavarta meg, ezért maradtak életben ilyen sokáig
Miért lett a két diagramos munkafüzetből három diagramrész?
Mert a betöltőnek volt egy tippelő tartalékútja. Ha egy munkalap .rels részében nem volt rajzreláció, a régi kód azt feltételezte, hogy a rajz a szokásos xl/drawings/drawing{i+1}.xml néven lakik, ahol i a lap pozíciója, és hozzákapcsolta ezt a részt, ha létezett az archívumban. A two-charts.xlsx-ben az első lapon nincs rajz, sőt .rels rész sem, az xl/drawings/drawing1.xml viszont létezik — az a második laphoz tartozik, amely a Target="../drawings/drawing1.xml" útvonalon éri el. Az 1. lap így örökölt egy diagramot, amelyre soha nem hivatkozott, a chart1.xml kétszer lett feldolgozva, a mentés pedig három diagramrésszel írta ki a munkafüzetet kettő helyett
A HotXLS v2.382.0 javítása teljesen kivette a tippelést. A munkalaprajz mostantól kizárólag a ParPartTargets[i].Values[XlsxRtDrawing] útvonalon töltődik be, vagyis azon a lapon a rajzrelációtípushoz feljegyzett célon, és amelyik lapon nincs ilyen reláció, az egyáltalán nem kap rajzot. Pontosan ezt követeli meg a formátum: a munkalap <drawing r:id="…"/> eleme (ECMA-376 Part 1 §18.3.1.36) az egyetlen kapcsolat a lap és a rajza között, az OPC-csomag résznevei pedig semmit nem jelentenek azon túl, amit a relációs gráf nekik tulajdonít. Az Excel által írt archívumok épp a szokásos neveket használják, ezért úszta meg a rövidítés ilyen sokáig; a HotXLS OPC-relációfeloldásáról szóló végigvezetés kifejti, miért soha nem biztonságos egy résznévre tippelni, még akkor sem, ha a tipp rendszerint bejön
// A v2.382.0 előtt: a hiányzó rajzreláció tippre esett vissza
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // lehet, hogy másik laphoz tartozik
// A v2.382.0 óta: reláció vagy semmi
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Mit garantál a diagram-ujjlenyomat?
Az ujjlenyomat diagramonként dönti el, hogy a mentés másolhatja-e az eredeti részt, vagy újra kell generálnia. Betöltéskor, ha a PreserveUnsupportedParts be van kapcsolva az Open előtt, a HotXLS megőrzi az egyes diagramrészek nyers UTF-8 bájtjait a FRawChartXml mezőben, felépíti a tipizált modell saját szerializációját a BuildChartKnownXml-lel, és a szerializáció hosszát a FRawChartModelLength, a hashét pedig a FRawChartModelHash mezőben tárolja. A hash FNV-1a a generált XML UTF-16 kódegységei felett, a szokásos 64 bites offset-bázissal 14695981039346656037 és a 1099511628211 prímmel. Mentéskor az XlsxChartRawModelUnchanged újraépíti az ismert XML-t, és összeveti a hosszt meg a hasht; egyezés esetén a tipizált modell pontosan az, ami betöltéskor volt, tehát semmi nem változott, amit az alkalmazás megváltoztathatott volna
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 // nincs mi megőrizve
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // szó szerinti visszajátszás
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // szerkezeti összefésülés
end;
Az XLSX-író egy lépéssel tovább megy a BuildChartXmlFromKnown-nél. Ha a modell változatlan és a StrictOOXML ki van kapcsolva, először megpróbálja a tömörített bejegyzést közvetlenül a forrásarchívumból a kimenetbe másolni a diagram új részneve alá, így a bájtokat nem is kell dekódolni és újra deflate-elni. Csak ha ez a másolás nem lehetséges, akkor esik vissza a dekódoló vagy összefésülő útra. Maga a mechanizmus — hossz plusz hash, egyezésnél visszajátszás, eltérésnél összefésülés — az, amelyet az Excel-diagramok ChartML-veszteség nélküli szerkesztéséről szóló jegyzet leír. Ez a cikk arról szól, hogyan állt le mindez csendben
Miért futott mégis minden diagram az összefésülő úton?
Mert az ujjlenyomat egy hívással túl korán készült el. A diagramfeldolgozás a HotXLS-ben egy SAX-menet a diagramrész felett, majd egy sor helyreállítási menet, amelyek olyan részleteket húznak ki a nyers szövegből, amelyeket a SAX-kezelők nem modelleznek közvetlenül: az XlsxChartParseSeriesFlags végigolvassa az egyes <c:ser> blokkokat a <c:smooth> jelzőjükért, valamint a marker kitöltésének és vonalának srgbClr értékeiért, majd helyreállítja a tengelykeresztezési módokat, illetve a kategória- és értéktengely fő és mellék osztásjeleinek stílusait. A v2.382.3 előtt a ParseChartXml végén ez volt a sorrend: a tengelycsoportok osztályozása, az ismert XML felépítése, hossz és hash rögzítése, és csak ezután futott az XlsxChartParseSeriesFlags. Az ujjlenyomat így olyan modellt írt le, amelyből még hiányoztak a smooth jelzők, a marker színei és az osztásjelek. Mentéskor a BuildChartKnownXml már a befejezett modellen futott, amely kiírta a <c:smooth val="1"/> elemet és a helyreállított marker színeket. Hosszabb XML, más hash, az XlsxChartRawModelUnchanged False-t adott vissza, és a diagram az XlsxMergeChartXml útra került. Az összefésülés helyes művelet egy szerkesztett diagramnál, bájtőrző viszont nem: újraszerializálja a fát, és az a tulajdonosi szabály, amely a sorozatok, tengelyek és plotcsoportok esetén a tipizált modellt hagyja nyerni, azt jelenti, hogy az újragenerált csomópontok lecserélik az eredetieket. A korpuszfutás látható eredménye elcsúszott sorozatszínek voltak olyan diagramokon, amelyekhez senki hozzá sem nyúlt — minden diagram minden megőrzött munkafüzetben, minden mentésnél, mindenféle diagnosztika nélkül
A javítás egyetlen átrendezés: az XlsxChartParseSeriesFlags mostantól az ismert XML felépítése előtt fut, így az ujjlenyomat azt a modellt írja le, ahogy az alkalmazás első ránézésre látni fogja. A tanulság túlmutat a diagramokon. Egy változásérzékelő ujjlenyomat csak annyit ér, amennyire jó a rögzítés pillanata, a biztonságos pillanat pedig az, amikor minden olyan menet lefutott, amely módosíthatja a modellt. A HotXLS-nek ugyanerre a két értékre van egy második rögzítési pontja is, az a bázis, amelyet sikeres mentés után a kimeneti fájlhoz képest újra megállapít, és az a pont mindig teljesen feldolgozott modellen futott; a betöltéskori pont volt a kakukktojás
Hová tűntek a horgonyeltolások?
Egy szó szerinti nullába. A rajzrészben lévő twoCellAnchor két cella közé szögez egy diagramot, és mindkét sarok hordoz egy cellaindexet meg egy eltolást azon a cellán belül: a from (ECMA-376 Part 1 §20.5.2.5) és a to (§20.5.2.32) egyaránt tartalmaz col, colOff (§20.5.2.4), row és rowOff értéket. Az eltolások angol metrikus egységben (EMU) vannak, 914400 egység egy hüvelykre, és az Excel mindig nem nulla értéket ír, ha a diagramot egérrel helyezték el vagy méretezték át — ami a diagramok többsége. A two-charts.xlsx első diagramja a 0. sornál kezdődik 19049-es rowOff-tal, és a 8. oszlop, 15. sor végén zárul 247650-es colOff-tal meg 66674-es rowOff-tal — nagyjából negyed hüvelykre az utolsó oszlopba. A HotXLS rajzfeldolgozója mindig is olvasta ezt a négy értéket — a képkezelő kód használta is őket —, a diagramíró viszont minden saroknál <xdr:colOff>0</xdr:colOff> és <xdr:rowOff>0</xdr:rowOff> elemet írt ki, így mentéskor minden diagramot a cellarácsra pattintott
// A v2.382.3 óta a horgonyíró visszajátssza a betöltött EMU-eltolásokat
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>' + ...
A TXLSXChart mostantól hordozza a FFromColOff, FFromRowOff, FToColOff és FToRowOff mezőket, amelyek a rajzfeldolgozóból töltődnek fel, és a diagram hozzárendelésekor a többi horgonyállapottal együtt másolódnak. Szándékosan privátak: a nyilvános horgonyfelület továbbra is a négy cellakoordináta, a FromRow, FromCol, ToRow és ToCol, és a Delphi-kódból létrehozott diagram a korábbiakhoz hasonlóan cellahatárokra esik. Az eltolások azért léteznek, hogy a körbejáratás hű legyen, nem azért, hogy a cellán belüli pozicionálást funkcióként kiadjuk. Fontos, hogy ez a javítás független az ujjlenyomattól: a horgony a rajzrészben lakik, nem a diagramrészben, így egy olyan diagram is a rácsra ugrott volna nélküle, amelynek a ChartML-je tökéletesen játszódott vissza. Az EMU-értékek mögötti egységátváltásokat a HotXLS képgometriáról és EMU-skálázásról szóló jegyzet tárgyalja
Hogyan bizonyítható, hogy egy diagram változatlanul járja körbe az utat?
Bájtok összehasonlításával, nem úgy, hogy az eredményt megnyitjuk Excelben. Az Excel betöltéskor annyit javít és normalizál, hogy egy elcsúszott diagram egészen addig rendben lévőnek látszik, amíg egy elemző észre nem veszi, hogy megváltozott a marker színe. A korpuszteszt, amely mindkét hibát elcsípte, három dolgot tesz egy szerkesztés nélküli megnyitás-mentés után: bejárja a munkalap-, rajz- és diagramrelációkat, és elbukik minden duplikált, árva vagy lógó diagramhivatkozáson; összeveti a diagramtípus, a sorozatképletek és a horgonygeometria lenyomatát az eredeti és a kimenet között; a two-charts.xlsx esetében pedig mindkét archívumból beolvassa az egyes xl/charts/chartN.xml fájlokat, és azonos bájtokat követel meg. Ugyanez az ellenőrzés Delphiben könnyen megírható az RTL TZipFile osztályával
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); // kivételt dob, ha a rész eltűnt
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;
Három feltétel teszi értelmessé ezt az összehasonlítást, és mindegyik csendben elbukik, ha megfeledkezünk róla. A PreserveUnsupportedParts értéke True kell legyen az Open előtt, különben nem rögzülnek a nyers bájtok, és minden diagram a modellből épül újra. A StrictOOXML értéke False kell legyen, mert a szigorú mód tervezésből fakadóan újragenerálásra kényszerít. Az alkalmazás pedig nem nyúlhat a diagramhoz a megnyitás és a mentés között — a tulajdonságok olvasása rendben van, de bármely setter, amely megváltoztatja a tipizált modellt, átbillenti az ujjlenyomatot, és az összefésülő útra küldi a diagramot, ami helyes viselkedés, csak épp nem ez a teszt célja. A diagramrészek mentéskor egy munkafüzet szintű számlálóról újraszámozódnak, így az a munkafüzet, amelynek megváltozott a lap- vagy diagramsorrendje, azonos bájtokat más chartN.xml név alá tesz; a korpuszellenőrző ezért nevek helyett relációkat követ
Mindkét javítás megjelent a HotXLS 2.382.0 és 2.382.3 verziójában, Win32 és Win64 alatt a helyi korpuszon verifikálva, az újramentett diagramminták pedig egy független irodai csomagon keresztül PDF-be is renderelődtek, és oldalról oldalra összevetődtek az eredetivel. A HotXLS natív Delphi és C++Builder kódból olvassa, szerkeszti és írja az XLSX-diagramokat, Excel-telepítés nélkül, és épp ez teszi ezt a fokú hűséget a könyvtár felelősségévé — a HotXLS Delphi táblázatkezelő komponens oldalán megtalálható a funkciólista és a próbaverzió