Tekninen artikkeli

BIFF8 FontIndex ohittaa 4: HotXLS:n rich text -runit

HotXLS numeroi jokaisen BIFF8-fonttivittauksen siten kuin [MS-XLS] §2.5.129 FontIndex määrittelee: arvot 0–3 lasketaan nollasta, arvot yli 4:n ykkösestä, ja 4 ei koskaan esiinny, joten viides FONT-tietue on ifnt 5 ja suurin kelvollinen ifnt on yhtä suuri kuin FONT-tietueiden määrä. HotXLS 2.384.4:stä alkaen XF-kirjoittaja, XF-lukija, rich text -merkkijonojen runit ja työkirjojen välinen run-migraatio noudattavat kaikkia tätä sääntöä, ja 2.384.5 ja 2.384.6 laajentavat sen kommentti- ja tekstilaatikkoruneihin, myös kopioiden ja rivien lisäysten läpi

Sääntö näyttää kirjoitusvirheeltä kunnes törmäät siihen. Joku avaa työkirjan jolla on kahdeksan FONT-tietuetta, löytää XF:n joka osoittaa fonttiin 8, ja päätteli kirjoittajan tuottaneen alueen ulkopuolisen indeksin. Juuri tuo päättely lähti matkaan HotXLS 2.384.1:ssä korjauksena, ja se muutti oikean toteutuksen sellaiseksi, että jokainen mukautettu fontti Excelillä avatussa tiedostossa laskeutui yhden paikan liian aikaisin. Kiinnostava osa ei ole off-by-one sinänsä, vaan se kuinka monessa paikassa BIFF8-kirjastossa sama konventio asuu, ja kuinka fonttisidennäisyys voi selvitä yhdestä tallennuksesta ja hajota toisella. Jos olet jo tapellut pituuden ja enkoodauksen erikoisuuksien kanssa artikkelissa BIFF8 XLUnicodeStringin cch:n ja fHigh:n dekoodauksesta käsiteltyjen asioiden parissa, tämä on samaa bugisukua: tiedosto on kunnossa, aritmetiikka ei ole

Mitä [MS-XLS]:n FontIndex-sääntö oikeasti sanoo?

[MS-XLS] §2.5.129 sanoo, että alle 4:n FontIndex on nollapohjainen tietuepaikka, yli 4:n FontIndex yhdestä laskettava tietuepaikka, ja arvoa 4 EI SAA käyttää. Samaa FontIndex-tyyppiä käyttävät XF-tietueet, SST-muotoilurunit ja TXO-muotoilurunit, joten yksi väärin luettu sääntö turmelee ne kaikki kolme. Todisteet ovat helppo toistaa Excelillä tehdyillä tiedostoilla: Officen mukana toimitettava SOLVSAMP.XLS sisältää 19 FONT-tietuetta ja suurin XF ifnt on 19, 43 tietueen työkirja pysähtyy lukemaan 43, ja Excel 16:lla tallennettu tiedosto jolla on 30 FONT-tietuetta osoittaa Courier New -solunsa ifnt 22:een, 22. tietueeseen. Mikään niistä ei koskaan sisällä nelosta. Jos sinun täytyy itse analysoida indeksien kuvausta diagnostiikkatyökalussa, konversio on kaksi lyhyttä funktiota

// [MS-XLS] 2.5.129 FontIndex: 0..3 nollapohjainen, > 4 ykkösestä, 4 virheellinen
function FontIndexToRecordNo(Ifnt: Word): Integer;  // FONT-tietue ykkösestä
begin
  if Ifnt < 4 then
    Result := Ifnt + 1
  else if Ifnt > 4 then
    Result := Ifnt
  else
    Result := -1;  // 4 ei saa esiintyä
end;

function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
  if RecordNo <= 4 then
    Result := RecordNo - 1
  else
    Result := RecordNo;
end;
HotXLS:n FontIndex-kuvaus MS-XLS 2.5.129:n mukaan: ifnt 0–3 ovat nollapohjaisia FONT-tietuepaikkoja, ifnt 5 ja siitä ylöspäin lasketaan ykkösestä ja arvo 4 ei koskaan esiinny, mukana FontIndexToRecordNo-konversio ja todisteet Excelillä tehdyistä työkirjoista kuten SOLVSAMP.XLS
Viides FONT-tietue on ifnt 5, ei 4 — 19 tietueen työkirja pysähtyy ifnt 19:ään, eikä yksikään Excelillä tehty tiedosto koskaan tallenna kiellettyä arvoa väliin

