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
This commit is contained in:
wp_xxyyzz
2020-07-29 22:51:39 +00:00
parent 3959cbbe45
commit 7c09abbb51
5 changed files with 213 additions and 22 deletions

View File

@ -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

View File

@ -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
'<dc:description>%s</dc:description>', [s]));
end;
if book.MetaData.Custom.Count > 0 then
begin
for i := 0 to book.Metadata.Custom.Count-1 do
AppendToStream(FSMeta, Format(
'<meta:user-defined meta:name="%s">%s</meta:user-defined>', [
book.Metadata.Custom.Names[i],
book.Metadata.Custom.ValueFromIndex[i]
]));
end;
AppendToStream(FSMeta,
'</office:meta>');
AppendToStream(FSMeta,

View File

@ -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;

View File

@ -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);
{ <CustomDocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
<Comparny dt:dt="string">Disney</Comparny>
<Status dt:dt="string">finished</Status>
</CustomDocumentProperties> }
var
book: TsWorkbook;
i: Integer;
begin
book := TsWorkbook(FWorkbook);
if book.MetaData.Custom.Count = 0 then
exit;
AppendToStream(AStream, INDENT1 +
'<CustomDocumentProperties xmlns="urn:schemas-microsoft-com:office:office">' + LF);
for i := 0 to book.MetaData.Custom.Count-1 do
AppendToStream(AStream, Format(INDENT2 +
'<%0:s dt:dt="string">%1:s</%0:s>' + LF, [
book.MetaData.Custom.Names[i],
book.MetaData.Custom.ValueFromIndex[i]
]));
AppendToStream(AStream, INDENT1 +
'</CustomDocumentProperties>' + 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 +
'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office" />' + LE);
'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office" />' + LF);
exit;
end;
if book.MetaData.Title <> '' then
sTitle := '<Title>' + book.MetaData.Title + '</Title>' + LE + INDENT2
sTitle := '<Title>' + book.MetaData.Title + '</Title>' + LF + INDENT2
else
sTitle := '';
if book.MetaData.Subject <> '' then
sSubject := '<Subject>' + book.MetaData.Subject + '</Subject>' + LE + INDENT2
sSubject := '<Subject>' + book.MetaData.Subject + '</Subject>' + LF + INDENT2
else
sSubject := '';
if book.MetaData.CreatedBy <> '' then
sAuthor := '<Author>' + book.MetaData.CreatedBy + '</Author>' + LE + INDENT2
sAuthor := '<Author>' + book.MetaData.CreatedBy + '</Author>' + LF + INDENT2
else
sAuthor := '';
if book.MetaData.LastModifiedBy <> '' then
sLastAuthor := '<LastAuthor>' + book.MetaData.LastModifiedBy + '</LastAuthor>' + LE + INDENT2
sLastAuthor := '<LastAuthor>' + book.MetaData.LastModifiedBy + '</LastAuthor>' + 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 := '<Created>' + sDateCreated + '</Created>' + LE + INDENT2;
sDateCreated := '<Created>' + sDateCreated + '</Created>' + LF + INDENT2;
end else
sDateCreated := '';
@ -2774,20 +2826,20 @@ begin
begin
dt := book.MetaData.DateLastModified + GetLocalTimeOffset / (24*60);
sDateLastSaved := FormatDateTime(ISO8601FormatExtendedUTC, dt);
sDateLastSaved := '<LastSaved>' + sDateLastSaved + '</LastSaved>' + LE + INDENT2;
sDateLastSaved := '<LastSaved>' + sDateLastSaved + '</LastSaved>' + LF + INDENT2;
end else
sDateLastSaved := '';
AppendToStream(AStream, INDENT1 +
'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">' + LE + INDENT2 +
'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">' + LF + INDENT2 +
sTitle +
sSubject +
sAuthor +
sLastAuthor +
sDateCreated +
sDateLastSaved +
'<Version>16.00</Version>' + LE + Indent1 +
'</DocumentProperties>' + LE
'<Version>16.00</Version>' + LF + INDENT1 +
'</DocumentProperties>' + 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);

View File

@ -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,
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" ' +
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">');
id := 2;
for i := 0 to book.MetaData.Custom.Count-1 do
begin
AppendToStream(AStream, Format(
'<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="%d" name="%s">' +
'<vt:lpwstr>%s</vt:lpwstr>' +
'</property>', [
id, book.MetaData.Custom.Names[i],
book.MetaData.Custom.ValueFromIndex[i]
]));
inc(id);
end;
AppendToStream(AStream,
'</Properties>');
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(
'<Relationships xmlns="%s">' + LineEnding, [SCHEMAS_RELS]));
AppendToStream(FSRelsRels, Format(
'<Relationship Id="rId2" Target="docProps/core.xml" Type="%s" />' + LineEnding,
[SCHEMAS_META_CORE]));
'<Relationships xmlns="%s">' + LineEnding,
[SCHEMAS_RELS]));
AppendToStream(FSRelsRels, Format(
'<Relationship Id="rId1" Target="xl/workbook.xml" Type="%s" />' + LineEnding,
[SCHEMAS_DOCUMENT]));
AppendToStream(FSRelsRels, Format(
'<Relationship Id="rId2" Target="docProps/core.xml" Type="%s" />' + LineEnding,
[SCHEMAS_META_CORE]));
if TsWorkbook(FWorkbook).MetaData.Custom.Count > 0 then
AppendToStream(FSRelsRels, Format(
'<Relationship Id="rId3" Target="docProps/custom.xml" Type="%s" />' + LineEnding,
[SCHEMAS_META_CUSTOM]));
AppendToStream(FSRelsRels,
'</Relationships>');
@ -6378,6 +6455,10 @@ begin
AppendToStream(FSContentTypes,
'<Override PartName="/docProps/core.xml" ContentType="' + MIME_CORE + '" />');
if book.MetaData.Custom.Count > 0 then
AppendToStream(FSContentTypes,
'<Override PartName="/docProps/custom.xml" ContentType="' + MIME_CUSTOM + '" />');
AppendToStream(FSContentTypes,
'</Types>');
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);