Műszaki cikk

BIFF8 Font index: miért nincs 4 a HotXLS rich text runokban

A HotXLS minden BIFF8 font hivatkozást úgy számoz, ahogy a [MS-XLS] §2.5.129 FontIndex definiálja: a 0–3 értékek nullaalapúak, a 4 felettiek egyalapúak, a 4 pedig sosem jelenik meg, így az ötödik FONT rekord az ifnt 5, a legnagyobb érvényes ifnt pedig a FONT rekordok számával egyenlő. A HotXLS 2.384.4 óta az XF író, az XF olvasó, a rich text sztring runok és a munkafüzetek közti run-migráció mind ezt a szabályt követi, a 2.384.5 és a 2.384.6 pedig kiterjeszti a megjegyzés- és szövegdoboz-runokra, másolások és sorbeszúrások útján is

A szabály elírásnak tűnik, amíg bele nem fut. Valaki megnyit egy nyolc FONT rekordos munkafüzetet, talál egy 8-as fontra mutató XF-et, és arra következtet, hogy az író tartományon kívüli indexet gyártott. Pontosan ez a következtetés ment ki a HotXLS 2.384.1-ben „javításként”, és egy helyes implementációból olyat faragott, amelyben az Excel által megnyitott fájl minden egyéni fontja egy réssel korábban landolt. Az érdekes rész nem az off-by-one maga, hanem az, hogy egy BIFF8 könyvtárban hány helyen él ugyanaz a konvenció, valamint az, hogy egy fontkötés hogyan élhet túl egy mentést, és hogyan törhet el a másodikon. Ha már megküzdött a BIFF8 XLUnicodeString cch és fHigh dekódolás hossz- és kódolási furcsaságaival, ez ugyanabból a hibacsaládból való: a fájl rendben van, a számítás nem

Mit mond valójában a [MS-XLS] FontIndex szabály?

A [MS-XLS] §2.5.129 szerint egy 4 alatti FontIndex nullaalapú rekordpozíció, egy 4 feletti egyalapú rekordpozíció, a 4-es érték használata pedig tilos. Ugyanazt a FontIndex típust használják az XF rekordok, az SST formázó runok és a TXO formázó runok, így egy félreolvasott szabály mindhármat megrongálja. A bizonyíték Excel-írt fájlokkal könnyen reprodukálható: az Office-szal szállított SOLVSAMP.XLS-ben 19 FONT rekord van, a legnagyobb XF ifnt 19, egy 43 rekordos munkafüzet 43-nál tetőzik, az Excel 16 által mentett, 30 FONT rekordos fájl pedig a Courier New celláit a 22. rekordra, az ifnt 22-re mutatja. Egyik sem tartalmaz soha 4-est. Ha diagnosztikai eszközben magának kell elemeznie a leképezést, az átalakítás két rövid függvény

// [MS-XLS] 2.5.129 FontIndex: 0..3 nullaalapú, > 4 egyalapú, a 4 érvénytelen
function FontIndexToRecordNo(Ifnt: Word): Integer;  // 1-alapú FONT rekord
begin
  if Ifnt < 4 then
    Result := Ifnt + 1
  else if Ifnt > 4 then
    Result := Ifnt
  else
    Result := -1;  // a 4 nem fordulhat elő
end;

function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
  if RecordNo <= 4 then
    Result := RecordNo - 1
  else
    Result := RecordNo;
end;
HotXLS FontIndex leképezés az MS-XLS 2.5.129 szerint, ahol az ifnt 0–3 nullaalapú FONT rekordpozíciók, az ifnt 5 és afölöttiek egyalapúak, a 4-es érték pedig sosem fordul elő, a FontIndexToRecordNo konverzióval és Excel-írt munkafüzetek, például a SOLVSAMP.XLS bizonyítékaival
Az ötödik FONT rekord az ifnt 5, nem 4 — egy 19 rekordos munkafüzet az ifnt 19-nél tetőzik, és egyetlen Excel-írt fájl sem tárolja soha a közbeni tiltott értéket

A HotXLS-en belül ugyanez a szabály két tükörhelyen lakik. A TXLSFontList.GetSaveIndex a font 1-alapú pozícióját kapja a hivatkozott listában, és csak az 1–4 pozíciókat csökkenti, így az 5. pozíció ifnt 5-ként íródik. A TXLSReader.ParseXF betöltéskor a fordítottat teszi: minden 5-ös vagy nagyobb ifnt a fontlista nullaalapú resére csökken, az alatta lévő pedig a helyén marad. Az SST rich-run átmapelés és a CountRichRunFontRefs ugyanazt a ifnt >= 5 konverziót alkalmazza, és ez a lényeg: egy konvenció, minden felhasználó

// TXLSFontList.GetSaveIndex (író oldal)
Result := inherited GetSaveIndex(Index);   // 1-alapú hivatkozott pozíció
if (Result > 0) and (Result < 5) then
  Dec(Result);                             // 1..4-ből 0..3 lesz, 5+ változatlan

