{------------------------------------------------------------------------------- Unit : xlsxml Implements a reader and writer for the SpreadsheetXML format. This document was introduced by Microsoft for Excel XP and 2003. REFERENCE: http://msdn.microsoft.com/en-us/library/aa140066%28v=office.15%29.aspx AUTHOR : Werner Pamler LICENSE : For details about the license, see the file COPYING.modifiedLGPL.txt included in the Lazarus distribution. -------------------------------------------------------------------------------} unit xlsxml; {$ifdef fpc} {$mode objfpc}{$H+} {$endif} interface uses Classes, SysUtils, laz2_xmlread, laz2_DOM, fpsTypes, fpspreadsheet, fpsReaderWriter, xlsCommon; type { TsSpreadExcelXMLWriter } TsSpreadExcelXMLWriter = class(TsCustomSpreadWriter) private FDateMode: TDateMode; FPointSeparatorSettings: TFormatSettings; function GetCommentStr(ACell: PCell): String; function GetFormulaStr(ACell: PCell): String; function GetFrozenPanesStr(AWorksheet: TsWorksheet; AIndent: String): String; function GetHyperlinkStr(ACell: PCell): String; function GetIndexStr(AIndex: Integer): String; function GetLayoutStr(AWorksheet: TsWorksheet): String; function GetMergeStr(ACell: PCell): String; function GetPageFooterStr(AWorksheet: TsWorksheet): String; function GetPageHeaderStr(AWorksheet: TsWorksheet): String; function GetPageMarginStr(AWorksheet: TsWorksheet): String; function GetStyleStr(AFormatIndex: Integer): String; procedure WriteExcelWorkbook(AStream: TStream); procedure WriteStyle(AStream: TStream; AIndex: Integer); procedure WriteStyles(AStream: TStream); procedure WriteTable(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteWorksheet(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteWorksheetOptions(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteWorksheets(AStream: TStream); protected procedure WriteBlank(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); override; procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal; const AValue: boolean; ACell: PCell); override; procedure WriteCellToStream(AStream: TStream; ACell: PCell); override; procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); override; procedure WriteError(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TsErrorValue; ACell: PCell); override; procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); override; procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); override; public constructor Create(AWorkbook: TsWorkbook); override; procedure WriteToStream(AStream: TStream; AParams: TsStreamParams = []); override; end; TExcelXmlSettings = record DateMode: TDateMode; end; var { Default parameters for reading/writing } ExcelXmlSettings: TExcelXmlSettings = ( DateMode: dm1900; ); sfidExcelXML: TsSpreadFormatID; implementation uses StrUtils, Math, fpsStrings, fpsUtils, fpsNumFormat, fpsXmlCommon, fpsHTMLUtils; const FMT_OFFSET = 61; INDENT1 = ' '; INDENT2 = ' '; INDENT3 = ' '; INDENT4 = ' '; INDENT5 = ' '; TABLE_INDENT = INDENT2; ROW_INDENT = INDENT3; COL_INDENT = INDENT3; CELL_INDENT = INDENT4; VALUE_INDENT = INDENT5; LF = LineEnding; const {TsFillStyle = ( fsNoFill, fsSolidFill, fsGray75, fsGray50, fsGray25, fsGray12, fsGray6, fsStripeHor, fsStripeVert, fsStripeDiagUp, fsStripeDiagDown, fsThinStripeHor, fsThinStripeVert, fsThinStripeDiagUp, fsThinStripeDiagDown, fsHatchDiag, fsThinHatchDiag, fsThickHatchDiag, fsThinHatchHor) } FILL_NAMES: array[TsFillStyle] of string = ( '', 'Solid', 'Gray75', 'Gray50', 'Gray25', 'Gray12', 'Gray0625', 'HorzStripe', 'VertStripe', 'DiagStripe', 'ReverseDiagStripe', 'ThinHorzStripe', 'ThinVertStripe', 'ThinDiagStripe', 'ThinReverseDiagStripe', 'DiagCross', 'ThinDiagCross', 'ThickDiagCross', 'ThinHorzCross' ); {TsCellBorder = (cbNorth, cbWest, cbEast, cbSouth, cbDiagUp, cbDiagDown); } BORDER_NAMES: array[TsCellBorder] of string = ( 'Top', 'Left', 'Right', 'Bottom', 'DiagonalRight', 'DiagonalLeft' ); {TsLineStyle = ( lsThin, lsMedium, lsDashed, lsDotted, lsThick, lsDouble, lsHair, lsMediumDash, lsDashDot, lsMediumDashDot, lsDashDotDot, lsMediumDashDotDot, lsSlantDashDot) } LINE_STYLES: array[TsLineStyle] of string = ( 'Continuous', 'Continuous', 'Dash', 'Dot', 'Continuous', 'Double', 'Continuous', 'Dash', 'DashDot', 'DashDot', 'DashDotDot', 'DashDotDot', 'SlantDashDot' ); LINE_WIDTHS: array[TsLineStyle] of Integer = ( 1, 2, 1, 1, 3, 3, 0, 2, 1, 2, 1, 2, 2 ); FALSE_TRUE: array[boolean] of string = ('False', 'True'); function GetCellContentTypeStr(ACell: PCell): String; begin case ACell^.ContentType of cctNumber : Result := 'Number'; cctUTF8String : Result := 'String'; cctDateTime : Result := 'DateTime'; cctBool : Result := 'Boolean'; cctError : Result := 'Error'; else raise EFPSpreadsheet.Create('Content type error in cell ' + GetCellString(ACell^.Row, ACell^.Col)); end; end; {@@ ---------------------------------------------------------------------------- Constructor of the ExcelXML writer Defines the date mode and the limitations of the file format. Initializes the format settings to be used when writing to xml. -------------------------------------------------------------------------------} constructor TsSpreadExcelXMLWriter.Create(AWorkbook: TsWorkbook); begin inherited Create(AWorkbook); // Initial base date in case it won't be set otherwise. // Use 1900 to get a bit more range between 1900..1904. FDateMode := ExcelXMLSettings.DateMode; // Special version of FormatSettings using a point decimal separator for sure. FPointSeparatorSettings := DefaultFormatSettings; FPointSeparatorSettings.DecimalSeparator := '.'; // http://en.wikipedia.org/wiki/List_of_spreadsheet_software#Specifications FLimitations.MaxColCount := 256; FLimitations.MaxRowCount := 65536; end; function TsSpreadExcelXMLWriter.GetCommentStr(ACell: PCell): String; var comment: PsComment; begin Result := ''; comment := FWorksheet.FindComment(ACell); if Assigned(comment) then Result := INDENT1 + '' + comment^.Text + '' + LF + CELL_INDENT; // If there will be some rich-text-like formatting in the future, use // Result := ''+comment^.Text+'': end; function TsSpreadExcelXMLWriter.GetFormulaStr(ACell: PCell): String; begin if HasFormula(ACell) then begin Result := UTF8TextToXMLText(FWorksheet.ConvertFormulaDialect(ACell, fdExcelR1C1)); Result := ' ss:Formula="=' + Result + '"'; end else Result := ''; end; function TsSpreadExcelXMLWriter.GetFrozenPanesStr(AWorksheet: TsWorksheet; AIndent: String): String; var activePane: Integer; begin if (soHasFrozenPanes in AWorksheet.Options) then begin Result := AIndent + '' + LF + AIndent + '' + LF; if FWorksheet.LeftPaneWidth > 0 then Result := Result + AIndent + '1' + LF + AIndent + '' + IntToStr(FWorksheet.LeftPaneWidth) + '' + LF; if FWorksheet.TopPaneHeight > 0 then Result := Result + AIndent + '1' + LF + AIndent + '' + IntToStr(FWorksheet.TopPaneHeight) + '' + LF; if (FWorksheet.LeftPaneWidth = 0) and (FWorkSheet.TopPaneHeight = 0) then activePane := 3 else if (FWorksheet.LeftPaneWidth = 0) then activePane := 2 else if (FWorksheet.TopPaneHeight = 0) then activePane := 1 else activePane := 0; Result := Result + AIndent + '' + IntToStr(activePane) + '' + LF; end else Result := ''; end; function TsSpreadExcelXMLWriter.GetHyperlinkStr(ACell: PCell): String; var hyperlink: PsHyperlink; begin Result := ''; hyperlink := FWorksheet.FindHyperlink(ACell); if Assigned(hyperlink) then Result := ' ss:HRef="' + hyperlink^.Target + '"'; end; function TsSpreadExcelXMLWriter.GetIndexStr(AIndex: Integer): String; begin Result := Format(' ss:Index="%d"', [AIndex]); end; function TsSpreadExcelXMLWriter.GetLayoutStr(AWorksheet: TsWorksheet): String; begin Result := ''; if AWorksheet.PageLayout.Orientation = spoLandscape then Result := Result + ' x:Orientation="Landscape"'; if (poHorCentered in AWorksheet.PageLayout.Options) then Result := Result + ' x:CenterHorizontal="1"'; if (poVertCentered in AWorksheet.PageLayout.Options) then Result := Result + ' x:CenterVertical="1"'; if (poUseStartPageNumber in AWorksheet.PageLayout.Options) then Result := Result + ' x:StartPageNumber="' + IntToStr(AWorksheet.PageLayout.StartPageNumber) + '"'; Result := ''; end; function TsSpreadExcelXMLWriter.GetMergeStr(ACell: PCell): String; var r1, c1, r2, c2: Cardinal; begin Result := ''; if FWorksheet.IsMerged(ACell) then begin FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2); if c2 > c1 then Result := Result + Format(' ss:MergeAcross="%d"', [c2-c1]); if r2 > r1 then Result := Result + Format(' ss:MergeDown="%d"', [r2-r1]); end; end; function TsSpreadExcelXMLWriter.GetPageFooterStr(AWorksheet: TsWorksheet): String; begin Result := Format('x:Margin="%g"', [mmToIn(AWorksheet.PageLayout.FooterMargin)], FPointSeparatorSettings); if (AWorksheet.PageLayout.Footers[HEADER_FOOTER_INDEX_ALL] <> '') then Result := Result + ' x:Data="' + UTF8TextToXMLText(AWorksheet.PageLayout.Footers[HEADER_FOOTER_INDEX_ALL], true) + '"'; Result := '