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;
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
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
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