// TXLSReader.ParseXF (olvasó oldal)
fnti := Data.GetWord(0);
if fnti >= 5 then
  Dec(fnti);                               // az ifnt 5 a fontlista 4. rése

Miért csúsztatta el eggyel a nullaalapú „javítás” az összes egyéni fontot?

A HotXLS 2.384.1-es nullaalapú átírása azért csúsztatta el eggyel az összes egyéni fontot, mert egyalapú indexet olvasott nullaalapúként, majd négy hívási helyet igazított ahhoz a félreolvasáshoz: a GetSaveIndex-t, a ParseXF-et, az SST run átmapelést és a Sheets.AddCopy munkafüzetek közti run-migrációját. A HotXLS round tripjei továbbra is rendben néztek ki, mert az író és az olvasó egymással egyetértett. Az Excel nem értett egyet. A 2.384.1 által írt fájl az első egyéni fontot ifnt 4-re tette, amelyet az Excel alapértelmezett fontként kezel, minden későbbi egyéni font pedig egy rekorddal korábban került; az Excel-fájl megnyitása fordítva ment, minden font egy rekorddal később kötődött

HotXLS 2.384.1 regresszió, amelyben a GetSaveIndex és a ParseXF az egyalapú FontIndex értékeket nullaalapúként olvasta, az első egyéni fontot a tiltott ifnt 4-ként írta, amelyet az Excel az alapértelmezett fontra old fel, minden későbbi font pedig egy rekorddal korábban landolt, miközben a round trip továbbra is átment
Az író és az olvasó ugyanabban a félreolvasásban egyetértett, így a mentést követő újranyitásos teszt zöld maradt, miközben minden Excel által megnyitott font egy réssel elcsúsztva landolt — ha egy konvenció hét helyen él, és Ön négyet változtat, előbb a saját változtatását gyanúsítsa

A jelzés, amelynek meg kellett volna állítania a változtatást, ugyanabban a kódbázisban ült. A CountRichRunFontRefs, a diagram FONTX és FBI átmapelés és a stílusmotor fontlistája sosem érintett, és továbbra is skip-4-et használt, így a könyvtár a 2.384.1 leszállásának pillanatában önmagával ellentmondott, és csak az a véletlen rejtette el az ellentmondást, hogy a rich text fontokra jellemzően valamilyen XF is hivatkozott. Ha egy konvenció hét helyen jelenik meg, és Ön négyet változtat, gyanúsítsa előbb a saját változtatását, ne a másik hármat. A 2.384.4 mind a négy helyen visszaállította a specifikáció számozását, és a régi regressziós tesztet, amely az ifnt < FontCount-ot állította, tehát magát a félreolvasást kódolta, olyan tesztek váltották, amelyek minden kiírt ifnt-et a specifikum képletén keresztül FONT rekordnévre képeznek vissza. Egy becsületes korlát megmaradt: a 2.384.1-től 2.384.3-ig mentett, öt vagy több fontos fájlok elcsúsztott indexeket hordoznak, amelyeket egy olvasó nem tud megkülönböztetni az érvényes adattól, így az egyetlen gyógymód az újragenerálásuk

Miért csak a második mentésnél törik el a megjegyzés font runjai?

A megjegyzés- és szövegdoboz-runok a második mentésnél törtek el, mert a HotXLS az első N-1 FONT rekordot feltétel nélkül megtartotta, és csak az utolsót dobta el, ha rá nem hivatkozott XF, miközben a TXO formázó runokat ([MS-XLS] §2.4.329) bájtpontról bájtpontra írta vissza újraszámozás nélkül. Az Excel-írt .xls fájlok mindig hivatkozatlan záró fonttal végződnek (egy 9 pontos DengXian kínai területi beállítású rendszeren), így az első mentéskor a csak megjegyzés-run által használt font sosem volt utolsó, és semmi nem mozdult láthatóan. Az első mentés viszont eldobta a záró fontot, és a csak megjegyzéses fontot az utolsó pozícióba léptette. A második mentés akkor hivatkozatlanul eldobta, a run ifnt-je a végén túlmutatott, és az Excel az alapértelmezett fontra esett vissza; ha a munkafüzet közben új fontot kapott, a run csendben ahhoz kötődött, ami a tesztben egy stílusos szövegdoboz-runból Arial-t csinált. A megjegyzés- és hiperhivatkozás-ellenőrzési munkafolyamatot építő, megjegyzésnehéz fájlok pontosan ott harapnak, mert ismételten megnyitják, annotálják és mentik őket

HotXLS fonttábla két mentésen át, ahol az Excel által mindig kiírt hivatkozatlan záró FONT rekord előbb esik el, a csak megjegyzéses font utolsó lesz, majd azért hullik ki, mert a TXO formázó runok hivatkozásszámolás nélkül íródtak vissza, míg a CountRichRunFontRefs a 2.384.5-ben meg nem javította a túlélési szűrőt
Az első mentés tisztának tűnt, mert a záró font vállalta a veszteséget, a megjegyzés fonta csak a másodikon tűnt el — mentések közt képezze vissza minden ifnt-et FONT rekordnévre, memóriabeli tesztbe vetett bizalom helyett

