{ fpolestorage.pas Writes an OLE document AUTHORS: Felipe Monteiro de Carvalho Properties of this unit for creating OLE documents: * Can only create documents with one stream. It's not possible to create real directory structures like the OLE format supports. This is no problem for most applications. * No limitations interfere with the creation of multi-sheet Excel files The Windows only code, which calls COM to write the documents should work very well. The cross-platform code at this moment has some limitations, but should work for most documents. Some limitations are: * Supports only 1 stream in the file * Fixed sectors size of 512 bytes * Fixed short sector size of 64 bytes * Never allocates more space for the MSAT, limiting the SAT to 109 sectors, which means a total * Never allocates more then 1 sector for the SAT, so the document may have only up to 512 / 4 = 128 sectors } unit fpolestorage; {$ifdef fpc} {$mode delphi}{$H+} {$endif} interface {$ifdef Windows} {$define FPOLESTORAGE_USE_COM} {$endif} uses {$ifdef FPOLESTORAGE_USE_COM} ActiveX, ComObj, {$endif} Classes, SysUtils, Math, fpsutils; type { Describes an OLE Document } TOLEDocument = record // Information about the document Stream: TMemoryStream; end; { TOLEStorage } TOLEStorage = class private { Information filled by the write routines for the helper routines } {$ifdef FPOLESTORAGE_USE_COM} FStorage: IStorage; FStream: IStream; {$endif} { Fields for the read routines } FReadingStreamSize: Int64; { Fields for both } FOLEDocument: TOLEDocument; FUseShortSectors: Boolean; FNumSATSectors, FNumStreamSectors: Cardinal; FNumStreamShortSectors: Cardinal; { Writer Helper routines } procedure WriteOLEHeader(AStream: TStream); procedure WriteSectorAllocationTable(AStream: TStream); procedure WriteDirectoryStream(AStream: TStream; const ABookStreamName: string='Book'); procedure WriteDirectoryEntry(AStream: TStream; AName: widestring; EntryType, EntryColor: Byte; AIsStorage: Boolean; AFirstSecID, AStreamSize: Cardinal); procedure WriteShortSectorAllocationTable(AStream: TStream); procedure WriteUserStream(ADest, ASource: TStream); { Reader helper routines } procedure ReadOLEHeader(AStream: TStream); procedure ReadSectorAllocationTable({%H-}AStream: TStream); procedure ReadDirectoryStream(AStream: TStream); procedure ReadDirectoryEntry(AStream: TStream; out AName: widestring; out EntryType, EntryColor: Byte; out AIsStorage: Boolean; out AFirstSecID, AStreamSize: Cardinal); procedure ReadShortSectorAllocationTable({%H-}AStream: TStream); procedure ReadUserStream(ADest, ASource: TStream); public constructor Create; destructor Destroy; override; procedure WriteOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const AOverwriteExisting: Boolean = False; const AStreamName: UTF8String='Book'); procedure ReadOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const {%H-}AStreamName: UTF8String='Book'); procedure FreeOLEDocumentData(AOLEDocument: TOLEDocument); end; implementation const INT_OLE_SECTOR_SIZE = 512; // in bytes {%H-}INT_OLE_SECTOR_DWORD_SIZE = 512 div 4; // in dwords INT_OLE_SHORT_SECTOR_SIZE = 64; // in bytes INT_OLE_MIN_SIZE_FOR_STANDARD_STREAMS = 4096; INT_OLE_DIR_ENTRY_TYPE_EMPTY = 0; INT_OLE_DIR_ENTRY_TYPE_USER_STREAM = 2; INT_OLE_DIR_ENTRY_TYPE_ROOT_STORAGE = 5; INT_OLE_DIR_COLOR_RED = 0; INT_OLE_DIR_COLOR_BLACK = 1; { TOLEStorage } { 4.1 Compound Document Header Contents The header is always located at the beginning of the file, and its size is exactly 512 bytes. This implies that the first sector (with SecID 0) always starts at file offset 512. } procedure TOLEStorage.WriteOLEHeader(AStream: TStream); var i: Integer; begin { Contents of the compound document header structure: Offset Size Contents 0 8 Compound document file identifier: D0H CFH 11H E0H A1H B1H 1AH E1H } AStream.WriteByte($D0); AStream.WriteByte($CF); AStream.WriteByte($11); AStream.WriteByte($E0); AStream.WriteByte($A1); AStream.WriteByte($B1); AStream.WriteByte($1A); AStream.WriteByte($E1); { 8 16 Unique identifier (UID) of this file (not of interest in the following, may be all 0) } AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); { 24 2 Revision number of the file format (most used is 003EH) } AStream.WriteWord(WordToLE($003E)); { 26 2 Version number of the file format (most used is 0003H) } AStream.WriteWord(WordToLE($0003)); { 28 2 Byte order identifier (➜4.2): FEH FFH = Little-Endian FFH FEH = Big-Endian Real applications only use Little-Endian, so we follow that } AStream.WriteByte($FE); AStream.WriteByte($FF); { 30 2 Size of a sector in the compound document file (➜3.1) in power-of-two (ssz), real sector size is sec_size = 2ssz bytes (minimum value is 7 which means 128 bytes, most used value is 9 which means 512 bytes) } AStream.WriteWord(WordToLE($0009)); { 32 2 Size of a short-sector in the short-stream container stream (➜6.1) in power-of-two (sssz), real short-sector size is short_sec_size = 2sssz bytes (maximum value is sector size ssz, see above, most used value is 6 which means 64 bytes) } AStream.WriteWord(WordToLE($0006)); { 34 10 Not used } AStream.WriteDWord($0); AStream.WriteDWord($0); AStream.WriteWord($0); { 44 4 Total number of sectors used for the sector allocation table (➜5.2) } AStream.WriteDWord(DWordToLE(1)); { 48 4 SecID of first sector of the directory stream (➜7) } AStream.WriteDWord(DWordToLE($01)); { 52 4 Not used } AStream.WriteDWord($0); { 56 4 Minimum size of a standard stream (in bytes, minimum allowed and most used size is 4096 bytes), streams with an actual size smaller than (and not equal to) this value are stored as short-streams (➜6) } AStream.WriteDWord(DWordToLE(4096)); { 60 4 SecID of first sector of the short-sector allocation table (➜6.2), or –2 (End Of Chain SecID, ➜3.1) if not extant } AStream.WriteDWord(DWordToLE(2)); { 64 4 Total number of sectors used for the short-sector allocation table (➜6.2) } AStream.WriteDWord(DWordToLE(1)); { 68 4 SecID of first sector of the master sector allocation table (➜5.1), or –2 (End Of Chain SecID, ➜3.1) if no additional sectors used } AStream.WriteDWord(DWORD(IntegerToLE(-2))); { 72 4 Total number of sectors used for the master sector allocation table (➜5.1) } AStream.WriteDWord(0); { 76 436 First part of the master sector allocation table (➜5.1) containing 109 SecIDs } AStream.WriteDWord(0); for i := 1 to 108 do AStream.WriteDWord($FFFFFFFF); end; { The file is organized as following using Short Sectors: HEADER SECTOR 0 - SAT SECTOR 1 - Directory stream SECTOR 2 - Short SAT SECTOR 3 and on - User data And this SAT will describe that. This results in the following SecID array for the SAT: Array indexes 0 1 2 3 ... N-1 N ... SecID array –3 –1 –2 4 ... -2 -1 ... As expected, sector 0 is marked with the special SAT SecID (➜3.1). Sector 1 and all sectors starting with sector 5 are not used (special Free SecID with value –1). Without Short Sectors the file will be similar, but more compact: HEADER SECTOR 0 - SAT SECTOR 1 - Directory stream SECTOR 2 and on - User data } procedure TOLEStorage.WriteSectorAllocationTable(AStream: TStream); var i, CurrentPos, NextSecID: Integer; begin { Example values: 00000200H FD FF FF FF FF FF FF FF FE FF FF FF 04 00 00 00 00000210H FE FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF And after that only $FFFFFFFF until $400 } AStream.WriteDWord(DWordToLE($FFFFFFFD)); // SAT AStream.WriteDWord($FFFFFFFE); // Empty, doesn't work with $FFFFFFFF // If we don't use short sectors we won't write a section for their SSAT if FUseShortSectors then begin AStream.WriteDWord(DWordToLE($FFFFFFFE)); // Start and End of Short SAT CurrentPos := $200 + 12; NextSecID := $00000004; end else begin CurrentPos := $200 + 8; NextSecID := $00000003; end; // Now write the user data for i := 2 to FNumStreamSectors do begin AStream.WriteDWord(DWordToLE(NextSecID)); Inc(NextSecID); CurrentPos := CurrentPos + 4; end; AStream.WriteDWord(DWordToLE($FFFFFFFE)); // End of user data CurrentPos := CurrentPos + 4; // Fill the rest of the sector with $FF for i := 1 to ($400 - CurrentPos) do AStream.WriteByte($FF); end; { 7.2.1 Directory Entry Structure The size of each directory entry is exactly 128 bytes. The formula to calculate an offset in the directory stream from a DirID is as follows: dir_entry_pos(DirID) = DirID ∙ 128 } procedure TOLEStorage.WriteDirectoryEntry(AStream: TStream; AName: widestring; EntryType, EntryColor: Byte; AIsStorage: Boolean; AFirstSecID, AStreamSize: Cardinal); var EntryName: array[0..31] of WideChar; begin { Contents of the directory entry structure: Offset Size Contents 0 64 Character array of the name of the entry, always 16-bit Unicode characters, with trailing zero character (results in a maximum name length of 31 characters) 00000400H 52 00 6F 00 6F 00 74 00 20 00 45 00 6E 00 74 00 } EntryName := AName; AStream.WriteBuffer(EntryName, 64); {Root Storage #1 00000440H 16 00 05 00 FF FF FF FF FF FF FF FF 01 00 00 00 Book #2 000004C0H 0A 00 02 01 FF FF FF FF FF FF FF FF FF FF FF FF Item #3 e #4 00000540H 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF 64 2 Size of the used area of the character buffer of the name (not character count), including the trailing zero character (e.g. 12 for a name with 5 characters: (5+1)∙2 = 12) 66 1 Type of the entry: 00H = Empty 03H = LockBytes (unknown) 01H = User storage 04H = Property (unknown) 02H = User stream 05H = Root storage 67 1 Node colour of the entry: 00H = Red 01H = Black 68 4 DirID of the left child node inside the red-black tree of all direct members of the parent storage (if this entry is a user storage or stream, ➜7.1), –1 if there is no left child 72 4 DirID of the right child node inside the red-black tree of all direct members of the parent storage (if this entry is a user storage or stream, ➜7.1), –1 if there is no right child 76 4 DirID of the root node entry of the red-black tree of all storage members (if this entry is a storage, ➜7.1), –1 otherwise } if AName = #0 then AStream.WriteWord($0000) else AStream.WriteWord(WordToLE(Length(AName) * 2)); AStream.WriteByte(EntryType); AStream.WriteByte(EntryColor); AStream.WriteDWord(DWordToLE($FFFFFFFF)); AStream.WriteDWord(DWordToLE($FFFFFFFF)); if AIsStorage then AStream.WriteDWord(DWordToLE($00000001)) else AStream.WriteDWord(DWordToLE($FFFFFFFF)); {00000450H 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 16 Unique identifier, if this is a storage (not of interest in the following, may be all 0) 00000460H 00 00 00 00 00 00 00 00 00 00 00 00 00 4E 67 0E 00000470H 39 6F C9 01 96 4 User flags (not of interest in the following, may be all 0) 100 8 Time stamp of creation of this entry (➜7.2.3). Most implementations do not write a valid time stamp, but fill up this space with zero bytes. 108 8 Time stamp of last modification of this entry (➜7.2.3). Most implementations do not write a valid time stamp, but fill up this space with zero bytes. } AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); AStream.WriteDWord(0); {Root Storage #1 00000470H XX XX XX XX 03 00 00 00 40 03 00 00 00 00 00 00 Book #2 000004F0H XX XX XX XX 00 00 00 00 3F 03 00 00 00 00 00 00 Item #3 e #4 00000570H XX XX XX XX 00 00 00 00 00 00 00 00 00 00 00 00 First 4 bytes still with the timestamp. 116 4 SecID of first sector or short-sector, if this entry refers to a stream (➜7.2.2), SecID of first sector of the short-stream container stream (➜6.1), if this is the root storage entry, 0 otherwise 120 4 Total stream size in bytes, if this entry refers to a stream (➜7.2.2), total size of the short-stream container stream (➜6.1), if this is the root storage entry, 0 otherwise 124 4 Not used } AStream.WriteDWord(DWordToLE(AFirstSecID)); AStream.WriteDWord(DWordToLE(AStreamSize)); AStream.WriteDWord(DWordToLE($00000000)); end; procedure TOLEStorage.WriteDirectoryStream(AStream: TStream; const ABookStreamName: string='Book'); var FContainerSize: Cardinal; begin { Size of the container stream } FContainerSize := Ceil(FOLEDocument.Stream.Size / INT_OLE_SECTOR_SIZE) * INT_OLE_SECTOR_SIZE; if FUseShortSectors then begin WriteDirectoryEntry(AStream, 'Root Entry'#0, INT_OLE_DIR_ENTRY_TYPE_ROOT_STORAGE, INT_OLE_DIR_COLOR_RED, True, $00000003, FContainerSize); WriteDirectoryEntry(AStream, Utf8Decode(ABookStreamName+#0), INT_OLE_DIR_ENTRY_TYPE_USER_STREAM, INT_OLE_DIR_COLOR_BLACK, False, 0, FOLEDocument.Stream.Size); WriteDirectoryEntry(AStream, #0, INT_OLE_DIR_ENTRY_TYPE_EMPTY, INT_OLE_DIR_COLOR_RED, False, 0, $00000000); WriteDirectoryEntry(AStream, #0, INT_OLE_DIR_ENTRY_TYPE_EMPTY, INT_OLE_DIR_COLOR_RED, False, 0, $00000000); end else begin WriteDirectoryEntry(AStream, 'Root Entry'#0, INT_OLE_DIR_ENTRY_TYPE_ROOT_STORAGE, INT_OLE_DIR_COLOR_RED, True, $FFFFFFFE, 0); WriteDirectoryEntry(AStream, Utf8Decode(ABookStreamName+#0), INT_OLE_DIR_ENTRY_TYPE_USER_STREAM, INT_OLE_DIR_COLOR_BLACK, False, $00000002, FOLEDocument.Stream.Size); WriteDirectoryEntry(AStream, #0, INT_OLE_DIR_ENTRY_TYPE_EMPTY, INT_OLE_DIR_COLOR_RED, False, 0, $00000000); WriteDirectoryEntry(AStream, #0, INT_OLE_DIR_ENTRY_TYPE_EMPTY, INT_OLE_DIR_COLOR_RED, False, 0, $00000000); end; end; { 8.4 Short-Sector Allocation Table The short-sector allocation table (SSAT) is an array of SecIDs and contains the SecID chains (➜3.2) of all short- streams, similar to the sector allocation table (➜5.2) that contains the SecID chains of standard streams. The first SecID of the SSAT is contained in the header (➜4.1), the remaining SecID chain is contained in the SAT. The SSAT is built by reading and concatenating the contents of all sectors. Contents of a sector of the SSAT (sec_size is the size of a sector in bytes, see ➜4.1): Offset Size Contents 0 sec_size Array of sec_size/4 SecIDs of the SSAT The SSAT will be used similarly to the SAT (➜5.2) with the difference that the SecID chains refer to short-sectors in the short-stream container stream (➜6.1). This results in the following SecID array for the SSAT: Array indexes 0 1 2 3 4 5 6 7 8 9 1011...4142434445464748495051525354... SecID array 1 2 3 4 5 6 7 8 9 101112...42434445–247–2–250515253–2–1... All short-sectors starting with sector 54 are not used (special Free SecID with value –1). } procedure TOLEStorage.WriteShortSectorAllocationTable(AStream: TStream); var i, NextShortSecID, CurrentPos: Integer; begin CurrentPos := $800; NextShortSecID := $00000001; for i := 2 to FNumStreamShortSectors do begin AStream.WriteDWord(DWordToLE(NextShortSecID)); Inc(NextShortSecID); Inc(CurrentPos, 4); end; AStream.WriteDWord(DWordToLE($FFFFFFFE)); Inc(CurrentPos, 4); for i := 1 to ($A00 - CurrentPos) do AStream.WriteByte($FF); end; procedure TOLEStorage.WriteUserStream(ADest, ASource: TStream); var i, PadSize: Integer; begin ADest.CopyFrom(ASource, 0); // Pad the sector with zeroes PadSize := INT_OLE_SECTOR_SIZE - (ASource.Size mod INT_OLE_SECTOR_SIZE); for i := 1 to PadSize do ADest.WriteByte($00); end; procedure TOLEStorage.ReadOLEHeader(AStream: TStream); var {%H-}BaseAddr: Int64; begin BaseAddr := AStream.Position; { 28 2 Byte order identifier (➜4.2): FEH FFH = Little-Endian FFH FEH = Big-Endian } // For now just assume little-endian { 30 2 Size of a sector in the compound document file (➜3.1) in power-of-two (ssz), real sector size is sec_size = 2ssz bytes (minimum value is 7 which means 128 bytes, most used value is 9 which means 512 bytes) } // Assume default value { 32 2 Size of a short-sector in the short-stream container stream (➜6.1) in power-of-two (sssz), real short-sector size is short_sec_size = 2sssz bytes (maximum value is sector size ssz, see above, most used value is 6 which means 64 bytes) } // Assume default value { 44 4 Total number of sectors used for the sector allocation table (➜5.2) } // Assume 1 { 48 4 SecID of first sector of the directory stream (➜7) } // Assume 1 { 56 4 Minimum size of a standard stream (in bytes, minimum allowed and most used size is 4096 bytes), streams with an actual size smaller than (and not equal to) this value are stored as short-streams (➜6) } // Assume 4096 { 60 4 SecID of first sector of the short-sector allocation table (➜6.2), or –2 (End Of Chain SecID, ➜3.1) if not extant } // Assume 2 { 64 4 Total number of sectors used for the short-sector allocation table (➜6.2) } // Assume 1 { 68 4 SecID of first sector of the master sector allocation table (➜5.1), or –2 (End Of Chain SecID, ➜3.1) if no additional sectors used } // Assume 2 { 72 4 Total number of sectors used for the master sector allocation table (➜5.1) } // Assume 0 { 76 436 First part of the master sector allocation table (➜5.1) containing 109 SecIDs } // Assume: 0, -1, -1, -1 ... end; procedure TOLEStorage.ReadSectorAllocationTable(AStream: TStream); begin end; procedure TOLEStorage.ReadDirectoryStream(AStream: TStream); var EntryName: widestring; EntryType, EntryColor: Byte; EntryIsStorage: Boolean; EntryFirstSecID, EntryStreamSize: Cardinal; begin ReadDirectoryEntry(AStream, EntryName, EntryType, EntryColor, EntryIsStorage, EntryFirstSecID, EntryStreamSize); ReadDirectoryEntry(AStream, EntryName, EntryType, EntryColor, EntryIsStorage, EntryFirstSecID, EntryStreamSize); FReadingStreamSize := EntryStreamSize; end; procedure TOLEStorage.ReadDirectoryEntry(AStream: TStream; out AName: widestring; out EntryType, EntryColor: Byte; out AIsStorage: Boolean; out AFirstSecID, AStreamSize: Cardinal); var BaseAddr: Int64; EntryName: array[0..63] of WideChar; begin BaseAddr := AStream.Position; { Contents of the directory entry structure: Offset Size Contents 0 64 Character array of the name of the entry, always 16-bit Unicode characters, with trailing zero character (results in a maximum name length of 31 characters) 00000400H 52 00 6F 00 6F 00 74 00 20 00 45 00 6E 00 74 00 } EntryName[0] := #0; // silence the compilere... AStream.ReadBuffer(EntryName, 64); AName := EntryName; {Root Storage #1 00000440H 16 00 05 00 FF FF FF FF FF FF FF FF 01 00 00 00 Book #2 000004C0H 0A 00 02 01 FF FF FF FF FF FF FF FF FF FF FF FF Item #3 e #4 00000540H 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF 64 2 Size of the used area of the character buffer of the name (not character count), including the trailing zero character (e.g. 12 for a name with 5 characters: (5+1)∙2 = 12) 66 1 Type of the entry: 00H = Empty 03H = LockBytes (unknown) 01H = User storage 04H = Property (unknown) 02H = User stream 05H = Root storage 67 1 Node colour of the entry: 00H = Red 01H = Black 68 4 DirID of the left child node inside the red-black tree of all direct members of the parent storage (if this entry is a user storage or stream, ➜7.1), –1 if there is no left child 72 4 DirID of the right child node inside the red-black tree of all direct members of the parent storage (if this entry is a user storage or stream, ➜7.1), –1 if there is no right child 76 4 DirID of the root node entry of the red-black tree of all storage members (if this entry is a storage, ➜7.1), –1 otherwise } AStream.ReadWord(); EntryType := AStream.ReadByte(); EntryColor := AStream.ReadByte(); AStream.ReadDWord(); AStream.ReadDWord(); AIsStorage := DWordLEtoN(AStream.ReadDWord) <> $FFFFFFFF; {Root Storage #1 00000470H XX XX XX XX 03 00 00 00 40 03 00 00 00 00 00 00 Book #2 000004F0H XX XX XX XX 00 00 00 00 3F 03 00 00 00 00 00 00 Item #3 e #4 00000570H XX XX XX XX 00 00 00 00 00 00 00 00 00 00 00 00 First 4 bytes still with the timestamp. 116 4 SecID of first sector or short-sector, if this entry refers to a stream (➜7.2.2), SecID of first sector of the short-stream container stream (➜6.1), if this is the root storage entry, 0 otherwise 120 4 Total stream size in bytes, if this entry refers to a stream (➜7.2.2), total size of the short-stream container stream (➜6.1), if this is the root storage entry, 0 otherwise 124 4 Not used } AStream.Seek(BaseAddr + $74, soFromBeginning); AFirstSecID := DWordLEtoN(AStream.ReadDWord); AStreamSize := DWordLEtoN(AStream.ReadDWord); AStream.ReadDWord(); end; procedure TOLEStorage.ReadShortSectorAllocationTable(AStream: TStream); begin end; procedure TOLEStorage.ReadUserStream(ADest, ASource: TStream); var i: Integer; begin // ADest.CopyFrom(ASource, FReadingStreamSize); for i := 1 to FReadingStreamSize do ADest.WriteByte(ASource.ReadByte); end; constructor TOLEStorage.Create; begin inherited Create; end; destructor TOLEStorage.Destroy; begin inherited Destroy; end; {@@ Writes the OLE document specified in AOLEDocument to the file with name AFileName. The routine will fail if the file already exists, or if the directory where it should be placed doesn't exist. } procedure TOLEStorage.WriteOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const AOverwriteExisting: Boolean; const AStreamName: UTF8String); var cbWritten: Cardinal; {%H-}lMode: Word; {$IFNDEF FPOLESTORAGE_USE_COM} AFileStream: TStream; {$ENDIF} begin // The behavior of LCL classes is failling to write to existing file, // But here we make this settable if AOverwriteExisting then lMode := fmCreate or fmOpenWrite else lMode := fmCreate; if (not AOverwriteExisting) and FileExists(AFileName) then Raise EStreamError.Createfmt('File already exists "%s"',[AFileName]); { Fill information for helper routines } FOLEDocument := AOLEDocument; { Calculate the number of sectors necessary for the stream } FNumStreamSectors := Ceil(AOLEDocument.Stream.Size / INT_OLE_SECTOR_SIZE); { Calculates the number of short sectors for the stream, if it applies } FUseShortSectors := AOLEDocument.Stream.Size < INT_OLE_MIN_SIZE_FOR_STANDARD_STREAMS; FNumStreamShortSectors := Ceil(AOLEDocument.Stream.Size / INT_OLE_SHORT_SECTOR_SIZE); { Numbers of sectors necessary for the SAT } FNumSATSectors := 1; // Ceil(FNumStreamSectors / INT_OLE_SECTOR_DWORD_SIZE); {$ifdef FPOLESTORAGE_USE_COM} { Initialize the Component Object Model (COM) before calling s functions } OleCheck(CoInitialize(nil)); { Create a Storage Object } OleCheck(StgCreateDocfile(PWideChar(WideString(AFileName)), STGM_READWRITE or STGM_CREATE or STGM_SHARE_EXCLUSIVE or STGM_DIRECT, 0, FStorage)); { Create a workbook stream in the storage. A BIFF5 file must have at least a workbook stream. This stream *must* be named 'Book', in BIFF8 it is 'Workbook', so use AStreamName } OleCheck(FStorage.CreateStream(PWideChar(UTF8Decode(AStreamName)), STGM_READWRITE or STGM_SHARE_EXCLUSIVE or STGM_DIRECT, 0, 0, FStream)); { Write all data } FStream.Write(FOLEDocument.Stream.Memory, FOLEDocument.Stream.Size, @cbWritten); {$else} AFileStream := TFileStream.Create(AFileName, lMode); try // Header WriteOLEHeader(AFileStream); // Record 0, the SAT WriteSectorAllocationTable(AFileStream); // Record 1, the directory stream WriteDirectoryStream(AFileStream,AStreamName); // Record 2, the Short SAT if FUseShortSectors then WriteShortSectorAllocationTable(AFileStream); // Records 3 and on (or 2 and on without Short Sectors), the user data WriteUserStream(AFileStream, FOLEDocument.Stream); finally AFileStream.Free; end; {$endif} end; {@@ Reads an OLE file. } procedure TOLEStorage.ReadOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const AStreamName: UTF8String='Book'); var AFileStream: TFileStream; CurrentSectorPos: Int64; begin FOLEDocument := AOLEDocument; if not Assigned(AOLEDocument.Stream) then AOLEDocument.Stream := TMemoryStream.Create; AFileStream := TFileStream.Create(AFileName, fmOpenRead); try // Header ReadOLEHeader(AFileStream); // Record 0, the SAT CurrentSectorPos := $200; AFileStream.Seek(CurrentSectorPos, soFromBeginning); ReadSectorAllocationTable(AFileStream); // Record 1, the directory stream CurrentSectorPos := $400; AFileStream.Seek(CurrentSectorPos, soFromBeginning); ReadDirectoryStream(AFileStream); // FReadingStreamSize is filled by ReadDirectoryStream FUseShortSectors := FReadingStreamSize < INT_OLE_MIN_SIZE_FOR_STANDARD_STREAMS; CurrentSectorPos := $600; // Record 2, the Short SAT, if exists if FUseShortSectors then begin AFileStream.Seek(CurrentSectorPos, soFromBeginning); ReadShortSectorAllocationTable(AFileStream); CurrentSectorPos := $800; end else CurrentSectorPos := $600; // Records 3 and on (or 2 and on without Short Sectors), the user data AFileStream.Seek(CurrentSectorPos, soFromBeginning); ReadUserStream(FOLEDocument.Stream, AFileStream); finally AFileStream.Free; end; end; {@@ Frees all internal objects storable in a TOLEDocument structure } procedure TOLEStorage.FreeOLEDocumentData(AOLEDocument: TOLEDocument); begin if Assigned(AOLEDocument.Stream) then FreeAndNil(AOLEDocument.Stream); end; end.