Tekninen artikkeli

HotXLS-kaavioiden sormenjälki ja ankkurisiirtymät Delphissä

HotXLS Delphi Component toistaa muokkaamattoman Excel-kaavion tavu tavulta vain kun kaksi asiaa pätee: kaavio tavoitettiin taulukkosivun piirustussuhteen eikä arvatun osanimen kautta, ja 64-bittinen mallin sormenjälki tallennettiin vasta kun kaaviomallin jäsennys oli valmis. Versio 2.382.0 korjasi ensimmäisen ehdon, versio 2.382.3 toisen ja alkoi samalla kierrättää nollasta poikkeavia xdr:colOff- ja xdr:rowOff-ankkurisiirtymiä, jotka piirustuskirjoittaja oli kovakoodannut nolliksi. Molemmat viat löytyivät yhdestä paikallisen korpuksen tapauksesta, two-charts.xlsx: ensin rakennevarmistus näki kahden kaavio-osan muuttuvan kolmeksi, sitten jokaisen xl/charts/chartN.xml-tiedoston tavuvertailu osoitti, että koskemattomiakin kaavioita kirjoitettiin yhä uudelleen — eikä kumpikaan ongelma nostanut poikkeusta tai saanut Exceliä valittamaan, minkä takia ne säilyivät niin kauan

Miksi kahden kaavion työkirja palasi kolmella kaavio-osalla?

Koska lataajassa oli arvaava varareitti. Kun taulukkosivulla ei ollut piirustussuhdetta .rels-osassaan, vanha koodi oletti piirustuksen sijaitsevan tavanomaisella nimellä xl/drawings/drawing{i+1}.xml, missä i on sivun sijainti, ja liitti sen osan, jos se oli arkistossa. Tiedostossa two-charts.xlsx ensimmäisellä sivulla ei ole piirustusta eikä .rels-osaa lainkaan, kun taas xl/drawings/drawing1.xml on olemassa — se kuuluu toiselle sivulle, joka tavoittaa sen polulla Target="../drawings/drawing1.xml". Sivu 1 peri siis kaavion, johon se ei koskaan viitannut, chart1.xml jäsennettiin kahdesti ja tallennus kirjoitti työkirjan ulos kolmella kaavio-osalla kahden sijaan

Kaavio siitä, miten HotXLS ratkaisee taulukkosivujen piirustukset two-charts-näytteessä: Sheet1 ei kanna piirustussuhdetta eikä rels-osaa, kun taas Sheet2 tavoittaa tiedoston xl/drawings/drawing1.xml ParPartTargets-rakenteen kautta, ja ennen versiota 2.382.0 varareitti arvasi tuon tavanomaisen nimen sivun sijainnista, joten chart1.xml jäsennettiin kahdesti ja tallennukset kirjoittivat kolme kaavio-osaa kunnes korjaus latasi piirustukset vain XlsxRtDrawing-tyypin kautta
Sheet1 ei koskaan viitannut kaavioon, joten suhdegraafi on ainoa turvallinen lähde piirustuksen kohteelle, ja arvattu tavanomainen nimi muutti kahden kaavion työkirjan kolmiosaiseksi tallennukseksi

Korjaus HotXLS:n versiossa 2.382.0 poisti arvauksen kokonaan. Taulukkosivun piirustus ladataan nyt vain kautta ParPartTargets[i].Values[XlsxRtDrawing], joka on kyseiselle sivulle tallennettu piirustussuhteen tyypin kohde, eikä sivu ilman sellaista suhdetta saa piirustusta lainkaan. Se on formaatin vaatima käytös: taulukkosivun <drawing r:id="…"/>-elementti (ECMA-376 Part 1 §18.3.1.36) on ainoa linkki sivun ja sen piirustuksen välillä, eivätkä OPC-paketin osanimet kanna mitään merkitystä sen lisäksi, mitä suhdegraafi niille antaa. Excelin kirjoittamat arkistot sattuvat käyttämään tavanomaisia nimiä, minkä ansiosta oikopolku kesti niin kauan; HotXLS:n OPC-suhderesoluutiota käsittelevä läpikäynti kertoo, miksei osanimen arvaaminen ole koskaan turvallista, vaikka arvaus olisi yleensä oikea

