Teknik Makale

BIFF8 Font Index 4'ü Atlar: HotXLS'te Zengin Metin Run'ları

HotXLS her BIFF8 font referansını [MS-XLS] §2.5.129 FontIndex'in tanımladığı gibi numaralar: 0 ile 3 arasındaki değerler 0 tabanlıdır, 4'ten büyük değerler 1 tabanlıdır ve 4 hiç görünmez; böylece beşinci FONT kaydı ifnt 5 olur ve geçerli en büyük ifnt, FONT kaydı sayısına eşittir. HotXLS 2.384.4'ten beri XF yazıcısı, XF okuyucusu, zengin metin dizesi çalıştırmaları ve çalışma kitapları arası çalıştırma göçü bu kuralı izler; 2.384.5 ve 2.384.6 ise onu yorum ve metin kutusu çalıştırmalarına, kopyalar ve satır eklemeleri dahil olmak üzere genişletir

Kural, üstüne çarpana dek yazım hatası gibi görünür. Biri sekiz FONT kayıtlı bir çalışma kitabı açar, font 8'i gösteren bir XF bulur ve yazıcının aralık dışı bir indeks ürettiği sonucuna varır. Tam da bu akıl yürütme HotXLS 2.384.1'de bir "düzeltme" olarak çıktı ve doğru bir uygulamayı, Excel'in açtığı dosyadaki her özel fontun bir yuva erken düştüğü bir uygulamaya dönüştürdü. İlginç olan off-by-one'ın kendisi değil; bir BIFF8 kütüphanesinde aynı kuralı kaç yerin taşıdığı ve bir font bağının bir kaydetmeyi atlarken ikincisinde nasıl bozulduğudur. BIFF8 XLUnicodeString cch ve fHigh çözümleme makalesindeki uzunluk ve kodlama tuhaflıklarıyla boğuştuysanız, bu aynı aileden bir hatadır: dosya sağlamdır, aritmetik değildir

[MS-XLS] FontIndex kuralı aslında ne der?

[MS-XLS] §2.5.129, 4'ten küçük bir FontIndex'in 0 tabanlı bir kayıt konumu, 4'ten büyük bir FontIndex'in 1 tabanlı bir kayıt konumu olduğunu ve 4 değerinin KULLANILMAMASI gerektiğini söyler. Aynı FontIndex türünü XF kayıtları, SST biçimlendirme çalıştırmaları ve TXO biçimlendirme çalıştırmaları kullanır; yanlış okunan tek kural üçünü birden bozar. Kanıt, Excel'in yazdığı dosyalarla kolayca yeniden üretilir: Office ile gelen SOLVSAMP.XLS 19 FONT kaydı ve en çok 19 olan XF ifnt'i taşır, 43 kayıtlı bir çalışma kitabı 43'te tavan yapar ve 30 FONT kaydıyla Excel 16'nın kaydettiği bir dosya Courier New hücrelerini 22. kayıt olan ifnt 22'ye gösterir. Hiçbiri bir 4 içermez. Tanı aracında eşlemeyi kendiniz analiz etmeniz gerekirse dönüşüm iki kısa fonksiyondur

// [MS-XLS] 2.5.129 FontIndex: 0..3 0 tabanlı, > 4 1 tabanlı, 4 geçersiz
function FontIndexToRecordNo(Ifnt: Word): Integer;  // 1 tabanlı FONT kaydı
begin
  if Ifnt < 4 then
    Result := Ifnt + 1
  else if Ifnt > 4 then
    Result := Ifnt
  else
    Result := -1;  // 4 oluşmamalı
end;

function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
  if RecordNo <= 4 then
    Result := RecordNo - 1
  else
    Result := RecordNo;
end;
HotXLS FontIndex eşlemesi, MS-XLS 2.5.129 uyarınca ifnt 0-3 arası 0 tabanlı FONT kayıt konumları, ifnt 5 ve üzeri 1 tabanlıdır ve 4 değeri hiç oluşmaz; FontIndexToRecordNo dönüşümü ve SOLVSAMP.XLS gibi Excel kaynaklı çalışma kitaplarından kanıt
Beşinci FONT kaydı ifnt 4 değil ifnt 5'tir — 19 kayıtlı bir çalışma kitabı ifnt 19'da tavan yapar ve hiçbir Excel kaynaklı dosya aradaki yasak değeri asla saklamaz

