chmwriter.pas 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  1. { Copyright (C) <2005> <Andrew Haines> chmwriter.pas
  2. This library is free software; you can redistribute it and/or modify it
  3. under the terms of the GNU Library General Public License as published by
  4. the Free Software Foundation; either version 2 of the License, or (at your
  5. option) any later version.
  6. This program is distributed in the hope that it will be useful, but WITHOUT
  7. ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  8. FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  9. for more details.
  10. You should have received a copy of the GNU Library General Public License
  11. along with this library; if not, write to the Free Software Foundation,
  12. Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  13. }
  14. {
  15. See the file COPYING.FPC, included in this distribution,
  16. for details about the copyright.
  17. }
  18. unit chmwriter;
  19. {$MODE OBJFPC}{$H+}
  20. interface
  21. uses Classes, ChmBase, chmtypes, chmspecialfiles;
  22. type
  23. TGetDataFunc = function (const DataName: String; out PathInChm: String; out FileName: String; var Stream: TStream): Boolean of object;
  24. // DataName : A FileName or whatever so that the getter can find and open the file to add
  25. // PathInChm: This is the absolute path in the archive. i.e. /home/user/helpstuff/
  26. // becomes '/' and /home/user/helpstuff/subfolder/ > /subfolder/
  27. // FileName : /home/user/helpstuff/index.html > index.html
  28. // Stream : the file opened with DataName should be written to this stream
  29. { TChmWriter }
  30. TChmWriter = class(TObject)
  31. FOnLastFile: TNotifyEvent;
  32. private
  33. ForceExit: Boolean;
  34. FDefaultFont: String;
  35. FDefaultPage: String;
  36. FFullTextSearch: Boolean;
  37. FInternalFiles: TFileEntryList; // Contains a complete list of files in the chm including
  38. FFrameSize: LongWord; // uncompressed files and special internal files of the chm
  39. FCurrentStream: TStream; // used to buffer the files that are to be compressed
  40. FCurrentIndex: Integer;
  41. FOnGetFileData: TGetDataFunc;
  42. FStringsStream: TMemoryStream;
  43. FContextStream: TMemoryStream; // the #IVB file
  44. FSection0: TMemoryStream;
  45. FSection1: TStream; // Compressed Stream
  46. FSection1Size: QWord;
  47. FSection1ResetTable: TMemoryStream; // has a list of frame positions NOT window positions
  48. FDirectoryListings: TStream;
  49. FOutStream: TStream;
  50. FFileNames: TStrings;
  51. FDestroyStream: Boolean;
  52. FTempStream: TStream;
  53. FPostStream: TStream;
  54. FTitle: String;
  55. FHasTOC: Boolean;
  56. FHasIndex: Boolean;
  57. FWindowSize: LongWord;
  58. FReadCompressedSize: QWord; // Current Size of Uncompressed data that went in Section1 (compressed)
  59. // Linear order of file
  60. ITSFHeader: TITSFHeader;
  61. HeaderSection0Table: TITSFHeaderEntry; // points to HeaderSection0
  62. HeaderSection1Table: TITSFHeaderEntry; // points to HeaderSection1
  63. HeaderSuffix: TITSFHeaderSuffix; //contains the offset of CONTENTSection0 from zero
  64. HeaderSection0: TITSPHeaderPrefix;
  65. HeaderSection1: TITSPHeader; // DirectoryListings header
  66. // DirectoryListings
  67. // CONTENT Section 0 (section 1 is contained in section 0)
  68. // EOF
  69. // end linear header parts
  70. procedure InitITSFHeader;
  71. procedure InitHeaderSectionTable;
  72. procedure SetTempRawStream(const AValue: TStream);
  73. procedure WriteHeader(Stream: TStream);
  74. procedure CreateDirectoryListings;
  75. procedure WriteDirectoryListings(Stream: TStream);
  76. procedure StartCompressingStream;
  77. procedure WriteSYSTEM;
  78. procedure WriteITBITS;
  79. procedure WriteSTRINGS;
  80. procedure WriteIVB; // context ids
  81. procedure WriteREADMEFile;
  82. procedure WriteSection0;
  83. procedure WriteSection1;
  84. procedure WriteDataSpaceFiles(const AStream: TStream);
  85. function AddString(AString: String): LongWord;
  86. // callbacks for lzxcomp
  87. function AtEndOfData: Longbool;
  88. function GetData(Count: LongInt; Buffer: PByte): LongInt;
  89. function WriteCompressedData(Count: Longint; Buffer: Pointer): LongInt;
  90. procedure MarkFrame(UnCompressedTotal, CompressedTotal: LongWord);
  91. // end callbacks
  92. public
  93. constructor Create(OutStream: TStream; FreeStreamOnDestroy: Boolean);
  94. destructor Destroy; override;
  95. procedure Execute;
  96. procedure AppendTOC(AStream: TStream);
  97. procedure AppendIndex(AStream: TStream);
  98. procedure AppendSearchDB(AName: String; AStream: TStream);
  99. procedure AddStreamToArchive(AFileName, APath: String; AStream: TStream; Compress: Boolean = True);
  100. procedure PostAddStreamToArchive(AFileName, APath: String; AStream: TStream; Compress: Boolean = True);
  101. procedure AddContext(AContext: DWord; ATopic: String);
  102. property WindowSize: LongWord read FWindowSize write FWindowSize default 2; // in $8000 blocks
  103. property FrameSize: LongWord read FFrameSize write FFrameSize default 1; // in $8000 blocks
  104. property FilesToCompress: TStrings read FFileNames;
  105. property OnGetFileData: TGetDataFunc read FOnGetFileData write FOnGetFileData;
  106. property OnLastFile: TNotifyEvent read FOnLastFile write FOnLastFile;
  107. property OutStream: TStream read FOutStream;
  108. property Title: String read FTitle write FTitle;
  109. property FullTextSearch: Boolean read FFullTextSearch write FFullTextSearch;
  110. property DefaultFont: String read FDefaultFont write FDefaultFont;
  111. property DefaultPage: String read FDefaultPage write FDefaultPage;
  112. property TempRawStream: TStream read FTempStream write SetTempRawStream;
  113. //property LocaleID: dword read ITSFHeader.LanguageID write ITSFHeader.LanguageID;
  114. end;
  115. implementation
  116. uses dateutils, sysutils, paslzxcomp;
  117. const
  118. LZX_WINDOW_SIZE = 16; // 16 = 2 frames = 1 shl 16
  119. LZX_FRAME_SIZE = $8000;
  120. { TChmWriter }
  121. procedure TChmWriter.InitITSFHeader;
  122. begin
  123. with ITSFHeader do begin
  124. ITSFsig := ITSFFileSig;
  125. Version := NToLE(DWord(3));
  126. // we fix endian order when this is written to the stream
  127. HeaderLength := NToLE(DWord(SizeOf(TITSFHeader) + (SizeOf(TITSFHeaderEntry)*2) + SizeOf(TITSFHeaderSuffix)));
  128. Unknown_1 := NToLE(DWord(1));
  129. TimeStamp:= NToBE(MilliSecondOfTheDay(Now)); //bigendian
  130. LanguageID := NToLE(DWord($0409)); // English / English_US
  131. Guid1 := ITSFHeaderGUID;
  132. Guid2 := ITSFHeaderGUID;
  133. end;
  134. end;
  135. procedure TChmWriter.InitHeaderSectionTable;
  136. begin
  137. // header section 0
  138. HeaderSection0Table.PosFromZero := LEToN(ITSFHeader.HeaderLength);
  139. HeaderSection0Table.Length := SizeOf(TITSPHeaderPrefix);
  140. // header section 1
  141. HeaderSection1Table.PosFromZero := HeaderSection0Table.PosFromZero + HeaderSection0Table.Length;
  142. HeaderSection1Table.Length := SizeOf(TITSPHeader)+FDirectoryListings.Size;
  143. //contains the offset of CONTENT Section0 from zero
  144. HeaderSuffix.Offset := HeaderSection1Table.PosFromZero + HeaderSection1Table.Length;
  145. // now fix endian stuff
  146. HeaderSection0Table.PosFromZero := NToLE(HeaderSection0Table.PosFromZero);
  147. HeaderSection0Table.Length := NToLE(HeaderSection0Table.Length);
  148. HeaderSection1Table.PosFromZero := NToLE(HeaderSection1Table.PosFromZero);
  149. HeaderSection1Table.Length := NToLE(HeaderSection1Table.Length);
  150. with HeaderSection0 do begin // TITSPHeaderPrefix;
  151. Unknown1 := NToLE(DWord($01FE));
  152. Unknown2 := 0;
  153. // at this point we are putting together the headers. content sections 0 and 1 are complete
  154. FileSize := NToLE(HeaderSuffix.Offset + FSection0.Size + FSection1Size);
  155. Unknown3 := 0;
  156. Unknown4 := 0;
  157. end;
  158. with HeaderSection1 do begin // TITSPHeader; // DirectoryListings header
  159. ITSPsig := ITSPHeaderSig;
  160. Version := NToLE(DWord(1));
  161. DirHeaderLength := NToLE(DWord(SizeOf(TITSPHeader))); // Length of the directory header
  162. Unknown1 := NToLE(DWord($0A));
  163. ChunkSize := NToLE(DWord($1000));
  164. Density := NToLE(DWord(2));
  165. // updated when directory listings were created
  166. //IndexTreeDepth := 1 ; // 1 if there is no index 2 if there is one level of PMGI chunks. will update as
  167. //IndexOfRootChunk := -1;// if no root chunk
  168. //FirstPMGLChunkIndex,
  169. //LastPMGLChunkIndex: LongWord;
  170. Unknown2 := NToLE(Longint(-1));
  171. //DirectoryChunkCount: LongWord;
  172. LanguageID := NToLE(DWord($0409));
  173. GUID := ITSPHeaderGUID;
  174. LengthAgain := NToLE(DWord($54));
  175. Unknown3 := NToLE(Longint(-1));
  176. Unknown4 := NToLE(Longint(-1));
  177. Unknown5 := NToLE(Longint(-1));
  178. end;
  179. // more endian stuff
  180. HeaderSuffix.Offset := NToLE(HeaderSuffix.Offset);
  181. end;
  182. procedure TChmWriter.SetTempRawStream(const AValue: TStream);
  183. begin
  184. if (FCurrentStream.Size > 0) or (FSection1.Size > 0) then
  185. raise Exception.Create('Cannot set the TempRawStream once data has been written to it!');
  186. if AValue = nil then
  187. raise Exception.Create('TempRawStream cannot be nil!');
  188. if FCurrentStream = AValue then
  189. exit;
  190. FCurrentStream.Free;
  191. FCurrentStream := AValue;
  192. end;
  193. procedure TChmWriter.WriteHeader(Stream: TStream);
  194. begin
  195. Stream.Write(ITSFHeader, SizeOf(TITSFHeader));
  196. Stream.Write(HeaderSection0Table, SizeOf(TITSFHeaderEntry));
  197. Stream.Write(HeaderSection1Table, SizeOf(TITSFHeaderEntry));
  198. Stream.Write(HeaderSuffix, SizeOf(TITSFHeaderSuffix));
  199. Stream.Write(HeaderSection0, SizeOf(TITSPHeaderPrefix));
  200. end;
  201. procedure TChmWriter.CreateDirectoryListings;
  202. type
  203. TFirstListEntry = record
  204. Entry: array[0..511] of byte;
  205. Size: Integer;
  206. end;
  207. var
  208. Buffer: array [0..511] of Byte;
  209. IndexBlock: TPMGIDirectoryChunk;
  210. ListingBlock: TDirectoryChunk;
  211. I: Integer;
  212. Size: Integer;
  213. FESize: Integer;
  214. FileName: String;
  215. FileNameSize: Integer;
  216. LastListIndex: Integer;
  217. FirstListEntry: TFirstListEntry;
  218. ChunkIndex: Integer;
  219. ListHeader: TPMGListChunk;
  220. const
  221. PMGL = 'PMGL';
  222. PMGI = 'PMGI';
  223. procedure UpdateLastListChunk;
  224. var
  225. Tmp: QWord;
  226. begin
  227. if ChunkIndex < 1 then begin
  228. Exit;
  229. end;
  230. Tmp := FDirectoryListings.Position;
  231. FDirectoryListings.Position := (LastListIndex) * $1000;
  232. FDirectoryListings.Read(ListHeader, SizeOf(TPMGListChunk));
  233. FDirectoryListings.Position := (LastListIndex) * $1000;
  234. ListHeader.NextChunkIndex := NToLE(ChunkIndex);
  235. FDirectoryListings.Write(ListHeader, SizeOf(TPMGListChunk));
  236. FDirectoryListings.Position := Tmp;
  237. end;
  238. procedure WriteIndexChunk(ShouldFinish: Boolean = False);
  239. var
  240. IndexHeader: TPMGIIndexChunk;
  241. ParentIndex,
  242. TmpIndex: TPMGIDirectoryChunk;
  243. begin
  244. with IndexHeader do begin
  245. PMGIsig := PMGI;
  246. UnusedSpace := NToLE(IndexBlock.FreeSpace);
  247. end;
  248. IndexBlock.WriteHeader(@IndexHeader);
  249. IndexBlock.WriteChunkToStream(FDirectoryListings, ChunkIndex, ShouldFinish);
  250. IndexBlock.Clear;
  251. if HeaderSection1.IndexOfRootChunk < 0 then HeaderSection1.IndexOfRootChunk := ChunkIndex;
  252. if ShouldFinish then begin;
  253. HeaderSection1.IndexTreeDepth := 2;
  254. ParentIndex := IndexBlock.ParentChunk;
  255. if ParentIndex <> nil then repeat // the parent index is notified by our child index when to write
  256. HeaderSection1.IndexOfRootChunk := ChunkIndex;
  257. TmpIndex := ParentIndex;
  258. ParentIndex := ParentIndex.ParentChunk;
  259. TmpIndex.Free;
  260. Inc(HeaderSection1.IndexTreeDepth);
  261. Inc(ChunkIndex);
  262. until ParentIndex = nil;
  263. end;
  264. Inc(ChunkIndex);
  265. end;
  266. procedure WriteListChunk;
  267. begin
  268. with ListHeader do begin
  269. PMGLsig := PMGL;
  270. UnusedSpace := NToLE(ListingBlock.FreeSpace);
  271. Unknown1 := 0;
  272. PreviousChunkIndex := NToLE(LastListIndex);
  273. NextChunkIndex := NToLE(Longint(-1)); // we update this when we write the next chunk
  274. end;
  275. if HeaderSection1.FirstPMGLChunkIndex <= 0 then
  276. HeaderSection1.FirstPMGLChunkIndex := NToLE(ChunkIndex);
  277. HeaderSection1.LastPMGLChunkIndex := NToLE(ChunkIndex);
  278. ListingBlock.WriteHeader(@ListHeader);
  279. ListingBlock.WriteChunkToStream(FDirectoryListings);
  280. ListingBlock.Clear;
  281. UpdateLastListChunk;
  282. LastListIndex := ChunkIndex;
  283. Inc(ChunkIndex);
  284. // now add to index
  285. if not IndexBlock.CanHold(FirstListEntry.Size) then
  286. WriteIndexChunk;
  287. IndexBlock.WriteEntry(FirstListEntry.Size, @FirstListEntry.Entry[0])
  288. end;
  289. begin
  290. // first sort the listings
  291. FInternalFiles.Sort;
  292. HeaderSection1.IndexTreeDepth := 1;
  293. HeaderSection1.IndexOfRootChunk := -1;
  294. ChunkIndex := 0;
  295. IndexBlock := TPMGIDirectoryChunk.Create(SizeOf(TPMGIIndexChunk));
  296. ListingBlock := TDirectoryChunk.Create(SizeOf(TPMGListChunk));
  297. LastListIndex := -1;
  298. // add files to a pmgl block until it is full.
  299. // after the block is full make a pmgi block and add the first entry of the pmgl block
  300. // repeat until the index block is full and start another.
  301. // the pmgi chunks take care of needed parent chunks in the tree
  302. for I := 0 to FInternalFiles.Count-1 do begin
  303. Size := 0;
  304. FileName := FInternalFiles.FileEntry[I].Path + FInternalFiles.FileEntry[I].Name;
  305. FileNameSize := Length(FileName);
  306. // filename length
  307. Inc(Size, WriteCompressedInteger(@Buffer[Size], FileNameSize));
  308. // filename
  309. Move(FileName[1], Buffer[Size], FileNameSize);
  310. Inc(Size, FileNameSize);
  311. FESize := Size;
  312. // File is compressed...
  313. Inc(Size, WriteCompressedInteger(@Buffer[Size], Ord(FInternalFiles.FileEntry[I].Compressed)));
  314. // Offset from section start
  315. Inc(Size, WriteCompressedInteger(@Buffer[Size], FInternalFiles.FileEntry[I].DecompressedOffset));
  316. // Size when uncompressed
  317. Inc(Size, WriteCompressedInteger(@Buffer[Size], FInternalFiles.FileEntry[I].DecompressedSize));
  318. if not ListingBlock.CanHold(Size) then
  319. WriteListChunk;
  320. ListingBlock.WriteEntry(Size, @Buffer[0]);
  321. if ListingBlock.ItemCount = 1 then begin // add the first list item to the index
  322. Move(Buffer[0], FirstListEntry.Entry[0], FESize);
  323. FirstListEntry.Size := FESize + WriteCompressedInteger(@FirstListEntry.Entry[FESize], ChunkIndex);
  324. end;
  325. end;
  326. if ListingBlock.ItemCount > 0 then WriteListChunk;
  327. if ChunkIndex > 1 then begin
  328. if (IndexBlock.ItemCount > 1)
  329. or ( (IndexBlock.ItemCount > 0) and (HeaderSection1.IndexOfRootChunk > -1) )
  330. then WriteIndexChunk(True);
  331. end;
  332. HeaderSection1.DirectoryChunkCount := NToLE(DWord(FDirectoryListings.Size div $1000));
  333. IndexBlock.Free;
  334. ListingBlock.Free;
  335. //now fix some endian stuff
  336. HeaderSection1.IndexOfRootChunk := NToLE(HeaderSection1.IndexOfRootChunk);
  337. HeaderSection1.IndexTreeDepth := NtoLE(HeaderSection1.IndexTreeDepth);
  338. end;
  339. procedure TChmWriter.WriteDirectoryListings(Stream: TStream);
  340. begin
  341. Stream.Write(HeaderSection1, SizeOf(HeaderSection1));
  342. FDirectoryListings.Position := 0;
  343. Stream.CopyFrom(FDirectoryListings, FDirectoryListings.Size);
  344. FDirectoryListings.Position := 0;
  345. //TMemoryStream(FDirectoryListings).SaveToFile('dirlistings.pmg');
  346. end;
  347. procedure TChmWriter.WriteSystem;
  348. var
  349. Entry: TFileEntryRec;
  350. TmpStr: String;
  351. TmpTitle: String;
  352. const
  353. VersionStr = 'HHA Version 4.74.8702'; // does this matter?
  354. begin
  355. // this creates the /#SYSTEM file
  356. Entry.Name := '#SYSTEM';
  357. Entry.Path := '/';
  358. Entry.Compressed := False;
  359. Entry.DecompressedOffset := FSection0.Position;
  360. // EntryCodeOrder: 10 9 4 2 3 16 6 0 1 5
  361. FSection0.WriteDWord(NToLE(Word(3))); // Version
  362. if Title <> '' then
  363. TmpTitle := Title
  364. else
  365. TmpTitle := 'default';
  366. // Code -> Length -> Data
  367. // 10
  368. FSection0.WriteWord(NToLE(Word(10)));
  369. FSection0.WriteWord(NToLE(Word(SizeOf(DWord))));
  370. FSection0.WriteDWord(NToLE(MilliSecondOfTheDay(Now)));
  371. // 9
  372. FSection0.WriteWord(NToLE(Word(9)));
  373. FSection0.WriteWord(NToLE(Word(SizeOf(VersionStr)+1)));
  374. FSection0.Write(VersionStr, SizeOf(VersionStr));
  375. FSection0.WriteByte(0);
  376. // 4 A struct that is only needed to set if full text search is on.
  377. FSection0.WriteWord(NToLE(Word(4)));
  378. FSection0.WriteWord(NToLE(Word(36))); // size
  379. FSection0.WriteDWord(NToLE(DWord($0409)));
  380. FSection0.WriteDWord(NToLE(DWord(Ord(FFullTextSearch))));
  381. FSection0.WriteDWord(0);
  382. FSection0.WriteDWord(0);
  383. FSection0.WriteDWord(0);
  384. // two for a QWord
  385. FSection0.WriteDWord(0);
  386. FSection0.WriteDWord(0);
  387. FSection0.WriteDWord(0);
  388. FSection0.WriteDWord(0);
  389. ////////////////////////<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  390. // 2 default page to load
  391. if FDefaultPage <> '' then begin
  392. FSection0.WriteWord(NToLE(Word(2)));
  393. FSection0.WriteWord(NToLE(Word(Length(FDefaultPage)+1)));
  394. FSection0.Write(FDefaultPage[1], Length(FDefaultPage));
  395. FSection0.WriteByte(0);
  396. end;
  397. // 3 Title
  398. if FTitle <> '' then begin
  399. FSection0.WriteWord(NToLE(Word(3)));
  400. FSection0.WriteWord(NToLE(Word(Length(FTitle)+1)));
  401. FSection0.Write(FTitle[1], Length(FTitle));
  402. FSection0.WriteByte(0);
  403. end;
  404. // 16 Default Font
  405. if FDefaultFont <> '' then begin
  406. FSection0.WriteWord(NToLE(Word(16)));
  407. FSection0.WriteWord(NToLE(Word(Length(FDefaultFont)+1)));
  408. FSection0.Write(FDefaultFont[1], Length(FDefaultFont));
  409. FSection0.WriteByte(0);
  410. end;
  411. // 6
  412. // unneeded. if output file is : /somepath/OutFile.chm the value here is outfile(lowercase)
  413. // 0 Table of contents filename
  414. if FHasTOC then begin
  415. TmpStr := 'default.hhc';
  416. FSection0.WriteWord(0);
  417. FSection0.WriteWord(NToLE(Word(Length(TmpStr)+1)));
  418. FSection0.Write(TmpStr[1], Length(TmpStr));
  419. FSection0.WriteByte(0);
  420. end;
  421. // 1
  422. // hhk Index
  423. if FHasIndex then begin
  424. TmpStr := 'default.hhk';
  425. FSection0.WriteWord(NToLE(Word(1)));
  426. FSection0.WriteWord(NToLE(Word(Length(TmpStr)+1)));
  427. FSection0.Write(TmpStr[1], Length(TmpStr));
  428. FSection0.WriteByte(0);
  429. end;
  430. // 5 Default Window.
  431. // Not likely needed
  432. Entry.DecompressedSize := FSection0.Position - Entry.DecompressedOffset;
  433. FInternalFiles.AddEntry(Entry);
  434. end;
  435. procedure TChmWriter.WriteITBITS;
  436. var
  437. Entry: TFileEntryRec;
  438. begin
  439. // This is an empty and useless file
  440. Entry.Name := '#ITBITS';
  441. Entry.Path := '/';
  442. Entry.Compressed := False;
  443. Entry.DecompressedOffset := FSection0.Position;
  444. Entry.DecompressedSize := 0;
  445. FInternalFiles.AddEntry(Entry);
  446. end;
  447. procedure TChmWriter.WriteSTRINGS;
  448. begin
  449. if FStringsStream.Size = 0 then;
  450. FStringsStream.WriteByte(0);
  451. FStringsStream.Position := 0;
  452. AddStreamToArchive('#STRINGS', '/', FStringsStream);
  453. end;
  454. procedure TChmWriter.WriteIVB;
  455. begin
  456. if FContextStream = nil then exit;
  457. FContextStream.Position := 0;
  458. // the size of all the entries
  459. FContextStream.WriteDWord(NToLE(DWord(FContextStream.Size-SizeOf(dword))));
  460. FContextStream.Position := 0;
  461. AddStreamToArchive('#IVB', '/', FContextStream);
  462. end;
  463. procedure TChmWriter.WriteREADMEFile;
  464. const DISCLAIMER_STR = 'This archive was not made by the MS HTML Help Workshop(r)(tm) program.';
  465. var
  466. Entry: TFileEntryRec;
  467. begin
  468. // This procedure puts a file in the archive that says it wasn't compiled with the MS compiler
  469. Entry.Compressed := False;
  470. Entry.DecompressedOffset := FSection0.Position;
  471. FSection0.Write(DISCLAIMER_STR, SizeOf(DISCLAIMER_STR));
  472. Entry.DecompressedSize := FSection0.Position - Entry.DecompressedOffset;
  473. Entry.Path := '/';
  474. Entry.Name := '_#_README_#_'; //try to use a name that won't conflict with normal names
  475. FInternalFiles.AddEntry(Entry);
  476. end;
  477. procedure TChmWriter.WriteSection0;
  478. begin
  479. FSection0.Position := 0;
  480. FOutStream.CopyFrom(FSection0, FSection0.Size);
  481. end;
  482. procedure TChmWriter.WriteSection1;
  483. begin
  484. WriteContentToStream(FOutStream, FSection1);
  485. end;
  486. procedure TChmWriter.WriteDataSpaceFiles(const AStream: TStream);
  487. var
  488. Entry: TFileEntryRec;
  489. begin
  490. // This procedure will write all files starting with ::
  491. Entry.Compressed := False; // None of these files are compressed
  492. // ::DataSpace/NameList
  493. Entry.DecompressedOffset := FSection0.Position;
  494. Entry.DecompressedSize := WriteNameListToStream(FSection0, [snUnCompressed,snMSCompressed]);
  495. Entry.Path := '::DataSpace/';
  496. Entry.Name := 'NameList';
  497. FInternalFiles.AddEntry(Entry, False);
  498. // ::DataSpace/Storage/MSCompressed/ControlData
  499. Entry.DecompressedOffset := FSection0.Position;
  500. Entry.DecompressedSize := WriteControlDataToStream(FSection0, 2, 2, 1);
  501. Entry.Path := '::DataSpace/Storage/MSCompressed/';
  502. Entry.Name := 'ControlData';
  503. FInternalFiles.AddEntry(Entry, False);
  504. // ::DataSpace/Storage/MSCompressed/SpanInfo
  505. Entry.DecompressedOffset := FSection0.Position;
  506. Entry.DecompressedSize := WriteSpanInfoToStream(FSection0, FReadCompressedSize);
  507. Entry.Path := '::DataSpace/Storage/MSCompressed/';
  508. Entry.Name := 'SpanInfo';
  509. FInternalFiles.AddEntry(Entry, False);
  510. // ::DataSpace/Storage/MSCompressed/Transform/List
  511. Entry.DecompressedOffset := FSection0.Position;
  512. Entry.DecompressedSize := WriteTransformListToStream(FSection0);
  513. Entry.Path := '::DataSpace/Storage/MSCompressed/Transform/';
  514. Entry.Name := 'List';
  515. FInternalFiles.AddEntry(Entry, False);
  516. // ::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/
  517. // ::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable
  518. Entry.DecompressedOffset := FSection0.Position;
  519. Entry.DecompressedSize := WriteResetTableToStream(FSection0, FSection1ResetTable);
  520. Entry.Path := '::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/';
  521. Entry.Name := 'ResetTable';
  522. FInternalFiles.AddEntry(Entry, True);
  523. // ::DataSpace/Storage/MSCompressed/Content do this last
  524. Entry.DecompressedOffset := FSection0.Position;
  525. Entry.DecompressedSize := FSection1Size; // we will write it directly to FOutStream later
  526. Entry.Path := '::DataSpace/Storage/MSCompressed/';
  527. Entry.Name := 'Content';
  528. FInternalFiles.AddEntry(Entry, False);
  529. end;
  530. function TChmWriter.AddString(AString: String): LongWord;
  531. begin
  532. // #STRINGS starts with a null char
  533. if FStringsStream.Size = 0 then FStringsStream.WriteByte(0);
  534. // each entry is a null terminated string
  535. Result := FStringsStream.Position;
  536. FStringsStream.WriteBuffer(AString[1], Length(AString));
  537. FStringsStream.WriteByte(0);
  538. end;
  539. function _AtEndOfData(arg: pointer): LongBool; cdecl;
  540. begin
  541. Result := TChmWriter(arg).AtEndOfData;
  542. end;
  543. function TChmWriter.AtEndOfData: LongBool;
  544. begin
  545. Result := ForceExit or (FCurrentIndex >= FFileNames.Count-1);
  546. if Result then
  547. Result := Integer(FCurrentStream.Position) >= Integer(FCurrentStream.Size)-1;
  548. end;
  549. function _GetData(arg: pointer; Count: LongInt; Buffer: Pointer): LongInt; cdecl;
  550. begin
  551. Result := TChmWriter(arg).GetData(Count, PByte(Buffer));
  552. end;
  553. function TChmWriter.GetData(Count: LongInt; Buffer: PByte): LongInt;
  554. var
  555. FileEntry: TFileEntryRec;
  556. begin
  557. Result := 0;
  558. while (Result < Count) and (not AtEndOfData) do begin
  559. Inc(Result, FCurrentStream.Read(Buffer[Result], Count-Result));
  560. if (Result < Count) and (not AtEndOfData)
  561. then begin
  562. // the current file has been read. move to the next file in the list
  563. FCurrentStream.Position := 0;
  564. Inc(FCurrentIndex);
  565. ForceExit := OnGetFileData(FFileNames[FCurrentIndex], FileEntry.Path, FileEntry.Name, FCurrentStream);
  566. FileEntry.DecompressedSize := FCurrentStream.Size;
  567. FileEntry.DecompressedOffset := FReadCompressedSize; //269047723;//to test writing really large numbers
  568. FileEntry.Compressed := True;
  569. FInternalFiles.AddEntry(FileEntry);
  570. // So the next file knows it's offset
  571. Inc(FReadCompressedSize, FileEntry.DecompressedSize);
  572. FCurrentStream.Position := 0;
  573. end;
  574. // this is intended for programs to add perhaps a file
  575. // after all the other files have been added.
  576. if (AtEndOfData)
  577. and (FCurrentStream <> FPostStream) then
  578. begin
  579. if Assigned(FOnLastFile) then
  580. FOnLastFile(Self);
  581. FCurrentStream.Free;
  582. FCurrentStream := FPostStream;
  583. FCurrentStream.Position := 0;
  584. Inc(FReadCompressedSize, FCurrentStream.Size);
  585. end;
  586. end;
  587. end;
  588. function _WriteCompressedData(arg: pointer; Count: LongInt; Buffer: Pointer): LongInt; cdecl;
  589. begin
  590. Result := TChmWriter(arg).WriteCompressedData(Count, Buffer);
  591. end;
  592. function TChmWriter.WriteCompressedData(Count: Longint; Buffer: Pointer): LongInt;
  593. begin
  594. // we allocate a MB at a time to limit memory reallocation since this
  595. // writes usually 2 bytes at a time
  596. if (FSection1 is TMemoryStream) and (FSection1.Position >= FSection1.Size-1) then begin
  597. FSection1.Size := FSection1.Size+$100000;
  598. end;
  599. Result := FSection1.Write(Buffer^, Count);
  600. Inc(FSection1Size, Result);
  601. end;
  602. procedure _MarkFrame(arg: pointer; UncompressedTotal, CompressedTotal: LongWord); cdecl;
  603. begin
  604. TChmWriter(arg).MarkFrame(UncompressedTotal, CompressedTotal);
  605. end;
  606. procedure TChmWriter.MarkFrame(UnCompressedTotal, CompressedTotal: LongWord);
  607. procedure WriteQWord(Value: QWord);
  608. begin
  609. FSection1ResetTable.Write(NToLE(Value), 8);
  610. end;
  611. procedure IncEntryCount;
  612. var
  613. OldPos: QWord;
  614. Value: DWord;
  615. begin
  616. OldPos := FSection1ResetTable.Position;
  617. FSection1ResetTable.Position := $4;
  618. Value := LeToN(FSection1ResetTable.ReadDWord)+1;
  619. FSection1ResetTable.Position := $4;
  620. FSection1ResetTable.WriteDWord(NToLE(Value));
  621. FSection1ResetTable.Position := OldPos;
  622. end;
  623. procedure UpdateTotalSizes;
  624. var
  625. OldPos: QWord;
  626. begin
  627. OldPos := FSection1ResetTable.Position;
  628. FSection1ResetTable.Position := $10;
  629. WriteQWord(FReadCompressedSize); // size of read data that has been compressed
  630. WriteQWord(CompressedTotal);
  631. FSection1ResetTable.Position := OldPos;
  632. end;
  633. begin
  634. if FSection1ResetTable.Size = 0 then begin
  635. // Write the header
  636. FSection1ResetTable.WriteDWord(NtoLE(DWord(2)));
  637. FSection1ResetTable.WriteDWord(0); // number of entries. we will correct this with IncEntryCount
  638. FSection1ResetTable.WriteDWord(NtoLE(DWord(8))); // Size of Entries (qword)
  639. FSection1ResetTable.WriteDWord(NtoLE(DWord($28))); // Size of this header
  640. WriteQWord(0); // Total Uncompressed Size
  641. WriteQWord(0); // Total Compressed Size
  642. WriteQWord(NtoLE($8000)); // Block Size
  643. WriteQWord(0); // First Block start
  644. end;
  645. IncEntryCount;
  646. UpdateTotalSizes;
  647. WriteQWord(CompressedTotal); // Next Block Start
  648. // We have to trim the last entry off when we are done because there is no next block in that case
  649. end;
  650. constructor TChmWriter.Create(OutStream: TStream; FreeStreamOnDestroy: Boolean);
  651. begin
  652. if OutStream = nil then Raise Exception.Create('TChmWriter.OutStream Cannot be nil!');
  653. FCurrentStream := TMemoryStream.Create;
  654. FCurrentIndex := -1;
  655. FOutStream := OutStream;
  656. FInternalFiles := TFileEntryList.Create;
  657. FStringsStream := TmemoryStream.Create;
  658. FSection0 := TMemoryStream.Create;
  659. FSection1 := TMemoryStream.Create;
  660. FSection1ResetTable := TMemoryStream.Create;
  661. FDirectoryListings := TMemoryStream.Create;
  662. FPostStream := TMemoryStream.Create;;
  663. FDestroyStream := FreeStreamOnDestroy;
  664. FFileNames := TStringList.Create;
  665. end;
  666. destructor TChmWriter.Destroy;
  667. begin
  668. if FDestroyStream then FOutStream.Free;
  669. if Assigned(FContextStream) then FContextStream.Free;
  670. FInternalFiles.Free;
  671. FCurrentStream.Free;
  672. FStringsStream.Free;
  673. FSection0.Free;
  674. FSection1.Free;
  675. FSection1ResetTable.Free;
  676. FDirectoryListings.Free;
  677. FFileNames.Free;
  678. inherited Destroy;
  679. end;
  680. procedure TChmWriter.Execute;
  681. begin
  682. InitITSFHeader;
  683. FOutStream.Position := 0;
  684. FSection1Size := 0;
  685. // write any internal files to FCurrentStream that we want in the compressed section
  686. WriteIVB;
  687. WriteSTRINGS;
  688. // written to Section0 (uncompressed)
  689. WriteREADMEFile;
  690. // move back to zero so that we can start reading from zero :)
  691. FReadCompressedSize := FCurrentStream.Size;
  692. FCurrentStream.Position := 0; // when compressing happens, first the FCurrentStream is read
  693. // before loading user files. So we can fill FCurrentStream with
  694. // internal files first.
  695. // this gathers ALL files that should be in section1 (the compressed section)
  696. StartCompressingStream;
  697. FSection1.Size := FSection1Size;
  698. // This creates and writes the #ITBITS (empty) file to section0
  699. WriteITBITS;
  700. // This creates and writes the #SYSTEM file to section0
  701. WriteSystem;
  702. //this creates all special files in the archive that start with ::DataSpace
  703. WriteDataSpaceFiles(FSection0);
  704. // creates all directory listings including header
  705. CreateDirectoryListings;
  706. // do this after we have compressed everything so that we know the values that must be written
  707. InitHeaderSectionTable;
  708. // Now we can write everything to FOutStream
  709. WriteHeader(FOutStream);
  710. WriteDirectoryListings(FOutStream);
  711. WriteSection0; //does NOT include section 1 even though section0.content IS section1
  712. WriteSection1; // writes section 1 to FOutStream
  713. end;
  714. procedure TChmWriter.AppendTOC(AStream: TStream);
  715. begin
  716. FHasTOC := True;
  717. PostAddStreamToArchive('default.hhc', '/', AStream, True);
  718. end;
  719. procedure TChmWriter.AppendIndex(AStream: TStream);
  720. begin
  721. FHasIndex := True;
  722. PostAddStreamToArchive('default.hhk', '/', AStream, True);
  723. end;
  724. procedure TChmWriter.AppendSearchDB(AName: String; AStream: TStream);
  725. begin
  726. PostAddStreamToArchive(AName, '/', AStream);
  727. end;
  728. // this procedure is used to manually add files to compress to an internal stream that is
  729. // processed before FileToCompress is called. Files added this way should not be
  730. // duplicated in the FilesToCompress property.
  731. procedure TChmWriter.AddStreamToArchive(AFileName, APath: String; AStream: TStream; Compress: Boolean = True);
  732. var
  733. TargetStream: TStream;
  734. Entry: TFileEntryRec;
  735. begin
  736. if AStream = nil then Exit;
  737. if Compress then
  738. TargetStream := FCurrentStream
  739. else
  740. TargetStream := FSection0;
  741. Entry.Name := AFileName;
  742. Entry.Path := APath;
  743. Entry.Compressed := Compress;
  744. Entry.DecompressedOffset := TargetStream.Position;
  745. Entry.DecompressedSize := AStream.Size;
  746. FInternalFiles.AddEntry(Entry);
  747. AStream.Position := 0;
  748. TargetStream.CopyFrom(AStream, AStream.Size);
  749. end;
  750. procedure TChmWriter.PostAddStreamToArchive(AFileName, APath: String;
  751. AStream: TStream; Compress: Boolean);
  752. var
  753. TargetStream: TStream;
  754. Entry: TFileEntryRec;
  755. begin
  756. if AStream = nil then Exit;
  757. if Compress then
  758. TargetStream := FPostStream
  759. else
  760. TargetStream := FSection0;
  761. Entry.Name := AFileName;
  762. Entry.Path := APath;
  763. Entry.Compressed := Compress;
  764. if not Compress then
  765. Entry.DecompressedOffset := TargetStream.Position
  766. else
  767. Entry.DecompressedOffset := FReadCompressedSize + TargetStream.Position;
  768. Entry.DecompressedSize := AStream.Size;
  769. FInternalFiles.AddEntry(Entry);
  770. AStream.Position := 0;
  771. TargetStream.CopyFrom(AStream, AStream.Size);
  772. end;
  773. procedure TChmWriter.AddContext(AContext: DWord; ATopic: String);
  774. var
  775. Offset: DWord;
  776. begin
  777. if FContextStream = nil then begin
  778. // #IVB starts with a dword which is the size of the stream - sizeof(dword)
  779. FContextStream.WriteDWord(0);
  780. // we will update this when we write the file to the final stream
  781. end;
  782. // an entry is a context id and then the offset of the name of the topic in the strings file
  783. FContextStream.WriteDWord(NToLE(AContext));
  784. Offset := NToLE(AddString(ATopic));
  785. FContextStream.WriteDWord(Offset);
  786. end;
  787. procedure TChmWriter.StartCompressingStream;
  788. var
  789. LZXdata: Plzx_data;
  790. WSize: LongInt;
  791. begin
  792. lzx_init(@LZXdata, LZX_WINDOW_SIZE, @_GetData, Self, @_AtEndOfData,
  793. @_WriteCompressedData, Self, @_MarkFrame, Self);
  794. WSize := 1 shl LZX_WINDOW_SIZE;
  795. while not AtEndOfData do begin
  796. lzx_reset(LZXdata);
  797. lzx_compress_block(LZXdata, WSize, True);
  798. end;
  799. //we have to mark the last frame manually
  800. MarkFrame(LZXdata^.len_uncompressed_input, LZXdata^.len_compressed_output);
  801. lzx_finish(LZXdata, nil);
  802. end;
  803. end.