{ 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 see also: http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP005199291.aspx AUTHORS: Felipe Monteiro de Carvalho Jose Mejuto } unit xlsbiff8; {$ifdef fpc} {$mode objfpc}{$H+} {$endif} {$I fps.inc} // The new OLE code is much better, so always use it {$define USE_NEW_OLE} interface uses Classes, SysUtils, fpcanvas, DateUtils, contnrs, lazutf8, {$IFDEF FPS_NEED_STRINGHASHLIST}fpsstringhashlist,{$ELSE}stringhashlist,{$ENDIF} fpstypes, xlscommon, {$IFDEF USE_NEW_OLE}fpolebasic,{$ELSE}fpolestorage,{$ENDIF} fpsutils; type { TsBiff8ExternSheet - Information on sheets used in out-of-sheet references } TsBIFF8ExternSheet = packed record ExternBookIndex: Word; FirstSheetIndex: Word; LastSheetIndex: Word; end; PsBIFF8ExternSheet = ^TsBIFF8ExternSheet; { TsBIFF8ExternBook - Information on where out-of-sheet references are stored. } TsBIFF8ExternBook = class Kind: TsBIFFExternKind; // The following fields are used only for external workbooks. DocumentURL: String; SheetNames: String; // List of worksheetnames separated by #1 function GetWorksheetName(AIndex: Integer): String; end; { TsBIFF8ExternBookList } TsBIFF8ExternBookList = class(TFPObjectlist) private function GetItem(AIndex: Integer): TsBIFF8ExternBook; procedure SetItem(AIndex: Integer; AValue: TsBIFF8ExternBook); public function AddBook(ABookName: String; ASheetNames: TStrings): Integer; function AddInternal: Integer; function FindBook(ABookName: String): TsBIFF8ExternBook; function FindInternalBook: TsBIFF8ExternBook; function IndexOfBook(ABookName: String): Integer; function IndexOfInternalbook: Integer; property Items[AIndex: Integer]: TsBIFF8ExternBook read GetItem write SetItem; default; end; { A list for sheets used in out-of-sheet references } TsBIFF8ExternSheetList = class(TFPList) private FBookList: TsBIFF8ExternBookList; function GetItem(AIndex: Integer): PsBIFF8ExternSheet; procedure SetItem(AIndex: Integer; AValue: PsBIFF8ExternSheet); public constructor Create(ABookList: TsBIFF8ExternBookList); destructor Destroy; override; function AddInternalSheets(ASheetIndex1, ASheetIndex2: Integer): Integer; function AddSheets(ABookName: String; ASheetNames: TStrings; ASheetIndex1, ASheetIndex2: Integer): Integer; procedure Clear; function IndexOfSheets(ABookName: String; ASheetIndex1, ASheetIndex2: Integer): Integer; property Item[AIndex: Integer]: PsBIFF8ExternSheet read GetItem write SetItem; default; end; { TsSpreadBIFF8Reader } TsSpreadBIFF8Reader = class(TsSpreadBIFFReader) private PendingRecordSize: SizeInt; FSharedStringTable: TStringList; FCommentList: TObjectList; FCommentPending: Boolean; FCommentID: Integer; FCommentLen: Integer; FBiff8ExternBooks: TsBiff8ExternBookList; FBiff8ExternSheetArray: array of TsBiff8ExternSheet; procedure FreeSharedStringTable; function ReadString(const AStream: TStream; const ALength: Word; out ARichTextParams: TsRichTextParams): String; function ReadUnformattedWideString(const AStream: TStream; const ALength: Word): WideString; function ReadWideString(const AStream: TStream; const ALength: Word; out ARichTextParams: TsRichTextParams): WideString; overload; function ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; overload; protected class function CheckFileFormatDetails(AStream: TStream): Boolean; override; procedure PopulatePalette; override; procedure ReadBOUNDSHEET(AStream: TStream); procedure ReadCONTINUE(const AStream: TStream); procedure ReadDEFINEDNAME(const AStream: TStream); procedure ReadEXTERNBOOK(const AStream: TStream); procedure ReadEXTERNSHEET(const AStream: TStream); procedure ReadFONT(const AStream: TStream); procedure ReadFORMAT(AStream: TStream); override; procedure ReadHeaderFooter(AStream: TStream; AIsHeader: Boolean); override; procedure ReadHyperLink(const AStream: TStream); procedure ReadHyperlinkToolTip(const AStream: TStream); procedure ReadLABEL(AStream: TStream); override; procedure ReadLabelSST(const AStream: TStream); procedure ReadMergedCells(const AStream: TStream); procedure ReadNOTE(const AStream: TStream); procedure ReadOBJ(const AStream: TStream); // procedure ReadRichString(const AStream: TStream); procedure ReadRPNCellAddress(AStream: TStream; out ARow, ACol: Cardinal; out AFlags: TsRelFlags); override; procedure ReadRPNCellAddressOffset(AStream: TStream; out ARowOffset, AColOffset: Integer; out AFlags: TsRelFlags); override; procedure ReadRPNCellRangeAddress(AStream: TStream; out ARow1, ACol1, ARow2, ACol2: Cardinal; out AFlags: TsRelFlags); override; procedure ReadRPNCellRangeAddressOffset(AStream: TStream; out ARowOffset1, AColOffset1, ARowOffset2, AColOffset2: Integer; out AFlags: TsRelFlags); override; // function ReadRPNCellRange3D(AStream: TStream; var ARPNItem: PRPNItem): Boolean; override; procedure ReadRPNCellRangeOffset(AStream: TStream; out ARow1Offset, ACol1Offset, ARow2Offset, ACol2Offset: Integer; out AFlags: TsRelFlags); override; procedure ReadRPNSheetIndex(AStream: TStream; out ADocumentURL: String; out ASheet1, ASheet2: Integer); override; procedure ReadRSTRING(AStream: TStream); procedure ReadSheetLayout(const AStream: TStream); procedure ReadSST(const AStream: TStream); function ReadString_8bitLen(AStream: TStream): String; override; procedure ReadStringRecord(AStream: TStream); override; procedure ReadTXO(const AStream: TStream); procedure ReadXF(const AStream: TStream); protected procedure ReadWorkbookGlobals(AStream: TStream); override; procedure ReadWorksheet(AStream: TStream); override; public constructor Create(AWorkbook: TsBasicWorkbook); override; destructor Destroy; override; procedure ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); override; end; { TsSpreadBIFF8Writer } TsSpreadBIFF8Writer = class(TsSpreadBIFFWriter) private FSharedStringTable: TStringHashList; FNumStrings: DWord; FBiff8ExternBooks: TsBIFF8ExternbookList; FBiff8ExternSheets: TsBIFF8ExternSheetList; private procedure BeginCONTINUERecord(AStream: TStream; out ASizePos: Int64); procedure CollectExternData; procedure FixRecordSize(AStream: TStream; ASizePos: Int64; ASize: Word); function WriteStringHelper(AStream: TStream; const AText: RawByteString; const ARichTextParams: TsRichTextParams; Is8BitString: Boolean; ABytesAvail: Integer; var ATextIndex, ARichIndex: Integer; out AComplete: Boolean): Integer; protected function GetPrintOptions: Word; override; function IndexOfSharedString(const AText: String; const ARichTextParams: TsRichTextParams): Integer; procedure InternalWriteToStream(AStream: TStream); procedure PopulatePalette(AWorkbook: TsBasicWorkbook); override; procedure PopulateSharedStringTable(AWorkbook: TsBasicWorkbook); { Record writing methods } procedure WriteBOF(AStream: TStream; ADataType: Word); function WriteBoundsheet(AStream: TStream; AWorksheet: TsBasicWorksheet): Int64; procedure WriteComment(AStream: TStream; ACell: PCell); override; procedure WriteComments(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteConditionalFormatting(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteDefinedName(AStream: TStream; AWorksheet: TsBasicWorksheet; const AName: String; AIndexToREF, ASheetIndex: Word; AKind: TsBIFFExternKind); reintroduce; procedure WriteDefinedNames(AStream: TStream); procedure WriteDimensions(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteEOF(AStream: TStream); procedure WriteEXTERNBOOK(AStream: TStream; AUrl: String); procedure WriteEXTERNSHEET(AStream: TStream); procedure WriteFONT(AStream: TStream; AFont: TsFont); procedure WriteFonts(AStream: TStream); procedure WriteFORMAT(AStream: TStream; ANumFormatStr: String; ANumFormatIndex: Integer); override; procedure WriteHeaderFooter(AStream: TStream; AIsHeader: Boolean); override; procedure WriteHyperlink(AStream: TStream; AHyperlink: PsHyperlink; AWorksheet: TsBasicWorksheet); procedure WriteHyperlinks(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteHyperlinkToolTip(AStream: TStream; const ARow, ACol: Cardinal; const ATooltip: String); procedure WriteINDEX(AStream: TStream); procedure WriteLABEL(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); override; procedure WriteMergedCells(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteMSODrawing1(AStream: TStream; ANumShapes: Word; AComment: PsComment); procedure WriteMSODrawing2(AStream: TStream; AComment: PsComment; AObjID: Word); procedure WriteMSODrawing2_Data(AStream: TStream; AComment: PsComment; AShapeID: Word); procedure WriteMSODrawing3(AStream: TStream); procedure WriteNOTE(AStream: TStream; AComment: PsComment; AObjID: Word); procedure WriteOBJ(AStream: TStream; AObjID: Word); function WriteRichTextStream(AStream: TStream; ABuffer: TMemoryStream; ABytesAvail: Integer; out ABytesWritten: Integer; out AContinueInString: Boolean): Boolean; function WriteRPNCellAddress(AStream: TStream; ARow, ACol: Cardinal; AFlags: TsRelFlags): word; override; function WriteRPNCellOffset(AStream: TStream; ARowOffset, AColOffset: Integer; AFlags: TsRelFlags): Word; override; function WriteRPNCellRangeAddress(AStream: TStream; ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags): Word; override; function WriteRPNSheetIndex(AStream: TStream; ADocumentURL: String; ASheet1, ASheet2: Integer): Word; override; // procedure WriteSelectionRange(AStream: TStream; ARange: TsCellRange); override; procedure WriteSheetLayout(AStream: TStream); procedure WriteSST(AStream: TStream); function WriteString_8bitLen(AStream: TStream; AString: String): Integer; override; procedure WriteSTRINGRecord(AStream: TStream; AString: string); override; procedure WriteSTYLE(AStream: TStream); procedure WriteTXO(AStream: TStream; AComment: PsComment); procedure WriteWINDOW2(AStream: TStream; ASheet: TsBasicWorksheet); procedure WriteXF(AStream: TStream; AFormatRecord: PsCellFormat; XFType_Prot: Byte = 0); override; public constructor Create(AWorkbook: TsBasicWorkbook); override; destructor Destroy; override; { General writing methods } procedure WriteToFile(const AFileName: string; const AOverwriteExisting: Boolean = False; AParams: TsStreamParams = []); override; procedure WriteToStream(AStream: TStream; AParams: TsStreamParams = []); override; end; TExcel8Settings = record DateMode: TDateMode; end; var {@@ Default settings for reading/writing Excel8 files } Excel8Settings: TExcel8Settings = ( DateMode: dm1900; ); {@@ palette of the 64 default BIFF8 colors as "big-endian color" values } PALETTE_BIFF8: array[$00..$3F] of TsColor = ( $000000, // $00: black // 8 built-in default colors $FFFFFF, // $01: white $FF0000, // $02: red $00FF00, // $03: green $0000FF, // $04: blue $FFFF00, // $05: yellow $FF00FF, // $06: magenta $00FFFF, // $07: cyan $000000, // $08: EGA black 1 $FFFFFF, // $09: EGA white 2 $FF0000, // $0A: EGA red 3 $00FF00, // $0B: EGA green 4 $0000FF, // $0C: EGA blue 5 $FFFF00, // $0D: EGA yellow 6 $FF00FF, // $0E: EGA magenta 7 pink $00FFFF, // $0F: EGA cyan 8 turqoise $800000, // $10=16: EGA dark red 9 $008000, // $11=17: EGA dark green 10 $000080, // $12=18: EGA dark blue 11 $808000, // $13=19: EGA olive 12 dark yellow $800080, // $14=20: EGA purple 13 violet $008080, // $15=21: EGA teal 14 $C0C0C0, // $16=22: EGA silver 15 gray 25% $808080, // $17=23: EGA gray 16 gray 50% $9999FF, // $18=24: Periwinkle $993366, // $19=25: Plum $FFFFCC, // $1A=26: Ivory $CCFFFF, // $1B=27: Light turquoise $660066, // $1C=28: Dark purple $FF8080, // $1D=29: Coral $0066CC, // $1E=30: Ocean blue $CCCCFF, // $1F=31: Ice blue $000080, // $20=32: Navy (repeated) $FF00FF, // $21=33: Pink (magenta repeated) $FFFF00, // $22=34: Yellow (repeated) $00FFFF, // $23=35: Turqoise (=cyan repeated) $800080, // $24=36: Purple (repeated) $800000, // $25=37: Dark red (repeated) $008080, // $26=38: Teal (repeated) $0000FF, // $27=39: Blue (repeated) $00CCFF, // $28=40: Sky blue $CCFFFF, // $29=41: Light turquoise (repeated) $CCFFCC, // $2A=42: Light green $FFFF99, // $2B=43: Light yellow $99CCFF, // $2C=44: Pale blue $FF99CC, // $2D=45: rose $CC99FF, // $2E=46: lavander $FFCC99, // $2F=47: tan $3366FF, // $30=48: Light blue $33CCCC, // $31=49: Aqua $99CC00, // $32=50: Lime $FFCC00, // $33=51: Gold $FF9900, // $34=52: Light orange $FF6600, // $35=53: Orange $666699, // $36=54: Blue gray $969696, // $37=55: Gray 40% $003366, // $38=56: Dark teal $339966, // $39=57: Sea green $003300, // $3A=58: very dark green $333300, // $3B=59: olive green $993300, // $3C=60: brown $993366, // $3D=61: plum $333399, // $3E=62: indigo $333333 // $3F=63: gray 80% ); // color names according to http://dmcritchie.mvps.org/EXCEL/COLORS.HTM sfidExcel8: TsSpreadFormatID; procedure InitBIFF8Limitations(out ALimitations: TsSpreadsheetFormatLimitations); implementation uses {$IFDEF FPSpreadDebug} LazLogger, {$ENDIF} Math, lconvencoding, LazFileUtils, URIParser, uvirtuallayer_ole, fpsStrings, {%H-}fpsPatches, fpsStreams, fpsReaderWriter, fpsPalette, fpspreadsheet, fpsNumFormat, fpsExprParser, xlsEscher; const { Excel record IDs } INT_EXCEL_ID_MERGEDCELLS = $00E5; // BIFF8 only INT_EXCEL_ID_MSODRAWING = $00EC; // BIFF8 only INT_EXCEL_ID_SST = $00FC; // BIFF8 only INT_EXCEL_ID_LABELSST = $00FD; // BIFF8 only INT_EXCEL_ID_EXTERNBOOK = $01AE; // BIFF8 only INT_EXCEL_ID_TXO = $01B6; // BIFF8 only INT_EXCEL_ID_HYPERLINK = $01B8; // BIFF8 only INT_EXCEL_ID_HLINKTOOLTIP = $0800; // BIFF8 only INT_EXCEL_ID_SHEETLAYOUT = $0862; // BIFF8 only {%H-}INT_EXCEL_ID_FORCEFULLCALCULATION = $08A3; { Excel OBJ subrecord IDs } INT_EXCEL_OBJID_FTEND = $0000; {%H-}INT_EXCEL_OBJID_FTMACRO = $0004; {%H-}INT_EXCEL_OBJID_FTBUTTON = $0005; {%H-}INT_EXCEL_OBJID_FTGMO = $0006; // Group marker {%H-}INT_EXCEL_OBJID_CF = $0007; // Clipboard format {%H-}INT_EXCEL_OBJID_PIOGRBIT = $0008; // Picture option flags {%H-}INT_EXCEL_OBJID_PICTFMLA = $0009; // Picture fmla-style macro {%H-}INT_EXCEL_OBJID_FTCBLS = $000A; // Checkbox link {%H-}INT_EXCEL_OBJID_FTRBO = $000B; // Radio button {%H-}INT_EXCEL_OBJID_FTSBS = $000C; // Scrollbar {%H-}INT_EXCEL_OBJID_FTNTS = $000D; // Notes structure (= Comment) {%H-}INT_EXCEL_OBJID_FTSBSFMLA = $000E; // Scroll bar fmla-style macro {%H-}INT_EXCEL_OBJID_FTGBODATA = $000F; // Group box data {%H-}INT_EXCEL_OBJID_FTEDODATA = $0010; // Edit control data {%H-}INT_EXCEL_OBJID_FTRBODATA = $0011; // Radio button data {%H-}INT_EXCEL_OBJID_FTCBLSDATA = $0012; // Check box data {%H-}INT_EXCEL_OBJID_FTLBSDATA = $0013; // List box data {%H-}INT_EXCEL_OBJID_FTCBLSFMLA = $0014; // Check box link fmla-style macro INT_EXCEL_OBJID_FTCMO = $0015; // Common object data { Cell Addresses constants } MASK_EXCEL_COL_BITS_BIFF8 = $00FF; MASK_EXCEL_RELATIVE_COL_BIFF8 = $4000; // This is according to Microsoft documentation, MASK_EXCEL_RELATIVE_ROW_BIFF8 = $8000; // but opposite to OpenOffice documentation! { BOF record constants } INT_BOF_BIFF8_VER = $0600; INT_BOF_WORKBOOK_GLOBALS = $0005; {%H-}INT_BOF_VB_MODULE = $0006; INT_BOF_SHEET = $0010; {%H-}INT_BOF_CHART = $0020; {%H-}INT_BOF_MACRO_SHEET = $0040; {%H-}INT_BOF_WORKSPACE = $0100; INT_BOF_BUILD_ID = $1FD2; INT_BOF_BUILD_YEAR = $07CD; { STYLE record constants } MASK_STYLE_BUILT_IN = $8000; { XF substructures } { XF_ROTATION } XF_ROTATION_HORIZONTAL = 0; XF_ROTATION_90DEG_CCW = 90; XF_ROTATION_90DEG_CW = 180; XF_ROTATION_STACKED = 255; // Letters stacked top to bottom, but not rotated TEXT_ROTATIONS: Array[TsTextRotation] of Byte = ( XF_ROTATION_HORIZONTAL, XF_ROTATION_90DEG_CW, XF_ROTATION_90DEG_CCW, XF_ROTATION_STACKED ); { XF CELL BORDER LINE STYLES } MASK_XF_BORDER_LEFT = $0000000F; MASK_XF_BORDER_RIGHT = $000000F0; MASK_XF_BORDER_TOP = $00000F00; MASK_XF_BORDER_BOTTOM = $0000F000; MASK_XF_BORDER_DIAGONAL = $01E00000; MASK_XF_BORDER_SHOW_DIAGONAL_DOWN = $40000000; MASK_XF_BORDER_SHOW_DIAGONAL_UP = $80000000; { XF CELL BORDER COLORS } MASK_XF_BORDER_LEFT_COLOR = $007F0000; MASK_XF_BORDER_RIGHT_COLOR = $3F800000; MASK_XF_BORDER_TOP_COLOR = $0000007F; MASK_XF_BORDER_BOTTOM_COLOR = $00003F80; MASK_XF_BORDER_DIAGONAL_COLOR = $001FC000; { XF CELL BACKGROUND PATTERN } MASK_XF_BACKGROUND_PATTERN = $FC000000; { HLINK FLAGS } MASK_HLINK_LINK = $00000001; MASK_HLINK_ABSOLUTE = $00000002; MASK_HLINK_DESCRIPTION = $00000014; MASK_HLINK_TEXTMARK = $00000008; {%H-}MASK_HLINK_TARGETFRAME = $00000080; {%H-}MASK_HLINK_UNCPATH = $00000100; { RIGHT-TO-LEFT FLAG } MASK_XF_BIDI = $C0; SHAPEID_BASE = 1024; MAX_BYTES_IN_RECORD = 8224; // without header MAX_CHARS_IN_WIDESTRING = 32758; type TBIFF8_DimensionsRecord = packed record RecordID: Word; RecordSize: Word; FirstRow: DWord; LastRowPlus1: DWord; FirstCol: Word; LastColPlus1: Word; NotUsed: Word; end; TBIFF8_LabelRecord = packed record RecordID: Word; RecordSize: Word; Row: Word; Col: Word; XFIndex: Word; TextLen: Word; TextFlags: Byte; end; TBIFF8_LabelSSTRecord = packed record RecordID: Word; RecordSize: Word; Row: Word; Col: Word; XFIndex: Word; SSTIndex: DWord; end; TBiff8_RichTextFormattingRun = packed record FirstIndex: Word; FontIndex: Word; end; TBiff8_RichTextFormattingRuns = array of TBiff8_RichTextFormattingRun; TBIFF8_XFRecord = packed record RecordID: Word; RecordSize: Word; FontIndex: Word; NumFormatIndex: Word; XFType_Prot_ParentXF: Word; Align_TextBreak: Byte; TextRotation: Byte; Indent_Shrink_TextDir: Byte; UsedAttrib: Byte; Border_BkGr1: DWord; Border_BkGr2: DWord; BkGr3: Word; end; TBIFF8TXORecord = packed record RecordID: Word; RecordSize: Word; OptionFlags: Word; TextRot: Word; Reserved1: Word; Reserved2: Word; Reserved3: Word; TextLen: Word; NumFormattingRuns: Word; Reserved4: Word; Reserved5: Word; end; TBIFF8Comment = class ID: Integer; Text: String; end; { -----------------------------------------------------------------------------} { TsBIFF8ExternBook } { -----------------------------------------------------------------------------} function TsBIFF8ExternBook.GetWorksheetName(AIndex: Integer): String; var L: TStrings; begin Result := ''; if Kind = ebkExternal then begin L := TStringList.Create; try L.Delimiter := #1; L.DelimitedText := SheetNames; Result := L[AIndex]; finally L.Free; end; end; end; {------------------------------------------------------------------------------} { TsBIFF8ExternBookList } {------------------------------------------------------------------------------} function TsBIFF8ExternBookList.AddBook(ABookName: String; ASheetNames: TStrings): Integer; var book: TsBIFF8ExternBook; s: String; i: Integer; begin if ABookName = '' then Result := AddInternal else begin Result := IndexOfBook(ABookName); if Result = -1 then begin book := TsBIFF8ExternBook.Create; book.DocumentURL := ABookName; book.Kind := ebkExternal; if ASheetNames.Count > 0 then begin s := ASheetNames[0]; for i:=1 to ASheetNames.Count-1 do s := s + #1 + ASheetNames[i]; book.SheetNames := s; end; Result := Add(book); end; end; end; function TsBIFF8ExternBookList.AddInternal: Integer; var book: TsBIFF8ExternBook; begin Result := IndexOfInternalBook; if Result = -1 then begin book := TsBIFF8ExternBook.Create; book.Kind := ebkInternal; Result := Add(book); end; end; function TsBIFF8ExternBookList.FindBook(ABookName: String): TsBIFF8ExternBook; var idx: Integer; begin idx := IndexOfBook(ABookName); if idx <> -1 then Result := Items[idx] else Result := nil; end; function TsBIFF8ExternBookList.FindInternalBook: TsBIFF8ExternBook; var idx: Integer; begin idx := IndexOfInternalBook; if idx <> -1 then Result := Items[idx] else Result := nil; end; function TsBIFF8ExternBookList.GetItem(AIndex: Integer): TsBIFF8ExternBook; begin Result := TsBIFF8ExternBook(inherited Items[AIndex]); end; function TsBIFF8ExternBookList.IndexOfBook(ABookName: String): Integer; var book: TsBIFF8ExternBook; begin if ABookName = '' then Result := IndexOfInternalBook else begin for Result := 0 to Count-1 do begin book := Items[Result]; if (book.Kind = ebkExternal) and (book.DocumentURL = ABookName) then exit; end; Result := -1; end; end; function TsBIFF8ExternBookList.IndexOfInternalBook: Integer; begin for Result := 0 to Count-1 do if Items[Result].Kind = ebkInternal then exit; Result := -1; end; procedure TsBIFF8ExternBookList.SetItem(AIndex: Integer; AValue: TsBIFF8ExternBook); begin inherited Items[AIndex] := AValue; end; {------------------------------------------------------------------------------} { TsBIFF8ExternSheetList } {------------------------------------------------------------------------------} constructor TsBIFF8ExternSheetList.Create(ABookList: TsBIFF8ExternBookList); begin inherited Create; FBookList := ABookList; end; destructor TsBIFF8ExternSheetList.Destroy; begin Clear; inherited; end; function TsBIFF8ExternSheetList.AddInternalSheets( ASheetIndex1, ASheetIndex2: Integer): Integer; begin Result := AddSheets('', nil, ASheetIndex1, ASheetIndex2); end; function TsBIFF8ExternSheetList.AddSheets(ABookName: String; ASheetNames: TStrings; ASheetIndex1, ASheetIndex2: Integer): Integer; var P: PsBIFF8ExternSheet; idx: Integer; begin Result := IndexOfSheets(ABookName, ASheetIndex1, ASheetIndex2); if Result = -1 then begin New(P); idx := FBookList.IndexOfBook(ABookName); if idx = -1 then idx := FBookList.AddBook(ABookName, ASheetNames); P^.ExternBookIndex := idx; if ASheetIndex2 = -1 then ASheetIndex2 := ASheetIndex1; if ASheetIndex2 < ASheetIndex1 then begin P^.FirstSheetIndex := ASheetIndex2; P^.LastSheetIndex := ASheetIndex1; end else begin P^.FirstSheetIndex := ASheetIndex1; P^.LastSheetIndex := ASheetIndex2; end; Result := Add(P); end; end; procedure TsBIFF8ExternSheetList.Clear; var i: Integer; P: PsBIFF8ExternSheet; begin for i:=0 to Count-1 do begin P := Item[i]; Dispose(P); end; inherited; end; function TsBIFF8ExternSheetList.IndexOfSheets(ABookName: String; ASheetIndex1, ASheetIndex2: Integer): Integer; var P: PsBIFF8ExternSheet; tmp: Integer; idx: Integer; begin if ASheetIndex2 = -1 then ASheetIndex2 := ASheetIndex1; if ASheetIndex2 < ASheetIndex1 then begin tmp := ASheetIndex1; ASheetIndex1 := ASheetIndex2; ASheetIndex2 := tmp; end; idx := FBookList.IndexOfBook(ABookName); if idx = -1 then exit(-1); for Result := 0 to Count-1 do begin P := Item[Result]; if (P^.ExternBookIndex = idx) and (P^.FirstSheetIndex = ASheetIndex1) and (P^.LastSheetIndex = ASheetIndex2) then exit; end; Result := -1; end; function TsBIFF8ExternSheetList.GetItem(AIndex: Integer): PsBIFF8ExternSheet; begin Result := PsBIFF8ExternSheet(inherited Items[AIndex]); end; procedure TsBIFF8ExternSheetList.SetItem(AIndex: Integer; AValue: PsBIFF8ExternSheet); begin inherited Items[AIndex] := AValue; end; {------------------------------------------------------------------------------} { TsSpreadBIFF8Reader } {------------------------------------------------------------------------------} constructor TsSpreadBIFF8Reader.Create(AWorkbook: TsBasicWorkbook); begin inherited; InitBIFF8Limitations(FLimitations); end; destructor TsSpreadBIFF8Reader.Destroy; begin { Destroy linked data } SetLength(FBiff8ExternSheetArray, 0); FBiff8ExternBooks.Free; { Destroy shared string table, as well as associated rich-text streams} FreeSharedStringTable; { Destroy comment list } FCommentList.Free; inherited; end; {@@ ---------------------------------------------------------------------------- Checks, for automatic file format detection, whether tie OLE stream is named 'Workbook' - this is typical of BIFF8 files. -------------------------------------------------------------------------------} class function TsSpreadBIFF8Reader.CheckFileFormatDetails(AStream: TStream): Boolean; var fsOLE: TVirtualLayer_OLE; begin AStream.Position := 0; fsOLE := TVirtualLayer_OLE.Create(AStream); try fsOLE.Initialize; Result := fsOLE.FileExists('/Workbook'); finally fsOLE.Free; end; end; {@@ ---------------------------------------------------------------------------- Frees the shared string table. Also: destroys the rich-text memory streams which may be assigned to string table items. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.FreeSharedStringTable; var j: Integer; begin if FSharedStringTable <> nil then begin for j := 0 to FSharedStringTable.Count-1 do TObject(FSharedStringTable.Objects[j]).Free; FreeAndNil(FSharedStringTable); end; end; {@@ ---------------------------------------------------------------------------- Populates the reader's default palette using the BIFF8 default colors. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.PopulatePalette; begin FPalette.Clear; FPalette.UseColors(PALETTE_BIFF8); end; {@@ ---------------------------------------------------------------------------- Reads a CONTINUE record. If the Flag "FCommentPending" is active then this record contains the text of a comment assigned to a cell. The length of the string is taken from the preceeding TXO record, and the ID of the comment is extracted in another preceeding record, an OBJ record. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.ReadCONTINUE(const AStream: TStream); var commentStr: String; comment: TBIFF8Comment; rtParams: TsRichTextParams; begin if FCommentPending and (FCommentLen > 0) then begin commentStr := Utf8Encode(ReadWideString(AStream, FCommentLen, rtParams)); if commentStr <> '' then begin comment := TBIFF8Comment.Create; comment.ID := FCommentID; comment.Text := commentStr; FCommentList.Add(comment); end; FCommentPending := false; end; end; { Reads a NOTE record (comment associated with a cell). All comments have been collected in the FCommentList of the reader from preceding OBJ, TXO and CONTINUE records. } procedure TsSpreadBIFF8Reader.ReadNOTE(const AStream: TStream); var r, c: Word; commentID: Word; commentText: String; i: Integer; begin { Row of the comment } r := WordLEToN(AStream.ReadWord); { Column of the comment } c := WordLEToN(AStream.ReadWord); { Option flags, not needed } WordLEToN(AStream.ReadWord); { Comment ID } commentID := WordLEToN(AStream.ReadWord); { Next would be the author - ignored... } { Seek comment with this ID in the comment list of the reader. } for i:=0 to FCommentList.Count-1 do if TBIFF8Comment(FCommentList[i]).ID = commentID then begin commentText := TBIFF8Comment(FCommentList[i]).Text; TsWorksheet(FWorksheet).WriteComment(r, c, commentText); exit; end; end; { Reads an OBJ record. So far, we only evaluate it to get the ID of the comment stored in a following TXO and CONTINUE record. } procedure TsSpreadBIFF8Reader.ReadOBJ(const AStream: TStream); var subrecID, subrecSize: Word; streamPos, p: Int64; streamSize: Int64; objType: Word; objID: Word; begin streamSize := AStream.Size; while true do begin subrecID := WordLEToN(AStream.ReadWord); subrecSize := WordLEToN(AStream.ReadWord); streamPos := AStream.Position; case subrecID of INT_EXCEL_OBJID_FTCMO: // common object data // This is the first sub-record of the OBJ record. begin objType := WordLEToN(AStream.ReadWord); objID := WordLEToN(AStream.ReadWord); if objType = $19 then begin // $19 = object is a "comment" FCommentPending := true; FCommentID := objID; FCommentLen := 0; // will be determined in following TX0 record exit; end else FCommentPending := false; end; INT_EXCEL_OBJID_FTLBSDATA: if subrecSize = $1FEE then // this cannot be the true sub-record size !!! // https://mail-archives.apache.org/mod_mbox/poi-dev/200409.mbox/%3CC1ECA5ECAA06A64D88D955E9E152680D32A5C5@SNOWBALL2.asc.com.au%3E // "Every sheet I have looked at seems to have a 16 byte ftLbsData sub-record." subrecSize := 16 // NOTE: // This is a risky assumption. A more robust implementation must look at // the individual elements of this subrecord, see https://searchcode.com/codesearch/view/47124816/ else if subrecSize = 0 then // From MS doc: "If cbFContinued is 0x0000, all of the fields in this // structure except ft and cbFContinued MUST NOT exist." exit; // We exit because the stream position cannot advance any more! INT_EXCEL_OBJID_FTEND: // This is the last sub-record. exit; end; // The structure of the OBJ records is very chaotic. Therefore, it can easily // occur that we are lost and read beyond stream end: Check for stream end // and store an error. Normal reading will we resumed at the correct position // by the main reading loop. p := streamPos + subrecSize; if p < streamSize then AStream.Position := p else begin FWorkbook.AddErrorMsg(Format(rsFileStructureError, ['OBJ', streamPos])); exit; end; end; end; { Reads a unicode string which does not contain rich-text information. This is needed for the RSTRING record. } function TsSpreadBIFF8Reader.ReadUnformattedWideString(const AStream: TStream; const ALength: WORD): WideString; var flags: Byte; DecomprStrValue: WideString = ''; i: Integer; len: SizeInt; recType: Word; {%H-}recSize: Word; C: WideChar; begin Result := ''; flags := AStream.ReadByte; dec(PendingRecordSize); if flags and 1 = 1 Then begin //String is WideStringLE if (ALength * SizeOf(WideChar)) > PendingRecordSize then begin SetLength(Result, PendingRecordSize div 2); AStream.ReadBuffer(Result[1], PendingRecordSize); Dec(PendingRecordSize, PendingRecordSize); 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 len := ALength; SetLength(DecomprStrValue, len); for i := 1 to len do begin C := WideChar(AStream.ReadByte); // Read 1 byte, but put it into a 2-byte char DecomprStrValue[i] := C; dec(PendingRecordSize); if (PendingRecordSize <= 0) and (i < len) then begin //A CONTINUE may have happened here recType := WordLEToN(AStream.ReadWord); recSize := WordLEToN(AStream.ReadWord); if recType <> INT_EXCEL_ID_CONTINUE then begin raise EFPSpreadsheetReader.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.'); end else begin PendingRecordSize := RecordSize; DecomprStrValue := copy(DecomprStrValue,1,i) + ReadUnformattedWideString(AStream, ALength-i); break; end; end; end; Result := DecomprStrValue; end; end; function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; const ALength: WORD; out ARichTextParams: TsRichTextParams): WideString; var StringFlags: BYTE; DecomprStrValue: WideString = ''; AnsiStrValue: ansistring = ''; RunsCounter: WORD; AsianPhoneticBytes: DWORD; rtf_dummy: TsRichTextParams; i: Integer; j: Integer; //j: SizeUInt; lLen: SizeInt; recType: WORD; recSize: WORD; C: WideChar; begin Result := ''; StringFlags := AStream.ReadByte; Dec(PendingRecordSize); if StringFlags and 8 = 8 then begin // Rich string RunsCounter := WordLEtoN(AStream.ReadWord); dec(PendingRecordSize,2); end; if StringFlags and 4 = 4 then begin // Asian phonetics // Read Asian phonetics Length (not used) AsianPhoneticBytes := DWordLEtoN(AStream.ReadDWord); dec(PendingRecordSize,4); end; if StringFlags and 1 = 1 Then begin // String is WideStringLE if (ALength*SizeOf(WideChar)) > PendingRecordSize then begin SetLength(Result, PendingRecordSize div 2); AStream.ReadBuffer(Result[1], PendingRecordSize); Dec(PendingRecordSize, PendingRecordSize); // We reached the end of the record and switch to the CONTINUE record recType := WordLEToN(AStream.ReadWord); recSize := WordLEToN(AStream.ReadWord); if recType <> INT_EXCEL_ID_CONTINUE then raise EFPSpreadsheetReader.Create('[TsSpreadBIFF8Reader.ReadWideString] CONTINUE record expected, but not found.'); PendingRecordSize := recSize; Result := Result + ReadWideString(AStream, ALength - Length(Result), rtf_dummy); 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 lLen := ALength; SetLength(DecomprStrValue, lLen); for i := 1 to lLen do begin C := WideChar(AStream.ReadByte); // Read 1 byte, but put it into a 2-byte char DecomprStrValue[i] := C; Dec(PendingRecordSize); if (PendingRecordSize <= 0) and (i < lLen) then begin //A CONTINUE may have happened here recType := WordLEToN(AStream.ReadWord); recSize := WordLEToN(AStream.ReadWord); if recType <> INT_EXCEL_ID_CONTINUE then begin Raise EFPSpreadsheetReader.Create('[TsSpreadBIFF8Reader.ReadWideString] CONTINUE record expected, but not found.'); end else begin PendingRecordSize := recSize; DecomprStrValue := copy(DecomprStrValue,1,i) + ReadWideString(AStream, ALength-i, ARichTextParams); break; end; end; end; Result := DecomprStrValue; end; if StringFlags and 8 = 8 then begin // Rich string (This only occurs in BIFF8) SetLength(ARichTextParams, RunsCounter); for j := 0 to SmallInt(RunsCounter) - 1 do begin if (PendingRecordSize <= 0) then begin // A CONTINUE may happened here recType := WordLEToN(AStream.ReadWord); recSize := WordLEToN(AStream.ReadWord); if recType <> INT_EXCEL_ID_CONTINUE then begin Raise EFPSpreadsheetReader.Create('[TsSpreadBIFF8Reader.ReadWideString] CONTINUE record expected, but not found.'); end else begin PendingRecordSize := recSize; end; end; // character start index: 0-based in file, 1-based in fps ARichTextParams[j].FirstIndex := WordLEToN(AStream.ReadWord) + 1; ARichTextParams[j].FontIndex := WordLEToN(AStream.ReadWord); ARichTextParams[j].HyperlinkIndex := -1; 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); dec(PendingRecordSize, AsianPhoneticBytes); end; end; function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; var Len: Word; rtParams: TsRichTextParams; begin if AUse8BitLength then Len := AStream.ReadByte() else Len := WordLEtoN(AStream.ReadWord()); Result := ReadWideString(AStream, Len, rtParams); end; procedure TsSpreadBIFF8Reader.ReadWorkbookGlobals(AStream: TStream); var SectionEOF: Boolean = False; RecordType: Word; CurStreamPos: Int64; begin if FCommentList = nil then FCommentList := TObjectList.Create else FCommentList.Clear; FreeSharedStringTable; 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_CODEPAGE : ReadCodepage(AStream); INT_EXCEL_ID_DATEMODE : ReadDateMode(AStream); INT_EXCEL_ID_DEFINEDNAME : ReadDEFINEDNAME(AStream); INT_EXCEL_ID_EOF : SectionEOF := True; INT_EXCEL_ID_EXTERNBOOK : ReadEXTERNBOOK(AStream); INT_EXCEL_ID_EXTERNSHEET : ReadEXTERNSHEET(AStream); INT_EXCEL_ID_FONT : ReadFont(AStream); INT_EXCEL_ID_FORMAT : ReadFormat(AStream); INT_EXCEL_ID_PALETTE : ReadPalette(AStream); INT_EXCEL_ID_PASSWORD : ReadPASSWORD(AStream); INT_EXCEL_ID_PROTECT : ReadPROTECT(AStream); INT_EXCEL_ID_SST : ReadSST(AStream); INT_EXCEL_ID_WINDOWPROTECT : ReadWindowProtect(AStream); INT_EXCEL_ID_XF : ReadXF(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; // Convert palette indexes to rgb colors FixColors; end; procedure TsSpreadBIFF8Reader.ReadWorksheet(AStream: TStream); var SectionEOF: Boolean = False; RecordType: Word; CurStreamPos: Int64; begin FWorksheet := (FWorkbook as TsWorkbook).GetWorksheetByIndex(FCurSheetIndex); while (not SectionEOF) do begin { Read the record header } RecordType := WordLEToN(AStream.ReadWord); RecordSize := WordLEToN(AStream.ReadWord); PendingRecordSize := RecordSize; // For debugging to find out in which record a crash happens: {$IFDEF FPSpreadDebug} DebugLn(Format('[ReadWorksheet] Stream.Pos: %d, RecordType: $%.04x, RecordSize: %d', [AStream.Position-4, RecordType, RecordSize])); {$ENDIF} CurStreamPos := AStream.Position; case RecordType of INT_EXCEL_ID_BLANK : ReadBlank(AStream); INT_EXCEL_ID_BOF : ; INT_EXCEL_ID_BOOLERROR : ReadBool(AStream); INT_EXCEL_ID_BOTTOMMARGIN : ReadMargin(AStream, 3); INT_EXCEL_ID_COLINFO : ReadColInfo(AStream); INT_EXCEL_ID_CONTINUE : ReadCONTINUE(AStream); INT_EXCEL_ID_DEFCOLWIDTH : ReadDefColWidth(AStream); INT_EXCEL_ID_DEFINEDNAME : ReadDefinedName(AStream); INT_EXCEL_ID_EOF : SectionEOF := True; INT_EXCEL_ID_FOOTER : ReadHeaderFooter(AStream, false); INT_EXCEL_ID_FORMULA : ReadFormula(AStream); INT_EXCEL_ID_HCENTER : ReadHCENTER(AStream); INT_EXCEL_ID_HEADER : ReadHeaderFooter(AStream, true); INT_EXCEL_ID_HORZPAGEBREAK : ReadHorizontalPageBreaks(AStream, FWorksheet); INT_EXCEL_ID_HLINKTOOLTIP : ReadHyperlinkToolTip(AStream); INT_EXCEL_ID_HYPERLINK : ReadHyperlink(AStream); INT_EXCEL_ID_LABEL : ReadLabel(AStream); INT_EXCEL_ID_LABELSST : ReadLabelSST(AStream); INT_EXCEL_ID_LEFTMARGIN : ReadMargin(AStream, 0); INT_EXCEL_ID_MERGEDCELLS : ReadMergedCells(AStream); INT_EXCEL_ID_MULBLANK : ReadMulBlank(AStream); INT_EXCEL_ID_MULRK : ReadMulRKValues(AStream); INT_EXCEL_ID_NOTE : ReadNOTE(AStream); INT_EXCEL_ID_NUMBER : ReadNumber(AStream); INT_EXCEL_ID_OBJ : ReadOBJ(AStream); INT_EXCEL_ID_OBJECTPROTECT : ReadObjectProtect(AStream, FWorksheet); INT_EXCEL_ID_PAGESETUP : ReadPageSetup(AStream); INT_EXCEL_ID_PANE : ReadPane(AStream); INT_EXCEL_ID_PASSWORD : ReadPASSWORD(AStream, FWorksheet); INT_EXCEL_ID_PRINTGRID : ReadPrintGridLines(AStream); INT_EXCEL_ID_PRINTHEADERS : ReadPrintHeaders(AStream); INT_EXCEL_ID_PROTECT : ReadPROTECT(AStream, FWorksheet); INT_EXCEL_ID_RIGHTMARGIN : ReadMargin(AStream, 1); INT_EXCEL_ID_ROW : ReadRowInfo(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_RSTRING : ReadRSTRING(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_RK : ReadRKValue(AStream); INT_EXCEL_ID_SCL : ReadSCLRecord(AStream); INT_EXCEL_ID_SELECTION : ReadSELECTION(AStream); INT_EXCEL_ID_SHAREDFMLA : ReadSharedFormula(AStream); INT_EXCEL_ID_SHEETLAYOUT : ReadSheetLayout(AStream); INT_EXCEL_ID_SHEETPR : ReadSHEETPR(AStream); INT_EXCEL_ID_STRING : ReadStringRecord(AStream); INT_EXCEL_ID_TOPMARGIN : ReadMargin(AStream, 2); INT_EXCEL_ID_TXO : ReadTXO(AStream); INT_EXCEL_ID_VCENTER : ReadVCENTER(AStream); INT_EXCEL_ID_VERTPAGEBREAK : ReadVerticalPageBreaks(AStream, FWorksheet); INT_EXCEL_ID_WINDOW2 : ReadWindow2(AStream); 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; FixCols(FWorksheet); FixRows(FWorksheet); end; procedure TsSpreadBIFF8Reader.ReadBoundsheet(AStream: TStream); var len: Byte; wideName: WideString; rtParams: TsRichTextParams; sheet: TsWorksheet; sheetstate: Byte; 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 } sheetstate := AStream.ReadByte(); // 0=visible, 1=hidden, 2="very" hidden { Sheet type } AStream.ReadByte(); { Sheet name: 8-bit length } len := AStream.ReadByte(); { Read string with flags } wideName := ReadWideString(AStream, len, rtParams); sheet := (FWorkbook as TsWorkbook).AddWorksheet(UTF8Encode(widename), true); if sheetState <> 0 then sheet.Options := sheet.Options + [soHidden]; end; function TsSpreadBIFF8Reader.ReadString(const AStream: TStream; const ALength: WORD; out ARichTextParams: TsRichTextParams): String; begin Result := UTF16ToUTF8(ReadWideString(AStream, ALength, ARichTextParams)); end; procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); var OLEStream: TMemoryStream; OLEStorage: TOLEStorage; OLEDocument: TOLEDocument; begin Unused(APassword, AParams); OLEStream := TMemoryStream.Create; try // Only one stream is necessary for any number of worksheets OLEStorage := TOLEStorage.Create; try OLEDocument.Stream := OLEStream; OLEStorage.ReadOLEStream(AStream, OLEDocument, 'Workbook'); InternalReadFromStream(OLEStream); finally OLEStorage.Free; end; finally OLEStream.Free; end; end; (* procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream); var BIFF8EOF: Boolean; begin { Initializations } BIFF8EOF := False; FWorksheetNames := TStringList.Create; FWorksheetNames.Clear; FCurrentWorksheet := 0; if FCommentList = nil then FCommentList := TObjectList.Create else FCommentList.Clear; { Read workbook globals } ReadWorkbookGlobals(AStream); // 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); // Check for the end of the file if AStream.Position >= AStream.Size then BIFF8EOF := True; // Final preparations Inc(FCurrentWorksheet); if FCurrentWorksheet = FWorksheetNames.Count then BIFF8EOF := True; // It can happen in files written by Office97 that the OLE directory is // at the end of the file. end; { Finalizations } FWorksheetNames.Free; end; *) procedure TsSpreadBIFF8Reader.ReadLABEL(AStream: TStream); var L, i: Word; ARow, ACol: Cardinal; XF: Word; wideStrValue: WideString; cell: PCell; rtParams: TsRichTextParams; fntIndex: Integer; fnt: TsFont; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; { BIFF Record data: Row, Column, XF Index } ReadRowColXF(AStream, ARow, ACol, XF); { Byte String with 16-bit size } L := WordLEtoN(AStream.ReadWord()); { Read wide string with flags } wideStrValue := ReadWideString(AStream, L, rtParams); { Save the data } if FIsVirtualMode then begin InitCell(FWorksheet, ARow, ACol, FVirtualCell); // "virtual" cell cell := @FVirtualCell; end else cell := (FWorksheet as TsWorksheet).AddCell(ARow, ACol); // "real" cell (FWorksheet as TsWorksheet).WriteText(cell, UTF16ToUTF8(wideStrValue)); { Add attributes } ApplyCellFormatting(cell, XF); { Apply rich-text formatting } if Length(rtParams) > 0 then begin SetLength(cell^.RichTextParams, Length(rtParams)); for i := 0 to High(rtParams) do begin // Character index where format starts: 0-based in file, 1-based in fps cell^.RichTextParams[i].FirstIndex := rtParams[i].FirstIndex + 1; // Font index of new format - need to adjust index! fntIndex := rtParams[i].FontIndex; fnt := TsFont(FFontList[fntIndex]); fntIndex := book.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); if fntIndex = -1 then fntIndex := book.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); cell^.RichTextParams[i].FontIndex := fntIndex; // Hyperlink index, not used here cell^.RichTextParams[i].HyperlinkIndex := -1; end; end; if FIsVirtualMode then book.OnReadCellData(Workbook, ARow, ACol, cell); end; procedure TsSpreadBIFF8Reader.ReadMergedCells(const AStream: TStream); var rng: packed record Row1, Row2, Col1, Col2: Word; end; i, n: word; begin rng.Row1 := 0; // to silence the compiler... // Count of merged ranges n := WordLEToN(AStream.ReadWord); for i:=1 to n do begin // Read range AStream.ReadBuffer(rng, SizeOf(rng)); // Transfer cell range to worksheet (FWorksheet as TsWorksheet).MergeCells( WordLEToN(rng.Row1), WordLEToN(rng.Col1), WordLEToN(rng.Row2), WordLEToN(rng.Col2) ); end; end; (* procedure TsSpreadBIFF8Reader.ReadRichString(const AStream: TStream); var L: Word; B: WORD; ARow, ACol: Cardinal; XF: Word; strValue: string; cell: PCell; rtfRuns: TsRichTextFormattingRuns; begin ReadRowColXF(AStream, ARow, ACol, XF); { Byte String with 16-bit size } L := WordLEtoN(AStream.ReadWord()); strValue := ReadString(AStream, L, rtfRuns); { Create cell } if FIsVirtualMode then begin InitCell(ARow, ACol, FVirtualCell); cell := @FVirtualCell; end else cell := FWorksheet.AddCell(ARow, ACol); { Save the data } FWorksheet.WriteUTF8Text(cell, strValue); { // Read rich-text formatting runs B := WordLEtoN(AStream.ReadWord); SetLength(rtfRuns, B); for L := 0 to B-1 do begin rtfRuns[L].FirstIndex := WordLEToN(AStream.ReadWord); // Index of first formatted character rtfRuns[L].FontIndex := WordLEToN(AStream.ReadByte); // Index of font used end; } {Add attributes} ApplyCellFormatting(cell, XF); ApplyRichTextFormattingRuns(cell, rtfRuns); if FIsVirtualMode then Workbook.OnReadCellData(Workbook, ARow, ACol, cell); end; *) { Reads the cell address used in an RPN formula element. Evaluates the corresponding bits to distinguish between absolute and relative addresses. Overriding the implementation in xlscommon. } procedure TsSpreadBIFF8Reader.ReadRPNCellAddress(AStream: TStream; out ARow, ACol: Cardinal; out AFlags: TsRelFlags); var c: word; begin // Read row index (2 bytes) ARow := WordLEToN(AStream.ReadWord); // Read column index; it contains info on absolute/relative address c := WordLEToN(AStream.ReadWord); // Extract column index ACol := c and MASK_EXCEL_COL_BITS_BIFF8; // Extract info on absolute/relative addresses. AFlags := []; if (c and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol); if (c and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow); end; { Reads the difference between cell row and column indexed of a cell and a reference cell. Overrides the implementation in xlscommon. } procedure TsSpreadBIFF8Reader.ReadRPNCellAddressOffset(AStream: TStream; out ARowOffset, AColOffset: Integer; out AFlags: TsRelFlags); var dr: SmallInt; dc: ShortInt; c: Word; begin // 2 bytes for row offset dr := ShortInt(WordLEToN(AStream.ReadWord)); ARowOffset := dr; // 2 bytes for column offset c := WordLEToN(AStream.ReadWord); dc := ShortInt(Lo(c)); AColOffset := dc; // Extract info on absolute/relative addresses. AFlags := []; if (c and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol); if (c and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow); end; { Reads a cell range address used in an RPN formula element. Evaluates the corresponding bits to distinguish between absolute and relative addresses. Overriding the implementation in xlscommon. } procedure TsSpreadBIFF8Reader.ReadRPNCellRangeAddress(AStream: TStream; out ARow1, ACol1, ARow2, ACol2: Cardinal; out AFlags: TsRelFlags); var c1, c2: word; begin // Read row index of first and last rows (2 bytes, each) ARow1 := WordLEToN(AStream.ReadWord); ARow2 := WordLEToN(AStream.ReadWord); // Read column index of first and last columns; they contain info on // absolute/relative address c1 := WordLEToN(AStream.ReadWord); c2 := WordLEToN(AStream.ReadWord); // Extract column index of rist and last columns ACol1 := c1 and MASK_EXCEL_COL_BITS_BIFF8; ACol2 := c2 and MASK_EXCEL_COL_BITS_BIFF8; // Extract info on absolute/relative addresses. AFlags := []; if (c1 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol); if (c1 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow); if (c2 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol2); if (c2 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow2); end; procedure TsSpreadBIFF8Reader.ReadRPNCellRangeAddressOffset(AStream: TStream; out ARowOffset1, AColOffset1, ARowOffset2, AColOffset2: Integer; out AFlags: TsRelFlags); var dr1, dr2: SmallInt; dc1, dc2: ShortInt; c1, c2: Word; begin // 2 bytes for row1 and row2 offsets, each dr1 := ShortInt(WordLEToN(AStream.ReadWord)); dr2 := ShortInt(WordLEToN(AStream.ReadWord)); ARowOffset1 := dr1; ARowOffset2 := dr2; // 2 bytes for col1 and col2 offsets, each c1 := WordLEToN(AStream.ReadWord); c2 := WordLEToN(AStream.ReadWord); dc1 := ShortInt(Lo(c1)); dc2 := ShortInt(Lo(c2)); AColOffset1 := dc1; AColOffset2 := dc2; // Extract info on absolute/relative addresses. AFlags := []; if (c1 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol); if (c1 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow); if (c2 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol2); if (c2 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow2); end; { function TsSpreadBIFF8Reader.ReadRPNCellRange3D(AStream: TStream; var ARPNItem: PRPNItem): Boolean; var sheetIndex1, sheetIndex2: Integer; r1, c1, r2, c2: Cardinal; flags: TsRelFlags; begin Result := true; ReadRPNSheetIndex(AStream, sheetIndex1, sheetIndex2); if (sheetIndex1 = -1) or (sheetIndex2 = -1) then exit(False); // unsupported case ReadRPNCellRangeAddress(AStream, r1, c1, r2, c2, flags); if r2 = $FFFF then r2 := Cardinal(-1); if c2 = $FF then c2 := Cardinal(-1); ARPNItem := RPNCellRange3D( sheetIndex1, r1, c1, sheetIndex2, r2, c2, flags, ARPNItem ); end;} { Reads the difference between row and column corner indexes of a cell range and a reference cell. Overriding the implementation in xlscommon. } procedure TsSpreadBIFF8Reader.ReadRPNCellRangeOffset(AStream: TStream; out ARow1Offset, ACol1Offset, ARow2Offset, ACol2Offset: Integer; out AFlags: TsRelFlags); var c1, c2: Word; begin // 2 bytes for offset of first row ARow1Offset := ShortInt(WordLEToN(AStream.ReadWord)); // 2 bytes for offset to last row ARow2Offset := ShortInt(WordLEToN(AStream.ReadWord)); // 2 bytes for offset of first column c1 := WordLEToN(AStream.ReadWord); ACol1Offset := Shortint(Lo(c1)); // 2 bytes for offset of last column c2 := WordLEToN(AStream.ReadWord); ACol2Offset := ShortInt(Lo(c2)); // Extract info on absolute/relative addresses. AFlags := []; if (c1 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol); if (c1 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow); if (c2 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol2); if (c2 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow2); end; procedure TsSpreadBIFF8Reader.ReadRPNSheetIndex(AStream: TStream; out ADocumentURL: String; out ASheet1, ASheet2: Integer); var refIndex: SmallInt; ref: TsBiff8ExternSheet; book: TsBiff8ExternBook; begin // Index to REF entry in EXTERNSHEET record refIndex := WordLEToN(AStream.ReadWord); ref := FBiff8ExternSheetArray[refIndex]; book := FBiff8ExternBooks[ref.ExternBookIndex] as TsBiff8ExternBook; // Only links to internal sheets supported so far. if book.Kind <> ebkInternal then begin ADocumentURL := ''; ASheet1 := -1; ASheet2 := -1; exit; end; ADocumentURL := book.DocumentURL; ASheet1 := ref.FirstSheetIndex; ASheet2 := ref.LastSheetIndex; end; procedure TsSpreadBIFF8Reader.ReadRSTRING(AStream: TStream); var j, L: Word; ARow, ACol: Cardinal; XF: Word; wideStrValue: WideString; cell: PCell; rtfRuns: TBiff8_RichTextFormattingRuns = nil; fntIndex: Integer; fnt: TsFont; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; { BIFF Record data: Row, Column, XF Index } ReadRowColXF(AStream, ARow, ACol, XF); { Data string: 16-bit length } L := WordLEtoN(AStream.ReadWord()); { Read wide string plus flag, but without processing it } wideStrValue := ReadUnformattedWideString(AStream, L); { Create cell } if FIsVirtualMode then begin InitCell(FWorksheet, ARow, ACol, FVirtualCell); // "virtual" cell cell := @FVirtualCell; end else cell := (FWorksheet as TsWorksheet).AddCell(ARow, ACol); // "real" cell { Save the data string} (FWorksheet as TsWorksheet).WriteText(cell, UTF16ToUTF8(wideStrValue)); { Read rich-text formatting runs } L := WordLEToN(AStream.ReadWord); SetLength(cell^.RichTextParams, L); SetLength(rtfRuns, L); AStream.ReadBuffer(rtfRuns[0], L * SizeOf(TBiff8_RichTextFormattingRun)); for j := 0 to L-1 do begin // Index of the font. Be aware that the index in the file is not // necessarily the same as the index used by the workbook! fntIndex := WordLEToN(rtfRuns[j].FontIndex); fnt := TsFont(FFontList[fntIndex]); fntIndex := book.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); if fntIndex = -1 then fntIndex := book.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); cell^.RichTextParams[j].FontIndex := fntIndex; // Index of the first character using this font: 0-based in file, 1-based in fps cell^.RichTextParams[j].FirstIndex := WordLEToN(rtfRuns[j].FirstIndex) + 1; // Hyperlink index - not used by biff cell^.RichTextParams[j].HyperlinkIndex := -1; end; {Add attributes} ApplyCellFormatting(cell, XF); if FIsVirtualMode then book.OnReadCellData(book, ARow, ACol, cell); end; procedure TsSpreadBIFF8Reader.ReadSheetLayout(const AStream: TStream); var iclr: word; i: Integer; begin if WordLEToN(AStream.ReadWord) <> INT_EXCEL_ID_SHEETLAYOUT then exit; for i := 1 to 7 do AStream.ReadWord; // not used, unknown data iclr := WordLEToN(AStream.ReadWord); // index of tab color TsWorksheet(FWorksheet).TabColor := FPalette[iclr]; end; procedure TsSpreadBIFF8Reader.ReadSST(const AStream: TStream); var Items: DWORD; StringLength, CurStrLen: WORD; LString: String; ContinueIndicator: WORD; rtParams: TsRichTextParams; ms: TMemoryStream; begin //Reads the shared string table, only compatible with BIFF8 if not Assigned(FSharedStringTable) then begin //First time SST creation FSharedStringTable := TStringList.Create; // Total number of strings in the workbook, not used DWordLEtoN(AStream.ReadDWord); // Number of following strings Items := DWordLEtoN(AStream.ReadDWord); Dec(PendingRecordSize, 8); end else begin //A second record must not happen. Garbage so skip. Exit; end; while Items > 0 do begin StringLength := 0; StringLength := WordLEtoN(AStream.ReadWord); Dec(PendingRecordSize ,2); LString := ''; // This loop takes care of the string being split between the STT and the CONTINUE, or between CONTINUE records while PendingRecordSize > 0 do begin if StringLength > 0 then //Read a stream of zero length reads all the stream. LString := LString + ReadString(AStream, StringLength, rtParams) 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; // Check if the record finished and we need a CONTINUE record to go on 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 EFPSpreadsheetReader.Create('[TsSpreadBIFF8Reader.ReadSST] Expected CONTINUE record not found.'); end; PendingRecordSize := WordLEtoN(AStream.ReadWord); CurStrLen := Length(UTF8ToUTF16(LString)); if StringLength < CurStrLen then raise EFPSpreadsheetReader.Create('[TsSpreadBIFF8Reader.ReadSST] StringLength= FSharedStringTable.Count then begin raise EFPSpreadsheetReader.CreateFmt(rsIndexInSSTOutOfRange, [ Integer(SSTIndex), FSharedStringTable.Count-1 ]); end; { Create cell } if FIsVirtualMode then begin InitCell(FWorksheet, ARow, ACol, FVirtualCell); cell := @FVirtualCell; end else cell := (FWorksheet as TsWorksheet).AddCell(ARow, ACol); { Read text from shared string table entry } (FWorksheet as TsWorksheet).WriteText(cell, FSharedStringTable.Strings[SSTIndex]); { Add attributes } ApplyCellFormatting(cell, XF); { Add rich text formatting } ms := TMemoryStream(FSharedStringTable.Objects[SSTIndex]); if ms <> nil then begin ms.Position := 0; n := WordLEToN(ms.ReadWord); SetLength(rtParams, n); ms.ReadBuffer(rtParams[0], n*SizeOf(TsRichTextParam)); SetLength(cell^.RichTextParams, n); for i:=0 to n-1 do begin cell^.RichTextParams[i].FirstIndex := rtParams[i].FirstIndex; fntIndex := rtParams[i].FontIndex; fnt := TsFont(FFontList[fntIndex]); fntIndex := book.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); if fntIndex = -1 then fntIndex := book.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); cell^.RichTextParams[i].FontIndex := fntIndex; cell^.RichTextParams[i].HyperlinkIndex := -1; end; end; if FIsVirtualMode then book.OnReadCellData(book, ARow, ACol, cell); end; { Helper function for reading a string with 8-bit length. } function TsSpreadBIFF8Reader.ReadString_8bitLen(AStream: TStream): String; const HAS_8BITLEN = true; var wideStr: widestring; begin wideStr := ReadWideString(AStream, HAS_8BITLEN); // Result := UTF8Encode(wideStr); // wp: this leads to string encoding error with fpc 3.0 (no UTF8RTL) Result := UTF16ToUTF8(wideStr); end; procedure TsSpreadBIFF8Reader.ReadStringRecord(AStream: TStream); var wideStr: WideString; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; wideStr := ReadWideString(AStream, false); if (FIncompleteCell <> nil) and (wideStr <> '') then begin FIncompleteCell^.UTF8StringValue := UTF8Encode(wideStr); FIncompleteCell^.ContentType := cctUTF8String; if FIsVirtualMode then book.OnReadCellData(book, FIncompleteCell^.Row, FIncompleteCell^.Col, FIncompleteCell); end; FIncompleteCell := nil; end; { Reads a TXO record (TEXT OBJECT). Needed to retrieve cell comments. We only extract the length of the comment text (in characters). The text itself is contained in the following CONTINUE record. } procedure TsSpreadBIFF8Reader.ReadTXO(const AStream: TStream); var rec: TBIFF8TXORecord; begin rec.OptionFlags := 0; // to silence the compiler AStream.ReadBuffer(rec.OptionFlags, Sizeof(Rec) - 2*SizeOf(Word)); FCommentLen := WordLEToN(rec.TextLen); end; procedure TsSpreadBIFF8Reader.ReadXF(const AStream: TStream); var rec: TBIFF8_XFRecord; fmt: TsCellFormat; b: Byte; dw: DWord; fill: Integer; fs: TsFillStyle; nfs: String; nfParams: TsNumFormatParams; iclr: Integer; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; InitFormatRecord(fmt); fmt.ID := FCellFormatList.Count; rec.FontIndex := 0; // to silence the compiler... // Read entire xf record into a buffer AStream.ReadBuffer(rec.FontIndex, SizeOf(rec) - 2*SizeOf(word)); // Font index fmt.FontIndex := FixFontIndex(WordLEToN(rec.FontIndex)); if fmt.FontIndex > 1 then Include(fmt.UsedFormattingFields, uffFont); // Number format index if rec.NumFormatIndex <> 0 then begin nfs := NumFormatList[rec.NumFormatIndex]; // "General" (NumFormatIndex = 0) not stored in workbook's NumFormatList if (rec.NumFormatIndex > 0) and not SameText(nfs, 'General') then begin fmt.NumberFormatIndex := book.AddNumberFormat(nfs); nfParams := book.GetNumberFormat(fmt.NumberFormatIndex); if nfParams <> nil then begin fmt.NumberFormat := nfParams.NumFormat; fmt.NumberFormatStr := nfs; Include(fmt.UsedFormattingFields, uffNumberFormat); end; end; end; // Horizontal text alignment b := rec.Align_TextBreak AND MASK_XF_HOR_ALIGN; if (b <= ord(High(TsHorAlignment))) then begin fmt.HorAlignment := TsHorAlignment(b); if fmt.HorAlignment <> haDefault then Include(fmt.UsedFormattingFields, uffHorAlign); end; // Vertical text alignment b := (rec.Align_TextBreak AND MASK_XF_VERT_ALIGN) shr 4; if (b + 1 <= ord(high(TsVertAlignment))) then begin fmt.VertAlignment := TsVertAlignment(b + 1); // + 1 due to vaDefault // Unfortunately BIFF does not provide a "default" vertical alignment code. // Without the following correction "non-formatted" cells would always have // the uffVertAlign FormattingField set which contradicts the statement of // not being formatted. if fmt.VertAlignment = vaBottom then fmt.VertAlignment := vaDefault; if fmt.VertAlignment <> vaDefault then Include(fmt.UsedFormattingFields, uffVertAlign); end; // Word wrap if (rec.Align_TextBreak and MASK_XF_TEXTWRAP) <> 0 then Include(fmt.UsedFormattingFields, uffWordwrap); // BiDi mode b := (rec.Indent_Shrink_TextDir and MASK_XF_BIDI) shr 6; if b in [0..2] then fmt.BiDiMode := TsBiDiMode(b); if b > 0 then Include(fmt.UsedFormattingFields, uffBiDi); // TextRotation case rec.TextRotation of XF_ROTATION_HORIZONTAL : fmt.TextRotation := trHorizontal; XF_ROTATION_90DEG_CCW : fmt.TextRotation := rt90DegreeCounterClockwiseRotation; XF_ROTATION_90DEG_CW : fmt.TextRotation := rt90DegreeClockwiseRotation; XF_ROTATION_STACKED : fmt.TextRotation := rtStacked; end; if fmt.TextRotation <> trHorizontal then Include(fmt.UsedFormattingFields, uffTextRotation); // Cell borders rec.Border_BkGr1 := DWordLEToN(rec.Border_BkGr1); rec.Border_BkGr2 := DWordLEToN(rec.Border_BkGr2); // the 4 masked bits encode the line style of the border line. 0 = no line dw := rec.Border_BkGr1 and MASK_XF_BORDER_LEFT; if dw <> 0 then begin Include(fmt.Border, cbWest); fmt.BorderStyles[cbWest].LineStyle := TsLineStyle(dw-1); Include(fmt.UsedFormattingFields, uffBorder); end; dw := rec.Border_BkGr1 and MASK_XF_BORDER_RIGHT; if dw <> 0 then begin Include(fmt.Border, cbEast); fmt.BorderStyles[cbEast].LineStyle := TsLineStyle((dw shr 4)-1); Include(fmt.UsedFormattingFields, uffBorder); end; dw := rec.Border_BkGr1 and MASK_XF_BORDER_TOP; if dw <> 0 then begin Include(fmt.Border, cbNorth); fmt.BorderStyles[cbNorth].LineStyle := TsLineStyle((dw shr 8)-1); Include(fmt.UsedFormattingFields, uffBorder); end; dw := rec.Border_BkGr1 and MASK_XF_BORDER_BOTTOM; if dw <> 0 then begin Include(fmt.Border, cbSouth); fmt.BorderStyles[cbSouth].LineStyle := TsLineStyle((dw shr 12)-1); Include(fmt.UsedFormattingFields, uffBorder); end; dw := rec.Border_BkGr2 and MASK_XF_BORDER_DIAGONAL; if dw <> 0 then begin fmt.BorderStyles[cbDiagUp].LineStyle := TsLineStyle((dw shr 21)-1); fmt.BorderStyles[cbDiagDown].LineStyle := fmt.BorderStyles[cbDiagUp].LineStyle; if rec.Border_BkGr1 and MASK_XF_BORDER_SHOW_DIAGONAL_UP <> 0 then Include(fmt.Border, cbDiagUp); if rec.Border_BkGr1 and MASK_XF_BORDER_SHOW_DIAGONAL_DOWN <> 0 then Include(fmt.Border, cbDiagDown); Include(fmt.UsedFormattingFields, uffBorder); end; // Border line colors // NOTE: It is possible that the palette is not yet known at this moment. // Therefore we store the palette index encoded into the colorx. // They will be converted to rgb in "FixColors". iclr := (rec.Border_BkGr1 and MASK_XF_BORDER_LEFT_COLOR) shr 16; fmt.BorderStyles[cbWest].Color := IfThen(iclr >= 64, scBlack, SetAsPaletteIndex(iclr)); iclr := (rec.Border_BkGr1 and MASK_XF_BORDER_RIGHT_COLOR) shr 23; fmt.BorderStyles[cbEast].Color := IfThen(iclr >= 64, scBlack, SetAsPaletteIndex(iclr)); iclr := (rec.Border_BkGr2 and MASK_XF_BORDER_TOP_COLOR); fmt.BorderStyles[cbNorth].Color := IfThen(iclr >= 64, scBlack, SetAsPaletteIndex(iclr)); iclr := (rec.Border_BkGr2 and MASK_XF_BORDER_BOTTOM_COLOR) shr 7; fmt.BorderStyles[cbSouth].Color := IfThen(iclr >= 64, scBlack, SetAsPaletteIndex(iclr)); iclr := (rec.Border_BkGr2 and MASK_XF_BORDER_DIAGONAL_COLOR) shr 14; fmt.BorderStyles[cbDiagUp].Color := IfThen(iclr >= 64, scBlack, SetAsPaletteIndex(iclr)); fmt.BorderStyles[cbDiagDown].Color := fmt.BorderStyles[cbDiagUp].Color; // Background fill pattern and color fill := (rec.Border_BkGr2 and MASK_XF_BACKGROUND_PATTERN) shr 26; if fill <> MASK_XF_FILL_PATT_EMPTY then begin rec.BkGr3 := DWordLEToN(rec.BkGr3); for fs in TsFillStyle do if fill = MASK_XF_FILL_PATT[fs] then begin // Pattern color iclr := rec.BkGr3 and $007F; fmt.Background.FgColor := IfThen(iclr = SYS_DEFAULT_FOREGROUND_COLOR, scBlack, SetAsPaletteIndex(iclr)); // Background color iclr := (rec.BkGr3 and $3F80) shr 7; fmt.Background.BgColor := IfThen(iclr = SYS_DEFAULT_BACKGROUND_COLOR, scTransparent, SetAsPaletteIndex(iclr)); // Fill style fmt.Background.Style := fs; Include(fmt.UsedFormattingFields, uffBackground); break; end; end; // Protection case WordLEToN(rec.XFType_Prot_ParentXF) and MASK_XF_TYPE_PROTECTION of 0: fmt.Protection := []; MASK_XF_TYPE_PROT_LOCKED: fmt.Protection := [cpLockCell]; MASK_XF_TYPE_PROT_FORMULA_HIDDEN: fmt.Protection := [cpHideFormulas]; MASK_XF_TYPE_PROT_LOCKED + MASK_XF_TYPE_PROT_FORMULA_HIDDEN: fmt.Protection := [cpLockCell, cpHideFormulas]; end; if fmt.Protection <> DEFAULT_CELL_PROTECTION then Include(fmt.UsedFormattingFields, uffProtection); // Add the XF to the internal cell format list FCellFormatList.Add(fmt); end; { Reads a DEFINEDNAME record. Currently only extract print ranges and titles. } procedure TsSpreadBIFF8Reader.ReadDEFINEDNAME(const AStream: TStream); var options: Word; len: byte; formulaSize: Word; widestr: WideString; defName: String; rpnformula: TsRPNFormula; rtf: TsRichTextParams; validOnSheet: Integer; begin // Options options := WordLEToN(AStream.ReadWord); if options and $0020 = 0 then // only support built-in names at the moment! exit; // Keyboard shortcut --> ignore AStream.ReadByte; // Length of name (character count) len := AStream.ReadByte; // Size of formula data formulasize := WordLEToN(AStream.ReadWord); // not used AStream.ReadWord; // Sheet index (1-based) on which the name is valid (0 = global) validOnSheet := SmallInt(WordLEToN(AStream.ReadWord)) - 1; // now 0-based! // Length of Menu text (ignore) AStream.ReadByte; // Length of description text(ignore) AStream.ReadByte; // Length of help topic text (ignore) AStream.ReadByte; // Length of status bar text (ignore) AStream.ReadByte; // Name wideStr := ReadWideString(AStream, len, rtf); defName := UTF8Encode(widestr); // Formula if not ReadRPNTokenArray(AStream, formulaSize, rpnFormula) then exit; // Store defined name in internal list FDefinedNames.Add(TsBIFFDefinedName.Create(defName, rpnFormula, validOnSheet)); // Skip rest... end; procedure TsSpreadBIFF8Reader.ReadEXTERNBOOK(const AStream: TStream); var i, n: Integer; url: widestring; sheetnames: widestring; book: TsBiff8Externbook; p: Int64; t: array[0..1] of byte = (0, 0); begin if FBiff8ExternBooks = nil then FBiff8ExternBooks := TsBIFF8ExternBookList.Create(true); book := TsBiff8ExternBook.Create; // Count of sheets in book n := WordLEToN(AStream.ReadWord); // Determine type of book p := AStream.Position; AStream.ReadBuffer(t[0], 2); if (t[0] = 1) and (t[1] = 4) then book.Kind := ebkInternal else if (t[0] = 1) and (t[1] = $3A) then book.Kind := ebkAddInFunc else if n = 0 then book.Kind := ebkDDE_OLE else book.Kind := ebkExternal; { External workbook } if (book.Kind = ebkExternal) then begin AStream.Position := p; // Encoded URL without sheet name (Unicode string, 16bit string length) url := ReadWideString(AStream, false); book.DocumentURL := UTF8Encode(url); if n = 0 then sheetnames := '' else begin // Sheet names (Unicode strings with 16bit string length) sheetnames := ReadWideString(AStream, false); for i := 2 to n do sheetnames := sheetnames + widechar(#1) + ReadWideString(AStream, false); end; book.SheetNames := UTF8Encode(sheetNames); end; FBiff8ExternBooks.Add(book); end; { Reads an EXTERNSHEET record. Needed for 3d-references, named cells and print ranges. } procedure TsSpreadBIFF8Reader.ReadEXTERNSHEET(const AStream: TStream); var numItems: Word; i: Integer; begin numItems := WordLEToN(AStream.ReadWord); SetLength(FBiff8ExternSheetArray, numItems); for i := 0 to numItems-1 do begin AStream.ReadBuffer(FBiff8ExternSheetArray[i], Sizeof(FBiff8ExternSheetArray[i])); with FBiff8ExternSheetArray[i] do begin ExternBookIndex := WordLEToN(ExternBookIndex); FirstSheetIndex := WordLEToN(FirstSheetIndex); LastSheetIndex := WordLEToN(LastSheetIndex); end; end; end; { Reads a FONT record. The retrieved font is stored in the workbook's FontList. } procedure TsSpreadBIFF8Reader.ReadFONT(const AStream: TStream); var {%H-}lCodePage: Word; lHeight: Word; lOptions: Word; lColor: Word; lWeight: Word; Len: Byte; font: TsFont; rtParams: TsRichTextParams; isDefaultFont: Boolean; begin font := TsFont.Create; { Height of the font in twips = 1/20 of a point } lHeight := WordLEToN(AStream.ReadWord); font.Size := lHeight / 20; { Option flags } lOptions := WordLEToN(AStream.ReadWord); font.Style := []; if lOptions and $0001 <> 0 then Include(font.Style, fssBold); if lOptions and $0002 <> 0 then Include(font.Style, fssItalic); if lOptions and $0004 <> 0 then Include(font.Style, fssUnderline); if lOptions and $0008 <> 0 then Include(font.Style, fssStrikeout); { Color index } // The problem is that the palette is loaded after the font list; therefore // we do not know the rgb color of the font here. We store the palette index // ("SetAsPaletteIndex") and replace it by the rgb color after reading of the // palette and after reading the workbook globals records. As an indicator // that the font does not yet contain an rgb color a control bit is set in // the high-byte of the TsColor. lColor := WordLEToN(AStream.ReadWord); if lColor < 8 then // Use built-in colors directly otherwise the Workbook's FindFont would not find the font in ReadXF font.Color := FPalette[lColor] else if lColor = SYS_DEFAULT_WINDOW_TEXT_COLOR then font.Color := scBlack else font.Color := SetAsPaletteIndex(lColor); { Font weight } lWeight := WordLEToN(AStream.ReadWord); if lWeight = 700 then Include(font.Style, fssBold); { Escapement type } font.Position := TsFontPosition(WordLEToN(AStream.ReadWord)); { Underline type } if AStream.ReadByte > 0 then Include(font.Style, fssUnderline); { Font family } AStream.ReadByte(); { Character set } lCodepage := AStream.ReadByte(); {$ifdef FPSpreadDebug} DebugLn('Reading Font Codepage='+IntToStr(lCodepage)); {$endif} { Not used } AStream.ReadByte(); { Font name: Unicodestring, char count in 1 byte } Len := AStream.ReadByte(); font.FontName := ReadString(AStream, Len, rtParams); // rtParams is not used here. isDefaultFont := FFontList.Count = 0; { Add font to internal font list; will be transferred to workbook later because the font index in the internal list (= index in file) is not the same as the index the font will have in the workbook's fontlist! } FFontList.Add(font); { Excel does not have zero-based font #4! } if FFontList.Count = 4 then FFontList.Add(nil); if isDefaultFont then (Workbook as TsWorkbook).SetDefaultFont(font.FontName, font.Size); end; {@@ ---------------------------------------------------------------------------- Reads the (number) FORMAT record for formatting numerical data and stores the format strings in an internal stringlist. The strings are put at the index specified by the FORMAT record. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.ReadFORMAT(AStream: TStream); var fmtString: String; fmtIndex: Integer; begin // Record FORMAT, BIFF 8 (5.49): // Offset Size Contents // 0 2 Format index used in other records // 2 var Number format string (Unicode string, 16-bit string length) // From BIFF5 on: indexes 0..163 are built in fmtIndex := WordLEtoN(AStream.ReadWord); if fmtIndex = 0 then // "General" already in list exit; // 2 var. Number format string (Unicode string, 16-bit string length, ➜2.5.3) // fmtString := UTF8Encode(ReadWideString(AStream, False)); fmtString := UTF16ToUTF8(ReadWideString(AStream, False)); // Add to the list at the specified index. If necessary insert empty strings while NumFormatList.Count <= fmtIndex do NumFormatList.Add(''); NumFormatList[fmtIndex] := fmtString; end; {@@ ---------------------------------------------------------------------------- Reads the header/footer to be used for printing. Overriden for BIFF8 because of wide strings -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.ReadHeaderFooter(AStream: TStream; AIsHeader: Boolean); var s: widestring; len: word; rtParams: TsRichTextParams; begin if RecordSize = 0 then exit; len := WordLEToN(AStream.ReadWord); if len = 0 then exit; s := ReadWideString(AStream, len, rtParams); with (FWorksheet as TsWorksheet).Pagelayout do if AIsHeader then Headers[1] := UTF8Encode(s) else Footers[1] := UTF8Encode(s); { Options poDifferentFirst and poDifferentOddEvent are not used, BIFF supports only common headers/footers } end; {@@ ---------------------------------------------------------------------------- Reads a HYPERLINK record -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.ReadHyperlink(const AStream: TStream); var row, col, row1, col1, row2, col2: word; guid: TGUID; flags: DWord; widestr: widestring = ''; len: DWord; link: String; linkDos: String; mark: String; dirUpCount: Word; ansistr: ansistring = ''; size: DWord; begin { Row and column index range of cells using the hyperlink } row1 := WordLEToN(AStream.ReadWord); row2 := WordLEToN(AStream.ReadWord); col1 := WordLEToN(AStream.ReadWord); col2 := WordLEToN(AStream.ReadWord); { GUID of standard link } AStream.ReadBuffer(guid{%H-}, SizeOf(guid)); { unknown DWord } AStream.ReadDWord; { Flags } flags := DWordLEToN(AStream.ReadDWord); { Description } if flags and MASK_HLINK_DESCRIPTION = MASK_HLINK_DESCRIPTION then begin // not used because there is always a "normal" cell to which the hyperlink is associated. // character count of description incl trailing zero len := DWordLEToN(AStream.ReadDWord); // Character array (16-bit characters, with trailing zero word) SetLength(wideStr, len); AStream.ReadBuffer(wideStr[1], len*SizeOf(wideChar)); end; { Target frame: external link (URI or local file) } link := ''; if flags and MASK_HLINK_LINK <> 0 then begin AStream.ReadBuffer(guid, SizeOf(guid)); // Check for URL if GuidToString(guid) = '{79EAC9E0-BAF9-11CE-8C82-00AA004BA90B}' then begin // Size of character array incl trailing zero size := DWordLEToN(AStream.ReadDWord); // Character array of URL (16-bit-characters, with trailing zero word) // See 3 lines below: This buffer is too large! len := size div 2 - 1; SetLength(wideStr, len); AStream.ReadBuffer(wideStr[1], size); // The buffer can be larger than the space occupied by the wideStr. // --> Find true string length and convert wide string to utf-8. // len := StrLen(PWideChar(widestr)); // wp: working fine except for Laz1.0 len := Length(widestr); // Is this ok? SetLength(widestr, len); link := UTF8Encode(widestr); end else // Check for local file if GuidToString(guid) = '{00000303-0000-0000-C000-000000000046}' then begin dirUpCount := WordLEToN(AStream.ReadWord); // Character count of the shortened file path and name, incl trailing zero byte len := DWordLEToN(AStream.ReadDWord); // Character array of the shortened file path and name in 8.3-DOS-format. // This field can be filled with a long file name too. // No unicode string header, always 8-bit characters, zero-terminated. SetLength(ansiStr, len); AStream.ReadBuffer(ansiStr[1], len*SizeOf(ansiChar)); SetLength(ansistr, len-1); // Remove trailing zero while dirUpCount > 0 do begin ansistr := '..' + PathDelim + ansistr; dec(dirUpCount); end; linkDos := AnsiToUTF8(ansiStr); // 6 unknown DWord values AStream.ReadDWord; AStream.ReadDWord; AStream.ReadDWord; AStream.ReadDWord; AStream.ReadDWord; AStream.ReadDWord; // Size of the following file link field including string length field // and additional data field size := DWordLEToN(AStream.ReadDWord); if size > 0 then begin // Size of the extended file path and name. size := DWordLEToN(AStream.ReadDWord); len := size div 2; // Unknown AStream.ReadWord; // Character array of the extended file path and name // no Unicode string header, always 16-bit characters, not zero-terminated SetLength(wideStr, len); AStream.ReadBuffer(wideStr[1], size); link := UTF8Encode(wideStr); end else link := linkDos; // An absolute path must be a fully qualified URI to be compatible with fps if flags and MASK_HLINK_ABSOLUTE <> 0 then link := FilenameToURI(link); end; end; { Text mark } if flags and MASK_HLINK_TEXTMARK = MASK_HLINK_TEXTMARK then begin // Character count of the text mark, including trailing zero word len := DWordLEToN(AStream.ReadDWord); // Character array of the text mark without "#" sign // no Unicode string header, always 16-bit characters, zero-terminated SetLength(wideStr, len); AStream.ReadBuffer(wideStr[1], len*SizeOf(wideChar)); SetLength(wideStr, len-1); // Remove trailing zero word mark := UTF8Encode(wideStr); end; // Add bookmark to hyperlink target if (link <> '') and (mark <> '') then link := link + '#' + mark else if (link = '') then link := '#' + mark; // Add hyperlink(s) to worksheet for row := row1 to row2 do for col := col1 to col2 do TsWorksheet(FWorksheet).WriteHyperlink(row, col, link); end; {@@ ---------------------------------------------------------------------------- Reads a HYPERLINK TOOLTIP record -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Reader.ReadHyperlinkToolTip(const AStream: TStream); var txt: String; widestr: widestring = ''; //row, col, row1, col1, row2, col2: Word; hyperlink: PsHyperlink; numbytes: Integer; sheet: TsWorksheet; begin { Record type; this matches the BIFF record type } AStream.ReadWord; { Row and column index range of cells using the hyperlink tooltip } row1 := WordLEToN(AStream.ReadWord); row2 := WordLEToN(AStream.ReadWord); col1 := WordLEToN(AStream.ReadWord); col2 := WordLEToN(AStream.ReadWord); { Hyperlink tooltip, a null-terminated unicode string } numbytes := RecordSize - 5*SizeOf(word); SetLength(wideStr, numbytes div 2); AStream.ReadBuffer(wideStr[1], numbytes); SetLength(wideStr, Length(wideStr)-1); // Remove trailing zero word txt := UTF8Encode(wideStr); { Add tooltip to hyperlinks } sheet := FWorksheet as TsWorksheet; for hyperlink in sheet.Hyperlinks.GetRangeEnumerator(row1, col1, row2, col2) do hyperlink^.ToolTip := txt; end; {------------------------------------------------------------------------------} { TsSpreadBIFF8Writer } {------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Constructor of the Excel 8 writer -------------------------------------------------------------------------------} constructor TsSpreadBIFF8Writer.Create(AWorkbook: TsBasicWorkbook); begin inherited Create(AWorkbook); InitBiff8Limitations(FLimitations); FDateMode := Excel8Settings.DateMode; PopulateSharedStringTable(AWorkbook); end; destructor TsSpreadBIFF8Writer.Destroy; begin FSharedStringTable.Free; FBiff8ExternSheets.Free; FBiff8ExternBooks.Free; inherited; end; {@@ ---------------------------------------------------------------------------- Writes the header of a CONTINUE record. Since the size of the record is not known at this moment the position of the BIFF size field is returned in ASizePos. Later the size field must be corrected by calling FixRecordSize -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.BeginCONTINUERecord(AStream: TStream; out ASizePos: Int64); begin AStream.WriteWord(WordToLE(INT_EXCEL_ID_CONTINUE)); ASizePos := AStream.Position; AStream.WriteWord(0); end; procedure DoCollectSheetsWith3dRefs(ANode: TsExprNode; AData1, AData2: Pointer; var MustRebuildFormulas: Boolean); var sheetlist: TsBIFF8ExternSheetList; sheetIdx, sheetIdx1, sheetIdx2: Integer; begin Unused(AData2, MustRebuildFormulas); sheetlist := TsBIFF8ExternSheetList(AData1); if (ANode is TsCellExprNode) and TsCellExprNode(ANode).Has3DLink then begin sheetIdx := TsCellExprNode(ANode).GetSheetIndex; sheetList.AddSheets('', nil, sheetIdx, sheetIdx); end else if (ANode is TsCellRangeExprNode) and TsCellRangeExprNode(ANode).Has3DLink then begin sheetIdx1 := TsCellRangeExprNode(ANode).GetSheetIndex(1); sheetIdx2 := TsCellRangeExprNode(ANode).GetSheetIndex(2); for sheetIdx := sheetIdx1 to sheetIdx2 do sheetList.AddSheets('', nil, sheetIdx1, sheetIdx2); end; end; {@@ ---------------------------------------------------------------------------- Collects the data for out-of-sheet links found in the specified worksheet (or all worksheets if the parameter is omitted). The found data are written to the FExternBooks and FExternSheets lists. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.CollectExternData; procedure DoCollectForSheet(ASheet: TsWorksheet); var formula: PsFormula; begin for formula in ASheet.Formulas do formula^.Parser.IterateNodes(@DoCollectSheetsWith3dRefs, FBiff8ExternSheets, nil); end; var book: TsWorkbook; sheet: TsWorksheet; i: Integer; writeIt: Boolean; begin if FBiff8ExternBooks <> nil then raise Exception.Create('[TsSpreadBIFF8Writer.CollectExternData] Can be entered only once.'); FBiff8ExternBooks := TsBIFF8ExternBookList.Create; FBiff8ExternSheets := TsBIFF8ExternSheetList.Create(FBiff8ExternBooks); book := FWorkbook as TsWorkbook; { Add sheets used in print ranges, repeated cols or repeated rows } for i:=0 to book.GetWorksheetCount-1 do begin sheet := book.GetWorksheetByIndex(i); with sheet.PageLayout do writeIt := (NumPrintRanges > 0) or HasRepeatedCols or HasRepeatedRows; if writeIt then FBiff8ExternSheets.AddInternalSheets(i, i); end; { Add sheets related to 3d references of all sheets } for i:=0 to book.GetWorksheetCount-1 do begin sheet := book.GetWorksheetByIndex(i); DoCollectForSheet(sheet); end; if FBiff8ExternSheets.Count = 0 then begin FreeAndNil(FBiff8ExternSheets); FreeAndNil(FBiff8ExternBooks); end; end; {@@ ---------------------------------------------------------------------------- Sometimes the size of records is not known when writing them (see BeginCONTINUERecord). This method rewinds the stream to the position where the record size is expected (ASizePos) and writes the record size (ASize). Then the stream returns to its original position. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.FixRecordSize(AStream: TStream; ASizePos: Int64; ASize: Word); var p: Int64; begin p := AStream.Position; AStream.Position := ASizePos; AStream.WriteWord(WordToLE(ASize)); AStream.Position := p; end; function TsSpreadBIFF8Writer.GetPrintOptions: Word; Begin Result := inherited GetPrintOptions; { The following flags are valid for BIFF8 only: Bit 9: 0 = Print notes as displayed; 1 = Print notes at end of sheet Bit 11-10: 00 = Print errors as displayed; 1 = Do not print errors 2 = Print errors as “--”; 3 = Print errors as “#N/A” } if poCommentsAtEnd in (FWorksheet as TsWorksheet).PageLayout.Options then Result := Result or $0200; end; {@@ ---------------------------------------------------------------------------- If the specified string and richtextparams are stored in the SharedStringTable then the index is returned, otherwise -1 -------------------------------------------------------------------------------} function TsSpreadBIFF8Writer.IndexOfSharedString(const AText: String; const ARichTextParams: TsRichTextParams): Integer; var s: String; begin if FSharedStringTable <> nil then begin s := CombineTextAndRichTextParams(AText, ARichTextParams); Result := FSharedStringTable.Find(s); end else Result := -1; end; {@@ ---------------------------------------------------------------------------- Writes an Excel BIFF8 record structure to a stream Be careful as this method doesn't write the OLE part of the document, just the BIFF records -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.InternalWriteToStream(AStream: TStream); const isBIFF8 = true; var currentPos: Int64; sheetPos: array of Int64 = nil; i: Integer; pane: Byte; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; CollectExternData; { Write workbook globals } WriteBOF(AStream, INT_BOF_WORKBOOK_GLOBALS); WriteCodePage(AStream, 'ucs2le'); // = utf-16 WriteWindowProtect(AStream, bpLockWindows in Workbook.Protection); WritePROTECT(AStream, bpLockStructure in Workbook.Protection); WritePASSWORD(AStream, book.CryptoInfo); WriteWINDOW1(AStream); WriteFonts(AStream); WriteNumFormats(AStream); WritePalette(AStream); WriteXFRecords(AStream); WriteStyle(AStream); // A BOUNDSHEET for each worksheet SetLength(sheetPos, book.GetWorksheetCount); for i := 0 to book.GetWorksheetCount - 1 do sheetPos[i] := WriteBoundsheet(AStream, book.GetWorksheetByIndex(i)); WriteEXTERNBOOK(AStream, ''); WriteEXTERNSHEET(AStream); WriteDefinedNames(AStream); WriteSST(AStream); WriteEOF(AStream); { Write each worksheet } for i := 0 to book.GetWorksheetCount - 1 do begin FWorksheet := book.GetWorksheetByIndex(i); { First goes back and writes the position of the BOF of the sheet on the respective BOUNDSHEET record } currentPos := AStream.Position; AStream.Position := sheetPos[i]; AStream.WriteDWord(DWordToLE(DWORD(CurrentPos))); AStream.Position := currentPos; WriteBOF(AStream, INT_BOF_SHEET); WriteIndex(AStream); WritePrintHeaders(AStream); WritePrintGridLines(AStream); WriteDefaultRowHeight(AStream, FWorksheet); WriteSheetPR(AStream); // Page setting block WriteHorizontalPageBreaks(AStream, FWorksheet); WriteVerticalPageBreaks(AStream, FWorksheet); WriteHeaderFooter(AStream, true); WriteHeaderFooter(AStream, false); WriteHCenter(AStream); WriteVCenter(AStream); WriteMargin(AStream, 0); // 0 = left margin WriteMargin(AStream, 1); // 1 = right margin WriteMargin(AStream, 2); // 2 = top margin WriteMargin(AStream, 3); // 3 = bottom margin WritePageSetup(AStream); WriteSheetLayout(AStream); // Protection if FWorksheet.IsProtected then begin WritePROTECT(AStream, true); // WriteScenarioProtect(AStream); WriteObjectProtect(AStream, FWorksheet); WritePASSWORD(AStream, (FWorksheet as TsWorksheet).CryptoInfo); end; WriteDefaultColWidth(AStream, FWorksheet); WriteColInfos(AStream, FWorksheet); WriteDimensions(AStream, FWorksheet); if (boVirtualMode in Workbook.Options) then WriteVirtualCells(AStream, FWorksheet) else begin WriteRows(AStream, FWorksheet); WriteCellsToStream(AStream, (FWorksheet as TsWorksheet).Cells); WriteComments(AStream, FWorksheet); end; // View settings block WriteWindow2(AStream, FWorksheet); WriteSCLRecord(AStream, FWorksheet); WritePane(AStream, FWorksheet, isBIFF8, pane); WriteSelection(AStream, FWorksheet, pane); WriteHyperlinks(AStream, FWorksheet); WriteMergedCells(AStream, FWorksheet); WriteConditionalFormatting(AStream, FWorksheet); WriteEOF(AStream); end; { Cleanup } SetLength(sheetPos, 0); end; {@@ ---------------------------------------------------------------------------- Populates the palette of the writer with the colors used by the workbook. BIFF8 begins with the 8 default colors which are duplicated. Then the user colors follow up to a max of total 64 entries. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.PopulatePalette(AWorkbook: TsBasicWorkbook); var i: Integer; begin FPalette.Clear; FPalette.AddBuiltinColors(true); FPalette.CollectFromWorkbook(AWorkbook); { Fill up Excel colors of the standard palette to avoid empty color place holders in Excel's colordialog. } i := 16; while (i <= High(PALETTE_BIFF8)) and (FPalette.Count < 64) do begin FPalette.AddColor(PALETTE_BIFF8[i]); // The BIFF8 palette contains duplicate colors -> don't use AddUniqueColor inc(i); end; end; {@@ ---------------------------------------------------------------------------- Collects all strings of the workbook in the SharedStringTable -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.PopulateSharedStringTable(AWorkbook: TsBasicWorkbook); var i, idx: Integer; cell: PCell; sheet: TsWorksheet; book: TsWorkbook absolute AWorkbook; s: String; begin FNumStrings := 0; FSharedStringTable := TStringHashList.Create(true); for i:=0 to book.GetWorksheetCount-1 do begin sheet := book.GetWorksheetByIndex(i); for cell in sheet.Cells do begin if (cell^.ContentType <> cctUTF8String) then Continue; if (cell^.UTF8StringValue = '') then Continue; // Only strings longer than 255 characters will be put into SST (speed) if Length(cell^.UTF8StringValue) < 256 then continue; idx := IndexOfSharedString(cell^.UTF8StringValue, cell^.RichTextParams); inc(FNumStrings); if idx > -1 then Continue; s := CombineTextAndRichTextParams(cell^.UTF8StringValue, cell^.RichTextParams); FSharedStringTable.Add(s); end; end; end; {@@ ---------------------------------------------------------------------------- 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; const AOverwriteExisting: Boolean; AParams: TsStreamParams = []); var Stream: TStream; OutputStorage: TOLEStorage; OLEDocument: TOLEDocument; begin Unused(AParams); if (boBufStream in Workbook.Options) then begin Stream := TBufStream.Create end else Stream := TMemoryStream.Create; try InternalWriteToStream(Stream); OutputStorage := TOLEStorage.Create; try // Only one stream is necessary for any number of worksheets OLEDocument.Stream := Stream; OutputStorage.WriteOLEFile(AFileName, OLEDocument, AOverwriteExisting, 'Workbook'); finally OutputStorage.Free; end; finally Stream.Free; end; end; {@@ ---------------------------------------------------------------------------- Writes an Excel BIFF8 record structure to a stream containing the OLE envelope of the document. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteToStream(AStream: TStream; AParams: TsStreamParams = []); var OutputStorage: TOLEStorage; OLEDocument: TOLEDocument; stream: TStream; begin Unused(AParams); if (boBufStream in Workbook.Options) then stream := TBufStream.Create else stream := TMemoryStream.Create; try InternalWriteToStream(stream); OutputStorage := TOLEStorage.Create; try // Only one stream is necessary for any number of worksheets OLEDocument.Stream := stream; OutputStorage.WriteOLEStream(AStream, OLEDocument, 'Workbook'); finally OutputStorage.Free; end; finally stream.Free; end; end; {@@ ---------------------------------------------------------------------------- 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 } WriteBIFFHeader(AStream, INT_EXCEL_ID_BOF, 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; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 BOUNDSHEET record Always located in the workbook globals substream. One BOUNDSHEET is written for each worksheet. @return 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; AWorksheet: TsBasicWorksheet): Int64; var len: Byte; wideSheetName: WideString; sheetState: Byte; begin wideSheetName := UTF8Decode(AWorksheet.Name); len := Length(wideSheetName); { BIFF Record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_BOUNDSHEET, 8 + 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 } sheetState := IfThen(soHidden in AWorksheet.Options, 1, 0); AStream.WriteByte(sheetState); { 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; {@@ ---------------------------------------------------------------------------- Inherited method for writing a cell comment immediately after cell content. A writing method has been implemented by xlscommon. But in BIFF8, this must not do anything because comments are collected in a list and written en-bloc. See WriteComments. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteComment(AStream: TStream; ACell: PCell); begin // Nothing to do. Reverts the behavior introduced by xlscommon. Unused(AStream, ACell); end; {@@ ---------------------------------------------------------------------------- Writes all comments to the worksheet stream -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteComments(AStream: TStream; AWorksheet: TsBasicWorksheet); var index: Integer; comment: PsComment; sheet: TsWorksheet; begin exit; // Remove after comments can be written correctly {$warning TODO: Fix writing of cell comments in BIFF8 (file is readable by OpenOffice, but not by Excel)} sheet := AWorksheet as TsWorksheet; { At first we have to write all Escher-related records for all comments; MSODRAWING - OBJ - MSODRAWING - TXO } index := 1; for comment in sheet.Comments do begin if index = 1 then WriteMSODrawing1(AStream, sheet.Comments.Count, comment) else WriteMSODrawing2(AStream, comment, index); WriteOBJ(AStream, index); WriteMSODrawing3(AStream); WriteTXO(AStream, comment); inc(index); end; { The NOTE records for all comments follow subsequently. } index := 1; for comment in sheet.Comments do begin WriteNOTE(AStream, comment, index); inc(index); end; end; {@@ ---------------------------------------------------------------------------- Writes the records needed by Conditional Formatting -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteConditionalFormatting(AStream: TStream; AWorksheet: TsBasicWorksheet); begin // to do: implementation: write CFHEADER and CFRULE records. end; {@@ ---------------------------------------------------------------------------- Writes a DEFINEDNAME record. Implements only the builtin defined names for print ranges and titles! -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteDefinedName(AStream: TStream; AWorksheet: TsBasicWorksheet; const AName: String; AIndexToREF, ASheetIndex: Word; AKind: TsBIFFExternKind); procedure WriteRangeFormula(MemStream: TMemoryStream; ARange: TsCellRange; AIndexToRef, ACounter: Word); begin { Token for tArea3dR } MemStream.WriteByte($3B); { Index to REF entry in EXTERNSHEET record } MemStream.WriteWord(WordToLE(AIndexToREF)); { First row index } MemStream.WriteWord(WordToLE(ARange.Row1)); { Last row index } MemStream.WriteWord(WordToLE(ARange.Row2)); { First column index } MemStream.WriteWord(WordToLE(ARange.Col1)); { Last column index } MemStream.WriteWord(WordToLE(ARange.Col2)); { Token for list if formula refers to more than 1 range } if ACounter > 1 then MemStream.WriteByte($10); end; var memstream: TMemoryStream; rng: TsCellRange; j: Integer; idx: Integer; sheet: TsWorksheet absolute AWorksheet; begin Unused(ASheetIndex, AKind); // Since this is a variable length record we begin by writing the formula // to a memory stream memstream := TMemoryStream.Create; try case AName of #06: begin // Print range for j := 0 to sheet.PageLayout.NumPrintRanges-1 do begin rng := sheet.PageLayout.PrintRange[j]; WriteRangeFormula(memstream, rng, AIndexToRef, j+1); end; end; #07: begin // Print titles j := 1; if sheet.PageLayout.HasRepeatedCols then begin rng.Col1 := sheet.PageLayout.RepeatedCols.FirstIndex; rng.Col2 := sheet.PageLayout.RepeatedCols.LastIndex; if rng.Col2 = UNASSIGNED_ROW_COL_INDEX then rng.Col2 := rng.Col1; rng.Row1 := 0; rng.Row2 := 65535; WriteRangeFormula(memstream, rng, AIndexToRef, j); inc(j); end; if sheet.PageLayout.HasRepeatedRows then begin rng.Row1 := sheet.PageLayout.RepeatedRows.FirstIndex; rng.Row2 := sheet.PageLayout.RepeatedRows.LastIndex; if rng.Row2 = UNASSIGNED_ROW_COL_INDEX then rng.Row2 := rng.Row1; rng.Col1 := 0; rng.Col2 := 255; WriteRangeFormula(memstream, rng, AIndexToRef, j); end; end; else raise EFPSpreadsheetWriter.Create('Name not supported'); end; // case { BIFF record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_DEFINEDNAME, 16 + memstream.Size); // NOTE: 16 only valid for internal names !!!! { Option flags: built-in defined names only } AStream.WriteWord(WordToLE($0020)); { Keyboard shortcut (only for command macro names) } AStream.WriteByte(0); { Length of name (character count). Always 1 for builtin names } AStream.WriteByte(1); { Size of formula data } AStream.WriteWord(WordToLE(memstream.Size)); { not used } AStream.WriteWord(0); { Index to sheet (1-based) } idx := (FWorkbook as TsWorkbook).GetWorksheetIndex(AWorksheet); AStream.WriteWord(WordToLE(idx+1)); { Length of menu text } AStream.WriteByte(0); { Length of description text } AStream.WriteByte(0); { Length of help topic text } AStream.WriteByte(0); { Length of status bar text } AStream.WriteByte(0); { Name } if (Length(AName) = 1) and (AName[1] < #32) then AStream.WriteWord(WordToLE(ord(AName[1]) shl 8)) else raise EFPSpreadsheetWriter.Create('Name not supported.'); { Formula } memstream.Position := 0; AStream.CopyFrom(memstream, memstream.Size); finally memstream.Free; end; end; procedure TsSpreadBIFF8Writer.WriteDefinedNames(AStream: TStream); var bookIdx: Integer; book: TsWorkbook; sheet: TsWorksheet; i: Integer; begin if (FBiff8ExternBooks = nil) or (FBiff8ExternSheets = nil) then exit; book := FWorkbook as TsWorkbook; // Defined names in "internal" book only bookIdx := FBiff8ExternBooks.IndexOfInternalbook; for i:=0 to book.GetWorksheetCount-1 do begin sheet := book.GetWorksheetByIndex(i); if (sheet.PageLayout.NumPrintRanges > 0) or sheet.PageLayout.HasRepeatedCols or sheet.PageLayout.HasRepeatedRows then begin // idx := sheetList.IndexOfSheet(sheet.Name); // Write 1-based index. And negate it to indicate an internal reference. if sheet.PageLayout.NumPrintRanges > 0 then WriteDefinedName(AStream, sheet, #6, bookIdx, i, ebkInternal); if sheet.PageLayout.HasRepeatedCols or sheet.PageLayout.HasRepeatedRows then WriteDefinedName(AStream, sheet, #7, bookIdx, i, ebkInternal); end; end; 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: TsBasicWorksheet); var firstRow, lastRow, firstCol, lastCol: Cardinal; rec: TBIFF8_DimensionsRecord; begin { Determine sheet size } GetSheetDimensions(AWorksheet, firstRow, lastRow, firstCol, lastCol); { Populate BIFF record } rec.RecordID := WordToLE(INT_EXCEL_ID_DIMENSIONS); rec.RecordSize := WordToLE(14); rec.FirstRow := DWordToLE(firstRow); rec.LastRowPlus1 := DWordToLE(lastRow+1); rec.FirstCol := WordToLE(firstCol); rec.LastColPlus1 := WordToLE(lastCol+1); rec.NotUsed := 0; { Write BIFF record to stream } AStream.WriteBuffer(rec, SizeOf(rec)); end; {@@ ---------------------------------------------------------------------------- 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 } WriteBIFFHeader(AStream, INT_EXCEL_ID_EOF, 0); end; {@@ ---------------------------------------------------------------------------- Writes an EXTERNBOOK record needed for defined names and links. NOTE: This writes only the case for "internal references" required for print ranges and titles. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteEXTERNBOOK(AStream: TStream; AUrl: string); begin if (FBiff8ExternBooks = nil) or (FBiff8ExternBooks.Count = 0) then exit; { BIFF record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_EXTERNBOOK, 4); // To do: When external books are activated then the "4" must be replaced !!! { Current workbook -- assuming that it has index 0 in list FExternBook8 } if AUrl = '' then begin { Number of sheets in this workbook } AStream.WriteWord(WordToLE((FWorkbook as TsWorkbook).GetWorksheetCount)); { Relict from BIFF5 } AStream.WriteWord(WordToLE($0401)); end else raise Exception.Create('[WriteEXTERNBOOK] External books not supported.'); end; {@@ ---------------------------------------------------------------------------- Writes an EXTERNSHEET record needed for defined names and links. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteEXTERNSHEET(AStream: TStream); var n, i: Integer; sheetRef: PsBIFF8ExternSheet; book: TsBIFF8ExternBook; begin if (FBiff8ExternSheets = nil) or (FBiff8ExternBooks = nil) then exit; { Count the following REF structures } { We support only internal links. Once external links are supported the following code probably can be dropped. } n := 0; for i := 0 to FBiff8ExternSheets.Count-1 do begin sheetRef := FBiff8ExternSheets[i]; book := FBiff8ExternBooks[sheetRef^.ExternBookIndex]; if (book.Kind = ebkInternal) then inc(n); end; { BIFF record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_EXTERNSHEET, 2 + 6*n); { Write the determined count of REF structures } AStream.WriteWord(WordToLE(n)); for i:= 0 to FBiff8ExternSheets.Count-1 do begin sheetRef := FBiff8ExternSheets[i]; book := FBiff8ExternBooks[sheetRef^.ExternBookIndex]; if (book.Kind = ebkInternal) then begin AStream.WriteWord(WordToLE(sheetRef^.ExternBookIndex)); AStream.WriteWord(WordToLE(sheetRef^.FirstSheetIndex)); AStream.WriteWord(WordToLE(sheetRef^.LastSheetIndex)); end; end; end; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 FONT record. The font data is passed as an instance of TsFont -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteFONT(AStream: TStream; AFont: TsFont); var Len: Byte; WideFontName: WideString; optn: Word; begin if AFont = nil then // this happens for FONT4 in case of BIFF exit; if AFont.FontName = '' then raise EFPSpreadsheetWriter.Create('Font name not specified.'); if AFont.Size <= 0.0 then raise EFPSpreadsheetWriter.Create('Font size not specified.'); WideFontName := UTF8Decode(AFont.FontName); Len := Length(WideFontName); { BIFF Record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_FONT, 16 + Len * Sizeof(WideChar)); { Height of the font in twips = 1/20 of a point } AStream.WriteWord(WordToLE(round(AFont.Size*20))); { Option flags } optn := 0; if fssBold in AFont.Style then optn := optn or $0001; if fssItalic in AFont.Style then optn := optn or $0002; if fssUnderline in AFont.Style then optn := optn or $0004; if fssStrikeout in AFont.Style then optn := optn or $0008; AStream.WriteWord(WordToLE(optn)); { Color index } AStream.WriteWord(WordToLE(PaletteIndex(AFont.Color))); { Font weight } if fssBold in AFont.Style then AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_BOLD)) else AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_NORMAL)); { Escapement type } AStream.WriteWord(WordToLE(ord(AFont.Position))); { Underline type } if fssUnderline in AFont.Style then AStream.WriteByte(1) else 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; {@@ ---------------------------------------------------------------------------- Writes the Excel 8 FONT records needed for the fonts used in the workbook. -------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteFonts(AStream: TStream); var i: Integer; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; for i:=0 to book.GetFontCount-1 do WriteFONT(AStream, book.GetFont(i)); end; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 FORMAT record ("Format" is to be understood as "number format" here). -------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteFORMAT(AStream: TStream; ANumFormatStr: String; ANumFormatIndex: Integer); type TNumFormatRecord = packed record RecordID: Word; RecordSize: Word; FormatIndex: Word; FormatStringLen: Word; FormatStringFlags: Byte; end; var len: Integer; ws: widestring; rec: TNumFormatRecord; buf: array of byte = nil; begin ws := UTF8Decode(ANumFormatStr); len := Length(ws); { BIFF record header } rec.RecordID := WordToLE(INT_EXCEL_ID_FORMAT); rec.RecordSize := WordToLE(2 + 2 + 1 + len * SizeOf(WideChar)); { Format index } rec.FormatIndex := WordToLE(ANumFormatIndex); { Format string } { - length of string = 16 bits } rec.FormatStringLen := WordToLE(len); { - Widestring flags, 1 = regular unicode LE string } rec.FormatStringFlags := 1; { - Copy the text characters into a buffer immediately after rec } SetLength(buf, SizeOf(rec) + SizeOf(WideChar)*len); Move(rec, buf[0], SizeOf(rec)); Move(ws[1], buf[SizeOf(rec)], len*SizeOf(WideChar)); { Write out } AStream.WriteBuffer(buf[0], SizeOf(rec) + SizeOf(WideChar)*len); { Clean up } SetLength(buf, 0); end; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 HEADER or FOOTER record, depending on AIsHeader. Overridden because of wide string -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteHeaderFooter(AStream: TStream; AIsHeader: Boolean); var wideStr: WideString; len: Integer; id: Word; begin with (FWorksheet as TsWorksheet).PageLayout do if AIsHeader then begin if (Headers[HEADER_FOOTER_INDEX_ALL] = '') then exit; wideStr := UTF8Decode(Headers[HEADER_FOOTER_INDEX_ALL]); id := INT_EXCEL_ID_HEADER; end else begin if (Footers[HEADER_FOOTER_INDEX_ALL] = '') then exit; wideStr := UTF8Decode(Footers[HEADER_FOOTER_INDEX_ALL]); id := INT_EXCEL_ID_FOOTER; end; len := Length(wideStr); { BIFF record header } WriteBiffHeader(AStream, id, 3 + len*sizeOf(wideChar)); { 16-bit string length } AStream.WriteWord(WordToLE(len)); { Widestring flags, 1=regular unicode LE string } AStream.WriteByte(1); { Characters } AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * SizeOf(WideChar)); end; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 HYPERLINK record -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteHyperlink(AStream: TStream; AHyperlink: PsHyperlink; AWorksheet: TsBasicWorksheet); var temp: TStream; guid: TGUID; widestr: widestring; ansistr: ansistring; descr: String; fn: String; flags: DWord; size: Integer; cell: PCell; target, bookmark: String; u: TUri; isInternal: Boolean; dirUpCounter: Integer; begin cell := (AWorksheet as TsWorksheet).FindCell(AHyperlink^.Row, AHyperlink^.Col); if (cell = nil) or (AHyperlink^.Target='') then exit; descr := (AWorksheet as TsWorksheet).ReadAsText(cell); // Hyperlink description SplitHyperlink(AHyperlink^.Target, target, bookmark); u := ParseURI(AHyperlink^.Target); isInternal := (target = '') and (bookmark <> ''); fn := ''; // Name of local file if target <> '' then begin if (u.Protocol='') then fn := target else UriToFileName(target, fn); ForcePathDelims(fn); end; // Since the length of the record is not known in the first place we write // the data to a temporary stream at first. temp := TMemoryStream.Create; try { Cell range using the same hyperlink - we support only single cells } temp.WriteWord(WordToLE(cell^.Row)); // first row temp.WriteWord(WordToLE(cell^.Row)); // last row temp.WriteWord(WordToLE(cell^.Col)); // first column temp.WriteWord(WordToLE(cell^.Col)); // last column { GUID of standard link } guid := StringToGuid('{79EAC9D0-BAF9-11CE-8C82-00AA004BA90B}'); temp.WriteBuffer(guid, SizeOf(guid)); { unknown } temp.WriteDWord(DWordToLe($00000002)); { option flags } flags := 0; if isInternal then flags := MASK_HLINK_TEXTMARK or MASK_HLINK_DESCRIPTION else flags := MASK_HLINK_LINK; if SameText(u.Protocol, 'file') or SameText(u.Protocol, 'http') or SameText(u.Protocol, 'ftp') then flags := flags or MASK_HLINK_ABSOLUTE; if descr <> AHyperlink^.Target then flags := flags or MASK_HLINK_DESCRIPTION; // has description if bookmark <> '' then flags := flags or MASK_HLINK_TEXTMARK; // link contains a bookmark temp.WriteDWord(DWordToLE(flags)); { description } if flags and MASK_HLINK_DESCRIPTION <> 0 then begin widestr := UTF8Decode(descr); { Character count incl trailing zero } temp.WriteDWord(DWordToLE(Length(wideStr) + 1)); { Character array (16-bit characters), plus trailing zeros } temp.WriteBuffer(wideStr[1], (Length(wideStr)+1)*SizeOf(widechar)); end; if target <> '' then begin if (fn <> '') then // URI is a local file begin { GUID of file moniker } guid := StringToGuid('{00000303-0000-0000-C000-000000000046}'); temp.WriteBuffer(guid, SizeOf(guid)); { Convert to ansi - should be DOS 8.3, but this is not necessary } ansistr := UTF8ToAnsi(fn); { Directory-up level counter } dirUpCounter := 0; if not FileNameIsAbsolute(ansistr) then while (pos ('..' + PathDelim, ansistr) = 1) do begin inc(dirUpCounter); Delete(ansistr, 1, Length('..'+PathDelim)); end; temp.WriteWord(WordToLE(dirUpCounter)); { Character count of file name incl trailing zero } temp.WriteDWord(DWordToLe(Length(ansistr)+1)); { Character array of file name (8-bit characters), plus trailing zero } temp.WriteBuffer(ansistr[1], Length(ansistr)+1); { Unknown } temp.WriteDWord(DWordToLE($DEADFFFF)); temp.WriteDWord(0); temp.WriteDWord(0); temp.WriteDWord(0); temp.WriteDWord(0); temp.WriteDWord(0); { Size of following file link fields } widestr := UTF8ToUTF16(fn); size := 4 + 2 + Length(wideStr)*SizeOf(widechar); temp.WriteDWord(DWordToLE(size)); if size > 0 then begin { Character count of extended file name } temp.WriteDWord(DWordToLE(Length(widestr)*SizeOf(WideChar))); { Unknown } temp.WriteWord(WordToLE($0003)); { Character array, 16-bit characters, NOT ZERO-TERMINATED! } temp.WriteBuffer(widestr[1], Length(wideStr)*SizeOf(WideChar)); end; end else begin { Hyperlink target is a URL } widestr := UTF8Decode(target); { GUID of URL Moniker } guid := StringToGUID('{79EAC9E0-BAF9-11CE-8C82-00AA004BA90B}'); temp.WriteBuffer(guid, SizeOf(guid)); { Character count incl trailing zero } temp.WriteDWord(DWordToLE(Length(wideStr)+1)*SizeOf(wideChar)); { Character array plus trailing zero (16-bit characters), plus trailing zeros } temp.WriteBuffer(wideStr[1], (length(wideStr)+1)*SizeOf(wideChar)); end; end; // hkURI // Hyperlink contains a text mark (#) if bookmark <> '' then begin // Convert to 16-bit characters widestr := UTF8Decode(bookmark); { Character count of text mark, incl trailing zero } temp.WriteDWord(DWordToLE(Length(wideStr) + 1)); { Character array (16-bit characters) plus trailing zeros } temp.WriteBuffer(wideStr[1], (Length(wideStr)+1) * SizeOf(WideChar)); end; { BIFF record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_HYPERLINK, temp.Size); { Record data } temp.Position := 0; AStream.CopyFrom(temp, temp.Size); finally temp.Free; end; end; {@@ ---------------------------------------------------------------------------- Writes all hyperlinks -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteHyperlinks(AStream: TStream; AWorksheet: TsBasicWorksheet); var hyperlink: PsHyperlink; begin for hyperlink in (AWorksheet as TsWorksheet).Hyperlinks do begin { Write HYPERLINK record } WriteHyperlink(AStream, hyperlink, AWorksheet); { Write HYPERLINK TOOLTIP record } if hyperlink^.Tooltip <> '' then WriteHyperlinkTooltip(AStream, hyperlink^.Row, hyperlink^.Col, hyperlink^.Tooltip); end; end; {@@ ---------------------------------------------------------------------------- Writes a HYPERLINK TOOLTIP record -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteHyperlinkTooltip(AStream: TStream; const ARow, ACol: Cardinal; const ATooltip: String); var widestr: widestring; begin widestr := UTF8Decode(ATooltip); { BIFF record header } WriteBiffHeader(AStream, INT_EXCEL_ID_HLINKTOOLTIP, 10 + (Length(wideStr)+1) * SizeOf(widechar)); { Repeated record ID } AStream.WriteWord(WordToLe(INT_EXCEL_ID_HLINKTOOLTIP)); { Cell range using the same hyperlink tooltip - we support only single cells } AStream.WriteWord(WordToLE(ARow)); // first row AStream.WriteWord(WordToLE(ARow)); // last row AStream.WriteWord(WordToLE(ACol)); // first column AStream.WriteWord(WordToLE(ACol)); // last column { Tooltop characters, no length, but trailing zero } AStream.WriteBuffer(wideStr[1], (Length(widestr)+1)*SizeOf(wideChar)); end; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 INDEX record nm = (rl - rf - 1) / 32 + 1 (using integer division) -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteINDEX(AStream: TStream); begin { BIFF Record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_INDEX, 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 seems to be no problem just ignoring this part of the record } end; {@@ ---------------------------------------------------------------------------- Depending on the presence of Rich-text formatting information in the cell record, writes an Excel 8 LABEL record (string cell value only), or RSTRING record (string cell value + rich-text formatting runs). If the cell text (incl its rich-text formatting) is found in the SharedStringtable then LABELSST record is written. If the string length exceeds 32758 bytes, the string will be truncated, a note will be left in the workbooks log. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteLABEL(AStream: TStream; const ARow, ACol: Cardinal; const AValue: String; ACell: PCell); var L: Word; WideStr: WideString; rec: TBIFF8_LabelRecord; recSST: TBIFF8_LabelSSTRecord; buf: array of byte = nil; i, nRuns, idx: Integer; rtfRuns: TBiff8_RichTextFormattingRuns = nil; begin if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then exit; if Length(ACell^.UTF8StringValue) < 256 then idx := -1 // Strings shorter than 256 were not stored in SST. else idx := IndexOfSharedString(ACell^.UTF8StringValue, ACell^.RichTextParams); // If string is in SST write a LABELSST record if idx > -1 then begin recSST.RecordID := WordToLE(INT_EXCEL_ID_LABELSST); recSST.RecordSize := WordToLE(SizeOf(recSST) - SizeOf(TsBiffHeader)); recSST.Row := WordToLE(ARow); recSST.Col := WordToLE(ACol); recSST.XFIndex := WordToLE(FindXFIndex(ACell^.FormatIndex)); recSST.SSTIndex := DWordToLE(idx); AStream.Write(recSST, SizeOf(recSST)); exit; end; // If string is not in SST write a standard LABEL cell WideStr := UTF8Decode(FixLineEnding(AValue)); //to UTF16 if WideStr = '' then begin // Badly formatted UTF8String (maybe ANSI?) if Length(AValue)<>0 then begin //Quite sure it was an ANSI string written as UTF8, so raise exception. raise EFPSpreadsheetWriter.CreateFmt(rsUTF8TextExpectedButANSIFoundInCell, [GetCellString(ARow, ACol)]); end; Exit; end; if Length(WideStr) > FLimitations.MaxCharsInTextCell then begin // Rather than lose data when reading it, let the application programmer deal // with the problem or purposefully ignore it. SetLength(WideStr, FLimitations.MaxCharsInTextCell); //may corrupt the string (e.g. in surrogate pairs), but... too bad. Workbook.AddErrorMsg(rsTruncateTooLongCellText, [ FLimitations.MaxCharsInTextCell, GetCellString(ARow, ACol) ]); end; L := Length(WideStr); nRuns := Length(ACell^.RichTextParams); { BIFF record header } rec.RecordID := WordToLE(IfThen(nRuns > 0, INT_EXCEL_ID_RSTRING, INT_EXCEL_ID_LABEL)); rec.RecordSize := SizeOf(TBiff8_LabelRecord) - SizeOf(TsBiffHeader) + L *SizeOf(WideChar); if nRuns > 0 then inc(rec.RecordSize, SizeOf(Word) + nRuns * SizeOf(TBiff8_RichTextFormattingRun)); rec.RecordSize := WordToLE(rec.RecordSize); { BIFF record data } rec.Row := WordToLE(ARow); rec.Col := WordToLE(ACol); { Index to XF record, according to formatting } rec.XFIndex := WordToLE(FindXFIndex(ACell^.FormatIndex)); { Byte String with 16-bit length } rec.TextLen := WordToLE(L); { Byte flags } rec.TextFlags := 1; // means regular unicode LE encoding // Excel does not write the Rich-Text flag probably because rich-text info // is located differently in the RSTRING record. { Copy the text characters into a buffer immediately after rec } SetLength(buf, SizeOf(rec) + L*SizeOf(WideChar)); Move(rec, buf[0], SizeOf(Rec)); Move(WideStringToLE(WideStr)[1], buf[SizeOf(Rec)], L*SizeOf(WideChar)); { Write out buffer } AStream.WriteBuffer(buf[0], SizeOf(rec) + L*SizeOf(WideChar)); { Write rich-text information in case of RSTRING record } if (nRuns > 0) then begin { Write number of rich-text formatting runs } AStream.WriteWord(WordToLE(nRuns)); { Write array of rich-text formatting runs } SetLength(rtfRuns, nRuns); for i:=0 to nRuns-1 do begin // index of first character of formatted part, 0-based in file, 1-based in fps rtfRuns[i].FirstIndex := WordToLE(ACell^.RichTextParams[i].FirstIndex - 1); // Index of new font. Be aware of font #4 missing in BIFF! if ACell^.RichTextParams[i].FontIndex >= 4 then rtfRuns[i].FontIndex := WordToLE(ACell^.RichTextParams[i].FontIndex + 1) else rtfRuns[i].FontIndex := WordToLE(ACell^.RichTextParams[i].FontIndex); end; AStream.WriteBuffer(rtfRuns[0], nRuns * SizeOf(TBiff8_RichTextFormattingRun)); end; { Clean up } SetLength(rtfRuns, 0); SetLength(buf, 0); end; procedure TsSpreadBIFF8Writer.WriteMergedCells(AStream: TStream; AWorksheet: TsBasicWorksheet); const MAX_PER_RECORD = 1026; var n0, n: Integer; rng: PsCellRange; newRecord: Boolean; sheet: TsWorksheet absolute AWorksheet; begin n0 := sheet.MergedCells.Count; n := Min(n0, MAX_PER_RECORD); newRecord := true; for rng in sheet.MergedCells do begin if newRecord then begin newRecord := false; { BIFF record header } WriteBIFFHeader(AStream, INT_EXCEL_ID_MERGEDCELLS, 2 + n*8); { Number of cell ranges in this record } AStream.WriteWord(WordToLE(n)); end; { Write range data } AStream.WriteWord(WordToLE(rng^.Row1)); AStream.WriteWord(WordToLE(rng^.Row2)); AStream.WriteWord(WordToLE(rng^.Col1)); AStream.WriteWord(WordToLE(rng^.Col2)); dec(n); if n = 0 then begin newRecord := true; dec(n0, MAX_PER_RECORD); n := Min(n0, MAX_PER_RECORD); end; end; end; {@@ ---------------------------------------------------------------------------- Writes the first MSODRAWING record to file. It is needed for a comment attached to a cell, but also for embedded shapes (currently not supported).
  Structure of this record:
                                                    Type    Ver   Inst
     Dg container                                   $F002          0
       |--- FDG record                              $F008    0     1
       |--- SpGr container                          $F003          0
             |---- Sp container  (group shape)      $F004          0
             |       |---- FSpGr record             $F009    1     0