HotXLS'in içinde aynı kural iki ayna konumda yaşar. TXLSFontList.GetSaveIndex, fontun referans verilen listedeki 1 tabanlı konumunu alır ve yalnızca 1 ile 4 arasındaki konumları eksiltir; böylece 5. konum ifnt 5 olarak yazılır. TXLSReader.ParseXF yüklerken tersini yapar: 5 ve üzeri her ifnt, 0 tabanlı bir font listesi yuvasına ekseltilir ve altındakiler yerinde kalır. SST zengin çalıştırma yeniden eşlemesi ile CountRichRunFontRefs aynı ifnt >= 5 dönüşümünü uygular; asıl mesele de bu: tek kural, tüm tüketiciler

// TXLSFontList.GetSaveIndex (yazıcı tarafı)
Result := inherited GetSaveIndex(Index);   // 1 tabanlı referans konumu
if (Result > 0) and (Result < 5) then
  Dec(Result);                             // 1..4 0..3 olur, 5+ değişmez

// TXLSReader.ParseXF (okuyucu tarafı)
fnti := Data.GetWord(0);
if fnti >= 5 then
  Dec(fnti);                               // ifnt 5, font listesi 4. yuvasıdır

0 tabanlı bir "düzeltme" her özel fontu neden bir kaydırdı?

HotXLS 2.384.1'deki 0 tabanlı yeniden yazım her özel fontu kaydırdı; çünkü 1 tabanlı bir indeksi 0 tabanlı okudu ve sonra dört çağrı noktasını bu yanlış okumaya uydurdu: GetSaveIndex, ParseXF, SST çalıştırma yeniden eşlemesi ve Sheets.AddCopy içindeki çalışma kitapları arası çalıştırma göçü. HotXLS gidiş dönüşleri hâlâ sağlam görünüyordu, çünkü yazıcı ile okuyıcı birbirine göre aynıydı. Excel aynı fikirde değildi. 2.384.1'in yazdığı bir dosya ilk özel fontu, Excel'in varsayılan font saydığı ifnt 4'e koydu ve sonraki her özel fontu bir kayıt erken yazdı; bir Excel dosyası açmak tam ters yönde ilerledi ve her fontu bir kayıt geç bağladı

HotXLS 2.384.1 gerilemesi: GetSaveIndex ve ParseXF 1 tabanlı FontIndex değerlerini 0 tabanlı okuyup ilk özel fontu, Excel'in varsayılan fonta çözümlediği yasak ifnt 4 olarak yazdı ve sonraki her fontu bir kayıt erken düşürdü; gidiş dönüş testleri ise yine de geçiyordu
Yazıcı ve okuyucu aynı yanlış okumada anlaştı; kaydet-sonra-yeniden aç testi yeşil kaldı ve Excel'in açtığı her font bir yuva saptı — bir kural yedi yerde yaşarken dördünü değiştiriyorsanız, önce kendi değişikliğinizden şüphelenin

Değişikliği durdurması gereken ipucu aynı kod tabanında duruyordu. CountRichRunFontRefs, grafik FONTX ve FBI yeniden eşlemesi ile stil motoru font listesi hiç dokunulmamış ve hâlâ 4-atlamayı kullanıyordu; kütüphane 2.384.1 indiği anda kendiyle çelişti ve zengin metin fontlarının genellikle bir XF tarafından da referans verilmiş olması çelişkiyi gizledi. Bir kural yedi yerde görünüyorken dördünü değiştiriyorsanız, öteki üçünden önce kendi değişikliğinizden şüphelenin. 2.384.4 sürümü spesifikasyon numaralandırmasını dört yerde de geri getirdi ve ifnt < FontCount assert ederek yanlış okumuş olan eski regresyon testinin yerine, yazılan her ifnt'i spesifikasyon formülü üzerinden bir FONT kayıt adına eşleyen testler geçti. Dürüst bir sınırlama kalır: 2.384.1 ile 2.384.3 arasında beş ve üzeri fontla kaydedilen dosyalar, okuyucunun geçerli veriden ayırt edemediği kaymış indeksler taşır; tek çare onları yeniden üretmektir

Yorum font çalıştırmaları neden yalnızca ikinci kaydetmede bozulur?

