HotXLS Delphi Component възпроизвежда немодифицирана Excel графика байт по байт само когато две условия са налице: графиката е достигната през worksheet drawing релацията, а не през предположено име на part, и 64-битовият model fingerprint е уловен след като chart моделът е приключил парсването. Версия 2.382.0 оправи първото условие, версия 2.382.3 оправи второто и започна да round-trip-ва ненулевите anchor отмествания xdr:colOff и xdr:rowOff, които drawing writer-ът hard-code-ваше на нула. И двата дефекта излязоха от един локален corpus случай, two-charts.xlsx: първо една структурна assertion видя как две chart части станат три, после байтово сравнение на всеки xl/charts/chartN.xml показа графики, които никой не беше пипнал, но продължаваха да се пренаписват — и нито единият проблем не вдигна exception, нито накара Excel да се оплаче, затова оцеляха толкова дълго
Защо работна книга с две графики се върна с три chart части?
Защото loader-ът имаше fallback, който познаваше. Когато един worksheet нямаше drawing релация в своя .rels part, старият код предполагаше, че drawing-ът живее на конвенционалното име xl/drawings/drawing{i+1}.xml, където i е позицията на листа, и прикачваше тази част, ако тя съществуваше в архива. В two-charts.xlsx първият лист няма drawing и изобщо няма .rels част, докато xl/drawings/drawing1.xml съществува — той принадлежи на втория лист, който го достига чрез Target="../drawings/drawing1.xml". Лист 1 следователно наследи графика, която никога не беше реферирал, chart1.xml беше парснат два пъти, а записът изведе работната книга с три chart части вместо две
Поправката в HotXLS v2.382.0 премахна предположението изцяло. Worksheet drawing вече се зарежда само чрез ParPartTargets[i].Values[XlsxRtDrawing] — целта, записана за типа drawing релация на този лист — а лист без такава релация не получава никакъв drawing. Това е поведението, което форматът изисква: елементът <drawing r:id="…"/> в worksheet-а (ECMA-376 Part 1 §18.3.1.36) е единствената връзка между лист и негов drawing, а имената на частите в OPC пакет не носят никакво значение отвъд това, което relationship графът им придава. Архивите, записани от Excel, случайно ползват конвенционалните имена, което позволи на този shortcut да минава толкова дълго; разходката из OPC relationship resolution в HotXLS обяснява защо познаването на име на part никога не е безопасно, дори когато предположението обикновено е вярно
// Преди v2.382.0: липсваща drawing релация се обръщаше към предположение
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // може да принадлежи на друг лист
// От v2.382.0: релация или нищо
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Какво гарантира chart fingerprint-ът?
Fingerprint-ът решава, за всяка графика поотделно, дали записът може да копира оригиналната част или трябва да я регенерира. При импорт, с PreserveUnsupportedParts включено преди Open, HotXLS пази суровите UTF-8 байтове на всяка chart част в FRawChartXml, изгражда собствената сериализация на typed модела с BuildChartKnownXml и записва дължината на тази сериализация в FRawChartModelLength, а hash-ът ѝ — в FRawChartModelHash. Hash-ът е FNV-1a върху UTF-16 code unit-ите на генерирания XML, със стандартния 64-битов offset basis 14695981039346656037 и простото число 1099511628211. По време на запис XlsxChartRawModelUnchanged пресъздава known XML-а и сравнява дължина и hash; съвпадение означава, че typed моделът е точно това, което беше при импорт, значи нищо, което приложението е можало да промени, не е променено
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 // нищо не е запазено
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // дословен replay
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // структурен merge
end;
XLSX writer-ът върви една стъпка по-далеч от BuildChartXmlFromKnown. Когато моделът е непроменен и StrictOOXML е изключен, той първо се опитва да копира компресирания entry направо от source архива в изхода под новото име на chart частта, така че байтовете дори не се декодират и пре-дефлират. Само ако това копиране не е възможно, пътят продължава към decode-или-merge варианта. Самият механизъм — дължина плюс hash, replay при равенство, merge при различие — е описаният в бележката за редактиране на Excel графики без загуба на ChartML. Тази статия е за начина, по който той тихо спря да работи
Защо тогава всяка графика поемаше по merge пътя?
Защото fingerprint-ът беше улавян едно извикване твърде рано. Парсването на графики в HotXLS е SAX преминаване върху chart частта, последвано от набор recovery преминавания, които изваждат от суровия текст детайли, които SAX handler-ите не моделират директно: XlsxChartParseSeriesFlags чете всеки <c:ser> блок за неговия флаг <c:smooth> и srgbClr стойностите на marker fill и marker line, след което възстановява axis crossing режимите и стиловете на major и minor tick marks за category и value осите. Преди v2.382.3 редът в края на ParseChartXml беше: класифицирай axis групите, изгради known XML, улови дължина и hash и чак тогава пусни XlsxChartParseSeriesFlags. Fingerprint-ът следователно описваше модел, на който още му липсваха smooth флаговете, marker цветове и tick marks. По време на запис BuildChartKnownXml вървеше върху завършения модел, който вече излъчваше <c:smooth val="1"/> и възстановените marker цветове. По-дълъг XML, различен hash, XlsxChartRawModelUnchanged връщаше False, а графиката минаваше през XlsxMergeChartXml. Merge-ът е коректна операция за графика, която някой е редактирал, но не е запазваща байтовете: той ресериализира дървото, а правилото за собственост, което позволява на typed модела да печели за series, оси и plot групи, означава, че регенерираните възли заместват оригиналите. Видимият резултат в corpus пробегът беше изместени series цветове по графики, които никой не беше пипнал — всяка графика във всяка запазена работна книга, при всеки запис, без никаква диагностика никъде
Поправката е едно единствено пренареждане: XlsxChartParseSeriesFlags вече върви преди known XML-ът да е изграден, така че fingerprint-ът описва модела така, както той ще съществува, когато приложението го види за първи път. Урокът се обобщава отвъд графиките. Fingerprint за засичане на промени е толкова добър, колкото моментът, в който е уловен, а безопасният момент е след като всяко преминаване, което може да мутира модела, е приключило. HotXLS има втора позиция на улавяне на същите две стойности — baseline-ът, който възстановява спрямо изходния файл след успешен запис — и тази позиция винаги е работила върху напълно парснат модел; импортната беше странното изключение
Накъде отидоха anchor отместванията?
В буквална нула. twoCellAnchor в drawing частта закача графика между две клетки, като всеки ъгъл носи клетъчен индекс плюс отместване вътре в тази клетка: from (ECMA-376 Part 1 §20.5.2.5) и to (§20.5.2.32) держат всяко по col, colOff (§20.5.2.4), row и rowOff. Отместванията са в English Metric Units, 914400 на инч, а Excel записва ненулеви стойности винаги, когато графиката е поставена или преоразмерена с мишката, което е повечето графики. Първата графика в two-charts.xlsx започва на ред 0 с rowOff 19049 и свършва на колона 8, ред 15 с colOff 247650 и rowOff 66674 — около четвърт инч навътре в последната колона. Drawing parser-ът в HotXLS винаги е четял тези четири стойности — image кодът ги ползваше — но chart writer-ът излъчваше <xdr:colOff>0</xdr:colOff> и <xdr:rowOff>0</xdr:rowOff> за всеки ъгъл, залепяйки всяка графика към клетъчната мрежа при запис
// От v2.382.3 anchor writer-ът възпроизвежда импортираните EMU отмествания
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 вече носи FFromColOff, FFromRowOff, FToColOff и FToRowOff, попълвани от drawing parser-а и копирани заедно с останалото anchor състояние, когато графика е присвоява. Те са нарочно private: публичната anchor повърхност си остава четирите клетъчни координати FromRow, FromCol, ToRow и ToCol, а графика, създадена от Delphi код, каца на клетъчни граници както преди. Отместванията съществуват, за да направят round trip-ът верен, а не за да излагат sub-cell позиционирането като функционалност. Обърнете внимание, че тази поправка е независима от fingerprint-а: anchor-ът живее в drawing частта, не в chart частта, така че графика, чийто ChartML се възпроизвеждаше идеально, пак би скачала към мрежата без нея. Конверсиите на единици зад тези EMU стойности са разгледани в бележката за image геометрията и EMU скалирането в HotXLS
Как доказвате, че една графика преминава round-trip непроменена?
Чрез сравняване на байтове, не чрез отваряне на резултата в Excel. Excel толкова много поправя и нормализира при зареждане, че изместена графика изглежда наред чак до момента, в който аналитик забележи, че marker цветът се е променил. Corpus тестът, който хвана и двата дефекта, прави три неща след open-and-save без редакции: обхожда worksheet, drawing и chart релациите и се проваля при всяко дублирано, осиротяло или висящо chart упоменание; сравнява сигнатура от chart тип, series формули и anchor геометрия между оригинал и изход; и за two-charts.xlsx чете всеки xl/charts/chartN.xml и от двата архива и изисква идентични байтове. Същата проверка се пише лесно в Delphi с 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); // вдига exception, ако частта е изчезнала
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;
Три условия правят това сравнение смислено, и всяко едно проваля тихо, ако бъде забравено. PreserveUnsupportedParts трябва да е True преди Open, или никакви сурови байтове не се улавят и всяка графика се възстановява от модела. StrictOOXML трябва да е False, защото strict режимът принуждава регенерация по замисъл. И приложението не трябва да пипа графиката между отваряне и запис — четенето на свойства е ок, но всеки setter, който променя typed модела, обръща fingerprint-а и праща графиката по merge пътя, което е коректно поведение и не е това, за което е този тест. Chart частите освен това се преименуват от брояч за цялата работна книга при запис, така че работна книга, чийто ред на листовете или на графиките е сменен, ще постави идентични байтове под различно име chartN.xml; corpus checker-ът следва релациите, а не имената, точно заради това
И двете поправки излязоха в HotXLS 2.382.0 и 2.382.3 и са проверени на Win32 и Win64 срещу локалния corpus, като презаписаните chart образци са рендерирани и през независим office suite към PDF и сравнени страница по страница с оригиналите. HotXLS чете, редактира и записва XLSX графики от нативен Delphi и C++Builder код без никаква инсталация на Excel, което прави това ниво на дословност отговорност на библиотеката — страницата на HotXLS Delphi spreadsheet компонента има списъка с функционалности и trial изтегляне