HotXLS numerotează fiecare referință de font BIFF8 așa cum o definește [MS-XLS] §2.5.129 FontIndex: valorile 0 până la 3 au bază zero, valorile peste 4 au bază unu, iar 4 nu apare niciodată, deci a cincea înregistrare FONT este ifnt 5, iar cel mai mare ifnt valid este egal cu numărul de înregistrări FONT. Din HotXLS 2.384.4, scriitorul XF, cititorul XF, run-urile de șiruri rich text și migrația de run-uri între registre de lucru urmează toate regula aceasta, iar 2.384.5 și 2.384.6 o extind la run-urile din comentarii și casete de text, inclusiv prin copii și inserări de rânduri
Regula pare o greșeală de tastare până când o lovești. Cineva deschide un registru de lucru cu opt înregistrări FONT, găsește un XF care arată spre fontul 8 și concluzionează că scriitorul a produs un index în afara intervalului. Exact acest raționament a ajuns în HotXLS 2.384.1 drept o „reparare", și a transformat o implementare corectă într-una în care fiecare font personalizat dintr-un fișier deschis în Excel ateriza un slot mai devreme. Partea interesantă nu este off-by-one-ul în sine, ci câte locuri dintr-o bibliotecă BIFF8 poartă aceeași convenție și cum o legătură de font poate supraviețui unei salvări și se rupe la a doua. Dacă v-ați mai luptat deja cu ciudățeniile de lungime și codare descrise în decodarea lui cch și fHigh din XLUnicodeString BIFF8, iată aceeași familie de bug: fișierul e în regulă, aritmetica nu
Ce spune de fapt regula FontIndex din [MS-XLS]?
[MS-XLS] §2.5.129 spune că un FontIndex sub 4 este o poziție de înregistrare cu bază zero, un FontIndex peste 4 este o poziție cu bază unu, iar valoarea 4 NU TREBUIE folosită. Același tip FontIndex este folosit de înregistrările XF, de run-urile de formatare SST și de cele TXO, deci o regulă citită greșit corupe toate trei. Dovezile se reproduc ușor cu fișiere create de Excel: SOLVSAMP.XLS livrat cu Office are 19 înregistrări FONT și un XF maxim cu ifnt 19, un registru cu 43 de înregistrări se oprește la 43, iar un fișier salvat de Excel 16 cu 30 de înregistrări FONT își arată celulele Courier New spre ifnt 22, a 22-a înregistrare. Niciunul nu conține vreodată un 4. Dacă trebuie să analizați singur maparea într-o unealtă de diagnostic, conversia înseamnă două funcții scurte
// [MS-XLS] 2.5.129 FontIndex: 0..3 cu bază zero, > 4 cu bază unu, 4 invalid
function FontIndexToRecordNo(Ifnt: Word): Integer; // înregistrare FONT cu bază 1
begin
if Ifnt < 4 then
Result := Ifnt + 1
else if Ifnt > 4 then
Result := Ifnt
else
Result := -1; // 4 nu trebuie să apară
end;
function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
if RecordNo <= 4 then
Result := RecordNo - 1
else
Result := RecordNo;
end;
În interiorul HotXLS, aceeași regulă trăiește în două locuri în oglindă. TXLSFontList.GetSaveIndex ia poziția cu bază 1 a unui font în lista referențiată și decrementează doar pozițiile 1 până la 4, deci poziția 5 se scrie ca ifnt 5. TXLSReader.ParseXF face inversul la încărcare: orice ifnt de 5 sau peste este decrementat la un slot cu bază zero din lista de fonturi, iar ce e mai jos rămâne pe loc. Remaparea run-urilor rich din SST și CountRichRunFontRefs aplică aceeași conversie ifnt >= 5, și acesta e chiar punctul: o convenție, fiecare consumator
// TXLSFontList.GetSaveIndex (partea de scriere)
Result := inherited GetSaveIndex(Index); // poziție referențiată cu bază 1
if (Result > 0) and (Result < 5) then
Dec(Result); // 1..4 devin 0..3, 5+ neschimbate
// TXLSReader.ParseXF (partea de citire)
fnti := Data.GetWord(0);
if fnti >= 5 then
Dec(fnti); // ifnt 5 este slotul 4 din lista de fonturi
De ce o „reparare" cu bază zero a mutat fiecare font personalizat cu unul?
Rescrierea cu bază zero din HotXLS 2.384.1 a mutat fiecare font personalizat pentru că a citit un index cu bază unu ca și cum ar fi avut bază zero și apoi a schimbat patru locuri de apel ca să se potrivească cu citirea greșită: GetSaveIndex, ParseXF, remaparea run-urilor SST și migrația de run-uri între registre din Sheets.AddCopy. Round-trip-urile HotXLS arătau în continuare bine, pentru că scriitorul și cititorul erau de acord unul cu celălalt. Excel nu era de acord. Un fișier scris de 2.384.1 punea primul font personalizat la ifnt 4, pe care Excel îl tratează ca font implicit, iar fiecare font personalizat următor cu o înregistrare mai devreme; deschiderea unui fișier Excel mergea în sens invers, legând fiecare font cu o înregistrare mai târziu
Indiciul care ar fi trebuit să oprească schimbarea stătea în același codebase. CountRichRunFontRefs, remaparea FONTX și FBI pentru grafice și lista de fonturi a motorului de stiluri nu au fost atinse niciodată și foloseau în continuare regula de salt peste 4, deci biblioteca se contrazicea singură din momentul în care 2.384.1 a apărut, și doar coincidența că fonturile din rich-text erau de obicei referențiate și de vreun XF a ținut contradicția ascunsă. Când o convenție apare în șapte locuri și schimbați patru, bănuiți-vă schimbarea înainte să bănuiți celelalte trei. Versiunea 2.384.4 a restabilit numerotarea din specificație în toate cele patru locuri, iar vechiul test de regresie, care afirma ifnt < FontCount și deci codifica citirea greșită, a fost înlocuit cu teste care mapează fiecare ifnt scris înapoi la un nume de înregistrare FONT prin formula din specificație. O limitare onestă rămâne: fișierele salvate de 2.384.1 până la 2.384.3 cu cinci sau mai multe fonturi poartă indexuri derapate pe care un cititor nu le poate deosebi de date valide, deci singurul leac este să le regenerați
De ce run-urile de fonturi din comentarii se rup doar la a doua salvare?
Run-urile din comentarii și casete de text se rupeau la a doua salvare pentru că HotXLS păstra primele N-1 înregistrări FONT necondiționat și renunța doar la ultima când niciun XF o referenția, în timp ce run-urile de formatare TXO ([MS-XLS] §2.4.329) erau scrise înapoi octet cu octet fără renumerotare. Fișierele .xls create în Excel se termină mereu cu un font final nereferențiat (un DengXian de 9pt pe un sistem cu locație chineză), deci la prima salvare fontul folosit doar de un run de comentariu nu era niciodată ultimul și nimic nu se mișca vizibil. Prima salvare renunța însă la fontul final și promova fontul existent doar în comentarii pe ultima poziție. A doua salvare îl arunca apoi ca nereferențiat, ifnt-ul run-ului arăta după sfârșit, iar Excel cădea pe fontul implicit; dacă registrul de lucru câștigase între timp un font nou, run-ul se lega în tăcere de acela, ceea ce în teste a transformat un run de casetă de text stilizat în Arial. Fișierele pline de comentarii, precum cele descrise în construirea unui flux de revizuire cu comentarii și hyperlinkuri, sunt exact locul unde asta mușcă, pentru că sunt deschise, adnotate și salvate în repetiție
HotXLS 2.384.5 tratează run-urile TXO ca pe cele SST. CountRichRunFontRefs parcurge acum fiecare TMSOShapeTextBox de pe fiecare foaie de lucru, convertește ifnt-ul cu salt peste 4 al fiecărui run într-un slot și îl numără ca referință, deci un font existent doar în run-uri supraviețuiește filtrului de salvare. Tabela rezultată slot-către-index-de-salvare intră în FontRunRemap-ul fiecărui drawing, iar TMSOShapeTextBox.Store rescrie indexurile de run pe o copie privată a octeților brute ai run-urilor, lăsând TxOLastRun-ul final în pace pentru că nu poartă niciun font. Pentru codul de aplicație contractul e simplu: TXLSComment.TextRuns.FontIndex și TXLSTextBox.TextRuns.FontIndex folosesc numerotarea din fișier, cu 4 sărit, exact așa cum au fost citite; indexurile de run au bază 1, iar CharIndex este offsetul de caracter unde începe run-ul. După o salvare numărul stocat poate diferi de cel pe care l-ați setat, dar arată în continuare spre același 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]; // numerotare din fișier, cu 4 sărit
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;
Copii, inserări de rânduri și migrația de run-uri între registre de lucru
Din HotXLS 2.384.6, fiecare cale de copiere a motorului clasic păstrează run-urile de formatare ale comentariilor, pentru că Range.Copy, CopyRange, Sheets.AddCopy și deplasările de celule din spatele lui Range.Insert și Range.Delete trec toate prin TXLSRange.CopyCell, iar CopyCell copia doar textul și autorul comentariului. O deplasare este o copiere plus o ștergere, deci inserarea unui singur rând deasupra unei note cu două run-uri o lăsa cu zero run-uri și un font. Repararea copiază fiecare run și îi mută fontul prin TXLSWorkbook.MigrateRunFontIndex, care convertește indexul cu salt peste 4 într-un slot, migrează fontul după valoare în tabela de fonturi destinație și convertește înapoi la numerotarea din fișier; migrația rich-text din SST din Sheets.AddCopy apelează acum aceeași funcție în loc să-și poarte propria copie a aritmeticii. Au venit la pachet două cazuri limită: o lipire în loc în care sursa și destinația sunt același comentariu nu trebuie să îi golească run-urile înainte să le citească, iar Sheets.AddCopy face acum o a doua trecere pentru comentariile atașate celulelor fără înregistrare de celulă stocată, pe care anterior le sărea cu totul. Partea de tabelă de fonturi a copierii între registre de lucru urmează aceeași logică după valoare ca partea de formule descrisă în copiere între registre de lucru și rebinding de formule. Pe motorul XLSX căile de copiere clonau deja run-urile după valoare; golul era chiar în partea de comentarii, unde cititorul ignora rFont, strike, u și vertAlign, iar scriitorul nu emitea niciodată u sau vertAlign, deci run-urile supraviețuiesc acum simetric salvării și redeschiderii
Cum ar trebui să testați indexurile de font în fișiere BIFF8?
Testați indexurile de font salvând și redeschizând, în mod ideal pe mai mult de o generație, și mapând fiecare ifnt înapoi la o înregistrare FONT în loc să afirmați un interval numeric. Fiecare bug din povestea aceasta a trecut un test în memorie: regresia din 2.384.1 trăia într-o pereche potrivită de scriitor și cititor, derapajul TXO avea nevoie de două salvări cu o schimbare a tabelei de fonturi între ele, iar run-urile de comentarii pierdute pe XLSX apăreau doar după o redeschidere. Un harness util deschide un exemplu creat în Excel, îl salvează de două ori prin HotXLS, adaugă sau elimină un font între salvări, apoi verifică pozițiile run-urilor plus, la nivel de octeți, numele fonturilor din spatele fiecărui ifnt. Nu comparați valorile FontIndex înainte și după o salvare, pentru că renumerotarea e 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); // creat în Excel, C2 are două run-uri
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 mută în C3
Assert(Book.SaveAs(OutFile) = 1);
Book := TXLSWorkbook.Create; // redeschidere, nu aveți încredere în memorie
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;
Dacă citiți și scrieți XLS clasic din Delphi sau C++Builder și nu prea vreți să urmăriți care dintre numeroșii consumatori de fonturi ai unei biblioteci mai este de acord cu [MS-XLS] §2.5.129, numerotarea cu salt peste 4, renumerotarea run-urilor la salvare și migrația de run-uri după valoare descrise aici sunt încorporate în componenta de spreadsheet HotXLS pentru Delphi, care citește și scrie XLS și XLSX fără Excel sau automatizare OLE