Skoroszyt z dwoma wykresami przechodzi przez otwarcie i zapis, których nic nie edytuje, a na wyjściu mogą wylądować trzy różne rzeczy. Jeden wynik ma trzy części wykresu tam, gdzie źródło miało dwie. Drugi wciąż ma dwie, ale porównanie bajtów pokazuje, że każdy xl/charts/chartN.xml został przepisany, a kolory serii się rozjechały. Trzeci trzyma XML wykresów w nienaruszonym stanie, podczas gdy każdy wykres przeskoczył o kilka pikseli na linie siatki komórek. Excel otwiera wszystkie trzy bez monitu o naprawę. Każdy z tych objawów bierze się z jednej decyzji w ścieżce wczytywania i zapisu: jak arkusz znajduje swój rysunek, kiedy pobierany jest odcisk zmian wykresu i czy offsety kotwicy wewnątrz części rysunku są zapisywane z powrotem
Arkusz znajduje swój rysunek przez relacje
Objaw trzech części bierze się z loadera, który zgaduje nazwy części. W pakiecie XLSX arkusz wskazuje swój rysunek elementem <drawing r:id="…"/> (ECMA-376 Part 1 §18.3.1.36), a r:id rozwiązuje się przez część .rels arkusza na cel typu ../drawings/drawing1.xml. Excel z przypadku numeruje rysunki przewidywalnie, więc ręcznie pisany czytnik z czasem dopisuje sobie fallback: gdy relacji brakuje, spróbuj xl/drawings/drawing{n}.xml dla pozycji arkusza n i użyj go, jeśli archiwum go ma
// Skrót, który pojawia się w ręcznie pisanych czytnikach XLSX (nie wypuszczaj tego)
function GuessDrawingPart(Zip: TZipFile; SheetPos: Integer;
const RelTarget: string): string;
begin
Result := RelTarget; // rozwiązany z .rels arkusza, '' gdy brak
if Result = '' then
begin
Result := 'xl/drawings/drawing' + IntToStr(SheetPos + 1) + '.xml';
if Zip.IndexOf(Result) < 0 then
Result := '';
end;
end;
Zgadywanie działa dopóki pierwszy nie wypadnie arkusz bez wykresów. W skoroszycie, którego pierwszy arkusz nie ma rysunku ani w ogóle części .rels, podczas gdy drugi arkusz sięga po xl/drawings/drawing1.xml przez Target="../drawings/drawing1.xml", fallback wręcza arkuszowi 1 rysunek, do którego nigdy się nie odwoływał. chart1.xml jest parsowany dwa razy, po jednym razie na arkusz, a zapis wypisuje trzy części wykresu. Nazwy części w pakiecie OPC nie niosą znaczenia poza tym, które nadaje im graf relacji — to cały argument omówienia rozwiązywania relacji OPC w HotXLS
HotXLS przyjmuje ścisłe odczytanie. Rysunek arkusza jest wczytywany wyłącznie z celu relacji rysunku tego arkusza, a arkusz bez takiej relacji nie dostaje żadnego rysunku, cokolwiek leży w xl/drawings/. Część rysunku, do której nic się nie odwołuje, nigdy nie jest przypinana do arkusza
Co decyduje, czy nietknięty wykres jest kopiowany, czy budowany od nowa?
Decyduje o tym odcisk per wykres. Gdy PreserveUnsupportedParts ma wartość True w chwili Open (True to domyślne), HotXLS trzyma oryginalne bajty każdej części wykresu obok sparsowanego modelu wykresu. Potem serializuje model własnym pisarzem wykresów i zapisuje dwie liczby dla tego wyjścia: długość oraz 64-bitowy hash FNV-1a po jednostkach kodu UTF-16, ze standardową podstawą przesunięcia 14695981039346656037 i liczbą pierwszą 1099511628211. TXLSXChart.HasPreservedXml mówi, czy wykres niesie te oryginalne bajty
Przy zapisie model wykresu jest serializowany ponownie i obie liczby są porównywane. Równa długość i równy hash znaczą, że model jest dokładnie taki, jaki był przy imporcie, więc nic, co aplikacja mogła zmienić, się nie zmieniło. W tym przypadku, przy wyłączonym StrictOOXML, pisarz kopiuje skompresowany wpis wprost z archiwum źródłowego do wyniku pod nową nazwą części wykresu; bajty nie są ani dekodowane, ani ponownie deflowane. Gdy któraś liczba się różni, oryginalny XML jest zamiast tego scalany z modelem. Zmiana formatowania linii słupka błędu albo linii trendu też liczy się jako edycja, choć tego formatowania nie ma w serializowanym modelu
Scalenie to właściwa operacja dla wykresu, który ktoś edytował. Zachowuje te części oryginalnego XML, których model nie pokrywa, pozwala modelowi wygrywać dla serii, osi i grup wykresu, po czym reserializuje wynik. Czego nie potrafi, to odtworzyć oryginalnych bajtów. Notatka o edycji wykresów Excela bez utraty ChartML opisuje, co scalenie zachowuje; ten artykuł jest o warunkach, przy których nieedytowany wykres nigdy do niego nie trafia
uses System.SysUtils, lxHandleX;
procedure ResaveUntouched(const Src, Dst: string);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
S, C: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.PreserveUnsupportedParts := True; // domyślne; musi obowiązywać przed Open
Book.StrictOOXML := False; // wyjście Strict przebudowuje każdy wykres
if Book.Open(Src) <> 1 then
raise Exception.Create('Cannot open ' + Src);
for S := 1 to Book.Sheets.Count do // Sheets[] jest indeksowane od 1
begin
Sheet := Book.Sheets[S];
for C := 0 to Sheet.Charts.Count - 1 do // Charts[] jest indeksowane od zera
Writeln(Sheet.Name, ' / ', Sheet.Charts[C].Name, ': original kept = ',
BoolToStr(Sheet.Charts[C].HasPreservedXml, True));
end;
if Book.SaveAs(Dst) <> 1 then
raise Exception.Create('Cannot save ' + Dst);
finally
Book.Free;
end;
end;
Gdy jakiś wykres ma zostać przebudowany z modelu celowo, na przykład żeby pozbyć się formatowania, które niesie źródło, TXLSXChart.ClearPreservedXml wyrzuca zapisane bajty i ten wykres jest pisany wyłącznie z modelu
Czemu i tak każdy wykres trafiałby na ścieżkę scalania?
Bo odcisk pobrany jedno wołanie za wcześnie nigdy się nie zgodzi. XML wykresu rzadko da się przeczytać w jednym przebiegu. Parsowanie strumieniowe buduje serie, osie i grupy wykresu, a potem zestaw przebiegów następczych wyciąga z surowego tekstu wykresu szczegóły, które łatwiej odzyskać, gdy serie już istnieją: flagę <c:smooth> każdego <c:ser>, wartości srgbClr wypełnienia i linii znacznika, tryby przecięcia osi oraz style znaczników głównych i pomocniczych. Kolekcja serii w czasie przebiegu strumieniowego po prostu jeszcze nie istnieje, więc te przebiegi muszą przyjść po nim
Załóż, że odcisk jest pobierany na końcu przebiegu strumieniowego, zanim ruszą przebiegi następcze. Opisuje wtedy model bez flag smooth, bez kolorów znaczników i z domyślnymi znacznikami osi. Przy zapisie serializator działa na gotowym modelu i emituje <c:smooth val="1"/> plus odzyskane kolory. XML jest dłuższy, hash inny, i każdy wykres w każdym zachowanym skoroszycie przechodzi przez scalenie przy każdym zapisie. Na wykresie liniowym z wygładzonymi seriami i własnymi znacznikami przebudowane węzły <c:ser> zastępują oryginały — i stąd bierze się rozjechane kolory
HotXLS pobiera odcisk dopiero po zakończeniu wszystkich przebiegów odzyskiwania, więc hash opisuje model taki, jaki widzi go aplikacja po raz pierwszy. Te same dwie liczby są odtwarzane po udanym zapisie, tym razem względem pliku wyjściowego, i to drugie pobranie też startuje z w pełni sparsowanego modelu. Hash wykrywający zmiany trzeba pobrać po ostatnim przebiegu, który jeszcze może zmutować model. Pobrany wcześniej daje sprawdzian, który zawodzi przy każdym zapisie
Offsety kotwicy mieszkają w części rysunku
Objaw przyciągania do siatki nie ma nic wspólnego z XML wykresu. Pozycja wykresu na arkuszu jest zapisana w części rysunku, a typowa forma to xdr:twoCellAnchor (ECMA-376 Part 1 §20.5): narożnik from i narożnik to, każdy z col, colOff, row i rowOff. Offsety są w English Metric Units, 914400 na cal i 12700 na punkt, a Excel zapisuje niezerowe wartości zawsze, gdy wykres jest przeciągany albo skalowany myszą. Dla pliku, który deklaruje narożnik from w kolumnie 0, wierszu 0 z rowOff równym 19049, oraz narożnik to w kolumnie 8, wierszu 15 z colOff 247650 i rowOff 66674, wykres startuje 1,5 punktu pod górą wiersza 1 i kończy się nieco ponad ćwierć cala w głąb kolumny I
Pisarz, który przeczyta te cztery liczby, a potem wyemituje <xdr:colOff>0</xdr:colOff> dla każdego narożnika, przenosi każdy wykres na linie siatki. Część wykresu może odtwarzać się bajt w bajt, a wykres i tak się przesunie, bo jedno i drugie mieszka w różnych częściach. HotXLS trzyma rodzaj kotwicy wczytany z pliku (twoCellAnchor, oneCellAnchor albo absoluteAnchor, wystawiony jako PhysicalAnchorKind) razem z wartościami EMU i zapisuje je bez zmian. Offsety są dostępne do odczytu jako FromOffsetXEMU, FromOffsetYEMU, ToOffsetXEMU i ToOffsetYEMU, a przeliczniki jednostek stojące za nimi są rozpracowane w notatce o geometrii obrazów HotXLS i skalowaniu EMU
procedure NudgeChart(Chart: TXLSXChart);
const
EmuPerPoint = 12700;
begin
if Chart.PhysicalAnchorKind <> xlsxAnchorTwoCell then
Exit;
Writeln(Format('from R%dC%d +(%d, %d) EMU to R%dC%d +(%d, %d) EMU',
[Chart.FromRow, Chart.FromCol, Chart.FromOffsetXEMU, Chart.FromOffsetYEMU,
Chart.ToRow, Chart.ToCol, Chart.ToOffsetXEMU, Chart.ToOffsetYEMU]));
// Podmień całą ramkę jednym wywołaniem: wiersze i kolumny liczone od 1,
// offsety to EMU wewnątrz komórki, a ostatni argument to zachowanie editAs
Chart.SetTwoCellAnchor(Chart.FromRow, Chart.FromCol, Chart.ToRow, Chart.ToCol,
Chart.FromOffsetXEMU + 6 * EmuPerPoint, Chart.FromOffsetYEMU,
Chart.ToOffsetXEMU + 6 * EmuPerPoint, Chart.ToOffsetYEMU, Chart.AnchorType);
end;
SetTwoCellAnchor używaj, gdy intencją jest przesunięcie albo zmiana rozmiaru. Przypisanie FromRow, FromCol, ToRow albo ToCol na wykresie dwukomórkowym zmienia tylko ten indeks komórki i zostawia offsety na miejscach — to właściwe zachowanie dla drobnej korekty i niewłaściwe dla stawiania wykresu od zera. SetTwoCellAnchor waliduje ramkę w całości i rzuca ERangeError, gdy narożnik końcowy wyprzedza startowy albo offsety wypadają poza zakres EMU Excela. Wykres utworzony przez AddChart startuje z zerowymi offsetami, więc leży dokładnie na granicach komórek, dopóki sam mu jakichś nie dasz
Dowodzenie round-tripu porównaniem bajtów
Otwarcie wyniku w Excelu niewiele tu dowodzi. Excel normalizuje przy wczytaniu tyle, że reserializowany wykres wygląda identycznie aż do momentu, gdy ktoś zauważy zmieniony kolor znacznika, a przeskok o kilka punktów do siatki łatwo przegapić na ekranie. Sprawdzenie, które warto zautomatyzować, porównuje części wykresów bajt w bajt po otwarciu i zapisie bez żadnych edycji, a do tego wystarczy TZipFile z RTL
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); // zgłasza wyjątek, jeśli część zniknęła
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;
Kilka warunków decyduje o tym, czy niezgodność wykryta przez to sprawdzenie coś znaczy:
PreserveUnsupportedPartsmusi mieć wartość True przedOpen; bez tego żadne oryginalne bajty nie są trzymane i każdy wykres jest pisany z modeluStrictOOXMLmusi mieć wartość False, bo wyjście Strict z założenia przebudowuje wykresy. W każdym potoku, który obiecuje, że nieedytowane wykresy przeżyją zapis, trzymaj je wyłączone- Pakiet źródłowy zapisany w nazwach przestrzeni Strict OOXML jest przy zapisie przejściowym konwertowany na przestrzenie przejściowe, więc bajty wykresów się różnią, choć nic innego się nie zmieniło
- Kod między
OpenaSaveAsmoże czytać właściwości wykresu do woli, ale każdy setter zmieniający model zmienia odcisk i wysyła ten wykres na ścieżkę scalenia — to poprawne zachowanie i poza zakresem tego testu - Części wykresów są przy zapisie przenumerowywane licznikiem obejmującym cały skoroszyt, więc skoroszyt, w którym zmieniła się kolejność arkuszy albo wykresów, umieści identyczne bajty pod inną nazwą
chartN.xml; sprawdzian na ten przypadek idzie za relacjami rysunków, a nie za nazwami
Kotwice potrzebują porównania po wartości. Część rysunku jest pisana od nowa wokół trzymanych wykresów, więc jej bajty to zły kandydat do diffowania; ponowne otwarcie obu plików i porównanie FromOffsetXEMU przez ToOffsetYEMU wykres po wykresie wyłapie pisarza, który zamienia 247650 na 0. Sprawdzenie strukturalne, które przechodzi relacje arkuszy, rysunków i wykresów i wywala się na każdym zduplikowanym, osieroconym albo wiszącym odwołaniu do wykresu, łapie przypadek trzech części, zanim porównanie bajtów w ogóle ruszy
To wszystko biegnie w natywnym kodzie Delphi i C++Buildera bez instalacji Excela na maszynie, więc skoroszyt wracający z zadania wsadowego jest jedynym dowodem tego, co stało się z jego wykresami. Licencjonowanie, pełne API wykresów i wersja próbna są na stronie komponentu arkuszy HotXLS dla Delphi