Artikel Teknis

Indeks Font BIFF8 Lewati 4: Rich Text Runs HotXLS di Delphi

HotXLS menomori setiap referensi font BIFF8 persis seperti yang didefinisikan [MS-XLS] §2.5.129 FontIndex: nilai 0 sampai 3 berbasis nol, nilai di atas 4 berbasis satu, dan 4 tidak pernah muncul, jadi record FONT kelima adalah ifnt 5 dan ifnt valid terbesar sama dengan jumlah record FONT. Sejak HotXLS 2.384.4, writer XF, reader XF, rich text string run, dan migrasi run lintas workbook semuanya mengikuti aturan itu, dan 2.384.5 serta 2.384.6 memperluasnya ke run comment dan text box, termasuk melewati copy dan insert baris

Aturan itu terlihat seperti typo sampai Anda menabraknya. Seseorang membuka workbook dengan delapan record FONT, menemukan sebuah XF yang menunjuk font 8, dan menyimpulkan writer menghasilkan indeks out-of-range. Penalaran persis itu ikut ter-shipping di HotXLS 2.384.1 sebagai "fix", dan mengubah implementasi yang benar menjadi versi di mana setiap font kustom di berkas yang dibuka Excel mendarat satu slot lebih awal. Bagian menariknya bukan off-by-one-nya, melainkan berapa banyak tempat di sebuah library BIFF8 yang membawa konvensi yang sama, dan bagaimana sebuah binding font bisa selamat dari satu save lalu patah di save kedua. Kalau Anda sudah pernah berkelahi dengan quirk panjang dan encoding yang dibahas di decoding cch dan fHigh XLUnicodeString BIFF8, ini satu keluarga bug yang sama: berkasnya baik-baik saja, aritmetikanya yang tidak

Apa isi sebenarnya aturan FontIndex [MS-XLS]?

[MS-XLS] §2.5.129 menyatakan FontIndex di bawah 4 adalah posisi record berbasis nol, FontIndex di atas 4 adalah posisi record berbasis satu, dan nilai 4 TIDAK BOLEH dipakai. Tipe FontIndex yang sama dipakai record XF, SST formatting run, dan TXO formatting run, jadi satu aturan yang salah baca merusak ketiganya. Buktilah mudah direproduksi dengan berkas buatan Excel: SOLVSAMP.XLS bawaan Office punya 19 record FONT dan ifnt XF maksimum 19, workbook 43 record mentok di 43, dan berkas yang disimpan Excel 16 dengan 30 record FONT menunjuk sel-sel Courier New-nya ke ifnt 22, record ke-22. Tidak satu pun pernah memuat angka 4. Kalau Anda perlu menganalisis pemetaan sendiri di sebuah tool diagnostik, konversinya cuma dua fungsi pendek

// [MS-XLS] 2.5.129 FontIndex: 0..3 berbasis nol, > 4 berbasis satu, 4 invalid
function FontIndexToRecordNo(Ifnt: Word): Integer;  // record FONT berbasis 1
begin
  if Ifnt < 4 then
    Result := Ifnt + 1
  else if Ifnt > 4 then
    Result := Ifnt
  else
    Result := -1;  // 4 tidak boleh muncul
end;

function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
  if RecordNo <= 4 then
    Result := RecordNo - 1
  else
    Result := RecordNo;
end;
Pemetaan FontIndex HotXLS per MS-XLS 2.5.129 di mana ifnt 0 sampai 3 adalah posisi record FONT berbasis nol, ifnt 5 ke atas berbasis satu dan nilai 4 tidak pernah muncul, dengan konversi FontIndexToRecordNo dan bukti dari workbook buatan Excel seperti SOLVSAMP.XLS
Record FONT kelima adalah ifnt 5, bukan 4 — workbook 19 record mentok di ifnt 19, dan tidak ada berkas buatan Excel yang menyimpan nilai terlarang di sela-selanya

Di dalam HotXLS aturan yang sama tinggal di dua titik yang saling mencerminkan. TXLSFontList.GetSaveIndex mengambil posisi berbasis 1 sebuah font di list yang dirujuk dan hanya mengurangi posisi 1 sampai 4, jadi posisi 5 ditulis sebagai ifnt 5. TXLSReader.ParseXF melakukan sebaliknya saat load: ifnt 5 atau lebih dikurangi jadi slot font-list berbasis nol, dan yang di bawahnya dibiarkan. SST rich-run remap dan CountRichRunFontRefs menerapkan konversi ifnt >= 5 yang sama, dan itulah intinya: satu konvensi, semua konsumen

// TXLSFontList.GetSaveIndex (sisi writer)
Result := inherited GetSaveIndex(Index);   // posisi rujukan berbasis 1
if (Result > 0) and (Result < 5) then
  Dec(Result);                             // 1..4 menjadi 0..3, 5+ tak berubah

