HotXLS svaku BIFF8 referencu na font broji onako kako je definiše [MS-XLS] §2.5.129 FontIndex: vrednosti od 0 do 3 su nula-bazirane, vrednosti iznad 4 su jedno-bazirane, a 4 se nikada ne pojavljuje, pa je peti FONT zapis ifnt 5 i najveća validna ifnt vrednost jednaka je broju FONT zapisa. Od HotXLS 2.384.4 tom pravilu slede XF pisac, XF čitač, rich text string runovi i migracija runova između radnih sveski, a 2.384.5 i 2.384.6 prostiru ga i na runove komentara i text boxova, uključujući i kopiranje i ubacivanje redova
Pravilo deluje kao greška u kucanju dok ne naletite na nju. Neko otvori radnu svesku sa osam FONT zapisa, pronađe XF koji pokazuje na font 8 i zaključi da je pisac proizveo indeks van opsega. Baš je to razmišljanje ušlo u HotXLS 2.384.1 kao „popravka", i pretvorilo je ispravnu implementaciju u onu u kojoj svaki prilagođeni font u fajlu koji otvori Excel dospeva jedno mesto ranije. Zanimljivo ovde nije sam off-by-one, nego koliko mesta u BIFF8 biblioteci nosi istu konvenciju i kako vezivanje fonta može preživeti jedno čuvanje a puknuti na drugom. Ako ste se već borili sa čudima dužine i enkodovanja iz teksta o dekodovanju BIFF8 XLUnicodeString cch i fHigh, ovo je ista porodica bugova: fajl je u redu, aritmetika nije
Šta pravilo [MS-XLS] FontIndex zapravo kaže?
[MS-XLS] §2.5.129 kaže da je FontIndex ispod 4 nula-bazirana pozicija zapisa, FontIndex iznad 4 jedno-bazirana pozicija zapisa, a vrednost 4 se NE SME koristiti. Isti FontIndex tip koriste XF zapisi, SST formatting runovi i TXO formatting runovi, pa jedno pogrešno pročitano pravilo pokvari sva tri. Dokaz je lako reprodukovati na fajlovima koje je pisao Excel: SOLVSAMP.XLS koji dolazi uz Office ima 19 FONT zapisa i maksimalan XF ifnt od 19, radna sveska sa 43 zapisa staje na 43, a fajl sačuvan u Excelu 16 sa 30 FONT zapisa usmerava ćelije sa Courier New na ifnt 22, dvadeset drugi zapis. Nijedan nikada ne sadrži 4. Ako preslikavanje morate analizirati sami u dijagnostičkom alatu, konverzija su dve kratke funkcije
// [MS-XLS] 2.5.129 FontIndex: 0..3 nula-bazirano, > 4 jedno-bazirano, 4 nevalidno
function FontIndexToRecordNo(Ifnt: Word): Integer; // FONT zapis od 1
begin
if Ifnt < 4 then
Result := Ifnt + 1
else if Ifnt > 4 then
Result := Ifnt
else
Result := -1; // 4 ne sme da se pojavi
end;
function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
if RecordNo <= 4 then
Result := RecordNo - 1
else
Result := RecordNo;
end;
Unutar HotXLS-a isto pravilo živi na dva ogledaljena mesta. TXLSFontList.GetSaveIndex uzima jedno-baziranu poziciju fonta u referenciranoj listi i umanjuje samo pozicije od 1 do 4, pa se pozicija 5 upisuje kao ifnt 5. TXLSReader.ParseXF radi obrnuto pri učitavanju: svaki ifnt od 5 naviše umanjuje se na nula-baziran slot font liste, a sve ispod ostaje na mestu. SST rich-run remap i CountRichRunFontRefs primenjuju istu ifnt >= 5 konverziju, i u tome je poenta: jedna konvencija, svaki potrošač
// TXLSFontList.GetSaveIndex (strana pisc)
Result := inherited GetSaveIndex(Index); // referencirana pozicija od 1
if (Result > 0) and (Result < 5) then
Dec(Result); // 1..4 postaju 0..3, 5+ nepromenjeno
// TXLSReader.ParseXF (strana čitača)
fnti := Data.GetWord(0);
if fnti >= 5 then
Dec(fnti); // ifnt 5 je slot 4 font liste
Zašto je nula-bazirana „popravka" pomerila svaki prilagođeni font za jedno mesto?
Nula-bazirano prepisivanje u HotXLS 2.384.1 pomerilo je svaki prilagođeni font jer je jedno-baziran indeks čitalo kao nula-baziran, a zatim promenilo četiri mesta poziva da se uklapi u to pogrešno čitanje: GetSaveIndex, ParseXF, SST run remap i migraciju runova između radnih sveski u Sheets.AddCopy. HotXLS round trip i dalje je izgledao uredno, jer su se pisac i čitač slagali međusobno. Excel se nije slagao. Fajl zapisan u 2.384.1 stavio je prvi prilagođeni font na ifnt 4, što Excel tretira kao podrazumevani font, a svaki sledeći prilagođeni font po zapis ranije; otvaranje Excel fajla išlo je u suprotnom smeru, vezujući svaki font po zapis kasnije
Trag koji je trebalo da zaustavi izmenu ležao je u istoj bazi koda. CountRichRunFontRefs, remap FONTX i FBI za grafikone i font lista style engine-a nikada nisu dirani i i dalje su koristili skip-4, pa se biblioteka suprotstavila samoj sebi trenutka kad je 2.384.1 stigla, i samo slučajnost da su rich-text fontove obično referencirali i neki XF držala je protivrečnost skrivenu. Kada se jedna konvencija pojavljuje na sedam mesta a vi menjate četiri, posumnjajte prvo u svoju izmenu, pa tek onda u ostala tri. Verzija 2.384.4 vratila je specifikacijsku numeraciju na sva četiri mesta, a stari regresioni test, koji je asertovao ifnt < FontCount i time kodirao pogrešno čitanje, zamenjen je testovima koji svaki upisani ifnt preslikavaju nazad na ime FONT zapisa kroz formulu iz specifikacije. Jedno pošteno ograničenje ostaje: fajlovi sačuvani u verzijama 2.384.1 do 2.384.3 sa pet ili više fontova nose pomerene indekse koje čitač ne može razlikovati od validnih podataka, pa je jedini lek da se regenerišu
Zašto se font runovi komentara kvaro tek na drugom čuvanju?
Runovi komentara i text boxova kvarili su se na drugom čuvanju jer je HotXLS prva N-1 FONT zapisa čuvao bezuslovno, a odbacivao samo poslednji kada ga nijedan XF ne referencira, dok su TXO formatting runovi ([MS-XLS] §2.4.329) zapisivani nazad bajt po bajt bez prenumeracije. .xls fajlovi koje piše Excel uvek se završavaju nereferenciranim pratećim fontom (9pt DengXian na sistemu sa kineskim lokalom), pa pri prvom čuvanju font koji je koristio samo run komentara nikada nije bio poslednji, i ništa se vidljivo nije pomerilo. To prvo čuvanje je ipak odbacilo prateći font i promovisalo font samo-komentara na poslednju poziciju. Drugo čuvanje ga je tada odbacilo kao nereferenciranog, ifnt runa pokazivao je iza kraja, a Excel padao je na podrazumevani font; ako je radna sveska u međuvremenu dobila novi font, run se tiho vezao za njega, što je u testiranju stilizovani text box run pretvorilo u Arial. Fajlovi bogati komentarima, poput onih opisanih u tekstu o izgradnji workflow-a za reviziju komentara i hiperlinkova, baš su mesto gde ovo ujeda, jer se otvaraju, anotiraju i čuvaju iznova i iznova
HotXLS 2.384.5 tretira TXO runove kao SST runove. CountRichRunFontRefs sada prolazi kroz svaki TMSOShapeTextBox na svakom radnom listu, pretvara skip-4 ifnt svakog runa u slot i broji ga kao referencu, pa font koji koristi samo run preživi filter čuvanja. Nastala tabela slot-to-save-index odlazi u FontRunRemap svakog crteža, a TMSOShapeTextBox.Store prepisuje indekse runova na privatnoj kopiji sirovih bajtova runova, ostavljajući prateći TxOLastRun na miru jer ne nosi font. Za kod aplikacije ugovor je jednostavan: TXLSComment.TextRuns.FontIndex i TXLSTextBox.TextRuns.FontIndex koriste numeraciju fajla, sa preskočenom 4, tačno kao što je pročitano; indeksi runova su jedno-bazirani a CharIndex je offset znaka na kom run počinje. Nakon čuvanja upisani broj može se razlikovati od onog koji ste postavili, ali i dalje pokazuje na isti 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]; // numeracija fajla, sa preskočenom 4
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;
Kopije, ubacivanje redova i migracija runova između radnih sveski
Od HotXLS 2.384.6 svaka copy putanja klasičnog engine-a čuva formatting runove komentara, jer Range.Copy, CopyRange, Sheets.AddCopy i pomeranja ćelija iza Range.Insert i Range.Delete sada prolaze kroz TXLSRange.CopyCell, a CopyCell je nekada kopirao samo tekst komentara i autora. Pomeranje je kopija plus brisanje, pa je ubacivanje jednog reda iznad napomene sa dva runa ostavljalo nulu runova i jedan font. Popravka kopira svaki run i premešta njegov font kroz TXLSWorkbook.MigrateRunFontIndex, koji preslikava skip-4 indeks u slot, migrira font po vrednosti u odredišnu tabelu fontova i preslikava nazad u numeraciju fajla; SST rich-text migracija u Sheets.AddCopy sada zove istu funkciju umesto da vuče sopstvenu kopiju aritmetike. Uz to su došla i dva granična slučaja: lepljenje na mestu gde su izvor i odredište isti komentar ne sme očistiti njegove runove pre nego što ih pročita, a Sheets.AddCopy sada radi drugi prolaz za komentare zakačene za ćelije bez sačuvanog zapisa ćelije, koje je ranije preskakao potpuno. Strana tabele fontova pri kopiranju između radnih sveski sledi istu logiku po vrednosti kao i strana formula koju pokriva kopiranje između radnih sveski i rebinding formula. Na XLSX engine-u copy putanje su runove već klonirale po vrednosti; rupa je bila u samom delu za komentare, gde je čitač ignorišao rFont, strike, u i vertAlign, a pisac nikada nije emitovao u ni vertAlign, pa runovi sada preživljavaju čuvanje i ponovno otvaranje simetrično
Kako biste trebali testirati font indekse u BIFF8 fajlovima?
Font indekse testirajte čuvanjem i ponovnim otvaranjem, idealno kroz više od jedne generacije, i preslikavanjem svakog ifnt nazad na FONT zapis umesto asertovanja numeričkog opsega. Svaki bug u ovoj priči prošao je test u memoriji: 2.384.1 regresija živela je u uparenom paru pisca i čitača, TXO odstupanje tražilo je dva čuvanja sa izmenom tabele fontova između, a izgubljeni runovi komentara na XLSX-u pokazali su se tek posle ponovnog otvaranja. Koristan harness otvara uzorak koji je pisao Excel, čuva ga dva puta kroz HotXLS, dodaje ili uklanja font između čuvanja, a zatim proverava pozicije runova plus, na nivou bajtova, imena fontova iza svakog ifnt. Ne poredite FontIndex vrednosti pre i posle čuvanja, jer je prenumeracija legitimna
procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
Book: IXLSWorkbook;
Note: TXLSComment;
RunCount: Integer;
SecondRunAt: Word;
begin
Book := TXLSWorkbook.Create;
Assert(Book.Open(SrcFile) = 1); // napisao Excel, C2 ima dva runa
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 se pomera na C3
Assert(Book.SaveAs(OutFile) = 1);
Book := TXLSWorkbook.Create; // ponovno otvaranje, ne veruj memoriji
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;
Ako iz Delphi-ja ili C++Builder-a čitate i upisujete klasični XLS i ne želite da pratite koji će od mnogih potrošača fontova u biblioteci i dalje biti usaglašen sa [MS-XLS] §2.5.129, skip-4 numeracija, prenumeracija runova pri čuvanju i migracija runova po vrednosti opisani ovde ugrađeni su u HotXLS Delphi spreadsheet komponentu, koja čita i upisuje XLS i XLSX bez Excela i OLE automation-a