Yorum ve metin kutusu çalıştırmaları ikinci kaydetmede bozuldu; çünkü HotXLS ilk N-1 FONT kaydını koşulsuz tutuyor ve hiçbir XF referans vermediğinde yalnızca sonuncuyu düşürüyor, TXO biçimlendirme çalıştırmaları ([MS-XLS] §2.4.329) ise yeniden numaralandırılmadan bayt bayt geri yazılıyordu. Excel'in yazdığı .xls dosyaları her zaman referanssız bir kuyruk fontuyla biter (Çince yerel ayarlı sistemde 9 punto DengXian); ilk kaydetmede yalnızca bir yorum çalıştırması tarafından kullanılan font hiçbir zaman sonuncu değildi ve görünürde hiçbir şey kıpırdamadı. Ama o ilk kaydetme kuyruk fontunu düşürdü ve yalnızca yorumda kullanılan fontu son konuma taşıdı. İkinci kaydetme onu referanssız diye attı, çalıştırmanın ifnt'i sonun ötesini gösterdi ve Excel varsayılan fonta döndü; arada çalışma kitabı yeni bir font kazandıysa çalıştırma sessizce ona bağlandı, ki testlerde biçimli bir metin kutusu çalıştırmasını Arial'e dönüştürdü. yorum ve köprü inceleme akışı kurma makalesinde anlatılan türden yorum yoğun dosyalar tam da bunun ısırığına uğrar; çünkü tekrar tekrar açılır, notlanır ve kaydedilirler

İki kaydetme boyunca HotXLS font tablosu: Excel'in her zaman yazdığı referanssız kuyruk FONT kaydı önce düşer, yalnızca yorumda kullanılan font sonuncu olur ve TXO biçimlendirme çalıştırmaları referans sayılmadan geri yazıldığı için sonra atılır; CountRichRunFontRefs hayatta kalma filtresini 2.384.5'te düzeltti
İlk kaydetme temiz görünüyordu çünkü kaybı kuyruk fontu üstlendi ve yorum fontu ikincide kayboldu — bellek içi teste güvenmek yerine her ifnt'i kaydetmeler boyunca bir FONT kayıt adına eşleyin

HotXLS 2.384.5, TXO çalıştırmalarına SST çalıştırmaları gibi davranır. CountRichRunFontRefs artık her çalışma sayfasındaki her TMSOShapeTextBox'ı gezer, her çalıştırmanın 4-atlayan ifnt'ini bir yuvaya çevirir ve onu bir referans sayar; böylece yalnızca çalıştırmanın kullandığı bir font kaydetme filtresini atlatır. Ortaya çıkan yuva-kaydet-indeksi tablosu her çizimin FontRunRemap'ine girer ve TMSOShapeTextBox.Store, çalıştırma indekslerini ham çalıştırma baytlarının özel bir kopyası üzerinde yeniden yazar; font taşımayan kuyruktaki TxOLastRun'a dokunmaz. Uygulama kodu için sözleşme basittir: TXLSComment.TextRuns.FontIndex ve TXLSTextBox.TextRuns.FontIndex dosya numaralandırmasını, 4 atlanmış olarak, tam okunduğu gibi kullanır; çalıştırma indeksleri 1 tabanlıdır ve CharIndex, çalıştırmanın başladığı karakter ofsetidir. Kaydetmeden sonra saklanan sayı sizin atadığınızdan farklı olabilir ama yine de aynı fontu gösterir

var
  Book: IXLSWorkbook;
  Note: TXLSComment;
  I: Integer;
  Ifnt: Word;
begin
  Book := TXLSWorkbook.Create;
  if Book.Open('review-notes.xls') <> 1 then
    raise Exception.Create('Cannot open review-notes.xls');
  Note := Book.Sheets[1].Range['C2', 'C2'].Comment;
  if Note <> nil then
    for I := 1 to Note.TextRuns.Count do
    begin
      Ifnt := Note.TextRuns.FontIndex[I];   // dosya numaralandırması, 4 atlanır
      if Ifnt = 4 then
        raise Exception.CreateFmt('Run %d uses invalid ifnt 4', [I]);
      Writeln(Format('run %d at char %d: ifnt %d = FONT record #%d',
        [I, Note.TextRuns.CharIndex[I], Ifnt, FontIndexToRecordNo(Ifnt)]));
    end;
end;

Kopyalar, satır eklemeleri ve çalışma kitapları arası çalıştırma göçü

