Teknik Makale

HotXLS grafiklerinde parmak izi zamanlaması ve anchor offset

İki grafikli bir çalışma kitabı, hiçbir şey düzenlemeyen bir aç-kaydet işleminden geçer ve ucundan üç farklı şey çıkabilir. Bir çıktıda kaynak iki grafik parçası yerine üçüne sahiptir. Bir diğerinde hâlâ ikisidir ama bayt karşılaştırması, her xl/charts/chartN.xml dosyasının yeniden yazıldığını ve seri renklerinin kaydığını gösterir. Üçüncüsü grafik XML’i olduğu gibi tutarken her grafik birkaç piksel kayarak hücre çizgilerine oturmuştur. Excel üçünü de onarım sorusu sormadan açar. Her belirti, yükleme ve kaydetme yolundaki tek bir karardan gelir: bir worksheet çizimini nasıl bulur, grafiğin değişiklik parmak izi ne zaman alınır ve çizim parçasının içindeki anchor offset değerleri geriye yazılıyor mu

Bir worksheet çizimini relationship’leri üzerinden bulur

Üç parçalı belirti, parça adlarını tahmin eden bir yükleyiciden gelir. Bir XLSX paketinde worksheet, çizimini <drawing r:id="…"/> öğesiyle işaret eder (ECMA-376 Bölüm 1 §18.3.1.36) ve r:id, sayfanın .rels parçası üzerinden ../drawings/drawing1.xml gibi bir hedefe çözülür. Excel, çizimleri öngörülebilir bir biçimde numaralandırır; bu yüzden elle yazılmış bir okuyucuda çoğunlukla bir yedek yol büyür: relationship yoksa, sayfa konumu n için xl/drawings/drawing{n}.xml denenir ve arşivde varsa o kullanılır

// Elle yazılmış XLSX okuyucularında görülen bir kısayol (bunu göndermeyin)
function GuessDrawingPart(Zip: TZipFile; SheetPos: Integer;
  const RelTarget: string): string;
begin
  Result := RelTarget;   // sayfanın .rels dosyasından çözülür, yokluğunda ''
  if Result = '' then
  begin
    Result := 'xl/drawings/drawing' + IntToStr(SheetPos + 1) + '.xml';
    if Zip.IndexOf(Result) < 0 then
      Result := '';
  end;
end;

Tahmin, grafiği olmayan bir sayfa başa gelene dek ayakta kalır. İlk sayfasında ne çizim ne de hiç bir .rels parçası olmayan, ikinci sayfasının ise Target="../drawings/drawing1.xml" üzerinden xl/drawings/drawing1.xml dosyasına ulaştığı bir çalışma kitabında yedek yol, 1. sayfaya hiç referans vermediği bir çizim verir. chart1.xml iki kez, sayfa başına bir kez ayrıştırılır ve kaydetme üç grafik parçası yazar. Bir OPC paketinde parça adları, relationship grafiğinin onlara atadığı anlamın ötesinde hiçbir anlam taşımaz; HotXLS’te OPC relationship çözümlemesi yürüyüşünün tüm argümanı da budur

İki grafikli bir çalışma kitabında bir worksheet’in çizimini nasıl bulduğu: Sheet1’de ne çizim relationship’i ne de rels parçası vardırken Sheet2, çizim relationship’i üzerinden xl/drawings/drawing1.xml dosyasına ulaşır; o geleneksel adı sayfa konumundan tahmin eden bir yükleyici chart1.xml’i iki kez ayrıştırıp üç grafik parçası kaydeder ve HotXLS bu yüzden bir çizimi yalnızca sayfanın çizim relationship’i üzerinden yükler
Sheet1 hiçbir grafiğe referans vermediği için relationship grafiği, çizim hedefinin tek güvenli kaynağıdır ve tahmin edilen geleneksel bir ad, iki grafikli bir çalışma kitabını üç parçalı bir kayda dönüştürür