// Ennen versiota 2.382.0: puuttuva piirustussuhde korvattiin arvauksella
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
  drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
  LoadDrawing(zip, drawingName);   // voi kuulua toiselle taulukkosivulle

// Versiosta 2.382.0 lähtien: suhde tai ei mitään
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
  LoadDrawing(zip, drawingName);

Mitä kaavion sormenjälki takaa?

Sormenjälki ratkaisee kaaviokohtaisesti, voiko tallennus kopioida alkuperäisen osan vai täytyykö se generoida uudelleen. Tuonnissa, kun PreserveUnsupportedParts on päällä ennen Open-kutsua, HotXLS säilyttää jokaisen kaavio-osan raa'at UTF-8-tavut kentässä FRawChartXml, rakentaa tyypitetyn mallin oman serialisoinnin komennolla BuildChartKnownXml ja tallentaa tuon serialisoinnin pituuden kenttään FRawChartModelLength ja tiivisteen kenttään FRawChartModelHash. Tiiviste on FNV-1a generoidun XML:n UTF-16-koodiyksiköiden yli, vakioisella 64-bittisellä offset-kannalla 14695981039346656037 ja alkuluvulla 1099511628211. Tallennushetkellä XlsxChartRawModelUnchanged rakentaa tunnetun XML:n uudelleen ja vertaa pituutta ja tiivistettä; täsmääminen tarkoittaa, että tyypitetty malli on täsmälleen se mikä se oli tuonnissa, joten mikään sovelluksen mahdollisesti muuttama ei ole muuttunut

