Teknisk artikkel

BIFF8-fontindeks hopper over 4: rich text-runs i Delphi

HotXLS nummererer hver BIFF8-fontreferanse slik [MS-XLS] §2.5.129 FontIndex definerer det: verdier 0 til 3 er nullbaserte, verdier over 4 er enbaserte, og 4 dukker aldri opp, så den femte FONT-recorden er ifnt 5 og den største gyldige ifnt er lik antall FONT-records. Siden HotXLS 2.384.4 følger XF-writeren, XF-leseren, rich text-streng-runs og migreringen av runs på tvers av arbeidsbøker den regelen, og 2.384.5 og 2.384.6 strekker den til kommentar- og tekstboks-runs, også gjennom kopieringer og radinnsettinger

Regelen ser ut som en skrivefeil helt til du treffer den. Noen åpner en arbeidsbok med åtte FONT-records, finner en XF som peker på font 8, og konkluderer med at writeren produserte en indeks utenfor gyldig område. Nettopp det resonnementet ble sendt ut i HotXLS 2.384.1 som en «fiks», og den gjorde en korrekt implementasjon om til én der hver egendefinerte font i en Excel-åpnet fil landet én plass for tidlig. Det interessante er ikke selve feilen på én, det er hvor mange steder i et BIFF8-bibliotek som bærer samme konvensjon, og hvordan en fontbinding kan overleve én lagring og gå i stykker på den andre. Har du allerede kjempet med lengde- og encoding-quirkene dekket i dekoding av BIFF8 XLUnicodeString cch og fHigh, er dette samme feilfamilie: filen er fin, aritmetikken er det ikke

Hva sier egentlig FontIndex-regelen i [MS-XLS]?

[MS-XLS] §2.5.129 sier at en FontIndex under 4 er en nullbasert record-posisjon, en FontIndex over 4 er en enbasert record-posisjon, og verdien 4 MÅ IKKE brukes. Samme FontIndex-type brukes av XF-records, SST formaterings-runs og TXO formaterings-runs, så én feiltolket regel ødelegger alle tre. Beviset er lett å reprodusere med Excel-genererte filer: SOLVSAMP.XLS som fulgte med Office har 19 FONT-records og en maksimal XF ifnt på 19, en arbeidsbok med 43 records topper på 43, og en fil lagret av Excel 16 med 30 FONT-records peker Courier New-cellene sine på ifnt 22, den 22. recorden. Ingen av dem inneholder noensinne en 4. Trenger du å analysere mappingen selv i et diagnostisk verktøy, er konverteringen to korte funksjoner

// [MS-XLS] 2.5.129 FontIndex: 0..3 nullbasert, > 4 enbasert, 4 ugyldig
function FontIndexToRecordNo(Ifnt: Word): Integer;  // enbasert FONT-record
begin
  if Ifnt < 4 then
    Result := Ifnt + 1
  else if Ifnt > 4 then
    Result := Ifnt
  else
    Result := -1;  // 4 må ikke forekomme
end;

function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
  if RecordNo <= 4 then
    Result := RecordNo - 1
  else
    Result := RecordNo;
end;
HotXLS FontIndex-mapping etter MS-XLS 2.5.129 der ifnt 0 til 3 er nullbaserte FONT-record-posisjoner, ifnt 5 og oppover er enbasert og verdien 4 aldri forekommer, med FontIndexToRecordNo-konverteringen og bevis fra Excel-genererte arbeidsbøker som SOLVSAMP.XLS
Den femte FONT-recorden er ifnt 5, ikke 4 — en arbeidsbok med 19 records topper på ifnt 19, og ingen Excel-generert fil lagrer noensinne den forbudte verdien imellom

Inni HotXLS bor samme regel på to speilvendte steder. TXLSFontList.GetSaveIndex tar den 1-baserte posisjonen til en font i listen det refereres til og reduserer bare posisjonene 1 til 4, så posisjon 5 skrives som ifnt 5. TXLSReader.ParseXF gjør det motsatte ved lasting: enhver ifnt på 5 eller mer reduseres til en nullbasert plass i fontlisten, og alt under forblir urørt. SST rich-run-remappingen og CountRichRunFontRefs anvender samme ifnt >= 5-konvertering, og det er poenget: én konvensjon, alle forbrukere

// TXLSFontList.GetSaveIndex (writersiden)
Result := inherited GetSaveIndex(Index);   // 1-basert referert posisjon
if (Result > 0) and (Result < 5) then
  Dec(Result);                             // 1..4 blir 0..3, 5+ uendret