// TXLSReader.ParseXF (sisi reader)
fnti := Data.GetWord(0);
if fnti >= 5 then
  Dec(fnti);                               // ifnt 5 adalah slot 4 font-list

Mengapa "fix" berbasis nol menggeser setiap font kustom satu tempat?

Rewrite berbasis nol di HotXLS 2.384.1 menggeser setiap font kustom karena membaca indeks berbasis satu sebagai berbasis nol lalu mengubah empat call site untuk mengikuti salah baca itu: GetSaveIndex, ParseXF, SST run remap, dan migrasi run lintas workbook di Sheets.AddCopy. Round trip HotXLS masih terlihat baik, karena writer dan reader sepakat satu sama lain. Excel tidak sepakat. Berkas yang ditulis 2.384.1 menaruh font kustom pertama di ifnt 4, yang Excel perlakukan sebagai font default, dan setiap font kustom berikutnya satu record lebih awal; membuka berkas Excel berjalan ke arah sebaliknya, mengikat tiap font satu record terlambat

Regresi HotXLS 2.384.1 di mana GetSaveIndex dan ParseXF membaca nilai FontIndex berbasis satu sebagai berbasis nol, menulis font kustom pertama sebagai ifnt 4 yang terlarang yang Excel resolusikan ke font default dan mendaratkan setiap font berikutnya satu record lebih awal sementara round trip tetap lolos
Writer dan reader sepakat pada salah baca yang sama, jadi tes save-lalu-reopen tetap hijau sementara setiap font yang dibuka Excel mendarat satu slot meleset — saat satu konvensi hidup di tujuh tempat dan Anda mengubah empat, curigai perubahan Anda lebih dulu

Petunjuk yang seharusnya menghentikan perubahan itu ada di codebase yang sama. CountRichRunFontRefs, remap FONTX dan FBI chart, serta list font engine style tidak pernah disentuh dan tetap memakai skip-4, jadi library itu kontradiksi dengan dirinya sendiri begitu 2.384.1 mendarat, dan hanya kebetulan bahwa font rich-text biasanya juga dirujuk beberapa XF yang menyembunyikan kontradiksinya. Saat satu konvensi muncul di tujuh tempat dan Anda mengubah empat, curigai perubahan Anda sebelum mencurigai tiga sisanya. Versi 2.384.4 memulihkan penomoran spec di keempat tempat itu, dan tes regresi lama yang meng-assert ifnt < FontCount dan karenanya mengekodekan salah baca itu diganti tes yang memetakan tiap ifnt yang ditulis kembali ke nama record FONT lewat formula spec. Satu keterbatasan yang jujur tetap ada: berkas yang disimpan 2.384.1 sampai 2.384.3 dengan lima font atau lebih membawa indeks tergeser yang tak bisa dibedakan reader dari data valid, jadi satu-satunya obat adalah meregenerasinya

Mengapa run font comment patah hanya di save kedua?

Run comment dan text box patah di save kedua karena HotXLS menyimpan N-1 record FONT pertama tanpa syarat dan hanya menjatuhkan yang terakhir kalau tidak ada XF yang merujuknya, sementara TXO formatting run ([MS-XLS] §2.4.329) ditulis kembali byte demi byte tanpa penomoran ulang. Berkas .xls buatan Excel selalu diakhiri font ekor yang tak dirujuk (DengXian 9pt di sistem locale China), jadi di save pertama font yang hanya dipakai sebuah run comment tidak pernah terakhir, dan tidak ada yang bergeser kelihatan. Tapi save pertama itu menjatuhkan font ekornya, dan mempromosikan font khusus comment ke posisi terakhir. Save kedua lalu membuangnya sebagai tak dirujuk, ifnt milik run menunjuk melewati ujung, dan Excel jatuh kembali ke font default; kalau workbook kebetulan mendapat font baru di sela-selanya, run diam-diam terikat ke font itu, yang dalam pengujian mengubah run text box bergaya menjadi Arial. Berkas yang padat comment seperti yang digambarkan di membangun workflow review comment dan hyperlink persis tempat ini menggigit, karena berkas-berkas itu dibuka, dianotasi, dan disimpan berulang-ulang

Tabel font HotXLS lintas dua save di mana record FONT ekor yang tak dirujuk yang selalu ditulis Excel jatuh lebih dulu, font khusus comment menjadi terakhir lalu dibuang karena TXO formatting run ditulis kembali tanpa menghitung referensi, sampai CountRichRunFontRefs memperbaiki filter survival di 2.384.5
Save pertama terlihat bersih karena font ekor yang menanggung kerugiannya, dan font comment baru hilang di save kedua — petakan tiap ifnt ke nama record FONT lintas save alih-alih percaya tes in-memory

