HotXLS Delphi Component nepakeistą Excel diagramą atkuria baitas į baitą tik tada, kai galioja dvi sąlygos: diagrama pasiekta per darbalapio piešinio ryšį, o ne per spėjamą dalies pavadinimą, ir 64 bitų modelio fingerprint užfiksuotas po to, kai diagramos modelio analizė jau baigta. Versija 2.382.0 sutvarkė pirmąją sąlygą, versija 2.382.3 — antrąją ir pradėjo atkurti nenulinius xdr:colOff ir xdr:rowOff inkarų poslinkius, kuriuos piešinio rašytojas iki tol užkietai rašydavo kaip nulius. Abu defektai išlindo iš vieno vietinio korpuso atvejo two-charts.xlsx: pirmiausia struktūrinė patikra pamatė, kad dvi diagramos dalys tapo trimis, o paskui kiekvieno xl/charts/chartN.xml baitų palyginimas parodė, kad diagramos, kurių niekas nelietė, vis tiek perrašomos — ir nė viena problema neišmetė išimties, o Excel nesiskundė, todėl jos ir gyvavo taip ilgai
Kodėl darbaknygė su dviem diagramomis grįžo su trimis diagramų dalimis?
Nes įkėlėjas turėjo atsarginį kelią, kuris spėliojo. Kai darbalapio .rels dalyje nebuvo piešinio ryšio, senasis kodas manydavo, kad piešinys yra įprastiniu pavadinimu xl/drawings/drawing{i+1}.xml, kur i yra lapo pozicija, ir prijungdavo tą dalį, jei ji archyve egzistavo. Faile two-charts.xlsx pirmas lapas neturi nei piešinio, nei .rels dalies, o xl/drawings/drawing1.xml egzistuoja — jis priklauso antram lapui, kuris jį pasiekia per Target="../drawings/drawing1.xml". Tad pirmas lapas paveldėjo diagramą, į kurią niekada nebuvo nurodęs, chart1.xml buvo perskaitytas du kartus, o įrašymas išleido darbaknygę su trimis diagramų dalimis vietoj dviejų
Pataisa HotXLS v2.382.0 spėliojimą pašalino visiškai. Darbalapio piešinys dabar įkeliamas tik per ParPartTargets[i].Values[XlsxRtDrawing] — taikinį, užregistruotą tam lapui pagal piešinio ryšio tipą — o lapas, neturintis tokio ryšio, negauna jokio piešinio. To ir reikalauja formatas: <drawing r:id="…"/> elementas darbalapyje (ECMA-376 1 dalis, §18.3.1.36) yra vienintelė grandis tarp lapo ir jo piešinio, o dalių pavadinimai OPC pakete neturi jokios reikšmės, išskyrus tą, kurią jiems priskiria ryšių grafas. Excel rašytuose archyvuose atsitiktinai naudojami įprastiniai pavadinimai, ir būtent tai ilgai leido šiam trumpikliui praeiti; OPC ryšių sprendimo HotXLS apžvalga paaiškina, kodėl spėlioti dalies pavadinimą niekada nėra saugu, net kai spėjimas paprastai pasiteisina
// Iki v2.382.0: trūkstamas piešinio ryšys virsdavo spėjimu
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // gali priklausyti kitam lapui
// Nuo v2.382.0: ryšys arba nieko
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Ką garantuoja diagramos fingerprint?
Fingerprint kiekvienai diagramai nusprendžia, ar įrašymas gali nukopijuoti originalią dalį, ar privalo ją atkurti iš naujo. Importuojant, kai prieš Open įjungiamas PreserveUnsupportedParts, HotXLS kiekvienos diagramos dalies neapdorotus UTF-8 baitus laiko FRawChartXml, su BuildChartKnownXml sukuria tipizuoto modelio serializaciją, o šios ilgį įrašo į FRawChartModelLength, o maišą — į FRawChartModelHash. Maišas yra FNV-1a per sugeneruoto XML UTF-16 kodo vienetus, su standartine 64 bitų poslinkio baze 14695981039346656037 ir pirminiu skaičiumi 1099511628211. Įrašymo metu XlsxChartRawModelUnchanged iš naujo surenka žinomą XML ir palygina ilgį bei maišą; sutapimas reiškia, kad tipizuotas modelis yra lygiai toks, koks buvo importuojant, tad niekas, ką programa galėjo pakeisti, nepasikeitė
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 // nieko neišsaugota
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // pažodinis atkūrimas
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // struktūrinis suliejimas
end;
XLSX rašytojas eina vienu žingsniu toliau nei BuildChartXmlFromKnown. Kai modelis nepakitęs ir StrictOOXML išjungtas, jis pirmiausia bando nukopijuoti suspaustą įrašą tiesiai iš šaltinio archyvo į išvestį nauju diagramos dalies pavadinimu, tad baitai net nedekoduojami ir iš naujo suspaudžiami. Tik jei to nukopijuoti nepavyksta, jis nusileidžia į dekodavimo arba suliejimo kelią. Pats mechanizmas — ilgis plius maišas, atkūrimas kai sutampa, suliejimas kai ne — yra tas, kuris aprašytas straipsnyje apie Excel diagramų redagavimą neprarandant ChartML. Šis straipsnis apie tai, kaip jis tyliai nustojo veikti
Kodėl vis tiek kiekviena diagrama ėjo suliejimo keliu?
Nes fingerprint buvo užfiksuotas vienu kvietimu per anksti. Diagramų analizė HotXLS yra SAX ėjimas per diagramos dalį, po kurio seka atkūrimo etapų rinkinys, ištraukiantis iš neapdoroto teksto detales, kurių SAX apdorotuvai nemodeliuoja tiesiogiai: XlsxChartParseSeriesFlags nuskaito kiekvieną <c:ser> bloką dėl jo <c:smooth> požymio ir žymeklio užpildo bei žymeklio linijos srgbClr reikšmių, o paskui atkuria ašių sankirtos režimus bei didelių ir mažų žymių stilius kategorijų ir reikšmių ašims. Iki v2.382.3 tvarka ParseChartXml pabaigoje buvo tokia: suklasifikuoti ašių grupes, sukurti žinomą XML, užfiksuoti ilgį ir maišą, ir tik tada paleisti XlsxChartParseSeriesFlags. Tad fingerprint aprašė modelį, kuriam dar trūko smooth požymių, žymeklių spalvų ir žymių. Įrašymo metu BuildChartKnownXml veikė su jau užbaigtu modeliu, kuris dabar išvesdavo <c:smooth val="1"/> ir atkurtas žymeklių spalvas. Ilgesnis XML, kitas maišas, XlsxChartRawModelUnchanged grąžindavo False, ir diagrama eidavo per XlsxMergeChartXml. Suliejimas yra teisinga operacija diagramai, kurią kas nors redagavo, bet ji nėra baitus išsauganti: ji iš naujo serializuoja medį, o nuosavybės taisyklė, leidžianti tipizuotam modeliui laimėti serijų, ašių ir sklypo grupių atveju, reiškia, kad atkurti mazgai pakeičia originalus. Matomas korpuso paleidimo rezultatas buvo nuslinkusios serijų spalvos diagramose, kurių niekas neredagavo — kiekviena diagrama kiekvienoje išsaugotoje darbaknygėje, kiekvieną įrašymą, ir jokio diagnostinio pranešimo niekur
Pataisa yra vienas perstymimas: XlsxChartParseSeriesFlags dabar paleidžiamas prieš sukuriant žinomą XML, tad fingerprint aprašo modelį tokį, koks jis bus, kai programa jį pirmą kartą pamatys. Pamoka galioja ne tik diagramoms. Pokyčių aptikimo fingerprint yra vertas tiek, kiek vertas jo užfiksavimo momentas, o saugus momentas yra po to, kai baigiasi visi modelį galintys pakeisti etapai. HotXLS turi antrą tų pačių dviejų reikšmių fiksavimo vietą — bazinę liniją, kurią po sėkmingo įrašymo jis vėl nustato pagal išvesties failą — ir ta vieta visada veikdavo su visiškai išanalizuotu modeliu; importo metu veikianti vieta buvo ta keistoji
Kur dingo inkarų poslinkiai?
Į pažodinį nulį. twoCellAnchor piešinio dalyje įtvirtina diagramą tarp dviejų ląstelių, ir kiekvienas kampas neša ląstelės indeksą plius poslinkį tos ląstelės viduje: from (ECMA-376 1 dalis, §20.5.2.5) ir to (§20.5.2.32) kiekvienas laiko col, colOff (§20.5.2.4), row ir rowOff. Poslinkiai išreikšti English Metric Units, 914400 colyje, ir Excel rašo nenulines reikšmes visada, kai diagrama buvo padėta ar pakeista dydžio pele — o taip nutinka su dauguma diagramų. Pirmoji two-charts.xlsx diagrama prasideda 0 eilutėje su rowOff 19049, o baigiasi 8 stulpelyje, 15 eilutėje su colOff 247650 ir rowOff 66674 — maždaug ketvirtis colio į paskutinį stulpelį. HotXLS piešinio analizatorius tuos keturis dydžius visada skaitydavo — vaizdų kodas juos naudojo — bet diagramų rašytojas kiekvienam kampui išvesdavo <xdr:colOff>0</xdr:colOff> ir <xdr:rowOff>0</xdr:rowOff>, taip įrašydamas kiekvieną diagramą pritraukdamas prie ląstelių tinklelio
// Nuo v2.382.3 inkarų rašytojas atkuria importuotus EMU poslinkius
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 dabar neša FFromColOff, FFromRowOff, FToColOff ir FToRowOff, užpildomus piešinio analizatoriaus ir nukopijuojamus kartu su kita inkaro būsena, kai diagrama priskiriama. Jie sąmoningai privatūs: viešasis inkaro paviršius tebėra keturios ląstelių koordinatės FromRow, FromCol, ToRow ir ToCol, o iš Delphi kodo sukurta diagrama, kaip ir anksčiau, nusileidžia ant ląstelių ribų. Poslinkiai egzistuoja tam, kad kelias pirmyn ir atgal būtų ištikimas, o ne tam, kad pozicionavimas ląstelės viduje taptų funkcija. Atkreipkite dėmesį, kad ši pataisa nepriklauso nuo fingerprint: inkaras yra piešinio, o ne diagramos dalyje, tad diagrama, kurios ChartML atsikurdavo nepriekaištingai, be jos vis tiek būtų šoktelėjusi prie tinklelio. Vienetų perskaičiavimai, slypintys už tų EMU reikšmių, aptarti straipsnyje apie HotXLS vaizdų geometriją ir EMU mastelį
Kaip įrodyti, kad diagrama atsikuria nepakitusi?
Lygindami baitus, o ne atidarydami rezultatą Excel. Excel įkeldamas tiek taiso ir normalizuoja, kad nuslinkusi diagrama atrodo gerai iki pat tos akimirkos, kai analitikas pastebi pasikeitusią žymeklio spalvą. Korpuso testas, pagavęs abu defektus, po atidarymo ir įrašymo be jokių pakeitimų daro tris dalykus: pereina darbalapio, piešinio ir diagramos ryšius ir krinta dėl bet kurios pasikartojančios, našlaičės ar pakibusios diagramos nuorodos; palygina diagramos tipo, serijų formulių ir inkaro geometrijos parašą tarp originalo ir išvesties; o two-charts.xlsx atveju perskaito kiekvieną xl/charts/chartN.xml iš abiejų archyvų ir reikalauja identiškų baitų. Tą pačią patikrą lengva parašyti Delphi su 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); // išmeta išimtį, jei dalis dingo
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;
Trys sąlygos daro tą palyginimą prasmingą, ir kiekviena jų tyliai sugenda, jei pamirštama. PreserveUnsupportedParts turi būti True prieš Open, kitaip neapdoroti baitai neužfiksuojami ir kiekviena diagrama atkuriama iš modelio. StrictOOXML turi būti False, nes griežtas režimas iš esmės verčia atkurti iš naujo. Ir programa neturi liesti diagramos tarp atidarymo ir įrašymo — savybių skaitymas nieko blogo, bet bet kuris nustatymas, keičiantis tipizuotą modelį, apverčia fingerprint ir nusiunčia diagramą suliejimo keliu, o tai yra teisingas elgesys ir ne tai, kam šis testas skirtas. Diagramų dalys įrašant taip pat pernumeruojamos iš visos darbaknygės skaitiklio, tad darbaknygė, kurios lapų ar diagramų tvarka pasikeitė, identiškus baitus padės po kitu chartN.xml pavadinimu; korpuso tikrintuvas dėl to seka ryšius, o ne pavadinimus
Abi pataisos pasirodė HotXLS 2.382.0 ir 2.382.3 ir patikrintos Win32 bei Win64 su vietiniu korpusu, o iš naujo įrašyti diagramų pavyzdžiai papildomai atvaizduoti per nepriklausomą biuro paketą į PDF ir palyginti puslapis po puslapio su originalais. HotXLS skaito, redaguoja ir rašo XLSX diagramas tiesiogiai iš Delphi ir C++Builder kodo be jokio įdiegto Excel, ir būtent todėl toks tikslumas yra bibliotekos atsakomybė — HotXLS Delphi spreadsheet komponento puslapyje rasite funkcijų sąrašą ir bandomąją versiją