From 7c09abbb51e68675395193a0118a463a686eba73 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Wed, 29 Jul 2020 22:51:39 +0000 Subject: [PATCH] fpspreadsheet: Add custom meta data support. Reading/writing for XLSX, Excel XML, ODS. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@7591 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../examples/other/metadata/demo_metadata.lpr | 10 +- .../source/common/fpsopendocument.pas | 17 ++++ .../fpspreadsheet/source/common/fpstypes.pas | 30 +++++- .../fpspreadsheet/source/common/xlsxml.pas | 84 ++++++++++++++--- .../fpspreadsheet/source/common/xlsxooxml.pas | 94 ++++++++++++++++++- 5 files changed, 213 insertions(+), 22 deletions(-) diff --git a/components/fpspreadsheet/examples/other/metadata/demo_metadata.lpr b/components/fpspreadsheet/examples/other/metadata/demo_metadata.lpr index 50fb7ad07..7ad702e35 100644 --- a/components/fpspreadsheet/examples/other/metadata/demo_metadata.lpr +++ b/components/fpspreadsheet/examples/other/metadata/demo_metadata.lpr @@ -58,6 +58,7 @@ end; var book: TsWorkbook; sheet: TsWorksheet; + i: Integer; begin book := TsWorkbook.Create; try @@ -72,6 +73,8 @@ begin book.MetaData.Comments.Add('Assign the creation date to the field CreatedAt.'); book.MetaData.Keywords.Add('Test'); book.MetaData.Keywords.Add('FPSpreadsheet'); + book.MetaData.AddCustom('Comparny', 'Disney'); + book.MetaData.AddCustom('Status', 'finished'); sheet := book.AddWorksheet('Test'); sheet.WriteText(2, 3, 'abc'); @@ -86,9 +89,9 @@ begin book := TsWorkbook.Create; try // Select one of these - book.ReadFromFile('test.ods'); +// book.ReadFromFile('test.ods'); // book.ReadFromFile('test.xlsx'); -// book.ReadFromFile('test.xml'); + book.ReadFromFile('test.xml'); WriteLn('Created by : ', book.MetaData.CreatedBy); WriteLn('Date created : ', DateTimeToStr(book.MetaData.DateCreated)); WriteLn('Modified by : ', book.MetaData.LastModifiedBy); @@ -96,6 +99,9 @@ begin WriteLn('Title : ', book.MetaData.Title); WriteLn('Subject : ', book.MetaData.Subject); WriteLn('Keywords : ', book.MetaData.Keywords.CommaText); + WriteLn('Custom : ', 'Name':20, 'Value':20); + for i := 0 to book.MetaData.Custom.Count-1 do + WriteLn(' ', book.MetaData.Custom.Names[i]:20, book.MetaData.Custom.ValueFromIndex[i]:20); WriteLn('Comments: '); WriteLn(book.MetaData.Comments.Text); finally diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index 0d658a528..6d2fe7ba4 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -1966,6 +1966,7 @@ var book: TsWorkbook; nodeName: String; s: String; + name: String; begin book := TsWorkbook(FWorkbook); @@ -1992,6 +1993,12 @@ begin book.MetaData.Title := s; 'dc:subject': book.Metadata.Subject := s; + 'meta:user-defined': + begin + name := GetAttrValue(ANode, 'meta:name'); + if name <> '' then + book.MetaData.AddCustom(name, s); + end; end; ANode := ANode.NextSibling; end; @@ -5911,6 +5918,16 @@ begin '%s', [s])); end; + if book.MetaData.Custom.Count > 0 then + begin + for i := 0 to book.Metadata.Custom.Count-1 do + AppendToStream(FSMeta, Format( + '%s', [ + book.Metadata.Custom.Names[i], + book.Metadata.Custom.ValueFromIndex[i] + ])); + end; + AppendToStream(FSMeta, ''); AppendToStream(FSMeta, diff --git a/components/fpspreadsheet/source/common/fpstypes.pas b/components/fpspreadsheet/source/common/fpstypes.pas index 20e6afb0b..447fa00c3 100644 --- a/components/fpspreadsheet/source/common/fpstypes.pas +++ b/components/fpspreadsheet/source/common/fpstypes.pas @@ -976,9 +976,12 @@ type FSubject: String; FComments: TStrings; FKeywords: TStrings; + FCustom: TStrings; public constructor Create; destructor Destroy; override; + function AddCustom(AName, AValue: String): Integer; + procedure Clear; function IsEmpty: Boolean; property CreatedBy: String read FCreatedBy write FCreatedBy; property LastModifiedBy: String read FLastModifiedBy write FLastModifiedBy; @@ -987,6 +990,7 @@ type property Subject: String read FSubject write FSubject; property Title: String read FTitle write FTitle; property Comments: TStrings read FComments write FComments; + property Custom: TStrings read FCustom write FCustom; property Keywords: TStrings read FKeywords write FKeywords; end; @@ -1199,20 +1203,44 @@ begin inherited; FComments := TStringList.Create; FKeywords := TStringList.Create; + FCustom := TStringList.Create; end; destructor TsMetaData.Destroy; begin FComments.Free; FKeywords.Free; + FCustom.Free; inherited; end; +procedure TsMetaData.Clear; +begin + FTitle := ''; + FSubject := ''; + FCreatedBy := ''; + FLastModifiedBy := ''; + FDateCreated := 0; + FDateLastModified := 0; + FComments.Clear; + FKeywords.Clear; + FCustom.Clear; +end; + +function TsMetaData.AddCustom(AName, AValue: String): Integer; +begin + Result := FCustom.IndexOf(AName); + if result > -1 then + FCustom.ValueFromIndex[Result] := AValue + else + Result := FCustom.Add(AName + '=' + AValue); +end; + function TsMetaData.IsEmpty: Boolean; begin Result := (FCreatedBy = '') and (FLastModifiedBy = '') and (FTitle = '') and (FSubject = '') and - (FComments.Count = 0) and (FKeywords.Count = 0) and + (FComments.Count = 0) and (FKeywords.Count = 0) and (FCustom.Count = 0) and (FDateCreated = 0) and (FDateLastModified = 0); end; diff --git a/components/fpspreadsheet/source/common/xlsxml.pas b/components/fpspreadsheet/source/common/xlsxml.pas index f43d6540e..f1c4f06b3 100644 --- a/components/fpspreadsheet/source/common/xlsxml.pas +++ b/components/fpspreadsheet/source/common/xlsxml.pas @@ -45,6 +45,7 @@ type procedure ReadCellProtection(ANode: TDOMNode; var AFormat: TsCellFormat); procedure ReadComment(ANode: TDOMNode; AWorksheet: TsBasicWorksheet; ACell: PCell); procedure ReadConditionalFormatting(ANode: TDOMNode; AWorksheet: TsBasicWorksheet); + procedure ReadCustomDocumentProperties(ANode: TDOMNode); procedure ReadDocumentProperties(ANode: TDOMNode); procedure ReadExcelWorkbook(ANode: TDOMNode); procedure ReadFont(ANode: TDOMNode; var AFormat: TsCellFormat); @@ -96,6 +97,7 @@ type procedure WriteConditionalFormat(AStream: TStream; AWorksheet: TsBasicWorksheet; AFormat: TsConditionalFormat); procedure WriteConditionalFormatting(AStream: TStream; AWorksheet: TsBasicWorksheet); + procedure WriteCustomDocumentProperties(AStream: TStream); procedure WriteDocumentProperties(AStream: TStream); procedure WriteExcelWorkbook(AStream: TStream); procedure WriteNames(AStream: TStream; AWorksheet: TsBasicWorksheet); @@ -1100,6 +1102,32 @@ begin sheet.WriteConditionalCellFormat(range, TsCFCondition(condition), op1, op2, fmtIndex); end; +{@@ ---------------------------------------------------------------------------- + Read the custom meta data fields +-------------------------------------------------------------------------------} +procedure TsSpreadExcelXMLReader.ReadCustomDocumentProperties(ANode: TDOMNode); +var + book: TsWorkbook; + value: String; + nodeName: String; +begin + if ANode = nil then + exit; + + book := TsWorkbook(FWorkbook); + ANode := ANode.FirstChild; + while ANode <> nil do + begin + nodeName := ANode.NodeName; + if nodeName <> '#text' then + begin + value := GetNodeValue(ANode); + book.MetaData.AddCustom(nodeName, value); + end; + ANode := ANode.NextSibling; + end; +end; + {@@ ---------------------------------------------------------------------------- Reads the meta data etc. -------------------------------------------------------------------------------} @@ -2069,6 +2097,7 @@ begin // Read meta data ReadDocumentProperties(doc.DocumentElement.FindNode('DocumentProperties')); + ReadCustomDocumentProperties(doc.DocumentElement.FindNode('CustomDocumentProperties')); // Read style list ReadStyles(doc.DocumentElement.FindNode('Styles')); @@ -2680,6 +2709,33 @@ begin end; end; +procedure TsSpreadExcelXMLWriter.WriteCustomDocumentProperties(AStream: TStream); +{ + Disney + finished + } +var + book: TsWorkbook; + i: Integer; +begin + book := TsWorkbook(FWorkbook); + if book.MetaData.Custom.Count = 0 then + exit; + + AppendToStream(AStream, INDENT1 + + '' + LF); + + for i := 0 to book.MetaData.Custom.Count-1 do + AppendToStream(AStream, Format(INDENT2 + + '<%0:s dt:dt="string">%1:s' + LF, [ + book.MetaData.Custom.Names[i], + book.MetaData.Custom.ValueFromIndex[i] + ])); + + AppendToStream(AStream, INDENT1 + + '' + LF); +end; + procedure TsSpreadExcelXMLWriter.WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); var @@ -2719,8 +2775,6 @@ begin end; procedure TsSpreadExcelXMLWriter.WriteDocumentProperties(AStream: TStream); -const - LE = LineEnding; var sTitle: String; sSubject: String; @@ -2733,32 +2787,30 @@ var begin book := TsWorkbook(FWorkbook); - if (book.MetaData.Title = '') and - (book.MetaData.CreatedBy = '') and (book.MetaData.LastModifiedBy = '') and - (book.MetaData.DateCreated <= 0) and (book.MetaData.DateLastModified <= 0) then + if book.MetaData.IsEmpty then begin AppendToStream(AStream, INDENT1 + - '' + LE); + '' + LF); exit; end; if book.MetaData.Title <> '' then - sTitle := '' + book.MetaData.Title + '' + LE + INDENT2 + sTitle := '' + book.MetaData.Title + '' + LF + INDENT2 else sTitle := ''; if book.MetaData.Subject <> '' then - sSubject := '' + book.MetaData.Subject + '' + LE + INDENT2 + sSubject := '' + book.MetaData.Subject + '' + LF + INDENT2 else sSubject := ''; if book.MetaData.CreatedBy <> '' then - sAuthor := '' + book.MetaData.CreatedBy + '' + LE + INDENT2 + sAuthor := '' + book.MetaData.CreatedBy + '' + LF + INDENT2 else sAuthor := ''; if book.MetaData.LastModifiedBy <> '' then - sLastAuthor := '' + book.MetaData.LastModifiedBy + '' + LE + INDENT2 + sLastAuthor := '' + book.MetaData.LastModifiedBy + '' + LF + INDENT2 else sLastAuthor := ''; @@ -2766,7 +2818,7 @@ begin if book.MetaData.DateCreated > 0 then begin dt := book.MetaData.DateCreated + GetLocalTimeOffset / (24*60); sDateCreated := FormatDateTime(ISO8601FormatExtendedUTC, dt); - sDateCreated := '' + sDateCreated + '' + LE + INDENT2; + sDateCreated := '' + sDateCreated + '' + LF + INDENT2; end else sDateCreated := ''; @@ -2774,20 +2826,20 @@ begin begin dt := book.MetaData.DateLastModified + GetLocalTimeOffset / (24*60); sDateLastSaved := FormatDateTime(ISO8601FormatExtendedUTC, dt); - sDateLastSaved := '' + sDateLastSaved + '' + LE + INDENT2; + sDateLastSaved := '' + sDateLastSaved + '' + LF + INDENT2; end else sDateLastSaved := ''; AppendToStream(AStream, INDENT1 + - '' + LE + INDENT2 + + '' + LF + INDENT2 + sTitle + sSubject + sAuthor + sLastAuthor + sDateCreated + sDateLastSaved + - '16.00' + LE + Indent1 + - '' + LE + '16.00' + LF + INDENT1 + + '' + LF ); end; @@ -3397,9 +3449,11 @@ begin ' xmlns:o="urn:schemas-microsoft-com:office:office"' + LF + ' xmlns:x="urn:schemas-microsoft-com:office:excel"' + LF + ' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"' + LF + + ' xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"' + LF + ' xmlns:html="http://www.w3.org/TR/REC-html40">' + LF); WriteDocumentProperties(AStream); + WriteCustomDocumentProperties(AStream); WriteOfficeDocumentSettings(AStream); WriteExcelWorkbook(AStream); WriteStyles(AStream); diff --git a/components/fpspreadsheet/source/common/xlsxooxml.pas b/components/fpspreadsheet/source/common/xlsxooxml.pas index a9702d7ee..0292176b4 100644 --- a/components/fpspreadsheet/source/common/xlsxooxml.pas +++ b/components/fpspreadsheet/source/common/xlsxooxml.pas @@ -173,6 +173,7 @@ type procedure WriteComments(AWorksheet: TsBasicWorksheet); procedure WriteConditionalFormat(AStream: TStream; AFormat: TsConditionalFormat; var APriority: Integer); procedure WriteConditionalFormats(AStream: TStream; AWorksheet: TsBasicWorksheet); + procedure WriteCustomMetaData(AStream: TStream); procedure WriteDefinedNames(AStream: TStream); procedure WriteDifferentialFormat(AStream: TStream; AFormat: PsCellFormat); procedure WriteDifferentialFormats(AStream: TStream); @@ -217,6 +218,7 @@ type FSWorkbook: TStream; FSWorkbookRels: TStream; FSMetaData: TStream; + FSCustomMetaData: TStream; FSStyles: TStream; FSSharedStrings: TStream; FSSharedStrings_complete: TStream; @@ -307,6 +309,7 @@ const OOXML_PATH_XL_THEME = 'xl/theme/theme1.xml'; OOXML_PATH_XL_MEDIA = 'xl/media/'; OOXML_PATH_DOCPROPS_CORE = 'docProps/core.xml'; + OOXML_PATH_DOCPROPS_CUSTOM = 'docProps/custom.xml'; { OOXML schemas constants } SCHEMAS_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types'; @@ -314,6 +317,7 @@ const SCHEMAS_DOC_RELS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; SCHEMAS_DOCUMENT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument'; SCHEMAS_META_CORE = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties'; + SCHEMAS_META_CUSTOM = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties'; SCHEMAS_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; SCHEMAS_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'; SCHEMAS_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'; @@ -330,6 +334,7 @@ const MIME_RELS = 'application/vnd.openxmlformats-package.relationships+xml'; MIME_OFFICEDOCUMENT = 'application/vnd.openxmlformats-officedocument'; MIME_CORE = 'application/vnd.openxmlformats-package.core-properties+xml'; + MIME_CUSTOM = 'application/vnd.openxmlformats-officedocument.custom-properties+xml'; MIME_SPREADML = MIME_OFFICEDOCUMENT + '.spreadsheetml'; MIME_SHEET = MIME_SPREADML + '.sheet.main+xml'; MIME_WORKSHEET = MIME_SPREADML + '.worksheet+xml'; @@ -2695,9 +2700,11 @@ end; procedure TsSpreadOOXMLReader.ReadMetaData(ANode: TDOMNode); var + childNode: TDOMNode; nodeName: string; book: TsWorkbook; s: String; + name: String; dt: TDateTime; fs: TFormatSettings; begin @@ -2714,6 +2721,7 @@ begin nodeName := ANode.NodeName; s := GetNodeValue(ANode); case nodeName of + // These fields are from "core.xml" 'dc:title': book.MetaData.Title := s; 'dc:subject': @@ -2740,6 +2748,21 @@ begin 'dcterms:modified': if s <> '' then book.MetaData.DateLastModified :=ISO8601StrToDateTime(s); + + // This field is from "custom.xml" + 'property': + begin + name := GetAttrValue(ANode, 'name'); + childNode := ANode.Firstchild; + while childNode <> nil do + begin + nodeName := childNode.NodeName; + s := GetNodeValue(childNode); + if (s <> '') then + book.MetaData.AddCustom(name, s); + break; + end; + end; end; ANode := ANode.NextSibling; end; @@ -3798,6 +3821,18 @@ begin finally XMLStream.Free; end; + // custom meta data + XMLStream := CreateXMLStream; + try + if UnzipToStream(AStream, OOXML_PATH_DOCPROPS_CUSTOM, XMLStream) then + begin + ReadXMLStream(Doc, XMLStream); + ReadMetaData(Doc.DocumentElement); + FreeAndNil(Doc); + end; + finally + XMLStream.Free; + end; finally FreeAndNil(Doc); @@ -4516,6 +4551,37 @@ begin end; end; +procedure TsSpreadOOXMLWriter.WriteCustomMetaData(AStream: TStream); +var + book: TsWorkbook; + i: Integer; + id: Integer; +begin + book := TsWorkbook(FWorkbook); + if book.MetaData.Custom.Count = 0 then + exit; + + AppendToStream(AStream, + ''); + + id := 2; + for i := 0 to book.MetaData.Custom.Count-1 do + begin + AppendToStream(AStream, Format( + '' + + '%s' + + '', [ + id, book.MetaData.Custom.Names[i], + book.MetaData.Custom.ValueFromIndex[i] + ])); + inc(id); + end; + + AppendToStream(AStream, + ''); +end; + procedure TsSpreadOOXMLWriter.WriteDimension(AStream: TStream; AWorksheet: TsBasicWorksheet); var @@ -6084,18 +6150,29 @@ begin { --- meta data ---- } WriteMetaData(FSMetaData); + WriteCustomMetaData(FSCustomMetaData); { --- _rels/.rels --- } AppendToStream(FSRelsRels, XML_HEADER + LineEnding); + AppendToStream(FSRelsRels, Format( - '' + LineEnding, [SCHEMAS_RELS])); - AppendToStream(FSRelsRels, Format( - '' + LineEnding, - [SCHEMAS_META_CORE])); + '' + LineEnding, + [SCHEMAS_RELS])); + AppendToStream(FSRelsRels, Format( '' + LineEnding, [SCHEMAS_DOCUMENT])); + + AppendToStream(FSRelsRels, Format( + '' + LineEnding, + [SCHEMAS_META_CORE])); + + if TsWorkbook(FWorkbook).MetaData.Custom.Count > 0 then + AppendToStream(FSRelsRels, Format( + '' + LineEnding, + [SCHEMAS_META_CUSTOM])); + AppendToStream(FSRelsRels, ''); @@ -6378,6 +6455,10 @@ begin AppendToStream(FSContentTypes, ''); + if book.MetaData.Custom.Count > 0 then + AppendToStream(FSContentTypes, + ''); + AppendToStream(FSContentTypes, ''); end; @@ -6808,6 +6889,7 @@ begin FSSharedStrings := CreateTempStream(FWorkbook, 'fpsSS'); FSSharedStrings_complete := CreateTempStream(FWorkbook, 'fpsSSC'); FSMetaData := CreateTempStream(FWorkbook, 'fpsMETA'); + FSCustomMetaData := CreateTempStream(FWorkbook, 'fpsCM'); { if boFileStream in FWorkbook.Options then begin @@ -6848,6 +6930,7 @@ procedure TsSpreadOOXMLWriter.DestroyStreams; var stream: TStream; begin + DestroyTempStream(FSCustomMetaData); DestroyTempStream(FSMetaData); DestroyTempStream(FSContentTypes); DestroyTempStream(FSRelsRels); @@ -6897,6 +6980,7 @@ begin ResetStream(FSStyles); ResetStream(FSSharedStrings_complete); ResetStream(FSMetaData); + ResetStream(FSCustomMetaData); for i:=0 to High(FSSheets) do ResetStream(FSSheets[i]); for i:=0 to High(FSSheetRels) do ResetStream(FSSheetRels[i]); for i:=0 to High(FSComments) do ResetStream(FSComments[i]); @@ -6964,6 +7048,8 @@ begin if FSSharedStrings_complete.Size > 0 then FZip.Entries.AddFileEntry(FSSharedStrings_complete, OOXML_PATH_XL_STRINGS); FZip.Entries.AddFileEntry(FSMetaData, OOXML_PATH_DOCPROPS_CORE); + if TsWorkbook(FWorkbook).MetaData.Custom.Count > 0 then + FZip.Entries.AddFileEntry(FSCustomMetaData, OOXML_PATH_DOCPROPS_CUSTOM); // Write embedded images WriteMedia(FZip);