From 71c6eea0fa42c289eb05be6927ebd10baed21aeb Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Fri, 30 Jan 2015 22:47:13 +0000 Subject: [PATCH] fpspreadsheet: Implement cell comments for biff2 and biff5 (reading and writing). Complete cell comments for ods. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3915 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- components/fpspreadsheet/fpsopendocument.pas | 31 ++++- components/fpspreadsheet/fpspreadsheet.pas | 14 ++ components/fpspreadsheet/xlsbiff2.pas | 1 + components/fpspreadsheet/xlsbiff5.pas | 1 + components/fpspreadsheet/xlscommon.pas | 138 +++++++++++++++++++ 5 files changed, 183 insertions(+), 2 deletions(-) diff --git a/components/fpspreadsheet/fpsopendocument.pas b/components/fpspreadsheet/fpsopendocument.pas index c50cd73b3..fb06e2695 100755 --- a/components/fpspreadsheet/fpsopendocument.pas +++ b/components/fpspreadsheet/fpsopendocument.pas @@ -3415,11 +3415,15 @@ var colsSpannedStr: String; rowsSpannedStr: String; spannedStr: String; + comment: String; r1,c1,r2,c2: Cardinal; fmt: TsCellFormat; begin Unused(ARow, ACol); + // Comment + comment := WriteCommentXMLAsString(ACell^.Comment); + // Merged? if FWorksheet.IsMergeBase(ACell) then begin @@ -3434,6 +3438,7 @@ begin if fmt.UsedFormattingFields <> [] then AppendToStream(AStream, Format( '', [ACell^.FormatIndex, spannedStr]), + comment, '') else AppendToStream(AStream, @@ -3449,6 +3454,7 @@ var lStyle, valType: String; r1,c1,r2,c2: Cardinal; rowsSpannedStr, colsSpannedStr, spannedStr: String; + comment: String; strValue: String; displayStr: String; fmt: TsCellFormat; @@ -3463,6 +3469,9 @@ begin else lStyle := ''; + // Comment + comment := WriteCommentXMLAsString(ACell^.Comment); + // Merged? if FWorksheet.IsMergeBase(ACell) then begin @@ -3486,6 +3495,7 @@ begin AppendToStream(AStream, Format( '' + + comment + '%s' + '', [ valType, StrValue, lStyle, spannedStr, @@ -3923,6 +3933,7 @@ var colsSpannedStr: String; rowsSpannedStr: String; spannedStr: String; + comment: String; r1,c1,r2,c2: Cardinal; fmt: TsCellFormat; begin @@ -3935,6 +3946,9 @@ begin else lStyle := ''; + // Comment + comment := WriteCommentXMLAsString(ACell^.Comment); + // Merged? if FWorksheet.IsMergeBase(ACell) then begin @@ -4009,6 +4023,7 @@ begin if ACell^.CalcState=csCalculated then AppendToStream(AStream, Format( '' + + comment + valueStr + '', [ formula, valuetype, value, lStyle, spannedStr @@ -4048,6 +4063,9 @@ begin else lStyle := ''; + // Comment + comment := WriteCommentXMLAsString(ACell^.Comment); + // Merged? if FWorksheet.IsMergeBase(ACell) then begin @@ -4066,8 +4084,6 @@ begin GetCellString(ARow, ACol) ]); - comment := WriteCommentXMLAsString(ACell^.Comment); - // Write it ... AppendToStream(AStream, Format( '' + @@ -4089,6 +4105,7 @@ var colsSpannedStr: String; rowsSpannedStr: String; spannedStr: String; + comment: String; r1,c1,r2,c2: Cardinal; fmt: TsCellFormat; begin @@ -4106,6 +4123,9 @@ begin end else lStyle := ''; + // Comment + comment := WriteCommentXMLAsString(ACell^.Comment); + // Merged? if FWorksheet.IsMergeBase(ACell) then begin @@ -4128,6 +4148,7 @@ begin AppendToStream(AStream, Format( '' + + comment + '%s' + '', [ valType, StrValue, lStyle, spannedStr, @@ -4152,6 +4173,7 @@ var colsSpannedStr: String; rowsSpannedStr: String; spannedStr: String; + comment: String; r1,c1,r2,c2: Cardinal; fmt: TsCellFormat; begin @@ -4173,6 +4195,9 @@ begin else lStyle := ''; + // Comment + comment := WriteCommentXMLAsString(ACell^.Comment); + // nfTimeInterval is a special case - let's handle it first: if (fmt.NumberFormat = nfTimeInterval) then @@ -4181,6 +4206,7 @@ begin displayStr := FormatDateTime(fmt.NumberFormatStr, AValue, [fdoInterval]); AppendToStream(AStream, Format( '' + + comment + '%s' + '', [ strValue, lStyle, spannedStr, @@ -4194,6 +4220,7 @@ begin displayStr := FormatDateTime(fmt.NumberFormatStr, AValue); AppendToStream(AStream, Format( '' + + comment + '%s ' + '', [ DT[isTimeOnly], DT[isTimeOnly], strValue, lStyle, spannedStr, diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index d34e8423c..43ec7d180 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -901,6 +901,8 @@ type {@@ Abstract method for writing a boolean cell. Must be overridden by descendent classes. } procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal; const AValue: Boolean; ACell: PCell); virtual; abstract; + {@@ (Pseudo-)abstract method for writing a cell comment. Must be overridden by descendent classes } + procedure WriteComment(AStream: TStream; ACell: PCell); virtual; {@@ Abstract method for writing a date/time value to a cell. Must be overridden by descendent classes. } procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); virtual; abstract; @@ -8708,6 +8710,8 @@ begin cctUTF8String: WriteLabel(AStream, ACell^.Row, ACell^.Col, ACell^.UTF8StringValue, ACell); end; + if ACell^.Comment <> '' then + WriteComment(AStream, ACell); end; {@@ ---------------------------------------------------------------------------- @@ -8724,6 +8728,16 @@ begin IterateThroughCells(AStream, ACells, WriteCellCallback); end; +{@@ ---------------------------------------------------------------------------- + (Pseudo-) abstract method writing a cell comment to the stream. + Must be overridden by descendents. + + @param ACell Pointer to the cell to be written +-------------------------------------------------------------------------------} +procedure TsCustomSpreadWriter.WriteComment(AStream: TStream; ACell: PCell); +begin +end; + {@@ ---------------------------------------------------------------------------- A generic method to iterate through all cells in a worksheet and call a callback routine for each cell. diff --git a/components/fpspreadsheet/xlsbiff2.pas b/components/fpspreadsheet/xlsbiff2.pas index 881976ea0..d6a51bfef 100755 --- a/components/fpspreadsheet/xlsbiff2.pas +++ b/components/fpspreadsheet/xlsbiff2.pas @@ -531,6 +531,7 @@ begin case RecordType of INT_EXCEL_ID_BLANK : ReadBlank(AStream); INT_EXCEL_ID_BOOLERROR : ReadBool(AStream); + INT_EXCEL_ID_NOTE : ReadComment(AStream); INT_EXCEL_ID_FONT : ReadFont(AStream); INT_EXCEL_ID_FONTCOLOR : ReadFontColor(AStream); INT_EXCEL_ID_FORMAT : ReadFormat(AStream); diff --git a/components/fpspreadsheet/xlsbiff5.pas b/components/fpspreadsheet/xlsbiff5.pas index 91ad3dde2..a342bae23 100755 --- a/components/fpspreadsheet/xlsbiff5.pas +++ b/components/fpspreadsheet/xlsbiff5.pas @@ -390,6 +390,7 @@ begin INT_EXCEL_ID_BLANK : ReadBlank(AStream); INT_EXCEL_ID_BOOLERROR : ReadBool(AStream); INT_EXCEL_ID_MULBLANK : ReadMulBlank(AStream); + INT_EXCEL_ID_NOTE : ReadComment(AStream); INT_EXCEL_ID_NUMBER : ReadNumber(AStream); INT_EXCEL_ID_LABEL : ReadLabel(AStream); INT_EXCEL_ID_RSTRING : ReadRichString(AStream); //(RSTRING) This record stores a formatted text cell (Rich-Text). In BIFF8 it is usually replaced by the LABELSST record. Excel still uses this record, if it copies formatted text cells to the clipboard. diff --git a/components/fpspreadsheet/xlscommon.pas b/components/fpspreadsheet/xlscommon.pas index 430a64dee..d6849050c 100644 --- a/components/fpspreadsheet/xlscommon.pas +++ b/components/fpspreadsheet/xlscommon.pas @@ -23,6 +23,7 @@ uses const { RECORD IDs which didn't change across versions 2-8 } INT_EXCEL_ID_EOF = $000A; + INT_EXCEL_ID_NOTE = $001C; INT_EXCEL_ID_SELECTION = $001D; INT_EXCEL_ID_CONTINUE = $003C; INT_EXCEL_ID_DATEMODE = $0022; @@ -221,6 +222,8 @@ type FDateMode: TDateMode; FPaletteFound: Boolean; FIncompleteCell: PCell; + FIncompleteNote: String; + FIncompleteNoteLength: Word; procedure ApplyCellFormatting(ACell: PCell; XFIndex: Word); virtual; //overload; procedure CreateNumFormatList; override; // Extracts a number out of an RK value @@ -238,6 +241,8 @@ type procedure ReadCodePage(AStream: TStream); // Read column info procedure ReadColInfo(const AStream: TStream); + // Read attached comment + procedure ReadComment(const AStream: TStream); // Figures out what the base year for dates is for this file procedure ReadDateMode(AStream: TStream); // Reads the default column width @@ -320,6 +325,8 @@ type // Writes out column info(s) procedure WriteColInfo(AStream: TStream; ACol: PCol); procedure WriteColInfos(AStream: TStream; ASheet: TsWorksheet); + // Writes out NOTE record(s) + procedure WriteComment(AStream: TStream; ACell: PCell); override; // Writes out DATEMODE record depending on FDateMode procedure WriteDateMode(AStream: TStream); // Writes out a TIME/DATE/TIMETIME @@ -462,6 +469,14 @@ type Value: Double; end; + TBIFF25NoteRecord = packed record + RecordID: Word; + RecordSize: Word; + Row: Word; + Col: Word; + TextLen: Word; + end; + function ConvertExcelDateTimeToDateTime(const AExcelDateNum: Double; ADateMode: TDateMode): TDateTime; begin @@ -882,6 +897,74 @@ begin FWorksheet.WriteColInfo(c, col); end; +// Read a NOTE record which describes an attached comment +// Valid for BIFF2-BIFF5 +procedure TsSpreadBIFFReader.ReadComment(const AStream: TStream); +var + rec: TBIFF25NoteRecord; + r, c: Cardinal; + n: Word; + s: ansiString; + List: TStringList; +begin + rec.Row := 0; // to silence the compiler... + AStream.ReadBuffer(rec.Row, SizeOf(TBIFF25NoteRecord) - 2*SizeOf(Word)); + r := WordLEToN(rec.Row); + c := WordLEToN(rec.Col); + n := WordLEToN(rec.TextLen); + // First NOTE record + if r <> $FFFF then + begin + // entire note is in this record + if n <= self.RecordSize - 3*SizeOf(word) then + begin + SetLength(s, n); + AStream.ReadBuffer(s[1], n); + FIncompleteNote := ''; + FIncompleteNoteLength := 0; + List := TStringList.Create; + try + List.Text := s; // Fix line endings which are #10 in file + s := Copy(List.Text, 1, Length(List.Text) - Length(LineEnding)); + FWorksheet.WriteComment(r, c, s); + finally + List.Free; + end; + end else + // note will be continued in following record(s): Store partial string + begin + FIncompleteNoteLength := n; + n := self.RecordSize - 3*SizeOf(Word); + SetLength(s, n); + AStream.ReadBuffer(s[1], n); + FIncompleteNote := s; + FIncompleteCell := FWorksheet.GetCell(r, c); + end; + end else + // One of the continuation records + begin + SetLength(s, n); + AStream.ReadBuffer(s[1], n); + FIncompleteNote := FIncompleteNote + s; + // last continuation record + if Length(FIncompleteNote) = FIncompleteNoteLength then + begin + List := TStringList.Create; + try + List.Text := FIncompleteNote; // Fix line endings which are #10 in file + s := Copy(List.Text, 1, Length(List.Text) - Length(LineEnding)); + FIncompleteCell^.Comment := s; + finally + List.Free; + end; + FIncompleteNote := ''; + FIncompleteCell := nil; + FIncompleteNoteLength := 0; + end; + end; +end; + + procedure TsSpreadBIFFReader.ReadDateMode(AStream: TStream); var lBaseMode: Word; @@ -1875,6 +1958,61 @@ begin end; end; +{ Writes a NOTE record which describes a comment attached to a cell } +procedure TsSpreadBIFFWriter.WriteComment(AStream: TStream; ACell: PCell); +const + CHUNK_SIZE = 2048; +var + rec: TBIFF25NoteRecord; + L: Integer; + base_size: Word; + p: Integer; + comment: ansistring; + List: TStringList; +begin + Unused(ACell); + + if (ACell^.Comment = '') then + exit; + + List := TStringList.Create; + try + List.Text := UTF8ToAnsi(ACell^.Comment); + comment := List[0]; + for p := 1 to List.Count-1 do + comment := comment + #$0A + List[p]; + finally + List.Free; + end; + + L := Length(comment); + base_size := SizeOf(rec) - 2*SizeOf(word); + + // First NOTE record + rec.RecordID := WordToLE(INT_EXCEL_ID_NOTE); + rec.Row := WordToLE(ACell^.Row); + rec.Col := WordToLE(ACell^.Col); + rec.TextLen := L; + rec.RecordSize := base_size + Min(L, CHUNK_SIZE); + AStream.WriteBuffer(rec, SizeOf(rec)); + AStream.WriteBuffer(comment[1], Min(L, CHUNK_SIZE)); // Write text + + // If the comment text does not fit into 2048 bytes continuation records + // have to be written. + rec.Row := $FFFF; // indicator that this will be a continuation record + rec.Col := 0; + p := CHUNK_SIZE; + dec(L, CHUNK_SIZE); + while L > 0 do begin + rec.TextLen := Min(L, CHUNK_SIZE); + rec.RecordSize := base_size + rec.TextLen; + AStream.WriteBuffer(rec, SizeOf(rec)); + AStream.WriteBuffer(comment[p], rec.TextLen); + dec(L, CHUNK_SIZE); + inc(p, CHUNK_SIZE); + end; +end; + procedure TsSpreadBIFFWriter.WriteDateMode(AStream: TStream); begin { BIFF Record header }