MSODRAWING1  |       |---- FSp record               $F00A    2     0
................................................................................
MSODRAWING2  |---- Sp container  (child shape)      $F004          0
                     |---- FSp record               $F00A    2    202 (Textbox)
                     |---- FOpt record              $F00B    3    13 (num props)
                     |---- Client anchor record     $F010    0     0
                     |---- Client data record       $F011    0     0
-------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteMSODrawing1(AStream: TStream; ANumShapes: Word; AComment: PsComment); const DRAWING_ID = 1; var len: DWord; tmpStream: TMemoryStream; begin tmpStream := TMemoryStream.Create; try { OfficeArtDgContainer record (container of drawing) } len := 224 + 152*(ANumShapes - 1); WriteMSODgContainer(tmpStream, len); { OfficeArtFdg record (info on shapes: num shapes, drawing ID, last Obj ID ) } WriteMSOFdgRecord(tmpStream, ANumShapes + 1, DRAWING_ID, SHAPEID_BASE + ANumShapes); { OfficeArtSpGrContainer record (shape group container) } len := 200 + 152*(ANumShapes - 1); WriteMSOSpGrContainer(tmpStream, len); { OfficeArtSpContainer record } WriteMSOSpContainer(tmpStream, 40); { OfficeArtFSpGr record } WriteMSOFSpGrRecord(tmpStream, 0, 0, 0, 0); // 16 + 8 bytes { OfficeArtFSp record } WriteMSOFSpRecord(tmpStream, SHAPEID_BASE, MSO_SPT_NOTPRIMITIVE, MSO_FSP_BITS_GROUP + MSO_FSP_BITS_PATRIARCH); // 8 + 8 bytes { Data for the 1st comment } WriteMSODrawing2_Data(tmpStream, AComment, SHAPEID_BASE + 1); { Write the BIFF stream } tmpStream.Position := 0; len := tmpStream.Size; WriteBiffHeader(AStream, INT_EXCEL_ID_MSODRAWING, tmpStream.Size); AStream.CopyFrom(tmpStream, tmpStream.Size); finally tmpStream.Free; end; end; {@@ ---------------------------------------------------------------------------- Writes the MSODRAWING record which occurs before the OBJ record. Not to be used for the very first OBJ record where the record must be WriteMSODrawing1 + WriteMSODrawing2_Data -------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteMSODrawing2(AStream: TStream; AComment: PsComment; AObjID: Word); var tmpStream: TStream; len: Word; begin tmpStream := TMemoryStream.Create; try { Shape data for cell comment } WriteMSODrawing2_Data(tmpStream, AComment, SHAPEID_BASE + AObjID); { Get size of data stream } len := tmpStream.Size; { BIFF Header } WriteBiffHeader(AStream, INT_EXCEL_ID_MSODRAWING, len); { Copy MSO data to BIFF stream } tmpStream.Position := 0; AStream.CopyFrom(tmpStream, len); finally tmpStream.Free; end; end; procedure TsSpreadBiff8Writer.WriteMSODrawing2_Data(AStream: TStream; AComment: PsComment; AShapeID: Word); var tmpStream: TStream; len: Cardinal; begin // We write all the record data to a temporary stream to get the record // size (it depends on the number of properties written to the FOPT record. // The record size is needed in the very first SpContainer record... tmpStream := TMemoryStream.Create; try { OfficeArtFSp record } WriteMSOFSpRecord(tmpStream, AShapeID, MSO_SPT_TEXTBOX, MSO_FSP_BITS_HASANCHOR + MSO_FSP_BITS_HASSHAPETYPE); { OfficeArtFOpt record } WriteMSOFOptRecord_Comment(tmpStream); { OfficeArtClientAnchor record } WriteMSOClientAnchorSheetRecord(tmpStream, AComment^.Row + 1, AComment^.Col + 1, AComment^.Row + 3, AComment^.Col + 5, 691, 486, 38, 26, true, true ); { OfficeArtClientData record } WriteMSOClientDataRecord(tmpStream); // Now we know the record size len := tmpStream.Size; // Write an OfficeArtSpContainer to the stream provided... WriteMSOSpContainer(AStream, len+8); // !!! for some reason, Excel wants here 8 additional bytes !!! // ... and append the data from the temporary stream. tmpStream.Position := 0; AStream.Copyfrom(tmpStream, len); finally tmpStream.Free; end; end; {@@ ---------------------------------------------------------------------------- Writes the MSODRAWING record which must occur immediately before a TXO record -------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteMSODRAWING3(AStream: TStream); begin { BIFF Header } WriteBiffHeader(AStream, INT_EXCEL_ID_MSODRAWING, 8); { OfficeArtClientTextbox record: Text-related data for a shape } WriteMSOClientTextBoxRecord(AStream); end; {@@ ---------------------------------------------------------------------------- Writes a NOTE record for a comment attached to a cell -------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteNOTE(AStream: TStream; AComment: PsComment; AObjID: Word); const AUTHOR: ansistring = 'author'; var len: Integer; begin len := Length(AUTHOR) * sizeOf(ansichar); { BIFF Header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_NOTE)); // ID of NOTE record AStream.WriteWord(WordToLE(12+len)); // Size of NOTE record { Record data } AStream.WriteWord(WordToLE(AComment^.Row)); // Row index of cell AStream.WriteWord(WordToLE(AComment^.Col)); // Column index of cell AStream.WriteWord(0); // Flags AStream.WriteWord(WordToLE(AObjID)); // Object identifier (1, ...) AStream.WriteWord(len); // Char length of author string AStream.WriteByte(0); // Flag for 8-bit characters AStream.WriteBuffer(AUTHOR[1], len); // Author AStream.WriteByte(0); // Unused end; {@@ ---------------------------------------------------------------------------- Writes an OBJ record - belongs to the records required for cell comments -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteOBJ(AStream: TStream; AObjID: Word); var guid: TGuid; begin AStream.WriteWord(WordToLE(INT_EXCEL_ID_OBJ)); AStream.WriteWord(WordToLE(52)); AStream.WriteWord(WordToLE($0015)); // Subrecord ftCmo AStream.WriteWord(WordToLE(18)); // Subrecord size: 18 bytes AStream.WriteWord(WordToLE($0019)); // Object type: Comment AStream.WriteWord(WordToLE(AObjID)); // Object ID number (1, ... ) AStream.WriteWord(WordToLE($4011)); // Option flags automatic line style, locked when sheet is protected AStream.WriteDWord(0); // Unused AStream.WriteDWord(0); // Unused AStream.WriteDWord(0); // Unused AStream.WriteWord(WordToLE($000D)); // Subrecord ftNts AStream.WriteWord(WordToLE(22)); // Size of subrecord: 22 bytes // CreateGUID(guid); FillChar(guid{%H-}, SizeOf(guid), 0); AStream.WriteBuffer(guid, 16); // GUID of comment AStream.WriteWord(WordToLE(0)); // shared note (0 = false) AStream.WriteDWord(0); // unused AStream.WriteWord(WordToLE($0000)); // Subrecord ftEnd AStream.WriteWord(0); // Size of subrecord: 0 bytes end; {@@ ---------------------------------------------------------------------------- Writes the address of a cell as used in an RPN formula and returns the number of bytes written. -------------------------------------------------------------------------------} function TsSpreadBIFF8Writer.WriteRPNCellAddress(AStream: TStream; ARow, ACol: Cardinal; AFlags: TsRelFlags): Word; var c: Cardinal; // column index with encoded relative/absolute address info begin AStream.WriteWord(WordToLE(ARow)); c := ACol and MASK_EXCEL_COL_BITS_BIFF8; if (rfRelRow in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW_BIFF8; if (rfRelCol in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL_BIFF8; AStream.WriteWord(WordToLE(c)); Result := 4; end; {@@ ---------------------------------------------------------------------------- Writes row and column offset needed in RPN formulas (unsigned integers!) Valid for BIFF2-BIFF5. -------------------------------------------------------------------------------} function TsSpreadBIFF8Writer.WriteRPNCellOffset(AStream: TStream; ARowOffset, AColOffset: Integer; AFlags: TsRelFlags): Word; var c: Word; r: SmallInt; begin // row address r := SmallInt(ARowOffset); AStream.WriteWord(WordToLE(Word(r))); // Encoded column address c := word(AColOffset) and MASK_EXCEL_COL_BITS_BIFF8; if (rfRelRow in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW_BIFF8; if (rfRelCol in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL_BIFF8; AStream.WriteWord(WordToLE(c)); Result := 4; end; {@@ ---------------------------------------------------------------------------- Writes the address of a cell range as used in an RPN formula and returns the count of bytes written. -------------------------------------------------------------------------------} function TsSpreadBIFF8Writer.WriteRPNCellRangeAddress(AStream: TStream; ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags): Word; var c: Cardinal; // column index with encoded relative/absolute address info begin AStream.WriteWord(WordToLE(ARow1)); AStream.WriteWord(WordToLE(ARow2)); c := ACol1; if (rfRelCol in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL; if (rfRelRow in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW; AStream.WriteWord(WordToLE(c)); c := ACol2; if (rfRelCol2 in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL; if (rfRelRow2 in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW; AStream.WriteWord(WordToLE(c)); Result := 8; end; function TsSpreadBIFF8Writer.WriteRPNSheetIndex(AStream: TStream; ADocumentURL: String; ASheet1, ASheet2: Integer): Word; var idx: Integer; begin idx := FBiff8ExternSheets.IndexOfSheets(ADocumentURL, ASheet1, ASheet2); if idx = -1 then Result := $FFFE // E at the end: sheets not found else begin AStream.WriteWord(WordToLE(word(idx))); Result := 2; end; end; { Writes a buffer containing a string (with header) and its associated rich-text parameters to an EXCEL record. Since the size in an EXCEL8 record is limited to 8224 bytes ABytesAvail specifies how many bytes are available. If the buffer is longer then the string is split at an appropriate position. The procedure also handles the case that the remaining bytes of the string/ rich-text are written in a following CONTINUE record. The number of written bytes is returned in the corresponding parameter. If a split has occured in the string part of the buffer the following CONTINUE record must begin with an Option byte; this is signaled by AContinueInString. The function returns TRUE if the entire buffer has been written completely, otherweise CONTINUE records are required. } function TsSpreadBIFF8Writer.WriteRichTextStream(AStream: TStream; ABuffer: TMemoryStream; ABytesAvail: Integer; out ABytesWritten: Integer; out AContinueInString: Boolean): Boolean; var strLen: Word; hasRtp: Boolean; nRtp: Integer; bytesToWrite: Integer; savedStartPos: Integer; strSize, rtpSize, hdrSize: Int64; begin Result := false; ABytesWritten := 0; AContinueInString := false; // Read string header and get string length and count of rich-text parameters savedStartPos := ABuffer.Position; ABuffer.Position := 0; strLen := LEToN(ABuffer.ReadWord); // string length (character count) hasRtp := ABuffer.ReadByte and 4 <> 0; // Rich-text params available? hdrSize := SizeOf(strlen) + SizeOf(byte); if hasRtp then begin nRtp := LEToN(ABuffer.ReadWord); // number of rich-text formatting runs inc(hdrSize, SizeOf(word)); rtpSize := Int64(nRtp) * 4; // 4 bytes per rich-text formatting run end else begin nRtp := 0; rtpSize := 0; end; strSize := Int64(strLen) * SizeOf(WideChar); // String length in bytes // Begin writing ABuffer.Position := savedStartPos; // Case 1: If the function has been called for the 1st time, // we must write the string header if ABuffer.Position = 0 then begin // Keep header plus 1st character together - they must not be separated bytesToWrite := hdrSize + SizeOf(WideChar); if ABytesAvail < bytesToWrite then begin // not enough memory left AContinueInString := true; // The following CONTINUE record must begin with the Options byte exit; end; AStream.CopyFrom(ABuffer, bytesToWrite); inc(ABytesWritten, bytesToWrite); end; // Case 2; Here some part of the string already has been written, and the // buffer stream is somewhere in the string part if ABuffer.Position < Int64(hdrSize) + Int64(strSize) - 1 then begin bytesToWrite := hdrSize + strSize - ABuffer.Position; if bytesToWrite > ABytesAvail then begin bytesToWrite := ABytesAvail; // Make sure to split between widechars if odd(bytesToWrite) then dec(bytesToWrite); AStream.CopyFrom(ABuffer, bytesToWrite); inc(ABytesWritten, bytesToWrite); AContinueInString := true; exit; end; AStream.CopyFrom(ABuffer, bytesToWrite); inc(ABytesWritten, bytesToWrite); end; // Case 3: The string has been written fully, but the buffer stream is somewhere // in the rich-text formatting runs if hasRtp and (ABuffer.Position >= hdrSize + strSize) then begin bytesToWrite := hdrSize + strSize + rtpSize - ABuffer.Position; if bytesToWrite > ABytesAvail then begin // Make sure to split between rich-text formatting runs. Each run is 4 bytes. bytesToWrite := (ABytesAvail div 4) * 4; AStream.CopyFrom(ABuffer, BytesToWrite); inc(ABytesWritten, bytesToWrite); exit; end; AStream.CopyFrom(ABuffer, BytesToWrite); inc(ABytesWritten, bytesToWrite); end; // If the procedure gets to this point the buffer has been written completely. Result := true; end; (* {@@ ---------------------------------------------------------------------------- Writes a selection range as part of the SELECTION record. Special BIFF8 version. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteSelectionRange(AStream: TStream; ARange: TsCellRange); begin // Index to first and last row of this selected range AStream.WriteWord(WordToLE(Word(ARange.Row1))); AStream.WriteWord(WordToLE(Word(ARange.Row2))); // Index to first and last column of this selected range // NOTE: The BIFF8 specification uses only a byte here for the value (256 cols) // but writes 2 bytes! AStream.WriteWord(WordToLE(Byte(ARange.Col1))); AStream.WriteWord(WordToLE(Byte(ARange.Col2))); end; *) {@@ ---------------------------------------------------------------------------- Writes a SHEETLAYOUT record which contains the color of the worksheet's tab offset size data 0 2 0862H (repeated record identifier) 2 10 Not used 12 4 Unknown data: 14H 00H 00H 00H 16 2 Colour index (➜5.74) for sheet name tab 18 2 Not used -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteSheetLayout(AStream: TStream); var i: Integer; iclr: Integer; sheet: TsWorksheet; begin sheet := TsWorksheet(FWorksheet); if sheet.TabColor = scNotDefined then exit; // Biff header WriteBIFFHeader(AStream, INT_EXCEL_ID_SHEETLAYOUT, 20); // repeated record identifier AStream.WriteWord(WordToLE(INT_EXCEL_ID_SHEETLAYOUT)); // not used for i:=1 to 5 do AStream.WriteWord(0); // Unknown data AStream.WriteByte($14); AStream.WriteByte(0); AStream.WriteByte(0); AStream.WriteByte(0); // palette index of tab color iclr := PaletteIndex(sheet.TabColor); AStream.WriteWord(WordToLE(iclr)); // not used AStream.WriteWord(0); end; {@@ ---------------------------------------------------------------------------- Writes the SharedStringTable (SST) to the stream -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteSST(AStream: TStream); function Is8BitString(s: String): boolean; var i: Integer; begin Result := false; for i:=1 to Length(s) do if s[i] > #127 then exit; Result := true; end; var sizePos: Int64; bytesWritten, totalBytesWritten: Integer; i, j: Integer; rtParams: TsRichTextParams; bytesAvail: Integer; isASCII: Boolean; textIndex, rtIndex: Integer; complete: Boolean; flag: Byte; ws: Widestring; s: String; rs: RawByteString; begin if FSharedStringTable.Count = 0 then exit; { Write BIFF header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_SST)); sizePos := AStream.Position; AStream.WriteWord(0); // Size of record will be written later when we know it { Number of strings in workbook } AStream.WriteDWord(DWordToLE(FNumStrings)); { Number of strings in SST } AStream.WriteDWord(DWordToLE(FSharedStringTable.Count)); { Now begins writing of strings. Take care of overflow into following CONTINUE records if the maximum record size (MAX_BYTES_IN_RECORD) is exceeded. } totalBytesWritten := 8; for i:=0 to FSharedStringTable.Count-1 do begin SplitTextAndRichTextParams(FSharedStringTable.List[i]^.Key, s, rtParams); s := FixLineEnding(s); isASCII := Is8BitString(s); if isASCII then begin rs := s; if Length(s) > FLimitations.MaxCharsInTextCell then begin SetLength(rs, FLimitations.MaxCharsInTextCell); FWorkbook.AddErrorMsg(rsTruncateTooLongText, [FLimitations.MaxCharsInTextCell]); end; end else begin ws := WideStringToLE(UTF8ToUTF16(s)); SetLength(rs, Length(ws) * SizeOf(widechar)); Move(ws[1], rs[1], Length(rs)); if Length(ws) > FLimitations.MaxCharsInTextCell then begin SetLength(ws, FLimitations.MaxCharsInTextCell); FWorkbook.AddErrorMsg(rsTruncateTooLongText, [FLimitations.MaxCharsInTextCell]); end; end; for j := 0 to High(rtParams) do // Be aware of font #4 missing in BIFF! if rtParams[j].FontIndex >= 4 then inc(rtParams[j].FontIndex); textIndex := 1; rtIndex := 0; repeat bytesAvail := MAX_BYTES_IN_RECORD - totalBytesWritten; bytesWritten := WriteStringHelper(AStream, rs, rtParams, isASCII, bytesAvail, textIndex, rtIndex, complete); inc(totalBytesWritten, bytesWritten); dec(bytesAvail, bytesWritten); // String is not complete --> we need a CONTINUE record if not complete then begin FixRecordSize(AStream, sizePos, totalBytesWritten); BeginCONTINUERecord(AStream, sizePos); if (textIndex <> -1) and (textIndex <> 1) then begin // Text is split: the string flag must be repeated flag := IfThen(IsASCII, 0, 1); AStream.WriteByte(flag); totalBytesWritten := 1; end else totalBytesWritten := 0; end; until complete; end; // Write size word of the current record FixRecordSize(AStream, sizePos, totalBytesWritten); end; {@@ ---------------------------------------------------------------------------- Helper function for writing a string with 8-bit length. Overridden version for BIFF8. Called for writing rpn formula string tokens. Returns the count of bytes written. -------------------------------------------------------------------------------} function TsSpreadBIFF8Writer.WriteString_8BitLen(AStream: TStream; AString: String): Integer; var len: Integer; wideStr: WideString; begin // string constant is stored as widestring in BIFF8 wideStr := UTF8Decode(AString); len := Length(wideStr); AStream.WriteByte(len); // char count in 1 byte AStream.WriteByte(1); // Widestring flags, 1=regular unicode LE string AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * Sizeof(WideChar)); Result := 1 + 1 + len * SizeOf(WideChar); end; {@@ ---------------------------------------------------------------------------- Helper function for writing a string which considers overflow into CONTINUE records. @@param AText Text to be written @@param ARichTextParams optional rich-text formatting runs for formatting individual characters @@param Is8BitString if true the string is a compressed 8-bit string, otherwise a widestring @@param ABytesAvail Specifies the number of bytes available in the current record. A BIFF record can hold only MAX_BYTES_IN_RECORD bytes. @@param ATextIndex Index at which writing of the text begins. @@param ARichIndex Index at which writing of the rich-text parameters begins @@param AComplete TRUE if writing is complete, FALSE if text or rich-text parameters do not fit into the current record. In the latter case ATextIndex and ARichIndex are updated for the next call of the method. ATextIndex returns -1, if the text is complete. @@return Number of bytes written -------------------------------------------------------------------------------} function TsSpreadBIFF8Writer.WriteStringHelper(AStream: TStream; const AText: RawByteString; const ARichTextParams: TsRichTextParams; Is8BitString: Boolean; ABytesAvail: Integer; var ATextIndex, ARichIndex: Integer; out AComplete: Boolean): Integer; const COMPRESSED_FLAG: array[boolean] of byte = (1, 0); var hdrSize: Integer; bytesToWrite: Integer; flags: Byte; len: Word; nRtp: Integer; rtp: TsRichTextParam; begin Result := 0; AComplete := false; // Text length in characters len := Length(AText); if not Is8BitString then len := len div 2; // Number of rich-text parameters nRtp := Length(ARichTextParams); // (1) String header if (ATextIndex = 1) then begin hdrSize := IfThen(nRtp = 0, 3, 3+2); // String header plus 1st character do not fit into current record // ---> the caller must move everything to a CONTINUE record if ABytesAvail < hdrSize + SizeOf(WideChar) then exit; { Write string length (in characters)} AStream.WriteWord(WordToLE(len)); inc(Result, 2); { Write string flags byte } flags := COMPRESSED_FLAG[Is8BitString]; if Length(ARichTextParams) > 0 then inc(flags, 8); // 8 = has rich-text formatting runs // Note: Asian phonetic not supported here! AStream.WriteByte(flags); inc(Result, 1); { Write number of rich-text formatting runs } if Length(ARichTextParams) > 0 then begin AStream.WriteWord(WordToLE(nRtp)); inc(Result, 2); end; end; // (2) String characters if ATextIndex <> -1 then begin bytesToWrite := Length(AText) - ATextIndex + 1; if bytesToWrite > ABytesAvail - Result then begin bytesToWrite := ABytesAvail - Result; // Make sure to split widestring between widechars if not Is8bitString and odd(bytesToWrite) then dec(bytesToWrite); inc(Result, AStream.Write(AText[ATextIndex], bytesToWrite)); inc(ATextIndex, bytesToWrite); exit; end; inc(Result, AStream.Write(AText[ATextIndex], bytesToWrite)); ATextIndex := -1; // String is complete here end; // (3) Rich-text formatting runs if nRtp = 0 then begin AComplete := true; exit; end; while (ARichIndex < nRtp) and (ABytesAvail - Result >= 4) do begin rtp := ARichTextParams[ARichIndex]; if rtp.FirstIndex > len then begin ARichIndex := MaxInt; break; end; // Make sure to split between runs AStream.WriteWord(WordToLE(rtp.FirstIndex - 1)); // character index is 0-based in file, but 1-based in fps AStream.WriteWord(WordtoLE(rtp.FontIndex)); inc(Result, 4); inc(ARichIndex); end; AComplete := (ARichIndex >= nRtp); end; {@@ ---------------------------------------------------------------------------- Write the result of a string formula in the preceding record. In BIFF8 files no STRING record occurs, if the result string is empty. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteSTRINGRecord(AStream: TStream; AString: String); // wp: This method might be imcomplete: // - Missing call to FixLineEnding() // - Missing RichText // - Missing check for length (max 8224 bytes per record, else use CONTINUE) var wideStr: widestring; len: Integer; strBytes: Integer; idx: Integer; needCONTINUE: Boolean; begin if AString = '' then exit; wideStr := WideStringToLE(UTF8Decode(FixLineEnding(AString))); len := Length(wideStr); strBytes := len * SizeOf(WideChar); needCONTINUE := 3 + strBytes > MAX_BYTES_IN_RECORD; if needCONTINUE then strBytes := MAX_BYTES_IN_RECORD - 4; // -4 = -3 (header) - 1 (even byte count) { BIFF STRING record header} AStream.WriteWord(WordToLE(INT_EXCEL_ID_STRING)); AStream.WriteWord(WordToLE(3 + strBytes)); { Write widestring length } AStream.WriteWord(WordToLE(len)); { Widestring flags, 1=regular unicode LE string } AStream.WriteByte(1); { Write characters } AStream.WriteBuffer(wideStr[1], strBytes); idx := 1 + strBytes div SizeOf(WideChar); while needCONTINUE and (idx < len) do begin strBytes := (len - idx) * SizeOf(WideChar); needCONTINUE := strBytes + 1 > MAX_BYTES_IN_RECORD; if needCONTINUE then strBytes := MAX_BYTES_IN_RECORD - 2; // -2 = -1 (flag byte) - 1 (for even count) { BIFF CONTINUE record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_CONTINUE)); AStream.WriteWord(WordToLE(1 + strBytes)); // for flag byte { Widestring flags, 1 = regular unicode LE string } AStream.WriteByte(1); {Write characters } AStream.WriteBuffer(wideStr[idx], strBytes); inc(idx, strBytes div SizeOf(WideChar)); end; end; {@@----------------------------------------------------------------------------- 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 } WriteBiffHeader(AStream, INT_EXCEL_ID_STYLE, 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; {@@ ---------------------------------------------------------------------------- Writes a TXO and two CONTINUE records as needed for cell comments. It can safely be assumed that the cell exists and contains a comment. -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteTXO(AStream: TStream; AComment: PsComment); var recTXO: TBIFF8TXORecord; comment: widestring; compressed: ansistring = ''; len: Integer; wchar: widechar; i: Integer; bytesFmtRuns: Integer; begin { Prepare comment string. It is stored as a string with 8-bit characters } comment := UTF8Decode(AComment^.Text); SetLength(compressed, length(comment)); for i:= 1 to Length(comment) do begin wchar := comment[i]; compressed[i] := wchar; end; len := Length(compressed); { (1) TXO record ---------------------------------------------------------- } { BIFF record header } FillChar(recTXO{%H-}, SizeOf(recTXO), 0); recTXO.RecordID := WordToLE(INT_EXCEL_ID_TXO); recTXO.RecordSize := SizeOf(recTXO) - 2*SizeOf(word); { Record data } recTXO.OptionFlags := WordToLE($0212); // Left & top aligned, lock option on recTXO.TextRot := 0; // Comment text not rotated recTXO.TextLen := WordToLE(len); bytesFmtRuns := 8*SizeOf(Word); // see (3) below recTXO.NumFormattingRuns := WordToLE(bytesFmtRuns); { Write out to file } AStream.WriteBuffer(recTXO, SizeOf(recTXO)); { (2) 1st CONTINUE record containing the comment text --------------------- } { BIFF record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_CONTINUE)); AStream.WriteWord(len+1); { Record data } AStream.WriteByte(0); AStream.WriteBuffer(compressed[1], len); { (3) 2nd CONTINUE record containing the formatting runs ------------------ } { BIFF record header } AStream.WriteWord(WordToLE(INT_EXCEL_ID_CONTINUE)); AStream.WriteWord(bytesFmtRuns); { Record data } AStream.WriteWord(0); // start index of 1st formatting run (we only use 1 run) AStream.WriteWord(WordToLE(1)); // Font index to be used (default font) AStream.WriteWord(0); // Not used AStream.WriteWord(0); // Not used AStream.WriteWord(WordToLE(len)); // lastRun: number of characters AStream.WriteWord(0); // Not used AStream.WriteWord(0); // Not used AStream.WriteWord(0); // Not used end; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 WINDOW2 record This record contains additional 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; ASheet: TsBasicWorksheet); var Options: Word; book: TsWorkbook; sheet, actSheet: TsWorksheet; begin book := FWorkbook as TsWorkbook; sheet := ASheet as TsWorksheet; { BIFF Record header } WriteBiffHeader(AStream, INT_EXCEL_ID_WINDOW2, 18); { Options flags } Options := 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_SELECTED or MASK_WINDOW2_OPTION_SHEET_ACTIVE;} { Bug 0026386 -> every sheet must be selected/active, otherwise Excel cannot print ---> wp: after changes for issue 0028452: this is not necessary any more. } if (soShowGridLines in sheet.Options) then Options := Options or MASK_WINDOW2_OPTION_SHOW_GRID_LINES; if (soShowHeaders in sheet.Options) then Options := Options or MASK_WINDOW2_OPTION_SHOW_SHEET_HEADERS; if (soHasFrozenPanes in sheet.Options) and ((sheet.LeftPaneWidth > 0) or (sheet.TopPaneHeight > 0)) then Options := Options or MASK_WINDOW2_OPTION_PANES_ARE_FROZEN; if book.ActiveWorksheet <> nil then actSheet := book.ActiveWorksheet else actSheet := book.GetWorksheetByIndex(0); if (sheet = actSheet) then Options := Options or MASK_WINDOW2_OPTION_SHEET_ACTIVE or MASK_WINDOW2_OPTION_SHEET_SELECTED; if (sheet.BiDiMode = bdRTL) then Options := Options or MASK_WINDOW2_OPTION_COLUMNS_RIGHT_TO_LEFT; 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; {@@ ---------------------------------------------------------------------------- Writes an Excel 8 XF record (cell format) -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.WriteXF(AStream: TStream; AFormatRecord: PsCellFormat; XFType_Prot: Byte = 0); var rec: TBIFF8_XFRecord; j: Integer; b: Byte; dw1, dw2: DWord; w3: Word; nfParams: TsNumFormatParams; nfs: String; begin { BIFF record header } rec.RecordID := WordToLE(INT_EXCEL_ID_XF); rec.RecordSize := WordToLE(SizeOf(TBIFF8_XFRecord) - SizeOf(TsBIFFHeader)); { Index to font record } rec.FontIndex := 0; if (AFormatRecord <> nil) then begin if (uffFont in AFormatRecord^.UsedFormattingFields) then begin rec.FontIndex := AFormatRecord^.FontIndex; if rec.FontIndex >= 4 then inc(rec.FontIndex); // Font #4 does not exist in BIFF end; end; rec.FontIndex := WordToLE(rec.FontIndex); { Index to number format } j := 0; if (AFormatRecord <> nil) and (uffNumberFormat in AFormatRecord^.UsedFormattingFields) then begin nfParams := (Workbook as TsWorkbook).GetNumberFormat(AFormatRecord^.NumberFormatIndex); if nfParams <> nil then begin nfs := nfParams.NumFormatStr; j := NumFormatList.IndexOf(nfs); if j = -1 then j := 0; end; end; rec.NumFormatIndex := WordToLE(j); { XF type, cell protection and parent style XF } if AFormatRecord = nil then begin rec.XFType_Prot_ParentXF := XFType_Prot and MASK_XF_TYPE_PROT; if XFType_Prot and MASK_XF_TYPE_PROT_STYLE_XF <> 0 then rec.XFType_Prot_ParentXF := rec.XFType_Prot_ParentXF or MASK_XF_TYPE_PROT_PARENT; end else begin rec.XFType_Prot_ParentXF := 0; if cpLockCell in AFormatRecord^.Protection then rec.XFType_Prot_ParentXF := rec.XFType_Prot_ParentXF or MASK_XF_TYPE_PROT_LOCKED; if cpHideFormulas in AFormatRecord^.Protection then rec.XFType_Prot_ParentXF := rec.XFType_Prot_ParentXF or MASK_XF_TYPE_PROT_FORMULA_HIDDEN; end; rec.XFType_Prot_ParentXF := WordToLE(rec.XFType_Prot_ParentXF); { Text alignment and text break } if AFormatRecord = nil then b := MASK_XF_VERT_ALIGN_BOTTOM else begin b := 0; if (uffHorAlign in AFormatRecord^.UsedFormattingFields) then case AFormatRecord^.HorAlignment of haDefault: ; haLeft : b := b or MASK_XF_HOR_ALIGN_LEFT; haCenter : b := b or MASK_XF_HOR_ALIGN_CENTER; haRight : b := b or MASK_XF_HOR_ALIGN_RIGHT; end; // Since the default vertical alignment is vaDefault but "0" corresponds // to vaTop, we alwys have to write the vertical alignment. case AFormatRecord^.VertAlignment of vaTop : b := b or MASK_XF_VERT_ALIGN_TOP; vaCenter : b := b or MASK_XF_VERT_ALIGN_CENTER; vaBottom : b := b or MASK_XF_VERT_ALIGN_BOTTOM; else b := b or MASK_XF_VERT_ALIGN_BOTTOM; end; if (uffWordWrap in AFormatRecord^.UsedFormattingFields) then b := b or MASK_XF_TEXTWRAP; end; rec.Align_TextBreak := b; { Text rotation } rec.TextRotation := 0; if (AFormatRecord <> nil) and (uffTextRotation in AFormatRecord^.UsedFormattingFields) then rec.TextRotation := TEXT_ROTATIONS[AFormatRecord^.TextRotation]; { Indentation, shrink, merge and text direction: see "Excel97-2007BinaryFileFormat(xls)Specification.pdf", p281 ff Bits 0-3: Indent value Bit 4: Shrink to fit Bit 5: MergeCell Bits 6-7: Reading direction } rec.Indent_Shrink_TextDir := 0; if (AFormatRecord <> nil) and (uffBiDi in AFormatRecord^.UsedFormattingFields) then begin b := ord(AFormatRecord^.BiDiMode); if b > 0 then rec.Indent_Shrink_TextDir := rec.Indent_Shrink_TextDir or (b shl 6); end; { Used attributes } rec.UsedAttrib := 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; { Cell border lines and background area } dw1 := 0; dw2 := 0; w3 := 0; if (AFormatRecord <> nil) and (uffBorder in AFormatRecord^.UsedFormattingFields) then begin // Left and right line colors dw1 := PaletteIndex(AFormatRecord^.BorderStyles[cbWest].Color) shl 16 + PaletteIndex(AFormatRecord^.BorderStyles[cbEast].Color) shl 23; // Border line styles if cbWest in AFormatRecord^.Border then dw1 := dw1 or (DWord(AFormatRecord^.BorderStyles[cbWest].LineStyle)+1); if cbEast in AFormatRecord^.Border then dw1 := dw1 or ((DWord(AFormatRecord^.BorderStyles[cbEast].LineStyle)+1) shl 4); if cbNorth in AFormatRecord^.Border then dw1 := dw1 or ((DWord(AFormatRecord^.BorderStyles[cbNorth].LineStyle)+1) shl 8); if cbSouth in AFormatRecord^.Border then dw1 := dw1 or ((DWord(AFormatRecord^.BorderStyles[cbSouth].LineStyle)+1) shl 12); if cbDiagDown in AFormatRecord^.Border then dw1 := dw1 or $40000000; if cbDiagUp in AFormatRecord^.Border then dw1 := dw1 or $80000000; // Top, bottom and diagonal line colors dw2 := PaletteIndex(AFormatRecord^.BorderStyles[cbNorth].Color) + PaletteIndex(AFormatRecord^.BorderStyles[cbSouth].Color) shl 7 + PaletteIndex(AFormatRecord^.BorderStyles[cbDiagUp].Color) shl 14; // In BIFF8 both diagonals have the same color - we use the color of the up-diagonal. // Diagonal line style if (AFormatRecord^.Border * [cbDiagUp, cbDiagDown] <> []) then dw2 := dw2 or ((DWord(AFormatRecord^.BorderStyles[cbDiagUp].LineStyle)+1) shl 21); // In BIFF8 both diagonals have the same line style - we use the color of the up-diagonal. end; { Background fill } if (AFormatRecord <> nil) and (uffBackground in AFormatRecord^.UsedFormattingFields) then begin // Fill pattern style dw2 := dw2 or DWORD(MASK_XF_FILL_PATT[AFormatRecord^.Background.Style] shl 26); // Pattern color if AFormatRecord^.Background.FgColor = scTransparent then w3 := w3 or SYS_DEFAULT_FOREGROUND_COLOR else w3 := w3 or PaletteIndex(AFormatRecord^.Background.FgColor); // Background color if AFormatRecord^.Background.BgColor = scTransparent then w3 := w3 or SYS_DEFAULT_BACKGROUND_COLOR shl 7 else w3 := w3 or (PaletteIndex(AFormatRecord^.Background.BgColor) shl 7); end; rec.Border_BkGr1 := DWordToLE(dw1); rec.Border_BkGr2 := DWordToLE(dw2); rec.BkGr3 := WordToLE(w3); { Write out } AStream.WriteBuffer(rec, SizeOf(rec)); end; {==============================================================================} { Global utilities } {==============================================================================} procedure InitBIFF8Limitations(out ALimitations: TsSpreadsheetFormatLimitations); begin InitBiffLimitations(ALimitations); end; initialization // Registers this reader / writer in fpSpreadsheet sfidExcel8 := RegisterSpreadFormat(sfExcel8, TsSpreadBIFF8Reader, TsSpreadBIFF8Writer, STR_FILEFORMAT_EXCEL_8, 'BIFF8', [STR_EXCEL_EXTENSION] ); // Converts the palette to litte-endian MakeLEPalette(PALETTE_BIFF8); end.