Un workbook con due grafici attraversa un open-and-save che non modifica nulla, e tre cose diverse possono uscire dall'altra parte. Un output ha tre parti chart dove la sorgente ne aveva due. Un altro ne ha ancora due, ma un confronto byte per byte mostra che ogni xl/charts/chartN.xml è stato riscritto e i colori delle serie hanno preso una deriva. Un terzo conserva l'XML dei grafici intatto mentre ogni grafico è saltato di qualche pixel sulle linee della griglia delle celle. Excel apre tutti e tre senza prompt di riparazione. Ogni sintomo viene da una decisione nel percorso di caricamento e salvataggio: come un foglio di lavoro trova il suo drawing, quando viene preso il fingerprint delle modifiche del grafico, e se gli offset di anchor dentro la parte drawing vengono riscritti
Un foglio di lavoro trova il suo drawing attraverso le relazioni
Il sintomo delle tre parti viene da un loader che tira a indovinare i nomi delle parti. In un pacchetto XLSX un foglio di lavoro punta al suo drawing con un elemento <drawing r:id="…"/> (ECMA-376 Part 1 §18.3.1.36), e l'r:id si risolve attraverso la parte .rels del foglio verso un target come ../drawings/drawing1.xml. Excel per abitudine numera i drawing in modo prevedibile, così un reader scritto a mano spesso sviluppa un fallback: quando la relazione manca, prova xl/drawings/drawing{n}.xml per la posizione n del foglio e lo usa se l'archivio ce l'ha
// Una scorciatoia che compare nei reader XLSX scritti a mano (non speditela)
function GuessDrawingPart(Zip: TZipFile; SheetPos: Integer;
const RelTarget: string): string;
begin
Result := RelTarget; // risolta dal .rels del foglio, '' quando assente
if Result = '' then
begin
Result := 'xl/drawings/drawing' + IntToStr(SheetPos + 1) + '.xml';
if Zip.IndexOf(Result) < 0 then
Result := '';
end;
end;
L'indovinello regge finché arriva prima un foglio senza grafici. In un workbook il cui primo foglio non ha né drawing né parte .rels, mentre il secondo foglio raggiunge xl/drawings/drawing1.xml tramite Target="../drawings/drawing1.xml", il fallback consegna al foglio 1 un drawing che non ha mai referenziato. chart1.xml viene analizzato due volte, una per foglio, e il salvataggio scrive tre parti chart. I nomi delle parti in un pacchetto OPC non hanno alcun significato oltre a quello che il grafo delle relazioni assegna loro, che è tutta la tesi della guida sulla risoluzione delle relazioni OPC in HotXLS
HotXLS Delphi Component prende la lettura rigorosa. Il drawing di un foglio di lavoro viene caricato solo dal target della relazione di drawing di quel foglio, e un foglio senza relazione non riceve alcun drawing, qualunque file si trovi in xl/drawings/. Una parte drawing che nessuno referenzia non viene mai attaccata a un foglio
Che cosa decide se un grafico intatto viene copiato o ricostruito?
Lo decide un fingerprint per grafico. Quando PreserveUnsupportedParts vale True al momento dell'Open (True è il valore predefinito), HotXLS conserva i byte originali di ogni parte chart accanto al modello di grafico analizzato. Poi serializza il modello con il proprio writer dei grafici e registra due numeri per quell'output: la sua lunghezza e un hash FNV-1a a 64 bit sulle sue unità di codice UTF-16, con l'offset basis standard 14695981039346656037 e il numero primo 1099511628211. TXLSXChart.HasPreservedXml riferisce se un grafico porta quei byte originali
Al momento del salvataggio il modello viene serializzato di nuovo ed entrambi i numeri vengono confrontati. Lunghezza uguale e hash uguale significano che il modello è esattamente com'era all'importazione, quindi nulla di ciò che l'applicazione poteva cambiare è cambiato. In quel caso, con StrictOOXML disattivato, il writer copia la voce compressa dall'archivio sorgente direttamente nell'output sotto il nuovo nome della parte chart; i byte non vengono mai decodificati né ricompressi. Se uno dei due numeri differisce, l'XML originale viene invece fuso con il modello. Cambiare la formattazione della linea di una barra di errore o di una trendline conta anch'esso come modifica, anche se quella formattazione non fa parte del modello serializzato
Il merge è l'operazione giusta per un grafico modificato da qualcuno. Conserva le parti dell'XML originale che il modello non copre, dà la precedenza al modello per serie, assi e gruppi di plot, e riserializza il risultato. Quello che non può fare è riprodurre i byte originali. La nota sulla modifica dei grafici Excel senza perdere il ChartML copre ciò che il merge conserva; questo articolo riguarda le condizioni nelle quali un grafico non modificato non ci arriva mai
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; // il valore predefinito; deve valere prima di Open
Book.StrictOOXML := False; // l'output Strict rigenera ogni grafico
if Book.Open(Src) <> 1 then
raise Exception.Create('Cannot open ' + Src);
for S := 1 to Book.Sheets.Count do // Sheets[] è 1-based
begin
Sheet := Book.Sheets[S];
for C := 0 to Sheet.Charts.Count - 1 do // Charts[] è 0-based
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;
Quando un grafico deve essere rigenerato dal modello di proposito, per esempio per eliminare formattazioni portate dalla sorgente, TXLSXChart.ClearPreservedXml scarta i byte conservati e quel grafico viene scritto dal solo modello
Perché mai ogni grafico prenderebbe il percorso di merge?
Perché un fingerprint preso un passaggio troppo presto non corrisponde mai. L'XML dei grafici raramente viene letto in un unico passaggio. Un parse in streaming costruisce serie, assi e gruppi di plot, e poi una serie di passaggi successivi legge dettagli dal testo grezzo del grafico che sono più facili da recuperare una volta che le serie esistono: il flag <c:smooth> su ogni <c:ser>, i valori srgbClr del riempimento e della linea del marker, le modalità di intersezione degli assi e gli stili dei tick mark maggiori e minori. La collezione delle serie semplicemente non esiste ancora mentre il passaggio in streaming gira, quindi quei passaggi devono venire dopo
Supponete che il fingerprint venga catturato alla fine del passaggio in streaming, prima che girino i passaggi successivi. Descrive allora un modello senza flag smooth, senza colori dei marker e con tick mark predefiniti. Al momento del salvataggio il serializzatore gira contro il modello completato ed emette <c:smooth val="1"/> più i colori recuperati. L'XML è più lungo, l'hash differisce, e ogni grafico di ogni workbook conservato passa per il merge a ogni salvataggio. Su un grafico a linee con serie smussate e marker personalizzati i nodi <c:ser> rigenerati sostituiscono gli originali, e da lì arriva la deriva dei colori
HotXLS cattura il fingerprint solo dopo che ogni passaggio di recupero è finito, così l'hash descrive il modello così come l'applicazione lo vede la prima volta. Gli stessi due numeri vengono ristabiliti dopo un salvataggio riuscito, questa volta dal file di output, e anche quella seconda cattura parte da un modello completamente analizzato. Un hash di rilevamento delle modifiche deve essere preso dopo l'ultimo passaggio che può ancora mutare il modello. Prenderlo prima produce un controllo che fallisce a ogni salvataggio
Gli offset di anchor vivono nella parte drawing
Il sintomo dell'aggancio alla griglia non c'entra nulla con l'XML dei grafici. La posizione di un grafico sul foglio è memorizzata nella parte drawing, e la forma consueta è xdr:twoCellAnchor (ECMA-376 Part 1 §20.5): un angolo from e un angolo to, ciascuno con col, colOff, row e rowOff. Gli offset sono English Metric Units, 914400 per pollice e 12700 per punto, ed Excel scrive valori non nulli ogniqualvolta un grafico viene trascinato o ridimensionato col mouse. Per un file che dichiara un angolo from a colonna 0, riga 0 con un rowOff di 19049, e un angolo to a colonna 8, riga 15 con un colOff di 247650 e un rowOff di 66674, il grafico inizia 1,5 punti sotto la cima della riga 1 e finisce poco più di un quarto di pollice dentro la colonna I
Un writer che legge quei quattro numeri e poi emette <xdr:colOff>0</xdr:colOff> per ogni angolo sposta ogni grafico sulle linee della griglia. La parte chart può riprodursi byte per byte e il grafico si sposta comunque, perché i due vivono in parti diverse. HotXLS conserva il tipo di anchor caricato (twoCellAnchor, oneCellAnchor o absoluteAnchor, esposto come PhysicalAnchorKind) insieme ai suoi valori EMU, e li riscrive invariati. Gli offset si leggono come FromOffsetXEMU, FromOffsetYEMU, ToOffsetXEMU e ToOffsetYEMU, e le conversioni di unità alle spalle sono sviluppate nella nota sulla geometria delle immagini e lo scaling EMU di HotXLS
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]));
// Sostituisce l'intero frame in una chiamata: righe e colonne sono 1-based,
// gli offset sono EMU dentro la cella, l'ultimo argomento è il comportamento 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;
Usate SetTwoCellAnchor quando l'intenzione è spostare o ridimensionare. Assegnare FromRow, FromCol, ToRow o ToCol su un grafico a due celle cambia solo quell'indice di cella e lascia gli offset dove sono, che è il comportamento giusto per una piccola correzione e quello sbagliato per piazzare un grafico da zero. SetTwoCellAnchor valida il frame nel suo complesso e solleva ERangeError quando l'angolo finale precede quello iniziale o gli offset escono dall'intervallo EMU di Excel. Un grafico creato con AddChart parte con offset a zero, quindi sta esattamente sui bordi delle celle finché non gliene date di propri
Dimostrare il round trip con un confronto byte per byte
Aprire l'output in Excel dimostra poco qui. Excel normalizza abbastanza al caricamento che un grafico riserializzato sembra identico finché qualcuno non nota che il colore di un marker è cambiato, e un aggancio alla griglia di pochi punti è facile da perdere sullo schermo. Il controllo che vale la pena automatizzare confronta le parti chart byte per byte dopo un open-and-save senza modifiche, e il TZipFile della RTL è tutto ciò che serve
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); // solleva un'eccezione se la parte è sparita
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;
Alcune condizioni decidono se una differenza in quel controllo significa qualcosa:
PreserveUnsupportedPartsdeve valere True prima dell'Open; con esso disattivato nessun byte originale viene conservato e ogni grafico viene scritto dal modelloStrictOOXMLdeve valere False, perché l'output Strict rigenera volutamente i grafici. Lasciatelo disattivato in ogni pipeline che promette che grafici intatti sopravvivono a un salvataggio- Un pacchetto sorgente scritto con namespace Strict OOXML viene convertito ai namespace transitional in un salvataggio transitional, quindi i byte dei suoi grafici differiscono anche quando tutto il resto è invariato
- Il codice fra
OpeneSaveAspuò leggere liberamente le proprietà dei grafici, ma qualsiasi setter che cambia il modello cambia il fingerprint e manda quel grafico nel merge, il che è corretto e resta fuori da ciò che questo test misura - Le parti chart vengono rinumerate da un contatore a livello di workbook al salvataggio, quindi un workbook il cui ordine di fogli o grafici è cambiato mette byte identici sotto un nome
chartN.xmldiverso; un checker per quel caso segue le relazioni dei drawing invece di confrontare i nomi
Gli anchor richiedono un confronto per valore. La parte drawing viene scritta da zero attorno ai grafici che contiene, quindi i suoi byte sono la cosa sbagliata da confrontare; riaprire entrambi i file e confrontare FromOffsetXEMU fino a ToOffsetYEMU grafico per grafico becca un writer che trasforma 247650 in 0. Un controllo strutturale che percorre le relazioni fra foglio di lavoro, drawing e chart e fallisce su qualsiasi riferimento chart duplicato, orfano o pendente becca il caso delle tre parti prima ancora che il confronto byte per byte parta
Tutto questo gira in codice nativo Delphi e C++Builder senza installazione di Excel sulla macchina, quindi il workbook che torna da un job batch è l'unica prova di ciò che è successo ai suoi grafici. Licenze, l'API completa dei grafici e una build di prova sono sulla pagina del componente foglio di calcolo Delphi HotXLS