lazarus-ccr/components/fpspreadsheet/xlsbiff8.pas

2024 lines
63 KiB
ObjectPascal
Executable File

{
xlsbiff8.pas
Writes an Excel 8 file
An Excel worksheet stream consists of a number of subsequent records.
To ensure a properly formed file, the following order must be respected:
1st record: BOF
2nd to Nth record: Any record
Last record: EOF
Excel 8 files are OLE compound document files, and must be written using the
fpOLE library.
Records Needed to Make a BIFF8 File Microsoft Excel Can Use:
Required Records:
BOF - Set the 6 byte offset to 0x0005 (workbook globals)
Window1
FONT - At least five of these records must be included
XF - At least 15 Style XF records and 1 Cell XF record must be included
STYLE
BOUNDSHEET - Include one BOUNDSHEET record per worksheet
EOF
BOF - Set the 6 byte offset to 0x0010 (worksheet)
INDEX
DIMENSIONS
WINDOW2
EOF
The row and column numbering in BIFF files is zero-based.
Excel file format specification obtained from:
http://sc.openoffice.org/excelfileformat.pdf
AUTHORS: Felipe Monteiro de Carvalho
Jose Mejuto
}
unit xlsbiff8;
{$ifdef fpc}
{$mode delphi}
{$endif}
// The new OLE code is much better, so always use it
{$define USE_NEW_OLE}
{.$define FPSPREADDEBUG} //define to print out debug info to console. Used to be XLSDEBUG;
interface
uses
Classes, SysUtils, fpcanvas, DateUtils,
fpspreadsheet, xlscommon,
{$ifdef USE_NEW_OLE}
fpolebasic,
{$else}
fpolestorage,
{$endif}
fpsutils, lazutf8;
type
{ TsSpreadBIFF8Reader }
TsSpreadBIFF8Reader = class(TsSpreadBIFFReader)
private
PendingRecordSize: SizeInt;
FWorksheetNames: TStringList;
FCurrentWorksheet: Integer;
FSharedStringTable: TStringList;
function ReadWideString(const AStream: TStream; const ALength: WORD): WideString; overload;
function ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; overload;
procedure ReadWorkbookGlobals(AStream: TStream; AData: TsWorkbook);
procedure ReadWorksheet(AStream: TStream; AData: TsWorkbook);
procedure ReadBoundsheet(AStream: TStream);
function ReadString(const AStream: TStream; const ALength: WORD): UTF8String;
protected
procedure ReadBlank(AStream: TStream); override;
procedure ReadFont(const AStream: TStream);
procedure ReadFormat(AStream: TStream); override;
procedure ReadLabel(AStream: TStream); override;
procedure ReadLabelSST(const AStream: TStream);
// procedure ReadNumber() --> xlscommon
procedure ReadRichString(const AStream: TStream);
procedure ReadRPNCellAddress(AStream: TStream; out ARow, ACol: Cardinal;
out AFlags: TsRelFlags); override;
procedure ReadRPNCellRangeAddress(AStream: TStream;
out ARow1, ACol1, ARow2, ACol2: Cardinal; out AFlags: TsRelFlags); override;
procedure ReadSST(const AStream: TStream);
function ReadString_8bitLen(AStream: TStream): String; override;
procedure ReadStringRecord(AStream: TStream); override;
procedure ReadXF(const AStream: TStream);
public
destructor Destroy; override;
{ General reading methods }
procedure ReadFromFile(AFileName: string; AData: TsWorkbook); override;
procedure ReadFromStream(AStream: TStream; AData: TsWorkbook); override;
end;
{ TsSpreadBIFF8Writer }
TsSpreadBIFF8Writer = class(TsSpreadBIFFWriter)
private
// Writes index to XF record according to cell's formatting
procedure WriteXFFieldsForFormattingStyles(AStream: TStream);
protected
{ Record writing methods }
procedure WriteBOF(AStream: TStream; ADataType: Word);
function WriteBoundsheet(AStream: TStream; ASheetName: string): Int64;
// procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal;
// const AValue: TDateTime; ACell: PCell); override;
procedure WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet);
procedure WriteEOF(AStream: TStream);
procedure WriteFont(AStream: TStream; AFont: TsFont);
procedure WriteFonts(AStream: TStream);
procedure WriteFormat(AStream: TStream; AFormatData: TsNumFormatData;
AListIndex: Integer); override;
procedure WriteFormula(AStream: TStream; const ARow, ACol: Cardinal;
const AFormula: TsFormula; ACell: PCell); override;
procedure WriteIndex(AStream: TStream);
procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: string; ACell: PCell); override;
function WriteRPNCellAddress(AStream: TStream; ARow, ACol: Cardinal;
AFlags: TsRelFlags): word; override;
function WriteRPNCellRangeAddress(AStream: TStream; ARow1, ACol1, ARow2, ACol2: Cardinal;
AFlags: TsRelFlags): Word; override;
function WriteString_8bitLen(AStream: TStream; AString: String): Integer; override;
procedure WriteStringRecord(AStream: TStream; AString: string); override;
procedure WriteStyle(AStream: TStream);
procedure WriteWindow2(AStream: TStream; ASheet: TsWorksheet);
procedure WriteXF(AStream: TStream; AFontIndex: Word;
AFormatIndex: Word; AXF_TYPE_PROT, ATextRotation: Byte; ABorders: TsCellBorders;
const ABorderStyles: TsCellBorderStyles; AHorAlignment: TsHorAlignment = haDefault;
AVertAlignment: TsVertAlignment = vaDefault; AWordWrap: Boolean = false;
AddBackground: Boolean = false; ABackgroundColor: TsColor = scSilver);
procedure WriteXFRecords(AStream: TStream);
public
constructor Create(AWorkbook: TsWorkbook); override;
{ General writing methods }
procedure WriteToFile(const AFileName: string;
const AOverwriteExisting: Boolean = False); override;
procedure WriteToStream(AStream: TStream); override;
end;
var
// the palette of the default BIFF8 colors as "big-endian color" values
PALETTE_BIFF8: array[$00..$3F] of TsColorValue = (
$000000, // $00: black // 8 built-in default colors
$FFFFFF, // $01: white
$FF0000, // $02: red
$00FF00, // $03: green
$0000FF, // $04: blue
$FFFF00, // $05: yellow
$FF00FF, // $06: magenta
$00FFFF, // $07: cyan
$000000, // $08: EGA black
$FFFFFF, // $09: EGA white
$FF0000, // $0A: EGA red
$00FF00, // $0B: EGA green
$0000FF, // $0C: EGA blue
$FFFF00, // $0D: EGA yellow
$FF00FF, // $0E: EGA magenta
$00FFFF, // $0F: EGA cyan
$800000, // $10: EGA dark red
$008000, // $11: EGA dark green
$000080, // $12: EGA dark blue
$808000, // $13: EGA olive
$800080, // $14: EGA purple
$008080, // $15: EGA teal
$C0C0C0, // $16: EGA silver
$808080, // $17: EGA gray
$9999FF, // $18:
$993366, // $19:
$FFFFCC, // $1A:
$CCFFFF, // $1B:
$660066, // $1C:
$FF8080, // $1D:
$0066CC, // $1E:
$CCCCFF, // $1F:
$000080, // $20:
$FF00FF, // $21:
$FFFF00, // $22:
$00FFFF, // $23:
$800080, // $24:
$800000, // $25:
$008080, // $26:
$0000FF, // $27:
$00CCFF, // $28:
$CCFFFF, // $29:
$CCFFCC, // $2A:
$FFFF99, // $2B:
$99CCFF, // $2C:
$FF99CC, // $2D:
$CC99FF, // $2E:
$FFCC99, // $2F:
$3366FF, // $30:
$33CCCC, // $31:
$99CC00, // $32:
$FFCC00, // $33:
$FF9900, // $34:
$FF6600, // $35:
$666699, // $36:
$969696, // $37:
$003366, // $38:
$339966, // $39:
$003300, // $3A:
$333300, // $3B:
$993300, // $3C:
$993366, // $3D:
$333399, // $3E:
$333333 // $3F:
);
implementation
uses
fpsStreams;
const
{ Excel record IDs }
INT_EXCEL_ID_SST = $00FC; //BIFF8 only
INT_EXCEL_ID_LABELSST = $00FD; //BIFF8 only
INT_EXCEL_ID_FORCEFULLCALCULATION = $08A3;
{ Cell Addresses constants }
MASK_EXCEL_COL_BITS_BIFF8 = $00FF;
MASK_EXCEL_RELATIVE_COL_BIFF8 = $4000; // This is according to Microsoft documentation,
MASK_EXCEL_RELATIVE_ROW_BIFF8 = $8000; // but opposite to OpenOffice documentation!
{ BOF record constants }
INT_BOF_BIFF8_VER = $0600;
INT_BOF_WORKBOOK_GLOBALS= $0005;
INT_BOF_VB_MODULE = $0006;
INT_BOF_SHEET = $0010;
INT_BOF_CHART = $0020;
INT_BOF_MACRO_SHEET = $0040;
INT_BOF_WORKSPACE = $0100;
INT_BOF_BUILD_ID = $1FD2;
INT_BOF_BUILD_YEAR = $07CD;
{ STYLE record constants }
MASK_STYLE_BUILT_IN = $8000;
{ XF substructures }
{ XF_ROTATION }
XF_ROTATION_HORIZONTAL = 0;
XF_ROTATION_90DEG_CCW = 90;
XF_ROTATION_90DEG_CW = 180;
XF_ROTATION_STACKED = 255; // Letters stacked top to bottom, but not rotated
{ XF CELL BORDER LINE STYLES }
MASK_XF_BORDER_LEFT = $0000000F;
MASK_XF_BORDER_RIGHT = $000000F0;
MASK_XF_BORDER_TOP = $00000F00;
MASK_XF_BORDER_BOTTOM = $0000F000;
{ XF CELL BORDER COLORS }
MASK_XF_BORDER_LEFT_COLOR = $007F0000;
MASK_XF_BORDER_RIGHT_COLOR = $3F800000;
MASK_XF_BORDER_TOP_COLOR = $0000007F;
MASK_XF_BORDER_BOTTOM_COLOR = $00003F80;
{ XF CELL BACKGROUND PATTERN }
MASK_XF_BACKGROUND_PATTERN = $FC000000;
TEXT_ROTATIONS: Array[TsTextRotation] of Byte = (
XF_ROTATION_HORIZONTAL,
XF_ROTATION_90DEG_CW,
XF_ROTATION_90DEG_CCW,
XF_ROTATION_STACKED
);
{ TsSpreadBIFF8Writer }
constructor TsSpreadBIFF8Writer.Create(AWorkbook: TsWorkbook);
begin
inherited Create(AWorkbook);
end;
procedure TsSpreadBIFF8Writer.WriteXFFieldsForFormattingStyles(AStream: TStream);
var
i, j: Integer;
lFontIndex: Word;
lFormatIndex: Word; //number format
lTextRotation: Byte;
lBorders: TsCellBorders;
lBorderStyles: TsCellBorderStyles;
lAddBackground: Boolean;
lBackgroundColor: TsColor;
lHorAlign: TsHorAlignment;
lVertAlign: TsVertAlignment;
lWordWrap: Boolean;
begin
// The first style was already added --> begin loop with 1
for i := 1 to Length(FFormattingStyles) - 1 do begin
// Default styles
lFontIndex := 0;
lFormatIndex := 0; //General format (one of the built-in number formats)
lTextRotation := XF_ROTATION_HORIZONTAL;
lBorders := [];
lBorderStyles := FFormattingStyles[i].BorderStyles;
lHorAlign := FFormattingStyles[i].HorAlignment;
lVertAlign := FFormattingStyles[i].VertAlignment;
lBackgroundColor := FFormattingStyles[i].BackgroundColor;
// Now apply the modifications.
if uffNumberFormat in FFormattingStyles[i].UsedFormattingFields then begin
// The number formats in the FormattingStyles are still in fpc dialect
// They will be converted to Excel syntax immediately before writing.
j := NumFormatList.FindFormatOf(@FFormattingStyles[i]);
if j > -1 then
lFormatIndex := NumFormatList[j].Index;
end;
if uffBorder in FFormattingStyles[i].UsedFormattingFields then
lBorders := FFormattingStyles[i].Border;
if uffTextRotation in FFormattingStyles[i].UsedFormattingFields then
lTextRotation := TEXT_ROTATIONS[FFormattingStyles[i].TextRotation];
if uffBold in FFormattingStyles[i].UsedFormattingFields then
lFontIndex := 1; // must be before uffFont which overrides uffBold
if uffFont in FFormattingStyles[i].UsedFormattingFields then
lFontIndex := FFormattingStyles[i].FontIndex;
lAddBackground := (uffBackgroundColor in FFormattingStyles[i].UsedFormattingFields);
lWordwrap := (uffWordwrap in FFormattingStyles[i].UsedFormattingFields);
// And finally write the style
WriteXF(AStream, lFontIndex, lFormatIndex, 0, lTextRotation, lBorders,
lBorderStyles, lHorAlign, lVertAlign, lWordwrap, lAddBackground,
lBackgroundColor);
end;
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteToFile ()
*
* DESCRIPTION: Writes an Excel BIFF8 file to the disc
*
* The BIFF 8 writer overrides this method because
* BIFF 8 is written as an OLE document, and our
* current OLE document writing method involves:
*
* 1 - Writing the BIFF data to a memory stream
*
* 2 - Write the memory stream data to disk using
* COM functions
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteToFile(const AFileName: string;
const AOverwriteExisting: Boolean);
var
Stream: TStream;
OutputStorage: TOLEStorage;
OLEDocument: TOLEDocument;
begin
if (woBufStream in Workbook.WritingOptions) then begin
Stream := TBufStream.Create
end else
Stream := TMemoryStream.Create;
OutputStorage := TOLEStorage.Create;
try
WriteToStream(Stream);
// Only one stream is necessary for any number of worksheets
OLEDocument.Stream := Stream;
OutputStorage.WriteOLEFile(AFileName, OLEDocument, AOverwriteExisting, 'Workbook');
finally
Stream.Free;
OutputStorage.Free;
end;
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteToStream ()
*
* DESCRIPTION: Writes an Excel BIFF8 record structure
*
* Be careful as this method doesn't write the OLE
* part of the document, just the BIFF records
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteToStream(AStream: TStream);
const
isBIFF8 = true;
var
CurrentPos: Int64;
Boundsheets: array of Int64;
sheet: TsWorksheet;
i, len: Integer;
pane: Byte;
begin
{ Write workbook globals }
WriteBOF(AStream, INT_BOF_WORKBOOK_GLOBALS);
WriteWindow1(AStream);
WriteFonts(AStream);
WriteFormats(AStream);
WritePalette(AStream);
WriteXFRecords(AStream);
WriteStyle(AStream);
// A BOUNDSHEET for each worksheet
SetLength(Boundsheets, 0);
for i := 0 to Workbook.GetWorksheetCount - 1 do
begin
len := Length(Boundsheets);
SetLength(Boundsheets, len + 1);
Boundsheets[len] := WriteBoundsheet(AStream, Workbook.GetWorksheetByIndex(i).Name);
end;
WriteEOF(AStream);
{ Write each worksheet }
for i := 0 to Workbook.GetWorksheetCount - 1 do
begin
sheet := Workbook.GetWorksheetByIndex(i);
{ First goes back and writes the position of the BOF of the
sheet on the respective BOUNDSHEET record }
CurrentPos := AStream.Position;
AStream.Position := Boundsheets[i];
AStream.WriteDWord(DWordToLE(DWORD(CurrentPos)));
AStream.Position := CurrentPos;
WriteBOF(AStream, INT_BOF_SHEET);
WriteIndex(AStream);
//WriteSheetPR(AStream);
// WritePageSetup(AStream);
WriteColInfos(AStream, sheet);
WriteDimensions(AStream, sheet);
//WriteRowAndCellBlock(AStream, sheet);
if (woVirtualMode in Workbook.WritingOptions) then
WriteVirtualCells(AStream)
else begin
WriteRows(AStream, sheet);
WriteCellsToStream(AStream, sheet.Cells);
end;
WriteWindow2(AStream, sheet);
WritePane(AStream, sheet, isBIFF8, pane);
WriteSelection(AStream, sheet, pane);
WriteEOF(AStream);
end;
{ Cleanup }
SetLength(Boundsheets, 0);
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteBOF ()
*
* DESCRIPTION: Writes an Excel 8 BOF record
*
* This must be the first record on an Excel 8 stream
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteBOF(AStream: TStream; ADataType: Word);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_BOF));
AStream.WriteWord(WordToLE(16)); //total record size
{ BIFF version. Should only be used if this BOF is for the workbook globals }
{ OpenOffice rejects to correctly read xls files if this field is
omitted as docs. says, or even if it is being written to zero value,
Not tested with Excel, but MSExcel reader opens it as expected }
AStream.WriteWord(WordToLE(INT_BOF_BIFF8_VER));
{ Data type }
AStream.WriteWord(WordToLE(ADataType));
{ Build identifier, must not be 0 }
AStream.WriteWord(WordToLE(INT_BOF_BUILD_ID));
{ Build year, must not be 0 }
AStream.WriteWord(WordToLE(INT_BOF_BUILD_YEAR));
{ File history flags }
AStream.WriteDWord(DWordToLE(0));
{ Lowest Excel version that can read all records in this file 5?}
AStream.WriteDWord(DWordToLE(0)); //?????????
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteBoundsheet ()
*
* DESCRIPTION: Writes an Excel 8 BOUNDSHEET record
*
* Always located on the workbook globals substream.
*
* One BOUNDSHEET is written for each worksheet.
*
* RETURNS: The stream position where the absolute stream position
* of the BOF of this sheet should be written (4 bytes size).
*
*******************************************************************}
function TsSpreadBIFF8Writer.WriteBoundsheet(AStream: TStream; ASheetName: string): Int64;
var
Len: Byte;
WideSheetName: WideString;
begin
WideSheetName:=UTF8Decode(ASheetName);
Len := Length(WideSheetName);
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_BOUNDSHEET));
AStream.WriteWord(WordToLE(6 + 1 + 1 + Len * Sizeof(WideChar)));
{ Absolute stream position of the BOF record of the sheet represented
by this record }
Result := AStream.Position;
AStream.WriteDWord(DWordToLE(0));
{ Visibility }
AStream.WriteByte(0);
{ Sheet type }
AStream.WriteByte(0);
{ Sheet name: Unicode string char count 1 byte }
AStream.WriteByte(Len);
{String flags}
AStream.WriteByte(1);
AStream.WriteBuffer(WideStringToLE(WideSheetName)[1], Len * Sizeof(WideChar));
end;
{
Writes an Excel 8 DIMENSIONS record
nm = (rl - rf - 1) / 32 + 1 (using integer division)
Excel, OpenOffice and FPSpreadsheet ignore the dimensions written in this record,
but some other applications really use them, so they need to be correct.
See bug 18886: excel5 files are truncated when imported
}
procedure TsSpreadBIFF8Writer.WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet);
var
firstRow, lastRow, firstCol, lastCol: Cardinal;
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_DIMENSIONS));
AStream.WriteWord(WordToLE(14));
{ Determine sheet size }
GetSheetDimensions(AWorksheet, firstRow, lastRow, firstCol, lastCol);
{ Index to first used row }
AStream.WriteDWord(DWordToLE(firstRow));
{ Index to last used row, increased by 1 }
AStream.WriteDWord(DWordToLE(lastRow+1));
{ Index to first used column }
AStream.WriteWord(WordToLE(firstCol));
{ Index to last used column, increased by 1 }
AStream.WriteWord(WordToLE(lastCol+1));
{ Not used }
AStream.WriteWord(WordToLE(0));
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteEOF ()
*
* DESCRIPTION: Writes an Excel 8 EOF record
*
* This must be the last record on an Excel 8 stream
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteEOF(AStream: TStream);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_EOF));
AStream.WriteWord(WordToLE($0000));
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteFont ()
*
* DESCRIPTION: Writes an Excel 8 FONT record
*
* The font data is passed in an instance of TsFont
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteFont(AStream: TStream; AFont: TsFont);
var
Len: Byte;
WideFontName: WideString;
optn: Word;
begin
if AFont = nil then // this happens for FONT4 in case of BIFF
exit;
if AFont.FontName = '' then
raise Exception.Create('Font name not specified.');
if AFont.Size <= 0.0 then
raise Exception.Create('Font size not specified.');
WideFontName := AFont.FontName;
Len := Length(WideFontName);
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_FONT));
AStream.WriteWord(WordToLE(14 + 1 + 1 + Len * Sizeof(WideChar)));
{ Height of the font in twips = 1/20 of a point }
AStream.WriteWord(WordToLE(round(AFont.Size*20)));
{ Option flags }
optn := 0;
if fssBold in AFont.Style then optn := optn or $0001;
if fssItalic in AFont.Style then optn := optn or $0002;
if fssUnderline in AFont.Style then optn := optn or $0004;
if fssStrikeout in AFont.Style then optn := optn or $0008;
AStream.WriteWord(WordToLE(optn));
{ Colour index }
AStream.WriteWord(WordToLE(ord(AFont.Color)));
{ Font weight }
if fssBold in AFont.Style then
AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_BOLD))
else
AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_NORMAL));
{ Escapement type }
AStream.WriteWord(WordToLE(0));
{ Underline type }
if fssUnderline in AFont.Style then
AStream.WriteByte(1)
else
AStream.WriteByte(0);
{ Font family }
AStream.WriteByte(0);
{ Character set }
AStream.WriteByte(0);
{ Not used }
AStream.WriteByte(0);
{ Font name: Unicodestring, char count in 1 byte }
AStream.WriteByte(Len);
{ Widestring flags, 1=regular unicode LE string }
AStream.WriteByte(1);
AStream.WriteBuffer(WideStringToLE(WideFontName)[1], Len * Sizeof(WideChar));
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteFonts ()
*
* DESCRIPTION: Writes the Excel 8 FONT records needed for the
* used fonts in the workbook.
*
*******************************************************************}
procedure TsSpreadBiff8Writer.WriteFonts(AStream: TStream);
var
i: Integer;
begin
for i:=0 to Workbook.GetFontCount-1 do
WriteFont(AStream, Workbook.GetFont(i));
end;
procedure TsSpreadBiff8Writer.WriteFormat(AStream: TStream;
AFormatData: TsNumFormatData; AListIndex: Integer);
type
TNumFormatRecord = packed record
RecordID: Word;
RecordSize: Word;
FormatIndex: Word;
FormatStringLen: Word;
FormatStringFlags: Byte;
end;
var
len: Integer;
s: widestring;
rec: TNumFormatRecord;
buf: array of byte;
begin
if (AFormatData = nil) or (AFormatData.FormatString = '') then
exit;
s := NumFormatList.FormatStringForWriting(AListIndex);
len := Length(s);
{ BIFF record header }
rec.RecordID := WordToLE(INT_EXCEL_ID_FORMAT);
rec.RecordSize := WordToLE(2 + 2 + 1 + len * SizeOf(WideChar));
{ Format index }
rec.FormatIndex := WordToLE(AFormatData.Index);
{ Format string }
{ - length of string = 16 bits }
rec.FormatStringLen := WordToLE(len);
{ - Widestring flags, 1 = regular unicode LE string }
rec.FormatStringFlags := 1;
{ - Copy the text characters into a buffer immediately after rec }
SetLength(buf, SizeOf(rec) + SizeOf(WideChar)*len);
Move(rec, buf[0], SizeOf(rec));
Move(s[1], buf[SizeOf(rec)], len*SizeOf(WideChar));
{ Write out }
AStream.WriteBuffer(buf[0], SizeOf(rec) + SizeOf(WideChar)*len);
{ Clean up }
SetLength(buf, 0);
(*
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMAT));
AStream.WriteWord(WordToLE(2 + 2 + 1 + len * SizeOf(WideChar)));
{ Format index }
AStream.WriteWord(WordToLE(AFormatData.Index));
{ Format string }
{ - Unicodestring, char count in 2 bytes }
AStream.WriteWord(WordToLE(len));
{ - Widestring flags, 1=regular unicode LE string }
AStream.WriteByte(1);
{ - String data }
AStream.WriteBuffer(WideStringToLE(s)[1], len * Sizeof(WideChar));
*)
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteFormula ()
*
* DESCRIPTION: Writes an Excel 5 FORMULA record
*
* To input a formula to this method, first convert it
* to RPN, and then list all it's members in the
* AFormula array
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteFormula(AStream: TStream; const ARow,
ACol: Cardinal; const AFormula: TsFormula; ACell: PCell);
{var
FormulaResult: double;
i: Integer;
RPNLength: Word;
TokenArraySizePos, RecordSizePos, FinalPos: Int64;}
begin
(* RPNLength := 0;
FormulaResult := 0.0;
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMULA));
RecordSizePos := AStream.Position;
AStream.WriteWord(WordToLE(22 + RPNLength));
{ BIFF Record data }
AStream.WriteWord(WordToLE(ARow));
AStream.WriteWord(WordToLE(ACol));
{ Index to XF Record }
AStream.WriteWord($0000);
{ Result of the formula in IEE 754 floating-point value }
AStream.WriteBuffer(FormulaResult, 8);
{ Options flags }
AStream.WriteWord(WordToLE(MASK_FORMULA_RECALCULATE_ALWAYS));
{ Not used }
AStream.WriteDWord(0);
{ Formula }
{ The size of the token array is written later,
because it's necessary to calculate if first,
and this is done at the same time it is written }
TokenArraySizePos := AStream.Position;
AStream.WriteWord(RPNLength);
{ Formula data (RPN token array) }
for i := 0 to Length(AFormula) - 1 do
begin
{ Token identifier }
AStream.WriteByte(AFormula[i].TokenID);
Inc(RPNLength);
{ Additional data }
case AFormula[i].TokenID of
{ binary operation tokens }
INT_EXCEL_TOKEN_TADD, INT_EXCEL_TOKEN_TSUB, INT_EXCEL_TOKEN_TMUL,
INT_EXCEL_TOKEN_TDIV, INT_EXCEL_TOKEN_TPOWER: begin end;
INT_EXCEL_TOKEN_TNUM:
begin
AStream.WriteBuffer(AFormula[i].DoubleValue, 8);
Inc(RPNLength, 8);
end;
INT_EXCEL_TOKEN_TREFR, INT_EXCEL_TOKEN_TREFV, INT_EXCEL_TOKEN_TREFA:
begin
AStream.WriteWord(AFormula[i].Row and MASK_EXCEL_ROW);
AStream.WriteByte(AFormula[i].Col);
Inc(RPNLength, 3);
end;
end;
end;
{ Write sizes in the end, after we known them }
FinalPos := AStream.Position;
AStream.position := TokenArraySizePos;
AStream.WriteByte(RPNLength);
AStream.Position := RecordSizePos;
AStream.WriteWord(WordToLE(22 + RPNLength));
AStream.position := FinalPos;*)
end;
{ Writes the address of a cell as used in an RPN formula and returns the
number of bytes written. }
function TsSpreadBIFF8Writer.WriteRPNCellAddress(AStream: TStream;
ARow, ACol: Cardinal; AFlags: TsRelFlags): Word;
var
c: Cardinal; // column index with encoded relative/absolute address info
begin
AStream.WriteWord(WordToLE(ARow));
c := ACol and MASK_EXCEL_COL_BITS_BIFF8;
if (rfRelRow in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW_BIFF8;
if (rfRelCol in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL_BIFF8;
AStream.WriteWord(WordToLE(c));
Result := 4;
end;
{ Writes the address of a cell range as used in an RPN formula and returns the
count of bytes written. }
function TsSpreadBIFF8Writer.WriteRPNCellRangeAddress(AStream: TStream;
ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags): Word;
var
c: Cardinal; // column index with encoded relative/absolute address info
begin
AStream.WriteWord(WordToLE(ARow1));
AStream.WriteWord(WordToLE(ARow2));
c := ACol1;
if (rfRelCol in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL;
if (rfRelRow in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW;
AStream.WriteWord(WordToLE(c));
c := ACol2;
if (rfRelCol2 in AFlags) then c := c or MASK_EXCEL_RELATIVE_COL;
if (rfRelRow2 in AFlags) then c := c or MASK_EXCEL_RELATIVE_ROW;
AStream.WriteWord(WordToLE(c));
Result := 8;
end;
{ Helper function for writing a string with 8-bit length. Overridden version
for BIFF8. Called for writing rpn formula string tokens.
Returns the count of bytes written}
function TsSpreadBIFF8Writer.WriteString_8BitLen(AStream: TStream;
AString: String): Integer;
var
len: Integer;
wideStr: WideString;
begin
// string constant is stored as widestring in BIFF8
wideStr := UTF8Decode(AString);
len := Length(wideStr);
AStream.WriteByte(len); // char count in 1 byte
AStream.WriteByte(1); // Widestring flags, 1=regular unicode LE string
AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * Sizeof(WideChar));
Result := 1 + 1 + len * SizeOf(WideChar);
end;
procedure TsSpreadBIFF8Writer.WriteStringRecord(AStream: TStream;
AString: String);
var
wideStr: widestring;
len: Integer;
begin
wideStr := UTF8Decode(AString);
len := Length(wideStr);
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_STRING));
AStream.WriteWord(WordToLE(3 + len*SizeOf(widechar)));
{ Write widestring length }
AStream.WriteWord(WordToLE(len));
{ Widestring flags, 1=regular unicode LE string }
AStream.WriteByte(1);
{ Write characters }
AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * SizeOf(WideChar));
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteIndex ()
*
* DESCRIPTION: Writes an Excel 8 INDEX record
*
* nm = (rl - rf - 1) / 32 + 1 (using integer division)
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteIndex(AStream: TStream);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_INDEX));
AStream.WriteWord(WordToLE(16));
{ Not used }
AStream.WriteDWord(DWordToLE(0));
{ Index to first used row, rf, 0 based }
AStream.WriteDWord(DWordToLE(0));
{ Index to first row of unused tail of sheet, rl, last used row + 1, 0 based }
AStream.WriteDWord(DWordToLE(0));
{ Absolute stream position of the DEFCOLWIDTH record of the current sheet.
If it doesn't exist, the offset points to where it would occur. }
AStream.WriteDWord(DWordToLE($00));
{ Array of nm absolute stream positions of the DBCELL record of each Row Block }
{ OBS: It seems to be no problem just ignoring this part of the record }
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteLabel ()
*
* DESCRIPTION: Writes an Excel 8 LABEL record
*
* Writes a string to the sheet
* If the string length exceeds 32758 bytes, the string
* will be silently truncated.
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteLabel(AStream: TStream; const ARow,
ACol: Cardinal; const AValue: string; ACell: PCell);
type
TLabelRecord = packed record
RecordID: Word;
RecordSize: Word;
Row: Word;
Col: Word;
XFIndex: Word;
TextLen: Word;
TextFlags: Byte;
end;
const
//limit for this format: 32767 bytes - header (see reclen below):
//37267-8-1=32758
MAXBYTES = 32758;
var
L, RecLen: Word;
TextTooLong: boolean=false;
WideValue: WideString;
rec: TLabelRecord;
buf: array of byte;
begin
WideValue := UTF8Decode(AValue); //to UTF16
if WideValue = '' then begin
// Badly formatted UTF8String (maybe ANSI?)
if Length(AValue)<>0 then begin
//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]);
end;
Exit;
end;
if Length(WideValue) > MAXBYTES then begin
// Rather than lose data when reading it, let the application programmer deal
// with the problem or purposefully ignore it.
TextTooLong := true;
SetLength(WideValue, MaxBytes); //may corrupt the string (e.g. in surrogate pairs), but... too bad.
end;
L := Length(WideValue);
{ BIFF record header }
rec.RecordID := WordToLE(INT_EXCEL_ID_LABEL);
rec.RecordSize := 8 + 1 + L * SizeOf(WideChar);
{ BIFF record data }
rec.Row := WordToLE(ARow);
rec.Col := WordToLE(ACol);
{ Index to XF record, according to formatting }
rec.XFIndex := WordToLE(FindXFIndex(ACell));
{ Byte String with 16-bit length }
rec.TextLen := WordToLE(L);
{ Byte flags, 1 means regular unicode LE encoding }
rec.TextFlags := 1;
{ Copy the text characters into a buffer immediately after rec }
SetLength(buf, SizeOf(rec) + L*SizeOf(WideChar));
Move(rec, buf[0], SizeOf(Rec));
Move(WideStringToLE(WideValue)[1], buf[SizeOf(Rec)], L*SizeOf(WideChar));
{ Write out }
AStream.WriteBuffer(buf[0], SizeOf(rec) + L*SizeOf(WideChar));
{ Clean up }
SetLength(buf, 0);
(*
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_LABEL));
RecLen := 8 + 1 + L * SizeOf(WideChar);
AStream.WriteWord(WordToLE(RecLen));
{ BIFF Record data }
AStream.WriteWord(WordToLE(ARow));
AStream.WriteWord(WordToLE(ACol));
{ Index to XF record, according to formatting }
WriteXFIndex(AStream, ACell);
{ Byte String with 16-bit size }
AStream.WriteWord(WordToLE(L));
{ Byte flags. 1 means regular Unicode LE encoding}
AStream.WriteByte(1);
AStream.WriteBuffer(WideStringToLE(WideValue)[1], L * Sizeof(WideChar));
{
//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;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteStyle ()
*
* DESCRIPTION: Writes an Excel 8 STYLE record
*
* Registers the name of a user-defined style or
* specific options for a built-in cell style.
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteStyle(AStream: TStream);
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_STYLE));
AStream.WriteWord(WordToLE(4));
{ Index to style XF and defines if it's a built-in or used defined style }
AStream.WriteWord(WordToLE(MASK_STYLE_BUILT_IN));
{ Built-in cell style identifier }
AStream.WriteByte($00);
{ Level if the identifier for a built-in style is RowLevel or ColLevel, $FF otherwise }
AStream.WriteByte($FF);
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteWindow2 ()
*
* DESCRIPTION: Writes an Excel 8 WINDOW2 record
*
* This record contains aditional settings for the
* document window (BIFF2-BIFF4) or for a specific
* worksheet (BIFF5-BIFF8).
*
* The values written here are reasonable defaults,
* which should work for most sheets.
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteWindow2(AStream: TStream;
ASheet: TsWorksheet);
var
Options: Word;
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_WINDOW2));
AStream.WriteWord(WordToLE(18));
{ Options flags }
Options :=
MASK_WINDOW2_OPTION_SHOW_ZERO_VALUES or
MASK_WINDOW2_OPTION_AUTO_GRIDLINE_COLOR or
MASK_WINDOW2_OPTION_SHOW_OUTLINE_SYMBOLS or
MASK_WINDOW2_OPTION_SHEET_SELECTED or
MASK_WINDOW2_OPTION_SHEET_ACTIVE;
{ Bug 0026386 -> every sheet must be selected/active, otherwise Excel cannot print }
if (soShowGridLines in ASheet.Options) then
Options := Options or MASK_WINDOW2_OPTION_SHOW_GRID_LINES;
if (soShowHeaders in ASheet.Options) then
Options := Options or MASK_WINDOW2_OPTION_SHOW_SHEET_HEADERS;
if (soHasFrozenPanes in ASheet.Options) and ((ASheet.LeftPaneWidth > 0) or (ASheet.TopPaneHeight > 0)) then
Options := Options or MASK_WINDOW2_OPTION_PANES_ARE_FROZEN;
AStream.WriteWord(WordToLE(Options));
{ Index to first visible row }
AStream.WriteWord(WordToLE(0));
{ Index to first visible column }
AStream.WriteWord(WordToLE(0));
{ Grid line index colour }
AStream.WriteWord(WordToLE(0));
{ Not used }
AStream.WriteWord(WordToLE(0));
{ Cached magnification factor in page break preview (in percent); 0 = Default (60%) }
AStream.WriteWord(WordToLE(0));
{ Cached magnification factor in normal view (in percent); 0 = Default (100%) }
AStream.WriteWord(WordToLE(0));
{ Not used }
AStream.WriteDWord(DWordToLE(0));
end;
{*******************************************************************
* TsSpreadBIFF8Writer.WriteXF ()
*
* DESCRIPTION: Writes an Excel 8 XF record
*
*
*
*******************************************************************}
procedure TsSpreadBIFF8Writer.WriteXF(AStream: TStream; AFontIndex: Word;
AFormatIndex: Word; AXF_TYPE_PROT, ATextRotation: Byte; ABorders: TsCellBorders;
const ABorderStyles: TsCellBorderStyles; AHorAlignment: TsHorAlignment = haDefault;
AVertAlignment: TsVertAlignment = vaDefault; AWordWrap: Boolean = false;
AddBackground: Boolean = false; ABackgroundColor: TsColor = scSilver);
var
XFOptions: Word;
XFAlignment, XFOrientationAttrib: Byte;
XFBorderDWord1, XFBorderDWord2: DWord;
begin
{ BIFF Record header }
AStream.WriteWord(WordToLE(INT_EXCEL_ID_XF));
AStream.WriteWord(WordToLE(20));
{ Index to FONT record }
AStream.WriteWord(WordToLE(AFontIndex));
{ Index to FORMAT record }
AStream.WriteWord(WordToLE(AFormatIndex));
{ XF type, cell protection and parent style XF }
XFOptions := AXF_TYPE_PROT and MASK_XF_TYPE_PROT;
if AXF_TYPE_PROT and MASK_XF_TYPE_PROT_STYLE_XF <> 0 then
XFOptions := XFOptions or MASK_XF_TYPE_PROT_PARENT;
AStream.WriteWord(WordToLE(XFOptions));
{ Alignment and text break }
XFAlignment := 0;
case AHorAlignment of
haLeft : XFAlignment := XFAlignment or MASK_XF_HOR_ALIGN_LEFT;
haCenter : XFAlignment := XFAlignment or MASK_XF_HOR_ALIGN_CENTER;
haRight : XFAlignment := XFAlignment or MASK_XF_HOR_ALIGN_RIGHT;
end;
case AVertAlignment of
vaTop : XFAlignment := XFAlignment or MASK_XF_VERT_ALIGN_TOP;
vaCenter : XFAlignment := XFAlignment or MASK_XF_VERT_ALIGN_CENTER;
vaBottom : XFAlignment := XFAlignment or MASK_XF_VERT_ALIGN_BOTTOM;
else XFAlignment := XFAlignment or MASK_XF_VERT_ALIGN_BOTTOM;
end;
if AWordWrap then
XFAlignment := XFAlignment or MASK_XF_TEXTWRAP;
AStream.WriteByte(XFAlignment);
{ Text rotation }
AStream.WriteByte(ATextRotation); // 0 is horizontal / normal
{ Indentation, shrink and text direction }
AStream.WriteByte(0);
{ Used attributes }
XFOrientationAttrib :=
MASK_XF_USED_ATTRIB_NUMBER_FORMAT or
MASK_XF_USED_ATTRIB_FONT or
MASK_XF_USED_ATTRIB_TEXT or
MASK_XF_USED_ATTRIB_BORDER_LINES or
MASK_XF_USED_ATTRIB_BACKGROUND or
MASK_XF_USED_ATTRIB_CELL_PROTECTION;
AStream.WriteByte(XFOrientationAttrib);
{ Cell border lines and background area }
// Left and Right line colors
XFBorderDWord1 := ABorderStyles[cbWest].Color shl 16 +
ABorderStyles[cbEast].Color shl 23;
// Border line styles
if cbWest in ABorders then
XFBorderDWord1 := XFBorderDWord1 or (ord(ABorderStyles[cbWest].LineStyle)+1);
if cbEast in ABorders then
XFBorderDWord1 := XFBorderDWord1 or ((ord(ABorderStyles[cbEast].LineStyle)+1) shl 4);
if cbNorth in ABorders then
XFBorderDWord1 := XFBorderDWord1 or ((ord(ABorderStyles[cbNorth].LineStyle)+1) shl 8);
if cbSouth in ABorders then
XFBorderDWord1 := XFBorderDWord1 or ((ord(ABorderStyles[cbSouth].LineStyle)+1) shl 12);
AStream.WriteDWord(DWordToLE(XFBorderDWord1));
// Top and Bottom line colors
XFBorderDWord2 := ABorderStyles[cbNorth].Color + ABorderStyles[cbSouth].Color shl 7;
// XFBorderDWord2 := 8 {top line - black} + 8 * $80 {bottom line - black};
// Add a background, if desired
if AddBackground then XFBorderDWord2 := XFBorderDWord2 or $4000000;
AStream.WriteDWord(DWordToLE(XFBorderDWord2));
// Background Pattern Color, always zeroed
if AddBackground then
AStream.WriteWord(WordToLE(ABackgroundColor))
else
AStream.WriteWord(0);
end;
procedure TsSpreadBIFF8Writer.WriteXFRecords(AStream: TStream);
begin
// XF0
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF1
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF2
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF3
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF4
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF5
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF6
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF7
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF8
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF9
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF10
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF11
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF12
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF13
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF14
WriteXF(AStream, 0, 0, MASK_XF_TYPE_PROT_STYLE_XF, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// XF15 - Default, no formatting
WriteXF(AStream, 0, 0, 0, XF_ROTATION_HORIZONTAL, [], DEFAULT_BORDERSTYLES);
// Add all further non-standard/built-in formatting styles
ListAllFormattingStyles;
WriteXFFieldsForFormattingStyles(AStream);
end;
{ TsSpreadBIFF8Reader }
destructor TsSpreadBIFF8Reader.Destroy;
begin
if Assigned(FSharedStringTable) then FSharedStringTable.Free;
inherited;
end;
function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream;
const ALength: WORD): WideString;
var
StringFlags: BYTE;
DecomprStrValue: WideString;
AnsiStrValue: ansistring;
RunsCounter: WORD;
AsianPhoneticBytes: DWORD;
i: Integer;
j: SizeUInt;
lLen: SizeInt;
RecordType: WORD;
RecordSize: WORD;
C: char;
begin
StringFlags:=AStream.ReadByte;
Dec(PendingRecordSize);
if StringFlags and 4 = 4 then begin
//Asian phonetics
//Read Asian phonetics Length (not used)
AsianPhoneticBytes:=DWordLEtoN(AStream.ReadDWord);
end;
if StringFlags and 8 = 8 then begin
//Rich string
RunsCounter:=WordLEtoN(AStream.ReadWord);
dec(PendingRecordSize,2);
end;
if StringFlags and 1 = 1 Then begin
//String is WideStringLE
if (ALength*SizeOf(WideChar)) > PendingRecordSize then begin
SetLength(Result,PendingRecordSize div 2);
AStream.ReadBuffer(Result[1],PendingRecordSize);
Dec(PendingRecordSize,PendingRecordSize);
end else begin
SetLength(Result,ALength);
AStream.ReadBuffer(Result[1],ALength * SizeOf(WideChar));
Dec(PendingRecordSize,ALength * SizeOf(WideChar));
end;
Result:=WideStringLEToN(Result);
end else begin
//String is 1 byte per char, this is UTF-16 with the high byte ommited because it is zero
//so decompress and then convert
lLen:=ALength;
SetLength(DecomprStrValue, lLen);
for i := 1 to lLen do
begin
C:=WideChar(AStream.ReadByte());
DecomprStrValue[i] := C;
Dec(PendingRecordSize);
if (PendingRecordSize<=0) and (i<lLen) then begin
//A CONTINUE may happend here
RecordType := WordLEToN(AStream.ReadWord);
RecordSize := WordLEToN(AStream.ReadWord);
if RecordType<>INT_EXCEL_ID_CONTINUE then begin
Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.');
end else begin
PendingRecordSize:=RecordSize;
DecomprStrValue:=copy(DecomprStrValue,1,i)+ReadWideString(AStream,ALength-i);
break;
end;
end;
end;
Result := DecomprStrValue;
end;
if StringFlags and 8 = 8 then begin
//Rich string (This only happened in BIFF8)
for j := 1 to RunsCounter do begin
if (PendingRecordSize<=0) then begin
//A CONTINUE may happend here
RecordType := WordLEToN(AStream.ReadWord);
RecordSize := WordLEToN(AStream.ReadWord);
if RecordType<>INT_EXCEL_ID_CONTINUE then begin
Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.');
end else begin
PendingRecordSize:=RecordSize;
end;
end;
AStream.ReadWord;
AStream.ReadWord;
dec(PendingRecordSize,2*2);
end;
end;
if StringFlags and 4 = 4 then begin
//Asian phonetics
//Read Asian phonetics, discarded as not used.
SetLength(AnsiStrValue,AsianPhoneticBytes);
AStream.ReadBuffer(AnsiStrValue[1],AsianPhoneticBytes);
end;
end;
function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream;
const AUse8BitLength: Boolean): WideString;
var
Len: Word;
begin
if AUse8BitLength then
Len := AStream.ReadByte()
else
Len := WordLEtoN(AStream.ReadWord());
Result := ReadWideString(AStream, Len);
end;
procedure TsSpreadBIFF8Reader.ReadWorkbookGlobals(AStream: TStream;
AData: TsWorkbook);
var
SectionEOF: Boolean = False;
RecordType: Word;
CurStreamPos: Int64;
begin
Unused(AData);
// Clear existing fonts. They will be replaced by those from the file.
FWorkbook.RemoveAllFonts;
if Assigned(FSharedStringTable) then FreeAndNil(FSharedStringTable);
while (not SectionEOF) do begin
{ Read the record header }
RecordType := WordLEToN(AStream.ReadWord);
RecordSize := WordLEToN(AStream.ReadWord);
PendingRecordSize := RecordSize;
CurStreamPos := AStream.Position;
if RecordType <> INT_EXCEL_ID_CONTINUE then begin
case RecordType of
INT_EXCEL_ID_BOF : ;
INT_EXCEL_ID_BOUNDSHEET: ReadBoundSheet(AStream);
INT_EXCEL_ID_EOF : SectionEOF := True;
INT_EXCEL_ID_SST : ReadSST(AStream);
INT_EXCEL_ID_CODEPAGE : ReadCodepage(AStream);
INT_EXCEL_ID_FONT : ReadFont(AStream);
INT_EXCEL_ID_FORMAT : ReadFormat(AStream);
INT_EXCEL_ID_XF : ReadXF(AStream);
INT_EXCEL_ID_DATEMODE : ReadDateMode(AStream);
INT_EXCEL_ID_PALETTE : ReadPalette(AStream);
else
// nothing
end;
end;
// Make sure we are in the right position for the next record
AStream.Seek(CurStreamPos + RecordSize, soFromBeginning);
// Check for the end of the file
if AStream.Position >= AStream.Size then SectionEOF := True;
end;
end;
procedure TsSpreadBIFF8Reader.ReadWorksheet(AStream: TStream; AData: TsWorkbook);
var
SectionEOF: Boolean = False;
RecordType: Word;
CurStreamPos: Int64;
begin
FWorksheet := AData.AddWorksheet(FWorksheetNames.Strings[FCurrentWorksheet]);
while (not SectionEOF) do
begin
{ Read the record header }
RecordType := WordLEToN(AStream.ReadWord);
RecordSize := WordLEToN(AStream.ReadWord);
PendingRecordSize:=RecordSize;
CurStreamPos := AStream.Position;
case RecordType of
INT_EXCEL_ID_BLANK : ReadBlank(AStream);
INT_EXCEL_ID_MULBLANK: ReadMulBlank(AStream);
INT_EXCEL_ID_NUMBER : ReadNumber(AStream);
INT_EXCEL_ID_LABEL : ReadLabel(AStream);
INT_EXCEL_ID_FORMULA : ReadFormula(AStream);
INT_EXCEL_ID_STRING : ReadStringRecord(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);
// (RK) This record represents a cell that contains an RK value
// (encoded integer or floating-point value). If a floating-point
// value cannot be encoded to an RK value, a NUMBER record will be written.
// This record replaces the record INTEGER written in BIFF2.
INT_EXCEL_ID_RK : ReadRKValue(AStream);
INT_EXCEL_ID_MULRK : ReadMulRKValues(AStream);
INT_EXCEL_ID_LABELSST: ReadLabelSST(AStream); //BIFF8 only
INT_EXCEL_ID_COLINFO : ReadColInfo(AStream);
INT_EXCEL_ID_ROW : ReadRowInfo(AStream);
INT_EXCEL_ID_WINDOW2 : ReadWindow2(AStream);
INT_EXCEL_ID_PANE : ReadPane(AStream);
INT_EXCEL_ID_BOF : ;
INT_EXCEL_ID_EOF : SectionEOF := True;
else
// nothing
end;
// Make sure we are in the right position for the next record
AStream.Seek(CurStreamPos + RecordSize, soFromBeginning);
// Check for the end of the file
if AStream.Position >= AStream.Size then SectionEOF := True;
end;
end;
procedure TsSpreadBIFF8Reader.ReadBoundsheet(AStream: TStream);
var
Len: Byte;
WideName: WideString;
begin
{ Absolute stream position of the BOF record of the sheet represented
by this record }
// Just assume that they are in order
AStream.ReadDWord();
{ Visibility }
AStream.ReadByte();
{ Sheet type }
AStream.ReadByte();
{ Sheet name: 8-bit length }
Len := AStream.ReadByte();
{ Read string with flags }
WideName:=ReadWideString(AStream,Len);
FWorksheetNames.Add(UTF8Encode(WideName));
end;
function TsSpreadBIFF8Reader.ReadString(const AStream: TStream;
const ALength: WORD): UTF8String;
begin
Result := UTF16ToUTF8(ReadWideString(AStream, ALength));
end;
procedure TsSpreadBIFF8Reader.ReadFromFile(AFileName: string; AData: TsWorkbook);
var
MemStream: TMemoryStream;
OLEStorage: TOLEStorage;
OLEDocument: TOLEDocument;
begin
MemStream := TMemoryStream.Create;
OLEStorage := TOLEStorage.Create;
try
// Only one stream is necessary for any number of worksheets
OLEDocument.Stream := MemStream;
OLEStorage.ReadOLEFile(AFileName, OLEDocument,'Workbook');
// Check if the operation succeded
if MemStream.Size = 0 then raise Exception.Create('FPSpreadsheet: Reading the OLE document failed');
// Rewind the stream and read from it
MemStream.Position := 0;
ReadFromStream(MemStream, AData);
// Uncomment to verify if the data was correctly optained from the OLE file
// MemStream.SaveToFile(SysUtils.ChangeFileExt(AFileName, 'bin.xls'));
finally
MemStream.Free;
OLEStorage.Free;
end;
end;
procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream; AData: TsWorkbook);
var
BIFF8EOF: Boolean;
begin
{ Initializations }
FWorksheetNames := TStringList.Create;
FWorksheetNames.Clear;
FCurrentWorksheet := 0;
BIFF8EOF := False;
{ Read workbook globals }
ReadWorkbookGlobals(AStream, AData);
// Check for the end of the file
if AStream.Position >= AStream.Size then BIFF8EOF := True;
{ Now read all worksheets }
while (not BIFF8EOF) do
begin
//Safe to not read beyond assigned worksheet names.
if FCurrentWorksheet>FWorksheetNames.Count-1 then break;
ReadWorksheet(AStream, AData);
// Check for the end of the file
if AStream.Position >= AStream.Size then BIFF8EOF := True;
// Final preparations
Inc(FCurrentWorksheet);
end;
if not FPaletteFound then
FWorkbook.UsePalette(@PALETTE_BIFF8, Length(PALETTE_BIFF8));
{ Finalizations }
FWorksheetNames.Free;
end;
procedure TsSpreadBIFF8Reader.ReadBlank(AStream: TStream);
var
ARow, ACol: Cardinal;
XF: Word;
begin
{ Read row, column, and XF index from BIFF file }
ReadRowColXF(AStream, ARow, ACol, XF);
{ Add attributes to cell}
ApplyCellFormatting(ARow, ACol, XF);
end;
procedure TsSpreadBIFF8Reader.ReadLabel(AStream: TStream);
var
L: Word;
ARow, ACol: Cardinal;
XF: Word;
WideStrValue: WideString;
begin
{ BIFF Record data: Row, Column, XF Index }
ReadRowColXF(AStream, ARow, ACol, XF);
{ Byte String with 16-bit size }
L := WordLEtoN(AStream.ReadWord());
{ Read string with flags }
WideStrValue:=ReadWideString(AStream,L);
{ Save the data }
FWorksheet.WriteUTF8Text(ARow, ACol, UTF16ToUTF8(WideStrValue));
{Add attributes}
ApplyCellFormatting(ARow, ACol, XF);
end;
procedure TsSpreadBIFF8Reader.ReadRichString(const AStream: TStream);
var
L: Word;
B: WORD;
ARow, ACol: Cardinal;
XF: Word;
AStrValue: ansistring;
begin
ReadRowColXF(AStream, ARow, ACol, XF);
{ Byte String with 16-bit size }
L := WordLEtoN(AStream.ReadWord());
AStrValue:=ReadString(AStream,L);
{ Save the data }
FWorksheet.WriteUTF8Text(ARow, ACol, AStrValue);
//Read formatting runs (not supported)
B:=WordLEtoN(AStream.ReadWord);
for L := 0 to B-1 do begin
AStream.ReadWord; // First formatted character
AStream.ReadWord; // Index to FONT record
end;
{Add attributes}
ApplyCellFormatting(ARow, ACol, XF);
end;
{ Reads the cell address used in an RPN formula element. Evaluates the corresponding
bits to distinguish between absolute and relative addresses.
Overriding the implementation in xlscommon. }
procedure TsSpreadBIFF8Reader.ReadRPNCellAddress(AStream: TStream;
out ARow, ACol: Cardinal; out AFlags: TsRelFlags);
var
c: word;
begin
// Read row index (2 bytes)
ARow := WordLEToN(AStream.ReadWord);
// Read column index; it contains info on absolute/relative address
c := WordLEToN(AStream.ReadWord);
// Extract column index
ACol := c and MASK_EXCEL_COL_BITS_BIFF8;
// Extract info on absolute/relative addresses.
AFlags := [];
if (c and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol);
if (c and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow);
end;
{ Reads a cell range address used in an RPN formula element.
Evaluates the corresponding bits to distinguish between absolute and
relative addresses.
Overriding the implementation in xlscommon. }
procedure TsSpreadBIFF8Reader.ReadRPNCellRangeAddress(AStream: TStream;
out ARow1, ACol1, ARow2, ACol2: Cardinal; out AFlags: TsRelFlags);
var
c1, c2: word;
begin
// Read row index of first and last rows (2 bytes, each)
ARow1 := WordLEToN(AStream.ReadWord);
ARow2 := WordLEToN(AStream.ReadWord);
// Read column index of first and last columns; they contain info on
// absolute/relative address
c1 := WordLEToN(AStream.ReadWord);
c2 := WordLEToN(AStream.ReadWord);
// Extract column index of rist and last columns
ACol1 := c1 and MASK_EXCEL_COL_BITS_BIFF8;
ACol2 := c2 and MASK_EXCEL_COL_BITS_BIFF8;
// Extract info on absolute/relative addresses.
AFlags := [];
if (c1 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol);
if (c1 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow);
if (c2 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol2);
if (c2 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow2);
end;
procedure TsSpreadBIFF8Reader.ReadSST(const AStream: TStream);
var
Items: DWORD;
StringLength, CurStrLen: WORD;
LString: String;
ContinueIndicator: WORD;
begin
//Reads the shared string table, only compatible with BIFF8
if not Assigned(FSharedStringTable) then begin
//First time SST creation
FSharedStringTable:=TStringList.Create;
DWordLEtoN(AStream.ReadDWord); //Apparences not used
Items:=DWordLEtoN(AStream.ReadDWord);
Dec(PendingRecordSize,8);
end else begin
//A second record must not happend. Garbage so skip.
Exit;
end;
while Items>0 do begin
StringLength:=0;
StringLength:=WordLEtoN(AStream.ReadWord);
Dec(PendingRecordSize,2);
LString:='';
// This loop takes care of the string being split between the STT and the CONTINUE, or between CONTINUE records
while PendingRecordSize>0 do
begin
if StringLength>0 then
begin
//Read a stream of zero length reads all the stream.
LString:=LString+ReadString(AStream, StringLength);
end
else
begin
//String of 0 chars in length, so just read it empty, reading only the mandatory flags
AStream.ReadByte; //And discard it.
Dec(PendingRecordSize);
//LString:=LString+'';
end;
// Check if the record finished and we need a CONTINUE record to go on
if (PendingRecordSize<=0) and (Items>1) then
begin
//A Continue will happend, read the
//tag and continue linking...
ContinueIndicator:=WordLEtoN(AStream.ReadWord);
if ContinueIndicator<>INT_EXCEL_ID_CONTINUE then begin
Raise Exception.Create('[TsSpreadBIFF8Reader.ReadSST] Expected CONTINUE record not found.');
end;
PendingRecordSize:=WordLEtoN(AStream.ReadWord);
CurStrLen := Length(UTF8ToUTF16(LString));
if StringLength<CurStrLen then Exception.Create('[TsSpreadBIFF8Reader.ReadSST] StringLength<CurStrLen');
Dec(StringLength, CurStrLen); //Dec the used chars
if StringLength=0 then break;
end else begin
break;
end;
end;
FSharedStringTable.Add(LString);
{$ifdef FPSPREADDEBUG}
WriteLn('Adding shared string: ' + LString);
{$endif}
dec(Items);
end;
end;
procedure TsSpreadBIFF8Reader.ReadLabelSST(const AStream: TStream);
var
ACol,ARow: Cardinal;
XF: WORD;
SSTIndex: DWORD;
begin
ReadRowColXF(AStream, ARow, ACol, XF);
SSTIndex := DWordLEtoN(AStream.ReadDWord);
if SizeInt(SSTIndex) >= FSharedStringTable.Count then begin
Raise Exception.CreateFmt('Index %d in SST out of range (0-%d)',[Integer(SSTIndex),FSharedStringTable.Count-1]);
end;
FWorksheet.WriteUTF8Text(ARow, ACol, FSharedStringTable[SSTIndex]);
{Add attributes}
ApplyCellFormatting(ARow, ACol, XF);
end;
{ Helper function for reading a string with 8-bit length. }
function TsSpreadBIFF8Reader.ReadString_8bitLen(AStream: TStream): String;
var
s: widestring;
begin
s := ReadWideString(AStream, true);
Result := UTF8Encode(s);
end;
procedure TsSpreadBIFF8Reader.ReadStringRecord(AStream: TStream);
var
s: String;
begin
s := ReadWideString(AStream, false);
if (FIncompleteCell <> nil) and (s <> '') then begin
FIncompleteCell^.UTF8StringValue := UTF8Encode(s);
FIncompleteCell^.ContentType := cctUTF8String;
end;
FIncompleteCell := nil;
end;
procedure TsSpreadBIFF8Reader.ReadXF(const AStream: TStream);
function FixLineStyle(dw: DWord): TsLineStyle;
{ Not all line styles defined in BIFF8 are supported by fpspreadsheet. }
begin
case dw of
$01..$07: result := TsLineStyle(dw-1);
// $07: Result := lsDotted;
else Result := lsDashed;
end;
end;
type
TXFRecord = packed record // see p. 224
FontIndex: Word; // Offset 0, Size 2
FormatIndex: Word; // Offset 2, Size 2
XFType_CellProt_ParentStyleXF: Word; // Offset 4, Size 2
Align_TextBreak: Byte; // Offset 6, Size 1
XFRotation: Byte; // Offset 7, Size 1
Indent_Shrink_TextDir: Byte; // Offset 8, Size 1
UnusedAttrib: Byte; // Offset 9, Size 1
Border_Background_1: DWord; // Offset 10, Size 4
Border_Background_2: DWord; // Offset 14, Size 4
Border_Background_3: DWord; // Offset 18, Size 2
end;
var
lData: TXFListData;
xf: TXFRecord;
b: Byte;
dw: DWord;
fill: Integer;
begin
AStream.ReadBuffer(xf, SizeOf(xf));
lData := TXFListData.Create;
// Font index
lData.FontIndex := WordLEToN(xf.FontIndex);
// Format index
lData.FormatIndex := WordLEToN(xf.FormatIndex);
// Horizontal text alignment
b := xf.Align_TextBreak AND MASK_XF_HOR_ALIGN;
if (b <= ord(High(TsHorAlignment))) then
lData.HorAlignment := TsHorAlignment(b)
else
lData.HorAlignment := haDefault;
// Vertical text alignment
b := (xf.Align_TextBreak AND MASK_XF_VERT_ALIGN) shr 4;
if (b + 1 <= ord(high(TsVertAlignment))) then
lData.VertAlignment := tsVertAlignment(b + 1) // + 1 due to vaDefault
else
lData.VertAlignment := vaDefault;
// Word wrap
lData.WordWrap := (xf.Align_TextBreak and MASK_XF_TEXTWRAP) <> 0;
// TextRotation
case xf.XFRotation of
XF_ROTATION_HORIZONTAL : lData.TextRotation := trHorizontal;
XF_ROTATION_90DEG_CCW : ldata.TextRotation := rt90DegreeCounterClockwiseRotation;
XF_ROTATION_90DEG_CW : lData.TextRotation := rt90DegreeClockwiseRotation;
XF_ROTATION_STACKED : lData.TextRotation := rtStacked;
end;
// Cell borders
xf.Border_Background_1 := DWordLEToN(xf.Border_Background_1);
lData.Borders := [];
lData.BorderStyles := DEFAULT_BORDERSTYLES;
// the 4 masked bits encode the line style of the border line. 0 = no line
dw := xf.Border_Background_1 and MASK_XF_BORDER_LEFT;
if dw <> 0 then begin
Include(lData.Borders, cbWest);
lData.BorderStyles[cbWest].LineStyle := FixLineStyle(dw);
end;
dw := xf.Border_Background_1 and MASK_XF_BORDER_RIGHT;
if dw <> 0 then begin
Include(lData.Borders, cbEast);
lData.BorderStyles[cbEast].LineStyle := FixLineStyle(dw shr 4);
end;
dw := xf.Border_Background_1 and MASK_XF_BORDER_TOP;
if dw <> 0 then begin
Include(lData.Borders, cbNorth);
lData.BorderStyles[cbNorth].LineStyle := FixLineStyle(dw shr 8);
end;
dw := xf.Border_Background_1 and MASK_XF_BORDER_BOTTOM;
if dw <> 0 then begin
Include(lData.Borders, cbSouth);
lData.BorderStyles[cbSouth].LineStyle := FixLineStyle(dw shr 12);
end;
// Border line colors
lData.BorderStyles[cbWest].Color := (xf.Border_Background_1 and MASK_XF_BORDER_LEFT_COLOR) shr 16;
lData.BorderStyles[cbEast].Color := (xf.Border_Background_1 and MASK_XF_BORDER_RIGHT_COLOR) shr 23;
lData.BorderStyles[cbNorth].Color := (xf.Border_Background_2 and MASK_XF_BORDER_TOP_COLOR);
lData.BorderStyles[cbSouth].Color := (xf.Border_Background_2 and MASK_XF_BORDER_BOTTOM_COLOR) shr 7;
// Background fill pattern
fill := (xf.Border_Background_2 and MASK_XF_BACKGROUND_PATTERN) shr 26;
// Background color
xf.Border_Background_3 := DWordLEToN(xf.Border_Background_3);
if fill <> 0 then
lData.BackgroundColor := xf.Border_Background_3 and $007F
else
lData.BackgroundColor := scTransparent; // this means "no fill"
// Add the XF to the list
FXFList.Add(lData);
end;
procedure TsSpreadBIFF8Reader.ReadFont(const AStream: TStream);
var
lCodePage: Word;
lHeight: Word;
lOptions: Word;
lColor: Word;
lWeight: Word;
Len: Byte;
font: TsFont;
begin
font := TsFont.Create;
{ Height of the font in twips = 1/20 of a point }
lHeight := WordLEToN(AStream.ReadWord); // WordToLE(200)
font.Size := lHeight/20;
{ Option flags }
lOptions := WordLEToN(AStream.ReadWord);
font.Style := [];
if lOptions and $0001 <> 0 then Include(font.Style, fssBold);
if lOptions and $0002 <> 0 then Include(font.Style, fssItalic);
if lOptions and $0004 <> 0 then Include(font.Style, fssUnderline);
if lOptions and $0008 <> 0 then Include(font.Style, fssStrikeout);
{ Colour index }
lColor := WordLEToN(AStream.ReadWord);
//font.Color := TsColor(lColor - 8); // Palette colors have an offset 8
font.Color := tsColor(lColor);
{ Font weight }
lWeight := WordLEToN(AStream.ReadWord);
if lWeight = 700 then Include(font.Style, fssBold);
{ Escape type }
AStream.ReadWord();
{ Underline type }
if AStream.ReadByte > 0 then Include(font.Style, fssUnderline);
{ Font family }
AStream.ReadByte();
{ Character set }
lCodepage := AStream.ReadByte();
{$ifdef FPSPREADDEBUG}
WriteLn('Reading Font Codepage='+IntToStr(lCodepage));
{$endif}
{ Not used }
AStream.ReadByte();
{ Font name: Unicodestring, char count in 1 byte }
Len := AStream.ReadByte();
font.FontName := ReadString(AStream, Len);
{ Add font to workbook's font list }
FWorkbook.AddFont(font);
end;
// Read the FORMAT record for formatting numerical data
procedure TsSpreadBIFF8Reader.ReadFormat(AStream: TStream);
var
fmtString: String;
fmtIndex: Integer;
begin
// Record FORMAT, BIFF 8 (5.49):
// Offset Size Contents
// 0 2 Format index used in other records
// 2 var Number format string (Unicode string, 16-bit string length)
// From BIFF5 on: indexes 0..163 are built in
fmtIndex := WordLEtoN(AStream.ReadWord);
// 2 var. Number format string (Unicode string, 16-bit string length, ➜2.5.3)
fmtString := UTF8Encode(ReadWideString(AStream, False));
// Analyze the format string and add format to the list
NumFormatList.AnalyzeAndAdd(fmtIndex, fmtString);
end;
{*******************************************************************
* Initialization section
*
* Registers this reader / writer on fpSpreadsheet
* Converts the palette to litte-endian
*
*******************************************************************}
initialization
RegisterSpreadFormat(TsSpreadBIFF8Reader, TsSpreadBIFF8Writer, sfExcel8);
MakeLEPalette(@PALETTE_BIFF8, Length(PALETTE_BIFF8));
end.