Tehnični članak

FontIndex BIFF8 izpušča 4: rich text runs HotXLS v Delphiju

HotXLS šteje vsak sklic na pisavo BIFF8 tako, kot ga definira [MS-XLS] §2.5.129 FontIndex: vrednosti 0 do 3 so ničelno osnovane, vrednosti nad 4 so osnovane z ena, 4 pa se nikoli ne pojavi, tako da je peti zapis FONT ifnt 5, največji veljaven ifnt pa enak številu zapisov FONT. Od HotXLS 2.384.4 pravilu sledijo pisatelj XF, bralec XF, runi nizov rich text in selitev runov med delovnimi zvezki, različici 2.384.5 in 2.384.6 pa pravilo razširita na rune komentarjev in besedilnih okvirjev, vključno skozi kopije in vstavljanje vrstic

Pravilo zgleda kot natipkanka, dokler se z njim ne srečate. Nekdo odpre delovni zvezek z osmimi zapisi FONT, najde XF, ki kaže na pisavo 8, in sklepa, da je pisatelj izdelal indeks izven obsega. Ta ista logika je odšla v HotXLS 2.384.1 kot »popravek« in iz pravilne izvedbe naredila takšno, v kateri je vsaka pisava po meri v datoteki, odprti v Excelu, pristala eno režo prezgodaj. Zanimivo ni samo odmik za ena, temveč to, koliko mest v knjižnici BIFF8 nosi isto konvencijo, in kako lahko vezava pisave preživi eno shranjevanje, pokvari pa se ob drugem. Če ste se že stežka prebili skozi zakrknjenosti dolžine in kodiranja, opisane v razčlenjevanju cch in fHigh pri BIFF8 XLUnicodeString, je to isti rod napak: datoteka je v redu, aritmetika ni

Kaj pravilo FontIndex iz [MS-XLS] dejansko pravi?

[MS-XLS] §2.5.129 pravi, da je FontIndex pod 4 ničelno osnovan položaj zapisa, FontIndex nad 4 enojno osnovan položaj zapisa, vrednosti 4 pa se NE SME uporabiti. Isti tip FontIndex uporabljajo zapisi XF, formatirni runi SST in formatirni runi TXO, zato eno napačno prebrano pravilo pokvari vse tri. Dokaz se zlahka ponovi z datotekami, napisanimi v Excelu: SOLVSAMP.XLS, poslan z Officeom, ima 19 zapisov FONT in najvišji XF ifnt 19, delovni zvezek s 43 zapisi se ustavi na 43, datoteka, shranjena z Excel 16 s 30 zapisi FONT, pa kaže svoje celice Courier New na ifnt 22, dvaindvajseti zapis. Noben nikoli ne vsebuje 4. Če morate preslikavo analizirati sami v diagnostičnem orodju, je pretvorba dve kratki funkciji

// [MS-XLS] 2.5.129 FontIndex: 0..3 ničelno, > 4 enojno, 4 neveljavno
function FontIndexToRecordNo(Ifnt: Word): Integer;  // zapis FONT z osnovo 1
begin
  if Ifnt < 4 then
    Result := Ifnt + 1
  else if Ifnt > 4 then
    Result := Ifnt
  else
    Result := -1;  // 4 ne sme nastopiti
end;

function RecordNoToFontIndex(RecordNo: Integer): Word;
begin
  if RecordNo <= 4 then
    Result := RecordNo - 1
  else
    Result := RecordNo;
end;
Preslikava FontIndex v HotXLS po MS-XLS 2.5.129, kjer so ifnt 0 do 3 ničelno osnovani položaji zapisov FONT, ifnt 5 in več osnovani z ena, vrednost 4 pa se nikoli ne pojavi, skupaj s pretvorbo FontIndexToRecordNo in dokazi iz delovnih zvezkov, napisanih v Excelu, kot je SOLVSAMP.XLS
Peti zapis FONT je ifnt 5, ne 4 — delovni zvezek s 19 zapisi se ustavi na ifnt 19, nobena datoteka, napisana v Excelu, pa nikoli ne shrani prepovedane vrednosti vmes