A HotXLS 2.384.5 a TXO runokat úgy kezeli, mint az SST runokat. A CountRichRunFontRefs ma már minden munkalap minden TMSOShapeTextBox-án végigmegy, minden run skip-4 ifnt-jét résre képezi, és hivatkozásként számolja, így egy csak run által használt font túléli a mentési szűrőt. A keletkező rés-mentésindex tábla mindegyik rajz FontRunRemap-jába kerül, és a TMSOShapeTextBox.Store a nyers runbájtok privát másolatán írja újra a run indexeket, a záró TxOLastRun-t békén hagyva, mert az nem hordoz fontot. Az alkalmazáskód számára a szerződés egyszerű: a TXLSComment.TextRuns.FontIndex és a TXLSTextBox.TextRuns.FontIndex a fájlszámozást használja, 4 kihagyva, pontosan olvasottan; a run indexek 1-alapúak, a CharIndex pedig a karaktereltolódás, ahol a run indul. Mentés után a tárolt szám eltérhet a beállítottól, de ugyanarra a fontra mutat

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];   // fájlszámozás, 4 kihagyva
      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;

Másolások, sorbeszúrások és munkafüzetek közti run-migráció

A HotXLS 2.384.6 óta a klasszikus motor minden másolási útja megőrzi a megjegyzés formázó runokat, mert a Range.Copy, a CopyRange, a Sheets.AddCopy és a Range.Insert, illetve Range.Delete mögötti cellacsúsztatások mind a TXLSRange.CopyCell-en mennek keresztül, a CopyCell pedig korábban csak a megjegyzés szövegét és szerzőjét másolta. A csúsztatás másolás plusz törlés, így egy két runos megjegyzés fölé beszúrt egyetlen sor nullára apasztotta a runjait, egy fontra. A javítás minden runot lemásol, és a fontját a TXLSWorkbook.MigrateRunFontIndex-en át mozgatja, amely a skip-4 indexet résre képezi, a fontot érték szerint migrálja a cél fonttáblájába, majd visszaképezi fájlszámozásra; a Sheets.AddCopy SST rich text migrációja ma már ugyanezt a függvényt hívja, ahelyett hogy a saját számtani példányát cipelné. Két szélsőség is jött vele: egy helyben beillesztésnél, ahol a forrás és a cél ugyanaz a megjegyzés, a runok olvasása előtt nem szabad törölni őket, és a Sheets.AddCopy ma már második menetet végez a tárolt cellarekord nélküli cellákra kapcsolt megjegyzésekért, amelyeket korábban teljesen kihagyott. A munkafüzetek közti másolás fonttábla-oldala ugyanazt az érték szerinti logikát követi, mint amit a munkafüzetek közti másolás és képlet-újrakötés a képletek oldalán leír. Az XLSX motoron a másolási utak már érték szerint klónozták a runokat; a rés magában a megjegyzés-résztben volt, ahol az olvasó figyelmen kívül hagyta a rFont-ot, a strike-ot, az u-t és a vertAlign-t, az író pedig sosem bocsátott ki u-t vagy vertAlign-t, így a runok ma már szimmetrikusan túlélik a mentést és az újranyitást

Hogyan tesztelje a font indexeket BIFF8 fájlokban?

A font indexeket mentéssel és újranyitással tesztelje, ideálisan egynél több generáción át, és minden ifnt-et FONT rekordra képezzen vissza numerikus tartomány állítása helyett. Ennek a történetnek minden hibája átment a memóriabeli teszten: a 2.384.1-es regresszió összepárosított író-olvasó duóban lakott, a TXO sodródáshoz két mentés kellett köztes fonttábla-változtatással, az XLSX elveszett megjegyzés-runjai pedig csak újranyitás után mutatkoztak. Egy hasznos keretrendszer megnyit egy Excel-írt mintát, kétszer menti át a HotXLS-en, mentések közt fontot ad hozzá vagy elvesz, majd ellenőrzi a run pozíciókat, bájtszinten pedig az egyes ifnt-ek mögötti fontneveket. Ne hasonlítsa össze a FontIndex értékeket mentés előtt és után, mert az újraszámozás 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);          // Excel-írt, a C2-ben két run van
  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);   // a C2 a C3-ba kerül
  Assert(Book.SaveAs(OutFile) = 1);

  Book := TXLSWorkbook.Create;               // újranyitás, a memóriában sosem bízunk
  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;

Ha Delphi vagy C++Builder alól olvas és ír klasszikus XLS-t, és inkább nem akarná követni, hogy a könyvtár sok font-felhasználója közül melyik ért egyet még a [MS-XLS] §2.5.129-tel, az itt leírt skip-4 számozás, a mentéskori run-újraszámozás és az érték szerinti run-migráció be van építve a HotXLS Delphi táblázatkezelő komponensbe, amely XLS-t és XLSX-et olvas és ír Excel vagy OLE automatizálás nélkül