{
xlsbiff2.pas

Writes an Excel 2.x file

Excel 2.x files support only one Worksheet per Workbook, so only the first
will be written.

An Excel file consists of a number of subsequent records.
To ensure a properly formed file, the following order must be respected:

1st record:        BOF
2nd to Nth record: Any record
Last record:       EOF

The row and column numbering in BIFF files is zero-based.

Excel file format specification obtained from:

http://sc.openoffice.org/excelfileformat.pdf

Encoding information: ISO_8859_1 is used, to have support to
other characters, please use a format which support unicode

AUTHORS: Felipe Monteiro de Carvalho
}
unit xlsbiff2;

{$ifdef fpc}
  {$mode delphi}{$H+}
{$endif}

interface

uses
  Classes, SysUtils, lconvencoding,
  fpsTypes, fpsUtils, xlscommon;

const
  BIFF2_MAX_PALETTE_SIZE = 8;
  // There are more colors but they do not seem to be controlled by a palette.

type

  { TsSpreadBIFF2Reader }

  TsSpreadBIFF2Reader = class(TsSpreadBIFFReader)
  private
    FFont: TsFont;
    FPendingXFIndex: Word;
  protected
    procedure AddBuiltinNumFormats; override;
    procedure ReadBlank(AStream: TStream); override;
    procedure ReadBool(AStream: TStream); override;
    procedure ReadColumnDefault(AStream: TStream);
    procedure ReadColWidth(AStream: TStream);
    procedure ReadDefRowHeight(AStream: TStream);
    procedure ReadFONT(AStream: TStream);
    procedure ReadFONTCOLOR(AStream: TStream);
    procedure ReadFORMAT(AStream: TStream); override;
    procedure ReadFormula(AStream: TStream); override;
    procedure ReadInteger(AStream: TStream);
    procedure ReadIXFE(AStream: TStream);
    procedure ReadLabel(AStream: TStream); override;
    procedure ReadNumber(AStream: TStream); override;
    procedure ReadPASSWORD(AStream: TStream);
    procedure ReadPROTECT(AStream: TStream);
    procedure ReadRowColXF(AStream: TStream; out ARow, ACol: Cardinal; out AXF: Word); override;
    procedure ReadRowInfo(AStream: TStream); override;
    procedure ReadRPNAttr(AStream: TStream; AIdentifier: Byte); override;
    function ReadRPNFunc(AStream: TStream): Word; override;
    procedure ReadRPNSharedFormulaBase(AStream: TStream; out ARow, ACol: Cardinal); override;
    function ReadRPNTokenArraySize(AStream: TStream): Word; override;
    procedure ReadStringRecord(AStream: TStream); override;
    procedure ReadWindow2(AStream: TStream); override;
    procedure ReadXF(AStream: TStream);
  public
    constructor Create(AWorkbook: TsBasicWorkbook); override;
    { General reading methods }
    procedure ReadFromStream(AStream: TStream; APassword: String = '';
      AParams: TsStreamParams = []); override;
    { File format detection }
    class function CheckfileFormat(AStream: TStream): Boolean; override;
  end;


  { TsSpreadBIFF2Writer }

  TsSpreadBIFF2Writer = class(TsSpreadBIFFWriter)
  private
    FSheetIndex: Integer;  // Index of worksheet to be written
    procedure GetAttributes(AFormatIndex: Integer; XFIndex: Word;
      out Attrib1, Attrib2, Attrib3: Byte);
    procedure GetFormatAndFontIndex(AFormatRecord: PsCellFormat;
      out AFormatIndex, AFontIndex: Integer);
    { Record writing methods }
    procedure WriteBOF(AStream: TStream);
    procedure WriteCellAttributes(AStream: TStream; AFormatIndex: Integer; XFIndex: Word);
    procedure WriteColWidth(AStream: TStream; ACol: PCol);
    procedure WriteColWidths(AStream: TStream);
//    procedure WriteColumnDefault(AStream: TStream; ACol: PCol);
    procedure WriteColumnDefault(AStream: TStream;
        AFirstColIndex, ALastColIndex: Word; AFormatIndex: Integer);
    procedure WriteColumnDefaults(AStream: TStream);
    procedure WriteDefaultRowHeight(AStream: TStream; AWorksheet: TsBasicWorksheet);
    procedure WriteDimensions(AStream: TStream; AWorksheet: TsBasicWorksheet);
    procedure WriteEOF(AStream: TStream);
    procedure WriteFont(AStream: TStream; AFontIndex: Integer);
    procedure WriteFonts(AStream: TStream);
    procedure WriteFORMATCOUNT(AStream: TStream);
    procedure WriteIXFE(AStream: TStream; XFIndex: Word);
  protected
    procedure AddBuiltinNumFormats; override;
    function FunctionSupported(AExcelCode: Integer;
      const AFuncName: String): Boolean; override;
    procedure PopulatePalette(AWorkbook: TsbasicWorkbook); override;
    procedure WriteBlank(AStream: TStream; const ARow, ACol: Cardinal;
      ACell: PCell); override;
    procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal;
      const AValue: Boolean; ACell: PCell); override;
    procedure WriteCodePage(AStream: TStream; ACodePage: String); override;
    procedure WriteError(AStream: TStream; const ARow, ACol: Cardinal;
      const AValue: TsErrorValue; ACell: PCell); override;
    procedure WriteFORMAT(AStream: TStream; ANumFormatStr: String;
      AFormatIndex: Integer); override;
    procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal;
      const AValue: string; ACell: PCell); override;
    procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal;
      const AValue: double; ACell: PCell); override;
    procedure WritePASSWORD(AStream: TStream);
    procedure WriteRow(AStream: TStream; ASheet: TsBasicWorksheet;
      ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow); override;
    procedure WriteRPNFormula(AStream: TStream; const ARow, ACol: Cardinal;
      AFormula: TsRPNFormula; ACell: PCell); override;
    function WriteRPNFunc(AStream: TStream; AIdentifier: Word): Word; override;
    procedure WriteRPNTokenArraySize(AStream: TStream; ASize: Word); override;
    procedure WriteStringRecord(AStream: TStream; AString: String); override;
    procedure WriteWindow1(AStream: TStream); override;
    procedure WriteWindow2(AStream: TStream; ASheet: TsBasicWorksheet);
    procedure WriteXF(AStream: TStream; AFormatRecord: PsCellFormat;
      XFType_Prot: Byte = 0); override;
  public
    constructor Create(AWorkbook: TsbasicWorkbook); override;
    procedure WriteToStream(AStream: TStream; AParams: TsStreamParams = []); override;
  end;

  TExcel2Settings = record
    // Settings used when writing to file
    DateMode: TDateMode;
    CodePage: String;
    SheetIndex: Integer;
  end;

var
  Excel2Settings: TExcel2Settings = (
    DateMode: dm1900;
    CodePage: 'cp1252';   // on Windows, will be replaced --> see initalization
    SheetIndex: 0;
  );

  { the palette of the default BIFF2 colors as "big-endian color" values }
  PALETTE_BIFF2: array[$0..$07] of TsColor = (
    $000000,  // $00: black
    $FFFFFF,  // $01: white
    $FF0000,  // $02: red
    $00FF00,  // $03: green
    $0000FF,  // $04: blue
    $FFFF00,  // $05: yellow
    $FF00FF,  // $06: magenta
    $00FFFF   // $07: cyan
  );

  sfidExcel2: TsSpreadFormatID;

procedure InitBiff2Limitations(out ALimitations: TsSpreadsheetFormatLimitations);


implementation

uses
 {$IFDEF FPSpreadDebug}
  LazLogger,
 {$ENDIF}
  Math,
  fpsStrings, fpspreadsheet, fpsReaderWriter, fpsPalette, fpsNumFormat;

const
  { Excel record IDs }
  INT_EXCEL_ID_DIMENSIONS    = $0000;
  INT_EXCEL_ID_BLANK         = $0001;
  INT_EXCEL_ID_INTEGER       = $0002;
  INT_EXCEL_ID_NUMBER        = $0003;
  INT_EXCEL_ID_LABEL         = $0004;
  INT_EXCEL_ID_BOOLERROR     = $0005;
  INT_EXCEL_ID_ROW           = $0008;
  INT_EXCEL_ID_BOF           = $0009;
  {%H-}INT_EXCEL_ID_INDEX    = $000B;
  INT_EXCEL_ID_FORMAT        = $001E;
  INT_EXCEL_ID_FORMATCOUNT   = $001F;
  INT_EXCEL_ID_COLUMNDEFAULT = $0020;
  INT_EXCEL_ID_COLWIDTH      = $0024;
  INT_EXCEL_ID_DEFROWHEIGHT  = $0025;
  INT_EXCEL_ID_WINDOW2       = $003E;
  INT_EXCEL_ID_XF            = $0043;
  INT_EXCEL_ID_IXFE          = $0044;
  INT_EXCEL_ID_FONTCOLOR     = $0045;

  { BOF record constants }
  INT_EXCEL_SHEET            = $0010;
  {%H-}INT_EXCEL_CHART       = $0020;
  {%H-}INT_EXCEL_MACRO_SHEET = $0040;

  MASK_XF_TYPE_PROT_LOCKED_BIFF2          = $40;
  MASK_XF_TYPE_PROT_FORMULA_HIDDEN_BIFF2  = $80;

type
  TBIFF2_BoolErrRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    Row: Word;
    Col: Word;
    Attrib1: Byte;
    Attrib2: Byte;
    Attrib3: Byte;
    BoolErrValue: Byte;
    ValueType: Byte;
  end;

  TBIFF2_DimensionsRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    FirstRow: Word;
    LastRowPlus1: Word;
    FirstCol: Word;
    LastColPlus1: Word;
  end;

  TBIFF2_LabelRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    Row: Word;
    Col: Word;
    Attrib1: Byte;
    Attrib2: Byte;
    Attrib3: Byte;
    TextLen: Byte;
  end;

  TBIFF2_NumberRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    Row: Word;
    Col: Word;
    Attrib1: Byte;
    Attrib2: Byte;
    Attrib3: Byte;
    Value: Double;
  end;

  TBIFF2_IntegerRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    Row: Word;
    Col: Word;
    Attrib1: Byte;
    Attrib2: Byte;
    Attrib3: Byte;
    Value: Word;
  end;

  TBIFF2_XFRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    FontIndex: Byte;
    NotUsed: Byte;
    NumFormat_Prot: Byte;
    HorAlign_Border_BkGr: Byte;
  end;


procedure InitBiff2Limitations(out ALimitations: TsSpreadsheetFormatLimitations);
begin
  InitBiffLimitations(ALimitations);
  ALimitations.MaxPaletteSize := BIFF2_MAX_PALETTE_SIZE;
end;

procedure InternalAddBuiltinNumFormats(AList: TStringList; AFormatSettings: TFormatSettings);
var
  fs: TFormatSettings absolute AFormatSettings;
  cs: String;