Znotraj HotXLS isto pravilo živi na dveh zrcalnih mestih. TXLSFontList.GetSaveIndex vzame položaj pisave v navedenem seznamu z osnovo 1 in odšteva le položaje 1 do 4, tako da se položaj 5 zapiše kot ifnt 5. TXLSReader.ParseXF ob nalaganju nareda obratno: vsak ifnt 5 ali več se odšteje na ničelno osnovano režo na seznamu pisav, karkoli pod tem pa ostane na mestu. Preslikava rich runov SST in CountRichRunFontRefs uporabljata isto pretvorbo ifnt >= 5, in to je bistvo: ena konvencija, vsak potrošnik

// TXLSFontList.GetSaveIndex (stran pisatelja)
Result := inherited GetSaveIndex(Index);   // navedeni položaj z osnovo 1
if (Result > 0) and (Result < 5) then
  Dec(Result);                             // 1..4 postane 0..3, 5+ nespremenjeno

// TXLSReader.ParseXF (stran bralca)
fnti := Data.GetWord(0);
if fnti >= 5 then
  Dec(fnti);                               // ifnt 5 je reža 4 na seznamu pisav

Zakaj je ničelno osnovan »popravek« premaknil vsako pisavo po meri za ena?

Ničelno osnovano prenovljanje v HotXLS 2.384.1 je premaknilo vsako pisavo po meri, ker je bralo enojno osnovan indeks kot ničelno osnovanega in nato spremenilo štiri klicne točke, da se ujemajo s tem napačnim branjem: GetSaveIndex, ParseXF, preslikavo runov SST in selitev runov med delovnimi zvezki v Sheets.AddCopy. Round-tripi HotXLS so še vedno zgledali v redu, ker sta se pisatelj in bralec strinjala med seboj. Excel ni strinjal. Datoteka, napisana z 2.384.1, je prvo pisavo po meri postavila na ifnt 4, kar Excel obravnava kot privzeto pisavo, vsako kasnejšo pisavo po meri pa en zapis prezgodaj; odpiranje datoteke iz Excela je šlo v obratno smer in vsako pisavo vezalo en zapis prepozno

Regresija HotXLS 2.384.1, kjer GetSaveIndex in ParseXF branita enojno osnovane vrednosti FontIndex kot ničelno osnovane, prvo pisavo po meri zapišejo kot prepovedani ifnt 4, ki ga Excel razreši na privzeto pisavo, vsaka kasnejša pisava pa pristane en zapis prezgodaj, medtem ko round-tripi še vedno uspevajo
Pisatelj in bralec sta se strinjala o istem napačnem branju, zato je preizkus shrani in ponovno odpri ostal zelen, medtem ko je vsaka pisava, odprta v Excelu, pristala eno režo zgrešeno — ko ena konvencija živi na sedmih mestih in spremenite štiri, najprej sumite svojo spremembo

Znak, ki naj bi spremembo ustavil, je sedel v isti kodni bazi. CountRichRunFontRefs, preslikava FONTX in FBI grafov ter seznam pisav stilskega motorja niso bili nikoli dotaknjeni in še vedno uporabljajo preskok 4, zato si je knjižnica protislovila tistega trenutka, ko je 2.384.1 pristala, skrito pa je bilo protislovje le po naključju, da so pisave rich text običajno hkrati sklicevale tudi s kakšnega XF. Ko se ena konvencija pojavi na sedmih mestih in vi spreminjate štiri, sumite svojo spremembo, preden sumite ostale tri. Različica 2.384.4 je v vseh štirih mestih obnovila številčenje po specifikaciji, stari regresijski preizkus, ki je trdil ifnt < FontCount in s tem kodiral napačno branje, pa so zamenjali preizkusi, ki vsak zapisani ifnt preslikajo nazaj na ime zapisa FONT po formuli specifikacije. Ostaja ena poštena omejitev: datoteke, shranjene z 2.384.1 do 2.384.3 s petimi ali več pisavami, nosijo premaknjene indekse, ki jih bralec ne more ločiti od veljavnih podatkov, zato je edino zdravilo, da jih znova ustvarite