HotXLS Delphi Component katı okumayı benimser. Bir worksheet’in çizimi yalnızca o sayfanın çizim relationship’inin hedefinden yüklenir ve böyle bir relationship’i olmayan sayfa, xl/drawings/ dizininde hangi dosyalar olursa olsun hiç çizim almaz. Hiçbir şeyin referans vermediği bir çizim parçası hiçbir sayfaya bağlanmaz

Dokunulmamış bir grafik kopyalanır mı yeniden mi üretilir, bunu ne belirler?

Kararı grafik başına bir parmak izi verir. PreserveUnsupportedParts, Open anında True ise (varsayılan True’dur), HotXLS her grafik parçasının orijinal baytlarını ayrıştırılmış grafik modelinin yanında tutar. Ardından modeli kendi grafik yazıcısıyla serileştirir ve bu çıktı için iki sayı kaydeder: uzunluğu ve UTF-16 kod birimleri üzerinden 64 bitlik bir FNV-1a hash’i; standart offset basis 14695981039346656037 ve asal sayı 1099511628211 kullanılır. TXLSXChart.HasPreservedXml, bir grafiğin bu orijinal baytları taşıyıp taşımadığını bildirir

Kaydetme zamanında grafik modeli bir kez daha serileştirilir ve iki sayı karşılaştırılır. Eşit uzunluk ve eşit hash, modelin içe aktarma anındaki hâliyle birebir aynı olduğu anlamına gelir; uygulamanın değiştirebileceği hiçbir şey değişmemiştir. Bu durumda, StrictOOXML kapalıyken yazıcı, sıkıştırılmış girdiyi kaynak arşivden çıkışa grafiğin yeni parça adı altında doğrudan kopyalar; baytlar hiç çözülmez ya da yeniden deflate edilmez. Sayılardan biri farklıysa bunun yerine orijinal XML, modelle birleştirilir. Bir hata çubuğunun ya da trend çizgisinin çizgi biçimlendirmesini değiştirmek de serileştirilmiş modelin parçası olmasa da bir düzenleme sayılır

HotXLS grafik parmak izini içe aktarmada alır: grafik parçasının orijinal baytlarını tutar, grafik modelinin kendi serileştirmesinin uzunluğunu ve 64 bitlik FNV-1a hash’ini kaydeder; kaydetme zamanında her iki değeri yeniden üretip 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 orijinal XML’i modelle birleştirir
Bir parmak izi ancak alındığı an kadar güvenilirdir; her kurtarma geçişi bitmeden alınması, tamamlanmış modelle bir daha asla eşleşmeyecek bir hash garantiler

Birleştirme, birisinin düzenlediği grafik için doğru işlemdir. Modelin kapsamadığı orijinal XML parçalarını tutar, seriler, eksenler ve plot grupları için modelin kazanmasını sağlar ve sonucu yeniden serileştirir. Yapamadığı şey orijinal baytları yeniden üretmektir. ChartML kaybetmeden Excel grafiklerini düzenleme notu birleştirmenin neleri koruduğunu ele alır; bu makale ise dokunulmamış bir grafiğin ona hiç ulaşmaması koşullarını konu alır

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;  // varsayılan; Open öncesinde geçerli olmalı
    Book.StrictOOXML := False;              // Strict çıktı her grafiği yeniden üretir
    if Book.Open(Src) <> 1 then
      raise Exception.Create('Cannot open ' + Src);
    for S := 1 to Book.Sheets.Count do      // Sheets[] 1 tabanlıdır
    begin
      Sheet := Book.Sheets[S];
      for C := 0 to Sheet.Charts.Count - 1 do  // Charts[] 0 tabanlıdır
        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;

Bir grafiğin modelden bilinçli olarak yeniden üretilmesi gerektiğinde, örneğin kaynağın taşıdığı biçimlendirmeyi atmak için, TXLSXChart.ClearPreservedXml saklanan baytları düşer ve o grafik yalnızca modelden yazılır

Peki her grafik neden yine de birleştirme yolunu izler?

