{ xlsbiff8.pas Writes an Excel 8 file An Excel worksheet stream 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 Excel 8 files are OLE compound document files, and must be written using the fpOLE library. Records Needed to Make a BIFF8 File Microsoft Excel Can Use: Required Records: BOF - Set the 6 byte offset to 0x0005 (workbook globals) Window1 FONT - At least five of these records must be included XF - At least 15 Style XF records and 1 Cell XF record must be included STYLE BOUNDSHEET - Include one BOUNDSHEET record per worksheet EOF BOF - Set the 6 byte offset to 0x0010 (worksheet) INDEX DIMENSIONS WINDOW2 EOF The row and column numbering in BIFF files is zero-based. Excel file format specification obtained from: http://sc.openoffice.org/excelfileformat.pdf AUTHORS: Felipe Monteiro de Carvalho Jose Mejuto } unit xlsbiff8; {$ifdef fpc} {$mode delphi} {$endif} // The new OLE code is much better, so always use it {$define USE_NEW_OLE} interface uses Classes, SysUtils, fpcanvas, DateUtils, fpspreadsheet, xlscommon, {$ifdef USE_NEW_OLE} fpolebasic, {$else} fpolestorage, {$endif} fpsutils, lazutf8; type TXFRecordData = class public FormatIndex: Integer; end; TFormatRecordData = class public Index: Integer; FormatString: widestring; end; { TsSpreadBIFF8Reader } TsSpreadBIFF8Reader = class(TsSpreadBIFFReader) private RecordSize: Word; PendingRecordSize: SizeInt; FWorksheet: TsWorksheet; FWorksheetNames: TStringList; FCurrentWorksheet: Integer; FSharedStringTable: TStringList; FXFList: TFPList; // of TXFRecordData FFormatList: TFPList; // of TFormatRecordData function DecodeRKValue(const ARK: DWORD): Double; function ReadWideString(const AStream: TStream;const ALength: WORD): WideString; overload; function ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; overload; procedure ReadWorkbookGlobals(AStream: TStream; AData: TsWorkbook); procedure ReadWorksheet(AStream: TStream; AData: TsWorkbook); procedure ReadBoundsheet(AStream: TStream); procedure ReadRKValue(const AStream: TStream); procedure ReadMulRKValues(const AStream: TStream); procedure ReadFormulaExcel(AStream: TStream); procedure ReadRowColXF(const AStream: TStream; out ARow,ACol,AXF: WORD); function ReadString(const AStream: TStream; const ALength: WORD): UTF8String; procedure ReadRichString(const AStream: TStream); procedure ReadSST(const AStream: TStream); procedure ReadLabelSST(const AStream: TStream); // procedure ReadXF(const AStream: TStream); procedure ReadFormat(const AStream: TStream); function FindFormatRecordForCell(const AFXIndex: Integer): TFormatRecordData; class function ConvertExcelDateToTDateTime(const AExcelDateNum: Double; ABaseDate: TDateTime): TDateTime; // Workbook Globals records // procedure ReadCodepage in xlscommon procedure ReadFont(const AStream: TStream); public constructor Create; override; destructor Destroy; override; { General reading methods } procedure ReadFromFile(AFileName: string; AData: TsWorkbook); override; procedure ReadFromStream(AStream: TStream; AData: TsWorkbook); override; { Record writing methods } procedure ReadFormula(AStream: TStream); override; procedure ReadLabel(AStream: TStream); override; procedure ReadNumber(AStream: TStream); override; end; { TsSpreadBIFF8Writer } TsSpreadBIFF8Writer = class(TsSpreadBIFFWriter) private procedure WriteXFIndex(AStream: TStream; ACell: PCell); procedure WriteXFFieldsForFormattingStyles(AStream: TStream); protected procedure AddDefaultFormats(); override; public // constructor Create; // destructor Destroy; override; { General writing methods } procedure WriteToFile(const AFileName: string; AData: TsWorkbook; const AOverwriteExisting: Boolean = False); override; procedure WriteToStream(AStream: TStream; AData: TsWorkbook); override; { Record writing methods } procedure WriteBOF(AStream: TStream; ADataType: Word); function WriteBoundsheet(AStream: TStream; ASheetName: string): Int64; // procedure WriteCodepage in xlscommon procedure WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteEOF(AStream: TStream); procedure WriteFont(AStream: TStream; AFont: TFPCustomFont); procedure WriteFormula(AStream: TStream; const ARow, ACol: Word; const AFormula: TsFormula); override; procedure WriteRPNFormula(AStream: TStream; const ARow, ACol: Word; const AFormula: TsRPNFormula); override; procedure WriteIndex(AStream: TStream); procedure WriteLabel(AStream: TStream; const ARow, ACol: Word; const AValue: string; ACell: PCell); override; procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); override; procedure WritePalette(AStream: TStream); procedure WriteStyle(AStream: TStream); procedure WriteWindow1(AStream: TStream); procedure WriteWindow2(AStream: TStream; ASheetSelected: Boolean); procedure WriteXF(AStream: TStream; AFontIndex: Word; AXF_TYPE_PROT, ATextRotation: Byte; ABorders: TsCellBorders; AddBackground: Boolean = False; ABackgroundColor: TsColor = scSilver); end; implementation const { Excel record IDs } INT_EXCEL_ID_BOF = $0809; INT_EXCEL_ID_BOUNDSHEET = $0085; // Renamed to SHEET in the latest OpenOffice docs INT_EXCEL_ID_EOF = $000A; INT_EXCEL_ID_DIMENSIONS = $0200; INT_EXCEL_ID_FONT = $0031; INT_EXCEL_ID_FORMULA = $0006; INT_EXCEL_ID_INDEX = $020B; INT_EXCEL_ID_LABEL = $0204; INT_EXCEL_ID_NUMBER = $0203; INT_EXCEL_ID_STYLE = $0293; INT_EXCEL_ID_WINDOW1 = $003D; INT_EXCEL_ID_WINDOW2 = $023E; INT_EXCEL_ID_XF = $00E0; INT_EXCEL_ID_RSTRING = $00D6; INT_EXCEL_ID_RK = $027E; INT_EXCEL_ID_MULRK = $00BD; INT_EXCEL_ID_SST = $00FC; //BIFF8 only INT_EXCEL_ID_CONTINUE = $003C; INT_EXCEL_ID_LABELSST = $00FD; //BIFF8 only INT_EXCEL_ID_PALETTE = $0092; INT_EXCEL_ID_CODEPAGE = $0042; INT_EXCEL_ID_FORMAT = $041E; { Cell Addresses constants } MASK_EXCEL_ROW = $3FFF; MASK_EXCEL_RELATIVE_ROW = $4000; MASK_EXCEL_RELATIVE_COL = $8000; { BOF record constants } INT_BOF_BIFF8_VER = $0600; INT_BOF_WORKBOOK_GLOBALS= $0005; INT_BOF_VB_MODULE = $0006; INT_BOF_SHEET = $0010; INT_BOF_CHART = $0020; INT_BOF_MACRO_SHEET = $0040; INT_BOF_WORKSPACE = $0100; INT_BOF_BUILD_ID = $1FD2; INT_BOF_BUILD_YEAR = $07CD; { FONT record constants } INT_FONT_WEIGHT_NORMAL = $0190; INT_FONT_WEIGHT_BOLD = $02BC; { FORMULA record constants } MASK_FORMULA_RECALCULATE_ALWAYS = $0001; MASK_FORMULA_RECALCULATE_ON_OPEN = $0002; MASK_FORMULA_SHARED_FORMULA = $0008; { STYLE record constants } MASK_STYLE_BUILT_IN = $8000; { WINDOW1 record constants } 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 } 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; MASK_WINDOW2_OPTION_SHEET_SELECTED = $0200; MASK_WINDOW2_OPTION_SHEET_ACTIVE = $0400; { 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 = $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_VERT_ALIGN } MASK_XF_VERT_ALIGN_TOP = $00; MASK_XF_VERT_ALIGN_CENTRED = $10; MASK_XF_VERT_ALIGN_BOTTOM = $20; MASK_XF_VERT_ALIGN_JUSTIFIED = $30; { XF_ROTATION } XF_ROTATION_HORIZONTAL = 0; XF_ROTATION_90_DEGREE_COUNTERCLOCKWISE = 90; XF_ROTATION_90_DEGREE_CLOCKWISE = 180; { XF record constants } MASK_XF_TYPE_PROT = $0007; MASK_XF_TYPE_PROT_PARENT = $FFF0; MASK_XF_VERT_ALIGN = $70; { Exported functions } { TsSpreadBIFF8Writer } { Index to XF record, according to formatting } procedure TsSpreadBIFF8Writer.WriteXFIndex(AStream: TStream; ACell: PCell); var lIndex: Integer; lXFIndex: Word; begin // First try the fast methods for default formats if ACell^.UsedFormattingFields = [] then begin AStream.WriteWord(WordToLE(15)); Exit; end; if ACell^.UsedFormattingFields = [uffTextRotation] then begin case ACell^.TextRotation of rt90DegreeCounterClockwiseRotation: AStream.WriteWord(WordToLE(16)); rt90DegreeClockwiseRotation: AStream.WriteWord(WordToLE(17)); else AStream.WriteWord(WordToLE(15)); end; Exit; end; if ACell^.UsedFormattingFields = [uffBold] then begin AStream.WriteWord(WordToLE(18)); Exit; end; // If not, then we need to search in the list of dynamic formats lIndex := FindFormattingInList(ACell); // Carefully check the index if (lIndex < 0) or (lIndex > Length(FFormattingStyles)) then raise Exception.Create('[TsSpreadBIFF8Writer.WriteXFIndex] Invalid Index, this should not happen!'); lXFIndex := FFormattingStyles[lIndex].Row; AStream.WriteWord(WordToLE(lXFIndex)); end; procedure TsSpreadBIFF8Writer.WriteXFFieldsForFormattingStyles(AStream: TStream); var i: Integer; lFontIndex: Word; lTextRotation: Byte; lBorders: TsCellBorders; lAddBackground: Boolean; lBackgroundColor: TsColor; begin // The first 4 styles were already added for i := 4 to Length(FFormattingStyles) - 1 do begin // Default styles lFontIndex := 0; lTextRotation := XF_ROTATION_HORIZONTAL; lBorders := []; lAddBackground := False; lBackgroundColor := FFormattingStyles[i].BackgroundColor; // Now apply the modifications if uffBorder in FFormattingStyles[i].UsedFormattingFields then lBorders := FFormattingStyles[i].Border; if uffTextRotation in FFormattingStyles[i].UsedFormattingFields then begin case FFormattingStyles[i].TextRotation of trHorizontal: lTextRotation := XF_ROTATION_HORIZONTAL; rt90DegreeClockwiseRotation: lTextRotation := XF_ROTATION_90_DEGREE_CLOCKWISE; rt90DegreeCounterClockwiseRotation: lTextRotation := XF_ROTATION_90_DEGREE_COUNTERCLOCKWISE; end; end; if uffBold in FFormattingStyles[i].UsedFormattingFields then lFontIndex := 1; if uffBackgroundColor in FFormattingStyles[i].UsedFormattingFields then lAddBackground := True; // And finally write the style WriteXF(AStream, lFontIndex, 0, lTextRotation, lBorders, lAddBackground, lBackgroundColor); end; end; {@@ These are default formats which are added as XF fields regardless of being used in the document or not. } procedure TsSpreadBIFF8Writer.AddDefaultFormats(); begin NextXFIndex := 19; SetLength(FFormattingStyles, 4); // XF15 - Default, no formatting FFormattingStyles[0].UsedFormattingFields := []; FFormattingStyles[0].Row := 15; // XF16 - Rotated FFormattingStyles[1].UsedFormattingFields := [uffTextRotation]; FFormattingStyles[1].Row := 16; FFormattingStyles[1].TextRotation := rt90DegreeCounterClockwiseRotation; // XF17 - Rotated FFormattingStyles[2].UsedFormattingFields := [uffTextRotation]; FFormattingStyles[2].Row := 17; FFormattingStyles[2].TextRotation := rt90DegreeClockwiseRotation; // XF18 - Bold FFormattingStyles[3].UsedFormattingFields := [uffBold]; FFormattingStyles[3].Row := 18; end; {******************************************************************* * TsSpreadBIFF8Writer.WriteToFile () * * DESCRIPTION: Writes an Excel BIFF8 file to the disc * * The BIFF 8 writer overrides this method because * BIFF 8 is written as an OLE document, and our * current OLE document writing method involves: * * 1 - Writing the BIFF data to a memory stream * * 2 - Write the memory stream data to disk using * COM functions * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteToFile(const AFileName: string; AData: TsWorkbook; const AOverwriteExisting: Boolean); var MemStream: TMemoryStream; OutputStorage: TOLEStorage; OLEDocument: TOLEDocument; begin MemStream := TMemoryStream.Create; OutputStorage := TOLEStorage.Create; try WriteToStream(MemStream, AData); // Only one stream is necessary for any number of worksheets OLEDocument.Stream := MemStream; OutputStorage.WriteOLEFile(AFileName, OLEDocument, AOverwriteExisting, 'Workbook'); finally MemStream.Free; OutputStorage.Free; end; end; {******************************************************************* * TsSpreadBIFF8Writer.WriteToStream () * * DESCRIPTION: Writes an Excel BIFF8 record structure * * Be careful as this method doesn't write the OLE * part of the document, just the BIFF records * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteToStream(AStream: TStream; AData: TsWorkbook); var FontData: TFPCustomFont; MyData: TMemoryStream; CurrentPos: Int64; Boundsheets: array of Int64; i, len: Integer; begin { Write workbook globals } WriteBOF(AStream, INT_BOF_WORKBOOK_GLOBALS); WriteWindow1(AStream); FontData := TFPCustomFont.Create; try FontData.Name := 'Arial'; // FONT0 - normal WriteFont(AStream, FontData); // FONT1 - bold FontData.Bold := True; WriteFont(AStream, FontData); FontData.Bold := False; // FONT2 WriteFont(AStream, FontData); // FONT3 WriteFont(AStream, FontData); // FONT5 WriteFont(AStream, FontData); finally FontData.Free; end; // PALETTE WritePalette(AStream); // XF0 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF1 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF2 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF3 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF4 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF5 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF6 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF7 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF8 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF9 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF10 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF11 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF12 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF13 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF14 WriteXF(AStream, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, []); // XF15 - Default, no formatting WriteXF(AStream, 0, 0, XF_ROTATION_HORIZONTAL, []); // XF16 - Rotated WriteXF(AStream, 0, 0, XF_ROTATION_90_DEGREE_COUNTERCLOCKWISE, []); // XF17 - Rotated WriteXF(AStream, 0, 0, XF_ROTATION_90_DEGREE_CLOCKWISE, []); // XF18 - Bold WriteXF(AStream, 1, 0, XF_ROTATION_HORIZONTAL, []); // Add further all non-standard formatting styles ListAllFormattingStyles(AData); WriteXFFieldsForFormattingStyles(AStream); WriteStyle(AStream); // A BOUNDSHEET for each worksheet for i := 0 to AData.GetWorksheetCount - 1 do begin len := Length(Boundsheets); SetLength(Boundsheets, len + 1); Boundsheets[len] := WriteBoundsheet(AStream, AData.GetWorksheetByIndex(i).Name); end; WriteEOF(AStream); { Write each worksheet } for i := 0 to AData.GetWorksheetCount - 1 do begin { First goes back and writes the position of the BOF of the sheet on the respective BOUNDSHEET record } CurrentPos := AStream.Position; AStream.Position := Boundsheets[i]; AStream.WriteDWord(DWordToLE(DWORD(CurrentPos))); AStream.Position := CurrentPos; WriteBOF(AStream, INT_BOF_SHEET); WriteIndex(AStream); WriteDimensions(AStream, AData.GetWorksheetByIndex(i)); WriteWindow2(AStream, True); WriteCellsToStream(AStream, AData.GetWorksheetByIndex(i).Cells); WriteEOF(AStream); end; { Cleanup } SetLength(Boundsheets, 0); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteBOF () * * DESCRIPTION: Writes an Excel 8 BOF record * * This must be the first record on an Excel 8 stream * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteBOF(AStream: TStream; ADataType: Word); begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_BOF)); AStream.WriteWord(WordToLE(16)); { BIFF version. Should only be used if this BOF is for the workbook globals } { OpenOffice rejects to correctly read xls files if this field is omitted as docs. says, or even if it is being written to zero value, Not tested with Excel, but MSExcel reader opens it as expected } AStream.WriteWord(WordToLE(INT_BOF_BIFF8_VER)); { Data type } AStream.WriteWord(WordToLE(ADataType)); { Build identifier, must not be 0 } AStream.WriteWord(WordToLE(INT_BOF_BUILD_ID)); { Build year, must not be 0 } AStream.WriteWord(WordToLE(INT_BOF_BUILD_YEAR)); { File history flags } AStream.WriteDWord(DWordToLE(0)); { Lowest Excel version that can read all records in this file 5?} AStream.WriteDWord(DWordToLE(0)); //????????? end; {******************************************************************* * TsSpreadBIFF8Writer.WriteBoundsheet () * * DESCRIPTION: Writes an Excel 8 BOUNDSHEET record * * Always located on the workbook globals substream. * * One BOUNDSHEET is written for each worksheet. * * RETURNS: The stream position where the absolute stream position * of the BOF of this sheet should be written (4 bytes size). * *******************************************************************} function TsSpreadBIFF8Writer.WriteBoundsheet(AStream: TStream; ASheetName: string): Int64; var Len: Byte; WideSheetName: WideString; begin WideSheetName:=UTF8Decode(ASheetName); Len := Length(WideSheetName); { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_BOUNDSHEET)); AStream.WriteWord(WordToLE(6 + 1 + 1 + Len * Sizeof(WideChar))); { Absolute stream position of the BOF record of the sheet represented by this record } Result := AStream.Position; AStream.WriteDWord(DWordToLE(0)); { Visibility } AStream.WriteByte(0); { Sheet type } AStream.WriteByte(0); { Sheet name: Unicode string char count 1 byte } AStream.WriteByte(Len); {String flags} AStream.WriteByte(1); AStream.WriteBuffer(WideStringToLE(WideSheetName)[1], Len * Sizeof(WideChar)); end; { Writes an Excel 8 DIMENSIONS record nm = (rl - rf - 1) / 32 + 1 (using integer division) Excel, OpenOffice and FPSpreadsheet ignore the dimensions written in this record, but some other applications really use them, so they need to be correct. See bug 18886: excel5 files are truncated when imported } procedure TsSpreadBIFF8Writer.WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet); var lLastCol: Word; lLastRow: Integer; begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_DIMENSIONS)); AStream.WriteWord(WordToLE(14)); { Index to first used row } AStream.WriteDWord(DWordToLE(0)); { Index to last used row, increased by 1 } lLastRow := GetLastRowIndex(AWorksheet)+1; AStream.WriteDWord(DWordToLE(lLastRow)); // Old dummy value: 33 { Index to first used column } AStream.WriteWord(WordToLE(0)); { Index to last used column, increased by 1 } lLastCol := GetLastColIndex(AWorksheet)+1; AStream.WriteWord(WordToLE(lLastCol)); // Old dummy value: 10 { Not used } AStream.WriteWord(WordToLE(0)); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteEOF () * * DESCRIPTION: Writes an Excel 8 EOF record * * This must be the last record on an Excel 8 stream * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteEOF(AStream: TStream); begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_EOF)); AStream.WriteWord(WordToLE($0000)); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteFont () * * DESCRIPTION: Writes an Excel 8 FONT record * * The font data is passed in an instance of TFPCustomFont * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteFont(AStream: TStream; AFont: TFPCustomFont); var Len: Byte; WideFontName: WideString; begin WideFontName:=AFont.Name; Len := Length(WideFontName); { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_FONT)); AStream.WriteWord(WordToLE(14 + 1 + 1 + Len * Sizeof(WideChar))); { Height of the font in twips = 1/20 of a point } AStream.WriteWord(WordToLE(200)); { Option flags } if AFont.Bold then AStream.WriteWord(WordToLE(1)) else AStream.WriteWord(WordToLE(0)); { Colour index } AStream.WriteWord(WordToLE($7FFF)); { Font weight } if AFont.Bold then AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_BOLD)) else AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_NORMAL)); { Escapement type } AStream.WriteWord(WordToLE(0)); { Underline type } AStream.WriteByte(0); { Font family } AStream.WriteByte(0); { Character set } AStream.WriteByte(0); { Not used } AStream.WriteByte(0); { Font name: Unicodestring, char count in 1 byte } AStream.WriteByte(Len); { Widestring flags, 1=regular unicode LE string } AStream.WriteByte(1); AStream.WriteBuffer(WideStringToLE(WideFontName)[1], Len * Sizeof(WideChar)); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteFormula () * * DESCRIPTION: Writes an Excel 5 FORMULA record * * To input a formula to this method, first convert it * to RPN, and then list all it's members in the * AFormula array * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteFormula(AStream: TStream; const ARow, ACol: Word; const AFormula: TsFormula); {var FormulaResult: double; i: Integer; RPNLength: Word; TokenArraySizePos, RecordSizePos, FinalPos: Int64;} begin (* RPNLength := 0; FormulaResult := 0.0; { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMULA)); RecordSizePos := AStream.Position; AStream.WriteWord(WordToLE(22 + RPNLength)); { BIFF Record data } AStream.WriteWord(WordToLE(ARow)); AStream.WriteWord(WordToLE(ACol)); { Index to XF Record } AStream.WriteWord($0000); { Result of the formula in IEE 754 floating-point value } AStream.WriteBuffer(FormulaResult, 8); { Options flags } AStream.WriteWord(WordToLE(MASK_FORMULA_RECALCULATE_ALWAYS)); { Not used } AStream.WriteDWord(0); { Formula } { The size of the token array is written later, because it's necessary to calculate if first, and this is done at the same time it is written } TokenArraySizePos := AStream.Position; AStream.WriteWord(RPNLength); { Formula data (RPN token array) } for i := 0 to Length(AFormula) - 1 do begin { Token identifier } AStream.WriteByte(AFormula[i].TokenID); Inc(RPNLength); { Additional data } case AFormula[i].TokenID of { binary operation tokens } INT_EXCEL_TOKEN_TADD, INT_EXCEL_TOKEN_TSUB, INT_EXCEL_TOKEN_TMUL, INT_EXCEL_TOKEN_TDIV, INT_EXCEL_TOKEN_TPOWER: begin end; INT_EXCEL_TOKEN_TNUM: begin AStream.WriteBuffer(AFormula[i].DoubleValue, 8); Inc(RPNLength, 8); end; INT_EXCEL_TOKEN_TREFR, INT_EXCEL_TOKEN_TREFV, INT_EXCEL_TOKEN_TREFA: begin AStream.WriteWord(AFormula[i].Row and MASK_EXCEL_ROW); AStream.WriteByte(AFormula[i].Col); Inc(RPNLength, 3); end; end; end; { Write sizes in the end, after we known them } FinalPos := AStream.Position; AStream.position := TokenArraySizePos; AStream.WriteByte(RPNLength); AStream.Position := RecordSizePos; AStream.WriteWord(WordToLE(22 + RPNLength)); AStream.position := FinalPos;*) end; procedure TsSpreadBIFF8Writer.WriteRPNFormula(AStream: TStream; const ARow, ACol: Word; const AFormula: TsRPNFormula); var FormulaResult: double; i: Integer; RPNLength: Word; TokenArraySizePos, RecordSizePos, FinalPos: Int64; TokenID: Byte; begin RPNLength := 0; FormulaResult := 0.0; { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMULA)); RecordSizePos := AStream.Position; AStream.WriteWord(WordToLE(22 + RPNLength)); { BIFF Record data } AStream.WriteWord(WordToLE(ARow)); AStream.WriteWord(WordToLE(ACol)); { Index to XF Record } AStream.WriteWord($0000); { Result of the formula in IEE 754 floating-point value } AStream.WriteBuffer(FormulaResult, 8); { Options flags } AStream.WriteWord(WordToLE(MASK_FORMULA_RECALCULATE_ALWAYS)); { Not used } AStream.WriteDWord(0); { Formula } { The size of the token array is written later, because it's necessary to calculate if first, and this is done at the same time it is written } TokenArraySizePos := AStream.Position; AStream.WriteWord(RPNLength); { Formula data (RPN token array) } for i := 0 to Length(AFormula) - 1 do begin { Token identifier } TokenID := FormulaElementKindToExcelTokenID(AFormula[i].ElementKind); AStream.WriteByte(TokenID); Inc(RPNLength); { Additional data } case TokenID of { Operand Tokens } INT_EXCEL_TOKEN_TREFR, INT_EXCEL_TOKEN_TREFV, INT_EXCEL_TOKEN_TREFA: { fekCell } begin AStream.WriteWord(AFormula[i].Row and MASK_EXCEL_ROW); AStream.WriteByte(AFormula[i].Col); Inc(RPNLength, 3); end; INT_EXCEL_TOKEN_TAREA_R: { fekCellRange } begin { Cell range address, BIFF8: Offset Size Contents 0 2 Index to first row (0…65535) or offset of first row (method [B], -32768…32767) 2 2 Index to last row (0…65535) or offset of last row (method [B], -32768…32767) 4 2 Index to first column or offset of first column, with relative flags (see table above) 6 2 Index to last column or offset of last column, with relative flags (see table above) } AStream.WriteWord(WordToLE(AFormula[i].Row)); AStream.WriteWord(WordToLE(AFormula[i].Row2)); AStream.WriteWord(WordToLE(AFormula[i].Col)); AStream.WriteWord(WordToLE(AFormula[i].Col2)); Inc(RPNLength, 8); end; INT_EXCEL_TOKEN_TNUM: { fekNum } begin AStream.WriteBuffer(AFormula[i].DoubleValue, 8); Inc(RPNLength, 8); end; { binary operation tokens } INT_EXCEL_TOKEN_TADD, INT_EXCEL_TOKEN_TSUB, INT_EXCEL_TOKEN_TMUL, INT_EXCEL_TOKEN_TDIV, INT_EXCEL_TOKEN_TPOWER: begin end; { Other operations } INT_EXCEL_TOKEN_TATTR: { fekOpSUM } begin // Uniry SUM Operation AStream.WriteByte($10); AStream.WriteByte(0); AStream.WriteByte(0); Inc(RPNLength, 3); end; else end; end; { Write sizes in the end, after we known them } FinalPos := AStream.Position; AStream.position := TokenArraySizePos; AStream.WriteByte(RPNLength); AStream.Position := RecordSizePos; AStream.WriteWord(WordToLE(22 + RPNLength)); AStream.position := FinalPos; end; {******************************************************************* * TsSpreadBIFF8Writer.WriteIndex () * * DESCRIPTION: Writes an Excel 8 INDEX record * * nm = (rl - rf - 1) / 32 + 1 (using integer division) * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteIndex(AStream: TStream); begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_INDEX)); AStream.WriteWord(WordToLE(16)); { Not used } AStream.WriteDWord(DWordToLE(0)); { Index to first used row, rf, 0 based } AStream.WriteDWord(DWordToLE(0)); { Index to first row of unused tail of sheet, rl, last used row + 1, 0 based } AStream.WriteDWord(DWordToLE(0)); { Absolute stream position of the DEFCOLWIDTH record of the current sheet. If it doesn't exist, the offset points to where it would occur. } AStream.WriteDWord(DWordToLE($00)); { Array of nm absolute stream positions of the DBCELL record of each Row Block } { OBS: It seams to be no problem just ignoring this part of the record } end; {******************************************************************* * TsSpreadBIFF8Writer.WriteLabel () * * DESCRIPTION: Writes an Excel 8 LABEL record * * Writes a string to the sheet * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteLabel(AStream: TStream; const ARow, ACol: Word; const AValue: string; ACell: PCell); var L, RecLen: Word; WideValue: WideString; begin WideValue := UTF8Decode(AValue); if WideValue = '' then begin // Bad formatted UTF8String (maybe ANSI?) if Length(AValue)<>0 then begin //It was an ANSI string written as UTF8 quite sure, so raise exception. Raise Exception.CreateFmt('Expected UTF8 text but probably ANSI text found in cell [%d,%d]',[ARow,ACol]); end; Exit; end; L := Length(WideValue); { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_LABEL)); RecLen := 8 + 1 + L * Sizeof(WideChar); AStream.WriteWord(WordToLE(RecLen)); { BIFF Record data } AStream.WriteWord(WordToLE(ARow)); AStream.WriteWord(WordToLE(ACol)); { Index to XF record, according to formatting } WriteXFIndex(AStream, ACell); { Byte String with 16-bit size } AStream.WriteWord(WordToLE(L)); { Byte flags. 1 means regular Unicode LE encoding} AStream.WriteByte(1); AStream.WriteBuffer(WideStringToLE(WideValue)[1], L * Sizeof(WideChar)); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteNumber () * * DESCRIPTION: Writes an Excel 8 NUMBER record * * Writes a number (64-bit floating point) to the sheet * *******************************************************************} procedure TsSpreadBIFF8Writer.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 } AStream.WriteWord(WordToLE($0)); { IEE 754 floating-point value (is different in BIGENDIAN???) } AStream.WriteBuffer(AValue, 8); end; procedure TsSpreadBIFF8Writer.WritePalette(AStream: TStream); begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_PALETTE)); AStream.WriteWord(WordToLE(2+4*56)); { Number of colors } AStream.WriteWord(WordToLE(56)); { Now the colors, first the standard 16 from Excel } AStream.WriteDWord(DWordToLE($000000)); // $08 AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FF0000)); AStream.WriteDWord(DWordToLE($00FF00)); AStream.WriteDWord(DWordToLE($0000FF)); AStream.WriteDWord(DWordToLE($FFFF00)); AStream.WriteDWord(DWordToLE($FF00FF)); AStream.WriteDWord(DWordToLE($00FFFF)); AStream.WriteDWord(DWordToLE($800000)); AStream.WriteDWord(DWordToLE($008000)); AStream.WriteDWord(DWordToLE($000080)); AStream.WriteDWord(DWordToLE($808000)); AStream.WriteDWord(DWordToLE($800080)); AStream.WriteDWord(DWordToLE($008080)); AStream.WriteDWord(DWordToLE($C0C0C0)); AStream.WriteDWord(DWordToLE($808080)); //$17 { Now some colors which we define ourselves } AStream.WriteDWord(DWordToLE($E6E6E6)); //$18 AStream.WriteDWord(DWordToLE($CCCCCC)); //$19 { And padding } AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); //$20 AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); //$30 AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFF)); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteStyle () * * DESCRIPTION: Writes an Excel 8 STYLE record * * Registers the name of a user-defined style or * specific options for a built-in cell style. * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteStyle(AStream: TStream); begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_STYLE)); AStream.WriteWord(WordToLE(4)); { Index to style XF and defines if it's a built-in or used defined style } AStream.WriteWord(WordToLE(MASK_STYLE_BUILT_IN)); { Built-in cell style identifier } AStream.WriteByte($00); { Level if the identifier for a built-in style is RowLevel or ColLevel, $FF otherwise } AStream.WriteByte($FF); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteWindow1 () * * DESCRIPTION: Writes an Excel 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. * *******************************************************************} procedure TsSpreadBIFF8Writer.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; {******************************************************************* * TsSpreadBIFF8Writer.WriteWindow2 () * * DESCRIPTION: Writes an Excel 8 WINDOW2 record * * This record contains aditional settings for the * document window (BIFF2-BIFF4) or for a specific * worksheet (BIFF5-BIFF8). * * The values written here are reasonable defaults, * which should work for most sheets. * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteWindow2(AStream: TStream; ASheetSelected: Boolean); var Options: Word; begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_WINDOW2)); AStream.WriteWord(WordToLE(18)); { Options flags } Options := MASK_WINDOW2_OPTION_SHOW_GRID_LINES or MASK_WINDOW2_OPTION_SHOW_SHEET_HEADERS or MASK_WINDOW2_OPTION_SHOW_ZERO_VALUES or MASK_WINDOW2_OPTION_AUTO_GRIDLINE_COLOR or MASK_WINDOW2_OPTION_SHOW_OUTLINE_SYMBOLS or MASK_WINDOW2_OPTION_SHEET_ACTIVE; if ASheetSelected then Options := Options or MASK_WINDOW2_OPTION_SHEET_SELECTED; AStream.WriteWord(WordToLE(Options)); { Index to first visible row } AStream.WriteWord(WordToLE(0)); { Index to first visible column } AStream.WriteWord(WordToLE(0)); { Grid line index colour } AStream.WriteWord(WordToLE(0)); { Not used } AStream.WriteWord(WordToLE(0)); { Cached magnification factor in page break preview (in percent); 0 = Default (60%) } AStream.WriteWord(WordToLE(0)); { Cached magnification factor in normal view (in percent); 0 = Default (100%) } AStream.WriteWord(WordToLE(0)); { Not used } AStream.WriteDWord(DWordToLE(0)); end; {******************************************************************* * TsSpreadBIFF8Writer.WriteXF () * * DESCRIPTION: Writes an Excel 8 XF record * * * *******************************************************************} procedure TsSpreadBIFF8Writer.WriteXF(AStream: TStream; AFontIndex: Word; AXF_TYPE_PROT, ATextRotation: Byte; ABorders: TsCellBorders; AddBackground: Boolean = False; ABackgroundColor: TsColor = scSilver); var XFOptions: Word; XFAlignment, XFOrientationAttrib: Byte; XFBorderDWord1, XFBorderDWord2: DWord; begin { BIFF Record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_XF)); AStream.WriteWord(WordToLE(20)); { Index to FONT record } AStream.WriteWord(WordToLE(AFontIndex)); { Index to FORMAT record } AStream.WriteWord(WordToLE($00)); { XF type, cell protection and parent style XF } XFOptions := AXF_TYPE_PROT and MASK_XF_TYPE_PROT; if AXF_TYPE_PROT and MASK_XF_TYPE_PROT_STYLE_XF <> 0 then XFOptions := XFOptions or MASK_XF_TYPE_PROT_PARENT; AStream.WriteWord(WordToLE(XFOptions)); { Alignment and text break } XFAlignment := MASK_XF_VERT_ALIGN_BOTTOM; AStream.WriteByte(XFAlignment); { Text rotation } AStream.WriteByte(ATextRotation); // 0 is horizontal / normal { Indentation, shrink and text direction } AStream.WriteByte(0); { Used attributes } XFOrientationAttrib := MASK_XF_USED_ATTRIB_NUMBER_FORMAT or MASK_XF_USED_ATTRIB_FONT or MASK_XF_USED_ATTRIB_TEXT or MASK_XF_USED_ATTRIB_BORDER_LINES or MASK_XF_USED_ATTRIB_BACKGROUND or MASK_XF_USED_ATTRIB_CELL_PROTECTION; AStream.WriteByte(XFOrientationAttrib); { Cell border lines and background area } // Left and Right line colors, use black XFBorderDWord1 := 8 * $10000 {left line - black} + 8 * $800000 {right line - black}; if cbNorth in ABorders then XFBorderDWord1 := XFBorderDWord1 or $100; if cbWest in ABorders then XFBorderDWord1 := XFBorderDWord1 or $1; if cbEast in ABorders then XFBorderDWord1 := XFBorderDWord1 or $10; if cbSouth in ABorders then XFBorderDWord1 := XFBorderDWord1 or $1000; AStream.WriteDWord(DWordToLE(XFBorderDWord1)); // Top and Bottom line colors, use black XFBorderDWord2 := 8 {top line - black} + 8 * $80 {bottom line - black}; // Add a background, if desired if AddBackground then XFBorderDWord2 := XFBorderDWord2 or $4000000; AStream.WriteDWord(DWordToLE(XFBorderDWord2)); // Background Pattern Color, always zeroed if AddBackground then AStream.WriteWord(WordToLE(FPSColorToEXCELPallete(ABackgroundColor))) else AStream.WriteWord(0); end; { TsSpreadBIFF8Reader } function TsSpreadBIFF8Reader.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; function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; const ALength: WORD): WideString; var StringFlags: BYTE; DecomprStrValue: WideString; AnsiStrValue: ansistring; RunsCounter: WORD; AsianPhoneticBytes: DWORD; i: Integer; j: SizeUInt; lLen: SizeInt; begin StringFlags:=AStream.ReadByte; Dec(PendingRecordSize); if StringFlags and 4 = 4 then begin //Asian phonetics //Read Asian phonetics Length (not used) AsianPhoneticBytes:=DWordLEtoN(AStream.ReadDWord); end; if StringFlags and 8 = 8 then begin //Rich string RunsCounter:=WordLEtoN(AStream.ReadWord); dec(PendingRecordSize,2); end; if StringFlags and 1 = 1 Then begin //String is WideStringLE if (ALength*SizeOf(WideChar)) > PendingRecordSize then begin SetLength(Result,PendingRecordSize); AStream.ReadBuffer(Result[1],PendingRecordSize * SizeOf(WideChar)); Dec(PendingRecordSize,PendingRecordSize * SizeOf(WideChar)); end else begin SetLength(Result,ALength); AStream.ReadBuffer(Result[1],ALength * SizeOf(WideChar)); Dec(PendingRecordSize,ALength * SizeOf(WideChar)); end; Result:=WideStringLEToN(Result); end else begin //String is 1 byte per char, this is UTF-16 with the high byte ommited because it is zero // so decompress and then convert if ALength > PendingRecordSize then lLen := PendingRecordSize else lLen := ALength; SetLength(DecomprStrValue, lLen); for i := 1 to lLen do begin DecomprStrValue[i] := WideChar(AStream.ReadByte()); end; Dec(PendingRecordSize, lLen); Result := DecomprStrValue; end; if StringFlags and 8 = 8 then begin //Rich string (This only happend in BIFF8) for j := 1 to RunsCounter do begin AStream.ReadWord; AStream.ReadWord; dec(PendingRecordSize,2*2); end; end; if StringFlags and 4 = 4 then begin //Asian phonetics //Read Asian phonetics, discarded as not used. SetLength(AnsiStrValue,AsianPhoneticBytes); AStream.ReadBuffer(AnsiStrValue[1],AsianPhoneticBytes); end; end; function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; var Len: Word; WideName: WideString; begin if AUse8BitLength then Len := AStream.ReadByte() else Len := WordLEtoN(AStream.ReadWord()); Result := ReadWideString(AStream, Len); end; procedure TsSpreadBIFF8Reader.ReadWorkbookGlobals(AStream: TStream; AData: TsWorkbook); var SectionEOF: Boolean = False; RecordType: Word; CurStreamPos: Int64; begin if Assigned(FSharedStringTable) then FreeAndNIL(FSharedStringTable); while (not SectionEOF) do begin { Read the record header } RecordType := WordLEToN(AStream.ReadWord); RecordSize := WordLEToN(AStream.ReadWord); PendingRecordSize:=RecordSize; CurStreamPos := AStream.Position; if RecordType<>INT_EXCEL_ID_CONTINUE then begin case RecordType of INT_EXCEL_ID_BOF: ; INT_EXCEL_ID_BOUNDSHEET: ReadBoundSheet(AStream); INT_EXCEL_ID_EOF: SectionEOF := True; INT_EXCEL_ID_SST: ReadSST(AStream); INT_EXCEL_ID_CODEPAGE: ReadCodepage(AStream); INT_EXCEL_ID_FONT: ReadFont(AStream); INT_EXCEL_ID_XF: ReadXF(AStream); INT_EXCEL_ID_FORMAT: ReadFormat(AStream); INT_EXCEL_ID_DATEMODE: ReadDateMode(AStream); else // nothing end; end; // Make sure we are in the right position for the next record AStream.Seek(CurStreamPos + RecordSize, soFromBeginning); // Check for the end of the file if AStream.Position >= AStream.Size then SectionEOF := True; end; end; procedure TsSpreadBIFF8Reader.ReadWorksheet(AStream: TStream; AData: TsWorkbook); var SectionEOF: Boolean = False; RecordType: Word; CurStreamPos: Int64; begin FWorksheet := AData.AddWorksheet(FWorksheetNames.Strings[FCurrentWorksheet]); while (not SectionEOF) do begin { Read the record header } RecordType := WordLEToN(AStream.ReadWord); RecordSize := WordLEToN(AStream.ReadWord); PendingRecordSize:=RecordSize; CurStreamPos := AStream.Position; case RecordType of INT_EXCEL_ID_NUMBER: ReadNumber(AStream); INT_EXCEL_ID_LABEL: ReadLabel(AStream); // INT_EXCEL_ID_FORMULA: ReadFormula(AStream); INT_EXCEL_ID_RSTRING: ReadRichString(AStream); //(RSTRING) This record stores a formatted text cell (Rich-Text). In BIFF8 it is usually replaced by the LABELSST record. Excel still uses this record, if it copies formatted text cells to the clipboard. INT_EXCEL_ID_RK: ReadRKValue(AStream); //(RK) This record represents a cell that contains an RK value (encoded integer or floating-point value). If a floating-point value cannot be encoded to an RK value, a NUMBER record will be written. This record replaces the record INTEGER written in BIFF2. INT_EXCEL_ID_MULRK: ReadMulRKValues(AStream); INT_EXCEL_ID_LABELSST:ReadLabelSST(AStream); //BIFF8 only INT_EXCEL_ID_FORMULA: ReadFormulaExcel(AStream); INT_EXCEL_ID_BOF: ; INT_EXCEL_ID_EOF: SectionEOF := True; else // nothing end; // Make sure we are in the right position for the next record AStream.Seek(CurStreamPos + RecordSize, soFromBeginning); // Check for the end of the file if AStream.Position >= AStream.Size then SectionEOF := True; end; end; procedure TsSpreadBIFF8Reader.ReadBoundsheet(AStream: TStream); var Len: Byte; WideName: WideString; begin { Absolute stream position of the BOF record of the sheet represented by this record } // Just assume that they are in order AStream.ReadDWord(); { Visibility } AStream.ReadByte(); { Sheet type } AStream.ReadByte(); { Sheet name: 8-bit length } Len := AStream.ReadByte(); { Read string with flags } WideName:=ReadWideString(AStream,Len); FWorksheetNames.Add(UTF8Encode(WideName)); end; procedure TsSpreadBIFF8Reader.ReadRKValue(const AStream: TStream); var RK: DWORD; ARow, ACol, XF: WORD; Number: Double; lFormatData: TFormatRecordData; lDateTime: TDateTime; begin ReadRowColXF(AStream,ARow,ACol,XF); {Encoded RK value} RK:=DWordLEtoN(AStream.ReadDWord); {Check RK codes} Number:=DecodeRKValue(RK); // Now try to figure out if the number is really a number of a date or time value // See: http://www.gaia-gis.it/FreeXL/freexl-1.0.0a-doxy-doc/Format.html // Unfornately Excel doesnt give us a direct way to find this, // we need to guess by the FORMAT field lFormatData := FindFormatRecordForCell(XF); if lFormatData <> nil then begin // Dates have / if Pos('/', lFormatData.FormatString) > 0 then begin lDateTime := ConvertExcelDateToTDateTime(Number, FBaseDate); FWorksheet.WriteDateTime(ARow,ACol,lDateTime); Exit; end; end; FWorksheet.WriteNumber(ARow,ACol,Number); end; procedure TsSpreadBIFF8Reader.ReadMulRKValues(const AStream: TStream); var ARow, fc,lc,XF: Word; Pending: integer; RK: DWORD; Number: Double; 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 (not used) RK:=DWordLEtoN(AStream.ReadDWord); Number:=DecodeRKValue(RK); FWorksheet.WriteNumber(ARow,fc,Number); 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; procedure TsSpreadBIFF8Reader.ReadFormulaExcel(AStream: TStream); var ARow, ACol, XF: WORD; ResultFormula: Double; Data: array [0..7] of BYTE; Flags: WORD; FormulaSize: BYTE; i: Integer; begin { BIFF Record header } { BIFF Record data } { Index to XF Record } ReadRowColXF(AStream,ARow,ACol,XF); { Result of the formula in IEE 754 floating-point value } AStream.ReadBuffer(Data,Sizeof(Data)); { Options flags } Flags:=WordLEtoN(AStream.ReadWord); { Not used } AStream.ReadDWord; { Formula size } FormulaSize := WordLEtoN(AStream.ReadWord); { Formula data, outputed as debug info } { Write('Formula Element: '); for i := 1 to FormulaSize do Write(IntToHex(AStream.ReadByte, 2) + ' '); WriteLn('');} //RPN data not used by now AStream.Position:=AStream.Position+FormulaSize; if SizeOf(Double)<>8 then Raise Exception.Create('Double is not 8 bytes'); Move(Data[0],ResultFormula,sizeof(Data)); FWorksheet.WriteNumber(ARow,ACol,ResultFormula); end; procedure TsSpreadBIFF8Reader.ReadRowColXF(const AStream: TStream; out ARow, ACol, AXF: WORD); begin { BIFF Record data } ARow := WordLEToN(AStream.ReadWord); ACol := WordLEToN(AStream.ReadWord); { Index to XF record } AXF:=WordLEtoN(AStream.ReadWord); end; function TsSpreadBIFF8Reader.ReadString(const AStream: TStream; const ALength: WORD): UTF8String; begin Result:=UTF16ToUTF8(ReadWideString(AStream, ALength)); end; constructor TsSpreadBIFF8Reader.Create; begin inherited Create; FXFList := TFPList.Create; FFormatList := TFPList.Create; end; destructor TsSpreadBIFF8Reader.Destroy; var j: integer; begin for j := FXFList.Count-1 downto 0 do TObject(FXFList[j]).Free; for j := FFormatList.Count-1 downto 0 do TObject(FFormatList[j]).Free; FXFList.Free; FFormatList.Free; if Assigned(FSharedStringTable) then FSharedStringTable.Free; end; procedure TsSpreadBIFF8Reader.ReadFromFile(AFileName: string; AData: TsWorkbook); var MemStream: TMemoryStream; OLEStorage: TOLEStorage; OLEDocument: TOLEDocument; begin MemStream := TMemoryStream.Create; OLEStorage := TOLEStorage.Create; try // Only one stream is necessary for any number of worksheets OLEDocument.Stream := MemStream; OLEStorage.ReadOLEFile(AFileName, OLEDocument,'Workbook'); // Check if the operation succeded if MemStream.Size = 0 then raise Exception.Create('FPSpreadsheet: Reading the OLE document failed'); // Rewind the stream and read from it MemStream.Position := 0; ReadFromStream(MemStream, AData); // Uncomment to verify if the data was correctly optained from the OLE file // MemStream.SaveToFile(SysUtils.ChangeFileExt(AFileName, 'bin.xls')); finally MemStream.Free; OLEStorage.Free; end; end; procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream; AData: TsWorkbook); var BIFF8EOF: Boolean; begin { Initializations } FWorksheetNames := TStringList.Create; FWorksheetNames.Clear; FCurrentWorksheet := 0; BIFF8EOF := False; { Read workbook globals } ReadWorkbookGlobals(AStream, AData); // Check for the end of the file if AStream.Position >= AStream.Size then BIFF8EOF := True; { Now read all worksheets } while (not BIFF8EOF) do begin //Safe to not read beyond assigned worksheet names. if FCurrentWorksheet>FWorksheetNames.Count-1 then break; ReadWorksheet(AStream, AData); // Check for the end of the file if AStream.Position >= AStream.Size then BIFF8EOF := True; // Final preparations Inc(FCurrentWorksheet); end; { Finalizations } FWorksheetNames.Free; end; procedure TsSpreadBIFF8Reader.ReadFormula(AStream: TStream); begin end; procedure TsSpreadBIFF8Reader.ReadLabel(AStream: TStream); var L: Word; StringFlags: BYTE; ARow, ACol: Word; WideStrValue: WideString; AnsiStrValue: AnsiString; begin { BIFF Record data } ARow := WordLEToN(AStream.ReadWord); ACol := WordLEToN(AStream.ReadWord); { Index to XF record, not used } AStream.ReadWord(); { Byte String with 16-bit size } L := WordLEtoN(AStream.ReadWord()); { Read string with flags } WideStrValue:=ReadWideString(AStream,L); { Save the data } FWorksheet.WriteUTF8Text(ARow, ACol, UTF16ToUTF8(WideStrValue)); end; procedure TsSpreadBIFF8Reader.ReadNumber(AStream: TStream); var ARow, ACol: Word; AValue: Double; begin { BIFF Record data } ARow := WordLEToN(AStream.ReadWord); ACol := WordLEToN(AStream.ReadWord); { Index to XF record, not used } AStream.ReadWord(); { IEE 754 floating-point value } AStream.ReadBuffer(AValue, 8); { Save the data } FWorksheet.WriteNumber(ARow, ACol, AValue); end; procedure TsSpreadBIFF8Reader.ReadRichString(const AStream: TStream); var L: Word; B: WORD; ARow, ACol, XF: Word; AStrValue: ansistring; begin ReadRowColXF(AStream,ARow,ACol,XF); { Byte String with 16-bit size } L := WordLEtoN(AStream.ReadWord()); AStrValue:=ReadString(AStream,L); { Save the data } FWorksheet.WriteUTF8Text(ARow, ACol, AStrValue); //Read formatting runs (not supported) B:=WordLEtoN(AStream.ReadWord); for L := 0 to B-1 do begin AStream.ReadWord; // First formatted character AStream.ReadWord; // Index to FONT record end; end; procedure TsSpreadBIFF8Reader.ReadSST(const AStream: TStream); var Items: DWORD; StringLength: WORD; LString: String; ContinueIndicator: WORD; begin //Reads the shared string table, only compatible with BIFF8 if not Assigned(FSharedStringTable) then begin //First time SST creation FSharedStringTable:=TStringList.Create; DWordLEtoN(AStream.ReadDWord); //Apparences not used Items:=DWordLEtoN(AStream.ReadDWord); Dec(PendingRecordSize,8); end else begin //A second record must not happend. Garbage so skip. Exit; end; while Items>0 do begin StringLength:=0; StringLength:=WordLEtoN(AStream.ReadWord); Dec(PendingRecordSize,2); LString:=''; while PendingRecordSize>0 do begin if StringLength>0 then begin //Read a stream of zero length reads all the stream. LString:=LString+ReadString(AStream,StringLength); end else begin //String of 0 chars in length, so just read it empty, reading only the mandatory flags AStream.ReadByte; //And discard it. Dec(PendingRecordSize); //LString:=LString+''; end; if (PendingRecordSize=0) and (Items>1) then begin //A Continue will happend, read the //tag and continue linking... ContinueIndicator:=WordLEtoN(AStream.ReadWord); if ContinueIndicator<>INT_EXCEL_ID_CONTINUE then begin Raise Exception.Create('Expected CONTINUE not found.'); end; PendingRecordSize:=WordLEtoN(AStream.ReadWord); Dec(StringLength,Length(UTF8ToUTF16(LString))); //Dec the used chars if StringLength=0 then break; end else begin break; end; end; FSharedStringTable.Add(LString); {$ifdef XLSDEBUG} WriteLn('Adding shared string: ' + LString); {$endif} dec(Items); end; end; procedure TsSpreadBIFF8Reader.ReadLabelSST(const AStream: TStream); var ACol,ARow,XF: WORD; SSTIndex: DWORD; begin ReadRowColXF(AStream,ARow,ACol,XF); SSTIndex:=DWordLEtoN(AStream.ReadDWord); if SizeInt(SSTIndex)>=FSharedStringTable.Count then begin Raise Exception.CreateFmt('Index %d in SST out of range (0-%d)',[Integer(SSTIndex),FSharedStringTable.Count-1]); end; FWorksheet.WriteUTF8Text(ARow, ACol, FSharedStringTable[SSTIndex]); end; procedure TsSpreadBIFF8Reader.ReadXF(const AStream: TStream); var lData: TXFRecordData; begin lData := TXFRecordData.Create; // Record XF, BIFF8: // Offset Size Contents // 0 2 Index to FONT record (➜5.45) WordLEtoN(AStream.ReadWord); // 2 2 Index to FORMAT record (➜5.49) lData.FormatIndex := WordLEtoN(AStream.ReadWord); {4 2 XF type, cell protection, and parent style XF: Bit Mask Contents 2-0 0007H XF_TYPE_PROT – XF type, cell protection (see above) 15-4 FFF0H Index to parent style XF (always FFFH in style XFs) 6 1 Alignment and text break: Bit Mask Contents 2-0 07H XF_HOR_ALIGN – Horizontal alignment (see above) 3 08H 1 = Text is wrapped at right border 6-4 70H XF_VERT_ALIGN – Vertical alignment (see above) 7 80H 1 = Justify last line in justified or distibuted text 7 1 XF_ROTATION: Text rotation angle (see above) 8 1 Indentation, shrink to cell size, and text direction: Bit Mask Contents 3-0 0FH Indent level 4 10H 1 = Shrink content to fit into cell 7-6 C0H Text direction: 0 = According to context 35 ; 1 = Left-to-right; 2 = Right-to-left 9 1 Flags for used attribute groups: ....} // Add the XF to the list FXFList.Add(lData); end; procedure TsSpreadBIFF8Reader.ReadFormat(const AStream: TStream); var lData: TFormatRecordData; begin lData := TFormatRecordData.Create; // Record FORMAT, BIFF8: // Offset Size Contents // 0 2 Format index used in other records lData.Index := WordLEtoN(AStream.ReadWord); // 2 var. Number format string (Unicode string, 16-bit string length, ➜2.5.3) lData.FormatString := ReadWideString(AStream, False); // Add to the list FFormatList.Add(lData); end; function TsSpreadBIFF8Reader.FindFormatRecordForCell(const AFXIndex: Integer ): TFormatRecordData; var lXFData: TXFRecordData; lFormatData: TFormatRecordData; i: Integer; begin Result := nil; lXFData := TXFRecordData(FXFList.Items[AFXIndex]); for i := 0 to FFormatList.Count-1 do begin lFormatData := TFormatRecordData(FFormatList.Items[i]); if lFormatData.Index = lXFData.FormatIndex then Exit(lFormatData); end; end; class function TsSpreadBIFF8Reader.ConvertExcelDateToTDateTime( const AExcelDateNum: Double; ABaseDate: TDateTime): TDateTime; begin Result := IncDay(ABaseDate, Round(AExcelDateNum)); end; procedure TsSpreadBIFF8Reader.ReadFont(const AStream: TStream); var lCodePage: Word; lHeight: Word; lOptions: Word; Len: Byte; lFontName: UTF8String; begin { Height of the font in twips = 1/20 of a point } lHeight := AStream.ReadWord(); // WordToLE(200) { Option flags } lOptions := AStream.ReadWord(); { Colour index } AStream.ReadWord(); { Font weight } AStream.ReadWord(); { Escapement type } AStream.ReadWord(); { Underline type } AStream.ReadByte(); { Font family } AStream.ReadByte(); { Character set } lCodepage := AStream.ReadByte(); {$ifdef XLSDEBUG} WriteLn('Reading Font Codepage='+IntToStr(lCodepage)); {$endif} { Not used } AStream.ReadByte(); { Font name: Unicodestring, char count in 1 byte } Len := AStream.ReadByte(); lFontName := ReadString(AStream, Len); end; {******************************************************************* * Initialization section * * Registers this reader / writer on fpSpreadsheet * *******************************************************************} initialization RegisterSpreadFormat(TsSpreadBIFF8Reader, TsSpreadBIFF8Writer, sfExcel8); end.