HotXLS Delphi Component přehraje nezměněný Excel graf po bajtech jen tehdy, když platí dvě věci: graf byl nalezen přes kreslicí relaci listu, nikoli přes hádaný název partu, a 64bitový otisk modelu byl zachycen poté, co model grafu dokončil parsování. Verze 2.382.0 opravila první podmínku, verze 2.382.3 druhou a začala round-tripovat nenulové posuny ukotvení xdr:colOff a xdr:rowOff, které zapisovač kresby tvrdě kódoval jako nulu. Oba defekty vypadly z jediného případu lokálního korpusu, two-charts.xlsx: nejdřív strukturní assertion viděl dva chart party proměnit se ve tři, pak bajtové porovnání všech xl/charts/chartN.xml ukázalo grafy, které nikdo nezměnil, a ty se stejně přepisovaly — a ani jeden problém nevyhodil výjimku ani nevyvolal stížnost Excelu, což je důvod, proč tak dlouho přežily
Proč se sešit se dvěma grafy vrátil se třemi chart party?
Protože loader měl fallback, který hádal. Když list neměl ve svém partu .rels kreslicí relaci, starý kód předpokládal, že kresba leží na konvenčním názvu xl/drawings/drawing{i+1}.xml, kde i je pozice listu, a připojil ten part, pokud v archivu existoval. V two-charts.xlsx první list nemá kresbu ani žádný part .rels, zatímco xl/drawings/drawing1.xml existuje — patří druhému listu, který se na něj dostává přes Target="../drawings/drawing1.xml". List 1 tedy zdědil graf, na který nikdy neodkazoval, chart1.xml se parsoval dvakrát a uložení zapsalo sešit se třemi chart party místo dvou
Oprava v HotXLS v2.382.0 odstranila hádání úplně. Kresba listu se teď načítá jen přes ParPartTargets[i].Values[XlsxRtDrawing], tedy cíl zaznamenaný pro typ kreslicí relace na daném listu, a list bez takové relace nedostane žádnou kresbu. To je chování, které formát vyžaduje: element <drawing r:id="…"/> v listu (ECMA-376 Part 1 §18.3.1.36) je jediné spojení mezi listem a jeho kresbou a názvy partů v OPC balíčku nenesou žádný význam nad to, co jim přidělí graf relací. Archivy psané Excelem konvenční názvy používají, a proto zkratka tak dlouho procházela; průchod v rozřešení OPC relací v HotXLS rozebírá, proč hádat název partu nikdy není bezpečné, ani když se hádání obvykle trefí
// Před v2.382.0: chybějící kreslicí relace spadla do hádaného fallbacku
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // může patřit jinému listu
// Od v2.382.0: relace nebo nic
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Co otisk grafu zaručuje?
Otisk rozhoduje, u každého grafu zvlášť, zda uložení může původní part zkopírovat, nebo ho musí vygenerovat znovu. Při importu, se zapnutým PreserveUnsupportedParts před Open, HotXLS uchová syrové UTF-8 bajty každého chart partu v FRawChartXml, postaví vlastní serializaci typovaného modelu přes BuildChartKnownXml a uloží její délku do FRawChartModelLength a její hash do FRawChartModelHash. Hash je FNV-1a nad UTF-16 kódovými jednotkami vygenerovaného XML se standardním 64bitovým offset základem 14695981039346656037 a prvočíslem 1099511628211. Při ukládání XlsxChartRawModelUnchanged znovu postaví známé XML a porovná délku a hash; shoda znamená, že typovaný model je přesně takový, jaký byl při importu, takže se nezměnilo nic z toho, co aplikace mohla změnit
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 // nic nebylo zachováno
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // doslovné přehrání
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // strukturální merge
end;
Zapisovač XLSX jde o krok dál než BuildChartXmlFromKnown. Když je model nezměněný a StrictOOXML je vypnuté, nejdřív zkusí zkopírovat komprimovanou položku rovnou ze zdrojového archivu do výstupu pod novým názvem partu grafu, takže bajty se ani nedekódují a znovu nezdeflují. Jen když ta kopie nejde, spadne to do cesty dekóduj-nebo-sluč. Samotný mechanismus — délka plus hash, přehrání při shodě, merge při neshodě — je ten popsaný v poznámce o úpravě Excel grafů bez ztráty ChartML. Tenhle článek je o tom, jak to potichu přestalo fungovat
Proč každý graf stejně skončil na cestě merge?
Protože se otisk zachytil jedno volání moc brzy. Parsování grafů v HotXLS je SAX průchod přes chart part následovaný sadou obnovovacích průchodů, které vytáhnou z raw textu detaily, které SAX handlery přímo nemodelují: XlsxChartParseSeriesFlags čte každý blok <c:ser> kvůli jeho příznaku <c:smooth> a hodnotám srgbClr výplně markeru a linky markeru, pak obnoví režimy průsečíků os a styly hlavních a vedlejších tick značek pro kategorické a hodnotové osy. Před v2.382.3 bylo pořadí na konci ParseChartXml takové: klasifikuj skupiny os, postav známé XML, zachyť délku a hash a teprve pak pusť XlsxChartParseSeriesFlags. Otisk tak popisoval model, kterému ještě chyběly smooth příznaky, barvy markerů a tick značky. Při ukládání běžel BuildChartKnownXml proti hotovému modelu, který teď emitoval <c:smooth val="1"/> a obnovené barvy markerů. Delší XML, jiný hash, XlsxChartRawModelUnchanged vrátilo False a graf šel přes XlsxMergeChartXml. Merge je správná operace pro graf, který někdo editoval, ale není bajtově zachovávající: znovu serializuje strom a vlastnické pravidlo, které nechává typovaný model vyhrát u sérií, os a skupin vykreslení, znamená, že regenerované uzly nahradí původní. Viditelný výsledek v běhu korpusu byly roztahané barvy sérií u grafů, které nikdo needitoval — každý graf v každém zachovávaném sešitu, při každém uložení, bez jediné diagnostiky
Oprava je jediné přeřazení: XlsxChartParseSeriesFlags teď běží, než se známé XML postaví, takže otisk popisuje model takový, jaký bude existovat, když ho aplikace poprvé uvidí. Poučení se vztahuje i za hranice grafů. Otisk pro detekci změn je jen tak dobrý jako okamžik zachycení a bezpečný okamžik je ten, po kterém doběhl každý průchod, který model může mutovat. HotXLS má druhé místo zachycení těch samých dvou hodnot, baseline obnovovanou proti výstupnímu souboru po úspěšném uložení, a toto místo vždy běželo proti plně parsovanému modelu; importní místo bylo to výjimečné
Kam se poděly posuny ukotvení?
Do doslovné nuly. twoCellAnchor v kreslicím partu připne graf mezi dvě buňky a každý roh nese index buňky plus posun uvnitř ní: from (ECMA-376 Part 1 §20.5.2.5) a to (§20.5.2.32) každý drží col, colOff (§20.5.2.4), row a rowOff. Posuny jsou v English Metric Units, 914400 na palec, a Excel píše nenulové hodnoty vždy, když byl graf umístěn nebo změněn myší, což je většina grafů. První graf v two-charts.xlsx začíná na řádku 0 s rowOff 19049 a končí ve sloupci 8, řádku 15 s colOff 247650 a rowOff 66674 — zhruba čtvrt palce do posledního sloupce. Parser kresby v HotXLS čtyři hodnoty četl vždycky — kód obrázků je používal — ale zapisovač grafů emitoval <xdr:colOff>0</xdr:colOff> a <xdr:rowOff>0</xdr:rowOff> pro každý roh a při uložení přichytil každý graf k mřížce buněk
// Od v2.382.3 zapisovač ukotvení přehraje importované EMU posuny
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 teď nese FFromColOff, FFromRowOff, FToColOff a FToRowOff, plněné z parseru kresby a kopírované spolu s ostatním stavem ukotvení, když se graf přiřadí. Záměrně jsou privátní: veřejný povrch ukotvení zůstávají čtyři souřadnice buněk FromRow, FromCol, ToRow a ToCol a graf vytvořený z kódu v Delphi dopadne na hranice buněk jako dřív. Posuny existují, aby round trip byl věrný, ne aby vystavily umísťování podúrovně buňky jako funkci. Všimněte si, že tahle oprava je nezávislá na otisku: ukotvení žije v kreslicím partu, ne v chart partu, takže graf, jehož ChartML se přehrál dokonale, by bez ní stejně vyskočil na mřížku. Konverze jednotek za těmi hodnotami EMU rozebírá poznámka o geometrii obrázků v HotXLS a škálování EMU
Jak prokážete, že graf projel round trip beze změny?
Porovnáním bajtů, ne otevřením výsledku v Excelu. Excel při načtení tolik opravuje a normalizuje, že roztahaný graf vypadá v pořádku, dokud analytik nezjistí, že se změnila barva markeru. Korpusový test, který oba defekty chytí, udělá po otevření a uložení bez editací tři věci: projde relace listů, kresby a grafů a selže na jakémkoli duplicitním, osiřelém nebo visícím odkazu na graf; porovná podpis typu grafu, vzorců sérií a geometrie ukotvení mezi originálem a výstupem; a u two-charts.xlsx přečte každé xl/charts/chartN.xml z obou archivů a vyžaduje identické bajty. Stejnou kontrolu lze v Delphi snadno napsat přes 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); // vyhodí výjimku, když part zmizel
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;
Tři podmínky dělají z toho porovnání smysluplnou věc a každá, když se zapomene, selhává potichu. PreserveUnsupportedParts musí být True před Open, jinak se nezachytí žádné syrové bajty a každý graf se postaví znovu z modelu. StrictOOXML musí být False, protože strict režim nutí regeneraci ze své podstaty. A aplikace nesmí graf mezi otevřením a uložením osahovat — čtení vlastností je v pohodě, ale jakýkoli setter, který změní typovaný model, překlopí otisk a pošle graf na cestu merge, což je správné chování a ne to, k čemu tenhle test slouží. Chart party se navíc při ukládání přečíslovávají z počítadla přes celý sešit, takže sešit se změněným pořadím listů nebo grafů uloží identické bajty pod jiným názvem chartN.xml; korpusová kontrola proto sleduje relace, ne názvy
Obě opravy vyšly v HotXLS 2.382.0 a 2.382.3 a jsou ověřené na Win32 a Win64 proti lokálnímu korpusu, přičemž znovu uložené ukázky grafů prošly ještě vykreslením přes nezávislý office balík do PDF a porovnáním stránku po stránce s originály. HotXLS čte, edituje a zapisuje XLSX grafy z nativního kódu Delphi a C++Builderu bez instalovaného Excelu, a právě proto je tahle úroveň věrnosti odpovědností knihovny — stránka komponenty HotXLS pro tabulky v Delphi má seznam funkcí a trial ke stažení