HotXLS nummert elke BIFF8-fontverwijzing zoals [MS-XLS] §2.5.129 FontIndex die definieert: waarden 0 tot 3 zijn zero-based, waarden boven 4 zijn one-based, en 4 komt nooit voor, dus het vijfde FONT-record is ifnt 5 en de grootste geldige ifnt is gelijk aan het aantal FONT-records. Sinds HotXLS 2.384.4 volgen de XF-writer, de XF-reader, rich text-string-runs en de migratie van runs over werkmappen heen die regel allemaal, en 2.384.5 en 2.384.6 breiden haar uit naar commentaar- en tekstvak-runs, ook door kopieën en rij-inservingen heen
De regel oogt als een typefout totdat u haar tegenkomt. Iemand opent een werkmap met acht FONT-records, vindt een XF die naar font 8 wijst, en concludeert dat de writer een out-of-range-index heeft geproduceerd. Precies dat redeneerschip verscheen in HotXLS 2.384.1 als "fix", en het veranderde een correcte implementatie in er een waarin elke custom font in een door Excel geopend bestand één plek te vroeg belandde. Het interessante is niet de off-by-one zelf, het is hoeveel plekken in een BIFF8-library dezelfde conventie dragen, en hoe een fontbinding één opslag kan overleven en bij de tweede breekt. Als u al eerder hebt gestreden met de lengte- en encoding-fratsen uit het decoderen van BIFF8 XLUnicodeString cch en fHigh, dit is dezelfde bugfamilie: het bestand is in orde, de rekenkunde niet
Wat zegt de FontIndex-regel uit [MS-XLS] nu precies?
[MS-XLS] §2.5.129 zegt dat een FontIndex onder 4 een zero-based recordpositie is, een FontIndex boven 4 een one-based recordpositie, en dat de waarde 4 NIET gebruikt mag worden. Hetzelfde FontIndex-type wordt gebruikt door XF-records, SST-opmaakruns en TXO-opmaakruns, dus één verkeerd gelezen regel corrumpeert alle drie. Het bewijs is makkelijk te reproduceren met door Excel gemaakte bestanden: SOLVSAMP.XLS die bij Office zit heeft 19 FONT-records en een maximale XF-ifnt van 19, een werkmap met 43 records komt niet boven 43 uit, en een door Excel 16 opgeslagen bestand met 30 FONT-records wijst zijn Courier New-cellen naar ifnt 22, het 22ste record. Geen van allen bevat ooit een 4. Wilt u de toewijzing zelf analyseren in een diagnostisch hulpmiddel, de conversie is twee korte functies
// [MS-XLS] 2.5.129 FontIndex: 0..3 zero-based, > 4 one-based, 4 ongeldig
function FontIndexToRecordNo(Ifnt: Word): Integer; // 1-based FONT-record
begin
if Ifnt < 4 then
Result := Ifnt + 1
else if Ifnt > 4 then
Result := Ifnt
else
Result := -1; // 4 mag niet voorkomen
end;
function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
if RecordNo <= 4 then
Result := RecordNo - 1
else
Result := RecordNo;
end;
Binnen HotXLS woont dezelfde regel op twee gespiegelde plekken. TXLSFontList.GetSaveIndex neemt de one-based positie van een font in de verwezen lijst en verlaagt alleen posities 1 tot 4, dus positie 5 wordt weggeschreven als ifnt 5. TXLSReader.ParseXF doet bij het laden het omgekeerde: elke ifnt van 5 of hoger wordt verlaagd naar een zero-based fontlijst-plek, en alles daaronder blijft staan. De SST-rich-run-herafbakening en CountRichRunFontRefs passen dezelfde ifnt >= 5-conversie toe, en dat is het punt: één conventie, elke consument
// TXLSFontList.GetSaveIndex (kant van de writer)
Result := inherited GetSaveIndex(Index); // 1-based verwezen positie
if (Result > 0) and (Result < 5) then
Dec(Result); // 1..4 worden 0..3, 5+ onveranderd
// TXLSReader.ParseXF (kant van de reader)
fnti := Data.GetWord(0);
if fnti >= 5 then
Dec(fnti); // ifnt 5 is fontlijst-plek 4
Waarom schoof een zero-based "fix" elke custom font één plek op?
De zero-based-herziening in HotXLS 2.384.1 verschoof elke custom font omdat hij een one-based index als zero-based las en daarna vier aanroepplekken aanpaste aan die verkeerde lezing: GetSaveIndex, ParseXF, de SST-run-herafbakening en de migratie van runs over werkmappen in Sheets.AddCopy. HotXLS-roundtrips zagen er nog prima uit, want writer en reader waren het met elkaar eens. Excel was het er niet mee eens. Een door 2.384.1 geschreven bestand zette de eerste custom font op ifnt 4, wat Excel als het standaardfont behandelt, en elk latere custom font één record te vroeg; het openen van een Excel-bestand ging de andere kant op en bond elk font één record te laat
Het signaal dat de wijziging had moeten stoppen, stond in dezelfde codebase. CountRichRunFontRefs, de chart-FONTX- en FBI-herafbakening en de fontlijst van de style-engine werden nooit aangeraakt en gebruikten nog steeds skip-4, dus de library sprak zichzelf tegen zodra 2.384.1 binnenkwam, en alleen het toeval dat rich-text-fonts meestal ook door een of andere XF werden verwezen hield de tegenstelling verborgen. Als één conventie op zeven plekken opduikt en u wijzigt er vier, verdenk dan uw eigen wijziging vóórdat u de andere drie verdenkt. Versie 2.384.4 herstelde de spec-nummering op alle vier de plekken, en de oude regressietest, die ifnt < FontCount eiste en daarmee de verkeerde lezing inkodificeerde, werd vervangen door tests die elke weggeschreven ifnt via de spec-formule terug afbeelden op een FONT-recordnaam. Eén eerlijke beperking blijft: bestanden die door 2.384.1 tot en met 2.384.3 zijn opgeslagen met vijf of meer fonts dragen verschoven indexen die een reader niet van geldige data kan onderscheiden, dus het enige geneesmiddel is ze opnieuw te genereren
Waarom breken commentaar-fontruns pas bij de tweede opslag?
Commentaar- en tekstvak-runs braken bij de tweede opslag omdat HotXLS de eerste N-1 FONT-records onvoorwaardelijk hield en alleen de laatste liet vallen wanneer geen XF ernaar verwees, terwijl de TXO-opmaakruns ([MS-XLS] §2.4.329) byte voor byte werden teruggeschreven zonder hernummering. Door Excel gemaakte .xls-bestanden eindigen altijd met een niet-verwezen achterste font (een 9pt-DengXian op een systeem met Chinese locale), dus bij de eerste opslag was het font dat alleen door een commentaarrun werd gebruikt nooit het laatste, en er verschoof niets zichtbaar. Die eerste opslag liet het achterste font wel vallen en promoveerde het alleen-door-commentaar-gebruikte font naar de laatste positie. De tweede opslag gooide het toen weg als niet-verwezen, de ifnt van de run wees voorbij het einde, en Excel viel terug op het standaardfont; had de werkmap intussen een nieuw font gekregen, dan bond de run geruisloos aan dat font, wat in de test een opgemaakte tekstvak-run in Arial veranderde. Bestanden vol commentaar zoals die in het bouwen van een review-workflow met commentaar en hyperlinks zijn precies waar dit bijt, want die worden herhaaldelijk geopend, van aantekeningen voorzien en opgeslagen
HotXLS 2.384.5 behandelt TXO-runs zoals SST-runs. CountRichRunFontRefs loopt nu elke TMSOShapeTextBox op elk werkblad af, zet de skip-4-ifnt van elke run om naar een plek en telt haar als referentie, dus een font dat alleen door runs wordt gebruikt overleeft het opslagfilter. De ontstane plek-naar-opslagindex-tabel gaat de FontRunRemap van elke tekening in, en TMSOShapeTextBox.Store herschrijft de run-indexen op een privé-kopie van de ruwe run-bytes, met de achterste TxOLastRun met rust omdat die geen font draagt. Voor applicatiecode is het contract eenvoudig: TXLSComment.TextRuns.FontIndex en TXLSTextBox.TextRuns.FontIndex gebruiken de bestandsnummering, 4 overgeslagen, precies zoals gelezen; run-indexen zijn one-based en CharIndex is de tekenoffset waar de run begint. Na een opslag kan het opgeslagen nummer afwijken van het door u gezette, maar het wijst nog steeds naar hetzelfde 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]; // bestandsnummering, 4 overgeslagen
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;
Kopieën, rij-inservingen en migratie van runs over werkmappen
Sinds HotXLS 2.384.6 houdt elk kopieerpad van de klassieke engine de opmaakruns van commentaar vast, want Range.Copy, CopyRange, Sheets.AddCopy en de celverschuivingen achter Range.Insert en Range.Delete lopen allemaal door TXLSRange.CopyCell, en CopyCell kopieerde voorheen alleen de commentaartekst en de auteur. Een verschuiving is een kopie plus een leegmaakactie, dus het inservoegen van één rij boven een notitie met twee runs liet die achter met nul runs en één font. De fix kopieert elke run en verplaatst haar font via TXLSWorkbook.MigrateRunFontIndex, dat de skip-4-index naar een plek omzet, het font op waarde naar de doelfonttabel migreert en terugconverteert naar bestandsnummering; de SST-rich-text-migratie in Sheets.AddCopy roept nu dezelfde functie aan in plaats van haar eigen exemplaar van de rekenkunde mee te slepen. Er kwamen twee randgevallen mee: een in-place plakking waarbij bron en doel hetzelfde commentaar zijn mag zijn runs niet leegmaken vóórdat hij ze leest, en Sheets.AddCopy maakt nu een tweede ronde voor commentaar dat aan cellen zonder opgeslagen celrecord hangt, wat voorheen geheel werd overgeslagen. De fonttabelkant van kopiëren over werkmappen volgt dezelfde logica op waarde als de formulekant uit kopiëren over werkmappen en formule-herbinding. Op de XLSX-engine kloonden de kopieerpaden runs al op waarde; het gat zat in het commentaardeel zelf, waar de reader rFont, strike, u en vertAlign negeerde en de writer u of vertAlign nooit uitzond, dus de runs overleven opslag en heropenen nu symmetrisch
Hoe test u fontindexen in BIFF8-bestanden?
Test fontindexen door op te slaan en te heropenen, bij voorkeur over meer dan één generatie, en door elke ifnt terug af te beelden op een FONT-record in plaats van een numeriek bereik te eisen. Elke bug in dit verhaal haalde een in-memory test: de 2.384.1-regressie woonde in een op elkaar afgestemd writer-reader-paar, de TXO-drift had twee opslagen met een fonttabelwijziging ertussen nodig, en de verloren commentaarruns op XLSX kwamen pas na een heropenen boven water. Een nuttig harnas opent een door Excel gemaakt voorbeeldbestand, slaat het twee keer door HotXLS op, voegt tussen de opslagen door een font toe of verwijdert er een, en controleert dan de runposities plus, op byteniveau, de fontnamen achter elke ifnt. Vergelijk FontIndex-waarden niet vóór en na een opslag, want hernummering is legitiem
procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
Book: IXLSWorkbook;
Note: TXLSComment;
RunCount: Integer;
SecondRunAt: Word;
begin
Book := TXLSWorkbook.Create;
Assert(Book.Open(SrcFile) = 1); // door Excel gemaakt, C2 heeft twee 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 schuift naar C3
Assert(Book.SaveAs(OutFile) = 1);
Book := TXLSWorkbook.Create; // heropen, vertrouw geheugen nooit
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;
Leest en schrijft u klassieke XLS vanuit Delphi of C++Builder en wilt u liever niet bijhouden welke van de vele fontconsumenten van een library nog met [MS-XLS] §2.5.129 meeloopt, dan zit de skip-4-nummering, de run-hernummering bij opslag en de run-migratie op waarde die hier is beschreven ingebouwd in de HotXLS Delphi-spreadsheetcomponent, die XLS en XLSX leest en schrijft zonder Excel of OLE-automatisering