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
This commit is contained in:
wp_xxyyzz
2015-01-30 22:47:13 +00:00
parent cf5ece6e86
commit 71c6eea0fa
5 changed files with 183 additions and 2 deletions

View File

@ -3415,11 +3415,15 @@ var
colsSpannedStr: String; colsSpannedStr: String;
rowsSpannedStr: String; rowsSpannedStr: String;
spannedStr: String; spannedStr: String;
comment: String;
r1,c1,r2,c2: Cardinal; r1,c1,r2,c2: Cardinal;
fmt: TsCellFormat; fmt: TsCellFormat;
begin begin
Unused(ARow, ACol); Unused(ARow, ACol);
// Comment
comment := WriteCommentXMLAsString(ACell^.Comment);
// Merged? // Merged?
if FWorksheet.IsMergeBase(ACell) then if FWorksheet.IsMergeBase(ACell) then
begin begin
@ -3434,6 +3438,7 @@ begin
if fmt.UsedFormattingFields <> [] then if fmt.UsedFormattingFields <> [] then
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell table:style-name="ce%d" %s>', [ACell^.FormatIndex, spannedStr]), '<table:table-cell table:style-name="ce%d" %s>', [ACell^.FormatIndex, spannedStr]),
comment,
'</table:table-cell>') '</table:table-cell>')
else else
AppendToStream(AStream, AppendToStream(AStream,
@ -3449,6 +3454,7 @@ var
lStyle, valType: String; lStyle, valType: String;
r1,c1,r2,c2: Cardinal; r1,c1,r2,c2: Cardinal;
rowsSpannedStr, colsSpannedStr, spannedStr: String; rowsSpannedStr, colsSpannedStr, spannedStr: String;
comment: String;
strValue: String; strValue: String;
displayStr: String; displayStr: String;
fmt: TsCellFormat; fmt: TsCellFormat;
@ -3463,6 +3469,9 @@ begin
else else
lStyle := ''; lStyle := '';
// Comment
comment := WriteCommentXMLAsString(ACell^.Comment);
// Merged? // Merged?
if FWorksheet.IsMergeBase(ACell) then if FWorksheet.IsMergeBase(ACell) then
begin begin
@ -3486,6 +3495,7 @@ begin
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell office:value-type="%s" office:boolean-value="%s" %s %s >' + '<table:table-cell office:value-type="%s" office:boolean-value="%s" %s %s >' +
comment +
'<text:p>%s</text:p>' + '<text:p>%s</text:p>' +
'</table:table-cell>', [ '</table:table-cell>', [
valType, StrValue, lStyle, spannedStr, valType, StrValue, lStyle, spannedStr,
@ -3923,6 +3933,7 @@ var
colsSpannedStr: String; colsSpannedStr: String;
rowsSpannedStr: String; rowsSpannedStr: String;
spannedStr: String; spannedStr: String;
comment: String;
r1,c1,r2,c2: Cardinal; r1,c1,r2,c2: Cardinal;
fmt: TsCellFormat; fmt: TsCellFormat;
begin begin
@ -3935,6 +3946,9 @@ begin
else else
lStyle := ''; lStyle := '';
// Comment
comment := WriteCommentXMLAsString(ACell^.Comment);
// Merged? // Merged?
if FWorksheet.IsMergeBase(ACell) then if FWorksheet.IsMergeBase(ACell) then
begin begin
@ -4009,6 +4023,7 @@ begin
if ACell^.CalcState=csCalculated then if ACell^.CalcState=csCalculated then
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell table:formula="=%s" office:value-type="%s" %s %s %s>' + '<table:table-cell table:formula="=%s" office:value-type="%s" %s %s %s>' +
comment +
valueStr + valueStr +
'</table:table-cell>', [ '</table:table-cell>', [
formula, valuetype, value, lStyle, spannedStr formula, valuetype, value, lStyle, spannedStr
@ -4048,6 +4063,9 @@ begin
else else
lStyle := ''; lStyle := '';
// Comment
comment := WriteCommentXMLAsString(ACell^.Comment);
// Merged? // Merged?
if FWorksheet.IsMergeBase(ACell) then if FWorksheet.IsMergeBase(ACell) then
begin begin
@ -4066,8 +4084,6 @@ begin
GetCellString(ARow, ACol) GetCellString(ARow, ACol)
]); ]);
comment := WriteCommentXMLAsString(ACell^.Comment);
// Write it ... // Write it ...
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell office:value-type="string" %s %s>' + '<table:table-cell office:value-type="string" %s %s>' +
@ -4089,6 +4105,7 @@ var
colsSpannedStr: String; colsSpannedStr: String;
rowsSpannedStr: String; rowsSpannedStr: String;
spannedStr: String; spannedStr: String;
comment: String;
r1,c1,r2,c2: Cardinal; r1,c1,r2,c2: Cardinal;
fmt: TsCellFormat; fmt: TsCellFormat;
begin begin
@ -4106,6 +4123,9 @@ begin
end else end else
lStyle := ''; lStyle := '';
// Comment
comment := WriteCommentXMLAsString(ACell^.Comment);
// Merged? // Merged?
if FWorksheet.IsMergeBase(ACell) then if FWorksheet.IsMergeBase(ACell) then
begin begin
@ -4128,6 +4148,7 @@ begin
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell office:value-type="%s" office:value="%s" %s %s >' + '<table:table-cell office:value-type="%s" office:value="%s" %s %s >' +
comment +
'<text:p>%s</text:p>' + '<text:p>%s</text:p>' +
'</table:table-cell>', [ '</table:table-cell>', [
valType, StrValue, lStyle, spannedStr, valType, StrValue, lStyle, spannedStr,
@ -4152,6 +4173,7 @@ var
colsSpannedStr: String; colsSpannedStr: String;
rowsSpannedStr: String; rowsSpannedStr: String;
spannedStr: String; spannedStr: String;
comment: String;
r1,c1,r2,c2: Cardinal; r1,c1,r2,c2: Cardinal;
fmt: TsCellFormat; fmt: TsCellFormat;
begin begin
@ -4173,6 +4195,9 @@ begin
else else
lStyle := ''; lStyle := '';
// Comment
comment := WriteCommentXMLAsString(ACell^.Comment);
// nfTimeInterval is a special case - let's handle it first: // nfTimeInterval is a special case - let's handle it first:
if (fmt.NumberFormat = nfTimeInterval) then if (fmt.NumberFormat = nfTimeInterval) then
@ -4181,6 +4206,7 @@ begin
displayStr := FormatDateTime(fmt.NumberFormatStr, AValue, [fdoInterval]); displayStr := FormatDateTime(fmt.NumberFormatStr, AValue, [fdoInterval]);
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell office:value-type="time" office:time-value="%s" %s %s>' + '<table:table-cell office:value-type="time" office:time-value="%s" %s %s>' +
comment +
'<text:p>%s</text:p>' + '<text:p>%s</text:p>' +
'</table:table-cell>', [ '</table:table-cell>', [
strValue, lStyle, spannedStr, strValue, lStyle, spannedStr,
@ -4194,6 +4220,7 @@ begin
displayStr := FormatDateTime(fmt.NumberFormatStr, AValue); displayStr := FormatDateTime(fmt.NumberFormatStr, AValue);
AppendToStream(AStream, Format( AppendToStream(AStream, Format(
'<table:table-cell office:value-type="%s" office:%s-value="%s" %s %s>' + '<table:table-cell office:value-type="%s" office:%s-value="%s" %s %s>' +
comment +
'<text:p>%s</text:p> ' + '<text:p>%s</text:p> ' +
'</table:table-cell>', [ '</table:table-cell>', [
DT[isTimeOnly], DT[isTimeOnly], strValue, lStyle, spannedStr, DT[isTimeOnly], DT[isTimeOnly], strValue, lStyle, spannedStr,

View File

@ -901,6 +901,8 @@ type
{@@ Abstract method for writing a boolean cell. Must be overridden by descendent classes. } {@@ Abstract method for writing a boolean cell. Must be overridden by descendent classes. }
procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal; procedure WriteBool(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: Boolean; ACell: PCell); virtual; abstract; 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. } {@@ 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; procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: TDateTime; ACell: PCell); virtual; abstract; const AValue: TDateTime; ACell: PCell); virtual; abstract;
@ -8708,6 +8710,8 @@ begin
cctUTF8String: cctUTF8String:
WriteLabel(AStream, ACell^.Row, ACell^.Col, ACell^.UTF8StringValue, ACell); WriteLabel(AStream, ACell^.Row, ACell^.Col, ACell^.UTF8StringValue, ACell);
end; end;
if ACell^.Comment <> '' then
WriteComment(AStream, ACell);
end; end;
{@@ ---------------------------------------------------------------------------- {@@ ----------------------------------------------------------------------------
@ -8724,6 +8728,16 @@ begin
IterateThroughCells(AStream, ACells, WriteCellCallback); IterateThroughCells(AStream, ACells, WriteCellCallback);
end; 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 A generic method to iterate through all cells in a worksheet and call a callback
routine for each cell. routine for each cell.

View File

@ -531,6 +531,7 @@ begin
case RecordType of case RecordType of
INT_EXCEL_ID_BLANK : ReadBlank(AStream); INT_EXCEL_ID_BLANK : ReadBlank(AStream);
INT_EXCEL_ID_BOOLERROR : ReadBool(AStream); INT_EXCEL_ID_BOOLERROR : ReadBool(AStream);
INT_EXCEL_ID_NOTE : ReadComment(AStream);
INT_EXCEL_ID_FONT : ReadFont(AStream); INT_EXCEL_ID_FONT : ReadFont(AStream);
INT_EXCEL_ID_FONTCOLOR : ReadFontColor(AStream); INT_EXCEL_ID_FONTCOLOR : ReadFontColor(AStream);
INT_EXCEL_ID_FORMAT : ReadFormat(AStream); INT_EXCEL_ID_FORMAT : ReadFormat(AStream);

View File

@ -390,6 +390,7 @@ begin
INT_EXCEL_ID_BLANK : ReadBlank(AStream); INT_EXCEL_ID_BLANK : ReadBlank(AStream);
INT_EXCEL_ID_BOOLERROR : ReadBool(AStream); INT_EXCEL_ID_BOOLERROR : ReadBool(AStream);
INT_EXCEL_ID_MULBLANK : ReadMulBlank(AStream); INT_EXCEL_ID_MULBLANK : ReadMulBlank(AStream);
INT_EXCEL_ID_NOTE : ReadComment(AStream);
INT_EXCEL_ID_NUMBER : ReadNumber(AStream); INT_EXCEL_ID_NUMBER : ReadNumber(AStream);
INT_EXCEL_ID_LABEL : ReadLabel(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. 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.

View File

@ -23,6 +23,7 @@ uses
const const
{ RECORD IDs which didn't change across versions 2-8 } { RECORD IDs which didn't change across versions 2-8 }
INT_EXCEL_ID_EOF = $000A; INT_EXCEL_ID_EOF = $000A;
INT_EXCEL_ID_NOTE = $001C;
INT_EXCEL_ID_SELECTION = $001D; INT_EXCEL_ID_SELECTION = $001D;
INT_EXCEL_ID_CONTINUE = $003C; INT_EXCEL_ID_CONTINUE = $003C;
INT_EXCEL_ID_DATEMODE = $0022; INT_EXCEL_ID_DATEMODE = $0022;
@ -221,6 +222,8 @@ type
FDateMode: TDateMode; FDateMode: TDateMode;
FPaletteFound: Boolean; FPaletteFound: Boolean;
FIncompleteCell: PCell; FIncompleteCell: PCell;
FIncompleteNote: String;
FIncompleteNoteLength: Word;
procedure ApplyCellFormatting(ACell: PCell; XFIndex: Word); virtual; //overload; procedure ApplyCellFormatting(ACell: PCell; XFIndex: Word); virtual; //overload;
procedure CreateNumFormatList; override; procedure CreateNumFormatList; override;
// Extracts a number out of an RK value // Extracts a number out of an RK value
@ -238,6 +241,8 @@ type
procedure ReadCodePage(AStream: TStream); procedure ReadCodePage(AStream: TStream);
// Read column info // Read column info
procedure ReadColInfo(const AStream: TStream); 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 // Figures out what the base year for dates is for this file
procedure ReadDateMode(AStream: TStream); procedure ReadDateMode(AStream: TStream);
// Reads the default column width // Reads the default column width
@ -320,6 +325,8 @@ type
// Writes out column info(s) // Writes out column info(s)
procedure WriteColInfo(AStream: TStream; ACol: PCol); procedure WriteColInfo(AStream: TStream; ACol: PCol);
procedure WriteColInfos(AStream: TStream; ASheet: TsWorksheet); 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 // Writes out DATEMODE record depending on FDateMode
procedure WriteDateMode(AStream: TStream); procedure WriteDateMode(AStream: TStream);
// Writes out a TIME/DATE/TIMETIME // Writes out a TIME/DATE/TIMETIME
@ -462,6 +469,14 @@ type
Value: Double; Value: Double;
end; end;
TBIFF25NoteRecord = packed record
RecordID: Word;
RecordSize: Word;
Row: Word;
Col: Word;
TextLen: Word;
end;
function ConvertExcelDateTimeToDateTime(const AExcelDateNum: Double; function ConvertExcelDateTimeToDateTime(const AExcelDateNum: Double;
ADateMode: TDateMode): TDateTime; ADateMode: TDateMode): TDateTime;
begin begin
@ -882,6 +897,74 @@ begin
FWorksheet.WriteColInfo(c, col); FWorksheet.WriteColInfo(c, col);
end; 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); procedure TsSpreadBIFFReader.ReadDateMode(AStream: TStream);
var var
lBaseMode: Word; lBaseMode: Word;
@ -1875,6 +1958,61 @@ begin
end; end;
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); procedure TsSpreadBIFFWriter.WriteDateMode(AStream: TStream);
begin begin
{ BIFF Record header } { BIFF Record header }