You've already forked lazarus-ccr
fpspreadsheet: Move some more translatable strings to fpsStrings unit. Some clean-up & cosmetics.
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3640 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
@ -194,7 +194,7 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
StrUtils, Variants, fpsStreams, fpsExprParser;
|
StrUtils, Variants, fpsStrings, fpsStreams, fpsExprParser;
|
||||||
|
|
||||||
const
|
const
|
||||||
{ OpenDocument general XML constants }
|
{ OpenDocument general XML constants }
|
||||||
@ -2680,82 +2680,6 @@ begin
|
|||||||
'</office:body>' +
|
'</office:body>' +
|
||||||
'</office:document-content>'
|
'</office:document-content>'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(*
|
|
||||||
lNumFmtCode := WriteNumFormatsXMLAsString;
|
|
||||||
|
|
||||||
lColStylesCode := WriteColStylesXMLAsString;
|
|
||||||
if lColStylesCode = '' then lColStylesCode :=
|
|
||||||
' <style:style style:name="co1" style:family="table-column">' + LineEnding +
|
|
||||||
' <style:table-column-properties fo:break-before="auto" style:column-width="2.267cm"/>' + LineEnding +
|
|
||||||
' </style:style>' + LineEnding;
|
|
||||||
|
|
||||||
lRowStylesCode := WriteRowStylesXMLAsString;
|
|
||||||
if lRowStylesCode = '' then lRowStylesCode :=
|
|
||||||
' <style:style style:name="ro1" style:family="table-row">' + LineEnding +
|
|
||||||
' <style:table-row-properties style:row-height="0.416cm" fo:break-before="auto" style:use-optimal-row-height="true"/>' + LineEnding +
|
|
||||||
' </style:style>' + LineEnding;
|
|
||||||
|
|
||||||
lCellStylesCode := WriteCellStylesXMLAsString;
|
|
||||||
|
|
||||||
FContent :=
|
|
||||||
XML_HEADER + LineEnding +
|
|
||||||
'<office:document-content xmlns:office="' + SCHEMAS_XMLNS_OFFICE +
|
|
||||||
'" xmlns:fo="' + SCHEMAS_XMLNS_FO +
|
|
||||||
'" xmlns:style="' + SCHEMAS_XMLNS_STYLE +
|
|
||||||
'" xmlns:text="' + SCHEMAS_XMLNS_TEXT +
|
|
||||||
'" xmlns:table="' + SCHEMAS_XMLNS_TABLE +
|
|
||||||
'" xmlns:svg="' + SCHEMAS_XMLNS_SVG +
|
|
||||||
'" xmlns:number="' + SCHEMAS_XMLNS_NUMBER +
|
|
||||||
'" xmlns:meta="' + SCHEMAS_XMLNS_META +
|
|
||||||
'" xmlns:chart="' + SCHEMAS_XMLNS_CHART +
|
|
||||||
'" xmlns:dr3d="' + SCHEMAS_XMLNS_DR3D +
|
|
||||||
'" xmlns:math="' + SCHEMAS_XMLNS_MATH +
|
|
||||||
'" xmlns:form="' + SCHEMAS_XMLNS_FORM +
|
|
||||||
'" xmlns:script="' + SCHEMAS_XMLNS_SCRIPT +
|
|
||||||
'" xmlns:ooo="' + SCHEMAS_XMLNS_OOO +
|
|
||||||
'" xmlns:ooow="' + SCHEMAS_XMLNS_OOOW +
|
|
||||||
'" xmlns:oooc="' + SCHEMAS_XMLNS_OOOC +
|
|
||||||
'" xmlns:dom="' + SCHEMAS_XMLNS_DOM +
|
|
||||||
'" xmlns:xforms="' + SCHEMAS_XMLNS_XFORMS +
|
|
||||||
'" xmlns:xsd="' + SCHEMAS_XMLNS_XSD +
|
|
||||||
'" xmlns:xsi="' + SCHEMAS_XMLNS_XSI + '">' + LineEnding +
|
|
||||||
' <office:scripts />' + LineEnding +
|
|
||||||
|
|
||||||
// Fonts
|
|
||||||
' <office:font-face-decls>' + LineEnding +
|
|
||||||
' ' + WriteFontNamesXMLAsString + LineEnding +
|
|
||||||
// ' <style:font-face style:name="Arial" svg:font-family="Arial" xmlns:v="urn:schemas-microsoft-com:vml" />' + LineEnding +
|
|
||||||
' </office:font-face-decls>' + LineEnding +
|
|
||||||
|
|
||||||
// Automatic styles
|
|
||||||
' <office:automatic-styles>' + LineEnding +
|
|
||||||
lNumFmtCode +
|
|
||||||
lColStylesCode +
|
|
||||||
lRowStylesCode +
|
|
||||||
' <style:style style:name="ta1" style:family="table" style:master-page-name="Default">' + LineEnding +
|
|
||||||
' <style:table-properties table:display="true" style:writing-mode="lr-tb"/>' + LineEnding +
|
|
||||||
' </style:style>' + LineEnding +
|
|
||||||
|
|
||||||
// Automatically Generated Styles
|
|
||||||
lCellStylesCode +
|
|
||||||
' </office:automatic-styles>' + LineEnding +
|
|
||||||
|
|
||||||
// Body
|
|
||||||
' <office:body>' + LineEnding +
|
|
||||||
' <office:spreadsheet>' + LineEnding;
|
|
||||||
|
|
||||||
// Write all worksheets
|
|
||||||
for i := 0 to Workbook.GetWorksheetCount - 1 do
|
|
||||||
WriteWorksheet(Workbook.GetWorksheetByIndex(i));
|
|
||||||
|
|
||||||
FContent := FContent +
|
|
||||||
' </office:spreadsheet>' + LineEnding +
|
|
||||||
' </office:body>' + LineEnding +
|
|
||||||
'</office:document-content>';
|
|
||||||
*)
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TsSpreadOpenDocWriter.WriteWorksheet(AStream: TStream;
|
procedure TsSpreadOpenDocWriter.WriteWorksheet(AStream: TStream;
|
||||||
@ -2892,7 +2816,7 @@ begin
|
|||||||
break;
|
break;
|
||||||
end;
|
end;
|
||||||
if stylename = '' then
|
if stylename = '' then
|
||||||
raise Exception.Create('Column style not found.');
|
raise Exception.Create(rsColumnStyleNotFound);
|
||||||
|
|
||||||
// Determine value for "number-columns-repeated"
|
// Determine value for "number-columns-repeated"
|
||||||
colsRepeated := 1;
|
colsRepeated := 1;
|
||||||
@ -3008,7 +2932,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
if styleName = '' then
|
if styleName = '' then
|
||||||
raise Exception.Create('Row style not found.');
|
raise Exception.Create(rsRowStyleNotFound);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Take care of empty rows above the first row
|
// Take care of empty rows above the first row
|
||||||
@ -3250,15 +3174,6 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
(*
|
|
||||||
procedure TsSpreadOpenDocWriter.WriteToStream(AStream: TStream);
|
|
||||||
begin
|
|
||||||
Unused(AStream);
|
|
||||||
// Not supported at the moment
|
|
||||||
raise Exception.Create('TsSpreadOpenDocWriter.WriteToStream not supported');
|
|
||||||
end;
|
|
||||||
*)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
Writes an empty cell
|
Writes an empty cell
|
||||||
}
|
}
|
||||||
@ -3381,8 +3296,6 @@ begin
|
|||||||
Workbook.GetPaletteColorAsHTMLStr(AFormat.BorderStyles[cbDiagDown].Color)
|
Workbook.GetPaletteColorAsHTMLStr(AFormat.BorderStyles[cbDiagDown].Color)
|
||||||
]);
|
]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TsSpreadOpenDocWriter.WriteDefaultFontXMLAsString: String;
|
function TsSpreadOpenDocWriter.WriteDefaultFontXMLAsString: String;
|
||||||
@ -3582,7 +3495,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
if styleName = '' then
|
if styleName = '' then
|
||||||
raise Exception.Create('Row style not found.');
|
raise Exception.Create(rsRowStyleNotFound);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// No empty rows allowed here for the moment!
|
// No empty rows allowed here for the moment!
|
||||||
@ -3761,7 +3674,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>' +
|
||||||
valueStr +
|
valueStr +
|
||||||
'</table:table-cell>', [
|
'</table:table-cell>', [
|
||||||
formula, valuetype, value, lStyle, spannedStr
|
formula, valuetype, value, lStyle, spannedStr
|
||||||
]))
|
]))
|
||||||
@ -3813,7 +3726,7 @@ begin
|
|||||||
str := AValue;
|
str := AValue;
|
||||||
if not ValidXMLText(str) then
|
if not ValidXMLText(str) then
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(
|
||||||
'Invalid character(s) in cell %s.', [
|
rsInvalidCharacterInCell, [
|
||||||
GetCellString(ARow, ACol)
|
GetCellString(ARow, ACol)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -3879,13 +3792,6 @@ begin
|
|||||||
valType, StrValue, lStyle, spannedStr,
|
valType, StrValue, lStyle, spannedStr,
|
||||||
DisplayStr
|
DisplayStr
|
||||||
]));
|
]));
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
'<table:table-cell office:value-type="' + valType + '" office:value="' + StrValue + '"' + lStyle + '>' +
|
|
||||||
'<text:p>' + DisplayStr + '</text:p>' +
|
|
||||||
'</table:table-cell>');
|
|
||||||
}
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{*******************************************************************
|
{*******************************************************************
|
||||||
|
@ -5945,9 +5945,9 @@ begin
|
|||||||
if valid then
|
if valid then
|
||||||
WriteToFile(AFileName, SheetType, AOverwriteExisting)
|
WriteToFile(AFileName, SheetType, AOverwriteExisting)
|
||||||
else
|
else
|
||||||
raise Exception.Create(Format(
|
raise Exception.Create(Format(rsInvalidExtension, [
|
||||||
'[TsWorkbook.WriteToFile] Attempt to save a spreadsheet by extension, ' +
|
ExtractFileExt(AFileName)
|
||||||
'but the extension %s is not valid.', [ExtractFileExt(AFileName)]));
|
]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{@@ ----------------------------------------------------------------------------
|
{@@ ----------------------------------------------------------------------------
|
||||||
@ -6643,7 +6643,8 @@ end;
|
|||||||
@param AColorIndex Palette index of the color to be replaced
|
@param AColorIndex Palette index of the color to be replaced
|
||||||
@param AColorValue Number containing the rgb components of the new color
|
@param AColorValue Number containing the rgb components of the new color
|
||||||
-------------------------------------------------------------------------------}
|
-------------------------------------------------------------------------------}
|
||||||
procedure TsWorkbook.SetPaletteColor(AColorIndex: TsColor; AColorValue: TsColorValue);
|
procedure TsWorkbook.SetPaletteColor(AColorIndex: TsColor;
|
||||||
|
AColorValue: TsColorValue);
|
||||||
begin
|
begin
|
||||||
if (AColorIndex >= 0) and (AColorIndex < GetPaletteSize) then
|
if (AColorIndex >= 0) and (AColorIndex < GetPaletteSize) then
|
||||||
begin
|
begin
|
||||||
|
@ -21,6 +21,8 @@ resourcestring
|
|||||||
rsTooManyPaletteColors = 'This workbook contains more colors (%d) than ' +
|
rsTooManyPaletteColors = 'This workbook contains more colors (%d) than ' +
|
||||||
'supported by the file format (%d). The additional colors are replaced by '+
|
'supported by the file format (%d). The additional colors are replaced by '+
|
||||||
'the best-matching palette colors.';
|
'the best-matching palette colors.';
|
||||||
|
rsInvalidExtension = 'Attempting to save a spreadsheet by extension, ' +
|
||||||
|
'but the extension %s is not valid.';
|
||||||
rsInvalidFontIndex = 'Invalid font index';
|
rsInvalidFontIndex = 'Invalid font index';
|
||||||
rsInvalidNumberFormat = 'Trying to use an incompatible number format.';
|
rsInvalidNumberFormat = 'Trying to use an incompatible number format.';
|
||||||
rsInvalidDateTimeFormat = 'Trying to use an incompatible date/time format.';
|
rsInvalidDateTimeFormat = 'Trying to use an incompatible date/time format.';
|
||||||
@ -33,8 +35,21 @@ resourcestring
|
|||||||
rsCircularReference = 'Circular reference found when calculating worksheet formulas';
|
rsCircularReference = 'Circular reference found when calculating worksheet formulas';
|
||||||
rsFileNotFound = 'File "%s" not found.';
|
rsFileNotFound = 'File "%s" not found.';
|
||||||
rsInvalidWorksheetName = '"%s" is not a valid worksheet name.';
|
rsInvalidWorksheetName = '"%s" is not a valid worksheet name.';
|
||||||
|
rsDefectiveInternalStructure = 'Defective internal structure of %s file.';
|
||||||
|
rsUnknownDataType = 'Unknown data type.';
|
||||||
|
rsUnknownErrorType = 'Unknown error type.';
|
||||||
|
rsTruncateTooLongCellText = 'Text value exceeds %d character limit in cell %s '+
|
||||||
|
'and has been truncated.';
|
||||||
|
rsColumnStyleNotFound = 'Column style not found.';
|
||||||
|
rsRowStyleNotFound = 'Row style not found.';
|
||||||
|
rsInvalidCharacterInCell = 'Invalid character(s) in cell %s.';
|
||||||
|
rsUTF8TextExpectedButANSIFoundInCell = 'Expected UTF8 text but probably ANSI '+
|
||||||
|
'text found in cell %s.';
|
||||||
|
rsIndexInSSTOutOfRange = 'Index %d in SST out of range (0-%d).';
|
||||||
|
|
||||||
rsTRUE = 'TRUE';
|
|
||||||
|
|
||||||
|
rsTRUE = 'TRUE'; // wp: Do we really want to translate these strings?
|
||||||
rsFALSE = 'FALSE';
|
rsFALSE = 'FALSE';
|
||||||
rsErrEmptyIntersection = '#NULL!';
|
rsErrEmptyIntersection = '#NULL!';
|
||||||
rsErrDivideByZero = '#DIV/0!';
|
rsErrDivideByZero = '#DIV/0!';
|
||||||
|
@ -107,9 +107,13 @@ type
|
|||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{ TsWikiTableNumFormatList }
|
uses
|
||||||
|
fpsStrings;
|
||||||
|
|
||||||
|
|
||||||
|
{ TsWikiTableNumFormatList }
|
||||||
|
//...
|
||||||
|
|
||||||
{ TWikiTableTokenizer }
|
{ TWikiTableTokenizer }
|
||||||
|
|
||||||
constructor TWikiTableTokenizer.Create;
|
constructor TWikiTableTokenizer.Create;
|
||||||
@ -456,8 +460,7 @@ begin
|
|||||||
|
|
||||||
// Check for invalid characters
|
// Check for invalid characters
|
||||||
if not ValidXMLText(lCurStr, false) then
|
if not ValidXMLText(lCurStr, false) then
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(rsInvalidCharacterInCell, [
|
||||||
'Invalid character(s) in cell %s.', [
|
|
||||||
GetCellString(i, j)
|
GetCellString(i, j)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ var
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Math, fpsNumFormatParser;
|
Math, fpsStrings, fpsNumFormatParser;
|
||||||
|
|
||||||
const
|
const
|
||||||
{ Excel record IDs }
|
{ Excel record IDs }
|
||||||
@ -559,7 +559,6 @@ var
|
|||||||
XF: Word;
|
XF: Word;
|
||||||
ok: Boolean;
|
ok: Boolean;
|
||||||
formulaResult: Double = 0.0;
|
formulaResult: Double = 0.0;
|
||||||
// rpnFormula: TsRPNFormula;
|
|
||||||
Data: array [0..7] of byte;
|
Data: array [0..7] of byte;
|
||||||
dt: TDateTime;
|
dt: TDateTime;
|
||||||
nf: TsNumberFormat;
|
nf: TsNumberFormat;
|
||||||
@ -1010,7 +1009,7 @@ begin
|
|||||||
|
|
||||||
// Carefully check the index
|
// Carefully check the index
|
||||||
if (lIndex < 0) or (lIndex > Length(FFormattingStyles)) then
|
if (lIndex < 0) or (lIndex > Length(FFormattingStyles)) then
|
||||||
raise Exception.Create('[TsSpreadBIFF2Writer.WriteXFIndex] Invalid Index, this should not happen!');
|
raise Exception.Create('[TsSpreadBIFF2Writer.WriteXFIndex] Invalid index, this should not happen!');
|
||||||
Result := FFormattingStyles[lIndex].Row;
|
Result := FFormattingStyles[lIndex].Row;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@ -1155,17 +1154,6 @@ begin
|
|||||||
|
|
||||||
{ Write out }
|
{ Write out }
|
||||||
AStream.WriteBuffer(rec, SizeOf(rec));
|
AStream.WriteBuffer(rec, SizeOf(rec));
|
||||||
|
|
||||||
(*
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_COLWIDTH)); // BIFF record header
|
|
||||||
AStream.WriteWord(WordToLE(4)); // Record size
|
|
||||||
AStream.WriteByte(ACol^.Col); // start column
|
|
||||||
AStream.WriteByte(ACol^.Col); // end column
|
|
||||||
{ calculate width to be in units of 1/256 of pixel width of character "0" }
|
|
||||||
w := round(ACol^.Width * 256);
|
|
||||||
AStream.WriteWord(WordToLE(w)); // write width
|
|
||||||
*)
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -1211,7 +1199,6 @@ begin
|
|||||||
AStream.WriteBuffer(rec, SizeOf(rec));
|
AStream.WriteBuffer(rec, SizeOf(rec));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
Writes an Excel 2 IXFE record
|
Writes an Excel 2 IXFE record
|
||||||
This record contains the "real" XF index if it is > 62.
|
This record contains the "real" XF index if it is > 62.
|
||||||
@ -1554,16 +1541,6 @@ begin
|
|||||||
|
|
||||||
{ Clean up }
|
{ Clean up }
|
||||||
SetLength(buf, 0);
|
SetLength(buf, 0);
|
||||||
|
|
||||||
(*
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMAT));
|
|
||||||
AStream.WriteWord(WordToLE(1 + len));
|
|
||||||
|
|
||||||
{ Format string }
|
|
||||||
AStream.WriteByte(len); // AnsiString, char count in 1 byte
|
|
||||||
AStream.WriteBuffer(s[1], len); // String data
|
|
||||||
*)
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TsSpreadBIFF2Writer.WriteFormatCount(AStream: TStream);
|
procedure TsSpreadBIFF2Writer.WriteFormatCount(AStream: TStream);
|
||||||
@ -1639,7 +1616,6 @@ begin
|
|||||||
{ Write following STRING record if formula result is a non-empty string }
|
{ Write following STRING record if formula result is a non-empty string }
|
||||||
if (ACell^.ContentType = cctUTF8String) and (ACell^.UTF8StringValue <> '') then
|
if (ACell^.ContentType = cctUTF8String) and (ACell^.UTF8StringValue <> '') then
|
||||||
WriteStringRecord(AStream, ACell^.UTF8StringValue);
|
WriteStringRecord(AStream, ACell^.UTF8StringValue);
|
||||||
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Writes the identifier for an RPN function with fixed argument count and
|
{ Writes the identifier for an RPN function with fixed argument count and
|
||||||
@ -1756,19 +1732,6 @@ begin
|
|||||||
|
|
||||||
{ Write out }
|
{ Write out }
|
||||||
AStream.WriteBuffer(rec, Sizeof(rec));
|
AStream.WriteBuffer(rec, Sizeof(rec));
|
||||||
|
|
||||||
(*
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_BLANK));
|
|
||||||
AStream.WriteWord(WordToLE(7));
|
|
||||||
|
|
||||||
{ BIFF Record data }
|
|
||||||
AStream.WriteWord(WordToLE(ARow));
|
|
||||||
AStream.WriteWord(WordToLE(ACol));
|
|
||||||
|
|
||||||
{ BIFF2 Attributes }
|
|
||||||
WriteCellFormatting(AStream, ACell, xf);
|
|
||||||
*)
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{*******************************************************************
|
{*******************************************************************
|
||||||
@ -1807,9 +1770,7 @@ begin
|
|||||||
// Rather than lose data when reading it, let the application programmer deal
|
// Rather than lose data when reading it, let the application programmer deal
|
||||||
// with the problem or purposefully ignore it.
|
// with the problem or purposefully ignore it.
|
||||||
AnsiText := Copy(AnsiText, 1, MAXBYTES);
|
AnsiText := Copy(AnsiText, 1, MAXBYTES);
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(rsTruncateTooLongCellText, [
|
||||||
'Text value exceeds %d character limit in cell %s. ' +
|
|
||||||
'Text has been truncated.', [
|
|
||||||
MAXBYTES, GetCellString(ARow, ACol)
|
MAXBYTES, GetCellString(ARow, ACol)
|
||||||
]);
|
]);
|
||||||
end;
|
end;
|
||||||
|
@ -215,7 +215,7 @@ var
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
fpsStreams;
|
fpsStrings, fpsStreams;
|
||||||
|
|
||||||
const
|
const
|
||||||
{ Excel record IDs }
|
{ Excel record IDs }
|
||||||
@ -448,28 +448,7 @@ begin
|
|||||||
|
|
||||||
SetLength(Boundsheets, 0);
|
SetLength(Boundsheets, 0);
|
||||||
end;
|
end;
|
||||||
(*
|
|
||||||
{*******************************************************************
|
|
||||||
* TsSpreadBIFF5Writer.WriteBlank
|
|
||||||
*
|
|
||||||
* DESCRIPTION: Writes the record for an empty cell
|
|
||||||
*
|
|
||||||
*******************************************************************}
|
|
||||||
procedure TsSpreadBIFF5Writer.WriteBlank(AStream: TStream;
|
|
||||||
const ARow, ACol: Cardinal; ACell: PCell);
|
|
||||||
begin
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_BLANK));
|
|
||||||
AStream.WriteWord(WordToLE(6));
|
|
||||||
|
|
||||||
{ BIFF Record data }
|
|
||||||
AStream.WriteWord(WordToLE(ARow));
|
|
||||||
AStream.WriteWord(WordToLE(ACol));
|
|
||||||
|
|
||||||
{ Index to XF record }
|
|
||||||
WriteXFIndex(AStream, ACell);
|
|
||||||
end;
|
|
||||||
*)
|
|
||||||
{*******************************************************************
|
{*******************************************************************
|
||||||
* TsSpreadBIFF5Writer.WriteBOF ()
|
* TsSpreadBIFF5Writer.WriteBOF ()
|
||||||
*
|
*
|
||||||
@ -795,7 +774,9 @@ begin
|
|||||||
// Bad formatted UTF8String (maybe ANSI?)
|
// Bad formatted UTF8String (maybe ANSI?)
|
||||||
if Length(AValue) <> 0 then begin
|
if Length(AValue) <> 0 then begin
|
||||||
//It was an ANSI string written as UTF8 quite sure, so raise exception.
|
//It was an ANSI string written as UTF8 quite sure, so raise exception.
|
||||||
Raise Exception.CreateFmt('Expected UTF8 text but probably ANSI text found in cell [%d,%d]',[ARow,ACol]);
|
Raise Exception.CreateFmt(rsUTF8TextExpectedButANSIFoundInCell, [
|
||||||
|
GetCellString(ARow, ACol)
|
||||||
|
]);
|
||||||
end;
|
end;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
@ -804,9 +785,7 @@ begin
|
|||||||
// Rather than lose data when reading it, let the application programmer deal
|
// Rather than lose data when reading it, let the application programmer deal
|
||||||
// with the problem or purposefully ignore it.
|
// with the problem or purposefully ignore it.
|
||||||
AnsiValue := Copy(AnsiValue, 1, MAXBYTES);
|
AnsiValue := Copy(AnsiValue, 1, MAXBYTES);
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(rsInvalidCharacterInCell, [
|
||||||
'Text value exceeds %d character limit in cell %s. ' +
|
|
||||||
'Text has been truncated.', [
|
|
||||||
MAXBYTES, GetCellString(ARow, ACol)
|
MAXBYTES, GetCellString(ARow, ACol)
|
||||||
]);
|
]);
|
||||||
end;
|
end;
|
||||||
@ -1345,7 +1324,7 @@ begin
|
|||||||
OLEStorage.ReadOLEFile(AFileName, OLEDocument);
|
OLEStorage.ReadOLEFile(AFileName, OLEDocument);
|
||||||
|
|
||||||
// Check if the operation succeded
|
// Check if the operation succeded
|
||||||
if MemStream.Size = 0 then raise Exception.Create('FPSpreadsheet: Reading the OLE document failed');
|
if MemStream.Size = 0 then raise Exception.Create('[TsSpreadBIFF5Reader.ReadFromFile] Reading of OLE document failed');
|
||||||
|
|
||||||
// Rewind the stream and read from it
|
// Rewind the stream and read from it
|
||||||
MemStream.Position := 0;
|
MemStream.Position := 0;
|
||||||
|
@ -226,7 +226,7 @@ var
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
Math, fpsStreams, fpsExprParser;
|
Math, fpsStrings, fpsStreams, fpsExprParser;
|
||||||
|
|
||||||
const
|
const
|
||||||
{ Excel record IDs }
|
{ Excel record IDs }
|
||||||
@ -930,7 +930,7 @@ begin
|
|||||||
// Badly formatted UTF8String (maybe ANSI?)
|
// Badly formatted UTF8String (maybe ANSI?)
|
||||||
if Length(AValue)<>0 then begin
|
if Length(AValue)<>0 then begin
|
||||||
//Quite sure it was an ANSI string written as UTF8, so raise exception.
|
//Quite sure it was an ANSI string written as UTF8, so raise exception.
|
||||||
Raise Exception.CreateFmt('Expected UTF8 text but probably ANSI text found in cell [%d,%d]',[ARow,ACol]);
|
raise Exception.CreateFmt(rsUTF8TextExpectedButANSIFoundInCell, [GetCellString(ARow,ACol)]);
|
||||||
end;
|
end;
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
@ -939,9 +939,7 @@ begin
|
|||||||
// Rather than lose data when reading it, let the application programmer deal
|
// Rather than lose data when reading it, let the application programmer deal
|
||||||
// with the problem or purposefully ignore it.
|
// with the problem or purposefully ignore it.
|
||||||
SetLength(WideValue, MAXBYTES); //may corrupt the string (e.g. in surrogate pairs), but... too bad.
|
SetLength(WideValue, MAXBYTES); //may corrupt the string (e.g. in surrogate pairs), but... too bad.
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(rsTruncateTooLongCellText, [
|
||||||
'Text value exceeds %d character limit in cell %s. ' +
|
|
||||||
'Text has been truncated.', [
|
|
||||||
MAXBYTES, GetCellString(ARow, ACol)
|
MAXBYTES, GetCellString(ARow, ACol)
|
||||||
]);
|
]);
|
||||||
end;
|
end;
|
||||||
@ -1528,7 +1526,7 @@ begin
|
|||||||
// Can't be shared with BIFF5 because of the parameter "Workbook" !!!)
|
// Can't be shared with BIFF5 because of the parameter "Workbook" !!!)
|
||||||
|
|
||||||
// Check if the operation succeded
|
// Check if the operation succeded
|
||||||
if MemStream.Size = 0 then raise Exception.Create('FPSpreadsheet: Reading the OLE document failed');
|
if MemStream.Size = 0 then raise Exception.Create('[TsSpreadBIFF8Reader.ReadFromFile] Reading of OLE document failed');
|
||||||
|
|
||||||
// Rewind the stream and read from it
|
// Rewind the stream and read from it
|
||||||
MemStream.Position := 0;
|
MemStream.Position := 0;
|
||||||
@ -1604,7 +1602,7 @@ begin
|
|||||||
|
|
||||||
{ Save the data }
|
{ Save the data }
|
||||||
if FIsVirtualMode then begin
|
if FIsVirtualMode then begin
|
||||||
InitCell(ARow, ACol, FVirtualCell); // "virtual" cell
|
InitCell(ARow, ACol, FVirtualCell); // "virtual" cell
|
||||||
cell := @FVirtualCell;
|
cell := @FVirtualCell;
|
||||||
end else
|
end else
|
||||||
cell := FWorksheet.GetCell(ARow, ACol); // "real" cell
|
cell := FWorksheet.GetCell(ARow, ACol); // "real" cell
|
||||||
@ -1866,7 +1864,9 @@ begin
|
|||||||
SSTIndex := DWordLEToN(rec.SSTIndex);
|
SSTIndex := DWordLEToN(rec.SSTIndex);
|
||||||
|
|
||||||
if SizeInt(SSTIndex) >= FSharedStringTable.Count then begin
|
if SizeInt(SSTIndex) >= FSharedStringTable.Count then begin
|
||||||
Raise Exception.CreateFmt('Index %d in SST out of range (0-%d)',[Integer(SSTIndex),FSharedStringTable.Count-1]);
|
raise Exception.CreateFmt(rsIndexInSSTOutOfRange, [
|
||||||
|
Integer(SSTIndex),FSharedStringTable.Count-1
|
||||||
|
]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Create cell }
|
{ Create cell }
|
||||||
|
@ -70,22 +70,22 @@ const
|
|||||||
INT_FONT_WEIGHT_BOLD = $02BC;
|
INT_FONT_WEIGHT_BOLD = $02BC;
|
||||||
|
|
||||||
{ CODEPAGE record constants }
|
{ CODEPAGE record constants }
|
||||||
WORD_ASCII = 367;
|
WORD_ASCII = 367;
|
||||||
WORD_UTF_16 = 1200; // BIFF 8
|
WORD_UTF_16 = 1200; // BIFF 8
|
||||||
WORD_CP_1250_Latin2 = 1250;
|
WORD_CP_1250_Latin2 = 1250;
|
||||||
WORD_CP_1251_Cyrillic = 1251;
|
WORD_CP_1251_Cyrillic = 1251;
|
||||||
WORD_CP_1252_Latin1 = 1252; // BIFF4-BIFF5
|
WORD_CP_1252_Latin1 = 1252; // BIFF4-BIFF5
|
||||||
WORD_CP_1253_Greek = 1253;
|
WORD_CP_1253_Greek = 1253;
|
||||||
WORD_CP_1254_Turkish = 1254;
|
WORD_CP_1254_Turkish = 1254;
|
||||||
WORD_CP_1255_Hebrew = 1255;
|
WORD_CP_1255_Hebrew = 1255;
|
||||||
WORD_CP_1256_Arabic = 1256;
|
WORD_CP_1256_Arabic = 1256;
|
||||||
WORD_CP_1257_Baltic = 1257;
|
WORD_CP_1257_Baltic = 1257;
|
||||||
WORD_CP_1258_Vietnamese = 1258;
|
WORD_CP_1258_Vietnamese = 1258;
|
||||||
WORD_CP_1258_Latin1_BIFF2_3 = 32769; // BIFF2-BIFF3
|
WORD_CP_1258_Latin1_BIFF2_3 = 32769; // BIFF2-BIFF3
|
||||||
|
|
||||||
{ DATEMODE record, 5.28 }
|
{ DATEMODE record, 5.28 }
|
||||||
DATEMODE_1900_BASE = 1; //1/1/1900 minus 1 day in FPC TDateTime
|
DATEMODE_1900_BASE = 1; //1/1/1900 minus 1 day in FPC TDateTime
|
||||||
DATEMODE_1904_BASE = 1462; //1/1/1904 in FPC TDateTime
|
DATEMODE_1904_BASE = 1462; //1/1/1904 in FPC TDateTime
|
||||||
|
|
||||||
{ WINDOW1 record constants - BIFF5-BIFF8 }
|
{ WINDOW1 record constants - BIFF5-BIFF8 }
|
||||||
MASK_WINDOW1_OPTION_WINDOW_HIDDEN = $0001;
|
MASK_WINDOW1_OPTION_WINDOW_HIDDEN = $0001;
|
||||||
@ -1786,25 +1786,7 @@ begin
|
|||||||
end else
|
end else
|
||||||
Result := AColor;
|
Result := AColor;
|
||||||
end;
|
end;
|
||||||
(*
|
|
||||||
function TsSpreadBIFFWriter.FormulaElementKindToExcelTokenID(
|
|
||||||
AElementKind: TFEKind; out ASecondaryID: Word): Word;
|
|
||||||
begin
|
|
||||||
if AElementKind = fekFunc then
|
|
||||||
if (AElementKind >= Low(TFuncTokens)) and (AElementKind <= High(TFuncTokens))
|
|
||||||
then begin
|
|
||||||
if FixedParamCount(AElementKind) then
|
|
||||||
Result := INT_EXCEL_TOKEN_FUNC_V
|
|
||||||
else
|
|
||||||
Result := INT_EXCEL_TOKEN_FUNCVAR_V;
|
|
||||||
ASecondaryID := TokenIDs[AElementKind];
|
|
||||||
end
|
|
||||||
else begin
|
|
||||||
Result := TokenIDs[AElementKind];
|
|
||||||
ASecondaryID := 0;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
*)
|
|
||||||
procedure TsSpreadBIFFWriter.GetLastRowCallback(ACell: PCell; AStream: TStream);
|
procedure TsSpreadBIFFWriter.GetLastRowCallback(ACell: PCell; AStream: TStream);
|
||||||
begin
|
begin
|
||||||
Unused(AStream);
|
Unused(AStream);
|
||||||
@ -1855,18 +1837,6 @@ begin
|
|||||||
|
|
||||||
{ Write out }
|
{ Write out }
|
||||||
AStream.WriteBuffer(rec, SizeOf(rec));
|
AStream.WriteBuffer(rec, SizeOf(rec));
|
||||||
(*
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_BLANK));
|
|
||||||
AStream.WriteWord(WordToLE(6));
|
|
||||||
|
|
||||||
{ Row and column index }
|
|
||||||
AStream.WriteWord(WordToLE(ARow));
|
|
||||||
AStream.WriteWord(WordToLE(ACol));
|
|
||||||
|
|
||||||
{ Index to XF record, according to formatting }
|
|
||||||
WriteXFIndex(AStream, ACell);
|
|
||||||
*)
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TsSpreadBIFFWriter.WriteCodepage(AStream: TStream;
|
procedure TsSpreadBIFFWriter.WriteCodepage(AStream: TStream;
|
||||||
@ -1933,20 +1903,6 @@ begin
|
|||||||
|
|
||||||
{ Write out }
|
{ Write out }
|
||||||
AStream.WriteBuffer(rec, SizeOf(rec));
|
AStream.WriteBuffer(rec, SizeOf(rec));
|
||||||
|
|
||||||
(*
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_COLINFO)); // BIFF record header
|
|
||||||
AStream.WriteWord(WordToLE(12)); // Record size
|
|
||||||
AStream.WriteWord(WordToLE(ACol^.Col)); // start column
|
|
||||||
AStream.WriteWord(WordToLE(ACol^.Col)); // end column
|
|
||||||
{ calculate width to be in units of 1/256 of pixel width of character "0" }
|
|
||||||
w := round(ACol^.Width * 256);
|
|
||||||
AStream.WriteWord(WordToLE(w)); // write width
|
|
||||||
AStream.WriteWord(15); // XF record, ignored
|
|
||||||
AStream.WriteWord(0); // option flags, ignored
|
|
||||||
AStream.WriteWord(0); // "not used"
|
|
||||||
*)
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -2062,22 +2018,6 @@ begin
|
|||||||
|
|
||||||
AStream.WriteBuffer(rec, sizeof(Rec));
|
AStream.WriteBuffer(rec, sizeof(Rec));
|
||||||
end;
|
end;
|
||||||
(*
|
|
||||||
|
|
||||||
{ BIFF Record header }
|
|
||||||
AStream.WriteWord(WordToLE(INT_EXCEL_ID_NUMBER));
|
|
||||||
AStream.WriteWord(WordToLE(14));
|
|
||||||
|
|
||||||
{ BIFF Record data }
|
|
||||||
AStream.WriteWord(WordToLE(ARow));
|
|
||||||
AStream.WriteWord(WordToLE(ACol));
|
|
||||||
|
|
||||||
{ Index to XF record }
|
|
||||||
WriteXFIndex(AStream, ACell);
|
|
||||||
|
|
||||||
{ IEE 754 floating-point value }
|
|
||||||
AStream.WriteBuffer(AValue, 8);
|
|
||||||
end; *)
|
|
||||||
|
|
||||||
procedure TsSpreadBIFFWriter.WritePalette(AStream: TStream);
|
procedure TsSpreadBIFFWriter.WritePalette(AStream: TStream);
|
||||||
var
|
var
|
||||||
|
@ -171,7 +171,8 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
variants, fileutil, strutils, math, lazutf8, fpsStreams, fpsNumFormatParser;
|
variants, fileutil, strutils, math, lazutf8,
|
||||||
|
fpsStrings, fpsStreams, fpsNumFormatParser;
|
||||||
|
|
||||||
const
|
const
|
||||||
{ OOXML general XML constants }
|
{ OOXML general XML constants }
|
||||||
@ -735,9 +736,9 @@ begin
|
|||||||
else if dataStr = '#N/A' then
|
else if dataStr = '#N/A' then
|
||||||
AWorksheet.WriteErrorValue(cell, errArgError)
|
AWorksheet.WriteErrorValue(cell, errArgError)
|
||||||
else
|
else
|
||||||
raise Exception.Create('unknown error type');
|
raise Exception.Create(rsUnknownErrorType);
|
||||||
end else
|
end else
|
||||||
raise Exception.Create('Unknown data type');
|
raise Exception.Create(rsUnknownDataType);
|
||||||
|
|
||||||
if FIsVirtualMode then
|
if FIsVirtualMode then
|
||||||
Workbook.OnReadCellData(Workbook, rowIndex, colIndex, cell);
|
Workbook.OnReadCellData(Workbook, rowIndex, colIndex, cell);
|
||||||
@ -960,33 +961,11 @@ begin
|
|||||||
colorNode := patternNode.FirstChild;
|
colorNode := patternNode.FirstChild;
|
||||||
while Assigned(colorNode) do begin
|
while Assigned(colorNode) do begin
|
||||||
nodeName := colorNode.NodeName;
|
nodeName := colorNode.NodeName;
|
||||||
if nodeName = 'fgColor' then begin
|
if nodeName = 'fgColor' then
|
||||||
fgclr := ReadColor(colorNode);
|
fgclr := ReadColor(colorNode)
|
||||||
{
|
|
||||||
s := GetAttrValue(colorNode, 'rgb');
|
|
||||||
if s <> '' then
|
|
||||||
fgclr := FWorkbook.AddColorToPalette(HTMLColorStrToColor('#' + s))
|
|
||||||
else begin
|
|
||||||
s := GetAttrValue(colorNode, 'indexed');
|
|
||||||
if s <> '' then
|
|
||||||
fgclr := StrToInt(s);
|
|
||||||
end;
|
|
||||||
}
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
if nodeName = 'bgColor' then begin
|
if nodeName = 'bgColor' then
|
||||||
bgclr := ReadColor(colorNode);
|
bgclr := ReadColor(colorNode);
|
||||||
{
|
|
||||||
s := GetAttrValue(colorNode, 'rgb');
|
|
||||||
if s <> '' then
|
|
||||||
bgclr := FWorkbook.AddColorToPalette(HTMLColorStrToColor('#' + s))
|
|
||||||
else begin
|
|
||||||
s := GetAttrValue(colorNode, 'indexed');
|
|
||||||
if s <> '' then
|
|
||||||
bgclr := StrToInt(s);
|
|
||||||
end;
|
|
||||||
}
|
|
||||||
end;
|
|
||||||
colorNode := colorNode.NextSibling;
|
colorNode := colorNode.NextSibling;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -1022,8 +1001,8 @@ begin
|
|||||||
fntStyles := fnt.Style;
|
fntStyles := fnt.Style;
|
||||||
fntColor := fnt.Color;
|
fntColor := fnt.Color;
|
||||||
end else begin
|
end else begin
|
||||||
fntName := 'Arial';
|
fntName := DEFAULTFONTNAME;
|
||||||
fntSize := 10;
|
fntSize := DEFAULTFONTSIZE;
|
||||||
fntStyles := [];
|
fntStyles := [];
|
||||||
fntColor := scBlack;
|
fntColor := scBlack;
|
||||||
end;
|
end;
|
||||||
@ -1272,7 +1251,6 @@ begin
|
|||||||
sheetName := GetAttrValue(node, 'name');
|
sheetName := GetAttrValue(node, 'name');
|
||||||
//sheetId := GetAttrValue(node, 'sheetId');
|
//sheetId := GetAttrValue(node, 'sheetId');
|
||||||
AList.Add(sheetName);
|
AList.Add(sheetName);
|
||||||
// AList.AddObject(sheetName, pointer(PtrInt(StrToInt(sheetId))));
|
|
||||||
node := node.NextSibling;
|
node := node.NextSibling;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@ -1465,7 +1443,7 @@ begin
|
|||||||
|
|
||||||
// process the workbook.xml file
|
// process the workbook.xml file
|
||||||
if not FileExists(FilePath + OOXML_PATH_XL_WORKBOOK) then
|
if not FileExists(FilePath + OOXML_PATH_XL_WORKBOOK) then
|
||||||
raise Exception.Create('Defective internal structure of xlsx file');
|
raise Exception.CreateFmt(rsDefectiveInternalStructure, ['xlsx']);
|
||||||
ReadXMLFile(Doc, FilePath + OOXML_PATH_XL_WORKBOOK);
|
ReadXMLFile(Doc, FilePath + OOXML_PATH_XL_WORKBOOK);
|
||||||
DeleteFile(FilePath + OOXML_PATH_XL_WORKBOOK);
|
DeleteFile(FilePath + OOXML_PATH_XL_WORKBOOK);
|
||||||
ReadFileVersion(Doc.DocumentElement.FindNode('fileVersion'));
|
ReadFileVersion(Doc.DocumentElement.FindNode('fileVersion'));
|
||||||
@ -2047,7 +2025,6 @@ begin
|
|||||||
AppendToStream(AStream, Format(
|
AppendToStream(AStream, Format(
|
||||||
'<sheetViews>' +
|
'<sheetViews>' +
|
||||||
'<sheetView workbookViewId="0" %s%s/>' +
|
'<sheetView workbookViewId="0" %s%s/>' +
|
||||||
// <sheetView workbookViewID="0" />
|
|
||||||
'</sheetViews>', [
|
'</sheetViews>', [
|
||||||
showGridLines, showHeaders
|
showGridLines, showHeaders
|
||||||
]))
|
]))
|
||||||
@ -2714,14 +2691,12 @@ begin
|
|||||||
if Length(AValue) > MAXBYTES then
|
if Length(AValue) > MAXBYTES then
|
||||||
begin
|
begin
|
||||||
ResultingValue := Copy(AValue, 1, MAXBYTES); //may chop off multicodepoint UTF8 characters but well...
|
ResultingValue := Copy(AValue, 1, MAXBYTES); //may chop off multicodepoint UTF8 characters but well...
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(rsTruncateTooLongCellText, [
|
||||||
'Text value exceeds %d character limit in cell %s. ' +
|
|
||||||
'Text has been truncated.', [
|
|
||||||
MAXBYTES, GetCellString(ARow, ACol)
|
MAXBYTES, GetCellString(ARow, ACol)
|
||||||
]);
|
]);
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
ResultingValue:=AValue;
|
ResultingValue := AValue;
|
||||||
|
|
||||||
if not ValidXMLText(ResultingValue) then
|
if not ValidXMLText(ResultingValue) then
|
||||||
Workbook.AddErrorMsg(
|
Workbook.AddErrorMsg(
|
||||||
@ -2740,15 +2715,6 @@ begin
|
|||||||
'<c r="%s" s="%d" t="s"><v>%d</v></c>', [CellPosText, lStyleIndex, FSharedStringsCount]));
|
'<c r="%s" s="%d" t="s"><v>%d</v></c>', [CellPosText, lStyleIndex, FSharedStringsCount]));
|
||||||
|
|
||||||
inc(FSharedStringsCount);
|
inc(FSharedStringsCount);
|
||||||
|
|
||||||
{
|
|
||||||
//todo: keep a log of errors and show with an exception after writing file or something.
|
|
||||||
We can't just do the following
|
|
||||||
|
|
||||||
if TextTooLong then
|
|
||||||
Raise Exception.CreateFmt('Text value exceeds %d character limit in cell [%d,%d]. Text has been truncated.',[MaxBytes,ARow,ACol]);
|
|
||||||
because the file wouldn't be written.
|
|
||||||
}
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user