From 0395d4247cb8499627f6b3d2219a86b18998b799 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Sun, 22 Oct 2023 08:33:55 +0000 Subject: [PATCH] fpspreadsheet: Add files dropped from previous commit git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8970 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../fpspreadsheet/laz_fpspreadsheet.lpk | 10 +- .../fpspreadsheet/source/common/fpschart.pas | 776 +++++++++++ .../source/common/fpsopendocument.pas | 1103 +++++++++++++++- .../source/common/fpspreadsheet.pas | 164 ++- .../source/common/fpspreadsheet_embobj.inc | 24 +- .../fpspreadsheet/source/common/fpsutils.pas | 64 +- .../fpspreadsheet/source/common/xlsxooxml.pas | 1164 +++++++++++++++-- 7 files changed, 3109 insertions(+), 196 deletions(-) create mode 100644 components/fpspreadsheet/source/common/fpschart.pas diff --git a/components/fpspreadsheet/laz_fpspreadsheet.lpk b/components/fpspreadsheet/laz_fpspreadsheet.lpk index d8d0ef99b..a9d3f21da 100644 --- a/components/fpspreadsheet/laz_fpspreadsheet.lpk +++ b/components/fpspreadsheet/laz_fpspreadsheet.lpk @@ -33,7 +33,7 @@ This package is all you need if you don't want graphical components (such as grids and charts)."/> - + @@ -297,6 +297,14 @@ This package is all you need if you don't want graphical components (such a + + + + + + + + diff --git a/components/fpspreadsheet/source/common/fpschart.pas b/components/fpspreadsheet/source/common/fpschart.pas new file mode 100644 index 000000000..a3ec0d3b9 --- /dev/null +++ b/components/fpspreadsheet/source/common/fpschart.pas @@ -0,0 +1,776 @@ +unit fpschart; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, Contnrs, fpsTypes, fpsUtils; + +const + clsNoLine = -2; + clsSolid = -1; + +{@@ Pre-defined chart line styles given as indexes into the chart's LineStyles + list. Get their value in the constructor of TsChart. Default here to -1 + while is the code for a solid line, just in case that something goes wrong } +var + clsFineDot: Integer = -1; + clsDot: Integer = -1; + clsDash: Integer = -1; + clsDashDot: Integer = -1; + clsLongDash: Integer = -1; + clsLongDashDot: Integer = -1; + clsLongDashDotDot: Integer = -1; + +type + TsChart = class; + + TsChartFill = record + Style: TsFillStyle; + FgColor: TsColor; + BgColor: TsColor; + end; + + TsChartLineSegment = record + Length: Double; // mm or % of linewidth + Count: Integer; + end; + + TsChartLineStyle = class + Name: String; + Segment1: TsChartLineSegment; + Segment2: TsChartLineSegment; + Distance: Double; // mm or % of linewidth + RelativeToLineWidth: Boolean; + function GetID: String; + end; + + TsChartLineStyleList = class(TFPObjectList) + private + function GetItem(AIndex: Integer): TsChartLineStyle; + procedure SetItem(AIndex: Integer; AValue: TsChartLineStyle); + public + function Add(AName: String; + ASeg1Length: Double; ASeg1Count: Integer; + ASeg2Length: Double; ASeg2Count: Integer; + ADistance: Double; ARelativeToLineWidth: Boolean): Integer; + property Items[AIndex: Integer]: TsChartLineStyle read GetItem write SetItem; default; + end; + + TsChartLine = record + Style: Integer; // index into chart's LineStyle list or predefined clsSolid/clsNoLine + Width: Double; // mm + Color: TsColor; // in hex: $00bbggrr, r=red, g=green, b=blue + Transparency: Double; // in percent + end; + + TsChartElement = class + private + FChart: TsChart; + FVisible: Boolean; + public + constructor Create(AChart: TsChart); + property Chart: TsChart read FChart; + property Visible: Boolean read FVisible write FVisible; + end; + + TsChartFillElement = class(TsChartElement) + private + FBackground: TsChartFill; + FBorder: TsChartLine; + public + constructor Create(AChart: TsChart); + property Background: TsChartFill read FBackground write FBackground; + property Border: TsChartLine read FBorder write FBorder; + end; + + TsChartText = class(TsChartFillElement) + private + FCaption: String; + FShowCaption: Boolean; + FFont: TsFont; + public + constructor Create(AChart: TsChart); + destructor Destroy; override; + property Caption: String read FCaption write FCaption; + property Font: TsFont read FFont write FFont; + property ShowCaption: Boolean read FShowCaption write FShowCaption; + end; + + TsChartAxisPosition = (capStart, capEnd, capValue); + TsChartType = (ctEmpty, ctBar, ctLine, ctArea, ctBarLine, ctScatter); + + TsChartAxis = class(TsChartText) + private + FAutomaticMax: Boolean; + FAutomaticMin: Boolean; + FAutomaticMajorInterval: Boolean; + FAutomaticMinorSteps: Boolean; + FAxisLine: TsChartLine; + FMajorGridLines: TsChartLine; + FMinorGridLines: TsChartline; + FInverted: Boolean; + FLabelFont: TsFont; + FLabelFormat: String; + FLogarithmic: Boolean; + FMajorInterval: Double; + FMajorTickLines: TsChartLine; + FMax: Double; + FMin: Double; + FMinorSteps: Double; + FMinorTickLines: TsChartLine; + FPosition: TsChartAxisPosition; + FPositionValue: Double; + FShowMajorGridLines: Boolean; + FShowMinorGridLines: Boolean; + FShowLabels: Boolean; + public + constructor Create(AChart: TsChart); + destructor Destroy; override; + + property AutomaticMax: Boolean read FAutomaticMax write FAutomaticMax; + property AutomaticMin: Boolean read FAutomaticMin write FAutomaticMin; + property AutomaticMajorInterval: Boolean read FAutomaticMajorInterval write FAutomaticMajorInterval; + property AutomaticMinorSteps: Boolean read FAutomaticMinorSteps write FAutomaticMinorSteps; + property AxisLine: TsChartLine read FAxisLine write FAxisLine; + property Inverted: Boolean read FInverted write FInverted; + property LabelFont: TsFont read FLabelFont write FLabelFont; + property LabelFormat: String read FLabelFormat write FLabelFormat; + property Logarithmic: Boolean read FLogarithmic write FLogarithmic; + property MajorGridLines: TsChartLine read FMajorGridLines write FMajorGridLines; + property MajorInterval: Double read FMajorInterval write FMajorInterval; + property MajorTickLines: TsChartLine read FMajorTickLines write FMajorTickLines; + property Max: Double read FMax write FMax; + property Min: Double read FMin write FMin; + property MinorGrid: TsChartLine read FMinorGridLines write FMinorGridLines; + property MinorSteps: Double read FMinorSteps write FMinorSteps; + property MinorTickLines: TsChartLine read FMinorTickLines write FMinorTickLines; + property Position: TsChartAxisPosition read FPosition write FPosition; + property PositionValue: Double read FPositionValue write FPositionValue; + property ShowMajorGridLines: Boolean read FShowMajorGridLines write FShowMajorGridLines; + property ShowMinorGridLines: Boolean read FShowMinorGridLines write FShowMinorGridLines; + property ShowLabels: Boolean read FShowLabels write FShowLabels; + end; + + TsChartLegend = class(TsChartText) + end; + + TsChartAxisLink = (alPrimary, alSecondary); + + TsChartSeries = class(TsChartElement) + private + FChartType: TsChartType; + FXRange: TsCellRange; // cell range containing the x data + FYRange: TsCellRange; + FLabelRange: TsCellRange; + FYAxis: TsChartAxisLink; + FTitleAddr: TsCellCoord; + FLabelFormat: String; + public + constructor Create(AChart: TsChart); + function GetCount: Integer; + function GetXCount: Integer; + function GetYCount: Integer; + function HasLabels: Boolean; + function HasXValues: Boolean; + function HasYValues: Boolean; + procedure SetTitleAddr(ARow, ACol: Cardinal); + procedure SetLabelRange(ARow1, ACol1, ARow2, ACol2: Cardinal); + procedure SetXRange(ARow1, ACol1, ARow2, ACol2: Cardinal); + procedure SetYRange(ARow1, ACol1, ARow2, ACol2: Cardinal); + function LabelsInCol: Boolean; + function XValuesInCol: Boolean; + function YValuesInCol: Boolean; + property ChartType: TsChartType read FChartType; + property Count: Integer read GetCount; + property LabelFormat: String read FLabelFormat write FLabelFormat; // Number format in Excel notation, e.g. '0.00' + property LabelRange: TsCellRange read FLabelRange; + property TitleAddr: TsCellCoord read FTitleAddr write FTitleAddr; + property XRange: TsCellRange read FXRange; + property YRange: TsCellRange read FYRange; + property YAxis: TsChartAxisLink read FYAxis write FYAxis; + end; + + TsChartSeriesSymbol = ( + cssRect, cssDiamond, cssTriangle, cssTriangleDown, cssCircle, cssStar + ); + + TsLineSeries = class(TsChartSeries) + private + FLineStyle: TsChartLine; + FShowLines: Boolean; + FShowSymbols: Boolean; + FSymbol: TsChartSeriesSymbol; + FSymbolFill: TsChartFill; + FSymbolBorder: TsChartLine; + FSymbolHeight: Double; // in mm + FSymbolWidth: Double; // in mm + public + constructor Create(AChart: TsChart); + property LineStyle: TsChartLine read FLineStyle write FLineStyle; + property ShowLines: Boolean read FShowLines write FShowLines; + property ShowSymbols: Boolean read FShowSymbols write FShowSymbols; + property Symbol: TsChartSeriesSymbol read FSymbol write FSymbol; + property SymbolBorder: TsChartLine read FSymbolBorder write FSymbolBorder; + property SymbolFill: TsChartFill read FSymbolFill write FSymbolFill; + property SymbolHeight: double read FSymbolHeight write FSymbolHeight; + property SymbolWidth: double read FSymbolWidth write FSymbolWidth; + end; + + TsChartSeriesList = class(TFPList) + private + function GetItem(AIndex: Integer): TsChartSeries; + procedure SetItem(AIndex: Integer; AValue: TsChartSeries); + public + property Items[AIndex: Integer]: TsChartSeries read GetItem write SetItem; default; + end; + + TsChart = class(TsChartFillElement) + private + FIndex: Integer; // Index in workbook's chart list + FSheetIndex: Integer; + FRow, FCol: Cardinal; + FOffsetX, FOffsetY: Double; + FWidth, FHeight: Double; // Width, Height of the chart, in mm. + + FPlotArea: TsChartFillElement; + FFloor: TsChartFillElement; + FXAxis: TsChartAxis; + FX2Axis: TsChartAxis; + FYAxis: TsChartAxis; + FY2Axis: TsChartAxis; + + FTitle: TsChartText; + FSubTitle: TsChartText; + FLegend: TsChartLegend; + FSeriesList: TsChartSeriesList; + + FLineStyles: TsChartLineStyleList; + function GetCategoryLabelRange: TsCellRange; + + public + constructor Create; + destructor Destroy; override; + function AddSeries(ASeries: TsChartSeries): Integer; + procedure DeleteSeries(AIndex: Integer); + + function GetChartType: TsChartType; + function GetLineStyle(AIndex: Integer): TsChartLineStyle; + function IsScatterChart: Boolean; + function NumLineStyles: Integer; + { + function CategoriesInCol: Boolean; + function CategoriesInRow: Boolean; + function GetCategoryCount: Integer; + function HasCategories: Boolean; + } + { Index of chart in workbook's chart list. } + property Index: Integer read FIndex write FIndex; + { Index of worksheet sheet which contains the chart. } + property SheetIndex: Integer read FSheetIndex write FSheetIndex; + { Row index of the cell in which the chart has its top/left corner (anchor) } + property Row: Cardinal read FRow write FRow; + { Column index of the cell in which the chart has its top/left corner (anchor) } + property Col: Cardinal read FCol write FCol; + { Offset of the left chart edge relative to the anchor cell, in mm } + property OffsetX: double read FOffsetX write FOffsetX; + { Offset of the top chart edge relative to the anchor cell, in mm } + property OffsetY: double read FOffsetY write FOffsetY; + { Width of the chart, in mm } + property Width: double read FWidth write FWidth; + { Height of the chart, in mm } + property Height: double read FHeight write FHeight; + + { Attributes of the plot area (rectangle enclosed by axes) } + property PlotArea: TsChartFillElement read FPlotArea write FPlotArea; + { Attributes of the floor of a 3D chart } + property Floor: TsChartFillElement read FFloor write FFloor; + + { Attributes of the chart's title } + property Title: TsChartText read FTitle write FTitle; + { Attributes of the chart's subtitle } + property Subtitle: TsChartText read FSubtitle write FSubTitle; + { Attributs of the chart's legend } + property Legend: TsChartLegend read FLegend write FLegend; + + { Attributes of the plot's primary x axis (bottom) } + property XAxis: TsChartAxis read FXAxis write FXAxis; + { Attributes of the plot's secondary x axis (top) } + property X2Axis: TsChartAxis read FX2Axis write FX2Axis; + { Attributes of the plot's primary y axis (left) } + property YAxis: TsChartAxis read FYAxis write FYAxis; + { Attributes of the plot's secondary y axis (right) } + property Y2Axis: TsChartAxis read FY2Axis write FY2Axis; + + property CategoryLabelRange: TsCellRange read GetCategoryLabelRange; + + { Attributes of the series } + property Series: TsChartSeriesList read FSeriesList write FSeriesList; + end; + + TsChartList = class(TObjectList) + private + function GetItem(AIndex: Integer): TsChart; + procedure SetItem(AIndex: Integer; AValue: TsChart); + public + property Items[AIndex: Integer]: TsChart read GetItem write SetItem; default; + end; + + +implementation + +const + DEFAULT_LINE_WIDTH = 0.75; // pts + +{ TsChartLineStyle } + +function TsChartLineStyle.GetID: String; +var + i: Integer; +begin + Result := Name; + for i:=1 to Length(Result) do + if Result[i] in [' ', '-'] then Result[i] := '_'; + Result := 'FPS' + Result; +end; + + +{ TsChartLineStyleList } + +function TsChartLineStyleList.Add(AName: String; + ASeg1Length: Double; ASeg1Count: Integer; + ASeg2Length: Double; ASeg2Count: Integer; + ADistance: Double; ARelativeToLineWidth: Boolean): Integer; +var + ls: TsChartLineStyle; +begin + ls := TsChartLineStyle.Create; + ls.Name := AName; + ls.Segment1.Count := ASeg1Count; + ls.Segment1.Length := ASeg1Length; + ls.Segment2.Count := ASeg2Count; + ls.Segment2.Length := ASeg2Length; + ls.Distance := ADistance; + ls.RelativeToLineWidth := ARelativeToLineWidth; + result := inherited Add(ls); +end; + +function TsChartLineStyleList.GetItem(AIndex: Integer): TsChartLineStyle; +begin + Result := TsChartLineStyle(inherited); +end; + +procedure TsChartLineStyleList.SetItem(AIndex: Integer; AValue: TsChartLineStyle); +begin + inherited Items[AIndex] := AValue; +end; + + +{ TsChartElement } + +constructor TsChartElement.Create(AChart: TsChart); +begin + inherited Create; + FChart := AChart; + FVisible := true; +end; + + +{ TsChartFillElement } + +constructor TsChartFillElement.Create(AChart: TsChart); +begin + inherited Create(AChart); + FBackground.Style := fsSolidFill; + FBackground.BgColor := scWhite; + FBackground.FgColor := scWhite; + FBorder.Style := clsSolid; + FBorder.Width := PtsToMM(DEFAULT_LINE_WIDTH); + FBorder.Color := scBlack; +end; + + +{ TsChartText } + +constructor TsChartText.Create(AChart: TsChart); +begin + inherited Create(AChart); + FShowCaption := true; + FFont := TsFont.Create; + FFont.FontName := ''; // replace by workbook's default font + FFont.Size := 0; // replace by workbook's default font size + FFont.Style := []; + FFont.Color := scBlack; +end; + +destructor TsChartText.Destroy; +begin + FFont.Free; + inherited; +end; + + +{ TsChartAxis } + +constructor TsChartAxis.Create(AChart: TsChart); +begin + inherited Create(AChart); + + FAutomaticMajorInterval := true; + FAutomaticMinorSteps := true; + + FLabelFont := TsFont.Create; + FLabelFont.FontName := ''; // replace by workbook's default font + FLabelFont.Size := 0; // Replace by workbook's default font size + FLabelFont.Style := []; + FLabelFont.Color := scBlack; + + FShowLabels := true; + + FAxisLine.Color := scBlack; + FAxisLine.Style := clsSolid; + FAxisLine.Width := PtsToMM(DEFAULT_LINE_WIDTH); + + FMajorTickLines.Color := scBlack; + FMajorTickLines.Style := clsSolid; + FMajorTickLines.Width := PtsToMM(DEFAULT_LINE_WIDTH); + + FMinorTickLines.Color := scBlack; + FMinorTickLines.Style := clsSolid; + FMinorTickLines.Width := PtsToMM(DEFAULT_LINE_WIDTH); + + FMajorGridLines.Color := scSilver; + FMajorGridLines.Style := clsSolid; + FMajorGridLines.Width := PtsToMM(DEFAULT_LINE_WIDTH); + + FMinorGridLines.Color := scSilver; + FMinorGridLines.Style := clsDot; + FMinorGridLines.Width := PtsToMM(DEFAULT_LINE_WIDTH); +end; + +destructor TsChartAxis.Destroy; +begin + FLabelFont.Free; + inherited; +end; + (* +{ Determines how many labels are provided in the CategoryLabelRange. } +function TsChartAxis.GetCategoryCount: Integer; +begin + if CategoriesInCol then + Result := FLabelRange.Col2 - FLabelRange.Col1 + 1 + else + if CategoriesInCol then + Result := FLabelRange.Row2 - FLabelRange.Row1 + 1 + else + Result := 0; +end; + +{ Returns true when the axis owns its own category labels. Otherwise labels + are taken from the series } +function TsChartAxis.HasCategoryLabels: Boolean; +begin + Result := CategoriesInCol or CategoriesInRow; +end; + +{ Determines whether the axis labels are taken from columns (true) or rows (false) } +function TsChartAxis.CategoriesInCol: Boolean; +begin + Result := (FCategoryLabelRange.Row1 <> FCategoryLabelRange.Row2) and + (FCategoryLabelRange.Col1 = FCategoryLabelRange.Col2); +end; + +function TsChartAxis.CategoriesInRow: Boolean; +begin + Result := (FCategoryLabelRange.Col1 <> FCategoryLabelRange.Col2) and + (FCategoryLabelRange.Row1 = FCategoryLabelRange.Row2); +end; + *) + +{ TsChartSeries } + +constructor TsChartSeries.Create(AChart: TsChart); +begin + inherited Create(AChart); + AChart.AddSeries(self); +end; + +function TsChartSeries.GetCount: Integer; +begin + Result := GetYCount; +end; + +function TsChartSeries.GetXCount: Integer; +begin + if (FXRange.Row1 = FXRange.Row2) and (FXRange.Col1 = FXRange.Col2) then + Result := 0 + else + if (FXRange.Row1 = FXRange.Row2) then + Result := FXRange.Col2 - FXRange.Col1 + 1 + else + Result := FXRange.Row2 - FXRange.Row1 + 1; +end; + +function TsChartSeries.GetYCount: Integer; +begin + if YValuesInCol then + Result := FYRange.Row2 - FYRange.Row1 + 1 + else + Result := FYRange.Col2 - FYRange.Col1 + 1; +end; + +function TsChartSeries.HasLabels: Boolean; +begin + Result := not ((FLabelRange.Row1 = FLabelRange.Row2) and (FLabelRange.Col1 = FLabelRange.Col2)); +end; + +function TsChartSeries.HasXValues: Boolean; +begin + Result := not ((FXRange.Row1 = FXRange.Row2) and (FXRange.Col1 = FXRange.Col2)); +end; + +function TsChartSeries.HasYValues: Boolean; +begin + Result := not ((FYRange.Row1 = FYRange.Row2) and (FYRange.Col1 = FYRange.Col2)); +end; + +function TsChartSeries.LabelsInCol: Boolean; +begin + Result := (FLabelRange.Col1 = FLabelRange.Col2) and (FLabelRange.Row1 <> FLabelRange.Row2); +end; + +procedure TsChartSeries.SetTitleAddr(ARow, ACol: Cardinal); +begin + FTitleAddr.Row := ARow; + FTitleAddr.Col := ACol; +end; + +procedure TsChartSeries.SetLabelRange(ARow1, ACol1, ARow2, ACol2: Cardinal); +begin + if (ARow1 <> ARow2) and (ACol1 <> ACol2) then + raise Exception.Create('Series labels can only be located in a single column or row.'); + FLabelRange.Row1 := ARow1; + FLabelRange.Col1 := ACol1; + FLabelRange.Row2 := ARow2; + FLabelRange.Col2 := ACol2; +end; + +procedure TsChartSeries.SetXRange(ARow1, ACol1, ARow2, ACol2: Cardinal); +begin + if (ARow1 <> ARow2) and (ACol1 <> ACol2) then + raise Exception.Create('Series x values can only be located in a single column or row.'); + FXRange.Row1 := ARow1; + FXRange.Col1 := ACol1; + FXRange.Row2 := ARow2; + FXRange.Col2 := ACol2; +end; + +procedure TsChartSeries.SetYRange(ARow1, ACol1, ARow2, ACol2: Cardinal); +begin + if (ARow1 <> ARow2) and (ACol1 <> ACol2) then + raise Exception.Create('Series y values can only be located in a single column or row.'); + FYRange.Row1 := ARow1; + FYRange.Col1 := ACol1; + FYRange.Row2 := ARow2; + FYRange.Col2 := ACol2; +end; + +function TsChartSeries.XValuesInCol: Boolean; +begin + Result := (FXRange.Col1 = FXRange.Col2) and (FXRange.Row1 <> FXRange.Row2); +end; + +function TsChartSeries.YValuesInCol: Boolean; +begin + Result := (FYRange.Col1 = FYRange.Col2) and (FYRange.Row1 <> FYRange.Row2); +end; + + +{ TsChartSeriesList } + +function TsChartSeriesList.GetItem(AIndex: Integer): TsChartSeries; +begin + Result := TsChartSeries(inherited Items[AIndex]); +end; + +procedure TsChartSeriesList.SetItem(AIndex: Integer; AValue: TsChartSeries); +begin + inherited Items[AIndex] := AValue; +end; + + +{ TsLineSeries } + +constructor TsLineSeries.Create(AChart: TsChart); +begin + inherited Create(AChart); + + FChartType := ctLine; + + FLineStyle.Color := scBlack; + FLineStyle.Style := clsSolid; + FLineStyle.Width := PtsToMM(DEFAULT_LINE_WIDTH); + + FSymbolBorder.Color := scBlack; + FSymbolBorder.Style := clsSolid; + FSymbolBorder.Width := PtsToMM(DEFAULT_LINE_WIDTH); + + FSymbolFill.FgColor := scWhite; + FSymbolFill.BgColor := scWhite; + FSymbolFill.Style := fsSolidFill; + + FSymbolWidth := 2.5; + FSymbolHeight := 2.5; +end; + + +{ TsChart } + +constructor TsChart.Create; +begin + inherited Create(nil); + + FLineStyles := TsChartLineStyleList.Create; + clsFineDot := FLineStyles.Add('fine-dot', 100, 1, 0, 0, 100, false); + clsDot := FLineStyles.Add('dot', 150, 1, 0, 0, 150, true); + clsDash := FLineStyles.Add('dash', 300, 1, 0, 0, 150, true); + clsDashDot := FLineStyles.Add('dash-dot', 300, 1, 100, 1, 150, true); + clsLongDash := FLineStyles.Add('long dash', 400, 1, 0, 0, 200, true); + clsLongDashDot := FLineStyles.Add('long dash-dot', 500, 1, 100, 1, 200, true); + clsLongDashDotDot := FLineStyles.Add('long dash-dot-dot', 500, 1, 100, 2, 200, true); + + FSheetIndex := 0; + FRow := 0; + FCol := 0; + FOffsetX := 0.0; + FOffsetY := 0.0; + FWidth := 12; + FHeight := 9; + + FPlotArea := TsChartFillElement.Create(self); + FFloor := TsChartFillElement.Create(self); + + FTitle := TsChartText.Create(self); + FTitle.Font.Size := 14; + + FSubTitle := TsChartText.Create(self); + FSubTitle.Font.Size := 12; + + FLegend := TsChartLegend.Create(self); + + FXAxis := TsChartAxis.Create(self); + FXAxis.Caption := 'x axis'; + FXAxis.LabelFont.Size := 9; + FXAxis.Font.Size := 10; + FXAxis.Font.Style := [fssBold]; + FXAxis.Position := capStart; + + FX2Axis := TsChartAxis.Create(self); + FX2Axis.Caption := 'Secondary x axis'; + FX2Axis.LabelFont.Size := 9; + FX2Axis.Font.Size := 10; + FX2Axis.Font.Style := [fssBold]; + FX2Axis.Visible := false; + FX2Axis.Position := capEnd; + + FYAxis := TsChartAxis.Create(self); + FYAxis.Caption := 'y axis'; + FYAxis.LabelFont.Size := 9; + FYAxis.Font.Size := 10; + FYAxis.Font.Style := [fssBold]; + FYAxis.Position := capStart; + + FY2Axis := TsChartAxis.Create(self); + FY2Axis.Caption := 'Secondary y axis'; + FY2Axis.LabelFont.Size := 9; + FY2Axis.Font.Size := 10; + FY2Axis.Font.Style := [fssBold]; + FY2Axis.Visible := false; + FY2Axis.Position := capEnd; + + FSeriesList := TsChartSeriesList.Create; +end; + +destructor TsChart.Destroy; +begin + FSeriesList.Free; + FXAxis.Free; + FYAxis.Free; + FY2Axis.Free; + FLegend.Free; + FTitle.Free; + FSubtitle.Free; + FLineStyles.Free; + FFloor.Free; + FPlotArea.Free; + inherited; +end; + +function TsChart.AddSeries(ASeries: TsChartSeries): Integer; +begin + Result := FSeriesList.IndexOf(ASeries); + if Result = -1 then + Result := FSeriesList.Add(ASeries); +end; + +procedure TsChart.DeleteSeries(AIndex: Integer); +begin + if (AIndex >= 0) and (AIndex < FSeriesList.Count) then + FSeriesList.Delete(AIndex); +end; + +function TsChart.GetCategoryLabelRange: TsCellRange; +begin + if FSeriesList.Count > 0 then + Result := Series[0].LabelRange + else + begin + Result.Row1 := 0; + Result.Col1 := 0; + Result.Row2 := 0; + Result.Col2 := 0; + end; +end; + +function TsChart.GetChartType: TsChartType; +begin + if FSeriesList.Count > 0 then + Result := Series[0].ChartType + else + Result := ctEmpty; +end; + +function TsChart.GetLineStyle(AIndex: Integer): TsChartLineStyle; +begin + if AIndex >= 0 then + Result := FLineStyles[AIndex] + else + Result := nil; +end; + +function TsChart.IsScatterChart: Boolean; +begin + Result := GetChartType = ctScatter; +end; + +function TsChart.NumLineStyles: Integer; +begin + Result := FLineStyles.Count; +end; + + +{ TsChartList } + +function TsChartList.GetItem(AIndex: Integer): TsChart; +begin + Result := TsChart(inherited Items[AIndex]); +end; + +procedure TsChartlist.SetItem(AIndex: Integer; AValue: TsChart); +begin + inherited Items[AIndex] := AValue; +end; + +end. + diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index ef4466f83..12cab99b1 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -40,7 +40,7 @@ uses fpszipper, {$ENDIF} fpstypes, fpsReaderWriter, fpsutils, fpsHeaderFooterParser, - fpsNumFormat, fpsxmlcommon, fpsPagelayout; + fpsNumFormat, fpsxmlcommon, fpsPagelayout, fpsChart; type TDateModeODS=( @@ -236,6 +236,7 @@ type procedure WriteCellRow(AStream: TStream; ASheet: TsBasicWorksheet; ARowIndex, ALastColIndex: Integer); procedure WriteCellStyles(AStream: TStream); + procedure WriteColStyles(AStream: TStream); procedure WriteColumns(AStream: TStream; ASheet: TsBasicWorksheet); procedure WriteConditionalFormats(AStream: TStream; ASheet: TsBasicWorksheet); @@ -244,10 +245,12 @@ type ARowIndex, AFirstColIndex, ALastColIndex, ALastRowIndex: Integer; out ARowsRepeated: Integer); procedure WriteFontNames(AStream: TStream); + procedure WriteGraphicStyles(AStream: TStream); procedure WriteMasterStyles(AStream: TStream); procedure WriteNamedExpressions(AStream: TStream; ASheet: TsBasicWorksheet); procedure WriteNumFormats(AStream: TStream); procedure WriteOfficeStyles(AStream: TStream); + procedure WriteParagraphStyles(AStream: TStream); procedure WriteRowStyles(AStream: TStream); procedure WriteRowsAndCells(AStream: TStream; ASheet: TsBasicWorksheet); procedure WriteShapes(AStream: TStream; ASheet: TsBasicWorksheet); @@ -281,11 +284,24 @@ type function WriteVertAlignmentStyleXMLAsString(const AFormat: TsCellFormat): String; function WriteWordwrapStyleXMLAsString(const AFormat: TsCellFormat): String; + { Chart support } + procedure PrepareChartTable(AChart: TsChart; AWorksheet: TsBasicWorksheet); + procedure WriteChart(AStream: TStream; AChart: TsChart); + procedure WriteChartAxis(AStream: TStream; AChart: TsChart; IsX, IsPrimary: Boolean; AIndent: Integer); + procedure WriteChartLegend(AStream: TStream; AChart: TsChart; AIndent: Integer); + procedure WriteChartPlotArea(AStream: TStream; AChart: TsChart; AIndent: Integer); + procedure WriteChartSeries(AStream: TStream; AChart: TsChart; ASeriesIndex: Integer; AIndent: Integer); + procedure WriteChartStyles(AStream: TStream; AChart: TsChart; AIndent: Integer); + procedure WriteChartTable(AStream: TStream; AChart: TsChart; AIndent: Integer); + procedure WriteChartTitle(AStream: TStream; AChart: TsChart; IsSubtitle: Boolean; AIndent: Integer); + protected FPointSeparatorSettings: TFormatSettings; + // Streams with the contents of files FSMeta, FSSettings, FSStyles, FSContent: TStream; FSMimeType, FSMetaInfManifest: TStream; + FSCharts: array of TStream; { Helpers } procedure AddBuiltinNumFormats; override; @@ -309,6 +325,7 @@ type procedure ResetStreams; { Routines to write those files } + procedure WriteCharts; procedure WriteContent; procedure WriteMetaInfManifest; procedure WriteMeta; @@ -360,6 +377,8 @@ uses fpsExprParser, fpsImages, fpsConditionalFormat; const + LE = LineEnding; + { OpenDocument general XML constants } XML_HEADER = ''; @@ -369,8 +388,9 @@ const OPENDOC_PATH_SETTINGS = 'settings.xml'; OPENDOC_PATH_STYLES = 'styles.xml'; OPENDOC_PATH_MIMETYPE = 'mimetype'; - {%H-}OPENDOC_PATH_METAINF = 'META-INF' + '/'; - {%H-}OPENDOC_PATH_METAINF_MANIFEST = 'META-INF' + '/' + 'manifest.xml'; + {%H-}OPENDOC_PATH_METAINF = 'META-INF/'; + OPENDOC_PATH_METAINF_MANIFEST = 'META-INF/manifest.xml'; + OPENDOC_PATH_CHART_CONTENT = 'Object %d/content.xml'; { OpenDocument schemas constants } SCHEMAS_XMLNS_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'; @@ -485,6 +505,11 @@ const '5Rating', '5Quarters', '5Boxes' // is5Rating, is5Quarters, is5Boxes ); + CHART_TYPE_NAMES: array[TsChartType] of string = ( + '', 'bar', 'line', 'area', 'barLine', 'scatter' + ); + + function CFOperandToStr(v: variant; AWorksheet: TsWorksheet; const AFormatSettings: TFormatSettings): String; var @@ -2912,7 +2937,6 @@ var sheetName: String; tablestyleName: String; err: String; - isEncrypted: Boolean = false; function CreateXMLStream: TStream; begin @@ -2930,10 +2954,10 @@ begin Doc := nil; try - // Read the META-INF/manifest.xml file to learn about encryption + // Read the "META-INF/manifest.xml" file to learn about encryption XMLStream := CreateXMLStream; try - if UnzipToStream(AStream, 'META-INF/manifest.xml', XMLStream) then + if UnzipToStream(AStream, OPENDOC_PATH_METAINF_MANIFEST, XMLStream) then begin ReadXMLStream(Doc, XMLStream); if Assigned(Doc) then @@ -5682,6 +5706,8 @@ end; { Creates the streams for the individual data files. Will be zipped into a single xlsx file. } procedure TsSpreadOpenDocWriter.CreateStreams; +var + i, n: Integer; begin FSMeta := CreateTempStream(FWorkbook, 'fpsM'); FSSettings := CreateTempStream(FWorkbook, 'fpsS'); @@ -5689,39 +5715,19 @@ begin FSContent := CreateTempStream(FWorkbook, 'fpsC'); FSMimeType := CreateTempStream(FWorkbook, 'fpsMT'); FSMetaInfManifest := CreateTempStream(FWorkbook, 'fpsMIM'); - { - if boFileStream in FWorkbook.Options then - begin - FSMeta := TFileStream.Create(GetTempFileName('', 'fpsM'), fmCreate); - FSSettings := TFileStream.Create(GetTempFileName('', 'fpsS'), fmCreate); - FSStyles := TFileStream.Create(GetTempFileName('', 'fpsSTY'), fmCreate); - FSContent := TFileStream.Create(GetTempFileName('', 'fpsC'), fmCreate); - FSMimeType := TFileStream.Create(GetTempFileName('', 'fpsMT'), fmCreate); - FSMetaInfManifest := TFileStream.Create(GetTempFileName('', 'fpsMIM'), fmCreate); - end else - if (boBufStream in Workbook.Options) then - begin - FSMeta := TBufStream.Create(GetTempFileName('', 'fpsM')); - FSSettings := TBufStream.Create(GetTempFileName('', 'fpsS')); - FSStyles := TBufStream.Create(GetTempFileName('', 'fpsSTY')); - FSContent := TBufStream.Create(GetTempFileName('', 'fpsC')); - FSMimeType := TBufStream.Create(GetTempFileName('', 'fpsMT')); - FSMetaInfManifest := TBufStream.Create(GetTempFileName('', 'fpsMIM')); - end else - begin - FSMeta := TMemoryStream.Create; - FSSettings := TMemoryStream.Create; - FSStyles := TMemoryStream.Create; - FSContent := TMemoryStream.Create; - FSMimeType := TMemoryStream.Create; - FSMetaInfManifest := TMemoryStream.Create; - end; - } + + n := TsWorkbook(FWorkbook).GetChartCount; + SetLength(FSCharts, n); + for i := 0 to n - 1 do + FSCharts[i] := CreateTempStream(FWorkbook, 'fpsCh'); + // FSSheets will be created when needed. end; { Destroys the temporary streams that were created by the writer } procedure TsSpreadOpenDocWriter.DestroyStreams; +var + i: Integer; begin DestroyTempStream(FSMeta); DestroyTempStream(FSSettings); @@ -5729,6 +5735,9 @@ begin DestroyTempStream(FSContent); DestroyTempStream(FSMimeType); DestroyTempStream(FSMetaInfManifest); + for i := 0 to High(FSCharts) do + DestroyTempStream(FSCharts[i]); + Setlength(FSCharts, 0); end; procedure TsSpreadOpenDocWriter.GetHeaderFooterImageName( @@ -5810,6 +5819,7 @@ end; procedure TsSpreadOpenDocWriter.InternalWriteToStream(AStream: TStream); var FZip: TZipper; + i: Integer; begin { Analyze the workbook and collect all information needed } ListAllNumFormats; @@ -5827,6 +5837,7 @@ begin WriteSettings(); WriteStyles(); WriteContent; + WriteCharts; { Now compress the files } FZip := TZipper.Create; @@ -5839,6 +5850,9 @@ begin FZip.Entries.AddFileEntry(FSMimetype, OPENDOC_PATH_MIMETYPE); FZip.Entries.AddFileEntry(FSMetaInfManifest, OPENDOC_PATH_METAINF_MANIFEST); ZipPictures(FZip); + for i := 0 to TsWorkbook(FWorkbook).GetChartCount-1 do + FZip.Entries.AddFileEntry( + FSCharts[i], Format(OPENDOC_PATH_CHART_CONTENT, [i+1])); ResetStreams; @@ -6028,6 +6042,8 @@ end; { Is called before zipping the individual file parts. Rewinds the streams. } procedure TsSpreadOpenDocWriter.ResetStreams; +var + i: Integer; begin FSMeta.Position := 0; FSSettings.Position := 0; @@ -6035,6 +6051,8 @@ begin FSContent.Position := 0; FSMimeType.Position := 0; FSMetaInfManifest.Position := 0; + for i := 0 to High(FSCharts) do + FSCharts[i].Position := 0; end; { Writes the node "office:automatic-styles". Although this node occurs in both @@ -6113,17 +6131,19 @@ var embObj: TsEmbeddedObj; begin AppendToStream(FSMetaInfManifest, - ''); + XML_HEADER); AppendToStream(FSMetaInfManifest, - ''); + '' + LE); AppendToStream(FSMetaInfManifest, - ''); + ' ' + LE); AppendToStream(FSMetaInfManifest, - ''); + ' ' + LE); AppendToStream(FSMetaInfManifest, - ''); + ' ' + LE); AppendToStream(FSMetaInfManifest, - ''); + ' ' + LE); + AppendToStream(FSMetaInfManifest, + ' ' + LE); for i:=0 to (FWorkbook as TsWorkbook).GetEmbeddedObjCount-1 do begin embObj := TsWorkbook(FWorkbook).GetEmbeddedObj(i); @@ -6133,10 +6153,22 @@ begin mime := GetImageMimeType(imgtype); ext := GetImageTypeExt(imgType); AppendToStream(FSMetaInfManifest, Format( - '', + ' ' + LE, [mime, i+1, ext] )); end; + for i:=0 to (FWorkbook as TsWorkbook).GetChartCount-1 do + begin + AppendToStream(FSMetaInfManifest, Format( + ' ' + LE, + [i+1] + )); + AppendToStream(FSMetaInfManifest, Format( + ' ' + LE, + [i+1] + )); + // Object X/styles.xml, Object X/meta.xml and ObjectReplacement/Object X not needed necessarily + end; AppendToStream(FSMetaInfManifest, ''); end; @@ -6150,57 +6182,49 @@ begin book := TsWorkbook(FWorkbook); AppendToStream(FSMeta, - XML_HEADER); + XML_HEADER + LE); AppendToStream(FSMeta, - ''); - { + '' + LE); AppendToStream(FSMeta, - ''); - } - AppendToStream(FSMeta, - '', - 'FPSpreadsheet Library' + - ''); + ' ' + LE, + ' FPSpreadsheet Library' + LE + + ' ' + LE); if book.Metadata.Title <> '' then begin s := book.Metadata.Title; AppendToStream(FSMeta, Format( - '%s', [UTF8TextToXMLText(s)])); + ' %s' + LE, [UTF8TextToXMLText(s)])); end; if book.Metadata.Subject <> '' then begin s := book.Metadata.Subject; AppendToStream(FSMeta, Format( - '%s', [UTF8TextToXMLText(s)])); + ' %s' + LE, [UTF8TextToXMLText(s)])); end; if book.Metadata.CreatedBy <> '' then AppendToStream(FSMeta, Format( - '%s', [UTF8TextToXMLText(book.MetaData.CreatedBy)])); + ' %s' + LE, [UTF8TextToXMLText(book.MetaData.CreatedBy)])); if book.MetaData.LastModifiedBy <> '' then AppendToStream(FSMeta, Format( - '%s', [UTF8TextToXMLText(book.Metadata.LastModifiedBy)])); + ' %s' + LE, [UTF8TextToXMLText(book.Metadata.LastModifiedBy)])); if book.MetaData.DateCreated > 0 then begin // ODS stored the creation date in UTC. s := FormatDateTime(ISO8601FormatExtendedUTC, book.MetaData.DateCreated); AppendToStream(FSMeta, Format( - '%s', [s])); + ' %s' + LE, [s])); end; if book.MetaData.DateLastModified > 0 then @@ -6208,13 +6232,13 @@ begin // Date of last modification is NOT UTC. s := FormatDateTime(ISO8601FormatExtended, book.MetaData.DateLastModified); AppendToStream(FSMeta, Format( - '%s', [s])); + ' %s' + LE, [s])); end; if book.MetaData.Keywords.Count > 0 then for i := 0 to book.MetaData.Keywords.Count-1 do AppendToStream(FSMeta, Format( - '%s', [UTF8TextToXMLText(book.MetaData.Keywords[i])])); + ' %s' + LE, [UTF8TextToXMLText(book.MetaData.Keywords[i])])); if book.MetaData.Comments.Count > 0 then begin @@ -6222,21 +6246,21 @@ begin for i := 1 to book.MetaData.Comments.Count-1 do s := s + #10 + book.MetaData.Comments[i]; AppendToStream(FSMeta, Format( - '%s', [UTF8TextToXMLText(s)])); + ' %s' + LE, [UTF8TextToXMLText(s)])); end; if book.MetaData.Custom.Count > 0 then begin for i := 0 to book.Metadata.Custom.Count-1 do AppendToStream(FSMeta, Format( - '%s', [ + ' %s' + LE, [ book.Metadata.Custom.Names[i], UTF8TextToXMLText(book.Metadata.Custom.ValueFromIndex[i]) ])); end; AppendToStream(FSMeta, - ''); + ' ' + LE); AppendToStream(FSMeta, ''); end; @@ -6411,6 +6435,18 @@ begin ''); end; +procedure TsSpreadOpenDocWriter.WriteCharts; +var + i: Integer; + chart: TsChart; +begin + for i := 0 to TsWorkbook(FWorkbook).GetChartCount - 1 do + begin + chart := TsWorkbook(FWorkbook).GetChartByIndex(i); + WriteChart(FSCharts[i], chart); + end; +end; + procedure TsSpreadOpenDocWriter.WriteContent; var i: Integer; @@ -6500,6 +6536,8 @@ begin WriteTableStyles(FSContent); // "ta1" ... WriteCellStyles(FSContent); // "ce1" ... WriteTextStyles(FSContent); // "T1" ... + WriteGraphicStyles(FSContent); // "gr1" ... + WriteParagraphStyles(FSContent); // "P1" ... AppendToStream(FSContent, ''); @@ -6636,6 +6674,827 @@ begin end; end; +procedure TsSpreadOpenDocWriter.WriteChart(AStream: TStream; AChart: TsChart); +var + chartClass: String; +begin + AppendToStream(AStream, + XML_HEADER + LE); + + AppendToStream(AStream, + '' + LE + ); + + WriteChartStyles(AStream, AChart, 2); + + AppendToStream(AStream, + ' ' + LE + + ' ' + LE + ); + + chartClass := CHART_TYPE_NAMES[AChart.GetChartType]; + if chartClass <> '' then + chartClass := ' chart:class="chart:' + chartClass + '"'; + + AppendToStream(AStream, Format( + ' ' + LE, + [ + AChart.Width, // Width, Height are in mm + AChart.Height + ], FPointSeparatorSettings + )); + + WriteChartTitle(AStream, AChart, false, 8); // Title + WriteChartTitle(AStream, AChart, true, 8); // Subtitle + WriteChartLegend(AStream, AChart, 8); + WriteChartPlotArea(AStream, AChart, 8); + WriteChartTable(AStream, AChart, 8); + + AppendToStream(AStream, + ' ' + LE + + ' ' + LE + + ' ' + LE + + ''); +end; + +procedure TsSpreadOpenDocWriter.WriteChartAxis(AStream: TStream; AChart: TsChart; + IsX, IsPrimary: Boolean; AIndent: Integer); +const + AXIS_ID: array[boolean] of string = ('y', 'x'); + AXIS_LEVEL: array[boolean] of string = ('secondary', 'primary'); +var + ind: String; + axis: TsChartAxis; + series: TsChartSeries; + sheet: TsWorksheet; + refStr: String; + styleID, titleStyleID: Integer; + majorGridStyleID: Integer = 8; + minorGridstyleID: Integer = 9; + r1, c1, r2, c2: Cardinal; +begin + ind := DupeString(' ', AIndent); + sheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(AChart.SheetIndex); + + if IsX then + begin + if IsPrimary then + begin + axis := AChart.XAxis; + styleID := 6; + end else + begin + axis := AChart.X2Axis; + styleID := 10; + end; + titleStyleID := 7; + end else + begin + if IsPrimary then + begin + axis := AChart.YAxis; + styleID := 11; + end else + begin + axis := AChart.Y2Axis; + styleID := 12; + end; + titleStyleID := 12; + end; + + AppendToStream(AStream, Format( + ind + '' + LE, + [ AXIS_ID[IsX], AXIS_LEVEL[IsPrimary], AXIS_ID[IsX], styleID ] + )); + + if IsX and (not AChart.IsScatterChart) and (AChart.Series.Count > 0) then + begin + series := AChart.Series[0]; + r1 := series.LabelRange.Row1; + c1 := series.LabelRange.Col1; + r2 := series.LabelRange.Row2; + c2 := series.LabelRange.Col2; + refStr := GetSheetCellRangeString_ODS(sheet.Name, sheet.Name, r1, c1, r2, c2, rfAllRel, false); + AppendToStream(AStream, Format( + ind + ' ' + LE, + [ refStr ] + )); + end; + + if axis.Caption <> '' then + AppendToStream(AStream, Format( + ind + ' ' + LE + + ind + ' %s' + LE + + ind + ' ' + LE, + [ titleStyleID, axis.Caption ] + )); + + if axis.ShowMajorGridLines then + AppendToStream(AStream, Format( + ind + ' ' + LE, + [ majorGridStyleID ] + )); + + if axis.ShowMinorGridLines then + AppendToStream(AStream, Format( + ind + ' ' + LE, + [ minorGridStyleID ] + )); + + AppendToStream(AStream, + ind + '' + LE + ); +end; + +{ Writes the chart's legend to the xml stream } +procedure TsSpreadOpenDocWriter.WriteChartLegend(AStream: TStream; AChart: TsChart; + AIndent: Integer); +var + ind: String; + legendStyleID: Integer = 4; +begin + if (not AChart.Legend.Visible) then + exit; + + ind := DupeString(' ', AIndent); + AppendToStream(AStream, Format( + ind + '' + LE, + [ legendStyleID ] + )); +end; + +procedure TsSpreadOpenDocWriter.WriteChartPlotArea(AStream: TStream; + AChart: TsChart; AIndent: Integer); +var + ind: String; + i: Integer; + plotAreaStyleID: Integer = 5; + wallStyleID: Integer = 22; // usually second to last of style list + floorstyleID: Integer = 23; // usually last of style list +begin + ind := DupeString(' ', AIndent); + AppendToStream(AStream, Format( + ind + '' + LE, + [ plotAreastyleID ] + )); + // ods has a table:cell-range-address here but it is reconstructed by Calc + + WriteChartAxis(AStream, AChart, true, true, AIndent + 2); // true = x axis true = primary + WriteChartAxis(AStream, AChart, false, true, AIndent + 2); // false = y axis true = primary + + for i := 0 to AChart.Series.Count-1 do + WriteChartSeries(AStream, AChart, i, AIndent + 2); + + AppendToStream(AStream, Format( + ind + ' ' + LE + + ind + ' ' + LE + + ind + '' + LE, + [ wallStyleID, floorStyleID ] + )); +end; + +procedure TsSpreadOpenDocWriter.WriteChartSeries(AStream: TStream; + AChart: TsChart; ASeriesIndex: Integer; AIndent: Integer); +var + ind: String; + sheet: TsWorksheet; + series: TsChartSeries; + valuesRange: String; + titleAddr: String; + count: Integer; + seriesStyleID: Integer; +begin + ind := DupeString(' ', AIndent); + + series := AChart.Series[ASeriesIndex]; + sheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(AChart.sheetIndex); + + valuesRange := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.YRange.Row1, series.YRange.Col1, + series.YRange.Row2, series.YRange.Col2, + rfAllRel, false + ); + titleAddr := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.TitleAddr.Row, series.TitleAddr.Col, + series.TitleAddr.Row, series.TitleAddr.Col, + rfAllRel, false); + count := series.YRange.Row2 - series.YRange.Row1 + 1; + + seriesStyleID := 14 + ASeriesIndex; + + AppendToStream(AStream, Format( + ind + '' + LE, + [ seriesStyleID, valuesRange, titleAddr, CHART_TYPE_NAMES[series.ChartType] ] + )); + AppendToStream(AStream, Format( + ind + ' ' + LE, + [ count ] + )); + AppendToStream(AStream, + ind + '' + LE + ); +end; + +{ To do: The list of styles must be updated to the real chart element settings. } +procedure TsSpreadOpenDocWriter.WriteChartStyles(AStream: TStream; + AChart: TsChart; AIndent: Integer); +var + ind: String; +begin + ind := DupeString(' ', AIndent); + + AppendToStream(AStream, + ind + '' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch1: style for element + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch2: style for element + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch3: style for ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch 4: style for element + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch5: style for element + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + + + { + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + } + + // ch6: style for first element, primary x axis + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch7: style for title at horizontal axes + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch8: style for major grid (all axes) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch9: style for minor grid (all axes) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch10: style for second element, secondary x axis + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch11: style for third element: primary y axis + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch12: style for vertical axis title (both y and y2 axis) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + + + // ch13: style for fourth element: secondary y axis + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch14: style for frist series - this is for a line series + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + + ind + ' ' + LE + + + (* + // ch14: style for first series - this is for a bar series + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + *) + + // ch15: style for second series - this is for a bar series + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch16: style for third series (bar series, here) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch17: style for 4th series (bar series, here) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch18: style for 5th series (bar series, here) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch19: style for 6th series (bar series, here) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch20: style for 7th series (bar series, here) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // ch21: style for 8th series (bar series, here) + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // next to last: style for wall + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + // last: last style for floor + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + + ind + '' + LE + ); +end; + +{ Extracts the cells needed by the given chart from the chart's worksheet and + copies their values into a temporary worksheet, AWorksheet, so that these + data can be written to the xml immediately. + Independently of the layout in the original worksheet, data are arranged in + columns of AWorksheet, starting at cell A1. + - First column: Categories (or index in case of scatter chart) + - Second column: + in case of category charts: y values of the first series, + in case of scatter series: x values of the first series + - Third column: + in case of category charts: y values of the second series + in case of scatter series. y values of the first series + - etc. + The first row contains + - nothing in case of the first column + - cell range reference in ODS syntax for the cells in the original worksheet. + The aux worksheet should be contained in a separate workbook to avoid + interfering with the writing process. + } +procedure TsSpreadOpenDocWriter.PrepareChartTable(AChart: TsChart; + AWorksheet: TsBasicWorksheet); +var + isScatterChart: Boolean; + series: TsChartSeries; + seriesSheet: TsWorksheet; + auxSheet: TsWorksheet; + refStr, txt: String; + i, j: Integer; + srcCell, destCell: PCell; + destCol: Cardinal; + r1, c1, r2, c2: Cardinal; + nRows: Integer; +begin + if AChart.Series.Count = 0 then + exit; + + auxSheet := TsWorksheet(AWorksheet); + seriesSheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(AChart.SheetIndex); + isScatterChart := AChart.IsScatterChart; + + // Determine the number of rows in auxiliary output worksheet. + nRows := 0; + for i := 0 to AChart.Series.Count-1 do + begin + series := AChart.Series[i]; + j := series.GetXCount; + if j > nRows then nRows := j; + j := series.GetYCount; + if j > nRows then nRows := j; + end; + + // Write label column. If missing, write consecutive numbers 1, 2, 3, ... + destCol := 0; + for i := 0 to AChart.Series.Count-1 do + begin + series := AChart.Series[i]; + + // Write the label column. Use consecutive numbers 1, 2, 3, ... if there + // are no labels. + if series.HasLabels then + begin + r1 := series.LabelRange.Row1; + c1 := series.LabelRange.Col1; + r2 := series.LabelRange.Row2; + c2 := series.LabelRange.Col2; + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); + end else + refStr := ''; + + auxSheet.WriteText(0, destCol, ''); + + for j := 1 to nRows do + begin + if series.HasLabels then + begin + if series.LabelsInCol then + srcCell := seriesSheet.FindCell(r1 + j - 1, c1) + else + srcCell := seriesSheet.FindCell(r1, c1 + j - 1); + end else + srcCell := nil; + if srcCell <> nil then + begin + destCell := auxsheet.GetCell(j, destCol); + seriesSheet.CopyValue(srcCell, destCell); + end else + destCell := auxSheet.WriteNumber(j, destCol, j); + end; + if (refStr <> '') then + auxsheet.WriteComment(1, destCol, refStr); + + // In case of scatter plot write the x column. Use consecutive numbers 1, 2, 3, ... + // if there are no x values. + if isScatterChart then + begin + inc(destCol); + + if series.HasXValues then + begin + r1 := series.XRange.Row1; + c1 := series.XRange.Col1; + r2 := series.XRange.Row2; + c2 := series.XRange.Col2; + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); + if series.XValuesInCol then + txt := 'Col ' + GetColString(c1) + else + txt := 'Row ' + GetRowString(r1); + end else + begin + refStr := ''; + txt := ''; + end; + + auxSheet.WriteText(0, destCol, txt); + + for j := 1 to nRows do + begin + if series.HasXValues then + begin + if series.XValuesInCol then + srcCell := seriesSheet.FindCell(r1 + j - 1, c1) + else + srcCell := seriesSheet.FindCell(r1, c1 + j - 1); + end else + srcCell := nil; + if srcCell <> nil then + begin + destCell := auxsheet.GetCell(j, destCol); + seriesSheet.CopyValue(srcCell, destCell); + end else + destCell := auxSheet.WriteNumber(j, destCol, j); + end; + if (refStr <> '') then + auxsheet.WriteComment(1, destCol, refStr); + end; + + // Write the y column + if not series.HasYValues then + Continue; + + inc(destCol); + + r1 := series.TitleAddr.Row; // Series title + c1 := series.TitleAddr.Col; + txt := seriesSheet.ReadAsText(r1, c1); + auxsheet.WriteText(0, destCol, txt); + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r1, c1, rfAllRel, false); + if (refStr <> '') then + auxSheet.WriteComment(0, destCol, refStr); // Store title reference as comment for svg node + + r1 := series.YRange.Row1; + c1 := series.YRange.Col1; + r2 := series.YRange.Row2; + c2 := series.YRange.Col2; + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); + for j := 1 to series.GetYCount do + begin + if series.YValuesInCol then + srcCell := seriesSheet.FindCell(r1 + j - 1, c1) + else + srcCell := seriesSheet.FindCell(r1, c1 + j - 1); + if srcCell <> nil then + begin + destCell := auxSheet.GetCell(j, destCol); + seriesSheet.CopyValue(srcCell, destCell); + end else + destCell := auxSheet.WriteNumber(j, destCol, j); + end; + + if (refStr <> '') then + auxSheet.WriteComment(1, destCol, refStr); // Store y range reference as comment for svg node + end; +end; + +{ Writes the chart's data table. NOTE: The chart gets its data from this table + rather than from the worksheet! } +procedure TsSpreadOpenDocWriter.WriteChartTable(AStream: TStream; AChart: TsChart; + AIndent: Integer); +var + auxBook: TsWorkbook; + auxSheet: TsWorksheet; + + procedure WriteAuxCell(AIndent: Integer; ACell: PCell); + var + ind: String; + valueType: String; + value: String; + officeValue: String; + draw: String; + begin + ind := DupeString(' ', AIndent); + if (ACell = nil) or (ACell^.ContentType = cctEmpty) then + begin + AppendToStream(AStream, + ind + '' + LE + + ind + ' ' + LE + + ind + '' + LE + ); + exit; + end; + + case ACell^.ContentType of + cctUTF8String: + begin + valueType := 'string'; + value := auxSheet.ReadAsText(ACell); + officeValue := ''; + end; + cctNumber, cctDateTime, cctBool: + begin + valueType := 'float'; + value := Format('%g', [auxSheet.ReadAsNumber(ACell)], FPointSeparatorSettings); + end; + cctError: + begin + valueType := 'float'; + value := 'NaN'; + end; + end; + + if ACell^.ContentType = cctUTF8String then + officeValue := '' + else + officeValue := ' office:value="' + value + '"'; + + if auxSheet.HasComment(ACell) then + begin + draw := auxSheet.ReadComment(ACell); + if draw <> '' then + draw := Format( + ind + ' ' + LE + + ind + ' %s' + LE + + ind + ' ' + LE, [draw]); + end else + draw := ''; + AppendToStream(AStream, Format( + ind + '' + LE + + ind + ' %s' + LE + + draw + + ind + '' + LE, + [ valueType, officevalue, value ] + )); + end; + +var + ind: String; + colCountStr: String; + n: Integer; + r, c: Cardinal; +begin + ind := DupeString(' ', AIndent); + n := AChart.Series.Count; + if n > 0 then + begin + if AChart.IsScatterChart then + n := n * 2; + colCountStr := Format('table:number-columns-repeated="%d"', [n]); + end else + colCountStr := ''; + + AppendToStream(AStream, Format( + ind + '' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE, [ colCountStr ] + )); + + auxBook := TsWorkbook.Create; + try + auxSheet := auxBook.AddWorksheet('chart'); + PrepareChartTable(AChart, auxSheet); + + // Header rows (containing the series names) + AppendToStream(AStream, + ind + ' ' + LE + + ind + ' ' + LE ); + for c := 0 to auxSheet.GetLastColIndex do + WriteAuxCell(AIndent + 6, auxSheet.FindCell(0, c)); + AppendToStream(AStream, + ind + ' ' + LE + + ind + ' ' + LE + ); + + // Write data rows + AppendToStream(AStream, + ind + ' ' + LE + ); + for r := 1 to auxSheet.GetLastRowIndex do + begin + AppendToStream(AStream, + ind + ' ' + LE + ); + for c := 0 to auxSheet.GetlastColIndex do + WriteAuxCell(AIndent + 6, auxSheet.FindCell(r, c)); + AppendToStream(AStream, + ind + ' ' + LE + ); + end; + AppendToStream(AStream, + ind + ' ' + LE + + ind + '' + LE + ); + + //auxBook.WriteToFile('table.ods', true); + finally + auxBook.Free; + end; +end; + +{ Writes the chart's title to the xml stream } +procedure TsSpreadOpenDocWriter.WriteChartTitle(AStream: TStream; AChart: TsChart; + IsSubTitle: Boolean; AIndent: Integer); +var + ind: String; + elementName: String; + titleStyleID: Integer; + cap: String; +begin + if (not AChart.Title.Visible) or (AChart.Title.Caption = '') then + exit; + + ind := DupeString(' ', AIndent); + if IsSubTitle then + begin + elementName := 'subtitle'; + titleStyleID := 3; + cap := AChart.SubTitle.Caption; + end else + begin + elementName := 'title'; + titleStyleID := 2; + cap := AChart.Title.Caption; + end; + AppendToStream(AStream, Format( +// ind + '' + LE + + ind + '' + LE + + ind + ' %s' + LE + + ind + '' + lE, + [ elementName, titleStyleID, cap, elementName ], FPointSeparatorSettings + )); +end; + { Writes the "office:automatic" > "style:style" node for table-column style family in content.xml for all column records. } procedure TsSpreadOpenDocWriter.WriteColStyles(AStream: TStream); @@ -7079,6 +7938,21 @@ begin end; end; +procedure TsSpreadOpenDocWriter.WriteGraphicStyles(AStream: TStream); +begin + if TsWorkbook(FWorkbook).GetChartCount = 0 then + exit; + + AppendToStream(AStream, + '' + + '' + + '' + + '' + ); +end; + procedure TsSpreadOpenDocWriter.WriteMasterStyles(AStream: TStream); var defFnt: TsHeaderFooterFont; @@ -7330,6 +8204,19 @@ begin end; +procedure TsSpreadOpenDocWriter.WriteParagraphStyles(AStream: TStream); +begin + if TsWorkbook(FWorkbook).GetChartCount = 0 then + exit; + + AppendToStream(AStream, + '' + + '' + + '' + + '' + ); +end; + function TsSpreadOpenDocWriter.WritePrintContentStyleXMLAsString( const AFormat: TsCellFormat): String; begin @@ -8905,22 +9792,94 @@ procedure TsSpreadOpenDocWriter.WriteShapes(AStream: TStream; } var + sheet: TsWorksheet absolute ASheet; i: Integer; + sheetIdx: Integer; + chart: TsChart; + series: TsChartSeries; img: TsImage; imgType: TsImageType; r1,c1,r2,c2: Cardinal; roffs1,coffs1, roffs2, coffs2: Double; x, y, w, h: Double; + xRngAddr, yRngAddr, titleAddr: String; xml: String; target, bookmark: String; u: TURI; begin - if (ASheet as TsWorksheet).GetImageCount = 0 then + if (sheet.GetImageCount = 0) and (sheet.GetChartCount = 0) then exit; AppendToStream(AStream, ''); + sheetIdx := sheet.Index; + for i:=0 to TsWorkbook(FWorkbook).GetChartCount-1 do + begin + chart := TsWorkbook(FWorkbook).GetChartByIndex(i); + if chart.SheetIndex <> sheetIdx then + Continue; + if chart.Series.Count = 0 then + Continue; + + r1 := chart.Row; + c1 := chart.Col; + rOffs1 := chart.OffsetX; + cOffs1 := chart.OffsetY; + w := chart.Width; + h := chart.Height; + sheet.CalcDrawingExtent(true, w, h, r1, c1, r2, c2, rOffs1, cOffs1, rOffs2, cOffs2, x, y); + + series := chart.Series[0]; + if (series.XRange.Row1 <> series.XRange.Row2) or (series.XRange.Col1 <> series.XRange.Col2) then + xRngAddr := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.XRange.Row1, series.XRange.Col1, + series.XRange.Row2, series.XRange.Col2, + rfAllRel, false + ) + else + xRngAddr := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.LabelRange.Row1, series.LabelRange.Col1, + series.LabelRange.Row2, series.LabelRange.Col2, + rfAllRel, false + ); + yRngAddr := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.YRange.Row1, series.YRange.Col1, + series.YRange.Row2, series.YRange.Col2, + rfAllRel, false + ); + titleAddr := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.TitleAddr.row, series.TitleAddr.Col, + series.TitleAddr.row, series.TitleAddr.Col, + rfAllRel, false + ); + + xml := Format( + '' + + ' ' + + '' + + '' + + '', [ + i+1, + w, h, + x, y, + xRngAddr + ' ' + titleAddr + ' ' + yrngAddr, + chart.Index+1 + ], FPointSeparatorSettings); + + AppendToStream(AStream, xml); + end; + for i:=0 to (ASheet as TsWorksheet).GetImageCount-1 do begin img := (ASheet as TsWorksheet).GetImage(i); diff --git a/components/fpspreadsheet/source/common/fpspreadsheet.pas b/components/fpspreadsheet/source/common/fpspreadsheet.pas index 3b2a243a7..3ab2e4828 100644 --- a/components/fpspreadsheet/source/common/fpspreadsheet.pas +++ b/components/fpspreadsheet/source/common/fpspreadsheet.pas @@ -24,7 +24,7 @@ uses {$endif}{$endif}{$endif} Classes, SysUtils, fpimage, avglvltree, lconvencoding, fpsTypes, fpsExprParser, fpsClasses, fpsNumFormat, fpsPageLayout, - fpsImages, fpsConditionalFormat; + fpsImages, fpsConditionalFormat, fpsChart; type { Forward declarations } @@ -625,6 +625,15 @@ type procedure AddHyperlinkToImage(AImageIndex: Integer; ATarget: String; AToolTip: String = ''); + procedure CalcDrawingExtent( + UsePixels: Boolean; AWidth, AHeight: Double; + var ARow1, ACol1: Cardinal; out ARow2, ACol2: Cardinal; + ARowOffs1, AColOffs1: Double; out ARowOffs2, AColOffs2: Double; + out x,y: Double); + + { Chart support } + function GetChartCount: Integer; + { Protection } procedure Protect(AEnable: Boolean); @@ -771,6 +780,7 @@ type FCellFormatList: TsCellFormatList; FConditionalFormatList: TsConditionalFormatList; FEmbeddedObjList: TFPList; + FCharts: TsChartList; { Internal methods } class procedure GetFormatFromFileHeader(const AFileName: TFileName; @@ -911,6 +921,12 @@ type function HasEmbeddedSheetImages: Boolean; procedure RemoveAllEmbeddedObj; + { Charts } + function AddChart(ASheet: TsBasicWorksheet; ARow, ACol: Cardinal; + AWidth, AHeight: Double; AOffsetX: Double = 0.0; AOffsetY: Double = 0.0): TsChart; + function GetChartByIndex(AIndex: Integer): TsChart; + function GetChartCount: Integer; + { Utilities } function ConvertUnits(AValue: Double; AFromUnits, AToUnits: TsSizeUnits): Double; procedure UpdateCaches; @@ -1412,6 +1428,147 @@ begin // to others sheets. If not call the faster "CalcSheet". end; +{@@ ---------------------------------------------------------------------------- + Calculates the extent of an image or chart (in Excel-speak: a "drawing") + + @param UsePixels If @TRUE then pixels are used for calculation - this improves the display of images in Excel + @param AWidth Width of the drawing + @param AHeight Height of the drawing + @param ARow1 Index of the row containing the top edge of the drawing + @param ACol1 Index of the column containing the left edege of the drawing + @param ARow2 Index of the row containing the right edge of the drawing + @param ACol2 Index of the column containing the bottom edge of the drawing + @param ARowOffs1 Distance between the top edge of drawing and row 1 + @param AColOffs1 Distance between the left edge of drawing and column 1 + @param ARowOffs2 Distance between the bottom edge of drawing and top of row 2 + @param AColOffs2 Distance between the right edge of drawing and left of col 2 + @param x Absolute coordinate of left edge of the drawing + @param y Absolute coordinate of top edge of the drawing + + All dimensions are in workbook units +-------------------------------------------------------------------------------} +procedure TsWorksheet.CalcDrawingExtent( + UsePixels: Boolean; AWidth, AHeight: Double; + var ARow1, ACol1: Cardinal; out ARow2, ACol2: Cardinal; + ARowOffs1, AColOffs1: Double; out ARowOffs2, AColOffs2: Double; + out x,y: Double); +var + colW, rowH: Double; + totW, totH: Double; + r, c: Integer; + w_px, h_px: Integer; + totH_px, rowH_px: Integer; + totW_px, colW_px: Integer; + ppi: Integer; + u: TsSizeUnits; +begin + // Abbreviations + ppi := ScreenPixelsPerInch; + u := FWorkbook.Units; + + // Find x coordinate of left graphic edge, in workbook units + x := AColOffs1; + for c := 0 to ACol1-1 do + begin + colW := GetColWidth(c, u); + x := x + colW; + end; + // Find y coordinate of top image edge, in workbook units. + y := ARowOffs1; + for r := 0 to ARow1 - 1 do + begin + rowH := CalcRowHeight(r); + y := y + rowH; + end; + + if UsePixels then + // Use pixels for calculation. Better for Excel, maybe due to rounding error? + begin + // If we don't know the ppi of the screen the calculation is not exact! + w_px := ptsToPx(FWorkbook.ConvertUnits(AWidth, u, suPoints), ppi); + h_px := ptsToPx(FWorkbook.ConvertUnits(AHeight, u, suPoints), ppi); + // Find column with right image edge. Find horizontal within-cell-offsets + totW_px := -ptsToPx(FWorkbook.ConvertUnits(AColOffs1, u, suPoints), ppi); + ACol2 := ACol1; + while (totW_px < w_px) do + begin + colW := GetColWidth(ACol2, u); + colW_px := ptsToPx(FWorkbook.ConvertUnits(colW, u, suPoints), ppi); + totW_px := totW_px + colW_px; + if totW_px > w_px then + begin + AColOffs2 := FWorkbook.ConvertUnits(pxToPts(colW_px - (totW_px - w_px), ppi), suPoints, u); + break; + end; + inc(ACol2); + end; + // Find row with bottom image edge. Find vertical within-cell-offset. + totH_px := -ptsToPx(FWorkbook.ConvertUnits(ARowOffs1, u, suPoints), ppi); + ARow2 := ARow1; + while (totH_px < h_px) do + begin + rowH := CalcRowHeight(ARow2); + rowH_px := ptsToPx(FWorkbook.ConvertUnits(rowH, u, suPoints), ppi); + totH_px := totH_px + rowH_px; + if totH_px > h_px then + begin + ARowOffs2 := FWorkbook.ConvertUnits(pxToPts(rowH_px - (totH_px - h_px), ppi), suPoints, u); + break; + end; + inc(ARow2); + end; + end + else // Use workbook units for calculation + begin + // Find row with bottom image edge. Find horizontal within-cell offset + totW := -AColOffs1; + ACol2 := ACol1; + while (totW < AWidth) do + begin + colW := GetColWidth(ACol2, u); + totW := totW + colW; + if totW >= AWidth then + begin + AColOffs2 := colW - (totW - AWidth); + break; + end; + inc(ACol2); + end; + // Find row with right image edge. Find vertical within-cell-offsets + totH := -ARowOffs1; + ARow2 := ARow1; + while (totH < AHeight) do + begin + rowH := CalcRowHeight(ARow2); + totH := totH + rowH; + if totH >= AHeight then + begin + ARowOffs2 := rowH - (totH - AHeight); + break; + end; + inc(ARow2); + end; + end; +end; + +{@@ ---------------------------------------------------------------------------- + Determines the count of charts on this worksheet +-------------------------------------------------------------------------------} +function TsWorksheet.GetChartCount: Integer; +var + i: Integer; + chart: TsChart; + idx: Integer; +begin + Result := 0; + idx := GetIndex; + for i := 0 to Workbook.GetChartCount-1 do + begin + chart := Workbook.GetChartByIndex(i); + if chart.SheetIndex = idx then inc(Result); + end; +end; + {@@ ---------------------------------------------------------------------------- Calculates all formulas of the worksheet @@ -2318,7 +2475,7 @@ begin WriteFormula(AToRow, AToCol, formulaStr); // Fix formula references to the source cell (ACell) - for i := 0 to FWorkbook.GetWorksheetcount-1 do begin + for i := 0 to FWorkbook.GetWorksheetCount-1 do begin sheet := FWorkbook.GetWorksheetByIndex(i); sheet.Formulas.FixReferenceToMovedCell(ACell, AToRow, AToCol, self); end; @@ -6383,6 +6540,7 @@ begin FCellFormatList := TsCellFormatList.Create(false); FConditionalFormatList := TsConditionalFormatList.Create; FEmbeddedObjList := TFPList.Create; + FCharts := TsChartList.Create; // Add default cell format InitFormatRecord(fmt); @@ -6415,6 +6573,7 @@ begin RemoveAllEmbeddedObj; FEmbeddedObjList.Free; + FCharts.Free; inherited Destroy; end; @@ -7627,6 +7786,7 @@ end; {$include fpspreadsheet_hyperlinks.inc} // hyperlinks {$include fpspreadsheet_embobj.inc} // embedded objects {$include fpspreadsheet_clipbrd.inc} // clipboard access +{$include fpspreadsheet_chart.inc} // chart support end. {** End Unit: fpspreadsheet } diff --git a/components/fpspreadsheet/source/common/fpspreadsheet_embobj.inc b/components/fpspreadsheet/source/common/fpspreadsheet_embobj.inc index 700e65ae1..e81334d34 100644 --- a/components/fpspreadsheet/source/common/fpspreadsheet_embobj.inc +++ b/components/fpspreadsheet/source/common/fpspreadsheet_embobj.inc @@ -72,6 +72,28 @@ end; All dimensions are in workbook units -------------------------------------------------------------------------------} +procedure TsWorksheet.CalcImageExtent(AIndex: Integer; UsePixels: Boolean; + out ARow1, ACol1, ARow2, ACol2: Cardinal; + out ARowOffs1, AColOffs1, ARowOffs2, AColOffs2: Double; + out x,y, AWidth, AHeight: Double); +var + img: TsImage; + obj: TsEmbeddedObj; +begin + img := GetImage(AIndex); + ARow1 := img.Row; + ACol1 := img.Col; + ARowOffs1 := img.OffsetX; // in workbook units + AColOffs1 := img.OffsetY; // in workbook units + + obj := FWorkbook.GetEmbeddedObj(img.Index); + AWidth := obj.ImageWidth * img.ScaleX; // in workbook units + AHeight := obj.ImageHeight * img.ScaleY; // in workbook units + + CalcDrawingExtent(UsePixels, AWidth, AHeight, ARow1, ACol1, ARow2, ACol2, + ARowOffs1, AColOffs1, ARowOffs2, AColOffs2, x, y); +end; +(* procedure TsWorksheet.CalcImageExtent(AIndex: Integer; UsePixels: Boolean; out ARow1, ACol1, ARow2, ACol2: Cardinal; out ARowOffs1, AColOffs1, ARowOffs2, AColOffs2: Double; @@ -172,7 +194,7 @@ begin end; end; end; - +*) {@@ ---------------------------------------------------------------------------- Returns the parameters of the image stored in the internal image list at diff --git a/components/fpspreadsheet/source/common/fpsutils.pas b/components/fpspreadsheet/source/common/fpsutils.pas index e69ab134a..176103d13 100644 --- a/components/fpspreadsheet/source/common/fpsutils.pas +++ b/components/fpspreadsheet/source/common/fpsutils.pas @@ -127,11 +127,18 @@ function TryStrToCellRange_ODS(const AStr: String; out ASheet1, ASheet2: String; out ARow1, ACol1, ARow2, ACol2: Cardinal; out AFlags: TsRelFlags): Boolean; function GetCellRangeString_ODS(ASheet1, ASheet2: String; ARow1, ACol1, ARow2, ACol2: Cardinal; - AFlags: TsRelFlags = rfAllRel): String; overload; + AFlags: TsRelFlags = rfAllRel; WithBrackets: Boolean = true): String; overload; function GetCellRangeString_ODS(ARow1, ACol1, ARow2, ACol2: Cardinal; - AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false): String; overload; + AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false; + WithBrackets: Boolean = true): String; overload; function GetCellRangeString_ODS(ARange: TsCellRange; - AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false): String; overload; + AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false; + WithBrackets: Boolean = true): String; overload; +function GetSheetCellString_ODS(ASheet: String; ARow, ACol: Cardinal; + AFlags: TsRelFlags = rfAllRel; WithBrackets: Boolean = true): String; +function GetSheetCellRangeString_ODS(ASheet1, ASheet2: String; + ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags = rfAllRel; + WithBrackets: Boolean = true): String; // Error strings @@ -1386,7 +1393,8 @@ end; Calculates a cell range string with sheet specification in OpenDocument syntax -------------------------------------------------------------------------------} function GetCellRangeString_ODS(ASheet1, ASheet2: String; - ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags = rfAllRel): String; + ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags = rfAllRel; + WithBrackets: Boolean = true): String; var s1, s2: String; begin @@ -1402,37 +1410,65 @@ begin if (ASheet1 = '') and (ASheet2 = '') then begin if s1 = s2 then - Result := '[.' + s1 + ']' // --> [.A1] + Result := '.' + s1 // --> [.A1] else - Result := Format('[.%s:.%s]', [s1, s2]) // --> [.A1:.B3] + Result := Format('.%s:.%s', [s1, s2]) // --> [.A1:.B3] end else if (ASheet2 = '') or (ASheet1 = ASheet2) then begin if s1 = s2 then - Result := Format('[%s.%s]', [ASheet1, s1]) // [Sheet1.A1] + Result := Format('%s.%s', [ASheet1, s1]) // [Sheet1.A1] else - Result := Format('[%s.%s:.%s]', [ASheet1, s1, s2]); // [Sheet1.A1:.B2] + Result := Format('%s.%s:.%s', [ASheet1, s1, s2]); // [Sheet1.A1:.B2] end else - Result := Format('[%s.%s:%s.%s]', [ASheet1, s1, ASheet2, s2]); // [Sheet.A1:Sheet2.B2] + Result := Format('%s.%s:%s.%s', [ASheet1, s1, ASheet2, s2]); // [Sheet.A1:Sheet2.B2] + + if WithBrackets then + Result := '[' + Result + ']'; end; function GetCellRangeString_ODS(ARow1, ACol1, ARow2, ACol2: Cardinal; - AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false): String; + AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false; WithBrackets: Boolean = true): String; begin if Compact and (ARow1 = ARow2) and (ACol1 = ACol2) then - Result := Format('[.%s]', [GetCellString(ARow1, ACol1, AFlags)]) + Result := Format('.%s', [GetCellString(ARow1, ACol1, AFlags)]) else - Result := Format('[.%s%s%s%d:.%s%s%s%d]', [ + Result := Format('.%s%s%s%d:.%s%s%s%d', [ RELCHAR[rfRelCol in AFlags], GetColString(ACol1), RELCHAR[rfRelRow in AFlags], ARow1 + 1, RELCHAR[rfRelCol2 in AFlags], GetColString(ACol2), RELCHAR[rfRelRow2 in AFlags], ARow2 + 1 ]); + if WithBrackets then + Result := '[' + Result + ']'; end; function GetCellRangeString_ODS(ARange: TsCellRange; - AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false): String; + AFlags: TsRelFlags = rfAllRel; Compact: Boolean = false; + WithBrackets: Boolean = true): String; begin - Result := GetCellRangeString_ODS(ARange, AFlags, Compact); + Result := GetCellRangeString_ODS(ARange, AFlags, Compact, WithBrackets); +end; + +function GetSheetCellString_ODS(ASheet: String; ARow, ACol: Cardinal; + AFlags: TsRelFlags = rfAllRel; WithBrackets: Boolean = true): String; +begin + Result := Format('%s.%s%s%s%d', [ + ASheet, RELCHAR[rfRelCol in AFlags], GetColString(ACol), RELCHAR[rfRelRow in AFlags], ARow + 1 + ]); + if WithBrackets then + Result := '[' + Result + ']'; +end; + +function GetSheetCellRangeString_ODS(ASheet1, ASheet2: String; + ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags = rfAllRel; + WithBrackets: Boolean = true): String; +begin + Result := Format('%s.%s%s%s%d:%s.%s%s%s%d', [ + ASheet1, RELCHAR[rfRelCol in AFlags], GetColString(ACol1), RELCHAR[rfRelRow in AFlags], ARow1 + 1, + ASheet2, RELCHAR[rfRelCol2 in AFlags], GetColString(ACol2), RELCHAR[rfRelRow2 in AFlags], ARow2 + 1 + ]); + if WithBrackets then + Result := '[' + Result + ']'; end; diff --git a/components/fpspreadsheet/source/common/xlsxooxml.pas b/components/fpspreadsheet/source/common/xlsxooxml.pas index 46b07054b..e98decf89 100644 --- a/components/fpspreadsheet/source/common/xlsxooxml.pas +++ b/components/fpspreadsheet/source/common/xlsxooxml.pas @@ -184,6 +184,11 @@ type procedure WriteCFColorRangeRule(AStream: TStream; ARule: TsCFColorRangeRule; APriority: Integer); procedure WriteCFDataBarRule(AStream: TStream; ARule: TsCFDatabarRule; APriority: Integer); procedure WriteCFIconSetRule(AStream: TStream; ARule: TsCFIconSetRule; APriority: Integer); + procedure WriteChart(AStream: TStream; AChartIndex: Integer); + procedure WriteChartColors; + procedure WriteChartRels; + procedure WriteCharts; + procedure WriteChartStyles; procedure WriteColBreaks(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteCols(AStream: TStream; AWorksheet: TsBasicWorksheet); procedure WriteComments(AWorksheet: TsBasicWorksheet); @@ -241,6 +246,10 @@ type FSMedia: array of TStream; FSSheets: array of TStream; FSSheetRels: array of TStream; + FSCharts: array of TStream; + FSChartRels: array of TStream; + FSChartStyles: array of TStream; + FSChartColors: array of TStream; FSComments: array of TStream; FSDrawings: array of TStream; FSDrawingsRels: array of TStream; @@ -297,7 +306,7 @@ implementation uses variants, strutils, dateutils, math, lazutf8, LazFileUtils, uriparser, typinfo, {%H-}fpsPatches, fpSpreadsheet, fpsCrypto, fpsExprParser, - fpsStrings, fpsStreams, fpsClasses, fpsImages; + fpsStrings, fpsStreams, fpsClasses, fpsImages, fpsChart; const { OOXML general XML constants } @@ -319,6 +328,8 @@ const OOXML_PATH_XL_STRINGS = 'xl/sharedStrings.xml'; OOXML_PATH_XL_WORKSHEETS = 'xl/worksheets/'; OOXML_PATH_XL_WORKSHEETS_RELS = 'xl/worksheets/_rels/'; + OOXML_PATH_XL_CHARTS = 'xl/charts/'; + OOXML_PATH_XL_CHARTS_RELS = 'xl/charts/_rels/'; OOXML_PATH_XL_DRAWINGS = 'xl/drawings/'; OOXML_PATH_XL_DRAWINGS_RELS = 'xl/drawings/_rels/'; OOXML_PATH_XL_THEME = 'xl/theme/theme1.xml'; @@ -341,8 +352,12 @@ const SCHEMAS_VMLDRAWING = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing'; SCHEMAS_HYPERLINK = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink'; SCHEMAS_IMAGE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'; + SCHEMAS_CHART = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart'; SCHEMAS_SPREADML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; SCHEMAS_CORE = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'; + SCHEMAS_DRAWINGML_CHART= 'http://schemas.openxmlformats.org/drawingml/2006/chart'; + SCHEMAS_CHART_COLORS = 'http://schemas.microsoft.com/office/2011/relationships/chartColorStyle'; + SCHEMAS_CHART_STYLE = 'http://schemas.microsoft.com/office/2011/relationships/chartStyle'; { OOXML mime types constants } MIME_XML = 'application/xml'; @@ -357,12 +372,15 @@ const MIME_STRINGS = MIME_SPREADML + '.sharedStrings+xml'; MIME_COMMENTS = MIME_SPREADML + '.comments+xml'; MIME_DRAWING = MIME_OFFICEDOCUMENT + '.drawing+xml'; // 'application/vnd.openxmlformats-officedocument.drawing+xml + MIME_DRAWINGML_CHART = MIME_OFFICEDOCUMENT + '.drawingml.chart+xml'; MIME_VMLDRAWING = MIME_OFFICEDOCUMENT + '.vmlDrawing'; LAST_PALETTE_INDEX = 63; CFB_SIGNATURE = $E11AB1A1E011CFD0; // Compound File Binary Signature + LE = LineEnding; + type TFillListData = class PatternType: String; @@ -4689,8 +4707,10 @@ procedure TsSpreadOOXMLWriter.Get_rId(AWorksheet: TsBasicWorksheet; var next_rId: Integer; sheet: TsWorksheet; + book: TsWorkbook; begin sheet := AWorksheet as TsWorksheet; + book := sheet.Workbook; AComment_rId := -1; AFirstHyperlink_rId := -1; @@ -4705,8 +4725,8 @@ begin inc(next_rId, 2); // there are two .rels entries in case of comments end; - // Embedded images next - if sheet.GetImageCount > 0 then + // Charts or embedded images next + if (sheet.GetImageCount > 0) or (book.GetChartCount > 0) then begin ADrawing_rId := next_rId; inc(next_rId); @@ -5126,6 +5146,762 @@ begin end; +procedure TsSpreadOOXMLWriter.WriteChart(AStream: TStream; + AChartIndex: Integer); + + function GetChartAxisXML(Indent: Integer; AChart: TsChart; + AxisID, OtherAxisID: Integer; NodeName, AxPos: String): String; + var + ind: String; + begin + ind := DupeString(' ', Indent); + Result := Format( + ind + '<%s>' + LE + // 1 + ind + ' ' + LE + // 2 + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + // 3 + ind + ' ' + LE + + IfThen(AxPos='l', ind + ' ' + LE, '') + + ind + ' ' + LE + // 4 + ind + ' ' + LE + + IfThen(AxPos='l', ind + ' ' + LE, '') + + IfThen(AxPos='b', ind + ' ' + LE, '') + + IfThen(AxPos='b', ind + ' ' + LE, '') + + IfThen(AxPos='b', ind + ' ' + LE, '') + + ind + '', [ // 5 + NodeName, // 1 + AxisID, // 2 + AxPos, // 3 + OtherAxisID, // 4 + NodeName // 5 + ]); + end; + + function GetBarChartXML(Indent: Integer; AChart: TsChart; CatAxID, ValAxID: Integer): String; + var + ind: String; + begin + ind := DupeString(' ', Indent); + Result := Format( + ind + '' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + // categories axis (x) + ind + ' ' + LE + // values axis (y) + ind + '', [ + CatAxID, + ValAxID + ]); + end; + + function GetLegendXML(Indent: Integer; AChart: TsChart): string; + var + ind: String; + begin + ind := DupeString(' ', Indent); + Result := + ind + '' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ''; + end; + +var + chart: TsChart; + xAxID, yAxID: Integer; +begin + chart := TsWorkbook(FWorkbook).GetChartByIndex(AChartIndex); + AppendToStream(AStream, + XML_HEADER + LE); + + AppendToStream(AStream, + '' + LE +// ' xmlns:c16r2="http://schemas.microsoft.com/office/drawing/2015/06/chart">' + LE + ); + + xAxID := Random(MaxInt); + yAxID := Random(MaxInt); + + AppendToStream(AStream, + ' ' + LE + // to do: get correct value + ' ' + LE + + // ' ' + LE + + // ' ' + LE + + ' ' + LE + + ' ' + LE + + GetBarChartXML(6, chart, xAxID, yAxID) + LE + // to be replaced by real series + GetChartAxisXML(6, chart, xAxID, yAxID, 'c:catAx', 'b') + LE + + GetChartAxisXML(6, chart, yAxID, xAxID, 'c:valAx', 'l') + LE + + ' ' + LE + + GetLegendXML(4, chart) + LE + + ' ' + LE + + ' ' + LE + ); + + AppendToStream(AStream, + '' + LE + ); +end; + +procedure TsSpreadOOXMLWriter.WriteChartColors; +var + i, n: Integer; +begin + n := TsWorkbook(FWorkbook).GetChartCount; + SetLength(FSChartColors, n); + + for i := 0 to n - 1 do + begin + FSChartColors[i] := CreateTempStream(FWorkbook, Format('fpsChCol%d', [i])); + + AppendToStream(FSChartColors[i], + XML_Header); + + AppendToStream(FSChartColors[i], + '' + LE + + '' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' '+ LE + + ' ' + LE + + ' '+ LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' '+ LE + + '' + LE + ); + end; +end; + +{ Write the relationship file for all workbook's chart. The file defines which + xml files contain the ChartStyles and Colors needed by each chart. } +procedure TsSpreadOOXMLWriter.WriteChartRels; +var + i, n: Integer; +begin + n := TsWorkbook(FWorkbook).GetChartCount; + SetLength(FSChartRels, n); + + for i := 0 to n-1 do + begin + FSChartRels[i] := CreateTempStream(FWorkbook, Format('fpsChRels%d', [i])); + AppendToStream(FSChartRels[i], + XML_HEADER); + AppendToStream(FSChartRels[i], Format( + '' + LE + + ' ' + LE + + ' ' + LE + + '' + LE, [ + SCHEMAS_RELS, + i+1, SCHEMAS_CHART_STYLE, + i+1, SCHEMAS_CHART_COLORS + ])); + end; +end; + +procedure TsSpreadOOXMLWriter.WriteCharts; +var + i, n: Integer; + chart: TsChart; +begin + n := TsWorkbook(FWorkbook).GetChartCount; + SetLength(FSCharts, n); + + for i := 0 to n - 1 do + begin + FSCharts[i] := CreateTempStream(FWorkbook, Format('fpsCh%d', [i])); + WriteChart(FSCharts[i], i); + end; +end; + +procedure TsSpreadOOXMLWriter.WriteChartStyles; +var + i, n: Integer; +begin + n := TsWorkbook(FWorkbook).GetChartCount; + SetLength(FSChartStyles, n); + + for i := 0 to n - 1 do + begin + FSChartStyles[i] := CreateTempStream(FWorkbook, Format('fpsChSty%d', [i])); + + AppendToStream(FSChartStyles[i], + XML_Header); + + AppendToStream(FSChartStyles[i], + '' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' '+ LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE+ + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + lE+ + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + lE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE+ + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE+ + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE+ + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE+ + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE+ + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + + '' + LE + ); + end; +end; + procedure TsSpreadOOXMLWriter.WriteColBreaks(AStream: TStream; AWorksheet: TsBasicWorksheet); var @@ -6271,41 +7047,47 @@ begin end; procedure TsSpreadOOXMLWriter.WriteDrawings(AWorksheet: TsBasicWorksheet); -var - i: Integer; - img: TsImage; - r1, c1, r2, c2: Cardinal; - roffs1, coffs1, roffs2, coffs2: Double; - x, y, w, h: Double; - descr: String; - hlink: String; - xdr_cNvPr: String; - rId: Integer; - book: TsWorkbook; - sheet: TsWorksheet absolute AWorksheet; -begin - if sheet.GetImageCount= 0 then - exit; - book := FWorkbook as TsWorkbook; - - SetLength(FSDrawings, FCurSheetNum + 1); - FSDrawings[FCurSheetNum] := CreateTempStream(FWorkbook, Format('fpsD%d', [FCurSheetNum])); - - // Header - AppendToStream(FSDrawings[FCurSheetNum], - XML_HEADER, - ''); - - // Repeat for each image - rId := 1; - for i:=0 to sheet.GetImageCount - 1 do + function AnchorAsXML(Indent: Integer; AName: String; ACol, ARow: Integer; + AColOffset, ARowOffset: Double): String; + var + ind: String; begin - img := sheet.GetImage(i); + ind := DupeString(' ', Indent); + Result := Format( + ind + '<%s>' + LE + + ind + ' %d' + LE + + ind + ' %d' + LE + + ind + ' %d' + LE + + ind + ' %d' + LE + + ind + '', [ + AName, + ACol, mmToEMU(AColOffset), + ARow, mmToEMU(ARowOffset), + AName + ]); + end; + + procedure DoWriteImage(AStream: TStream; AIndex: Integer; var RelID: Integer); + var + img: TsImage; + book: TsWorkbook; + sheet: TsWorksheet; + r1, c1, r2, c2: Cardinal; + roffs1, coffs1, roffs2, coffs2: Double; + x, y, w, h: Double; + descr: String; + hlink: String; + xdr_cNvPr: String; + begin + book := FWorkbook as TsWorkbook; + sheet := TsWorksheet(AWorksheet); + + img := sheet.GetImage(AIndex); if book.GetEmbeddedObj(img.Index).ImageType = itUnknown then - Continue; - sheet.CalcImageExtent(i, true, + exit; + + sheet.CalcImageExtent(AIndex, true, r1, c1, r2, c2, roffs1, coffs1, roffs2, coffs2, // mm x, y, w, h); // mm; @@ -6315,96 +7097,184 @@ begin // This part defines the relationship to the graphic and, if available, to // a hyperlink. - xdr_cNvPr := Format('id="%d" name="Graphic %d" descr="%s"', [i+3,i+2, descr]); - if img.HyperlinkTarget <> '' then begin + xdr_cNvPr := Format('id="%d" name="Graphic %d" descr="%s"', [AIndex+3, AIndex+2, descr]); + if img.HyperlinkTarget <> '' then + begin hlink := Format(' '' then hlink := hlink + Format('tooltip="%s" ', [img.HyperlinkToolTip]); hlink := hlink + '/>'; - xdr_cNvPr := '' + - hlink + - ''; + xdr_cNvPr := ' ' + LE + + ' ' + hlink + LE + + ' ' + LE; end else - xdr_cNvPr := ''; + xdr_cNvPr := ' '; - AppendToStream(FSDrawings[FCurSheetNum], - ''); - AppendToStream(FSDrawings[FCurSheetNum], Format( - ''+ - '%d' + - '%d'+ - '%d'+ - '%d'+ - '', [ - c1, mmToEMU(coffs1), - r1, mmToEMU(roffs1) - ])); - AppendToStream(FSDrawings[FCurSheetNum], Format( - ''+ - '%d'+ - '%d'+ - '%d'+ - '%d'+ - '', [ - c2, mmToEMU(coffs2), - r2, mmToEMU(roffs2) - ])); - AppendToStream(FSDrawings[FCurSheetNum], Format( - ''+ - ''+ - xdr_cNvPr + - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '' + - ''+ - '' + - '' + // size in EMU - ''+ - ''+ - ''+ - ''+ - ''+ - '' + - '', [ + AppendToStream(AStream, + ' ' + LE); + AppendToStream(AStream, + AnchorAsXML(4, 'xdr:from', c1, r1, coffs1, roffs1) + LE + ); + AppendToStream(AStream, + AnchorAsXML(4, 'xdr:to', c2, r2, coffs2, roffs2) + LE + ); + AppendToStream(AStream, Format( + ' ' + LE + + ' ' + LE + + xdr_cNvPr + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + // size in EMU + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE, [ // i + 3, i+2, descr, - SCHEMAS_DOC_RELS, rId, + SCHEMAS_DOC_RELS, RelID, mmToEMU(x), mmToEMU(y), mmToEMU(w), mmToEMU(h) ])); - AppendToStream(FSDrawings[FCurSheetNum], - ''); + AppendToStream(AStream, + ' ' + LE); + end; + + procedure DoWriteChart(AStream: TStream; AChart: TsChart; + AChartNoInSheet: Integer; var RelID: Integer); + var + r1, c1, r2, c2: Cardinal; + roffs1, coffs1, roffs2, coffs2: Double; + x, y, w, h: Double; + sheet: TsWorksheet; + begin + r1 := AChart.Row; + c1 := AChart.Col; + rOffs1 := AChart.OffsetX; + cOffs1 := AChart.OffsetY; + w := AChart.Width; + h := AChart.Height; + sheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(AChart.SheetIndex); + sheet.CalcDrawingExtent(true, w, h, r1, c1, r2, c2, rOffs1, cOffs1, rOffs2, cOffs2, x, y); + + AppendToStream(AStream, + ' ' + LE); + + AppendToStream(AStream, + AnchorAsXML(4, 'xdr:from', c1, r1, coffs1, roffs1) + LE + ); + AppendToStream(AStream, + AnchorAsXML(4, 'xdr:to', c2, r2, coffs2, roffs2) + LE + ); + + AppendToStream(AStream, Format( + ' ' + LE + + ' ' + LE + + ' ' + LE + // line 1 + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE + // line 2 + ' '+ LE + // line 3 + ' ' + LE + + ' ' + LE + + ' ' + LE + + ' ' + LE, + [ + AChartNoInSheet + 1, AChartNoInSheet, // --> line 1 + SCHEMAS_DRAWINGML_CHART, // --> line 2 + SCHEMAS_DRAWINGML_CHART, SCHEMAS_DOC_RELS, RelID // --> line 3 + ])); + + AppendToStream(AStream, + ' ' + LE); + end; + +var + i, j: Integer; + rId: Integer; + sheet: TsWorksheet absolute AWorksheet; + sheetIdx: Integer; + chart: TsChart; +begin + if (sheet.GetImageCount = 0) and (sheet.GetChartCount = 0) then + exit; + + SetLength(FSDrawings, FCurSheetNum + 1); + FSDrawings[FCurSheetNum] := CreateTempStream(FWorkbook, Format('fpsD%d', [FCurSheetNum])); + + // Header + AppendToStream(FSDrawings[FCurSheetNum], + XML_HEADER + LE + + '' + LE + ); + + // Repeat for each image + rId := 1; + for i:=0 to sheet.GetImageCount - 1 do + begin + DoWriteImage(FSDrawings[FCurSheetNum], i, rId); inc(rId, 1); end; + + // Repeat for each chart + sheetIdx := sheet.Index; + j := 1; // Counts the charts in the current sheet + for i := 0 to TsWorkbook(FWorkbook).GetChartCount - 1 do + begin + chart := TsWorkbook(FWorkbook).GetChartByIndex(i); + if chart.SheetIndex = sheetIdx then + begin + DoWriteChart(FSDrawings[FCurSheetNum], chart, j, rId); + inc(j); + inc(rId); + end; + end; + + // Close node AppendToStream(FSDrawings[FCurSheetNum], ''); end; // For each sheet, writes a "drawingX.xml.rels" file to -// folder "../drawings/_rels". X matches the (1-base) sheet index. +// folder "../drawings/_rels". +// X is a sequential number (starting 1), not neccessarily identical with the +// sheet index. // See also: WriteVmlDrawingRels procedure TsSpreadOOXMLWriter.WriteDrawingRels(AWorksheet: TsBasicWorksheet); var i: Integer; ext: String; img: TsImage; + chart: TsChart; rId: Integer; target, bookmark: String; u: TURI; sheet: TsWorksheet absolute AWorksheet; + sheetIdx: Integer; begin - if (sheet.GetImageCount = 0) then + if (sheet.GetImageCount = 0) and (sheet.GetChartCount = 0) then exit; SetLength(FSDrawingsRels, FCurSheetNum + 1); @@ -6413,7 +7283,7 @@ begin // Header AppendToStream(FSDrawingsRels[FCurSheetNum], XML_HEADER + LineEnding, - '' + LineEnding); + '' + LE); // Repeat for each image rId := 1; @@ -6433,7 +7303,7 @@ begin target := target + '#' + bookmark; AppendToStream(FSDrawingsRels[FCurSheetNum], Format( - ' ' + LineEnding, [ + ' ' + LE, [ rId, SCHEMAS_HYPERLINK, target ])); inc(rId); @@ -6441,12 +7311,27 @@ begin ext := GetImageTypeExt((FWorkbook as TsWorkbook).GetEmbeddedObj(img.Index).Imagetype); AppendToStream(FSDrawingsRels[FCurSheetNum], Format( - ' ' + LineEnding, [ + ' ' + LE, [ rId, SCHEMAS_IMAGE, img.Index+1, ext ])); inc(rId); end; + // Repeat for each chart + sheetIdx := sheet.Index; + for i := 0 to TsWorkbook(FWorkbook).GetChartCount - 1 do + begin + chart := TsWorkbook(FWorkbook).GetChartByIndex(i); + if chart.SheetIndex = sheetIdx then + begin + AppendToStream(FSDrawingsRels[FCurSheetNum], Format( + ' ' + LE, [ + rId, SCHEMAS_CHART, i+1 + ])); + inc(rId); + end; + end; + AppendToStream(FSDrawingsRels[FCurSheetNum], ''); end; @@ -6812,7 +7697,8 @@ begin // Anything to write? if (sheet.Comments.Count = 0) and (sheet.Hyperlinks.Count = 0) and - (sheet.GetImageCount = 0) and not (sheet.PageLayout.HasHeaderFooterImages) + (sheet.GetImageCount = 0) and (sheet.GetChartCount = 0) and + (not (sheet.PageLayout.HasHeaderFooterImages)) then exit; @@ -6861,10 +7747,10 @@ begin end; end; - // Relationships for embedded images + // Relationships for charts or embedded images // relationship with to the ../drawings/drawingX.xml file containing all - // image infos. X is the 1-base sheet index - if sheet.GetImageCount > 0 then + // chart/image infos. X is the 1-based sheet index + if (sheet.GetImageCount > 0) or (sheet.GetChartCount > 0) then AppendToStream(FSSheetRels[FCurSheetNum], Format( ' ' + LineEnding, [rId_Drawing, FCurSheetNum + 1, SCHEMAS_DRAWING] @@ -7124,13 +8010,19 @@ begin ''); end; + { Write all charts } + WriteChartRels; + WriteChartStyles; + WriteChartColors; + WriteCharts; + { Workbook relations - Mark relation to all sheets } WriteWorkbookRels(FSWorkbookRels); end; procedure TsSpreadOOXMLWriter.WriteContentTypes; var - i,j: Integer; + i, j, n: Integer; imgext: TStringList; ext: String; sheet: TsWorksheet; @@ -7172,6 +8064,19 @@ begin AppendToStream(FSContentTypes, '' + LineEnding); + n := 1; + for i:=0 to book.GetWorksheetCount-1 do + begin + sheet := book.GetWorksheetByIndex(i); + for j:=0 to sheet.GetChartCount-1 do + begin + AppendToStream(FSContentTypes, Format( + '' + LE, + [n, MIME_DRAWINGML_CHART])); + inc(n); + end; + end; + for i:=1 to book.GetWorksheetCount do begin AppendToStream(FSContentTypes, Format( @@ -7541,7 +8446,10 @@ var rId_Comments: Integer; rId_FirstHyperlink: Integer; rId_Drawing, rId_DrawingHF: Integer; + worksheet: TsWorksheet; begin + worksheet := TsWorksheet(AWorksheet); + FCurSheetNum := Length(FSSheets); SetLength(FSSheets, FCurSheetNum + 1); @@ -7575,12 +8483,12 @@ begin WriteColBreaks(FSSheets[FCurSheetNum], AWorksheet); WriteHeaderFooter(FSSheets[FCurSheetNum], AWorksheet); - { This item is required for all embedded images. + { This item is required for all embedded images and charts. There must be a matching file in "drawingX.xml" file in "../drawings" - which contains the image-related data of all images in this sheet. + which contains the image/chart-related data of all images/charts in this sheet. The file in turn requires an entry "drawingX.xml.rels" in the drawings rels folder } - if (AWorksheet as TsWorksheet).GetImageCount > 0 then + if (worksheet.GetImageCount > 0) or (worksheet.GetChartCount > 0) then AppendToStream(FSSheets[FCurSheetNum], Format( '', [rId_Drawing])); @@ -7682,6 +8590,14 @@ begin DestroyTempStream(FSSharedStrings_complete); for stream in FSSheets do DestroyTempStream(stream); SetLength(FSSheets, 0); + for stream in FSCharts do DestroyTempStream(stream); + SetLength(FSCharts, 0); + for stream in FSChartRels do DestroyTempStream(stream); + SetLength(FSChartRels, 0); + for stream in FSChartStyles do DestroyTempStream(stream); + SetLength(FSChartStyles, 0); + for stream in FSChartColors do DestroyTempStream(stream); + SetLength(FSChartColors, 0); for stream in FSComments do DestroyTempStream(stream); SetLength(FSComments, 0); for stream in FSSheetRels do DestroyTempStream(stream); @@ -7815,6 +8731,42 @@ begin FZip.Entries.AddFileEntry(FSSheetRels[i], OOXML_PATH_XL_WORKSHEETS_RELS + Format('sheet%d.xml.rels', [i+1])); end; + { not used by Excel 2007 + // Write chart styles + for i:=0 to High(FSChartStyles) do + begin + if (FSChartStyles[i] = nil) or (FSChartStyles[i].Size = 0) then Continue; + FSChartStyles[i].Position := 0; + FZip.Entries.AddFileEntry(FSChartStyles[i], OOXML_PATH_XL_CHARTS + Format('style%d.xml', [i+1])); + end; + + // Write chart colors + for i:=0 to High(FSChartColors) do + begin + if (FSChartColors[i] = nil) or (FSChartColors[i].Size = 0) then Continue; + FSChartColors[i].Position := 0; + FZip.Entries.AddFileEntry(FSChartColors[i], OOXML_PATH_XL_CHARTS + Format('colors%d.xml', [i+1])); + end; + } + + // Write charts + for i:=0 to High(FSCharts) do + begin + if (FSCharts[i] = nil) or (FSCharts[i].Size = 0) then Continue; + FSCharts[i].Position := 0; + FZip.Entries.AddFileEntry(FSCharts[i], OOXML_PATH_XL_CHARTS + Format('chart%d.xml', [i+1])); + end; + + { not used by Excel 2007 + // Write chart relationships + for i := 0 to High(FSChartRels) do + begin + if (FSChartRels[i] = nil) or (FSChartRels[i].Size = 0) then Continue; + FSChartRels[i].Position := 0; + FZip.Entries.AddFileEntry(FSChartRels[i], OOXML_PATH_XL_CHARTS_RELS + Format('chart%d.xml.rels', [i+1])); + end; + } + // Write drawings for i:=0 to High(FSDrawings) do begin if (FSDrawings[i] = nil) or (FSDrawings[i].Size = 0) then continue;