mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-05-28 04:22:52 +02:00
2265 lines
78 KiB
ObjectPascal
2265 lines
78 KiB
ObjectPascal
{ Copyright (C) <2005> <Andrew Haines> chmwriter.pas
|
|
|
|
This library is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU Library General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or (at your
|
|
option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
|
|
for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; if not, write to the Free Software Foundation,
|
|
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
}
|
|
{
|
|
See the file COPYING.FPC, included in this distribution,
|
|
for details about the copyright.
|
|
}
|
|
unit wikichmwriter;
|
|
{$MODE OBJFPC}{$H+}
|
|
{ $DEFINE LZX_USETHREADS}
|
|
|
|
interface
|
|
uses Classes,
|
|
wikiChmBase, wikichmtypes, wikichmspecialfiles, wikiHtmlIndexer, wikichmsitemap,
|
|
contnrs, Avl_Tree{$IFDEF LZX_USETHREADS}, lzxcompressthread{$ENDIF};
|
|
|
|
Const
|
|
DefaultHHC = 'Default.hhc';
|
|
DefaultHHK = 'Default.hhk';
|
|
|
|
Type
|
|
TGetDataFunc = function (const DataName: String; out PathInChm: String; out FileName: String; var Stream: TStream): Boolean of object;
|
|
// DataName : A FileName or whatever so that the getter can find and open the file to add
|
|
// PathInChm: This is the absolute path in the archive. i.e. /home/user/helpstuff/
|
|
// becomes '/' and /home/user/helpstuff/subfolder/ > /subfolder/
|
|
// FileName : /home/user/helpstuff/index.html > index.html
|
|
// Stream : the file opened with DataName should be written to this stream
|
|
|
|
Type
|
|
TStringIndex = Class // AVLTree needs wrapping in non automated reference type also used in filewriter.
|
|
TheString : String;
|
|
StrId : Integer;
|
|
end;
|
|
TUrlStrIndex = Class
|
|
UrlStr : String;
|
|
UrlStrId : Integer;
|
|
end;
|
|
|
|
{ TITSFWriter }
|
|
|
|
TITSFWriter = class(TObject)
|
|
FOnLastFile: TNotifyEvent;
|
|
private
|
|
ForceExit: Boolean;
|
|
FInternalFiles: TFileEntryList; // Contains a complete list of files in the chm including
|
|
FFrameSize: LongWord; // uncompressed files and special internal files of the chm
|
|
FCurrentStream: TStream; // used to buffer the files that are to be compressed
|
|
FCurrentIndex: Integer;
|
|
FOnGetFileData: TGetDataFunc;
|
|
FSection0: TMemoryStream;
|
|
FSection1: TStream; // Compressed Stream
|
|
FSection1Size: QWord;
|
|
FSection1ResetTable: TMemoryStream; // has a list of frame positions NOT window positions
|
|
FDirectoryListings: TStream;
|
|
FOutStream: TStream;
|
|
FFileNames: TStrings;
|
|
FDestroyStream: Boolean;
|
|
FTempStream: TStream;
|
|
FPostStream: TStream;
|
|
FWindowSize: LongWord;
|
|
FReadCompressedSize: QWord; // Current Size of Uncompressed data that went in Section1 (compressed)
|
|
FPostStreamActive: Boolean;
|
|
// Linear order of file
|
|
ITSFHeader: TITSFHeader;
|
|
HeaderSection0Table: TITSFHeaderEntry; // points to HeaderSection0
|
|
HeaderSection1Table: TITSFHeaderEntry; // points to HeaderSection1
|
|
HeaderSuffix: TITSFHeaderSuffix; //contains the offset of CONTENTSection0 from zero
|
|
HeaderSection0: TITSPHeaderPrefix;
|
|
HeaderSection1: TITSPHeader; // DirectoryListings header
|
|
FReadmeMessage : String;
|
|
// DirectoryListings
|
|
// CONTENT Section 0 (section 1 is contained in section 0)
|
|
// EOF
|
|
// end linear header parts
|
|
procedure InitITSFHeader;
|
|
procedure InitHeaderSectionTable;
|
|
procedure SetTempRawStream(const AValue: TStream);
|
|
procedure WriteHeader(Stream: TStream);
|
|
procedure CreateDirectoryListings;
|
|
procedure WriteDirectoryListings(Stream: TStream);
|
|
procedure WriteInternalFilesBefore; virtual;
|
|
procedure WriteInternalFilesAfter; virtual;
|
|
procedure StartCompressingStream;
|
|
procedure WriteREADMEFile;
|
|
procedure WriteFinalCompressedFiles; virtual;
|
|
procedure WriteSection0;
|
|
procedure WriteSection1;
|
|
procedure WriteDataSpaceFiles(const AStream: TStream);
|
|
|
|
procedure FileAdded(AStream: TStream; const AEntry: TFileEntryRec); virtual;
|
|
// callbacks for lzxcomp
|
|
function AtEndOfData: Longbool;
|
|
function GetData(Count: LongInt; Buffer: PByte): LongInt;
|
|
function WriteCompressedData(Count: Longint; Buffer: Pointer): LongInt;
|
|
procedure MarkFrame(UnCompressedTotal, CompressedTotal: LongWord);
|
|
// end callbacks
|
|
{$IFDEF LZX_USETHREADS}
|
|
// callbacks for lzx compress threads
|
|
function LTGetData(Sender: TLZXCompressor; WantedByteCount: Integer; Buffer: Pointer): Integer;
|
|
function LTIsEndOfFile(Sender: TLZXCompressor): Boolean;
|
|
procedure LTChunkDone(Sender: TLZXCompressor; CompressedSize: Integer; UncompressedSize: Integer; Buffer: Pointer);
|
|
procedure LTMarkFrame(Sender: TLZXCompressor; CompressedTotal: Integer; UncompressedTotal: Integer);
|
|
{$ENDIF}
|
|
// end callbacks
|
|
public
|
|
constructor Create(AOutStream: TStream; FreeStreamOnDestroy: Boolean); virtual;
|
|
destructor Destroy; override;
|
|
procedure Execute;
|
|
procedure AddStreamToArchive(AFileName, APath: String; AStream: TStream; Compress: Boolean = True);
|
|
procedure PostAddStreamToArchive(AFileName, APath: String; AStream: TStream; Compress: Boolean = True);
|
|
property WindowSize: LongWord read FWindowSize write FWindowSize default 2; // in $8000 blocks
|
|
property FrameSize: LongWord read FFrameSize write FFrameSize default 1; // in $8000 blocks
|
|
property FilesToCompress: TStrings read FFileNames;
|
|
property OnGetFileData: TGetDataFunc read FOnGetFileData write FOnGetFileData;
|
|
property OnLastFile: TNotifyEvent read FOnLastFile write FOnLastFile;
|
|
property OutStream: TStream read FOutStream;
|
|
property TempRawStream: TStream read FTempStream write SetTempRawStream;
|
|
property ReadmeMessage : String read fReadmeMessage write fReadmeMessage;
|
|
//property LocaleID: dword read ITSFHeader.LanguageID write ITSFHeader.LanguageID;
|
|
end;
|
|
|
|
{ TChmWriter }
|
|
|
|
TChmWriter = class(TITSFWriter)
|
|
private
|
|
FHasBinaryTOC: Boolean;
|
|
FHasBinaryIndex: Boolean;
|
|
FDefaultFont: String;
|
|
FDefaultPage: String;
|
|
FFullTextSearch: Boolean;
|
|
FFullTextSearchAvailable: Boolean;
|
|
FSearchTitlesOnly: Boolean;
|
|
FStringsStream: TMemoryStream; // the #STRINGS file
|
|
FTopicsStream: TMemoryStream; // the #TOPICS file
|
|
FURLTBLStream: TMemoryStream; // the #URLTBL file. has offsets of strings in URLSTR
|
|
FURLSTRStream: TMemoryStream; // the #URLSTR file
|
|
FFiftiMainStream: TMemoryStream;
|
|
FContextStream: TMemoryStream; // the #IVB file
|
|
FTitle: String;
|
|
FHasTOC: Boolean;
|
|
FHasIndex: Boolean;
|
|
FIndexedFiles: TIndexedWordList;
|
|
FAvlStrings : TAVLTree; // dedupe strings
|
|
FAvlURLStr : TAVLTree; // dedupe urltbl + binindex must resolve URL to topicid
|
|
SpareString : TStringIndex;
|
|
SpareUrlStr : TUrlStrIndex;
|
|
FWindows : TObjectList;
|
|
FDefaultWindow: String;
|
|
FTocName : String;
|
|
FIndexName : String;
|
|
protected
|
|
procedure FileAdded(AStream: TStream; const AEntry: TFileEntryRec); override;
|
|
private
|
|
procedure WriteInternalFilesBefore; override;
|
|
procedure WriteInternalFilesAfter; override;
|
|
procedure WriteFinalCompressedFiles; override;
|
|
procedure WriteSYSTEM;
|
|
procedure WriteITBITS;
|
|
procedure WriteSTRINGS;
|
|
procedure WriteTOPICS;
|
|
procedure WriteIVB; // context ids
|
|
procedure WriteURL_STR_TBL;
|
|
procedure WriteOBJINST;
|
|
procedure WriteFiftiMain;
|
|
procedure WriteWindows;
|
|
|
|
function AddString(AString: String): LongWord;
|
|
function AddURL(AURL: String; TopicsIndex: DWord): LongWord;
|
|
procedure CheckFileMakeSearchable(AStream: TStream; AFileEntry: TFileEntryRec);
|
|
function AddTopic(ATitle,AnUrl:AnsiString):integer;
|
|
function NextTopicIndex: Integer;
|
|
procedure Setwindows (AWindowList:TObjectList);
|
|
|
|
public
|
|
constructor Create(AOutStream: TStream; FreeStreamOnDestroy: Boolean); override;
|
|
destructor Destroy; override;
|
|
procedure AppendTOC(AStream: TStream);
|
|
procedure AppendBinaryTOCFromSiteMap(ASiteMap: TChmSiteMap);
|
|
procedure AppendBinaryIndexFromSiteMap(ASiteMap: TChmSiteMap;chw:boolean);
|
|
procedure AppendBinaryTOCStream(AStream: TStream);
|
|
procedure AppendBinaryIndexStream(IndexStream,DataStream,MapStream,Propertystream: TStream;chw:boolean);
|
|
procedure AppendIndex(AStream: TStream);
|
|
procedure AppendSearchDB(AName: String; AStream: TStream);
|
|
procedure AddContext(AContext: DWord; ATopic: String);
|
|
|
|
property Title: String read FTitle write FTitle;
|
|
property FullTextSearch: Boolean read FFullTextSearch write FFullTextSearch;
|
|
property SearchTitlesOnly: Boolean read FSearchTitlesOnly write FSearchTitlesOnly;
|
|
property HasBinaryTOC: Boolean read FHasBinaryTOC write FHasBinaryTOC;
|
|
property HasBinaryIndex: Boolean read FHasBinaryIndex write FHasBinaryIndex;
|
|
property DefaultFont: String read FDefaultFont write FDefaultFont;
|
|
property DefaultPage: String read FDefaultPage write FDefaultPage;
|
|
property Windows : TObjectlist read fwindows write setwindows;
|
|
property TOCName : String read FTocName write FTocName;
|
|
property IndexName : String read FIndexName write FIndexName;
|
|
property DefaultWindow : string read fdefaultwindow write fdefaultwindow;
|
|
end;
|
|
|
|
Function CompareStrings(Node1, Node2: Pointer): integer; // also used in filewriter
|
|
|
|
implementation
|
|
uses dateutils, sysutils, paslzxcomp, wikichmFiftiMain;
|
|
|
|
const
|
|
LZX_WINDOW_SIZE = 16; // 16 = 2 frames = 1 shl 16
|
|
LZX_FRAME_SIZE = $8000;
|
|
|
|
{$ifdef binindex}
|
|
procedure logentry(s:string);
|
|
begin
|
|
Writeln(s);
|
|
flush(stdout);
|
|
end;
|
|
{$endif}
|
|
{$I wikichmobjinstconst.inc}
|
|
|
|
|
|
Function CompareStrings(Node1, Node2: Pointer): integer;
|
|
var n1,n2 : TStringIndex;
|
|
begin
|
|
n1:=TStringIndex(Node1); n2:=TStringIndex(Node2);
|
|
Result := CompareText(n1.TheString, n2.TheString);
|
|
if Result < 0 then Result := -1
|
|
else if Result > 0 then Result := 1;
|
|
end;
|
|
|
|
|
|
Function CompareUrlStrs(Node1, Node2: Pointer): integer;
|
|
var n1,n2 : TUrlStrIndex;
|
|
begin
|
|
n1:=TUrlStrIndex(Node1); n2:=TUrlStrIndex(Node2);
|
|
Result := CompareText(n1.UrlStr, n2.UrlStr);
|
|
if Result < 0 then Result := -1
|
|
else if Result > 0 then Result := 1;
|
|
end;
|
|
|
|
{ TChmWriter }
|
|
|
|
procedure TITSFWriter.InitITSFHeader;
|
|
begin
|
|
with ITSFHeader do begin
|
|
ITSFsig := ITSFFileSig;
|
|
Version := NToLE(DWord(3));
|
|
// we fix endian order when this is written to the stream
|
|
HeaderLength := NToLE(DWord(SizeOf(TITSFHeader) + (SizeOf(TGuid)*2)+ (SizeOf(TITSFHeaderEntry)*2) + SizeOf(TITSFHeaderSuffix)));
|
|
Unknown_1 := NToLE(DWord(1));
|
|
TimeStamp:= NToBE(MilliSecondOfTheDay(Now)); //bigendian
|
|
LanguageID := NToLE(DWord($0409)); // English / English_US
|
|
end;
|
|
end;
|
|
|
|
procedure TITSFWriter.InitHeaderSectionTable;
|
|
begin
|
|
// header section 0
|
|
HeaderSection0Table.PosFromZero := LEToN(ITSFHeader.HeaderLength);
|
|
HeaderSection0Table.Length := SizeOf(TITSPHeaderPrefix);
|
|
// header section 1
|
|
HeaderSection1Table.PosFromZero := HeaderSection0Table.PosFromZero + HeaderSection0Table.Length;
|
|
HeaderSection1Table.Length := SizeOf(TITSPHeader)+FDirectoryListings.Size;
|
|
|
|
//contains the offset of CONTENT Section0 from zero
|
|
HeaderSuffix.Offset := HeaderSection1Table.PosFromZero + HeaderSection1Table.Length;
|
|
|
|
// now fix endian stuff
|
|
HeaderSection0Table.PosFromZero := NToLE(HeaderSection0Table.PosFromZero);
|
|
HeaderSection0Table.Length := NToLE(HeaderSection0Table.Length);
|
|
HeaderSection1Table.PosFromZero := NToLE(HeaderSection1Table.PosFromZero);
|
|
HeaderSection1Table.Length := NToLE(HeaderSection1Table.Length);
|
|
|
|
with HeaderSection0 do begin // TITSPHeaderPrefix;
|
|
Unknown1 := NToLE(DWord($01FE));
|
|
Unknown2 := 0;
|
|
// at this point we are putting together the headers. content sections 0 and 1 are complete
|
|
FileSize := NToLE(HeaderSuffix.Offset + FSection0.Size + FSection1Size);
|
|
Unknown3 := 0;
|
|
Unknown4 := 0;
|
|
end;
|
|
with HeaderSection1 do begin // TITSPHeader; // DirectoryListings header
|
|
ITSPsig := ITSPHeaderSig;
|
|
Version := NToLE(DWord(1));
|
|
DirHeaderLength := NToLE(DWord(SizeOf(TITSPHeader))); // Length of the directory header
|
|
Unknown1 := NToLE(DWord($0A));
|
|
ChunkSize := NToLE(DWord($1000));
|
|
Density := NToLE(DWord(2));
|
|
// updated when directory listings were created
|
|
//IndexTreeDepth := 1 ; // 1 if there is no index 2 if there is one level of PMGI chunks. will update as
|
|
//IndexOfRootChunk := -1;// if no root chunk
|
|
//FirstPMGLChunkIndex,
|
|
//LastPMGLChunkIndex: LongWord;
|
|
|
|
Unknown2 := NToLE(Longint(-1));
|
|
//DirectoryChunkCount: LongWord;
|
|
LanguageID := NToLE(DWord($0409));
|
|
GUID := ITSPHeaderGUID;
|
|
LengthAgain := NToLE(DWord($54));
|
|
Unknown3 := NToLE(Longint(-1));
|
|
Unknown4 := NToLE(Longint(-1));
|
|
Unknown5 := NToLE(Longint(-1));
|
|
end;
|
|
|
|
// more endian stuff
|
|
HeaderSuffix.Offset := NToLE(HeaderSuffix.Offset);
|
|
end;
|
|
|
|
procedure TITSFWriter.SetTempRawStream(const AValue: TStream);
|
|
begin
|
|
if (FCurrentStream.Size > 0) or (FSection1.Size > 0) then
|
|
raise Exception.Create('Cannot set the TempRawStream once data has been written to it!');
|
|
if AValue = nil then
|
|
raise Exception.Create('TempRawStream cannot be nil!');
|
|
if FCurrentStream = AValue then
|
|
exit;
|
|
FCurrentStream.Free;
|
|
FCurrentStream := AValue;
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteHeader(Stream: TStream);
|
|
begin
|
|
Stream.Write(ITSFHeader, SizeOf(TITSFHeader));
|
|
|
|
if ITSFHeader.Version < 4 then
|
|
begin
|
|
Stream.Write(ITSFHeaderGUID, SizeOf(TGuid));
|
|
Stream.Write(ITSFHeaderGUID, SizeOf(TGuid));
|
|
end;
|
|
Stream.Write(HeaderSection0Table, SizeOf(TITSFHeaderEntry));
|
|
Stream.Write(HeaderSection1Table, SizeOf(TITSFHeaderEntry));
|
|
Stream.Write(HeaderSuffix, SizeOf(TITSFHeaderSuffix));
|
|
Stream.Write(HeaderSection0, SizeOf(TITSPHeaderPrefix));
|
|
|
|
end;
|
|
|
|
procedure TITSFWriter.CreateDirectoryListings;
|
|
type
|
|
TFirstListEntry = record
|
|
Entry: array[0..511] of byte;
|
|
Size: Integer;
|
|
end;
|
|
var
|
|
Buffer: array [0..511] of Byte;
|
|
IndexBlock: TPMGIDirectoryChunk;
|
|
ListingBlock: TDirectoryChunk;
|
|
I: Integer;
|
|
Size: Integer;
|
|
FESize: Integer;
|
|
FileName: String;
|
|
FileNameSize: Integer;
|
|
LastListIndex: Integer;
|
|
FirstListEntry: TFirstListEntry;
|
|
ChunkIndex: Integer;
|
|
ListHeader: TPMGListChunk;
|
|
const
|
|
PMGL = 'PMGL';
|
|
PMGI = 'PMGI';
|
|
procedure UpdateLastListChunk;
|
|
var
|
|
Tmp: QWord;
|
|
begin
|
|
if ChunkIndex < 1 then begin
|
|
Exit;
|
|
end;
|
|
Tmp := FDirectoryListings.Position;
|
|
FDirectoryListings.Position := (LastListIndex) * $1000;
|
|
FDirectoryListings.Read(ListHeader, SizeOf(TPMGListChunk));
|
|
FDirectoryListings.Position := (LastListIndex) * $1000;
|
|
ListHeader.NextChunkIndex := NToLE(ChunkIndex);
|
|
FDirectoryListings.Write(ListHeader, SizeOf(TPMGListChunk));
|
|
FDirectoryListings.Position := Tmp;
|
|
end;
|
|
procedure WriteIndexChunk(ShouldFinish: Boolean = False);
|
|
var
|
|
IndexHeader: TPMGIIndexChunk;
|
|
ParentIndex,
|
|
TmpIndex: TPMGIDirectoryChunk;
|
|
begin
|
|
with IndexHeader do
|
|
begin
|
|
PMGIsig := PMGI;
|
|
UnusedSpace := NToLE(IndexBlock.FreeSpace);
|
|
end;
|
|
IndexBlock.WriteHeader(@IndexHeader);
|
|
IndexBlock.WriteChunkToStream(FDirectoryListings, ChunkIndex, ShouldFinish);
|
|
IndexBlock.Clear;
|
|
if HeaderSection1.IndexOfRootChunk < 0 then HeaderSection1.IndexOfRootChunk := ChunkIndex;
|
|
if ShouldFinish then
|
|
begin
|
|
HeaderSection1.IndexTreeDepth := 2;
|
|
ParentIndex := IndexBlock.ParentChunk;
|
|
if ParentIndex <> nil then
|
|
repeat // the parent index is notified by our child index when to write
|
|
HeaderSection1.IndexOfRootChunk := ChunkIndex;
|
|
TmpIndex := ParentIndex;
|
|
ParentIndex := ParentIndex.ParentChunk;
|
|
TmpIndex.Free;
|
|
Inc(HeaderSection1.IndexTreeDepth);
|
|
Inc(ChunkIndex);
|
|
until ParentIndex = nil;
|
|
end;
|
|
Inc(ChunkIndex);
|
|
|
|
end;
|
|
procedure WriteListChunk;
|
|
begin
|
|
with ListHeader do begin
|
|
PMGLsig := PMGL;
|
|
UnusedSpace := NToLE(ListingBlock.FreeSpace);
|
|
Unknown1 := 0;
|
|
PreviousChunkIndex := NToLE(LastListIndex);
|
|
NextChunkIndex := NToLE(Longint(-1)); // we update this when we write the next chunk
|
|
end;
|
|
if HeaderSection1.FirstPMGLChunkIndex <= 0 then
|
|
HeaderSection1.FirstPMGLChunkIndex := NToLE(ChunkIndex);
|
|
HeaderSection1.LastPMGLChunkIndex := NToLE(ChunkIndex);
|
|
ListingBlock.WriteHeader(@ListHeader);
|
|
ListingBlock.WriteChunkToStream(FDirectoryListings);
|
|
ListingBlock.Clear;
|
|
UpdateLastListChunk;
|
|
|
|
LastListIndex := ChunkIndex;
|
|
Inc(ChunkIndex);
|
|
// now add to index
|
|
if not IndexBlock.CanHold(FirstListEntry.Size) then
|
|
WriteIndexChunk;
|
|
IndexBlock.WriteEntry(FirstListEntry.Size, @FirstListEntry.Entry[0])
|
|
end;
|
|
begin
|
|
// first sort the listings
|
|
FInternalFiles.Sort;
|
|
HeaderSection1.IndexTreeDepth := 1;
|
|
HeaderSection1.IndexOfRootChunk := -1;
|
|
|
|
ChunkIndex := 0;
|
|
|
|
IndexBlock := TPMGIDirectoryChunk.Create(SizeOf(TPMGIIndexChunk));
|
|
ListingBlock := TDirectoryChunk.Create(SizeOf(TPMGListChunk));
|
|
|
|
LastListIndex := -1;
|
|
|
|
// add files to a pmgl block until it is full.
|
|
// after the block is full make a pmgi block and add the first entry of the pmgl block
|
|
// repeat until the index block is full and start another.
|
|
// the pmgi chunks take care of needed parent chunks in the tree
|
|
for I := 0 to FInternalFiles.Count-1 do begin
|
|
Size := 0;
|
|
FileName := FInternalFiles.FileEntry[I].Path + FInternalFiles.FileEntry[I].Name;
|
|
FileNameSize := Length(FileName);
|
|
// filename length
|
|
Inc(Size, WriteCompressedInteger(@Buffer[Size], FileNameSize));
|
|
// filename
|
|
Move(FileName[1], Buffer[Size], FileNameSize);
|
|
Inc(Size, FileNameSize);
|
|
FESize := Size;
|
|
// File is compressed...
|
|
Inc(Size, WriteCompressedInteger(@Buffer[Size], Ord(FInternalFiles.FileEntry[I].Compressed)));
|
|
// Offset from section start
|
|
Inc(Size, WriteCompressedInteger(@Buffer[Size], FInternalFiles.FileEntry[I].DecompressedOffset));
|
|
// Size when uncompressed
|
|
Inc(Size, WriteCompressedInteger(@Buffer[Size], FInternalFiles.FileEntry[I].DecompressedSize));
|
|
|
|
if not ListingBlock.CanHold(Size) then
|
|
WriteListChunk;
|
|
|
|
ListingBlock.WriteEntry(Size, @Buffer[0]);
|
|
|
|
if ListingBlock.ItemCount = 1 then begin // add the first list item to the index
|
|
Move(Buffer[0], FirstListEntry.Entry[0], FESize);
|
|
FirstListEntry.Size := FESize + WriteCompressedInteger(@FirstListEntry.Entry[FESize], ChunkIndex);
|
|
end;
|
|
end;
|
|
if ListingBlock.ItemCount > 0 then WriteListChunk;
|
|
|
|
if ChunkIndex > 1 then begin
|
|
if (IndexBlock.ItemCount > 1)
|
|
or ( (IndexBlock.ItemCount > 0) and (HeaderSection1.IndexOfRootChunk > -1) )
|
|
then WriteIndexChunk(True);
|
|
end;
|
|
|
|
HeaderSection1.DirectoryChunkCount := NToLE(DWord(FDirectoryListings.Size div $1000));
|
|
|
|
IndexBlock.Free;
|
|
ListingBlock.Free;
|
|
|
|
//now fix some endian stuff
|
|
HeaderSection1.IndexOfRootChunk := NToLE(HeaderSection1.IndexOfRootChunk);
|
|
HeaderSection1.IndexTreeDepth := NtoLE(HeaderSection1.IndexTreeDepth);
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteDirectoryListings(Stream: TStream);
|
|
begin
|
|
Stream.Write(HeaderSection1, SizeOf(HeaderSection1));
|
|
FDirectoryListings.Position := 0;
|
|
Stream.CopyFrom(FDirectoryListings, FDirectoryListings.Size);
|
|
FDirectoryListings.Position := 0;
|
|
//TMemoryStream(FDirectoryListings).SaveToFile('dirlistings.pmg');
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteInternalFilesBefore;
|
|
begin
|
|
// written to Section0 (uncompressed)
|
|
WriteREADMEFile;
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteInternalFilesAfter;
|
|
begin
|
|
end;
|
|
|
|
procedure IterateWord(aword:TIndexedWord;State:pointer);
|
|
var i,cnt : integer;
|
|
begin
|
|
cnt:=pinteger(state)^;
|
|
for i := 0 to AWord.DocumentCount-1 do
|
|
Inc(cnt, AWord.GetLogicalDocument(i).NumberOfIndexEntries);
|
|
// was commented in original procedure, seems to list index entries per doc.
|
|
//WriteLn(AWord.TheWord,' documents = ', AWord.DocumentCount, ' h
|
|
pinteger(state)^:=cnt;
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteREADMEFile;
|
|
const DISCLAIMER_STR = 'This archive was not made by the MS HTML Help Workshop(r)(tm) program, but by Free Pascal''s chm package '+chmpackageversion+'.'#13#10;
|
|
var
|
|
Entry: TFileEntryRec;
|
|
begin
|
|
// This procedure puts a file in the archive that says it wasn't compiled with the MS compiler
|
|
Entry.Compressed := False;
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
FSection0.Write(DISCLAIMER_STR, SizeOf(DISCLAIMER_STR));
|
|
if length(FReadmeMessage)>0 then
|
|
FSection0.Write(FReadmeMessage[1], length(FReadmeMessage));
|
|
Entry.DecompressedSize := FSection0.Position - Entry.DecompressedOffset;
|
|
Entry.Path := '/';
|
|
Entry.Name := '_#_README_#_'; //try to use a name that won't conflict with normal names
|
|
FInternalFiles.AddEntry(Entry);
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteFinalCompressedFiles;
|
|
begin
|
|
|
|
end;
|
|
|
|
|
|
procedure TITSFWriter.WriteSection0;
|
|
begin
|
|
FSection0.Position := 0;
|
|
FOutStream.CopyFrom(FSection0, FSection0.Size);
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteSection1;
|
|
begin
|
|
WriteContentToStream(FOutStream, FSection1);
|
|
end;
|
|
|
|
procedure TITSFWriter.WriteDataSpaceFiles(const AStream: TStream);
|
|
var
|
|
Entry: TFileEntryRec;
|
|
begin
|
|
// This procedure will write all files starting with ::
|
|
Entry.Compressed := False; // None of these files are compressed
|
|
|
|
// ::DataSpace/NameList
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
Entry.DecompressedSize := WriteNameListToStream(FSection0, [snUnCompressed,snMSCompressed]);
|
|
Entry.Path := '::DataSpace/';
|
|
Entry.Name := 'NameList';
|
|
FInternalFiles.AddEntry(Entry, False);
|
|
|
|
// ::DataSpace/Storage/MSCompressed/ControlData
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
Entry.DecompressedSize := WriteControlDataToStream(FSection0, 2, 2, 1);
|
|
Entry.Path := '::DataSpace/Storage/MSCompressed/';
|
|
Entry.Name := 'ControlData';
|
|
FInternalFiles.AddEntry(Entry, False);
|
|
|
|
// ::DataSpace/Storage/MSCompressed/SpanInfo
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
Entry.DecompressedSize := WriteSpanInfoToStream(FSection0, FReadCompressedSize);
|
|
Entry.Path := '::DataSpace/Storage/MSCompressed/';
|
|
Entry.Name := 'SpanInfo';
|
|
FInternalFiles.AddEntry(Entry, False);
|
|
|
|
// ::DataSpace/Storage/MSCompressed/Transform/List
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
Entry.DecompressedSize := WriteTransformListToStream(FSection0);
|
|
Entry.Path := '::DataSpace/Storage/MSCompressed/Transform/';
|
|
Entry.Name := 'List';
|
|
FInternalFiles.AddEntry(Entry, False);
|
|
|
|
// ::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/
|
|
// ::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
Entry.DecompressedSize := WriteResetTableToStream(FSection0, FSection1ResetTable);
|
|
Entry.Path := '::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/';
|
|
Entry.Name := 'ResetTable';
|
|
FInternalFiles.AddEntry(Entry, True);
|
|
|
|
|
|
// ::DataSpace/Storage/MSCompressed/Content do this last
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
Entry.DecompressedSize := FSection1Size; // we will write it directly to FOutStream later
|
|
Entry.Path := '::DataSpace/Storage/MSCompressed/';
|
|
Entry.Name := 'Content';
|
|
FInternalFiles.AddEntry(Entry, False);
|
|
|
|
|
|
end;
|
|
|
|
procedure TITSFWriter.FileAdded(AStream: TStream; const AEntry: TFileEntryRec);
|
|
begin
|
|
// do nothing here
|
|
end;
|
|
|
|
function _AtEndOfData(arg: pointer): LongBool; cdecl;
|
|
begin
|
|
Result := TITSFWriter(arg).AtEndOfData;
|
|
end;
|
|
|
|
function TITSFWriter.AtEndOfData: LongBool;
|
|
begin
|
|
Result := ForceExit or (FCurrentIndex >= FFileNames.Count-1);
|
|
if Result then
|
|
Result := Integer(FCurrentStream.Position) >= Integer(FCurrentStream.Size)-1;
|
|
end;
|
|
|
|
function _GetData(arg: pointer; Count: LongInt; Buffer: Pointer): LongInt; cdecl;
|
|
begin
|
|
Result := TITSFWriter(arg).GetData(Count, PByte(Buffer));
|
|
end;
|
|
|
|
function TITSFWriter.GetData(Count: LongInt; Buffer: PByte): LongInt;
|
|
var
|
|
FileEntry: TFileEntryRec;
|
|
begin
|
|
Result := 0;
|
|
while (Result < Count) and (not AtEndOfData) do begin
|
|
Inc(Result, FCurrentStream.Read(Buffer[Result], Count-Result));
|
|
if (Result < Count) and (not AtEndOfData)
|
|
then begin
|
|
// the current file has been read. move to the next file in the list
|
|
FCurrentStream.Position := 0;
|
|
Inc(FCurrentIndex);
|
|
ForceExit := OnGetFileData(FFileNames[FCurrentIndex], FileEntry.Path, FileEntry.Name, FCurrentStream);
|
|
FileEntry.DecompressedSize := FCurrentStream.Size;
|
|
FileEntry.DecompressedOffset := FReadCompressedSize; //269047723;//to test writing really large numbers
|
|
FileEntry.Compressed := True;
|
|
|
|
FileAdded(FCurrentStream, FileEntry);
|
|
|
|
FInternalFiles.AddEntry(FileEntry);
|
|
// So the next file knows it's offset
|
|
Inc(FReadCompressedSize, FileEntry.DecompressedSize);
|
|
FCurrentStream.Position := 0;
|
|
end;
|
|
|
|
// this is intended for programs to add perhaps a file
|
|
// after all the other files have been added.
|
|
if (AtEndOfData)
|
|
and (FCurrentStream <> FPostStream) then
|
|
begin
|
|
FPostStreamActive := True;
|
|
if Assigned(FOnLastFile) then
|
|
FOnLastFile(Self);
|
|
FCurrentStream.Free;
|
|
WriteFinalCompressedFiles;
|
|
FCurrentStream := FPostStream;
|
|
FCurrentStream.Position := 0;
|
|
Inc(FReadCompressedSize, FCurrentStream.Size);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function _WriteCompressedData(arg: pointer; Count: LongInt; Buffer: Pointer): LongInt; cdecl;
|
|
begin
|
|
Result := TITSFWriter(arg).WriteCompressedData(Count, Buffer);
|
|
end;
|
|
|
|
function TITSFWriter.WriteCompressedData(Count: Longint; Buffer: Pointer): LongInt;
|
|
begin
|
|
// we allocate a MB at a time to limit memory reallocation since this
|
|
// writes usually 2 bytes at a time
|
|
if (FSection1 is TMemoryStream) and (FSection1.Position >= FSection1.Size-1) then begin
|
|
FSection1.Size := FSection1.Size+$100000;
|
|
end;
|
|
Result := FSection1.Write(Buffer^, Count);
|
|
Inc(FSection1Size, Result);
|
|
end;
|
|
|
|
procedure _MarkFrame(arg: pointer; UncompressedTotal, CompressedTotal: LongWord); cdecl;
|
|
begin
|
|
TITSFWriter(arg).MarkFrame(UncompressedTotal, CompressedTotal);
|
|
end;
|
|
|
|
procedure TITSFWriter.MarkFrame(UnCompressedTotal, CompressedTotal: LongWord);
|
|
procedure WriteQWord(Value: QWord);
|
|
begin
|
|
FSection1ResetTable.Write(NToLE(Value), 8);
|
|
end;
|
|
procedure IncEntryCount;
|
|
var
|
|
OldPos: QWord;
|
|
Value: DWord;
|
|
begin
|
|
OldPos := FSection1ResetTable.Position;
|
|
FSection1ResetTable.Position := $4;
|
|
Value := LeToN(FSection1ResetTable.ReadDWord)+1;
|
|
FSection1ResetTable.Position := $4;
|
|
FSection1ResetTable.WriteDWord(NToLE(Value));
|
|
FSection1ResetTable.Position := OldPos;
|
|
end;
|
|
procedure UpdateTotalSizes;
|
|
var
|
|
OldPos: QWord;
|
|
begin
|
|
OldPos := FSection1ResetTable.Position;
|
|
FSection1ResetTable.Position := $10;
|
|
WriteQWord(FReadCompressedSize); // size of read data that has been compressed
|
|
WriteQWord(CompressedTotal);
|
|
FSection1ResetTable.Position := OldPos;
|
|
end;
|
|
begin
|
|
if FSection1ResetTable.Size = 0 then begin
|
|
// Write the header
|
|
FSection1ResetTable.WriteDWord(NtoLE(DWord(2)));
|
|
FSection1ResetTable.WriteDWord(0); // number of entries. we will correct this with IncEntryCount
|
|
FSection1ResetTable.WriteDWord(NtoLE(DWord(8))); // Size of Entries (qword)
|
|
FSection1ResetTable.WriteDWord(NtoLE(DWord($28))); // Size of this header
|
|
WriteQWord(0); // Total Uncompressed Size
|
|
WriteQWord(0); // Total Compressed Size
|
|
WriteQWord(NtoLE($8000)); // Block Size
|
|
WriteQWord(0); // First Block start
|
|
end;
|
|
IncEntryCount;
|
|
UpdateTotalSizes;
|
|
WriteQWord(CompressedTotal); // Next Block Start
|
|
// We have to trim the last entry off when we are done because there is no next block in that case
|
|
end;
|
|
|
|
{$IFDEF LZX_USETHREADS}
|
|
function TITSFWriter.LTGetData(Sender: TLZXCompressor; WantedByteCount: Integer;
|
|
Buffer: Pointer): Integer;
|
|
begin
|
|
Result := GetData(WantedByteCount, Buffer);
|
|
//WriteLn('Wanted ', WantedByteCount, ' got ', Result);
|
|
end;
|
|
|
|
function TITSFWriter.LTIsEndOfFile(Sender: TLZXCompressor): Boolean;
|
|
begin
|
|
Result := AtEndOfData;
|
|
end;
|
|
|
|
procedure TITSFWriter.LTChunkDone(Sender: TLZXCompressor;
|
|
CompressedSize: Integer; UncompressedSize: Integer; Buffer: Pointer);
|
|
begin
|
|
WriteCompressedData(CompressedSize, Buffer);
|
|
end;
|
|
|
|
procedure TITSFWriter.LTMarkFrame(Sender: TLZXCompressor;
|
|
CompressedTotal: Integer; UncompressedTotal: Integer);
|
|
begin
|
|
MarkFrame(UncompressedTotal, CompressedTotal);
|
|
//WriteLn('Mark Frame C = ', CompressedTotal, ' U = ', UncompressedTotal);
|
|
end;
|
|
{$ENDIF}
|
|
|
|
|
|
constructor TITSFWriter.Create(AOutStream: TStream; FreeStreamOnDestroy: Boolean);
|
|
begin
|
|
if AOutStream = nil then Raise Exception.Create('TITSFWriter.OutStream Cannot be nil!');
|
|
FOutStream := AOutStream;
|
|
FCurrentIndex := -1;
|
|
FCurrentStream := TMemoryStream.Create;
|
|
FInternalFiles := TFileEntryList.Create;
|
|
FSection0 := TMemoryStream.Create;
|
|
FSection1 := TMemoryStream.Create;
|
|
FSection1ResetTable := TMemoryStream.Create;
|
|
FDirectoryListings := TMemoryStream.Create;
|
|
FPostStream := TMemoryStream.Create;;
|
|
FDestroyStream := FreeStreamOnDestroy;
|
|
FFileNames := TStringList.Create;
|
|
end;
|
|
|
|
destructor TITSFWriter.Destroy;
|
|
begin
|
|
if FDestroyStream then FOutStream.Free;
|
|
FInternalFiles.Free;
|
|
FCurrentStream.Free;
|
|
FSection0.Free;
|
|
FSection1.Free;
|
|
FSection1ResetTable.Free;
|
|
FDirectoryListings.Free;
|
|
FFileNames.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TITSFWriter.Execute;
|
|
begin
|
|
writeln('TITSFWriter.Execute START');
|
|
InitITSFHeader;
|
|
FOutStream.Position := 0;
|
|
FSection1Size := 0;
|
|
|
|
// write any internal files to FCurrentStream that we want in the compressed section
|
|
WriteInternalFilesBefore;
|
|
|
|
writeln('TITSFWriter.Execute AAA1');
|
|
// move back to zero so that we can start reading from zero :)
|
|
FReadCompressedSize := FCurrentStream.Size;
|
|
FCurrentStream.Position := 0; // when compressing happens, first the FCurrentStream is read
|
|
// before loading user files. So we can fill FCurrentStream with
|
|
// internal files first.
|
|
|
|
// this gathers ALL files that should be in section1 (the compressed section)
|
|
writeln('TITSFWriter.Execute AAA2');
|
|
StartCompressingStream;
|
|
FSection1.Size := FSection1Size;
|
|
|
|
writeln('TITSFWriter.Execute AAA3');
|
|
WriteInternalFilesAfter;
|
|
|
|
//this creates all special files in the archive that start with ::DataSpace
|
|
writeln('TITSFWriter.Execute AAA4');
|
|
WriteDataSpaceFiles(FSection0);
|
|
|
|
// creates all directory listings including header
|
|
writeln('TITSFWriter.Execute AAA5');
|
|
CreateDirectoryListings;
|
|
|
|
// do this after we have compressed everything so that we know the values that must be written
|
|
writeln('TITSFWriter.Execute AAA6');
|
|
InitHeaderSectionTable;
|
|
|
|
// Now we can write everything to FOutStream
|
|
writeln('TITSFWriter.Execute AAA7');
|
|
WriteHeader(FOutStream);
|
|
writeln('TITSFWriter.Execute AAA8');
|
|
WriteDirectoryListings(FOutStream);
|
|
writeln('TITSFWriter.Execute AAA9');
|
|
WriteSection0; //does NOT include section 1 even though section0.content IS section1
|
|
writeln('TITSFWriter.Execute AAA10');
|
|
WriteSection1; // writes section 1 to FOutStream
|
|
writeln('TITSFWriter.Execute END');
|
|
end;
|
|
|
|
// this procedure is used to manually add files to compress to an internal stream that is
|
|
// processed before FileToCompress is called. Files added this way should not be
|
|
// duplicated in the FilesToCompress property.
|
|
procedure TITSFWriter.AddStreamToArchive(AFileName, APath: String; AStream: TStream; Compress: Boolean = True);
|
|
var
|
|
TargetStream: TStream;
|
|
Entry: TFileEntryRec;
|
|
begin
|
|
// in case AddStreamToArchive is used after we should be writing to the post stream
|
|
if FPostStreamActive then
|
|
begin
|
|
PostAddStreamToArchive(AFileName, APath, AStream, Compress);
|
|
Exit;
|
|
end;
|
|
if AStream = nil then Exit;
|
|
if Compress then
|
|
TargetStream := FCurrentStream
|
|
else
|
|
TargetStream := FSection0;
|
|
|
|
Entry.Name := AFileName;
|
|
Entry.Path := APath;
|
|
Entry.Compressed := Compress;
|
|
Entry.DecompressedOffset := TargetStream.Position;
|
|
Entry.DecompressedSize := AStream.Size;
|
|
FileAdded(AStream,Entry);
|
|
FInternalFiles.AddEntry(Entry);
|
|
AStream.Position := 0;
|
|
TargetStream.CopyFrom(AStream, AStream.Size);
|
|
end;
|
|
|
|
procedure TITSFWriter.PostAddStreamToArchive(AFileName, APath: String;
|
|
AStream: TStream; Compress: Boolean);
|
|
var
|
|
TargetStream: TStream;
|
|
Entry: TFileEntryRec;
|
|
begin
|
|
if AStream = nil then Exit;
|
|
if Compress then
|
|
TargetStream := FPostStream
|
|
else
|
|
TargetStream := FSection0;
|
|
|
|
Entry.Name := AFileName;
|
|
Entry.Path := APath;
|
|
Entry.Compressed := Compress;
|
|
if not Compress then
|
|
Entry.DecompressedOffset := TargetStream.Position
|
|
else
|
|
Entry.DecompressedOffset := FReadCompressedSize + TargetStream.Position;
|
|
Entry.DecompressedSize := AStream.Size;
|
|
FInternalFiles.AddEntry(Entry);
|
|
AStream.Position := 0;
|
|
TargetStream.CopyFrom(AStream, AStream.Size);
|
|
FileAdded(AStream, Entry);
|
|
end;
|
|
|
|
procedure TITSFWriter.StartCompressingStream;
|
|
var
|
|
{$IFNDEF LZX_USETHREADS}
|
|
LZXdata: Plzx_data;
|
|
WSize: LongInt;
|
|
{$ELSE}
|
|
Compressor: TLZXCompressor;
|
|
{$ENDIF}
|
|
begin
|
|
{$IFNDEF LZX_USETHREADS}
|
|
lzx_init(@LZXdata, LZX_WINDOW_SIZE, @_GetData, Self, @_AtEndOfData,
|
|
@_WriteCompressedData, Self, @_MarkFrame, Self);
|
|
|
|
WSize := 1 shl LZX_WINDOW_SIZE;
|
|
while not AtEndOfData do begin
|
|
lzx_reset(LZXdata);
|
|
lzx_compress_block(LZXdata, WSize, True);
|
|
end;
|
|
|
|
//we have to mark the last frame manually
|
|
MarkFrame(LZXdata^.len_uncompressed_input, LZXdata^.len_compressed_output);
|
|
|
|
lzx_finish(LZXdata, nil);
|
|
{$ELSE}
|
|
Compressor := TLZXCompressor.Create(4);
|
|
Compressor.OnChunkDone :=@LTChunkDone;
|
|
Compressor.OnGetData :=@LTGetData;
|
|
Compressor.OnIsEndOfFile:=@LTIsEndOfFile;
|
|
Compressor.OnMarkFrame :=@LTMarkFrame;
|
|
Compressor.Execute(True);
|
|
//Sleep(20000);
|
|
Compressor.Free;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
|
|
procedure TChmWriter.WriteSystem;
|
|
var
|
|
Entry: TFileEntryRec;
|
|
TmpStr: String;
|
|
TmpTitle: String;
|
|
const
|
|
VersionStr = 'HHA Version 4.74.8702'; // does this matter?
|
|
begin
|
|
|
|
|
|
// this creates the /#SYSTEM file
|
|
Entry.Name := '#SYSTEM';
|
|
Entry.Path := '/';
|
|
Entry.Compressed := False;
|
|
Entry.DecompressedOffset := FSection0.Position;
|
|
|
|
{ if FileExists('#SYSTEM') then
|
|
begin
|
|
TmpStream := TMemoryStream.Create;
|
|
TmpStream.LoadFromFile('#SYSTEM');
|
|
TmpStream.Position := 0;
|
|
FSection0.CopyFrom(TmpStream, TmpStream.Size);
|
|
end; }
|
|
// EntryCodeOrder: 10 9 4 2 3 16 6 0 1 5
|
|
FSection0.WriteDWord(NToLE(Word(3))); // Version
|
|
if Title <> '' then
|
|
TmpTitle := Title
|
|
else
|
|
TmpTitle := 'default';
|
|
|
|
// Code -> Length -> Data
|
|
// 10
|
|
FSection0.WriteWord(NToLE(Word(10)));
|
|
FSection0.WriteWord(NToLE(Word(SizeOf(DWord))));
|
|
FSection0.WriteDWord(NToLE(MilliSecondOfTheDay(Now)));
|
|
// 9
|
|
FSection0.WriteWord(NToLE(Word(9)));
|
|
FSection0.WriteWord(NToLE(Word(SizeOf(VersionStr)+1)));
|
|
FSection0.Write(VersionStr, SizeOf(VersionStr));
|
|
FSection0.WriteByte(0);
|
|
// 4 A struct that is only needed to set if full text search is on.
|
|
FSection0.WriteWord(NToLE(Word(4)));
|
|
FSection0.WriteWord(NToLE(Word(36))); // size
|
|
|
|
FSection0.WriteDWord(NToLE(DWord($0409)));
|
|
FSection0.WriteDWord(1);
|
|
writeln('TChmWriter.WriteSystem FFullTextSearch=',FFullTextSearch,' FFullTextSearchAvailable=',FFullTextSearchAvailable);
|
|
FSection0.WriteDWord(NToLE(DWord(Ord(FFullTextSearch and FFullTextSearchAvailable))));
|
|
FSection0.WriteDWord(0);
|
|
FSection0.WriteDWord(0);
|
|
|
|
// two for a QWord
|
|
FSection0.WriteDWord(0);
|
|
FSection0.WriteDWord(0);
|
|
|
|
FSection0.WriteDWord(0);
|
|
FSection0.WriteDWord(0);
|
|
|
|
|
|
|
|
|
|
////////////////////////<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
// 2 default page to load
|
|
if FDefaultPage <> '' then begin
|
|
FSection0.WriteWord(NToLE(Word(2)));
|
|
FSection0.WriteWord(NToLE(Word(Length(FDefaultPage)+1)));
|
|
FSection0.Write(FDefaultPage[1], Length(FDefaultPage));
|
|
FSection0.WriteByte(0);
|
|
end;
|
|
// 3 Title
|
|
if FTitle <> '' then begin
|
|
FSection0.WriteWord(NToLE(Word(3)));
|
|
FSection0.WriteWord(NToLE(Word(Length(FTitle)+1)));
|
|
FSection0.Write(FTitle[1], Length(FTitle));
|
|
FSection0.WriteByte(0);
|
|
end;
|
|
|
|
// 16 Default Font
|
|
if FDefaultFont <> '' then begin
|
|
FSection0.WriteWord(NToLE(Word(16)));
|
|
FSection0.WriteWord(NToLE(Word(Length(FDefaultFont)+1)));
|
|
FSection0.Write(FDefaultFont[1], Length(FDefaultFont));
|
|
FSection0.WriteByte(0);
|
|
end;
|
|
|
|
// 6
|
|
// unneeded. if output file is : /somepath/OutFile.chm the value here is outfile(lowercase)
|
|
{FSection0.WriteWord(6);
|
|
FSection0.WriteWord(Length('test1')+1);
|
|
Fsection0.Write('test1', 5);
|
|
FSection0.WriteByte(0);}
|
|
|
|
// 0 Table of contents filename
|
|
if FHasTOC then begin
|
|
if fTocName ='' then
|
|
TmpStr := DefaultHHC
|
|
else
|
|
TmpStr := fTocName;
|
|
FSection0.WriteWord(0);
|
|
FSection0.WriteWord(NToLE(Word(Length(TmpStr)+1)));
|
|
FSection0.Write(TmpStr[1], Length(TmpStr));
|
|
FSection0.WriteByte(0);
|
|
end;
|
|
// 1
|
|
// hhk Index
|
|
if FHasIndex then begin
|
|
if fIndexName='' then
|
|
TmpStr := DefaultHHK
|
|
else
|
|
TmpStr := fIndexName;
|
|
FSection0.WriteWord(NToLE(Word(1)));
|
|
FSection0.WriteWord(NToLE(Word(Length(TmpStr)+1)));
|
|
FSection0.Write(TmpStr[1], Length(TmpStr));
|
|
FSection0.WriteByte(0);
|
|
end;
|
|
// 5 Default Window
|
|
|
|
if FDefaultWindow<>'' then
|
|
begin
|
|
FSection0.WriteWord(NTOLE(Word(5)));
|
|
tmpstr:=FDefaultWindow;
|
|
FSection0.WriteWord(NToLE(Word(Length(TmpStr)+1)));
|
|
FSection0.Write(TmpStr[1], Length(TmpStr));
|
|
FSection0.WriteByte(0);
|
|
end;
|
|
|
|
// 7 Binary Index
|
|
if FHasBinaryIndex then
|
|
begin
|
|
{$ifdef binindex}
|
|
logentry('binary index!');
|
|
{$endif}
|
|
FSection0.WriteWord(NToLE(Word(7)));
|
|
FSection0.WriteWord(NToLE(Word(4)));
|
|
FSection0.WriteDWord(DWord(0)); // what is this number to be?
|
|
end;
|
|
|
|
// 11 Binary TOC
|
|
if FHasBinaryTOC then
|
|
begin
|
|
FSection0.WriteWord(NToLE(Word(11)));
|
|
FSection0.WriteWord(NToLE(Word(4)));
|
|
FSection0.WriteDWord(DWord(0)); // what is this number to be?
|
|
end;
|
|
|
|
|
|
Entry.DecompressedSize := FSection0.Position - Entry.DecompressedOffset;
|
|
FInternalFiles.AddEntry(Entry);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteITBITS;
|
|
var
|
|
Entry: TFileEntryRec;
|
|
begin
|
|
// This is an empty and useless file
|
|
Entry.Name := '#ITBITS';
|
|
Entry.Path := '/';
|
|
Entry.Compressed := False;
|
|
Entry.DecompressedOffset :=0;// FSection0.Position;
|
|
Entry.DecompressedSize := 0;
|
|
|
|
FInternalFiles.AddEntry(Entry);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteSTRINGS;
|
|
begin
|
|
if FStringsStream.Size = 0 then;
|
|
FStringsStream.WriteByte(0);
|
|
FStringsStream.Position := 0;
|
|
PostAddStreamToArchive('#STRINGS', '/', FStringsStream);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteTOPICS;
|
|
//var
|
|
//FHits: Integer;
|
|
begin
|
|
if FTopicsStream.Size = 0 then
|
|
Exit;
|
|
FTopicsStream.Position := 0;
|
|
PostAddStreamToArchive('#TOPICS', '/', FTopicsStream);
|
|
// I commented the code below since the result seemed unused
|
|
// FHits:=0;
|
|
// FIndexedFiles.ForEach(@IterateWord,FHits);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteIVB;
|
|
begin
|
|
if FContextStream = nil then exit;
|
|
|
|
FContextStream.Position := 0;
|
|
// the size of all the entries
|
|
FContextStream.WriteDWord(NToLE(DWord(FContextStream.Size-SizeOf(dword))));
|
|
|
|
FContextStream.Position := 0;
|
|
AddStreamToArchive('#IVB', '/', FContextStream);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteURL_STR_TBL;
|
|
begin
|
|
if FURLSTRStream.Size <> 0 then begin
|
|
FURLSTRStream.Position := 0;
|
|
PostAddStreamToArchive('#URLSTR', '/', FURLSTRStream);
|
|
end;
|
|
if FURLTBLStream.Size <> 0 then begin
|
|
FURLTBLStream.Position := 0;
|
|
PostAddStreamToArchive('#URLTBL', '/', FURLTBLStream);
|
|
end;
|
|
end;
|
|
|
|
procedure TChmWriter.WriteOBJINST;
|
|
var
|
|
i: Integer;
|
|
ObjStream: TMemoryStream;
|
|
//Flags: Word;
|
|
begin
|
|
ObjStream := TMemorystream.Create;
|
|
// this file is needed to enable searches for the ms reader
|
|
ObjStream.WriteDWord(NtoLE($04000000));
|
|
ObjStream.WriteDWord(NtoLE(Dword(2))); // two entries
|
|
|
|
ObjStream.WriteDWord(NtoLE(DWord(24))); // offset into file of entry
|
|
ObjStream.WriteDWord(NtoLE(DWord(2691))); // size
|
|
|
|
ObjStream.WriteDWord(NtoLE(DWord(2715))); // offset into file of entry
|
|
ObjStream.WriteDWord(NtoLE(DWord(36))); // size
|
|
|
|
// first entry
|
|
// write guid 4662DAAF-D393-11D0-9A56-00C04FB68BF7
|
|
ObjStream.WriteDWord(NtoLE($4662DAAF));
|
|
ObjStream.WriteWord(NtoLE($D393));
|
|
ObjStream.WriteWord(NtoLE($11D0));
|
|
ObjStream.WriteWord(NtoLE($569A));
|
|
ObjStream.WriteByte($00);
|
|
ObjStream.WriteByte($C0);
|
|
ObjStream.WriteByte($4F);
|
|
ObjStream.WriteByte($B6);
|
|
ObjStream.WriteByte($8B);
|
|
ObjStream.WriteByte($F7);
|
|
|
|
ObjStream.WriteDWord(NtoLE($04000000));
|
|
ObjStream.WriteDWord(NtoLE(11)); // bit flags
|
|
ObjStream.WriteDWord(NtoLE(DWord(1252)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(1033)));
|
|
ObjStream.WriteDWord(NtoLE($00000000));
|
|
ObjStream.WriteDWord(NtoLE($00000000));
|
|
ObjStream.WriteDWord(NtoLE($00145555));
|
|
ObjStream.WriteDWord(NtoLE($00000A0F));
|
|
ObjStream.WriteWord(NtoLE($0100));
|
|
ObjStream.WriteDWord(NtoLE($00030005));
|
|
for i := 0 to 5 do
|
|
ObjStream.WriteDWord($00000000);
|
|
ObjStream.WriteWord($0000);
|
|
// okay now the fun stuff
|
|
for i := 0 to $FF do
|
|
ObjStream.Write(ObjInstEntries[i], SizeOF(TObjInstEntry));
|
|
{begin
|
|
if i = 1 then
|
|
Flags := 7
|
|
else
|
|
Flags := 0;
|
|
if (i >= $41) and (i <= $5A) then
|
|
Flags := Flags or 2;
|
|
if (i >= $61) and (i <= $7A) then
|
|
Flags := Flags or 1;
|
|
if i = $27 then
|
|
Flags := Flags or 6;
|
|
ObjStream.WriteWord(NtoLE(Flags));
|
|
ObjStream.WriteWord(NtoLE(Word(i)));
|
|
if (i >= $41) and (i <= $5A) then
|
|
ObjStream.WriteByte(NtoLE(i+$20))
|
|
else
|
|
ObjStream.WriteByte(NtoLE(i));
|
|
ObjStream.WriteByte(NtoLE(i));
|
|
ObjStream.WriteByte(NtoLE(i));
|
|
ObjStream.WriteByte(NtoLE(i));
|
|
ObjStream.WriteWord(NtoLE($0000));
|
|
end;}
|
|
ObjStream.WriteDWord(NtoLE($E66561C6));
|
|
ObjStream.WriteDWord(NtoLE($73DF6561));
|
|
ObjStream.WriteDWord(NtoLE($656F8C73));
|
|
ObjStream.WriteWord(NtoLE($6F9C));
|
|
ObjStream.WriteByte(NtoLE($65));
|
|
// third bit of second entry
|
|
// write guid 8FA0D5A8-DEDF-11D0-9A61-00C04FB68BF7
|
|
ObjStream.WriteDWord(NtoLE($8FA0D5A8));
|
|
ObjStream.WriteWord(NtoLE($DEDF));
|
|
ObjStream.WriteWord(NtoLE($11D0));
|
|
ObjStream.WriteWord(NtoLE($619A));
|
|
ObjStream.WriteByte($00);
|
|
ObjStream.WriteByte($C0);
|
|
ObjStream.WriteByte($4F);
|
|
ObjStream.WriteByte($B6);
|
|
ObjStream.WriteByte($8B);
|
|
ObjStream.WriteByte($F7);
|
|
|
|
ObjStream.WriteDWord(NtoLE($04000000));
|
|
ObjStream.WriteDWord(NtoLE(DWord(1)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(1252)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(1033)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(0)));
|
|
|
|
// second entry
|
|
// write guid 4662DAB0-D393-11D0-9A56-00C04FB68B66
|
|
ObjStream.WriteDWord(NtoLE($4662DAB0));
|
|
ObjStream.WriteWord(NtoLE($D393));
|
|
ObjStream.WriteWord(NtoLE($11D0));
|
|
ObjStream.WriteWord(NtoLE($569A));
|
|
ObjStream.WriteByte($00);
|
|
ObjStream.WriteByte($C0);
|
|
ObjStream.WriteByte($4F);
|
|
ObjStream.WriteByte($B6);
|
|
ObjStream.WriteByte($8B);
|
|
ObjStream.WriteByte($66);
|
|
|
|
ObjStream.WriteDWord(NtoLE(DWord(666))); // not kidding
|
|
ObjStream.WriteDWord(NtoLE(DWord(1252)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(1033)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(10031)));
|
|
ObjStream.WriteDWord(NtoLE(DWord(0)));
|
|
|
|
ObjStream.Position := 0;
|
|
AddStreamToArchive('$OBJINST', '/', ObjStream, True);
|
|
ObjStream.Free;
|
|
|
|
end;
|
|
|
|
procedure TChmWriter.WriteFiftiMain;
|
|
var
|
|
SearchWriter: TChmSearchWriter;
|
|
begin
|
|
if FTopicsStream.Size = 0 then
|
|
Exit;
|
|
SearchWriter := TChmSearchWriter.Create(FFiftiMainStream, FIndexedFiles);
|
|
// do not add an empty $FIftiMain
|
|
writeln('TChmWriter.WriteFiftiMain SearchWriter.HasData=',SearchWriter.HasData);
|
|
if not SearchWriter.HasData then
|
|
begin
|
|
FFullTextSearchAvailable := False;
|
|
SearchWriter.Free;
|
|
Exit;
|
|
end;
|
|
FFullTextSearchAvailable := True;
|
|
SearchWriter.WriteToStream;
|
|
SearchWriter.Free;
|
|
|
|
if FFiftiMainStream.Size = 0 then
|
|
Exit;
|
|
|
|
FFiftiMainStream.Position := 0;
|
|
PostAddStreamToArchive('$FIftiMain', '/', FFiftiMainStream);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteWindows;
|
|
Var WindowStream : TMemoryStream;
|
|
i,j : Integer;
|
|
win : TChmWindow;
|
|
begin
|
|
if FWindows.Count>0 then
|
|
begin
|
|
WindowStream:=TMemoryStream.Create;
|
|
WindowStream.WriteDword(NToLE(dword(FWindows.Count)));
|
|
WindowStream.WriteDword(NToLE(dword(196))); // 1.1 or later. 188 is old style.
|
|
for i:=0 to FWindows.Count-1 Do
|
|
begin
|
|
Win:=TChmWindow(FWindows[i]);
|
|
WindowStream.WriteDword(NToLE(dword(196 ))); // 0 size of entry.
|
|
WindowStream.WriteDword(NToLE(dword(0 ))); // 4 unknown (bool Unicodestrings?)
|
|
WindowStream.WriteDword(NToLE(addstring(win.window_type ))); // 8 Arg 0, name of window
|
|
WindowStream.WriteDword(NToLE(dword(win.flags ))); // C valid fields
|
|
WindowStream.WriteDword(NToLE(dword(win.nav_style))); // 10 arg 10 navigation pane style
|
|
WindowStream.WriteDword(NToLE(addstring(win.title_bar_text))); // 14 Arg 1, title bar text
|
|
WindowStream.WriteDword(NToLE(dword(win.styleflags))); // 18 Arg 14, style flags
|
|
WindowStream.WriteDword(NToLE(dword(win.xtdstyleflags))); // 1C Arg 15, xtd style flags
|
|
WindowStream.WriteDword(NToLE(dword(win.left))); // 20 Arg 13, rect.left
|
|
WindowStream.WriteDword(NToLE(dword(win.top))); // 24 Arg 13, rect.top
|
|
WindowStream.WriteDword(NToLE(dword(win.right))); // 28 Arg 13, rect.right
|
|
WindowStream.WriteDword(NToLE(dword(win.bottom))); // 2C Arg 13, rect.bottom
|
|
WindowStream.WriteDword(NToLE(dword(win.window_show_state))); // 30 Arg 16, window show state
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 34 - , HWND hwndhelp OUT: window handle"
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 38 - , HWND hwndcaller OUT: who called this window"
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 3C - , HH_INFO_TYPE paINFO_TYPES IN: Pointer to an array of Information Types"
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 40 - , HWND hwndtoolbar OUT: toolbar window in tri-pane window"
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 44 - , HWND hwndnavigation OUT: navigation window in tri-pane window"
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 48 - , HWND hwndhtml OUT: window displaying HTML in tri-pane window"
|
|
WindowStream.WriteDword(NToLE(dword(win.navpanewidth))); // 4C Arg 11, width of nav pane
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 50 - , rect.left, OUT:Specifies the coordinates of the Topic pane
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 54 - , rect.top , OUT:Specifies the coordinates of the Topic pane
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 58 - , rect.right, OUT:Specifies the coordinates of the Topic pane
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 5C - , rect.bottom, OUT:Specifies the coordinates of the Topic pane
|
|
WindowStream.WriteDword(NToLE(addstring(win.toc_file))); // 60 Arg 2, toc file
|
|
WindowStream.WriteDword(NToLE(addstring(win.index_file))); // 64 Arg 3, index file
|
|
WindowStream.WriteDword(NToLE(addstring(win.default_file))); // 68 Arg 4, default file
|
|
WindowStream.WriteDword(NToLE(addstring(win.home_button_file))); // 6c Arg 5, home button file.
|
|
WindowStream.WriteDword(NToLE(dword(win.buttons))); // 70 arg 12,
|
|
WindowStream.WriteDword(NToLE(dword(win.navpane_initially_closed))); // 74 arg 17
|
|
WindowStream.WriteDword(NToLE(dword(win.navpane_default))); // 78 arg 18,
|
|
WindowStream.WriteDword(NToLE(dword(win.navpane_location))); // 7C arg 19,
|
|
WindowStream.WriteDword(NToLE(dword(win.wm_notify_id))); // 80 arg 20,
|
|
for j:=0 to 4 do
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 84 - byte[20] unknown - "BYTE tabOrder[HH_MAX_TABS + 1]; // IN/OUT: tab order: Contents, Index, Search, History, Favorites, Reserved 1-5, Custom tabs"
|
|
WindowStream.WriteDword(NToLE(dword(0))); // 94 - int cHistory; // IN/OUT: number of history items to keep (default is 30)
|
|
WindowStream.WriteDword(NToLE(addstring(win.Jumpbutton_1_Text))); // 9C Arg 7, The text of the Jump 1 button.
|
|
WindowStream.WriteDword(NToLE(addstring(win.Jumpbutton_2_Text))); // A0 Arg 9, The text of the Jump 2 button.
|
|
WindowStream.WriteDword(NToLE(addstring(win.Jumpbutton_1_File))); // A4 Arg 6, The file shown for Jump 1 button.
|
|
WindowStream.WriteDword(NToLE(addstring(win.Jumpbutton_2_File))); // A8 Arg 8, The file shown for Jump 1 button.
|
|
for j:=0 to 3 do
|
|
WindowStream.WriteDword(NToLE(dword(0))); // AA - byte[16] (TRECT) "RECT rcMinSize; // Minimum size for window (ignored in version 1)"
|
|
// 1.1+ fields
|
|
WindowStream.WriteDword(NToLE(dword(0))); // BC - int cbInfoTypes; // size of paInfoTypes;
|
|
WindowStream.WriteDword(NToLE(dword(0))); // C0 - LPCTSTR pszCustomTabs; // multiple zero-terminated strings
|
|
end;
|
|
WindowStream.Position := 0;
|
|
AddStreamToArchive('#WINDOWS', '/', WindowStream, True);
|
|
WindowStream.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TChmWriter.WriteInternalFilesAfter;
|
|
begin
|
|
// This creates and writes the #ITBITS (empty) file to section0
|
|
WriteITBITS;
|
|
// This creates and writes the #SYSTEM file to section0
|
|
WriteSystem;
|
|
end;
|
|
|
|
procedure TChmWriter.WriteFinalCompressedFiles;
|
|
begin
|
|
inherited WriteFinalCompressedFiles;
|
|
WriteTOPICS;
|
|
WriteURL_STR_TBL;
|
|
WriteSTRINGS;
|
|
WriteWINDOWS;
|
|
WriteFiftiMain;
|
|
end;
|
|
|
|
procedure TChmWriter.FileAdded(AStream: TStream; const AEntry: TFileEntryRec);
|
|
begin
|
|
inherited FileAdded(AStream, AEntry);
|
|
//writeln('TChmWriter.FileAdded FullTextSearch=',FullTextSearch);
|
|
if FullTextSearch then
|
|
CheckFileMakeSearchable(AStream, AEntry);
|
|
end;
|
|
|
|
procedure TChmWriter.WriteInternalFilesBefore;
|
|
begin
|
|
inherited WriteInternalFilesBefore;
|
|
WriteIVB;
|
|
WriteOBJINST;
|
|
end;
|
|
|
|
constructor TChmWriter.Create(AOutStream: TStream; FreeStreamOnDestroy: Boolean);
|
|
begin
|
|
inherited Create(AOutStream, FreeStreamOnDestroy);
|
|
FStringsStream := TmemoryStream.Create;
|
|
FTopicsStream := TMemoryStream.Create;
|
|
FURLSTRStream := TMemoryStream.Create;
|
|
FURLTBLStream := TMemoryStream.Create;
|
|
FFiftiMainStream := TMemoryStream.Create;
|
|
FIndexedFiles := TIndexedWordList.Create;
|
|
FAvlStrings := TAVLTree.Create(@CompareStrings); // dedupe strings
|
|
FAvlURLStr := TAVLTree.Create(@CompareUrlStrs); // dedupe urltbl + binindex must resolve URL to topicid
|
|
SpareString := TStringIndex.Create; // We need an object to search in avltree
|
|
SpareUrlStr := TUrlStrIndex.Create; // to avoid create/free circles we keep one in spare
|
|
// for searching purposes
|
|
FWindows := TObjectlist.Create(True);
|
|
FDefaultWindow:= '';
|
|
end;
|
|
|
|
destructor TChmWriter.Destroy;
|
|
begin
|
|
if Assigned(FContextStream) then FContextStream.Free;
|
|
FIndexedFiles.Free;
|
|
FStringsStream.Free;
|
|
FTopicsStream.Free;
|
|
FURLSTRStream.Free;
|
|
FURLTBLStream.Free;
|
|
FFiftiMainStream.Free;
|
|
SpareString.free;
|
|
SpareUrlStr.free;
|
|
FAvlUrlStr.FreeAndClear;
|
|
FAvlUrlStr.Free;
|
|
FAvlStrings.FreeAndClear;
|
|
FAvlStrings.Free;
|
|
FWindows.Free;
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
function TChmWriter.AddString(AString: String): LongWord;
|
|
var
|
|
NextBlock: DWord;
|
|
Pos: DWord;
|
|
n : TAVLTreeNode;
|
|
StrRec : TStringIndex;
|
|
begin
|
|
// #STRINGS starts with a null char
|
|
if FStringsStream.Size = 0 then FStringsStream.WriteByte(0);
|
|
|
|
SpareString.TheString:=AString;
|
|
n:=fAvlStrings.FindKey(SpareString,@CompareStrings);
|
|
if assigned(n) then
|
|
exit(TStringIndex(n.data).strid);
|
|
|
|
// each entry is a null terminated string
|
|
Pos := DWord(FStringsStream.Position);
|
|
|
|
// Strings are contained in $1000 byte blocks and cannot cross blocks
|
|
NextBlock := ($0000F000 and Pos) + $00001000;
|
|
if Length(AString) + 1 > NextBlock then
|
|
begin
|
|
FStringsStream.Size:= NextBlock;
|
|
FStringsStream.Position := NextBlock;
|
|
end;
|
|
|
|
Result := FStringsStream.Position;
|
|
FStringsStream.WriteBuffer(AString[1], Length(AString));
|
|
FStringsStream.WriteByte(0);
|
|
|
|
StrRec:=TStringIndex.Create;
|
|
StrRec.TheString:=AString;
|
|
StrRec.Strid :=Result;
|
|
fAvlStrings.Add(StrRec);
|
|
end;
|
|
|
|
function TChmWriter.AddURL ( AURL: String; TopicsIndex: DWord ) : LongWord;
|
|
|
|
procedure CheckURLStrBlockCanHold(Const AString: String);
|
|
var
|
|
Rem: LongWord;
|
|
Len: LongWord;
|
|
begin
|
|
Rem := $4000 - (FURLSTRStream.Size mod $4000);
|
|
Len := 9 + Length(AString); // 2 dwords the string and NT
|
|
if Rem < Len then
|
|
while Rem > 0 do
|
|
begin
|
|
FURLSTRStream.WriteByte(0);
|
|
Dec(Rem);
|
|
end;
|
|
end;
|
|
|
|
function AddURLString(Const AString: String): DWord;
|
|
var urlstrrec : TUrlStrIndex;
|
|
begin
|
|
CheckURLStrBlockCanHold(AString);
|
|
if FURLSTRStream.Size mod $4000 = 0 then
|
|
FURLSTRStream.WriteByte(0);
|
|
Result := FURLSTRStream.Position;
|
|
UrlStrRec:=TUrlStrIndex.Create;
|
|
UrlStrRec.UrlStr:=AString;
|
|
UrlStrRec.UrlStrid:=result;
|
|
FAvlUrlStr.Add(UrlStrRec);
|
|
FURLSTRStream.WriteDWord(NToLE(DWord(0))); // URL Offset for topic after the the "Local" value
|
|
FURLSTRStream.WriteDWord(NToLE(DWord(0))); // Offset of FrameName??
|
|
if Length(AString) > 0 then
|
|
FURLSTRStream.Write(AString[1], Length(AString));
|
|
FURLSTRStream.WriteByte(0); //NT
|
|
end;
|
|
|
|
function LookupUrlString(const AUrl : String):DWord;
|
|
var n :TAvlTreeNode;
|
|
begin
|
|
SpareUrlStr.UrlStr:=AUrl;
|
|
n:=FAvlUrlStr.FindKey(SpareUrlStr,@CompareUrlStrs);
|
|
if assigned(n) Then
|
|
result:=TUrlStrIndex(n.data).UrlStrId
|
|
else
|
|
result:=AddUrlString(AUrl);
|
|
end;
|
|
|
|
|
|
var UrlIndex : Integer;
|
|
|
|
begin
|
|
if (Length(AURL) > 0) and (AURL[1] = '/') then Delete(AURL,1,1);
|
|
UrlIndex:=LookupUrlString(AUrl);
|
|
|
|
//if $1000 - (FURLTBLStream.Size mod $1000) = 4 then // we are at 4092
|
|
if FURLTBLStream.Size and $FFC = $FFC then // faster :)
|
|
FURLTBLStream.WriteDWord(0);
|
|
Result := FURLTBLStream.Position;
|
|
FURLTBLStream.WriteDWord(0);//($231e9f5c); //unknown
|
|
FURLTBLStream.WriteDWord(NtoLE(TopicsIndex)); // Index of topic in #TOPICS
|
|
FURLTBLStream.WriteDWord(NtoLE(UrlIndex));
|
|
end;
|
|
|
|
|
|
|
|
procedure TChmWriter.CheckFileMakeSearchable(AStream: TStream; AFileEntry: TFileEntryRec);
|
|
|
|
var
|
|
TopicEntry: TTopicEntry;
|
|
ATitle: String;
|
|
begin
|
|
//writeln('TChmWriter.CheckFileMakeSearchable ',AFileEntry.Name);
|
|
if Pos('.ht', AFileEntry.Name) > 0 then
|
|
begin
|
|
//writeln('TChmWriter.CheckFileMakeSearchable indexing...');
|
|
ATitle := FIndexedFiles.IndexFile(AStream, NextTopicIndex, FSearchTitlesOnly);
|
|
if ATitle <> '' then
|
|
TopicEntry.StringsOffset := AddString(ATitle)
|
|
else
|
|
TopicEntry.StringsOffset := $FFFFFFFF;
|
|
TopicEntry.URLTableOffset := AddURL(AFileEntry.Path+AFileEntry.Name, NextTopicIndex);
|
|
TopicEntry.InContents := 2;
|
|
TopicEntry.Unknown := 0;
|
|
TopicEntry.TocOffset := 0;
|
|
FTopicsStream.WriteDWord(LEtoN(TopicEntry.TocOffset));
|
|
FTopicsStream.WriteDWord(LEtoN(TopicEntry.StringsOffset));
|
|
FTopicsStream.WriteDWord(LEtoN(TopicEntry.URLTableOffset));
|
|
FTopicsStream.WriteWord(LEtoN(TopicEntry.InContents));
|
|
FTopicsStream.WriteWord(LEtoN(TopicEntry.Unknown));
|
|
end;
|
|
end;
|
|
|
|
function TChmWriter.AddTopic(ATitle,AnUrl:AnsiString):integer;
|
|
|
|
var
|
|
TopicEntry: TTopicEntry;
|
|
|
|
begin
|
|
if ATitle <> '' then
|
|
TopicEntry.StringsOffset := AddString(ATitle)
|
|
else
|
|
TopicEntry.StringsOffset := $FFFFFFFF;
|
|
result:=NextTopicIndex;
|
|
TopicEntry.URLTableOffset := AddURL(AnUrl, Result);
|
|
TopicEntry.InContents := 2;
|
|
TopicEntry.Unknown := 0;
|
|
TopicEntry.TocOffset := 0;
|
|
FTopicsStream.WriteDWord(LEtoN(TopicEntry.TocOffset));
|
|
FTopicsStream.WriteDWord(LEtoN(TopicEntry.StringsOffset));
|
|
FTopicsStream.WriteDWord(LEtoN(TopicEntry.URLTableOffset));
|
|
FTopicsStream.WriteWord(LEtoN(TopicEntry.InContents));
|
|
FTopicsStream.WriteWord(LEtoN(TopicEntry.Unknown));
|
|
end;
|
|
|
|
function TChmWriter.NextTopicIndex: Integer;
|
|
begin
|
|
Result := FTopicsStream.Size div 16;
|
|
end;
|
|
|
|
procedure TChmWriter.AppendTOC(AStream: TStream);
|
|
|
|
var tmpstr : string;
|
|
begin
|
|
fHasTOC := True;
|
|
if fTocName = '' then
|
|
tmpstr := defaulthhc
|
|
else
|
|
tmpstr := fTocName;
|
|
PostAddStreamToArchive(tmpstr, '/', AStream, True);
|
|
end;
|
|
|
|
procedure TChmWriter.AppendBinaryTOCFromSiteMap(ASiteMap: TChmSiteMap);
|
|
var
|
|
Header: TTOCIdxHeader;
|
|
Entry: TTocEntry;
|
|
EntryInfo: TTOCEntryPageBookInfo;
|
|
|
|
|
|
EntryInfoStream,
|
|
EntryTopicOffsetStream,
|
|
EntryStream: TMemoryStream;
|
|
|
|
TOCIDXStream: TMemoryStream;
|
|
|
|
NextLevelItems,
|
|
CurrentLevelItems: TFPList;
|
|
i,j: Integer;
|
|
MenuItem: TChmSiteMapItem;
|
|
MenuItems: TChmSiteMapItems;
|
|
TopicEntry: TTopicEntry;
|
|
EntryCount: DWord = $29A;
|
|
procedure FixParentBookFirstChildOffset(AChildOffset: DWord);
|
|
var
|
|
ParentEntry: TTOCEntryPageBookInfo;
|
|
begin
|
|
// read parent entry
|
|
EntryInfoStream.Position := MenuItems.InternalData;
|
|
EntryInfoStream.Read(ParentEntry, SizeOf(ParentEntry));
|
|
// update child offset
|
|
ParentEntry.FirstChildOffset:= NtoLE(DWord(4096 + AChildOffset));
|
|
// write back to stream
|
|
EntryInfoStream.Position := MenuItems.InternalData;
|
|
EntryInfoStream.Write(ParentEntry, SizeOf(ParentEntry));
|
|
// move to end of stream
|
|
EntryInfoStream.Position := AChildOffset;
|
|
end;
|
|
|
|
begin
|
|
FillChar(Header, 4096, 0);
|
|
// create streams
|
|
TOCIDXStream := TMemoryStream.Create;
|
|
EntryInfoStream := TMemoryStream.Create;
|
|
EntryTopicOffsetStream := TMemoryStream.Create;
|
|
EntryStream := TMemoryStream.Create;
|
|
|
|
NextLevelItems := TFPList.Create;
|
|
|
|
NextLevelItems.Add(ASiteMap.Items);
|
|
|
|
if NextLevelItems.Count = 0 then
|
|
FreeAndNil(NextLevelItems);
|
|
|
|
while NextLevelItems <> nil do
|
|
begin
|
|
CurrentLevelItems := NextLevelItems;
|
|
NextLevelItems := TFPList.Create;
|
|
|
|
for i := 0 to CurrentLevelItems.Count-1 do
|
|
begin
|
|
MenuItems := TChmSiteMapItems(CurrentLevelItems.Items[i]);
|
|
|
|
for j := 0 to MenuItems.Count-1 do
|
|
begin
|
|
MenuItem := MenuItems.Item[j];
|
|
// first figure out the props
|
|
EntryInfo.Props := 0;
|
|
if MenuItem.Children.Count > 0 then
|
|
EntryInfo.Props := EntryInfo.Props or TOC_ENTRY_HAS_CHILDREN;
|
|
if Length(MenuItem.Local) > 0 then
|
|
EntryInfo.Props := EntryInfo.Props or TOC_ENTRY_HAS_LOCAL;
|
|
|
|
|
|
if EntryInfo.Props and TOC_ENTRY_HAS_LOCAL > 0 then
|
|
begin
|
|
// Write #TOPICS entry
|
|
TopicEntry.TocOffset := NtoLE(DWord(4096 + EntryInfoStream.Position));
|
|
TopicEntry.StringsOffset := NtoLE(AddString(MenuItem.Text));
|
|
TopicEntry.URLTableOffset := NtoLE(AddURL(MenuItem.Local, NextTopicIndex));
|
|
TopicEntry.InContents := NtoLE(Word( 2 ));
|
|
TopicEntry.Unknown := 0;
|
|
EntryInfo.TopicsIndexOrStringsOffset := NtoLE(Dword(NextTopicIndex));;
|
|
FTopicsStream.Write(TopicEntry, SizeOf(TopicEntry));
|
|
EntryTopicOffsetStream.WriteDWord(EntryInfo.TopicsIndexOrStringsOffset);
|
|
|
|
// write TOCEntry
|
|
Entry.PageBookInfoOffset:= NtoLE(4096 + EntryInfoStream.Position);
|
|
Entry.IncrementedInt := NtoLE(EntryCount);
|
|
EntryStream.Write(Entry, SizeOf(Entry));
|
|
Inc(EntryCount);
|
|
|
|
end
|
|
else
|
|
begin
|
|
EntryInfo.TopicsIndexOrStringsOffset := NtoLE(AddString(MenuItem.Text));
|
|
end;
|
|
|
|
|
|
// write TOCEntryInfo
|
|
|
|
EntryInfo.Unknown1 := 0;
|
|
EntryInfo.EntryIndex := NtoLE(Word(EntryCount - $29A)); //who knows how useful any of this is
|
|
|
|
if MenuItems.InternalData <> maxLongint then
|
|
EntryInfo.ParentPageBookInfoOffset := MenuItems.InternalData
|
|
else
|
|
EntryInfo.ParentPageBookInfoOffset := 0;
|
|
|
|
if j = MenuItems.Count-1 then
|
|
EntryInfo.NextPageBookOffset := 0
|
|
else if (EntryInfo.Props and TOC_ENTRY_HAS_CHILDREN) > 0 then
|
|
EntryInfo.NextPageBookOffset := 4096 + EntryInfoStream.Position + 28
|
|
else
|
|
EntryInfo.NextPageBookOffset := 4096 + EntryInfoStream.Position + 20;
|
|
|
|
// Only if TOC_ENTRY_HAS_CHILDREN is set are these written
|
|
EntryInfo.FirstChildOffset := 0; // we will update this when the child is written
|
|
// in fact lets update the *parent* of this item now if needed
|
|
if (j = 0) and (MenuItems.InternalData <> maxLongint) then
|
|
FixParentBookFirstChildOffset(EntryInfoStream.Position);
|
|
|
|
EntryInfo.Unknown3 := 0;
|
|
|
|
// fix endian order
|
|
EntryInfo.Props := NtoLE(EntryInfo.Props);
|
|
EntryInfo.ParentPageBookInfoOffset := NtoLE(EntryInfo.ParentPageBookInfoOffset);
|
|
EntryInfo.NextPageBookOffset := NtoLE(EntryInfo.NextPageBookOffset);
|
|
|
|
if MenuItem.Children.Count > 0 then
|
|
begin
|
|
NextLevelItems.Add(MenuItem.Children);
|
|
MenuItem.Children.InternalData := EntryInfoStream.Position;
|
|
end;
|
|
|
|
// write to stream
|
|
EntryInfoStream.Write(EntryInfo, PageBookInfoRecordSize(@EntryInfo));
|
|
end;
|
|
end;
|
|
|
|
FreeAndNil(CurrentLevelItems);
|
|
if NextLevelItems.Count = 0 then
|
|
FreeAndNil(NextLevelItems);
|
|
end;
|
|
|
|
// write all streams to TOCIdxStream and free everything
|
|
EntryInfoStream.Position:=0;
|
|
EntryTopicOffsetStream.Position:=0;
|
|
EntryStream.Position:=0;
|
|
|
|
Header.BlockSize := NtoLE(DWord(4096));
|
|
Header.EntriesCount := NtoLE(DWord(EntryCount - $29A));
|
|
Header.EntriesOffset := NtoLE(DWord(4096 + EntryInfoStream.Size + EntryTopicOffsetStream.Size));
|
|
Header.TopicsOffset := NtoLE(DWord(4096 + EntryInfoStream.Size));
|
|
|
|
TOCIDXStream.Write(Header, SizeOf(Header));
|
|
|
|
TOCIDXStream.CopyFrom(EntryInfoStream, EntryInfoStream.Size);
|
|
EntryInfoStream.Free;
|
|
|
|
TOCIDXStream.CopyFrom(EntryTopicOffsetStream, EntryTopicOffsetStream.Size);
|
|
EntryTopicOffsetStream.Free;
|
|
|
|
TOCIDXStream.CopyFrom(EntryStream, EntryStream.Size);
|
|
EntryStream.Free;
|
|
|
|
TOCIDXStream.Position := 0;
|
|
AppendBinaryTOCStream(TOCIDXStream);
|
|
TOCIDXStream.Free;
|
|
end;
|
|
|
|
Const
|
|
BinIndexIdent : array[0..1] of char = (CHR($3B),CHR($29));
|
|
AlwaysX44 : Array[0..15] of char = ('X','4','4',#0,#0,#0,#0,#0,
|
|
#0,#0,#0,#0,#0,#0,#0,#0);
|
|
DataEntry : Array[0..12] of Byte = ($00,$00,$00,$00,$05,$00,$00,$00,$80,$00,$00,$00,$00);
|
|
{
|
|
IndexStream:=TMemoryStream.Create;
|
|
IndexStream.Write(BinIndexIdent,2);
|
|
IndexStream.Write(NToLE(word(2)),2);
|
|
IndexStream.Write(NToLE(word(2048)),2);
|
|
IndexStream.Write(AlwaysX44,sizeof(AlwaysX44));
|
|
IndexStrem.Write (dword(0),2);
|
|
}
|
|
|
|
Const DefBlockSize = 2048;
|
|
|
|
Type TIndexBlock = Array[0..DefBlockSize-1] of Byte;
|
|
|
|
procedure writeword(var p:pbyte;w:word); inline;
|
|
|
|
begin
|
|
pword(p)^:=NToLE(w);
|
|
inc(pword(p));
|
|
end;
|
|
|
|
procedure writedword(var p:pbyte;d:dword); inline;
|
|
|
|
begin
|
|
pdword(p)^:=NToLE(d);
|
|
inc(pdword(p));
|
|
end;
|
|
|
|
procedure TChmWriter.AppendBinaryIndexFromSiteMap(ASiteMap: TChmSiteMap;chw:boolean);
|
|
|
|
Var
|
|
IndexStream : TMemoryStream;
|
|
//n : Integer;
|
|
curblock : TIndexBlock; // current listing block being built
|
|
TestBlock : TIndexBlock; // each entry is first built here. then moved to curblock
|
|
curind : integer; // next byte to write in testblock.
|
|
blocknr : Integer; // blocknr of block in testblock;
|
|
lastblock : Integer; // blocknr of last block.
|
|
Entries : Integer; // Number of entries in this block so far
|
|
TotalEntries: Integer; // Total number of entries
|
|
MapEntries : Integer;
|
|
MapIndex : Integer;
|
|
indexblocknr: Integer;
|
|
blockind : Integer; // next byte to write in blockn[blocknr]
|
|
blockentries: Integer; // entries so far ins blockn[blocknr]
|
|
blockn : Array Of TIndexBlock;
|
|
BlockNPlus1 : Array of TIndexBlock;
|
|
Mod13value : integer; // A value that is increased by 13 for each entry. (?!?!)
|
|
EntryToIndex: boolean; // helper var to make sure the first block is always indexed.
|
|
blocknplusindex : Integer; // blocks in level n+1 (second part)
|
|
blocknplusentries : Integer; // The other blocks indexed on creation.
|
|
datastream,mapstream,propertystream : TMemoryStream;
|
|
|
|
procedure preparecurrentblock;
|
|
|
|
var p: PBTreeBlockHeader;
|
|
|
|
begin
|
|
p:=@curblock[0];
|
|
p^.Length:=NToLE(Defblocksize-curind);
|
|
p^.NumberOfEntries:=Entries;
|
|
p^.IndexOfPrevBlock:=dword(lastblock);
|
|
p^.IndexOfNextBlock:=Blocknr;
|
|
IndexStream.Write(curblock[0],Defblocksize);
|
|
MapStream.Write(NToLE(MapEntries),sizeof(dword));
|
|
MapStream.Write(NToLE(BlockNr),Sizeof(DWord));
|
|
MapEntries:=TotalEntries;
|
|
curind:=sizeof(TBtreeBlockHeader); // index into current block;
|
|
lastblock:=blocknr;
|
|
inc(blocknr);
|
|
end;
|
|
|
|
procedure prepareindexblockn(listingblocknr:integer);
|
|
var p:PBTreeIndexBlockHeader;
|
|
begin
|
|
p:=@Blockn[IndexBlockNr];
|
|
p^.Length:=defblocksize-BlockInd;
|
|
p^.NumberOfEntries:=BlockEntries;
|
|
|
|
// p^.IndexOfChildBlock // already entered on block creation, since of first entry, not last.
|
|
inc(Indexblocknr);
|
|
BlockEntries:=0;
|
|
BlockInd:=0;
|
|
if Indexblocknr>=length(blockn) then
|
|
setlength(blockn,length(blockn)+1); // larger increments also possible. #blocks is kept independantly.
|
|
p:=@Blockn[IndexBlockNr];
|
|
p^.IndexOfChildBlock:=ListingBlockNr;
|
|
blockind:=sizeof(TBTreeIndexBlockHeader);
|
|
end;
|
|
|
|
procedure finalizeindexblockn(p:pbyte;var ind:integer;Entries:integer);
|
|
var ph:PBTreeIndexBlockHeader;
|
|
begin
|
|
ph:=PBTreeIndexBlockHeader(p);
|
|
ph^.Length:=defblocksize-Ind;
|
|
ph^.NumberOfEntries:=Entries;
|
|
// p^.IndexOfChildBlock // already entered on block creation, since of first entry, not last.
|
|
// inc(Ind);
|
|
end;
|
|
|
|
procedure CurEntryToIndex(entrysize:integer);
|
|
var p,pentry : pbyte;
|
|
indexentrysize : integer;
|
|
begin
|
|
indexentrysize:=entrysize-sizeof(dword); // index entry is 4 bytes shorter, and only the last dword differs
|
|
if (blockind+indexentrysize)>=Defblocksize then
|
|
prepareindexblockn(blocknr);
|
|
p:=@blockn[Indexblocknr][blockind];
|
|
move(testblock[0],p^,indexentrysize);
|
|
pentry:=@p[indexentrysize-sizeof(dword)]; // ptr to last dword
|
|
writedword(pentry,blocknr); // patch up the "index of child field"
|
|
inc(blockind,indexentrysize);
|
|
end;
|
|
|
|
procedure CreateEntry(Item:TChmSiteMapItem;Str:WideString;commaatposition:integer);
|
|
|
|
var p : pbyte;
|
|
topicid: integer;
|
|
seealso: Integer;
|
|
entrysize:Integer;
|
|
i : Integer;
|
|
begin
|
|
inc(TotalEntries);
|
|
p:=@TestBlock[0];
|
|
for i:=1 to Length(str) do
|
|
WriteWord(p,Word(str[i])); // write the wstr in little endian
|
|
WriteWord(p,0); // NT
|
|
// if item.seealso='' then // no seealso for now
|
|
seealso:=0;
|
|
// else
|
|
// seealso:=2;
|
|
WriteWord(p,seealso); // =0 not a see also 2 =seealso
|
|
WriteWord(p,2); // Entrydepth. We can't know it, so write 2.
|
|
WriteDword(p,commaatposition); // position of the comma
|
|
WriteDword(p,0); // unused 0
|
|
WriteDword(p,1); // for now only local pair.
|
|
TopicId:=AddTopic(Item.Text,item.Local);
|
|
WriteDword(p,TopicId);
|
|
// if seealso then _here_ a wchar NT string with seealso?
|
|
WriteDword(p,1); // always 1 (unknown);
|
|
WriteDword(p,mod13value); //a value that increments with 13.
|
|
mod13value:=mod13value+13;
|
|
entrysize:=p-pbyte(@testblock[0]);
|
|
if (curind+entrysize)>=Defblocksize then
|
|
begin
|
|
preparecurrentblock;
|
|
EntrytoIndex:=true;
|
|
end;
|
|
if EntryToIndex Then
|
|
begin
|
|
CurEntryToIndex(entrysize);
|
|
EntryToIndex:=False;
|
|
end;
|
|
move(testblock[0],curblock[curind],entrysize);
|
|
inc(curind,entrysize);
|
|
datastream.write(DataEntry,Sizeof(DataEntry));
|
|
end;
|
|
|
|
procedure MoveIndexEntry(nr:integer;bytes:integer;childblock:integer);
|
|
var
|
|
pscr,pdest : pbyte;
|
|
begin
|
|
{$ifdef binindex}
|
|
writeln(' moveindexentry ',nr,' bytes:',bytes,' childblock:',childblock);
|
|
flush(stdout);
|
|
{$endif}
|
|
|
|
if ((blockind+bytes)>=defblocksize) then
|
|
begin
|
|
{$ifdef binindex}
|
|
writeln(' in scalecheck ',blockind);
|
|
flush(stdout);
|
|
{$endif}
|
|
|
|
FinalizeIndexBlockn(@blocknplus1[blocknplusindex][0],blockind,blocknplusentries);
|
|
inc(blocknplusindex);
|
|
if blocknplusindex>=length(blocknplus1) then
|
|
setlength(blocknplus1,length(blocknplus1)+1);
|
|
blockInd:=Sizeof(TBTreeIndexBlockHeader);
|
|
pdword(@blocknplus1[blocknplusindex][0])[4]:=NToLE(ChildBlock); /// init 2nd level index to first 1st level index block
|
|
end;
|
|
{$ifdef binindex}
|
|
writeln(' len:',length(blocknplus1),' blockind:',blockind,' index:',blocknplusindex);
|
|
flush(stdout);
|
|
{$endif}
|
|
|
|
// copy entry from one indexblock to another
|
|
pscr:=@blockn[nr][sizeof(TBtreeIndexBlockHeader)];
|
|
pdest:=@blocknplus1[blocknplusindex][blockind];
|
|
move(pscr^,pdest^,bytes);
|
|
pdword(@pdest[bytes-sizeof(dword)])^:=NToLE(childblock); // correcting the childindex
|
|
inc (blockind,bytes);
|
|
inc(blocknplusentries); // not needed for writing, but used to check if something has been written. End condition
|
|
end;
|
|
|
|
function ScanIndexBlock(blk:Pbyte):Integer;
|
|
|
|
var start : pbyte;
|
|
n : Integer;
|
|
i : Integer;
|
|
begin
|
|
start:=@blk[sizeof(TBtreeIndexBlockHeader)];
|
|
blk:=start;
|
|
while pword(blk)^<>0 do // skip wchar
|
|
inc(pword(blk));
|
|
inc(pword(blk)); // skip NT
|
|
inc(pword(blk)); // skip see also
|
|
inc(pword(blk)); // skip depth
|
|
inc(pdword(blk)); // skip Character Index.
|
|
inc(pdword(blk)); // skip always 0
|
|
n:=LEToN(pdword(blk)^);
|
|
inc(pdword(blk)); // skip nr of pairs.
|
|
for i:= 1 to n do
|
|
inc(pdword(blk)); // skip <n> topicids
|
|
inc(pdword(blk)); // skip childindex
|
|
Result:=blk-start;
|
|
end;
|
|
|
|
procedure CombineWithChildren(ParentItem:TChmSiteMapItem;Str:WideString;commaatposition:integer;first:boolean);
|
|
var i : Integer;
|
|
Item : TChmSiteMapItem;
|
|
begin
|
|
if ParentItem.Children.Count = 0 Then
|
|
Begin
|
|
// comment/fix next
|
|
// if commatposition=length(str) then commaatposition:=0;
|
|
if first then
|
|
CreateEntry(ParentItem,Str,0)
|
|
else
|
|
CreateEntry(ParentItem,Str,commaatposition);
|
|
End
|
|
Else
|
|
for i:=0 to ParentItem.Children.Count-1 do
|
|
begin
|
|
item := TChmSiteMapItem(ParentItem.Children.Item[i]);
|
|
if first Then
|
|
CombineWithChildren(Item,Str+', '+item.text,commaatposition+2,false)
|
|
else
|
|
CombineWithChildren(Item,Str+', '+item.text,commaatposition,false);
|
|
end;
|
|
end;
|
|
|
|
Var i : Integer;
|
|
Key : WideString;
|
|
Item : TChmSiteMapItem;
|
|
ListingBlocks : Integer;
|
|
EntryBytes : Integer;
|
|
Hdr : TBTreeHeader;
|
|
TreeDepth : Integer;
|
|
|
|
{$ifdef binindex}
|
|
procedure printloopvars(i:integer);
|
|
|
|
begin
|
|
Writeln('location :' ,i, ' blocknr :', blocknr,' level:',TreeDepth);
|
|
Writeln('blockn length: ',length(blockn),' indexblocknr: ',indexblocknr,' blockind ',blockind);
|
|
Writeln('blocknplus1 length: ',length(blocknplus1),' blocknplusindex:',blocknplusindex,' entries:',blocknplusentries);
|
|
flush(stdout);
|
|
end;
|
|
{$endif}
|
|
begin
|
|
IndexStream:=TMemoryStream.Create;
|
|
indexstream.size:=sizeof(TBTreeHeader);
|
|
IndexStream.position:=Sizeof(TBTreeHeader);
|
|
datastream:=TMemoryStream.Create;
|
|
mapstream :=TMemoryStream.Create;
|
|
mapstream.size:=2;
|
|
mapstream.position:=2;
|
|
propertystream :=TMemoryStream.Create;
|
|
propertystream.write(NToLE(0),sizeof(4));
|
|
// we iterate over all entries and write listingblocks directly to the stream.
|
|
// and the first (and maybe last) level is written to blockn.
|
|
// we can't do higher levels yet because we don't know how many listblocks we get
|
|
BlockNr :=0; // current block number
|
|
Lastblock :=-1; // previous block nr or -1 if none.
|
|
Entries :=0; // entries in this block
|
|
TotalEntries:=0; // entries so far.
|
|
Mod13value :=0; // value that increments by 13 entirely.
|
|
indexblocknr:=0; // nr of first index block.
|
|
BlockEntries:=0; // entries into current block;
|
|
MapEntries :=0; // entries before the current listing block, for MAP file
|
|
TreeDepth :=0;
|
|
|
|
curind :=sizeof(TBTreeBlockHeader); // index into current listing block;
|
|
blockind :=sizeof(TBtreeIndexBlockHeader); // index into current index block
|
|
|
|
Setlength(blockn,1);
|
|
pdword(@blockn[0][4])^:=NToLE(0); /// init first listingblock nr to 0 in the first index block
|
|
EntryToIndex := True;
|
|
for i:=0 to ASiteMap.Items.Count-1 do
|
|
begin
|
|
item := TChmSiteMapItem(ASiteMap.Items.Item[i]);
|
|
key :=Item.Text;
|
|
{$ifdef chm_windowsbinindex}
|
|
// append 2 to all index level 0 entries. This
|
|
// so we can see if Windows loads the binary or textual index.
|
|
CombineWithChildren(Item,Key+'2',length(key)+1,true);
|
|
{$else}
|
|
CombineWithChildren(Item,Key,length(key),true);
|
|
{$endif}
|
|
end;
|
|
PrepareCurrentBlock; // flush last listing block.
|
|
Listingblocks:=blocknr; // blocknr is from now on the number of the first block in blockn.
|
|
// we still need the # of listingblocks for the header though
|
|
|
|
{$ifdef binindex}
|
|
writeln('binindex: listingblocks : '+inttostr(listingblocks),' indexblocks: ',indexblocknr,' entries:',blockentries);
|
|
{$endif}
|
|
|
|
// we have now created and written the listing blocks, and created the first level of index in <blockn>
|
|
// the following loop uses <blockn> to calculate the next level (in blocknplus1), then write out blockn,
|
|
// and repeat until we have no entries left.
|
|
|
|
// First we finalize the current set of blocks
|
|
|
|
if Blockind<>sizeof(TBtreeIndexBlockHeader) Then
|
|
begin
|
|
{$ifdef binindex}
|
|
writeln('finalizing level 1 index');
|
|
{$endif}
|
|
FinalizeIndexBlockN(@blockn[indexblocknr][0],blockind,blockentries); // also increasing indexblocknr
|
|
inc(IndexBlockNr);
|
|
end;
|
|
{$ifdef binindex}
|
|
writeln('binindex: listingblocks : '+inttostr(listingblocks),' indexblocks: ',indexblocknr,' entries:',blockentries);
|
|
{$endif}
|
|
|
|
|
|
while (Indexblocknr>1) do
|
|
begin
|
|
{$ifdef binindex}
|
|
printloopvars(1);
|
|
{$endif}
|
|
|
|
blockind :=sizeof(TBtreeIndexBlockHeader);
|
|
pdword(@blockn[0][4])^:=NToLE(Listingblocks); /// init 2nd level index to first 1st level index block
|
|
blocknplusindex :=0;
|
|
blocknplusentries :=0;
|
|
if length(blocknplus1)<1 then
|
|
Setlength(blocknplus1,1);
|
|
|
|
EntryToIndex :=True;
|
|
{$ifdef binindex}
|
|
printloopvars(2);
|
|
{$endif}
|
|
for i:=0 to Indexblocknr-1 do
|
|
begin
|
|
Entrybytes:=ScanIndexBlock(@blockn[i][0]);
|
|
// writeln('after scan ,',i, ' bytes: ',entrybytes,' blocknr:',blocknr,' indexblocknr:',indexblocknr,' to:',blocknr+i);
|
|
MoveIndexEntry(i,Entrybytes,blocknr+i);
|
|
indexStream.Write(blockn[i][0],defblocksize);
|
|
end;
|
|
|
|
{$ifdef binindex}
|
|
printloopvars(3);
|
|
{$endif}
|
|
|
|
If Blockind<>sizeof(TBtreeIndexBlockHeader) Then
|
|
begin
|
|
{$ifdef binindex}
|
|
logentry('finalizing');
|
|
{$endif}
|
|
FinalizeIndexBlockn(@blocknplus1[blocknplusindex][0],blockind,blocknplusentries);
|
|
inc(blocknplusindex);
|
|
end;
|
|
|
|
inc(blocknr,indexblocknr);
|
|
|
|
indexblocknr:=blocknplusindex;
|
|
blockn:=copy(blocknplus1); setlength(blocknplus1,1);
|
|
{$ifdef binindex}
|
|
printloopvars(5);
|
|
{$endif}
|
|
|
|
inc(TreeDepth);
|
|
end;
|
|
indexStream.Write(blockn[0][0],defblocksize);
|
|
inc(blocknr);
|
|
// Fixup header.
|
|
hdr.ident[0]:=chr($3B); hdr.ident[1]:=chr($29);
|
|
hdr.flags :=NToLE(word($2)); // bit $2 is always 1, bit $0400 1 if dir? (always on)
|
|
hdr.blocksize :=NToLE(word(defblocksize)); // size of blocks (2048)
|
|
hdr.dataformat :=AlwaysX44; // "X44" always the same, see specs.
|
|
hdr.unknown0 :=NToLE(0); // always 0
|
|
hdr.lastlstblock :=NToLE(dword(ListingBlocks-1)); // index of last listing block in the file;
|
|
hdr.indexrootblock :=NToLE(dword(blocknr-1)); // Index of the root block in the file.
|
|
hdr.unknown1 :=NToLE(dword(-1)); // always -1
|
|
hdr.nrblock :=NToLE(blocknr); // Number of blocks
|
|
hdr.treedepth :=NToLE(word(TreeDepth)); // The depth of the tree of blocks (1 if no index blocks, 2 one level of index blocks, ...)
|
|
hdr.nrkeywords :=NToLE(Totalentries); // number of keywords in the file.
|
|
hdr.codepage :=NToLE(dword(1252)); // Windows code page identifier (usually 1252 - Windows 3.1 US (ANSI))
|
|
hdr.lcid :=NToLE(0); // ???? LCID from the HHP file.
|
|
if not chw then
|
|
hdr.ischm :=NToLE(dword(1)) // 0 if this a BTREE and is part of a CHW file, 1 if it is a BTree and is part of a CHI or CHM file
|
|
else
|
|
hdr.ischm :=NToLE(0);
|
|
hdr.unknown2 :=NToLE(dword(10031)); // Unknown. Almost always 10031. Also 66631 (accessib.chm, ieeula.chm, iesupp.chm, iexplore.chm, msoe.chm, mstask.chm, ratings.chm, wab.chm).
|
|
hdr.unknown3 :=NToLE(0); // unknown 0
|
|
hdr.unknown4 :=NToLE(0); // unknown 0
|
|
hdr.unknown5 :=NToLE(0); // unknown 0
|
|
|
|
IndexStream.Position:=0;
|
|
IndexStream.write(hdr,sizeof(hdr));
|
|
{$ifdef binindex}
|
|
logentry('before append');
|
|
{$endif}
|
|
|
|
AppendBinaryIndexStream(IndexStream,datastream,MapStream,PropertyStream,chw);
|
|
IndexStream.Free;
|
|
PropertyStream.Free;
|
|
MapStream.Free;
|
|
DataStream.Free;
|
|
end;
|
|
|
|
procedure TChmWriter.AppendBinaryTOCStream(AStream: TStream);
|
|
begin
|
|
AddStreamToArchive('#TOCIDX', '/', AStream, True);
|
|
end;
|
|
|
|
procedure TChmWriter.AppendBinaryIndexStream(IndexStream,DataStream,MapStream,Propertystream: TStream;chw:boolean);
|
|
|
|
procedure stadd(fn:string;stream:TStream);
|
|
|
|
begin
|
|
Stream.Position:=0;
|
|
if CHW then
|
|
fn:=uppercase(fn);
|
|
{$ifdef binindex}
|
|
logentry('before append '+fn);
|
|
{$endif}
|
|
AddStreamToArchive(fn,'/$WWKeywordLinks/',stream,True);
|
|
end;
|
|
|
|
begin
|
|
stadd('BTree',IndexStream);
|
|
stadd('Data', DataStream);
|
|
stadd('Map' , MapStream);
|
|
stadd('Property', PropertyStream);
|
|
end;
|
|
|
|
procedure TChmWriter.AppendIndex(AStream: TStream);
|
|
var tmpstr : string;
|
|
begin
|
|
FHasIndex := True;
|
|
if fIndexName = '' then
|
|
tmpstr:=defaulthhk
|
|
else
|
|
tmpstr:=fIndexName;
|
|
PostAddStreamToArchive(tmpstr, '/', AStream, True);
|
|
end;
|
|
|
|
procedure TChmWriter.AppendSearchDB(AName: String; AStream: TStream);
|
|
begin
|
|
PostAddStreamToArchive(AName, '/', AStream);
|
|
end;
|
|
|
|
|
|
procedure TChmWriter.AddContext(AContext: DWord; ATopic: String);
|
|
var
|
|
Offset: DWord;
|
|
begin
|
|
if FContextStream = nil then begin
|
|
FContextStream:=TMemoryStream.Create;
|
|
// #IVB starts with a dword which is the size of the stream - sizeof(dword)
|
|
FContextStream.WriteDWord(0);
|
|
// we will update this when we write the file to the final stream
|
|
end;
|
|
// an entry is a context id and then the offset of the name of the topic in the strings file
|
|
FContextStream.WriteDWord(NToLE(AContext));
|
|
Offset := NToLE(AddString(ATopic));
|
|
FContextStream.WriteDWord(Offset);
|
|
end;
|
|
|
|
procedure TChmWriter.SetWindows(AWindowList:TObjectList);
|
|
|
|
var i : integer;
|
|
x : TCHMWindow;
|
|
begin
|
|
FWindows.Clear;
|
|
for i:=0 to AWindowList.count -1 do
|
|
begin
|
|
x:=TChmWindow.Create;
|
|
x.assign(TChmWindow(AWindowList[i]));
|
|
Fwindows.Add(x);
|
|
end;
|
|
end;
|
|
|
|
|
|
end.
|
|
|