From 7a0f60f4eadf2b2befb98d73c2bcafbe77302f18 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Mon, 2 Feb 2015 18:51:13 +0000 Subject: [PATCH] fpspreadsheet: Initial implementation of writing cell comments to xlsx files - not working yet: comments are in file, but do not show up in Excel. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3921 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- components/fpspreadsheet/fpsopendocument.pas | 2 +- components/fpspreadsheet/fpsstreams.pas | 3 +- components/fpspreadsheet/xlsxooxml.pas | 312 +++++++++++++++++-- 3 files changed, 286 insertions(+), 31 deletions(-) diff --git a/components/fpspreadsheet/fpsopendocument.pas b/components/fpspreadsheet/fpsopendocument.pas index 45c667f88..ca8ec3fe4 100755 --- a/components/fpspreadsheet/fpsopendocument.pas +++ b/components/fpspreadsheet/fpsopendocument.pas @@ -3048,7 +3048,7 @@ begin Result := ''; if AComment = '' then exit; - result := ''; + result := ''; err := false; L := TStringList.Create; try diff --git a/components/fpspreadsheet/fpsstreams.pas b/components/fpspreadsheet/fpsstreams.pas index 8e25e4069..b7ddbdbab 100644 --- a/components/fpspreadsheet/fpsstreams.pas +++ b/components/fpspreadsheet/fpsstreams.pas @@ -54,7 +54,8 @@ uses { Resets the stream position to the beginning of the stream. } procedure ResetStream(var AStream: TStream); begin - AStream.Position := 0; + if AStream <> nil then + AStream.Position := 0; end; {@@ diff --git a/components/fpspreadsheet/xlsxooxml.pas b/components/fpspreadsheet/xlsxooxml.pas index 7e77797f8..e4efd3d8a 100755 --- a/components/fpspreadsheet/xlsxooxml.pas +++ b/components/fpspreadsheet/xlsxooxml.pas @@ -103,12 +103,18 @@ type { TsSpreadOOXMLWriter } TsSpreadOOXMLWriter = class(TsCustomSpreadWriter) + private + procedure WriteCommentsCallback(ACell: PCell; AStream: TStream); + procedure WriteVmlDrawingsCallback(ACell: PCell; AStream: TStream); + protected FDateMode: TDateMode; FPointSeparatorSettings: TFormatSettings; FSharedStringsCount: Integer; FFillList: array of PsCellFormat; FBorderList: array of PsCellFormat; + FDrawingCounter: Integer; + FNumCommentsOnSheet: Integer; protected { Helper routines } procedure CreateNumFormatList; override; @@ -123,6 +129,8 @@ type procedure ResetStreams; procedure WriteBorderList(AStream: TStream); procedure WriteCols(AStream: TStream; AWorksheet: TsWorksheet); + procedure WriteComments(AWorksheet: TsWorksheet); + procedure WriteDimension(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteFillList(AStream: TStream); procedure WriteFontList(AStream: TStream); procedure WriteMergedCells(AStream: TStream; AWorksheet: TsWorksheet); @@ -131,6 +139,9 @@ type procedure WriteSheetData(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteSheetViews(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteStyleList(AStream: TStream; ANodeName: String); + procedure WriteVmlDrawings(AWorksheet: TsWorksheet); + procedure WriteWorksheet(AWorksheet: TsWorksheet); + procedure WriteWorksheetRels(AWorksheet: TsWorksheet); protected { Streams with the contents of files } FSContentTypes: TStream; @@ -141,12 +152,15 @@ type FSSharedStrings: TStream; FSSharedStrings_complete: TStream; FSSheets: array of TStream; + FSSheetRels: array of TStream; + FSComments: array of TStream; + FSVmlDrawings: array of TStream; FCurSheetNum: Integer; protected { Routines to write the files } - procedure WriteGlobalFiles; procedure WriteContent; - procedure WriteWorksheet(AWorksheet: TsWorksheet); + procedure WriteContentTypes; + procedure WriteGlobalFiles; protected { Record writing methods } //todo: add WriteDate @@ -199,6 +213,7 @@ const OOXML_PATH_XL_STRINGS = 'xl/sharedStrings.xml'; OOXML_PATH_XL_WORKSHEETS = 'xl/worksheets/'; OOXML_PATH_XL_WORKSHEETS_RELS = 'xl/worksheets/_rels/'; + OOXML_PATH_XL_DRAWINGS = 'xl/drawings/'; OOXML_PATH_XL_THEME = 'xl/theme/theme1.xml'; { OOXML schemas constants } @@ -209,11 +224,10 @@ const 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'; + SCHEMAS_COMMENTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments'; + SCHEMAS_DRAWINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing'; SCHEMAS_SPREADML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; - { OOXML relationship type constants } - OOXML_RELTYPE_COMMENTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments'; - { OOXML mime types constants } {%H-}MIME_XML = 'application/xml'; MIME_RELS = 'application/vnd.openxmlformats-package.relationships+xml'; @@ -222,6 +236,8 @@ const MIME_WORKSHEET = MIME_SPREADML + '.worksheet+xml'; MIME_STYLES = MIME_SPREADML + '.styles+xml'; MIME_STRINGS = MIME_SPREADML + '.sharedStrings+xml'; + MIME_COMMENTS = MIME_SPREADML + '.comments+xml'; + MIME_VMLDRAWING = MIME_SPREADML + '.vmlDrawing'; LAST_PALETTE_COLOR = $3F; // 63 @@ -445,7 +461,7 @@ begin begin nodeName := ANode.NodeName; s := GetAttrValue(ANode, 'Type'); - if s = OOXML_RELTYPE_COMMENTS then + if s = SCHEMAS_COMMENTS then begin Result := ExtractFileName(GetAttrValue(ANode, 'Target')); exit; @@ -937,7 +953,6 @@ var s: String; r, c: Cardinal; comment: String; - list: TStringList; begin comment := ''; node := ANode.FirstChild; @@ -947,6 +962,7 @@ begin cellAddr := GetAttrValue(node, 'ref'); if cellAddr <> '' then begin + comment := ''; txtNode := node.FirstChild; while txtNode <> nil do begin @@ -968,21 +984,12 @@ begin if (comment <> '') and ParseCellString(cellAddr, r, c) then begin // Fix line endings // #10 --> "LineEnding" comment := UTF8StringReplace(comment, #10, LineEnding, [rfReplaceAll]); - { - list := TStringList.Create; - try - list.Text := comment; - comment := Copy(list.Text, 1, Length(list.Text) - Length(LineEnding)); - finally - list.Free; - end; - } AWorksheet.WriteComment(r, c, comment); end; txtNode := txtNode.NextSibling; end; - node := node.NextSibling; end; + node := node.NextSibling; end; end; @@ -1809,6 +1816,76 @@ begin ''); end; +procedure TsSpreadOOXMLWriter.WriteComments(AWorksheet: TsWorksheet); +begin + // Create the comments stream + SetLength(FSComments, FCurSheetNum + 1); + if (boBufStream in Workbook.Options) then + FSComments[FCurSheetNum] := TBufStream.Create(GetTempFileName('', Format('fpsCMNT%d', [FCurSheetNum]))) + else + FSComments[FCurSheetNum] := TMemoryStream.Create; + + // Header + AppendToStream(FSComments[FCurSheetNum], + XML_HEADER); + AppendToStream(FSComments[FCurSheetNum], Format( + '', [SCHEMAS_SPREADML])); + AppendToStream(FSComments[FCurSheetNum], + ''+ + ''+ + ''); + AppendToStream(FSComments[FCurSheetNum], + ''); + + // Comments + IterateThroughCells(FSComments[FCurSheetNum], AWorksheet.Cells, WriteCommentsCallback); + + // Footer + AppendToStream(FSComments[FCurSheetNum], + ''); + AppendToStream(FSComments[FCurSheetNum], + ''); +end; + +procedure TsSpreadOOXMLWriter.WriteCommentsCallback(ACell: PCell; + AStream: TStream); +var + comment: String; +begin + if (ACell = nil) or (ACell^.Comment = '') then + exit; + + comment := ACell^.Comment; + ValidXMLText(comment); + + // Write comment to Comments stream + AppendToStream(AStream, Format( + '', [GetCellString(ACell^.Row, ACell^.Col)])); + AppendToStream(AStream, + ''+ + ''+ + ''+ comment + '' + + ''+ + ''); + AppendToStream(AStream, + ''); +end; + +procedure TsSpreadOOXMLWriter.WriteDimension(AStream: TStream; + AWorksheet: TsWorksheet); +var + r1,c1,r2,c2: Cardinal; + dim: String; +begin + GetSheetDimensions(AWorksheet, r1, r2, c1, c2); + if (r1=r2) and (c1=c2) then + dim := GetCellString(r1, c1) + else + dim := GetCellRangeString(r1, c1, r2, c2); + AppendToStream(AStream, Format( + '', [dim])); +end; + procedure TsSpreadOOXMLWriter.WriteFillList(AStream: TStream); var i: Integer; @@ -2057,8 +2134,10 @@ begin lCell.Row := r; lCell.Col := c; AVLNode := AWorksheet.Cells.Find(@lCell); - if Assigned(AVLNode) then + if Assigned(AVLNode) then begin WriteCellCallback(PCell(AVLNode.Data), AStream); + if PCell(AVLNode.Data)^.Comment <> '' then inc(FNumCommentsOnSheet); + end; end; AppendToStream(AStream, ''); @@ -2246,10 +2325,105 @@ begin '', [ANodeName])); end; +procedure TsSpreadOOXMLWriter.WriteVmlDrawings(AWorksheet: TsWorksheet); +begin + SetLength(FSVmlDrawings, FCurSheetNum + 1); + if (boBufStream in Workbook.Options) then + FSVmlDrawings[FCurSheetNum] := TBufStream.Create(GetTempFileName('', Format('fpsVMLD%d', [FCurSheetNum]))) + else + FSVmlDrawings[FCurSheetNum] := TMemoryStream.Create; + + FDrawingCounter := 0; + + // Header + AppendToStream(FSVmlDrawings[FCurSheetNum], + '' + LineEnding); + // My xml viewer does not format vml files property --> format in code. + AppendToStream(FSVmlDrawings[FCurSheetNum], + ' '+LineEnding+{Format(} + ' ', [FCurSheetNum+1]) + LineEnding + + ' ' + LineEnding); + AppendToStream(FSVmlDrawings[FCurSheetNum], + ' '+LineEnding+ + ' '+LineEnding+ + ' '+LineEnding+ + ' ' + LineEnding); + + // Write vmlDrawings for each comment (formatting and position of comment box) + IterateThroughCells(FSVmlDrawings[FCurSheetNum], AWorksheet.Cells, WriteVmlDrawingsCallback); + + // Footer + AppendToStream(FSVmlDrawings[FCurSheetNum], + ''); +end; + +procedure TsSpreadOOXMLWriter.WriteVmlDrawingsCallback(ACell: PCell; + AStream: TStream); +var + id: Integer; +begin +// id := (FCurSheetNum+1) * 1024 + ACell^.Col + ACell^.Row; + id := 1025 + FDrawingCounter; // if more than 1024 comments then use data="1,2,etc" above! -- not implemented yet + // My xml viewer does not format vml files property --> format in code. + AppendToStream(AStream, LineEnding + Format( + ' '+ LineEnding + + ' '+LineEnding+ + ' '+LineEnding+ + ' '+LineEnding+ + ' '+LineEnding+ + '
'+LineEnding+ + '
' + LineEnding + + ' '+LineEnding+ + ' '+LineEnding+ + ' '+LineEnding+ + ' 1, 15, 0, 2, 2, 79, 4, 4'+LineEnding+ + ' False'+LineEnding + Format( + ' %d', [ACell^.Row]) + LineEnding + Format( + ' %d', [ACell^.Col]) + LineEnding + + ' '+ LineEnding+ + '
' + LineEnding); + inc(FDrawingCounter); +end; + +procedure TsSpreadOOXMLWriter.WriteWorksheetRels(AWorksheet: TsWorksheet); +begin + // Create stream + SetLength(FSSheetRels, FCurSheetNum + 1); + if (boBufStream in Workbook.Options) then + FSSheetRels[FCurSheetNum] := TBufStream.Create(GetTempFileName('', Format('fpsWSR%d', [FCurSheetNum]))) + else + FSSheetRels[FCurSheetNum] := TMemoryStream.Create; + + // Header + AppendToStream(FSSheetRels[FCurSheetNum], + XML_HEADER); + AppendToStream(FSSheetRels[FCurSheetNum], Format( + '', [SCHEMAS_RELS])); + // Relationships + AppendToStream(FSSheetRels[FCurSheetNum], Format( + '', + [SCHEMAS_COMMENTS, FCurSheetNum+1])); + AppendToStream(FSSheetRels[FCurSheetNum], Format( + '', + [SCHEMAS_DRAWINGS, FCurSheetNum+1])); + // Footer + AppendToStream(FSSheetRels[FCurSheetNum], + ''); +end; + procedure TsSpreadOOXMLWriter.WriteGlobalFiles; var i: Integer; begin + (* { --- Content Types --- } AppendToStream(FSContentTypes, XML_HEADER); @@ -2273,7 +2447,7 @@ begin ''); AppendToStream(FSContentTypes, ''); - +*) { --- RelsRels --- } AppendToStream(FSRelsRels, XML_HEADER); @@ -2347,7 +2521,7 @@ begin for i:=1 to Workbook.GetWorksheetCount do AppendToStream(FSWorkbookRels, Format( '', - [SCHEMAS_WORKSHEET, i, i+2])); + [SCHEMAS_WORKSHEET, i, i+2])); // +2 because of styles.xml and sharedStrings.xml AppendToStream(FSWorkbookRels, ''); @@ -2381,9 +2555,19 @@ begin // Preparation for shared strings FSharedStringsCount := 0; - // Write all worksheets which fills also the shared strings + // Write all worksheets which fills also the shared strings. + // Also: write comments and related files for i := 0 to Workbook.GetWorksheetCount - 1 do - WriteWorksheet(Workbook.GetWorksheetByIndex(i)); + begin + FWorksheet := Workbook.GetWorksheetByIndex(i); + WriteWorksheet(FWorksheet); + if FNumCommentsOnSheet <> 0 then + begin + WriteComments(FWorksheet); + WriteVmlDrawings(FWorksheet); + WriteWorksheetRels(FWorksheet); + end; + end; // Finalization of the shared strings document AppendToStream(FSSharedStrings_complete, @@ -2396,12 +2580,53 @@ begin ''); end; +procedure TsSpreadOOXMLWriter.WriteContentTypes; +var + i: Integer; +begin + AppendToStream(FSContentTypes, + XML_HEADER); + AppendToStream(FSContentTypes, + ''); + (* + AppendToStream(FSContentTypes, + ''); + AppendToStream(FSContentTypes, + ''); + *) + AppendToStream(FSContentTypes, Format( + '', [MIME_RELS])); + AppendToStream(FSContentTypes, Format( + '', [MIME_XML])); + AppendToStream(FSContentTypes, Format( + '', [MIME_VMLDRAWING])); + + AppendToStream(FSContentTypes, + ''); + + for i:=1 to Workbook.GetWorksheetCount do + AppendToStream(FSContentTypes, Format( + '', + [i, MIME_WORKSHEET])); + + for i:=1 to Length(FSComments) do + AppendToStream(FSContentTypes, Format( + '', + [i, MIME_COMMENTS])); + + AppendToStream(FSContentTypes, + ''); + AppendToStream(FSContentTypes, + ''); + AppendToStream(FSContentTypes, + ''); +end; + procedure TsSpreadOOXMLWriter.WriteWorksheet(AWorksheet: TsWorksheet); begin - FWorksheet := AWorksheet; - FCurSheetNum := Length(FSSheets); SetLength(FSSheets, FCurSheetNum + 1); + FNumCommentsOnSheet := 0; // Create the stream if (boBufStream in Workbook.Options) then @@ -2415,12 +2640,16 @@ begin AppendToStream(FSSheets[FCurSheetNum], Format( '', [SCHEMAS_SPREADML, SCHEMAS_DOC_RELS])); + WriteDimension(FSSheets[FCurSheetNum], AWorksheet); WriteSheetViews(FSSheets[FCurSheetNum], AWorksheet); WriteCols(FSSheets[FCurSheetNum], AWorksheet); WriteSheetData(FSSheets[FCurSheetNum], AWorksheet); WriteMergedCells(FSSheets[FCurSheetNum], AWorksheet); // Footer + if FNumCommentsOnSheet > 0 then + AppendToStream(FSSheets[FCurSheetNum], + ''); AppendToStream(FSSheets[FCurSheetNum], ''); end; @@ -2497,6 +2726,12 @@ begin DestroyStream(FSSharedStrings_complete); for stream in FSSheets do DestroyStream(stream); SetLength(FSSheets, 0); + for stream in FSComments do DestroyStream(stream); + SetLength(FSComments, 0); + for stream in FSSheetRels do DestroyStream(stream); + SetLength(FSSheetRels, 0); + for stream in FSVmlDrawings do DestroyStream(stream); + SetLength(FSVmlDrawings, 0); end; { Prepares a string formula for writing } @@ -2510,7 +2745,7 @@ end; { Is called before zipping the individual file parts. Rewinds the streams. } procedure TsSpreadOOXMLWriter.ResetStreams; var - i: Integer; + stream: TStream; begin ResetStream(FSContentTypes); ResetStream(FSRelsRels); @@ -2518,8 +2753,10 @@ begin ResetStream(FSWorkbook); ResetStream(FSStyles); ResetStream(FSSharedStrings_complete); - for i := 0 to High(FSSheets) do - ResetStream(FSSheets[i]); + for stream in FSSheets do ResetStream(stream); + for stream in FSSheetRels do ResetStream(stream); + for stream in FSComments do ResetStream(stream); + for stream in FSVmlDrawings do ResetStream(stream); end; { @@ -2564,6 +2801,7 @@ procedure TsSpreadOOXMLWriter.WriteToStream(AStream: TStream); var FZip: TZipper; i: Integer; + stream: TStream; begin { Analyze the workbook and collect all information needed } ListAllNumFormats; @@ -2576,6 +2814,7 @@ begin { Fill the streams with the contents of the files } WriteGlobalFiles; WriteContent; + WriteContentTypes; // Stream positions must be at beginning, they were moved to end during adding of xml strings. ResetStreams; @@ -2591,9 +2830,24 @@ begin FZip.Entries.AddFileEntry(FSStyles, OOXML_PATH_XL_STYLES); FZip.Entries.AddFileEntry(FSSharedStrings_complete, OOXML_PATH_XL_STRINGS); - for i := 0 to Length(FSSheets) - 1 do begin + for i:=0 to High(FSSheets) do begin FSSheets[i].Position:= 0; - FZip.Entries.AddFileEntry(FSSheets[i], OOXML_PATH_XL_WORKSHEETS + 'sheet' + IntToStr(i + 1) + '.xml'); + FZip.Entries.AddFileEntry(FSSheets[i], OOXML_PATH_XL_WORKSHEETS + Format('sheet%d.xml', [i+1])); + end; + + for i:=0 to High(FSComments) do begin + FSComments[i].Position := 0; + FZip.Entries.AddFileEntry(FSComments[i], OOXML_PATH_XL + Format('comments%d.xml', [i+1])); + end; + + for i:=0 to High(FSSheetRels) do begin + FSSheetRels[i].Position := 0; + FZip.Entries.AddFileEntry(FSSheetRels[i], OOXML_PATH_XL_WORKSHEETS_RELS + Format('sheet%d.xml.rels', [i+1])); + end; + + for i:=0 to High(FSVmlDrawings) do begin + FSVmlDrawings[i].Position := 0; + FZip.Entries.AddFileEntry(FSVmlDrawings[i], OOXML_PATH_XL_DRAWINGS + Format('vmlDrawing%d.vml', [i+1])); end; FZip.SaveToStream(AStream);