HotXLS tallentaa kaavion sormenjäljen tuonnissa pitäen raa'at UTF-8-tavut FRawChartXml-kentässä, kun BuildChartKnownXml tuottaa FRawChartModelLength-arvon ja FNV-1a-tiivisteen, ja tallennushetkellä XlsxChartRawModelUnchanged rakentaa molemmat uudelleen ja vertaa niitä, joten täsmääminen toistaa alkuperäiset tavut tai kopioi pakatun merkinnän ja eroavuus putoaa XlsxMergeChartXml-polulle
Sormenjälki on yhtä hyvä kuin hetki, jolloin se otetaan, ja sen tallentaminen ennen kuin kaikki palautusvaiheet ovat valmiit takaa tiivisteen, joka ei enää koskaan täsmää valmiiseen malliin
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                                  // mitään ei säilytetty
  else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
    Result := XlsxDecodeChartUtf8(Chart.FRawChartXml)   // sanatarkka toisto
  else
    Result := XlsxMergeChartXml(
      XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // rakenteellinen yhdistäminen
end;

XLSX-kirjoittaja menee askeleen pidemmälle kuin BuildChartXmlFromKnown. Kun malli on muuttumaton ja StrictOOXML on pois päältä, se yrittää ensin kopioida pakatun merkinnän suoraan lähdearkistosta tulosteeseen kaavion uudella osanimellä, jolloin tavuja ei edes pureta ja pakata uudelleen. Vasta jos kopiointi ei ole mahdollista, se putoaa purku-tai-yhdistä-polulle. Mekanismi itsessään — pituus plus tiiviste, toisto kun ne ovat yhtä, yhdistäminen kun eivät — on se, jota kuvataan muistiinpanossa Excel-kaavioiden muokkaamisesta ChartML:ää menettämättä. Tämä artikkeli kertoo tavasta, jolla se lakkasi hiljaisesti toimimasta

Miksi jokainen kaavio päätyi silti yhdistämispolulle?

Koska sormenjälki tallennettiin yhden kutsun liian aikaisin. Kaavion jäsennys HotXLS:ssä on SAX-kierros kaavio-osan yli ja sen perään joukko palautusvaiheita, jotka poimivat raakatekstistä yksityiskohtia, joita SAX-käsittelijät eivät mallinna suoraan: XlsxChartParseSeriesFlags lukee jokaisesta <c:ser>-lohkosta sen <c:smooth>-lipun sekä markkerin täytön ja markkeriviivan srgbClr-arvot ja palauttaa sitten akselien risteämistilat sekä luokka- ja arvoakselien pää- ja sivuasteikon viivamerkkityylit. Ennen versiota 2.382.3 järjestys ParseChartXml-funktion lopussa oli: luokittele akseliryhmät, rakenna tunnettu XML, tallenna pituus ja tiiviste ja vasta sitten aja XlsxChartParseSeriesFlags. Sormenjälki kuvasi siis mallia, josta puuttuivat yhä smooth-liput, markkerivärit ja viivamerkit. Tallennushetkellä BuildChartKnownXml ajettiin valmista mallia vasten, joka tuotti nyt <c:smooth val="1"/> ja palautetut markkerivärit. Pidempi XML, eri tiiviste, XlsxChartRawModelUnchanged palautti False, ja kaavio meni XlsxMergeChartXml-polulle. Yhdistäminen on oikea toimenpide kaaviolle, jota joku muokkasi, mutta se ei säilytä tavuja: se serialisoi puun uudelleen, ja omistussääntö, joka antaa tyypitetylle mallille vallan sarjoissa, akseleissa ja kuvaajaryhmissä, tarkoittaa että uudelleen luodut solmut korvaavat alkuperäiset. Näkyvä tulos korpusajossa oli ajautuneet sarjavärit kaavioissa, joita kukaan ei ollut muokannut — jokainen kaavio jokaisessa säilytetyssä työkirjassa, jokaisella tallennuksella, ilman mitään diagnostiikkaa missään

Korjaus on yksi uudelleenjärjestely: XlsxChartParseSeriesFlags ajetaan nyt ennen kuin tunnettu XML rakennetaan, joten sormenjälki kuvaa mallin sellaisena kuin se on, kun sovellus näkee sen ensimmäisen kerran. Oppi yleistyy kaavioita pidemmälle. Muutoksen tunnistava sormenjälki on yhtä hyvä kuin hetki, jolloin se otetaan, ja turvallinen hetki on jokaisen mallia mahdollisesti muuttavan vaiheen jälkeen. HotXLS:llä on toinen tallennuspaikka samoille kahdelle arvolle, perustaso, jonka se asettaa uudelleen tulostetiedostoa vasten onnistuneen tallennuksen jälkeen, ja se paikka oli aina ajanut täysin jäsennettyä mallia vasten; tuontinaikainen paikka oli se joukosta poikkeava

Minne ankkurisiirtymät katosivat?

Nollaliteraaliin. Piirustusosan twoCellAnchor kiinnittää kaavion kahden solun väliin, ja jokainen kulma kantaa solun indeksiä sekä siirtymää kyseisen solun sisällä: from (ECMA-376 Part 1 §20.5.2.5) ja to (§20.5.2.32) sisältävät kumpikin col-, colOff- (§20.5.2.4), row- ja rowOff-arvot. Siirtymät ovat English Metric Units -yksiköitä, 914400 yksikköä tuumaa kohden, ja Excel kirjoittaa nollasta poikkeavia arvoja aina kun kaavio on sijoitettu tai koottu uudelleen hiirellä, eli useimmissa kaavioissa. Tiedoston two-charts.xlsx ensimmäinen kaavio alkaa riviltä 0 rowOff-arvolla 19049 ja päättyy sarakkeeseen 8, riville 15 colOff-arvolla 247650 ja rowOff-arvolla 66674 — noin neljännes tuumaa viimeiseen sarakkeeseen. HotXLS:n piirustusjäsennin oli aina lukenut nuo neljä arvoa — kuvakoodi käytti niitä — mutta kaaviokirjoittaja tuotti jokaiselle kulmalle <xdr:colOff>0</xdr:colOff> ja <xdr:rowOff>0</xdr:rowOff>, napsauttaen jokaisen kaavion soluruudukkoon tallennuksessa

HotXLS-näytteen ensimmäisen kaavion xdr:twoCellAnchor-kulmien rakenne: from sisältää col-arvon 0 ja rowOff-arvon 19049, kun taas to sisältää col-arvon 8, colOff-arvon 247650 ja rowOff-arvon 66674 EMU-yksiköissä 914400 yksikköä tuumaa kohden, ja nollasiirtymiä tuottanut kirjoittaja napsautti kaaviot ruudukkoon kunnes FFromColOff, FToColOff ja niiden sisarukset toistivat tuodut arvot
Ankkuri sijaitsee piirustusosassa eikä kaavio-osassa, joten tämä korjaus on riippumaton sormenjälkikorjauksesta, ja molempien piti valmistua ennen kuin työkirja todella kulki edestakaisin muuttumattomana
// Versiosta 2.382.3 lähtien ankkurikirjoittaja toistaa tuodut EMU-siirtymät
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 kantaa nyt kenttiä FFromColOff, FFromRowOff, FToColOff ja FToRowOff, jotka täytetään piirustusjäsentimestä ja kopioidaan muun ankkuritilan mukana, kun kaavio sijoitetaan. Ne ovat tarkoituksella yksityisiä: julkinen ankkuripinta on edelleen neljä solukoordinaattia FromRow, FromCol, ToRow ja ToCol, ja Delphi-koodista luotu kaavio laskeutuu solurajoille kuten ennenkin. Siirtymät ovat olemassa, jotta edestakainen kierros olisi uskollinen, eivät siksi että solun sisäinen sijoittelu vietäisiin ominaisuutena julki. Huomaa, että tämä korjaus on riippumaton sormenjäljestä: ankkuri sijaitsee piirustusosassa eikä kaavio-osassa, joten kaavio, jonka ChartML toistui täydellisesti, olisi silti hypännyt ruudukkoon ilman tätä. Noiden EMU-arvojen taustalla olevat yksikkömuunnokset käsitellään muistiinpanossa HotXLS:n kuvageometriasta ja EMU-skaalauksesta

Miten todistat, että kaavio kulkee edestakaisin muuttumattomana?

Vertaa tavuja, älä avaa tulosta Excelissä. Excel korjaa ja normalisoi latauksessa niin paljon, että ajautunut kaavio näyttää hyvältä siihen asti, kun analyytikko huomaa markkerin värin muuttuneen. Korpusstesti, joka nappasi molemmat viat, tekee kolme asiaa avaus-ja-tallennuskierroksen jälkeen ilman muokkauksia: se käy läpi taulukkosivun, piirustuksen ja kaavion suhteet ja epäonnistuu jokaisesta päällekkäisestä, orvosta tai roikkuvasta kaavioviittauksesta; se vertaa kaaviotyypin, sarjojen kaavojen ja ankkurigeometrian allekirjoitusta alkuperäisen ja tulosteen välillä; ja tiedostolle two-charts.xlsx se lukee jokaisen xl/charts/chartN.xml-tiedoston molemmista arkistoista ja vaatii identtiset tavut. Sama tarkistus on helppo kirjoittaa Delphissä RTL:n TZipFile-luokalla

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);   // heittää poikkeuksen, jos osa on kadonnut
        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;