HotXLS 2.384.6'dan beri her klasik motor kopya yolu yorum biçimlendirme çalıştırmalarını korur; çünkü Range.Copy, CopyRange, Sheets.AddCopy ve Range.Insert ile Range.Delete arkasındaki hücre kaydırmalarının hepsi TXLSRange.CopyCell'den geçer ve CopyCell eskiden yalnızca yorum metnini ve yazarını kopyalıyordu. Kaydırma, kopya artı temizlemedir; dolayısıyla iki çalıştırmalı bir notun üstüne tek satır eklemek onu sıfır çalıştırma ve tek fontla bırakıyordu. Düzeltme her çalıştırmayı kopyalar ve fontunu TXLSWorkbook.MigrateRunFontIndex üzerinden taşır; bu fonksiyon 4-atlayan indeksi bir yuvaya çevirir, fontu değere göre hedef font tablosuna göç ettirir ve dosya numaralandırmasına geri çevirir. Sheets.AddCopy içindeki SST zengin metin göçü artık aritmetiğin kendi kopyasını taşımak yerine aynı fonksiyonu çağırıyor. İki uç durum da geldi: kaynak ve hedef aynı yorum olan yerinde bir yapıştırma, çalıştırmalarını okumadan önce temizlememelidir ve Sheets.AddCopy artık saklanmış hücre kaydı olmayan hücrelere bağlı yorumlar için ikinci bir geçiş yapıyor; bunu eskiden tamamen atlıyordu. Çalışma kitapları arası kopyalamanın font tablosu tarafı, çalışma kitapları arası kopya ve formül yeniden bağlama makalesinde ele alınan formül tarafıyla aynı değere göre mantığı izler. XLSX motorunda kopya yolları çalıştırmaları hâlihazırda değere göre klonluyordu; eksik yorum parçasının kendisindeydi: okuyucu rFont, strike, u ve vertAlign'i yok sayıyor, yazıcı ise u ya da vertAlign hiç yaymıyordu; çalıştırmalar artık kaydetme ve yeniden açmada simetrik hayatta kalıyor

BIFF8 dosyalarında font indekslerini nasıl test etmelisiniz?

Font indekslerini kaydedip yeniden açarak, ideali birden fazla nesil boyunca, ve sayısal bir aralık assert etmek yerine her ifnt'i bir FONT kaydına eşleyerek test edin. Bu hikayedeki her hata bir bellek içi testten geçti: 2.384.1 gerilemesi eşleşmiş bir yazıcı-okuyıcı çiftinde yaşıyordu, TXO kayması arada font tablosu değişimiyle iki kaydetme gerektiriyordu ve XLSX'te kaybolan yorum çalıştırmaları ancak yeniden açıldıktan sonra ortaya çıktı. İşe yarar bir test düzeneği Excel'in yazdığı bir örneği açar, onu HotXLS üzerinden iki kez kaydeder, kaydetmeler arasında bir font ekler ya da çıkarır ve sonra çalıştırma konumlarını artı bayt düzeyinde her ifnt'in ardındaki font adlarını denetler. Kaydetme öncesi ve sonrası FontIndex değerlerini karşılaştırmayın; yeniden numaralandırma meşrudur

procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
  Book: IXLSWorkbook;
  Note: TXLSComment;
  RunCount: Integer;
  SecondRunAt: Word;
begin
  Book := TXLSWorkbook.Create;
  Assert(Book.Open(SrcFile) = 1);          // Excel kaynaklı, C2 iki çalıştırma taşır
  Note := Book.Sheets[1].Range['C2', 'C2'].Comment;
  RunCount := Note.TextRuns.Count;
  SecondRunAt := Note.TextRuns.CharIndex[2];

  Book.Sheets[1].Range['C2', 'C2'].Copy(Book.Sheets[1].Range['E5', 'E5']);
  Book.Sheets[1].Range['C1', 'C1'].Insert(xlShiftDown);   // C2, C3'e taşınır
  Assert(Book.SaveAs(OutFile) = 1);

  Book := TXLSWorkbook.Create;               // yeniden aç, belleğe asla güvenme
  Assert(Book.Open(OutFile) = 1);
  Note := Book.Sheets[1].Range['C3', 'C3'].Comment;
  Assert((Note <> nil) and (Note.TextRuns.Count = RunCount));
  Assert(Note.TextRuns.CharIndex[2] = SecondRunAt);
  Note := Book.Sheets[1].Range['E5', 'E5'].Comment;
  Assert((Note <> nil) and (Note.TextRuns.Count = RunCount));
end;

Delphi ya da C++Builder'dan klasik XLS okuyup yazıyor ve bir kütüphanenin çok sayıda font tüketicisinden hangisinin hâlâ [MS-XLS] §2.5.129'a uyduğunu izlemek istemiyorsanız, burada anlatılan 4-atlayan numaralandırma, kaydetmede çalıştırma yeniden numaralandırması ve değere göre çalıştırma göçü HotXLS Delphi elektronik tablo bileşenine gömülüdür; bileşen XLS ve XLSX'i Excel ya da OLE otomasyonu olmadan okur ve yazar