Teknisk artikel

BIFF8-teckensnittsindex hoppar över 4 i Delphi med HotXLS

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;
HotXLS FontIndex-mappning enligt MS-XLS 2.5.129 där ifnt 0 till 3 är nollbaserade FONT-postpositioner, ifnt 5 och uppåt är ettbaserade och värdet 4 aldrig förekommer, med omvandlingen FontIndexToRecordNo och bevis från Excel-skapade arbetsböcker som SOLVSAMP.XLS
Den femte FONT-posten är ifnt 5, inte 4 — en arbetsbok med 19 poster stannar vid ifnt 19, och ingen Excel-skapad fil lagrar någonsin det förbjudna värdet emellan

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

Regressionen i HotXLS 2.384.1 där GetSaveIndex och ParseXF läser ettbaserade FontIndex-värden som nollbaserade, skriver första egna typsnittet som det förbjudna ifnt 4 som Excel upplöser till standardtypsnittet och landar med varje senare typsnitt en post för tidigt medan rundturer fortfarande gick igenom
Skrivare och läsare höll med om samma feltolkning, så ett spara-sedan-öppna-test förblev grönt medan varje Excel-öppnat typsnitt hamnade en plats fel — när en konvention bor på sju ställen och du ändrar fyra, misstänk din egen ändring först

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 typsnittstabell över två sparningar där den orefererade avslutande FONT-post som Excel alltid skriver tappas först, kommentarstypsnittet blir sist och sedan kasseras eftersom TXO-formateringsruns skrevs tillbaka utan referensräkning, tills CountRichRunFontRefs fixade överlevnadsfiltret i 2.384.5
Den första sparningen såg ren ut eftersom sluttypsnittet tog smällen, och kommentarstypsnittet försvann först vid den andra — mappa varje ifnt till ett FONT-postnamn över sparningar i stället för att lita på ett minnesinternt test

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