begin
  cs := fs.CurrencyString;
  with AList do
  begin
    Clear;
    Add('');          // 0
    Add('0');         // 1
    Add('0.00');      // 2
    Add('#,##0');     // 3
    Add('#,##0.00');  // 4
    Add(BuildCurrencyFormatString(nfCurrency, fs, 0, fs.CurrencyFormat, fs.NegCurrFormat, cs));     // 5
    Add(BuildCurrencyFormatString(nfCurrencyRed, fs, 0, fs.CurrencyFormat, fs.NegCurrFormat, cs));  // 6
    Add(BuildCurrencyFormatString(nfCurrency, fs, 2, fs.CurrencyFormat, fs.NegCurrFormat, cs));     // 7
    Add(BuildCurrencyFormatString(nfCurrencyRed, fs, 2, fs.CurrencyFormat, fs.NegCurrFormat, cs));  // 8
    Add('0%');        // 9
    Add('0.00%');     // 10
    Add('0.00E+00');  // 11
    Add(BuildDateTimeFormatString(nfShortDate, fs));     // 12
    Add(BuildDateTimeFormatString(nfLongDate, fs));      // 13
    Add(BuildDateTimeFormatString(nfDayMonth, fs));      // 14: 'd/mmm'
    Add(BuildDateTimeFormatString(nfMonthYear, fs));     // 15: 'mmm/yy'
    Add(BuildDateTimeFormatString(nfShortTimeAM, fs));   // 16;
    Add(BuildDateTimeFormatString(nfLongTimeAM, fs));    // 17
    Add(BuildDateTimeFormatString(nfShortTime, fs));     // 18
    Add(BuildDateTimeFormatString(nfLongTime, fs));      // 19
    Add(BuildDateTimeFormatString(nfShortDateTime, fs)); // 20
  end;
end;


{------------------------------------------------------------------------------}
{                             TsSpreadBIFF2Reader                              }
{------------------------------------------------------------------------------}

constructor TsSpreadBIFF2Reader.Create(AWorkbook: TsBasicWorkbook);
begin
  inherited Create(AWorkbook);
  InitBiff2Limitations(FLimitations);
end;

procedure TsSpreadBIFF2Reader.AddBuiltInNumFormats;
begin
  FFirstNumFormatIndexInFile := 0;
end;

{@@ ----------------------------------------------------------------------------
  Checks the header of the stream for the signature of BIFF2 files
-------------------------------------------------------------------------------}
class function TsSpreadBIFF2Reader.CheckFileFormat(AStream: TStream): Boolean;
const
  BIFF2_HEADER: packed array[0..3] of byte = (
    $09,$00, $04,$00);  // they are common to all BIFF2 files that I've seen
var
  P: Int64;
  buf: packed array[0..3] of byte = (0, 0, 0, 0);
  n: Integer;
begin
  Result := false;
  P := AStream.Position;
  try
    AStream.Position := 0;
    n := AStream.Read(buf, SizeOf(buf));
    if n < Length(BIFF2_HEADER) then
      exit;
    for n:=0 to High(buf) do
      if buf[n] <> BIFF2_HEADER[n] then
        exit;
    Result := true;
  finally
    AStream.Position := P;
  end;
end;


procedure TsSpreadBIFF2Reader.ReadBlank(AStream: TStream);
var
  ARow, ACol: Cardinal;
  XF: Word;
  cell: PCell;
