HotXLS čísluje každú referenciu na font v BIFF8 tak, ako ju definuje [MS-XLS] §2.5.129 FontIndex: hodnoty 0 až 3 sú zero-based, hodnoty nad 4 sú one-based a 4 sa nikdy neobjaví, takže piaty FONT record je ifnt 5 a najväčšie platné ifnt sa rovná počtu FONT recordov. Od HotXLS 2.384.4 sa týmto pravidlom riadia XF writer, XF reader, runy rich text reťazcov aj migrácia runov medzi workbookmi a 2.384.5 a 2.384.6 ho rozširujú na runy komentárov a text boxov, vrátane kópií a vkladania riadkov
Pravidlo vyzerá ako preklep, kým doňho narazíte. Niekto otvorí workbook s ôsmimi FONT recordmi, nájde XF ukazujúci na font 8 a uzavrie, že writer vyprodukoval index mimo rozsahu. Presne tento úsudok sa dostal do HotXLS 2.384.1 ako "oprava" a zmenil korektnú implementáciu na takú, v ktorej každé vlastné písmo v súbore otvorenom v Exceli pristalo jednu pozíciu skôr. Zaujímavé nie je samotné off-by-one, ale to, koľko miest v BIFF8 knižnici nesie tú istú konvenciu a ako dokáže fontová väzba prežiť prvé uloženie a pokoriť sa pri druhom. Ak ste sa už porekali s kvirkami dĺžky a kódovania, ktoré popisuje dekódovanie cch a fHigh v BIFF8 XLUnicodeString, je to tá istá rodina bugov: súbor je v poriadku, aritmetika nie
Čo pravidlo FontIndex v [MS-XLS] vlastne hovorí?
[MS-XLS] §2.5.129 hovorí, že FontIndex pod 4 je zero-based pozícia recordu, FontIndex nad 4 je one-based pozícia recordu a hodnota 4 sa POUŽÍVAŤ NESMIE. Rovnaký typ FontIndex používajú XF recordy, SST formatting runy aj TXO formatting runy, takže jedno zle prečítané pravidlo pokorí všetky tri. Dôkaz sa ľahko reprodukuje na súboroch od Excelu: SOLVSAMP.XLS dodávaný s Office má 19 FONT recordov a maximálne XF ifnt 19, workbook s 43 recordmi vrcholí na 43 a súbor uložený Excelom 16 s 30 FONT recordmi ukazuje bunky s Courier New na ifnt 22, teda dvadsiaty druhý record. Ani jeden nikdy neobsahuje 4. Ak si potrebujete analyzovať mapovanie v diagnostickom nástroji, konverzia sú dve krátke funkcie
// [MS-XLS] 2.5.129 FontIndex: 0..3 zero-based, > 4 one-based, 4 invalid
function FontIndexToRecordNo(Ifnt: Word): Integer; // 1-based číslo FONT recordu
begin
if Ifnt < 4 then
Result := Ifnt + 1
else if Ifnt > 4 then
Result := Ifnt
else
Result := -1; // 4 nesmie nastať
end;
function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
if RecordNo <= 4 then
Result := RecordNo - 1
else
Result := RecordNo;
end;
V HotXLS žije to isté pravidlo na dvoch zrkadlových miestach. TXLSFontList.GetSaveIndex vezme 1-based pozíciu fontu v referencovanom zozname a dekrementuje len pozície 1 až 4, takže pozícia 5 sa zapisuje ako ifnt 5. TXLSReader.ParseXF robí pri načítaní opak: každé ifnt 5 a viac sa dekrementuje na zero-based slot v zozname fontov a všetko pod tým zostáva. SST rich-run remap a CountRichRunFontRefs aplikujú tú istú konverziu ifnt >= 5 — a presne o to ide: jedna konvencia, každý konzument
// TXLSFontList.GetSaveIndex (strana writeru)
Result := inherited GetSaveIndex(Index); // 1-based referencovaná pozícia
if (Result > 0) and (Result < 5) then
Dec(Result); // 1..4 sa stanú 0..3, 5+ bez zmeny
// TXLSReader.ParseXF (strana readera)
fnti := Data.GetWord(0);
if fnti >= 5 then
Dec(fnti); // ifnt 5 je slot 4 v zozname fontov
Prečo "oprava" na zero-based posunula každé vlastné písmo o jednu pozíciu?
Zero-based prepis v HotXLS 2.384.1 posunul každé vlastné písmo, pretože čítal one-based index ako zero-based a potom zmenil štyri call sites tak, aby zodpovedali tomuto zlému čítaniu: GetSaveIndex, ParseXF, SST run remap a migráciu runov medzi workbookmi v Sheets.AddCopy. Round tripy HotXLS stále vyzerali v poriadku, lebo writer a reader sa zhodli navzájom. Excel nesúhlasil. Súbor zapísaný 2.384.1 dal prvé vlastné písmo na ifnt 4, čo Excel prekladá na default font, a každý ďalší vlastný font jeden record skôr; otvorenie excelového súboru išlo opačným smerom a viazalo každý font jeden record neskôr
Stopa, ktorá by túto zmenu mala zastaviť včas, sedela v tom istom kóde. CountRichRunFontRefs, remap FONTX a FBI v grafoch a zoznam fontov v style engine sa nikdy nezmenili a naďalej používali skip-4, takže knižnica sama sebe odporovala v momente, keď 2.384.1 pristála, a odhalilo to len to, že písma rich textu zvyčajne referenceoval aj nejaký XF, takže rozpor ostal skrytý. Keď sa jedna konvencia objavuje na siedmich miestach a meníte štyri, podozrivajte svoju zmenu skôr než zvyšné tri. Verzia 2.384.4 obnovila číslovanie zo špecifikácie na všetkých štyroch miestach a starý regresný test, ktorý assertoval ifnt < FontCount a tým zlé čítanie zakódoval, nahradili testy mapujúce každé zapísané ifnt späť na meno FONT recordu cez vzorec zo špecifikácie. Jedno úprimné obmedzenie zostáva: súbory uložené 2.384.1 až 2.384.3 s piatimi a viac fontmi nesú posunuté indexy, ktoré reader nedokáže odlíšiť od platných dát, takže jediný liek je vygenerovať ich nanovo
Prečo sa font runy komentárov pokoria až pri druhom uložení?
Runy komentárov a text boxov sa pokorili pri druhom uložení, pretože HotXLS držal prvých N-1 FONT recordov bezpodmienečne a zahodil len posledný, keď naňho nereferenceoval žiadny XF, zatiaľ čo TXO formatting runy ([MS-XLS] §2.4.329) sa zapisovali späť bajt za bajtom bez prečíslovania. Súbory .xls od Excelu vždy končia nereferencovaným trailing fontom (9pt DengXian na systéme s čínskou locale), takže pri prvom uložení font použitý len runom komentára nikdy nebol posledný a nič sa viditeľne nepohlo. To prvé uloženie však trailing font zahodilo a font len pre komentár povýšilo na poslednú pozíciu. Druhé uloženie ho potom zahodilo ako nereferencovaný, ifnt runu ukazoval za koniec a Excel prepadol na default font; ak si workbook medzitým pripísal nový font, run sa poticho zviazal s ním, čo v testovaní zmenilo štylovaný run text boxu na Arial. Súbory plné komentárov, ako tie popísané v stavbe workflow na revíziu komentárov a hypertextových odkazov, sú presne miesto, kde to zahryzie, lebo sa otvárajú, anotujú a ukladajú opakovane
HotXLS 2.384.5 zaobchádza s TXO runami ako s SST runami. CountRichRunFontRefs teraz prejde každý TMSOShapeTextBox na každom hárku, prekonvertuje skip-4 ifnt každého runu na slot a započíta ho ako referenciu, takže font použitý len runmi prežije save filter. Výsledná tabuľka slot-to-save-index ide do FontRunRemap každého drawingu a TMSOShapeTextBox.Store prepisuje indexy runov na súkromnej kópií surových bajtov runov, pričom trailing TxOLastRun necháva na pokoji, lebo font nenesie. Pre aplikačný kód je kontrakt jednoduchý: TXLSComment.TextRuns.FontIndex a TXLSTextBox.TextRuns.FontIndex používajú číslovanie súboru so skip 4, presne ako boli načítané; indexy runov sú 1-based a CharIndex je znakový offset, kde run začína. Po uložení sa uložené číslo môže líšiť od toho, ktoré ste nastavili, ale stále ukazuje na ten istý font
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]; // číslovanie súboru, 4 preskočené
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;
Kópie, vkladanie riadkov a migrácia runov medzi workbookmi
Od HotXLS 2.384.6 každá copy path klasického enginu zachováva formatting runy komentárov, lebo Range.Copy, CopyRange, Sheets.AddCopy aj posuny buniek za Range.Insert a Range.Delete prechádzajú cez TXLSRange.CopyCell a CopyCell kopíroval len text komentára a autora. Posun je kópia plus zmazanie, takže vloženie jediného riadka nad poznámkou s dvomi runami ho nechalo s nulou runov a jedným fontom. Oprava kopíruje každý run a presúva jeho font cez TXLSWorkbook.MigrateRunFontIndex, ktoré prekonvertuje skip-4 index na slot, presťahuje font hodnotou do cieľovej tabuľky fontov a prekonvertuje späť na číslovanie súboru; SST rich-text migrácia v Sheets.AddCopy teraz volá tú istú funkciu namiesto toho, aby nosila vlastnú kópiu aritmetiky. Prišli aj dva edge cases: in-place vloženie, kde zdroj a cieľ je ten istý komentár, nesmie vymazať jeho runy skôr, než ich prečíta, a Sheets.AddCopy robí teraz druhý priebeh pre komentáre pripojené k bunkám bez uloženého cell recordu, ktoré predtým preskočil úplne. Strana fontovej tabuľky pri kopírovaní medzi workbookmi nasleduje tú istú by-value logiku ako strana formulí popísaná v kopírovaní medzi workbookmi a rebindingu formulí. Na XLSX engine už copy pathy klonovali runy hodnotou; medzera bola v samotnej časti pre komentáre, kde reader ignoroval rFont, strike, u a vertAlign a writer nikdy neemitoval u ani vertAlign, takže runy teraz prežívajú uloženie aj znovuotvorenie symetricky
Ako testovať font indexy v súboroch BIFF8?
Font indexy testujte uložením a znovuotvorením, ideálne cez viac než jednu generáciu, a mapovaním každého ifnt späť na FONT record namiesto asertovania číselného rozsahu. Každý bug v tomto príbehu prešiel in-memory testom: regresia 2.384.1 žila vo vzájomne zladenom páre writer a reader, TXO drift potreboval dve uloženia so zmenou tabuľky fontov medzi tým a stratené runy komentárov na XLSX sa ukázali až po znovuotvorení. Užitočný harness otvorí vzorku od Excelu, uloží ju dvakrát cez HotXLS, medzi uloženiami pridá alebo odoberie font a potom skontroluje pozície runov plus na úrovni bajtov mená fontov za každým ifnt. Nevedte porovnávať hodnoty FontIndex pred a po uložení, prečíslovanie je legitimné
procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
Book: IXLSWorkbook;
Note: TXLSComment;
RunCount: Integer;
SecondRunAt: Word;
begin
Book := TXLSWorkbook.Create;
Assert(Book.Open(SrcFile) = 1); // od Excelu, C2 má dva runy
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 sa presunie na C3
Assert(Book.SaveAs(OutFile) = 1);
Book := TXLSWorkbook.Create; // znovuotvorenie, pamäti nikdy never
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;
Ak čítate a zapisujete klasické XLS z Delphi alebo C++Builderu a nechcete sledovať, ktorý z mnohých fontových konzumentov knižnice sa stále zhoduje s [MS-XLS] §2.5.129, skip-4 číslovanie, prečíslovanie runov pri uložení a by-value migrácia runov popísané tu sú zabudované v HotXLS Delphi spreadsheet component, ktorý číta a zapisuje XLS a XLSX bez Excelu alebo OLE automation