HotXLS:n sisällä sama sääntö asuu kahdessa peilikuvapaikassa. TXLSFontList.GetSaveIndex ottaa fontin yhdestä lasketun paikan viitatussa luettelossa ja vähentää yhden vain paikoista 1–4, joten paikka 5 kirjoitetaan muotoon ifnt 5. TXLSReader.ParseXF tekee käänteisen operaation luettaessa: mikä tahansa ifnt vähintään 5 vähennetään nollapohjaiseksi fonttiluettelon paikaksi, ja kaikki sitä alemmat jäävät ennalleen. SST:n rich-run-uudelleenkartoitus ja CountRichRunFontRefs soveltavat samaa ifnt >= 5 -konversiota, ja se on koko pointti: yksi konventio, jokainen kuluttaja

// TXLSFontList.GetSaveIndex (kirjoittajan puoli)
Result := inherited GetSaveIndex(Index);   // viitattu paikka ykkösestä
if (Result > 0) and (Result < 5) then
  Dec(Result);                             // 1..4 muuttuu 0..3:ksi, 5+ ennallaan

// TXLSReader.ParseXF (lukijan puoli)
fnti := Data.GetWord(0);
if fnti >= 5 then
  Dec(fnti);                               // ifnt 5 on fonttiluettelon paikka 4

Miksi nollapohjainen "korjaus" siirsi jokaista mukautettua fonttia yhdellä?

Nollapohjainen uudelleenkirjoitus HotXLS 2.384.1:ssä siirsi jokaista mukautettua fonttia, koska se luki yhdestä lasketun indeksin nollapohjaisena ja muutti sitten neljä kutsupaikkaa vastaamaan tuota väärinlukemaa: GetSaveIndex, ParseXF, SST-runkien uudelleenkartoitus ja työkirjojen välinen run-migraatio metodissa Sheets.AddCopy. HotXLS:n omat round tripit näyttivät yhä hyviltä, koska kirjoittaja ja lukija olivat keskenään samaa mieltä. Excel ei ollut samaa mieltä. 2.384.1:n kirjoittama tiedosto pani ensimmäisen mukautetun fontin arvoon ifnt 4, jota Excel pitää oletusfonttina, ja jokainen myöhempi mukautettu fontti yhden tietueen liian aikaisin; Excel-tiedoston avaaminen meni toisin päin ja kytki jokaisen fontin yhden tietueen liian myöhään

HotXLS 2.384.1:n regressio jossa GetSaveIndex ja ParseXF lukevat yhdestä lasketut FontIndex-arvot nollapohjaisina, kirjoittaen ensimmäisen mukautetun fontin kiellettynä ifnt 4:nä jonka Excel tulkitsee oletusfontiksi ja laskeutuen jokaisella myöhemmällä fontilla yhden tietueen liian aikaisin, vaikka round tripit menivät yhä läpi
Kirjoittaja ja lukija olivat samaa mieltä samasta väärinlukemasta, joten tallenna ja avaa uudelleen -testi pysyi vihreänä kun taas jokainen Excelillä avattu fontti laskeutui yhden paikan ohi — kun yksi konventio asuu seitsemässä paikassa ja muutat neljää, epäile ensin omaa muutostasi

Väärinlukeman joka olisi pysäyttänyt muutoksen olisi löytynyt samasta koodikannasta. CountRichRunFontRefs, kaavioiden FONTX- ja FBI-uudelleenkartoitus sekä tyylikoneen fonttiluettelo jäivät koskemattomiksi ja käyttivät yhä nelosen ohitusta, joten kirjasto ajatteli itsensä vastaan heti kun 2.384.1 laskeutui, ja vain se sattuma että rich text -fontteihin viittasi yleensä jokin XF piti ristiriidan piilossa. Kun yksi konventio esiintyy seitsemässä paikassa ja muutat neljää, epäile muutostasi ennen kuin epäilet kolmea muuta. Versio 2.384.4 palautti spekin numeroinnin kaikissa neljässä paikassa, ja vanha regressiotesti, joka assertoi ifnt < FontCount ja siten koodasi väärinlukeman sisäänsä, korvattiin testeillä jotka kuvaavat jokaisen kirjoitetun ifnt:n takaisin FONT-tietueen nimeen spekin kaavalla. Yksi rehellinen rajoitus jää: 2.384.1–2.384.3-versioilla viidellä tai useammalla fontilla tallennetuissa tiedostoissa on siirtyneitä indeksejä, joita lukija ei erota kelvollisesta datasta, joten ainoa parannus on luoda ne uudelleen