begin
  ReadRowColXF(AStream, ARow, ACol, XF);
  if FIsVirtualMode then begin
    InitCell(FWorksheet, ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := TsWorksheet(FWorksheet).AddCell(ARow, ACol);
  ApplyCellFormatting(cell, XF);
  if FIsVirtualMode then
    TsWorkbook(FWorkbook).OnReadCellData(Workbook, ARow, ACol, cell);
end;

{@@ ----------------------------------------------------------------------------
  The name of this method is misleading - it reads a BOOLEAN cell value,
  but also an ERROR value; BIFF stores them in the same record.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadBool(AStream: TStream);
var
  rec: TBIFF2_BoolErrRecord;
  r, c: Cardinal;
  xf: Word;
  cell: PCell;
  sheet: TsWorksheet;
begin
  sheet := FWorksheet as TsWorksheet;

  { Read entire record, starting at Row }
  rec.Row := 0;  // to silence the compiler...
  AStream.ReadBuffer(rec.Row, SizeOf(TBIFF2_BoolErrRecord) - 2*SizeOf(Word));
  r := WordLEToN(rec.Row);
  c := WordLEToN(rec.Col);
  xf := rec.Attrib1 and $3F;
  if xf = 63 then xf := FPendingXFIndex;

  { Create cell }
  if FIsVirtualMode then begin
    InitCell(FWorksheet, r, c, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := sheet.AddCell(r, c);

  { Retrieve boolean or error value depending on the "ValueType" }
  case rec.ValueType of
    0: sheet.WriteBoolValue(cell, boolean(rec.BoolErrValue));
    1: sheet.WriteErrorValue(cell, ConvertFromExcelError(rec.BoolErrValue));
  end;

  { Apply formatting }
  ApplyCellFormatting(cell, xf);

  if FIsVirtualMode then
    TsWorkbook(FWorkbook).OnReadCellData(Workbook, r, c, cell);
end;

procedure TsSpreadBIFF2Reader.ReadColumnDefault(AStream: TStream);
var
  c, col1, col2: Word;
  attr2, attr3: Byte;
  fmt: TsCellFormat;
  fmtIndex: Integer;
  fontIndex: Integer;
  fnt: TsFont;
  nf: TsNumFormatParams;
  nfs: String;
  b: Byte;
  book: TsWorkbook;
begin
  book := TsWorkbook(FWorkbook);

  { Index of first column }
  col1 := WordLEToN(AStream.ReadWord);

  { Index of last column - note: the file value is incremented by 1 }
  col2 := WordLEToN(AStream.ReadWord) - 1;

  { Attributes }
  {attr1 := }AStream.ReadByte;     // Avoid compiler warning of unused attr1
  attr2 := AStream.ReadByte;
  attr3 := AStream.ReadByte;

  InitFormatRecord(fmt);
  fmt.ID := FCellFormatList.Count;

  // Font index
  fontIndex := (attr2 and $C0) shr 6;
  if fontIndex > 4 then dec(fontIndex);  // Watch out for the nasty missing font #4...
  fnt := TsFont(FFontList[fontIndex]);
  fmt.FontIndex := book.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex = -1 then
    fmt.FontIndex := book.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex > 0 then
    Include(fmt.UsedFormattingFields, uffFont);

  // Number format index
  b := attr2 and $3F;
  nfs := NumFormatList[b];
  if nfs <> '' then begin
    fmt.NumberFormatIndex := book.AddNumberFormat(nfs);
    nf := book.GetNumberFormat(fmt.NumberFormatIndex);
    fmt.NumberFormat := nf.NumFormat;
    fmt.NumberFormatStr := nf.NumFormatStr;
    if fmt.NumberFormat <> nfGeneral then
      Include(fmt.UsedFormattingfields, uffNumberFormat);
  end;

  // Horizontal alignment
  b := attr3 and MASK_XF_HOR_ALIGN;
  if (b <= ord(High(TsHorAlignment))) then
  begin
    fmt.HorAlignment := TsHorAlignment(b);
    if fmt.HorAlignment <> haDefault then
      Include(fmt.UsedFormattingFields, uffHorAlign);
  end;

  // Vertical alignment - not used in BIFF2
  fmt.VertAlignment := vaDefault;

  // Word wrap - not used in BIFF2
  // -- nothing to do here

  // Text rotation - not used in BIFF2
  // -- nothing to do here

  // Borders
  fmt.Border := [];
  if attr3 and $08 <> 0 then
    Include(fmt.Border, cbWest);
  if attr3 and $10 <> 0 then
    Include(fmt.Border, cbEast);
  if attr3 and $20 <> 0 then
    Include(fmt.Border, cbNorth);
  if attr3 and $40 <> 0 then
    Include(fmt.Border, cbSouth);
  if fmt.Border <> [] then
    Include(fmt.UsedFormattingFields, uffBorder);

  // Background color not supported, only shaded background
  if attr3 and $80 <> 0 then
  begin
    fmt.Background.Style := fsGray50;
    fmt.Background.FgColor := scBlack;
    fmt.Background.BgColor := scTransparent;
    Include(fmt.UsedFormattingFields, uffBackground);
  end;

  // Add the decoded data to the format list
  FCellFormatList.Add(fmt);
  fmtIndex := book.AddCellFormat(fmt);

  for c := col1 to col2 do
    TsWorksheet(FWorksheet).WriteColFormatIndex(c, fmtIndex);
end;

procedure TsSpreadBIFF2Reader.ReadColWidth(AStream: TStream);
const
  EPS = 1E-3;
var
  c, c1, c2: Cardinal;
  w: Word;
  colwidth: Single;
  sheet: TsWorksheet;
begin
  sheet := TsWorksheet(FWorksheet);

  // read column start and end index of column range
  c1 := AStream.ReadByte;
  c2 := AStream.ReadByte;
  // read col width in 1/256 of the width of "0" character
  w := WordLEToN(AStream.ReadWord);
  // calculate width in units of "characters"
  colwidth := (FWorkbook as TsWorkbook).ConvertUnits(w / 256, suChars, FWorkbook.Units);
  // assign width to columns, but only if different from default column width.
  if not SameValue(colwidth, sheet.ReadDefaultColWidth(FWorkbook.Units), EPS) then
    for c := c1 to c2 do
      sheet.WriteColWidth(c, colwidth, FWorkbook.Units);
end;

procedure TsSpreadBIFF2Reader.ReadDefRowHeight(AStream: TStream);
var
  hw: word;
  h: Single;
begin
  hw := WordLEToN(AStream.ReadWord);
  h := TwipsToPts(hw and $7FFF);
  (FWorksheet as TsWorksheet).WriteDefaultRowHeight(h, suPoints);
end;

procedure TsSpreadBIFF2Reader.ReadFONT(AStream: TStream);
var
  lHeight: Word;
  lOptions: Word;
  Len: Byte;
  lFontName: UTF8String;
  isDefaultFont: Boolean;
begin
  FFont := TsFont.Create;

  { Height of the font in twips = 1/20 of a point }
  lHeight := WordLEToN(AStream.ReadWord);
  FFont.Size := lHeight/20;

  { Option flags }
  lOptions := WordLEToN(AStream.ReadWord);
  FFont.Style := [];
  if lOptions and $0001 <> 0 then Include(FFont.Style, fssBold);
  if lOptions and $0002 <> 0 then Include(FFont.Style, fssItalic);
  if lOptions and $0004 <> 0 then Include(FFont.Style, fssUnderline);
  if lOptions and $0008 <> 0 then Include(FFont.Style, fssStrikeout);

  { Font name: Unicodestring, char count in 1 byte }
  Len := AStream.ReadByte();
  SetLength(lFontName, Len);
  AStream.ReadBuffer(lFontName[1], Len);
  FFont.FontName := lFontName;

  isDefaultFont := FFontList.Count = 0;

  { Add font to internal font list }
  FFontList.Add(FFont);

  if isDefaultFont then
    TsWorkbook(FWorkbook).SetDefaultFont(FFont.FontName, FFont.Size);
end;

procedure TsSpreadBIFF2Reader.ReadFONTCOLOR(AStream: TStream);
var
  lColor: Word;
begin
  lColor := WordLEToN(AStream.ReadWord);   // Palette index
  FFont.Color := IfThen(lColor = SYS_DEFAULT_WINDOW_TEXT_COLOR,
    scBlack, FPalette[lColor]);
end;

{@@ ----------------------------------------------------------------------------
  Reads the FORMAT record required for formatting numerical data
-------------------------------------------------------------------------------}
(*
procedure TsSpreadBIFF2Reader.ReadFORMAT(AStream: TStream);
begin
  Unused(AStream);
  // We ignore the formats in the file, everything is known
  // (Using the formats in the file would require de-localizing them).
end;*)
procedure TsSpreadBIFF2Reader.ReadFormat(AStream: TStream);
var
  len: byte;
  fmtString: AnsiString;
  nfs: String;
begin
  // number format string
  len := AStream.ReadByte;
  SetLength(fmtString, len);
  AStream.ReadBuffer(fmtString[1], len);

  // We need the format string as utf8 and non-localized
  nfs := ConvertEncoding(fmtString, FCodePage, encodingUTF8);

  // Add to the end of the list.
  NumFormatList.Add(nfs);
end;


procedure TsSpreadBIFF2Reader.ReadFromStream(AStream: TStream;
  APassword: String = ''; AParams: TsStreamParams = []);
var
  BIFF2EOF: Boolean;
  RecordType: Word;
  CurStreamPos: Int64;
  BOFFound: Boolean;
begin
  Unused(APassword, AParams);
  BIFF2EOF := False;

  { In BIFF2 files there is only one worksheet, let's create it }
  FWorksheet := TsWorkbook(FWorkbook).AddWorksheet('Sheet', true);

  { Read all records in a loop }
  BOFFound := false;
  while not BIFF2EOF do
  begin
    { Read the record header }
    RecordType := WordLEToN(AStream.ReadWord);
    RecordSize := WordLEToN(AStream.ReadWord);

    CurStreamPos := AStream.Position;

    case RecordType of
      INT_EXCEL_ID_BLANK         : ReadBlank(AStream);
      INT_EXCEL_ID_BOF           : BOFFound := true;
      INT_EXCEL_ID_BOOLERROR     : ReadBool(AStream);
      INT_EXCEL_ID_BOTTOMMARGIN  : ReadMargin(AStream, 3);
      INT_EXCEL_ID_CODEPAGE      : ReadCodePage(AStream);
      INT_EXCEL_ID_COLUMNDEFAULT : ReadColumnDefault(AStream);
      INT_EXCEL_ID_COLWIDTH      : ReadColWidth(AStream);
      INT_EXCEL_ID_DEFCOLWIDTH   : ReadDefColWidth(AStream);
      INT_EXCEL_ID_EOF           : BIFF2EOF := True;
      INT_EXCEL_ID_FONT          : ReadFont(AStream);
      INT_EXCEL_ID_FONTCOLOR     : ReadFontColor(AStream);
      INT_EXCEL_ID_FOOTER        : ReadHeaderFooter(AStream, false);
      INT_EXCEL_ID_FORMAT        : ReadFormat(AStream);
      INT_EXCEL_ID_FORMULA       : ReadFormula(AStream);
      INT_EXCEL_ID_HEADER        : ReadHeaderFooter(AStream, true);
      INT_EXCEL_ID_HORZPAGEBREAK : ReadHorizontalPageBreaks(AStream, FWorksheet);
      INT_EXCEL_ID_INTEGER       : ReadInteger(AStream);
      INT_EXCEL_ID_IXFE          : ReadIXFE(AStream);
      INT_EXCEL_ID_LABEL         : ReadLabel(AStream);
      INT_EXCEL_ID_LEFTMARGIN    : ReadMargin(AStream, 0);
      INT_EXCEL_ID_NOTE          : ReadComment(AStream);
      INT_EXCEL_ID_NUMBER        : ReadNumber(AStream);
      INT_EXCEL_ID_PANE          : ReadPane(AStream);
      INT_EXCEL_ID_OBJECTPROTECT : ReadObjectProtect(AStream);
      INT_EXCEL_ID_PASSWORD      : ReadPASSWORD(AStream);
      INT_EXCEL_ID_PRINTGRID     : ReadPrintGridLines(AStream);
      INT_EXCEL_ID_PRINTHEADERS  : ReadPrintHeaders(AStream);
      INT_EXCEL_ID_PROTECT       : ReadPROTECT(AStream);
      INT_EXCEL_ID_RIGHTMARGIN   : ReadMargin(AStream, 1);
      INT_EXCEL_ID_ROW           : ReadRowInfo(AStream);
      INT_EXCEL_ID_SELECTION     : ReadSELECTION(AStream);
      INT_EXCEL_ID_STRING        : ReadStringRecord(AStream);
      INT_EXCEL_ID_TOPMARGIN     : ReadMargin(AStream, 2);
      INT_EXCEL_ID_DEFROWHEIGHT  : ReadDefRowHeight(AStream);
      INT_EXCEL_ID_VERTPAGEBREAK : ReadVerticalPageBreaks(AStream, FWorksheet);
      INT_EXCEL_ID_WINDOW2       : ReadWindow2(AStream);
      INT_EXCEL_ID_WINDOWPROTECT : ReadWindowProtect(AStream);
      INT_EXCEL_ID_XF            : ReadXF(AStream);
    else
      // nothing
    end;

    // Make sure we are in the right position for the next record
    AStream.Seek(CurStreamPos + RecordSize, soFromBeginning);

    if AStream.Position >= AStream.Size then
      BIFF2EOF := True;

    if not BOFFound then
      raise EFPSpreadsheetReader.Create('BOF record not found.');
  end;

  FixCols(FWorksheet);
  FixRows(FWorksheet);
end;

procedure TsSpreadBIFF2Reader.ReadFormula(AStream: TStream);
var
  ARow, ACol: Cardinal;
  XF: Word;
  ok: Boolean;
  formulaResult: Double = 0.0;
  Data: array [0..7] of byte;
  dt: TDateTime;
  nf: TsNumberFormat;
  nfs: String;
  err: TsErrorValue;
  cell: PCell;
  sheet: TsWorksheet;
begin
  sheet := TsWorksheet(FWorksheet);

  { BIFF Record row/column/style }
  ReadRowColXF(AStream, ARow, ACol, XF);

  { Result of the formula result in IEEE 754 floating-point value }
  Data[0] := 0;   // to silence the compiler...
  AStream.ReadBuffer(Data, Sizeof(Data));

  { Recalculation byte - currently not used }
  AStream.ReadByte;

  { Create cell }
  if FIsVirtualMode then begin
    InitCell(FWorksheet, ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := sheet.AddCell(ARow, ACol);

  // Now determine the type of the formula result
  if (Data[6] = $FF) and (Data[7] = $FF) then
    case Data[0] of
      0: // String -> Value is found in next record (STRING)
         FIncompleteCell := cell;
      1: // Boolean value
         sheet.WriteBoolValue(cell, Data[2] = 1);
      2: begin  // Error value
           case Data[2] of
             ERR_INTERSECTION_EMPTY   : err := errEmptyIntersection;
             ERR_DIVIDE_BY_ZERO       : err := errDivideByZero;
             ERR_WRONG_TYPE_OF_OPERAND: err := errWrongType;
             ERR_ILLEGAL_REFERENCE    : err := errIllegalRef;
             ERR_WRONG_NAME           : err := errWrongName;
             ERR_OVERFLOW             : err := errOverflow;
             ERR_ARG_ERROR            : err := errArgError;
           end;
           sheet.WriteErrorValue(cell, err);
         end;
      3: // Empty cell
         sheet.WriteBlank(cell);
    end
  else
  begin
    // Result is a number or a date/time
    Move(Data[0], formulaResult, SizeOf(Data));

    {Find out what cell type, set content type and value}
    ExtractNumberFormat(XF, nf, nfs);
    if IsDateTime(formulaResult, nf, nfs, dt) then
      sheet.WriteDateTime(cell, dt, nf, nfs)
    else
      sheet.WriteNumber(cell, formulaResult, nf, nfs);
  end;

  { Formula token array }
  if (boReadFormulas in FWorkbook.Options) then
  begin
    ok := ReadRPNTokenArray(AStream, cell);
    if not ok then sheet.WriteErrorValue(cell, errFormulaNotSupported);
  end;

  { Apply formatting to cell }
  ApplyCellFormatting(cell, XF);

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    TsWorkbook(FWorkbook).OnReadCellData(Workbook, ARow, ACol, cell);
end;

procedure TsSpreadBIFF2Reader.ReadLabel(AStream: TStream);
var
  rec: TBIFF2_LabelRecord;
  L: Byte;
  ARow, ACol: Cardinal;
  XF: Word;
  ansiStr: ansistring;
  valueStr: UTF8String;
  cell: PCell;
  sheet: TsWorksheet;
begin
  sheet := FWorksheet as TsWorksheet;

  { Read entire record, starting at Row, except for string data }
  rec.Row := 0;  // to silence the compiler...
  AStream.ReadBuffer(rec.Row, SizeOf(TBIFF2_LabelRecord) - 2*SizeOf(Word));
  ARow := WordLEToN(rec.Row);
  ACol := WordLEToN(rec.Col);
  XF := rec.Attrib1 and $3F;
  if XF = 63 then XF := FPendingXFIndex;

  { String with 8-bit size }
  L := rec.TextLen;
  SetLength(ansiStr, L);
  AStream.ReadBuffer(ansiStr[1], L);

  { Save the data }
  valueStr := ConvertEncoding(ansiStr, FCodePage, encodingUTF8);

  { Create cell }
  if FIsVirtualMode then begin
    InitCell(FWorksheet, ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := sheet.AddCell(ARow, ACol);
  sheet.WriteText(cell, valueStr);

  { Apply formatting to cell }
  ApplyCellFormatting(cell, XF);

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    TsWorkbook(FWorkbook).OnReadCellData(Workbook, ARow, ACol, cell);
end;

procedure TsSpreadBIFF2Reader.ReadNumber(AStream: TStream);
var
  rec: TBIFF2_NumberRecord;
  ARow, ACol: Cardinal;
  XF: Word;
  value: Double = 0.0;
  dt: TDateTime;
  nf: TsNumberFormat;
  nfs: String;
  cell: PCell;
  sheet: TsWorksheet;
begin
  sheet := FWorksheet as TsWorksheet;

  { Read entire record, starting at Row }
  rec.Row := 0;  // to silence the compiler...
  AStream.ReadBuffer(rec.Row, SizeOf(TBIFF2_NumberRecord) - 2*SizeOf(Word));
  ARow := WordLEToN(rec.Row);
  ACol := WordLEToN(rec.Col);
  XF := rec.Attrib1 and $3F;
  if XF = 63 then XF := FPendingXFIndex;
  value := rec.Value;

  {Create cell}
  if FIsVirtualMode then begin
    InitCell(FWorksheet, ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := sheet.AddCell(ARow, ACol);

  {Find out what cell type, set content type and value}
  ExtractNumberFormat(XF, nf, nfs);
  if IsDateTime(value, nf, nfs, dt) then
    sheet.WriteDateTime(cell, dt, nf, nfs)
  else
    sheet.WriteNumber(cell, value, nf, nfs);

  { Apply formatting to cell }
  ApplyCellFormatting(cell, XF);

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    TsWorkbook(FWorkbook).OnReadCellData(Workbook, ARow, ACol, cell);
end;

procedure TsSpreadBIFF2Reader.ReadInteger(AStream: TStream);
var
  sheet: TsWorksheet;
  ARow, ACol: Cardinal;
  XF: Word;
  AWord  : Word = 0;
  cell: PCell;
  rec: TBIFF2_IntegerRecord;
begin
  sheet := FWorksheet as TsWorksheet;

  { Read record into buffer }
  rec.Row := 0;   // to silence the comiler...
  AStream.ReadBuffer(rec.Row, SizeOf(TBIFF2_NumberRecord) - 2*SizeOf(Word));
  ARow := WordLEToN(rec.Row);
  ACol := WordLEToN(rec.Col);
  XF := rec.Attrib1 and $3F;
  if XF = 63 then XF := FPendingXFIndex;
  AWord := WordLEToN(rec.Value);

  { Create cell }
  if FIsVirtualMode then
  begin
    InitCell(FWorksheet, ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := sheet.AddCell(ARow, ACol);

  { Save the data }
  sheet.WriteNumber(cell, AWord);

  { Apply formatting to cell }
  ApplyCellFormatting(cell, XF);

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    TsWorkbook(FWorkbook).OnReadCellData(Workbook, ARow, ACol, cell);
end;

{@@ ----------------------------------------------------------------------------
  Reads an IXFE record. This record contains the "true" XF index of a cell. It
  is used if there are more than 62 XF records (XF field is only 6-bit). The
  IXFE record is used in front of the cell record using it
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadIXFE(AStream: TStream);
begin
  FPendingXFIndex := WordLEToN(AStream.ReadWord);
end;

{@@ ----------------------------------------------------------------------------
  Reads a PASSWORD record. Since BIFF2 does not have multiple worksheets the
  same password is stored in the workbook and worksheet cryptoinfo records.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadPASSWORD(AStream: TStream);
var
  hash: Word;
  cinfo: TsCryptoInfo;
begin
  hash := WordLEToN(AStream.ReadWord);
  if hash = 0 then
    exit;  // no password

  InitCryptoInfo(cinfo);
  cinfo.PasswordHash := Format('%.4x', [hash]);
  cinfo.Algorithm := caExcel;

  // Use the same password for workbook and worksheet protection because
  // BIFF2 can have only a single sheet.
  (FWorkbook as TsWorkbook).CryptoInfo := cinfo;
  (FWorksheet as TsWorksheet).CryptoInfo := cinfo;
end;

procedure TsSpreadBIFF2Reader.ReadPROTECT(AStream: TStream);
begin
  inherited ReadPROTECT(AStream);
  (FWorksheet as TsWorksheet).Protect(Workbook.IsProtected);
end;

{@@ ----------------------------------------------------------------------------
  Reads the row, column and xf index from the stream
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadRowColXF(AStream: TStream;
  out ARow, ACol: Cardinal; out AXF: WORD);
begin
  { BIFF Record data for row and column}
  ARow := WordLEToN(AStream.ReadWord);
  ACol := WordLEToN(AStream.ReadWord);

  { Index to XF record }
  AXF := AStream.ReadByte and $3F;
  // If AXF = $3F = 63 then there is an IXFE record containing the true XF index!
  if AXF = $3F then
    AXF := FPendingXFIndex;

  { Index to format and font record, cell style - ignored because contained in XF
    Must read to keep the record in sync. }
  AStream.ReadWord;
end;

procedure TsSpreadBIFF2Reader.ReadRowInfo(AStream: TStream);
type
  TRowRecord = packed record
    RowIndex: Word;
    Col1: Word;
    Col2: Word;
    Height: Word;
    NotUsed: Word;
    ContainsXF: Byte;
    OffsetToCell: Word;
    Attributes1: Byte;
    Attributes2: Byte;
    Attributes3: Byte;
    XFIndex: Word;
  end;
var
  rowrec: TRowRecord;
  lRow: PRow;
  h: word;
  auto: Boolean;
  rowheight: Single;
  defRowHeight: Single;
  containsXF: Boolean;
  xf: Word;
  book: TsWorkbook;
  sheet: TsWorksheet;
begin
  book := FWorkbook as TsWorkbook;
  sheet := FWorksheet as TsWorksheet;

  rowRec.RowIndex := 0;  // to silence the compiler...
  AStream.ReadBuffer(rowrec, SizeOf(TRowRecord));
  h := WordLEToN(rowrec.Height);
  auto := h and $8000 <> 0;
  rowheight := book.ConvertUnits(TwipsToPts(h and $7FFF), suPoints, FWorkbook.Units);
  defRowHeight := sheet.ReadDefaultRowHeight(FWorkbook.Units);
  containsXF := rowRec.ContainsXF = 1;
  xf := WordLEToN(rowRec.XFIndex);

  // No row record if rowheight in file is the same as the default rowheight and
  // if there is no formatting record.
  if SameValue(rowheight, defRowHeight, ROWHEIGHT_EPS) and (not containsXF) then
    exit;

  // Otherwise: create a row record
  lRow := sheet.GetRow(WordLEToN(rowrec.RowIndex));
  lRow^.Height := rowHeight;
  if auto then
    lRow^.RowHeightType := rhtAuto else
    lRow^.RowHeightType := rhtCustom;
  lRow^.FormatIndex := XFToFormatIndex(xf);
end;

{ ------------------------------------------------------------------------------
  Reads the RPN attribute. Most attributes are not handled, but the
  data associated with it must be read to keep the stream in sync with the
  data structure.
  This version of the method is valid for BIFF2 only.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadRPNAttr(AStream: TStream; AIdentifier: Byte);
var
  nc: Byte;
begin
  case AIdentifier of
    $01: AStream.ReadByte;   // tAttrVolatile, data not used
    $02: AStream.ReadByte;   // tAttrIF, not explicitely used
    $04: begin               // tAttrChoose, not supported
           nc := AStream.ReadByte;
           AStream.Position := AStream.Position + 2*Int64(nc) + 1;
         end;
    $08: AStream.ReadByte;   // tAttrSkip, data not used
    $10: AStream.ReadByte;   // tAttrSum, to be processed by ReadRPNTokenArray, byte not used
    $20: AStream.ReadByte;   // tAttrAssign, not used
  end;
end;

{@@ ----------------------------------------------------------------------------
  Reads the identifier for an RPN function with fixed argument count from the
  stream.
  Valid for BIFF2-BIFF3.
-------------------------------------------------------------------------------}
function TsSpreadBIFF2Reader.ReadRPNFunc(AStream: TStream): Word;
var
  b: Byte;
begin
  b := AStream.ReadByte;
  Result := b;
end;

{@@ ----------------------------------------------------------------------------
  Reads the cell coordiantes of the top/left cell of a range using a
  shared formula.
  This cell contains the rpn token sequence of the formula.
  Is overridden because BIFF2 has 1 byte for column.
  Code is not called for shared formulas (which are not supported by BIFF2), but
  maybe for array formulas.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadRPNSharedFormulaBase(AStream: TStream;
  out ARow, ACol: Cardinal);
begin
  // 2 bytes for row of first cell in shared formula
  ARow := WordLEToN(AStream.ReadWord);
  // 1 byte for column of first cell in shared formula
  ACol := AStream.ReadByte;
end;

{@@ ----------------------------------------------------------------------------
  Helper funtion for reading of the size of the token array of an RPN formula.
  Is overridden because BIFF2 uses 1 byte only.
-------------------------------------------------------------------------------}
function TsSpreadBIFF2Reader.ReadRPNTokenArraySize(AStream: TStream): Word;
begin
  Result := AStream.ReadByte;
end;

{@@ ----------------------------------------------------------------------------
  Reads a STRING record which contains the result of string formula.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadStringRecord(AStream: TStream);
var
  len: Byte;
  s: ansistring;
begin
  // The string is a byte-string with 8 bit length
  len := AStream.ReadByte;
  if len > 0 then
  begin
    SetLength(s, Len);
    AStream.ReadBuffer(s[1], len);
    if (FIncompleteCell <> nil) and (s <> '') then
    begin
      // The "IncompleteCell" has been identified in the sheet when reading
      // the FORMULA record which precedes the String record.
//      FIncompleteCell^.UTF8StringValue := AnsiToUTF8(s);
      FIncompleteCell^.UTF8StringValue := ConvertEncoding(s, FCodePage, encodingUTF8);
      FIncompleteCell^.ContentType := cctUTF8String;
      if FIsVirtualMode then
        TsWorkbook(FWorkbook).OnReadCellData(FWorkbook,
          FIncompleteCell^.Row, FIncompleteCell^.Col, FIncompleteCell
        );
    end;
  end;
  FIncompleteCell := nil;
end;

{@@ ----------------------------------------------------------------------------
  Reads the WINDOW2 record containing information like "show grid lines",
  "show sheet headers", "panes are frozen", etc.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadWindow2(AStream: TStream);
begin
  // Show formulas, not results
  AStream.ReadByte;

  // Show grid lines
  if AStream.ReadByte <> 0 then
    FWorksheet.Options := FWorksheet.Options + [soShowGridLines]
  else
    FWorksheet.Options := FWorksheet.Options - [soShowGridLines];

  // Show sheet headers
  if AStream.ReadByte <> 0 then
    FWorksheet.Options := FWorksheet.Options + [soShowHeaders]
  else
    FWorksheet.Options := FWorksheet.Options - [soShowHeaders];

  // Panes are frozen
  if AStream.ReadByte <> 0 then
    FWorksheet.Options := FWorksheet.Options + [soHasFrozenPanes]
  else
    FWorksheet.Options := FWorksheet.Options - [soHasFrozenPanes];

  // Show zero values
  AStream.ReadByte;

  // Index to first visible row
  WordLEToN(AStream.ReadWord);

  // Indoex to first visible column
  WordLEToN(AStream.ReadWord);

  // Use automatic grid line color (0= manual)
  AStream.ReadByte;

  // Manual grid line line color (rgb)
  DWordToLE(AStream.ReadDWord);
end;

procedure TsSpreadBIFF2Reader.ReadXF(AStream: TStream);
var
  rec: TBIFF2_XFRecord;
  fmt: TsCellFormat;
  b: Byte;
  nf: TsNumFormatParams;
  nfs: String;
  i: Integer;
  fnt: TsFont;
  book: TsWorkbook;
begin
  book := FWorkbook as TsWorkbook;

  // Read entire xf record into buffer
  InitFormatRecord(fmt);
  fmt.ID := FCellFormatList.Count;

  rec.FontIndex := 0;  // to silence the compiler...
  AStream.ReadBuffer(rec.FontIndex, SizeOf(rec) - 2*SizeOf(word));

  // Font index
  i := rec.FontIndex;
  if i > 4 then dec(i);  // Watch out for the nasty missing font #4...
  fnt := TsFont(FFontList[i]);
  fmt.FontIndex := book.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex = -1 then
    fmt.FontIndex := book.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex > 0 then
    Include(fmt.UsedFormattingFields, uffFont);

  // Number format index
  b := rec.NumFormat_Prot and $3F;
  nfs := NumFormatList[b];
  if nfs <> '' then
  begin
    fmt.NumberFormatIndex := book.AddNumberFormat(nfs);
    nf := book.GetNumberFormat(fmt.NumberFormatIndex);
    fmt.NumberFormat := nf.NumFormat;
    fmt.NumberFormatStr := nf.NumFormatStr;
    if fmt.NumberFormat <> nfGeneral then
      Include(fmt.UsedFormattingFields, uffNumberFormat);
  end;

  // Horizontal alignment
  b := rec.HorAlign_Border_BkGr and MASK_XF_HOR_ALIGN;
  if (b <= ord(High(TsHorAlignment))) then
  begin
    fmt.HorAlignment := TsHorAlignment(b);
    if fmt.HorAlignment <> haDefault then
      Include(fmt.UsedFormattingFields, uffHorAlign);
  end;

  // Vertical alignment - not used in BIFF2
  fmt.VertAlignment := vaDefault;

  // Word wrap - not used in BIFF2
  // -- nothing to do here

  // Text rotation - not used in BIFF2
  // -- nothing to do here

  // Borders
  fmt.Border := [];
  if rec.HorAlign_Border_BkGr and $08 <> 0 then
    Include(fmt.Border, cbWest);
  if rec.HorAlign_Border_BkGr and $10 <> 0 then
    Include(fmt.Border, cbEast);
  if rec.HorAlign_Border_BkGr and $20 <> 0 then
    Include(fmt.Border, cbNorth);
  if rec.HorAlign_Border_BkGr and $40 <> 0 then
    Include(fmt.Border, cbSouth);
  if fmt.Border <> [] then
    Include(fmt.UsedFormattingFields, uffBorder);

  // Background color not supported, only shaded background
  if rec.HorAlign_Border_BkGr and $80 <> 0 then
  begin
    fmt.Background.Style := fsGray50;
    fmt.Background.FgColor := scBlack;
    fmt.Background.BgColor := scTransparent;
    Include(fmt.UsedFormattingFields, uffBackground);
  end;

  // Protection
  b := rec.NumFormat_Prot and $C0;
  case b of
    $00: fmt.Protection := [];
    $40: fmt.Protection := [cpLockCell];
    $80: fmt.Protection := [cpHideFormulas];
    $C0: fmt.Protection := [cpLockCell, cpHideFormulas];
  end;
  if fmt.Protection <> DEFAULT_CELL_PROTECTION then
    Include(fmt.UsedFormattingFields, uffProtection);

  // Add the decoded data to the format list
  FCellFormatList.Add(fmt);
end;


{------------------------------------------------------------------------------}
{                           TsSpreadBIFF2Writer                                }
{------------------------------------------------------------------------------}

constructor TsSpreadBIFF2Writer.Create(AWorkbook: TsBasicWorkbook);
begin
  inherited Create(AWorkbook);

  InitBiff2Limitations(FLimitations);

  FDateMode := Excel2Settings.DateMode;
  FCodePage := Excel2Settings.CodePage;
  FSheetIndex := Excel2Settings.SheetIndex;
end;

{@@ ----------------------------------------------------------------------------
  Adds the built-in number formats to the NumFormatList.
  Inherited method overridden for BIFF2 specialties.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.AddBuiltInNumFormats;
begin
  FFirstNumFormatIndexInFile := 0;
  InternalAddBuiltInNumFormats(FNumFormatList, Workbook.FormatSettings);
end;

function TsSpreadBIFF2Writer.FunctionSupported(AExcelCode: Integer;
  const AFuncName: String): Boolean;
begin
  Result := inherited and (AExcelCode < 200);
end;

{@@ ----------------------------------------------------------------------------
  Determines the formatting attributes of a cell, row or column. This is needed,
  for example, for writing a cell content record, such as WriteLabel,
  WriteNumber, etc.

  The attributes contain, in bit masks, xf record index, font index,
  borders, etc.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.GetAttributes(AFormatIndex: Integer;
  XFIndex: Word; out Attrib1, Attrib2, Attrib3: Byte);
var
  fmt: PsCellFormat;
  fontIdx, formatIdx: Integer;
begin
  fmt := (Workbook as TsWorkbook).GetPointerToCellFormat(AFormatIndex);

  if fmt^.UsedFormattingFields = [] then begin
    Attrib1 := 15 + MASK_XF_TYPE_PROT_LOCKED_BIFF2;  // $40
    Attrib2 := 0;
    Attrib3 := 0;
    exit;
  end;

  // 1st byte:
  //   Mask $3F: Index to XF record
  //   Mask $40: 1 = Cell is locked
  //   Mask $80: 1 = Formula is hidden
  Attrib1 := Min(XFIndex, $3F) and $3F;
  if cpLockCell in fmt^.Protection then
    Attrib1 := Attrib1 or MASK_XF_TYPE_PROT_LOCKED_BIFF2;
  if cpHideFormulas in fmt^.Protection then
    Attrib1 := Attrib1 or MASK_XF_TYPE_PROT_FORMULA_HIDDEN_BIFF2;

  // 2nd byte:
  //   Mask $3F: Index to FORMAT record ("FORMAT" = number format!)
  //   Mask $C0: Index to FONT record
  GetFormatAndFontIndex(fmt, formatIdx, fontIdx);
  Attrib2 := formatIdx + fontIdx shr 6;
//  Attrib2 := fmt^.FontIndex shr 6;

  // 3rd byte
  //   Mask $07: horizontal alignment
  //   Mask $08: Cell has left border
  //   Mask $10: Cell has right border
  //   Mask $20: Cell has top border
  //   Mask $40: Cell has bottom border
  //   Mask $80: Cell has shaded background
  Attrib3 := 0;
  if uffHorAlign in fmt^.UsedFormattingFields then
    Attrib3 := ord (fmt^.HorAlignment);
  if uffBorder in fmt^.UsedFormattingFields then begin
    if cbNorth in fmt^.Border then Attrib3 := Attrib3 or $20;
    if cbWest in fmt^.Border then Attrib3 := Attrib3 or $08;
    if cbEast in fmt^.Border then Attrib3 := Attrib3 or $10;
    if cbSouth in fmt^.Border then Attrib3 := Attrib3 or $40;
  end;
  if (uffBackground in fmt^.UsedFormattingFields) then
    Attrib3 := Attrib3 or $80;
end;

procedure TsSpreadBIFF2Writer.GetFormatAndFontIndex(AFormatRecord: PsCellFormat;
  out AFormatIndex, AFontIndex: Integer);
var
  nfparams: TsNumFormatParams;
  nfs: String;
begin
  { Index to FORMAT record }
  AFormatIndex := 0;
  if (AFormatRecord <> nil) and (uffNumberFormat in AFormatRecord^.UsedFormattingFields) then
  begin
    nfParams := TsWorkbook(FWorkbook).GetNumberFormat(AFormatRecord^.NumberFormatIndex);
    nfs := nfParams.NumFormatStr;
    AFormatIndex := NumFormatList.IndexOf(nfs);
    if AFormatIndex = -1 then AFormatIndex := 0;
  end;

  { Index to FONT record }
  AFontIndex := 0;
  if (AFormatRecord <> nil) and (uffFont in AFormatRecord^.UsedFormattingFields) then
  begin
    AFontIndex := AFormatRecord^.FontIndex;
    if AFontIndex >= 4 then inc(AFontIndex);  // Font #4 does not exist in BIFF
  end;
end;

procedure TsSpreadBIFF2Writer.PopulatePalette(AWorkbook: TsBasicWorkbook);
begin
  FPalette.Clear;
  FPalette.AddBuiltinColors(false);
  { The next instruction creates an error log entry in CheckLimitations
    if the workbook contains more colors than the default 8.
    This is because BIFF2 can only have a palette with 8 colors. }
  FPalette.CollectFromWorkbook(AWorkbook);
end;

{@@ ----------------------------------------------------------------------------
  Attaches cell formatting data for the given cell to the current record.
  Is called from all writing methods of cell contents and rows

  @param  AFormatIndex  Index into the workbook's FCellFormatList
  @param  XFIndex       Index of the XF record used here
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteCellAttributes(AStream: TStream;
  AFormatIndex: Integer; XFIndex: Word);
type
  TCellFmtRecord = packed record
    XFIndex_Locked_Hidden: Byte;
    Format_Font: Byte;
    Align_Border_BkGr: Byte;
  end;
var
  rec: TCellFmtRecord;
  fmt: PsCellFormat;
  w: Word;
begin
  fmt := TsWorkbook(FWorkbook).GetPointerToCellFormat(AFormatIndex);
  rec.XFIndex_Locked_Hidden := 0;  // to silence the compiler...
  FillChar(rec, SizeOf(rec), 0);

  if fmt^.UsedFormattingFields <> [] then
  begin
    // 1st byte:
    //   Mask $3F: Index to XF record
    //   Mask $40: 1 = Cell is locked
    //   Mask $80: 1 = Formula is hidden
    rec.XFIndex_Locked_Hidden := Min(XFIndex, $3F) and $3F;
    if cpLockCell in fmt^.Protection then
      rec.XFIndex_Locked_Hidden := rec.XFIndex_Locked_Hidden or MASK_XF_TYPE_PROT_LOCKED_BIFF2;
    if cpHideFormulas in fmt^.Protection then
      rec.XFIndex_Locked_Hidden := rec.XFIndex_Locked_Hidden or MASK_XF_TYPE_PROT_FORMULA_HIDDEN_BIFF2;

    // 2nd byte:
    //   Mask $3F: Index to FORMAT record
    //   Mask $C0: Index to FONT record
    w := fmt^.FontIndex shr 6;   // was shl --> MUST BE shr!   // ??????????????????????
    rec.Format_Font := Lo(w);

    // 3rd byte
    //   Mask $07: horizontal alignment
    //   Mask $08: Cell has left border
    //   Mask $10: Cell has right border
    //   Mask $20: Cell has top border
    //   Mask $40: Cell has bottom border
    //   Mask $80: Cell has shaded background
    if uffHorAlign in fmt^.UsedFormattingFields then
      rec.Align_Border_BkGr := ord(fmt^.HorAlignment);
    if uffBorder in fmt^.UsedFormattingFields then begin
      if cbNorth in fmt^.Border then
        rec.Align_Border_BkGr := rec.Align_Border_BkGr or $20;
      if cbWest in fmt^.Border then
        rec.Align_Border_BkGr := rec.Align_Border_BkGr or $08;
      if cbEast in fmt^.Border then
        rec.Align_Border_BkGr := rec.Align_Border_BkGr or $10;
      if cbSouth in fmt^.Border then
        rec.Align_Border_BkGr := rec.Align_Border_BkGr or $40;
    end;
    if uffBackground in fmt^.UsedFormattingFields then
      rec.Align_Border_BkGr := rec.Align_Border_BkGr or $80;
  end;
  AStream.WriteBuffer(rec, SizeOf(rec));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 CODEPAGE record
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteCodePage(AStream: TStream; ACodePage: String);
//  AEncoding: TsEncoding);
begin
  if ACodePage = 'cp1251' then begin
    AStream.WriteWord(WordToLE(INT_EXCEL_ID_CODEPAGE));
    AStream.WriteWord(WordToLE(2));
    AStream.WriteWord(WordToLE(WORD_CP_1258_Latin1_BIFF2_3));
    FCodePage := ACodePage;
  end else
    inherited;
              (*
  if AEncoding = seLatin1 then begin
    cp := WORD_CP_1258_Latin1_BIFF2_3;
    FCodePage := 'cp1252';

    { BIFF Record header }
    AStream.WriteWord(WordToLE(INT_EXCEL_ID_CODEPAGE));
    AStream.WriteWord(WordToLE(2));
    AStream.WriteWord(WordToLE(cp));
  end else
    inherited; *)
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 COLUMNDEFAULT record containing default column formatting of
  specified columns
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteColumnDefault(AStream: TStream;
  AFirstColIndex, ALastColIndex: Word; AFormatIndex: Integer);
//ACol: PCol);
var
  attr1, attr2, attr3: Byte;
  xf: Word;
begin
  { BIFF record header }
  WriteBIFFHeader(AStream, INT_EXCEL_ID_COLUMNDEFAULT, 2+2+3+2);

  { Index to first column }
  AStream.WriteWord(WordToLE(AFirstColIndex));

  { Index to last column }
  AStream.WriteWord(WordToLE(ALastColIndex + 1));
  // Unlike specified in the excelfileformat.pdf, Excel 2 wants to have the
  // last column index incremented by 1!

  { Attributes }
  xf := FindXFIndex(AFormatIndex);
  GetAttributes(AFormatIndex, xf, attr1, attr2, attr3);
  AStream.WriteByte(attr1);
  AStream.WriteByte(attr2);
  AStream.WriteByte(attr3);

  { Not used }
  AStream.WriteWord(0);
end;

procedure TsSpreadBIFF2Writer.WriteColumnDefaults(AStream: TStream);
var
  j, j1: Integer;
  sheet: TsWorksheet;
  lCol, lCol1: PCol;
  lastcol: Integer;
begin
  sheet := TsWorkbook(FWorkbook).GetFirstWorksheet;
  j := 0;
  while (j < sheet.Cols.Count) do begin
    lCol := PCol(sheet.Cols[j]);
    j1 := j;
    lastcol := lCol^.Col;
    while (j1 < sheet.Cols.Count) do begin
      lCol1 := PCol(sheet.Cols[j1]);
      if lCol1^.FormatIndex <> lCol^.FormatIndex then
        break;
      lastCol := lCol1^.Col;
      inc(j1);
    end;
    WriteColumnDefault(AStream, lCol^.Col, lastCol, lCol^.FormatIndex);
    j := j1;
  end;
{
  for j := 0 to sheet.Cols.Count-1 do begin
    lCol := PCol(sheet.Cols[j]);
    if lCol^.FormatIndex > 0 then
      WriteColumnDefault(AStream, lCol);
  end;
  }
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 COLWIDTH record
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteColWidth(AStream: TStream; ACol: PCol);
type
  TColRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    StartCol: Byte;
    EndCol: Byte;
    ColWidth: Word;
  end;
var
  rec: TColRecord;
  w: Single;
begin
  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_COLWIDTH);
  rec.RecordSize := WordToLE(SizeOf(TColRecord) - 4);

  { Start and end column }
  rec.StartCol := ACol^.Col;
  rec.EndCol := ACol^.Col;

  { Column width }
  { calculate width to be in units of 1/256 of pixel width of character "0" }
  if ACol^.ColWidthType = cwtDefault then
    w := TsWorksheet(FWorksheet).ReadDefaultColWidth(suChars)
  else
    w := tsWorkbook(FWorkbook).ConvertUnits(ACol^.Width, FWorkbook.Units, suChars);
  rec.ColWidth := WordToLE(round(w*256));

  { Write out }
  AStream.WriteBuffer(rec, SizeOf(rec));
end;

{@@ ----------------------------------------------------------------------------
  Write COLWIDTH records for all columns
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteColWidths(AStream: TStream);
var
  j: Integer;
  sheet: TsWorksheet;
  col: PCol;
begin
  sheet := TsWorkbook(FWorkbook).GetFirstWorksheet;
  for j := 0 to sheet.Cols.Count-1 do begin
    col := PCol(sheet.Cols[j]);
    WriteColWidth(AStream, col);
  end;
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 DIMENSIONS record
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteDimensions(AStream: TStream;
  AWorksheet: TsBasicWorksheet);
var
  firstRow, lastRow, firstCol, lastCol: Cardinal;
  rec: TBIFF2_DimensionsRecord;
begin
  { Determine sheet size }
  GetSheetDimensions(AWorksheet, firstRow, lastRow, firstCol, lastCol);

  { Populate BIFF record }
  rec.RecordID := WordToLE(INT_EXCEL_ID_DIMENSIONS);
  rec.RecordSize := WordToLE(8);
  rec.FirstRow := WordToLE(firstRow);
  if lastRow < $FFFF then             // avoid WORD overflow when adding 1
    rec.LastRowPlus1 := WordToLE(lastRow+1)
  else
    rec.LastRowPlus1 := $FFFF;
  rec.FirstCol := WordToLE(firstCol);
  rec.LastColPlus1 := WordToLE(lastCol+1);

  { Write BIFF record to stream }
  AStream.WriteBuffer(rec, SizeOf(rec));
end;

{ ------------------------------------------------------------------------------
  Writes an Excel 2 IXFE record
  This record contains the "real" XF index if it is > 62.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteIXFE(AStream: TStream; XFIndex: Word);
begin
  { BIFF Record header }
  AStream.WriteWord(WordToLE(INT_EXCEL_ID_IXFE));
  AStream.WriteWord(WordToLE(2));
  AStream.WriteWord(WordToLE(XFIndex));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 file to a stream

  Excel 2.x files support only one Worksheet per Workbook,
  so only the first one will be written.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteToStream(AStream: TStream;
  AParams: TsStreamParams = []);
var
  pane: Byte;
begin
  Unused(AParams);

  FWorksheet := (FWorkbook as TsWorkbook).GetWorksheetByIndex(FSheetIndex);
  if FWorksheet = nil then
    raise EFPSpreadsheetWriter.Create(rsWorksheetNotFound1);

  WriteBOF(AStream);
    WriteCodePage(AStream, FCodePage);
    WritePrintHeaders(AStream);
    WritePrintGridLines(AStream);
    WriteDefaultRowHeight(AStream, FWorksheet);
    WriteHorizontalPageBreaks(AStream, FWorksheet);
    WriteVerticalPageBreaks(AStream, FWorksheet);
    WriteFonts(AStream);

    // Page settings block
    WriteHeaderFooter(AStream, true);    // true = header
    WriteHeaderFooter(AStream, false);   // false = footer
    WriteMargin(AStream, 0);             // 0 = left margin
    WriteMargin(AStream, 1);             // 1 = right margin
    WriteMargin(AStream, 2);             // 2 = top margin
    WriteMargin(AStream, 3);             // 3 = bottom margin

    WriteFormatCount(AStream);
    WriteNumFormats(AStream);

    if (bpLockStructure in Workbook.Protection) or FWorksheet.IsProtected then
      WritePROTECT(AStream, true);
    WriteWindowProtect(AStream, bpLockWindows in Workbook.Protection);
    WriteObjectProtect(AStream, FWorksheet);
    WritePASSWORD(AStream);

    WriteXFRecords(AStream);
    WriteDefaultColWidth(AStream, FWorksheet);
    WriteColWidths(AStream);
    WriteDimensions(AStream, FWorksheet);
    WriteColumnDefaults(AStream);
    WriteRows(AStream, FWorksheet);

    if (boVirtualMode in Workbook.Options) then
      WriteVirtualCells(AStream, FWorksheet)
    else
      WriteCellsToStream(AStream, TsWorksheet(FWorksheet).Cells);

    WriteWindow1(AStream);
    //  { -- currently not working
    WriteWindow2(AStream, FWorksheet);
    WritePane(AStream, FWorksheet, false, pane);  // false = "is not BIFF5 or BIFF8"
    WriteSelections(AStream, FWorksheet);
      //}
  WriteEOF(AStream);
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 WINDOW1 record
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteWindow1(AStream: TStream);
begin
  { BIFF Record header }
  AStream.WriteWord(WordToLE(INT_EXCEL_ID_WINDOW1));
  AStream.WriteWord(WordToLE(9));

  { Horizontal position of the document window, in twips = 1 / 20 of a point }
  AStream.WriteWord(WordToLE(0));

  { Vertical position of the document window, in twips = 1 / 20 of a point }
  AStream.WriteWord(WordToLE($0069));

  { Width of the document window, in twips = 1 / 20 of a point }
  AStream.WriteWord(WordToLE($339F));

  { Height of the document window, in twips = 1 / 20 of a point }
  AStream.WriteWord(WordToLE($1B5D));

  { Window is visible (1) / hidden (0) }
  AStream.WriteByte(WordToLE(0));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 WINDOW2 record
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteWindow2(AStream: TStream;
  ASheet: TsBasicWorksheet);
var
  b: Byte;
  sheet: TsWorksheet absolute ASheet;
begin
  { BIFF Record header }
  AStream.WriteWord(WordToLE(INT_EXCEL_ID_WINDOW2));
  AStream.WriteWord(WordToLE(14));

  { Show formulas, not results }
  AStream.WriteByte(0);

  { Show grid lines }
  b := IfThen(soShowGridLines in sheet.Options, 1, 0);
  AStream.WriteByte(b);

  { Show sheet headers }
  b := IfThen(soShowHeaders in sheet.Options, 1, 0);
  AStream.WriteByte(b);

  { Panes are frozen? }
  b := 0;
  if (soHasFrozenPanes in sheet.Options) and
     ((sheet.LeftPaneWidth > 0) or (sheet.TopPaneHeight > 0))
  then
    b := 1;
  AStream.WriteByte(b);

  { Show zero values as zeros, not empty cells }
  AStream.WriteByte(1);

  { Index to first visible row }
  AStream.WriteWord(0);

  { Index to first visible column }
  AStream.WriteWord(0);

  { Use automatic grid line color }
  AStream.WriteByte(1);

  { RGB of manual grid line color }
  AStream.WriteDWord(0);
end;

procedure TsSpreadBIFF2Writer.WriteXF(AStream: TStream;
 AFormatRecord: PsCellFormat; XFType_Prot: Byte = 0);
var
  rec: TBIFF2_XFRecord;
  b: Byte;
  formatIdx, fontIdx: Integer;
  fmtProt: byte;
begin
  Unused(XFType_Prot);
  GetFormatAndFontIndex(AFormatRecord, formatIdx, fontIdx);

  { BIFF Record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_XF);
  rec.RecordSize := WordToLE(SizeOf(TBIFF2_XFRecord) - 2*SizeOf(word));

  { Index to FONT record }
  rec.FontIndex := WordToLE(fontIdx);

  { Not used byte }
  rec.NotUsed := 0;

  { Number format index and cell flags
      Bit   Mask  Contents
      ----- ----  --------------------------------
      5-0   $3F   Index to (number) FORMAT record
       6    $40   1 = Cell is locked
       7    $80   1 = Formula is hidden }
  fmtProt := formatIdx + MASK_XF_TYPE_PROT_LOCKED_BIFF2;
  if AFormatRecord <> nil then
  begin
    if not (cpLockCell in AFormatRecord^.Protection) then
      fmtProt := fmtProt and not MASK_XF_TYPE_PROT_LOCKED_BIFF2;
    if (cpHideFormulas in AFormatRecord^.Protection) then
      fmtProt := fmtProt or MASK_XF_TYPE_PROT_FORMULA_HIDDEN_BIFF2;
  end;
  rec.NumFormat_Prot := WordToLE(fmtProt);

  {Horizontal alignment, border style, and background
  Bit  Mask  Contents
  ---  ----  ------------------------------------------------
  2-0  $07   XF_HOR_ALIGN – Horizontal alignment (0=General, 1=Left, 2=Centered, 3=Right)
   3   $08   1 = Cell has left black border
   4   $10   1 = Cell has right black border
   5   $20   1 = Cell has top black border
   6   $40   1 = Cell has bottom black border
   7   $80   1 = Cell has shaded background }
  b := 0;
  if (AFormatRecord <> nil) then
  begin
    if (uffHorAlign in AFormatRecord^.UsedFormattingFields) then
      b := b + byte(AFormatRecord^.HorAlignment);
    if (uffBorder in AFormatRecord^.UsedFormattingFields) then
    begin
      if cbWest in AFormatRecord^.Border then b := b or $08;
      if cbEast in AFormatRecord^.Border then b := b or $10;
      if cbNorth in AFormatRecord^.Border then b := b or $20;
      if cbSouth in AFormatRecord^.Border then b := b or $40;
    end;
    if (uffBackground in AFormatRecord^.UsedFormattingFields) then
      b := b or $80;
  end;
  rec.HorAlign_Border_BkGr:= b;

  { Write out }
  AStream.WriteBuffer(rec, SizeOf(rec));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 BOF record
  This must be the first record in an Excel 2 stream
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteBOF(AStream: TStream);
begin
  { BIFF Record header }
  WriteBiffHeader(AStream, INT_EXCEL_ID_BOF, 4);

  { Unused }
  AStream.WriteWord($0000);

  { Data type }
  AStream.WriteWord(WordToLE(INT_EXCEL_SHEET));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 EOF record
  This must be the last record in an Excel 2 stream
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteEOF(AStream: TStream);
begin
  { BIFF Record header }
  WriteBiffHeader(AStream, INT_EXCEL_ID_EOF, 0);
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 font record
  The font data is passed as font index.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteFont(AStream: TStream; AFontIndex: Integer);
var
  Len: Byte;
  lFontName: AnsiString;
  optn: Word;
  font: TsFont;
begin
  font := TsWorkbook(FWorkbook).GetFont(AFontIndex);
  if font = nil then  // this happens for FONT4 in case of BIFF
    exit;

  if font.FontName = '' then
    raise EFPSpreadsheetWriter.Create('Font name not specified.');
  if font.Size <= 0.0 then
    raise EFPSpreadsheetWriter.Create('Font size not specified.');

  lFontName := font.FontName;
  Len := Length(lFontName);

  { BIFF Record header }
  WriteBiffHeader(AStream, INT_EXCEL_ID_FONT, 4 + 1 + Len * SizeOf(AnsiChar));

  { Height of the font in twips = 1/20 of a point }
  AStream.WriteWord(WordToLE(round(font.Size*20)));

  { Option flags }
  optn := 0;
  if fssBold in font.Style then optn := optn or $0001;
  if fssItalic in font.Style then optn := optn or $0002;
  if fssUnderline in font.Style then optn := optn or $0004;
  if fssStrikeout in font.Style then optn := optn or $0008;
  AStream.WriteWord(WordToLE(optn));

  { Font name: Unicodestring, char count in 1 byte }
  AStream.WriteByte(Len);
  AStream.WriteBuffer(lFontName[1], Len * Sizeof(AnsiChar));

  { Font color: goes into next record! }

  { BIFF Record header }
  AStream.WriteWord(WordToLE(INT_EXCEL_ID_FONTCOLOR));
  AStream.WriteWord(WordToLE(2));

  { Font color index, only first 8 palette entries allowed! }
  AStream.WriteWord(WordToLE(PaletteIndex(font.Color)));
end;

{@@ ----------------------------------------------------------------------------
  Writes all font records to the stream
  @see WriteFont
-------------------------------------------------------------------------------}
procedure TsSpreadBiff2Writer.WriteFonts(AStream: TStream);
var
  i: Integer;
begin
  for i:=0 to TsWorkbook(FWorkbook).GetFontCount-1 do
    WriteFont(AStream, i);
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 FORMAT record which describes formatting of numerical data.
-------------------------------------------------------------------------------}
procedure TsSpreadBiff2Writer.WriteFORMAT(AStream: TStream;
  ANumFormatStr: String; AFormatIndex: Integer);
type
  TNumFormatRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    FormatLen: Byte;
  end;
var
  len: Integer;
  s: string; //ansistring;
  rec: TNumFormatRecord;
  buf: array of byte;
begin
  Unused(AFormatIndex);

  { Convert format string to code page used by the writer }
  s := ConvertEncoding(ANumFormatStr, encodingUTF8, FCodePage);
  len := Length(s);

  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_FORMAT);
  rec.RecordSize := WordToLE(1 + len);

  { Length byte of format string }
  rec.FormatLen := len;

  { Copy the format string characters into a buffer immediately after rec }
  SetLength(buf, SizeOf(rec) + SizeOf(ansiChar)*len);
  Move(rec, buf[0], SizeOf(rec));
  Move(s[1], buf[SizeOf(rec)], len*SizeOf(ansiChar));

  { Write out }
  AStream.WriteBuffer(buf[0], SizeOf(rec) + SizeOf(ansiChar)*len);

  { Clean up }
  SetLength(buf, 0);
end;

{@@ ----------------------------------------------------------------------------
  Writes the number of FORMAT records contained in the file.

  There are 21 built-in formats. The file may contain more, but Excel
  expects a "21" here...
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteFORMATCOUNT(AStream: TStream);
begin
  WriteBiffHeader(AStream, INT_EXCEL_ID_FORMATCOUNT, 2);
  AStream.WriteWord(WordToLE(21));
//  AStream.WriteWord(WordToLE(NumFormatList.Count));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 FORMULA record
  The formula is an RPN formula that was converted from usual user-readable
  string to an RPN array by the calling method.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteRPNFormula(AStream: TStream;
  const ARow, ACol: Cardinal; AFormula: TsRPNFormula; ACell: PCell);
var
  RPNLength: Word;
  RecordSizePos, FinalPos: Cardinal;
  xf: Word;
  isSupported: Boolean;
  unsupportedFormulas: String;
begin
  if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then
    exit;

  { Check if formula is supported by this file format. If not, write only
    the result }
  isSupported := FormulaSupported(AFormula, unsupportedFormulas);
  if not IsSupported then
    Workbook.AddErrorMsg(rsFormulaNotSupported, [
      GetCellString(ARow, ACol), unsupportedformulas
    ]);

  RPNLength := 0;

  xf := FindXFIndex(ACell^.FormatIndex);
  if xf >= 63 then
    WriteIXFE(AStream, xf);

  { BIFF Record header }
  AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMULA));
  RecordSizePos := AStream.Position;
  AStream.WriteWord(0); // We don't know the record size yet. It will be replaced at end.

  { Row and column }
  AStream.WriteWord(WordToLE(ARow));
  AStream.WriteWord(WordToLE(ACol));

  { BIFF2 Attributes }
  WriteCellAttributes(AStream, ACell^.FormatIndex, xf);

  { Encoded result of RPN formula }
  WriteRPNResult(AStream, ACell);

  { 0 = Do not recalculate
    1 = Always recalculate }
  AStream.WriteByte(1);

  { Formula data (RPN token array) }
  WriteRPNTokenArray(AStream, ACell, AFormula, false, IsSupported, RPNLength);

  { Finally write sizes after we know them }
  FinalPos := AStream.Position;
  AStream.Position := RecordSizePos;
  AStream.WriteWord(WordToLE(17 + RPNLength));
  AStream.Position := FinalPos;

  { Write following STRING record if formula result is a non-empty string }
  if (ACell^.ContentType = cctUTF8String) and (ACell^.UTF8StringValue <> '') then
    WriteSTRINGRecord(AStream, ACell^.UTF8StringValue);
end;

{@@ ----------------------------------------------------------------------------
  Writes the identifier for an RPN function with fixed argument count and
  returns the number of bytes written.
-------------------------------------------------------------------------------}
function TsSpreadBIFF2Writer.WriteRPNFunc(AStream: TStream;
  AIdentifier: Word): Word;
begin
  AStream.WriteByte(Lo(AIdentifier));
  Result := 1;
end;

{@@ ----------------------------------------------------------------------------
  Writes the size of the RPN token array. Called from WriteRPNFormula.
  Overrides xlscommon.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteRPNTokenArraySize(AStream: TStream;
  ASize: Word);
begin
  AStream.WriteByte(ASize);
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 STRING record which immediately follows a FORMULA record
  when the formula result is a string.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteStringRecord(AStream: TStream;
  AString: String);
var
  s: ansistring;
  len: Integer;
begin
  s := ConvertEncoding(AString, encodingUTF8, FCodePage);
  len := Length(s);

  { BIFF Record header }
  WriteBiffHeader(AStream, INT_EXCEL_ID_STRING, 1 + len*SizeOf(ansichar));

  { Write string length }
  AStream.WriteByte(len);
  { Write characters }
  AStream.WriteBuffer(s[1], len * SizeOf(ansichar));
end;

{@@ ----------------------------------------------------------------------------
  Writes a Excel 2 BOOLEAN cell record.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteBool(AStream: TStream;
  const ARow, ACol: Cardinal; const AValue: Boolean; ACell: PCell);
var
  rec: TBIFF2_BoolErrRecord;
  xf: Integer;
begin
  if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then
    exit;

  xf := FindXFIndex(ACell^.FormatIndex);
  if xf >= 63 then
    WriteIXFE(AStream, xf);

  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_BOOLERROR);
  rec.RecordSize := WordToLE(9);

  { Row and column index }
  rec.Row := WordToLE(ARow);
  rec.Col := WordToLE(ACol);

  { BIFF2 attributes }
  GetAttributes(ACell^.FormatIndex, xf, rec.Attrib1, rec.Attrib2, rec.Attrib3);

  { Cell value }
  rec.BoolErrValue := ord(AValue);
  rec.ValueType := 0;  // 0 = boolean value, 1 = error value

  { Write out }
  AStream.WriteBuffer(rec, SizeOf(rec));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 DEFAULTROWHEIGHT record
  Specifies the default height and default flags for rows that do not have a
  corresponding ROW record
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteDefaultRowHeight(AStream: TStream;
  AWorksheet: TsBasicWorksheet);
var
  h: Single;
begin
  { BIFF record header }
  WriteBIFFHeader(AStream, INT_EXCEL_ID_DEFROWHEIGHT, 2);

  { Default height for unused rows, in twips = 1/20 of a point
    Bits 0-14: Default height for unused rows, in twips
    Bit 15 = 1: Row height not changed manually }
  h := TsWorksheet(AWorksheet).ReadDefaultRowHeight(suPoints);  // h is in points
  AStream.WriteWord(WordToLE(PtsToTwips(h)));      // write as twips
end;


{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 ERROR cell record.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteError(AStream: TStream;
  const ARow, ACol: Cardinal; const AValue: TsErrorValue; ACell: PCell);
var
  rec: TBIFF2_BoolErrRecord;
  xf: Integer;
begin
  if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then
    exit;

  xf := FindXFIndex(ACell^.FormatIndex);
  if xf >= 63 then
    WriteIXFE(AStream, xf);

  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_BOOLERROR);
  rec.RecordSize := WordToLE(9);

  { Row and column index }
  rec.Row := WordToLE(ARow);
  rec.Col := WordToLE(ACol);

  { BIFF2 attributes }
  GetAttributes(ACell^.FormatIndex, xf, rec.Attrib1, rec.Attrib2, rec.Attrib3);

  { Cell value }
  rec.BoolErrValue := ConvertToExcelError(AValue);
  rec.ValueType := 1;  // 0 = boolean value, 1 = error value

  { Write out }
  AStream.WriteBuffer(rec, SizeOf(rec));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 record for an empty cell
  Required if this cell should contain formatting, but no data.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteBlank(AStream: TStream;
  const ARow, ACol: Cardinal; ACell: PCell);
type
  TBlankRecord = packed record
    RecordID: Word;
    RecordSize: Word;
    Row: Word;
    Col: Word;
    Attrib1, Attrib2, Attrib3: Byte;
  end;
var
  xf: Word;
  rec: TBlankRecord;
begin
  if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then
    exit;

  xf := FindXFIndex(ACell^.FormatIndex);
  if xf >= 63 then
    WriteIXFE(AStream, xf);

  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_BLANK);
  rec.RecordSize := WordToLE(7);

  { BIFF record data }
  rec.Row := WordToLE(ARow);
  rec.Col := WordToLE(ACol);

  { BIFF2 attributes }
  GetAttributes(ACell^.FormatIndex, xf, rec.Attrib1, rec.Attrib2, rec.Attrib3);

  { Write out }
  AStream.WriteBuffer(rec, Sizeof(rec));
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 LABEL record
  If the string length exceeds 255 bytes, the string will be truncated and an
  error message will be logged.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteLabel(AStream: TStream; const ARow,
  ACol: Cardinal; const AValue: string; ACell: PCell);
const
  MAXBYTES = 255; //limit for this format
var
  L: Byte;
  AnsiText: ansistring;
  rec: TBIFF2_LabelRecord;
  buf: array of byte;
var
  xf: Word;
begin
  if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then
    exit;

  if AValue = '' then Exit; // Writing an empty text doesn't work

  AnsiText := UTF8ToISO_8859_1(FixLineEnding(AValue));

  if Length(AnsiText) > MAXBYTES then begin
    // BIFF 5 does not support labels/text bigger than 255 chars,
    // so BIFF2 won't either
    // Rather than lose data when reading it, let the application programmer deal
    // with the problem or purposefully ignore it.
    AnsiText := Copy(AnsiText, 1, MAXBYTES);
    Workbook.AddErrorMsg(rsTruncateTooLongCellText, [
      MAXBYTES, GetCellString(ARow, ACol)
    ]);
  end;
  L := Length(AnsiText);

  xf := FindXFIndex(ACell^.FormatIndex);
  if xf >= 63 then
    WriteIXFE(AStream, xf);

  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_LABEL);
  rec.RecordSize := WordToLE(8 + L);

  { BIFF record data }
  rec.Row := WordToLE(ARow);
  rec.Col := WordToLE(ACol);

  { BIFF2 attributes }
  GetAttributes(ACell^.FormatIndex, xf, rec.Attrib1, rec.Attrib2, rec.Attrib3);

  { Text length: 8 bit }
  rec.TextLen := L;

  { Copy the text characters into a buffer immediately after rec }
  SetLength(buf, SizeOf(rec) + SizeOf(ansiChar)*L);
  Move(rec, buf[0], SizeOf(rec));
  Move(AnsiText[1], buf[SizeOf(rec)], L*SizeOf(ansiChar));

  { Write out }
  AStream.WriteBuffer(buf[0], SizeOf(Rec) + SizeOf(ansiChar)*L);
end;

{@@ ----------------------------------------------------------------------------
  Writes an Excel 2 NUMBER record
  A "number" is a 64-bit IEE 754 floating point.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.WriteNumber(AStream: TStream; const ARow,
  ACol: Cardinal; const AValue: double; ACell: PCell);
var
  xf: Word;
  rec: TBIFF2_NumberRecord;
begin
  if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then
    exit;

  xf := FindXFIndex(ACell^.FormatIndex);
  if xf >= 63 then
    WriteIXFE(AStream, xf);

  { BIFF record header }
  rec.RecordID := WordToLE(INT_EXCEL_ID_NUMBER);
  rec.RecordSize := WordToLE(15);

  { BIFF record data }
  rec.Row := WordToLE(ARow);
  rec.Col := WordToLE(ACol);

  { BIFF2 attributes }
  GetAttributes(ACell^.FormatIndex, xf, rec.Attrib1, rec.Attrib2, rec.Attrib3);

  { Number value }
  rec.Value := AValue;

  { Write out }
  AStream.WriteBuffer(rec, SizeOf(Rec));
end;

procedure TsSpreadBIFF2Writer.WritePassword(AStream: TStream);
var
  hash: Word;
  hb, hs: LongInt;
  book: TsWorkbook;
  sheet: TsWorksheet;
begin
  book := FWorkbook as TsWorkbook;
  sheet := FWorksheet as TsWorksheet;

  hb := 0;
  if (book.CryptoInfo.PasswordHash <> '') and
     not TryStrToInt('$' + book.CryptoInfo.PasswordHash, hb) then
  begin
    book.AddErrorMsg(rsPasswordRemoved_NotValid);
    exit;
  end;

  hs := 0;
  if (sheet.CryptoInfo.PasswordHash <> '') and
     not TryStrToInt('$' + sheet.CryptoInfo.PasswordHash, hs) then
  begin
    book.AddErrorMsg(rsPasswordRemoved_NotValid);
    exit;
  end;

  // Neither workbook nor worksheet password set
  if (hb = 0) and (hs = 0) then
    exit;

  // Only workbook password set. Check for Excel algorithm.
  if (hb <> 0) and (hs = 0) then begin
    if book.CryptoInfo.Algorithm <> caExcel then begin
      book.AddErrorMsg(rsPasswordRemoved_Excel);
      exit;
    end;
    hash := hb;
  end else
  // Only worksheet password set, check for Excel algorithm
  if (hs <> 0) and (hb = 0) then begin
    if sheet.CryptoInfo.Algorithm <> caExcel then begin
      book.AddErrorMsg(rsPasswordRemoved_Excel);
      exit;
    end;
    hash := hs;
  end else
  if (hs <> hb) then begin
    book.AddErrorMsg(rsPasswordRemoved_BIFF2);
    exit;
  end else
  if (book.CryptoInfo.Algorithm <> caExcel) or
     (sheet.CryptoInfo.Algorithm <> caExcel) then
  begin
    book.AddErrorMsg(rsPasswordRemoved_Excel);
    exit;
  end else
    hash := hs;  // or hb -- they are equal here.

  // Write out record
  WriteBIFFHeader(AStream, INT_EXCEL_ID_PASSWORD, 2);
  AStream.WriteWord(WordToLE(hash));
end;

procedure TsSpreadBIFF2Writer.WriteRow(AStream: TStream; ASheet: TsBasicWorksheet;
  ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow);
var
  containsXF: Boolean;
  rowheight: Word;
  auto: Boolean;
  w: Word;
  xf: Word;
  book: TsWorkbook;
  sheet: TsWorksheet;
begin
  if (ARowIndex >= FLimitations.MaxRowCount) or
     (AFirstColIndex >= FLimitations.MaxColCount) or
     (ALastColIndex >= FLimitations.MaxColCount)
  then
    exit;

  book := FWorkbook as TsWorkbook;
  sheet := ASheet as TsWorksheet;

  containsXF := (ARow <> nil) and (ARow^.FormatIndex > 0);

  { BIFF record header }
  WriteBiffHeader(AStream, INT_EXCEL_ID_ROW, IfThen(containsXF, 18, 13));

  { Index of row }
  AStream.WriteWord(WordToLE(Word(ARowIndex)));

  { Index to column of the first cell which is described by a cell record }
  AStream.WriteWord(WordToLE(Word(AFirstColIndex)));

  { Index to column of the last cell which is described by a cell record, increased by 1 }
  AStream.WriteWord(WordToLE(Word(ALastColIndex) + 1));

  auto := true;
  { Row height (in twips, 1/20 point) and info on custom row height }
  if (ARow = nil) or (ARow^.RowHeightType = rhtDefault) then
    rowheight := PtsToTwips(sheet.ReadDefaultRowHeight(suPoints))
  else
  if (ARow^.Height = 0) then
    rowheight := 0
  else begin
    rowheight := PtsToTwips(book.ConvertUnits(ARow^.Height, book.Units, suPoints));
    auto := ARow^.RowHeightType <> rhtCustom;
  end;
  w := rowheight and $7FFF;
  if auto then
    w := w or $8000;
  AStream.WriteWord(WordToLE(w));

  { not used }
  AStream.WriteWord(0);

  { Does the record contain row attribute field and XF index? }
  AStream.WriteByte(ord(containsXF));

  { Relative offset to calculate stream position of the first cell record for this row }
  AStream.WriteWord(0);

  if containsXF then begin
    xf := FindXFIndex(ARow^.FormatIndex);

  { Default row attributes }
    WriteCellAttributes(AStream, ARow^.FormatIndex, xf);

    { Index to XF record }
    AStream.WriteWord(WordToLE(xf));
  end;
end;

{*******************************************************************
*  Initialization section
*
*  Registers this reader / writer to fpspreadsheet
*  Converts the palette to litte-endian
*
*******************************************************************}

initialization

 {$IFDEF MSWINDOWS}
  Excel2Settings.CodePage := GetDefaultTextEncoding;
 {$ENDIF}

  sfidExcel2 := RegisterSpreadFormat(sfExcel2,
    TsSpreadBIFF2Reader, TsSpreadBIFF2Writer,
    STR_FILEFORMAT_EXCEL_2, 'BIFF2', [STR_EXCEL_EXTENSION]
  );

  MakeLEPalette(PALETTE_BIFF2);

end.