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;
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
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 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