{------------------------------------------------------------------------------- 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, fpsReaderWriter, fpsConditionalFormat, fpsXMLCommon, xlsCommon; type { TsSpreadExcelXMLReader } TsSpreadExcelXMLReader = class(TsSpreadXMLReader) private FDateMode: TDateMode; FPointSeparatorSettings: TFormatSettings; function ExtractDateTime(AText: String): TDateTime; protected FFirstNumFormatIndexInFile: Integer; procedure AddBuiltinNumFormats; override; protected procedure ReadAlignment(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadBorder(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadBorders(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadCell(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ARow, ACol: Integer); procedure ReadCellProtection(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadComment(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ACell: PCell); procedure ReadConditionalFormatting(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadExcelWorkbook(ANode: TDOMNode); procedure ReadFont(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadInterior(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadNames(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadNumberFormat(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadPageBreak(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadPageBreaks(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadPageSetup(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadPrint(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadRow(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ARow: Integer); procedure ReadStyle(ANode: TDOMNode); procedure ReadStyles(ANode: TDOMNode); procedure ReadTable(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadWorksheet(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadWorksheetOptions(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure ReadWorksheets(ANode: TDOMNode); public constructor Create(AWorkbook: TsBasicWorkbook); override; procedure ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); override; end; { TsSpreadExcelXMLWriter } TsSpreadExcelXMLWriter = class(TsCustomSpreadWriter) private FDateMode: TDateMode; FPointSeparatorSettings: TFormatSettings; FFirstRow, FFirstCol: Cardinal; FlastRow, FLastCol: Cardinal; FPrevRow, FPrevCol: Cardinal; function GetCommentStr(ACell: PCell): String; function GetFormulaStr(ACell: PCell): String; function GetFrozenPanesStr(AWorksheet: TsBasicWorksheet; AIndent: String): String; function GetHyperlinkStr(ACell: PCell): String; function GetIndexStr(AIndex, APrevIndex: Cardinal): String; function GetLayoutStr(AWorksheet: TsBasicWorksheet): String; function GetMergeStr(ACell: PCell): String; function GetPageFooterStr(AWorksheet: TsBasicWorksheet): String; function GetPageHeaderStr(AWorksheet: TsBasicWorksheet): String; function GetPageMarginStr(AWorksheet: TsBasicWorksheet): String; function GetPrintStr(AWorksheet: TsBasicWorksheet): String; function GetStyleStr(AFormatIndex: Integer): String; procedure WriteCellNodes(AStream: TStream; AWorksheet: TsBasicWorksheet; ARow: Cardinal); procedure WriteColumns(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteConditionalFormat(AStream: TStream; AWorksheet: TsBasicWorksheet; AFormat: TsConditionalFormat); procedure WriteConditionalFormatting(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteDocumentProperties(AStream: TStream); procedure WriteExcelWorkbook(AStream: TStream); procedure WriteNames(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteOfficeDocumentSettings(AStream: TStream); procedure WritePageBreaks(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteRows(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteStyle(AStream: TStream; AIndex: Integer); procedure WriteStyles(AStream: TStream); procedure WriteTable(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteWorksheet(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteWorksheetOptions(AStream: TStream; AWorksheet: TsBasicWorksheet); 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 WriteFormula(AStream: TStream; const ARow, ACol: Cardinal; 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: TsBasicWorkbook); 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, DateUtils, Math, Variants, TypInfo, fpsStrings, fpsClasses, fpspreadsheet, fpsUtils, fpsNumFormat, fpsHTMLUtils, fpsExprParser; const FMT_OFFSET = 61; INDENT1 = ' '; INDENT2 = ' '; INDENT3 = ' '; INDENT4 = ' '; INDENT5 = ' '; NAMES_INDENT = INDENT2; NAME_INDENT = INDENT3; 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', // 'Solid', 'Solid', 'Solid', 'Solid', 'Solid', 'Gray75', 'Gray50', 'Gray25', 'Gray125', 'Gray0625', 'HorzStripe', 'VertStripe', 'DiagStripe', 'ReverseDiagStripe', 'ThinHorzStripe', 'ThinVertStripe', 'ThinDiagStripe', 'ThinReverseDiagStripe', 'DiagCross', 'ThinDiagCross', 'ThickDiagCross', 'ThinHorzCross' ); { Fill style names as used in the Style attribute for conditional formatting -- not all tested... } CF_FILL_NAMES: array[TsFillStyle] of string = ( '', 'solid', 'gray-75', 'gray-50', 'gray-25', 'gray-125', 'gray-0625', 'horz-stripe', 'vert-stripe', 'diag-stripe', 'reverse-diag-stripe', 'thin-horz-stripe', 'thin-vert-stripe', 'thin-diag-stripe', 'thin-reverse-diag-stripe', 'diag-cross', 'thin-diag-cross', 'thick-diag-cross', 'thin-horz-cross' ); {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' ); CF_LINE_STYLES: array[TsLineStyle] of string = ( 'solid', 'solid', 'dashed', 'dotted', 'solid', 'double', 'hairline', 'dashed', 'dot-dash', 'dot-dash', 'dot-dot-dash', 'dot-dot-dash', 'dot-dash' ); 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'); CF_CONDITIONS: array[TsCFCondition] of string = ( 'Equal', 'NotEqual', // cfcEqual, cfcNotEqual, 'Greater', 'Less', 'GreaterOrEqual', 'LessOrEqual', // cfcGreaterThan, cfcLessThan, cfcGreaterEqual, cfcLessEqual, 'Between', 'NotBetween', // cfcBetween, cfcNotBetween, // the following 4 formulas are copies of Excel-generated files, they exist in the xmls file, but Excel does not display them... '@RC>AVERAGE( IF(ISERROR(%2:s), "", IF(ISBLANK(%2:s), "", %2:s)))', // cfcAboveAverage '@RC<AVERAGE( IF(ISERROR(%2:s), "", IF(ISBLANK(%2:s), "", %2:s)))', // cfcBelowAverage '@RC>=AVERAGE( IF(ISERROR(%2:s), "", IF(ISBLANK(%2:s), "", %2:s)))', // cfcAboveEqualAverage '@RC<=AVERAGE( IF(ISERROR(%2:s), "", IF(ISBLANK(%2:s), "", %2:s)))', // cfcBelowEqualAverage // The next 4 formulas are not supported by Excel-XML '', '', '', '', // cfcTop, cfcBottom, cfcTopPercent, cfcBottomPercent, '@AND(COUNTIF(%2:s, RC)>1,NOT(ISBLANK(RC)))', // cfcDuplicate '@AND(COUNTIF(%2:s, RC)=1,NOT(ISBLANK(RC)))', // cfcUnique '@LEFT(RC,LEN(%0:s))=%0:s', // cfcBeginsWith '@RIGHT(RC,LEN(%0:s))=%0:s', // cfcEndsWith '@NOT(ISERROR(SEARCH(%0:s,RC)))', // cfcContainsText '@ISERROR(SEARCH(%0:s,RC))', // cfcNotContainsText, '@ISERROR(RC)', // cfcContainsErrors '@NOT(ISERROR(RC))', // cfcNotContainsErrors '@' // cfcExpression ); // The leading '@' indicates that the formula will be used in <Value1> node // Parameter 0 is Operand1, parameter 1 is Operand2 and parameter 2 is Range 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; { Helper routine to rebuild the html content of the "ss:Data" nodes } procedure RebuildChildNodes(ANode: TDOMNode; var AText: String); var nodeName: String; s: String; i: Integer; begin if ANode = nil then exit; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = '#text' then AText := AText + ANode.NodeValue else begin s := ''; for i := 0 to ANode.Attributes.Length-1 do s := Format('%s %s="%s"', [s, ANode.Attributes.Item[i].NodeName, ANode.Attributes.Item[i].NodeValue]); AText := Format('%s<%s%s>', [AText, nodeName, s]); s := ''; RebuildChildNodes(ANode.FirstChild, s); if s <> '' then AText := Format('%s%s</%s>', [AText, s, nodeName]); end; ANode := ANode.NextSibling; end; end; function CFOperandToStr(v: variant; AWorksheet: TsWorksheet): String; var r,c: Cardinal; parser: TsSpreadsheetParser; begin Result := VarToStr(v); if Result = '' then exit; if VarIsStr(v) then begin // Special case: v is a formula, i.e. begins with '=' if (Length(Result) > 1) and (Result[1] = '=') then begin parser := TsSpreadsheetParser.Create(AWorksheet); try try parser.Expression[fdExcelA1] := Result; // Parse in Excel-A1 dialect Result := parser.R1C1Expression[nil]; // Convert to R1C1 dialect except on EGeneralExprParserError do begin Result := VarToStr(v); AWorksheet.Workbook.AddErrorMsg('Error in CF Expression ' + Result); end; end; // Note: Using nil here to get absolute references. finally parser.Free; end; end else // Special case: cell reference (Note: relative refs are made absolute!) if ParseCellString(Result, r, c) then Result := GetCellString_R1C1(r, c, []) // Need absolute reference! else Result := UTF8TextToXMLText(SafeQuoteStr(Result)) end; end; function TryStrToCFLineStyle(s: String; out ALineStyle: TsLineStyle): Boolean; var ls: TsLineStyle; begin for ls in TsLineStyle do if s = CF_LINE_STYLES[ls] then begin Result := true; ALineStyle := ls; exit; end; Result := false; end; function TryStrToCFCellBorder(s: String; out ABorder: TsCellBorder): Boolean; begin Result := true; if s = 'border-left' then ABorder := cbWest else if s = 'border-right' then Aborder := cbEast else if s = 'border-top' then ABorder := cbNorth else if s = 'border-bottom' then ABorder := cbSouth else if s = 'border-diagonal-right' then // not tested ! ABorder := cbDiagUp else if s = 'border-diagonal-left' then // not tested ! ABorder := cbDiagDown else Result := false; end; { Analyzes the given expression. Using the @ templates of CF_CONDITIONS it determines the condition type as well as the parameters. } procedure AnalyzeCFExpression(AExpr: String; out ACondition: TsCFCondition; out AParam: String); var p, n: Integer; begin AParam := ''; //AExpr := UTF8TextToXMLText(AExpr); if pos('RC>AVERAGE(', AExpr) = 1 then ACondition := cfcAboveAverage else if pos ('RC<AVERAGE(', AExpr) = 1 then ACondition := cfcBelowAverage else if pos('RC>=AVERAGE(', AExpr) = 1 then ACondition := cfcAboveEqualAverage else if pos('RC<=AVERAGE(', AExpr) = 1 then ACondition := cfcBelowEqualAverage else if (pos('AND(COUNTIF(', AExpr) = 1) and (pos('>', AExpr) > 0) then ACondition := cfcDuplicate else if (pos('AND(COUNTIF(', AExpr) = 1) and (pos('=1', AExpr) > 0) then ACondition := cfcUnique else if pos('LEFT(RC,LEN(', AExpr) = 1 then begin ACondition := cfcBeginsWith; p := pos(')', AExpr); n := Length('LEFT(RC,LEN('); AParam := UnquoteStr(Trim(Copy(AExpr, n+1, p-n-1))); end else if pos('RIGHT(RC,LEN(',AExpr) = 1 then begin ACondition := cfcEndsWith; p := pos(')', AExpr); n := Length('RIGHT(RC,LEN('); AParam := UnquoteStr(Trim(Copy(AExpr, n+1, p-n-1))); end else if pos('NOT(ISERROR(SEARCH(', AExpr) = 1 then begin ACondition := cfcContainsText; p := pos(',', AExpr); n := Length('NOT(ISERROR(SEARCH('); AParam := UnquoteStr(Trim(Copy(AExpr, n+1, p-n-1))); end else if pos('ISERROR(SEARCH(', AExpr) = 1 then begin ACondition := cfcNotContainsText; p := pos(',', AExpr); n := Length('ISERROR(SEARCH('); AParam := UnquoteStr(Trim(Copy(AExpr, n+1, p-n-1))); end else if AExpr = 'ISERROR(RC)' then ACondition := cfcContainsErrors else if AExpr = 'NOT(ISERROR(RC))' then ACondition := cfcNotContainsErrors else begin ACondition := cfcExpression; AParam := AExpr; end; end; {=============================================================================== TsSpreadExcelXMLReader ===============================================================================} {@@ ---------------------------------------------------------------------------- Constructor of the ExcelXML reader -------------------------------------------------------------------------------} constructor TsSpreadExcelXMLReader.Create(AWorkbook: TsBasicWorkbook); begin inherited; // Cell formats (named "Styles" here). FCellFormatList := TsCellFormatList.Create(true); // is destroyed by ancestor // Special version of FormatSettings using a point decimal separator for sure. FPointSeparatorSettings := DefaultFormatSettings; FPointSeparatorSettings.DecimalSeparator := '.'; end; procedure TsSpreadExcelXMLReader.AddBuiltinNumFormats; begin FFirstNumFormatIndexInFile := 164; AddBuiltInBiffFormats( FNumFormatList, FWorkbook.FormatSettings, FFirstNumFormatIndexInFile-1 ); end; {@@ ---------------------------------------------------------------------------- Extracts the date/time value from the given string. The string is formatted as 'yyyy-mm-dd"T"hh:nn:ss.zzz' -------------------------------------------------------------------------------} function TsSpreadExcelXMLReader.ExtractDateTime(AText: String): TDateTime; var dateStr, timeStr: String; begin dateStr := Copy(AText, 1, 10); timeStr := Copy(AText, 12, MaxInt); Result := ScanDateTime('yyyy-mm-dd', dateStr) + ScanDateTime('hh:nn:ss.zzz', timeStr); end; {@@ ---------------------------------------------------------------------------- Reads the cell alignment from the given node attributes -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadAlignment(ANode: TDOMNode; var AFormat: TsCellFormat); var s: String; begin // Vertical alignment s := GetAttrValue(ANode, 'ss:Vertical'); if s <> '' then with AFormat do begin Include(UsedFormattingFields, uffVertAlign); case s of 'Top': VertAlignment := vaTop; 'Center': VertAlignment := vaCenter; 'Bottom': VertAlignment := vaBottom; else Exclude(UsedFormattingFields, uffVertAlign); end; end; // Horizontal alignment s := GetAttrValue(ANode, 'ss:Horizontal'); if s <> '' then with AFormat do begin Include(UsedFormattingFields, uffHorAlign); case s of 'Left': HorAlignment := haLeft; 'Center': HorAlignment := haCenter; 'Right': HorAlignment := haRight; else Exclude(UsedFormattingFields, uffHorAlign); end; end; // Vertical text s := GetAttrValue(ANode, 'ss:Rotate'); if s = '90' then with AFormat do begin TextRotation := rt90DegreeCounterClockwiseRotation; Include(UsedFormattingFields, uffTextRotation); end else if s = '-90' then with AFormat do begin TextRotation := rt90DegreeClockwiseRotation; Include(UsedFormattingFields, uffTextRotation); end; s := GetAttrValue(ANode, 'ss:VerticalText'); if s <> '' then with AFormat do begin TextRotation := rtStacked; Include(UsedFormattingFields, uffTextRotation); end; // Word wrap s := GetAttrValue(ANode, 'ss:WrapText'); if s = '1' then with AFormat do Include(UsedFormattingFields, uffWordWrap); // BiDi s := GetAttrValue(ANode, 'ss:ReadingOrder'); if s <> '' then with AFormat do begin case s of 'RightToLeft': BiDiMode := bdRTL; 'LeftToRight': BiDiMode := bdLTR; end; Include(UsedFormattingFields, uffBiDi); end; end; {@@ ---------------------------------------------------------------------------- Read a "Style/Borders/Border" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadBorder(ANode: TDOMNode; var AFormat: TsCellFormat); // <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="3" ss:Color="#ED7D31"/> var s, sw: String; b: TsCellBorder; begin AFormat.UsedFormattingFields := AFormat.UsedFormattingFields + [uffBorder]; // Border position s := GetAttrValue(ANode, 'ss:Position'); case s of 'Left': b := cbWest; 'Right': b := cbEast; 'Top': b := cbNorth; 'Bottom': b := cbSouth; 'DiagonalRight': b := cbDiagUp; 'DiagonalLeft': b := cbDiagDown; end; Include(AFormat.Border, b); // Border color s := GetAttrValue(ANode, 'ss:Color'); if s = '' then AFormat.BorderStyles[b].Color := scBlack else AFormat.BorderStyles[b].Color := HTMLColorStrToColor(s); // Line style s := GetAttrValue(ANode, 'ss:LineStyle'); sw := GetAttrValue(ANode, 'ss:Weight'); case s of 'Continuous': if sw = '1' then AFormat.BorderStyles[b].LineStyle := lsThin else if sw = '2' then AFormat.BorderStyles[b].LineStyle := lsMedium else if sw = '3' then AFormat.BorderStyles[b].LineStyle := lsThick else if sw = '' then AFormat.BorderStyles[b].LineStyle := lsHair; 'Double': AFormat.BorderStyles[b].LineStyle := lsDouble; 'Dot': AFormat.BorderStyles[b].LineStyle := lsDotted; 'Dash': if sw = '2' then AFormat.BorderStyles[b].LineStyle := lsMediumDash else AFormat.BorderStyles[b].LineStyle := lsDashed; 'DashDot': if sw = '2' then AFormat.BorderStyles[b].LineStyle := lsMediumDashDot else AFormat.BorderStyles[b].LineStyle := lsDashDot; 'DashDotDot': if sw = '2' then AFormat.BorderStyles[b].LineStyle := lsMediumDashDotDot else AFormat.BorderStyles[b].LineStyle := lsDashDotDot; 'SlantDashDot': AFormat.BorderStyles[b].LineStyle := lsSlantDashDot; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Styles/Style/Borders" nodes -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadBorders(ANode: TDOMNode; var AFormat: TsCellFormat); var nodeName: String; begin if ANode = nil then exit; ANode := ANode.FirstChild; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Border' then ReadBorder(ANode, AFormat); ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads a "Worksheet/Table/Row/Cell" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadCell(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ARow, ACol: Integer); var book: TsWorkbook; sheet: TsWorksheet absolute AWorksheet; nodeName: string; s, st, sv: String; txt: String; node: TDOMNode; err: TsErrorValue; cell: PCell; fmt: TsCellFormat; nfp: TsNumFormatParams; idx: Integer; mergedCols, mergedRows: Integer; font: TsFont; dt: TDateTime; begin if ANode = nil then exit; nodeName := ANode.NodeName; if nodeName <> 'Cell' then raise Exception.Create('[ReadCell] "Cell" node expected.'); book := TsWorkbook(FWorkbook); font := book.GetDefaultFont; if FIsVirtualMode then begin if not Assigned(book.OnReadCellData) then exit; InitCell(FWorksheet, ARow, ACol, FVirtualCell); cell := @FVirtualCell; end else cell := sheet.AddCell(ARow, ACol); s := GetAttrValue(ANode, 'ss:StyleID'); if s <> '' then begin idx := FCellFormatList.FindIndexOfName(s); if idx <> -1 then begin fmt := FCellFormatList.Items[idx]^; cell^.FormatIndex := book.AddCellFormat(fmt); font := book.GetFont(fmt.FontIndex); end; end else begin InitFormatRecord(fmt); cell^.FormatIndex := 0; end; // Merged cells s := GetAttrValue(ANode, 'ss:MergeAcross'); if not ((s <> '') and TryStrToInt(s, mergedCols)) then mergedCols := 0; s := GetAttrValue(ANode, 'ss:MergeDown'); if not ((s <> '') and TryStrToint(s, mergedRows)) then mergedRows := 0; if (mergedCols > 0) or (mergedRows > 0) then sheet.MergeCells(ARow, ACol, ARow + mergedRows, ACol + mergedCols); // Formula s := GetAttrValue(ANode, 'ss:Formula'); if s <> '' then begin try sheet.WriteFormula(cell, s, false, true); except on E:EExprParser do begin FWorkbook.AddErrorMsg(E.Message); if (boAbortReadOnFormulaError in FWorkbook.Options) then raise; end; on E:ECalcEngine do begin FWorkbook.AddErrorMsg(E.Message); if (boAbortReadOnFormulaError in FWorkbook.Options) then raise; end; end; end; // Hyperlink s := GetAttrValue(ANode, 'ss:HRef'); if s <> '' then begin st := GetAttrValue(ANode, 'x:HRefScreenTip'); sheet.WriteHyperlink(cell, s, st); end; // Cell data and comment node := ANode.FirstChild; if node = nil then sheet.WriteBlank(cell) else begin book.LockFormulas; // Protect formulas from being deleted by the WriteXXXX calls try while node <> nil do begin nodeName := node.NodeName; if (nodeName = 'Data') or (nodeName = 'ss:Data') then begin sv := node.TextContent; st := GetAttrValue(node, 'ss:Type'); case st of 'String': sheet.WriteText(cell, sv); 'Number': sheet.WriteNumber(cell, StrToFloat(sv, FPointSeparatorSettings)); 'DateTime': begin dt := ExtractDateTime(sv); if (cell^.FormatIndex > 0) then begin nfp := TsWorkbook(FWorkbook).GetNumberFormat(fmt.NumberFormatIndex); if not IsTimeIntervalFormat(nfp) then dt := ConvertExcelDateTimeToDateTime(dt, FDateMode); end; sheet.WriteDateTime(cell, dt); end; 'Boolean': if sv = '1' then sheet.WriteBoolValue(cell, true) else if sv = '0' then sheet.WriteBoolValue(cell, false); 'Error': if TryStrToErrorValue(sv, err) then sheet.WriteErrorValue(cell, err); end; if nodeName = 'ss:Data' then begin txt := ''; RebuildChildNodes(node, txt); HTMLToRichText(FWorkbook, font, txt, s, cell^.RichTextParams, 'html:'); end; end else if (nodeName = 'Comment') then ReadComment(node, AWorksheet, cell); node := node.NextSibling; end; if FIsVirtualMode then book.OnReadCellData(book, ARow, ACol, cell); finally book.UnlockFormulas; end; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Styles/Style/Protection" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadCellProtection(ANode: TDOMNode; var AFormat: TsCellFormat); var s: String; begin if ANode = nil then exit; s := GetAttrValue(ANode, 'ss:Protected'); if s = '0' then Exclude(AFormat.Protection, cpLockCell); s := GetAttrValue(ANode, 'x:HideFormula'); if s = '1' then Include(AFormat.Protection, cpHideFormulas); if AFormat.Protection <> DEFAULT_CELL_PROTECTION then Include(AFormat.UsedFormattingFields, uffProtection); end; {@@ ---------------------------------------------------------------------------- Reads the "Worksheet/Table/Row/Cell/Comment" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadComment(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ACell: PCell); var txt: String; begin txt := ANode.TextContent; TsWorksheet(AWorksheet).WriteComment(ACell, txt); end; {@@ ---------------------------------------------------------------------------- Reads the "Worksheet/ConditionalFormatting" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadConditionalFormatting(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var book: TsWorkbook; sheet: TsWorksheet; childNode: TDOMNode; nodeName: String; s, nameStr, valueStr, tmpStr: String; range: TsCellRange; flags: TsRelFlags; i, j: Integer; c: TsCFCondition; condition: Integer; op1, op2: Variant; fgColor, bgColor: TsColor; fs, fill: TsFillStyle; p: Integer; L: TStrings; sa: TStringArray; fmt: TsCellFormat; fmtIndex: Integer; fntstyle: TsFontStyles; fntColor: TsColor; fnt: TsFont; cb: TsCellBorder; borders: TsCellBorders; lineStyle: Integer; lineColor: TsColor; commonBorder: TsCellBorderStyle; borderStyles: TsCellBorderStyles; parser: TsSpreadsheetParser; begin sheet := TsWorksheet(AWorksheet); book := TsWorkbook(FWorkbook); // initialize parameters condition := -1; range := fpsUtils.Range(Cardinal(-1), Cardinal(-1), Cardinal(-1), Cardinal(-1)); VarClear(op1{%H-}); VarClear(op2{%H-}); bgColor := scNotDefined; fgColor := scNotDefined; fill := fsNoFill; fntStyle := []; fntColor := scNotDefined; commonBorder := NO_CELL_BORDER; borderStyles[cbNorth] := NO_CELL_BORDER; borderStyles[cbSouth] := NO_CELL_BORDER; borderStyles[cbEast] := NO_CELL_BORDER; borderStyles[cbWest] := NO_CELL_BORDER; borders := []; nodeName := ANode.NodeName; // for debugging // Read nodes while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Range' then begin s := GetNodeValue(ANode); if not ParseCellRangeString_R1C1(s, 0, 0, range.Row1, range.Col1, range.Row2, range.Col2, flags) then begin book.AddErrorMsg('Conditional format range %s not readable', [s]); exit; end; end; if nodeName = 'Condition' then begin childNode := ANode.FirstChild; while childNode <> nil do begin nodeName := childNode.NodeName; if nodeName = 'Qualifier' then begin s := GetNodeValue(childNode); if (s <> '') and (s[1] <> '@') then begin for c in TsCFCondition do if s = CF_CONDITIONS[c] then begin condition := ord(c); break; end; end; end else if nodeName = 'Value1' then begin s := GetNodeValue(childNode); if s <> '' then op1 := s; end else if nodeName = 'Value2' then begin s := GetNodeValue(childNode); if s <> '' then op2 := s; end else if nodeName = 'Format' then begin s := GetAttrValue(childNode, 'Style'); L := TStringList.Create; try L.Delimiter := ';'; L.NameValueSeparator := ':'; L.StrictDelimiter := true; L.DelimitedText := s; for i := 0 to L.Count-1 do begin nameStr := Trim(L.Names[i]); valueStr := Trim(L.ValueFromIndex[i]); case nameStr of 'background': bgColor := HTMLColorStrToColor(valueStr); 'mso-pattern': for fs in TsFillStyle do begin p := pos(CF_FILL_NAMES[fs], valueStr); if p > 0 then begin fill := fs; Delete(valueStr, p, Length(CF_FILL_NAMES[fs])); fgColor := HTMLColorStrToColor(Trim(valueStr)); break; end; end; 'font-style': if valueStr = 'italic' then fntStyle := fntStyle + [fssItalic]; 'font-weight': if StrToInt(valueStr) > 500 then fntStyle := fntStyle + [fssBold]; 'text-line-through': fntStyle := fntStyle + [fssStrikeOut]; 'color': fntColor := HTMLColorStrToColor(valueStr); 'border', 'border-top', 'border-bottom', 'border-left', 'border-right': begin if nameStr = 'border' then borders := ALL_BORDERS else begin if not TryStrToCFCellBorder(nameStr, cb) then Continue; if valueStr = 'none' then Continue; end; sa := valueStr.Split(' '); lineColor := scNotDefined; lineStyle := -1; for j := 0 to High(sa) do begin tmpStr := Trim(sa[j]); // Line width not supported if pos('pt', tmpStr) > 0 then Continue; // Extract line style if (linestyle = -1) and TryStrToCFLineStyle(tmpStr, TsLineStyle(linestyle)) then Continue; // Extract line color if (lineColor = scNotDefined) then lineColor := HTMLColorStrToColor(tmpStr); end; if nameStr = 'border' then begin if linestyle = -1 then commonBorder.LineStyle := lsThin else commonBorder.LineStyle := TsLineStyle(linestyle); commonBorder.Color := lineColor; end else begin Include(borders, cb); if lineStyle = -1 then borderStyles[cb].LineStyle := lsThin else borderStyles[cb].LineStyle := TsLineStyle(linestyle); borderStyles[cb].Color := lineColor; end; end; end; end; finally L.Free; end; end; childNode := childNode.NextSibling; end; if (condition = -1) and (op1 <> '') then begin AnalyzeCFExpression(op1, TsCFCondition(condition), s); if s = '' then VarClear(op1) else if TsCFCondition(condition) = cfcExpression then begin parser := TsSpreadsheetParser.Create(AWorksheet); try try parser.R1C1Expression[nil] := s; // Parse in Excel-R1C1 dialect op1 := parser.Expression[fdExcelA1]; // Convert to Excel-A1 dialect except VarClear(op1); end; finally parser.Free; end; end else op1 := s; end; end; ANode := ANode.NextSibling; end; if (range.Row1 = Cardinal(-1)) or (range.Col1 = Cardinal(-1)) or (range.Row2 = Cardinal(-1)) or (Range.Col2 = Cardinal(-1)) then begin book.AddErrorMsg('Missing cell range for conditional formatting.'); exit; end; if condition = -1 then begin book.AddErrorMsg('No condition given in conditional format.'); exit; end; // Prepare format record used by the conditional format InitFormatRecord(fmt); // ... background if (bgColor <> scNotDefined) or (fgColor <> scNotDefined) or (fill <> fsNoFill) then begin if fgColor = scNotDefined then fmt.SetBackgroundColor(bgColor) else fmt.SetBackground(fill, fgColor, bgColor); end; // ... font if (fntStyle <> []) or (fntColor <> scNotDefined) then begin fnt := book.CloneFont(fmt.FontIndex); if fntStyle <> [] then fnt.Style := fntStyle; if fntColor <> scNotDefined then fnt.Color := fntColor; fmt.SetFont(book.AddFont(fnt)); end; // .. borders if commonBorder.Color <> scNotDefined then fmt.SetBorders(ALL_BORDERS, commonBorder.Color, commonBorder.LineStyle) else for cb in borders do fmt.SetBorders([cb], borderStyles[cb].Color, borderStyles[cb].LineStyle); // Add format record to format list fmtIndex := book.AddCellFormat(fmt); // Attach as conditional format to the given cell range of the worksheet sheet.WriteConditionalCellFormat(range, TsCFCondition(condition), op1, op2, fmtIndex); end; {@@ ---------------------------------------------------------------------------- Reads the "ExcelWorkbook" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadExcelWorkbook(ANode: TDOMNode); var s: String; nodeName: String; n: Integer; begin if ANode = nil then exit; ANode := ANode.FirstChild; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'ActiveSheet' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then with TsWorkbook(FWorkbook) do SelectWorksheet(GetWorksheetByIndex(n)); end else if nodeName = 'ProtectStructure' then begin s := ANode.TextContent; if s = 'True' then FWorkbook.Protection := FWorkbook.Protection + [bpLockStructure]; end else if nodeName = 'ProtectWindows' then begin s := ANode.TextContent; if s = 'True' then FWorkbook.Protection := FWorkbook.Protection + [bpLockWindows]; end else if nodeName = 'Date1904' then FDateMode := dm1904; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Styles/Style/Font" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLreader.ReadFont(ANode: TDOMNode; var AFormat: TsCellFormat); var book: TsWorkbook; fname: String; fsize: Single; fcolor: TsColor; fstyle: TsFontStyles; s: String; begin if ANode = nil then exit; book := TsWorkbook(FWorkbook); fname := GetAttrValue(ANode, 'ss:FontName'); if fname = '' then fname := book.GetDefaultFont.FontName; s := GetAttrValue(ANode, 'ss:Size'); if (s = '') or not TryStrToFloat(s, fsize, FPointSeparatorSettings) then fsize := book.GetDefaultFont.Size; s := GetAttrValue(ANode, 'ss:Color'); if s <> '' then fcolor := HTMLColorStrToColor(s) else fcolor := book.GetDefaultFont.Color; fstyle := []; s := GetAttrValue(ANode, 'ss:Bold'); if s = '1' then Include(fstyle, fssBold); s := GetAttrValue(ANode, 'ss:Italic'); if s = '1' then Include(fstyle, fssItalic); s := GetAttrValue(ANode, 'ss:Underline'); if s <> '' then Include(fstyle, fssUnderline); s := GetAttrValue(ANode, 'ss:StrikeThrough'); if s = '1' then Include(fstyle, fssStrikeout); AFormat.FontIndex := book.AddFont(fname, fsize, fstyle, fcolor); Include(AFormat.UsedFormattingFields, uffFont); end; {@@ ---------------------------------------------------------------------------- Reads the "Styles/Style/Interior" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadInterior(ANode: TDOMNode; var AFormat: TsCellFormat); var s, sfg, sbg: String; fs: TsFillStyle; begin if ANode = nil then exit; // Pattern s := GetAttrValue(ANode, 'ss:Pattern'); if s = '' then exit; for fs in TsFillStyle do if FILL_NAMES[fs] = s then begin AFormat.Background.Style := fs; break; end; // Foreground color (pattern color) sfg := GetAttrValue(ANode, 'ss:PatternColor'); if sfg = '' then AFormat.Background.FgColor := scBlack else AFormat.Background.FgColor := HTMLColorStrToColor(sfg); // Background color sbg := GetAttrValue(ANode, 'ss:Color'); if sbg = '' then AFormat.Background.BgColor := scWhite else AFormat.Background.BgColor := HTMLColorStrToColor(sbg); // Fix solid fill colors: make foreground and background color the same if AFormat.Background.Style = fsSolidFill then begin if (sfg <> '') then AFormat.Background.BgColor := AFormat.Background.FgColor // Forground priority else if (sfg = '') and (sbg <> '') then AFormat.Background.FgColor := AFormat.Background.BgColor; end; Include(AFormat.UsedFormattingFields, uffBackground); end; {@@ ---------------------------------------------------------------------------- Reads a "Worksheet/Names" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadNames(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); procedure DoProcess(AStr: String; var ARowIndex, AColIndex: Cardinal; out IsRow: Boolean); var p: Integer; begin p := pos('!', AStr); if p > 0 then AStr := Copy(AStr, p+1, MaxInt); IsRow := AStr[1] in ['R', 'r']; Delete(AStr, 1, 1); if IsRow then ARowIndex := StrToInt(AStr) - 1 else AColIndex := StrToInt(AStr) - 1; end; procedure DoRepeatedRowsCols(AStr: String); var p: Integer; isRow: Boolean; r1: Cardinal = UNASSIGNED_ROW_COL_INDEX; c1: Cardinal = UNASSIGNED_ROW_COL_INDEX; r2: Cardinal = UNASSIGNED_ROW_COL_INDEX; c2: Cardinal = UNASSIGNED_ROW_COL_INDEX; begin p := pos(':', AStr); // No colon --> Single range, e.g. "=Sheet1!C1" if p = 0 then begin DoProcess(AStr, r1, c1, isRow); r2 := r1; c2 := c1; end else // Colon --> Range block, e.g. "Sheet1!R1:R2" begin DoProcess(copy(AStr, 1, p-1), r1, c1, isRow); DoProcess(copy(AStr, p+1, MaxInt), r2, c2, isRow); end; if isRow then TsWorksheet(AWorksheet).PageLayout.SetRepeatedRows(r1, r2) else TsWorksheet(AWorksheet).PageLayout.SetRepeatedCols(c1, c2); end; var sheet: TsWorksheet absolute AWorksheet; s, sr: String; nodeName: String; sheet1, sheet2: String; r1, c1, r2, c2: Cardinal; flags: TsRelFlags; p: Integer; ok: Boolean; begin ok := true; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'NamedRange' then begin s := GetAttrValue(ANode, 'ss:Name'); if s = 'Print_Area' then begin // <NamedRange ss:Name="Print_Area" ss:RefersTo="=Tabelle2!R2C2:R5C7"/> s := GetAttrValue(ANode, 'ss:RefersTo'); if (s <> '') then begin p := pos(',', s); while p > 0 do begin sr := Copy(s, 1, p-1); if ParseCellRangeString_R1C1(sr, 0, 0, sheet1, sheet2, r1, c1, r2, c2, flags) then sheet.PageLayout.AddPrintRange(r1, c1, r2, c2) else begin FWorkbook.AddErrorMsg('Invalid print range.'); ok := false; break; end; s := copy(s, p+1, MaxInt); p := pos(',', s); end; if ok then begin if ParseCellRangeString_R1C1(s, 0, 0, sheet1, sheet2, r1, c1, r2, c2, flags) then sheet.PageLayout.AddPrintRange(r1, c1, r2, c2) else FWorkbook.AddErrorMsg('Invalid print range.'); end; end; end else if s = 'Print_Titles' then begin // <NamedRange ss:Name="Print_Titles" ss:RefersTo="=Tabelle2!C1,Tabelle2!R1:R2"/> s := GetAttrValue(ANode, 'ss:RefersTo'); if s <> '' then begin p := pos(',', s); if p > 0 then begin DoRepeatedRowsCols(copy(s, 1, p-1)); DoRepeatedRowsCols(copy(s, p+1, MaxInt)); end else DoRepeatedRowsCols(s); end; end; end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads a "Styles/Style/NumberFormat" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadNumberFormat(ANode: TDOMNode; var AFormat: TsCellFormat); var s: String; nf: TsNumberFormat = nfGeneral; nfs: String; begin if ANode = nil then exit; s := GetAttrValue(ANode, 'ss:Format'); case s of 'General': Exit; 'Standard': begin nf := nfFixedTh; nfs := BuildNumberFormatString(nf, FWorkbook.FormatSettings, 2); end; 'Fixed': begin nf := nfFixed; nfs := BuildNumberFormatString(nf, FWorkbook.FormatSettings, 2); end; 'Percent': begin nf := nfPercentage; nfs := BuildNumberFormatString(nf, FWorkbook.FormatSettings, 2); end; 'Scientific': begin nf := nfExp; nfs := BuildNumberFormatString(nf, FWorkbook.FormatSettings); end; 'Short Date': begin nf := nfShortDate; nfs := BuildDateTimeFormatString(nf, FWorkbook.FormatSettings); end; 'Short Time': begin nf := nfShortTime; nfs := BuildDateTimeFormatString(nf, FWorkbook.FormatSettings); end; else nfs := s; end; if nfs = '' then exit; AFormat.NumberFormatIndex := TsWorkbook(FWorkbook).AddNumberFormat(nfs); AFormat.NumberFormatStr := nfs; AFormat.NumberFormat := nf; Include(AFormat.UsedFormattingFields, uffNumberFormat); end; {@@ ---------------------------------------------------------------------------- Reads a "Worksheet / PageBreaks / RowBreaks / RowBreak" node or a "Worksheet / PageBreaks / ColBreaks / ColBreak" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadPageBreak(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; nodeName: String; s: String; n: Integer; begin while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Row' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.AddPageBreakToRow(n); end else if nodeName = 'Column' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.AddPageBreakToCol(n); end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Wrksheet / PageBreaks" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadPageBreaks(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; nodeName: String; node: TDOMNode; begin while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'RowBreaks' then begin node := ANode.FirstChild; while node <> nil do begin nodeName := node.NodeName; if nodeName = 'RowBreak' then ReadPageBreak(node.FirstChild, AWorksheet); node := node.NextSibling; end; end else if nodeName = 'ColBreaks' then begin node := ANode.FirstChild; while node <> nil do begin nodeName := node.NodeName; if nodeName = 'ColBreak' then ReadPageBreak(node.FirstChild, AWorksheet); node := node.NextSibling; end; end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "WorksheetOptions/PageSetup" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadPageSetup(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; nodeName: String; s: String; n: Integer; x: Double; begin while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Layout' then begin s := GetAttrValue(ANode, 'x:Orientation'); if s = 'Landscape' then sheet.PageLayout.Orientation := spoLandscape; s := GetAttrValue(ANode, 'x:CenterHorizontal'); if s = '1' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poHorCentered]; s := GetAttrValue(ANode, 'x:CenterVertical'); if s = '1' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poVertCentered]; s := GetAttrValue(ANode, 'x:StartPageNumber'); if (s <> '') and TryStrToInt(s, n) then sheet.PageLayout.StartPageNumber := n; end else if nodeName = 'Header' then begin s := GetAttrValue(ANode, 'x:Margin'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.PageLayout.HeaderMargin := InToMM(x); s := GetAttrValue(ANode, 'x:Data'); sheet.PageLayout.Headers[0] := s; sheet.PageLayout.Headers[1] := s; sheet.PageLayout.Headers[2] := s; end else if nodeName = 'Footer' then begin s := GetAttrValue(ANode, 'x:Margin'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.PageLayout.FooterMargin := InToMM(x); s := GetAttrValue(ANode, 'x:Data'); sheet.PageLayout.Footers[0] := s; sheet.PageLayout.Footers[1] := s; sheet.PageLayout.Footers[2] := s; end else if nodeName = 'PageMargins' then begin s := GetAttrValue(ANode, 'x:Bottom'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.PageLayout.BottomMargin := InToMM(x); s := GetAttrValue(ANode, 'x:Top'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.PageLayout.TopMargin := InToMM(x); s := GetAttrValue(ANode, 'x:Left'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.PageLayout.LeftMargin := InToMM(x); s := GetAttrValue(ANode, 'x:Right'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.PageLayout.RightMargin := InToMM(x); end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "WorksheetOptions/Print" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadPrint(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; nodeName: String; s: String; n: Integer; begin while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'PaperSizeIndex' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) and (n < Length(PAPER_SIZES)) then begin sheet.PageLayout.PageWidth := PAPER_SIZES[n, 0]; sheet.PageLayout.pageHeight := PAPER_SIZES[n, 1]; end; end else if nodeName = 'FitHeight' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.PageLayout.FitHeightToPages := n; end else if nodeName = 'FitWidth' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.PageLayout.FitWidthToPages := n; end else if nodeName = 'Scale' then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.PageLayout.ScalingFactor := n; end else if nodeName = 'Gridlines' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poPrintGridLines] else if nodeName = 'BlackAndWhite' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poMonochrome] else if nodeName = 'DraftQuality' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poDraftQuality] else if nodeName = 'LeftToRight' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poPrintPagesByRows] else if nodeName = 'RowColHeadings' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poPrintHeaders] else if nodeName = 'CommentsLayout' then begin s := ANode.TextContent; if s = 'SheetEnd' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poCommentsAtEnd] else if s = 'InPlace' then sheet.PageLayout.Options := sheet.PageLayout.Options + [poPrintCellComments]; end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads a "Worksheet/Table/Row" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadRow(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ARow: Integer); var nodeName: String; s: String; c: Integer; begin c := 0; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Cell' then begin s := GetAttrValue(ANode, 'ss:Index'); if s <> '' then c := StrToInt(s) - 1; ReadCell(ANode, AWorksheet, ARow, c); inc(c); end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads a "Styles/Style" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadStyle(ANode: TDOMNode); var nodeName: String; fmt: TsCellFormat; s: String; idx: Integer; childNode: TDOMNode; begin // Respect ancestor of current style s := GetAttrValue(ANode, 'ss:Parent'); if s <> '' then begin idx := FCellFormatList.FindIndexOfName(s); if idx > -1 then fmt := FCellFormatList.Items[idx]^; end else InitFormatRecord(fmt); // ID of current style. We store it in the "Name" field of the TsCellFormat // because it is a string while ID is an Integer (mostly "s<number>", but also // "Default"). fmt.Name := GetAttrValue(ANode, 'ss:ID'); if fmt.Name = 's125' then idx := 0; // Style elements childNode := ANode.FirstChild; while childNode <> nil do begin nodeName := childNode.NodeName; if nodeName = 'Alignment' then ReadAlignment(childNode, fmt) else if nodeName = 'Borders' then ReadBorders(childNode, fmt) else if nodeName = 'Interior' then ReadInterior(childNode, fmt) else if nodeName = 'Font' then ReadFont(childNode, fmt) else if nodeName = 'NumberFormat' then ReadNumberFormat(childnode, fmt) else if nodeName = 'Protection' then ReadCellProtection(childNode, fmt); childNode := childNode.NextSibling; end; FCellFormatList.Add(fmt); end; {@@ ---------------------------------------------------------------------------- Reads the "Styles" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadStyles(ANode: TDOMNode); var nodeName: String; styleNode: TDOMNode; begin if ANode = nil then exit; styleNode := ANode.FirstChild; while styleNode <> nil do begin nodeName := styleNode.NodeName; if nodeName = 'Style' then ReadStyle(styleNode); styleNode := styleNode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Worksheet/Table" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadTable(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; nodeName: String; s: String; r, c: Integer; x: Double; idx: Integer; fmt: TsCellFormat; rht: TsRowHeightType; begin r := 0; c := 0; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Column' then begin // Default column width s := GetAttrValue(ANode, 'ss:DefaultColumnWidth'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.WriteDefaultColWidth(x, suPoints); // Column index s := GetAttrValue(ANode, 'ss:Index'); if (s <> '') and TryStrToInt(s, c) then dec(c); // Column width, in Points s := GetAttrValue(ANode, 'ss:Width'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.WriteColWidth(c, x, suPoints); // Column format s := GetAttrValue(ANode, 'ss:StyleID'); if s <> '' then begin idx := FCellFormatList.FindIndexOfName(s); if idx <> -1 then begin fmt := FCellFormatList.Items[idx]^; idx := TsWorkbook(FWorkbook).AddCellFormat(fmt); sheet.WriteColFormatIndex(c, idx); end; end; // Hidden s := GetAttrValue(ANode, 'ss:Hidden'); if s = '1' then sheet.HideCol(c); inc(c); end else if nodeName = 'Row' then begin // Default row height s := GetAttrValue(ANode, 'ss:DefaultRowHeight'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.WriteDefaultRowHeight(x, suPoints); // Index s := GetAttrValue(ANode, 'ss:Index'); if s <> '' then r := StrToInt(s) - 1; // AutoFitHeight s := GetAttrValue(ANode, 'ss:AutoFitHeight'); if s = '1' then rht := rhtAuto else rht := rhtCustom; // Height s := GetAttrValue(ANode, 'ss:Height'); if (s <> '') and TryStrToFloat(s, x, FPointSeparatorSettings) then sheet.WriteRowHeight(r, x, suPoints, rht); // Hidden s := GetAttrValue(ANode, 'ss:Hidden'); if (s = '1') then sheet.HideRow(r); // Row format s := GetAttrValue(ANode, 'ss:StyleID'); if s <> '' then begin idx := FCellFormatList.FindIndexOfName(s); if idx <> -1 then begin fmt := FCellFormatList.Items[idx]^; idx := TsWorkbook(FWorkbook).AddCellFormat(fmt); sheet.WriteRowFormatIndex(r, idx); end; end; // Cells in row ReadRow(ANode.FirstChild, AWorksheet, r); inc(r); end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Worksheet" node -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadWorksheet(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var nodeName: String; s: String; begin if ANode = nil then exit; s := GetAttrValue(ANode, 'ss:Protected'); if s ='1' then AWorksheet.Options := AWorksheet.Options + [soProtected]; ANode := ANode.FirstChild; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Table' then ReadTable(ANode.FirstChild, AWorksheet) else if nodeName = 'WorksheetOptions' then ReadWorksheetOptions(ANode.FirstChild, AWorksheet) else if nodeName = 'Names' then ReadNames(ANode.FirstChild, AWorksheet) else if nodeName = 'PageBreaks' then ReadPageBreaks(ANode.FirstChild, AWorksheet) else if nodeName = 'ConditionalFormatting' then ReadConditionalFormatting(ANode.FirstChild, AWorksheet); ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the "Worksheet/WorksheetOptions" nodes -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadWorksheetOptions(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; node, childnode: TDOMNode; nodeName: String; s: String; x: Double; n: Integer; hasFitToPage: Boolean = false; c, r: Cardinal; begin if ANode = nil then exit; while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'PageSetup' then ReadPageSetup(ANode.FirstChild, AWorksheet) else if nodeName = 'FitToPage' then begin hasFitToPage := true; sheet.PageLayout.Options := sheet.PageLayout.Options + [poFitPages]; end else if nodeName = 'Print' then begin node := ANode.FirstChild; ReadPrint(ANode.FirstChild, AWorksheet); end else if nodeName = 'Selected' then TsWorkbook(FWorkbook).ActiveWorksheet := sheet else if nodeName = 'Panes' then begin c := sheet.ActiveCellCol; r := sheet.ActiveCellRow; node := ANode.FirstChild; while node <> nil do begin nodeName := node.NodeName; if nodeName = 'Pane' then begin childnode := node.FirstChild; while childnode <> nil do begin nodeName := childNode.NodeName; if nodeName = 'ActiveRow' then begin s := childNode.TextContent; if (s <> '') and TryStrToInt(s, n) then r := n; end else if nodeName = 'ActiveCol' then begin s := childNode.TextContent; if (s <> '') and TryStrToInt(s, n) then c := n; end; childnode := childNode.NextSibling; end; end; node := node.NextSibling; end; sheet.SelectCell(r, c); end else if nodeName = 'FreezePanes' then sheet.Options := sheet.Options + [soHasFrozenPanes] else if (nodeName = 'TopRowBottomPane') then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.TopPaneHeight := n; end else if (nodeName = 'LeftColumnRightPane') then begin s := ANode.TextContent; if (s <> '') and TryStrToInt(s, n) then sheet.LeftPaneWidth := n; end else if nodeName = 'DoNotDisplayGridlines' then sheet.Options := sheet.Options - [soShowGridLines] else if nodeName = 'DoNotDisplayHeadings' then sheet.Options := sheet.Options - [soShowHeaders] else if nodeName = 'Zoom' then begin s := ANode.TextContent; if (s <> '') and TryStrToFloat(s, x) then sheet.Zoomfactor := x * 0.01; end else if nodeName = 'Visible' then begin s := ANode.TextContent; if s = 'SheetHidden' then sheet.Options := sheet.Options + [soHidden]; end else if nodeName = 'AllowFormatCells' then sheet.Protection := sheet.Protection - [spFormatCells] else if nodeName = 'AllowSizeCols' then sheet.Protection := sheet.Protection - [spFormatColumns] else if nodeName = 'AllowSizeRows' then sheet.Protection := sheet.Protection - [spFormatRows] else if nodeName = 'AllowInsertCols' then sheet.Protection := sheet.Protection - [spInsertColumns] else if nodeName = 'AllowInsertRows' then sheet.Protection := sheet.Protection - [spInsertRows] else if nodeName = 'AllowInsertHyperlinks' then sheet.Protection := sheet.Protection - [spInsertHyperLinks] else if nodeName = 'AllowDeleteCols' then sheet.Protection := sheet.Protection - [spDeleteColumns] else if nodeName = 'AllowDeleteRows' then sheet.Protection := sheet.Protection - [spDeleteRows] else if nodeName = 'AllowSort' then sheet.Protection := sheet.Protection - [spSort] else if nodeName = 'ProtectObjects' then sheet.Protection := sheet.Protection + [spObjects] else { if nodeName = 'ProtectScenarios' then sheet.Protection := sheet.Protection + [spScenarios]; else } if nodeName = 'EnableSelection' then begin s := ANode.TextContent; if s = 'NoSelection' then sheet.Protection := sheet.Protection + [spSelectLockedCells, spSelectUnlockedCells] else if s = 'Unlocked' then sheet.Protection := sheet.Protection + [spSelectLockedCells]; end; ANode := ANode.NextSibling; end; if hasFitToPage then begin // The ScalingFactor is always written to the xml file. This makes TsPageLayout // automatically remove the poFitPages option which is restored here. if (sheet.PageLayout.ScalingFactor <> 100) then begin sheet.PageLayout.ScalingFactor := 100; sheet.Pagelayout.Options := sheet.PageLayout.Options + [poFitPages]; end; // When FitToPages is active, but FitWidthToPages and/or FitHeightToPages // are not specified, they should be set to 1 if sheet.PageLayout.FitWidthToPages = 0 then sheet.PageLayout.FitWidthToPages := 1; if sheet.PageLayout.FitHeightToPages = 0 then sheet.PageLayout.FitHeightToPages := 1; end; end; (* function TsSpreadExcelXMLWriter.GetLayoutStr(AWorksheet: TsBasicWorksheet): String; var sheet: TsWorksheet absolute AWorksheet; begin Result := ''; if sheet.PageLayout.Orientation = spoLandscape then Result := Result + ' x:Orientation="Landscape"'; if (poHorCentered in sheet.PageLayout.Options) then Result := Result + ' x:CenterHorizontal="1"'; if (poVertCentered in sheet.PageLayout.Options) then Result := Result + ' x:CenterVertical="1"'; if (poUseStartPageNumber in sheet.PageLayout.Options) then Result := Result + ' x:StartPageNumber="' + IntToStr(sheet.PageLayout.StartPageNumber) + '"'; Result := '<Layout' + Result + '/>'; end; *) {@@ ---------------------------------------------------------------------------- Reads the "Worksheet" nodes -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadWorksheets(ANode: TDOMNode); var node: TDOMNode; nodeName: String; s: String; begin node := ANode; // first iterate through all worksheets, get the name and add them to the // workbook. This is because 3D formulas may refer to sheets not yet loaded. while node <> nil do begin nodeName := node.NodeName; if nodeName = 'Worksheet' then begin s := GetAttrValue(node, 'ss:Name'); if s <> '' then // the case of '' should not happen... FWorksheet := TsWorkbook(FWorkbook).AddWorksheet(s); end; node := node.NextSibling; end; // Now iterate through the worksheets again and read their contents while ANode <> nil do begin nodeName := ANode.NodeName; if nodeName = 'Worksheet' then begin s := GetAttrValue(ANode, 'ss:Name'); FWorksheet := TsWorkbook(FWorkbook).GetWorksheetByName(s); ReadWorksheet(ANode, FWorksheet); end; ANode := ANode.NextSibling; end; end; {@@ ---------------------------------------------------------------------------- Reads the workbook from the specified stream -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLReader.ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); var doc: TXMLDocument; begin Unused(APassword, AParams); try ReadXMLStream(doc, AStream); // Read style list ReadStyles(doc.DocumentElement.FindNode('Styles')); // Read worksheets and their contents ReadWorksheets(doc.DocumentElement.FindNode('Worksheet')); // Read ExcelWorkbook node after worksheet nodes although before it is // found before the worksheet nodes in the file, because is requires // worksheets to be existing. ReadExcelWorkbook(doc.DocumentElement.FindNode('ExcelWorkbook')); finally doc.Free; end; end; {=============================================================================== TsSpreadExcelXMLWriter ===============================================================================} {@@ ---------------------------------------------------------------------------- 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: TsBasicWorkbook); 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 as TsWorksheet).FindComment(ACell); if Assigned(comment) then Result := INDENT1 + '<Comment><Data>' + UTF8TextToXMLText(comment^.Text) + '</Data></Comment>' + LF + CELL_INDENT; // If there will be some rich-text-like formatting in the future, use // Result := '<Comment><ss:Data xmlns="http://www.w3.org/TR/REC-html40">'+comment^.Text+'</ss:Data></Comment>': end; function TsSpreadExcelXMLWriter.GetFormulaStr(ACell: PCell): String; begin if HasFormula(ACell) then begin Result := UTF8TextToXMLText((FWorksheet as TsWorksheet).ConvertFormulaDialect(ACell, fdExcelR1C1)); Result := ' ss:Formula="=' + Result + '"'; end else Result := ''; end; function TsSpreadExcelXMLWriter.GetFrozenPanesStr(AWorksheet: TsBasicWorksheet; AIndent: String): String; var activePane: Integer; sheet: TsWorksheet absolute AWorksheet; begin if (soHasFrozenPanes in sheet.Options) then begin Result := AIndent + '<FreezePanes/>' + LF + AIndent + '<FrozenNoSplit/>' + LF; if sheet.LeftPaneWidth > 0 then Result := Result + AIndent + '<SplitVertical>1</SplitVertical>' + LF + AIndent + '<LeftColumnRightPane>' + IntToStr(sheet.LeftPaneWidth) + '</LeftColumnRightPane>' + LF; if sheet.TopPaneHeight > 0 then Result := Result + AIndent + '<SplitHorizontal>1</SplitHorizontal>' + LF + AIndent + '<TopRowBottomPane>' + IntToStr(sheet.TopPaneHeight) + '</TopRowBottomPane>' + LF; if (sheet.LeftPaneWidth = 0) and (sheet.TopPaneHeight = 0) then activePane := 3 else if (sheet.LeftPaneWidth = 0) then activePane := 2 else if (sheet.TopPaneHeight = 0) then activePane := 1 else activePane := 0; Result := Result + AIndent + '<ActivePane>' + IntToStr(activePane) + '</ActivePane>' + LF; end else Result := ''; end; function TsSpreadExcelXMLWriter.GetHyperlinkStr(ACell: PCell): String; var hyperlink: PsHyperlink; begin hyperlink := (FWorksheet as TsWorksheet).FindHyperlink(ACell); if Assigned(hyperlink) then begin Result := ' ss:HRef="' + hyperlink^.Target + '"'; if hyperlink^.ToolTip <> '' then Result := Result + ' x:HRefScreenTip="' + UTF8TextToXMLText(hyperlink^.ToolTip) + '"'; end else Result := ''; end; function TsSpreadExcelXMLWriter.GetIndexStr(AIndex, APrevIndex: Cardinal): String; begin if (APrevIndex = Cardinal(-1)) and (AIndex = 0) then Result := '' else if {(APrevIndex >= 0) and} (AIndex = APrevIndex + 1) then Result := '' else Result := Format(' ss:Index="%d"', [AIndex + 1]); end; function TsSpreadExcelXMLWriter.GetLayoutStr(AWorksheet: TsBasicWorksheet): String; var sheet: TsWorksheet absolute AWorksheet; begin Result := ''; if sheet.PageLayout.Orientation = spoLandscape then Result := Result + ' x:Orientation="Landscape"'; if (poHorCentered in sheet.PageLayout.Options) then Result := Result + ' x:CenterHorizontal="1"'; if (poVertCentered in sheet.PageLayout.Options) then Result := Result + ' x:CenterVertical="1"'; if (poUseStartPageNumber in sheet.PageLayout.Options) then Result := Result + ' x:StartPageNumber="' + IntToStr(sheet.PageLayout.StartPageNumber) + '"'; Result := '<Layout' + Result + '/>'; end; function TsSpreadExcelXMLWriter.GetMergeStr(ACell: PCell): String; var r1, c1, r2, c2: Cardinal; begin Result := ''; if (FWorksheet as TsWorksheet).IsMerged(ACell) then begin (FWorksheet as TsWorksheet).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: TsBasicWorksheet): String; var sheet: TsWorksheet absolute AWorksheet; begin Result := Format('x:Margin="%g"', [mmToIn(sheet.PageLayout.FooterMargin)], FPointSeparatorSettings); if (sheet.PageLayout.Footers[HEADER_FOOTER_INDEX_ALL] <> '') then Result := Result + ' x:Data="' + UTF8TextToXMLText(sheet.PageLayout.Footers[HEADER_FOOTER_INDEX_ALL], true) + '"'; Result := '<Footer ' + result + '/>'; end; function TsSpreadExcelXMLWriter.GetPageHeaderStr( AWorksheet: TsBasicWorksheet): String; var sheet: TsWorksheet absolute AWorksheet; begin Result := Format('x:Margin="%g"', [mmToIn(sheet.PageLayout.HeaderMargin)], FPointSeparatorSettings); if (sheet.PageLayout.Headers[HEADER_FOOTER_INDEX_ALL] <> '') then Result := Result + ' x:Data="' + UTF8TextToXMLText(sheet.PageLayout.Headers[HEADER_FOOTER_INDEX_ALL], true) + '"'; Result := '<Header ' + Result + '/>'; end; function TsSpreadExcelXMLWriter.GetPageMarginStr( AWorksheet: TsBasicWorksheet): String; var sheet: TsWorksheet absolute AWorksheet; begin Result := Format('x:Bottom="%g" x:Left="%g" x:Right="%g" x:Top="%g"', [ mmToIn(sheet.PageLayout.BottomMargin), mmToIn(sheet.PageLayout.LeftMargin), mmToIn(sheet.PageLayout.RightMargin), mmToIn(sheet.PageLayout.TopMargin) ], FPointSeparatorSettings); Result := '<PageMargins ' + Result + '/>'; end; { Todo: When can the "Print" node be skipped? } function TsSpreadExcelXMLWriter.GetPrintStr(AWorksheet: TsBasicWorksheet): String; var sheet: TsWorksheet absolute AWorksheet; i, pgSizeIdx: Integer; scalestr: String; begin Result := ''; pgSizeIdx := -1; for i:=0 to High(PAPER_SIZES) do if (SameValue(PAPER_SIZES[i,0], sheet.PageLayout.PageHeight) and SameValue(PAPER_SIZES[i,1], sheet.PageLayout.PageWidth)) or (SameValue(PAPER_SIZES[i,1], sheet.PageLayout.PageHeight) and SameValue(PAPER_SIZES[i,0], sheet.PageLayout.PageWidth)) then begin pgSizeIdx := i; break; end; if pgSizeidx = -1 then exit; // Scaling factor if sheet.PageLayout.ScalingFactor <> 100 then scaleStr := INDENT4 + '<Scale>' + IntToStr(sheet.PageLayout.ScalingFactor) + '</Scale>' + LF else scaleStr := ''; Result := INDENT4 + '<ValidPrinterInfo/>' + LF + INDENT4 + '<PaperSizeIndex>' + IntToStr(pgSizeIdx) + '</PaperSizeIndex>' + LF + scaleStr + INDENT4 + '<VerticalResolution>0</VerticalResolution>'; if sheet.PageLayout.FitHeightToPages > 1 then Result := Result + LF + INDENT4 + '<FitHeight>' + IntToStr(sheet.PageLayout.FitHeightToPages) + '</FitHeight>'; if sheet.PageLayout.FitWidthToPages > 1 then Result := result + LF + INDENT4 + '<FitWidth>' + IntToStr(sheet.PageLayout.FitWidthToPages) + '</FitWidth>'; end; function TsSpreadExcelXMLWriter.GetStyleStr(AFormatIndex: Integer): String; begin Result := ''; if AFormatIndex > 0 then Result := Format(' ss:StyleID="s%d"', [AFormatIndex + FMT_OFFSET]); end; procedure TsSpreadExcelXMLWriter.WriteBlank(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); begin Unused(ARow, ACol); AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s>' + // colIndex, style, hyperlink, merge '%s' + // Comment <Comment>...</Comment> '</Cell>' + LF, [ GetIndexStr(ACol, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetHyperlinkStr(ACell), GetMergeStr(ACell), GetCommentStr(ACell) ])); end; procedure TsSpreadExcelXMLWriter.WriteBool(AStream: TStream; const ARow, ACol: Cardinal; const AValue: boolean; ACell: PCell); begin Unused(ARow, ACol); AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s%s>' + // colIndex, style, formula, hyperlink, merge '<Data ss:Type="%s">' + // data type '%s' + // value string '</Data>' + '%s' + // Comment <Comment>...</Comment> '</Cell>' + LF, [ GetIndexStr(ACol, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetFormulaStr(ACell), GetHyperlinkStr(ACell), GetMergeStr(ACell), StrUtils.IfThen(HasFormula(ACell), GetCellContentTypeStr(ACell), 'Boolean'), StrUtils.IfThen(AValue, '1', '0'), GetCommentStr(ACell) ])); end; procedure TsSpreadExcelXMLWriter.WriteCellToStream(AStream: TStream; ACell: PCell); begin 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); cctFormula: WriteFormula(AStream, ACell^.Row, ACell^.Col, ACell); end; if (FWorksheet as TsWorksheet).ReadComment(ACell) <> '' then WriteComment(AStream, ACell); end; procedure TsSpreadExcelXMLWriter.WriteCellNodes(AStream: TStream; AWorksheet: TsBasicWorksheet; ARow: Cardinal); var c: Cardinal; cell: PCell; lCell: TCell; styleCell: PCell; value: variant; sheet: TsWorksheet absolute AWorksheet; begin if (boVirtualMode in FWorkbook.Options) and (not Assigned(sheet.OnWriteCellData)) then exit; FPrevCol := UNASSIGNED_ROW_COL_INDEX; for c := 0 to FLastCol do begin if (boVirtualMode in FWorkbook.Options) then begin lCell.Row := ARow; // to silence a compiler hint InitCell(lCell); value := varNull; styleCell := nil; sheet.OnWriteCellData(sheet, ARow, c, value, styleCell); if styleCell <> nil then lCell := styleCell^; lCell.Row := ARow; lCell.Col := c; if VarIsNull(value) then begin if styleCell <> nil then lCell.ContentType := cctEmpty else Continue; end else if VarIsNumeric(value) then begin lCell.ContentType := cctNumber; lCell.NumberValue := value; end else if VarType(value) = varDate then begin lCell.ContentType := cctDateTime; lCell.DateTimeValue := StrToDateTime(VarToStr(value), Workbook.FormatSettings); // was: StrToDate end else if VarIsStr(value) then begin lCell.ContentType := cctUTF8String; lCell.UTF8StringValue := VarToStrDef(value, ''); end else if VarIsBool(value) then begin lCell.ContentType := cctBool; lCell.BoolValue := value <> 0; end; WriteCellToStream(AStream, @lCell); varClear(value); FPrevCol := c; end else begin // Normal mode cell := sheet.Findcell(ARow, c); if cell <> nil then begin if sheet.IsMerged(cell) and not sheet.IsMergeBase(cell) then Continue; WriteCellToStream(AStream, cell); FPrevCol := c; end; end; end; end; procedure TsSpreadExcelXMLWriter.WriteColumns(AStream: TStream; AWorksheet: TsBasicWorksheet); var c, c1, c2: Cardinal; colwidthStr: String; styleStr: String; hiddenStr: String; col: PCol; begin c1 := 0; c2 := TsWorksheet(AWorksheet).GetLastColIndex; FPrevCol := UNASSIGNED_ROW_COL_INDEX; for c := c1 to c2 do begin col := TsWorksheet(AWorksheet).FindCol(c); styleStr := ''; colWidthStr := ''; hiddenStr := ''; if Assigned(col) then begin // column width is needed in pts. if col^.ColWidthType = cwtCustom then colwidthStr := Format(' ss:Width="%0.2f" ss:AutoFitWidth="0"', [(FWorkbook as TsWorkbook).ConvertUnits(col^.Width, FWorkbook.Units, suPoints)], FPointSeparatorSettings); // column style if col^.FormatIndex > 0 then styleStr := GetStyleStr(col^.FormatIndex); end; if TsWorksheet(AWorksheet).ColHidden(c) then hiddenStr := ' ss:Hidden="1"'; if (colWidthStr <> '') or (stylestr <> '') or (hiddenstr <> '') then begin AppendToStream(AStream, COL_INDENT + Format( '<Column%s%s%s%s />' + LF, [GetIndexStr(c, FPrevCol), colWidthStr, styleStr, hiddenStr])); FPrevCol := c; end; end; end; procedure TsSpreadExcelXMLWriter.WriteConditionalFormat(AStream: TStream; AWorksheet: TsBasicWorksheet; AFormat: TsConditionalFormat); function BackgroundStyle(AFormat: TsCellFormat): String; begin Result := ''; if not (uffBackground in AFormat.UsedFormattingFields) then exit; if AFormat.Background.Style = fsSolidFill then Result := Format('background:%s;', [ColorToHTMLColorStr(AFormat.Background.BgColor)]) else Result := Format('background:%s;mso-pattern:%s %s;', [ ColorToHTMLColorStr(AFormat.Background.BgColor), CF_FILL_NAMES[AFormat.Background.Style], ColorToHTMLColorStr(AFormat.Background.FgColor) ]); end; function BorderStyle(AFormat: TsCellFormat): String; var cb: TsCellBorder; allEqual: Boolean; bs: TsCellBorderStyle; begin Result := ''; if not (uffBorder in AFormat.UsedFormattingFields) then exit; allEqual := ([cbEast, cbWest, cbNorth, cbSouth] = AFormat.Border); if allEqual then begin bs := AFormat.BorderStyles[cbEast]; for cb in TsCellBorders do if not (cb in [cbDiagUp, cbDiagDown]) then if (AFormat.BorderStyles[cb].Color <> bs.Color) or (AFormat.BorderStyles[cb].LineStyle <> bs.LineStyle) then begin allEqual := false; break; end; end; if allEqual then Result := Format('border:0.5pt %s %s;', [ CF_LINE_STYLES[bs.LineStyle], ColorToHTMLColorStr(bs.Color) ]) else for cb in TsCellBorders do begin if cb in [cbDiagUp, cbDiagDown] then Continue; bs := AFormat.BorderStyles[cb]; if (cb in AFormat.Border) then Result := Result + Format('border-%s:0.5pt %s %s;', [ Lowercase(BORDER_NAMES[cb]), CF_LINE_STYLES[bs.LineStyle], ColorToHTMLColorStr(bs.Color) ]); end; end; function FontStyle(AFormat: TsCellFormat): String; var fnt: TsFont; begin Result := ''; if not (uffFont in AFormat.UsedFormattingFields) then exit; fnt := TsWorkbook(FWorkbook).GetFont(AFormat.FontIndex); if (fssItalic in fnt.Style) then Result := Result + 'font-style:italic;'; if (fssBold in fnt.Style) then Result := Result + 'font-weight:700;'; if (fssStrikeOut in fnt.Style) then Result := Result + 'text-line-through:single;'; if fnt.Color <> scNotDefined then Result := Result + 'color:' + ColorToHTMLColorStr(fnt.Color) + ';'; end; var rangeStr: String; cfRule: TsCFCellRule; i: Integer; value1Str, value2Str: String; sheet: TsWorksheet; book: TsWorkbook; fmt: TsCellFormat; s: String; needToExit: Boolean; begin book := TsWorkbook(FWorkbook); sheet := TsWorksheet(AWorksheet); needToExit := false; for i := 0 to AFormat.RulesCount-1 do if not (AFormat.Rules[i] is TsCFCellRule) then begin FWorkbook.AddErrorMsg('Conditional formatting rule ' + AFormat.Rules[i].ClassName + ' not supported by Excel-XML.'); needToExit := true; end; if needToExit then exit; AppendToStream(AStream, INDENT2 + '<ConditionalFormatting xmlns="urn:schemas-microsoft-com:office:excel">'); with AFormat.CellRange do rangeStr := GetCellRangeString_R1C1(Row1, Col1, Row2, Col2, [], Row1, Col1); AppendToStream(AStream, LF + INDENT3 + '<Range>' + rangeStr + '</Range>'); for i := 0 to AFormat.RulesCount-1 do begin if AFormat.Rules[i] is TsCFCellRule then begin cfRule := TsCFCellRule(AFormat.Rules[i]); if CF_CONDITIONS[cfRule.Condition] = '' then begin s := GetEnumName(TypeInfo(TsCFCondition), Ord(cfRule.Condition)); FWorkbook.AddErrorMsg('Conditional formatting rule "' + s + '" not supported by ExcelXML.'); Continue; end; if cfRule.Condition = cfcExpression then begin s := cfRule.Operand1; if (s <> '') and (s[1] <> '=') then s := '=' + s; value1Str := CFOperandToStr(s, sheet); value2Str := ''; end else begin value1Str := CFOperandToStr(cfRule.Operand1, sheet); value2Str := CFOperandToStr(cfRule.Operand2, sheet); end; s := CF_CONDITIONS[cfRule.Condition]; if s[1] = '@' then begin Delete(s, 1,1); if s = '' then s := value1Str else s := Format(s, [value1Str, value2Str, rangeStr]); value1Str := s; s := ''; end; AppendToStream(AStream, LF + INDENT3 + '<Condition>'); if s <> '' then AppendToStream(AStream, LF + INDENT4 + '<Qualifier>' + s + '</Qualifier>'); if value1Str <> '' then AppendToStream(AStream, LF + INDENT4 + '<Value1>' + value1Str + '</Value1>'); if (cfRule.Condition in [cfcBetween, cfcNotBetween]) and (value2Str <> '') then AppendToStream(AStream, LF + INDENT4 + '<Value2>' + value2Str + '</Value2>'); fmt := book.GetCellFormat(cfRule.FormatIndex); s := BackgroundStyle(fmt) + BorderStyle(fmt) + FontStyle(fmt); if s <> '' then begin if s[Length(s)] = ';' then Delete(s, Length(s), 1); AppendToStream(AStream, LF + INDENT4 + '<Format Style=''' + s + '''/>'); end; AppendToStream(AStream, LF + INDENT3 + '</Condition>' ); end; end; AppendToStream(AStream, LF + INDENT2 + '</ConditionalFormatting>' + LF); end; procedure TsSpreadExcelXMLWriter.WriteConditionalFormatting(AStream: TStream; AWorksheet: TsBasicWorksheet); var book: TsWorkbook; cf: TsConditionalFormat; i: Integer; begin book := TsWorkbook(FWorkbook); for i := 0 to book.GetNumConditionalFormats-1 do begin cf := book.GetConditionalFormat(i); WriteConditionalFormat(AStream, AWorksheet, cf); end; end; procedure TsSpreadExcelXMLWriter.WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); var valueStr: String; ExcelDate: TDateTime; nfp: TsNumFormatParams; fmt: PsCellFormat; begin Unused(ARow, ACol); ExcelDate := AValue; fmt := (FWorkbook as TsWorkbook).GetPointerToCellFormat(ACell^.FormatIndex); // Times have an offset of 1 day! if (fmt <> nil) and (uffNumberFormat in fmt^.UsedFormattingFields) then begin nfp := (FWorkbook as TsWorkbook).GetNumberFormat(fmt^.NumberFormatIndex); if IsTimeIntervalFormat(nfp) or IsTimeFormat(nfp) then case FDateMode of dm1900: ExcelDate := AValue + DATEMODE_1900_BASE; dm1904: ExcelDate := AValue + DATEMODE_1904_BASE; end; end; valueStr := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', ExcelDate); AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s%s>' + LF + VALUE_INDENT + // colIndex, style, formula, hyperlink, merge '<Data ss:Type="%s">' + // data type '%s' + // value string '</Data>' + LF + CELL_INDENT + '%s' + // Comment <Comment>...</Comment> '</Cell>' + LF, [ GetIndexStr(ACol, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetFormulaStr(ACell), GetHyperlinkStr(ACell), GetMergeStr(ACell), StrUtils.IfThen(HasFormula(ACell), GetCellContentTypeStr(ACell), 'DateTime'), valueStr, GetCommentStr(ACell) ])); end; procedure TsSpreadExcelXMLWriter.WriteDocumentProperties(AStream: TStream); begin AppendToStream(AStream, INDENT1 + '<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office" />' + LineEnding); // replace by these when fpspreadsheet supports these meta data. { AppendToSstream(AStream, INDENT1 + '<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">' + LineEnding + INDENT2 + '<Author></Author>' + LineEnding + INDENT2 + '<LastAuthor></LastAuthor>' + LineEnding + INDENT2 + '<Created></Created>' + LineEnding + Indent2 + // Date in format YYYY-mm-ddThh:nn:ssZ '<Version>16.00</Version>' + LineEnding + Indent1 + '</DocumentProperties>' + LineEnding ); } end; procedure TsSpreadExcelXMLWriter.WriteError(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TsErrorValue; ACell: PCell); begin Unused(ARow, ACol); AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s%s>' + LF + VALUE_INDENT + // colIndex, style, formula, hyperlink, merge '<Data ss:Type="%s">' + // data type '%s' + // value string '</Data>' + LF + CELL_INDENT + '%s' + // Comment <Comment>...</Comment> '</Cell>' + LF, [ GetIndexStr(ACol, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetFormulaStr(ACell), GetHyperlinkStr(ACell), GetMergeStr(ACell), StrUtils.IfThen(HasFormula(ACell), GetCellContentTypeStr(ACell), 'Error'), GetErrorValueStr(AValue), GetCommentStr(ACell) ])); end; procedure TsSpreadExcelXMLWriter.WriteExcelWorkbook(AStream: TStream); var datemodeStr: String; protectStr: String; begin if FDateMode = dm1904 then datemodeStr := INDENT2 + '<Date1904/>' + LF else datemodeStr := ''; protectStr := Format( '<ProtectStructure>%s</ProtectStructure>' + LF + INDENT2 + '<ProtectWindows>%s</ProtectWindows>' + LF, [ FALSE_TRUE[bpLockStructure in Workbook.Protection], FALSE_TRUE[bpLockWindows in Workbook.Protection] ]); AppendToStream(AStream, INDENT1 + '<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">' + LF + datemodeStr + INDENT2 + protectStr + INDENT1 + '</ExcelWorkbook>' + LF); end; procedure TsSpreadExcelXMLWriter.WriteFormula(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); var xmlnsStr: String; dataTagStr: String; begin Unused(ARow); Unused(ACol); if ACell^.ContentType <> cctFormula then raise Exception.Create('WriteFormula called for calculated cell.'); xmlnsStr := ' xmlns="http://www.w3.org/TR/REC-html40"'; dataTagStr := ''; // or 'ss:' -- to do... AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s%s>' + LF + VALUE_INDENT + // colIndex, style, formula, hyperlink, merge '<%sData%s>'+ // "ss:", data type, "xmlns=.." '</%sData>' + LF + CELL_INDENT + // "ss:" '%s' + // Comment '</Cell>' + LF, [ GetIndexStr(ACell^.Col, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetFormulaStr(ACell), GetHyperlinkStr(ACell), GetMergeStr(ACell), dataTagStr, xmlnsStr, dataTagStr, GetCommentStr(ACell) ])); end; procedure TsSpreadExcelXMLWriter.WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); const MAXBYTES = 32767; // limit for this format var valueStr: String; cctStr: String; xmlnsStr: String; dataTagStr: String; p: Integer; tmp: String; ResultingValue: String; begin // Office 2007-2010 (at least) supports no more characters in a cell; if Length(AValue) > MAXBYTES then begin ResultingValue := Copy(AValue, 1, MAXBYTES); //may chop off multicodepoint UTF8 characters but well... Workbook.AddErrorMsg(rsTruncateTooLongCellText, [ MAXBYTES, GetCellString(ARow, ACol) ]); end else resultingValue := AValue; { Check for invalid characters } if not ValidXMLText(ResultingValue) then Workbook.AddErrorMsg( rsInvalidCharacterInCell, [ GetCellString(ARow, ACol) ]); if Length(ACell^.RichTextParams) > 0 then begin RichTextToHTML( FWorkbook as TsWorkbook, (FWorksheet as TsWorksheet).ReadCellFont(ACell), ResultingValue, ACell^.RichTextParams, valueStr, // html-formatted rich text 'html:', tcProperCase ); xmlnsStr := ' xmlns="http://www.w3.org/TR/REC-html40"'; dataTagStr := 'ss:'; // Excel does not like units in font size specification... tmp := valueStr; p := pos('<Font html:Size="', valueStr); if p > 0 then begin valueStr := ''; while p > 0 do begin inc(p, Length('<Font html:Size="')); valueStr := valueStr + copy(tmp, 1, p-1); while (tmp[p] <> '"') do begin if (tmp[p] in ['0'..'9', '.']) then valueStr := valueStr + tmp[p]; inc(p); end; tmp := copy(tmp, p, MaxInt); p := pos('<Font html:Size="', tmp); end; valueStr := valuestr + tmp; end; end else begin valueStr := ResultingValue; if not ValidXMLText(valueStr, true, true) then Workbook.AddErrorMsg( rsInvalidCharacterInCell, [ GetCellString(ARow, ACol) ]); xmlnsStr := ''; dataTagStr := ''; end; cctStr := 'String'; if HasFormula(ACell) then cctStr := GetCellContentTypeStr(ACell) else cctStr := 'String'; AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s%s>' + LF + VALUE_INDENT + // colIndex, style, formula, hyperlink, merge '<%sData ss:Type="%s"%s>'+ // "ss:", data type, "xmlns=.." '%s' + // value string '</%sData>' + LF + CELL_INDENT + // "ss:" '%s' + // Comment '</Cell>' + LF, [ GetIndexStr(ACol, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetFormulaStr(ACell), GetHyperlinkStr(ACell), GetMergeStr(ACell), dataTagStr, cctStr, xmlnsStr, valueStr, dataTagStr, GetCommentStr(ACell) ])); end; procedure TsSpreadExcelXMLWriter.WriteNames(AStream: TStream; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; print_titles_str: string = ''; print_range_str: String = ''; s: String; rng: TsCellRange; i: Integer; begin with sheet.PageLayout do begin // Print ranges --> Name "Print_Area" for i:=0 to NumPrintRanges-1 do begin rng := GetPrintRange(i); s := GetCellRangeString_R1C1(sheet.Name, sheet.Name, rng.Row1, rng.Col1, rng.Row2, rng.Col2, []); if print_range_str = '' then print_range_str := s else print_range_str := print_range_str + ',' + s; end; if print_range_str <> '' then print_range_str := NAME_INDENT + '<NamedRange ss:Name="Print_Area" ss:RefersTo="' + print_range_str + '"/>' + LF; // Repeated columns --> Name "Print_Titles" if (RepeatedCols.FirstIndex <> UNASSIGNED_ROW_COL_INDEX) and (RepeatedCols.LastIndex <> UNASSIGNED_ROW_COL_INDEX) then begin s := 'C' + {%H-}IntToStr(RepeatedCols.FirstIndex + 1); if RepeatedCols.FirstIndex <> RepeatedCols.LastIndex then s := s + ':C' + {%H-}IntToStr(RepeatedCols.LastIndex + 1); s := sheet.Name + '!' + s; print_titles_str := s; end; // Repeated rows --> Name "Print_Titles" if (RepeatedRows.FirstIndex <> UNASSIGNED_ROW_COL_INDEX) and (RepeatedRows.LastIndex <> UNASSIGNED_ROW_COL_INDEX) then begin s := 'R' + {%H-}IntToStr(RepeatedRows.FirstIndex + 1); if RepeatedRows.FirstIndex <> RepeatedRows.LastIndex then s := s + ':R' + {%H-}IntToStr(RepeatedRows.LastIndex + 1); s := sheet.Name + '!' + s; if print_titles_str = '' then print_titles_str := s else print_titles_str := print_titles_str + ',' + s; end; if print_titles_str <> '' then print_titles_str := NAME_INDENT + '<NamedRange ss:Name="Print_Titles" ss:RefersTo="' + print_titles_str + '"/>' + LF; end; if (print_range_str = '') and (print_titles_str = '') then exit; AppendToStream(AStream, NAMES_INDENT + '<Names>' + LF + print_titles_str + NAMES_INDENT + print_range_str + NAMES_INDENT + '</Names>' + LF); end; procedure TsSpreadExcelXMLWriter.WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); begin Unused(ARow, ACol); AppendToStream(AStream, Format(CELL_INDENT + '<Cell%s%s%s%s%s>' + LF + VALUE_INDENT + // colIndex, style, formula, hyperlink, merge '<Data ss:Type="%s">' + // data type '%g' + // value '</Data>' + LF + CELL_INDENT + '%s' + // Comment <Comment>...</Comment> '</Cell>' + LF, [ GetIndexStr(ACol, FPrevCol), GetStyleStr(ACell^.FormatIndex), GetFormulaStr(ACell), GetHyperlinkStr(ACell), GetMergeStr(ACell), StrUtils.IfThen(HasFormula(ACell), GetCellContentTypeStr(ACell), 'Number'), AValue, GetCommentStr(ACell)], FPointSeparatorSettings) ); end; procedure TsSpreadExcelXMLWriter.WriteOfficeDocumentSettings(AStream: TStream); begin AppendToStream(AStream, INDENT1 + '<OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">' + LineEnding + INDENT2 + '<AllowPNG/>' + LineEnding + INDENT1 + '</OfficeDocumentSettings>' + LineEnding ); end; procedure TsSpreadExcelXMLWriter.WritePageBreaks(AStream: TStream; AWorksheet: TsBasicWorksheet); var i: Integer; nc, nr: Integer; sheet: TsWorksheet absolute AWorksheet; s: String; col: PCol; row: PRow; begin nc := 0; for i := 0 to sheet.Cols.Count - 1 do if (croPageBreak in PCol(sheet.Cols[i])^.Options) then inc(nc); nr := 0; for i:= 0 to sheet.Rows.Count - 1 do if (croPageBreak in PRow(sheet.Rows[i])^.Options) then inc(nr); if (nc = 0) and (nr = 0) then exit; s := INDENT2 + '<PageBreaks xmlns="urn:schemas-microsoft-com:office:excel">' + LF; if nc > 0 then begin s := s + INDENT3 + '<ColBreaks>' + LF; for i := 0 to sheet.Cols.Count - 1 do begin col := PCol(sheet.Cols[i]); if (croPageBreak in col^.Options) then s := s + INDENT4 + '<ColBreak>' + LF + INDENT5 + '<Column>' + IntToStr(col^.Col) + '</Column>' + LF + INDENT4 + '</ColBreak>' + LF; end; s := s + INDENT3 + '</ColBreaks>' + LF; end; if nr > 0 then begin s := s + INDENT3 + '<RowBreaks>' + LF; for i := 0 to sheet.Rows.Count - 1 do begin row := PRow(sheet.Rows[i]); if (croPageBreak in row^.Options) then s := s + INDENT4 + '<RowBreak>' + LF + INDENT5 + '<Row>' + IntToStr(row^.Row) + '</Row>' + LF + INDENT4 + '</RowBreak>' + LF; end; s := s + INDENT3 + '</RowBreaks>' + LF; end; s := s + INDENT2 + '</PageBreaks>' + LF; AppendToStream(AStream, s); end; procedure TsSpreadExcelXMLWriter.WriteRows(AStream: TStream; AWorksheet: TsBasicWorksheet); var c: Cardinal; r: Cardinal; rowheightStr: String; hiddenStr: String; styleStr: String; s: String; row: PRow; cell: PCell; hasCells: Boolean; sheet: TsWorksheet absolute AWorksheet; begin FPrevRow := UNASSIGNED_ROW_COL_INDEX; for r := 0 to FLastRow do begin row := sheet.FindRow(r); styleStr := ''; hiddenStr := ''; // Row height is needed in pts. if Assigned(row) then begin rowheightStr := Format(' ss:Height="%.2f"', [(FWorkbook as TsWorkbook).ConvertUnits(row^.Height, FWorkbook.Units, suPoints)], FPointSeparatorSettings ); if row^.RowHeightType = rhtCustom then rowHeightStr := ' ss:AutoFitHeight="0"' + rowHeightStr else rowHeightStr := ' ss:AutoFitHeight="1"' + rowHeightStr; if row^.FormatIndex > 0 then styleStr := GetStyleStr(row^.FormatIndex); end else rowheightStr := ' ss:AutoFitHeight="1"'; if sheet.RowHidden(r) then hiddenStr := ' ss:Hidden="1"'; if boVirtualMode in FWorkbook.Options then hasCells := true else begin hasCells := false; for c := 0 to FLastCol do begin cell := sheet.FindCell(r, c); if cell <> nil then begin hasCells := true; break; end; end; end; s := Format('%s%s%s%s', [GetIndexStr(r, FPrevRow), rowheightStr, styleStr, hiddenStr]); if hasCells then begin AppendToStream(AStream, ROW_INDENT + Format( '<Row%s>', [s]) + LF); WriteCellNodes(AStream, AWorksheet, r); AppendToStream(AStream, ROW_INDENT + '</Row>' + LF); FPrevRow := r; end else if (rowheightStr <> '') or (styleStr <> '') or (hiddenStr <> '') then begin AppendToStream(AStream, ROW_INDENT + Format( '<Row%s/>', [s]) + LF); FPrevRow := r; end; end; end; procedure TsSpreadExcelXMLWriter.WriteStyle(AStream: TStream; AIndex: Integer); var fmt: PsCellFormat; deffnt, fnt: TsFont; s, fmtVert, fmtHor, fmtWrap, fmtRot: String; nfp: TsNumFormatParams; fill: TsFillPattern; cb: TsCellBorder; cbs: TsCellBorderStyle; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; deffnt := book.GetDefaultFont; if AIndex = 0 then begin AppendToStream(AStream, Format(INDENT2 + '<Style ss:ID="Default" ss:Name="Normal">' + LF + INDENT3 + '<Aligment ss:Vertical="Bottom" />' + LF + INDENT3 + '<Borders />' + LF + INDENT3 + '<Font ss:FontName="%s" x:Family="Swiss" ss:Size="%d" ss:Color="%s" />' + LF + INDENT3 + '<Interior />' + LF + INDENT3 + '<NumberFormat />' + LF + INDENT3 + '<Protection />' + LF + INDENT2 + '</Style>' + LF, [deffnt.FontName, round(deffnt.Size), ColorToHTMLColorStr(deffnt.Color)] ) ) end else begin AppendToStream(AStream, Format(INDENT2 + '<Style ss:ID="s%d">' + LF, [AIndex + FMT_OFFSET])); fmt := book.GetPointerToCellFormat(AIndex); // Horizontal alignment fmtHor := ''; if uffHorAlign in fmt^.UsedFormattingFields then case fmt^.HorAlignment of haDefault: ; haLeft : fmtHor := 'ss:Horizontal="Left" '; haCenter : fmtHor := 'ss:Horizontal="Center" '; haRight : fmtHor := 'ss:Horizontal="Right" '; else raise EFPSpreadsheetWriter.Create('[TsSpreadXMLWriter.WriteStyle] Horizontal alignment cannot be handled.'); end; // Vertical alignment fmtVert := ''; if uffVertAlign in fmt^.UsedFormattingFields then case fmt^.VertAlignment of vaDefault: ; vaTop : fmtVert := 'ss:Vertical="Top" '; vaCenter : fmtVert := 'ss:Vertical="Center" '; vaBottom : fmtVert := 'ss:Vertical="Bottom" '; else raise EFPSpreadsheetWriter.Create('[TsSpreadXMLWriter.WriteStyle] Vertical alignment cannot be handled.'); end; // Wrap text if uffWordwrap in fmt^.UsedFormattingFields then fmtWrap := 'ss:WrapText="1" ' else fmtWrap := ''; // Text rotation fmtRot := ''; if uffTextRotation in fmt^.UsedFormattingFields then case fmt^.TextRotation of rt90DegreeClockwiseRotation : fmtRot := 'ss:Rotate="-90" '; rt90DegreeCounterClockwiseRotation : fmtRot := 'ss:Rotate="90" '; rtStacked : fmtRot := 'ss:VerticalText="1" '; end; // Write all the alignment, text rotation and wordwrap attributes to stream AppendToStream(AStream, Format(INDENT3 + '<Alignment %s%s%s%s />' + LF, [fmtHor, fmtVert, fmtWrap, fmtRot]) ); // Font if (uffFont in fmt^.UsedFormattingFields) then begin fnt := book.GetFont(fmt^.FontIndex); s := ''; if fnt.FontName <> deffnt.FontName then s := s + Format('ss:FontName="%s" ', [fnt.FontName]); if not SameValue(fnt.Size, deffnt.Size, 1E-3) then s := s + Format('ss:Size="%g" ', [fnt.Size], FPointSeparatorSettings); if fnt.Color <> deffnt.Color then s := s + Format('ss:Color="%s" ', [ColorToHTMLColorStr(fnt.Color)]); if fssBold in fnt.Style then s := s + 'ss:Bold="1" '; if fssItalic in fnt.Style then s := s + 'ss:Italic="1" '; if fssUnderline in fnt.Style then s := s + 'ss:Underline="Single" '; // or "Double", not supported by fps if fssStrikeout in fnt.Style then s := s + 'ss:StrikeThrough="1" '; if s <> '' then AppendToStream(AStream, INDENT3 + '<Font ' + s + '/>' + LF); end; // Number Format if (uffNumberFormat in fmt^.UsedFormattingFields) then begin nfp := book.GetNumberFormat(fmt^.NumberFormatIndex); nfp.AllowLocalizedAMPM := false; // Replace "AMPM" by "AM/PM" AppendToStream(AStream, Format(INDENT3 + '<NumberFormat ss:Format="%s"/>' + LF, [UTF8TextToXMLText(nfp.NumFormatStr)])); end; // Background if (uffBackground in fmt^.UsedFormattingFields) then begin fill := fmt^.Background; if fill.Style = fsNoFill then AppendToStream(AStream, INDENT3 + '<Interior />' + LF) else begin if fill.Style = fsSolidFill then s := 'ss:Color="' + ColorToHtmlColorStr(fill.FgColor) + '" ' else s := Format('ss:Color="%s" ss:PatternColor="%s" ', [ ColorToHTMLColorStr(fill.BgColor), ColorToHTMLColorStr(fill.FgColor) ]); s := s + 'ss:Pattern="' + FILL_NAMES[fill.Style] + '" '; AppendToStream(AStream, INDENT3 + '<Interior ' + s + '/>' + LF) end; end; // Borders if (uffBorder in fmt^.UsedFormattingFields) then begin s := ''; for cb in TsCellBorder do if cb in fmt^.Border then begin cbs := fmt^.BorderStyles[cb]; s := s + INDENT4 + Format('<Border ss:Position="%s" ss:LineStyle="%s"', [ BORDER_NAMES[cb], LINE_STYLES[cbs.LineStyle]]); if fmt^.BorderStyles[cb].LineStyle <> lsHair then s := Format('%s ss:Weight="%d"', [s, LINE_WIDTHS[cbs.LineStyle]]); s := Format('%s ss:Color="%s"', [s, ColorToHTMLColorStr(cbs.Color)]); s := s + '/>' + LF; end; if s <> '' then AppendToStream(AStream, INDENT3 + '<Borders>' + LF + s + INDENT3 + '</Borders>' + LF); end; // Protection s := ''; if not (cpLockCell in fmt^.Protection) then s := s + 'ss:Protected="0" '; if cpHideFormulas in fmt^.Protection then s := s + 'x:HideFormula="1" '; if s <> '' then AppendToStream(AStream, INDENT3 + '<Protection ' + s + '/>' + LF); AppendToStream(AStream, INDENT2 + '</Style>' + LF); end; end; procedure TsSpreadExcelXMLWriter.WriteStyles(AStream: TStream); var i: Integer; begin AppendToStream(AStream, INDENT1 + '<Styles>' + LF); for i:=0 to (FWorkbook as TsWorkbook).GetNumCellFormats-1 do WriteStyle(AStream, i); AppendToStream(AStream, INDENT1 + '</Styles>' + LF); end; procedure TsSpreadExcelXMLWriter.WriteTable(AStream: TStream; AWorksheet: TsBasicWorksheet); var sheet: TsWorksheet absolute AWorksheet; begin AppendToStream(AStream, TABLE_INDENT + Format( '<Table ss:ExpandedColumnCount="%d" ss:ExpandedRowCount="%d" ' + 'x:FullColumns="1" x:FullRows="1" ' + 'ss:DefaultColumnWidth="%.2f" ' + 'ss:DefaultRowHeight="%.2f">' + LF, [ FLastCol + 1, FLastRow + 1, sheet.ReadDefaultColWidth(suPoints), sheet.ReadDefaultRowHeight(suPoints) ], FPointSeparatorSettings )); WriteColumns(AStream, AWorksheet); WriteRows(AStream, AWorksheet); AppendToStream(AStream, TABLE_INDENT + '</Table>' + LF); end; {@@ ---------------------------------------------------------------------------- Writes an ExcelXML document to a stream -------------------------------------------------------------------------------} procedure TsSpreadExcelXMLWriter.WriteToStream(AStream: TStream; AParams: TsStreamParams = []); begin Unused(AParams); AppendToStream(AStream, '<?xml version="1.0"?>' + LF + '<?mso-application progid="Excel.Sheet"?>' + LF ); AppendToStream(AStream, '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"' + LF + ' xmlns:o="urn:schemas-microsoft-com:office:office"' + LF + ' xmlns:x="urn:schemas-microsoft-com:office:excel"' + LF + ' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"' + LF + ' xmlns:html="http://www.w3.org/TR/REC-html40">' + LF); WriteDocumentProperties(AStream); WriteOfficeDocumentSettings(AStream); WriteExcelWorkbook(AStream); WriteStyles(AStream); WriteWorksheets(AStream); AppendToStream(AStream, '</Workbook>'); end; procedure TsSpreadExcelXMLWriter.WriteWorksheet(AStream: TStream; AWorksheet: TsBasicWorksheet); var protectedStr: String; begin FWorksheet := AWorksheet; GetSheetDimensions(FWorksheet, FFirstRow, FLastRow, FFirstCol, FLastCol); if FWorksheet.IsProtected then protectedStr := ' ss:Protected="1"' else protectedStr := ''; AppendToStream(AStream, Format( ' <Worksheet ss:Name="%s"%s>' + LF, [ UTF8TextToXMLText(AWorksheet.Name), protectedStr ]) ); WriteNames(AStream, AWorksheet); WriteTable(AStream, AWorksheet); WriteWorksheetOptions(AStream, AWorksheet); WriteConditionalFormatting(AStream, AWorksheet); WritePageBreaks(AStream, AWorksheet); AppendToStream(AStream, ' </Worksheet>' + LF ); end; procedure TsSpreadExcelXMLWriter.WriteWorksheetOptions(AStream: TStream; AWorksheet: TsBasicWorksheet); var footerStr, headerStr: String; hideGridStr: String; hideHeadersStr: String; frozenStr: String; layoutStr: String; marginStr: String; selectedStr: String; protectStr: String; visibleStr: String; printStr: String; fitToPageStr: String; enableSelectionStr: String; sheet: TsWorksheet absolute AWorksheet; begin // Orientation, some PageLayout.Options layoutStr := GetLayoutStr(AWorksheet); if layoutStr <> '' then layoutStr := INDENT4 + layoutStr + LF; // Header headerStr := GetPageHeaderStr(AWorksheet); if headerStr <> '' then headerStr := INDENT4 + headerStr + LF; // Footer footerStr := GetPageFooterStr(AWorksheet); if footerStr <> '' then footerStr := INDENT4 + footerStr + LF; // Page margins marginStr := GetPageMarginStr(AWorksheet); if marginStr <> '' then marginStr := INDENT4 + marginStr + LF; // Show/hide grid lines if not (soShowGridLines in AWorksheet.Options) then hideGridStr := INDENT3 + '<DoNotDisplayGridlines/>' + LF else hideGridStr := ''; // Show/hide column/row headers if not (soShowHeaders in AWorksheet.Options) then hideHeadersStr := INDENT3 + '<DoNotDisplayHeadings/>' + LF else hideHeadersStr := ''; if (FWorkbook as TsWorkbook).ActiveWorksheet = AWorksheet then selectedStr := INDENT3 + '<Selected/>' + LF else selectedStr := ''; // FitToPage node if poFitPages in sheet.PageLayout.Options then fitToPageStr := INDENT3 + '<FitToPage/>' + LF else fitToPageStr := ''; // Print node printStr := GetPrintStr(AWorksheet); // Visible if (soHidden in AWorksheet.Options) then visibleStr := INDENT3 + '<Visible>SheetHidden</Visible>' + LF else visibleStr := ''; // Frozen panes frozenStr := GetFrozenPanesStr(AWorksheet, INDENT3); // Protection protectStr := Format(INDENT3 + '<ProtectObjects>%s</ProtectObjects>' + LF + INDENT3 + '<ProtectScenarios>%s</ProtectScenarios>' + LF, [ StrUtils.IfThen(spObjects in AWorksheet.Protection, 'True', 'False'), StrUtils.IfThen(AWorksheet.IsProtected {and [spScenarios in AWorksheet.Protection])}, 'True', 'False') ]); // Enable selection enableSelectionStr := ''; if (sheet.Protection * [spSelectLockedCells, spSelectUnlockedCells] <> []) then begin enableSelectionStr := INDENT3 + '<EnableSelection>' + LF; if spSelectUnlockedCells in sheet.Protection then enableSelectionStr := enableSelectionStr + INDENT4 + '<NoSelection/>' + LF; if (sheet.Protection * [spSelectLockedCells, spSelectUnlockedCells] = [spSelectLockedCells]) then enableSelectionStr := enableSelectionStr + INDENT4 + '<Unlocked/>' + LF; enableSelectionStr := INDENT3 + '</EnableSelection>' + LF; end; // todo - Several protection options // Put it all together... AppendToStream(AStream, INDENT2 + '<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">' + LF + INDENT3 + '<PageSetup>' + LF + layoutStr + headerStr + footerStr + marginStr + INDENT3 + '</PageSetup>' + LF + fitToPageStr + INDENT3 + '<Print>' + LF + printStr + LF + INDENT3 + '</Print>' + LF + visibleStr + selectedStr + IfThen(not (spFormatCells in sheet.Protection), INDENT4 + '<AllowFormatCells/>' + LF) + IfThen(not (spFormatColumns in sheet.Protection), INDENT4 + '<AllowSizeCols/>' + LF) + IfThen(not (spFormatRows in sheet.Protection), INDENT4 + '<AllowSizeRows/>' + LF) + IfThen(not (spDeleteColumns in sheet.Protection), INDENT4 + '<AllowDeleteCols/>' + LF) + IfThen(not (spDeleteRows in sheet.Protection), INDENT4 + '<AllowDeleteRows/>' + LF) + IfThen(not (spInsertColumns in sheet.Protection), INDENT4 + '<AllowInsertCols/>' + LF) + IfThen(not (spInsertHyperlinks in sheet.Protection), INDENT4 + '<AllowInsertHyperlinks/>' + LF) + IfThen(not (spInsertRows in sheet.Protection), INDENT4 + '<AllowInsertRows/>' + LF) + IfThen(not (spSort in sheet.Protection), INDENT4 + '<AllowSort/>' + LF) + enableSelectionStr + protectStr + frozenStr + hideGridStr + hideHeadersStr + INDENT2 + '</WorksheetOptions>' + LF ); end; procedure TsSpreadExcelXMLWriter.WriteWorksheets(AStream: TStream); var i: Integer; book: TsWorkbook; begin book := FWorkbook as TsWorkbook; for i:=0 to book.GetWorksheetCount-1 do WriteWorksheet(AStream, book.GetWorksheetByIndex(i)); end; initialization // Registers this reader / writer in fpSpreadsheet sfidExcelXML := RegisterSpreadFormat(sfExcelXML, TsSpreadExcelXMLReader, TsSpreadExcelXMLWriter, STR_FILEFORMAT_EXCEL_XML, 'ExcelXML', [STR_XML_EXCEL_EXTENSION] ); end.