Miksi kommenttien fonttirunit hajoavat vasta toisella tallennuksella?

Kommentti- ja tekstilaatikkorunit hajoivat toisella tallennuksella, koska HotXLS piti ensimmäiset N-1 FONT-tietuetta ehdottomasti ja pudotti vain viimeisen kun yksikään XF ei viitannut siihen, kun taas TXO-muotoilurunit ([MS-XLS] §2.4.329) kirjoitettiin takaisin tavu tavulta uudelleennumeroimatta. Excelillä tehdyt .xls-tiedostot päättyvät aina viittamattomaan häntäfonttiin (9 pt:n DengXian kiinalaisen localen järjestelmässä), joten ensimmäisellä tallennuksella vain kommenttiruin käyttämä fontti ei koskaan ollut viimeinen, eikä mikään liikkunut näkyvästi. Se ensimmäinen tallennus pudotti kuitenkin häntäfontin ja ylensi vain kommenttia palvelevan fontin viimeiselle paikalle. Toinen tallennus sitten hylkäsi sen viittamattomana, runin ifnt osoitti lopun ohi, ja Excel palautui oletusfonttiin; jos työkirja oli sillä välin saanut uuden fontin, runi kytkeytyi hiljaisena siihen sen sijaan, mikä testissä muutti tyylitetyn tekstilaatikkorunin Arialiksi. Kommenttipainotteiset tiedostot kuten artikkelissa kommenttien ja hyperlinkkien katselmointiprosessin rakentamisesta kuvatut ovat täsmälleen se paikka jossa tämä puraisee, koska niitä avataan, annotoidaan ja tallennetaan toistuvasti

HotXLS:n fonttitaulukko kahden tallennuksen yli: viittamaton häntä-FONT-tietue jonka Excel aina kirjoittaa putoaa ensin, vain kommenttia palveleva fontti päätyy viimeiseksi ja hylätään sitten koska TXO-muotoilurunit kirjoitettiin takaisin laskematta viittauksia, kunnes CountRichRunFontRefs korjasi selviytymissuodattimen versiossa 2.384.5
Ensimmäinen tallennus näytti puhtaalta koska häntäfontti otti vahingon, ja kommenttifontti katosi vasta toisella — kuvaa jokainen ifnt FONT-tietueen nimeen tallennusten yli sen sijaan että luottaisit muistissa tehtyyn testiin

HotXLS 2.384.5 kohtelee TXO-runeja kuten SST-runeja. CountRichRunFontRefs kävelee nyt jokaisen TMSOShapeTextBoxin jokaisella laskentataulukolla, konvertoi jokaisen runin nelosen ohittavan ifntin paikaksi ja laskee sen viittaukseksi, joten vain runien käyttämä fontti selviää tallennussuodattimesta. Syntyvä paikasta tallennusindeksiin -taulukko menee jokaisen piirroksen FontRunRemapiin, ja TMSOShapeTextBox.Store kirjoittaa run-indeksit uudelleen raakarunitavujen yksityiselle kopiolle jättäen häntä-TxOLastRunin rauhaan koska se ei kanna fonttia. Sovelluskoodille sopimus on yksinkertainen: TXLSComment.TextRuns.FontIndex ja TXLSTextBox.TextRuns.FontIndex käyttävät tiedostonumerointia, nelonen ohitettuna, täsmälleen kuten luettiin; run-indeksit ovat yhdestä laskettuja ja CharIndex on merkkisiirtymä josta runi alkaa. Tallennuksen jälkeen tallennettu numero voi poiketa asettamastasi, mutta se osoittaa yhä samaan fonttiin

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];   // tiedostonumerointi, 4 ohitettu
      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;

Kopiot, rivin lisäykset ja työkirjojen välinen run-migraatio

