HotXLS Delphi Component, değiştirilmemiş bir Excel grafiğini ancak iki koşul birlikte sağlandığında bayt bayt yeniden oynatır: grafiğe tahmin edilen bir parça adıyla değil, worksheet çizim relationship'i üzerinden ulaşılmış olması ve 64 bitlik model parmak izinin grafik modeli ayrıştırmayı bitirdikten sonra alınmış olması. Sürüm 2.382.0 ilk koşulu düzeltti, 2.382.3 ise ikincisini düzeltti ve çizim yazıcısının o zamana kadar sıfıra sabitlediği sıfırdan farklı xdr:colOff ile xdr:rowOff anchor offset değerlerini round-trip etmeye başladı. Her iki kusur da tek bir yerel corpus vakasından, two-charts.xlsx dosyasından çıktı: önce yapısal bir assertion iki grafik parçasının üçe çıktığını gördü, sonra her xl/charts/chartN.xml dosyasının bayt karşılaştırması kimsenin dokunmadığı grafiklerin hâlâ yeniden yazıldığını gösterdi — ve iki sorun da ne bir istisna fırlattı ne de Excel'i şikâyet ettirdi, bu yüzden bu kadar uzun süre hayatta kaldılar
İki grafikli bir workbook neden üç grafik parçasıyla geri döndü?
Çünkü yükleyicinin tahmin yürüten bir fallback'i vardı. Bir worksheet'in .rels parçasında çizim relationship'i yoksa eski kod, çizimin geleneksel xl/drawings/drawing{i+1}.xml adında yaşadığını varsayıyordu — i burada sayfa konumudur — ve bu parça arşivde varsa onu iliştiriyordu. two-charts.xlsx içinde ilk sayfanın ne çizimi ne de bir .rels parçası var; xl/drawings/drawing1.xml ise gerçekten var, ikinci sayfaya ait ve o sayfa ona Target="../drawings/drawing1.xml" üzerinden ulaşıyor. Böylece Sheet1 hiç referans vermediği bir grafiği miras aldı, chart1.xml iki kez ayrıştırıldı ve kayıt workbook'u iki değil üç grafik parçasıyla yazdı
HotXLS v2.382.0'daki düzeltme tahmini tamamen kaldırdı. Bir worksheet çizimi artık yalnızca ParPartTargets[i].Values[XlsxRtDrawing] üzerinden, yani o sayfada çizim relationship tipi için kaydedilen hedef üzerinden yükleniyor ve böyle bir relationship'i olmayan sayfa hiç çizim almıyor. Biçimin talep ettiği davranış da bu: worksheet içindeki <drawing r:id="…"/> öğesi (ECMA-376 Part 1 §18.3.1.36) bir sayfa ile çizimi arasındaki tek bağdır ve bir OPC paketindeki parça adları, relationship grafiğinin onlara atadığı anlamın ötesinde hiçbir anlam taşımaz. Excel'in yazdığı arşivler geleneksel adları kullanır, kısayolun bu kadar uzun süre sırıtmadan geçmesinin nedeni de budur; HotXLS'te OPC relationship çözümlemesi yürüyüşü, tahmin çoğu zaman doğru çıksa bile bir parça adını tahmin etmenin neden asla güvenli olmadığını ele alıyor
// v2.382.0 öncesi: eksik çizim relationship'i bir tahmine düşüyordu
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if drawingName = '' then
drawingName := 'xl/drawings/drawing' + IntToStr(i + 1) + '.xml';
if zip.Exists(drawingName) then
LoadDrawing(zip, drawingName); // başka bir sayfaya ait olabilir
// v2.382.0'dan beri: ya relationship ya hiç
drawingName := ParPartTargets[i].Values[XlsxRtDrawing];
if (drawingName <> '') and zip.Exists(drawingName) then
LoadDrawing(zip, drawingName);
Grafik parmak izi neyi garanti ediyor?
Parmak izi, grafik başına kaydın orijinal parçayı kopyalayabileceğine mi yoksa onu yeniden üretmek zorunda olduğuna mı karar verir. İçe aktarma sırasında, Open öncesinde PreserveUnsupportedParts etkinleştirilmişse HotXLS her grafik parçasının ham UTF-8 baytlarını FRawChartXml içinde tutar, tipli modelin kendi serileştirmesini BuildChartKnownXml ile kurar ve bu serileştirmenin uzunluğunu FRawChartModelLength, hash'ini de FRawChartModelHash içinde saklar. Hash, üretilen XML'in UTF-16 kod birimleri üzerinden FNV-1a'dır; standart 64 bit offset basis 14695981039346656037 ve asal sayı 1099511628211 kullanılır. Kayıt sırasında XlsxChartRawModelUnchanged bilinen XML'i yeniden kurar ve uzunluk ile hash'i karşılaştırır; eşleşme, tipli modelin içe aktarma anındaki hâliyle birebir aynı olduğu anlamına gelir, yani uygulamanın değiştirebileceği hiçbir şey değişmemiştir
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 // hiçbir şey korunmadı
else if XlsxChartRawModelUnchanged(Chart, KnownXml) then
Result := XlsxDecodeChartUtf8(Chart.FRawChartXml) // birebir yeniden oynatma
else
Result := XlsxMergeChartXml(
XlsxDecodeChartUtf8(Chart.FRawChartXml), KnownXml); // yapısal birleştirme
end;
XLSX yazıcısı BuildChartXmlFromKnown'dan bir adım öteye gider. Model değişmemişse ve StrictOOXML kapalıysa, önce sıkıştırılmış girdiyi kaynak arşivden çıkış arşivine doğrudan, grafiğin yeni parça adı altında kopyalamayı dener; yani baytlar çözülüp yeniden deflate bile edilmez. Bu kopya mümkün değilse decode-or-merge yoluna düşer. Mekanizmanın kendisi — uzunluk artı hash, eşitse yeniden oynat, değilse birleştir — ChartML kaybetmeden Excel grafiklerini düzenleme notunda anlatılan mekanizmadır. Bu makale ise onun sessizce nasıl çalışmaz hâle geldiğini ele alıyor
Peki her grafik neden yine de merge yolunu tuttu?
Çünkü parmak izi bir çağrı erken alınmıştı. HotXLS'te grafik ayrıştırma, grafik parçası üzerinde bir SAX geçişi ve ardından ham metinden SAX handler'larının doğrudan modellemediği ayrıntıları çeken bir dizi kurtarma geçişinden oluşur: XlsxChartParseSeriesFlags her <c:ser> bloğunu <c:smooth> bayrağı ile marker fill ve marker line'ın srgbClr değerleri için okur, ardından kategori ve değer eksenleri için axis crossing modlarını ve major/minor tick-mark stillerini kurtarır. v2.382.3 öncesinde ParseChartXml sonundaki sıra şuydu: eksen gruplarını sınıflandır, bilinen XML'i kur, uzunluk ve hash'i al ve ancak ondan sonra XlsxChartParseSeriesFlags çalıştır. Yani parmak izi, hâlâ smooth bayrakları, marker renkleri ve tick mark'ları eksik olan bir modeli tarif ediyordu. Kayıt sırasında BuildChartKnownXml tamamlanmış model üzerinde çalıştı ve model artık <c:smooth val="1"/> ile kurtarılan marker renklerini de üretiyordu. Daha uzun XML, farklı hash, XlsxChartRawModelUnchanged False döndü ve grafik XlsxMergeChartXml yolundan geçti. Merge, birinin düzenlediği grafik için doğru bir işlemdir ama bayt koruyan bir işlem değildir: ağacı yeniden serileştirir ve seri, eksen ile plot grubu için tipli modelin kazanmasını sağlayan sahiplik kuralı, yeniden üretilen düğümlerin orijinallerin yerini alması demektir. Corpus koşusunda görünen sonuç, kimsenin düzenlemediği grafiklerde kayan seri renkleriydi — korunan her workbook'taki her grafik, her kayıtta, hiçbir yerde hiçbir tanılama olmadan
Onarım tek bir yeniden sıralama: XlsxChartParseSeriesFlags artık bilinen XML kurulmadan önce çalışıyor, böylece parmak izi modeli uygulamanın onu ilk gördüğü andaki hâliyle tarif ediyor. Ders grafiklerin ötesine geçiyor. Bir değişiklik algılama parmak izi ancak alındığı an kadar güvenilirdir ve güvenli an, modeli değiştirebilecek her geçiş bittikten sonraki andır. HotXLS'te aynı iki değer için ikinci bir yakalama noktası var: başarılı bir kayıttan sonra çıkış dosyasına karşı yeniden kurulan baseline. O nokta her zaman tamamen ayrıştırılmış bir model üzerinde çalışırdı; sıra dışı olan, içe aktarma anındaki noktaydı
Anchor offset değerleri nereye gitti?
Bir literal sıfıra. Çizim parçasındaki bir twoCellAnchor bir grafiği iki hücre arasına sabitler ve her köşe bir hücre indeksi ile o hücre içindeki bir offset taşır: from (ECMA-376 Part 1 §20.5.2.5) ve to (§20.5.2.32) ikisi de col, colOff (§20.5.2.4), row ve rowOff tutar. Offset'ler English Metric Units cinsindendir, inçte 914400; Excel ise bir grafik fareyle yerleştirildiğinde ya da yeniden boyutlandırıldığında — ki grafiklerin çoğu böyledir — sıfırdan farklı değerler yazar. two-charts.xlsx içindeki ilk grafik satır 0'da 19049 rowOff ile başlar ve sütun 8, satır 15'te 247650 colOff ve 66674 rowOff ile biter — son sütunun yaklaşık çeyrek inç içinde. HotXLS'teki çizim ayrıştırıcısı bu dört değeri her zaman okuyordu — resim kodu onları kullanıyordu — ama grafik yazıcısı her köşe için <xdr:colOff>0</xdr:colOff> ve <xdr:rowOff>0</xdr:rowOff> üretiyordu, yani kayıtta her grafiği hücre ızgarasına oturtuyordu
// v2.382.3'ten beri anchor yazıcısı içe aktarılan EMU offset'lerini yeniden oynatıyor
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 artık FFromColOff, FFromRowOff, FToColOff ve FToRowOff taşıyor; bunlar çizim ayrıştırıcısından doldurulur ve bir grafik atandığında diğer anchor durumuyla birlikte kopyalanır. Bilinçli olarak private: genel anchor yüzeyi hâlâ dört hücre koordinatı — FromRow, FromCol, ToRow ve ToCol — ve Delphi kodundan oluşturulan bir grafik eskisi gibi hücre sınırlarına oturur. Offset'ler hücre altı konumlandırmayı bir özellik olarak açmak için değil, round-trip'i sadık kılmak için var. Şunu da not edelim: bu düzeltme parmak izinden bağımsızdır, çünkü anchor grafik parçasında değil çizim parçasında yaşar; ChartML'i kusursuz yeniden oynayan bir grafik bile o olmadan ızgaraya atlardı. Bu EMU değerlerinin arkasındaki birim dönüşümleri HotXLS resim geometrisi ve EMU ölçekleme notunda ele alınıyor
Bir grafiğin değişmeden round-trip yaptığını nasıl kanıtlarsınız?
Baytları karşılaştırarak, sonucu Excel'de açarak değil. Excel yükleme sırasında o kadar çok şeyi onarır ve normalleştirir ki kayan bir grafik, bir analist marker renginin değiştiğini fark edene kadar sorunsuz görünür. Her iki kusuru da yakalayan corpus testi, düzenlemesiz bir aç-kaydet sonrasında üç şey yapar: worksheet, drawing ve chart relationship'lerini dolaşır ve yinelenen, sahipsiz ya da sarkan her grafik referansında başarısız olur; grafik tipi, seri formülleri ve anchor geometrisinden çıkarılan bir imzayı orijinal ile çıktı arasında karşılaştırır; two-charts.xlsx için de her xl/charts/chartN.xml dosyasını iki arşivden de okuyup baytların birebir aynı olmasını şart koşar. Aynı kontrolü Delphi'de RTL TZipFile ile yazmak kolaydır
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); // parça kaybolduysa hata fırlatır
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;
Bu karşılaştırmayı anlamlı kılan üç koşul var ve her biri unutulursa sessizce başarısız olur. PreserveUnsupportedParts Open öncesinde True olmalı, yoksa hiç ham bayt yakalanmaz ve her grafik modelden yeniden kurulur. StrictOOXML False olmalı, çünkü strict mod tasarım gereği yeniden üretmeye zorlar. Uygulama da açma ile kaydetme arasında grafiğe dokunmamalı — özellik okumakta sorun yok, ama tipli modeli değiştiren her setter parmak izini çevirir ve grafiği merge yoluna gönderir; bu doğru davranıştır ve bu testin konusu değildir. Grafik parçaları ayrıca kayıtta workbook genelinde bir sayaçtan yeniden numaralandırılır, yani sayfa sırası ya da grafik sırası değişen bir workbook birebir aynı baytları farklı bir chartN.xml adı altına koyar; corpus denetleyicisi bu yüzden adları değil relationship'leri takip eder
Her iki düzeltme de HotXLS 2.382.0 ve 2.382.3 ile yayınlandı ve Win32 ile Win64 üzerinde yerel corpus'a karşı doğrulandı; yeniden kaydedilen grafik örnekleri ayrıca bağımsız bir ofis paketiyle PDF'e render edilip orijinallerle sayfa sayfa karşılaştırıldı. HotXLS, XLSX grafiklerini Excel kurulumu gerektirmeden yerel Delphi ve C++Builder kodundan okur, düzenler ve yazar; bu sadakat düzeyini bir kütüphane sorumluluğu yapan da budur — HotXLS Delphi spreadsheet component sayfasında özellik listesi ve deneme indirmesi var