Eine Mappe mit zwei Charts durchläuft einen Open-and-Save, der nichts ändert, und am anderen Ende können drei verschiedene Dinge herauskommen. Eine Ausgabe hat drei Chart-Parts, wo die Quelle zwei hatte. Eine andere hat immer noch zwei, aber ein Byte-Vergleich zeigt, dass jede xl/charts/chartN.xml neu geschrieben wurde und die Serienfarben gedriftet sind. Eine dritte hält das Chart-XML intakt, während jedes Chart ein paar Pixel aufs Zellraster gesprungen ist. Excel öffnet alle drei ohne Reparatur-Prompt. Jedes Symptom kommt von einer Entscheidung im Load- und Save-Pfad: wie ein Worksheet sein Drawing findet, wann der Änderungs-Fingerprint des Charts genommen wird, und ob die Anker-Offsets im Drawing-Part zurückgeschrieben werden
Ein Worksheet findet sein Drawing über seine Beziehungen
Das Drei-Parts-Symptom kommt von einem Loader, der Part-Namen rät. In einem XLSX-Paket zeigt ein Worksheet mit einem <drawing r:id="…"/>-Element (ECMA-376 Part 1 §18.3.1.36) auf sein Drawing, und die r:id löst sich über das .rels-Part des Sheets zu einem Ziel wie ../drawings/drawing1.xml auf. Excel nummeriert Drawings zufällig vorhersagbar, deshalb wächst in handgeschriebenen Readern oft ein Fallback: Fehlt die Beziehung, xl/drawings/drawing{n}.xml für die Sheet-Position n versuchen und benutzen, wenn das Archiv ihn hat
// Ein Shortcut, der in handgeschriebenen XLSX-Readern auftaucht (nicht ausliefern)
function GuessDrawingPart(Zip: TZipFile; SheetPos: Integer;
const RelTarget: string): string;
begin
Result := RelTarget; // aus dem .rels-Part des Sheets aufgelöst, '' wenn fehlend
if Result = '' then
begin
Result := 'xl/drawings/drawing' + IntToStr(SheetPos + 1) + '.xml';
if Zip.IndexOf(Result) < 0 then
Result := '';
end;
end;
Der Rat hält, bis ein Sheet ohne Charts zuerst kommt. In einer Mappe, deren erstes Sheet weder ein Drawing noch überhaupt ein .rels-Part hat, während das zweite Sheet xl/drawings/drawing1.xml über Target="../drawings/drawing1.xml" erreicht, gibt der Fallback Sheet 1 ein Drawing, auf das es nie verwiesen hat. chart1.xml wird zweimal geparst, einmal pro Sheet, und der Save schreibt drei Chart-Parts. Part-Namen in einem OPC-Paket tragen keine Bedeutung über das hinaus, was der Beziehungsgraph ihnen zuweist, was das ganze Argument des Rundgangs zur OPC-Beziehungsauflösung in HotXLS ist
HotXLS Delphi Component nimmt die strikte Lesart. Das Drawing eines Worksheets wird nur aus dem Ziel der Drawing-Beziehung dieses Sheets geladen, und ein Sheet ohne eine solche bekommt kein Drawing, ganz gleich, welche Dateien in xl/drawings/ liegen. Ein Drawing-Part, auf den nichts verweist, wird nie an ein Sheet gehängt
Was entscheidet, ob ein unangetastetes Chart kopiert oder neu gebaut wird?
Ein pro-Chart-Fingerprint entscheidet es. Ist PreserveUnsupportedParts zum Zeitpunkt des Open True (True ist der Default), bewahrt HotXLS die Original-Bytes jedes Chart-Parts neben dem geparsten Chart-Modell auf. Dann serialisiert es das Modell mit seinem eigenen Chart-Writer und zeichnet zwei Zahlen für diese Ausgabe auf: ihre Länge und einen 64-Bit-FNV-1a-Hash über ihre UTF-16-Code-Units, mit der Standard-Offset-Basis 14695981039346656037 und der Primzahl 1099511628211. TXLSXChart.HasPreservedXml meldet, ob ein Chart diese Original-Bytes trägt
Zur Save-Zeit wird das Chart-Modell erneut serialisiert und beide Zahlen werden verglichen. Gleiche Länge und gleicher Hash bedeuten, dass das Modell exakt das vom Import ist, also hat nichts geändert, was die Anwendung hätte ändern können. In dem Fall kopiert der Writer bei ausgeschaltetem StrictOOXML den komprimierten Eintrag aus dem Quellarchiv direkt unter dem neuen Part-Namen des Charts in die Ausgabe; die Bytes werden nie dekodiert oder neu gepackt. Weicht eine der beiden Zahlen ab, wird das Original-XML stattdessen mit dem Modell gemerged. Auch das Ändern der Linienformatierung eines Fehlerbalkens oder einer Trendline zählt als Bearbeitung, obwohl diese Formatierung nicht Teil des serialisierten Modells ist
Der Merge ist die richtige Operation für ein Chart, das jemand bearbeitet hat. Er behält die Teile des Original-XML, die das Modell nicht abdeckt, lässt das Modell bei Serien, Achsen und Plot-Gruppen gewinnen und serialisiert das Ergebnis neu. Was er nicht kann, ist die Original-Bytes reproduzieren. Die Notiz zu Excel-Charts bearbeiten, ohne ChartML zu verlieren behandelt, was der Merge behält; dieser Artikel handelt von den Bedingungen, unter denen ein unbearbeitetes Chart ihn nie erreicht
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; // der Default; muss vor dem Open gelten
Book.StrictOOXML := False; // Strict-Ausgabe regeneriert jedes Chart
if Book.Open(Src) <> 1 then
raise Exception.Create('Cannot open ' + Src);
for S := 1 to Book.Sheets.Count do // Sheets[] ist 1-basiert
begin
Sheet := Book.Sheets[S];
for C := 0 to Sheet.Charts.Count - 1 do // Charts[] ist 0-basiert
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;
Wenn ein Chart absichtlich aus dem Modell regeneriert werden soll, zum Beispiel um Formatierung loszuwerden, die die Quelle trug, wirft TXLSXChart.ClearPreservedXml die gespeicherten Bytes weg, und dieses Chart wird allein aus dem Modell geschrieben
Warum würde sowieso jedes Chart den Merge-Pfad nehmen?
Weil ein Fingerprint, der einen Schritt zu früh genommen wurde, nie wieder matcht. Chart-XML wird selten in einem einzigen Durchlauf gelesen. Ein Streaming-Parse baut Serien, Achsen und Plot-Gruppen auf, und dann lesen eine Reihe von Follow-up-Pässen Details aus dem rohen Chart-Text, die sich erst recovern lassen, wenn die Serien existieren: das <c:smooth>-Flag jedes <c:ser>, die srgbClr-Werte von Marker-Füllung und Marker-Linie, Achsenschnitt-Modi und die Major- und Minor-Tick-Mark-Styles. Die Serien-Collection existiert schlicht noch nicht, während der Streaming-Pass läuft, also müssen diese Pässe danach kommen
Angenommen, der Fingerprint wird am Ende des Streaming-Passes genommen, bevor die Follow-up-Pässe laufen. Er beschreibt dann ein Modell ohne Smooth-Flags, ohne Marker-Farben und mit Default-Tick-Marks. Zur Save-Zeit läuft der Serializer gegen das fertige Modell und emitiert <c:smooth val="1"/> plus die wiederhergestellten Farben. Das XML ist länger, der Hash weicht ab, und jedes Chart in jeder bewahrten Mappe geht bei jedem Save durch den Merge. Auf einem Liniendiagramm mit geglätteten Serien und eigenen Markern ersetzen die regenerierten <c:ser>-Knoten die Originale, und genau daraus kommt der Farbdrift
HotXLS nimmt den Fingerprint erst, nachdem jeder Recovery-Pass abgeschlossen ist, also beschreibt der Hash das Modell so, wie die Anwendung es zum ersten Mal sieht. Dieselben beiden Zahlen werden nach einem erfolgreichen Save neu etabliert, diesmal aus der Ausgabedatei, und diese zweite Aufnahme startet ebenfalls von einem vollständig geparsten Modell. Ein Change-Detection-Hash muss nach dem letzten Pass genommen werden, der das Modell noch verändern kann. Ihn früher zu nehmen ergibt einen Check, der bei jedem Save fehlschlägt
Anker-Offsets leben im Drawing-Part
Das Grid-Snap-Symptom hat mit Chart-XML nichts zu tun. Wo ein Chart auf dem Sheet sitzt, steht im Drawing-Part, und die übliche Form ist xdr:twoCellAnchor (ECMA-376 Part 1 §20.5): eine from-Ecke und eine to-Ecke, jeweils mit col, colOff, row und rowOff. Die Offsets sind English Metric Units, 914400 pro Zoll und 12700 pro Punkt, und Excel schreibt Werte ungleich null, wann immer ein Chart mit der Maus gezogen oder in der Größe geändert wurde. Für eine Datei, die eine from-Ecke bei Spalte 0, Zeile 0 mit einem rowOff von 19049 deklariert und eine to-Ecke bei Spalte 8, Zeile 15 mit einem colOff von 247650 und einem rowOff von 66674, beginnt das Chart 1,5 Punkte unter der Oberkante von Zeile 1 und endet etwas mehr als ein Viertelzoll in Spalte I hinein
Ein Writer, der diese vier Zahlen liest und dann für jede Ecke <xdr:colOff>0</xdr:colOff> ausgibt, rückt jedes Chart auf die Gitterlinien. Der Chart-Part kann Byte für Byte zurückgespielt werden, und das Chart bewegt sich trotzdem, weil beide in unterschiedlichen Parts leben. HotXLS behält die geladene Anker-Art (twoCellAnchor, oneCellAnchor oder absoluteAnchor, erreichbar als PhysicalAnchorKind) zusammen mit ihren EMU-Werten und schreibt sie unverändert zurück. Die Offsets sind als FromOffsetXEMU, FromOffsetYEMU, ToOffsetXEMU und ToOffsetYEMU lesbar, und die Einheitenumrechnungen dahinter sind in der Notiz zu HotXLS-Bildgeometrie und EMU-Skalierung durchgearbeitet
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]));
// Den ganzen Rahmen in einem Aufruf ersetzen: Zeilen und Spalten sind 1-basiert,
// Offsets sind EMU innerhalb der Zelle, das letzte Argument ist das editAs-Verhalten
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;
Benutzen Sie SetTwoCellAnchor, wenn die Absicht ist, zu verschieben oder die Größe zu ändern. FromRow, FromCol, ToRow oder ToCol an einem Two-Cell-Chart zuzuweisen ändert nur diesen Zellindex und lässt die Offsets, wo sie waren, was für eine kleine Anpassung das richtige und für das Platzieren eines Charts von Grund auf das falsche Verhalten ist. SetTwoCellAnchor validiert den Rahmen als Ganzes und wirft einen ERangeError, wenn die Ende-Ecke vor der Start-Ecke liegt oder die Offsets außerhalb des Excel-EMU-Bereichs fallen. Ein mit AddChart erzeugtes Chart startet mit Null-Offsets und sitzt also exakt auf Zellgrenzen, bis Sie ihm welche geben
Den Roundtrip mit einem Byte-Vergleich nachweisen
Die Ausgabe in Excel zu öffnen beweist hier wenig. Excel normalisiert beim Laden so viel, dass ein neu serialisiertes Chart identisch aussieht, bis jemand merkt, dass sich eine Marker-Farbe geändert hat, und ein Grid-Snap von ein paar Punkten ist auf dem Bildschirm leicht zu übersehen. Der Check, der sich zu automatisieren lohnt, vergleicht die Chart-Parts Byte für Byte nach einem Open-and-Save ohne Bearbeitungen, und das RTL-TZipFile ist alles, was es dafür braucht
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); // wirft eine Exception, wenn der Part fehlt
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;
Ein paar Bedingungen entscheiden, ob eine Abweichung in diesem Check etwas bedeutet:
PreserveUnsupportedPartsmuss vor demOpenTrue sein; ohne es werden keine Original-Bytes bewahrt und jedes Chart aus dem Modell geschriebenStrictOOXMLmuss False sein, denn die Strict-Ausgabe regeneriert Charts by Design. Lassen Sie es in jeder Pipeline aus, die verspricht, dass unbearbeitete Charts einen Save überleben- Ein Quellpaket, das mit Strict-OOXML-Namespaces geschrieben wurde, wird bei einem transitionalen Save in transzionale Namespaces konvertiert, also weichen seine Chart-Bytes ab, selbst wenn sich nichts anderes geändert hat
- Der Code zwischen
OpenundSaveAsdarf Chart-Properties frei lesen, aber jeder Setter, der das Modell ändert, ändert den Fingerprint und schickt das Chart durch den Merge, was korrekt ist und außerhalb dessen liegt, was dieser Test misst - Chart-Parts werden beim Save aus einem arbeitsmappenweiten Zähler neu nummeriert, also legt eine Mappe, deren Sheet- oder Chart-Reihenfolge sich geändert hat, identische Bytes unter einem anderen
chartN.xml-Namen ab; ein Checker für diesen Fall folgt den Drawing-Beziehungen, statt Namen zu matchen
Anker brauchen einen Vergleich nach Werten. Der Drawing-Part wird um die Charts, die er hält, neu geschrieben, also sind seine Bytes das falsche Diff-Objekt; beide Dateien neu zu öffnen und FromOffsetXEMU bis ToOffsetYEMU Chart für Chart zu vergleichen erwischt einen Writer, der 247650 in 0 verwandelt. Ein struktureller Check, der Worksheet-, Drawing- und Chart-Beziehungen durchläuft und bei jeder duplizierten, verwaisten oder hängenden Chart-Referenz fehlschlägt, erwischt den Drei-Parts-Fall, bevor der Byte-Vergleich überhaupt läuft
Das alles läuft in nativem Delphi- und C++Builder-Code ohne Excel-Installation auf der Maschine, also ist die Mappe, die aus einem Batch-Job zurückkommt, der einzige Beweis dafür, was mit ihren Charts passiert ist. Lizenzierung, die volle Chart-API und ein Trial-Build finden sich auf der Seite zur HotXLS Delphi spreadsheet component