
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@5831 8e941d3f-bd1b-0410-a28a-d453659cc2b4
7908 lines
264 KiB
ObjectPascal
7908 lines
264 KiB
ObjectPascal
{
|
|
fpsopendocument.pas
|
|
|
|
Writes an OpenDocument 1.0 Spreadsheet document
|
|
|
|
An OpenDocument document is a compressed ZIP file with the following files inside:
|
|
|
|
content.xml - Actual contents
|
|
meta.xml - Authoring data
|
|
settings.xml - User persistent viewing information, such as zoom, cursor position, etc.
|
|
styles.xml - Styles, which are the only way to do formatting
|
|
mimetype - application/vnd.oasis.opendocument.spreadsheet
|
|
META-INF\manifest.xml - Describes the other files in the archive
|
|
|
|
Specifications obtained from:
|
|
|
|
http://docs.oasis-open.org/office/v1.1/OS/OpenDocument-v1.1.pdf
|
|
|
|
AUTHORS: Felipe Monteiro de Carvalho / Jose Luis Jurado Rincon / Werner Pamler
|
|
}
|
|
|
|
|
|
unit fpsopendocument;
|
|
|
|
{$ifdef fpc}
|
|
{$mode objfpc}{$H+}
|
|
{$endif}
|
|
|
|
{$I ..\fps.inc}
|
|
|
|
{.$define FPSPREADDEBUG} //used to be XLSDEBUG
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
laz2_xmlread, laz2_DOM,
|
|
avglvltree, math, dateutils, contnrs,
|
|
{$IF FPC_FULLVERSION >= 20701}
|
|
zipper,
|
|
{$ELSE}
|
|
fpszipper,
|
|
{$ENDIF}
|
|
fpstypes, fpspreadsheet, fpsReaderWriter, fpsutils, fpsHeaderFooterParser,
|
|
fpsNumFormat, fpsxmlcommon, fpsPagelayout;
|
|
|
|
type
|
|
TDateModeODS=(
|
|
dmODS1899 {default for ODF; almost same as Excel 1900},
|
|
dmODS1900 {StarCalc legacy only},
|
|
dmODS1904 {e.g. Quattro Pro, Mac Excel compatibility}
|
|
);
|
|
|
|
{ TsSpreadOpenDocHeaderFooterParser }
|
|
|
|
TsSpreadOpenDocHeaderFooterParser = class(TsHeaderFooterParser)
|
|
private
|
|
FNode: TDOMNode;
|
|
XMLMode: Boolean;
|
|
protected
|
|
procedure AddNodeElement(ANode: TDOMNode);
|
|
function FindStyle(AStyleName: String): Integer;
|
|
function GetCurrFontIndex: Integer; override;
|
|
procedure Parse; override;
|
|
public
|
|
constructor Create(ANode: TDOMNode; AFontList: TList;
|
|
ADefaultFont: TsHeaderFooterFont); overload;
|
|
function BuildHeaderFooterAsXMLString: String;
|
|
end;
|
|
|
|
{ TsSpreadOpenDocNumFormatParser }
|
|
|
|
TsSpreadOpenDocNumFormatParser = class(TsNumFormatParser)
|
|
protected
|
|
function BuildXMLAsStringFromSection(ASection: Integer;
|
|
AFormatName: String): String;
|
|
public
|
|
function BuildXMLAsString(AFormatName: String): String;
|
|
end;
|
|
|
|
{ TsSpreadOpenDocReader }
|
|
|
|
TsSpreadOpenDocReader = class(TsSpreadXMLReader)
|
|
private
|
|
FTableStyleList: TFPList;
|
|
FColumnStyleList: TFPList;
|
|
FColumnList: TFPList;
|
|
FRowStyleList: TFPList;
|
|
FRowList: TFPList;
|
|
FPageLayoutList: TFPList;
|
|
FMasterPageList: TFPList;
|
|
FHeaderFooterFontList: TObjectList;
|
|
FActiveSheet: String;
|
|
FDateMode: TDateModeODS;
|
|
FFontFaces: TStringList;
|
|
FRichTextFontList: TFPList;
|
|
FRepeatedCols: TsRowColRange;
|
|
FRepeatedRows: TsRowColRange;
|
|
procedure ApplyColWidths;
|
|
procedure ApplyStyleToCell(ACell: PCell; AStyleIndex: Integer);
|
|
function ApplyStyleToCell(ACell: PCell; AStyleName: String): Boolean;
|
|
function ApplyTableStyle(ASheet: TsWorksheet; AStyleName: String): Boolean;
|
|
function ExtractBoolFromNode(ANode: TDOMNode): Boolean;
|
|
function ExtractDateTimeFromNode(ANode: TDOMNode;
|
|
ANumFormat: TsNumberFormat; const AFormatStr: String): TDateTime;
|
|
function ExtractErrorFromNode(ANode: TDOMNode; out AErrorValue: TsErrorValue): Boolean;
|
|
function ExtractFormatIndexFromStyle(ACellStyleName: String; ACol: Integer): Integer;
|
|
function FindColumnByCol(AColIndex: Integer): Integer;
|
|
function FindColStyleByName(AStyleName: String): integer;
|
|
function FindNumFormatByName(ANumFmtName: String): Integer;
|
|
function FindRowStyleByName(AStyleName: String): Integer;
|
|
function FindTableStyleByName(AStyleName: String): Integer;
|
|
procedure ReadCell(ANode: TDOMNode; ARow, ACol: Integer;
|
|
AFormatIndex: Integer; out AColsRepeated: Integer);
|
|
procedure ReadColumns(ATableNode: TDOMNode);
|
|
procedure ReadColumnStyle(AStyleNode: TDOMNode);
|
|
procedure ReadDateMode(SpreadSheetNode: TDOMNode);
|
|
procedure ReadDocumentProtection(ANode: TDOMNode);
|
|
procedure ReadFont(ANode: TDOMNode; var AFontName: String;
|
|
var AFontSize: Single; var AFontStyle: TsFontStyles; var AFontColor: TsColor;
|
|
var AFontPosition: TsFontPosition);
|
|
// function ReadFont(ANode: TDOMnode; APreferredIndex: Integer = -1): Integer;
|
|
procedure ReadFontFaces(ANode: TDOMNode);
|
|
procedure ReadHeaderFooterFont(ANode: TDOMNode; var AFontName: String;
|
|
var AFontSize: Double; var AFontStyle: TsHeaderFooterFontStyles;
|
|
var AFontColor: TsColor);
|
|
function ReadHeaderFooterText(ANode: TDOMNode): String;
|
|
procedure ReadPictures(AStream: TStream);
|
|
procedure ReadPrintRanges(ATableNode: TDOMNode; ASheet: TsWorksheet);
|
|
procedure ReadRowsAndCells(ATableNode: TDOMNode);
|
|
procedure ReadRowStyle(AStyleNode: TDOMNode);
|
|
procedure ReadShapes(ATableNode: TDOMNode);
|
|
procedure ReadSheetProtection(ANode: TDOMNode; ASheet: TsWorksheet);
|
|
procedure ReadTableStyle(AStyleNode: TDOMNode);
|
|
|
|
protected
|
|
FPointSeparatorSettings: TFormatSettings;
|
|
procedure AddBuiltinNumFormats; override;
|
|
procedure ReadAutomaticStyles(AStylesNode: TDOMNode);
|
|
procedure ReadMasterStyles(AStylesNode: TDOMNode);
|
|
procedure ReadNumFormats(AStylesNode: TDOMNode);
|
|
procedure ReadPageLayout(AStylesNode: TDOMNode; ATableStyleName: String;
|
|
APageLayout: TsPageLayout);
|
|
procedure ReadSettings(AOfficeSettingsNode: TDOMNode);
|
|
procedure ReadStyles(AStylesNode: TDOMNode);
|
|
{ Record writing methods }
|
|
procedure ReadBlank(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode); reintroduce;
|
|
procedure ReadBoolean(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
procedure ReadComment(ARow, ACol: Cardinal; ACellNode: TDOMNode);
|
|
procedure ReadDateTime(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
procedure ReadError(ARow, ACol: Cardinal; AStyleIndex: Integer;
|
|
ACellNode: TDOMNode);
|
|
procedure ReadFormula(ARow, ACol: Cardinal; AstyleIndex: Integer;
|
|
ACellNode: TDOMNode); reintroduce;
|
|
procedure ReadLabel(ARow, ACol: Cardinal; AStyleIndex: Integer;
|
|
ACellNode: TDOMNode); reintroduce;
|
|
procedure ReadNumber(ARow, ACol: Cardinal; AStyleIndex: Integer;
|
|
ACellNode: TDOMNode); reintroduce;
|
|
|
|
public
|
|
constructor Create(AWorkbook: TsWorkbook); override;
|
|
destructor Destroy; override;
|
|
|
|
{ General reading methods }
|
|
procedure ReadFromStream(AStream: TStream;
|
|
APassword: String = ''; AParams: TsStreamParams = []); override;
|
|
end;
|
|
|
|
{ TsSpreadOpenDocWriter }
|
|
|
|
TsSpreadOpenDocWriter = class(TsCustomSpreadWriter)
|
|
private
|
|
FColumnStyleList: TFPList;
|
|
FRowStyleList: TFPList;
|
|
FRichTextFontList: TStringList;
|
|
FHeaderFooterFontList: TObjectList;
|
|
FHasColFormats: Boolean;
|
|
FHasRowFormats: Boolean;
|
|
|
|
// Routines to write parts of files
|
|
procedure WriteAutomaticStyles(AStream: TStream);
|
|
procedure WriteCellRow(AStream: TStream; ASheet: TsWorksheet;
|
|
ARowIndex, ALastColIndex: Integer);
|
|
procedure WriteCellStyles(AStream: TStream);
|
|
procedure WriteColStyles(AStream: TStream);
|
|
procedure WriteColumns(AStream: TStream; ASheet: TsWorksheet);
|
|
procedure WriteEmptyRow(AStream: TStream; ASheet: TsWorksheet;
|
|
ARowIndex, AFirstColIndex, ALastColIndex, ALastRowIndex: Integer;
|
|
out ARowsRepeated: Integer);
|
|
procedure WriteFontNames(AStream: TStream);
|
|
procedure WriteMasterStyles(AStream: TStream);
|
|
procedure WriteNamedExpressions(AStream: TStream; ASheet: TsWorksheet);
|
|
procedure WriteNumFormats(AStream: TStream);
|
|
procedure WriteRowStyles(AStream: TStream);
|
|
procedure WriteRowsAndCells(AStream: TStream; ASheet: TsWorksheet);
|
|
procedure WriteShapes(AStream: TStream; ASheet: TsWorksheet);
|
|
procedure WriteTableSettings(AStream: TStream);
|
|
procedure WriteTableStyles(AStream: TStream);
|
|
procedure WriteTextStyles(AStream: TStream);
|
|
procedure WriteVirtualCells(AStream: TStream; ASheet: TsWorksheet);
|
|
|
|
function WriteBackgroundColorStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WriteBiDiModeStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WriteBorderStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WriteCellProtectionStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WriteCommentXMLAsString(AComment: String): String;
|
|
function WriteDefaultFontXMLAsString: String;
|
|
function WriteDefaultGraphicStyleXMLAsString: String; overload;
|
|
function WriteDocumentProtectionXMLAsString: String;
|
|
function WriteFontStyleXMLAsString(const AFormat: TsCellFormat): String; overload;
|
|
function WriteFontStyleXMLAsString(AFont: TsFont): String; overload;
|
|
function WriteHeaderFooterFontXMLAsString(AFont: TsHeaderFooterFont): String;
|
|
function WriteHorAlignmentStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WritePageLayoutXMLAsString(AStyleName: String; const APageLayout: TsPageLayout): String;
|
|
function WritePrintRangesXMLAsString(ASheet: TsWorksheet): String;
|
|
function WriteSheetProtectionXMLAsString(ASheet: TsWorksheet): String;
|
|
function WriteSheetProtectionDetailsXMLAsString(ASheet: TsWorksheet): String;
|
|
function WriteTextRotationStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WriteVertAlignmentStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
function WriteWordwrapStyleXMLAsString(const AFormat: TsCellFormat): String;
|
|
|
|
protected
|
|
FPointSeparatorSettings: TFormatSettings;
|
|
// Streams with the contents of files
|
|
FSMeta, FSSettings, FSStyles, FSContent: TStream;
|
|
FSMimeType, FSMetaInfManifest: TStream;
|
|
|
|
{ Helpers }
|
|
procedure AddBuiltinNumFormats; override;
|
|
procedure CreateStreams;
|
|
procedure DestroyStreams;
|
|
procedure GetHeaderFooterImageName(APageLayout: TsPageLayout;
|
|
out AHeader, AFooter: String);
|
|
procedure GetHeaderFooterImagePosStr(APagelayout: TsPageLayout;
|
|
out AHeader, AFooter: String);
|
|
procedure GetRowStyleAndHeight(ASheet: TsWorksheet; ARowIndex: Integer;
|
|
out AStyleName: String; out AHeight: Single);
|
|
procedure InternalWriteToStream(AStream: TStream);
|
|
procedure ListAllColumnStyles;
|
|
procedure ListAllHeaderFooterFonts;
|
|
procedure ListAllNumFormats; override;
|
|
procedure ListAllRowStyles;
|
|
procedure ResetStreams;
|
|
|
|
{ Routines to write those files }
|
|
procedure WriteContent;
|
|
procedure WriteMetaInfManifest;
|
|
procedure WriteMeta;
|
|
procedure WriteMimetype;
|
|
procedure WriteSettings;
|
|
procedure WriteStyles;
|
|
procedure WriteWorksheet(AStream: TStream; ASheetIndex: Integer);
|
|
procedure ZipPictures(AZip: TZipper);
|
|
|
|
{ Record writing methods }
|
|
procedure WriteBlank(AStream: TStream; const ARow, ACol: Cardinal;
|
|
ACell: PCell); override;
|
|
procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal;
|
|
const AValue: Boolean; ACell: PCell); override;
|
|
procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal;
|
|
const AValue: TDateTime; ACell: PCell); override;
|
|
procedure WriteError(AStream: TStream; const ARow, ACol: Cardinal;
|
|
const AValue: TsErrorValue; ACell: PCell); override;
|
|
procedure WriteFormula(AStream: TStream; const ARow, ACol: Cardinal;
|
|
ACell: PCell); override;
|
|
procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal;
|
|
const AValue: string; ACell: PCell); override;
|
|
procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal;
|
|
const AValue: double; ACell: PCell); override;
|
|
|
|
public
|
|
constructor Create(AWorkbook: TsWorkbook); override;
|
|
destructor Destroy; override;
|
|
|
|
{ General writing methods }
|
|
procedure WriteStringToFile(AString, AFileName: string);
|
|
procedure WriteToStream(AStream: TStream; AParams: TsStreamParams = []); override;
|
|
end;
|
|
|
|
{ procedure WriteStarObjectDescriptorToStream(AStream: TStream); }
|
|
|
|
var
|
|
sfidOpenDocument: TsSpreadFormatID;
|
|
|
|
implementation
|
|
|
|
uses
|
|
StrUtils, Variants, LazFileUtils, URIParser, LazUTF8,
|
|
{$IFDEF FPS_VARISBOOL}
|
|
fpsPatches,
|
|
{$ENDIF}
|
|
fpsStrings, fpsStreams, fpsCrypto, fpsClasses, fpsExprParser, fpsImages;
|
|
|
|
const
|
|
{ OpenDocument general XML constants }
|
|
XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>';
|
|
|
|
{ OpenDocument Directory structure constants }
|
|
OPENDOC_PATH_CONTENT = 'content.xml';
|
|
OPENDOC_PATH_META = 'meta.xml';
|
|
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';
|
|
|
|
{ OpenDocument schemas constants }
|
|
SCHEMAS_XMLNS_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0';
|
|
SCHEMAS_XMLNS_DCTERMS = 'http://purl.org/dc/terms/';
|
|
SCHEMAS_XMLNS_META = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0';
|
|
SCHEMAS_XMLNS = 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties';
|
|
SCHEMAS_XMLNS_CONFIG = 'urn:oasis:names:tc:opendocument:xmlns:config:1.0';
|
|
SCHEMAS_XMLNS_OOO = 'http://openoffice.org/2004/office';
|
|
SCHEMAS_XMLNS_DRAW = 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0';
|
|
SCHEMAS_XMLNS_MANIFEST = 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0';
|
|
SCHEMAS_XMLNS_FO = 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0';
|
|
SCHEMAS_XMLNS_STYLE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
|
|
SCHEMAS_XMLNS_SVG = 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0';
|
|
SCHEMAS_XMLNS_TABLE = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0';
|
|
SCHEMAS_XMLNS_TEXT = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0';
|
|
SCHEMAS_XMLNS_V = 'urn:schemas-microsoft-com:vml';
|
|
SCHEMAS_XMLNS_XLINK = 'http://www.w3.org/1999/xlink';
|
|
{%H-}SCHEMAS_XMLNS_NUMBER = 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0';
|
|
{%H-}SCHEMAS_XMLNS_CHART = 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0';
|
|
{%H-}SCHEMAS_XMLNS_DR3D = 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0';
|
|
{%H-}SCHEMAS_XMLNS_MATH = 'http://www.w3.org/1998/Math/MathML';
|
|
{%H-}SCHEMAS_XMLNS_FORM = 'urn:oasis:names:tc:opendocument:xmlns:form:1.0';
|
|
{%H-}SCHEMAS_XMLNS_SCRIPT = 'urn:oasis:names:tc:opendocument:xmlns:script:1.0';
|
|
{%H-}SCHEMAS_XMLNS_OOOW = 'http://openoffice.org/2004/writer';
|
|
{%H-}SCHEMAS_XMLNS_OOOC = 'http://openoffice.org/2004/calc';
|
|
{%H-}SCHEMAS_XMLNS_DOM = 'http://www.w3.org/2001/xml-events';
|
|
{%H-}SCHEMAS_XMLNS_XFORMS = 'http://www.w3.org/2002/xforms';
|
|
{%H-}SCHEMAS_XMLNS_XSD = 'http://www.w3.org/2001/XMLSchema';
|
|
{%H-}SCHEMAS_XMLNS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
|
|
|
|
{ DATEMODE similar to but not the same as XLS format; used in time only values. }
|
|
DATEMODE_1899_BASE=0; //apparently 1899-12-30 for ODF in FPC DateTime;
|
|
// due to Excel's leap year bug, the date floats in the spreadsheets are the same starting
|
|
// 1900-03-01
|
|
DATEMODE_1900_BASE=2; //StarCalc compatibility, 1900-01-01 in FPC DateTime
|
|
DATEMODE_1904_BASE=1462; //1/1/1904 in FPC TDateTime
|
|
|
|
const
|
|
// lsThin, lsMedium, lsDashed, lsDotted, lsThick, lsDouble, lsHair
|
|
// lsMediumDash, lsDashDot, lsMediumDashDot, lsDashDotDot, lsMediumDashDotDot, lsSlantDashDot
|
|
BORDER_LINESTYLES: array[TsLineStyle] of string = (
|
|
'solid', 'solid', 'dashed', 'fine-dashed', 'solid', 'double-thin', 'dotted',
|
|
'dashed', 'dash-dot', 'dash-dot', 'dash-dot-dot', 'dash-dot-dot', 'dash-dot'
|
|
);
|
|
BORDER_LINEWIDTHS: array[TsLinestyle] of string =
|
|
('0.74pt', '1.76pt', '0.74pt', '0.74pt', '2.49pt', '0.74pt', '0.74pt',
|
|
'1.76pt', '0.74pt', '1.76pt', '0.74pt', '1.76pt', '1.76pt');
|
|
|
|
FALSE_TRUE: Array[boolean] of String = ('false', 'true');
|
|
|
|
COLWIDTH_EPS = 1e-3;
|
|
ROWHEIGHT_EPS = 1e-3;
|
|
|
|
type
|
|
|
|
{ Table style items stored in TableStyleList of the reader }
|
|
TTableStyleData = class
|
|
public
|
|
Name: String;
|
|
BiDiMode: TsBiDiMode;
|
|
Hidden: boolean;
|
|
end;
|
|
|
|
{ Column style items stored in ColStyleList of the reader }
|
|
TColumnStyleData = class
|
|
public
|
|
Name: String;
|
|
ColWidth: Double; // in workbook units
|
|
end;
|
|
|
|
{ Column data items stored in the ColumnList }
|
|
TColumnData = class
|
|
public
|
|
Col: Integer;
|
|
ColStyleIndex: integer; // index into FColumnStyleList of reader
|
|
DefaultCellStyleIndex: Integer; // Index of default cell style in FCellStyleList of reader
|
|
end;
|
|
|
|
{ Row style items stored in RowStyleList of the reader }
|
|
TRowStyleData = class
|
|
public
|
|
Name: String;
|
|
RowHeight: Double; // in workbook units
|
|
RowHeightType: TsRowHeightType;
|
|
end;
|
|
|
|
{ PageLayout items stored in PageLayoutList }
|
|
TPageLayoutData = class
|
|
public
|
|
Name: String;
|
|
PageLayout: TsPageLayout;
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
end;
|
|
|
|
constructor TPageLayoutData.Create;
|
|
begin
|
|
inherited;
|
|
PageLayout := TsPageLayout.Create(nil);
|
|
end;
|
|
|
|
destructor TPageLayoutData.destroy;
|
|
begin
|
|
PageLayout.Free;
|
|
inherited;
|
|
end;
|
|
|
|
type
|
|
{ MasterPage items stored in MasterPageList }
|
|
TMasterPageData = class
|
|
public
|
|
Name: String;
|
|
PageLayoutName: String;
|
|
end;
|
|
|
|
(* --- presently not used, but this may change... ---
|
|
|
|
{ Row data items stored in the RowList of the reader }
|
|
TRowData = class
|
|
Row: Integer;
|
|
RowStyleIndex: Integer; // index into FRowStyleList of reader
|
|
DefaultCellStyleIndex: Integer; // Index of default row style in FCellStyleList of reader
|
|
end;
|
|
*)
|
|
|
|
|
|
{******************************************************************************}
|
|
{ Clipboard utility }
|
|
{******************************************************************************}
|
|
(*
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes the "Star Object Descriptor". This is written to the clipboard by
|
|
Open/LibreOffice. No idea about the meaning of this...
|
|
-------------------------------------------------------------------------------}
|
|
procedure WriteStarObjectDescriptorToStream(AStream: TStream);
|
|
const
|
|
BYTES: packed array[0..$38] of byte = (
|
|
$39,$00,$00,$00,$CB,$B4,$BB,$47,$4C,$CE,$80,$4E,$A5,$91,$42,$D9,
|
|
$AE,$74,$95,$0F,$01,$00,$00,$00,$D2,$08,$00,$00,$C4,$01,$00,$00,
|
|
$00,$00,$00,$00,$00,$00,$00,$00,$05,$00,$63,$61,$6C,$63,$38,$00,
|
|
$00,$67,$45,$23,$01,$EF,$CD,$AB,$89);
|
|
begin
|
|
AStream.Write(BYTES, SizeOf(BYTES));
|
|
end; *)
|
|
|
|
|
|
{******************************************************************************}
|
|
{ TXMLHeaderFooterFont }
|
|
{******************************************************************************}
|
|
type
|
|
TXMLHeaderFooterFont = class(TsHeaderFooterFont)
|
|
StyleName: String;
|
|
end;
|
|
|
|
|
|
{******************************************************************************}
|
|
{ TsSpreadOpenDocHeaderFooterParser }
|
|
{******************************************************************************}
|
|
|
|
constructor TsSpreadOpenDocHeaderFooterParser.Create(ANode: TDOMNode;
|
|
AFontList: TList; ADefaultFont: TsHeaderFooterFont);
|
|
begin
|
|
inherited Create;
|
|
XMLMode := true;
|
|
FNode := ANode; // this is a child of the "<style-header>" etc nodes.
|
|
FFontList := AFontList;
|
|
FDefaultFont := ADefaultFont;
|
|
FFontClass := TXMLHeaderFooterFont;
|
|
Parse;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocHeaderFooterParser.AddNodeElement(ANode: TDOMNode);
|
|
var
|
|
nodeName: String;
|
|
begin
|
|
nodeName := ANode.NodeName;
|
|
case nodeName of
|
|
'text:sheet-name':
|
|
AddElement(hftSheetName);
|
|
'text:file-name' :
|
|
case GetAttrValue(ANode, 'text:display') of
|
|
'full': begin
|
|
AddElement(hftPath);
|
|
AddElement(hftFileName);
|
|
end;
|
|
'path': AddElement(hftPath);
|
|
else AddElement(hftFileName);
|
|
end;
|
|
'text:date':
|
|
AddElement(hftDate);
|
|
'text:time':
|
|
AddElement(hftTime);
|
|
'text:page-number':
|
|
AddElement(hftPage);
|
|
'text:page-count':
|
|
AddElement(hftPageCount);
|
|
'#text':
|
|
begin
|
|
FCurrText := ANode.NodeValue; //GetNodeValue(ANode);
|
|
AddCurrTextElement;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocHeaderFooterParser.BuildHeaderFooterAsXMLString: String;
|
|
var
|
|
regionStr: array[TsHeaderFooterSectionIndex] of String;
|
|
sec: TsHeaderFooterSectionIndex;
|
|
element: TsHeaderFooterElement;
|
|
styleName: String;
|
|
s: String;
|
|
begin
|
|
for sec := hfsLeft to hfsRight do
|
|
begin
|
|
if Length(FSections[sec]) = 0 then
|
|
Continue;
|
|
regionStr[sec] := '';
|
|
for element in FSections[sec] do
|
|
begin
|
|
stylename := TXMLHeaderFooterFont(FFontList[element.FontIndex]).StyleName;
|
|
case element.Token of
|
|
hftText:
|
|
s := Format('<text:span text:style-name="%s">%s</text:span>', [
|
|
stylename, UTF8TextToXMLText(element.TextValue)]);
|
|
hftNewLine:
|
|
s := '</text:p><text:p>';
|
|
hftSheetName:
|
|
s := '<text:sheet-name>???</text:sheet-name>';
|
|
hftPath:
|
|
s := '<text:file-name text:display="path">???</text:file-name>';
|
|
hftFileName:
|
|
s := '<text:file-name text:display="name-and-extension">???</text:file-name>';
|
|
hftDate:
|
|
s := Format('<text:date style:data-style-name="N2" text:date-value="%s">%s</text:date>', [
|
|
FormatDateTime('yyyy"-"mm"-"dd', date()), DateToStr(date())]);
|
|
hftTime:
|
|
s := Format('<text:time>%s</text:time>', [
|
|
FormatDateTime('hh:nn:ss', time()) ]);
|
|
hftPage:
|
|
s := '<text:page-number>1</text:page-number>';
|
|
hftPageCount:
|
|
s := '<text:page-count>1</text:page-count>';
|
|
else
|
|
s := '';
|
|
end;
|
|
if s <> '' then
|
|
regionStr[sec] := regionStr[sec] + s;
|
|
end; // for element
|
|
if regionStr[sec] <> '' then
|
|
regionStr[sec] := '<text:p>' + regionStr[sec] + '</text:p>';
|
|
end; // for sec
|
|
|
|
Result := '';
|
|
for sec := hfsLeft to hfsRight do
|
|
begin
|
|
case sec of
|
|
hfsLeft : s := 'style:region-left';
|
|
hfsCenter : s := 'style:region-center';
|
|
hfsRight : s := 'style:region-right';
|
|
end;
|
|
if regionStr[sec] = '' then
|
|
Result := Result + '<' + s + ' />' else
|
|
Result := Result + '<' + s + '>' + regionStr[sec] + '</' + s + '>';
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocHeaderFooterParser.FindStyle(AStyleName: String): Integer;
|
|
var
|
|
fnt: TXMLHeaderFooterFont;
|
|
begin
|
|
for Result := 0 to FFontList.Count-1 do
|
|
begin
|
|
fnt := TXMLHeaderFooterFont(FFontList[Result]);
|
|
if SameText(fnt.StyleName, AStyleName) then
|
|
exit;
|
|
end;
|
|
Result := -1;
|
|
end;
|
|
|
|
function TsSpreadOpenDocHeaderFooterParser.GetCurrFontIndex: Integer;
|
|
begin
|
|
if XMLMode then
|
|
Result := FCurrFontIndex
|
|
else
|
|
Result := inherited GetCurrFontIndex;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocHeaderFooterParser.Parse;
|
|
var
|
|
node, pnode, childpnode, childspannode: TDOMNode;
|
|
nodeName: String;
|
|
s: String;
|
|
firstP: Boolean;
|
|
begin
|
|
FFontClass := TXMLHeaderFooterFont;
|
|
|
|
if not XMLMode then
|
|
begin
|
|
inherited Parse;
|
|
exit;
|
|
end;
|
|
|
|
node := FNode;
|
|
while Assigned(node) do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
case nodeName of
|
|
'style:region-left' : FCurrSection := hfsLeft;
|
|
'style:region-center' : FCurrSection := hfsCenter;
|
|
'style:region-right' : FCurrSection := hfsRight;
|
|
end;
|
|
firstP := true;
|
|
pnode := node.FirstChild;
|
|
while Assigned(pnode) do
|
|
begin
|
|
nodeName := pnode.NodeName;
|
|
if nodeName = 'text:p' then
|
|
begin
|
|
if not firstP then AddElement(hftNewLine);
|
|
childpnode := pnode.FirstChild;
|
|
while Assigned(childpnode) do
|
|
begin
|
|
nodeName := childpnode.NodeName;
|
|
if nodeName = 'text:span' then begin
|
|
s := GetAttrValue(childpnode, 'text:style-name');
|
|
if s <> '' then
|
|
FCurrFontIndex := FindStyle(s) else
|
|
FCurrFontIndex := -1;
|
|
childspannode := childpnode.FirstChild;
|
|
while Assigned(childspannode) do
|
|
begin
|
|
nodeName := childspannode.NodeName;
|
|
AddNodeElement(childspannode);
|
|
childspannode := childspannode.NextSibling;
|
|
end;
|
|
end else
|
|
AddNodeElement(childpnode);
|
|
childpnode := childpnode.NextSibling;
|
|
end;
|
|
firstP := false;
|
|
end;
|
|
pnode := pnode.NextSibling;
|
|
end;
|
|
node := node.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
|
|
{******************************************************************************}
|
|
{ TsSpreadOpenDocNumFormatParser }
|
|
{******************************************************************************}
|
|
|
|
function TsSpreadOpenDocNumFormatParser.BuildXMLAsString(AFormatName: String): String;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result := '';
|
|
{ When there is only one section the next statement is the only one executed.
|
|
When there are several sections the file contains at first the
|
|
positive section (index 0), then the negative section (index 1), and
|
|
finally the zero section (index 2) which contains the style-map. }
|
|
for i:=0 to Length(FSections)-1 do
|
|
Result := Result + BuildXMLAsStringFromSection(i, AFormatName);
|
|
end;
|
|
|
|
function TsSpreadOpenDocNumFormatParser.BuildXMLAsStringFromSection(
|
|
ASection: Integer; AFormatName: String): String;
|
|
var
|
|
n: Integer;
|
|
el, nEl: Integer;
|
|
ns: Integer;
|
|
clr: TsColor;
|
|
mask: String;
|
|
timeIntervalStr: String;
|
|
styleMapStr: String;
|
|
int,num,denom: Integer;
|
|
begin
|
|
Result := '';
|
|
|
|
ns := Length(FSections);
|
|
if (ns = 0) then
|
|
exit;
|
|
|
|
styleMapStr := '';
|
|
timeIntervalStr := '';
|
|
|
|
if (ns > 1) then
|
|
begin
|
|
// The file corresponding to the last section contains the styleMap.
|
|
if (ASection = ns - 1) then
|
|
case ns of
|
|
2: styleMapStr :=
|
|
'<style:map ' +
|
|
'style:apply-style-name="' + AFormatName + 'P0" ' +
|
|
'style:condition="value()>=0" />'; // >= 0
|
|
3: styleMapStr :=
|
|
'<style:map '+
|
|
'style:apply-style-name="' + AFormatName + 'P0" ' + // > 0
|
|
'style:condition="value()>0" />' +
|
|
'<style:map '+
|
|
'style:apply-style-name="' + AFormatName + 'P1" ' + // < 0
|
|
'style:condition="value()<0" />';
|
|
else
|
|
raise Exception.Create('At most 3 format sections allowed.');
|
|
end
|
|
else
|
|
AFormatName := AFormatName + 'P' + IntToStr(ASection);
|
|
end;
|
|
|
|
with FSections[ASection] do
|
|
begin
|
|
nEl := Length(Elements);
|
|
el := 0;
|
|
while (el < nEl) do begin
|
|
case Elements[el].Token of
|
|
nftColor:
|
|
begin
|
|
clr := TsColor(Elements[el].IntValue);
|
|
Result := Result + '<style:text-properties fo:color="' + ColorToHTMLColorStr(clr) + '" />';
|
|
end;
|
|
|
|
nftSign, nftSignBracket, nftText, nftSpace:
|
|
if Elements[el].TextValue = ' ' then
|
|
Result := Result + '<number:text><![CDATA[ ]]></number:text>'
|
|
else
|
|
Result := Result + '<number:text>' + Elements[el].TextValue + '</number:text>';
|
|
|
|
nftPercent:
|
|
Result := Result + '<number:text>%</number:text>';
|
|
|
|
nftFactor:
|
|
;
|
|
|
|
nftCurrSymbol:
|
|
Result := Result + '<number:currency-symbol>' + Elements[el].TextValue + '</number:currency-symbol>';
|
|
|
|
nftGeneral:
|
|
Result := Result + '<number:number number:min-integer-digits="1" />';
|
|
|
|
nftIntTh:
|
|
begin
|
|
Result := Result + '<number:number number:min-integer-digits="1" number:grouping="true"';
|
|
if (el+2 < nel) and (Elements[el+1].Token = nftDecSep) and
|
|
(Elements[el+2].Token in [nftZeroDecs, nftOptDecs, nftSpaceDecs]) then
|
|
begin
|
|
n := IfThen(Elements[el+2].Token = nftZeroDecs, Elements[el+2].IntValue, 1);
|
|
Result := Result + ' number:decimal-places="' + IntToStr(n) + '"';
|
|
inc(el, 2);
|
|
end else
|
|
if (el = nel-1) or (Elements[el+1].Token <> nftDecSep) then
|
|
Result := Result + ' number:decimal-places="0"';
|
|
if (nfkHasFactor in Kind) and (Factor <> 0) then
|
|
Result := Result + Format(' number:display-factor="%.0f"', [1.0/Factor]);
|
|
Result := Result + ' />';
|
|
end;
|
|
|
|
nftFracNumZeroDigit, nftFracNumOptDigit, nftFracNumSpaceDigit:
|
|
begin
|
|
num := Elements[el].IntValue;
|
|
inc(el);
|
|
while (el < nel) and (Elements[el].Token in [nftSpace, nftText, nftEscaped]) do
|
|
inc(el);
|
|
if (el < nel) and (Elements[el].Token <> nftFracSymbol) then
|
|
Continue;
|
|
while (el < nel) and (Elements[el].Token in [nftSpace, nftText, nftEscaped]) do
|
|
inc(el);
|
|
if (el < nel) and
|
|
(Elements[el].Token in [nftFracDenomOptDigit, nftFracDenomSpaceDigit, nftFracDenomZeroDigit, nftFracDenom])
|
|
then
|
|
denom := Elements[el].IntValue
|
|
else
|
|
Continue;
|
|
if Elements[el].Token = nftFracDenom then // fixed denominator
|
|
Result := Result +
|
|
'<number:fraction' +
|
|
' number:min-numerator-digits="' + IntToStr(num) +
|
|
'" number:min-denominator-digits="' + IntToStr(num) +
|
|
'" number:denominator-value="' + IntToStr(denom) +
|
|
'" />'
|
|
else
|
|
Result := Result +
|
|
'<number:fraction' +
|
|
' number:min-numerator-digits="' + IntToStr(num) +
|
|
'" number:min-denominator-digits="' + IntToStr(num) +
|
|
'" />'
|
|
end;
|
|
|
|
nftIntZeroDigit, nftIntOptDigit, nftIntSpaceDigit:
|
|
begin
|
|
// Mixed fraction
|
|
if nfkFraction in Kind then
|
|
begin
|
|
if Elements[el].Token = nftIntOptDigit
|
|
then int := 0
|
|
else int := Elements[el].IntValue;
|
|
inc(el);
|
|
while (el < nel) and not
|
|
(Elements[el].Token in [nftFracNumZeroDigit, nftFracNumOptDigit, nftFracNumSpaceDigit])
|
|
do
|
|
inc(el);
|
|
if el = nel then
|
|
Continue;
|
|
num := Elements[el].IntValue;
|
|
while (el < nel) and not
|
|
(Elements[el].Token in [nftFracDenomZeroDigit, nftFracDenomOptDigit, nftFracDenomSpaceDigit, nftFracDenom])
|
|
do
|
|
inc(el);
|
|
if el = nel then
|
|
Continue;
|
|
denom := Elements[el].IntValue;
|
|
if (Elements[el].Token = nftFracDenom) then
|
|
Result := Result +
|
|
'<number:fraction' +
|
|
' number:min-integer-digits="' + IntToStr(int) +
|
|
'" number:min-numerator-digits="' + IntToStr(num) +
|
|
'" number:min-denominator-digits="' + IntToStr(num) +
|
|
'" number:denominator-value="' + IntToStr(denom) +
|
|
'" />'
|
|
else
|
|
Result := Result +
|
|
'<number:fraction' +
|
|
' number:min-integer-digits="' + IntToStr(int) +
|
|
'" number:min-numerator-digits="' + IntToStr(num) +
|
|
'" number:min-denominator-digits="' + IntToStr(denom) +
|
|
'" />';
|
|
end
|
|
else
|
|
// Scientific, no decimals
|
|
if (el+3 < nel) and (Elements[el+1].Token = nftExpChar) then
|
|
begin
|
|
Result := Result + '<number:scientific-number number:decimal-places="0"';
|
|
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
|
|
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
|
|
n := Elements[el+3].IntValue;
|
|
Result := Result + ' number:min-exponent-digits="' + IntToStr(n) + '"';
|
|
Result := Result + ' />';
|
|
inc(el, 3);
|
|
end
|
|
else
|
|
// Scientific, with decimals
|
|
if (el+5 < nel) and (Elements[el+1].Token = nftDecSep) and (Elements[el+3].Token = nftExpChar)
|
|
then begin
|
|
Result := Result + '<number:scientific-number';
|
|
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
|
|
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
|
|
n := IfThen(Elements[el+2].Token = nftZeroDecs, Elements[el+2].IntValue, 1);
|
|
Result := Result + ' number:decimal-places="' + IntToStr(n) + '"';
|
|
Result := Result + ' number:min-exponent-digits="' + IntToStr(Elements[el+5].IntValue) + '"';
|
|
Result := Result + ' />';
|
|
inc(el, 5);
|
|
end
|
|
else
|
|
// Standard decimal number
|
|
if (el+2 < nel) and (Elements[el+1].Token = nftDecSep) then
|
|
begin
|
|
Result := Result + '<number:number';
|
|
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
|
|
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
|
|
n := IfThen(Elements[el+2].Token = nftZeroDecs, Elements[el+2].IntValue, 1);
|
|
Result := Result + ' number:decimal-places="' + IntToStr(n) + '"';
|
|
if (nfkHasFactor in Kind) and (Factor <> 0) then
|
|
Result := Result + Format(' number:display-factor="%.0f"', [1.0/Factor]);
|
|
Result := Result + ' />';
|
|
inc(el, 2);
|
|
end
|
|
else
|
|
// Standard integer
|
|
if (el = nel-1) or (Elements[el+1].Token <> nftDecSep) then
|
|
begin
|
|
Result := Result + '<number:number number:decimal-places="0"';
|
|
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
|
|
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
|
|
if (nfkHasFactor in Kind) and (Factor <> 0) then
|
|
Result := Result + Format(' number:display-factor="%.0f"', [1.0/Factor]);
|
|
Result := Result + ' />';
|
|
end;
|
|
end;
|
|
|
|
nftYear:
|
|
if Elements[el].IntValue > 2 then
|
|
Result := Result + '<number:year number:style="long" />'
|
|
else
|
|
Result := Result + '<number:year />';
|
|
|
|
nftMonth:
|
|
case Elements[el].IntValue of
|
|
1: Result := Result + '<number:month />';
|
|
2: Result := Result + '<number:month number:style="long" />';
|
|
3: Result := Result + '<number:month number:textual="true" />';
|
|
4: Result := Result + '<number:month number:style="long" number:textual="true" />';
|
|
end;
|
|
|
|
nftDay:
|
|
case Elements[el].IntValue of
|
|
1: Result := Result + '<number:day />';
|
|
2: Result := Result + '<number:day number:style="long" />';
|
|
3: Result := Result + '<number:day-of-week />';
|
|
4: Result := Result + '<number:day-of-week number:style="long" />';
|
|
end;
|
|
|
|
nftHour:
|
|
begin
|
|
case abs(Elements[el].IntValue) of
|
|
1: Result := Result + '<number:hours />';
|
|
2: Result := Result + '<number:hours number:style="long" />';
|
|
end;
|
|
if Elements[el].IntValue < 0 then
|
|
timeIntervalStr := ' number:truncate-on-overflow="false"';
|
|
end;
|
|
|
|
nftMinute:
|
|
begin
|
|
case abs(Elements[el].IntValue) of
|
|
1: Result := Result + '<number:minutes />';
|
|
2: Result := Result + '<number:minutes number:style="long" />';
|
|
end;
|
|
if Elements[el].IntValue < 0 then
|
|
timeIntervalStr := ' number:truncate-on-overflow="false"';
|
|
end;
|
|
|
|
nftSecond:
|
|
begin
|
|
case abs(Elements[el].IntValue) of
|
|
1: Result := Result + '<number:seconds />';
|
|
2: Result := Result + '<number:seconds number:style="long" />';
|
|
end;
|
|
if Elements[el].IntValue < 0 then
|
|
timeIntervalStr := ' number:truncate-on-overflow="false"';
|
|
end;
|
|
|
|
nftMilliseconds:
|
|
; // ???
|
|
|
|
nftAMPM:
|
|
Result := Result + '<number:am-pm />';
|
|
|
|
nftDateTimeSep:
|
|
case Elements[el].TextValue of
|
|
'/': Result := Result + '<number:text>' + FFormatSettings.DateSeparator + '</number:text>';
|
|
':': Result := Result + '<number:text>' + FFormatSettings.TimeSeparator + '</number:text>';
|
|
' ': Result := Result + '<number:text><![CDATA[ ]]></number:text>';
|
|
else Result := Result + '<number:text>' + Elements[el].TextValue + '</number:text>';
|
|
end;
|
|
|
|
nftTextFormat:
|
|
Result := Result + '<number:text-content />';
|
|
end;
|
|
|
|
inc(el);
|
|
end;
|
|
|
|
if (nfkPercent in Kind) then
|
|
mask := '<number:percentage-style style:name="%s"%s>%s%s</number:percentage-style>'
|
|
else
|
|
if (nfkCurrency in Kind) then
|
|
mask := '<number:currency-style style:name="%s"%s>%s%s</number:currency-style>'
|
|
else
|
|
if (nfkDate in Kind) then
|
|
mask := '<number:date-style style:name="%s"%s>%s%s</number:date-style>'
|
|
else
|
|
if (Kind * [nfkDate, nfkTime] = [nfkTime]) then
|
|
mask := '<number:time-style style:name="%s"%s>%s%s</number:time-style>'
|
|
else
|
|
if (Kind = [nfkText]) then
|
|
mask := '<number:text-style style:name="%s"%s>%s%s</number:text-style>'
|
|
else
|
|
mask := '<number:number-style style:name="%s"%s>%s%s</number:number-style>';
|
|
|
|
Result := Format(mask, [AFormatName, TimeIntervalStr, Result, StyleMapStr]);
|
|
end;
|
|
end;
|
|
|
|
|
|
{******************************************************************************}
|
|
{ TsSpreadOpenDocReader }
|
|
{******************************************************************************}
|
|
|
|
constructor TsSpreadOpenDocReader.Create(AWorkbook: TsWorkbook);
|
|
begin
|
|
inherited Create(AWorkbook);
|
|
|
|
// http://en.wikipedia.org/wiki/List_of_spreadsheet_software#Specifications
|
|
FLimitations.MaxColCount := 1024;
|
|
FLimitations.MaxRowCount := 1048576;
|
|
|
|
FPointSeparatorSettings := DefaultFormatSettings;
|
|
FPointSeparatorSettings.DecimalSeparator := '.';
|
|
FPointSeparatorSettings.ListSeparator := ';'; // for formulas
|
|
|
|
FCellFormatList := TsCellFormatList.Create(true);
|
|
// true = allow duplicates because style names used in cell records will not be found any more.
|
|
|
|
FTableStyleList := TFPList.Create;
|
|
FColumnStyleList := TFPList.Create;
|
|
FColumnList := TFPList.Create;
|
|
FRowStyleList := TFPList.Create;
|
|
FRowList := TFPList.Create;
|
|
FPageLayoutList := TFPList.Create;
|
|
FMasterPageList := TFPList.Create;
|
|
FHeaderFooterFontList := TObjectList.Create; // frees objects
|
|
FFontFaces := TStringList.Create;
|
|
FRichTextFontList := TFPList.Create;
|
|
|
|
FRepeatedRows.FirstIndex := UNASSIGNED_ROW_COL_INDEX;
|
|
FRepeatedRows.LastIndex := UNASSIGNED_ROW_COL_INDEX;
|
|
FRepeatedCols.FirstIndex := UNASSIGNED_ROW_COL_INDEX;
|
|
FRepeatedCols.LastIndex := UNASSIGNED_ROW_COL_INDEX;
|
|
|
|
// Initial base date in case it won't be read from file
|
|
FDateMode := dmODS1899;
|
|
end;
|
|
|
|
destructor TsSpreadOpenDocReader.Destroy;
|
|
var
|
|
j: integer;
|
|
begin
|
|
FFontFaces.Free;
|
|
|
|
for j := FRichTextFontList.Count-1 downto 0 do TObject(FRichTextFontList[j]).Free;
|
|
FreeAndNil(FRichTextFontList);
|
|
|
|
for j := FColumnList.Count-1 downto 0 do TObject(FColumnList[j]).Free;
|
|
FColumnList.Free;
|
|
|
|
for j := FTableStyleList.Count-1 downto 0 do TObject(FTableStyleList[j]).Free;
|
|
FTableStyleList.Free;
|
|
|
|
for j := FColumnStyleList.Count-1 downto 0 do TObject(FColumnStyleList[j]).Free;
|
|
FColumnStyleList.Free;
|
|
|
|
for j := FRowList.Count-1 downto 0 do TObject(FRowList[j]).Free;
|
|
FRowList.Free;
|
|
|
|
for j := FRowStyleList.Count-1 downto 0 do TObject(FRowStyleList[j]).Free;
|
|
FRowStyleList.Free;
|
|
|
|
for j := FPageLayoutList.Count-1 downto 0 do TObject(FPageLayoutList[j]).Free;
|
|
FPageLayoutList.Free;
|
|
|
|
for j := FMasterPageList.Count-1 downto 0 do TObject(FMasterPageList[j]).Free;
|
|
FMasterPageList.Free;
|
|
|
|
FHeaderFooterFontList.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.AddBuiltinNumFormats;
|
|
begin
|
|
FNumFormatList.Clear;
|
|
FNumFormatList.Add('N0:');
|
|
end;
|
|
|
|
{ Creates for each non-default column width stored internally in FColumnList
|
|
a TCol record in the current worksheet. }
|
|
procedure TsSpreadOpenDocReader.ApplyColWidths;
|
|
var
|
|
colIndex: Integer;
|
|
colData: TColumnData;
|
|
colStyleIndex: Integer;
|
|
colStyle: TColumnStyleData;
|
|
i: Integer;
|
|
defColWidth: Single;
|
|
colWidth: Single;
|
|
colWidthType: TsColWidthType;
|
|
lastOccCol: Integer;
|
|
begin
|
|
defColWidth := FWorksheet.ReadDefaultColWidth(FWorkbook.Units);
|
|
lastOccCol := FWorksheet.GetLastOccupiedColIndex;
|
|
for i:=0 to FColumnList.Count-1 do
|
|
begin
|
|
colData := TColumnData(FColumnList[i]);
|
|
colIndex := colData.Col;
|
|
|
|
// Skip column records beyond the last data column - there's a bug in OO/LO
|
|
// which adds column records up to the max column limit.
|
|
if colIndex > lastOccCol then
|
|
Continue;
|
|
|
|
colStyleIndex := colData.ColStyleIndex;
|
|
colStyle := TColumnStyleData(FColumnStyleList[colStyleIndex]);
|
|
//defCellStyleIndex := colData.DefaultCellStyleIndex;
|
|
{
|
|
// Get column format
|
|
fmt := FCellFormatList.Items[defCellStyleIndex];
|
|
if fmt <> nil then
|
|
fmtIndex := FWorkbook.AddCellFormat(fmt^)
|
|
else
|
|
fmtIndex := 0;
|
|
}
|
|
// Prepare column record for the worksheet
|
|
colWidth := colStyle.ColWidth; // is already in workbook units
|
|
if SameValue(colWidth, defColWidth, COLWIDTH_EPS) then
|
|
colWidthType := cwtDefault
|
|
else
|
|
colWidthType := cwtCustom;
|
|
|
|
// Write non-default column width to the worksheet
|
|
if (colWidthType = cwtCustom) then
|
|
FWorksheet.WriteColWidth(colIndex, colWidth, FWorkbook.Units);
|
|
|
|
// Note: we don't store the column format index here; this is done in the
|
|
// row/cell reading method (ReadRowsAndCells).
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.ExtractFormatIndexFromStyle(ACellStyleName: String;
|
|
ACol: Integer): Integer;
|
|
var
|
|
idx: Integer;
|
|
begin
|
|
Result := -1;
|
|
if ACellStyleName <> '' then
|
|
Result := FCellFormatList.FindIndexOfName(ACellStyleName);
|
|
if Result = -1 then begin
|
|
idx := FindColumnByCol(ACol);
|
|
if idx > -1 then
|
|
Result := TColumnData(FColumnList[idx]).DefaultCellStyleIndex;
|
|
end;
|
|
if Result = -1 then
|
|
Result := 0;
|
|
end;
|
|
|
|
|
|
procedure TsSpreadOpenDocReader.ApplyStyleToCell(ACell: PCell; AStyleIndex: Integer);
|
|
var
|
|
fmt: TsCellFormat;
|
|
begin
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorksheet.WriteFont(ACell, HYPERLINK_FONTINDEX);
|
|
|
|
fmt := FCellFormatList.Items[AStyleIndex]^;
|
|
if (AStyleIndex = 0) and FWorksheet.HasHyperlink(ACell) then begin
|
|
// Make sure to use hyperlink font for hyperlink cells in case of default cell style
|
|
fmt.FontIndex := HYPERLINK_FONTINDEX;
|
|
Include(fmt.UsedFormattingFields, uffFont);
|
|
end;
|
|
ACell^.FormatIndex := FWorkbook.AddCellFormat(fmt);
|
|
end;
|
|
|
|
{ Applies the style data referred to by the style name to the specified cell
|
|
The function result is false if a style with the given name could not be found }
|
|
function TsSpreadOpenDocReader.ApplyStyleToCell(ACell: PCell; AStyleName: String): Boolean;
|
|
var
|
|
fmt: TsCellFormat;
|
|
styleIndex: Integer;
|
|
//i: Integer;
|
|
begin
|
|
Result := false;
|
|
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorksheet.WriteFont(ACell, HYPERLINK_FONTINDEX);
|
|
|
|
styleIndex := ExtractFormatIndexFromStyle(AStyleName, ACell^.Col);
|
|
(*
|
|
// Is there a style attached to the cell?
|
|
styleIndex := -1;
|
|
if AStyleName <> '' then
|
|
styleIndex := FCellFormatList.FindIndexOfName(AStyleName);
|
|
if (styleIndex = -1) then
|
|
begin
|
|
// No - look for the style attached to the column of the cell and
|
|
// find the cell style by the DefaultCellStyleIndex stored in the column list.
|
|
i := FindColumnByCol(ACell^.Col);
|
|
if i = -1 then
|
|
exit;
|
|
styleIndex := TColumnData(FColumnList[i]).DefaultCellStyleIndex;
|
|
end;
|
|
*)
|
|
|
|
fmt := FCellFormatList.Items[styleIndex]^;
|
|
if (styleIndex = 0) and FWorksheet.HasHyperlink(ACell) then
|
|
begin
|
|
// Make sure to use hyperlink font for hyperlink cells in case of default cell style
|
|
fmt.FontIndex := HYPERLINK_FONTINDEX;
|
|
Include(fmt.UsedFormattingFields, uffFont);
|
|
end;
|
|
ACell^.FormatIndex := FWorkbook.AddCellFormat(fmt);
|
|
|
|
Result := true;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.ApplyTableStyle(ASheet: TsWorksheet;
|
|
AStyleName: String): Boolean;
|
|
var
|
|
styleIndex: Integer;
|
|
tableStyle: TTableStyleData;
|
|
begin
|
|
Result := false;
|
|
if (AStyleName = '') or (ASheet = nil) then
|
|
exit;
|
|
styleIndex := FindTableStyleByName(AStyleName);
|
|
if styleIndex = -1 then
|
|
exit;
|
|
tableStyle := TTableStyleData(FTableStyleList[styleIndex]);
|
|
if (tableStyle.BiDiMode = bdRTL) or (tableStyle.BiDiMode = bdLTR) then
|
|
ASheet.BiDiMode := tableStyle.BiDiMode;
|
|
if tableStyle.Hidden then
|
|
ASheet.Options := ASheet.Options + [soHidden];
|
|
Result := true;
|
|
end;
|
|
|
|
|
|
{ Extracts a boolean value from a "boolean" cell node.
|
|
Is called from ReadBoolean }
|
|
function TsSpreadOpenDocReader.ExtractBoolFromNode(ANode: TDOMNode): Boolean;
|
|
var
|
|
value: String;
|
|
begin
|
|
value := GetAttrValue(ANode, 'office:boolean-value');
|
|
if (lowercase(value) = 'true') then
|
|
Result := true
|
|
else
|
|
Result := false;
|
|
end;
|
|
|
|
{ Extracts a date/time value from a "date-value" or "time-value" cell node.
|
|
Requires the number format and format strings to optimize agreement with
|
|
fpc date/time values.
|
|
Is called from "ReadDateTime". }
|
|
function TsSpreadOpenDocReader.ExtractDateTimeFromNode(ANode: TDOMNode;
|
|
ANumFormat: TsNumberFormat; const AFormatStr: String): TDateTime;
|
|
var
|
|
Value: String;
|
|
Fmt : TFormatSettings;
|
|
FoundPos : integer;
|
|
Hours, Minutes, Days: integer;
|
|
Seconds: Double;
|
|
HoursPos, MinutesPos, SecondsPos: integer;
|
|
begin
|
|
Unused(AFormatStr);
|
|
|
|
// Format expects ISO 8601 type date string or time string
|
|
fmt := DefaultFormatSettings;
|
|
fmt.ShortDateFormat := 'yyyy-mm-dd';
|
|
fmt.DateSeparator := '-';
|
|
fmt.LongTimeFormat := 'hh:nn:ss';
|
|
fmt.TimeSeparator := ':';
|
|
|
|
Value := GetAttrValue(ANode, 'office:date-value');
|
|
|
|
if Value <> '' then
|
|
begin
|
|
// Date or date/time string
|
|
Value := StringReplace(Value,'T',' ',[rfIgnoreCase,rfReplaceAll]);
|
|
// Strip milliseconds?
|
|
FoundPos := Pos('.',Value);
|
|
if (FoundPos > 1) then
|
|
Value := Copy(Value, 1, FoundPos-1);
|
|
Result := StrToDateTime(Value, Fmt);
|
|
|
|
// If the date/time is within 1 day of the base date the value is most
|
|
// probably a time-only value (< 1).
|
|
// We need to subtract the datemode offset, otherwise the date/time value
|
|
// would not be < 1 for fpc.
|
|
case FDateMode of
|
|
dmODS1899: if Result - DATEMODE_1899_BASE < 1 then Result := Result - DATEMODE_1899_BASE;
|
|
dmODS1900: if Result - DATEMODE_1900_BASE < 1 then Result := Result - DATEMODE_1900_BASE;
|
|
dmODS1904: if Result - DATEMODE_1904_BASE < 1 then Result := Result - DATEMODE_1904_BASE;
|
|
end;
|
|
|
|
end else begin
|
|
// Try time only, e.g. PT23H59M59S
|
|
// 12345678901
|
|
Value := GetAttrValue(ANode, 'office:time-value');
|
|
if (Value <> '') and (Pos('PT', Value) = 1) then
|
|
begin
|
|
// Get hours
|
|
HoursPos := Pos('H', Value);
|
|
if (HoursPos > 0) then
|
|
Hours := StrToInt(Copy(Value, 3, HoursPos-3))
|
|
else
|
|
Hours := 0;
|
|
|
|
// Get minutes
|
|
MinutesPos := Pos('M', Value);
|
|
if (MinutesPos > 0) and (MinutesPos > HoursPos) then
|
|
Minutes := StrToInt(Copy(Value, HoursPos+1, MinutesPos-HoursPos-1))
|
|
else
|
|
Minutes := 0;
|
|
|
|
// Get seconds
|
|
SecondsPos := Pos('S', Value);
|
|
if (SecondsPos > 0) and (SecondsPos > MinutesPos) then
|
|
Seconds := StrToFloat(Copy(Value, MinutesPos+1, SecondsPos-MinutesPos-1), FPointSeparatorSettings)
|
|
else
|
|
Seconds := 0;
|
|
|
|
Days := Hours div 24;
|
|
Hours := Hours mod 24;
|
|
Result := Days + (Hours + (Minutes + Seconds/60)/60)/24;
|
|
|
|
{ Values < 1 day are certainly time-only formats --> no datemode correction
|
|
nfTimeInterval formats are differences --> no date mode correction
|
|
In all other case, we have a date part that needs to be corrected for
|
|
the file's datemode. }
|
|
if (ANumFormat <> nfTimeInterval) and (abs(Days) > 0) then
|
|
begin
|
|
case FDateMode of
|
|
dmODS1899: Result := Result + DATEMODE_1899_BASE;
|
|
dmODS1900: Result := Result + DATEMODE_1900_BASE;
|
|
dmODS1904: Result := Result + DATEMODE_1904_BASE;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.ExtractErrorFromNode(ANode: TDOMNode;
|
|
out AErrorValue: TsErrorValue): Boolean;
|
|
var
|
|
s: String;
|
|
begin
|
|
s := GetAttrValue(ANode, 'table:formula');
|
|
if s = '' then
|
|
begin
|
|
AErrorValue := errOK;
|
|
Result := true;
|
|
exit;
|
|
end;
|
|
if pos('of:', s) = 1 then Delete(s, 1, 3);
|
|
Delete(s, 1, 1); // Delete '='
|
|
if s = '' then
|
|
begin
|
|
AErrorValue := errOK;
|
|
Result := true;
|
|
exit;
|
|
end;
|
|
|
|
Result := TryStrToErrorValue(s, AErrorValue);
|
|
if not Result then
|
|
begin
|
|
s := ANode.NodeName;
|
|
ANode:= ANode.FirstChild;
|
|
while Assigned(ANode) do
|
|
begin
|
|
s := ANode.NodeName;
|
|
if s = 'text:p' then
|
|
begin
|
|
s := GetNodeValue(ANode);
|
|
Result := TryStrToErrorValue(s, AErrorValue);
|
|
exit;
|
|
end;
|
|
ANode := ANode.NextSibling;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.FindColumnByCol(AColIndex: Integer): Integer;
|
|
begin
|
|
for Result := 0 to FColumnList.Count-1 do
|
|
if TColumnData(FColumnList[Result]).Col = AColIndex then
|
|
exit;
|
|
Result := -1;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.FindColStyleByName(AStyleName: String): Integer;
|
|
begin
|
|
for Result := 0 to FColumnStyleList.Count-1 do
|
|
if TColumnStyleData(FColumnStyleList[Result]).Name = AStyleName then
|
|
exit;
|
|
Result := -1;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.FindNumFormatByName(ANumFmtName: String): Integer;
|
|
begin
|
|
for Result := 0 to FNumFormatList.Count-1 do
|
|
if pos(ANumFmtName+':', FNumFormatList[Result]) = 1 then
|
|
exit;
|
|
Result := -1;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.FindRowStyleByName(AStyleName: String): Integer;
|
|
begin
|
|
for Result := 0 to FRowStyleList.Count-1 do
|
|
if TRowStyleData(FRowStyleList[Result]).Name = AStyleName then
|
|
exit;
|
|
Result := -1;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.FindTableStyleByName(AStyleName: String): Integer;
|
|
begin
|
|
for Result := 0 to FTableStyleList.Count-1 do
|
|
if TTableStyleData(FTableStyleList[Result]).Name = AStyleName then
|
|
exit;
|
|
Result := -1;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadAutomaticStyles(AStylesNode: TDOMNode);
|
|
var
|
|
nodeName: String;
|
|
layoutNode, fontNode: TDOMNode;
|
|
node, child, childchild: TDOMNode;
|
|
s: String;
|
|
href: String;
|
|
imgpos: String;
|
|
data: TPageLayoutData;
|
|
isHeader: Boolean;
|
|
h, dist: Double;
|
|
fnt: TXMLHeaderFooterFont;
|
|
defFnt: TsFont;
|
|
fntName: String;
|
|
fntSize: Double;
|
|
fntStyle: TsHeaderFooterFontStyles;
|
|
fntColor: TsColor;
|
|
n: Integer;
|
|
hfs: TsHeaderFooterSectionIndex;
|
|
begin
|
|
if not Assigned(AStylesNode) then
|
|
exit;
|
|
defFnt := Workbook.GetDefaultFont;
|
|
layoutNode := AStylesNode.FirstChild;
|
|
while layoutNode <> nil do
|
|
begin
|
|
nodeName := layoutNode.NodeName;
|
|
if nodeName = 'style:style' then
|
|
begin
|
|
// Read fonts used by page layout's header/footer
|
|
fntName := defFnt.FontName;
|
|
fntSize := defFnt.Size;
|
|
fntColor := defFnt.Color;
|
|
fntStyle := [];
|
|
s := GetAttrValue(layoutNode, 'style:family');
|
|
if s = 'text' then
|
|
begin
|
|
s := GetAttrValue(layoutNode, 'style:name');
|
|
fontNode := layoutNode.FirstChild;
|
|
ReadHeaderFooterFont(fontNode, fntName, fntSize, fntStyle, fntColor);
|
|
fnt := TXMLHeaderFooterFont.Create(fntName, fntSize, fntStyle, fntColor);
|
|
fnt.StyleName := s;
|
|
FHeaderFooterFontList.Add(fnt);
|
|
end;
|
|
end
|
|
else
|
|
if nodeName = 'style:page-layout' then
|
|
begin
|
|
// Read page layout parameters
|
|
data := TPageLayoutData.Create;
|
|
data.Name := GetAttrValue(layoutNode, 'style:name');
|
|
|
|
node := layoutNode.FirstChild;
|
|
while node <> nil do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
if nodeName = 'style:page-layout-properties' then
|
|
begin
|
|
s := GetAttrValue(node, 'style:print-orientation');
|
|
if s = 'landscape' then
|
|
data.PageLayout.Orientation := spoLandscape
|
|
else if s = 'portrait' then
|
|
data.PageLayout.Orientation := spoPortrait;
|
|
|
|
s := GetAttrValue(node, 'fo:page-width');
|
|
if s <> '' then
|
|
data.PageLayout.PageWidth := PtsToMM(HTMLLengthStrToPts(s));
|
|
|
|
s := GetAttrValue(node, 'fo:page-height');
|
|
if s <> '' then
|
|
data.PageLayout.PageHeight := PtsToMM(HTMLLengthStrToPts(s));
|
|
|
|
s := GetAttrValue(node, 'fo:margin-top');
|
|
if s <> '' then
|
|
data.PageLayout.TopMargin := PtsToMM(HTMLLengthStrToPts(s));
|
|
// But: if there is a header this value is the headermargin!
|
|
|
|
s := GetAttrValue(node, 'fo:margin-bottom');
|
|
if s <> '' then
|
|
data.PageLayout.BottomMargin := PtsToMM(HTMLLengthStrToPts(s));
|
|
// But: if there is a footer this value is the footermargin!
|
|
|
|
s := GetAttrValue(node, 'fo:margin-left');
|
|
if s <> '' then
|
|
data.PageLayout.LeftMargin := PtsToMM(HTMLLengthStrToPts(s));
|
|
|
|
s := GetAttrValue(node, 'fo:margin-right');
|
|
if s <> '' then
|
|
data.PageLayout.RightMargin := PtsToMM(HTMLLengthStrToPts(s));
|
|
|
|
s := GetAttrValue(node, 'style:scale-to');
|
|
if (s <> '') then
|
|
begin
|
|
if s[Length(s)] = '%' then Delete(s, Length(s), 1);
|
|
data.PageLayout.ScalingFactor := round(StrToFloat(s, FPointSeparatorSettings));
|
|
with data.PageLayout do Options := Options - [poFitPages];
|
|
end;
|
|
|
|
s := GetAttrValue(node, 'style:scale-to-X');
|
|
if s <> '' then
|
|
begin
|
|
data.PageLayout.FitWidthToPages := StrToInt(s);
|
|
with data.PageLayout do Options := Options + [poFitPages];
|
|
end;
|
|
|
|
s := GetAttrValue(node, 'style:scale-to-Y');
|
|
if s <> '' then
|
|
begin
|
|
data.PageLayout.FitHeightToPages := StrToInt(s);
|
|
with data.PageLayout do Options := Options + [poFitPages];
|
|
end;
|
|
|
|
s := GetAttrValue(node, 'style:table-centering');
|
|
case s of
|
|
'both':
|
|
with data.PageLayout do Options := Options + [poHorCentered, poVertCentered];
|
|
'horizontal':
|
|
with data.PageLayout do Options := Options + [poHorCentered] - [poVertCentered];
|
|
'vertical':
|
|
with data.PageLayout do Options := Options - [poHorCentered] + [poVertCentered];
|
|
end;
|
|
|
|
s := GetAttrValue(node, 'style:print');
|
|
if pos('grid', s) > 0 then
|
|
with data.PageLayout do Options := Options + [poPrintGridLines];
|
|
if pos('headers', s) > 0 then
|
|
with data.PageLayout do Options := Options + [poPrintHeaders];
|
|
if pos('annotations', s) > 0 then
|
|
with data.PageLayout do Options := Options + [poPrintCellComments];
|
|
|
|
s := GetAttrValue(node, 'style:print-page-order');
|
|
if s = 'ltr' then // "left-to-right", the other option is "ttb = top-to-bottom"
|
|
with data.PageLayout do Options := Options + [poPrintPagesByRows];
|
|
|
|
s := GetAttrValue(node, 'style:first-page-number');
|
|
if s = 'continue' then
|
|
with Data.PageLayout do Options := Options - [poUseStartPageNumber]
|
|
else
|
|
if TryStrToInt(s, n) then
|
|
data.PageLayout.StartPageNumber := n;
|
|
// Sets poUseStartPageNumber automatically
|
|
|
|
FPageLayoutList.Add(data);
|
|
end else
|
|
if (nodeName = 'style:header-style') or (nodeName = 'style:footer-style')
|
|
then
|
|
begin
|
|
isHeader := nodeName = 'style:header-style';
|
|
child := node.FirstChild;
|
|
while child <> nil do
|
|
begin
|
|
nodeName := child.NodeName;
|
|
if nodeName = 'style:header-footer-properties' then
|
|
begin
|
|
h := 0;
|
|
dist := 0;
|
|
s := GetAttrValue(child, 'svg:height');
|
|
if s <> '' then
|
|
h := PtsToMM(HTMLLengthStrToPts(s))
|
|
else begin
|
|
s := GetAttrValue(child, 'fo:min-height');
|
|
if s <> '' then
|
|
h := PtsToMM(HTMLLengthStrToPts(s)) else h := 0;
|
|
end;
|
|
if isHeader then
|
|
s := GetAttrValue(child, 'fo:margin-bottom') else
|
|
s := GetAttrValue(child, 'fo:margin-top');
|
|
if s <> '' then
|
|
dist := PtsToMM(HTMLLengthStrToPts(s));
|
|
if isHeader then
|
|
begin
|
|
data.PageLayout.HeaderMargin := h + dist;
|
|
// Note: TopMargin and HeaderMargin are not yet the same as in Excel
|
|
// Will be fixed in ReadMasterStyles where it will be known
|
|
// whether the header is displayed.
|
|
end else
|
|
begin
|
|
data.Pagelayout.FooterMargin := h + dist;
|
|
end;
|
|
end;
|
|
childchild := child.FirstChild;
|
|
while Assigned(childchild) do
|
|
begin
|
|
nodeName := childchild.NodeName;
|
|
if nodeName = 'style:background-image' then
|
|
begin
|
|
href := GetAttrValue(childchild, 'xlink:href');
|
|
imgpos := GetAttrValue(childchild, 'style:position');
|
|
if (href <> '') and (imgpos <> '') then
|
|
begin
|
|
n := FWorkbook.FindEmbeddedObj(ExtractFileName(href));
|
|
if n > -1 then
|
|
begin
|
|
if pos('left', imgpos) > 0 then hfs := hfsLeft else
|
|
if pos('right', imgpos) > 0 then hfs := hfsRight else
|
|
hfs := hfsCenter;
|
|
if isHeader then
|
|
data.PageLayout.AddHeaderImage(HEADER_FOOTER_INDEX_ALL, hfs, n) else
|
|
data.PageLayout.AddFooterImage(HEADER_FOOTER_INDEX_ALL, hfs, n);
|
|
end;
|
|
end;
|
|
end;
|
|
childchild := childchild.NextSibling;
|
|
end;
|
|
child := child.NextSibling;
|
|
end;
|
|
end;
|
|
node := node.NextSibling;
|
|
end;
|
|
end;
|
|
layoutNode := layoutNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocReader.ReadHeaderFooterText(ANode: TDOMNode): String;
|
|
var
|
|
parser: TsSpreadOpenDocHeaderFooterParser;
|
|
defFnt: TsHeaderFooterFont;
|
|
begin
|
|
defFnt := TsHeaderFooterFont.Create(Workbook.GetDefaultFont);
|
|
parser := TsSpreadOpenDocHeaderFooterParser.Create(ANode.FirstChild,
|
|
FHeaderFooterFontList, defFnt);
|
|
try
|
|
Result := parser.BuildHeaderFooter;
|
|
finally
|
|
parser.Free;
|
|
defFnt.Free;
|
|
end;
|
|
end;
|
|
|
|
{ Reads the master styles nodes which contain the header/footer texts }
|
|
procedure TsSpreadOpenDocReader.ReadMasterStyles(AStylesNode: TDOMNode);
|
|
var
|
|
masternode, stylenode: TDOMNode;
|
|
nodeName: String;
|
|
s: String;
|
|
data: TMasterPageData;
|
|
pagelayout: TsPageLayout;
|
|
j: Integer;
|
|
h: Double;
|
|
hfs: TsHeaderFooterSectionIndex;
|
|
hfnew: Array[TsHeaderFooterSectionIndex] of string;
|
|
|
|
begin
|
|
if AStylesNode = nil then
|
|
exit;
|
|
|
|
masterNode := AStylesNode.FirstChild;
|
|
while (masterNode <> nil) do
|
|
begin
|
|
nodeName := masterNode.NodeName;
|
|
if nodeName = 'style:master-page' then begin
|
|
s := GetAttrvalue(masterNode, 'style:page-layout-name');
|
|
|
|
{ Find the page layout data belonging to the current node }
|
|
pageLayout := nil;
|
|
for j:=0 to FPageLayoutList.Count-1 do
|
|
if TPageLayoutData(FPageLayoutList[j]).Name = s then
|
|
begin
|
|
pageLayout := TPageLayoutData(FPageLayoutList[j]).PageLayout;
|
|
break;
|
|
end;
|
|
if pagelayout = nil then
|
|
exit;
|
|
|
|
data := TMasterPageData.Create;
|
|
data.Name := GetAttrValue(masternode, 'style:name');
|
|
data.PageLayoutName := s;
|
|
FMasterPageList.Add(data);
|
|
|
|
styleNode := masterNode.FirstChild;
|
|
while styleNode <> nil do begin
|
|
nodeName := styleNode.NodeName;
|
|
if nodeName = 'style:header' then
|
|
begin
|
|
s := ReadHeaderFooterText(styleNode);
|
|
if s <> '' then
|
|
begin
|
|
// If the header contains an image add the code &G.
|
|
with pagelayout do begin
|
|
SplitHeaderFooterText(s,
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
for hfs in TsHeaderFooterSectionIndex do
|
|
if HeaderImages[hfs].Index > -1 then hfnew[hfs] := '&G' + hfnew[hfs];
|
|
Headers[HEADER_FOOTER_INDEX_ODD] := JoinHeaderFooterText(
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
end;
|
|
end;
|
|
s := GetAttrValue(styleNode, 'style:display');
|
|
if s <> 'false' then
|
|
begin
|
|
h := pageLayout.HeaderMargin;
|
|
pagelayout.HeaderMargin := pageLayout.TopMargin;
|
|
pagelayout.TopMargin := pageLayout.TopMargin + h;
|
|
end;
|
|
end else
|
|
if nodeName = 'style:header-left' then
|
|
begin
|
|
s := ReadHeaderFooterText(styleNode);
|
|
if s <> '' then
|
|
begin
|
|
// If the header contains an image add the code &G.
|
|
with pagelayout do begin
|
|
SplitHeaderFooterText(s,
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
for hfs in TsHeaderFooterSectionIndex do
|
|
if HeaderImages[hfs].Index > -1 then hfnew[hfs] := '&G' + hfnew[hfs];
|
|
Headers[HEADER_FOOTER_INDEX_ODD] := JoinHeaderFooterText(
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
Options := Options + [poDifferentOddEven];
|
|
end;
|
|
end;
|
|
s := GetAttrValue(styleNode, 'style:display');
|
|
if s = 'false' then
|
|
pageLayout.Options := pagelayout.Options - [poDifferentOddEven]
|
|
else begin
|
|
h := pageLayout.HeaderMargin;
|
|
pageLayout.HeaderMargin := pageLayout.TopMargin;
|
|
pagelayout.TopMargin := pageLayout.TopMargin + h;
|
|
end;
|
|
end else
|
|
if nodeName = 'style:footer' then
|
|
begin
|
|
s := ReadHeaderFooterText(styleNode);
|
|
if s <> '' then
|
|
with pagelayout do begin
|
|
SplitHeaderFooterText(s,
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
for hfs in TsHeaderFooterSectionIndex do
|
|
if FooterImages[hfs].Index > -1 then hfnew[hfs] := '&G' + hfnew[hfs];
|
|
Footers[HEADER_FOOTER_INDEX_ODD] := JoinHeaderFooterText(
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
end;
|
|
s := GetAttrValue(styleNode, 'style:display');
|
|
if s <> 'false' then
|
|
begin
|
|
h := pageLayout.FooterMargin;
|
|
pageLayout.FooterMargin := pageLayout.BottomMargin;
|
|
pageLayout.BottomMargin := pageLayout.BottomMargin + h;
|
|
end;
|
|
end else
|
|
if nodeName = 'style:footer-left' then
|
|
begin
|
|
s := ReadHeaderFooterText(styleNode);
|
|
if s <> '' then
|
|
begin
|
|
with pagelayout do begin
|
|
SplitHeaderFooterText(s,
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
for hfs in TsHeaderFooterSectionIndex do
|
|
if FooterImages[hfs].Index > -1 then hfnew[hfs] := '&G' + hfnew[hfs];
|
|
Footers[HEADER_FOOTER_INDEX_EVEN] := JoinHeaderFooterText(
|
|
hfnew[hfsLeft], hfnew[hfsCenter], hfnew[hfsRight]);
|
|
Options := Options + [poDifferentOddEven];
|
|
end;
|
|
end;
|
|
s := GetAttrValue(styleNode, 'style:display');
|
|
if s = 'false' then
|
|
pagelayout.Options := pagelayout.Options - [poDifferentOddEven]
|
|
else begin
|
|
h := pagelayout.FooterMargin;
|
|
pagelayout.FooterMargin := pagelayout.BottomMargin;
|
|
pagelayout.BottomMargin := pagelayout.BottomMargin + h;
|
|
end;
|
|
end;
|
|
styleNode := styleNode.NextSibling;
|
|
end;
|
|
end;
|
|
masterNode := masterNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadBlank(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
cell: PCell;
|
|
begin
|
|
Unused(ACellNode);
|
|
|
|
// No need to store a record for an empty, unformatted cell
|
|
if AStyleIndex = 0 then
|
|
exit;
|
|
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.AddCell(ARow, ACol);
|
|
FWorkSheet.WriteBlank(cell);
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadBoolean(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
// styleName: String;
|
|
cell: PCell;
|
|
boolValue: Boolean;
|
|
begin
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.AddCell(ARow, ACol);
|
|
|
|
boolValue := ExtractBoolFromNode(ACellNode);
|
|
FWorkSheet.WriteBoolValue(cell, boolValue);
|
|
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
{
|
|
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
|
ApplyStyleToCell(cell, stylename);
|
|
}
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
{ Collection columns used in the given table. The columns contain links to
|
|
styles that must be used when cells in that columns are without styles. }
|
|
procedure TsSpreadOpenDocReader.ReadColumns(ATableNode: TDOMNode);
|
|
var
|
|
col: Integer;
|
|
colNode, childnode: TDOMNode;
|
|
nodeName: String;
|
|
i: Integer;
|
|
|
|
procedure ProcessCol(AColNode: TDOMNode);
|
|
var
|
|
s: String;
|
|
colStyleIndex: Integer;
|
|
colData: TColumnData;
|
|
defCellStyleIndex: Integer;
|
|
colsRepeated: Integer;
|
|
j: Integer;
|
|
begin
|
|
s := GetAttrValue(AColNode, 'table:style-name');
|
|
colStyleIndex := FindColStyleByName(s);
|
|
if colStyleIndex <> -1 then
|
|
begin
|
|
defCellStyleIndex := -1;
|
|
s := GetAttrValue(AColNode, 'table:default-cell-style-name');
|
|
if s <> '' then
|
|
begin
|
|
defCellStyleIndex := FCellFormatList.FindIndexOfName(s); //FindCellStyleByName(s);
|
|
colData := TColumnData.Create;
|
|
colData.Col := col;
|
|
colData.ColStyleIndex := colStyleIndex;
|
|
colData.DefaultCellStyleIndex := defCellStyleIndex;
|
|
FColumnList.Add(colData);
|
|
end;
|
|
|
|
s := GetAttrValue(AColNode, 'table:number-columns-repeated');
|
|
if s = '' then
|
|
inc(col)
|
|
else
|
|
begin
|
|
colsRepeated := StrToInt(s);
|
|
if defCellStyleIndex > -1 then begin
|
|
for j:=1 to colsRepeated-1 do
|
|
begin
|
|
colData := TColumnData.Create;
|
|
colData.Col := col + j;
|
|
colData.ColStyleIndex := colStyleIndex;
|
|
colData.DefaultCellStyleIndex := defCellStyleIndex;
|
|
FColumnList.Add(colData);
|
|
end;
|
|
end;
|
|
inc(col, colsRepeated);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
// clear previous column list (from other sheets)
|
|
for i := FColumnList.Count-1 downto 0 do TObject(FColumnList[i]).Free;
|
|
FColumnList.Clear;
|
|
|
|
col := 0;
|
|
colNode := ATableNode.FirstChild;
|
|
while Assigned(colNode) do
|
|
begin
|
|
nodename := colNode.NodeName;
|
|
if nodeName = 'table:table-header-columns' then
|
|
begin
|
|
if FRepeatedCols.FirstIndex = cardinal(UNASSIGNED_ROW_COL_INDEX) then
|
|
FRepeatedCols.FirstIndex := col;
|
|
childnode := colNode.FirstChild;
|
|
while Assigned(childnode) do
|
|
begin
|
|
ProcessCol(childnode);
|
|
childnode := childnode.NextSibling;
|
|
end;
|
|
FRepeatedCols.LastIndex := col-1;
|
|
end
|
|
else
|
|
if nodeName = 'table:table-column' then
|
|
ProcessCol(colnode);
|
|
colNode := colNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
{ Reads the column styles and stores them in the FColumnStyleList for later use }
|
|
procedure TsSpreadOpenDocReader.ReadColumnStyle(AStyleNode: TDOMNode);
|
|
var
|
|
colStyle: TColumnStyleData;
|
|
styleName: String;
|
|
styleChildNode: TDOMNode;
|
|
colWidth: double;
|
|
s: String;
|
|
begin
|
|
styleName := GetAttrValue(AStyleNode, 'style:name');
|
|
styleChildNode := AStyleNode.FirstChild;
|
|
colWidth := -1;
|
|
|
|
while Assigned(styleChildNode) do
|
|
begin
|
|
if styleChildNode.NodeName = 'style:table-column-properties' then
|
|
begin
|
|
s := GetAttrValue(styleChildNode, 'style:column-width');
|
|
if s <> '' then
|
|
begin
|
|
colWidth := FWorkbook.ConvertUnits(HTMLLengthStrToPts(s), suPoints, FWorkbook.Units);
|
|
// convert to workbook units
|
|
break;
|
|
end;
|
|
end;
|
|
styleChildNode := styleChildNode.NextSibling;
|
|
end;
|
|
|
|
colStyle := TColumnStyleData.Create;
|
|
colStyle.Name := styleName;
|
|
colStyle.ColWidth := colWidth;
|
|
FColumnStyleList.Add(colStyle);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadComment(ARow, ACol: Cardinal;
|
|
ACellNode: TDOMNode);
|
|
var
|
|
cellChildNode, pNode, pChildNode: TDOMNode;
|
|
comment, line: String;
|
|
nodeName: String;
|
|
s: String;
|
|
found: Boolean;
|
|
begin
|
|
if ACellNode = nil then
|
|
exit;
|
|
|
|
comment := '';
|
|
found := false;
|
|
|
|
cellChildNode := ACellNode.FirstChild;
|
|
while cellChildNode <> nil do begin
|
|
nodeName := cellChildNode.NodeName;
|
|
if nodeName = 'office:annotation' then begin
|
|
pNode := cellChildNode.FirstChild;
|
|
while pNode <> nil do begin
|
|
nodeName := pNode.NodeName;
|
|
if nodeName = 'text:p' then
|
|
begin
|
|
line := '';
|
|
pChildNode := pNode.FirstChild;
|
|
while pChildNode <> nil do
|
|
begin
|
|
nodeName := pChildNode.NodeName;
|
|
if nodeName = '#text' then
|
|
begin
|
|
s := pChildNode.NodeValue;
|
|
line := IfThen(line = '', s, line + s);
|
|
found := true;
|
|
end else
|
|
if nodeName = 'text:span' then
|
|
begin
|
|
s := GetNodeValue(pChildNode);
|
|
line := IfThen(line = '', s, line + s);
|
|
found := true;
|
|
end;
|
|
pChildNode := pChildNode.NextSibling;
|
|
end;
|
|
comment := IfThen(comment = '', line, comment + LineEnding + line);
|
|
end;
|
|
pNode := pNode.NextSibling;
|
|
end;
|
|
end;
|
|
cellChildNode := cellChildNode.NextSibling;
|
|
end;
|
|
if found then
|
|
FWorksheet.WriteComment(ARow, ACol, comment);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadDateTime(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
dt: TDateTime;
|
|
// styleName: String;
|
|
cell: PCell;
|
|
fmt: PsCellFormat;
|
|
begin
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.AddCell(ARow, ACol);
|
|
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
//styleName := GetAttrValue(ACellNode, 'table:style-name');
|
|
//ApplyStyleToCell(cell, stylename);
|
|
fmt := FWorkbook.GetPointerToCellFormat(cell^.FormatIndex);;
|
|
|
|
dt := ExtractDateTimeFromNode(ACellNode, fmt^.NumberFormat, fmt^.NumberFormatStr);
|
|
FWorkSheet.WriteDateTime(cell, dt, fmt^.NumberFormat, fmt^.NumberFormatStr);
|
|
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadError(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
//styleName: String;
|
|
cell: PCell;
|
|
errValue: TsErrorValue;
|
|
begin
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.AddCell(ARow, ACol);
|
|
|
|
if ExtractErrorFromNode(ACellNode, errValue) then
|
|
FWorkSheet.WriteErrorValue(cell, errValue) else
|
|
FWorksheet.WriteText(cell, 'ERROR');
|
|
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
{
|
|
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
|
ApplyStyleToCell(cell, stylename);
|
|
}
|
|
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadDateMode(SpreadSheetNode: TDOMNode);
|
|
var
|
|
CalcSettingsNode, NullDateNode: TDOMNode;
|
|
NullDateSetting: string;
|
|
begin
|
|
// Default datemode for ODF:
|
|
NullDateSetting := '1899-12-30';
|
|
CalcSettingsNode := SpreadsheetNode.FindNode('table:calculation-settings');
|
|
if Assigned(CalcSettingsNode) then
|
|
begin
|
|
NullDateNode := CalcSettingsNode.FindNode('table:null-date');
|
|
if Assigned(NullDateNode) then
|
|
NullDateSetting := GetAttrValue(NullDateNode,'table:date-value');
|
|
end;
|
|
if NullDateSetting = '1899-12-30' then
|
|
FDateMode := dmODS1899
|
|
else if NullDateSetting = '1900-01-01' then
|
|
FDateMode := dmODS1900
|
|
else if NullDateSetting = '1904-01-01' then
|
|
FDateMode := dmODS1904
|
|
else
|
|
raise Exception.CreateFmt('Spreadsheet file corrupt: cannot handle null-date format %s', [NullDateSetting]);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadDocumentProtection(ANode: TDOMNode);
|
|
var
|
|
cinfo: TsCryptoInfo;
|
|
begin
|
|
if ANode = nil then
|
|
exit;
|
|
|
|
if GetAttrValue(ANode, 'table:structure-protected') = 'true' then
|
|
Workbook.Protection := Workbook.Protection + [bpLockStructure]
|
|
else
|
|
exit;
|
|
|
|
InitCryptoInfo(cinfo);
|
|
cinfo.PasswordHash := GetAttrValue(ANode, 'table:protection-key');
|
|
cinfo.Algorithm := StrToAlgorithm(GetAttrValue(ANode, 'table:protection-key-digest-algorithm'));
|
|
Workbook.CryptoInfo := cinfo;
|
|
end;
|
|
|
|
{ Reads font data from an xml node and returns the font elements. }
|
|
procedure TsSpreadOpenDocReader.ReadFont(ANode: TDOMNode; var AFontName: String;
|
|
var AFontSize: Single; var AFontStyle: TsFontStyles; var AFontColor: TsColor;
|
|
var AFontPosition: TsFontPosition);
|
|
const
|
|
EPS = 1E-6;
|
|
var
|
|
stylename, s, s1, s2: String;
|
|
i, p: Integer;
|
|
begin
|
|
if ANode = nil then
|
|
exit;
|
|
|
|
stylename := GetAttrValue(ANode, 'style:font-name');
|
|
if stylename <> '' then
|
|
// Look for the true font name in the FFontFaces list. The items in
|
|
// FFontfaces are "style name"|"font name" pairs.
|
|
for i:=0 to FFontFaces.Count-1 do
|
|
begin
|
|
p := pos('|', FFontFaces[i]);
|
|
if p > 0 then begin
|
|
s := copy(FFontfaces[i], 1, p-1); // The first part is the style name
|
|
if s = styleName then
|
|
begin
|
|
AFontName := copy(FFontfaces[i], p+1, MaxInt);
|
|
// the second part is the font name
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
// In all other case, leave the AFontName of the input untouched.
|
|
|
|
s := GetAttrValue(ANode, 'fo:font-size');
|
|
if s <> '' then
|
|
AFontSize := HTMLLengthStrToPts(s);
|
|
|
|
if GetAttrValue(ANode, 'fo:font-style') = 'italic' then
|
|
Include(AFontStyle, fssItalic);
|
|
|
|
if GetAttrValue(ANode, 'fo:font-weight') = 'bold' then
|
|
Include(AFontStyle, fssBold);
|
|
|
|
s := GetAttrValue(ANode, 'style:text-underline-style');
|
|
if not ((s = '') or (s = 'none')) then
|
|
Include(AFontStyle, fssUnderline);
|
|
|
|
s := GetAttrValue(ANode, 'style:text-line-through-style');
|
|
if s = '' then s := GetAttrValue(ANode, 'style:text-line-through-type');
|
|
if not ((s = '') or (s = 'none')) then
|
|
Include(AFontStyle, fssStrikeout);
|
|
|
|
{ The "style:text-position" attribute specifies whether text is positioned
|
|
above or below the baseline and defines the relative font height that is
|
|
used for this text. The attribute can have one or two values.
|
|
1st value: percentage of vertical text displacement, or "super" or "sub"
|
|
2nd value: percentage of font height used for text (optional) }
|
|
s := GetAttrValue(ANode, 'style:text-position');
|
|
if s <> '' then
|
|
begin
|
|
p := pos(' ', s);
|
|
if p > 0 then
|
|
begin
|
|
s1 := Copy(s, 1, p-1);
|
|
s2 := Copy(s, p+1, MaxInt);
|
|
end else
|
|
begin
|
|
s1 := s;
|
|
s2 := '100';
|
|
end;
|
|
if s1[Length(s1)] = '%' then SetLength(s1, Length(s1) - 1);
|
|
if s2[Length(s2)] = '%' then SetLength(s2, Length(s2) - 1);
|
|
if s1 = 'super' then
|
|
AFontPosition := fpSuperscript
|
|
else if s1 = 'sub' then
|
|
AFontPosition := fpSubscript
|
|
else if SameValue(StrToFloat(s1, FPointSeparatorSettings), 0.0, EPS) then
|
|
AFontPosition := fpNormal
|
|
else if s1[1] = '-' then
|
|
AFontPosition := fpSubScript;
|
|
end;
|
|
|
|
s := GetAttrValue(ANode, 'fo:color');
|
|
if s <> '' then
|
|
AFontColor := HTMLColorStrToColor(s);
|
|
end;
|
|
|
|
|
|
(*
|
|
{ Reads font data from an xml node, adds the font to the workbooks FontList
|
|
(if not yet contained), and returns the index in the font list.
|
|
If the font is a special font (such as DefaultFont, or HyperlinkFont) then
|
|
APreferredIndex defines the index under which the font should be stored in the
|
|
list. }
|
|
function TsSpreadOpenDocReader.ReadFont(ANode: TDOMnode;
|
|
APreferredIndex: Integer = -1): Integer;
|
|
var
|
|
fntName: String;
|
|
fntSize: Single;
|
|
fntStyles: TsFontStyles;
|
|
fntColor: TsColor;
|
|
fntPosition: TsFontPosition;
|
|
s: String;
|
|
i: Integer;
|
|
p: Integer;
|
|
begin
|
|
if ANode = nil then
|
|
begin
|
|
Result := 0;
|
|
exit;
|
|
end;
|
|
|
|
fntName := GetAttrValue(ANode, 'style:font-name');
|
|
if fntName = '' then
|
|
fntName := FWorkbook.GetDefaultFont.FontName
|
|
else
|
|
// Look for the true font name in the FFontFaces list. The items in
|
|
// FFontfaces are "style name"|"font name" pairs.
|
|
for i:=0 to FFontFaces.Count-1 do
|
|
begin
|
|
p := pos('|', FFontFaces[i]);
|
|
if p > 0 then begin
|
|
s := copy(FFontfaces[i], 1, p-1);
|
|
if s = fntName then
|
|
begin
|
|
fntName := copy(FFontfaces[i], p+1, MaxInt);
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
s := GetAttrValue(ANode, 'fo:font-size');
|
|
if s <> '' then
|
|
fntSize := HTMLLengthStrToPts(s)
|
|
else
|
|
fntSize := FWorkbook.GetDefaultFontSize;
|
|
|
|
fntStyles := [];
|
|
if GetAttrValue(ANode, 'fo:font-style') = 'italic' then
|
|
Include(fntStyles, fssItalic);
|
|
if GetAttrValue(ANode, 'fo:font-weight') = 'bold' then
|
|
Include(fntStyles, fssBold);
|
|
s := GetAttrValue(ANode, 'style:text-underline-style');
|
|
if not ((s = '') or (s = 'none')) then
|
|
Include(fntStyles, fssUnderline);
|
|
s := GetAttrValue(ANode, 'style:text-line-through-style');
|
|
if s = '' then s := GetAttrValue(ANode, 'style:text-line-through-type');
|
|
if not ((s = '') or (s = 'none')) then
|
|
Include(fntStyles, fssStrikeout);
|
|
|
|
fntPosition := fpNormal;
|
|
s := GetAttrValue(ANode, 'style:text-position');
|
|
if Length(s) >= 3 then
|
|
begin
|
|
if (s[3] = 'b') or (s[1] = '-') then
|
|
fntPosition := fpSubscript
|
|
else
|
|
fntPosition := fpSuperscript;
|
|
end;
|
|
|
|
s := GetAttrValue(ANode, 'fo:color');
|
|
if s <> '' then
|
|
fntColor := HTMLColorStrToColor(s)
|
|
else
|
|
fntColor := FWorkbook.GetDefaultFont.Color;
|
|
|
|
if APreferredIndex = 0 then
|
|
begin
|
|
FWorkbook.SetDefaultFont(fntName, fntSize);
|
|
Result := 0;
|
|
end else
|
|
if (APreferredIndex > -1) then
|
|
begin
|
|
FWorkbook.ReplaceFont(APreferredIndex, fntName, fntSize, fntStyles, fntColor, fntPosition);
|
|
Result := APreferredIndex;
|
|
end else
|
|
begin
|
|
Result := FWorkbook.FindFont(fntName, fntSize, fntStyles, fntColor, fntPosition);
|
|
if Result = -1 then
|
|
Result := FWorkbook.AddFont(fntName, fntSize, fntStyles, fntColor, fntPosition);
|
|
end;
|
|
end;
|
|
*)
|
|
{ Collects the fontnames associated with a style-name in the list FFontfaces.
|
|
stylenames and fontnames are packed into a single string using | as a
|
|
separator. }
|
|
procedure TsSpreadOpenDocReader.ReadFontFaces(ANode: TDOMNode);
|
|
var
|
|
faceNode: TDOMNode;
|
|
nodename: String;
|
|
stylename: String;
|
|
fontfamily: String;
|
|
begin
|
|
faceNode := ANode.FirstChild;
|
|
while Assigned(faceNode) do
|
|
begin
|
|
nodename := faceNode.NodeName;
|
|
if nodename = 'style:font-face' then
|
|
begin
|
|
stylename := GetAttrValue(faceNode, 'style:name');
|
|
fontfamily := GetAttrValue(faceNode, 'svg:font-family');
|
|
if FFontFaces.IndexOf(stylename + '|' + fontfamily) = -1 then
|
|
FFontFaces.Add(stylename + '|' + fontfamily);
|
|
end;
|
|
faceNode := faceNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadFormula(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
cell: PCell;
|
|
formula: String;
|
|
// stylename: String;
|
|
floatValue: Double;
|
|
boolValue: Boolean;
|
|
errorValue: TsErrorValue;
|
|
valueType, calcExtValueType: String;
|
|
valueStr: String;
|
|
node: TDOMNode;
|
|
parser: TsSpreadsheetParser;
|
|
p: Integer;
|
|
fmt: PsCellFormat;
|
|
ns: String;
|
|
begin
|
|
// Create cell and apply format
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.GetCell(ARow, ACol); // Don't use AddCell here
|
|
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
{
|
|
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
|
ApplyStyleToCell(cell, stylename);
|
|
}
|
|
fmt := Workbook.GetPointerToCellFormat(cell^.FormatIndex);
|
|
|
|
formula := '';
|
|
if (boReadFormulas in FWorkbook.Options) then
|
|
begin
|
|
// Read formula, trim it, ...
|
|
formula := GetAttrValue(ACellNode, 'table:formula');
|
|
if formula <> '' then
|
|
begin
|
|
// Formulas written by Spread begin with 'of:=', by Excel with 'msof:='.
|
|
// Remove that. And both use different list separators
|
|
p := pos('=', formula);
|
|
ns := Copy(formula, 1, p-2);
|
|
case ns of
|
|
'of' : FPointSeparatorSettings.ListSeparator := ';';
|
|
'msoxl': FPointSeparatorSettings.ListSeparator := ',';
|
|
end;
|
|
Delete(formula, 1, p);
|
|
end;
|
|
// ... convert to Excel "A1" dialect used by fps by defailt
|
|
parser := TsSpreadsheetParser.Create(FWorksheet);
|
|
try
|
|
parser.Dialect := fdOpenDocument;
|
|
parser.LocalizedExpression[FPointSeparatorSettings] := formula;
|
|
parser.Dialect := fdExcelA1;
|
|
formula := parser.Expression;
|
|
finally
|
|
parser.Free;
|
|
end;
|
|
// ... and store in cell's FormulaValue field.
|
|
cell^.FormulaValue := formula;
|
|
end;
|
|
|
|
// Read formula results
|
|
valueType := GetAttrValue(ACellNode, 'office:value-type');
|
|
valueStr := GetAttrValue(ACellNode, 'office:value');
|
|
calcExtValueType := GetAttrValue(ACellNode, 'calcext:value-type');
|
|
// ODS wants a 0 in the NumberValue field in case of an error. If there is
|
|
// no error, this value will be corrected below.
|
|
cell^.NumberValue := 0.0;
|
|
// (a) number value
|
|
if (valueType = 'float') then
|
|
begin
|
|
if UpperCase(valueStr) = '1.#INF' then
|
|
FWorksheet.WriteNumber(cell, 1.0/0.0)
|
|
else
|
|
begin
|
|
floatValue := StrToFloat(valueStr, FPointSeparatorSettings);
|
|
FWorksheet.WriteNumber(cell, floatValue);
|
|
end;
|
|
if IsDateTimeFormat(fmt^.NumberFormat) then
|
|
begin
|
|
cell^.ContentType := cctDateTime;
|
|
// No datemode correction for intervals and for time-only values
|
|
if (fmt^.NumberFormat = nfTimeInterval) or (cell^.NumberValue < 1) then
|
|
cell^.DateTimeValue := cell^.NumberValue
|
|
else
|
|
case FDateMode of
|
|
dmODS1899: cell^.DateTimeValue := cell^.NumberValue + DATEMODE_1899_BASE;
|
|
dmODS1900: cell^.DateTimeValue := cell^.NumberValue + DATEMODE_1900_BASE;
|
|
dmODS1904: cell^.DateTimeValue := cell^.NumberValue + DATEMODE_1904_BASE;
|
|
end;
|
|
end;
|
|
end else
|
|
// (b) Date/time value
|
|
if (valueType = 'date') or (valueType = 'time') then
|
|
begin
|
|
floatValue := ExtractDateTimeFromNode(ACellNode, fmt^.NumberFormat, fmt^.NumberFormatStr);
|
|
FWorkSheet.WriteDateTime(cell, floatValue);
|
|
end else
|
|
// (c) text
|
|
if (valueType = 'string') and (calcextValueType <> 'error') then
|
|
begin
|
|
node := ACellNode.FindNode('text:p');
|
|
if (node <> nil) and (node.FirstChild <> nil) then
|
|
begin
|
|
valueStr := node.FirstChild.Nodevalue;
|
|
FWorksheet.WriteText(cell, valueStr);
|
|
end;
|
|
end else
|
|
// (d) boolean
|
|
if (valuetype = 'boolean') then
|
|
begin
|
|
boolValue := ExtractBoolFromNode(ACellNode);
|
|
FWorksheet.WriteBoolValue(cell, boolValue);
|
|
end else
|
|
if (calcextValuetype = 'error') then
|
|
begin
|
|
if ExtractErrorFromNode(ACellNode, errorValue) then
|
|
FWorksheet.WriteErrorValue(cell, errorValue) else
|
|
FWorksheet.WriteText(cell, 'ERROR');
|
|
end else
|
|
// (e) Text
|
|
if (valueStr <> '') then
|
|
FWorksheet.WriteText(cell, valueStr);
|
|
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadFromStream(AStream: TStream;
|
|
APassword: String = ''; AParams: TsStreamParams = []);
|
|
var
|
|
Doc : TXMLDocument;
|
|
BodyNode, SpreadSheetNode, TableNode: TDOMNode;
|
|
StylesNode: TDOMNode;
|
|
OfficeSettingsNode: TDOMNode;
|
|
nodename: String;
|
|
XMLStream: TStream;
|
|
sheet: TsWorksheet;
|
|
tablestyleName: String;
|
|
|
|
function CreateXMLStream: TStream;
|
|
begin
|
|
if boFileStream in FWorkbook.Options then
|
|
Result := TFileStream.Create(GetTempFileName, fmCreate)
|
|
else
|
|
if boBufStream in FWorkbook.Options then
|
|
Result := TBufStream.Create(GetTempFileName, fmCreate)
|
|
else
|
|
Result := TMemoryStream.Create;
|
|
end;
|
|
|
|
begin
|
|
Unused(APassword, AParams);
|
|
|
|
Doc := nil;
|
|
try
|
|
// Extract the embedded pictures
|
|
ReadPictures(AStream);
|
|
|
|
// process the styles.xml file
|
|
XMLStream := CreateXMLStream;
|
|
try
|
|
if UnzipToStream(AStream, 'styles.xml', XMLStream) then
|
|
ReadXMLStream(Doc, XMLStream);
|
|
finally
|
|
XMLStream.Free;
|
|
end;
|
|
|
|
ReadFontFaces(Doc.DocumentElement.FindNode('office:font-face-decls'));
|
|
|
|
StylesNode := Doc.DocumentElement.FindNode('office:styles');
|
|
ReadNumFormats(StylesNode);
|
|
ReadStyles(StylesNode);
|
|
ReadAutomaticStyles(Doc.DocumentElement.FindNode('office:automatic-styles'));
|
|
ReadMasterStyles(Doc.DocumentElement.FindNode('office:master-styles'));
|
|
FreeAndNil(Doc);
|
|
|
|
//process the content.xml file
|
|
XMLStream := CreateXMLStream;
|
|
try
|
|
if UnzipToStream(AStream, 'content.xml', XMLStream) then
|
|
ReadXMLStream(Doc, XMLStream);
|
|
finally
|
|
XMLStream.Free;
|
|
end;
|
|
|
|
ReadFontFaces(Doc.DocumentElement.FindNode('office:font-face-decls'));
|
|
StylesNode := Doc.DocumentElement.FindNode('office:automatic-styles');
|
|
ReadNumFormats(StylesNode);
|
|
ReadStyles(StylesNode);
|
|
|
|
BodyNode := Doc.DocumentElement.FindNode('office:body');
|
|
if not Assigned(BodyNode) then
|
|
raise Exception.Create('[TsSpreadOpenDocReader.ReadFromStream] Node "office:body" not found.');
|
|
|
|
SpreadSheetNode := BodyNode.FindNode('office:spreadsheet');
|
|
if not Assigned(SpreadSheetNode) then
|
|
raise Exception.Create('[TsSpreadOpenDocReader.ReadFromStream] Node "office:spreadsheet" not found.');
|
|
|
|
ReadDocumentProtection(SpreadsheetNode);
|
|
ReadDateMode(SpreadSheetNode);
|
|
|
|
//process each table (sheet)
|
|
TableNode := SpreadSheetNode.FindNode('table:table');
|
|
while Assigned(TableNode) do
|
|
begin
|
|
nodename := TableNode.Nodename;
|
|
// These nodes occur due to leading spaces which are not skipped
|
|
// automatically any more due to PreserveWhiteSpace option applied
|
|
// to ReadXMLFile
|
|
if nodeName <> 'table:table' then
|
|
begin
|
|
TableNode := TableNode.NextSibling;
|
|
continue;
|
|
end;
|
|
FWorkSheet := FWorkbook.AddWorksheet(GetAttrValue(TableNode, 'table:name'), true);
|
|
tablestyleName := GetAttrValue(TableNode, 'table:style-name');
|
|
// Read protection
|
|
ReadSheetProtection(TableNode, FWorksheet);
|
|
// Collect embedded images
|
|
ReadShapes(TableNode);
|
|
// Collect column styles used
|
|
ReadColumns(TableNode);
|
|
// Process each row inside the sheet and process each cell of the row
|
|
ReadRowsAndCells(TableNode);
|
|
// Read page layout
|
|
ReadPageLayout(StylesNode, GetAttrValue(TableNode, 'table:style-name'), FWorksheet.PageLayout);
|
|
// Repeated cols/rows already have been determined.
|
|
FWorksheet.PageLayout.SetRepeatedRows(FRepeatedRows.FirstIndex, FRepeatedRows.LastIndex);
|
|
FWorksheet.PageLayout.SetRepeatedCols(FRepeatedCols.FirstIndex, FRepeatedCols.LastIndex);
|
|
// Read print ranges
|
|
ReadPrintRanges(TableNode, FWorksheet);
|
|
// Apply table style
|
|
ApplyTableStyle(FWorksheet, tablestylename);
|
|
// Handle columns and rows
|
|
ApplyColWidths;
|
|
// Page layout
|
|
FixCols(FWorksheet);
|
|
FixRows(FWorksheet);
|
|
// Continue with next table
|
|
TableNode := TableNode.NextSibling;
|
|
end; //while Assigned(TableNode)
|
|
|
|
FreeAndNil(Doc);
|
|
|
|
// process the settings.xml file (Note: it does not always exist!)
|
|
XMLStream := CreateXMLStream;
|
|
try
|
|
if UnzipToStream(AStream, 'settings.xml', XMLStream) then
|
|
begin
|
|
ReadXMLStream(Doc, XMLStream);
|
|
OfficeSettingsNode := Doc.DocumentElement.FindNode('office:settings');
|
|
ReadSettings(OfficeSettingsNode);
|
|
end;
|
|
finally
|
|
XMLStream.Free;
|
|
end;
|
|
|
|
// Active sheet
|
|
if FActiveSheet <> '' then
|
|
sheet := FWorkbook.GetWorksheetByName(FActiveSheet) else
|
|
sheet := FWorkbook.GetWorksheetByIndex(0);
|
|
FWorkbook.SelectWorksheet(sheet);
|
|
|
|
finally
|
|
FreeAndNil(Doc);
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadHeaderFooterFont(ANode: TDOMNode;
|
|
var AFontName: String; var AFontSize: Double;
|
|
var AFontStyle: TsHeaderFooterFontStyles; var AFontColor: TsColor);
|
|
var
|
|
s: String;
|
|
begin
|
|
if ANode = nil then
|
|
exit;
|
|
|
|
AFontName := GetAttrValue(ANode, 'style:font-name');
|
|
|
|
s := GetAttrValue(ANode, 'fo:font-size');
|
|
if s <> '' then
|
|
AFontSize := HTMLLengthStrToPts(s);
|
|
|
|
AFontStyle := [];
|
|
|
|
if GetAttrValue(ANode, 'fo:font-style') = 'italic' then
|
|
Include(AFontStyle, hfsItalic);
|
|
|
|
if GetAttrValue(ANode, 'fo:font-weight') = 'bold' then
|
|
Include(AFontStyle, hfsBold);
|
|
|
|
s := GetAttrValue(ANode, 'style:text-underline-style');
|
|
if not ((s = '') or (s = 'none')) then
|
|
begin
|
|
if GetAttrValue(ANode, 'style:text-underline-type') = 'double' then
|
|
Include(AFontStyle, hfsDblUnderline)
|
|
else
|
|
Include(AFontStyle, hfsUnderline);
|
|
end;
|
|
|
|
s := GetAttrValue(ANode, 'style:text-line-through-style');
|
|
if not ((s = '') or (s = 'none')) then
|
|
Include(AFontStyle, hfsStrikeout);
|
|
|
|
if GetAttrValue(ANode, 'style:text-outline') = 'true' then
|
|
Include(AFontStyle, hfsOutline);
|
|
|
|
s := GetAttrValue(ANode, 'fo:text-shadow');
|
|
if not ((s = '') or (s = 'none')) then
|
|
Include(AFontStyle, hfsShadow);
|
|
|
|
s := GetAttrValue(ANode, 'style:text-position');
|
|
if pos('sub', s) = 1 then
|
|
Include(AFontStyle, hfsSubscript)
|
|
else if pos('super', s) = 1 then
|
|
Include(AFontStyle, hfsSuperscript);
|
|
|
|
s := GetAttrValue(ANode, 'fo:color');
|
|
if s <> '' then
|
|
AFontColor := HTMLColorStrToColor(s);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadLabel(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
cellText, spanText: String;
|
|
styleName: String;
|
|
childnode: TDOMNode;
|
|
subnode: TDOMNode;
|
|
nodeName: String;
|
|
cell: PCell;
|
|
hyperlink: string;
|
|
rtParams: TsRichTextParams;
|
|
idx: Integer;
|
|
rtFntIndex, fntIndex: Integer;
|
|
rtFnt, fnt: TsFont;
|
|
fmt: PsCellFormat;
|
|
|
|
procedure AddToCellText(AText: String);
|
|
begin
|
|
if cellText = ''
|
|
then cellText := AText
|
|
else cellText := cellText + AText;
|
|
end;
|
|
|
|
begin
|
|
// Initalize cell
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.AddCell(ARow, ACol);
|
|
|
|
// Apply style to cell
|
|
// We do this already here because we need the cell font for rich-text
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
{
|
|
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
|
ApplyStyleToCell(cell, stylename);
|
|
}
|
|
fmt := FWorkbook.GetPointerToCellFormat(cell^.FormatIndex);
|
|
fntIndex := fmt^.FontIndex;
|
|
fnt := FWorkbook.GetFont(fntIndex);
|
|
|
|
// Prepare reading of node data
|
|
cellText := '';
|
|
hyperlink := '';
|
|
SetLength(rtParams, 0);
|
|
childnode := ACellNode.FirstChild;
|
|
while Assigned(childnode) do
|
|
begin
|
|
nodeName := childNode.NodeName;
|
|
if nodeName = 'text:p' then begin
|
|
// Each 'text:p' node is a paragraph --> we insert a line break after the first paragraph
|
|
if cellText <> '' then
|
|
cellText := cellText + LineEnding;
|
|
subnode := childnode.FirstChild;
|
|
while Assigned(subnode) do
|
|
begin
|
|
nodename := subnode.NodeName;
|
|
case nodename of
|
|
'#text' :
|
|
begin
|
|
if Length(rtParams) > 0 then
|
|
begin
|
|
SetLength(rtParams, Length(rtParams) + 1);
|
|
rtParams[High(rtParams)].FirstIndex := UTF8Length(cellText) + 1;
|
|
rtParams[High(rtParams)].FontIndex := fntIndex;
|
|
rtParams[High(rtParams)].HyperlinkIndex := -1; // TO DO !!!!
|
|
end;
|
|
AddToCellText(subnode.TextContent);
|
|
end;
|
|
'text:a': // "hyperlink anchor"
|
|
begin
|
|
hyperlink := GetAttrValue(subnode, 'xlink:href');
|
|
AddToCellText(subnode.TextContent);
|
|
end;
|
|
'text:span':
|
|
begin
|
|
spanText := subnode.TextContent;
|
|
stylename := GetAttrValue(subnode, 'text:style-name');
|
|
if stylename <> '' then begin
|
|
idx := FCellFormatList.FindIndexOfName(stylename);
|
|
if idx > -1 then
|
|
begin
|
|
rtFntIndex := FCellFormatList[idx]^.FontIndex;
|
|
rtFnt := TsFont(FRichTextFontList[rtFntIndex]);
|
|
// Replace missing font elements by those from the cell font
|
|
if rtFnt.FontName = '' then rtFnt.FontName := fnt.FontName;
|
|
if rtFnt.Size = -1 then rtFnt.Size := fnt.Size;
|
|
if rtFnt.Style = [] then rtFnt.Style := fnt.Style;
|
|
if rtFnt.Color = scNone then rtFnt.Color := fnt.Color;
|
|
if rtFnt.Position = fpNormal then rtFnt.Position := fnt.Position;
|
|
// Find this font in the workbook's font list
|
|
rtfntIndex := FWorkbook.FindFont(rtFnt.FontName, rtFnt.Size, rtFnt.Style, rtFnt.Color, rtFnt.Position);
|
|
// If not found add to font list
|
|
if rtfntIndex = -1 then
|
|
rtfntIndex := FWorkbook.AddFont(rtFnt.FontName, rtFnt.Size, rtFnt.Style, rtFnt.Color, rtFnt.Position);
|
|
// Use this font index in the rich-text parameter
|
|
SetLength(rtParams, Length(rtParams)+1);
|
|
rtParams[High(rtParams)].FirstIndex := UTF8Length(cellText) + 1; // 1-based character index
|
|
rtParams[High(rtParams)].FontIndex := rtFntIndex;
|
|
rtParams[High(rtParams)].HyperlinkIndex := -1; // TO DO !!!!
|
|
end;
|
|
end;
|
|
AddToCellText(spanText);
|
|
end;
|
|
'text:line-break':
|
|
AddToCellText(FPS_LINE_ENDING);
|
|
end;
|
|
subnode := subnode.NextSibling;
|
|
end;
|
|
end;
|
|
childnode := childnode.NextSibling;
|
|
end;
|
|
|
|
FWorkSheet.WriteText(cell, cellText, rtParams);
|
|
if hyperlink <> '' then
|
|
begin
|
|
// ODS sees relative paths relative to the internal own file structure
|
|
// --> we must remove 1 level-up to be at the same level where fps expects
|
|
// the file.
|
|
if pos('../', hyperlink) = 1 then
|
|
Delete(hyperlink, 1, Length('../'));
|
|
FWorksheet.WriteHyperlink(cell, hyperlink);
|
|
FWorksheet.WriteFont(cell, HYPERLINK_FONTINDEX);
|
|
end;
|
|
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadNumber(ARow, ACol: Cardinal;
|
|
AStyleIndex: Integer; ACellNode: TDOMNode);
|
|
var
|
|
Value, Str: String;
|
|
lNumber: Double;
|
|
// styleName: String;
|
|
cell: PCell;
|
|
fmt: PsCellFormat;
|
|
numFmt: TsNumFormatParams;
|
|
txtNode: TDOMNode;
|
|
begin
|
|
if FIsVirtualMode then
|
|
begin
|
|
InitCell(ARow, ACol, FVirtualCell);
|
|
cell := @FVirtualCell;
|
|
end else
|
|
cell := FWorksheet.AddCell(ARow, ACol);
|
|
|
|
Value := GetAttrValue(ACellNode,'office:value');
|
|
if UpperCase(Value)='1.#INF' then
|
|
FWorkSheet.WriteNumber(cell, 1.0/0.0)
|
|
else
|
|
begin
|
|
// Don't merge, or else we can't debug
|
|
Str := GetAttrValue(ACellNode, 'office:value');
|
|
lNumber := StrToFloat(Str, FPointSeparatorSettings);
|
|
FWorkSheet.WriteNumber(cell, lNumber);
|
|
end;
|
|
|
|
ApplyStyleToCell(cell, AStyleIndex);
|
|
{
|
|
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
|
ApplyStyleToCell(cell, stylename);
|
|
}
|
|
fmt := Workbook.GetPointerToCellFormat(cell^.FormatIndex);
|
|
numFmt := Workbook.GetNumberFormat(fmt^.NumberFormatIndex);
|
|
|
|
// Sometimes date/time cells are stored as "float".
|
|
// We convert them to date/time and also correct the date origin offset if
|
|
// needed.
|
|
if IsDateTimeFormat(numFmt) then
|
|
begin
|
|
cell^.ContentType := cctDateTime;
|
|
// No datemode correction for intervals and for time-only values
|
|
if (numFmt.NumFormat = nfTimeInterval) or (cell^.NumberValue < 1) then
|
|
cell^.DateTimeValue := cell^.NumberValue
|
|
else
|
|
case FDateMode of
|
|
dmODS1899: cell^.DateTimeValue := cell^.NumberValue + DATEMODE_1899_BASE;
|
|
dmODS1900: cell^.DateTimeValue := cell^.NumberValue + DATEMODE_1900_BASE;
|
|
dmODS1904: cell^.DateTimeValue := cell^.NumberValue + DATEMODE_1904_BASE;
|
|
end;
|
|
end else
|
|
if IsTextFormat(numFmt) then begin
|
|
// Cell has TEXT format @ --> store number as text
|
|
txtNode := ACellNode.FirstChild;
|
|
if txtNode.NodeName = 'text:p' then
|
|
FWorksheet.WriteText(cell, GetNodeValue(txtNode));
|
|
end;
|
|
|
|
if FIsVirtualMode then
|
|
Workbook.OnReadCellData(Workbook, ARow, ACol, cell);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadNumFormats(AStylesNode: TDOMNode);
|
|
|
|
procedure ReadStyleMap(ANode: TDOMNode; var ANumFormat: TsNumberFormat;
|
|
var AFormatStr: String);
|
|
var
|
|
condition: String;
|
|
stylename: String;
|
|
styleindex: Integer;
|
|
fmt: String;
|
|
posfmt, negfmt, zerofmt, currfmt: String;
|
|
nf: TsNumberFormat;
|
|
parser: TsNumFormatParser;
|
|
counter: Integer;
|
|
begin
|
|
posfmt := '';
|
|
negfmt := '';
|
|
zerofmt := '';
|
|
currfmt := AFormatStr;
|
|
counter := 0;
|
|
|
|
AFormatStr := '';
|
|
ANumFormat := nfCustom;
|
|
|
|
while ANode <> nil do
|
|
begin
|
|
condition := ANode.NodeName;
|
|
|
|
if (ANode.NodeName = '#text') or not ANode.HasAttributes then
|
|
begin
|
|
ANode := ANode.NextSibling;
|
|
Continue;
|
|
end;
|
|
|
|
condition := GetAttrValue(ANode, 'style:condition');
|
|
stylename := GetAttrValue(ANode, 'style:apply-style-name');
|
|
if (condition = '') or (stylename = '') then
|
|
begin
|
|
ANode := ANode.NextSibling;
|
|
continue;
|
|
end;
|
|
|
|
Delete(condition, 1, Length('value()'));
|
|
styleindex := -1;
|
|
styleIndex := FindNumFormatByName(stylename);
|
|
if (styleindex = -1) or (condition = '') then
|
|
begin
|
|
ANode := ANode.NextSibling;
|
|
continue;
|
|
end;
|
|
|
|
fmt := NumFormatList[styleIndex];
|
|
fmt := Copy(fmt, pos(':', fmt)+1, Length(fmt));
|
|
parser := TsNumFormatParser.Create(fmt, Workbook.FormatSettings);
|
|
try
|
|
nf := parser.NumFormat;
|
|
if (nf = nfCurrency) and (parser.ParsedSections[0].Color = scRed) then
|
|
nf := nfCurrencyRed;
|
|
if nf in [nfCurrency, nfCurrencyRed] then
|
|
ANumFormat := nf;
|
|
finally
|
|
parser.Free;
|
|
end;
|
|
|
|
case condition[1] of
|
|
'>': begin
|
|
posfmt := fmt;
|
|
if (Length(condition) > 1) and (condition[2] = '=') then
|
|
zerofmt := fmt;
|
|
end;
|
|
'<': begin
|
|
negfmt := fmt;
|
|
if (Length(condition) > 1) and (condition[2] = '=') then
|
|
zerofmt := fmt;
|
|
end;
|
|
'=': begin
|
|
zerofmt := fmt;
|
|
end;
|
|
end;
|
|
ANode := ANode.NextSibling;
|
|
inc(counter);
|
|
end;
|
|
{
|
|
if posfmt = '' then posfmt := currFmt;
|
|
if negfmt = '' then negfmt := currFmt;
|
|
}
|
|
// if posfmt = '' then posfmt := AFormatStr;
|
|
// if negfmt = '' then negfmt := AFormatStr;
|
|
|
|
case counter of
|
|
1: begin
|
|
if negfmt = '' then negfmt := currfmt;
|
|
AFormatStr := posfmt + ';' + negfmt;
|
|
end;
|
|
2: begin
|
|
if zerofmt = '' then zerofmt := currfmt;
|
|
AFormatStr := posfmt + ';' + negfmt + ';' + zerofmt;
|
|
end;
|
|
3: AFormatStr := posfmt + ';' + negfmt + ';' + zerofmt;
|
|
end;
|
|
|
|
if not (ANumFormat in [nfCurrency, nfCurrencyRed]) then
|
|
ANumFormat := nfCustom;
|
|
end;
|
|
|
|
procedure ReadNumberStyle(ANumFormatNode: TDOMNode; ANumFormatName: String);
|
|
var
|
|
node, childNode: TDOMNode;
|
|
nodeName: String;
|
|
nf: TsNumberFormat;
|
|
nfs: String;
|
|
decs: Byte;
|
|
s: String;
|
|
f: Double;
|
|
fracInt, fracNum, fracDenom: Integer;
|
|
grouping: Boolean;
|
|
nex: Integer;
|
|
cs: String;
|
|
color: TsColor;
|
|
hasColor: Boolean;
|
|
begin
|
|
nfs := '';
|
|
cs := '';
|
|
hasColor := false;
|
|
node := ANumFormatNode.FirstChild;
|
|
while Assigned(node) do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
if nodeName = '#text' then
|
|
begin
|
|
node := node.NextSibling;
|
|
Continue;
|
|
end else
|
|
if nodeName = 'number:number' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:decimal-places');
|
|
if s = '' then
|
|
s := GetAttrValue(node, 'decimal-places');
|
|
if s = '' then
|
|
begin
|
|
if nfs='' then nf := nfGeneral else nf := nfCustom;
|
|
nfs := nfs + 'General';
|
|
end else
|
|
begin
|
|
decs := StrToInt(s);
|
|
grouping := GetAttrValue(node, 'number:grouping') = 'true';
|
|
s := GetAttrValue(node, 'number:display-factor');
|
|
if s <> '' then f := StrToFloat(s, FPointSeparatorSettings) else f := 1.0;
|
|
nf := IfThen(grouping, nfFixedTh, nfFixed);
|
|
nfs := nfs + BuildNumberFormatString(nf, Workbook.FormatSettings, decs);
|
|
if f <> 1.0 then begin
|
|
nf := nfCustom;
|
|
while (f > 1.0) do
|
|
begin
|
|
nfs := nfs + ',';
|
|
f := f / 1000;
|
|
end;
|
|
end;
|
|
end;
|
|
end else
|
|
if nodeName = 'number:fraction' then
|
|
begin
|
|
nf := nfFraction;
|
|
s := GetAttrValue(node, 'number:min-integer-digits');
|
|
if s <> '' then fracInt := StrToInt(s) else fracInt := -1;
|
|
s := GetAttrValue(node, 'number:min-numerator-digits');
|
|
if s <> '' then fracNum := StrToInt(s) else fracNum := 0;
|
|
s := GetAttrValue(node, 'number:min-denominator-digits');
|
|
if s <> '' then fracDenom := StrToInt(s) else fracDenom := 0;
|
|
s := GetAttrValue(node, 'number:denominator-value');
|
|
if s <> '' then fracDenom := -StrToInt(s);
|
|
nfs := nfs + BuildFractionFormatString(fracInt > -1, fracNum, fracDenom);
|
|
end else
|
|
if nodeName = 'number:scientific-number' then
|
|
begin
|
|
nf := nfExp;
|
|
s := GetAttrValue(node, 'number:decimal-places');
|
|
if s <> '' then decs := StrToInt(s) else decs := 0;
|
|
s := GetAttrValue(node, 'number:min-exponent-digits');
|
|
if s <> '' then nex := StrToInt(s) else nex := 1;
|
|
nfs := nfs + BuildNumberFormatString(nfFixed, Workbook.FormatSettings, decs);
|
|
nfs := nfs + 'E+' + DupeString('0', nex);
|
|
end else
|
|
if nodeName = 'number:currency-symbol' then
|
|
begin
|
|
childnode := node.FirstChild;
|
|
while childnode <> nil do
|
|
begin
|
|
cs := cs + childNode.NodeValue;
|
|
nfs := nfs + '"' + childNode.NodeValue + '"';
|
|
childNode := childNode.NextSibling;
|
|
end;
|
|
end else
|
|
if nodeName = 'number:text' then
|
|
begin
|
|
childNode := node.FirstChild;
|
|
while childNode <> nil do
|
|
begin
|
|
nfs := nfs + childNode.NodeValue;
|
|
childNode := childNode.NextSibling;
|
|
end;
|
|
end else
|
|
if nodeName = 'style:text-properties' then
|
|
begin
|
|
s := GetAttrValue(node, 'fo:color');
|
|
if s <> '' then
|
|
begin
|
|
hasColor := true;
|
|
color := HTMLColorStrToColor(s);
|
|
case color of
|
|
scBlack : nfs := '[black]' + nfs;
|
|
scWhite : nfs := '[white]' + nfs;
|
|
scRed : nfs := '[red]' + nfs;
|
|
scGreen : nfs := '[green]' + nfs;
|
|
scBlue : nfs := '[blue]' + nfs;
|
|
scYellow : nfs := '[yellow]' + nfs;
|
|
scMagenta : nfs := '[magenta]' + nfs;
|
|
scCyan : nfs := '[cyan]' + nfs;
|
|
end;
|
|
end;
|
|
end;
|
|
node := node.NextSibling;
|
|
end;
|
|
|
|
node := ANumFormatNode.FindNode('style:map');
|
|
if node <> nil then
|
|
ReadStyleMap(node, nf, nfs);
|
|
|
|
if ANumFormatNode.NodeName = 'number:percentage-style' then
|
|
nf := nfPercentage
|
|
else
|
|
if (ANumFormatNode.NodeName = 'number:currency-style') then
|
|
nf := IfThen(hasColor, nfCurrencyRed, nfCurrency);
|
|
|
|
NumFormatList.Add(Format('%s:%s', [ANumFormatName, nfs]));
|
|
end;
|
|
|
|
procedure ReadDateTimeStyle(ANumFormatNode: TDOMNode; ANumFormatName: String);
|
|
var
|
|
node, childNode: TDOMNode;
|
|
nf: TsNumberFormat;
|
|
nfs: String;
|
|
nodeName: String;
|
|
s, stxt, sovr: String;
|
|
isInterval: Boolean;
|
|
begin
|
|
nfs := '';
|
|
isInterval := false;
|
|
sovr := GetAttrValue(ANumFormatNode, 'number:truncate-on-overflow');
|
|
if (sovr = 'false') then
|
|
isInterval := true;
|
|
node := ANumFormatNode.FirstChild;
|
|
while Assigned(node) do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
if nodeName = '#text' then
|
|
begin
|
|
node := node.NextSibling;
|
|
Continue;
|
|
end else
|
|
if nodeName = 'number:year' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
nfs := nfs + IfThen(s = 'long', 'yyyy', 'yy');
|
|
end else
|
|
if nodeName = 'number:month' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
stxt := GetAttrValue(node, 'number:textual');
|
|
if (stxt = 'true') then // Month as text
|
|
nfs := nfs + IfThen(s = 'long', 'mmmm', 'mmm')
|
|
else // Month as number
|
|
nfs := nfs + IfThen(s = 'long', 'mm', 'm');
|
|
end else
|
|
if nodeName = 'number:day' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
nfs := nfs + IfThen(s = 'long', 'dd', 'd');
|
|
end else
|
|
if nodeName = 'number:day-of-week' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
nfs := nfs + IfThen(s = 'long', 'dddd', 'ddd');
|
|
end else
|
|
if nodeName = 'number:hours' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
if (sovr = 'false') then
|
|
nfs := nfs + IfThen(s = 'long', '[hh]', '[h]')
|
|
else
|
|
nfs := nfs + IfThen(s = 'long', 'hh', 'h');
|
|
sovr := '';
|
|
end else
|
|
if nodeName = 'number:minutes' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
if (sovr = 'false') then
|
|
nfs := nfs + IfThen(s = 'long', '[nn]', '[n]')
|
|
else
|
|
nfs := nfs + IfThen(s = 'long', 'nn', 'n');
|
|
sovr := '';
|
|
end else
|
|
if nodeName = 'number:seconds' then
|
|
begin
|
|
s := GetAttrValue(node, 'number:style');
|
|
if (sovr = 'false') then
|
|
nfs := nfs + IfThen(s = 'long', '[ss]', '[s]')
|
|
else
|
|
nfs := nfs + IfThen(s = 'long', 'ss', 's');
|
|
sovr := '';
|
|
s := GetAttrValue(node, 'number:decimal-places');
|
|
if (s <> '') and (s <> '0') then
|
|
nfs := nfs + '.' + DupeString('0', StrToInt(s));
|
|
end else
|
|
if nodeName = 'number:am-pm' then
|
|
nfs := nfs + 'AM/PM'
|
|
else
|
|
if nodeName = 'number:text' then
|
|
begin
|
|
childnode := node.FirstChild;
|
|
if childnode <> nil then
|
|
begin
|
|
s := childNode.NodeValue;
|
|
if pos(';', s) > 0 then
|
|
nfs := nfs + '"' + s + '"'
|
|
// avoid "misunderstanding" the semicolon as a section separator!
|
|
else
|
|
nfs := nfs + childnode.NodeValue;
|
|
end;
|
|
end;
|
|
node := node.NextSibling;
|
|
end;
|
|
|
|
nf := IfThen(isInterval, nfTimeInterval, nfCustom);
|
|
node := ANumFormatNode.FindNode('style:map');
|
|
if node <> nil then
|
|
ReadStyleMap(node, nf, nfs);
|
|
|
|
NumFormatList.Add(ANumFormatName + ':' + nfs);
|
|
// NumFormatList.AddFormat(ANumFormatName, nf, nfs);
|
|
end;
|
|
|
|
procedure ReadTextStyle(ANumFormatNode: TDOMNode; ANumFormatName: String);
|
|
var
|
|
node, childNode: TDOMNode;
|
|
nf: TsNumberFormat = nfGeneral;
|
|
nfs: String;
|
|
nodeName: String;
|
|
begin
|
|
nfs := '';
|
|
node := ANumFormatNode.FirstChild;
|
|
while Assigned(node) do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
if nodeName = '#text' then
|
|
begin
|
|
node := node.NextSibling;
|
|
Continue;
|
|
end else
|
|
if nodeName = 'number:text-content' then
|
|
begin
|
|
nfs := nfs + '@';
|
|
end else
|
|
if nodeName = 'number:text' then
|
|
begin
|
|
childnode := node.FirstChild;
|
|
if childnode <> nil then
|
|
nfs := nfs + childnode.NodeValue;
|
|
end;
|
|
node := node.NextSibling;
|
|
end;
|
|
|
|
node := ANumFormatNode.FindNode('style:map');
|
|
if node <> nil then
|
|
ReadStyleMap(node, nf, nfs);
|
|
nf := nfCustom;
|
|
|
|
NumFormatList.Add(Format('%s:%s', [ANumFormatName, nfs]));
|
|
|
|
//NumFormatList.AddFormat(ANumFormatName, nf, nfs);
|
|
end;
|
|
|
|
var
|
|
NumFormatNode: TDOMNode;
|
|
numfmt_nodename, numfmtname: String;
|
|
|
|
begin
|
|
if not Assigned(AStylesNode) then
|
|
exit;
|
|
|
|
NumFormatNode := AStylesNode.FirstChild;
|
|
while Assigned(NumFormatNode) do
|
|
begin
|
|
numfmt_nodename := NumFormatNode.NodeName;
|
|
|
|
if NumFormatNode.HasAttributes then
|
|
numfmtName := GetAttrValue(NumFormatNode, 'style:name') else
|
|
numfmtName := '';
|
|
|
|
// Numbers (nfFixed, nfFixedTh, nfExp, nfPercentage)
|
|
if (numfmt_nodename = 'number:number-style') or
|
|
(numfmt_nodename = 'number:percentage-style') or
|
|
(numfmt_nodename = 'number:currency-style')
|
|
then
|
|
ReadNumberStyle(NumFormatNode, numfmtName);
|
|
|
|
// Date/time values
|
|
if (numfmt_nodename = 'number:date-style') or (numfmt_nodename = 'number:time-style') then
|
|
ReadDateTimeStyle(NumFormatNode, numfmtName);
|
|
|
|
// Text values
|
|
if (numfmt_nodename = 'number:text-style') then
|
|
ReadTextStyle(NumFormatNode, numfmtName);
|
|
|
|
// Next node
|
|
NumFormatNode := NumFormatNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
{ Finds the PageLayout record for a given TableStyle name in the "styles" nodes.
|
|
First, seeks the TableStyle among the children of the "styles" node in the
|
|
contents.xml - this node contains the name of the used master page.
|
|
Then seeks the FMasterPageList for the entry with the determined master page
|
|
name. This entry contains the name of the associated PageLayoutData stored in
|
|
the PageLayoutList which, finally, contains the requested PageLayout record. }
|
|
procedure TsSpreadOpenDocReader.ReadPageLayout(AStylesNode: TDOMNode;
|
|
ATableStyleName: String; APageLayout: TsPageLayout);
|
|
var
|
|
nodeName, s: String;
|
|
node: TDOMNode;
|
|
masterPageName: String;
|
|
masterPageData: TMasterPageData;
|
|
pageLayoutData: TPageLayoutData;
|
|
i, j: Integer;
|
|
begin
|
|
if AStylesNode = nil then
|
|
exit;
|
|
|
|
{ Looking through the "styles" node...}
|
|
node := AStylesNode.FirstChild;
|
|
while node <> nil do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
{ ... for the node which is named like the requested TableStyle }
|
|
if nodeName = 'style:style' then
|
|
begin
|
|
s := GetAttrValue(node, 'style:name');
|
|
if s = ATableStyleName then
|
|
begin
|
|
{ Found: extract the name of the master page }
|
|
masterPageName := GetAttrValue(node, 'style:master-page-name');
|
|
if masterPageName = '' then
|
|
exit;
|
|
|
|
{ Looking through the MasterPage list...}
|
|
for i:=0 to FMasterPageList.Count-1 do
|
|
begin
|
|
masterPageData := TMasterPageData(FMasterPageList[i]);
|
|
{ ... for the entry with the found master page name }
|
|
if masterPageData.Name = masterPageName then
|
|
begin
|
|
{ Found: looking through the PageLayout list ...}
|
|
for j:=0 to FPageLayoutList.Count-1 do
|
|
begin
|
|
pageLayoutData := TPageLayoutData(FPageLayoutList[j]);
|
|
{ ... for the entry with the name specified by the master page }
|
|
if pageLayoutData.Name = masterPageData.PageLayoutName then
|
|
begin
|
|
{ Found: Return a pointer to the PageLayout record stored in the list }
|
|
APageLayout.Assign(pageLayoutData.PageLayout);
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
{ Not found: try next node in the styles list }
|
|
node := node.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadPictures(AStream: TStream);
|
|
var
|
|
memstream: TMemoryStream;
|
|
unzip: TStreamUnzipper;
|
|
fn: String;
|
|
i: Integer;
|
|
begin
|
|
unzip := TStreamUnzipper.Create(AStream);
|
|
try
|
|
unzip.Examine;
|
|
for i := 0 to unzip.Entries.Count-1 do begin
|
|
fn := unzip.Entries.Entries[i].ArchiveFileName;
|
|
if ExtractFileDir(fn) = 'Pictures' then begin
|
|
memStream := TMemoryStream.Create;
|
|
unzip.UnzipFile(fn, memStream);
|
|
memstream.Position := 0;
|
|
FWorkbook.AddEmbeddedObj(memstream, ExtractFileName(fn));
|
|
memStream.Free;
|
|
end;
|
|
end;
|
|
finally
|
|
unzip.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadPrintRanges(ATableNode: TDOMNode;
|
|
ASheet: TsWorksheet);
|
|
var
|
|
L: TStringList;
|
|
s, sheetname: String;
|
|
i, p: Integer;
|
|
r1,c1,r2,c2: Cardinal;
|
|
inName: Boolean;
|
|
begin
|
|
s := GetAttrValue(ATableNode, 'table:print-ranges');
|
|
if s = '' then
|
|
exit;
|
|
L := TStringList.Create;
|
|
try
|
|
// Scan the string for spaces. But note: Spaces may be contained also in
|
|
// the sheet names!
|
|
s := s + ' ';
|
|
i := 1;
|
|
p := 1;
|
|
inName := false;
|
|
while (i <= Length(s)) do
|
|
begin
|
|
case s[i] of
|
|
'''': inName := not inName;
|
|
' ' : if not inName then begin
|
|
L.Add(Copy(s, p, i-p));
|
|
while (i <= Length(s)) and (s[i] = ' ') do
|
|
inc(i);
|
|
p := i;
|
|
if p <= Length(s) then
|
|
Continue
|
|
else
|
|
break;
|
|
end;
|
|
end;
|
|
inc(i);
|
|
end;
|
|
|
|
// L lists all the ranges. Split each range into its components.
|
|
for i:=0 to L.Count-1 do begin
|
|
s := L[i];
|
|
p := pos(':', L[i]);
|
|
s := Copy(L[i], 1, p-1);
|
|
ParseSheetCellString(s, sheetname, r1, c1, '.');
|
|
if (sheetname <> '') then
|
|
begin
|
|
if (sheetname[1] = '''') then
|
|
Delete(sheetname, 1,1);
|
|
if (sheetname[Length(sheetname)] = '''') then
|
|
Delete(sheetname, Length(sheetname), 1);
|
|
if (sheetname <> ASheet.Name) then
|
|
begin
|
|
FWorkbook.AddErrorMsg(rsDifferentSheetPrintRange, [L[i]]);
|
|
Continue;
|
|
end;
|
|
end;
|
|
s := Copy(L[i], p+1, Length(L[i]));
|
|
ParseSheetCellString(s, sheetname, r2, c2, '.');
|
|
if (sheetname <> '') then begin
|
|
if (sheetname[1] = '''') then
|
|
Delete(sheetname, 1, 1);
|
|
if (sheetname[Length(sheetname)] = '''') then
|
|
Delete(sheetname, Length(sheetname), 1);
|
|
if (sheetname <> ASheet.name) then
|
|
begin
|
|
FWorkbook.AddErrorMsg(rsDifferentSheetPrintRange, [L[i]]);
|
|
Continue;
|
|
end;
|
|
end;
|
|
// Add found range to worksheet
|
|
ASheet.PageLayout.AddPrintRange(r1, c1, r2, c2);
|
|
end;
|
|
finally
|
|
L.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadCell(ANode: TDOMNode; ARow, ACol: Integer;
|
|
AFormatIndex: Integer; out AColsRepeated: Integer);
|
|
var
|
|
paramValueType, paramFormula: String;
|
|
s: String;
|
|
colsSpanned, rowsSpanned: Integer;
|
|
begin
|
|
// Workaround for Excel files converted to ods by Calc: These files are
|
|
// expanded to fill the entire max worksheet. They also have single empty
|
|
// cell in the outermost cells --> don't write anything here to prevent this.
|
|
if (ARow > FLimitations.MaxRowCount - 10) or (ACol > FLimitations.MaxColCount - 10) then
|
|
exit;
|
|
|
|
// select this cell value's type
|
|
paramValueType := GetAttrValue(ANode, 'office:value-type');
|
|
paramFormula := GetAttrValue(ANode, 'table:formula');
|
|
|
|
if paramFormula <> '' then
|
|
ReadFormula(ARow, ACol, AFormatIndex, ANode)
|
|
else
|
|
begin
|
|
if paramValueType = 'string' then
|
|
ReadLabel(ARow, ACol, AFormatIndex, ANode)
|
|
else
|
|
if (paramValueType = 'float') or
|
|
(paramValueType = 'percentage') or
|
|
(paramValueType = 'currency')
|
|
then
|
|
ReadNumber(ARow, ACol, AFormatIndex, ANode)
|
|
else if (paramValueType = 'date') or (paramValueType = 'time') then
|
|
ReadDateTime(ARow, ACol, AFormatIndex, ANode)
|
|
else if (paramValueType = 'boolean') then
|
|
ReadBoolean(ARow, ACol, AFormatIndex, ANode)
|
|
else
|
|
if (paramValueType = '') and (AFormatIndex > 0) and
|
|
(ARow < FLimitations.MaxRowCount-10) and
|
|
(ACol < FLimitations.MaxColCount-10)
|
|
then
|
|
ReadBlank(ARow, ACol, AFormatIndex, ANode);
|
|
{ NOTE 1: Empty cells having no cell format, but a column format only,
|
|
are skipped here. --> Currently the reader does not detect the format
|
|
of empty cells correctly.
|
|
It would work if the "(cellStyleName <> '')" would be omitted, but // <--- wp: still up-to-date?
|
|
then the reader would create a record for all 1E9 cells prepared by
|
|
the Excel2007 export --> crash!
|
|
The column format is available in the FColumnList, but since the usage
|
|
of colsSpanned in the row it is possible to miss the correct column format.
|
|
Pretty nasty situation!
|
|
|
|
NOTE 2: Sometimes, ods files have an additional empty cell at the end
|
|
of the spreadsheet range. Adding a cell to the worksheet here would
|
|
extend the sheet range unrealistically, and, if using the WorksheetGrid,
|
|
would add unnecessary rows/columns to the grid. --> Check against
|
|
FLimitations.MaxRowCount/MaxColCount; use some spare values because I
|
|
don't understand this mechanism of ods at all }
|
|
end;
|
|
|
|
// Read cell comment
|
|
ReadComment(ARow, ACol, ANode);
|
|
|
|
s := GetAttrValue(ANode, 'table:number-columns-spanned');
|
|
if s <> '' then
|
|
colsSpanned := StrToInt(s) - 1
|
|
else
|
|
colsSpanned := 0;
|
|
|
|
s := GetAttrValue(ANode, 'table:number-rows-spanned');
|
|
if s <> '' then
|
|
rowsSpanned := StrToInt(s) - 1
|
|
else
|
|
rowsSpanned := 0;
|
|
|
|
if (colsSpanned <> 0) or (rowsSpanned <> 0) then
|
|
FWorksheet.MergeCells(ARow, ACol, ARow + rowsSpanned, ACol + colsSpanned);
|
|
|
|
s := GetAttrValue(ANode, 'table:number-columns-repeated');
|
|
if s <> '' then
|
|
AColsRepeated := StrToInt(s)
|
|
else
|
|
AColsRepeated := 1;
|
|
end;
|
|
|
|
{ Reads the cells in the given table. Loops through all rows, and then finds all
|
|
cells of each row. }
|
|
procedure TsSpreadOpenDocReader.ReadRowsAndCells(ATableNode: TDOMNode);
|
|
var
|
|
row: Integer;
|
|
rNode, childnode: TDOMNode;
|
|
nodeName: String;
|
|
rowsRepeated: Integer;
|
|
colFmt: array of Integer;
|
|
isFirstRow: Boolean;
|
|
|
|
procedure ProcessRow(ARowNode: TDOMNode; GetRowFormat: Boolean);
|
|
var
|
|
rowStyleName: String;
|
|
rowStyleIndex: Integer;
|
|
rowStyle: TRowStyleData;
|
|
rowHeight: Double;
|
|
rowHeightType: TsRowHeightType;
|
|
col: Integer;
|
|
cellNode: TDOMNode;
|
|
nodeName: String;
|
|
lRow: PRow;
|
|
cellRecord: TCell;
|
|
cell: PCell;
|
|
cellStyleName: String;
|
|
s: String;
|
|
colsRepeated: Integer;
|
|
i: Integer;
|
|
hasRowFormat: Boolean;
|
|
styleIndex: Integer;
|
|
firstStyleIndex: Integer;
|
|
begin
|
|
// Read rowstyle
|
|
rowStyleName := GetAttrValue(ARowNode, 'table:style-name');
|
|
rowStyleIndex := FindRowStyleByName(rowStyleName);
|
|
if rowStyleIndex > -1 then // just for safety
|
|
begin
|
|
rowStyle := TRowStyleData(FRowStyleList[rowStyleIndex]);
|
|
rowHeight := rowStyle.RowHeight; // in Workbook units (see ReadRowStyles)
|
|
rowHeightType := rowStyle.RowHeightType;
|
|
end else begin
|
|
rowHeight := FWorksheet.ReadDefaultRowHeight(FWorkbook.Units);
|
|
rowHeightTYpe := rhtDefault;
|
|
end;
|
|
|
|
col := 0;
|
|
firstStyleIndex := -1;
|
|
hasRowFormat := true;
|
|
|
|
//process each cell of the row
|
|
cellNode := ARowNode.FirstChild;
|
|
// cellNode := rowNode.FindNode('table:table-cell');
|
|
while Assigned(cellNode) do
|
|
begin
|
|
nodeName := cellNode.NodeName;
|
|
if nodeName = 'table:table-cell' then
|
|
begin
|
|
cellStyleName := GetAttrValue(CellNode, 'table:style-name');
|
|
styleIndex := ExtractFormatIndexFromStyle(cellStyleName, col);
|
|
ReadCell(cellNode, row, col, styleIndex, colsRepeated);
|
|
|
|
// Check whether the current cell format is still the same as for the
|
|
// first cell. If it is then we might have a row format here.
|
|
if (firstStyleIndex = -1) and hasRowFormat then
|
|
firstStyleIndex := styleIndex
|
|
else if (styleIndex <> firstStyleIndex) and (cellStyleName <> 'Default') then
|
|
hasRowFormat := false;
|
|
|
|
// If all cell styles in the row are the same then hasRowFormat is true
|
|
// and we can store the format of the first cell in the row record.
|
|
if GetRowFormat and hasRowFormat and
|
|
(col + colsRepeated >= LongInt(FLimitations.MaxColCount) - 10) and
|
|
(row < FLimitations.MaxRowCount - 10) then
|
|
begin
|
|
lRow := FWorksheet.GetRow(row);
|
|
// Find first cell in row, all cells have the same format here.
|
|
cell := FWorksheet.FindNextCellInRow(row, 0);
|
|
if cell <> nil then
|
|
// Cell found --> copy its format index to cell record
|
|
lRow^.FormatIndex := cell^.FormatIndex
|
|
else
|
|
begin
|
|
// No cell in row --> appy format to dummy cell to get its format index
|
|
InitCell(row, 0, cellRecord);
|
|
ApplyStyleToCell(@cellRecord, styleIndex);
|
|
lRow^.FormatIndex := cellRecord.FormatIndex;
|
|
end;
|
|
end;
|
|
|
|
if (colsRepeated > 1) and (col + colsRepeated < LongInt(FLimitations.MaxColCount) - 10) then
|
|
begin
|
|
// The 2nd condition belongs to a workaround for a bug of LO/OO whichs
|
|
// extends imported xlsx files with blank cols up to their
|
|
// specification limit.
|
|
// React some columns earlier because the added column range is
|
|
// sometimes split into two parts.
|
|
cell := FWorksheet.FindCell(row, col);
|
|
if cell <> nil then
|
|
for i:=1 to colsRepeated-1 do begin
|
|
cell := FWorksheet.CopyCell(row, col, row, col+i);
|
|
styleIndex := ExtractFormatIndexFromStyle(cellStyleName, col+i);
|
|
ApplyStyleToCell(cell, styleIndex);
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
if nodeName = 'table:covered-table-cell' then
|
|
begin
|
|
s := GetAttrValue(cellNode, 'table:number-columns-repeated');
|
|
if s = '' then colsRepeated := 1;
|
|
end else
|
|
colsRepeated := 0;
|
|
|
|
col := col + colsRepeated;
|
|
cellNode := cellNode.NextSibling;
|
|
end; //while Assigned(cellNode)
|
|
|
|
s := GetAttrValue(ARowNode, 'table:number-rows-repeated');
|
|
if s = '' then
|
|
rowsRepeated := 1
|
|
else
|
|
rowsRepeated := StrToInt(s);
|
|
|
|
// Transfer non-default row heights to sheet's rows
|
|
// This first "if" is a workaround for a bug of LO/OO whichs extends imported
|
|
// xlsx files with blank rows up to their specification limit.
|
|
// Process some rows earlier because the added row range is sometimes split
|
|
// into two parts.
|
|
if row + rowsRepeated < LongInt(FLimitations.MaxRowCount) - 10 then
|
|
for i:=1 to rowsRepeated do
|
|
FWorksheet.WriteRowHeight(row + i - 1, rowHeight, FWorkbook.Units, rowHeightType);
|
|
|
|
// Prepare checking of column format
|
|
if GetRowFormat then begin
|
|
// Store the format indexes of all cells in the first row
|
|
if isFirstRow then begin
|
|
SetLength(colFmt, col);
|
|
for col:=0 to High(colFmt) do begin
|
|
cell := FWorksheet.FindCell(row, col);
|
|
if cell <> nil then
|
|
colFmt[col] := cell^.FormatIndex
|
|
else begin
|
|
InitCell(row, col, cellRecord);
|
|
ApplyStyleToCell(@cellRecord, styleIndex);
|
|
colFmt[col] := cellRecord.FormatIndex;
|
|
end;
|
|
end;
|
|
end else
|
|
// In the other rows compare the cell format indexes with those stored
|
|
// from the first row. If an index does not match then this col cannot
|
|
// have a column format.
|
|
for col:=0 to High(colFmt) do begin
|
|
if colFmt[col] > -1 then begin
|
|
cell := FWorksheet.FindCell(row, col);
|
|
if ((cell <> nil) and (cell^.FormatIndex <> colFmt[col])) then
|
|
colFmt[col] := -1;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
row := row + rowsRepeated;
|
|
isFirstRow := false;
|
|
end;
|
|
|
|
var
|
|
PrintRowMode: Boolean;
|
|
c: Cardinal;
|
|
|
|
begin
|
|
rowsRepeated := 0;
|
|
row := 0;
|
|
isFirstRow := true;
|
|
PrintRowMode := false;
|
|
|
|
rnode := ATableNode.FirstChild;
|
|
while Assigned(rNode) do
|
|
begin
|
|
nodename := rNode.NodeName;
|
|
|
|
// Repeated print rows
|
|
if nodeName = 'table:table-header-rows' then
|
|
begin
|
|
PrintRowMode := true;
|
|
if FRepeatedRows.FirstIndex = Cardinal(UNASSIGNED_ROW_COL_INDEX) then
|
|
FRepeatedRows.FirstIndex := row;
|
|
childnode := rNode.FirstChild;
|
|
while Assigned(childnode) do
|
|
begin
|
|
nodename := childnode.NodeName;
|
|
if nodename = 'table:table-row' then
|
|
begin
|
|
ProcessRow(childnode, false);
|
|
end;
|
|
childnode := childnode.NextSibling;
|
|
end;
|
|
FRepeatedRows.LastIndex := row-1;
|
|
end
|
|
else
|
|
// "normal" rows
|
|
if nodeName = 'table:table-row' then
|
|
ProcessRow(rNode, true);
|
|
|
|
rNode := rNode.NextSibling;
|
|
end;
|
|
|
|
// Construct column records with column format
|
|
if not PrintRowMode and (row > FLimitations.MaxRowCount-10) then begin
|
|
for c := 0 to High(colFmt) do
|
|
if colFmt[c] > 0 then
|
|
FWorksheet.WriteColFormatIndex(c, colFmt[c]);
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadRowStyle(AStyleNode: TDOMNode);
|
|
var
|
|
styleName, nodename: String;
|
|
styleChildNode: TDOMNode;
|
|
rowHeight: Double;
|
|
s: String;
|
|
rowStyle: TRowStyleData;
|
|
rowHeightType: TsRowHeightType;
|
|
begin
|
|
styleName := GetAttrValue(AStyleNode, 'style:name');
|
|
styleChildNode := AStyleNode.FirstChild;
|
|
rowHeight := 0;
|
|
rowHeightType := rhtCustom;
|
|
|
|
while Assigned(styleChildNode) do
|
|
begin
|
|
nodename := styleChildNode.NodeName;
|
|
if nodeName = 'style:table-row-properties' then
|
|
begin
|
|
s := GetAttrValue(styleChildNode, 'style:row-height');
|
|
if s <> '' then
|
|
rowHeight := FWorkbook.ConvertUnits(HTMLLengthStrToPts(s), suPoints, FWorkbook.Units);
|
|
// convert to workbook units
|
|
s := GetAttrValue(styleChildNode, 'style:use-optimal-row-height');
|
|
if s = 'true' then
|
|
rowHeightType := rhtAuto;
|
|
end;
|
|
styleChildNode := styleChildNode.NextSibling;
|
|
end;
|
|
|
|
rowStyle := TRowStyleData.Create;
|
|
rowStyle.Name := styleName;
|
|
rowStyle.RowHeight := rowHeight;
|
|
rowStyle.RowHeightType := rowHeightType;
|
|
FRowStyleList.Add(rowStyle);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadSettings(AOfficeSettingsNode: TDOMNode);
|
|
var
|
|
cfgItemSetNode, cfgItemNode, cfgItemMapEntryNode, cfgEntryItemNode, cfgTableItemNode, node: TDOMNode;
|
|
nodeName, cfgName, cfgValue, tblName: String;
|
|
sheet: TsWorksheet;
|
|
vsm, hsm, hsp, vsp: Integer;
|
|
zoom: Double;
|
|
showGrid, showHeaders: Boolean;
|
|
actCol, actRow: Cardinal;
|
|
i: Integer;
|
|
begin
|
|
showGrid := true;
|
|
showHeaders := true;
|
|
zoom := 100.0;
|
|
actRow := 0;
|
|
actCol := 0;
|
|
cfgItemSetNode := AOfficeSettingsNode.FirstChild;
|
|
while Assigned(cfgItemSetNode) do
|
|
begin
|
|
if (cfgItemSetNode.NodeName <> '#text') and
|
|
(GetAttrValue(cfgItemSetNode, 'config:name') = 'ooo:view-settings') then
|
|
begin
|
|
cfgItemNode := cfgItemSetNode.FirstChild;
|
|
while Assigned(cfgItemNode) do begin
|
|
if (cfgItemNode.NodeName <> '#text') and
|
|
(cfgItemNode.NodeName = 'config:config-item-map-indexed') and
|
|
(GetAttrValue(cfgItemNode, 'config:name') = 'Views') then
|
|
begin
|
|
cfgItemMapEntryNode := cfgItemNode.FirstChild;
|
|
while Assigned(cfgItemMapEntryNode) do
|
|
begin
|
|
cfgEntryItemNode := cfgItemMapEntryNode.FirstChild;
|
|
while Assigned(cfgEntryItemNode) do
|
|
begin
|
|
nodeName := cfgEntryItemNode.NodeName;
|
|
if (nodeName = 'config:config-item') then
|
|
begin
|
|
cfgName := lowercase(GetAttrValue(cfgEntryItemNode, 'config:name'));
|
|
if cfgName = 'activetable' then
|
|
begin
|
|
cfgValue := GetNodeValue(cfgEntryItemNode);
|
|
FActiveSheet := cfgValue;
|
|
end else
|
|
if cfgName = 'showgrid' then
|
|
begin
|
|
cfgValue := GetNodeValue(cfgEntryItemNode);
|
|
if cfgValue = 'false' then showGrid := false;
|
|
end else
|
|
if cfgName = 'hascolumnrowheaders' then
|
|
begin
|
|
cfgValue := GetNodeValue(cfgEntryItemNode);
|
|
if cfgValue = 'false' then showHeaders := false;
|
|
end;
|
|
end else
|
|
if (nodeName = 'config:config-item-map-named') and
|
|
(GetAttrValue(cfgEntryItemNode, 'config:name') = 'Tables') then
|
|
begin
|
|
cfgTableItemNode := cfgEntryItemNode.FirstChild;
|
|
while Assigned(cfgTableItemNode) do
|
|
begin
|
|
nodeName := cfgTableItemNode.NodeName;
|
|
if nodeName <> '#text' then
|
|
begin
|
|
tblName := GetAttrValue(cfgTableItemNode, 'config:name');
|
|
if tblName <> '' then
|
|
begin
|
|
hsm := 0; vsm := 0;
|
|
sheet := Workbook.GetWorksheetByName(tblName);
|
|
if sheet <> nil then
|
|
begin
|
|
node := cfgTableItemNode.FirstChild;
|
|
while Assigned(node) do
|
|
begin
|
|
nodeName := node.NodeName;
|
|
if nodeName <> '#text' then
|
|
begin
|
|
cfgName := GetAttrValue(node, 'config:name');
|
|
cfgValue := GetNodeValue(node);
|
|
case cfgName of
|
|
'CursorPositionX': actCol := StrToInt(cfgValue);
|
|
'CursorPositionY': actRow := StrToInt(cfgValue);
|
|
'VerticalSplitMode': vsm := StrToInt(cfgValue);
|
|
'HorizontalSplitMode': hsm := StrToInt(cfgValue);
|
|
'VerticalSplitPosition': vsp := StrToInt(cfgValue);
|
|
'HorizontalSplitPosition': hsp := StrToInt(cfgValue);
|
|
'ZoomValue': zoom := StrToFloat(cfgValue, FPointSeparatorSettings);
|
|
end;
|
|
end;
|
|
node := node.NextSibling;
|
|
end;
|
|
if (hsm = 2) or (vsm = 2) then
|
|
begin
|
|
sheet.Options := sheet.Options + [soHasFrozenPanes];
|
|
sheet.LeftPaneWidth := hsp;
|
|
sheet.TopPaneHeight := vsp;
|
|
end else
|
|
sheet.Options := sheet.Options - [soHasFrozenPanes];
|
|
// Active cell
|
|
sheet.SelectCell(actRow, actCol);
|
|
// Zoom factor
|
|
sheet.ZoomFactor := zoom / 100.0;
|
|
end;
|
|
end;
|
|
end;
|
|
cfgTableItemNode := cfgTableItemNode.NextSibling;
|
|
end;
|
|
end;
|
|
cfgEntryItemNode := cfgEntryItemNode.NextSibling;
|
|
end;
|
|
cfgItemMapEntryNode := cfgItemMapEntryNode.NextSibling;
|
|
end;
|
|
end;
|
|
cfgItemNode := cfgItemNode.NextSibling;
|
|
end;
|
|
end;
|
|
cfgItemSetNode := cfgItemSetNode.NextSibling;
|
|
end;
|
|
|
|
{ Now let's apply the showGrid and showHeader values to all sheets - they
|
|
are document-wide settings (although there is a ShowGrid in the Tables node) }
|
|
for i:=0 to Workbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := Workbook.GetWorksheetByIndex(i);
|
|
if not showGrid then sheet.Options := sheet.Options - [soShowGridLines];
|
|
if not showHeaders then sheet.Options := sheet.Options - [soShowHeaders];
|
|
end;
|
|
end;
|
|
|
|
{ '<draw:frame draw:z-index="%d" draw:name="Image %d" '+
|
|
'draw:style-name="gr1" draw:text-style-name="P1" '+
|
|
'svg:width="%.2fmm" svg:height="%.2fmm" '+
|
|
'svg:x="%.2fmm" svg:y="%.2fmm">' +
|
|
'<draw:image xlink:href="Pictures/%d.%s" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad">' +
|
|
'<text:p />' +
|
|
'</draw:image>' +
|
|
'</draw:frame>', [
|
|
}
|
|
procedure TsSpreadOpenDocReader.ReadShapes(ATableNode: TDOMNode);
|
|
var
|
|
shapesNode, shapeNode, childShapeNode: TDOMNode;
|
|
nodeName: String;
|
|
r, c: Cardinal;
|
|
w, h, x, y: Double;
|
|
dr, dc, sx, sy: Double;
|
|
idx: Integer;
|
|
href: String;
|
|
begin
|
|
shapesNode := ATableNode.FirstChild;
|
|
while Assigned(shapesNode) do
|
|
begin
|
|
nodeName := shapesNode.NodeName;
|
|
if nodeName = 'table:shapes' then
|
|
begin
|
|
shapeNode := shapesNode.FirstChild;
|
|
while Assigned(shapeNode) do
|
|
begin
|
|
nodeName := shapeNode.NodeName;
|
|
if nodeName = 'draw:frame' then
|
|
begin
|
|
x := PtsToMM(HTMLLengthStrToPts(GetAttrValue(shapeNode, 'svg:x')));
|
|
y := PtsToMM(HTMLLengthStrToPts(GetAttrValue(shapeNode, 'svg:y')));
|
|
w := PtsToMM(HTMLLengthStrToPts(GetAttrValue(shapeNode, 'svg:width')));
|
|
h := PtsToMM(HTMLLengthStrToPts(GetAttrValue(shapeNode, 'svg:height')));
|
|
childShapeNode := shapeNode.FirstChild;
|
|
while Assigned(childShapeNode) do
|
|
begin
|
|
href := GetAttrValue(childShapeNode, 'xlink:href');
|
|
if href <> '' then
|
|
begin
|
|
idx := FWorkbook.FindEmbeddedObj(ExtractFileName(href));
|
|
FWorksheet.CalcImageCell(idx, x, y, w, h, r, c, dr, dc, sx, sy);
|
|
FWorksheet.WriteImage(r, c, idx, dc, dr, sx, sy); // order of dc and dr is correct!
|
|
end;
|
|
childShapeNode := childShapeNode.NextSibling;
|
|
end;
|
|
end;
|
|
shapeNode := shapeNode.NextSibling;
|
|
end;
|
|
end;
|
|
shapesNode := shapesNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadSheetProtection(ANode: TDOMNode;
|
|
ASheet: TsWorksheet);
|
|
var
|
|
s: String;
|
|
sp: TsWorksheetProtections;
|
|
cinfo: TsCryptoInfo;
|
|
childNode: TDOMNode;
|
|
nodeName: String;
|
|
begin
|
|
if ANode = nil then
|
|
exit;
|
|
s := GetAttrValue(ANode, 'table:protected');
|
|
if s = 'true' then begin
|
|
sp := DEFAULT_SHEET_PROTECTION;
|
|
Include(sp, spCells);
|
|
|
|
// These items are ALLOWED (unlike Excel where they are FORBIDDEN).
|
|
// <loext:table-protection loext:select-unprotected-cells="true" />
|
|
// <loext:table-protection loext:select-protected-cells="true" />
|
|
// <loext:table-protection />
|
|
childNode := ANode.FirstChild;
|
|
while childNode <> nil do
|
|
begin
|
|
nodeName := childnode.NodeName;
|
|
if nodeName = 'loext:table-protection' then begin
|
|
s := GetAttrValue(childnode, 'loext:select-unprotected-cells');
|
|
if s='true' then Exclude(sp, spSelectUnlockedCells)
|
|
else Include(sp, spSelectUnlockedCells);
|
|
|
|
s := GetAttrValue(childnode, 'loext:select-protected-cells');
|
|
if s='true' then Exclude(sp, spSelectLockedCells)
|
|
else Include(sp, spSelectLockedCells);
|
|
end;
|
|
childNode := childNode.NextSibling;
|
|
end;
|
|
ASheet.Protection := sp;
|
|
ASheet.Protect(true);
|
|
|
|
InitCryptoInfo(cinfo);
|
|
cinfo.PasswordHash := GetAttrValue(ANode, 'table:protection-key');
|
|
cinfo.Algorithm := StrToAlgorithm(GetAttrValue(ANode, 'table:protection-key-digest-algorithm'));
|
|
ASheet.CryptoInfo := cinfo;
|
|
end else
|
|
ASheet.Protect(false);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadStyles(AStylesNode: TDOMNode);
|
|
var
|
|
styleNode: TDOMNode;
|
|
styleChildNode: TDOMNode;
|
|
nodeName: String;
|
|
family: String;
|
|
styleName: String;
|
|
parentstyle: String;
|
|
fmt: TsCellFormat;
|
|
numFmtIndexDefault: Integer;
|
|
numFmtName: String;
|
|
numFmtStr: String;
|
|
numFmtIndex: Integer;
|
|
numFmtParams: TsNumFormatParams;
|
|
clr: TsColor;
|
|
fnt: TsFont;
|
|
fntName: String;
|
|
fntSize: Single;
|
|
fntStyle: TsFontStyles;
|
|
fntColor: TsColor;
|
|
fntPos: TsFontPosition;
|
|
fntIndex: Integer;
|
|
s: String;
|
|
idx: Integer;
|
|
|
|
procedure SetBorderStyle(ABorder: TsCellBorder; AStyleValue: String);
|
|
const
|
|
EPS = 0.1; // takes care of rounding errors for line widths
|
|
var
|
|
L: TStringList;
|
|
i: Integer;
|
|
s: String;
|
|
wid: Double;
|
|
linestyle: String;
|
|
rgb: TsColor;
|
|
p: Integer;
|
|
begin
|
|
L := TStringList.Create;
|
|
try
|
|
L.Delimiter := ' ';
|
|
L.StrictDelimiter := true;
|
|
L.DelimitedText := AStyleValue;
|
|
wid := 0;
|
|
rgb := scNotDefined;
|
|
linestyle := '';
|
|
for i:=0 to L.Count-1 do
|
|
begin
|
|
s := L[i];
|
|
if (s = 'solid') or (s = 'dashed') or (s = 'fine-dashed') or
|
|
(s = 'dotted') or (s = 'double') or (s = 'dash-dot') or
|
|
(s = 'dash-dot-dot') or (s = 'double-thin')
|
|
then begin
|
|
linestyle := s;
|
|
continue;
|
|
end;
|
|
p := pos('pt', s);
|
|
if p = Length(s)-1 then
|
|
begin
|
|
wid := StrToFloat(copy(s, 1, p-1), FPointSeparatorSettings);
|
|
continue;
|
|
end;
|
|
p := pos('mm', s);
|
|
if p = Length(s)-1 then
|
|
begin
|
|
wid := mmToPts(StrToFloat(copy(s, 1, p-1), FPointSeparatorSettings));
|
|
Continue;
|
|
end;
|
|
p := pos('cm', s);
|
|
if p = Length(s)-1 then
|
|
begin
|
|
wid := cmToPts(StrToFloat(copy(s, 1, p-1), FPointSeparatorSettings));
|
|
Continue;
|
|
end;
|
|
rgb := HTMLColorStrToColor(s);
|
|
end;
|
|
fmt.BorderStyles[ABorder].LineStyle := lsThin;
|
|
if (linestyle = 'solid') then
|
|
begin
|
|
if (wid >= 2.4 - EPS) then fmt.BorderStyles[ABorder].LineStyle := lsThick
|
|
else if (wid >= 1.7 - EPS) then fmt.BorderStyles[ABorder].LineStyle := lsMedium
|
|
end else
|
|
if (linestyle = 'dotted') then
|
|
fmt.BorderStyles[ABorder].LineStyle := lsHair
|
|
else
|
|
if (linestyle = 'dashed') then
|
|
begin
|
|
if (wid >= 1.7 - EPS) then fmt.BorderStyles[ABorder].LineStyle := lsMediumDash
|
|
else fmt.BorderStyles[ABorder].LineStyle := lsDashed
|
|
end else
|
|
if (linestyle = 'dash-dot') then
|
|
begin
|
|
if (wid >= 1.7 - EPS) then fmt.BorderStyles[ABorder].LineStyle := lsMediumDashDot
|
|
else fmt.BorderStyles[ABorder].LineStyle := lsDashDot
|
|
end else
|
|
if (linestyle = 'dash-dot-dot') then
|
|
begin
|
|
if (wid >= 1.7 - EPS) then fmt.BorderStyles[ABorder].LineStyle := lsMediumDashDotDot
|
|
else fmt.BorderStyles[ABorder].LineStyle := lsDashDotDot
|
|
end else
|
|
if (linestyle = 'fine-dashed') then
|
|
fmt.BorderStyles[ABorder].LineStyle := lsDotted
|
|
else
|
|
if (linestyle = 'double') or (linestyle = 'double-thin') then
|
|
fmt.BorderStyles[ABorder].LineStyle := lsDouble;
|
|
fmt.BorderStyles[ABorder].Color := IfThen(rgb = scNotDefined, scBlack, rgb);
|
|
finally
|
|
L.Free;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
if not Assigned(AStylesNode) then
|
|
exit;
|
|
|
|
nodeName := AStylesNode.NodeName;
|
|
numFmtIndexDefault := FindNumFormatByName('N0');
|
|
|
|
styleNode := AStylesNode.FirstChild;
|
|
while Assigned(styleNode) do begin
|
|
nodeName := styleNode.NodeName;
|
|
if nodeName = 'style:default-style' then
|
|
begin
|
|
family := GetAttrValue(stylenode, 'style:family');
|
|
if family = 'table-cell' then begin
|
|
InitFormatRecord(fmt);
|
|
fmt.Name := 'DefaultStyle';
|
|
fnt := FWorkbook.GetFont(fmt.FontIndex);
|
|
fntName := fnt.FontName;
|
|
fntSize := fnt.Size;
|
|
fntStyle := fnt.Style;
|
|
fntColor := fnt.Color;
|
|
fntPos := fnt.Position;
|
|
styleChildNode := stylenode.FirstChild;
|
|
while Assigned(styleChildNode) do begin
|
|
nodename := styleChildNode.NodeName;
|
|
if nodename = 'style:text-properties' then
|
|
ReadFont(
|
|
styleNode.FindNode('style:text-properties'),
|
|
fntName, fntSize, fntStyle, fntColor, fntPos
|
|
)
|
|
// fmt.FontIndex := ReadFont(styleNode.FindNode('style:text-properties'), DEFAULT_FONTINDEX)
|
|
else
|
|
if nodename = 'style:paragraph-properties' then;
|
|
// not used;
|
|
styleChildNode := styleChildNode.nextSibling;
|
|
end;
|
|
fmt.FontIndex := FWorkbook.FindFont(fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
if fmt.FontIndex = -1 then
|
|
fmt.FontIndex := FWorkbook.AddFont(fntname, fntsize, fntstyle, fntColor, fntPos);
|
|
if fmt.FontIndex > 0 then
|
|
Include(fmt.UsedFormattingFields, uffFont);
|
|
FCellFormatList.Add(fmt);
|
|
end;
|
|
end else
|
|
if nodeName = 'style:style' then
|
|
begin
|
|
family := GetAttrValue(styleNode, 'style:family');
|
|
parentstyle := GetAttrValue(stylenode, 'style:parent-style-name');
|
|
|
|
// Table styles
|
|
if family = 'table' then
|
|
ReadTableStyle(styleNode);
|
|
|
|
// Column styles
|
|
if family = 'table-column' then
|
|
ReadColumnStyle(styleNode);
|
|
|
|
// Row styles
|
|
if family = 'table-row' then
|
|
ReadRowStyle(styleNode);
|
|
|
|
// Cell styles
|
|
if family = 'table-cell' then
|
|
begin
|
|
styleName := GetAttrValue(styleNode, 'style:name');
|
|
|
|
InitFormatRecord(fmt);
|
|
|
|
if parentstyle <> '' then
|
|
begin
|
|
idx := FCellFormatList.FindIndexOfName(parentstyle);
|
|
if idx > -1 then
|
|
fmt := FCellFormatList[idx]^;
|
|
end else
|
|
if styleName <> '' then
|
|
begin
|
|
idx := FCellFormatList.FindIndexOfName(stylename);
|
|
if idx > -1 then
|
|
fmt := FCellFormatList[idx]^;
|
|
end;
|
|
fmt.Name := styleName;
|
|
|
|
fnt := FWorkbook.GetFont(fmt.FontIndex);
|
|
fntName := fnt.FontName;
|
|
fntSize := fnt.Size;
|
|
fntStyle := fnt.Style;
|
|
fntColor := fnt.Color;
|
|
fntPos := fnt.Position;
|
|
|
|
numFmtIndex := -1;
|
|
numFmtName := GetAttrValue(styleNode, 'style:data-style-name');
|
|
if numFmtName <> '' then numFmtIndex := FindNumFormatByName(numFmtName);
|
|
if numFmtIndex = -1 then numFmtIndex := numFmtIndexDefault;
|
|
numFmtStr := NumFormatList[numFmtIndex];
|
|
numFmtStr := Copy(numFmtStr, pos(':', numFmtStr)+1, Length(numFmtStr));
|
|
fmt.NumberFormatIndex := Workbook.AddNumberFormat(numFmtStr);
|
|
numFmtParams := Workbook.GetNumberFormat(fmt.NumberFormatIndex);
|
|
if numFmtParams <> nil then begin
|
|
fmt.NumberFormat := numFmtParams.NumFormat;
|
|
fmt.NumberFormatStr := numFmtStr;
|
|
Include(fmt.UsedFormattingFields, uffNumberFormat);
|
|
end;
|
|
|
|
styleChildNode := styleNode.FirstChild;
|
|
while Assigned(styleChildNode) do
|
|
begin
|
|
nodeName := styleChildNode.NodeName;
|
|
if nodeName = 'style:text-properties' then
|
|
begin
|
|
ReadFont(styleChildNode, fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
if SameText(stylename, 'Default') then
|
|
begin
|
|
FWorkbook.ReplaceFont(DEFAULT_FONTINDEX, fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
fmt.FontIndex := DEFAULT_FONTINDEX;
|
|
//fntIndex := ReadFont(styleChildNode, DEFAULT_FONTINDEX)
|
|
end else
|
|
if SameText(stylename, 'Excel_20_Built-in_20_Hyperlink') then
|
|
begin
|
|
FWorkbook.ReplaceFont(HYPERLINK_FONTINDEX, fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
fmt.FontIndex := HYPERLINK_FONTINDEX;
|
|
//fntIndex := ReadFont(styleChildNode, HYPERLINK_FONTINDEX)
|
|
end else
|
|
begin
|
|
fmt.FontIndex := FWorkbook.FindFont(fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
if fmt.FontIndex = -1 then
|
|
fmt.FontIndex := FWorkbook.AddFont(fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
end;
|
|
if fmt.FontIndex > 0 then
|
|
Include(fmt.UsedFormattingFields, uffFont);
|
|
// fntIndex := ReadFont(styleChildNode);
|
|
// fnt := FWorkbook.GetFont(fntIndex);
|
|
end else
|
|
if nodeName = 'style:table-cell-properties' then
|
|
begin
|
|
// Background color
|
|
s := GetAttrValue(styleChildNode, 'fo:background-color');
|
|
if (s <> '') and (s <> 'transparent') then begin
|
|
clr := HTMLColorStrToColor(s);
|
|
// ODS does not support background fill patterns!
|
|
fmt.Background.FgColor := IfThen(clr = scNotDefined, scTransparent, clr);
|
|
fmt.Background.BgColor := fmt.Background.FgColor;
|
|
if (fmt.Background.BgColor <> scTransparent) then
|
|
begin
|
|
fmt.Background.Style := fsSolidFill;
|
|
Include(fmt.UsedFormattingFields, uffBackground);
|
|
end;
|
|
end;
|
|
// Borders
|
|
s := GetAttrValue(styleChildNode, 'fo:border');
|
|
if (s <> '') and (s <> 'none') then
|
|
begin
|
|
fmt.Border := fmt.Border + [cbNorth, cbSouth, cbEast, cbWest];
|
|
SetBorderStyle(cbNorth, s);
|
|
SetBorderStyle(cbSouth, s);
|
|
SetBorderStyle(cbEast, s);
|
|
SetBorderStyle(cbWest, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
s := GetAttrValue(styleChildNode, 'fo:border-top');
|
|
if (s <> '') and (s <> 'none') then
|
|
begin
|
|
Include(fmt.Border, cbNorth);
|
|
SetBorderStyle(cbNorth, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
s := GetAttrValue(styleChildNode, 'fo:border-right');
|
|
if (s <> '') and (s <> 'none') then
|
|
begin
|
|
Include(fmt.Border, cbEast);
|
|
SetBorderStyle(cbEast, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
s := GetAttrValue(styleChildNode, 'fo:border-bottom');
|
|
if (s <> '') and (s <> 'none') then
|
|
begin
|
|
Include(fmt.Border, cbSouth);
|
|
SetBorderStyle(cbSouth, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
s := GetAttrValue(styleChildNode, 'fo:border-left');
|
|
if (s <> '') and (s <> 'none') then
|
|
begin
|
|
Include(fmt.Border, cbWest);
|
|
SetBorderStyle(cbWest, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
s := GetAttrValue(styleChildNode, 'style:diagonal-bl-tr');
|
|
if (s <> '') and (s <> 'none') then
|
|
begin
|
|
Include(fmt.Border, cbDiagUp);
|
|
SetBorderStyle(cbDiagUp, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
s := GetAttrValue(styleChildNode, 'style:diagonal-tl-br');
|
|
if (s <> '') and (s <>'none') then
|
|
begin
|
|
Include(fmt.Border, cbDiagDown);
|
|
SetBorderStyle(cbDiagDown, s);
|
|
Include(fmt.UsedFormattingFields, uffBorder);
|
|
end;
|
|
|
|
// Text wrap
|
|
s := GetAttrValue(styleChildNode, 'fo:wrap-option');
|
|
if (s='wrap') then
|
|
Include(fmt.UsedFormattingFields, uffWordwrap);
|
|
|
|
// Test rotation
|
|
s := GetAttrValue(styleChildNode, 'style:rotation-angle');
|
|
if s = '90' then
|
|
fmt.TextRotation := rt90DegreeCounterClockwiseRotation
|
|
else if s = '270' then
|
|
fmt.TextRotation := rt90DegreeClockwiseRotation;
|
|
s := GetAttrValue(styleChildNode, 'style:direction');
|
|
if s = 'ttb' then
|
|
fmt.TextRotation := rtStacked;
|
|
if fmt.TextRotation <> trHorizontal then
|
|
Include(fmt.UsedFormattingFields, uffTextRotation);
|
|
|
|
// Vertical text alignment
|
|
s := GetAttrValue(styleChildNode, 'style:vertical-align');
|
|
if s = 'top' then
|
|
fmt.VertAlignment := vaTop
|
|
else if s = 'middle' then
|
|
fmt.VertAlignment := vaCenter
|
|
else if s = 'bottom' then
|
|
fmt.VertAlignment := vaBottom;
|
|
if fmt.VertAlignment <> vaDefault then
|
|
Include(fmt.UsedFormattingFields, uffVertAlign);
|
|
|
|
// Protection
|
|
s := GetAttrValue(styleChildNode, 'style:cell-protect');
|
|
if s = 'none' then
|
|
fmt.Protection := []
|
|
else if (s = 'protected formula-hidden') or (s = 'formula-hidden protected') then
|
|
fmt.Protection := [cpLockCell, cpHideFormulas]
|
|
else if s = 'protected' then
|
|
fmt.Protection := [cpLockCell]
|
|
else if s = 'formula-hidden' then
|
|
fmt.Protection := [cpHideFormulas]
|
|
else if s = 'hidden-and-protected' then
|
|
fmt.Protection := [cpLockCell, cpHideFormulas];
|
|
// NOTE: This not exact... According to
|
|
// https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html,
|
|
// section 20.246, this hides and locks cell content, not just
|
|
// formulas...
|
|
if fmt.Protection <> DEFAULT_CELL_PROTECTION then
|
|
Include(fmt.UsedFormattingFields, uffProtection);
|
|
end
|
|
else
|
|
if nodeName = 'style:paragraph-properties' then
|
|
begin
|
|
// Horizontal text alignment
|
|
s := GetAttrValue(styleChildNode, 'fo:text-align');
|
|
if s = 'start' then
|
|
fmt.HorAlignment := haLeft
|
|
else if s = 'end' then
|
|
fmt.HorAlignment := haRight
|
|
else if s = 'center' then
|
|
fmt.HorAlignment := haCenter;
|
|
if fmt.HorAlignment <> haDefault then
|
|
Include(fmt.UsedFormattingFields, uffHorAlign);
|
|
// BiDi mode
|
|
s := GetAttrValue(styleChildNode, 'style:writing-mode');
|
|
if s = 'lr-tb' then
|
|
fmt.BiDiMode := bdRTL
|
|
else if s = 'rl-tb' then
|
|
fmt.BiDiMode := bdRTL;
|
|
if fmt.BiDiMode <> bdDefault then
|
|
Include(fmt.UsedFormattingFields, uffBiDi);
|
|
end;
|
|
styleChildNode := styleChildNode.NextSibling;
|
|
end;
|
|
FCellFormatList.Add(fmt);
|
|
end
|
|
else
|
|
if family = 'text' then
|
|
begin
|
|
// "Rich-text formatting run" style
|
|
// Nodes are named "T1", "T2", etc.
|
|
styleName := GetAttrValue(styleNode, 'style:name');
|
|
styleChildNode := styleNode.FirstChild;
|
|
while Assigned(styleChildNode) do
|
|
begin
|
|
nodeName := styleChildNode.NodeName;
|
|
if nodeName = 'style:text-properties' then
|
|
begin
|
|
// Setup default values which identify font elements to be replaced
|
|
// by the cell font value
|
|
fntName := '';
|
|
fntSize := -1;
|
|
fntStyle := [];
|
|
fntColor := scNone;
|
|
fntPos := fpNormal;
|
|
ReadFont(styleChildNode, fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
// Does this font already exist in the FRichTextFontList?
|
|
fntIndex := FindFontInList(FRichTextFontList, fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
// No - add the font to the list.
|
|
if fntIndex = -1 then
|
|
begin
|
|
fnt := TsFont.Create(fntName, fntSize, fntStyle, fntColor, fntPos);
|
|
fntIndex := FRichTextFontList.Add(fnt);
|
|
end;
|
|
|
|
// Store this is in a dummy format in the cell format list
|
|
InitFormatRecord(fmt);
|
|
fmt.Name := styleName;
|
|
fmt.FontIndex := fntIndex;
|
|
Include(fmt.UsedFormattingFields, uffFont);
|
|
FCellFormatList.Add(fmt);
|
|
end;
|
|
styleChildNode := stylechildNode.NextSibling;
|
|
end;
|
|
end;
|
|
end;
|
|
styleNode := styleNode.NextSibling;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocReader.ReadTableStyle(AStyleNode: TDOMNode);
|
|
var
|
|
stylename, nodename: String;
|
|
styleChildNode: TDOMNode;
|
|
bidi: String;
|
|
tablestyle: TTableStyleData;
|
|
display: String;
|
|
begin
|
|
// nodeName := GetAttrValue(AStyleNode, 'style:name');
|
|
stylename := GetAttrValue(AStyleNode, 'style:name');
|
|
styleChildNode := AStyleNode.FirstChild;
|
|
|
|
while Assigned(styleChildNode) do
|
|
begin
|
|
nodename := styleChildNode.NodeName;
|
|
if nodeName = 'style:table-properties' then
|
|
begin
|
|
// stylename := GetAttrValue(styleChildNode, 'style:name');
|
|
bidi := GetAttrValue(styleChildNode, 'style:writing-mode');
|
|
display := GetAttrValue(styleChildNode, 'table:display');
|
|
end;
|
|
styleChildNode := styleChildNode.NextSibling;
|
|
end;
|
|
|
|
tablestyle := TTableStyleData.Create;
|
|
tablestyle.Name := styleName;
|
|
if bidi = 'rl-tb' then
|
|
tablestyle.BiDiMode := bdRTL else
|
|
tablestyle.BiDiMode := bdLTR;
|
|
tablestyle.Hidden := display = 'false';
|
|
FTableStyleList.Add(tablestyle);
|
|
end;
|
|
|
|
|
|
{ TsSpreadOpenDocWriter }
|
|
|
|
procedure TsSpreadOpenDocWriter.AddBuiltinNumFormats;
|
|
begin
|
|
FNumFormatList.Clear;
|
|
FNumFormatList.Add('N0:');
|
|
end;
|
|
|
|
{ Creates the streams for the individual data files. Will be zipped into a
|
|
single xlsx file. }
|
|
procedure TsSpreadOpenDocWriter.CreateStreams;
|
|
begin
|
|
FSMeta := CreateTempStream(FWorkbook, 'fpsM');
|
|
FSSettings := CreateTempStream(FWorkbook, 'fpsS');
|
|
FSStyles := CreateTempStream(FWorkbook, 'fpsSTY');
|
|
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;
|
|
}
|
|
// FSSheets will be created when needed.
|
|
end;
|
|
|
|
{ Destroys the temporary streams that were created by the writer }
|
|
procedure TsSpreadOpenDocWriter.DestroyStreams;
|
|
begin
|
|
DestroyTempStream(FSMeta);
|
|
DestroyTempStream(FSSettings);
|
|
DestroyTempStream(FSStyles);
|
|
DestroyTempStream(FSContent);
|
|
DestroyTempStream(FSMimeType);
|
|
DestroyTempStream(FSMetaInfManifest);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.GetHeaderFooterImageName(
|
|
APageLayout: TsPageLayout; out AHeader, AFooter: String);
|
|
var
|
|
sct: TsHeaderFooterSectionIndex;
|
|
img: TsHeaderFooterImage;
|
|
ext: String;
|
|
begin
|
|
AHeader := '';
|
|
AFooter := '';
|
|
if APageLayout.HasHeaderFooterImages then
|
|
begin
|
|
// ods supports only a single image per header/footer. We use the first one.
|
|
for sct in TsHeaderFooterSectionIndex do
|
|
if APageLayout.HeaderImages[sct].Index > -1 then
|
|
begin
|
|
img := APageLayout.HeaderImages[sct];
|
|
ext := GetImageTypeExt(FWorkbook.GetEmbeddedObj(img.Index).ImageType);
|
|
AHeader := Format('%d.%s', [img.Index+1, ext]);
|
|
break;
|
|
end;
|
|
for sct in TsHeaderFooterSectionIndex do
|
|
if APageLayout.FooterImages[sct].Index > -1 then
|
|
begin
|
|
img := APageLayout.FooterImages[sct];
|
|
ext := GetImageTypeExt(FWorkbook.GetEmbeddedObj(img.Index).Imagetype);
|
|
AFooter := Format('%d.%s', [img.Index+1, ext]);
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.GetHeaderFooterImagePosStr(
|
|
APagelayout: TsPageLayout; out AHeader, AFooter: String);
|
|
|
|
function GetPosStr(tags: String): String;
|
|
begin
|
|
if tags[1] in ['L', 'x'] then
|
|
Result := 'left' else
|
|
if tags[2] in ['C', 'x'] then
|
|
Result := 'center' else
|
|
if tags[3] in ['R', 'x'] then
|
|
Result := 'right'
|
|
else
|
|
Result := '';
|
|
end;
|
|
|
|
var
|
|
hdrTags, ftrTags: String;
|
|
begin
|
|
APageLayout.GetImageSections(hdrTags, ftrTags);
|
|
AHeader := GetPosStr(hdrTags);
|
|
AFooter := GetPosStr(ftrTags);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.InternalWriteToStream(AStream: TStream);
|
|
var
|
|
FZip: TZipper;
|
|
begin
|
|
{ Analyze the workbook and collect all information needed }
|
|
ListAllNumFormats;
|
|
ListAllColumnStyles;
|
|
ListAllRowStyles;
|
|
ListAllHeaderFooterFonts;
|
|
|
|
{ Create the streams that will hold the file contents }
|
|
CreateStreams;
|
|
|
|
{ Fill the strings with the contents of the files }
|
|
WriteMimetype();
|
|
WriteMetaInfManifest();
|
|
WriteMeta();
|
|
WriteSettings();
|
|
WriteStyles();
|
|
WriteContent;
|
|
|
|
{ Now compress the files }
|
|
FZip := TZipper.Create;
|
|
try
|
|
FZip.FileName := GetTempFilename; // needed if the zipped file is too big for in-memory processing
|
|
FZip.Entries.AddFileEntry(FSMeta, OPENDOC_PATH_META);
|
|
FZip.Entries.AddFileEntry(FSSettings, OPENDOC_PATH_SETTINGS);
|
|
FZip.Entries.AddFileEntry(FSStyles, OPENDOC_PATH_STYLES);
|
|
FZip.Entries.AddFileEntry(FSContent, OPENDOC_PATH_CONTENT);
|
|
FZip.Entries.AddFileEntry(FSMimetype, OPENDOC_PATH_MIMETYPE);
|
|
FZip.Entries.AddFileEntry(FSMetaInfManifest, OPENDOC_PATH_METAINF_MANIFEST);
|
|
ZipPictures(FZip);
|
|
|
|
ResetStreams;
|
|
|
|
FZip.SaveToStream(AStream);
|
|
|
|
finally
|
|
DestroyStreams;
|
|
FZip.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.ListAllColumnStyles;
|
|
var
|
|
i, j, c: Integer;
|
|
sheet: TsWorksheet;
|
|
found: Boolean;
|
|
colstyle: TColumnStyleData;
|
|
w: Double;
|
|
col: PCol;
|
|
begin
|
|
{ At first, add the default column width }
|
|
colStyle := TColumnStyleData.Create;
|
|
colStyle.Name := 'co1';
|
|
colStyle.ColWidth := FWorkbook.ConvertUnits(12, suChars, FWorkbook.Units);
|
|
FColumnStyleList.Add(colStyle);
|
|
|
|
{ Then iterate through all sheets and all columns and store the unique
|
|
column widths in the FColumnStyleList. }
|
|
for i:=0 to Workbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := Workbook.GetWorksheetByIndex(i);
|
|
for c := 0 to sheet.Cols.Count-1 do
|
|
begin
|
|
col := PCol(sheet.Cols[c]);
|
|
if (col <> nil) and (col^.ColWidthType = cwtCustom) then
|
|
begin
|
|
w := col^.Width; // is in workbook units
|
|
// Look for this width in the current ColumnStyleList
|
|
found := false;
|
|
for j := 0 to FColumnStyleList.Count - 1 do
|
|
if SameValue(TColumnStyleData(FColumnstyleList[j]).ColWidth, w, COLWIDTH_EPS) then
|
|
begin
|
|
found := true;
|
|
break;
|
|
end;
|
|
// Not found? Then add the column as a new column style
|
|
if not found then
|
|
begin
|
|
colStyle := TColumnStyleData.Create;
|
|
colStyle.Name := Format('co%d', [FColumnStyleList.Count + 1]);
|
|
colStyle.ColWidth := w;
|
|
FColumnStyleList.Add(colStyle);
|
|
end;
|
|
end;
|
|
end;
|
|
{
|
|
for c:=0 to sheet.GetLastColIndex do
|
|
begin
|
|
w := sheet.GetColWidth(c, FWorkbook.Units);
|
|
// Look for this width in the current ColumnStyleList
|
|
found := false;
|
|
for j := 0 to FColumnStyleList.Count-1 do
|
|
if SameValue(TColumnStyleData(FColumnStyleList[j]).ColWidth, w, COLWIDTH_EPS)
|
|
then begin
|
|
found := true;
|
|
break;
|
|
end;
|
|
// Not found? Then add the column as new column style
|
|
if not found then
|
|
begin
|
|
colStyle := TColumnStyleData.Create;
|
|
colStyle.Name := Format('co%d', [FColumnStyleList.Count+1]);
|
|
colStyle.ColWidth := w;
|
|
FColumnStyleList.Add(colStyle);
|
|
end;
|
|
end;
|
|
}
|
|
end;
|
|
(*
|
|
{ fpspreadsheet's column width is the count of '0' characters of the
|
|
default font. On average, the width of the '0' is about half of the
|
|
point size of the font. --> we can convert the fps col width to pts and
|
|
then to millimeters. }
|
|
multiplier := Workbook.GetFont(0).Size / 2;
|
|
for i:=0 to FColumnStyleList.Count-1 do
|
|
begin
|
|
w := TColumnStyleData(FColumnStyleList[i]).ColWidth * multiplier;
|
|
TColumnStyleData(FColumnStyleList[i]).ColWidth := PtsToMM(w);
|
|
end;
|
|
*)
|
|
end;
|
|
|
|
{ Collects the fonts used by headers and footers in the FHeaderFooterFontList }
|
|
procedure TsSpreadOpenDocWriter.ListAllHeaderFooterFonts;
|
|
|
|
{ Add the fonts used in the specified header/footer line to the
|
|
HeaderFooterFontList. This is done while the HeaderFooterParser is created. }
|
|
procedure AddFontsOfHeaderFooter(AText: String; ADefaultFont: TsHeaderFooterFont);
|
|
begin
|
|
TsSpreadOpenDocHeaderFooterParser.Create(AText, FHeaderFooterFontList, ADefaultFont).Free;
|
|
end;
|
|
|
|
var
|
|
defFnt: TsHeaderFooterFont;
|
|
i: Integer;
|
|
sheet: TsWorksheet;
|
|
begin
|
|
defFnt := TsHeaderFooterFont.Create(Workbook.GetDefaultFont);
|
|
try
|
|
for i:=0 to Workbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := Workbook.GetWorksheetByIndex(i);
|
|
AddFontsOfHeaderFooter(sheet.pageLayout.Headers[1], defFnt);
|
|
AddFontsOfHeaderFooter(sheet.PageLayout.Headers[2], defFnt);
|
|
AddFontsOfHeaderFooter(sheet.PageLayout.Footers[1], defFnt);
|
|
AddFontsOfHeaderFooter(sheet.PageLayout.Footers[2], defFnt);
|
|
end;
|
|
finally
|
|
defFnt.Free;
|
|
end;
|
|
end;
|
|
|
|
{ Contains all number formats used in the workbook. Overrides the inherited
|
|
method to assign a unique name according to the OpenDocument syntax ("N<number>"
|
|
to the format items. }
|
|
procedure TsSpreadOpenDocWriter.ListAllNumFormats;
|
|
const
|
|
FMT_BASE = 1000; // Format number to start with. Not clear if this is correct...
|
|
var
|
|
i: Integer;
|
|
nfparams: TsNumFormatParams;
|
|
begin
|
|
// The default format has already been added.
|
|
for i:=0 to Workbook.GetNumberFormatCount - 1 do
|
|
begin
|
|
nfParams := Workbook.GetNumberFormat(i);
|
|
if nfParams <> nil then
|
|
FNumFormatList.Add(Format('N%d:%s', [FMT_BASE+i, nfParams.NumFormatStr]));
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.ListAllRowStyles;
|
|
var
|
|
i, j, r: Integer;
|
|
sheet: TsWorksheet;
|
|
row: PRow;
|
|
found: Boolean;
|
|
rowstyle: TRowStyleData;
|
|
h: Double;
|
|
begin
|
|
{ At first, add the default row height }
|
|
{ Initially, row height units will be the same as in the workbook }
|
|
rowStyle := TRowStyleData.Create;
|
|
rowStyle.Name := 'ro1';
|
|
rowStyle.RowHeight := FWorkbook.ConvertUnits(15, suPoints, FWorkbook.Units);
|
|
rowStyle.RowHeightType := rhtAuto;
|
|
FRowStyleList.Add(rowStyle);
|
|
|
|
for i:=0 to Workbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := Workbook.GetWorksheetByIndex(i);
|
|
for r:=0 to sheet.GetLastRowIndex do
|
|
begin
|
|
row := sheet.FindRow(r);
|
|
if row <> nil then
|
|
begin
|
|
h := sheet.GetRowHeight(r, FWorkbook.Units);
|
|
// Look for this height in the current RowStyleList
|
|
found := false;
|
|
for j:=0 to FRowStyleList.Count-1 do
|
|
if SameValue(TRowStyleData(FRowStyleList[j]).RowHeight, h, ROWHEIGHT_EPS)
|
|
and (TRowStyleData(FRowStyleList[j]).RowHeightType = row^.RowHeightType)
|
|
then begin
|
|
found := true;
|
|
break;
|
|
end;
|
|
// Not found? Then add the row as a new row style
|
|
if not found then
|
|
begin
|
|
rowStyle := TRowStyleData.Create;
|
|
rowStyle.Name := Format('ro%d', [FRowStyleList.Count+1]);
|
|
rowStyle.RowHeight := h;
|
|
rowStyle.RowHeightType := row^.RowHeightType;
|
|
FRowStyleList.Add(rowStyle);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ Is called before zipping the individual file parts. Rewinds the streams. }
|
|
procedure TsSpreadOpenDocWriter.ResetStreams;
|
|
begin
|
|
FSMeta.Position := 0;
|
|
FSSettings.Position := 0;
|
|
FSStyles.Position := 0;
|
|
FSContent.Position := 0;
|
|
FSMimeType.Position := 0;
|
|
FSMetaInfManifest.Position := 0;
|
|
end;
|
|
|
|
{ Writes the node "office:automatic-styles". Although this node occurs in both
|
|
"contents.xml" and "styles.xml" files, this method is called only for writing
|
|
to "styles.xml". }
|
|
procedure TsSpreadOpenDocWriter.WriteAutomaticStyles(AStream: TStream);
|
|
var
|
|
i: Integer;
|
|
sheet: TsWorksheet;
|
|
fnt: TXMLHeaderFooterFont;
|
|
|
|
begin
|
|
AppendToStream(AStream,
|
|
'<office:automatic-styles>');
|
|
|
|
AppendToStream(AStream,
|
|
'<style:page-layout style:name="Mpm1">' +
|
|
|
|
'<style:page-layout-properties '+
|
|
'fo:margin-top="1.25cm" '+
|
|
'fo:margin-bottom="1.25cm" '+
|
|
'fo:margin-left="1.905cm" '+
|
|
'fo:margin-right="1.905cm" />' +
|
|
|
|
'<style:header-style>' +
|
|
'<style:header-footer-properties '+
|
|
'fo:min-height="0.751cm" '+
|
|
'fo:margin-left="0cm" '+
|
|
'fo:margin-right="0cm" '+
|
|
'fo:margin-bottom="0.25cm" '+
|
|
'fo:margin-top="0cm" />' +
|
|
'</style:header-style>' +
|
|
|
|
'<style:footer-style>' +
|
|
'<style:header-footer-properties '+
|
|
'fo:min-height="0.751cm" '+
|
|
'fo:margin-left="0cm" '+
|
|
'fo:margin-right="0cm" '+
|
|
'fo:margin-top="0.25cm" '+
|
|
'fo:margin-bottom="0cm" />' +
|
|
'</style:footer-style>' +
|
|
|
|
'</style:page-layout>');
|
|
|
|
for i:=0 to FWorkbook.GetWorksheetCount-1 do begin
|
|
sheet := FWorkbook.GetWorksheetByIndex(i);
|
|
AppendToStream(AStream,
|
|
WritePageLayoutXMLAsString('Mpm' + IntToStr(3+i), sheet.PageLayout));
|
|
end;
|
|
|
|
for i:=0 to FHeaderFooterFontList.Count-1 do
|
|
begin
|
|
fnt := TXMLHeaderFooterFont(FHeaderFooterFontList[i]);
|
|
fnt.StyleName := 'MT' + IntToStr(i+1);
|
|
AppendToStream(AStream, Format(
|
|
'<style:style style:name="%s" style:family="text">' +
|
|
'<style:text-properties %s />' +
|
|
'</style:style>', [
|
|
fnt.StyleName, WriteHeaderFooterFontXMLAsString(fnt)
|
|
]));
|
|
end;
|
|
|
|
AppendToStream(AStream,
|
|
'</office:automatic-styles>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteMetaInfManifest;
|
|
var
|
|
i: Integer;
|
|
ext: String;
|
|
mime: String;
|
|
imgtype: Integer;
|
|
embObj: TsEmbeddedObj;
|
|
begin
|
|
AppendToStream(FSMetaInfManifest,
|
|
'<manifest:manifest xmlns:manifest="' + SCHEMAS_XMLNS_MANIFEST + '">');
|
|
AppendToStream(FSMetaInfManifest,
|
|
'<manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.spreadsheet" manifest:full-path="/" />');
|
|
AppendToStream(FSMetaInfManifest,
|
|
'<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml" />');
|
|
AppendToStream(FSMetaInfManifest,
|
|
'<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="styles.xml" />');
|
|
AppendToStream(FSMetaInfManifest,
|
|
'<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="meta.xml" />');
|
|
AppendToStream(FSMetaInfManifest,
|
|
'<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="settings.xml" />');
|
|
for i:=0 to FWorkbook.GetEmbeddedObjCount-1 do
|
|
begin
|
|
embObj := FWorkbook.GetEmbeddedObj(i);
|
|
imgtype := embObj.ImageType;
|
|
if imgtype = itUnknown then
|
|
continue;
|
|
mime := GetImageMimeType(imgtype);
|
|
ext := GetImageTypeExt(imgType);
|
|
AppendToStream(FSMetaInfManifest, Format(
|
|
'<manifest:file-entry manifest:media-type="%s" manifest:full-path="Pictures/%d.%s" />',
|
|
[mime, i+1, ext]
|
|
));
|
|
end;
|
|
AppendToStream(FSMetaInfManifest,
|
|
'</manifest:manifest>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteMeta;
|
|
begin
|
|
AppendToStream(FSMeta,
|
|
XML_HEADER);
|
|
AppendToStream(FSMeta,
|
|
'<office:document-meta xmlns:office="' + SCHEMAS_XMLNS_OFFICE +
|
|
'" xmlns:dcterms="' + SCHEMAS_XMLNS_DCTERMS +
|
|
'" xmlns:meta="' + SCHEMAS_XMLNS_META +
|
|
'" xmlns="' + SCHEMAS_XMLNS +
|
|
'" xmlns:ex="' + SCHEMAS_XMLNS + '">');
|
|
AppendToStream(FSMeta,
|
|
'<office:meta>',
|
|
'<meta:generator>FPSpreadsheet Library</meta:generator>' +
|
|
'<meta:document-statistic />',
|
|
'</office:meta>');
|
|
AppendToStream(FSMeta,
|
|
'</office:document-meta>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteMimetype;
|
|
begin
|
|
AppendToStream(FSMimeType,
|
|
'application/vnd.oasis.opendocument.spreadsheet'
|
|
);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.ZipPictures(AZip: TZipper);
|
|
var
|
|
i: Integer;
|
|
embObj: TsEmbeddedObj;
|
|
embName: String;
|
|
ext: String;
|
|
begin
|
|
for i:=0 to FWorkbook.GetEmbeddedObjCount-1 do
|
|
begin
|
|
embObj := FWorkbook.GetEmbeddedObj(i);
|
|
// The original ods files have a very long, ranomd, unique (?) filename.
|
|
// Tests show that a simple, unique, increasing number works as well.
|
|
ext := GetImageTypeExt(embObj.ImageType);
|
|
embName := Format('%d.%s', [i+1, ext]);
|
|
embObj.Stream.Position := 0;
|
|
AZip.Entries.AddFileEntry(embObj.Stream, 'Pictures/' + embname);
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteSettings;
|
|
var
|
|
i: Integer;
|
|
showGrid, showHeaders: Boolean;
|
|
sheet: TsWorksheet;
|
|
actSheet: String;
|
|
zoomvalue: String;
|
|
begin
|
|
// Open/LibreOffice allow to change showGrid and showHeaders only globally.
|
|
// As a compromise, we check whether there is at least one page with these
|
|
// settings off. Then we assume it to be valid also for the other sheets.
|
|
showGrid := true;
|
|
showHeaders := true;
|
|
actSheet := 'Table1';
|
|
zoomValue := '100';
|
|
for i:=0 to Workbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := Workbook.GetWorksheetByIndex(i);
|
|
if sheet = Workbook.ActiveWorksheet then
|
|
actSheet := UTF8TextToXMLText(sheet.Name);
|
|
if not (soShowGridLines in sheet.Options) then showGrid := false;
|
|
if not (soShowHeaders in sheet.Options) then showHeaders := false;
|
|
end;
|
|
|
|
AppendToStream(FSSettings,
|
|
XML_HEADER);
|
|
AppendToStream(FSSettings,
|
|
'<office:document-settings xmlns:office="' + SCHEMAS_XMLNS_OFFICE +
|
|
'" xmlns:config="' + SCHEMAS_XMLNS_CONFIG +
|
|
'" xmlns:ooo="' + SCHEMAS_XMLNS_OOO + '">');
|
|
AppendToStream(FSSettings,
|
|
'<office:settings>' +
|
|
'<config:config-item-set config:name="ooo:view-settings">' +
|
|
'<config:config-item-map-indexed config:name="Views">' +
|
|
'<config:config-item-map-entry>' +
|
|
'<config:config-item config:name="ActiveTable" config:type="string">'+actSheet+'</config:config-item>' +
|
|
'<config:config-item config:name="ZoomValue" config:type="int">'+zoomValue+'</config:config-item>' +
|
|
'<config:config-item config:name="PageViewZoomValue" config:type="int">100</config:config-item>' +
|
|
'<config:config-item config:name="ShowPageBreakPreview" config:type="boolean">false</config:config-item>' +
|
|
'<config:config-item config:name="ShowGrid" config:type="boolean">'+FALSE_TRUE[showGrid]+'</config:config-item>' +
|
|
'<config:config-item config:name="HasColumnRowHeaders" config:type="boolean">'+FALSE_TRUE[showHeaders]+'</config:config-item>' +
|
|
'<config:config-item-map-named config:name="Tables">');
|
|
|
|
WriteTableSettings(FSSettings);
|
|
|
|
AppendToStream(FSSettings,
|
|
'</config:config-item-map-named>' +
|
|
'</config:config-item-map-entry>' +
|
|
'</config:config-item-map-indexed>' +
|
|
'</config:config-item-set>' +
|
|
'</office:settings>' +
|
|
'</office:document-settings>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteStyles;
|
|
begin
|
|
AppendToStream(FSStyles,
|
|
XML_HEADER);
|
|
|
|
AppendToStream(FSStyles,
|
|
'<office:document-styles xmlns:office="' + SCHEMAS_XMLNS_OFFICE +
|
|
'" xmlns:fo="' + SCHEMAS_XMLNS_FO +
|
|
'" xmlns:style="' + SCHEMAS_XMLNS_STYLE +
|
|
'" xmlns:svg="' + SCHEMAS_XMLNS_SVG +
|
|
'" xmlns:table="' + SCHEMAS_XMLNS_TABLE +
|
|
'" xmlns:text="' + SCHEMAS_XMLNS_TEXT +
|
|
'" xmlns:xlink="' + SCHEMAS_XMLNS_XLINK +
|
|
'" xmlns:draw="' + SCHEMAS_XMLNS_DRAW +
|
|
'" xmlns:v="' + SCHEMAS_XMLNS_V + '">');
|
|
|
|
AppendToStream(FSStyles,
|
|
'<office:font-face-decls>');
|
|
WriteFontNames(FSStyles);
|
|
AppendToStream(FSStyles,
|
|
'</office:font-face-decls>');
|
|
|
|
AppendToStream(FSStyles,
|
|
'<office:styles>');
|
|
AppendToStream(FSStyles,
|
|
'<style:style style:name="Default" style:family="table-cell">',
|
|
WriteDefaultFontXMLAsString,
|
|
'</style:style>');
|
|
if FWorkbook.HasEmbeddedSheetImages then
|
|
AppendToStream(FSStyles,
|
|
'<style:default-style style:family="graphic">',
|
|
WriteDefaultGraphicStyleXMLAsString,
|
|
'</style:default-style>');
|
|
AppendToStream(FSStyles,
|
|
'</office:styles>');
|
|
|
|
WriteAutomaticStyles(FSStyles);
|
|
WriteMasterStyles(FSStyles);
|
|
|
|
AppendToStream(FSStyles,
|
|
'</office:document-styles>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteContent;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
AppendToStream(FSContent,
|
|
XML_HEADER);
|
|
AppendToStream(FSContent,
|
|
'<office:document-content ' +
|
|
'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" '+
|
|
'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" '+
|
|
'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" '+
|
|
'xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" '+
|
|
'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" '+
|
|
'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" '+
|
|
'xmlns:xlink="http://www.w3.org/1999/xlink" '+
|
|
'xmlns:dc="http://purl.org/dc/elements/1.1/" '+
|
|
'xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" '+
|
|
'xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" '+
|
|
'xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" '+
|
|
'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" '+
|
|
'xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" '+
|
|
'xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" '+
|
|
'xmlns:math="http://www.w3.org/1998/Math/MathML" '+
|
|
'xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" '+
|
|
'xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" '+
|
|
'xmlns:ooo="http://openoffice.org/2004/office" '+
|
|
'xmlns:ooow="http://openoffice.org/2004/writer" '+
|
|
'xmlns:oooc="http://openoffice.org/2004/calc" '+
|
|
'xmlns:dom="http://www.w3.org/2001/xml-events" '+
|
|
'xmlns:xforms="http://www.w3.org/2002/xforms" '+
|
|
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '+
|
|
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '+
|
|
'xmlns:rpt="http://openoffice.org/2005/report" '+
|
|
'xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" '+
|
|
'xmlns:xhtml="http://www.w3.org/1999/xhtml" '+
|
|
'xmlns:grddl="http://www.w3.org/2003/g/data-view#" '+
|
|
'xmlns:tableooo="http://openoffice.org/2009/table" '+
|
|
'xmlns:drawooo="http://openoffice.org/2010/draw" '+
|
|
'xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" '+
|
|
'xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" '+
|
|
'xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" '+
|
|
'xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" '+
|
|
'xmlns:css3t="http://www.w3.org/TR/css3-text/" '+
|
|
'xmlns:rdfa="http://docs.oasis-open.org/opendocument/meta/rdfa#" '+
|
|
'office:version="1.2">' +
|
|
'<office:scripts />'
|
|
);
|
|
|
|
{
|
|
'<office:document-content xmlns:office="' + SCHEMAS_XMLNS_OFFICE +
|
|
'" xmlns:fo="' + SCHEMAS_XMLNS_FO +
|
|
'" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/' +
|
|
'" xmlns:style="' + SCHEMAS_XMLNS_STYLE +
|
|
'" xmlns:text="' + SCHEMAS_XMLNS_TEXT +
|
|
'" xmlns:table="' + SCHEMAS_XMLNS_TABLE +
|
|
'" xmlns:svg="' + SCHEMAS_XMLNS_SVG +
|
|
'" xmlns:number="' + SCHEMAS_XMLNS_NUMBER +
|
|
'" xmlns:meta="' + SCHEMAS_XMLNS_META +
|
|
'" xmlns:chart="' + SCHEMAS_XMLNS_CHART +
|
|
'" xmlns:dr3d="' + SCHEMAS_XMLNS_DR3D +
|
|
'" xmlns:math="' + SCHEMAS_XMLNS_MATH +
|
|
'" xmlns:form="' + SCHEMAS_XMLNS_FORM +
|
|
'" xmlns:script="' + SCHEMAS_XMLNS_SCRIPT +
|
|
'" xmlns:ooo="' + SCHEMAS_XMLNS_OOO +
|
|
'" xmlns:ooow="' + SCHEMAS_XMLNS_OOOW +
|
|
'" xmlns:oooc="' + SCHEMAS_XMLNS_OOOC +
|
|
'" xmlns:dom="' + SCHEMAS_XMLNS_DOM +
|
|
'" xmlns:xforms="' + SCHEMAS_XMLNS_XFORMS +
|
|
'" xmlns:xsd="' + SCHEMAS_XMLNS_XSD +
|
|
'" xmlns:xsi="' + SCHEMAS_XMLNS_XSI + '">' +
|
|
'<office:scripts />');
|
|
}
|
|
// Fonts
|
|
AppendToStream(FSContent,
|
|
'<office:font-face-decls>');
|
|
WriteFontNames(FSContent);
|
|
AppendToStream(FSContent,
|
|
'</office:font-face-decls>');
|
|
|
|
// Automatic styles
|
|
AppendToStream(FSContent,
|
|
'<office:automatic-styles>');
|
|
|
|
WriteNumFormats(FSContent); // "N1" ...
|
|
WriteColStyles(FSContent); // "co1" ...
|
|
WriteRowStyles(FSContent); // "ro1" ...
|
|
WriteTableStyles(FSContent); // "ta1" ...
|
|
WriteCellStyles(FSContent); // "ce1" ...
|
|
WriteTextStyles(FSContent); // "T1" ...
|
|
|
|
AppendToStream(FSContent,
|
|
'</office:automatic-styles>');
|
|
|
|
// Body
|
|
AppendToStream(FSContent,
|
|
'<office:body>' +
|
|
'<office:spreadsheet' + WriteDocumentProtectionXMLAsString + '>');
|
|
|
|
// Write all worksheets
|
|
for i := 0 to Workbook.GetWorksheetCount - 1 do
|
|
WriteWorksheet(FSContent, i);
|
|
|
|
AppendToStream(FSContent,
|
|
'</office:spreadsheet>' +
|
|
'</office:body>' +
|
|
'</office:document-content>'
|
|
);
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteWorksheet(AStream: TStream;
|
|
ASheetIndex: Integer);
|
|
begin
|
|
FWorksheet := FWorkbook.GetWorksheetByIndex(ASheetIndex);
|
|
|
|
// Buffer the information whether the worksheet contains column or row formats
|
|
// Needed for writing rows and cells
|
|
FHasColFormats := FWorksheet.HasColFormats;
|
|
FHasRowFormats := FWorksheet.HasRowFormats;
|
|
|
|
// Header
|
|
AppendToStream(AStream, Format(
|
|
'<table:table table:name="%s" table:style-name="ta%d"%s%s>%s', [
|
|
UTF8TextToXMLText(FWorkSheet.Name),
|
|
ASheetIndex+1,
|
|
WriteSheetProtectionXMLAsString(FWorksheet),
|
|
WritePrintRangesXMLAsString(FWorksheet),
|
|
WriteSheetProtectionDetailsXMLAsString(FWorksheet)
|
|
]));
|
|
|
|
// shapes
|
|
WriteShapes(AStream, FWorksheet);
|
|
|
|
// columns
|
|
WriteColumns(AStream, FWorkSheet);
|
|
|
|
// rows and cells
|
|
// The cells need to be written in order, row by row, cell by cell
|
|
if (boVirtualMode in Workbook.Options) then
|
|
WriteVirtualCells(AStream, FWorksheet)
|
|
else
|
|
WriteRowsAndCells(AStream, FWorksheet);
|
|
|
|
// named expressions, i.e. print range, repeated cols/rows
|
|
WriteNamedExpressions(AStream, FWorksheet);
|
|
|
|
// Footer
|
|
AppendToStream(AStream,
|
|
'</table:table>');
|
|
end;
|
|
|
|
{ Writes the cell styles ("ce0", "ce1", ...). Directly maps to the CellFormats
|
|
list of the workbook. "ce0" is the default format }
|
|
procedure TsSpreadOpenDocWriter.WriteCellStyles(AStream: TStream);
|
|
var
|
|
i, j, p: Integer;
|
|
s: String;
|
|
nfidx: Integer;
|
|
nfs: String;
|
|
fmt: TsCellFormat;
|
|
nfParams: TsNumFormatParams;
|
|
begin
|
|
for i := 0 to FWorkbook.GetNumCellFormats - 1 do
|
|
begin
|
|
fmt := FWorkbook.GetCellFormat(i);
|
|
nfs := '';
|
|
nfidx := fmt.NumberFormatIndex;
|
|
if nfidx <> -1 then
|
|
begin
|
|
nfParams := FWorkbook.GetNumberFormat(nfidx);
|
|
if nfParams <> nil then
|
|
begin
|
|
nfs := nfParams.NumFormatStr;
|
|
for j:=0 to NumFormatList.Count-1 do
|
|
begin
|
|
s := NumFormatList[j];
|
|
p := pos(':', s);
|
|
if SameText(Copy(s, p+1, Length(s)), nfs) then
|
|
begin
|
|
nfs := Format('style:data-style-name="%s"', [copy(s, 1, p-1)]);
|
|
break;
|
|
end;
|
|
p := 0;
|
|
end;
|
|
if p = 0 then // not found
|
|
nfs := '';
|
|
end;
|
|
end;
|
|
|
|
// Start and name
|
|
AppendToStream(AStream,
|
|
'<style:style style:name="ce' + IntToStr(i) + '" style:family="table-cell" ' +
|
|
'style:parent-style-name="Default" '+ nfs + '>');
|
|
|
|
// style:text-properties
|
|
// - font
|
|
s := WriteFontStyleXMLAsString(fmt);
|
|
if s <> '' then
|
|
AppendToStream(AStream,
|
|
'<style:text-properties '+ s + '/>');
|
|
|
|
// - border, background, wordwrap, text rotation, vertical alignment
|
|
s := WriteBorderStyleXMLAsString(fmt) +
|
|
WriteBackgroundColorStyleXMLAsString(fmt) +
|
|
WriteWordwrapStyleXMLAsString(fmt) +
|
|
WriteTextRotationStyleXMLAsString(fmt) +
|
|
WriteVertAlignmentStyleXMLAsString(fmt) +
|
|
WriteCellProtectionStyleXMLAsString(fmt);
|
|
if s <> '' then
|
|
AppendToStream(AStream,
|
|
'<style:table-cell-properties ' + s + '/>');
|
|
|
|
// style:paragraph-properties
|
|
// - hor alignment, bidi
|
|
s := WriteHorAlignmentStyleXMLAsString(fmt) +
|
|
WriteBiDiModeStyleXMLAsString(fmt);
|
|
if s <> '' then
|
|
AppendToStream(AStream,
|
|
'<style:paragraph-properties ' + s + '/>');
|
|
|
|
// End
|
|
AppendToStream(AStream,
|
|
'</style:style>');
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteColStyles(AStream: TStream);
|
|
var
|
|
i: Integer;
|
|
colstyle: TColumnStyleData;
|
|
begin
|
|
if FColumnStyleList.Count = 0 then
|
|
begin
|
|
AppendToStream(AStream,
|
|
'<style:style style:name="co1" style:family="table-column">',
|
|
'<style:table-column-properties fo:break-before="auto" style:column-width="2.267cm"/>',
|
|
'</style:style>');
|
|
exit;
|
|
end;
|
|
|
|
for i := 0 to FColumnStyleList.Count-1 do
|
|
begin
|
|
colStyle := TColumnStyleData(FColumnStyleList[i]);
|
|
|
|
// Start and Name
|
|
AppendToStream(AStream, Format(
|
|
'<style:style style:name="%s" style:family="table-column">', [colStyle.Name]));
|
|
|
|
// Column width
|
|
AppendToStream(AStream, Format(
|
|
'<style:table-column-properties style:column-width="%.3fmm" fo:break-before="auto"/>',
|
|
[FWorkbook.ConvertUnits(colStyle.ColWidth, FWorkbook.Units, suMillimeters)],
|
|
FPointSeparatorSettings));
|
|
|
|
// End
|
|
AppendToStream(AStream,
|
|
'</style:style>');
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteColumns(AStream: TStream;
|
|
ASheet: TsWorksheet);
|
|
var
|
|
lastCol: Integer;
|
|
c, k: Integer;
|
|
w: Double;
|
|
styleName: String;
|
|
colsRepeated: Integer;
|
|
colsRepeatedStr: String;
|
|
firstRepeatedPrintCol, lastRepeatedPrintCol: Longint;
|
|
headerCols: Boolean;
|
|
begin
|
|
// widthMultiplier := Workbook.GetFont(0).Size / 2;
|
|
lastCol := ASheet.GetLastColIndex;
|
|
firstRepeatedPrintCol := longInt(ASheet.PageLayout.RepeatedCols.FirstIndex);
|
|
lastRepeatedPrintCol := longint(ASheet.PageLayout.RepeatedCols.LastIndex);
|
|
if (firstRepeatedPrintCol <> Longint(UNASSIGNED_ROW_COL_INDEX)) and
|
|
(lastRepeatedPrintCol = LongInt(UNASSIGNED_ROW_COL_INDEX))
|
|
then
|
|
lastRepeatedPrintCol := firstRepeatedPrintCol;
|
|
|
|
headerCols := false;
|
|
c := 0;
|
|
while (c <= lastCol) do
|
|
begin
|
|
w := ASheet.GetColWidth(c, FWorkbook.Units);
|
|
|
|
if (c = firstRepeatedPrintCol) then
|
|
begin
|
|
headerCols := true;
|
|
AppendToStream(AStream, '<table:table-header-columns>');
|
|
end;
|
|
|
|
// Find width in ColumnStyleList to retrieve corresponding style name
|
|
styleName := '';
|
|
for k := 0 to FColumnStyleList.Count-1 do
|
|
if SameValue(TColumnStyleData(FColumnStyleList[k]).ColWidth, w, COLWIDTH_EPS) then begin
|
|
styleName := TColumnStyleData(FColumnStyleList[k]).Name;
|
|
break;
|
|
end;
|
|
if stylename = '' then
|
|
stylename := 'co1';
|
|
{
|
|
if stylename = '' then
|
|
raise Exception.Create(rsColumnStyleNotFound);
|
|
}
|
|
|
|
// Determine value for "number-columns-repeated"
|
|
colsRepeated := 1;
|
|
k := c+1;
|
|
if headerCols then
|
|
while (k <= lastCol) and (k <= lastRepeatedPrintCol) do
|
|
begin
|
|
if ASheet.GetColWidth(k, FWorkbook.Units) = w then
|
|
inc(colsRepeated)
|
|
else
|
|
break;
|
|
inc(k);
|
|
end
|
|
else
|
|
while (k <= lastCol) and (k < firstRepeatedPrintCol) do
|
|
begin
|
|
if ASheet.GetColWidth(k, FWorkbook.Units) = w then
|
|
inc(colsRepeated)
|
|
else
|
|
break;
|
|
inc(k);
|
|
end;
|
|
if FHasRowFormats and (k = lastcol) then
|
|
colsRepeated := FLimitations.MaxColCount - c;
|
|
|
|
colsRepeatedStr := IfThen(colsRepeated = 1, '', Format(' table:number-columns-repeated="%d"', [colsRepeated]));
|
|
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-column table:style-name="%s"%s table:default-cell-style-name="Default" />',
|
|
[styleName, colsRepeatedStr]));
|
|
|
|
if headerCols and (k >= lastRepeatedPrintCol) then
|
|
begin
|
|
AppendToStream(AStream, '</table:table-header-columns>');
|
|
headerCols := false;
|
|
end;
|
|
|
|
c := c + colsRepeated;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteCellProtectionStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
// style:cell-protect="protected formula-hidden"
|
|
begin
|
|
if AFormat.Protection * [cpLockCell, cpHideFormulas] = [] then
|
|
Result := 'none'
|
|
else if (AFormat.Protection * [cpLockCell, cpHideFormulas] = [cpLockCell]) then
|
|
Result := 'protected'
|
|
else if (AFormat.Protection *[cpLockCell, cpHideFormulas] = [cpHideFormulas]) then
|
|
Result := 'formula-hidden'
|
|
else
|
|
Result := 'protected formula-hidden'; // or: 'hidden-and-protected'
|
|
Result := ' style:cell-protect="' + Result + '"';
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteCommentXMLAsString(AComment: String): String;
|
|
var
|
|
L: TStringList;
|
|
s: String;
|
|
err: Boolean;
|
|
i: Integer;
|
|
begin
|
|
Result := '';
|
|
if AComment = '' then exit;
|
|
|
|
result := '<office:annotation office:display="false">';
|
|
err := false;
|
|
L := TStringList.Create;
|
|
try
|
|
L.Text := AComment;
|
|
for i:=0 to L.Count-1 do begin
|
|
s := L[i];
|
|
if not ValidXMLText(s) then begin
|
|
if not err then
|
|
Workbook.AddErrorMsg(rsInvalidCharacterInCellComment, [AComment]);
|
|
err := true;
|
|
end;
|
|
Result := Result + '<text:p>' + s + '</text:p>';
|
|
end;
|
|
finally
|
|
L.Free;
|
|
end;
|
|
|
|
Result := Result + '</office:annotation>';
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes the declaration of the font faces used in the workbook.
|
|
Is used in styles.xml and content.xml.
|
|
|
|
Procedure must be enclosed by
|
|
<office:font-face-decls> ... </office:font-face-decls>
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteFontNames(AStream: TStream);
|
|
var
|
|
L: TStringList;
|
|
fnt: TsFont;
|
|
hfFnt: TXMLHeaderFooterFont;
|
|
i: Integer;
|
|
begin
|
|
// Collect all unique font names in a string list
|
|
L := TStringList.Create;
|
|
try
|
|
// First collect the font names from the workbook's FontList
|
|
for i:=0 to Workbook.GetFontCount-1 do
|
|
begin
|
|
fnt := Workbook.GetFont(i);
|
|
if (fnt <> nil) and (L.IndexOf(fnt.FontName) = -1) then
|
|
L.Add(fnt.FontName);
|
|
end;
|
|
// Then collect the header/footer font names from the HeaderFooterFontList
|
|
for i:=0 to FHeaderFooterFontList.Count-1 do
|
|
begin
|
|
hfFnt := TXMLHeaderFooterFont(FHeaderFooterFontList[i]);
|
|
if (hfFnt <> nil) and (L.Indexof(hfFnt.FontName) = -1) then
|
|
L.Add(hfFnt.FontName);
|
|
end;
|
|
// Done. Now write all font names as xml nodes to the stream
|
|
for i:=0 to L.Count-1 do
|
|
AppendToStream(AStream, Format(
|
|
'<style:font-face style:name="%s" svg:font-family="%s" />', [L[i], L[i]]));
|
|
finally
|
|
L.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteMasterStyles(AStream: TStream);
|
|
var
|
|
defFnt: TsHeaderFooterFont;
|
|
i: Integer;
|
|
sheet: TsWorksheet;
|
|
|
|
function HeaderFooterAsString(AIndex: Integer; AIsHeader: Boolean;
|
|
const APageLayout: TsPageLayout): String;
|
|
var
|
|
parser: TsSpreadOpenDocHeaderFooterParser;
|
|
str: String;
|
|
begin
|
|
if AIsHeader then
|
|
str := APageLayout.Headers[AIndex] else
|
|
str := APageLayout.Footers[AIndex];
|
|
if str = '' then
|
|
exit;
|
|
parser := TsSpreadOpenDocHeaderFooterParser.Create(str, FHeaderFooterFontList,
|
|
defFnt);
|
|
try
|
|
Result := parser.BuildHeaderFooterAsXMLString;
|
|
finally
|
|
parser.Free;
|
|
end;
|
|
end;
|
|
|
|
function MasterPageAsString(AStyleName, ADisplayName, APageLayoutName: String;
|
|
const APageLayout: TsPageLayout): String;
|
|
const
|
|
IS_HEADER = true;
|
|
IS_FOOTER = false;
|
|
begin
|
|
Result := Format(
|
|
'<style:master-page style:name="%s" ' +
|
|
'style:display-name="%s" ' +
|
|
'style:page-layout-name="%s">', [
|
|
AStyleName, ADisplayName, APageLayoutName
|
|
]);
|
|
|
|
if (APageLayout.Headers[1] <> '') then
|
|
Result := Result +
|
|
'<style:header>' +
|
|
HeaderFooterAsString(1, IS_HEADER, APageLayout) +
|
|
'</style:header>'
|
|
else
|
|
Result := Result +
|
|
'<style:header style:display="false" />';
|
|
|
|
if (APageLayout.Footers[1] <> '') then
|
|
Result := Result +
|
|
'<style:footer>' +
|
|
HeaderFooterAsString(1, IS_FOOTER, APageLayout) +
|
|
'</style:footer>'
|
|
else
|
|
Result := Result +
|
|
'<style:footer style:display="false" />';
|
|
|
|
if poDifferentOddEven in APageLayout.Options then
|
|
begin
|
|
if (APageLayout.Headers[2] <> '') then
|
|
Result := Result +
|
|
'<style:header-left>' +
|
|
HeaderFooterAsString(2, IS_HEADER, APageLayout) +
|
|
'</style:header-left>'
|
|
else
|
|
Result := Result +
|
|
'<style:header-left style:display="false" />';
|
|
|
|
if (APageLayout.Footers[2] <> '') then
|
|
Result := Result +
|
|
'<style:footer-left>' +
|
|
HeaderFooterAsString(2, IS_FOOTER, APageLayout) +
|
|
'</style:footer-left>'
|
|
else
|
|
Result := Result +
|
|
'<style:footer-left display="false" />';
|
|
end;
|
|
Result := Result + '</style:master-page>';
|
|
end;
|
|
|
|
var
|
|
sheetname: String;
|
|
begin
|
|
defFnt := TsHeaderFooterFont.Create(Workbook.GetDefaultFont);
|
|
|
|
AppendToStream(AStream,
|
|
'<office:master-styles>');
|
|
|
|
AppendToStream(AStream,
|
|
'<style:master-page style:name="Default" style:page-layout-name="Mpm1">' +
|
|
'<style:header />' +
|
|
'<style:header-left style:display="false" />' +
|
|
'<style:footer />' +
|
|
'<style:footer-left style:display="false" />' +
|
|
'</style:master-page>');
|
|
|
|
for i:=0 to FWorkbook.GetWorksheetCount-1 do begin
|
|
sheet := FWorkbook.GetWorksheetByIndex(i);
|
|
sheetname := UTF8TextToXMLText(sheet.name);
|
|
AppendToStream(AStream,
|
|
MasterPageAsString('PageStyle_5f_' + sheetName, 'PageStyle_' + sheetname,
|
|
'Mpm' + IntToStr(3+i), sheet.PageLayout));
|
|
end;
|
|
|
|
AppendToStream(AStream,
|
|
'</office:master-styles>');
|
|
|
|
defFnt.Free;
|
|
end;
|
|
|
|
{<table:named-expressions>
|
|
<table:named-expression table:name="_xlnm.Print_Area" table:base-cell-address="$Sheet1.$A$1" table:expression="[$Sheet1.$A$2:.$F$6];[$Sheet1.$A$11:.$K$21]" />
|
|
<table:named-expression table:name="_xlnm.Print_Titles" table:base-cell-address="$Sheet1.$A$1" table:expression="[$Sheet1.$A$1:.$D$1048576];[$Sheet1.$A$1:.$AMJ$2]" />
|
|
</table:named-expressions>}
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteNamedExpressions(AStream: TStream;
|
|
ASheet: TsWorksheet);
|
|
var
|
|
stotal, srng, sheetname: String;
|
|
j: Integer;
|
|
prng: TsCellRange;
|
|
begin
|
|
sheetname := UTF8TextToXMLText(ASheet.Name);
|
|
stotal := '';
|
|
|
|
// Cell block of print range
|
|
srng := '';
|
|
for j := 0 to ASheet.PageLayout.NumPrintRanges - 1 do
|
|
begin
|
|
prng := ASheet.PageLayout.PrintRange[j];
|
|
srng := srng + ';' + Format('[$%s.%s]', [
|
|
sheetname, GetCellRangeString(prng.Row1, prng.Col1, prng.Row2, prng.Col2, [])
|
|
]);
|
|
end;
|
|
if srng <> '' then
|
|
begin
|
|
Delete(srng, 1, 1);
|
|
stotal := stotal + Format(
|
|
'<table:named-expression table:name="_xlnm.Print_Area" table:base-cell-address="$%s.$A$1" table:expression="%s" />',
|
|
[sheetname, srng]
|
|
);
|
|
end;
|
|
|
|
// Next commented part appears only in files converted from Excel
|
|
|
|
{
|
|
// repeated columns ...
|
|
srng := '';
|
|
if ASheet.PageLayout.RepeatedCols.FirstIndex <> UNASSIGNED_ROW_COL_INDEX then
|
|
begin
|
|
if ASheet.PageLayout.RepeatedCols.LastIndex = UNASSIGNED_ROW_COL_INDEX then
|
|
srng := srng + ';' + Format('[$%s.$%s]',
|
|
[ASheet.Name, GetColString(ASheet.pageLayout.RepeatedCols.FirstIndex)]
|
|
)
|
|
else
|
|
srng := srng + ';' + Format('[$%s.$%s1:.$%s1048576]', [ // [$Sheet1.$A$1:.$D$1048576]
|
|
ASheet.Name,
|
|
GetColString(ASheet.Pagelayout.RepeatedCols.FirstIndex),
|
|
GetColString(ASheet.PageLayout.RepeatedCols.LastIndex)
|
|
]);
|
|
end;
|
|
// ... and repeated rows
|
|
if ASheet.PageLayout.RepeatedRows.FirstIndex <> UNASSIGNED_ROW_COL_INDEX then
|
|
begin
|
|
if ASheet.PageLayout.RepeatedRows.LastIndex = UNASSIGNED_ROW_COL_INDEX then
|
|
srng := srng + ';' + Format('[$%s.$%d]',
|
|
[ASheet.Name, ASheet.pageLayout.RepeatedRows.FirstIndex]
|
|
)
|
|
else
|
|
srng := srng + ';' + Format('[$%s.$A$%d:.$AMJ$%d]', [ // [$Sheet1.$A$1:.$AMJ$2]"
|
|
ASheet.Name,
|
|
ASheet.Pagelayout.RepeatedRows.FirstIndex+1,
|
|
ASheet.PageLayout.RepeatedRows.LastIndex+1
|
|
]);
|
|
end;
|
|
if srng <> '' then begin
|
|
Delete(srng, 1,1);
|
|
stotal := stotal + Format(
|
|
'<table:named-expression table:name="_xlnm.Print_Titles" table:bases-cell-address="$%s.$A$1" table:expression="%s" />',
|
|
[ASheet.Name, srng]
|
|
);
|
|
end;
|
|
}
|
|
// Write to stream if any defined names exist
|
|
if stotal <> '' then
|
|
AppendtoStream(AStream,
|
|
'<table:named-expressions>' + stotal + '</table:named-expressions>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteNumFormats(AStream: TStream);
|
|
var
|
|
i, p: Integer;
|
|
numFmtXML: String;
|
|
numFmtStr: String;
|
|
numFmtName: String;
|
|
parser: TsSpreadOpenDocNumFormatParser;
|
|
begin
|
|
for i:=0 to NumFormatList.Count-1 do
|
|
begin
|
|
numFmtStr := NumFormatList[i];
|
|
p := pos(':', numFmtStr);
|
|
numFmtName := Copy(numFmtStr, 1, p-1);
|
|
numFmtStr := Copy(numFmtStr, p+1, Length(numFmtStr));
|
|
parser := TsSpreadOpenDocNumFormatParser.Create(numFmtStr, Workbook.FormatSettings);
|
|
try
|
|
numFmtXML := parser.BuildXMLAsString(numFmtName);
|
|
if numFmtXML <> '' then
|
|
AppendToStream(AStream, numFmtXML);
|
|
finally
|
|
parser.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteRowsAndCells(AStream: TStream; ASheet: TsWorksheet);
|
|
var
|
|
r: Integer;
|
|
rowsRepeated: Integer;
|
|
firstCol, firstRow, lastCol, lastRow: Cardinal;
|
|
firstRepeatedPrintRow, lastRepeatedPrintRow: Integer;
|
|
headerRows: Boolean;
|
|
begin
|
|
// some abbreviations...
|
|
GetSheetDimensions(ASheet, firstRow, lastRow, firstCol, lastCol);
|
|
|
|
headerRows := false;
|
|
firstRepeatedPrintRow := Integer(ASheet.PageLayout.RepeatedRows.FirstIndex);
|
|
lastRepeatedPrintRow := Integer(ASheet.PageLayout.RepeatedRows.LastIndex);
|
|
if (firstRepeatedPrintRow <> Integer(UNASSIGNED_ROW_COL_INDEX)) and
|
|
(lastRepeatedPrintRow = Integer(UNASSIGNED_ROW_COL_INDEX))
|
|
then
|
|
lastRepeatedPrintRow := firstRepeatedPrintRow;
|
|
|
|
r := 0;
|
|
while r <= Integer(lastRow) do
|
|
begin
|
|
if (r = firstRepeatedPrintRow) then begin
|
|
AppendToStream(AStream, '<table:table-header-rows>');
|
|
headerRows := true;
|
|
end;
|
|
|
|
// Write rows
|
|
if ASheet.IsEmptyRow(r) then
|
|
WriteEmptyRow(AStream, ASheet, r, firstCol, lastCol, lastRow, rowsRepeated)
|
|
else begin
|
|
WriteCellRow(AStream, ASheet, r, lastCol);
|
|
rowsRepeated := 1;
|
|
end;
|
|
r := r + rowsRepeated;
|
|
|
|
// Header rows need a special tag
|
|
if headerRows and (r > lastRepeatedPrintRow) then
|
|
begin
|
|
AppendToStream(AStream, '</table:table-header-rows>');
|
|
headerRows := false;
|
|
end;
|
|
end;
|
|
|
|
// Finally, if the sheet contains column formats an empty row has to be
|
|
// added which is repeated up to the max worksheet size.
|
|
if FHasColFormats then
|
|
WriteEmptyRow(AStream, ASheet, r, firstCol, lastCol, -1, rowsRepeated);
|
|
end;
|
|
|
|
(*
|
|
// Now loop through all rows
|
|
r := firstRow;
|
|
while (r <= lastRow) do
|
|
begin
|
|
rowsRepeated := 1;
|
|
|
|
// Header rows need a special tag
|
|
if (r = firstRepeatedPrintRow) then
|
|
begin
|
|
AppendToStream(AStream, '<table:table-header-rows>');
|
|
headerRows := true;
|
|
end;
|
|
|
|
// Look for the row style of the current row (r): row style contains only
|
|
// row height, no row format!
|
|
row := ASheet.FindRow(r);
|
|
styleName := '';
|
|
if row <> nil then
|
|
begin
|
|
h := row^.Height; // row height in workbook units
|
|
for k := 0 to FRowStyleList.Count-1 do begin
|
|
rowStyleData := TRowStyleData(FRowStyleList[k]);
|
|
// Compare row heights, but be aware of rounding errors
|
|
if SameValue(rowStyleData.RowHeight, h, ROWHEIGHT_EPS) and
|
|
(rowstyleData.RowHeightType = row^.RowHeightType) and
|
|
(rowstyleData.RowHeightType <> rhtDefault)
|
|
then begin
|
|
styleName := rowStyleData.Name;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
if styleName = '' then begin
|
|
styleName := 'ro1'; // "ro1" is default row record - see ListAllRowStyles
|
|
h := ASheet.ReadDefaultRowHeight(FWorkbook.Units);
|
|
end;
|
|
|
|
// Take care of empty rows above the first row with cells
|
|
if (r = firstRow) and emptyRowsAbove then
|
|
begin
|
|
rowsRepeated := r;
|
|
rowsRepeatedStr := IfThen(rowsRepeated = 1, '',
|
|
Format('table:number-rows-repeated="%d"', [rowsRepeated]));
|
|
if FHasRowFormats then
|
|
colsRepeated := FLimitations.MaxColCount else
|
|
colsRepeated := lastCol + 1;
|
|
colsRepeatedStr := IfThen(colsRepeated = 1, '',
|
|
Format('table:number-columns-repeated="%d"', [colsRepeated]));
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s" %s>' +
|
|
'<table:table-cell %s/>' +
|
|
'</table:table-row>',
|
|
[styleName, rowsRepeatedStr, colsRepeatedStr]));
|
|
rowsRepeated := 1;
|
|
end
|
|
else
|
|
// Look for empty rows with the same style, they need the "number-rows-repeated" element.
|
|
if (ASheet.Cells.GetFirstCellOfRow(r) = nil) then
|
|
begin
|
|
rr := r + 1;
|
|
while (rr <= lastRow) do
|
|
begin
|
|
if ASheet.Cells.GetFirstCellOfRow(rr) <> nil then
|
|
break;
|
|
h1 := ASheet.GetRowHeight(rr, FWorkbook.Units);
|
|
if not SameValue(h, h1, ROWHEIGHT_EPS) then
|
|
break;
|
|
inc(rr);
|
|
end;
|
|
rowsRepeated := rr - r;
|
|
rowsRepeatedStr := IfThen(rowsRepeated = 1, '',
|
|
Format('table:number-rows-repeated="%d"', [rowsRepeated]));
|
|
if FHasRowFormats then
|
|
colsRepeated := FLimitations.MaxColCount else
|
|
colsRepeated := lastCol - firstCol + 1;
|
|
colsRepeatedStr := IfThen(colsRepeated = 1, '',
|
|
Format('table:number-columns-repeated="%d"', [colsRepeated]));
|
|
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s" %s>' +
|
|
'<table:table-cell %s/>' +
|
|
'</table:table-row>',
|
|
[styleName, rowsRepeatedStr, colsRepeatedStr]));
|
|
|
|
r := rr;
|
|
|
|
// Header rows need a special tag
|
|
if headerRows and (r >= lastRepeatedPrintRow) then
|
|
begin
|
|
AppendToStream(AStream, '</table:table-header-rows>');
|
|
headerRows := false;
|
|
end;
|
|
|
|
continue;
|
|
end;
|
|
|
|
// Now we know that there are cells.
|
|
// Write the row XML
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s">', [styleName]));
|
|
|
|
// Loop along the row and find the cells.
|
|
c := 0;
|
|
while c <= lastCol do
|
|
begin
|
|
// Get the cell from the sheet
|
|
cell := ASheet.FindCell(r, c);
|
|
|
|
// Belongs to merged block?
|
|
if (cell <> nil) and not FWorksheet.IsMergeBase(cell) and FWorksheet.IsMerged(cell) then
|
|
// this means: all cells of a merged block except for the merge base
|
|
begin
|
|
AppendToStream(AStream,
|
|
'<table:covered-table-cell />');
|
|
inc(c);
|
|
continue;
|
|
end;
|
|
|
|
colsRepeated := 1;
|
|
if cell <> nil then
|
|
WriteCellToStream(AStream, cell)
|
|
else
|
|
begin
|
|
row := ASheet.FindRow(r);
|
|
col := ASheet.FindCol(c);
|
|
// Empty cell with column format
|
|
if (col <> nil) and (col^.FormatIndex > 0) and
|
|
((row = nil) or (row^.FormatIndex = 0))
|
|
then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" />',
|
|
[col^.FormatIndex]))
|
|
else
|
|
begin
|
|
// Empty cell? Need to count how often to add "table:number-columns-repeated"
|
|
cc := c + 1;
|
|
while (cc <= lastCol) do
|
|
begin
|
|
col := nil;
|
|
cell := ASheet.FindCell(r, cc);
|
|
if cell <> nil then
|
|
break;
|
|
if (row = nil) or (row^.FormatIndex = 0) then
|
|
begin
|
|
col := ASheet.FindCol(cc);
|
|
if (col <> nil) and (col^.FormatIndex > 0) then
|
|
break;
|
|
end;
|
|
inc(cc)
|
|
end;
|
|
if FHasRowFormats and (cc > lastcol) then
|
|
colsRepeated := FLimitations.MaxColCount - c
|
|
else
|
|
colsRepeated := cc - c;
|
|
colsRepeatedStr := IfThen(colsRepeated = 1, '',
|
|
Format(' table:number-columns-repeated="%d"', [colsRepeated]));
|
|
row := ASheet.FindRow(r);
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
stylename := Format(' table:style-name="ce%d"', [row^.FormatIndex]) else
|
|
stylename := '';
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell%s%s />', [colsRepeatedStr, stylename]));
|
|
if (col <> nil) then //and ((row = nil) or (row^.FormatIndex = 0)) then
|
|
begin
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" />', [col^.FormatIndex]));
|
|
end;
|
|
if (col <> nil) and (cc = lastcol) then
|
|
break;
|
|
end;
|
|
end;
|
|
inc(c, colsRepeated);
|
|
end;
|
|
|
|
AppendToStream(AStream,
|
|
'</table:table-row>');
|
|
|
|
// Header rows need a special tag
|
|
if headerRows and (r = lastRepeatedPrintRow) then
|
|
begin
|
|
AppendToStream(AStream, '</table:table-header-rows>');
|
|
headerRows := false;
|
|
end;
|
|
|
|
// Next row
|
|
inc(r, rowsRepeated);
|
|
end;
|
|
|
|
// Finally, if the sheet contains column formats an empty row has to be
|
|
// added which is repeated up to the max worksheet size.
|
|
if FHasColFormats then begin
|
|
k := 0;
|
|
c := 0;
|
|
cellStr := '';
|
|
while k < ASheet.Cols.Count do begin
|
|
col := PCol(ASheet.Cols[k]);
|
|
if col^.FormatIndex > 0 then
|
|
begin
|
|
colsRepeated := col^.Col - c;
|
|
if colsRepeated > 0 then begin
|
|
cellStr := cellStr + Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />',
|
|
[colsRepeated]);
|
|
end;
|
|
cellStr := cellStr + Format(
|
|
'<table:table-cell table:style-name="ce%d" />',
|
|
[col^.FormatIndex]);
|
|
c := col^.Col + 1;
|
|
end;
|
|
inc(k);
|
|
end;
|
|
|
|
colsRepeated := IfThen(FHasRowFormats, FLimitations.MaxColCount, lastcol) - c;
|
|
if colsRepeated > 0 then
|
|
cellStr := cellStr + Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />',
|
|
[colsRepeated]);
|
|
|
|
rowsRepeated := FLimitations.MaxRowCount - r;
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="ro1" table:number-rows-repeated="%d">' +
|
|
'%s' +
|
|
'</table:table-row>', [
|
|
rowsRepeated,
|
|
cellStr
|
|
]));
|
|
end;
|
|
end;
|
|
*)
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteCellRow(AStream: TStream;
|
|
ASheet: TsWorksheet; ARowIndex, ALastColIndex: Integer);
|
|
var
|
|
row: PRow;
|
|
col: PCol;
|
|
cell: PCell;
|
|
stylename: string;
|
|
h: Single;
|
|
firstcol: Integer;
|
|
lastcol: Integer;
|
|
c, cc: integer;
|
|
colsRepeated: Integer;
|
|
fmtIndex: integer;
|
|
begin
|
|
// Get row
|
|
row := ASheet.FindRow(ARowIndex);
|
|
|
|
// Get style and height of row
|
|
GetRowStyleAndHeight(ASheet, ARowIndex, stylename, h);
|
|
|
|
// Write opening row tag. We don't support repeatedRows here.
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s">', [stylename]));
|
|
|
|
// Find first cell or column in this row
|
|
cell := ASheet.Cells.GetFirstCellOfRow(ARowIndex); // first cell
|
|
col := ASheet.FindFirstCol; // left-most column
|
|
if col <> nil then
|
|
firstcol := Min(col^.Col, cell^.Col) else
|
|
firstcol := cell^.Col;
|
|
|
|
// Find last cell or column in this row
|
|
cell := ASheet.Cells.GetlastCellOfRow(ARowIndex);
|
|
if ASheet.Cols.Count = 0 then
|
|
lastCol := cell^.Col
|
|
else begin
|
|
col := ASheet.Cols[ASheet.Cols.Count-1];
|
|
if col <> nil then
|
|
lastcol := Max(col^.Col, cell^.Col) else
|
|
lastCol := cell^.Col;
|
|
end;
|
|
|
|
// Cells left to the first col are "empty" with default format
|
|
if firstcol > 0 then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />', [firstcol]));
|
|
|
|
// Iterate between first and last column
|
|
c := firstcol;
|
|
while (c <= lastcol) do
|
|
begin
|
|
cell := ASheet.FindCell(ARowIndex, c);
|
|
if cell <> nil then
|
|
begin
|
|
// Belongs to merged block?
|
|
if not FWorksheet.IsMergeBase(cell) and FWorksheet.IsMerged(cell) then
|
|
// this means: all cells of a merged block except for the merge base
|
|
begin
|
|
AppendToStream(AStream,
|
|
'<table:covered-table-cell />');
|
|
inc(c);
|
|
continue;
|
|
end;
|
|
// Ordinary cell
|
|
WriteCellToStream(AStream, cell);
|
|
inc(c);
|
|
Continue;
|
|
end;
|
|
|
|
// Column format
|
|
col := ASheet.FindCol(c);
|
|
if (col <> nil) and (col^.FormatIndex > 0) then
|
|
begin
|
|
// row format has priority...
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
fmtIndex := row^.FormatIndex else
|
|
fmtIndex := col^.FormatIndex;
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" />', [fmtIndex]));
|
|
inc(c);
|
|
Continue;
|
|
end;
|
|
|
|
// Empty cell
|
|
cc := c + 1;
|
|
while (cc <= lastcol) do begin
|
|
cell := ASheet.FindCell(ARowIndex, cc);
|
|
if cell <> nil then
|
|
break;
|
|
col := ASheet.FindCol(cc);
|
|
if (col <> nil) and (col^.FormatIndex > 0) then
|
|
break;
|
|
inc(cc);
|
|
end;
|
|
colsRepeated := cc - c;
|
|
// Empty cell with row format?
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" table:number-columns-repeated="%d" />',
|
|
[row^.FormatIndex, colsRepeated]))
|
|
else
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />',
|
|
[colsRepeated]));
|
|
inc(c, colsRepeated);
|
|
end;
|
|
|
|
// Fill empty cells at right, in case of RowFormats up to limit of format.
|
|
if FHasRowFormats then
|
|
colsRepeated := FLimitations.MaxColCount - c
|
|
else if c <= ALastColIndex then
|
|
colsRepeated := ALastColIndex - c
|
|
else
|
|
colsRepeated := 0;
|
|
if colsRepeated > 0 then
|
|
begin
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" table:number-columns-repeated="%d" />',
|
|
[row^.FormatIndex, colsRepeated]))
|
|
else
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />',
|
|
[colsRepeated]));
|
|
end;
|
|
|
|
// Write closing row tag.
|
|
AppendToStream(AStream,
|
|
'</table:table-row>');
|
|
end;
|
|
|
|
{ Writes a complete row node for the specified row of the worksheet. Correctly
|
|
handles row and column formats.
|
|
If ALastRowIndex = -1 then the filler rows below the used sheet are written }
|
|
procedure TsSpreadOpenDocWriter.WriteEmptyRow(AStream: TStream;
|
|
ASheet: TsWorksheet; ARowIndex, AFirstColIndex, ALastColIndex, ALastRowIndex: Integer;
|
|
out ARowsRepeated: Integer);
|
|
var
|
|
row: PRow;
|
|
col: PCol;
|
|
c, cc, r: Integer;
|
|
colsRepeated: Integer;
|
|
stylename: String;
|
|
h, h1: Single;
|
|
fmtIndex: Integer;
|
|
begin
|
|
// Get style and height of row
|
|
GetRowStyleAndHeight(ASheet, ARowIndex, stylename, h);
|
|
|
|
// Determine how often this row is repeated
|
|
row := ASheet.FindRow(ARowIndex);
|
|
// Rows with format are not repeated - too complicated...
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
ARowsRepeated := 1
|
|
else
|
|
// Count how many rows are empty and have the same height
|
|
if ALastRowIndex > -1 then begin
|
|
r := ARowIndex + 1;
|
|
while r <= ALastRowIndex do
|
|
begin
|
|
if not ASheet.IsEmptyRow(r) then
|
|
break;
|
|
row := ASheet.FindRow(r);
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
break;
|
|
h1 := ASheet.GetRowHeight(r, FWorkbook.Units);
|
|
if not SameValue(h, h1, ROWHEIGHT_EPS) then
|
|
break;
|
|
inc(r);
|
|
end;
|
|
ARowsRepeated := r - ARowIndex;
|
|
end else
|
|
ARowsRepeated := FLimitations.MaxRowCount - ARowIndex;
|
|
|
|
// Write opening row tag
|
|
if ARowsRepeated > 1 then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s" table:number-rows-repeated="%d">',
|
|
[stylename, ARowsRepeated]))
|
|
else
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s">',
|
|
[styleName]));
|
|
|
|
// Empty cells left of the first column
|
|
colsRepeated := AFirstColIndex;
|
|
if colsRepeated > 0 then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />', [colsRepeated]));
|
|
|
|
// Cells between first and last columns
|
|
r := ARowIndex;
|
|
c := AFirstColIndex;
|
|
|
|
row := ASheet.FindRow(r);
|
|
while (c <= ALastColIndex) do
|
|
begin
|
|
// Empty cell in a column with a column format
|
|
col := ASheet.FindCol(c);
|
|
if (col <> nil) and (col^.FormatIndex > 0) then
|
|
begin
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
fmtIndex := row^.FormatIndex
|
|
else
|
|
fmtIndex := col^.FormatIndex;
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" />', [fmtIndex]));
|
|
inc(c);
|
|
Continue;
|
|
end;
|
|
|
|
// Empty cell? Need to count how often to add "table:number-columns-repeated"
|
|
cc := c + 1;
|
|
while (cc <= ALastColIndex) do
|
|
begin
|
|
col := ASheet.FindCol(cc);
|
|
if (col <> nil) and (col^.FormatIndex > 0) then
|
|
break;
|
|
inc(cc);
|
|
end;
|
|
|
|
if (c = ALastColIndex) and FHasRowFormats then
|
|
colsRepeated := FLimitations.MaxColCount - c else
|
|
colsRepeated := cc - c;
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" table:number-columns-repeated="%d" />',
|
|
[row^.FormatIndex, colsRepeated]))
|
|
else
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />',
|
|
[colsRepeated]));
|
|
c := cc
|
|
end;
|
|
|
|
// in case of row formats: extend up to the max column limit of the format
|
|
if FHasRowFormats then begin
|
|
colsRepeated := FLimitations.MaxColCount - ALastColIndex;
|
|
if (row <> nil) and (row^.FormatIndex > 0) then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d" table:number-columns-repeated="%d" />',
|
|
[row^.FormatIndex, colsRepeated]))
|
|
else
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:number-columns-repeated="%d" />',
|
|
[colsRepeated]));
|
|
end;
|
|
|
|
// Write out closing tag for this row
|
|
AppendToStream(AStream,
|
|
'</table:table-row>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.GetRowStyleAndHeight(ASheet: TsWorksheet;
|
|
ARowIndex: Integer; out AStyleName: String; out AHeight: Single);
|
|
var
|
|
row: PRow;
|
|
rowStyleData: TRowStyleData;
|
|
k: Integer;
|
|
begin
|
|
AStyleName := '';
|
|
row := ASheet.FindRow(ARowIndex);
|
|
if row <> nil then
|
|
begin
|
|
AHeight := row^.Height; // row height in workbook units
|
|
for k := 0 to FRowStyleList.Count-1 do begin
|
|
rowStyleData := TRowStyleData(FRowStyleList[k]);
|
|
// Compare row heights, but be aware of rounding errors
|
|
if SameValue(rowStyleData.RowHeight, AHeight, ROWHEIGHT_EPS) and
|
|
(rowstyleData.RowHeightType = row^.RowHeightType) and
|
|
(rowstyleData.RowHeightType <> rhtDefault)
|
|
then begin
|
|
AStyleName := rowStyleData.Name;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
if AStyleName = '' then begin
|
|
AStyleName := 'ro1'; // "ro1" is default row record - see ListAllRowStyles
|
|
AHeight := ASheet.ReadDefaultRowHeight(FWorkbook.Units);
|
|
end;
|
|
end;
|
|
|
|
{ Write the style nodes for rows ("ro1", "ro2", ...); they contain only
|
|
row height information. "ro1" is the default row height }
|
|
procedure TsSpreadOpenDocWriter.WriteRowStyles(AStream: TStream);
|
|
var
|
|
i: Integer;
|
|
rowstyle: TRowStyleData;
|
|
begin
|
|
if FRowStyleList.Count = 0 then
|
|
begin
|
|
AppendToStream(AStream, Format(
|
|
'<style:style style:name="ro1" style:family="table-row">' +
|
|
'<style:table-row-properties style:row-height="%.3fmm" ' +
|
|
'fo:break-before="auto" style:use-optimal-row-height="true"/>' +
|
|
'</style:style>',
|
|
[FWorksheet.ReadDefaultRowHeight(suMillimeters)]
|
|
));
|
|
exit;
|
|
end;
|
|
|
|
for i := 0 to FRowStyleList.Count-1 do
|
|
begin
|
|
rowStyle := TRowStyleData(FRowStyleList[i]);
|
|
|
|
// Start and Name
|
|
AppendToStream(AStream, Format(
|
|
'<style:style style:name="%s" style:family="table-row">', [rowStyle.Name]));
|
|
|
|
// Row height
|
|
AppendToStream(AStream, Format(
|
|
'<style:table-row-properties style:row-height="%.3fmm" ',
|
|
[FWorkbook.ConvertUnits(rowStyle.RowHeight, FWorkbook.Units, suMillimeters)],
|
|
FPointSeparatorSettings));
|
|
AppendToStream(AStream, Format(
|
|
'style:use-optimal-row-height="%s" ', [FALSE_TRUE[rowstyle.RowHeightType <> rhtCustom]]));
|
|
AppendToStream(AStream,
|
|
'fo:break-before="auto"/>');
|
|
|
|
// End
|
|
AppendToStream(AStream,
|
|
'</style:style>');
|
|
end;
|
|
end;
|
|
|
|
|
|
constructor TsSpreadOpenDocWriter.Create(AWorkbook: TsWorkbook);
|
|
begin
|
|
inherited Create(AWorkbook);
|
|
|
|
FColumnStyleList := TFPList.Create;
|
|
FRowStyleList := TFPList.Create;
|
|
FRichTextFontList := TStringList.Create;
|
|
FHeaderFooterFontList := TObjectList.Create;
|
|
|
|
FPointSeparatorSettings := SysUtils.DefaultFormatSettings;
|
|
FPointSeparatorSettings.DecimalSeparator:='.';
|
|
FPointSeparatorSettings.ListSeparator := ';'; // for formulas
|
|
|
|
// http://en.wikipedia.org/wiki/List_of_spreadsheet_software#Specifications
|
|
FLimitations.MaxColCount := 1024;
|
|
FLimitations.MaxRowCount := 1048576;
|
|
end;
|
|
|
|
destructor TsSpreadOpenDocWriter.Destroy;
|
|
var
|
|
j: Integer;
|
|
begin
|
|
for j:=FColumnStyleList.Count-1 downto 0 do TObject(FColumnStyleList[j]).Free;
|
|
FColumnStyleList.Free;
|
|
|
|
for j:=FRowStyleList.Count-1 downto 0 do TObject(FRowStyleList[j]).Free;
|
|
FRowStyleList.Free;
|
|
|
|
FRichTextFontList.Free; // Do not destroy fonts, they are owned by Workbook
|
|
FHeaderFooterFontList.Free;
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes a string to a file. Helper convenience method.
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteStringToFile(AString, AFileName: string);
|
|
var
|
|
TheStream : TFileStream;
|
|
S : String;
|
|
begin
|
|
TheStream := TFileStream.Create(AFileName, fmCreate);
|
|
S := AString;
|
|
TheStream.WriteBuffer(Pointer(S)^, Length(S));
|
|
TheStream.Free;
|
|
end;
|
|
(*
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes an OOXML document to a file.
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteToFile(const AFileName: string;
|
|
const AOverwriteExisting: Boolean);
|
|
var
|
|
lStream: TStream;
|
|
lMode: word;
|
|
begin
|
|
if AOverwriteExisting
|
|
then lMode := fmCreate or fmOpenWrite
|
|
else lMode := fmCreate;
|
|
|
|
if (boBufStream in Workbook.Options) then
|
|
lStream := TBufStream.Create(AFileName, lMode)
|
|
else
|
|
lStream := TFileStream.Create(AFileName, lMode);
|
|
|
|
try
|
|
WriteToStream(lStream);
|
|
finally
|
|
FreeAndNil(lStream);
|
|
end;
|
|
end; *)
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteToStream(AStream: TStream;
|
|
AParams: TsStreamParams = []);
|
|
begin
|
|
Unused(AParams);
|
|
InternalWriteToStream(AStream);
|
|
end;
|
|
|
|
{ Writes an empty cell to the stream }
|
|
procedure TsSpreadOpenDocWriter.WriteBlank(AStream: TStream;
|
|
const ARow, ACol: Cardinal; ACell: PCell);
|
|
var
|
|
colsSpannedStr: String;
|
|
rowsSpannedStr: String;
|
|
spannedStr: String;
|
|
comment: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
fmt: TsCellFormat;
|
|
begin
|
|
Unused(ARow, ACol);
|
|
|
|
// Hyperlink
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]);
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
rowsSpannedStr := Format(' table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
colsSpannedStr := Format(' table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
spannedStr := colsSpannedStr + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex);
|
|
if (fmt.UsedFormattingFields <> []) then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:style-name="ce%d"%s>', [ACell^.FormatIndex, spannedStr]),
|
|
comment,
|
|
'</table:table-cell>')
|
|
else
|
|
if comment <> '' then
|
|
AppendToStream(AStream,
|
|
'<table:table-cell' + spannedStr + '>' + comment + '</table:table-cell>')
|
|
else
|
|
AppendToStream(AStream,
|
|
'<table:table-cell' + spannedStr + '/>');
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes a boolean cell to the stream
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteBool(AStream: TStream;
|
|
const ARow, ACol: Cardinal; const AValue: Boolean; ACell: PCell);
|
|
var
|
|
lStyle, valType: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
rowsSpannedStr, colsSpannedStr, spannedStr: String;
|
|
comment: String;
|
|
strValue: String;
|
|
displayStr: String;
|
|
fmt: TsCellFormat;
|
|
begin
|
|
Unused(ARow, ACol);
|
|
|
|
valType := 'boolean';
|
|
|
|
fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex);
|
|
if fmt.UsedFormattingFields <> [] then
|
|
lStyle := ' table:style-name="ce' + IntToStr(ACell^.FormatIndex) + '" '
|
|
else
|
|
lStyle := '';
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
rowsSpannedStr := Format('table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
colsSpannedStr := Format('table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
spannedStr := colsSpannedStr + ' ' + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
// Displayed value
|
|
if AValue then
|
|
begin
|
|
StrValue := 'true';
|
|
DisplayStr := STR_TRUE;
|
|
end else
|
|
begin
|
|
strValue := 'false';
|
|
DisplayStr := STR_FALSE;
|
|
end;
|
|
|
|
// Hyperlink
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]);
|
|
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell office:value-type="%s" office:boolean-value="%s" %s %s >' +
|
|
comment +
|
|
'<text:p>%s</text:p>' +
|
|
'</table:table-cell>', [
|
|
valType, StrValue, lStyle, spannedStr,
|
|
DisplayStr
|
|
]));
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Creates an XML string for inclusion of the background color into the
|
|
written file from the backgroundcolor setting in the given format record.
|
|
Is called from WriteStyles (via WriteStylesXMLAsString).
|
|
|
|
NOTE: ODS does not support fill patterns. Fill patterns are converted to
|
|
solid fills by mixing pattern and background colors in the ratio defined
|
|
by the fill pattern. Result agrees with that what LO/OO show for an imported
|
|
xls file.
|
|
-------------------------------------------------------------------------------}
|
|
function TsSpreadOpenDocWriter.WriteBackgroundColorStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
const // fraction of pattern color in fill pattern
|
|
FRACTION: array[TsFillStyle] of Double = (
|
|
0.0, 1.0, 0.75, 0.50, 0.25, 0.125, 0.0625, // fsNoFill..fsGray6
|
|
0.5, 0.5, 0.5, 0.5, // fsStripeHor..fsStripeDiagDown
|
|
0.25, 0.25, 0.25, 0.25, // fsThinStripeHor..fsThinStripeDiagDown
|
|
0.5, 6.0/16, 0.75, 7.0/16); // fsHatchDiag..fsThinHatchHor
|
|
var
|
|
fc,bc: TsColor;
|
|
mix: TRgba;
|
|
fraction_fc, fraction_bc: Double;
|
|
begin
|
|
Result := '';
|
|
|
|
if not (uffBackground in AFormat.UsedFormattingFields) then
|
|
exit;
|
|
|
|
// Foreground and background colors
|
|
fc := AFormat.Background.FgColor;
|
|
if Aformat.Background.BgColor = scTransparent then
|
|
bc := scWhite
|
|
else
|
|
bc := AFormat.Background.BgColor;
|
|
|
|
// Mixing fraction
|
|
fraction_fc := FRACTION[AFormat.Background.Style];
|
|
fraction_bc := 1.0 - fraction_fc;
|
|
|
|
// Mixed color
|
|
mix.r := Min(round(fraction_fc*TRgba(fc).r + fraction_bc*TRgba(bc).r), 255);
|
|
mix.g := Min(round(fraction_fc*TRgba(fc).g + fraction_bc*TRgba(bc).g), 255);
|
|
mix.b := Min(round(fraction_fc*TRgba(fc).b + fraction_bc*TRgba(bc).b), 255);
|
|
|
|
Result := Format('fo:background-color="%s" ', [ColorToHTMLColorStr(TsColor(mix))]);
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteBiDiModeStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
Result := '';
|
|
if not (uffBiDi in AFormat.UsedFormattingFields) then
|
|
exit;
|
|
case AFormat.BiDiMode of
|
|
bdLTR : Result := 'style:writing-mode="lr-tb" ';
|
|
bdRTL : Result := 'style:writing-mode="rl-tb" ';
|
|
end;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Creates an XML string for inclusion of borders and border styles into the
|
|
written file from the border settings in the given format record.
|
|
Is called from WriteStyles (via WriteStylesXMLAsString).
|
|
-------------------------------------------------------------------------------}
|
|
function TsSpreadOpenDocWriter.WriteBorderStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
Result := '';
|
|
|
|
if not (uffBorder in AFormat.UsedFormattingFields) then
|
|
exit;
|
|
|
|
if cbSouth in AFormat.Border then
|
|
begin
|
|
Result := Result + Format('fo:border-bottom="%s %s %s" ', [
|
|
BORDER_LINEWIDTHS[AFormat.BorderStyles[cbSouth].LineStyle],
|
|
BORDER_LINESTYLES[AFormat.BorderStyles[cbSouth].LineStyle],
|
|
ColorToHTMLColorStr(AFormat.BorderStyles[cbSouth].Color)
|
|
]);
|
|
if AFormat.BorderStyles[cbSouth].LineStyle = lsDouble then
|
|
Result := Result + 'style:border-linewidth-bottom="0.002cm 0.035cm 0.002cm" ';
|
|
end
|
|
else
|
|
Result := Result + 'fo:border-bottom="none" ';
|
|
|
|
if cbWest in AFormat.Border then
|
|
begin
|
|
Result := Result + Format('fo:border-left="%s %s %s" ', [
|
|
BORDER_LINEWIDTHS[AFormat.BorderStyles[cbWest].LineStyle],
|
|
BORDER_LINESTYLES[AFormat.BorderStyles[cbWest].LineStyle],
|
|
ColorToHTMLColorStr(AFormat.BorderStyles[cbWest].Color)
|
|
]);
|
|
if AFormat.BorderStyles[cbWest].LineStyle = lsDouble then
|
|
Result := Result + 'style:border-linewidth-left="0.002cm 0.035cm 0.002cm" ';
|
|
end
|
|
else
|
|
Result := Result + 'fo:border-left="none" ';
|
|
|
|
if cbEast in AFormat.Border then
|
|
begin
|
|
Result := Result + Format('fo:border-right="%s %s %s" ', [
|
|
BORDER_LINEWIDTHS[AFormat.BorderStyles[cbEast].LineStyle],
|
|
BORDER_LINESTYLES[AFormat.BorderStyles[cbEast].LineStyle],
|
|
ColorToHTMLColorStr(AFormat.BorderStyles[cbEast].Color)
|
|
]);
|
|
if AFormat.BorderStyles[cbSouth].LineStyle = lsDouble then
|
|
Result := Result + 'style:border-linewidth-right="0.002cm 0.035cm 0.002cm" ';
|
|
end
|
|
else
|
|
Result := Result + 'fo:border-right="none" ';
|
|
|
|
if cbNorth in AFormat.Border then
|
|
begin
|
|
Result := Result + Format('fo:border-top="%s %s %s" ', [
|
|
BORDER_LINEWIDTHS[AFormat.BorderStyles[cbNorth].LineStyle],
|
|
BORDER_LINESTYLES[AFormat.BorderStyles[cbNorth].LineStyle],
|
|
ColorToHTMLColorStr(AFormat.BorderStyles[cbNorth].Color)
|
|
]);
|
|
if AFormat.BorderStyles[cbSouth].LineStyle = lsDouble then
|
|
Result := Result + 'style:border-linewidth-top="0.002cm 0.035cm 0.002cm" ';
|
|
end else
|
|
Result := Result + 'fo:border-top="none" ';
|
|
|
|
if cbDiagUp in AFormat.Border then
|
|
begin
|
|
Result := Result + Format('style:diagonal-bl-tr="%s %s %s" ', [
|
|
BORDER_LINEWIDTHS[AFormat.BorderStyles[cbDiagUp].LineStyle],
|
|
BORDER_LINESTYLES[AFormat.BorderStyles[cbDiagUp].LineStyle],
|
|
ColorToHTMLColorStr(AFormat.BorderStyles[cbDiagUp].Color)
|
|
]);
|
|
end;
|
|
|
|
if cbDiagDown in AFormat.Border then
|
|
begin
|
|
Result := Result + Format('style:diagonal-tl-br="%s %s %s" ', [
|
|
BORDER_LINEWIDTHS[AFormat.BorderStyles[cbDiagDown].LineStyle],
|
|
BORDER_LINESTYLES[AFormat.BorderStyles[cbDiagDown].LineStyle],
|
|
ColorToHTMLColorStr(AFormat.BorderStyles[cbDiagDown].Color)
|
|
]);
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteDefaultFontXMLAsString: String;
|
|
var
|
|
fnt: TsFont;
|
|
begin
|
|
fnt := Workbook.GetDefaultFont;
|
|
Result := Format(
|
|
'<style:text-properties style:font-name="%s" fo:font-size="%.1fpt" />',
|
|
[fnt.FontName, fnt.Size], FPointSeparatorSettings
|
|
);
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteDefaultGraphicStyleXMLAsString: String;
|
|
begin
|
|
Result :=
|
|
'<style:graphic-properties svg:stroke-color="#3465a4" '+
|
|
'draw:fill-color="#729fcf" fo:wrap-option="no-wrap" '+
|
|
'draw:shadow-offset-x="3mm" draw:shadow-offset-y="3mm" />' +
|
|
'<style:paragraph-properties style:text-autospace="ideograph-alpha" '+
|
|
'style:punctuation-wrap="simple" style:line-break="strict" '+
|
|
'style:writing-mode="page" style:font-independent-line-spacing="false">'+
|
|
'<style:tab-stops />'+
|
|
'</style:paragraph-properties>'+
|
|
'<style:text-properties style:use-window-font-color="true" '+
|
|
'fo:font-family="''Liberation Serif''" style:font-family-generic="roman" '+
|
|
'style:font-pitch="variable" fo:font-size="12pt" ' +
|
|
//'fo:language="de" fo:country="DE" '+
|
|
'style:letter-kerning="true" '+
|
|
'style:font-name-asian="Segoe UI" style:font-size-asian="12pt" '+
|
|
'style:language-asian="zh" style:country-asian="CN" '+
|
|
'style:font-name-complex="Tahoma" style:font-size-complex="12pt" '+
|
|
'style:language-complex="hi" style:country-complex="IN" />';
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteDocumentProtectionXMLAsString: String;
|
|
var
|
|
cinfo: TsCryptoInfo;
|
|
pwd, algo: String;
|
|
begin
|
|
if bpLockStructure in Workbook.Protection then
|
|
begin
|
|
cinfo := Workbook.CryptoInfo;
|
|
if cinfo.PasswordHash <> '' then
|
|
pwd := Format(' table:protection-key="%s"', [cinfo.PasswordHash])
|
|
else
|
|
pwd := '';
|
|
if cinfo.Algorithm <> caUnknown then
|
|
algo := Format(' table:protection-key-digest-algorithm="%s"',
|
|
[AlgorithmToStr(cinfo.Algorithm, auOpenDocument)])
|
|
else
|
|
algo := '';
|
|
Result := ' table:structure-protected="true"' + pwd + algo;
|
|
end
|
|
else
|
|
Result := '';
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteError(AStream: TStream;
|
|
const ARow, ACol: Cardinal; const AValue: TsErrorValue; ACell: PCell);
|
|
var
|
|
fmt: PsCellFormat;
|
|
lStyle: String;
|
|
comment: String;
|
|
rowsSpannedStr, colsSpannedStr: String;
|
|
spannedStr: String;
|
|
valueStr: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
begin
|
|
Unused(ARow, ACol, AValue);
|
|
|
|
fmt := FWorkbook.GetPointerToCellFormat(ACell^.FormatIndex);
|
|
if fmt^.UsedFormattingFields <> [] then
|
|
lStyle := ' table:style-name="ce' + IntToStr(ACell^.FormatIndex) + '" '
|
|
else
|
|
lStyle := '';
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
rowsSpannedStr := Format(' table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
colsSpannedStr := Format(' table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
spannedStr := colsSpannedStr + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
// Displayed value
|
|
valueStr := GetErrorValueStr(ACell^.ErrorValue);
|
|
|
|
// Hyperlink
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]);
|
|
|
|
// Write to stream
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:formula="%s" office:value-type="string"%s%s>'+
|
|
'office:string-value="" calcext:value-type="error">' +
|
|
comment +
|
|
'<text:p>%s</text:p>' +
|
|
'</table:table-cell>', [
|
|
valueStr, lStyle, spannedStr,
|
|
valueStr
|
|
]));
|
|
(*
|
|
<table:table-cell table:formula="of:=#N/A" office:value-type="string"
|
|
office:string-value="" calcext:value-type="error">
|
|
<text:p>#NV</text:p>
|
|
</table:table-cell>
|
|
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell office:value-type="%s" office:boolean-value="%s" %s %s >' +
|
|
comment +
|
|
'<text:p>%s</text:p>' +
|
|
'</table:table-cell>', [
|
|
valType, StrValue, lStyle, spannedStr,
|
|
DisplayStr
|
|
]));
|
|
*)
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteFontStyleXMLAsString(AFont: TsFont): String;
|
|
var
|
|
defFnt: TsFont;
|
|
begin
|
|
Result := '';
|
|
|
|
defFnt := Workbook.GetDefaultFont;
|
|
if AFont = nil then AFont := defFnt;
|
|
|
|
Result := Result + Format('style:font-name="%s" ', [AFont.FontName]);
|
|
|
|
Result := Result + Format('fo:font-size="%.1fpt" style:font-size-asian="%.1fpt" style:font-size-complex="%.1fpt" ',
|
|
[AFont.Size, AFont.Size, AFont.Size], FPointSeparatorSettings);
|
|
|
|
if fssBold in AFont.Style then
|
|
Result := Result + 'fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" ';
|
|
|
|
if fssItalic in AFont.Style then
|
|
Result := Result + 'fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic" ';
|
|
|
|
if fssUnderline in AFont.Style then
|
|
Result := Result + 'style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color" ';
|
|
|
|
if fssStrikeout in AFont.Style then
|
|
Result := Result + 'style:text-line-through-style="solid" ';
|
|
|
|
if AFont.Position = fpSubscript then
|
|
Result := Result + 'style:text-position="sub 58%" ';
|
|
|
|
if AFont.Position = fpSuperscript then
|
|
Result := Result + 'style:text-position="super 58%" ';
|
|
|
|
if AFont.Color <> defFnt.Color then
|
|
Result := Result + Format('fo:color="%s" ', [ColorToHTMLColorStr(AFont.Color)]);
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteFontStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
Result := '';
|
|
if (uffFont in AFormat.UsedFormattingFields) then
|
|
Result := WriteFontStyleXMLAsString(Workbook.GetFont(AFormat.FontIndex));
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteHeaderFooterFontXMLAsString(
|
|
AFont: TsHeaderFooterFont): String;
|
|
begin
|
|
Result := Format('style:font-name="%s" fo:font-size="%dpt" ', [
|
|
AFont.FontName, round(AFont.Size)
|
|
]);
|
|
|
|
if hfsBold in AFont.Style then
|
|
Result := Result + 'fo:font-weight="bold" ';
|
|
|
|
if hfsItalic in AFont.Style then
|
|
Result := Result + 'fo:font-style="italic" ';
|
|
|
|
if hfsUnderline in AFont.Style then
|
|
Result := Result + 'style:text-underline-style="solid" '+
|
|
'style:text-underline-width="auto" '+
|
|
'style:text-underline-color="font-color" ';
|
|
|
|
if hfsDblUnderline in AFont.Style then
|
|
Result := Result + 'style:text-underline-style="solid" '+
|
|
'style:text-underline-type="double" ' +
|
|
'style:text-underline-width="auto" '+
|
|
'style:text-underline-color="font-color" ';
|
|
|
|
if hfsStrikeout in AFont.Style then
|
|
Result := Result + 'style:text-line-through-style="solid" ';
|
|
|
|
if hfsOutline in AFont.Style then
|
|
Result := Result + 'style:text-outline="true" ';
|
|
|
|
if hfsShadow in AFont.Style then
|
|
Result := Result + 'fo:text-shadow="1pt 1pt" ' +
|
|
'style:text-outline="none" ';
|
|
|
|
if hfsSubscript in AFont.Style then
|
|
Result := Result + 'style:text-position="sub 58%" ';
|
|
|
|
if hfsSuperscript in AFont.Style then
|
|
Result := Result + 'style:text-position="super 58%" ';
|
|
|
|
if AFont.Color <> 0 then
|
|
Result := Result + Format('fo:color="%s" ', [ColorToHTMLColorStr(AFont.Color)]);
|
|
end;
|
|
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Creates an XML string for inclusion of the horizontal alignment into the
|
|
written file from the horizontal alignment setting in the format cell.
|
|
Is called from WriteStyles (via WriteStylesXMLAsString).
|
|
-------------------------------------------------------------------------------}
|
|
function TsSpreadOpenDocWriter.WriteHorAlignmentStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
Result := '';
|
|
if not (uffHorAlign in AFormat.UsedFormattingFields) then
|
|
exit;
|
|
case AFormat.HorAlignment of
|
|
haLeft : Result := 'fo:text-align="start" ';
|
|
haCenter : Result := 'fo:text-align="center" ';
|
|
haRight : Result := 'fo:text-align="end" ';
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WritePageLayoutXMLAsString(AStyleName: String;
|
|
const APageLayout: TsPageLayout): String;
|
|
|
|
function CalcPageLayoutPropStr: String;
|
|
var
|
|
topmargin, bottommargin: Double;
|
|
options: String;
|
|
begin
|
|
topMargin := IfThen(APageLayout.HasHeader,
|
|
APageLayout.HeaderMargin, APageLayout.TopMargin);
|
|
bottomMargin := IfThen(APageLayout.HasFooter,
|
|
APageLayout.FooterMargin, APageLayout.BottomMargin);
|
|
|
|
Result := Format(
|
|
'fo:page-width="%.2fmm" fo:page-height="%.2fmm" '+
|
|
'fo:margin-top="%.2fmm" fo:margin-bottom="%.2fmm" '+
|
|
'fo:margin-left="%.2fmm" fo:margin-right="%.2fmm" ', [
|
|
APageLayout.PageWidth, APageLayout.PageHeight,
|
|
topmargin, bottommargin,
|
|
APageLayout.LeftMargin, APageLayout.RightMargin
|
|
], FPointSeparatorSettings);
|
|
|
|
if APageLayout.Orientation = spoLandscape then
|
|
Result := Result + 'style:print-orientation="landscape" ';
|
|
|
|
if poPrintPagesByRows in APageLayout.Options then
|
|
Result := Result + 'style:print-page-order="ltr" ';
|
|
|
|
if poUseStartPageNumber in APageLayout.Options then
|
|
Result := Result + 'style:first-page-number="' + IntToStr(APageLayout.StartPageNumber) +'" '
|
|
else
|
|
Result := Result + 'style:first-page-number="continue" ';
|
|
|
|
if APageLayout.Options * [poHorCentered, poVertCentered] = [poHorCentered, poVertCentered] then
|
|
Result := Result + 'style:table-centering="both" '
|
|
else if poHorCentered in APageLayout.Options then
|
|
Result := Result + 'style:table-centering="horizontal" '
|
|
else if poVertCentered in APageLayout.Options then
|
|
Result := Result + 'style:table-centering="vertical" ';
|
|
|
|
if poFitPages in APageLayout.Options then
|
|
begin
|
|
if APageLayout.FitWidthToPages > 0 then
|
|
Result := Result + 'style:scale-to-X="' + IntToStr(APageLayout.FitWidthToPages) + '" ';
|
|
if APageLayout.FitHeightToPages > 0 then
|
|
Result := Result + 'style:scale-to-Y="' + IntToStr(APageLayout.FitHeightToPages) + '" ';
|
|
end else
|
|
Result := Result + 'style:scale-to="' + IntToStr(APageLayout.ScalingFactor) + '%" ';
|
|
|
|
options := 'charts drawings objects zero-values';
|
|
if poPrintGridLines in APageLayout.Options then
|
|
options := options + ' grid';
|
|
if poPrintHeaders in APageLayout.Options then
|
|
options := options + ' headers';
|
|
if poPrintCellComments in APageLayout.Options then
|
|
options := options + ' annotations';
|
|
|
|
Result := Result + 'style:print="' + options + '" ';
|
|
end;
|
|
|
|
function CalcStyleStr(AName, AHeaderFooterImageStr: String;
|
|
APageMargin, AHeaderFooterMargin: Double): String;
|
|
var
|
|
marginKind: String;
|
|
begin
|
|
if AName = 'header' then marginKind := 'bottom' else marginKind := 'top';
|
|
Result := Format(
|
|
'<style:%s-style>' + // e.g. <style:header-style>
|
|
'<style:header-footer-properties ' +
|
|
'fo:margin-left="0mm" fo:margin-right="0mm" '+
|
|
'svg:height="%.2fmm" fo:margin-%s="%.2fmm" ' + // fo:margin-bottom or -top
|
|
'fo:background-color="transparent">' +
|
|
'%s' +
|
|
'</style:header-footer-properties>' +
|
|
'</style:%s-style>', [
|
|
AName,
|
|
APageMargin - AHeaderFooterMargin, marginKind, 0.0,
|
|
AHeaderFooterImageStr,
|
|
AName
|
|
], FPointSeparatorSettings);
|
|
end;
|
|
|
|
procedure CalcHeaderFooterImageStr(out AHeaderImageStr, AFooterImageStr: String);
|
|
var
|
|
hdrImg, ftrImg, hdrImgPos, ftrImgPos: String;
|
|
begin
|
|
GetHeaderFooterImageName(APageLayout, hdrImg, ftrImg);
|
|
GetHeaderFooterImagePosStr(APageLayout, hdrImgPos, ftrImgPos);
|
|
|
|
AHeaderImageStr := IfThen((hdrImg = '') or (hdrImgPos = ''), '', Format(
|
|
'<style:background-image xlink:href="Pictures/%s" '+
|
|
'xlink:type="simple" xlink:actuate="onLoad" '+
|
|
'style:position="center %s" style:repeat="no-repeat" />',
|
|
[hdrImg, hdrImgPos] ));
|
|
|
|
AFooterImageStr := IfThen((ftrImg = '') or (ftrImgPos = ''), '', Format(
|
|
'<style:background-image xlink:href="Pictures/%s" '+
|
|
'xlink:type="simple" xlink:actuate="onLoad" '+
|
|
'style:position="center %s" style:repeat="no-repeat" />',
|
|
[ftrImg, ftrImgPos]));
|
|
end;
|
|
|
|
var
|
|
hdrImgStr: String = '';
|
|
ftrImgStr: String = '';
|
|
begin
|
|
CalcHeaderFooterImageStr(hdrImgStr, ftrImgStr);
|
|
Result :=
|
|
'<style:page-layout style:name="' + AStyleName + '">' +
|
|
'<style:page-layout-properties ' + CalcPageLayoutPropStr + '/>'+
|
|
CalcStyleStr('header', hdrImgStr, APageLayout.TopMargin, APageLayout.HeaderMargin) +
|
|
CalcStyleStr('footer', ftrImgStr, APageLayout.BottomMargin, APageLayout.FooterMargin) +
|
|
'</style:page-layout>';
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WritePrintRangesXMLAsString(ASheet: TsWorksheet): String;
|
|
var
|
|
i: Integer;
|
|
rng: TsCellRange;
|
|
sheetName: String;
|
|
begin
|
|
Result := '';
|
|
if ASheet.PageLayout.NumPrintRanges > 0 then
|
|
begin
|
|
for i := 0 to ASheet.PageLayout.NumPrintRanges - 1 do
|
|
begin
|
|
rng := ASheet.PageLayout.PrintRange[i];
|
|
if pos(' ', ASheet.Name) > 0 then
|
|
sheetName := ''' + UTF8TextToXMLText(ASheet.Name) + ''' else
|
|
sheetname := UTF8TextToXMLText(ASheet.Name);
|
|
Result := Result + ' ' + Format('%s.%s:%s.%s', [
|
|
sheetName, GetCellString(rng.Row1,rng.Col1),
|
|
sheetName, GetCellString(rng.Row2,rng.Col2)
|
|
]);
|
|
end;
|
|
if Result <> '' then
|
|
begin
|
|
Delete(Result, 1, 1);
|
|
Result := ' table:print-ranges="' + Result + '"';
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteSheetProtectionXMLAsString(
|
|
ASheet: TsWorksheet): String;
|
|
{table:protected="true" table:protection-key="h/jtkVcSX/xNqeBqe4ARrYClP+E=" table:protection-key-digest-algorithm="http://www.w3.org/2000/09/xmldsig#sha1"}
|
|
var
|
|
pwd: String;
|
|
algo: String;
|
|
begin
|
|
Result := '';
|
|
if ASheet.IsProtected then
|
|
begin
|
|
if ASheet.CryptoInfo.PasswordHash <> '' then
|
|
pwd := ' table:protection-key="' + ASheet.CryptoInfo.PasswordHash + '"' else
|
|
pwd := '';
|
|
algo := AlgorithmToStr(ASheet.CryptoInfo.Algorithm, auOpenDocument);
|
|
if algo <> '' then
|
|
algo := ' table:protection-key-digest-algorithm="%s"';
|
|
Result := ' table:protected="true"' + pwd + algo;
|
|
end;
|
|
end;
|
|
|
|
function TsSpreadOpenDocWriter.WriteSheetProtectionDetailsXMLAsString(
|
|
ASheet: TsWorksheet): String;
|
|
// <loext:table-protection loext:select-unprotected-cells="true" />
|
|
begin
|
|
Result := '';
|
|
if ASheet.IsProtected then
|
|
begin
|
|
if not (spSelectUnlockedCells in ASheet.Protection) then
|
|
Result := Result + ' loext:select-unprotected-cells="true"';
|
|
if not (spSelectLockedCells in ASheet.Protection) then
|
|
Result := Result + ' loext:select-protected-cells="true"';
|
|
Result := '<loext:table-protection' + Result + '/>';
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteShapes(AStream: TStream;
|
|
ASheet: TsWorksheet);
|
|
{
|
|
<table:shapes>
|
|
<draw:frame draw:z-index="0" draw:name="Bild 1" draw:style-name="gr1" draw:text-style-name="P1"
|
|
svg:width="4.45mm" svg:height="4.24mm" svg:x="0mm" svg:y="0mm">
|
|
<draw:image xlink:href="Pictures/100002010000001000000010DC3B2E96AAE6D486.png" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad">
|
|
<text:p />
|
|
</draw:image>
|
|
</draw:frame>
|
|
</table:shapes>
|
|
}
|
|
var
|
|
i: Integer;
|
|
img: TsImage;
|
|
imgType: TsImageType;
|
|
r1,c1,r2,c2: Cardinal;
|
|
roffs1,coffs1, roffs2, coffs2: Double;
|
|
x, y, w, h: Double;
|
|
begin
|
|
if ASheet.GetImageCount = 0 then
|
|
exit;
|
|
|
|
AppendToStream(AStream,
|
|
'<table:shapes>');
|
|
|
|
for i:=0 to ASheet.GetImageCount-1 do
|
|
begin
|
|
img := ASheet.GetImage(i);
|
|
imgType := FWorkbook.GetEmbeddedObj(img.Index).ImageType;
|
|
if imgType = itUnknown then
|
|
Continue;
|
|
|
|
ASheet.CalcImageExtent(i, false, // not clear if UsePixels=false is correct. Not harmful at least
|
|
r1, c1, r2, c2,
|
|
roffs1, coffs1, roffs2, coffs2, // mm
|
|
x, y, w, h); // mm
|
|
|
|
AppendToStream(AStream, Format(
|
|
'<draw:frame draw:z-index="%d" draw:name="Image %d" '+
|
|
'draw:style-name="gr1" draw:text-style-name="P1" '+
|
|
'svg:width="%.2fmm" svg:height="%.2fmm" '+
|
|
'svg:x="%.2fmm" svg:y="%.2fmm">' +
|
|
'<draw:image xlink:href="Pictures/%d.%s" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad">' +
|
|
'<text:p />' +
|
|
'</draw:image>' +
|
|
'</draw:frame>', [
|
|
i+1, i+1,
|
|
w, h,
|
|
x, y,
|
|
img.Index+1, GetImageTypeExt(imgType)
|
|
], FPointSeparatorSettings));
|
|
end;
|
|
|
|
AppendToStream(AStream,
|
|
'</table:shapes>');
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteTableSettings(AStream: TStream);
|
|
var
|
|
i: Integer;
|
|
sheet: TsWorkSheet;
|
|
sheetname: String;
|
|
hsm: Integer; // HorizontalSplitMode
|
|
vsm: Integer; // VerticalSplitMode
|
|
asr: Integer; // ActiveSplitRange
|
|
actX, actY: Integer; // Active cell col/row index
|
|
zoom: String;
|
|
begin
|
|
zoom := '100';
|
|
for i:=0 to Workbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := Workbook.GetWorksheetByIndex(i);
|
|
sheetname := UTF8TextToXMLText(sheet.Name);
|
|
|
|
AppendToStream(AStream,
|
|
'<config:config-item-map-entry config:name="' + sheetname + '">');
|
|
|
|
hsm := 0; vsm := 0; asr := 2;
|
|
if (soHasFrozenPanes in sheet.Options) then
|
|
begin
|
|
if (sheet.LeftPaneWidth > 0) and (sheet.TopPaneHeight > 0) then
|
|
begin
|
|
hsm := 2; vsm := 2; asr := 3;
|
|
end else
|
|
if (sheet.LeftPaneWidth > 0) then
|
|
begin
|
|
hsm := 2; vsm := 0; asr := 3;
|
|
end else if (sheet.TopPaneHeight > 0) then
|
|
begin
|
|
hsm := 0; vsm := 2; asr := 2;
|
|
end;
|
|
end;
|
|
{showGrid := (soShowGridLines in sheet.Options);}
|
|
|
|
if (sheet.ActiveCellRow <> cardinal(-1)) and (sheet.ActiveCellCol <> cardinal(-1)) then
|
|
begin
|
|
actX := sheet.ActiveCellCol;
|
|
actY := sheet.ActiveCellRow;
|
|
end else
|
|
begin
|
|
actX := sheet.LeftPaneWidth;
|
|
actY := sheet.TopPaneHeight;
|
|
end;
|
|
|
|
if boWriteZoomFactor in FWorkbook.Options then
|
|
zoom := IntToStr(round(sheet.ZoomFactor*100.0));
|
|
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="CursorPositionX" config:type="int">'+IntToStr(actX)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="CursorPositionY" config:type="int">'+IntToStr(actY)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="HorizontalSplitMode" config:type="short">'+IntToStr(hsm)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="VerticalSplitMode" config:type="short">'+IntToStr(vsm)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="HorizontalSplitPosition" config:type="int">'+IntToStr(sheet.LeftPaneWidth)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="VerticalSplitPosition" config:type="int">'+IntToStr(sheet.TopPaneHeight)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="ActiveSplitRange" config:type="short">'+IntToStr(asr)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="PositionLeft" config:type="int">0</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="PositionRight" config:type="int">'+IntToStr(sheet.LeftPaneWidth)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="PositionTop" config:type="int">0</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="PositionBottom" config:type="int">'+IntToStr(sheet.TopPaneHeight)+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="ZoomValue" config:type="int">'+zoom+'</config:config-item>');
|
|
AppendToStream(AStream,
|
|
'<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>');
|
|
// this "ShowGrid" overrides the global setting. But Open/LibreOffice do not allow to change ShowGrid per sheet.
|
|
AppendToStream(AStream,
|
|
'</config:config-item-map-entry>');
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteTableStyles(AStream: TStream);
|
|
var
|
|
i: Integer;
|
|
sheet: TsWorksheet;
|
|
sheetname, bidi: String;
|
|
begin
|
|
for i:=0 to FWorkbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := FWorkbook.GetWorksheetByIndex(i);
|
|
sheetname := UTF8TextToXMLText(sheet.Name);
|
|
case sheet.BiDiMode of
|
|
bdDefault: bidi := '';
|
|
bdLTR : bidi := 'style:writing-mode="lr-tb" ';
|
|
bdRTL : bidi := 'style:writing-mode="rl-tb" ';
|
|
end;
|
|
AppendToStream(AStream, Format(
|
|
'<style:style style:name="ta%d" style:family="table" style:master-page-name="PageStyle_5f_%s">' +
|
|
'<style:table-properties table:display="%s" %s/>' +
|
|
'</style:style>', [
|
|
i+1, UTF8TextToXMLText(sheetname),
|
|
FALSE_TRUE[not (soHidden in sheet.Options)], bidi
|
|
]));
|
|
if sheet.GetImageCount > 0 then
|
|
begin
|
|
// Embedded images written by fps refer to a graphic style "gr1"...
|
|
AppendToStream(AStream,
|
|
'<style:style style:name="gr1" style:family="graphic">'+
|
|
'<style:graphic-properties draw:stroke="none" draw:fill="none" '+
|
|
'draw:textarea-horizontal-align="center" '+
|
|
'draw:textarea-vertical-align="middle" '+
|
|
'draw:color-mode="standard" '+
|
|
'draw:luminance="0%" draw:contrast="0%" draw:image-opacity="100%" '+
|
|
'draw:gamma="100%" draw:red="0%" draw:green="0%" draw:blue="0%" '+
|
|
'fo:clip="rect(0mm, 0mm, 0mm, 0mm)" '+
|
|
'style:mirror="none"/>'+
|
|
'</style:style>');
|
|
// ... and a paragraph style named "P1"
|
|
AppendToStream(AStream,
|
|
'<style:style style:name="P1" style:family="paragraph">' +
|
|
'<loext:graphic-properties draw:fill="none" />' +
|
|
'<style:paragraph-properties fo:text-align="center" />' +
|
|
'</style:style>');
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteTextStyles(AStream: TStream);
|
|
var
|
|
cell: PCell;
|
|
rtp: TsRichTextParam;
|
|
styleCounter: Integer;
|
|
fnt: TsFont;
|
|
fntStr: String;
|
|
styleName: String;
|
|
sheet: TsWorksheet;
|
|
i: Integer;
|
|
begin
|
|
styleCounter := 0;
|
|
for i := 0 to FWorkbook.GetWorksheetCount-1 do
|
|
begin
|
|
sheet := FWorkbook.GetWorksheetByIndex(i);
|
|
for cell in sheet.Cells do
|
|
begin
|
|
if Length(cell^.RichTextParams) = 0 then
|
|
Continue;
|
|
for rtp in cell^.RichTextParams do
|
|
begin
|
|
inc(styleCounter);
|
|
stylename := Format('T%d', [stylecounter]);
|
|
fnt := FWorkbook.GetFont(rtp.FontIndex);
|
|
FRichTextFontList.AddObject(stylename, fnt);
|
|
fntStr := WriteFontStyleXMLAsString(fnt);
|
|
AppendToStream(AStream,
|
|
'<style:style style:name="' + stylename + '" style:family="text">' +
|
|
'<style:text-properties ' + fntStr + '/>' +
|
|
'</style:style>');
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Creates an XML string for inclusion of the text rotation style option into the
|
|
written file from the textrotation setting in the format cell.
|
|
Is called from WriteStyles (via WriteStylesXMLAsString).
|
|
-------------------------------------------------------------------------------}
|
|
function TsSpreadOpenDocWriter.WriteTextRotationStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
Result := '';
|
|
if not (uffTextRotation in AFormat.UsedFormattingFields) then
|
|
exit;
|
|
|
|
case AFormat.TextRotation of
|
|
rt90DegreeClockwiseRotation : Result := 'style:rotation-angle="270" ';
|
|
rt90DegreeCounterClockwiseRotation : Result := 'style:rotation-angle="90" ';
|
|
rtStacked : Result := 'style:direction="ttb" ';
|
|
end;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Creates an XML string for inclusion of the vertical alignment into the
|
|
written file from the vertical alignment setting in the given format record.
|
|
Is called from WriteStyles (via WriteStylesXMLAsString).
|
|
-------------------------------------------------------------------------------}
|
|
function TsSpreadOpenDocWriter.WriteVertAlignmentStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
Result := '';
|
|
if not (uffVertAlign in AFormat.UsedFormattingFields) then
|
|
exit;
|
|
case AFormat.VertAlignment of
|
|
vaTop : Result := 'style:vertical-align="top" ';
|
|
vaCenter : Result := 'style:vertical-align="middle" ';
|
|
vaBottom : Result := 'style:vertical-align="bottom" ';
|
|
end;
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteVirtualCells(AStream: TStream;
|
|
ASheet: TsWorksheet);
|
|
var
|
|
r, c, cc: Cardinal;
|
|
lCell: TCell;
|
|
row: PRow;
|
|
value: variant;
|
|
styleCell: PCell;
|
|
styleName: String;
|
|
h: Single; // row height workbook units
|
|
k: Integer;
|
|
rowStyleData: TRowStyleData;
|
|
rowsRepeated: Cardinal;
|
|
colsRepeated: Cardinal;
|
|
colsRepeatedStr: String;
|
|
lastCol, lastRow: Cardinal;
|
|
begin
|
|
if ASheet.VirtualColCount = 0 then
|
|
exit;
|
|
if ASheet.VirtualRowCount = 0 then
|
|
exit;
|
|
if not Assigned(ASheet.OnWriteCellData) then
|
|
exit;
|
|
|
|
// some abbreviations...
|
|
lastCol := LongInt(ASheet.VirtualColCount) - 1;
|
|
lastRow := LongInt(ASheet.VirtualRowCount) - 1;
|
|
|
|
rowsRepeated := 1;
|
|
r := 0;
|
|
while (r <= lastRow) do
|
|
begin
|
|
// Look for the row style of the current row (r)
|
|
row := ASheet.FindRow(r);
|
|
if row = nil then
|
|
styleName := 'ro1'
|
|
else
|
|
begin
|
|
styleName := '';
|
|
|
|
h := row^.Height; // row height in workbook units
|
|
for k := 0 to FRowStyleList.Count-1 do
|
|
begin
|
|
rowStyleData := TRowStyleData(FRowStyleList[k]);
|
|
// Compare row heights, but be aware of rounding errors
|
|
if SameValue(rowStyleData.RowHeight, h, ROWHEIGHT_EPS) then
|
|
begin
|
|
styleName := rowStyleData.Name;
|
|
break;
|
|
end;
|
|
end;
|
|
if styleName = '' then
|
|
raise Exception.Create(rsRowStyleNotFound);
|
|
end;
|
|
|
|
// No empty rows allowed here for the moment!
|
|
|
|
// Write the row XML
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-row table:style-name="%s">', [styleName]));
|
|
|
|
// Loop along the row and write the cells.
|
|
c := 0;
|
|
while c <= lastCol do
|
|
begin
|
|
// Empty cell? Need to count how many "table:number-columns-repeated" to be added
|
|
colsRepeated := 1;
|
|
|
|
lCell.Row := r; // to silence a compiler hint...
|
|
InitCell(r, c, lCell);
|
|
value := varNull;
|
|
styleCell := nil;
|
|
|
|
ASheet.OnWriteCellData(ASheet, r, c, value, styleCell);
|
|
|
|
if VarIsNull(value) then
|
|
begin
|
|
// Local loop to count empty cells
|
|
cc := c + 1;
|
|
while (cc <= lastCol) do
|
|
begin
|
|
InitCell(r, cc, lCell);
|
|
value := varNull;
|
|
styleCell := nil;
|
|
ASheet.OnWriteCellData(ASheet, r, cc, value, styleCell);
|
|
if not VarIsNull(value) then
|
|
break;
|
|
inc(cc);
|
|
end;
|
|
colsRepeated := cc - c;
|
|
colsRepeatedStr := IfThen(colsRepeated = 1, '',
|
|
Format('table:number-columns-repeated="%d"', [colsRepeated]));
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell %s />', [colsRepeatedStr]));
|
|
end else begin
|
|
if VarIsNumeric(value) then
|
|
begin
|
|
lCell.ContentType := cctNumber;
|
|
lCell.NumberValue := value;
|
|
end else
|
|
if VarType(value) = varDate then
|
|
begin
|
|
lCell.ContentType := cctDateTime;
|
|
lCell.DateTimeValue := StrToDateTime(VarToStr(value), Workbook.FormatSettings); // was: StrToDate
|
|
end else
|
|
if VarIsStr(value) then
|
|
begin
|
|
lCell.ContentType := cctUTF8String;
|
|
lCell.UTF8StringValue := VarToStrDef(value, '');
|
|
end else
|
|
if VarIsBool(value) then
|
|
begin
|
|
lCell.ContentType := cctBool;
|
|
lCell.BoolValue := value <> 0;
|
|
end else
|
|
lCell.ContentType := cctEmpty;
|
|
WriteCellToStream(AStream, @lCell);
|
|
end;
|
|
inc(c, colsRepeated);
|
|
end;
|
|
|
|
AppendToStream(AStream,
|
|
'</table:table-row>');
|
|
|
|
// Next row
|
|
inc(r, rowsRepeated);
|
|
end;
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Creates an XML string for inclusion of the wordwrap option into the
|
|
written file from the wordwrap setting in the format cell.
|
|
Is called from WriteStyles (via WriteStylesXMLAsString).
|
|
-------------------------------------------------------------------------------}
|
|
function TsSpreadOpenDocWriter.WriteWordwrapStyleXMLAsString(
|
|
const AFormat: TsCellFormat): String;
|
|
begin
|
|
if (uffWordWrap in AFormat.UsedFormattingFields) then
|
|
Result := 'fo:wrap-option="wrap" '
|
|
else
|
|
Result := '';
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes a string formula
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteFormula(AStream: TStream; const ARow,
|
|
ACol: Cardinal; ACell: PCell);
|
|
var
|
|
lStyle: String = '';
|
|
parser: TsExpressionParser;
|
|
formula: String;
|
|
valuetype: String;
|
|
value: string;
|
|
valueStr: String;
|
|
colsSpannedStr: String;
|
|
rowsSpannedStr: String;
|
|
spannedStr: String;
|
|
comment: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
fmt: TsCellFormat;
|
|
begin
|
|
Unused(ARow, ACol);
|
|
|
|
// Style
|
|
fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex);
|
|
if fmt.UsedFormattingFields <> [] then
|
|
lStyle := ' table:style-name="ce' + IntToStr(ACell^.FormatIndex) + '" '
|
|
else
|
|
lStyle := '';
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
rowsSpannedStr := Format(' table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
colsSpannedStr := Format(' table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
spannedStr := colsSpannedStr + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
// Hyperlink
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]);
|
|
|
|
// Convert string formula to the format needed by ods: semicolon list separators!
|
|
parser := TsSpreadsheetParser.Create(FWorksheet);
|
|
try
|
|
parser.Dialect := fdOpenDocument;
|
|
parser.Expression := ACell^.FormulaValue;
|
|
formula := Parser.LocalizedExpression[FPointSeparatorSettings];
|
|
finally
|
|
parser.Free;
|
|
end;
|
|
|
|
valueStr := '';
|
|
case ACell^.ContentType of
|
|
cctNumber:
|
|
begin
|
|
valuetype := 'float';
|
|
value := ' office:value="' + Format('%g', [ACell^.NumberValue], FPointSeparatorSettings) + '"';
|
|
end;
|
|
cctDateTime:
|
|
if trunc(ACell^.DateTimeValue) = 0 then
|
|
begin
|
|
valuetype := 'time';
|
|
value := ' office:time-value="' + FormatDateTime(ISO8601FormatTimeOnly, ACell^.DateTimeValue) + '"';
|
|
end
|
|
else
|
|
begin
|
|
valuetype := 'date';
|
|
if frac(ACell^.DateTimeValue) = 0.0 then
|
|
value := ' office:date-value="' + FormatDateTime(ISO8601FormatDateOnly, ACell^.DateTimeValue) + '"'
|
|
else
|
|
value := ' office:date-value="' + FormatDateTime(ISO8601FormatExtended, ACell^.DateTimeValue) + '"';
|
|
end;
|
|
cctUTF8String:
|
|
begin
|
|
valuetype := 'string';
|
|
value := ' office:string-value="' + ACell^.UTF8StringValue +'"';
|
|
valueStr := '<text:p>' + ACell^.UTF8StringValue + '</text:p>';
|
|
end;
|
|
cctBool:
|
|
begin
|
|
valuetype := 'boolean';
|
|
value := ' office:boolean-value="' + BoolToStr(ACell^.BoolValue, 'true', 'false') + '"';
|
|
end;
|
|
cctError:
|
|
if HasFormula(ACell) then
|
|
begin
|
|
// Open/LibreOffice always writes a float value 0 to the cell
|
|
valuetype := 'float'; // error as result of a formula
|
|
value := ' office:value="0"';
|
|
end else
|
|
begin
|
|
valuetype := 'string" calcext:value-type="error'; // an error "constant"
|
|
value := ' office:value=""';
|
|
end;
|
|
end;
|
|
|
|
{ Fix special xml characters }
|
|
formula := UTF8TextToXMLText(formula);
|
|
|
|
{ We are writing a very rudimentary formula here without result and result
|
|
data type. Seems to work... }
|
|
if FWorksheet.GetCalcState(ACell) = csCalculated then
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:formula="=%s" office:value-type="%s"%s%s%s>' +
|
|
comment +
|
|
valueStr +
|
|
'</table:table-cell>', [
|
|
formula, valuetype, value, lStyle, spannedStr
|
|
]))
|
|
else
|
|
begin
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell table:formula="=%s"%s%s', [
|
|
formula, lStyle, spannedStr]));
|
|
if comment <> '' then
|
|
AppendToStream(AStream, '>' + comment + '</table:table-cell>')
|
|
else
|
|
AppendToStream(AStream, '/>');
|
|
end;
|
|
end;
|
|
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes a cell with text content
|
|
|
|
The UTF8 Text needs to be converted, because some chars are invalid in XML
|
|
See bug with patch 19422
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteLabel(AStream: TStream; const ARow,
|
|
ACol: Cardinal; const AValue: string; ACell: PCell);
|
|
var
|
|
lStyle: string = '';
|
|
colsSpannedStr: String;
|
|
rowsSpannedStr: String;
|
|
spannedStr: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
totaltxt, target, bookmark, comment: String;
|
|
fmt: TsCellFormat;
|
|
fnt: TsFont;
|
|
fntName: String;
|
|
hyperlink: PsHyperlink;
|
|
u: TUri;
|
|
i, idx, endidx, fntidx, len: Integer;
|
|
rtParam: TsRichTextParam;
|
|
wideStr, txt: WideString;
|
|
ch: WideChar;
|
|
|
|
function IsNewLine(var idx: Integer): Boolean;
|
|
begin
|
|
if (wideStr[idx] = #13) or (wideStr[idx] = #10) then
|
|
begin
|
|
Result := true;
|
|
if (idx < len) and (
|
|
((wideStr[idx] = #13) and (wideStr[idx+1] = #10)) or
|
|
((wideStr[idx] = #10) and (wideStr[idx+1] = #13)) ) then inc(idx);
|
|
end else
|
|
Result := false;
|
|
end;
|
|
|
|
procedure AppendTxt(NewLine: Boolean; FntStyle: String);
|
|
var
|
|
s: String;
|
|
begin
|
|
s := UTF8Encode(txt);
|
|
ValidXMLText(s);
|
|
{
|
|
if FntStyle <> '' then
|
|
FntStyle := ' text:style-name="' + FntStyle + '"';
|
|
}
|
|
if NewLine and (s = '') then
|
|
totaltxt := totaltxt + '</text:p><text:p>'
|
|
else
|
|
begin
|
|
if FntStyle = '' then
|
|
totaltxt := totaltxt + s
|
|
else
|
|
totaltxt := totaltxt +
|
|
'<text:span text:style-name="' + FntStyle + '">' + s + '</text:span>';
|
|
if NewLine then
|
|
totaltxt := totaltxt + '</text:p><text:p>';
|
|
end;
|
|
txt := '';
|
|
end;
|
|
|
|
begin
|
|
Unused(ARow, ACol);
|
|
|
|
// Style
|
|
fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex);
|
|
if fmt.UsedFormattingFields <> [] then
|
|
lStyle := ' table:style-name="ce' + IntToStr(ACell^.FormatIndex) + '"'
|
|
else
|
|
lStyle := '';
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
rowsSpannedStr := Format(' table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
colsSpannedStr := Format(' table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
spannedStr := colsSpannedStr + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
// Check for invalid characters, get the error message
|
|
totaltxt := AValue;
|
|
if not ValidXMLText(totaltxt) then
|
|
Workbook.AddErrorMsg(
|
|
rsInvalidCharacterInCell, [
|
|
GetCellString(ARow, ACol)
|
|
]);
|
|
|
|
// Hyperlink?
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
begin
|
|
hyperlink := FWorksheet.FindHyperlink(ACell);
|
|
SplitHyperlink(hyperlink^.Target, target, bookmark);
|
|
|
|
if (target <> '') and (pos('file:', target) = 0) then
|
|
begin
|
|
u := ParseURI(target);
|
|
if u.Protocol = '' then
|
|
target := '../' + target;
|
|
end;
|
|
|
|
// ods absolutely wants "/" path delimiters in the file uri!
|
|
FixHyperlinkPathdelims(target);
|
|
|
|
if (bookmark <> '') then
|
|
target := target + '#' + bookmark;
|
|
|
|
totaltxt := Format(
|
|
'<text:p>'+
|
|
'<text:a xlink:href="%s" xlink:type="simple">%s</text:a>'+
|
|
'</text:p>', [target, totaltxt]);
|
|
end
|
|
else
|
|
begin
|
|
// No hyperlink, normal text only
|
|
if Length(ACell^.RichTextParams) = 0 then
|
|
begin
|
|
// Standard text formatting
|
|
(*
|
|
{ ods writes "<text:line-break/>" nodes for line-breaks. BUT:
|
|
LibreOffice Calc fails to detect these during reading.
|
|
OpenOffice Calc and Excel are ok.
|
|
Therefore, we skip this part until LO gets fixed. }
|
|
|
|
wideStr := UTF8Decode(AValue);
|
|
len := Length(wideStr);
|
|
idx := 1;
|
|
totaltxt := '<text:p>';
|
|
while idx <= len do
|
|
begin
|
|
ch := widestr[idx];
|
|
totaltxt := totaltxt + IfThen(IsNewLine(idx), '<text:line-break />', ch);
|
|
inc(idx);
|
|
end;
|
|
totaltxt := totaltxt + '</text:p>';
|
|
*)
|
|
totaltxt := '<text:p>' + totaltxt + '</text:p>' ; // has and for line breaks
|
|
end else
|
|
begin
|
|
// "Rich-text" formatting
|
|
wideStr := UTF8Decode(AValue); // Convert to unicode
|
|
// Before the first formatted section having the cell's format
|
|
len := Length(wideStr);
|
|
totaltxt := '<text:p>';
|
|
rtParam := ACell^.RichTextParams[0];
|
|
idx := 1;
|
|
txt := '';
|
|
if rtParam.FirstIndex > 1 then
|
|
begin
|
|
while (idx <= len) and (idx < rtParam.FirstIndex) do
|
|
begin
|
|
ch := wideStr[idx];
|
|
if IsNewLine(idx) then
|
|
AppendTxt(true, '')
|
|
else
|
|
txt := txt + ch;
|
|
inc(idx);
|
|
end;
|
|
if txt <> '' then
|
|
AppendTxt(false, '');
|
|
end;
|
|
txt := '';
|
|
for i := 0 to High(ACell^.RichTextParams) do
|
|
begin
|
|
// Formatted parts of the string according the RichTextParams
|
|
rtParam := ACell^.RichTextParams[i];
|
|
fnt := FWorkbook.GetFont(rtParam.FontIndex);
|
|
fntidx := FRichTextFontList.IndexOfObject(fnt);
|
|
fntName := FRichTextFontList[fntIdx];
|
|
if i < High(ACell^.RichTextParams) then
|
|
endidx := ACell^.RichTextParams[i+1].FirstIndex-1 else
|
|
endidx := len;
|
|
while (idx <= len) and (idx <= endidx) do
|
|
begin
|
|
ch := wideStr[idx];
|
|
if IsNewLine(idx) then
|
|
AppendTxt(true, fntName)
|
|
else
|
|
txt := txt + ch;
|
|
inc(idx);
|
|
end;
|
|
if txt <> '' then
|
|
AppendTxt(false, fntName);
|
|
end;
|
|
totaltxt := totaltxt + '</text:p>';
|
|
end;
|
|
end;
|
|
|
|
// Write it ...
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell office:value-type="string"%s%s>' +
|
|
comment +
|
|
totaltxt +
|
|
'</table:table-cell>', [
|
|
lStyle, spannedStr
|
|
]));
|
|
end;
|
|
|
|
procedure TsSpreadOpenDocWriter.WriteNumber(AStream: TStream; const ARow,
|
|
ACol: Cardinal; const AValue: double; ACell: PCell);
|
|
var
|
|
StrValue: string;
|
|
DisplayStr: string;
|
|
lStyle: string = '';
|
|
valType: String;
|
|
colsSpannedStr: String;
|
|
rowsSpannedStr: String;
|
|
spannedStr: String;
|
|
comment: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
fmt: TsCellFormat;
|
|
numFmt: TsNumFormatParams;
|
|
nfSection: TsNumFormatSection;
|
|
begin
|
|
Unused(ARow, ACol);
|
|
|
|
valType := 'float';
|
|
fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex);
|
|
if fmt.UsedFormattingFields <> [] then
|
|
begin
|
|
numFmt := FWorkbook.GetNumberFormat(fmt.NumberFormatIndex);
|
|
if (numFmt <> nil) then begin
|
|
if (Length(numFmt.Sections) > 1) and (AValue < 0) then
|
|
nfSection := numFmt.Sections[1]
|
|
else
|
|
if (Length(numFmt.Sections) > 2) and (AValue = 0) then
|
|
nfSection := NumFmt.Sections[2]
|
|
else
|
|
nfSection := numFmt.Sections[0];
|
|
if (nfkPercent in nfSection.Kind) then
|
|
valType := 'percentage'
|
|
else
|
|
if (nfkCurrency in nfSection.Kind) then
|
|
valtype := 'currency'
|
|
end;
|
|
lStyle := ' table:style-name="ce' + IntToStr(ACell^.FormatIndex) + '"';
|
|
end else
|
|
lStyle := '';
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
rowsSpannedStr := Format(' table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
colsSpannedStr := Format(' table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
spannedStr := colsSpannedStr + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
// Displayed value
|
|
if IsInfinite(AValue) then
|
|
begin
|
|
StrValue := '1.#INF';
|
|
DisplayStr := '1.#INF';
|
|
end else begin
|
|
StrValue := FloatToStr(AValue, FPointSeparatorSettings); // Uses '.' as decimal separator
|
|
DisplayStr := FWorksheet.ReadAsText(ACell); //FloatToStr(AValue); // Uses locale decimal separator
|
|
end;
|
|
|
|
// Hyperlink
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]);
|
|
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell office:value-type="%s" office:value="%s"%s%s >' +
|
|
comment +
|
|
'<text:p>%s</text:p>' +
|
|
'</table:table-cell>', [
|
|
valType, StrValue, lStyle, spannedStr,
|
|
DisplayStr
|
|
]));
|
|
end;
|
|
|
|
{@@ ----------------------------------------------------------------------------
|
|
Writes a date/time value
|
|
-------------------------------------------------------------------------------}
|
|
procedure TsSpreadOpenDocWriter.WriteDateTime(AStream: TStream;
|
|
const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell);
|
|
const
|
|
DATE_FMT: array[boolean] of string = (ISO8601FormatExtended, ISO8601FormatTimeOnly);
|
|
DT: array[boolean] of string = ('date', 'time');
|
|
// Index "boolean" is to be understood as "isTimeOnly"
|
|
var
|
|
lStyle: string;
|
|
strValue: String;
|
|
displayStr: String;
|
|
isTimeOnly: Boolean;
|
|
colsSpannedStr: String;
|
|
rowsSpannedStr: String;
|
|
spannedStr: String;
|
|
comment: String;
|
|
r1,c1,r2,c2: Cardinal;
|
|
fmt: TsCellFormat;
|
|
numFmtParams: TsNumFormatParams;
|
|
h,m,s,ms: Word;
|
|
begin
|
|
Unused(ARow, ACol);
|
|
|
|
// Merged?
|
|
if FWorksheet.IsMergeBase(ACell) then
|
|
begin
|
|
FWorksheet.FindMergedRange(ACell, r1, c1, r2, c2);
|
|
colsSpannedStr := Format(' table:number-columns-spanned="%d"', [c2 - c1 + 1]);
|
|
rowsSpannedStr := Format(' table:number-rows-spanned="%d"', [r2 - r1 + 1]);
|
|
spannedStr := colsSpannedStr + rowsSpannedStr;
|
|
end else
|
|
spannedStr := '';
|
|
|
|
fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex);
|
|
numFmtParams := FWorkbook.GetNumberFormat(fmt.NumberFormatIndex);
|
|
if fmt.UsedFormattingFields <> [] then
|
|
lStyle := Format(' table:style-name="ce%d"', [ACell^.FormatIndex])
|
|
else
|
|
lStyle := '';
|
|
|
|
// Comment
|
|
comment := WriteCommentXMLAsString(FWorksheet.ReadComment(ACell));
|
|
|
|
// Hyperlink
|
|
if FWorksheet.HasHyperlink(ACell) then
|
|
FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]);
|
|
|
|
// nfTimeInterval is a special case - let's handle it first:
|
|
|
|
if IsTimeIntervalformat(numFmtParams) then
|
|
begin
|
|
DecodeTime(AValue, h,m,s,ms);
|
|
strValue := Format('PT%.2dH%.2dM%.2d.%.3dS', [trunc(AValue)*24+h, m, s, ms], FPointSeparatorSettings);
|
|
// strValue := FormatDateTime(ISO8601FormatHoursOverflow, AValue, [fdoInterval]);
|
|
displayStr := FWorksheet.ReadAsText(ACell);
|
|
// displayStr := FormatDateTime(fmt.NumberFormatStr, AValue, [fdoInterval]);
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell office:value-type="time" office:time-value="%s"%s%s>' +
|
|
comment +
|
|
'<text:p>%s</text:p>' +
|
|
'</table:table-cell>', [
|
|
strValue, lStyle, spannedStr,
|
|
displayStr
|
|
]));
|
|
end else
|
|
begin
|
|
// We have to distinguish between time-only values and values that contain date parts.
|
|
if (numFmtParams <> nil) then
|
|
isTimeOnly := Assigned(numFmtParams) and (numFmtParams.Sections[0].Kind * [nfkDate, nfkTime] = [nfkTime])
|
|
else
|
|
isTimeOnly := false;
|
|
strValue := FormatDateTime(DATE_FMT[isTimeOnly], AValue);
|
|
displayStr := FWorksheet.ReadAsText(ACell);
|
|
AppendToStream(AStream, Format(
|
|
'<table:table-cell office:value-type="%s" office:%s-value="%s" %s %s>' +
|
|
comment +
|
|
'<text:p>%s</text:p> ' +
|
|
'</table:table-cell>', [
|
|
DT[isTimeOnly], DT[isTimeOnly], strValue, lStyle, spannedStr,
|
|
displayStr
|
|
]));
|
|
end;
|
|
end;
|
|
|
|
|
|
initialization
|
|
|
|
// Registers this reader / writer in fpSpreadsheet
|
|
sfidOpenDocument := RegisterSpreadFormat(sfOpenDocument,
|
|
TsSpreadOpenDocReader, TsSpreadOpenDocWriter,
|
|
STR_FILEFORMAT_OPENDOCUMENT, 'ODS', [STR_OPENDOCUMENT_CALC_EXTENSION]
|
|
);
|
|
|
|
end.
|
|
|