{ fpsReaderWriter } {@@ ---------------------------------------------------------------------------- Unit fpsReaderWriter implements basic reading/writing support for fpspreadsheet, as well as registration of the file formats supported. AUTHORS: Felipe Monteiro de Carvalho, Reinier Olislagers, Werner Pamler LICENSE: See the file COPYING.modifiedLGPL.txt, included in the Lazarus distribution, for details about the license. USAGE: Each unit implementing a new spreadsheet format must register the reader/writer and some specific data by calling "RegisterSpreadFormat". -------------------------------------------------------------------------------} unit fpsReaderWriter; {$ifdef fpc} {$mode delphi}{$H+} {$endif} interface uses Classes, Sysutils, fpsTypes, fpsClasses, fpSpreadsheet; type { TsBasicSpreadReaderWriter } TsBasicSpreadReaderWriter = class protected {@@ Instance of the workbook which is currently being read or written. } FWorkbook: TsWorkbook; {@@ Instance of the worksheet which is currently being read or written. } FWorksheet: TsWorksheet; {@@ Limitations for the specific data file format } FLimitations: TsSpreadsheetFormatLimitations; public constructor Create(AWorkbook: TsWorkbook); virtual; // to allow descendents to override it function Limitations: TsSpreadsheetFormatLimitations; {@@ Instance of the workbook which is currently being read/written. } property Workbook: TsWorkbook read FWorkbook; end; { TsBasicSpreadReader } TsBasicSpreadReader = class(TsBasicSpreadReaderWriter) public { General writing methods } procedure ReadFromFile(AFileName: string; APassword: String = ''; AParams: TsStreamParams = []); virtual; abstract; procedure ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); virtual; abstract; procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); virtual; abstract; end; { TsBasicSpreadWriter } TsBasicSpreadWriter = class(TsBasicSpreadReaderWriter) public { Helpers } procedure CheckLimitations; virtual; { General writing methods } procedure WriteToFile(const AFileName: string; const AOverwriteExisting: Boolean = False; AParams: TsStreamParams = []); virtual; abstract; procedure WriteToStream(AStream: TStream; AParams: TsStreamParams = []); virtual; abstract; procedure WriteToStrings(AStrings: TStrings; AParams: TsStreamParams = []); virtual; abstract; end; {@@ TsSpreadReader class reference type } TsSpreadReaderClass = class of TsBasicSpreadReader; {@@ TsSpreadWriter class reference type } TsSpreadWriterClass = class of TsBasicSpreadWriter; {@@ Custom reader of spreadsheet files. "Custom" means that it provides only the basic functionality. The main implementation is done in derived classes for each individual file format. } TsCustomSpreadReader = class(TsBasicSpreadReader) protected {@@ list of format records collected from the file } FCellFormatList: TsCellFormatList; {@@ List of fonts collected from the file } FFontList: TFPList; {@@ Temporary cell for virtual mode} FVirtualCell: TCell; {@@ Stores if the reader is in virtual mode } FIsVirtualMode: Boolean; {@@ List of number formats } FNumFormatList: TStringList; { Helper methods } procedure AddBuiltinNumFormats; virtual; {@@ Removes column records if all of them have the same column width } procedure FixCols(AWorksheet: TsWorksheet); {@@ Removes row records if all of them have the same row height } procedure FixRows(AWorksheet: TsWorksheet); { Record reading methods } {@@ Abstract method for reading a blank cell. Must be overridden by descendent classes. } procedure ReadBlank(AStream: TStream); virtual; abstract; {@@ Abstract method for reading a BOOLEAN cell. Must be overridden by descendent classes. } procedure ReadBool(AStream: TSTream); virtual; abstract; {@@ Abstract method for reading a formula cell. Must be overridden by descendent classes. } procedure ReadFormula(AStream: TStream); virtual; abstract; {@@ Abstract method for reading a text cell. Must be overridden by descendent classes. } procedure ReadLabel(AStream: TStream); virtual; abstract; {@@ Abstract method for reading a number cell. Must be overridden by descendent classes. } procedure ReadNumber(AStream: TStream); virtual; abstract; public constructor Create(AWorkbook: TsWorkbook); override; destructor Destroy; override; { General writing methods } procedure ReadFromFile(AFileName: string; APassword: String = ''; AParams: TsStreamParams = []); override; procedure ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); override; procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); override; {@@ List of number formats found in the workbook. } property NumFormatList: TStringList read FNumFormatList; end; {@@ Callback function when iterating cells while accessing a stream } TCellsCallback = procedure (ACell: PCell; AStream: TStream) of object; {@@ Callback function when iterating comments while accessing a stream } TCommentsCallback = procedure (AComment: PsComment; ACommentIndex: Integer; AStream: TStream) of object; {@@ Callback function when iterating hyperlinks while accessing a stream } THyperlinksCallback = procedure (AHyperlink: PsHyperlink; AStream: TStream) of object; {@@ Custom writer of spreadsheet files. "Custom" means that it provides only the basic functionality. The main implementation is done in derived classes for each individual file format. } TsCustomSpreadWriter = class(TsBasicSpreadWriter) protected {@@ List of number formats found in the file } FNumFormatList: TStringList; procedure AddBuiltinNumFormats; virtual; function FindNumFormatInList(ANumFormatStr: String): Integer; // function FixColor(AColor: TsColor): TsColor; virtual; procedure FixFormat(ACell: PCell); virtual; procedure GetSheetDimensions(AWorksheet: TsWorksheet; out AFirstRow, ALastRow, AFirstCol, ALastCol: Cardinal); virtual; procedure ListAllNumFormats; virtual; { Helpers for writing } procedure WriteCellToStream(AStream: TStream; ACell: PCell); virtual; procedure WriteCellsToStream(AStream: TStream; ACells: TsCells); { Record writing methods } procedure WriteBlank(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); virtual; abstract; procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal; const AValue: Boolean; ACell: PCell); virtual; abstract; procedure WriteComment(AStream: TStream; ACell: PCell); virtual; procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); virtual; abstract; procedure WriteError(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TsErrorValue; ACell: PCell); virtual; abstract; procedure WriteFormula(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); virtual; procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); virtual; abstract; procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); virtual; abstract; public constructor Create(AWorkbook: TsWorkbook); 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; procedure WriteToStrings(AStrings: TStrings; AParams: TsStreamParams = []); override; {@@ List of number formats found in the workbook. } property NumFormatList: TStringList read FNumFormatList; end; type TsSpreadFileAccess = (faRead, faWrite); function RegisterSpreadFormat( AFormat: TsSpreadsheetFormat; AReaderClass: TsSpreadReaderClass; AWriterClass: TsSpreadWriterClass; AFormatName, ATechnicalName: String; const AFileExtensions: array of String): TsSpreadFormatID; function GetFileFormatFilter(AListSeparator, AExtSeparator: Char; AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID; AllSpreadFormats: Boolean = false; AllExcelFormats: Boolean = false): String; function GetSpreadFormats(AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID): TsSpreadFormatIDArray; function GetSpreadFormatsFromFileName(AFileAccess: TsSpreadFileAccess; AFileName: TFileName; APriorityFormat: TsSpreadFormatID = sfidUnknown): TsSpreadFormatIDArray; function GetSpreadFormatExt(AFormatID: TsSpreadFormatID): String; function GetSpreadFormatName(AFormatID: TsSpreadFormatID): String; function GetSpreadTechnicalName(AFormatID: TsSpreadFormatID): String; function GetSpreadReaderClass(AFormatID: TsSpreadFormatID): TsSpreadReaderClass; function GetSpreadWriterClass(AFormatID: TsSpreadFormatID): TsSpreadWriterClass; implementation uses Math, LazUTF8, fpsStrings, fpsUtils, fpsNumFormat, fpsStreams; {------------------------------------------------------------------------------} { TsBasicSpreadReaderWriter } {------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Constructor of the reader/writer. Has the workbook to be read/written as a parameter to apply the localization information found in its FormatSettings. @param AWorkbook Workbook into which the file is being read or from with the file is written. This parameter is passed from the workbook which creates the reader/writer. -------------------------------------------------------------------------------} constructor TsBasicSpreadReaderWriter.Create(AWorkbook: TsWorkbook); begin inherited Create; FWorkbook := AWorkbook; { A good starting point valid for many formats... } FLimitations.MaxColCount := 256; FLimitations.MaxRowCount := 65536; FLimitations.MaxPaletteSize := MaxInt; FLimitations.MaxSheetnameLength := MaxInt; end; {@@ ---------------------------------------------------------------------------- Returns a record containing limitations of the specific file format of the writer. -------------------------------------------------------------------------------} function TsBasicSpreadReaderWriter.Limitations: TsSpreadsheetFormatLimitations; begin Result := FLimitations; end; {------------------------------------------------------------------------------} { TsBasicSpreadWriter } {------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Checks limitations of the writer, e.g max row/column count -------------------------------------------------------------------------------} procedure TsBasicSpreadWriter.CheckLimitations; var lastCol, lastRow: Cardinal; i: Integer; sheet: TsWorksheet; begin Workbook.GetLastRowColIndex(lastRow, lastCol); // Check row count if lastRow >= FLimitations.MaxRowCount then Workbook.AddErrorMsg(rsMaxRowsExceeded, [lastRow+1, FLimitations.MaxRowCount]); // Check column count if lastCol >= FLimitations.MaxColCount then Workbook.AddErrorMsg(rsMaxColsExceeded, [lastCol+1, FLimitations.MaxColCount]); // Check worksheet names for i:=0 to Workbook.GetWorksheetCount-1 do begin sheet := Workbook.GetWorksheetByIndex(i); if UTF8Length(sheet.Name) > FLimitations.MaxSheetNameLength then // Worksheet name is too long. // We abort saving here because it is not safe to chop the sheet name // to its allowed length - it may be used as a reference in formulas. raise Exception.CreateFmt(rsWriteError_WorksheetNameTooLong, [sheet.Name, FLimitations.MaxSheetNameLength]); end; end; {------------------------------------------------------------------------------} { TsCustomSpreadReader } {------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Constructor of the reader. Has the workbook to be read as a parameter to apply the localization information found in its FormatSettings. Creates an internal instance of the number format list according to the file format being read/written. @param AWorkbook Workbook into which the file is being read. This parameter is passed from the workbook which creates the reader. -------------------------------------------------------------------------------} constructor TsCustomSpreadReader.Create(AWorkbook: TsWorkbook); begin inherited Create(AWorkbook); // Font list FFontList := TFPList.Create; // Number formats FNumFormatList := TStringList.Create; AddBuiltinNumFormats; // Virtual mode FIsVirtualMode := (boVirtualMode in FWorkbook.Options) and Assigned(FWorkbook.OnReadCellData); end; {@@ ---------------------------------------------------------------------------- Destructor of the reader. Destroys the internal number format list and the error log list. -------------------------------------------------------------------------------} destructor TsCustomSpreadReader.Destroy; var j: Integer; begin for j:=FFontList.Count-1 downto 0 do if FFontList[j] <> nil then TObject(FFontList[j]).Free; // font #4 can add a nil! FreeAndNil(FFontList); FreeAndNil(FNumFormatList); FreeAndNil(FCellFormatList); inherited Destroy; end; {@@ ---------------------------------------------------------------------------- Adds the built-in number formats to the internal NumFormatList. Must be overridden by descendants because they know about the details of the file format. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.AddBuiltinNumFormats; begin // to be overridden by descendants end; {@@ ---------------------------------------------------------------------------- Deletes unnecessary column records as they are written by some Office applications when they convert a file to another format. @param AWorksheet The columns in this worksheet are processed. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.FixCols(AWorkSheet: TsWorksheet); const EPS = 1E-3; var c: LongInt; w: Single; lCol: PCol; sameWidth: Boolean; begin // If the count of columns is equal to the max colcount of the file format // then it is likely that dummy columns have been added -> delete all empty // columns (starting at the right) until the first non-empty column is found if AWorksheet.Cols.Count = SizeInt(FLimitations.MaxColCount) then begin c := AWorksheet.Cols.Count - 1; lCol := PCol(AWorksheet.Cols[c]); w := lCol.Width; while c >= 0 do begin lCol := PCol(AWorksheet.Cols[c]); if not SameValue(lCol^.Width, w, EPS) then break; if AWorksheet.FindNextCellInCol(0, c) <> nil then break; AWorksheet.RemoveCol(c); dec(c); end; end; if AWorksheet.Cols.Count < 2 then exit; // Check whether all columns have the same column width sameWidth := true; w := PCol(AWorksheet.Cols[0])^.Width; for c := 1 to AWorksheet.Cols.Count-1 do begin lCol := PCol(AWorksheet.Cols[c]); if not SameValue(lCol^.Width, w, EPS) then begin sameWidth := false; break; end; end; if sameWidth then begin // At this point we know that all columns have the same width. We pass this // to the DefaultColWidth ... AWorksheet.WriteDefaultColWidth(w, FWorkbook.Units); // ...and delete all column records with non-default format for c := AWorksheet.Cols.Count-1 downto 0 do begin lCol := PCol(AWorksheet.Cols[c]); if lCol^.FormatIndex = 0 then AWorksheet.RemoveCol(c); end; end; end; {@@ ---------------------------------------------------------------------------- This procedure checks whether all rows have the same height and removes the row records if they do. Such unnecessary row records are often written when an Office application converts a file to another format. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.FixRows(AWorkSheet: TsWorksheet); const EPS = 1E-3; var r, rLast: Cardinal; h: Single; lRow: PRow; begin if AWorksheet.Rows.Count <= 1 then exit; // Check whether all rows have the same height h := PRow(AWorksheet.Rows[0])^.Height; for r := 1 to AWorksheet.Rows.Count-1 do begin lRow := PRow(AWorksheet.Rows[r]); if not SameValue(lRow^.Height, h, EPS) then exit; end; // If there are more rows than row records and the common row height is not // the default row height (i.e. the row height of the non-record rows) then // the row heights are different rLast := AWorksheet.GetLastRowIndex; if (AWorksheet.Rows.Count > 0) and (rLast <> PRow(AWorksheet.Rows[AWorksheet.Rows.Count-1]).Row) and not SameValue(h, AWorksheet.ReadDefaultRowHeight(FWorkbook.Units), EPS) then exit; // At this point we know that all rows have the same height. We pass this // to the DefaultRowHeight ... AWorksheet.WriteDefaultRowHeight(h, FWorkbook.Units); // ... and delete all row records with default format. for r := AWorksheet.Rows.Count-1 downto 0 do begin lRow := PRow(AWorksheet.Rows[r]); if lRow^.FormatIndex = 0 then AWorksheet.RemoveRow(r); end; end; {@@ ---------------------------------------------------------------------------- Default file reading method. Opens the file and calls ReadFromStream. Data are stored in the workbook specified during construction. @param AFileName The input file name. @see TsWorkbook -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.ReadFromFile(AFileName: string; APassword: String = ''; AParams: TsStreamParams = []); var stream, fs: TStream; begin if (boFileStream in Workbook.Options) then stream := TFileStream.Create(AFileName, fmOpenRead + fmShareDenyNone) else if (boBufStream in Workbook.Options) then stream := TBufStream.Create(AFileName, fmOpenRead + fmShareDenyNone) else begin stream := TMemoryStream.Create; fs := TFileStream.Create(AFilename, fmOpenRead + fmShareDenyNone); try (stream as TMemoryStream).CopyFrom(fs, fs.Size); stream.Position := 0; finally fs.Free; end; end; try ReadFromStream(stream, APassword, AParams); finally stream.Free; end; end; {@@ ---------------------------------------------------------------------------- This routine has the purpose to read the workbook data from the stream. It should be overriden in descendent classes. Its basic implementation here assumes that the stream is a TStringStream and the data are provided by calling ReadFromStrings. This mechanism is valid for wikitables. Data will be stored in the workbook defined at construction. @param AData Workbook which is filled by the data from the stream. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.ReadFromStream(AStream: TStream; APassword: String; AParams: TsStreamParams = []); var AStringStream: TStringStream; AStrings: TStringList; begin Unused(APassword); AStringStream := TStringStream.Create(''); AStrings := TStringList.Create; try AStringStream.CopyFrom(AStream, AStream.Size); AStringStream.Seek(0, soFromBeginning); AStrings.Text := AStringStream.DataString; ReadFromStrings(AStrings, AParams); finally AStringStream.Free; AStrings.Free; end; end; {@@ ---------------------------------------------------------------------------- Reads workbook data from a string list. This abstract implementation does nothing and raises an exception. Must be overridden, like for wikitables. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); begin Unused(AStrings, AParams); raise Exception.Create(rsUnsupportedReadFormat); end; {------------------------------------------------------------------------------} { TsCustomSpreadWriter } {------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Constructor of the writer. Has the workbook to be written as a parameter to apply the localization information found in its FormatSettings. Creates an internal instance of the number format list according to the file format being read/written. @param AWorkbook Workbook from with the file is written. This parameter is passed from the workbook which creates the writer. -------------------------------------------------------------------------------} constructor TsCustomSpreadWriter.Create(AWorkbook: TsWorkbook); begin inherited Create(AWorkbook); // Number formats FNumFormatList := TStringList.Create; AddBuiltinNumFormats; end; {@@ ---------------------------------------------------------------------------- Destructor of the writer. Destroys the internal number format list. -------------------------------------------------------------------------------} destructor TsCustomSpreadWriter.Destroy; begin FreeAndNil(FNumFormatList); inherited Destroy; end; {@@ ---------------------------------------------------------------------------- Adds the built-in number formats to the NumFormatList The method has to be overridden because the descendants know the special requirements of the file format. -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.AddBuiltinNumFormats; begin // to be overridden by descendents end; {@@ ---------------------------------------------------------------------------- Checks whether the specified number format string is already contained in the the writer's internal number format list. If yes, the list index is returned. -------------------------------------------------------------------------------} function TsCustomSpreadWriter.FindNumFormatInList(ANumFormatStr: String): Integer; begin for Result:=0 to FNumFormatList.Count-1 do if SameText(ANumFormatStr, FNumFormatList[Result]) then exit; Result := -1; end; {@@ ---------------------------------------------------------------------------- If formatting features of a cell are not supported by the destination file format of the writer, here is the place to apply replacements. Must be overridden by descendants, nothin happens here. See BIFF2. @param ACell Pointer to the cell being investigated. Note that this cell does not belong to the workbook, but is a cell of the FFormattingStyles array. -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.FixFormat(ACell: PCell); begin Unused(ACell); // to be overridden end; {@@ ---------------------------------------------------------------------------- Determines the size of the worksheet to be written. VirtualMode is respected. Is called when the writer needs the size for output. Column and row count limitations are repsected as well. @param AWorksheet Worksheet to be written @param AFirsRow Index of first row to be written @param ALastRow Index of last row @param AFirstCol Index of first column to be written @param ALastCol Index of last column to be written -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.GetSheetDimensions(AWorksheet: TsWorksheet; out AFirstRow, ALastRow, AFirstCol, ALastCol: Cardinal); begin if (boVirtualMode in AWorksheet.Workbook.Options) then begin AFirstRow := 0; AFirstCol := 0; ALastRow := LongInt(AWorksheet.VirtualRowCount)-1; ALastCol := LongInt(AWorksheet.VirtualColCount)-1; end else begin Workbook.UpdateCaches; AFirstRow := AWorksheet.GetFirstRowIndex; if AFirstRow = Cardinal(-1) then AFirstRow := 0; // this happens if the sheet is empty and does not contain row records AFirstCol := AWorksheet.GetFirstColIndex; if AFirstCol = Cardinal(-1) then AFirstCol := 0; // this happens if the sheet is empty and does not contain col records ALastRow := AWorksheet.GetLastRowIndex; ALastCol := AWorksheet.GetLastColIndex; end; if AFirstCol >= Limitations.MaxColCount then AFirstCol := Limitations.MaxColCount-1; if AFirstRow >= Limitations.MaxRowCount then AFirstRow := Limitations.MaxRowCount-1; if ALastCol >= Limitations.MaxColCount then ALastCol := Limitations.MaxColCount-1; if ALastRow >= Limitations.MaxRowCount then ALastRow := Limitations.MaxRowCount-1; end; {@@ ---------------------------------------------------------------------------- Copies the format strings from the workbook's NumFormatList to the writer's internal NumFormatList. -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.ListAllNumFormats; var i: Integer; numFmt: TsNumFormatParams; numFmtStr: String; begin for i:=0 to Workbook.GetNumberFormatCount - 1 do begin numFmt := Workbook.GetNumberFormat(i); if numFmt <> nil then begin numFmtStr := numFmt.NumFormatStr; if FindNumFormatInList(numFmtStr) = -1 then FNumFormatList.Add(numFmtStr); end; end; end; {@@ ---------------------------------------------------------------------------- Helper function for the spreadsheet writers. Writes the cell value to the stream. Calls the WriteNumber method of the worksheet for writing a number, the WriteDateTime method for writing a date/time etc. @param ACell Pointer to the worksheet cell being written @param AStream Stream to which data are written @see TsCustomSpreadWriter.WriteCellsToStream -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteCellToStream(AStream: TStream; ACell: PCell); begin if HasFormula(ACell) then WriteFormula(AStream, ACell^.Row, ACell^.Col, ACell) else case ACell.ContentType of cctBool: WriteBool(AStream, ACell^.Row, ACell^.Col, ACell^.BoolValue, ACell); cctDateTime: WriteDateTime(AStream, ACell^.Row, ACell^.Col, ACell^.DateTimeValue, ACell); cctEmpty: WriteBlank(AStream, ACell^.Row, ACell^.Col, ACell); cctError: WriteError(AStream, ACell^.Row, ACell^.Col, ACell^.ErrorValue, ACell); cctNumber: WriteNumber(AStream, ACell^.Row, ACell^.Col, ACell^.NumberValue, ACell); cctUTF8String: WriteLabel(AStream, ACell^.Row, ACell^.Col, ACell^.UTF8StringValue, ACell); end; if FWorksheet.ReadComment(ACell) <> '' then WriteComment(AStream, ACell); end; {@@ ---------------------------------------------------------------------------- Helper function for the spreadsheet writers. Iterates all cells on a list, calling the appropriate write method for them. @param AStream The output stream. @param ACells List of cells to be writeen -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteCellsToStream(AStream: TStream; ACells: TsCells); var cell: PCell; begin for cell in ACells do WriteCellToStream(AStream, cell); end; {@@ ---------------------------------------------------------------------------- (Pseudo-) abstract method writing a cell comment to the stream. The cell comment is written immediately after the cell content. NOTE: This is not good for XLSX and BIFF8. Must be overridden by descendents. @param ACell Pointer to the cell containing the comment to be written -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteComment(AStream: TStream; ACell: PCell); begin Unused(AStream, ACell); end; {@@ ---------------------------------------------------------------------------- Basic method which is called when writing a formula to a stream. The formula is already stored in the cell fields. Present implementation does nothing. Needs to be overridden by descendants. @param AStream Stream to be written @param ARow Row index of the cell containing the formula @param ACol Column index of the cell containing the formula @param ACell Pointer to the cell containing the formula and being written to the stream -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteFormula(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); begin Unused(AStream); Unused(ARow, ACol, ACell); end; {@@ ---------------------------------------------------------------------------- Default file writing method. Opens the file and calls WriteToStream The workbook written is the one specified in the constructor of the writer. @param AFileName The output file name. @param AOverwriteExisting If the file already exists it will be replaced. @param AParams Optional parameters to control stream access @see TsWorkbook -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteToFile(const AFileName: string; const AOverwriteExisting: Boolean = False; AParams: TsStreamParams = []); var OutputFile: TStream; lMode: Word; begin if AOverwriteExisting then lMode := fmCreate or fmOpenWrite else lMode := fmCreate; if (boFileStream in FWorkbook.Options) then OutputFile := TFileStream.Create(AFileName, lMode) else if (boBufStream in Workbook.Options) then OutputFile := TBufStream.Create(AFileName, lMode) else OutputFile := TMemoryStream.Create; try WriteToStream(OutputFile, AParams); if OutputFile is TMemoryStream then (OutputFile as TMemoryStream).SaveToFile(AFileName); finally OutputFile.Free; end; end; {@@ ---------------------------------------------------------------------------- This routine has the purpose to write the workbook to a stream. Present implementation writes to a stringlists by means of WriteToStrings; this behavior is required for wikitables. Must be overriden in descendent classes for all other cases. @param AStream Stream to which the workbook is written @param AParams Optional parameters to control stream access -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteToStream(AStream: TStream; AParams: TsStreamParams = []); var list: TStringList; begin list := TStringList.Create; try WriteToStrings(list, AParams); list.SaveToStream(AStream); finally list.Free; end; end; {@@ ---------------------------------------------------------------------------- Writes the worksheet to a list of strings. Not implemented here, needs to be overridden by descendants. See wikitables. -------------------------------------------------------------------------------} procedure TsCustomSpreadWriter.WriteToStrings(AStrings: TStrings; AParams: TsStreamParams = []); begin Unused(AStrings, AParams); raise Exception.Create(rsUnsupportedWriteFormat); end; type TsSpreadFormatData = class private FFormatID: TsSpreadFormatID; // Format identifier FName: String; // Text to be used in FileDialog filter FTechnicalName: String; // Text to be used e.g. in Titlebar FFileExtensions: array of String; // File extensions used by this format FReaderClass: TsSpreadReaderClass; // Class for reading these files FWriterClass: TsSpreadWriterClass; // Class for writing these files function GetFileExtension(AIndex: Integer): String; function GetFileExtensionCount: Integer; public constructor Create(AFormatID: TsSpreadFormatID; AReaderClass: TsSpreadReaderClass; AWriterClass: TsSpreadWriterClass; AFormatName, ATechnicalName: String; const AExtensions: Array of String); // ACanReadFromClipboard, ACanWriteToClipboard: Boolean); function GetFileFilterMask(ASeparator: Char): String; // property CanReadFromClipboard: boolean read FCanReadClipboard; // property CanWriteToClipboard: boolean read FCanWriteClipboard; property FormatID: TsSpreadFormatID read FFormatID; property FormatName: String read FName; property FileExtension[AIndex: Integer]: String read GetFileExtension; property FileExtensionCount: Integer read GetFileExtensionCount; property ReaderClass: TsSpreadReaderClass read FReaderClass; property TechnicalName: String read FTechnicalName; property WriterClass: TsSpreadWriterClass read FWriterClass; end; { TsSpreadFormatRegistry } TsSpreadFormatRegistry = class private FList: TFPList; FCachedData: TsSpreadFormatData; FCachedFormatID: TsSpreadFormatID; function GetDefaultExt(AFormatID: TsSpreadFormatID): String; function GetFormatName(AFormatID: TsSpreadFormatID): String; function GetReaderClass(AFormatID: TsSpreadFormatID): TsSpreadReaderClass; function GetTechnicalName(AFormatID: TsSpreadFormatID): String; function GetWriterClass(AFormatID: TsSpreadFormatID): TsSpreadWriterClass; protected function Add(AData: TsSpreadFormatData): Integer; function FindFormatID(AFormatID: TsSpreadFormatID): TsSpreadFormatData; function IndexOf(AFormatID: TsSpreadFormatID): Integer; public constructor Create; destructor Destroy; override; function GetAllSpreadFilesMask(AExtSeparator: Char; AFileAccess: TsSpreadFileAccess): String; function GetAllExcelFilesMask(AExtSeparator: Char): String; function GetFileFilter(AListSeparator, AExtSeparator: Char; AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID; AllSpreadFormats: Boolean = false; AllExcelFormats: Boolean = false): String; function GetFormatArray(AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID): TsSpreadFormatIDArray; function GetFormatArrayFromFileName(AFileAccess: TsSpreadFileAccess; const AFileName: String; APriorityFormat: TsSpreadFormatID = sfidUnknown): TsSpreadFormatIDArray; property DefaultExt[AFormatID: TsSpreadFormatID]: String read GetDefaultExt; property FormatName[AFormatID: TsSpreadFormatID]: String read GetFormatName; property ReaderClass[AFormatID: TsSpreadFormatID]: TsSpreadReaderClass read GetReaderClass; property TechnicalName[AFormatID: TsSpreadFormatID]: String read GetTechnicalName; property WriterClass[AFormatID: TsSpreadFormatID]: TsSpreadWriterClass read GetWriterClass; end; var SpreadFormatRegistry: TsSpreadFormatRegistry; {==============================================================================} { TsSpreadFormatData } {==============================================================================} constructor TsSpreadFormatData.Create(AFormatID: TsSpreadFormatID; AReaderClass: TsSpreadReaderClass; AWriterClass: TsSpreadWriterClass; AFormatName, ATechnicalName: String; const AExtensions: array of String); var i: Integer; begin FFormatID := AFormatID; FReaderClass := AReaderClass; FWriterClass := AWriterClass; FName := AFormatName; FTechnicalName := ATechnicalName; SetLength(FFileExtensions, Length(AExtensions)); for i:=0 to High(FFileExtensions) do FFileExtensions[i] := AExtensions[i]; end; function TsSpreadFormatData.GetFileExtension(AIndex: Integer): String; begin Result := FFileExtensions[AIndex]; end; function TsSpreadFormatData.GetFileExtensionCount: Integer; begin Result := Length(FFileExtensions); end; function TsSpreadFormatData.GetFileFilterMask(ASeparator: Char): String; var i: Integer; begin Result := '*' + FFileExtensions[0]; for i:= 1 to High(FFileExtensions) do Result := Result + ASeparator + '*' + FFileExtensions[i]; end; {==============================================================================} { TsSpreadFormatRegistry } {==============================================================================} constructor TsSpreadFormatRegistry.Create; begin inherited; FList := TFPList.Create; FCachedFormatID := sfidUnknown; FCachedData := nil; end; destructor TsSpreadFormatRegistry.Destroy; var i: Integer; begin for i := FList.Count-1 downto 0 do TObject(FList[i]).Free; FList.Free; inherited; end; function TsSpreadFormatRegistry.Add(AData: TsSpreadFormatData): Integer; begin Result := FList.Add(AData); end; function TsSpreadFormatRegistry.FindFormatID(AFormatID: TsSpreadFormatID): TsSpreadFormatData; var idx: Integer; begin if AFormatID <> FCachedFormatID then begin idx := IndexOf(AFormatID); if idx = -1 then begin FCachedData := nil; FCachedFormatID := sfidUnknown; end else begin FCachedData := TsSpreadFormatData(FList[idx]); FCachedFormatID := AFormatID; end; end; Result := FCachedData; end; function TsSpreadFormatRegistry.GetDefaultExt(AFormatID: TsSpreadFormatID): String; var data: TsSpreadFormatData; begin data := FindFormatID(AFormatID); if data <> nil then Result := data.FileExtension[0] else Result := ''; end; function TsSpreadFormatRegistry.GetAllSpreadFilesMask(AExtSeparator: Char; AFileAccess: TsSpreadFileAccess): String; var L: TStrings; data: TsSpreadFormatData; ext: String; i, j: Integer; begin Result := ''; L := TStringList.Create; try for i:=0 to FList.Count-1 do begin data := TsSpreadFormatData(FList[i]); case AFileAccess of faRead : if data.ReaderClass = nil then continue; faWrite : if data.WriterClass = nil then continue; end; for j:=0 to data.FileExtensionCount-1 do begin ext := data.FileExtension[j]; if L.IndexOf(ext) = -1 then L.Add(ext); end; end; if L.Count > 0 then begin Result := '*' + L[0]; for i := 1 to L.Count-1 do Result := Result + AExtSeparator + '*' + L[i]; end; finally L.Free; end; end; function TsSpreadFormatRegistry.GetAllExcelFilesMask(AExtSeparator: Char): String; var j: Integer; L: TStrings; data: TsSpreadFormatData; ext: String; begin L := TStringList.Create; try // good old BIFF... if (IndexOf(ord(sfExcel8)) <> -1) or (IndexOf(ord(sfExcel5)) <> -1) or (IndexOf(ord(sfExcel2)) <> -1) then L.Add('*.xls'); // Excel 2007+ j := IndexOf(ord(sfOOXML)); if j <> -1 then begin data := TsSpreadFormatData(FList[j]); for j:=0 to data.FileExtensionCount-1 do begin ext := data.FileExtension[j]; if L.IndexOf(ext) = -1 then L.Add('*' + ext); end; end; L.Delimiter := AExtSeparator; L.StrictDelimiter := true; Result := L.DelimitedText; finally L.Free; end; end; function TsSpreadFormatRegistry.GetFileFilter(AListSeparator, AExtSeparator: Char; AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID; AllSpreadFormats: Boolean = false; AllExcelFormats: Boolean = false): String; var i, idx: Integer; L: TStrings; s: String; data: TsSpreadFormatData; begin // Bring the formats listed in APriorityFormats to the top if Length(APriorityFormats) > 0 then for i := High(APriorityFormats) downto Low(APriorityFormats) do begin idx := IndexOf(APriorityFormats[i]); data := TsSpreadFormatData(FList[idx]); FList.Delete(idx); FList.Insert(0, data); end; L := TStringList.Create; try L.Delimiter := AListSeparator; L.StrictDelimiter := true; if AllSpreadFormats then begin s := GetAllSpreadFilesMask(AExtSeparator, AFileAccess); if s <> '' then begin L.Add(rsAllSpreadsheetFiles); L.Add(GetAllSpreadFilesMask(AExtSeparator, AFileAccess)); end; end; if AllExcelFormats then begin s := GetAllExcelFilesMask(AExtSeparator); if s <> '' then begin L.Add(Format('%s (%s)', [rsAllExcelFiles, s])); L.Add(s); end; end; for i:=0 to FList.Count-1 do begin data := TsSpreadFormatData(FList[i]); case AFileAccess of faRead : if data.ReaderClass = nil then Continue; faWrite : if data.WriterClass = nil then Continue; end; s := data.GetFileFilterMask(AExtSeparator); L.Add(Format('%s %s (%s)', [data.FormatName, rsFiles, s])); L.Add(s); end; Result := L.DelimitedText; finally L.Free; end; end; function TsSpreadFormatRegistry.GetFormatArray(AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID): TsSpreadFormatIDArray; var i, n, idx: Integer; data: TsSpreadFormatData; begin // Rearrange the formats such the one noted in APriorityFormats are at the top if Length(APriorityFormats) > 0 then for i := High(APriorityFormats) downto Low(APriorityFormats) do begin idx := IndexOf(APriorityFormats[i]); data := TsSpreadFormatData(FList[idx]); FList.Delete(idx); FList.Insert(0, data); end; SetLength(Result, FList.Count); n := 0; for i := 0 to FList.Count-1 do begin data := TsSpreadFormatData(FList[i]); case AFileAccess of faRead : if data.ReaderClass = nil then Continue; faWrite : if data.WriterClass = nil then Continue; end; Result[n] := data.FormatID; inc(n); end; SetLength(Result, n); end; function TsSpreadFormatRegistry.GetFormatArrayFromFileName( AFileAccess: TsSpreadFileAccess; const AFileName: String; APriorityFormat: TsSpreadFormatID = sfidUnknown): TsSpreadFormatIDArray; var idx: Integer; i, j, n: Integer; ext: String; data: TsSpreadFormatData; begin ext := Lowercase(ExtractFileExt(AFileName)); if APriorityFormat <> sfidUnknown then begin // Bring the priority format to the top idx := IndexOf(APriorityFormat); FList.Exchange(0, idx); end; SetLength(Result, FList.Count); n := 0; for i := 0 to FList.Count - 1 do begin data := TsSpreadFormatData(FList[i]); case AFileAccess of faRead : if data.ReaderClass = nil then Continue; faWrite : if data.WriterClass = nil then Continue; end; for j:=0 to data.FileExtensionCount-1 do if Lowercase(data.FileExtension[j]) = ext then begin Result[n] := data.FormatID; inc(n); end; end; SetLength(Result, n); if APriorityFormat <> sfidUnknown then // Restore original order FList.Exchange(idx, 0); end; function TsSpreadFormatRegistry.GetFormatName(AFormatID: TsSpreadFormatID): String; var data: TsSpreadFormatData; begin data := FindFormatID(AFormatID); if data <> nil then Result := data.FormatName else Result := ''; end; function TsSpreadFormatRegistry.GetReaderClass(AFormatID: TsSpreadFormatID): TsSpreadReaderClass; var data: TsSpreadFormatData; begin data := FindFormatID(AFormatID); if data <> nil then Result := data.ReaderClass else Result := nil; end; function TsSpreadFormatRegistry.GetTechnicalName(AFormatID: TsSpreadFormatID): String; var data: TsSpreadFormatData; begin data := FindFormatID(AFormatID); if data <> nil then Result := data.TechnicalName else Result := ''; end; function TsSpreadFormatRegistry.GetWriterClass(AFormatID: TsSpreadFormatID): TsSpreadWriterClass; var data: TsSpreadFormatData; begin data := FindFormatID(AFormatID); if data <> nil then Result := data.WriterClass else Result := nil; end; function TsSpreadFormatRegistry.IndexOf(AFormatID: TsSpreadFormatID): Integer; begin for Result := 0 to FList.Count - 1 do if TsSpreadFormatData(FList[Result]).FormatID = AFormatID then exit; Result := -1; end; {==============================================================================} { Public utility functions } {==============================================================================} {@@ ---------------------------------------------------------------------------- Registers a new reader/writer pair for a given spreadsheet file format AFormat identifies the file format, see sfXXXX declarations in built-in fpstypes. The system is open to user-defined formats. In this case, AFormat must have the value "sfUser". The format identifier is calculated as a negative number, stored in the TsSpreadFormatData class and returned as function result. This value is needed when calling fpspreadsheet's ReadFromXXXX and WriteToXXXX methods to specify the file format. -------------------------------------------------------------------------------} function RegisterSpreadFormat(AFormat: TsSpreadsheetFormat; AReaderClass: TsSpreadReaderClass; AWriterClass: TsSpreadWriterClass; AFormatName, ATechnicalName: String; const AFileExtensions: array of String): TsSpreadFormatID; var fmt: TsSpreadFormatData; n: Integer; begin if AFormat <> sfUser then begin n := SpreadFormatRegistry.IndexOf(ord(AFormat)); if n >= 0 then raise Exception.Create('[RegisterSpreadFormat] Spreadsheet format is already registered.'); end; if Length(AFileExtensions) = 0 then raise Exception.Create('[RegisterSpreadFormat] File extensions needed for registering a file format.'); if (AFormatName = '') or (ATechnicalName = '') then raise Exception.Create('[RegisterSpreadFormat] File format name is not specified.'); fmt := TsSpreadFormatData.Create(ord(AFormat), AReaderClass, AWriterClass, AFormatName, ATechnicalName, AFileExtensions); n := SpreadFormatRegistry.Add(fmt); if (AFormat = sfUser) then begin if (n <= ord(sfUser)) then n := n + ord(sfUser) + 1; fmt.FFormatID := -n; end; Result := fmt.FormatID; end; function GetFileFormatFilter(AListSeparator, AExtSeparator: Char; AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID; AllSpreadFormats: Boolean = false; AllExcelFormats: Boolean = false): String; begin Result := SpreadFormatRegistry.GetFileFilter(AListSeparator, AExtSeparator, AFileAccess, APriorityFormats, AllSpreadFormats, AllExcelFormats); end; function GetSpreadFormats(AFileAccess: TsSpreadFileAccess; const APriorityFormats: array of TsSpreadFormatID): TsSpreadFormatIDArray; begin Result := SpreadFormatRegistry.GetFormatArray(AFileAccess, APriorityFormats); end; function GetSpreadFormatsFromFileName( AFileAccess: TsSpreadFileAccess; AFileName: TFileName; APriorityFormat: TsSpreadFormatID = sfidUnknown): TsSpreadFormatIDArray; begin Result := SpreadFormatRegistry.GetFormatArrayFromFileName( AFileAccess, AFileName, APriorityFormat); end; function GetSpreadFormatExt(AFormatID: TsSpreadFormatID): String; begin Result := SpreadFormatRegistry.DefaultExt[AFormatID]; end; function GetSpreadFormatName(AFormatID: TsSpreadFormatID): String; begin Result := SpreadFormatRegistry.FormatName[AFormatID]; end; function GetSpreadTechnicalName(AFormatID: TsSpreadFormatID): String; begin Result := SpreadFormatRegistry.TechnicalName[AFormatID]; end; function GetSpreadReaderClass(AFormatID: TsSpreadFormatID): TsSpreadReaderClass; begin Result := SpreadFormatRegistry.ReaderClass[AFormatID]; end; function GetSpreadWriterClass(AFormatID: TsSpreadFormatID): TsSpreadWriterClass; begin Result := SpreadFormatRegistry.WriterClass[AFormatID]; end; initialization SpreadFormatRegistry := TsSpreadFormatRegistry.Create; finalization SpreadFormatRegistry.Free; end.