HotXLS numrerar varje BIFF8-teckensnittsreferens som [MS-XLS] §2.5.129 FontIndex definierar den: värdena 0 till 3 är nollbaserade, värdena över 4 är ettbaserade, och 4 förekommer aldrig, så den femte FONT-posten är ifnt 5 och det största giltiga ifnt är lika med antalet FONT-poster. Sedan HotXLS 2.384.4 följer XF-skrivaren, XF-läsaren, rich text-sträng-runs och migreringen av runs mellan arbetsböcker den regeln, och 2.384.5 och 2.384.6 utökar den till kommentar- och textruta-runs, även genom kopieringar och radinsättningar
Regeln ser ut som ett tryckfel tills du råkar ut för den. Någon öppnar en arbetsbok med åtta FONT-poster, hittar en XF som pekar på typsnitt 8 och drar slutsatsen att skrivaren producerade ett index utanför intervallet. Precis det resonemanget låg bakom en "fix" i HotXLS 2.384.1, och den förvandlade en korrekt implementation till en där varje eget typsnitt i en Excel-öppnad fil hamnade en plats för tidigt. Det intressanta är inte off-by-one i sig, det är hur många ställen i ett BIFF8-bibliotek som bär samma konvention, och hur en typsnittsbindning kan överleva en sparning och gå sönder vid den andra. Om du redan har stridd med längd- och kodningsquirkarna som tas upp i avkodning av BIFF8 XLUnicodeString cch och fHigh, är det här samma buggfamilj: filen är fin, aritmetiken är det inte
Vad säger [MS-XLS] FontIndex-regeln egentligen?
[MS-XLS] §2.5.129 säger att ett FontIndex under 4 är en nollbaserad postposition, ett FontIndex över 4 är en ettbaserad postposition, och värdet 4 FÅR INTE användas. Samma FontIndex-typ används av XF-poster, SST-formateringsruns och TXO-formateringsruns, så en feltolkad regel förstör alla tre. Bevisen är lätta att återskapa med filer från Excel: SOLVSAMP.XLS som medföljer Office har 19 FONT-poster och ett största XF-ifnt på 19, en arbetsbok med 43 poster stannar vid 43, och en fil sparad av Excel 16 med 30 FONT-poster pekar sina Courier New-celler på ifnt 22, den 22:a posten. Ingen av dem innehåller någonsin en 4a. Om du själv behöver analysera mappningen i ett diagnostikverktyg är omvandlingen två korta funktioner
// [MS-XLS] 2.5.129 FontIndex: 0..3 nollbaserat, > 4 ettbaserat, 4 ogiltigt
function FontIndexToRecordNo(Ifnt: Word): Integer; // 1-baserad FONT-post
begin
if Ifnt < 4 then
Result := Ifnt + 1
else if Ifnt > 4 then
Result := Ifnt
else
Result := -1; // 4 får inte förekomma
end;
function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
if RecordNo <= 4 then
Result := RecordNo - 1
else
Result := RecordNo;
end;
Inuti HotXLS bor samma regel på två speglade ställen. TXLSFontList.GetSaveIndex tar den 1-baserade positionen för ett typsnitt i den refererade listan och minskar bara positionerna 1 till 4, så att position 5 skrivs som ifnt 5. TXLSReader.ParseXF gör det omvända vid inläsning: ett ifnt på 5 eller mer minskas till en nollbaserad plats i typsnittslistan, och allt därunder lämnas orört. SST rich-run-ommapningen och CountRichRunFontRefs tillämpar samma omvandling ifnt >= 5, vilket är poängen: en konvention, alla konsumenter
// TXLSFontList.GetSaveIndex (skrivarsidan)
Result := inherited GetSaveIndex(Index); // 1-baserad refererad position
if (Result > 0) and (Result < 5) then
Dec(Result); // 1..4 blir 0..3, 5+ oförändrat
// TXLSReader.ParseXF (läsarsidan)
fnti := Data.GetWord(0);
if fnti >= 5 then
Dec(fnti); // ifnt 5 är plats 4 i typsnittslistan
Varför flyttade en nollbaserad "fix" varje eget typsnitt ett steg?
Omskrivningen till nollbaserad i HotXLS 2.384.1 flyttade varje eget typsnitt eftersom den läste ett ettbaserat index som ett nollbaserat och sedan ändrade fyra anropsställen för att passa feltolkningen: GetSaveIndex, ParseXF, SST-run-ommapningen och migreringen av runs mellan arbetsböcker i Sheets.AddCopy. HotXLS rundturer såg fortfarande fina ut, för skrivare och läsare höll ihop med varandra. Excel höll inte med. En fil skriven av 2.384.1 lade första egna typsnittet på ifnt 4, som Excel behandlar som standardtypsnittet, och vartdera senare eget typsnitt en post för tidigt; att öppna en Excel-fil gick åt andra hållet och band varje typsnitt en post för sent
Varningssignalen som borde ha stoppat ändringen fanns i samma kodbas. CountRichRunFontRefs, chart-ommapningen FONTX och FBI samt style-motorns typsnittslista rördes aldrig och använde fortfarande skip-4, så biblioteket motsade sig självt i samma stund som 2.384.1 landade, och bara omständigheten att rich text-typsnitt vanligtvis också refererades av någon XF höll motsägelsen dold. När en konvention förekommer på sju ställen och du ändrar fyra, misstänk din ändring innan du misstänker de andra tre. Version 2.384.4 återställde specifikationens numrering på alla fyra ställen, och det gamla regressionstestet, som hävdade ifnt < FontCount och därmed kodade in feltolkningen, ersattes av tester som mappar vartenda skrivet ifnt tillbaka till ett FONT-postnamn via specformeln. En ärlig begränsning återstår: filer sparade av 2.384.1 till 2.384.3 med fem eller fler typsnitt bär förskjutna index som en läsare inte kan skilja från giltig data, så enda boten är att generera om dem
Varför går kommentarernas typsnittsruns sönder först vid andra sparningen?
Kommentar- och textruta-runs gick sönder vid andra sparningen eftersom HotXLS behöll de första N-1 FONT-posterna villkorslöst och kastade bara den sista när ingen XF refererade den, medan TXO-formateringsruns ([MS-XLS] §2.4.329) skrevs tillbaka byte för byte utan omnumrering. Excel-skapade .xls-filer slutar alltid med en orefererad avslutande font (en 9pt DengXian på ett kinesiskt system), så vid första sparningen var typsnittet som bara användes av en kommentarsrun aldrig sist, och ingenting flyttades synligt. Den första sparningen tappade dock sluttypsnittet och befordrade kommentarstypsnittet till sista position. Den andra sparningen kastade det sedan som orefererat, runens ifnt pekade förbi slutet, och Excel föll tillbaka på standardtypsnittet; hade arbetsboken fått ett nytt typsnitt emellan band sig runen tyst till det i stället, vilket i testerna förvandlade en formaterad textruta-run till Arial. Kommentarrika filer som de som beskrivs i att bygga ett granskningsflöde för kommentarer och hyperlänkar är precis där det här biter, för de öppnas, annoteras och sparas om och om igen
HotXLS 2.384.5 behandlar TXO-runs som SST-runs. CountRichRunFontRefs går nu igenom varje TMSOShapeTextBox på varje kalkylblad, omvandlar skip-4-ifnt för varje run till en plats och räknar den som en referens, så att ett typsnitt som bara används av runs överlever sparningsfiltret. Den framkomna plats-till-sparindex-tabellen går in i varje ritytas FontRunRemap, och TMSOShapeTextBox.Store skriver om runindexen på en privat kopia av de råa runbytena, och lämnar avslutande TxOLastRun orörd eftersom den inte bär något typsnitt. För programkod är kontraktet enkelt: TXLSComment.TextRuns.FontIndex och TXLSTextBox.TextRuns.FontIndex använder filnumreringen, med 4 överhoppad, exakt som läst; runindex är 1-baserade och CharIndex är teckenoffseten där runen börjar. Efter en sparning kan det lagrade talet skilja sig från det du satte, men det pekar fortfarande på samma typsnitt
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]; // filnumrering, 4 överhoppad
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;
Kopior, radinsättningar och runmigrering mellan arbetsböcker
Sedan HotXLS 2.384.6 bevarar varje kopieringsväg i den klassiska motorn kommentarformateringsruns, eftersom Range.Copy, CopyRange, Sheets.AddCopy och cellskiftena bakom Range.Insert och Range.Delete alla går genom TXLSRange.CopyCell, och CopyCell kopierade förr bara kommentartexten och upphovsmannen. Ett skifte är en kopia plus en rensning, så en radinsättning ovanför en anteckning med två runs lämnade den med noll runs och en font. Fixen kopierar varje run och flyttar dess font genom TXLSWorkbook.MigrateRunFontIndex, som omvandlar skip-4-indexet till en plats, migrerar fonten efter värde till destinationens typsnittstabell och omvandlar tillbaka till filnumrering; SST rich text-migreringen i Sheets.AddCopy anropar nu samma funktion i stället för att bära en egen kopia av aritmetiken. Två specialfall följde med: en inklistring på plats där källa och mål är samma kommentar får inte rensa sina runs innan de lästs, och Sheets.AddCopy gör nu ett andra varv för kommentarer fästa vid celler utan lagrad cellpost, som den tidigare hoppade över helt. Typsnittstabellssidan av kopiering mellan arbetsböcker följer samma efter-värde-logik som formelsidan som tas upp i kopiering mellan arbetsböcker och formelombindning. På XLSX-motorn klonade kopieringsvägarna redan runs efter värde; luckan fanns i kommentarsdelen själv, där läsaren ignorerade rFont, strike, u och vertAlign och skrivaren aldrig emitterade u eller vertAlign, så runs överlever nu sparning och återöppning symmetriskt
Hur ska du testa typsnittsindex i BIFF8-filer?
Testa typsnittsindex genom att spara och öppna igen, helst över mer än en generation, och genom att mappa varje ifnt tillbaka till en FONT-post i stället för att hävda ett numeriskt intervall. Varje bugg i den här historiken gick igenom ett minnesinternt test: regressionen i 2.384.1 bodde i ett matchat skrivar- och läsapar, TXO-driften behövde två sparningar med en typsnittstabellsändring emellan, och att kommentarsruns försvann på XLSX syntes först efter en återöppning. Ett nyttigt testramverk öppnar ett Excel-skapat exempel, sparar det två gånger genom HotXLS, lägger till eller tar bort en font mellan sparningarna och kontrollerar sedan runpositionerna plus, på bytenivå, typsnittsnamnen bakom varje ifnt. Jämför inte FontIndex-värden före och efter en sparning, eftersom omnumrering är 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); // från Excel, C2 har två runs
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 flyttas till C3
Assert(Book.SaveAs(OutFile) = 1);
Book := TXLSWorkbook.Create; // öppna igen, lita aldrig på minnet
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;
Om du läser och skriver klassisk XLS från Delphi eller C++Builder och helst inte vill hålla reda på vilken av ett biblioteks många typsnittskonsumenter som fortfarande håller med [MS-XLS] §2.5.129, är skip-4-numreringen, runomnumreringen vid sparning och efter-värde-migreringen av runs som beskrivs här inbyggda i HotXLS Delphi-kalkylbladskomponenten, som läser och skriver XLS och XLSX utan Excel eller OLE-automatisering