Çünkü bir adım erken alınan bir parmak izi asla eşleşmez. Grafik XML’i nadiren tek geçişte okunur. Bir akış ayrıştırması serileri, eksenleri ve plot gruplarını kurar; ardından bir dizi izleyen geçiş, seriler var olduğunda geri kazanması kolay olan ayrıntıları ham grafik metninden okur: her <c:ser> üzerindeki <c:smooth> bayrağı, marker fill ve marker line’ın srgbClr değerleri, eksen kesişim modları ve major ile minor tick-mark stilleri. Akış geçişi çalışırken seri koleksiyonu henüz yoktur; bu yüzden o geçişlerin ondan sonra gelmesi gerekir

Parmak izinin, izleyen geçişler çalışmadan akış geçişinin sonunda alındığını varsayın. Bu durumda iz; smooth bayrağı olmayan, marker rengi olmayan ve varsayılan tick mark’lı bir modeli tarif eder. Kaydetme zamanında serileştirici tamamlanmış model üzerinde çalışır ve <c:smooth val="1"/> ile geri kazanılan renkleri üretir. XML uzar, hash farklılaşır ve korunan her çalışma kitabındaki her grafik her kayıtta birleştirmeden geçer. Yumuşatılmış serili ve özel markerli bir çizgi grafikte yeniden üretilen <c:ser> düğümleri orijinallerinin yerine geçer; renk kayması tam da oradan gelir

HotXLS parmak izini yalnızca her kurtarma geçişi bittikten sonra alır; böylece hash, modeli uygulamanın onu ilk gördüğü hâliyle tarif eder. Aynı iki sayı, başarılı bir kayıttan sonra yeniden kurulur; bu kez çıktı dosyasından ve bu ikinci yakalama da tamamen ayrıştırılmış bir modelden başlar. Bir değişiklik algılama hash’i, modeli hâlâ değiştirebilecek son geçişten sonra alınmalıdır. Erken alınması, her kayıtta başarısız olan bir kontrol üretir

Anchor offset değerleri çizim parçasında yaşar

Izgara oturması belirtisinin grafik XML’le bir ilgisi yoktur. Bir grafiğin sayfada nerede durduğu çizim parçasında saklanır ve alışılmış biçim xdr:twoCellAnchor’dır (ECMA-376 Bölüm 1 §20.5): bir from köşesi ve bir to köşesi; her biri col, colOff, row ve rowOff taşır. Offset’ler English Metric Units cinsindendir: inç başına 914400, punto başına 12700; Excel ise bir grafik fareyle sürüklendiğinde ya da yeniden boyutlandırıldığında sıfırdan farklı değerler yazar. from köşesini sütun 0, satır 0’da rowOff 19049 ile ve to köşesini sütun 8, satır 15’te colOff 247650 ve rowOff 66674 ile bildiren bir dosyada grafik, 1. satırın tepesinin 1,5 punto altında başlar ve I sütununa çeyrek inçten biraz fazlasına kadar uzanır

Örnek bir grafiğin xdr:twoCellAnchor köşelerinin anatomisi: from col 0 ve rowOff 19049 tutarken to, inç başına 914400 EMU ile col 8, colOff 247650 ve rowOff 66674 tutar; sıfır offset üreten bir yazıcı grafikleri ızgaraya oturtacağı için HotXLS, içe aktarılan offset değerlerini kayıtta geriye yazar
Anchor, grafik parçasında değil çizim parçasında yaşar; bu yüzden offset yeniden oynatması parmak izinden bağımsızdır ve sadık bir round-trip ikisini de gerektirir

O dört sayıyı okuyup her köşe için <xdr:colOff>0</xdr:colOff> üreten bir yazıcı, her grafiği ızgara çizgilerine taşır. Grafik parçası bayt bayt yeniden oynayabilir ve grafik yine de kayar, çünkü ikisi farklı parçalarda yaşar. HotXLS yüklediği anchor türünü (twoCellAnchor, oneCellAnchor ya da absoluteAnchor; PhysicalAnchorKind olarak açığa çıkar) EMU değerleriyle birlikte tutar ve onları değiştirmeden geriye yazar. Offset’ler FromOffsetXEMU, FromOffsetYEMU, ToOffsetXEMU ve ToOffsetYEMU olarak okunabilir; arkalarındaki birim dönüşümleri HotXLS resim geometrisi ve EMU ölçekleme notunda işlenir

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]));
  // Çerçevenin tamamını tek çağrıda değiştir: satırlar ve sütunlar 1 tabanlı,
  // offset’ler hücre içinde EMU cinsindendir, son bağımsız değişken editAs davranışıdır
  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;