// TXLSReader.ParseXF (lesersiden)
fnti := Data.GetWord(0);
if fnti >= 5 then
  Dec(fnti);                               // ifnt 5 er fontliste-plass 4

Hvorfor flyttet en nullbasert «fiks» hver egendefinerte font med én?

Omskrivingen til nullbasert i HotXLS 2.384.1 flyttet hver egendefinerte font fordi den leste en enbasert indeks som nullbasert og så endret fire kallsteder for å passe til den feiltolkningen: GetSaveIndex, ParseXF, SST run-remappingen og migreringen av runs på tvers av arbeidsbøker i Sheets.AddCopy. HotXLS-rundturer så fortsatt fine ut, fordi writer og leser var enige med hverandre. Excel var ikke enig. En fil skrevet av 2.384.1 la den første egendefinerte fonten på ifnt 4, som Excel behandler som standardfonten, og hver senere egendefinerte font én record for tidlig; åpning av en Excel-fil gikk den andre veien og bandt hver font én record for sent

HotXLS 2.384.1-regresjon der GetSaveIndex og ParseXF leste enbaserte FontIndex-verdier som nullbaserte, skrev den første egendefinerte fonten som den forbudte ifnt 4 som Excel løser til standardfonten, og landet hver senere font én record for tidlig mens rundturene fortsatt passerte
Writer og leser var enige om samme feiltolkning, så en lagre-og-gjenåpne-test forble grønn mens hver Excel-åpnet font landet én plass feil — når én konvensjon bor syv steder og du endrer fire, mistenk endringen din først

Det som skulle ha stoppet endringen, lå i samme kodebase. CountRichRunFontRefs, FONTX- og FBI-remappingen for chart og fontlisten i style-motoren ble aldri rørt og brukte fortsatt skip-4, så biblioteket motsa seg selv i det øyeblikket 2.384.1 landet, og bare tilfeldigheten at rich text-fonter vanligvis også ble referert av en eller annen XF, holdt motsetningen skjult. Når én konvensjon opptrer syv steder og du endrer fire, mistenk endringen din før du mistenker de andre tre. Versjon 2.384.4 gjenopprettet spesifikasjonsnummereringen på alle fire stedene, og den gamle regresjonstesten, som assertet ifnt < FontCount og dermed innkodet feiltolkningen, ble erstattet av tester som mapper hver skrevne ifnt tilbake til et FONT-record-navn gjennom spesifikasjonsformelen. Én ærlig begrensning gjenstår: filer lagret av 2.384.1 til 2.384.3 med fem eller flere fonter bærer forskjøvede indekser som en leser ikke kan skille fra gyldige data, så den eneste kuren er å generere dem på nytt

Hvorfor går kommentar-font-runs i stykker først ved andre lagring?

Kommentar- og tekstboks-runs gikk i stykker ved andre lagring fordi HotXLS beholdt de første N-1 FONT-recordene betingelsesløst og bare slapp den siste når ingen XF refererte den, mens TXO formaterings-runsene ([MS-XLS] §2.4.329) ble skrevet tilbake byte for byte uten omnummerering. Excel-genererte .xls-filer ender alltid med en ureferert etterfølgende font (en 9pt DengXian på et system med kinesisk locale), så ved første lagring var fonten brukt bare av en kommentar-run aldri sist, og ingenting flyttet seg synlig. Den første lagringen slapp imidlertid etterfølgerfonten og forfremmet fonten brukt bare av kommentaren til siste posisjon. Den andre lagringen forkastet den så som ureferert, runens ifnt pekte forbi slutten, og Excel falt tilbake til standardfonten; hadde arbeidsboken fått en ny font i mellomtiden, bandt runen seg stille til den i stedet, noe som i testing gjorde en stylet tekstboks-run om til Arial. Kommentartunge filer som de beskrevet i å bygge en arbeidsflyt for kommentarer og hyperkoblinger er akkurat der dette biter, for de åpnes, annoteres og lagres om og om igjen

HotXLS fonttabell over to lagringer der den urefererte etterfølgende FONT-recorden Excel alltid skriver slippes først, fonten brukt bare av kommentaren blir sist og forkastes så fordi TXO formaterings-runs ble skrevet tilbake uten å telle referanser, helt til CountRichRunFontRefs fikset overlevelsesfilteret i 2.384.5
Den første lagringen så ren ut fordi etterfølgerfonten tok tapet, og kommentarfonten forsvant først på den andre — map hver ifnt til et FONT-record-navn på tvers av lagringer i stedet for å stole på en test i minnet