Kolme ehtoa tekevät vertailusta mielekkään, ja jokainen niistä pettää hiljaa, jos se unohtuu. PreserveUnsupportedParts-asetuksen on oltava True ennen Open-kutsua, muuten raakoja tavuja ei tallenneta ja jokainen kaavio rakennetaan mallista uudelleen. StrictOOXML:n on oltava False, koska tiukka tila pakottaa uudelleengeneroinnin suunnittelun mukaan. Eikä sovellus saa koskea kaavioon avauksen ja tallennuksen välillä — ominaisuuksien luku on ok, mutta mikä tahansa setteri, joka muuttaa tyypitettyä mallia, kääntää sormenjäljen ja lähettää kaavion yhdistämispolulle, mikä on oikea käytös eikä se, mitä tämä testi hakee. Kaavio-osat numeroidaan lisäksi uudelleen työkirjan laajuisesta laskurista tallennuksessa, joten työkirja, jonka sivujärjestys tai kaaviojärjestys muuttui, sijoittaa identtiset tavut eri chartN.xml-nimen alle; korpuksen tarkistaja seuraa suhteita nimien sijaan juuri siksi

Molemmat korjaukset toimitettiin HotXLS-versioissa 2.382.0 ja 2.382.3, ja ne on vahvistettu Win32- ja Win64-alustoilla paikallista korpusta vasten, mukaan lukien uudelleen tallennetut kaavionäytteet, jotka on myös renderöity riippumattomalla toimisto-ohjelmistolla PDF:ksi ja verrattu sivu sivulta alkuperäisiin. HotXLS lukee, muokkaa ja kirjoittaa XLSX-kaavioita natiivista Delphistä ja C++Builderistä ilman asennettua Exceliä, ja juuri se tekee tämän tason uskollisuudesta kirjaston vastuun — HotXLS Delphi -taulukkokomponentin sivulla on ominaisuusluettelo ja kokeiluversio