chmwriter.pas 29 KB

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