{
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, fpspreadsheet, 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 ReadRowColXF(AStream: TStream; out ARow, ACol: Cardinal; out AXF: Word); override;
    procedure ReadRowInfo(AStream: TStream); override;
    function ReadRPNAttr(AStream: TStream; AIdentifier: Byte): Boolean; 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: TsWorkbook); override;
    { General reading methods }
    procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); 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: TsWorksheet);
    procedure WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet);
    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 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 WriteRow(AStream: TStream; ASheet: TsWorksheet;
      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: TsWorksheet);
    procedure WriteXF(AStream: TStream; AFormatRecord: PsCellFormat;
      XFType_Prot: Byte = 0); override;
  public
    constructor Create(AWorkbook: TsWorkbook); 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;

implementation

uses
  Math,
  fpsStrings, 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;

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;
    NumFormatIndex_Flags: Byte;
    HorAlign_Border_BkGr: Byte;
  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: TsWorkbook);
begin
  inherited Create(AWorkbook);
  FLimitations.MaxPaletteSize := BIFF2_MAX_PALETTE_SIZE;
end;

procedure TsSpreadBIFF2Reader.AddBuiltInNumFormats;
begin
  FFirstNumFormatIndexInFile := 0;
  //InternalAddBuiltInNumFormats(FNumFormatList, Workbook.FormatSettings);
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(ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := FWorksheet.AddCell(ARow, ACol);
  ApplyCellFormatting(cell, XF);
  if FIsVirtualMode then
    Workbook.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;
begin
  { 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(r, c, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := FWorksheet.AddCell(r, c);

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

  { Apply formatting }
  ApplyCellFormatting(cell, xf);

  if FIsVirtualMode then
    Workbook.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;
begin
  { 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 := Workbook.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex = -1 then
    fmt.FontIndex := Workbook.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 := Workbook.AddNumberFormat(nfs);
    nf := Workbook.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 := FWorkbook.AddCellFormat(fmt);

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

procedure TsSpreadBIFF2Reader.ReadColWidth(AStream: TStream);
const
  EPS = 1E-3;
var
  c, c1, c2: Cardinal;
  w: Word;
  colwidth: Single;
begin
  // 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.ConvertUnits(w / 256, suChars, FWorkbook.Units);
  // assign width to columns, but only if different from default column width.
  if not SameValue(colwidth, FWorksheet.ReadDefaultColWidth(FWorkbook.Units), EPS) then
    for c := c1 to c2 do
      FWorksheet.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.WriteDefaultRowHeight(h, suPoints);
  {
  h := TwipsToPts(hw and $8000) / FWorkbook.GetDefaultFontSize;
  if h > ROW_HEIGHT_CORRECTION then
    FWorksheet.DefaultRowHeight := h - ROW_HEIGHT_CORRECTION;
    }
end;

procedure TsSpreadBIFF2Reader.ReadFONT(AStream: TStream);
var
  lHeight: Word;
  lOptions: Word;
  Len: Byte;
  lFontName: UTF8String;
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;

  { Add font to internal font list }
  FFontList.Add(FFont);
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;
  AParams: TsStreamParams = []);
var
  BIFF2EOF: Boolean;
  RecordType: Word;
  CurStreamPos: Int64;
  BOFFound: Boolean;
begin
  Unused(AParams);
  BIFF2EOF := False;

  { In BIFF2 files there is only one worksheet, let's create it }
  FWorksheet := 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_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_PRINTGRID     : ReadPrintGridLines(AStream);
      INT_EXCEL_ID_PRINTHEADERS  : ReadPrintHeaders(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_WINDOW2       : ReadWindow2(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 Exception.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;
begin
  { 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(ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := FWorksheet.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
         FWorksheet.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;
           FWorksheet.WriteErrorValue(cell, err);
         end;
      3: // Empty cell
         FWorksheet.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
      FWorksheet.WriteDateTime(cell, dt, nf, nfs)
    else
      FWorksheet.WriteNumber(cell, formulaResult, nf, nfs);
  end;

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

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

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    Workbook.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;
begin
  { 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);
  {
  case WorkBookEncoding of
    seLatin2:   AStrValue := CP1250ToUTF8(AValue);
    seCyrillic: AStrValue := CP1251ToUTF8(AValue);
    seGreek:    AStrValue := CP1253ToUTF8(AValue);
    seTurkish:  AStrValue := CP1254ToUTF8(AValue);
    seHebrew:   AStrValue := CP1255ToUTF8(AValue);
    seArabic:   AStrValue := CP1256ToUTF8(AValue);
  else
    // Latin 1 is the default
    AStrValue := CP1252ToUTF8(AValue);
  end;
  }

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

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

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    Workbook.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;
begin
  { 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(ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := FWorksheet.AddCell(ARow, ACol);

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

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

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

procedure TsSpreadBIFF2Reader.ReadInteger(AStream: TStream);
var
  ARow, ACol: Cardinal;
  XF: Word;
  AWord  : Word = 0;
  cell: PCell;
  rec: TBIFF2_IntegerRecord;
begin
  { 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(ARow, ACol, FVirtualCell);
    cell := @FVirtualCell;
  end else
    cell := FWorksheet.AddCell(ARow, ACol);

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

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

  if FIsVirtualMode and (cell <> FIncompleteCell) then
    Workbook.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 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;
begin
  rowRec.RowIndex := 0;  // to silence the compiler...
  AStream.ReadBuffer(rowrec, SizeOf(TRowRecord));
  h := WordLEToN(rowrec.Height);
  auto := h and $8000 <> 0;
  rowheight := FWorkbook.ConvertUnits(TwipsToPts(h and $7FFF), suPoints, FWorkbook.Units);
  defRowHeight := FWorksheet.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 := FWorksheet.GetRow(WordLEToN(rowrec.RowIndex));
  lRow^.Height := rowHeight;
  if auto then
    lRow^.RowHeightType := rhtAuto else
    lRow^.RowHeightType := rhtCustom;
  lRow^.FormatIndex := XFToFormatIndex(xf);
end;

function TsSpreadBIFF2Reader.ReadRPNAttr(AStream: TStream; AIdentifier: Byte): Boolean;
begin
  Result := false;
  case AIdentifier of
    $01: AStream.ReadByte;   // tAttrVolatile
    else exit;       // others not supported by fpspreadsheet --> Result = false
  end;
  Result := true;
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
        Workbook.OnReadCellData(Workbook, 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;
begin
  // 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 := Workbook.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex = -1 then
    fmt.FontIndex := Workbook.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color);
  if fmt.FontIndex > 0 then
    Include(fmt.UsedFormattingFields, uffFont);

  // Number format index
  b := rec.NumFormatIndex_Flags and $3F;
  nfs := NumFormatList[b];
  if nfs <> '' then
  begin
    fmt.NumberFormatIndex := Workbook.AddNumberFormat(nfs);
    nf := Workbook.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;

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


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

constructor TsSpreadBIFF2Writer.Create(AWorkbook: TsWorkbook);
begin
  inherited Create(AWorkbook);
  FLimitations.MaxPaletteSize := BIFF2_MAX_PALETTE_SIZE;
  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.GetPointerToCellFormat(AFormatIndex);

  if fmt^.UsedFormattingFields = [] then begin
    Attrib1 := 15;
    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;

  // 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;
(*
{@@ ----------------------------------------------------------------------------
  Determines the cell attributes needed for writing a cell content record, such
  as WriteLabel, WriteNumber, etc.
  The cell attributes contain, in bit masks, xf record index, font index,
  borders, etc.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.GetCellAttributes(ACell: PCell; XFIndex: Word;
  out Attrib1, Attrib2, Attrib3: Byte);
begin
  fmt := Workbook.GetPointerToCellFormat(ACell^.FormatIndex);

  if fmt^.UsedFormattingFields = [] then begin
    Attrib1 := 15;
    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;

  // 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 := Workbook.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;

                   (*
{@@ ----------------------------------------------------------------------------
  Builds up the list of number formats to be written to the biff2 file.
  Unlike biff5+ no formats are added here because biff2 supports only 21
  standard formats; these formats have been added by AddBuiltInFormats.
  Nothing to do here.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Writer.ListAllNumFormats;
begin
  // Nothing to do here.
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 := Workbook.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;

    // 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 := Workbook.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 := FWorksheet.ReadDefaultColWidth(suChars)
  else
    w := 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 := Workbook.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: TsWorksheet);
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 := Workbook.GetWorksheetByIndex(FSheetIndex);
  if FWorksheet = nil then
    raise Exception.Create(rsWorksheetNotFound1);

  WriteBOF(AStream);
    WriteCodePage(AStream, FCodePage);
    WritePrintHeaders(AStream);
    WritePrintGridLines(AStream);
    WriteDefaultRowHeight(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);
    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, 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: TsWorksheet);
var
  b: Byte;
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 ASheet.Options, 1, 0);
  AStream.WriteByte(b);

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

  { Panes are frozen? }
  b := 0;
  if (soHasFrozenPanes in ASheet.Options) and
     ((ASheet.LeftPaneWidth > 0) or (ASheet.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;
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 }
  rec.NumFormatIndex_Flags := WordToLE(formatIdx);
  // Cell flags not used, so far...

  {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 := Workbook.GetFont(AFontIndex);
  if font = nil then  // this happens for FONT4 in case of BIFF
    exit;

  if font.FontName = '' then
    raise Exception.Create('Font name not specified.');
  if font.Size <= 0.0 then
    raise Exception.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 Workbook.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: TsWorksheet);
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 := 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.WriteRow(AStream: TStream; ASheet: TsWorksheet;
  ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow);
var
  containsXF: Boolean;
  rowheight: Word;
  auto: Boolean;
  w: Word;
  xf: Word;
begin
  if (ARowIndex >= FLimitations.MaxRowCount) or
     (AFirstColIndex >= FLimitations.MaxColCount) or
     (ALastColIndex >= FLimitations.MaxColCount)
  then
    exit;

  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(ASheet.ReadDefaultRowHeight(suPoints))
  else
  if (ARow^.Height = 0) then
    rowheight := 0
  else begin
    rowheight := PtsToTwips(FWorkbook.ConvertUnits(ARow^.Height, FWorkbook.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.