Zakaj se runi pisav komentarjev pokvarijo šele pri drugem shranjevanju?

Runi komentarjev in besedilnih okvirjev so se pokvarili pri drugem shranjevanju, ker je HotXLS prvih N-1 zapisov FONT obdržal brezpogojno, izpustil pa le zadnjega, kadar se nanj ni skliceval noben XF, formatirni runi TXO ([MS-XLS] §2.4.329) pa so se zapisali nazaj bajt za bajtom brez ponovnega številčenja. Datoteke .xls, napisane v Excelu, se vedno končajo z neklicano zaključno pisavo (9 pt DengXian na sistemu s kitajskim localem), zato pisava, uporabljena le s strani runa komentarja, pri prvem shranjevanju nikoli ni bila zadnja in nič se ni vidno premaknilo. To prvo shranjevanje pa je izpustilo zaključno pisavo in pripeljalo pisavo, ki jo uporablja samo komentar, na zadnji položaj. Drugo shranjevanje jo je nato zavrnilo kot neklicano, runov ifnt je kazal preko konca, Excel pa se je vrnil na privzeto pisavo; če je delovni zvezek vmes dobil novo pisavo, se je run tiho vezal raje na njo, kar je v preizkusu iz stiliziranega runa besedilnega okvirja naredilo Arial. Datoteke, bogate s komentarji, kot so tiste iz gradnje delovnega toka pregledovanja komentarjev in hiperpovezav, so točno tam, kjer to ugrizne, ker jih vedno znova odpirajo, označujejo in shranjujejo

Tabela pisav HotXLS skozi dve shranjevanji, kjer neklicani zaključni zapis FONT, ki ga Excel vedno zapiše, izpade prvi, pisava samo za komentar postane zadnja in jo nato zavrejo, ker so bili formatirni runi TXO zapisani nazaj brez štetja sklicev, dokler CountRichRunFontRefs v 2.384.5 ni popravil filtra preživetja
Prvo shranjevanje je zgledalo čisto, ker je zaključna pisava prevzela izgubo, pisava komentarja pa je izginila šele ob drugem — vsak ifnt preslikajte na ime zapisa FONT čez shranjevanja, namesto da bi zaupali preizkusu v pomnilniku

HotXLS 2.384.5 obravnava rune TXO kot rune SST. CountRichRunFontRefs zdaj obide vsak TMSOShapeTextBox na vsakem delovnem listu, pretvori runov preskok-4 ifnt v režo in ga prešteje kot sklic, tako da pisava, ki jo uporablja samo run, preživi filter shranjevanja. Nastala tabela rež v shranjevalne indekse gre v FontRunRemap vsake risbe, TMSOShapeTextBox.Store pa prepiše indekse runov na zasebni kopiji surovih bajtov runov, zaključni TxOLastRun pa pusti pri miru, ker ne nosi pisave. Za aplikacijsko kodo je pogodba preprosta: TXLSComment.TextRuns.FontIndex in TXLSTextBox.TextRuns.FontIndex uporabljata številčenje datoteke, 4 izpuščeno, točno tako, kot je prebrano; indeksi runov so z osnovo 1, CharIndex pa je odmik znaka, kjer se run začne. Po shranjevanju se shranjena številka lahko razlikuje od tiste, ki ste jo nastavili, kaže pa še vedno na isto pisavo

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];   // številčenje datoteke, 4 izpuščeno
      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;

Kopije, vstavljanje vrstic in selitev runov med delovnimi zvezki