HotXLS 2.384.6:sta alkaen jokainen klassisen koneen kopiointipolku säilyttää kommenttien muotoilurunit, koska Range.Copy, CopyRange, Sheets.AddCopy ja solusiirrot joita Range.Insert ja Range.Delete tekevät kulkevat kaikkien TXLSRange.CopyCellin kautta, ja CopyCell kopioi ennen vain kommenttitekstin ja tekijän. Siirto on kopio plus tyhjennys, joten yhden rivin lisääminen kaksirunisen muistiinpanon yläpuolelle jätti sille nolla runia ja yhden fontin. Korjaus kopioi jokaisen runin ja siirtää sen fontin TXLSWorkbook.MigrateRunFontIndexin kautta, joka konvertoi nelosen ohittavan indeksin paikaksi, siirtää fontin arvona kohdefonttitaulukkoon ja konvertoi takaisin tiedostonumerointiin; SST:n rich text -migraatio metodissa Sheets.AddCopy kutsuu nyt samaa funktiota sen sijaan että kantaisi omaa kopiotaan aritmetiikasta. Kaksi reunatapausta tuli mukana: paikalleen liittäminen jossa lähde ja kohde ovat sama kommentti ei saa tyhjentää sen runeja ennen kuin ne on luettu, ja Sheets.AddCopy tekee nyt toisen kierroksen kommentteille jotka on kiinnitetty soluihin ilman tallennettua solutietuetta, jotka se aiemmin ohitti kokonaan. Fonttitaulukon puoli työkirjojen välisessä kopioinnissa noudattaa samaa arvona siirtämisen logiikkaa kuin kaavojen puoli jota käsittelee artikkeli työkirjojen välinen kopiointi ja kaavojen uudelleenkytkentä. XLSX-koneella kopiointipolut kloonasivat runit jo arvona; aukko oli itse kommenttiosassa, jossa lukija jätti huomiotta rFontin, striken, u:n ja vertAlignin ja kirjoittaja ei koskaan kirjoittanut uta tai vertAlignia, joten runit selviävät nyt tallennuksesta ja uudelleenavauksesta symmetrisesti

Miten fontti-indeksejä kannattaa testata BIFF8-tiedostoissa?

Testaa fontti-indeksejä tallentamalla ja avaamalla uudelleen, mieluiten yli yhden generoin, ja kuvaamalla jokainen ifnt takaisin FONT-tietueeseen numeerisen välin assertoimisen sijaan. Jokainen tämän tarinan bugi pääsi muistissa tehdyn testin läpi: 2.384.1:n regressio asui toisiaan vastaavassa kirjoittaja–lukijaparissa, TXO:n ajautuminen vaati kaksi tallennusta ja fonttitaulukon muutoksen väliin, ja XLSX:n kadonneet kommenttirunit näkyivät vasta uudelleenavauksen jälkeen. Käypä testiharness avaa Excelillä tehdyn näytteen, tallentaa sen kahdesti HotXLS:n läpi, lisää tai poistaa fontin tallennusten välissä ja tarkistaa sitten runien paikat sekä tavutasolla jokaisen ifnt:n takana olevat fonttinimet. Älä vertaa FontIndex-arvoja tallennuksen jälkeen ja ennen sitä, koska uudelleennumerointi on laillista

procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
  Book: IXLSWorkbook;
  Note: TXLSComment;
  RunCount: Integer;
  SecondRunAt: Word;
begin
  Book := TXLSWorkbook.Create;
  Assert(Book.Open(SrcFile) = 1);          // Excelin tekemä, C2:ssa kaksi runia
  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 siirtyy C3:een
  Assert(Book.SaveAs(OutFile) = 1);

  Book := TXLSWorkbook.Create;               // avaa uudelleen, älä luota muistiin
  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;

Jos luet ja kirjoitat klassista XLS:ää Delphillä tai C++Builderilla etkä halua seurata minkä kirjaston monista fonttikuluttajista yhä noudattaa [MS-XLS] §2.5.129:ää, tässä kuvattu nelosen ohittava numerointi, runien uudelleennumerointi tallennuksessa ja runien siirto arvona ovat sisäänrakennettuina HotXLS Delphi Excel -komponentissa, joka lukee ja kirjoittaa XLS- ja XLSX-muotoja ilman Exceliä tai OLE automationia