lazarus-ccr/components/fpspreadsheet/xlscommon.pas

2161 lines
75 KiB
ObjectPascal

unit xlscommon;
{ Comments often have links to sections in the
OpenOffice Microsoft Excel File Format document }
{$ifdef fpc}
{$mode delphi}
{$endif}
interface
uses
Classes, SysUtils, DateUtils,
fpspreadsheet,
fpsutils, lconvencoding;
const
{ RECORD IDs which didn't change across versions 2-8 }
INT_EXCEL_ID_EOF = $000A;
INT_EXCEL_ID_SELECTION = $001D;
INT_EXCEL_ID_CONTINUE = $003C;
INT_EXCEL_ID_PANE = $0041;
INT_EXCEL_ID_CODEPAGE = $0042;
INT_EXCEL_ID_DATEMODE = $0022;
INT_EXCEL_ID_WINDOW1 = $003D;
{ RECORD IDs which did not change across versions 2, 5, 8}
INT_EXCEL_ID_FORMULA = $0006; // BIFF3: $0206, BIFF4: $0406
INT_EXCEL_ID_FONT = $0031; // BIFF3-4: $0231
{ RECORD IDs which did not change across version 3-8}
INT_EXCEL_ID_COLINFO = $007D; // does not exist in BIFF2
INT_EXCEL_ID_COUNTRY = $008C; // does not exist in BIFF2
INT_EXCEL_ID_PALETTE = $0092; // does not exist in BIFF2
INT_EXCEL_ID_DIMENSIONS = $0200; // BIFF2: $0000
INT_EXCEL_ID_BLANK = $0201; // BIFF2: $0001
INT_EXCEL_ID_NUMBER = $0203; // BIFF2: $0003
INT_EXCEL_ID_LABEL = $0204; // BIFF2: $0004
INT_EXCEL_ID_STRING = $0207; // BIFF2: $0007;
INT_EXCEL_ID_ROW = $0208; // BIFF2: $0008
INT_EXCEL_ID_INDEX = $020B; // BIFF2: $000B
INT_EXCEL_ID_WINDOW2 = $023E; // BIFF2: $003E
INT_EXCEL_ID_RK = $027E; // does not exist in BIFF2
INT_EXCEL_ID_STYLE = $0293; // does not exist in BIFF2
{ RECORD IDs which did not change across version 4-8 }
INT_EXCEL_ID_FORMAT = $041E; // BIFF2-3: $001E
{ RECORD IDs which did not change across versions 5-8 }
INT_EXCEL_ID_BOUNDSHEET = $0085; // Renamed to SHEET in the latest OpenOffice docs, does not exist before 5
INT_EXCEL_ID_MULRK = $00BD; // does not exist before BIFF5
INT_EXCEL_ID_MULBLANK = $00BE; // does not exist before BIFF5
INT_EXCEL_ID_XF = $00E0; // BIFF2:$0043, BIFF3:$0243, BIFF4:$0443
INT_EXCEL_ID_RSTRING = $00D6; // does not exist before BIFF5
INT_EXCEL_ID_BOF = $0809; // BIFF2:$0009, BIFF3:$0209; BIFF4:$0409
{ FONT record constants }
INT_FONT_WEIGHT_NORMAL = $0190;
INT_FONT_WEIGHT_BOLD = $02BC;
{ Formula constants TokenID values }
{ Binary Operator Tokens 3.6}
INT_EXCEL_TOKEN_TADD = $03;
INT_EXCEL_TOKEN_TSUB = $04;
INT_EXCEL_TOKEN_TMUL = $05;
INT_EXCEL_TOKEN_TDIV = $06;
INT_EXCEL_TOKEN_TPOWER = $07; // Power Exponentiation ^
INT_EXCEL_TOKEN_TCONCAT = $08; // Concatenation &
INT_EXCEL_TOKEN_TLT = $09; // Less than <
INT_EXCEL_TOKEN_TLE = $0A; // Less than or equal <=
INT_EXCEL_TOKEN_TEQ = $0B; // Equal =
INT_EXCEL_TOKEN_TGE = $0C; // Greater than or equal >=
INT_EXCEL_TOKEN_TGT = $0D; // Greater than >
INT_EXCEL_TOKEN_TNE = $0E; // Not equal <>
INT_EXCEL_TOKEN_TISECT = $0F; // Cell range intersection
INT_EXCEL_TOKEN_TLIST = $10; // Cell range list
INT_EXCEL_TOKEN_TRANGE = $11; // Cell range
INT_EXCEL_TOKEN_TUPLUS = $12; // Unary plus +
INT_EXCEL_TOKEN_TUMINUS = $13; // Unary minus +
INT_EXCEL_TOKEN_TPERCENT= $14; // Percent (%, divides operand by 100)
INT_EXCEL_TOKEN_TPAREN = $15; // Operator in parenthesis
{ Constant Operand Tokens, 3.8}
INT_EXCEL_TOKEN_TMISSARG= $16; //missing operand
INT_EXCEL_TOKEN_TSTR = $17; //string
INT_EXCEL_TOKEN_TERR = $1C; //error value
INT_EXCEL_TOKEN_TBOOL = $1D; //boolean
INT_EXCEL_TOKEN_TINT = $1E; //(unsigned) integer
INT_EXCEL_TOKEN_TNUM = $1F; //floating-point
{ Operand Tokens }
// _R: reference; _V: value; _A: array
INT_EXCEL_TOKEN_TREFR = $24;
INT_EXCEL_TOKEN_TREFV = $44;
INT_EXCEL_TOKEN_TREFA = $64;
INT_EXCEL_TOKEN_TAREA_R = $25;
INT_EXCEL_TOKEN_TAREA_V = $45;
INT_EXCEL_TOKEN_TAREA_A = $65;
{ Function Tokens }
// _R: reference; _V: value; _A: array
// Offset 0: token; offset 1: index to a built-in sheet function ( ➜ 3.111)
INT_EXCEL_TOKEN_FUNC_R = $21;
INT_EXCEL_TOKEN_FUNC_V = $41;
INT_EXCEL_TOKEN_FUNC_A = $61;
//VAR: variable number of arguments:
INT_EXCEL_TOKEN_FUNCVAR_R = $22;
INT_EXCEL_TOKEN_FUNCVAR_V = $42;
INT_EXCEL_TOKEN_FUNCVAR_A = $62;
{ Built-in/worksheet functions }
INT_EXCEL_SHEET_FUNC_COUNT = 0;
INT_EXCEL_SHEET_FUNC_IF = 1;
INT_EXCEL_SHEET_FUNC_ISNA = 2;
INT_EXCEL_SHEET_FUNC_ISERROR = 3;
INT_EXCEL_SHEET_FUNC_SUM = 4;
INT_EXCEL_SHEET_FUNC_AVERAGE = 5;
INT_EXCEL_SHEET_FUNC_MIN = 6;
INT_EXCEL_SHEET_FUNC_MAX = 7;
INT_EXCEL_SHEET_FUNC_ROW = 8;
INT_EXCEL_SHEET_FUNC_COLUMN = 9;
INT_EXCEL_SHEET_FUNC_STDEV = 12;
INT_EXCEL_SHEET_FUNC_SIN = 15;
INT_EXCEL_SHEET_FUNC_COS = 16;
INT_EXCEL_SHEET_FUNC_TAN = 17;
INT_EXCEL_SHEET_FUNC_ATAN = 18;
INT_EXCEL_SHEET_FUNC_PI = 19;
INT_EXCEL_SHEET_FUNC_SQRT = 20;
INT_EXCEL_SHEET_FUNC_EXP = 21;
INT_EXCEL_SHEET_FUNC_LN = 22;
INT_EXCEL_SHEET_FUNC_LOG10 = 23;
INT_EXCEL_SHEET_FUNC_ABS = 24; // $18
INT_EXCEL_SHEET_FUNC_INT = 25;
INT_EXCEL_SHEET_FUNC_SIGN = 26;
INT_EXCEL_SHEET_FUNC_ROUND = 27; // $1B
INT_EXCEL_SHEET_FUNC_MID = 31;
INT_EXCEL_SHEET_FUNC_VALUE = 33;
INT_EXCEL_SHEET_FUNC_TRUE = 34;
INT_EXCEL_SHEET_FUNC_FALSE = 35;
INT_EXCEL_SHEET_FUNC_AND = 36;
INT_EXCEL_SHEET_FUNC_OR = 37;
INT_EXCEL_SHEET_FUNC_NOT = 38;
INT_EXCEL_SHEET_FUNC_VAR = 46;
INT_EXCEL_SHEET_FUNC_PV = 56;
INT_EXCEL_SHEET_FUNC_FV = 57;
INT_EXCEL_SHEET_FUNC_NPER = 58;
INT_EXCEL_SHEET_FUNC_PMT = 59;
INT_EXCEL_SHEET_FUNC_RATE = 60;
INT_EXCEL_SHEET_FUNC_RAND = 63;
INT_EXCEL_SHEET_FUNC_DATE = 65; // $41
INT_EXCEL_SHEET_FUNC_TIME = 66; // $42
INT_EXCEL_SHEET_FUNC_DAY = 67;
INT_EXCEL_SHEET_FUNC_MONTH = 68;
INT_EXCEL_SHEET_FUNC_YEAR = 69;
INT_EXCEL_SHEET_FUNC_WEEKDAY = 70;
INT_EXCEL_SHEET_FUNC_HOUR = 71;
INT_EXCEL_SHEET_FUNC_MINUTE = 72;
INT_EXCEL_SHEET_FUNC_SECOND = 73;
INT_EXCEL_SHEET_FUNC_NOW = 74;
INT_EXCEL_SHEET_FUNC_ROWS = 76;
INT_EXCEL_SHEET_FUNC_COLUMNS = 77;
INT_EXCEL_SHEET_FUNC_ASIN = 98;
INT_EXCEL_SHEET_FUNC_ACOS = 99;
INT_EXCEL_SHEET_FUNC_ISREF = 105;
INT_EXCEL_SHEET_FUNC_LOG = 109;
INT_EXCEL_SHEET_FUNC_CHAR = 111;
INT_EXCEL_SHEET_FUNC_LOWER = 112;
INT_EXCEL_SHEET_FUNC_UPPER = 113;
INT_EXCEL_SHEET_FUNC_PROPER = 114;
INT_EXCEL_SHEET_FUNC_LEFT = 115;
INT_EXCEL_SHEET_FUNC_RIGHT = 116;
INT_EXCEL_SHEET_FUNC_TRIM = 118;
INT_EXCEL_SHEET_FUNC_REPLACE = 119;
INT_EXCEL_SHEET_FUNC_SUBSTITUTE = 120;
INT_EXCEL_SHEET_FUNC_CODE = 121;
INT_EXCEL_SHEET_FUNC_CELL = 125;
INT_EXCEL_SHEET_FUNC_ISERR = 126;
INT_EXCEL_SHEET_FUNC_ISTEXT = 127;
INT_EXCEL_SHEET_FUNC_ISNUMBER = 128;
INT_EXCEL_SHEET_FUNC_ISBLANK = 129;
INT_EXCEL_SHEET_FUNC_DATEVALUE = 140;
INT_EXCEL_SHEET_FUNC_TIMEVALUE = 141;
INT_EXCEL_SHEET_FUNC_COUNTA = 169;
INT_EXCEL_SHEET_FUNC_PRODUCT = 183;
INT_EXCEL_SHEET_FUNC_ISNONTEXT = 190;
INT_EXCEL_SHEET_FUNC_STDEVP = 193;
INT_EXCEL_SHEET_FUNC_VARP = 194;
INT_EXCEL_SHEET_FUNC_ISLOGICAL = 198;
INT_EXCEL_SHEET_FUNC_TODAY = 221; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_MEDIAN = 227; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_SINH = 229; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_COSH = 230; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_TANH = 231; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_ASINH = 232; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_ACOSH = 233; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_ATANH = 234; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_INFO = 244; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_AVEDEV = 269; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_BETADIST = 270; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_BETAINV = 272; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_BINOMDIST = 273; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_CHIDIST = 274; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_CHIINV = 275; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_PERMUT = 299; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_POISSON = 300; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_SUMSQ = 321; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_RADIANS = 342; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_DEGREES = 343; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_SUMIF = 345; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_COUNTIF = 346; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_COUNTBLANK = 347; // not available in BIFF2
INT_EXCEL_SHEET_FUNC_DATEDIF = 351; // not available in BIFF2
{ Control Tokens, Special Tokens }
// 01H tExp Matrix formula or shared formula
// 02H tTbl Multiple operation table
// 15H tParen Parentheses
// 18H tNlr Natural language reference (BIFF8)
INT_EXCEL_TOKEN_TATTR = $19; // tAttr Special attribute
// 1AH tSheet Start of external sheet reference (BIFF2-BIFF4)
// 1BH tEndSheet End of external sheet reference (BIFF2-BIFF4)
{ CODEPAGE record constants }
WORD_ASCII = 367;
WORD_UTF_16 = 1200; // BIFF 8
WORD_CP_1250_Latin2 = 1250;
WORD_CP_1251_Cyrillic = 1251;
WORD_CP_1252_Latin1 = 1252; // BIFF4-BIFF5
WORD_CP_1253_Greek = 1253;
WORD_CP_1254_Turkish = 1254;
WORD_CP_1255_Hebrew = 1255;
WORD_CP_1256_Arabic = 1256;
WORD_CP_1257_Baltic = 1257;
WORD_CP_1258_Vietnamese = 1258;
WORD_CP_1258_Latin1_BIFF2_3 = 32769; // BIFF2-BIFF3
{ DATEMODE record, 5.28 }
DATEMODE_1900_BASE=1; //1/1/1900 minus 1 day in FPC TDateTime
DATEMODE_1904_BASE=1462; //1/1/1904 in FPC TDateTime
{ WINDOW1 record constants - BIFF5-BIFF8 }
MASK_WINDOW1_OPTION_WINDOW_HIDDEN = $0001;
MASK_WINDOW1_OPTION_WINDOW_MINIMISED = $0002;
MASK_WINDOW1_OPTION_HORZ_SCROLL_VISIBLE = $0008;
MASK_WINDOW1_OPTION_VERT_SCROLL_VISIBLE = $0010;
MASK_WINDOW1_OPTION_WORKSHEET_TAB_VISIBLE = $0020;
{ WINDOW2 record constants - BIFF3-BIFF8 }
MASK_WINDOW2_OPTION_SHOW_FORMULAS = $0001;
MASK_WINDOW2_OPTION_SHOW_GRID_LINES = $0002;
MASK_WINDOW2_OPTION_SHOW_SHEET_HEADERS = $0004;
MASK_WINDOW2_OPTION_PANES_ARE_FROZEN = $0008;
MASK_WINDOW2_OPTION_SHOW_ZERO_VALUES = $0010;
MASK_WINDOW2_OPTION_AUTO_GRIDLINE_COLOR = $0020;
MASK_WINDOW2_OPTION_COLUMNS_RIGHT_TO_LEFT = $0040;
MASK_WINDOW2_OPTION_SHOW_OUTLINE_SYMBOLS = $0080;
MASK_WINDOW2_OPTION_REMOVE_SPLITS_ON_UNFREEZE = $0100; //BIFF5-BIFF8
MASK_WINDOW2_OPTION_SHEET_SELECTED = $0200; //BIFF5-BIFF8
MASK_WINDOW2_OPTION_SHEET_ACTIVE = $0400; //BIFF5-BIFF8
{ XF substructures }
{ XF_TYPE_PROT - XF Type and Cell protection (3 Bits) - BIFF3-BIFF8 }
MASK_XF_TYPE_PROT_LOCKED = $1;
MASK_XF_TYPE_PROT_FORMULA_HIDDEN = $2;
MASK_XF_TYPE_PROT_STYLE_XF = $4; // 0 = CELL XF
{ XF_USED_ATTRIB - Attributes from parent Style XF (6 Bits) - BIFF3-BIFF8
- In a CELL XF a cleared bit means that the parent attribute is used,
while a set bit indicates that the data in this XF is used
- In a STYLE XF a cleared bit means that the data in this XF is used,
while a set bit indicates that the attribute should be ignored }
MASK_XF_USED_ATTRIB_NUMBER_FORMAT = $01;
MASK_XF_USED_ATTRIB_FONT = $02;
MASK_XF_USED_ATTRIB_TEXT = $04;
MASK_XF_USED_ATTRIB_BORDER_LINES = $08;
MASK_XF_USED_ATTRIB_BACKGROUND = $10;
MASK_XF_USED_ATTRIB_CELL_PROTECTION = $20;
{ the following values do not agree with the documentation !!!
MASK_XF_USED_ATTRIB_NUMBER_FORMAT = $04;
MASK_XF_USED_ATTRIB_FONT = $08;
MASK_XF_USED_ATTRIB_TEXT = $10;
MASK_XF_USED_ATTRIB_BORDER_LINES = $20;
MASK_XF_USED_ATTRIB_BACKGROUND = $40;
MASK_XF_USED_ATTRIB_CELL_PROTECTION = $80; }
{ XF record constants }
MASK_XF_TYPE_PROT = $0007;
MASK_XF_TYPE_PROT_PARENT = $FFF0;
MASK_XF_HOR_ALIGN = $07;
MASK_XF_VERT_ALIGN = $70;
MASK_XF_TEXTWRAP = $08;
{ XF HORIZONTAL ALIGN }
MASK_XF_HOR_ALIGN_LEFT = $01;
MASK_XF_HOR_ALIGN_CENTER = $02;
MASK_XF_HOR_ALIGN_RIGHT = $03;
MASK_XF_HOR_ALIGN_FILLED = $04;
MASK_XF_HOR_ALIGN_JUSTIFIED = $05; // BIFF4-BIFF8
MASK_XF_HOR_ALIGN_CENTERED_SELECTION = $06; // BIFF4-BIFF8
MASK_XF_HOR_ALIGN_DISTRIBUTED = $07; // BIFF8
{ XF_VERT_ALIGN }
MASK_XF_VERT_ALIGN_TOP = $00;
MASK_XF_VERT_ALIGN_CENTER = $10;
MASK_XF_VERT_ALIGN_BOTTOM = $20;
MASK_XF_VERT_ALIGN_JUSTIFIED = $30;
{ Cell Addresses constants, valid for BIFF2-BIFF5 }
MASK_EXCEL_ROW = $3FFF;
MASK_EXCEL_RELATIVE_COL = $4000;
MASK_EXCEL_RELATIVE_ROW = $8000;
{ Note: The assignment of the RELATIVE_COL and _ROW masks is according to
Microsoft's documentation, but opposite to the OpenOffice documentation. }
{ Error codes }
ERR_INTERSECTION_EMPTY = $00; // #NULL!
ERR_DIVIDE_BY_ZERO = $07; // #DIV/0!
ERR_WRONG_TYPE_OF_OPERAND = $0F; // #VALUE!
ERR_ILLEGAL_REFERENCE = $17; // #REF!
ERR_WRONG_NAME = $1D; // #NAME?
ERR_OVERFLOW = $24; // #NUM!
ERR_ARG_ERROR = $2A; // #N/A (not enough, or too many, arguments)
type
TDateMode=(dm1900,dm1904); //DATEMODE values, 5.28
// Adjusts Excel float (date, date/time, time) with the file's base date to get a TDateTime
function ConvertExcelDateTimeToDateTime
(const AExcelDateNum: Double; ADateMode: TDateMode): TDateTime;
// Adjusts TDateTime with the file's base date to get
// an Excel float value representing a time/date/datetime
function ConvertDateTimeToExcelDateTime
(const ADateTime: TDateTime; ADateMode: TDateMode): Double;
type
{ Contents of the XF record to be stored in the XFList of the reader }
TXFListData = class
public
FontIndex: Integer;
FormatIndex: Integer;
HorAlignment: TsHorAlignment;
VertAlignment: TsVertAlignment;
WordWrap: Boolean;
TextRotation: TsTextRotation;
Borders: TsCellBorders;
BorderStyles: TsCellBorderStyles;
BackgroundColor: TsColor;
end;
{ TsBIFFNumFormatList }
TsBIFFNumFormatList = class(TsCustomNumFormatList)
protected
procedure AddBuiltinFormats; override;
procedure ConvertBeforeWriting(var AFormatString: String;
var ANumFormat: TsNumberFormat); override;
public
end;
{ TsSpreadBIFFReader }
TsSpreadBIFFReader = class(TsCustomSpreadReader)
protected
RecordSize: Word;
FCodepage: string; // in a format prepared for lconvencoding.ConvertEncoding
FDateMode: TDateMode;
FPaletteFound: Boolean;
FXFList: TFPList; // of TXFListData
FIncompleteCell: PCell;
procedure ApplyCellFormatting(ARow, ACol: Cardinal; XFIndex: Word); virtual;
procedure CreateNumFormatList; override;
// Extracts a number out of an RK value
function DecodeRKValue(const ARK: DWORD): Double;
// Returns the numberformat for a given XF record
procedure ExtractNumberFormat(AXFIndex: WORD;
out ANumberFormat: TsNumberFormat; //out ADecimals: Byte;
//out ACurrencySymbol: String;
out ANumberFormatStr: String); virtual;
// Finds format record for XF record pointed to by cell
// Will not return info for built-in formats
function FindNumFormatDataForCell(const AXFIndex: Integer): TsNumFormatData;
// Tries to find if a number cell is actually a date/datetime/time cell and retrieves the value
function IsDateTime(Number: Double; ANumberFormat: TsNumberFormat;
ANumberFormatStr: String; var ADateTime: TDateTime): Boolean;
// Here we can add reading of records which didn't change across BIFF5-8 versions
procedure ReadCodePage(AStream: TStream);
// Read column info
procedure ReadColInfo(const AStream: TStream);
// Figures out what the base year for dates is for this file
procedure ReadDateMode(AStream: TStream);
// Read FORMAT record (cell formatting)
procedure ReadFormat(AStream: TStream); virtual;
// Read FORMULA record
procedure ReadFormula(AStream: TStream); override;
// Read multiple blank cells
procedure ReadMulBlank(AStream: TStream);
// Read multiple RK cells
procedure ReadMulRKValues(const AStream: TStream);
// Read floating point number
procedure ReadNumber(AStream: TStream); override;
// Read palette
procedure ReadPalette(AStream: TStream);
// Read PANE record
procedure ReadPane(AStream: TStream);
// Read an RK value cell
procedure ReadRKValue(AStream: TStream);
// Read the row, column, and XF index at the current stream position
procedure ReadRowColXF(AStream: TStream; out ARow, ACol: Cardinal; out AXF: Word); virtual;
// Read row info
procedure ReadRowInfo(AStream: TStream); virtual;
// Read the array of RPN tokens of a formula
procedure ReadRPNCellAddress(AStream: TStream; var ARow, ACol: Cardinal;
var AFlags: TsRelFlags); virtual;
procedure ReadRPNCellRangeAddress(AStream: TStream;
var ARow1, ACol1, ARow2, ACol2: Cardinal; var AFlags: TsRelFlags); virtual;
function ReadRPNFunc(AStream: TStream): Word; virtual;
function ReadRPNTokenArray(AStream: TStream; var AFormula: TsRPNFormula): Boolean;
function ReadRPNTokenArraySize(AStream: TStream): word; virtual;
// Helper function for reading a string with 8-bit length
function ReadString_8bitLen(AStream: TStream): String; virtual;
// Read STRING record (result of string formula)
procedure ReadStringRecord(AStream: TStream); virtual;
// Read WINDOW2 record (gridlines, sheet headers)
procedure ReadWindow2(AStream: TStream); virtual;
public
constructor Create(AWorkbook: TsWorkbook); override;
destructor Destroy; override;
end;
{ TsSpreadBIFFWriter }
TsSpreadBIFFWriter = class(TsCustomSpreadWriter)
protected
FDateMode: TDateMode;
FLastRow: Integer;
FLastCol: Word;
procedure AddDefaultFormats; override;
procedure CreateNumFormatList; override;
procedure GetLastRowCallback(ACell: PCell; AStream: TStream);
function GetLastRowIndex(AWorksheet: TsWorksheet): Integer;
procedure GetLastColCallback(ACell: PCell; AStream: TStream);
function GetLastColIndex(AWorksheet: TsWorksheet): Word;
function FormulaElementKindToExcelTokenID(AElementKind: TFEKind; out ASecondaryID: Word): Word;
// Write out BLANK cell record
procedure WriteBlank(AStream: TStream; const ARow, ACol: Cardinal;
ACell: PCell); override;
// Write out used codepage for character encoding
procedure WriteCodepage(AStream: TStream; AEncoding: TsEncoding);
// Writes out column info(s)
procedure WriteColInfo(AStream: TStream; ACol: PCol);
procedure WriteColInfos(AStream: TStream; ASheet: TsWorksheet);
// Writes out DATEMODE record depending on FDateMode
procedure WriteDateMode(AStream: TStream);
// Writes out a TIME/DATE/TIMETIME
procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: TDateTime; ACell: PCell); override;
// Writes out a FORMAT record
procedure WriteFormat(AStream: TStream; AFormatData: TsNumFormatData;
AListIndex: Integer); virtual;
// Writes out all FORMAT records
procedure WriteFormats(AStream: TStream);
// Writes out a floating point NUMBER record
procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: Double; ACell: PCell); override;
// Writes out a PALETTE record containing all colors defined in the workbook
procedure WritePalette(AStream: TStream);
// Writes out a PANE record
procedure WritePane(AStream: TStream; ASheet: TsWorksheet; IsBiff58: Boolean);
// Writes out a ROW record
procedure WriteRow(AStream: TStream; ASheet: TsWorksheet;
ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow); virtual;
// Write all ROW records for a sheet
procedure WriteRows(AStream: TStream; ASheet: TsWorksheet);
// Writes out a SELECTION record
procedure WriteSelection(AStream: TStream; ASheet: TsWorksheet; APane: Byte);
procedure WriteSelections(AStream: TStream; ASheet: TsWorksheet);
// Writes out a WINDOW1 record
procedure WriteWindow1(AStream: TStream); virtual;
// Writes the index of the XF record used in the given cell
procedure WriteXFIndex(AStream: TStream; ACell: PCell);
public
constructor Create(AWorkbook: TsWorkbook); override;
destructor Destroy; override;
end;
implementation
uses
StrUtils, fpsNumFormatParser;
{ Helper table for rpn formulas:
Assignment of FormulaElementKinds (fekXXXX) to EXCEL_TOKEN IDs. }
const
TokenIDs: array[TFEKind] of Word = (
// Basic operands
INT_EXCEL_TOKEN_TREFV, {fekCell}
INT_EXCEL_TOKEN_TREFR, {fekCellRef}
INT_EXCEL_TOKEN_TAREA_R, {fekCellRange}
INT_EXCEL_TOKEN_TNUM, {fekNum}
INT_EXCEL_TOKEN_TINT, {fekInteger}
INT_EXCEL_TOKEN_TSTR, {fekString}
INT_EXCEL_TOKEN_TBOOL, {fekBool}
INT_EXCEL_TOKEN_TERR, {fekErr}
INT_EXCEL_TOKEN_TMISSARG, {fekMissArg, missing argument}
// Basic operations
INT_EXCEL_TOKEN_TADD, {fekAdd, +}
INT_EXCEL_TOKEN_TSUB, {fekSub, -}
INT_EXCEL_TOKEN_TMUL, {fekMul, *}
INT_EXCEL_TOKEN_TDIV, {fekDiv, /}
INT_EXCEL_TOKEN_TPERCENT, {fekPercent, %}
INT_EXCEL_TOKEN_TPOWER, {fekPower, ^}
INT_EXCEL_TOKEN_TUMINUS, {fekUMinus, -}
INT_EXCEL_TOKEN_TUPLUS, {fekUPlus, +}
INT_EXCEL_TOKEN_TCONCAT, {fekConcat, &, for strings}
INT_EXCEL_TOKEN_TEQ, {fekEqual, =}
INT_EXCEL_TOKEN_TGT, {fekGreater, >}
INT_EXCEL_TOKEN_TGE, {fekGreaterEqual, >=}
INT_EXCEL_TOKEN_TLT, {fekLess <}
INT_EXCEL_TOKEN_TLE, {fekLessEqual, <=}
INT_EXCEL_TOKEN_TNE, {fekNotEqual, <>}
INT_EXCEL_TOKEN_TPAREN, {Operator in parenthesis}
// Math functions
INT_EXCEL_SHEET_FUNC_ABS, {fekABS}
INT_EXCEL_SHEET_FUNC_ACOS, {fekACOS}
INT_EXCEL_SHEET_FUNC_ACOSH, {fekACOSH}
INT_EXCEL_SHEET_FUNC_ASIN, {fekASIN}
INT_EXCEL_SHEET_FUNC_ASINH, {fekASINH}
INT_EXCEL_SHEET_FUNC_ATAN, {fekATAN}
INT_EXCEL_SHEET_FUNC_ATANH, {fekATANH}
INT_EXCEL_SHEET_FUNC_COS, {fekCOS}
INT_EXCEL_SHEET_FUNC_COSH, {fekCOSH}
INT_EXCEL_SHEET_FUNC_DEGREES, {fekDEGREES}
INT_EXCEL_SHEET_FUNC_EXP, {fekEXP}
INT_EXCEL_SHEET_FUNC_INT, {fekINT}
INT_EXCEL_SHEET_FUNC_LN, {fekLN}
INT_EXCEL_SHEET_FUNC_LOG, {fekLOG}
INT_EXCEL_SHEET_FUNC_LOG10, {fekLOG10}
INT_EXCEL_SHEET_FUNC_PI, {fekPI}
INT_EXCEL_SHEET_FUNC_RADIANS, {fekRADIANS}
INT_EXCEL_SHEET_FUNC_RAND, {fekRAND}
INT_EXCEL_SHEET_FUNC_ROUND, {fekROUND}
INT_EXCEL_SHEET_FUNC_SIGN, {fekSIGN}
INT_EXCEL_SHEET_FUNC_SIN, {fekSIN}
INT_EXCEL_SHEET_FUNC_SINH, {fekSINH}
INT_EXCEL_SHEET_FUNC_SQRT, {fekSQRT}
INT_EXCEL_SHEET_FUNC_TAN, {fekTAN}
INT_EXCEL_SHEET_FUNC_TANH, {fekTANH}
// Date/time functions
INT_EXCEL_SHEET_FUNC_DATE, {fekDATE}
INT_EXCEL_SHEET_FUNC_DATEDIF, {fekDATEDIF}
INT_EXCEL_SHEET_FUNC_DATEVALUE, {fekDATEVALUE}
INT_EXCEL_SHEET_FUNC_DAY, {fekDAY}
INT_EXCEL_SHEET_FUNC_HOUR, {fekHOUR}
INT_EXCEL_SHEET_FUNC_MINUTE, {fekMINUTE}
INT_EXCEL_SHEET_FUNC_MONTH, {fekMONTH}
INT_EXCEL_SHEET_FUNC_NOW, {fekNOW}
INT_EXCEL_SHEET_FUNC_SECOND, {fekSECOND}
INT_EXCEL_SHEET_FUNC_TIME, {fekTIME}
INT_EXCEL_SHEET_FUNC_TIMEVALUE, {fekTIMEVALUE}
INT_EXCEL_SHEET_FUNC_TODAY, {fekTODAY}
INT_EXCEL_SHEET_FUNC_WEEKDAY, {fekWEEKDAY}
INT_EXCEL_SHEET_FUNC_YEAR, {fekYEAR}
// Statistical functions
INT_EXCEL_SHEET_FUNC_AVEDEV, {fekAVEDEV}
INT_EXCEL_SHEET_FUNC_AVERAGE, {fekAVERAGE}
INT_EXCEL_SHEET_FUNC_BETADIST, {fekBETADIST}
INT_EXCEL_SHEET_FUNC_BETAINV, {fekBETAINV}
INT_EXCEL_SHEET_FUNC_BINOMDIST, {fekBINOMDIST}
INT_EXCEL_SHEET_FUNC_CHIDIST, {fekCHIDIST}
INT_EXCEL_SHEET_FUNC_CHIINV, {fekCHIINV}
INT_EXCEL_SHEET_FUNC_COUNT, {fekCOUNT}
INT_EXCEL_SHEET_FUNC_COUNTA, {fekCOUNTA}
INT_EXCEL_SHEET_FUNC_COUNTBLANK,{fekCOUNTBLANK}
INT_EXCEL_SHEET_FUNC_COUNTIF, {fekCOUNTIF}
INT_EXCEL_SHEET_FUNC_MAX, {fekMAX}
INT_EXCEL_SHEET_FUNC_MEDIAN, {fekMEDIAN}
INT_EXCEL_SHEET_FUNC_MIN, {fekMIN}
INT_EXCEL_SHEET_FUNC_PERMUT, {fekPERMUT}
INT_EXCEL_SHEET_FUNC_POISSON, {fekPOISSON}
INT_EXCEL_SHEET_FUNC_PRODUCT, {fekPRODUCT}
INT_EXCEL_SHEET_FUNC_STDEV, {fekSTDEV}
INT_EXCEL_SHEET_FUNC_STDEVP, {fekSTDEVP}
INT_EXCEL_SHEET_FUNC_SUM, {fekSUM}
INT_EXCEL_SHEET_FUNC_SUMIF, {fekSUMIF}
INT_EXCEL_SHEET_FUNC_SUMSQ, {fekSUMSQ}
INT_EXCEL_SHEET_FUNC_VAR, {fekVAR}
INT_EXCEL_SHEET_FUNC_VARP, {fekVARP}
// Financial functions
INT_EXCEL_SHEET_FUNC_FV, {fekFV}
INT_EXCEL_SHEET_FUNC_NPER, {fekNPER}
INT_EXCEL_SHEET_FUNC_PMT, {fekPMT}
INT_EXCEL_SHEET_FUNC_PV, {fekPV}
INT_EXCEL_SHEET_FUNC_RATE, {fekRATE}
// Logical functions
INT_EXCEL_SHEET_FUNC_AND, {fekAND}
INT_EXCEL_SHEET_FUNC_FALSE, {fekFALSE}
INT_EXCEL_SHEET_FUNC_IF, {fekIF}
INT_EXCEL_SHEET_FUNC_NOT, {fekNOT}
INT_EXCEL_SHEET_FUNC_OR, {fekOR}
INT_EXCEL_SHEET_FUNC_TRUE, {fekTRUE}
// String functions
INT_EXCEL_SHEET_FUNC_CHAR, {fekCHAR}
INT_EXCEL_SHEET_FUNC_CODE, {fekCODE}
INT_EXCEL_SHEET_FUNC_LEFT, {fekLEFT}
INT_EXCEL_SHEET_FUNC_LOWER, {fekLOWER}
INT_EXCEL_SHEET_FUNC_MID, {fekMID}
INT_EXCEL_SHEET_FUNC_PROPER, {fekPROPER}
INT_EXCEL_SHEET_FUNC_REPLACE, {fekREPLACE}
INT_EXCEL_SHEET_FUNC_RIGHT, {fekRIGHT}
INT_EXCEL_SHEET_FUNC_SUBSTITUTE,{fekSUBSTITUTE}
INT_EXCEL_SHEET_FUNC_TRIM, {fekTRIM}
INT_EXCEL_SHEET_FUNC_UPPER, {fekUPPER}
// lookup/reference functions
INT_EXCEL_SHEET_FUNC_COLUMN, {fekCOLUMN}
INT_EXCEL_SHEET_FUNC_COLUMNS, {fekCOLUMNS}
INT_EXCEL_SHEET_FUNC_ROW, {fekROW}
INT_EXCEL_SHEET_FUNC_ROWS, {fekROWS}
// Info functions
INT_EXCEL_SHEET_FUNC_CELL, {fekCELLINFO}
INT_EXCEL_SHEET_FUNC_INFO, {fekINFO}
INT_EXCEL_SHEET_FUNC_ISBLANK, {fekIsBLANK}
INT_EXCEL_SHEET_FUNC_ISERR, {fekIsERR}
INT_EXCEL_SHEET_FUNC_ISERROR, {fekIsERROR}
INT_EXCEL_SHEET_FUNC_ISLOGICAL, {fekIsLOGICAL}
INT_EXCEL_SHEET_FUNC_ISNA, {fekIsNA}
INT_EXCEL_SHEET_FUNC_ISNONTEXT, {fekIsNONTEXT}
INT_EXCEL_SHEET_FUNC_ISNUMBER, {fekIsNUMBER}
INT_EXCEL_SHEET_FUNC_ISREF, {fekIsREF}
INT_EXCEL_SHEET_FUNC_ISTEXT, {fekIsTEXT}
INT_EXCEL_SHEET_FUNC_VALUE, {fekValue}
// Other operations
INT_EXCEL_TOKEN_TATTR {fekOpSum}
);
function ConvertExcelDateTimeToDateTime(
const AExcelDateNum: Double; ADateMode: TDateMode): TDateTime;
begin
// Time only:
if (AExcelDateNum<1) and (AExcelDateNum>=0) then
begin
Result:=AExcelDateNum;
end
else
begin
case ADateMode of
dm1900:
begin
// Check for Lotus 1-2-3 bug with 1900 leap year
if AExcelDateNum=61.0 then
// 29 feb does not exist, change to 28
// Spell out that we remove a day for ehm "clarity".
result:=61.0-1.0+DATEMODE_1900_BASE-1.0
else
result:=AExcelDateNum+DATEMODE_1900_BASE-1.0;
end;
dm1904:
result:=AExcelDateNum+DATEMODE_1904_BASE;
else
raise Exception.CreateFmt('ConvertExcelDateTimeToDateTime: unknown datemode %d. Please correct fpspreadsheet source code. ', [ADateMode]);
end;
end;
end;
function ConvertDateTimeToExcelDateTime(const ADateTime: TDateTime;
ADateMode: TDateMode): Double;
begin
// Time only:
if (ADateTime<1) and (ADateTime>=0) then
begin
Result:=ADateTime;
end
else
begin
case ADateMode of
dm1900:
result:=ADateTime-DATEMODE_1900_BASE+1.0;
dm1904:
result:=ADateTime-DATEMODE_1904_BASE;
else
raise Exception.CreateFmt('ConvertDateTimeToExcelDateTime: unknown datemode %d. Please correct fpspreadsheet source code. ', [ADateMode]);
end;
end;
end;
{ TsBIFFNumFormatList }
{ These are the built-in number formats as expected in the biff spreadsheet file.
In BIFF5+ they are not written to file but they are used for lookup of the
number format that Excel used. They are specified here in fpc dialect. }
procedure TsBIFFNumFormatList.AddBuiltinFormats;
var
fs: TFormatSettings;
cs: String;
begin
fs := Workbook.FormatSettings;
cs := AnsiToUTF8(Workbook.FormatSettings.CurrencyString);
AddFormat( 0, '', nfGeneral);
AddFormat( 1, '0', nfFixed);
AddFormat( 2, '0.00', nfFixed);
AddFormat( 3, '#,##0', nfFixedTh);
AddFormat( 4, '#,##0.00', nfFixedTh);
AddFormat( 5, '"'+cs+'"#,##0_);("'+cs+'"#,##0)', nfCurrency);
AddFormat( 6, '"'+cs+'"#,##0_);[Red]("'+cs+'"#,##0)', nfCurrencyRed);
AddFormat( 7, '"'+cs+'"#,##0.00_);("'+cs+'"#,##0.00)', nfCurrency);
AddFormat( 8, '"'+cs+'"#,##0.00_);[Red]("'+cs+'"#,##0.00)', nfCurrencyRed);
AddFormat( 9, '0%', nfPercentage);
AddFormat(10, '0.00%', nfPercentage);
AddFormat(11, '0.00E+00', nfExp);
// fraction formats 12 ('# ?/?') and 13 ('# ??/??') not supported
AddFormat(14, fs.ShortDateFormat, nfShortDate); // 'M/D/YY'
AddFormat(15, fs.LongDateFormat, nfLongDate); // 'D-MMM-YY'
AddFormat(16, 'd/mmm', nfCustom); // 'D-MMM'
AddFormat(17, 'mmm/yy', nfCustom); // 'MMM-YY'
AddFormat(18, AddAMPM(fs.ShortTimeFormat, fs), nfShortTimeAM); // 'h:mm AM/PM'
AddFormat(19, AddAMPM(fs.LongTimeFormat, fs), nfLongTimeAM); // 'h:mm:ss AM/PM'
AddFormat(20, fs.ShortTimeFormat, nfShortTime); // 'h:mm'
AddFormat(21, fs.LongTimeFormat, nfLongTime); // 'h:mm:ss'
AddFormat(22, fs.ShortDateFormat + ' ' + fs.ShortTimeFormat, nfShortDateTime); // 'M/D/YY h:mm' (localized)
// 23..36 not supported
AddFormat(37, '_(#,##0_);(#,##0)', nfCurrency);
AddFormat(38, '_(#,##0_);[Red](#,##0)', nfCurrencyRed);
AddFormat(39, '_(#,##0.00_);(#,##0.00)', nfCurrency);
AddFormat(40, '_(#,##0.00_);[Red](#,##0.00)', nfCurrencyRed);
AddFormat(41, '_("'+cs+'"* #,##0_);_("'+cs+'"* (#,##0);_("'+cs+'"* "-"_);_(@_)', nfAccounting);
AddFormat(42, '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)', nfAccounting);
AddFormat(43, '_("'+cs+'"* #,##0.00_);_("'+cs+'"* (#,##0.00);_("'+cs+'"* "-"??_);_(@_)', nfAccounting);
AddFormat(44, '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)', nfAccounting);
AddFormat(45, 'nn:ss', nfCustom);
AddFormat(46, '[h]:nn:ss', nfTimeInterval);
AddFormat(47, 'nn:ss.z', nfCustom);
AddFormat(48, '##0.0E+00', nfSci);
// 49 ("Text") not supported
// All indexes from 0 to 163 are reserved for built-in formats.
// The first user-defined format starts at 164.
FFirstFormatIndexInFile := 164;
FNextFormatIndex := 164;
end;
procedure TsBIFFNumFormatList.ConvertBeforeWriting(var AFormatString: String;
var ANumFormat: TsNumberFormat);
var
parser: TsNumFormatParser;
fmt: String;
begin
parser := TsNumFormatParser.Create(Workbook, AFormatString, ANumFormat);
try
if parser.Status = psOK then begin
// For writing, we have to convert the fpc format string to Excel dialect
AFormatString := parser.FormatString[nfdExcel];
ANumFormat := parser.NumFormat;
end;
finally
parser.Free;
end;
end;
{ TsSpreadBIFFReader }
constructor TsSpreadBIFFReader.Create(AWorkbook: TsWorkbook);
begin
inherited Create(AWorkbook);
FXFList := TFPList.Create;
// Initial base date in case it won't be read from file
FDateMode := dm1900;
end;
destructor TsSpreadBIFFReader.Destroy;
var
j: integer;
begin
for j := FXFList.Count-1 downto 0 do TObject(FXFList[j]).Free;
FXFList.Free;
inherited Destroy;
end;
{ Applies the XF formatting referred to by XFIndex to the specified cell }
procedure TsSpreadBIFFReader.ApplyCellFormatting(ARow, ACol: Cardinal;
XFIndex: Word);
var
lCell: PCell;
XFData: TXFListData;
begin
lCell := FWorksheet.GetCell(ARow, ACol);
if Assigned(lCell) then begin
XFData := TXFListData(FXFList.Items[XFIndex]);
// Font
if XFData.FontIndex = 1 then
Include(lCell^.UsedFormattingFields, uffBold)
else
if XFData.FontIndex > 1 then
Include(lCell^.UsedFormattingFields, uffFont);
lCell^.FontIndex := XFData.FontIndex;
// Alignment
lCell^.HorAlignment := XFData.HorAlignment;
lCell^.VertAlignment := XFData.VertAlignment;
// Word wrap
if XFData.WordWrap then
Include(lCell^.UsedFormattingFields, uffWordWrap)
else
Exclude(lCell^.UsedFormattingFields, uffWordWrap);
// Text rotation
if XFData.TextRotation > trHorizontal then
Include(lCell^.UsedFormattingFields, uffTextRotation)
else
Exclude(lCell^.UsedFormattingFields, uffTextRotation);
lCell^.TextRotation := XFData.TextRotation;
// Borders
lCell^.BorderStyles := XFData.BorderStyles;
if XFData.Borders <> [] then begin
Include(lCell^.UsedFormattingFields, uffBorder);
lCell^.Border := XFData.Borders;
end else
Exclude(lCell^.UsedFormattingFields, uffBorder);
// Background color
if XFData.BackgroundColor <> 0 then begin
Include(lCell^.UsedFormattingFields, uffBackgroundColor);
lCell^.BackgroundColor := XFData.BackgroundColor;
end;
end;
end;
{ Creates the correct version of the number format list. It is for BIFF file
formats.
Valid for BIFF5.BIFF8. Needs to be overridden for BIFF2. }
procedure TsSpreadBIFFReader.CreateNumFormatList;
var
i: Integer;
item: TsNumFormatData;
begin
FreeAndNil(FNumFormatList);
FNumFormatList := TsBIFFNumFormatList.Create(Workbook);
end;
{ Extracts a number out of an RK value.
Valid since BIFF3. }
function TsSpreadBIFFReader.DecodeRKValue(const ARK: DWORD): Double;
var
Number: Double;
Tmp: LongInt;
begin
if ARK and 2 = 2 then begin
// Signed integer value
if LongInt(ARK) < 0 then begin
//Simulates a sar
Tmp := LongInt(ARK) * (-1);
Tmp := Tmp shr 2;
Tmp := Tmp * (-1);
Number := Tmp - 1;
end else begin
Number := ARK shr 2;
end;
end else begin
// Floating point value
// NOTE: This is endian dependent and IEEE dependent (Not checked) (working win-i386)
(PDWORD(@Number))^ := $00000000;
(PDWORD(@Number)+1)^ := ARK and $FFFFFFFC;
end;
if ARK and 1 = 1 then begin
// Encoded value is multiplied by 100
Number := Number / 100;
end;
Result := Number;
end;
{ Extracts number format data from an XF record index by AXFIndex.
Valid for BIFF5-BIFF8. Needs to be overridden for BIFF2 }
procedure TsSpreadBIFFReader.ExtractNumberFormat(AXFIndex: WORD;
out ANumberFormat: TsNumberFormat; //out ADecimals: Byte;
//out ACurrencySymbol: String;
out ANumberFormatStr: String);
var
lNumFormatData: TsNumFormatData;
begin
lNumFormatData := FindNumFormatDataForCell(AXFIndex);
if lNumFormatData <> nil then begin
ANumberFormat := lNumFormatData.NumFormat;
ANumberFormatStr := lNumFormatData.FormatString;
end else begin
ANumberFormat := nfGeneral;
ANumberFormatStr := '';
end;
end;
{ Determines the format data (for numerical formatting) which belong to a given
XF record. }
function TsSpreadBIFFReader.FindNumFormatDataForCell(const AXFIndex: Integer
): TsNumFormatData;
var
lXFData: TXFListData;
i: Integer;
begin
Result := nil;
lXFData := TXFListData(FXFList.Items[AXFIndex]);
i := NumFormatList.FindByIndex(lXFData.FormatIndex);
if i <> -1 then Result := NumFormatList[i];
end;
{ Convert the number to a date/time and return that if it is }
function TsSpreadBIFFReader.IsDateTime(Number: Double;
ANumberFormat: TsNumberFormat; ANumberFormatStr: String;
var ADateTime: TDateTime): boolean;
var
parser: TsNumFormatParser;
begin
Result := true;
if ANumberFormat in [
nfShortDateTime, {nfFmtDateTime, }nfShortDate, nfLongDate,
nfShortTime, nfLongTime, nfShortTimeAM, nfLongTimeAM]
then
ADateTime := ConvertExcelDateTimeToDateTime(Number, FDateMode)
else
if ANumberFormat = nfTimeInterval then
ADateTime := Number
else begin
parser := TsNumFormatParser.Create(Workbook, ANumberFormatStr);
try
if (parser.Status = psOK) and parser.IsDateTimeFormat then
ADateTime := ConvertExcelDateTimeToDateTime(Number, FDateMode)
else
Result := false;
finally
parser.Free;
end;
end;
end;
// In BIFF8 it seams to always use the UTF-16 codepage
procedure TsSpreadBIFFReader.ReadCodePage(AStream: TStream);
var
lCodePage: Word;
begin
{ Codepage }
lCodePage := WordLEToN(AStream.ReadWord());
case lCodePage of
// 016FH = 367 = ASCII
// 01B5H = 437 = IBM PC CP-437 (US)
//02D0H = 720 = IBM PC CP-720 (OEM Arabic)
//02E1H = 737 = IBM PC CP-737 (Greek)
//0307H = 775 = IBM PC CP-775 (Baltic)
//0352H = 850 = IBM PC CP-850 (Latin I)
$0352: FCodepage := 'cp850';
//0354H = 852 = IBM PC CP-852 (Latin II (Central European))
$0354: FCodepage := 'cp852';
//0357H = 855 = IBM PC CP-855 (Cyrillic)
$0357: FCodepage := 'cp855';
//0359H = 857 = IBM PC CP-857 (Turkish)
$0359: FCodepage := 'cp857';
//035AH = 858 = IBM PC CP-858 (Multilingual Latin I with Euro)
//035CH = 860 = IBM PC CP-860 (Portuguese)
//035DH = 861 = IBM PC CP-861 (Icelandic)
//035EH = 862 = IBM PC CP-862 (Hebrew)
//035FH = 863 = IBM PC CP-863 (Canadian (French))
//0360H = 864 = IBM PC CP-864 (Arabic)
//0361H = 865 = IBM PC CP-865 (Nordic)
//0362H = 866 = IBM PC CP-866 (Cyrillic (Russian))
//0365H = 869 = IBM PC CP-869 (Greek (Modern))
//036AH = 874 = Windows CP-874 (Thai)
//03A4H = 932 = Windows CP-932 (Japanese Shift-JIS)
//03A8H = 936 = Windows CP-936 (Chinese Simplified GBK)
//03B5H = 949 = Windows CP-949 (Korean (Wansung))
//03B6H = 950 = Windows CP-950 (Chinese Traditional BIG5)
//04B0H = 1200 = UTF-16 (BIFF8)
$04B0: FCodepage := 'utf-16';
//04E2H = 1250 = Windows CP-1250 (Latin II) (Central European)
//04E3H = 1251 = Windows CP-1251 (Cyrillic)
//04E4H = 1252 = Windows CP-1252 (Latin I) (BIFF4-BIFF5)
//04E5H = 1253 = Windows CP-1253 (Greek)
//04E6H = 1254 = Windows CP-1254 (Turkish)
$04E6: FCodepage := 'cp1254';
//04E7H = 1255 = Windows CP-1255 (Hebrew)
//04E8H = 1256 = Windows CP-1256 (Arabic)
//04E9H = 1257 = Windows CP-1257 (Baltic)
//04EAH = 1258 = Windows CP-1258 (Vietnamese)
//0551H = 1361 = Windows CP-1361 (Korean (Johab))
//2710H = 10000 = Apple Roman
//8000H = 32768 = Apple Roman
//8001H = 32769 = Windows CP-1252 (Latin I) (BIFF2-BIFF3)
end;
end;
{ Read column info (column width) from the stream.
Valid for BIFF3-BIFF8.
For BIFF2 use the records COLWIDTH and COLUMNDEFAULT. }
procedure TsSpreadBiffReader.ReadColInfo(const AStream: TStream);
var
c, c1, c2: Cardinal;
w: Word;
col: TCol;
begin
// read column start and end index of column range
c1 := WordLEToN(AStream.ReadWord);
c2 := WordLEToN(AStream.ReadWord);
// read col width in 1/256 of the width of "0" character
w := WordLEToN(AStream.ReadWord);
// calculate width in units of "characters"
col.Width := w / 256;
// assign width to columns
for c := c1 to c2 do
FWorksheet.WriteColInfo(c, col);
end;
procedure TsSpreadBIFFReader.ReadDateMode(AStream: TStream);
var
lBaseMode: Word;
begin
//5.28 DATEMODE
//BIFF2 BIFF3 BIFF4 BIFF5 BIFF8
//0022H 0022H 0022H 0022H 0022H
//This record specifies the base date for displaying date values. All dates are stored as count of days past this base date. In
//BIFF2-BIFF4 this record is part of the Calculation Settings Block (➜4.3). In BIFF5-BIFF8 it is stored in the Workbook
//Globals Substream.
//Record DATEMODE, BIFF2-BIFF8:
//Offset Size Contents
//0 2 0 = Base date is 1899-Dec-31 (the cell value 1 represents 1900-Jan-01)
// 1 = Base date is 1904-Jan-01 (the cell value 1 represents 1904-Jan-02)
lBaseMode := WordLEtoN(AStream.ReadWord);
case lBaseMode of
0: FDateMode := dm1900;
1: FDateMode := dm1904;
else raise Exception.CreateFmt('Error reading file. Got unknown date mode number %d.',[lBaseMode]);
end;
end;
// Read the FORMAT record for formatting numerical data
procedure TsSpreadBIFFReader.ReadFormat(AStream: TStream);
begin
// to be overridden
end;
{ Reads a FORMULA record, retrieves the RPN formula and puts the result in the
corresponding field. The formula is not recalculated here!
Valid for BIFF5 and BIFF8. }
procedure TsSpreadBIFFReader.ReadFormula(AStream: TStream);
var
ARow, ACol: Cardinal;
XF: WORD;
ResultFormula: Double;
Data: array [0..7] of byte;
Flags: WORD;
i: Integer;
dt: TDateTime;
nf: TsNumberFormat;
nd: Byte;
ncs: String;
nfs: String;
resultStr: String;
err: TsErrorValue;
ok: Boolean;
cell: PCell;
begin
{ BIFF Record header }
{ BIFF Record data }
{ Index to XF Record }
ReadRowColXF(AStream, ARow, ACol, XF);
{ Result of the formula result in IEEE 754 floating-point value }
AStream.ReadBuffer(Data, Sizeof(Data));
{ Options flags }
Flags := WordLEtoN(AStream.ReadWord);
{ Not used }
AStream.ReadDWord;
// 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 := FWorksheet.GetCell(ARow, ACol);
1: // Boolean value
FWorksheet.WriteBoolValue(ARow, ACol, 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(ARow, ACol, err);
end;
3: FWorksheet.WriteBlank(ARow, ACol);
end
else begin
if SizeOf(Double) <> 8 then
raise Exception.Create('Double is not 8 bytes');
// Result is a number or a date/time
Move(Data[0], ResultFormula, SizeOf(Data));
{Find out what cell type, set content type and value}
ExtractNumberFormat(XF, nf, nfs);
if IsDateTime(ResultFormula, nf, nfs, dt) then
FWorksheet.WriteDateTime(ARow, ACol, dt, nf, nfs)
else
FWorksheet.WriteNumber(ARow, ACol, ResultFormula, nf, nfs); //, nd, ncs);
end;
{ Formula token array }
if FWorkbook.ReadFormulas then begin
cell := FWorksheet.FindCell(ARow, ACol);
ok := ReadRPNTokenArray(AStream, cell^.RPNFormulaValue);
if not ok then FWorksheet.WriteErrorValue(cell, errFormulaNotSupported);
end;
{Add attributes}
ApplyCellFormatting(ARow, ACol, XF);
end;
// Reads multiple blank cell records
// Valid for BIFF5 and BIFF8 (does not exist before)
procedure TsSpreadBIFFReader.ReadMulBlank(AStream: TStream);
var
ARow, fc, lc, XF: Word;
pending: integer;
begin
ARow := WordLEtoN(AStream.ReadWord);
fc := WordLEtoN(AStream.ReadWord);
pending := RecordSize - Sizeof(fc) - Sizeof(ARow);
while pending > SizeOf(XF) do begin
XF := AStream.ReadWord; //XF record (not used)
FWorksheet.WriteBlank(ARow, fc);
ApplyCellFormatting(ARow, fc, XF);
inc(fc);
dec(pending, SizeOf(XF));
end;
if pending = 2 then begin
//Just for completeness
lc := WordLEtoN(AStream.ReadWord);
if lc + 1 <> fc then begin
//Stream error... bypass by now
end;
end;
end;
{ Reads multiple RK records.
Valid for BIFF5 and BIFF8 (does not exist before) }
procedure TsSpreadBIFFReader.ReadMulRKValues(const AStream: TStream);
var
ARow, fc, lc, XF: Word;
lNumber: Double;
lDateTime: TDateTime;
pending: integer;
RK: DWORD;
nf: TsNumberFormat;
nd: Byte;
nfs: String;
ncs: String;
begin
ARow := WordLEtoN(AStream.ReadWord);
fc := WordLEtoN(AStream.ReadWord);
pending := RecordSize - SizeOf(fc) - SizeOf(ARow);
while pending > SizeOf(XF) + SizeOf(RK) do begin
XF := AStream.ReadWord; //XF record (used for date checking)
RK := DWordLEtoN(AStream.ReadDWord);
lNumber := DecodeRKValue(RK);
{Find out what cell type, set contenttype and value}
ExtractNumberFormat(XF, nf, nfs);
if IsDateTime(lNumber, nf, nfs, lDateTime) then
FWorksheet.WriteDateTime(ARow, fc, lDateTime, nf, nfs)
else
FWorksheet.WriteNumber(ARow, fc, lNumber, nf, nfs);
inc(fc);
dec(pending, SizeOf(XF) + SizeOf(RK));
end;
if pending = 2 then begin
//Just for completeness
lc := WordLEtoN(AStream.ReadWord);
if lc + 1 <> fc then begin
//Stream error... bypass by now
end;
end;
end;
// Reads a floating point number and seeks the number format
// NOTE: This procedure is valid after BIFF 3.
procedure TsSpreadBIFFReader.ReadNumber(AStream: TStream);
var
ARow, ACol: Cardinal;
XF: WORD;
value: Double;
dt: TDateTime;
nf: TsNumberFormat;
nd: Byte;
nfs: String;
ncs: String;
begin
ReadRowColXF(AStream, ARow, ACol, XF);
{ IEE 754 floating-point value }
AStream.ReadBuffer(value, 8);
{Find out what cell type, set content type and value}
ExtractNumberFormat(XF, nf, nfs);
if IsDateTime(value, nf, nfs, dt) then
FWorksheet.WriteDateTime(ARow, ACol, dt, nf, nfs)
else
FWorksheet.WriteNumber(ARow, ACol, value, nf, nfs);
{ Add attributes to cell }
ApplyCellFormatting(ARow, ACol, XF);
end;
// Read the palette
procedure TsSpreadBIFFReader.ReadPalette(AStream: TStream);
var
i, n: Word;
pal: Array of TsColorValue;
begin
n := WordLEToN(AStream.ReadWord) + 8;
SetLength(pal, n);
for i:=0 to 7 do
pal[i] := Workbook.GetPaletteColor(i);
for i:=8 to n-1 do
pal[i] := DWordLEToN(AStream.ReadDWord);
Workbook.UsePalette(@pal[0], n, false);
FPaletteFound := true;
end;
{ Read pane sizes
Valid for all BIFF versions }
procedure TsSpreadBIFFReader.ReadPane(AStream: TStream);
begin
{ Position of horizontal split:
- Unfrozen pane: Width of the left pane(s) (in twips = 1/20 of a point)
- Frozen pane: Number of visible columns in left pane(s) }
FWorksheet.LeftPaneWidth := WordLEToN(AStream.ReadWord);
{ Position of vertical split:
- Unfrozen pane: Height of the top pane(s) (in twips = 1/20 of a point)
- Frozen pane: Number of visible rows in top pane(s) }
FWorksheet.TopPaneHeight := WordLEToN(AStream.ReadWord);
{ There's more information which is not supported here:
Offset Size Description
4 2 Index to first visible row in bottom pane(s)
6 2 Index to first visible column in right pane(s)
8 1 Identifier of pane with active cell cursor (see below)
[9] 1 Not used (BIFF5-BIFF8 only, not written in BIFF2-BIFF4) }
end;
// Read the row, column and xf index
// NOT VALID for BIFF2
procedure TsSpreadBIFFReader.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 := WordLEtoN(AStream.ReadWord);
end;
{ Reads an RK value cell from the stream
Valid since BIFF3. }
procedure TsSpreadBIFFReader.ReadRKValue(AStream: TStream);
var
RK: DWord;
ARow, ACol: Cardinal;
XF: Word;
lDateTime: TDateTime;
Number: Double;
nf: TsNumberFormat; // Number format
nfs: String; // Number format string
begin
{Retrieve XF record, row and column}
ReadRowColXF(AStream, ARow, ACol, XF);
{Encoded RK value}
RK := DWordLEtoN(AStream.ReadDWord);
{Check RK codes}
Number := DecodeRKValue(RK);
{Find out what cell type, set contenttype and value}
ExtractNumberFormat(XF, nf, nfs);
if IsDateTime(Number, nf, nfs, lDateTime) then
FWorksheet.WriteDateTime(ARow, ACol, lDateTime, nf, nfs)
else
FWorksheet.WriteNumber(ARow, ACol, Number, nf, nfs);
{Add attributes}
ApplyCellFormatting(ARow, ACol, XF);
end;
// Read the part of the ROW record that is common to BIFF3-8 versions
procedure TsSpreadBIFFReader.ReadRowInfo(AStream: TStream);
type
TRowRecord = packed record
RowIndex: Word;
Col1: Word;
Col2: Word;
Height: Word;
NotUsed1: Word;
NotUsed2: Word; // not used in BIFF5-BIFF8
Flags: DWord;
end;
var
rowrec: TRowRecord;
lRow: PRow;
h: word;
begin
AStream.ReadBuffer(rowrec, SizeOf(TRowRecord));
// if bit 6 is set in the flags row height does not match the font size.
// Only for this case we create a row record for fpspreadsheet
if rowrec.Flags and $00000040 <> 0 then begin
lRow := FWorksheet.GetRow(WordLEToN(rowrec.RowIndex));
// row height is encoded into the 15 lower bits in units "twips" (1/20 pt)
// we need it in "lines", i.e. we divide the points by the point size of the default font
h := WordLEToN(rowrec.Height) and $7FFF;
lRow^.Height := TwipsToPts(h) / FWorkbook.GetDefaultFontSize;
if lRow^.Height > ROW_HEIGHT_CORRECTION then
lRow^.Height := lRow^.Height - ROW_HEIGHT_CORRECTION
else
lRow^.Height := 0;
end;
{
h := WordLEToN(rowrec.Height);
if h and $8000 = 0 then begin // if this bit were set, rowheight would be default
lRow := FWorksheet.GetRow(WordLEToN(rowrec.RowIndex));
// Row height is encoded into the 15 remaining bits in units "twips" (1/20 pt)
// We need it in "lines", i.e. we divide the points by the point size of the default font
lRow^.Height := TwipsToPts(h and $7FFF) / FWorkbook.GetFont(0).Size;
if lRow^.Height > ROW_HEIGHT_CORRECTION then
lRow^.Height := lRow^.Height - ROW_HEIGHT_CORRECTION
else
lRow^.Height := 0;
end;
}
end;
{ Reads the cell address used in an RPN formula element. Evaluates the corresponding
bits to distinguish between absolute and relative addresses.
Implemented here for BIFF2-BIFF5. BIFF8 must be overridden. }
procedure TsSpreadBIFFReader.ReadRPNCellAddress(AStream: TStream;
var ARow, ACol: Cardinal; var AFlags: TsRelFlags);
var
r: word;
begin
// 2 bytes for row (including absolute/relative info)
r := WordLEToN(AStream.ReadWord);
// 1 byte for column index
ACol := AStream.ReadByte;
// Extract row index
ARow := r and MASK_EXCEL_ROW;
// Extract absolute/relative flags
AFlags := [];
if (r and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol);
if (r and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow);
end;
{ Reads the cell address used in an RPN formula element. Evaluates the corresponding
bits to distinguish between absolute and relative addresses.
Implemented here for BIFF2-BIFF5. BIFF8 must be overridden. }
procedure TsSpreadBIFFReader.ReadRPNCellRangeAddress(AStream: TStream;
var ARow1, ACol1, ARow2, ACol2: Cardinal; var AFlags: TsRelFlags);
var
r1, r2: word;
begin
// 2 bytes, each, for first and last row (including absolute/relative info)
r1 := WordLEToN(AStream.ReadWord);
r2 := WordLEToN(AStream.ReadWord);
// 1 byte each for fist and last column index
ACol1 := AStream.ReadByte;
ACol2 := AStream.ReadByte;
// Extract row index of first and last row
ARow1 := r1 and MASK_EXCEL_ROW;
ARow2 := r2 and MASK_EXCEL_ROW;
// Extract absolute/relative flags
AFlags := [];
if (r1 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol);
if (r2 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol2);
if (r1 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow);
if (r2 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow2);
end;
{ Reads the identifier for an RPN function with fixed argument count.
Valid for BIFF4-BIFF8. Override in BIFF2-BIFF3 }
function TsSpreadBIFFReader.ReadRPNFunc(AStream: TStream): Word;
begin
Result := WordLEToN(AStream.ReadWord);
end;
function TsSpreadBIFFReader.ReadRPNTokenArray(AStream: TStream;
var AFormula: TsRPNFormula): Boolean;
var
n: Word;
p0: Int64;
token: Byte;
rpnItem: PRPNItem;
supported: boolean;
wordVal: Word; // 2 byte unsigned integer
dblVal: Double; // IEEE 8 byte floating point number
flags: TsRelFlags;
r, c, r2, c2: Cardinal;
fek: TFEKind;
func: Word;
b: Byte;
found: Boolean;
begin
rpnItem := nil;
n := ReadRPNTokenArraySize(AStream);
p0 := AStream.Position;
supported := true;
while (AStream.Position < p0 + n) and supported do begin
token := AStream.ReadByte;
case token of
INT_EXCEL_TOKEN_TREFV, INT_EXCEL_TOKEN_TREFR:
begin
ReadRPNCellAddress(AStream, r, c, flags);
rpnItem := RPNCellRef(r, c, flags, rpnItem);
end;
INT_EXCEL_TOKEN_TAREA_R, INT_EXCEL_TOKEN_TAREA_V:
begin
ReadRPNCellRangeAddress(AStream, r, c, r2, c2, flags);
rpnItem := RPNCellRange(r, c, r2, c2, flags, rpnItem);
end;
INT_EXCEL_TOKEN_TMISSARG:
rpnItem := RPNMissingArg(rpnItem);
INT_EXCEL_TOKEN_TSTR:
rpnItem := RPNString(ReadString_8BitLen(AStream), rpnItem);
INT_EXCEL_TOKEN_TERR:
rpnItem := RPNErr(AStream.ReadByte, rpnItem);
INT_EXCEL_TOKEN_TBOOL:
rpnItem := RPNBool(AStream.ReadByte=1, rpnItem);
INT_EXCEL_TOKEN_TINT:
rpnItem := RPNInteger(WordLEToN(AStream.ReadWord), rpnItem);
INT_EXCEL_TOKEN_TNUM:
begin
AStream.ReadBuffer(dblVal, 8);
rpnItem := RPNNumber(dblVal, rpnItem);
end;
INT_EXCEL_TOKEN_TPAREN:
rpnItem := RPNParenthesis(rpnItem);
INT_EXCEL_TOKEN_FUNC_R,
INT_EXCEL_TOKEN_FUNC_V,
INT_EXCEL_TOKEN_FUNC_A:
// functions with fixed argument count
begin
func := ReadRPNFunc(AStream);
found := false;
for fek in TFuncTokens do begin
if (TokenIDs[fek] = func) and FixedParamCount(fek) then begin
rpnItem := RPNFunc(fek, rpnItem);
found := true;
break;
end;
end;
if not found then
supported := false;
end;
INT_EXCEL_TOKEN_FUNCVAR_R,
INT_EXCEL_TOKEN_FUNCVAR_V,
INT_EXCEL_TOKEN_FUNCVAR_A:
// functions with variable argument count
begin
b := AStream.ReadByte;
func := ReadRPNFunc(AStream);
found := false;
for fek in TFuncTokens do
if (TokenIDs[fek] = func) and not FixedParamCount(fek) then begin
rpnItem := RPNFunc(fek, b, rpnItem);
found := true;
break;
end;
if not found then
supported := false;
end
else
found := false;
for fek in TBasicOperationTokens do
if (TokenIDs[fek] = token) then begin
rpnItem := RPNFunc(fek, rpnItem);
found := true;
break;
end;
if not found then
supported := false;
end;
end;
if not supported then begin
DestroyRPNFormula(rpnItem);
SetLength(AFormula, 0);
Result := false;
end
else begin
AFormula := CreateRPNFormula(rpnItem);
Result := true;
end;
end;
{ Helper funtion for reading of the size of the token array of an RPN formula.
Is implemented here for BIFF3-BIFF8 where the size is a 2-byte value.
Needs to be rewritten for BIFF2 using a 1-byte size. }
function TsSpreadBIFFReader.ReadRPNTokenArraySize(AStream: TStream): Word;
begin
Result := WordLEToN(AStream.ReadWord);
end;
{ Helper function for reading a string with 8-bit length. Here, we implement the
version for ansistrings since it is valid for all BIFF versions except BIFF8
where it has to overridden. }
function TsSpreadBIFFReader.ReadString_8bitLen(AStream: TStream): String;
var
len: Byte;
s: ansistring;
begin
len := AStream.ReadByte;
SetLength(s, len);
AStream.ReadBuffer(s[1], len);
Result := s;
end;
{ Reads a STRING record. It immediately precedes a FORMULA record which has a
string result. The read value is applied to the FIncompleteCell.
Must be overridden because the implementation depends on BIFF version. }
procedure TsSpreadBIFFReader.ReadStringRecord(AStream: TStream);
begin
//
end;
{ Reads the WINDOW2 record containing information like "show grid lines",
"show sheet headers", "panes are frozen", etc.
The record structure is slightly different for BIFF5 and BIFF8, but we use
here only the common part.
BIFF2 has a different structure and has to be re-written. }
procedure TsSpreadBIFFReader.ReadWindow2(AStream: TStream);
var
flags: Word;
begin
flags := WordLEToN(AStream.ReadWord);
if (flags and MASK_WINDOW2_OPTION_SHOW_GRID_LINES <> 0) then
FWorksheet.Options := FWorksheet.Options + [soShowGridLines]
else
FWorksheet.Options := FWorksheet.Options - [soShowGridLines];
if (flags and MASK_WINDOW2_OPTION_SHOW_SHEET_HEADERS <> 0) then
FWorksheet.Options := FWorksheet.Options + [soShowHeaders]
else
FWorksheet.Options := FWorksheet.Options - [soShowHeaders];
if (flags and MASK_WINDOW2_OPTION_SHEET_SELECTED <> 0) then
FWorksheet.Options := FWorksheet.Options + [soSelected]
else
FWorksheet.Options := FWorksheet.Options - [soSelected];
if (flags and MASK_WINDOW2_OPTION_PANES_ARE_FROZEN <> 0) then
FWorksheet.Options := FWorksheet.Options + [soHasFrozenPanes]
else
FWorksheet.Options := FWorksheet.Options - [soHasFrozenPanes];
end;
{ TsSpreadBIFFWriter }
constructor TsSpreadBIFFWriter.Create(AWorkbook: TsWorkbook);
begin
inherited Create(AWorkbook);
// Initial base date in case it won't be set otherwise.
// Use 1900 to get a bit more range between 1900..1904.
FDateMode := dm1900;
end;
destructor TsSpreadBIFFWriter.Destroy;
begin
inherited Destroy;
end;
{ These are default style formats which are added as XF fields regardless of
being used in the document or not.
Currently, only one additional default format is supported ("bold").
Here are the changes to be made when extending this list:
- SetLength(FFormattingstyles, <number of predefined styles>)
}
procedure TsSpreadBIFFWriter.AddDefaultFormats();
begin
// XF0..XF14: Normal style, Row Outline level 1..7,
// Column Outline level 1..7.
// XF15 - Default cell format, no formatting (4.6.2)
SetLength(FFormattingStyles, 1);
FFormattingStyles[0].UsedFormattingFields := [];
FFormattingStyles[0].BorderStyles := DEFAULT_BORDERSTYLES;
FFormattingStyles[0].Row := 15;
NextXFIndex := 15 + Length(FFormattingStyles);
// "15" is the index of the last pre-defined xf record
end;
{ Creates the correct version of the number format list. It is for BIFF file
formats.
Valid for BIFF5.BIFF8. Needs to be overridden for BIFF2. }
procedure TsSpreadBIFFWriter.CreateNumFormatList;
begin
FreeAndNil(FNumFormatList);
FNumFormatList := TsBIFFNumFormatList.Create(Workbook);
end;
function TsSpreadBIFFWriter.FormulaElementKindToExcelTokenID(
AElementKind: TFEKind; out ASecondaryID: Word): Word;
begin
if (AElementKind >= Low(TFuncTokens)) and (AElementKind <= High(TFuncTokens))
then begin
if FixedParamCount(AElementKind) then
Result := INT_EXCEL_TOKEN_FUNC_V
else
Result := INT_EXCEL_TOKEN_FUNCVAR_V;
ASecondaryID := TokenIDs[AElementKind];
end
else begin
Result := TokenIDs[AElementKind];
ASecondaryID := 0;
end;
end;
procedure TsSpreadBIFFWriter.GetLastRowCallback(ACell: PCell; AStream: TStream);
begin
if ACell^.Row > FLastRow then FLastRow := ACell^.Row;
end;
function TsSpreadBIFFWriter.GetLastRowIndex(AWorksheet: TsWorksheet): Integer;
begin
FLastRow := 0;
IterateThroughCells(nil, AWorksheet.Cells, GetLastRowCallback);
Result := FLastRow;
end;
procedure TsSpreadBIFFWriter.GetLastColCallback(ACell: PCell; AStream: TStream);
begin
if ACell^.Col > FLastCol then FLastCol := ACell^.Col;
end;
function TsSpreadBIFFWriter.GetLastColIndex(AWorksheet: TsWorksheet): Word;
begin
FLastCol := 0;
IterateThroughCells(nil, AWorksheet.Cells, GetLastColCallback);
Result := FLastCol;
end;
{ Writes an empty ("blank") cell. Needed for formatting empty cells.
Valid for BIFF5 and BIFF8. Needs to be overridden for BIFF2 which has a
different record structure. }
procedure TsSpreadBIFFWriter.WriteBlank(AStream: TStream;
const ARow, ACol: Cardinal; ACell: PCell);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_BLANK));
AStream.WriteWord(WordToLE(6));
{ Row and column index }
AStream.WriteWord(WordToLE(ARow));
AStream.WriteWord(WordToLE(ACol));
{ Index to XF record, according to formatting }
WriteXFIndex(AStream, ACell);
end;
procedure TsSpreadBIFFWriter.WriteCodepage(AStream: TStream;
AEncoding: TsEncoding);
var
lCodepage: Word;
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_CODEPAGE));
AStream.WriteWord(WordToLE(2));
{ Codepage }
case AEncoding of
seLatin2: lCodepage := WORD_CP_1250_Latin2;
seCyrillic: lCodepage := WORD_CP_1251_Cyrillic;
seGreek: lCodepage := WORD_CP_1253_Greek;
seTurkish: lCodepage := WORD_CP_1254_Turkish;
seHebrew: lCodepage := WORD_CP_1255_Hebrew;
seArabic: lCodepage := WORD_CP_1256_Arabic;
else
// Default is Latin1
lCodepage := WORD_CP_1252_Latin1;
end;
AStream.WriteWord(WordToLE(lCodepage));
end;
{ Writes column info for the given column. Currently only the colum width is used.
Valid for BIFF5 and BIFF8 (BIFF2 uses a different record. }
procedure TsSpreadBIFFWriter.WriteColInfo(AStream: TStream; ACol: PCol);
var
w: Integer;
begin
if Assigned(ACol) then begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_COLINFO)); // BIFF record header
AStream.WriteWord(WordToLE(12)); // Record size
AStream.WriteWord(WordToLE(ACol^.Col)); // start column
AStream.WriteWord(WordToLE(ACol^.Col)); // end column
{ calculate width to be in units of 1/256 of pixel width of character "0" }
w := round(ACol^.Width * 256);
AStream.WriteWord(WordToLE(w)); // write width
AStream.WriteWord(15); // XF record, ignored
AStream.WriteWord(0); // option flags, ignored
AStream.WriteWord(0); // "not used"
end;
end;
{ Writes the column info records for all used columns. }
procedure TsSpreadBIFFWriter.WriteColInfos(AStream: TStream;
ASheet: TsWorksheet);
var
j: Integer;
col: PCol;
begin
for j := 0 to ASheet.Cols.Count-1 do begin
col := PCol(ASheet.Cols[j]);
WriteColInfo(AStream, col);
end;
end;
procedure TsSpreadBIFFWriter.WriteDateMode(AStream: TStream);
begin
{ BIFF Record header }
// todo: check whether this is in the right place. should end up in workbook globals stream
AStream.WriteWord(WordToLE(INT_EXCEL_ID_DATEMODE));
AStream.WriteWord(WordToLE(2));
case FDateMode of
dm1900: AStream.WriteWord(WordToLE(0));
dm1904: AStream.WriteWord(WordToLE(1));
else raise Exception.CreateFmt('Unknown datemode number %d. Please correct fpspreadsheet code.', [FDateMode]);
end;
end;
{ Writes a date/time/datetime to a Biff NUMBER record, with a date/time format
(There is no separate date record type in xls)
Valid for all BIFF versions. }
procedure TsSpreadBIFFWriter.WriteDateTime(AStream: TStream; const ARow,
ACol: Cardinal; const AValue: TDateTime; ACell: PCell);
var
ExcelDateSerial: double;
begin
ExcelDateSerial := ConvertDateTimeToExcelDateTime(AValue, FDateMode);
// fpspreadsheet must already have set formatting to a date/datetime format, so
// this will get written out as a pointer to the relevant XF record.
// In the end, dates in xls are just numbers with a format. Pass it on to WriteNumber:
WriteNumber(AStream, ARow, ACol, ExcelDateSerial, ACell);
end;
{ Writes a BIFF format record defined in AFormatData. AListIndex the index of
the formatdata in the format list (not the FormatIndex!).
Needs to be overridden by descendants. }
procedure TsSpreadBIFFWriter.WriteFormat(AStream: TStream;
AFormatData: TsNumFormatData; AListIndex: Integer);
begin
// needs to be overridden
end;
{ Writes all number formats to the stream. Saving starts at the item with the
FirstFormatIndexInFile. }
procedure TsSpreadBIFFWriter.WriteFormats(AStream: TStream);
var
i: Integer;
item: TsNumFormatData;
begin
ListAllNumFormats;
i := NumFormatList.FindByIndex(NumFormatList.FirstFormatIndexInFile);
if i > -1 then
while i < NumFormatList.Count do begin
item := NumFormatList[i];
if item <> nil then begin
WriteFormat(AStream, item, i);
end;
inc(i);
end;
end;
{ Writes a 64-bit floating point NUMBER record.
Valid for BIFF5 and BIFF8 (BIFF2 has a different record structure.). }
procedure TsSpreadBIFFWriter.WriteNumber(AStream: TStream;
const ARow, ACol: Cardinal; const AValue: double; ACell: PCell);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_NUMBER));
AStream.WriteWord(WordToLE(14));
{ BIFF Record data }
AStream.WriteWord(WordToLE(ARow));
AStream.WriteWord(WordToLE(ACol));
{ Index to XF record }
WriteXFIndex(AStream, ACell);
{ IEE 754 floating-point value }
AStream.WriteBuffer(AValue, 8);
end;
procedure TsSpreadBIFFWriter.WritePalette(AStream: TStream);
var
i, n: Integer;
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_PALETTE));
AStream.WriteWord(WordToLE(2 + 4*56));
{ Number of colors }
AStream.WriteWord(WordToLE(56));
{ Take the colors from the palette of the Worksheet }
{ Skip the first 8 entries - they are hard-coded into Excel }
n := Workbook.GetPaletteSize;
for i:=8 to 63 do
if i < n then
AStream.WriteDWord(DWordToLE(Workbook.GetPaletteColor(i)))
else
AStream.WriteDWord(DWordToLE($FFFFFF));
end;
{ Writes a PANE record to the stream.
Valid for all BIFF versions. The difference for BIFF5-BIFF8 is a non-used
byte at the end. Activate IsBiff58 in these cases. }
procedure TsSpreadBIFFWriter.WritePane(AStream: TStream; ASheet: TsWorksheet;
IsBiff58: Boolean);
var
n: Word;
active_pane: Byte;
begin
if (ASheet.LeftPaneWidth = 0) and (ASheet.TopPaneHeight = 0) then
exit;
if not (soHasFrozenPanes in ASheet.Options) then
exit;
{ Non-frozen panes should work in principle, but they are not read without
error. They possibly require an additional SELECTION record. }
{ BIFF record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_PANE));
if isBIFF58 then n := 10 else n := 9;
AStream.WriteWord(WordToLE(n));
{ Position of the vertical split (px, 0 = No vertical split):
- Unfrozen pane: Width of the left pane(s) (in twips = 1/20 of a point)
- Frozen pane: Number of visible columns in left pane(s) }
AStream.WriteWord(WordToLE(ASheet.LeftPaneWidth));
{ Position of the horizontal split (py, 0 = No horizontal split):
- Unfrozen pane: Height of the top pane(s) (in twips = 1/20 of a point)
- Frozen pane: Number of visible rows in top pane(s) }
AStream.WriteWord(WordToLE(ASheet.TopPaneHeight));
{ Index to first visible row in bottom pane(s) }
if (soHasFrozenPanes in ASheet.Options) then
AStream.WriteWord(WordToLE(ASheet.TopPaneHeight))
else
AStream.WriteWord(WordToLE(0));
{ Index to first visible column in right pane(s) }
if (soHasFrozenPanes in ASheet.Options) then
AStream.WriteWord(WordToLE(ASheet.LeftPaneWidth))
else
AStream.WriteWord(WordToLE(0));
{ Identifier of pane with active cell cursor
0 = right-bottom
1 = right-top
2 = left-bottom
3 = left-top }
if (soHasFrozenPanes in ASheet.Options) then begin
if (ASheet.LeftPaneWidth = 0) and (ASheet.TopPaneHeight = 0) then
active_pane := 3
else
if (ASheet.LeftPaneWidth = 0) then
active_pane := 2
else
if (ASheet.TopPaneHeight =0) then
active_pane := 1
else
active_pane := 0;
end else
active_pane := 0;
AStream.WriteByte(active_pane);
if IsBIFF58 then
AStream.WriteByte(0);
{ Not used (BIFF5-BIFF8 only, not written in BIFF2-BIFF4 }
end;
{ Writes an Excel 3-8 ROW record
Valid for BIFF3-BIFF8 }
procedure TsSpreadBIFFWriter.WriteRow(AStream: TStream; ASheet: TsWorksheet;
ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow);
var
w: Word;
dw: DWord;
cell: PCell;
spaceabove, spacebelow: Boolean;
colindex: Cardinal;
rowheight: Word;
h: Single;
begin
// Check for additional space above/below row
spaceabove := false;
spacebelow := false;
colindex := AFirstColIndex;
while colindex <= ALastColIndex do begin
cell := ASheet.FindCell(ARowindex, colindex);
if (cell <> nil) and (uffBorder in cell^.UsedFormattingFields) then begin
if (cbNorth in cell^.Border) and (cell^.BorderStyles[cbNorth].LineStyle = lsThick)
then spaceabove := true;
if (cbSouth in cell^.Border) and (cell^.BorderStyles[cbSouth].LineStyle = lsThick)
then spacebelow := true;
end;
if spaceabove and spacebelow then break;
inc(colindex);
end;
{ BIFF record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_ROW));
AStream.WriteWord(WordToLE(16));
{ 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));
{ Row height (in twips, 1/20 point) and info on custom row height }
h := Workbook.GetFont(0).Size; // Point size of default font
if (ARow = nil) or (ARow^.Height = Workbook.DefaultRowHeight) then
rowheight := PtsToTwips((Workbook.DefaultRowHeight + ROW_HEIGHT_CORRECTION) * h)
else
if (ARow^.Height = 0) then
rowheight := 0
else
rowheight := PtsToTwips((ARow^.Height + ROW_HEIGHT_CORRECTION)*h);
w := rowheight and $7FFF;
AStream.WriteWord(WordToLE(w));
{ 2 words not used }
AStream.WriteDWord(0);
{ Option flags }
dw := $00000100; // bit 8 is always 1
if spaceabove then dw := dw or $10000000;
if spacebelow then dw := dw or $20000000;
if (ARow <> nil) then
dw := dw or $00000040; // Row height and font height do not match
AStream.WriteDWord(DWordToLE(dw));
end;
{ Writes all ROW records for the given sheet.
Note that the OpenOffice documentation says that rows must be written in
groups of 32, followed by the cells on these rows, etc. THIS IS NOT NECESSARY!
Valid for BIFF2-BIFF8 }
procedure TsSpreadBIFFWriter.WriteRows(AStream: TStream; ASheet: TsWorksheet);
var
row: PRow;
i: Integer;
cell1, cell2: PCell;
begin
for i := 0 to ASheet.Rows.Count-1 do begin
row := ASheet.Rows[i];
cell1 := ASheet.GetFirstCellOfRow(row^.Row);
if cell1 <> nil then begin
cell2 := ASheet.GetLastCellOfRow(row^.Row);
WriteRow(AStream, ASheet, row^.Row, cell1^.Col, cell2^.Col, row);
end else
WriteRow(AStream, ASheet, row^.Row, 0, 0, row);
end;
end;
{ Writes an Excel 2-8 SELECTION record
Writes just reasonable default values
APane is 0..3 (see below)
Valid for BIFF2-BIFF8 }
procedure TsSpreadBIFFWriter.WriteSelection(AStream: TStream;
ASheet: TsWorksheet; APane: Byte);
var
activeCellRow, activeCellCol: Word;
begin
case APane of
0: begin // right-bottom
activeCellRow := ASheet.TopPaneHeight;
activeCellCol := ASheet.LeftPaneWidth;
end;
1: begin // right-top
activeCellRow := 0;
activeCellCol := ASheet.LeftPaneWidth;
end;
2: begin // left-bottom
activeCellRow := ASheet.TopPaneHeight;
activeCellCol := 0;
end;
3: begin // left-top
activeCellRow := 0;
activeCellCol := 0;
end;
end;
{ BIFF record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_SELECTION));
AStream.WriteWord(WordToLE(15));
{ Pane identifier }
AStream.WriteByte(APane);
{ Index to row of the active cell }
AStream.WriteWord(WordToLE(activeCellRow));
{ Index to column of the active cell }
AStream.WriteWord(WordToLE(activeCellCol));
{ Index into the following cell range list to the entry that contains the active cell }
AStream.WriteWord(WordToLE(0)); // there's only 1 item
{ Cell range array }
{ Count of items }
AStream.WriteWord(WordToLE(1)); // only 1 item
{ Index to first and last row - are the same here }
AStream.WriteWord(WordTOLE(activeCellRow));
AStream.WriteWord(WordTOLE(activeCellRow));
{ Index to first and last column - they are the same here again. }
{ Note: BIFF8 writes bytes here! }
AStream.WriteByte(activeCellCol);
AStream.WriteByte(activeCellCol);
end;
procedure TsSpreadBIFFWriter.WriteSelections(AStream: TStream;
ASheet: TsWorksheet);
begin
WriteSelection(AStream, ASheet, 3);
if (ASheet.LeftPaneWidth = 0) then begin
if ASheet.TopPaneHeight > 0 then WriteSelection(AStream, ASheet, 2);
end else begin
WriteSelection(AStream, ASheet, 1);
if ASheet.TopPaneHeight > 0 then begin
WriteSelection(AStream, ASheet, 2);
WriteSelection(AStream, ASheet, 0);
end;
end;
end;
{ Writes an Excel 5/8 WINDOW1 record
This record contains general settings for the document window and
global workbook settings.
The values written here are reasonable defaults which should work for most
sheets.
Valid for BIFF5-BIFF8. }
procedure TsSpreadBIFFWriter.WriteWindow1(AStream: TStream);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_WINDOW1));
AStream.WriteWord(WordToLE(18));
{ 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));
{ Option flags }
AStream.WriteWord(WordToLE(
MASK_WINDOW1_OPTION_HORZ_SCROLL_VISIBLE or
MASK_WINDOW1_OPTION_VERT_SCROLL_VISIBLE or
MASK_WINDOW1_OPTION_WORKSHEET_TAB_VISIBLE));
{ Index to active (displayed) worksheet }
AStream.WriteWord(WordToLE($00));
{ Index of first visible tab in the worksheet tab bar }
AStream.WriteWord(WordToLE($00));
{ Number of selected worksheets }
AStream.WriteWord(WordToLE(1));
{ Width of worksheet tab bar (in 1/1000 of window width).
The remaining space is used by the horizontal scroll bar }
AStream.WriteWord(WordToLE(600));
end;
{ Write the index of the XF record, according to formatting of the given cell
Valid for BIFF5 and BIFF8.
BIFF2 is handled differently. }
procedure TsSpreadBIFFWriter.WriteXFIndex(AStream: TStream; ACell: PCell);
var
lIndex: Integer;
lXFIndex: Word;
lCell: TCell;
begin
// First try the fast methods for default formats
if ACell^.UsedFormattingFields = [] then begin
AStream.WriteWord(WordToLE(15)); //XF15; see TsSpreadBIFF8Writer.AddDefaultFormats
Exit;
end;
// If not, then we need to search in the list of dynamic formats
// But we have to consider that the number formats of the cell is in fpc syntax,
// but the number format list of the writer is in Excel syntax.
lCell := ACell^;
lIndex := FindFormattingInList(@lCell);
// Carefully check the index
if (lIndex < 0) or (lIndex > Length(FFormattingStyles)) then
raise Exception.Create('[TsSpreadBIFFWriter.WriteXFIndex] Invalid Index, this should not happen!');
lXFIndex := FFormattingStyles[lIndex].Row;
AStream.WriteWord(WordToLE(lXFIndex));
end;
end.