HotXLS 2.384.5 memperlakukan TXO run seperti SST run. CountRichRunFontRefs kini menelusuri setiap TMSOShapeTextBox di tiap worksheet, mengonversi ifnt skip-4 tiap run ke slot, dan menghitungnya sebagai referensi, jadi font yang hanya dipakai run selamat dari filter save. Tabel slot-ke-save-index yang dihasilkan masuk ke FontRunRemap milik tiap drawing, dan TMSOShapeTextBox.Store menulis ulang indeks run di salinan privat byte run mentah, membiarkan TxOLastRun ekor sendirian karena tidak membawa font. Untuk kode aplikasi kontraknya sederhana: TXLSComment.TextRuns.FontIndex dan TXLSTextBox.TextRuns.FontIndex memakai penomoran berkas, 4 dilewati, persis seperti yang dibaca; indeks run berbasis 1 dan CharIndex adalah offset karakter tempat run dimulai. Setelah save, angka tersimpan bisa berbeda dari yang Anda set, tapi tetap menunjuk font yang sama

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];   // penomoran berkas, 4 dilewati
      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;

Copy, insert baris, dan migrasi run lintas workbook

Sejak HotXLS 2.384.6, setiap jalur copy engine klasik menjaga formatting run comment, karena Range.Copy, CopyRange, Sheets.AddCopy, dan pergeseran sel di balik Range.Insert dan Range.Delete semuanya lewat TXLSRange.CopyCell, dan CopyCell dulu hanya menyalin teks comment dan author-nya. Shift adalah copy plus clear, jadi menyisipkan satu baris di atas note dua run membuatnya tinggal nol run dan satu font. Fix-nya menyalin tiap run dan memindahkan fontnya lewat TXLSWorkbook.MigrateRunFontIndex, yang mengonversi indeks skip-4 ke slot, memigrasikan font by value ke tabel font tujuan, lalu mengonversi kembali ke penomoran berkas; migrasi rich-text SST di Sheets.AddCopy kini memanggil fungsi yang sama alih-alih membawa salinan aritmetika sendiri. Dua edge case ikut tertangani: paste in-place di mana sumber dan tujuan adalah comment yang sama tidak boleh mengosongkan run-nya sebelum membacanya, dan Sheets.AddCopy kini melakukan pass kedua untuk comment yang menempel di sel tanpa record sel tersimpan, yang sebelumnya dilewati seluruhnya. Sisi tabel font dari copy lintas workbook mengikuti logika by value yang sama dengan sisi formula yang dibahas di copy lintas workbook dan rebinding formula. Di engine XLSX jalur copy sudah lama meng-clone run by value; celahnya ada di bagian comment sendiri, di mana reader mengabaikan rFont, strike, u, dan vertAlign dan writer tidak pernah menge emit u maupun vertAlign, jadi kini run selamat dari save dan reopen secara simetris

Bagaimana seharusnya Anda menguji indeks font di berkas BIFF8?

Uji indeks font dengan menyimpan dan membuka kembali, idealnya lintas lebih dari satu generasi, dan dengan memetakan tiap ifnt kembali ke record FONT alih-alih meng-assert rentang numerik. Setiap bug dalam cerita ini lolos tes in-memory: regresi 2.384.1 hidup di pasangan writer dan reader yang cocok, drift TXO butuh dua save dengan perubahan tabel font di sela-selanya, dan run comment yang hilang di XLSX baru muncul setelah reopen. Harness yang berguna membuka sampel buatan Excel, menyimpannya dua kali lewat HotXLS, menambah atau menghapus font di antara save, lalu memeriksa posisi run plus, di level byte, nama font di balik tiap ifnt. Jangan membandingkan nilai FontIndex sebelum dan sesudah save, karena penomoran ulang itu legitim

procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
  Book: IXLSWorkbook;
  Note: TXLSComment;
  RunCount: Integer;
  SecondRunAt: Word;
begin
  Book := TXLSWorkbook.Create;
  Assert(Book.Open(SrcFile) = 1);          // buatan Excel, C2 punya dua run
  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 pindah ke C3
  Assert(Book.SaveAs(OutFile) = 1);

  Book := TXLSWorkbook.Create;               // reopen, jangan pernah percaya memori
  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;

Kalau Anda membaca dan menulis XLS klasik dari Delphi atau C++Builder dan tak mau repot melacak konsumen font milik library yang mana masih sepakat dengan [MS-XLS] §2.5.129, penomoran skip-4, penomoran ulang run saat save, dan migrasi run by value yang dibahas di sini sudah tertanam di komponen spreadsheet Delphi HotXLS, yang membaca dan menulis XLS dan XLSX tanpa Excel atau OLE automation