Teknik Makale

Delphi'de HotXLS Grafik Parmak İzi ve Anchor Offset'leri

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 two-charts örneğinde worksheet çizimlerini nasıl çözümler: Sheet1 hiç çizim relationship'i ve rels parçası taşımazken Sheet2 xl/drawings/drawing1.xml dosyasına ParPartTargets üzerinden ulaşır; 2.382.0 öncesi fallback bu geleneksel adı sayfa konumundan tahmin ettiği için chart1.xml iki kez ayrıştırıldı ve düzeltme çizimleri yalnızca XlsxRtDrawing üzerinden yükleyene kadar kayıtlar üç grafik parçası yazdı
Sheet1 hiçbir grafiğe referans vermediğinden çizim hedefinin tek güvenilir kaynağı relationship grafiğidir; tahmin edilen geleneksel bir ad ise iki grafikli bir workbook'u üç parçalı bir kayda dönüştürdü

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

HotXLS grafik parmak izini içe aktarmada alır: ham UTF-8 baytlarını FRawChartXml içinde tutarken BuildChartKnownXml FRawChartModelLength değerini ve bir FNV-1a hash üretir; kayıt sırasında XlsxChartRawModelUnchanged her iki değeri yeniden kurup karşılaştırır, eşleşme orijinal baytları yeniden oynatır ya da sıkıştırılmış girdiyi kopyalar, uyuşmazlık ise XlsxMergeChartXml yoluna düşer
Bir parmak izi ancak alındığı an kadar güvenilirdir; her kurtarma geçişi bitmeden alınan iz ise tamamlanmış modelle bir daha asla eşleşmeyen bir hash garanti eder
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

HotXLS örneğindeki ilk grafiğin xdr:twoCellAnchor köşelerinin anatomisi: from col 0 ve rowOff 19049 tutarken to col 8, colOff 247650 ve rowOff 66674 tutar (inçte 914400 EMU); sıfır offset üreten yazıcı, FFromColOff, FToColOff ve kardeşleri içe aktarılan değerleri yeniden oynatana kadar grafikleri ızgaraya oturtuyordu
Anchor grafik parçasında değil çizim parçasında yaşar, bu yüzden bu onarım parmak izi düzeltmesinden bağımsızdır; workbook gerçekten round-trip edene kadar ikisinin de yayınlanmış olması gerekiyordu
// 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