Amacınız taşımak ya da boyutlandırmak olduğunda SetTwoCellAnchor kullanın. İki hücreli bir grafikte FromRow, FromCol, ToRow ya da ToCol atamak yalnızca o hücre indeksini değiştirir ve offset’leri eski yerinde bırakır; bu küçük bir ayar için doğru, grafiği sıfırdan yerleştirmek için yanlış davranıştır. SetTwoCellAnchor çerçeveyi bir bütün olarak doğrular ve bitiş köşesi baştan önceye düşerse ya da offset’ler Excel’in EMU aralığının dışına çıkarsa ERangeError fırlatır. AddChart ile oluşturulan bir grafik sıfır offset’lerle başlar; siz ona bir değer verene dek tam hücre sınırlarının üzerinde durur

Round-trip’i bayt karşılaştırmasıyla kanıtlamak

Çıktıyı Excel’de açmak burada pek bir şey kanıtlamaz. Excel yüklemede o kadar çok şeyi normalize eder ki yeniden serileştirilmiş bir grafik, biri marker renginin değiştiğini fark edene dek aynı görünür; birkaç puanlık ızgara oturması da ekranda gözden kaçması kolaydır. Otomatikleştirmeye değer kontrol, düzenlemesiz bir aç-kaydet sonrasında grafik parçalarını bayt bayt karşılaştırır ve bunun için RTL’nin TZipFile’ı yeter

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 kontroldeki bir uyuşmazlığın bir şey ifade edip etmeyeceğini birkaç koşul belirler:

  • PreserveUnsupportedParts, Open öncesinde True olmalı; kapalıyken hiçbir orijinal bayt saklanmaz ve her grafik modelden yazılır
  • StrictOOXML False olmalı, çünkü Strict çıktı tasarım gereği grafikleri yeniden üretir. Dokunulmamış grafiklerin kayıttan sağ çıktığını vaat eden her işlem hattında kapalı bırakın
  • Strict OOXML ad alanlarıyla yazılmış bir kaynak paket, geçişli (transitional) bir kayıtta geçişli ad alanlarına çevrilir; başka hiçbir şey değişmese de grafik baytları farklılaşır
  • Open ile SaveAs arasındaki kod grafik özelliklerini serbestçe okuyabilir; ama modeli değiştiren her setter parmak izini değiştirir ve o grafiği birleştirmeye gönderir, bu doğrudur ve bu testin ölçtüğü şeyin dışındadır
  • Grafik parçaları kayıtta çalışma kitabı genelindeki bir sayaçtan yeniden numaralandırılır; böylece sayfa ya da grafik sırası değişen bir çalışma kitabı, birebir aynı baytları farklı bir chartN.xml adının altına koyar, bu durumu yakalayan bir denetçi adları eşlemek yerine çizim relationship’lerini izler

Anchor’lar değere göre karşılaştırma ister. Çizim parçası, barındırdığı grafiklerin çevresinde baştan yazılır; onun baytları karşılaştırılacak yanlış şeydir. Her iki dosyayı yeniden açıp FromOffsetXEMU’dan ToOffsetYEMU’ya kadar grafik grafik karşılaştırmak, 247650’yi 0’a çeviren bir yazıcıyı yakalar. Worksheet, drawing ve chart relationship’lerini dolaşan ve yinelenen, sahipsiz ya da sarkan her grafik referansında başarısız olan yapısal bir kontrol ise bayt karşılaştırması daha çalışmadan üç parçalı durumu yakalar

Bunların tamamı, makinede Excel kurulumu gerektirmeyen yerel Delphi ve C++Builder kodunda çalışır; böylece bir toplu işten dönen çalışma kitabı, grafiklerine olanların tek kanıtıdır. Lisanslama, tam grafik API’si ve deneme sürümü HotXLS Delphi spreadsheet component sayfasında bulunur