Od HotXLS 2.384.6 vsaka kopirna pot klasičnega pogona obdrži formatirne rune komentarjev, ker vse poti Range.Copy, CopyRange, Sheets.AddCopy in premiki celic za Range.Insert ter Range.Delete gredo skozi TXLSRange.CopyCell, CopyCell pa je kopiral le besedilo komentarja in avtorja. Premik je kopija plus čiščenje, zato je vstavljanje ene same vrstice nad opombo z dvema runoma pustilo nič runov in eno pisavo. Popravek vsak run prekopira in njegovo pisavo prestavi prek TXLSWorkbook.MigrateRunFontIndex, ki preskok-4 indeks pretvori v režo, pisavo po vrednosti preseli v ciljno tabelo pisav in pretvori nazaj v številčenje datoteke; selitev rich text SST v Sheets.AddCopy zdaj pokliče isto funkcijo, namesto da bi nosila svojo kopijo aritmetike. Zraven sta prišla dva robna primera: lepljenje na mestu, kjer sta vir in cilj isti komentar, ne sme počistiti njegovih runov, preden jih prebere, Sheets.AddCopy pa zdaj naredi drugi prehod za komentarje, pripete celicam brez shranjenega zapisa celice, ki jih je prej popolnoma preskočila. Stran tabele pisav pri kopiranju med delovnimi zvezki sledi isti logiki po vrednosti kot stran formul, ki jo pokriva kopiranje med delovnimi zvezki in ponovna vezava formul. Na pogonu XLSX so kopirne poti rune že klonirale po vrednosti; vrzel je bila v samem delu za komentarje, kjer je bralec ignoriral rFont, strike, u in vertAlign, pisatelj pa nikoli ni izdal u ali vertAlign, tako da runi zdaj preživijo shranjevanje in ponovno odpiranje simetrično

Kako naj preizkusite indekse pisav v datotekah BIFF8?

Indekse pisav preizkušajte s shranjevanjem in ponovnim odpiranjem, najbolje čez več kot eno generacijo, in s preslikavo vsakega ifnt nazaj na zapis FONT, namesto da bi trdili številčni obseg. Vsaka napaka v tej zgodbi je prešla preizkus v pomnilniku: regresija 2.384.1 je živela v usklajenem paru pisatelj in bralec, odmik TXO je potreboval dve shranjevanji s spremembo tabele pisav vmes, izgubljeni runi komentarjev na XLSX pa so se pokazali šele po ponovnem odpiranju. Uporaben testni harness odpre vzorec, napisan v Excelu, dvakrat ga shrani skozi HotXLS, med shranjevanji doda ali odstrani pisavo, nato pa preveri položaje runov in na ravni bajtov imena pisav za vsakim ifnt. Ne primerjajte vrednosti FontIndex pred in po shranjevanju, saj je ponovno številčenje zakonito

procedure CheckNoteSurvivesShiftAndSave(const SrcFile, OutFile: string);
var
  Book: IXLSWorkbook;
  Note: TXLSComment;
  RunCount: Integer;
  SecondRunAt: Word;
begin
  Book := TXLSWorkbook.Create;
  Assert(Book.Open(SrcFile) = 1);          // napisano v Excelu, C2 ima dva runa
  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 gre v C3
  Assert(Book.SaveAs(OutFile) = 1);

  Book := TXLSWorkbook.Create;               // ponovno odpri, nikoli ne zaupaj pomnilniku
  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;

Če iz Delphija ali C++Builder berete in zapisujete klasični XLS in se vam ne da spremljati, kateri od številnih potrošnikov pisav v knjižnici se še strinja z [MS-XLS] §2.5.129, so številčenje s preskokom 4, ponovno številčenje runov ob shranjevanju in selitev runov po vrednosti, opisani tukaj, vgrajeni v komponento preglednic HotXLS za Delphi, ki bere in zapisuje XLS ter XLSX brez Excela ali OLE avtomatizacije