HotXLS 2.384.5 behandler TXO-runs som SST-runs. CountRichRunFontRefs går nå gjennom hver TMSOShapeTextBox på hvert regneark, konverterer hver runs skip-4 ifnt til en plass og teller den som en referanse, så en font brukt bare av runs overlever lagringsfilteret. Den resulterende plass-til-lagringsindeks-tabellen går inn i hver tegnings FontRunRemap, og TMSOShapeTextBox.Store skriver om run-indeksene på en privat kopi av de rå run-bytene, og lar den etterfølgende TxOLastRun være i fred fordi den ikke bærer noen font. For applikasjonskode er kontrakten enkel: TXLSComment.TextRuns.FontIndex og TXLSTextBox.TextRuns.FontIndex bruker filnummereringen, med 4 hoppet over, nøyaktig som lest; run-indekser er 1-baserte og CharIndex er tegn-offsetet der runen starter. Etter en lagring kan det lagrede tallet avvike fra det du satte, men det peker fortsatt på samme 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];   // filnummerering, 4 hoppet over
      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;

Kopieringer, radinnsettinger og run-migrering på tvers av arbeidsbøker

Siden HotXLS 2.384.6 beholder hver kopieringsvei i classic-motoren kommentarenes formaterings-runs, fordi Range.Copy, CopyRange, Sheets.AddCopy og celleskiftene bak Range.Insert og Range.Delete alle går gjennom TXLSRange.CopyCell, og CopyCell kopierte bare kommentarteksten og forfatteren. Et skift er en kopi pluss en tømming, så å sette inn én enkelt rad over et notat med to runs etterlot det med null runs og én font. Fiksen kopierer hver run og flytter fonten dens gjennom TXLSWorkbook.MigrateRunFontIndex, som konverterer skip-4-indeksen til en plass, migrerer fonten etter verdi inn i måltabellen og konverterer tilbake til filnummerering; SST rich text-migreringen i Sheets.AddCopy kaller nå samme funksjon i stedet for å bære sin egen kopi av aritmetikken. To kanttilfeller fulgte med: en liming på stedet der kilde og mål er samme kommentar, må ikke tømme runsene sine før de er lest, og Sheets.AddCopy tar nå et andre pass for kommentarer festet til celler uten lagret cellerecord, noe den tidligere hoppet helt over. Fonttabelldelen av kopiering på tvers av arbeidsbøker følger samme etter-verdi-logikk som formeldelen dekket i kopiering på tvers av arbeidsbøker og rebinding av formler. På XLSX-motoren klonet kopieringsveiene allerede runs etter verdi; hullet lå i selve kommentardelen, der leseren ignorerte rFont, strike, u og vertAlign og writeren aldri skrev u eller vertAlign, så runsene overlever nå lagring og gjenåpning symmetrisk

Hvordan bør du teste fontindekser i BIFF8-filer?

Test fontindekser ved å lagre og gjenåpne, helst over mer enn én generasjon, og ved å mappe hver ifnt tilbake til en FONT-record i stedet for å asserte et numerisk område. Hver feil i denne historien passerte en test i minnet: 2.384.1-regresjonen levde i et matchet writer- og leserpar, TXO-driften trengte to lagringer med en fonttabellendring imellom, og de tapte kommentar-runsene på XLSX viste seg først etter en gjenåpning. Et nyttig harness åpner en Excel-generert prøvefil, lagrer den to ganger gjennom HotXLS, legger til eller fjerner en font mellom lagringene, og sjekker deretter run-posisjoner pluss, på bytenivå, fontnavnene bak hver ifnt. Ikke sammenlign FontIndex-verdier før og etter en lagring, siden omnummerering er 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);          // Excel-generert, C2 har to 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 flyttes til C3
  Assert(Book.SaveAs(OutFile) = 1);

  Book := TXLSWorkbook.Create;               // gjenåpning, stol aldri 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;

Leser og skriver du classic XLS fra Delphi eller C++Builder og orker ikke å holde styr på hvilke av bibliotekets mange fontforbrukere som fortsatt er enige med [MS-XLS] §2.5.129, er skip-4-nummereringen, omnummereringen av runs ved lagring og etter-verdi-migreringen av runs beskrevet her innebygd i HotXLS Delphi regnearkkomponent, som leser og skriver XLS og XLSX uten Excel eller OLE automation