PageRenderTime 61ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/tigerutil.pas

https://bitbucket.org/reiniero/papertiger
Pascal | 561 lines | 361 code | 56 blank | 144 comment | 33 complexity | 4f898d94d40dd9d24f24efe9cfe9a3d7 MD5 | raw file
  1. unit tigerutil;
  2. { Utility functions such as logging support.
  3. Copyright (c) 2012-2014 Reinier Olislagers
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to
  6. deal in the Software without restriction, including without limitation the
  7. rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. sell copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in
  11. all copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  17. FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  18. IN THE SOFTWARE.
  19. }
  20. {$i tigerserver.inc}
  21. // Use imagemagick bindings
  22. {$DEFINE USEMAGICK}
  23. {$IFDEF MSWINDOWS}
  24. {$R fclel.res} //needed for message files to get Windows to display event log contents correctly
  25. // Not needed for *nix
  26. {$ENDIF}
  27. interface
  28. uses
  29. Classes, SysUtils, eventlog
  30. {$IFDEF USEMAGICK}
  31. ,magick_wand, ImageMagick {for image conversion}
  32. {$ENDIF USEMAGICK}
  33. ;
  34. type
  35. // Orientation of scanned image versus the "right side up".
  36. // E.g. TurnedAntiClock means the scanned image should be rotated 90 degrees
  37. // clockwise to get the correct orientation
  38. Orientation=(orNormal,orUpsideDown,orTurnedClock,orTurnedAntiClock,orUnknown);
  39. type
  40. { TLogger }
  41. TLogger = class(TObject)
  42. private
  43. FLog: TEventLog; //Logging/debug output to syslog/eventlog
  44. public
  45. property EventLog: TEventLog read FLog;
  46. // Write to log and optionally console with seriousness etInfo
  47. procedure WriteLog(Message: string; ToConsole: boolean = False);
  48. // Write to log and optionally console with specified seriousness
  49. procedure WriteLog(EventType: TEventType; Message: string;
  50. ToConsole: boolean = False);
  51. constructor Create;
  52. destructor Destroy; override;
  53. end;
  54. {todo: need translation array:
  55. - tesseract language code (nld, eng, fra...)
  56. - cuneiform language code (=ISO x letter code):
  57. cuneiform -l
  58. Cuneiform for Linux 1.1.0
  59. ...
  60. eng ger fra rus swe spa ita ruseng ukr srp hrv pol
  61. dan por dut cze rum hun bul slv lav lit est tur
  62. - possibly LANG code for Linux environment for Hunspell
  63. and lookup/translation
  64. }
  65. var
  66. TigerLog: TLogger; //Created by unit initialization so available for every referencing unit
  67. {$IFDEF HELLFREEZESOVER}
  68. //todo: debug: use if needed later
  69. // Converts bmp image in stream to tiff image in memory stream
  70. // Memory stream must exist before calling this function
  71. function ConvertStreamBMP_TIFF(Source: TStream; Destination: TMemoryStream): boolean;
  72. {$ENDIF HELLFREEZESOVER}
  73. // Converts image file to black and white CCIT Group 4 compressed image
  74. procedure ConvertTIFFCCITT4(InputFile, OutputFile: string);
  75. // Checks if an image is a TIFF black and white CCIT Group 4 compressed image
  76. function IsTIFFCCITT4(InputFile: string): boolean;
  77. // Converts image in memory to black and white CCIT Group 4 compressed image
  78. // Calling function should clean up memory pointed to by OlImageMemoryPtr if needed
  79. // Returns success status
  80. //todo: don't use this as is it currently crashes imagemagick!!!
  81. function ConvertMemTIFFCCITTGroup4(OldImageMemoryPtr: Pointer; OldImageSize: integer;
  82. out NewImageMemoryPtr: Pointer; out NewImageSize: integer): boolean;
  83. // Copy file to same or other filesystem, overwriting existing files
  84. function FileCopy(Source, Target: string): boolean;
  85. // Delete length characters from starting position from a stream
  86. procedure DeleteFromStream(Stream: TStream; Start, Length: Int64);
  87. // Searches for SearchFor in Stream starting at Start.
  88. // Returns -1 or position in stream (0-based)
  89. function FindInStream(Stream: TStream; Start: int64; SearchFor: string): int64;
  90. //Shows non-debug messages on screen; also shows debug messages if DEBUG defined
  91. procedure infoln(Message: string; Level: TEventType);
  92. implementation
  93. uses math;
  94. {$IFDEF USEMAGICK}
  95. // Imagemagick command+error handling
  96. procedure MagickCommand(CallingFunction: string;
  97. wand: PMagickWand;
  98. const status: MagickBooleanType;
  99. CommandDescription: string);
  100. var
  101. description: PChar;
  102. severity: ExceptionType;
  103. begin
  104. if (status = MagickFalse) then
  105. begin
  106. description := MagickGetException(wand, @severity);
  107. try
  108. raise Exception.Create(Format
  109. ('%s: an error ocurred running %s. Description: %s',
  110. [CallingFunction, CommandDescription,description]));
  111. finally
  112. description := MagickRelinquishMemory(description);
  113. end;
  114. end;
  115. end;
  116. {$ENDIF USEMAGICK}
  117. {$IFDEF HELLFREEZESOVER}
  118. //todo: debug: use if needed later
  119. {$IFDEF USEMAGICK}
  120. function ConvertStreamBMP_TIFF(Source: TStream; Destination: TMemoryStream): boolean;
  121. const
  122. CallF='ConvertStreamBMP_TIFF';
  123. var
  124. wand: PMagickWand;
  125. begin
  126. result:=false;
  127. if not(assigned(Destination)) then
  128. raise Exception.Create('ConvertStreamBMP_TIFF: Destination memorystream must be assigned before calling. Please fix the code.');
  129. if not(assigned(Source)) then
  130. raise Exception.Create('ConvertStreamBMP_TIFF: Source stream must be assigned before calling. Please fix the code.');
  131. wand := NewMagickWand;
  132. try
  133. MagickCommand(CallF,wand,MagickReadImageBlob(wand, OldImageMemoryPtr, OldImageSize),'MagickReadImageBlob');
  134. // Force TIFF format so this can also be used for converting from e.g. BMP or JPG
  135. MagickCommand(CallF,wand,MagickSetImageFormat(wand,'TIFF'),'GetImageFormat');
  136. MagickCommand(CallF,wand,MagickSetImageCompression(wand,Group4Compression),'MagickSetImageCompression');
  137. // Get result into new memory segment
  138. NewImageSize:=0;
  139. NewImageMemoryPtr:=MagickGetImageBlob(wand,Pointer(NewImageSize));
  140. if NewImageMemoryPtr<>nil then
  141. result:=true;
  142. //Calling function should clean up original memory
  143. finally
  144. wand := DestroyMagickWand(wand);
  145. end;
  146. result:=true;
  147. end;
  148. {$ENDIF USEMAGICK}
  149. {$ENDIF HELLFREEZESOVER}
  150. {$IFDEF USEMAGICK}
  151. procedure ConvertTIFFCCITT4(InputFile, OutputFile: string);
  152. // Let imagemagick convert an image file to TIFF Fax compressed B/W
  153. var
  154. status: MagickBooleanType;
  155. wand: PMagickWand;
  156. description: PChar;
  157. severity: ExceptionType;
  158. procedure HandleError;
  159. begin
  160. description := MagickGetException(wand, @severity);
  161. try
  162. raise Exception.Create(Format('ConvertTIFFCCITT4: an error ocurred. Description: %s', [description]));
  163. finally
  164. description := MagickRelinquishMemory(description);
  165. end;
  166. end;
  167. begin
  168. wand := NewMagickWand;
  169. try
  170. status := MagickReadImage(wand,PChar(InputFile));
  171. if (status = MagickFalse) then HandleError;
  172. status := MagickSetImageFormat(wand,'TIFF');
  173. if (status = MagickFalse) then HandleError;
  174. // Perhaps this helps?
  175. //todo: not supported in pascalmagick?
  176. {
  177. status := MagickSetOption(wand,'tiff:rows-per-strip','1');
  178. if (status = MagickFalse) then HandleError;
  179. }
  180. { perhaps needed for some images: remove the alpha channel:
  181. MagickSetImageMatte(magick_wand,MagickFalse);
  182. MagickQuantizeImage(magick_wand,2,GRAYColorspace,0,MagickFalse,MagickFalse);
  183. }
  184. // convert to black & white/lineart
  185. status := MagickSetImageType(wand,BilevelType);
  186. if (status = MagickFalse) then HandleError;
  187. // Compress with CCIT group 4 compression (fax compression); best for B&W
  188. {$IF FPC_FULLVERSION<20701}
  189. //Group4Compression seems defined as 4 which apparently doesn't match imagemagick source
  190. //http://mantis.freepascal.org/view.php?id=26723
  191. status := MagickSetImageCompression(wand,CompressionType(7));
  192. {$ELSE}
  193. status := MagickSetImageCompression(wand,Group4Compression);
  194. {$ENDIF}
  195. if (status = MagickFalse) then HandleError;
  196. // Apparently set(image)compresionquality and
  197. // stripimage are necessary to actually compress
  198. status := MagickSetImageCompressionQuality(wand,0);
  199. if (status = MagickFalse) then HandleError;
  200. status := MagickStripImage(wand);
  201. if (status = MagickFalse) then HandleError;
  202. status := MagickWriteImage(wand,PChar(OutputFile));
  203. if (status = MagickFalse) then HandleError;
  204. finally
  205. wand := DestroyMagickWand(wand);
  206. end;
  207. end;
  208. {$ENDIF USEMAGICK}
  209. {$IFDEF USEMAGICK}
  210. function IsTIFFCCITT4(InputFile: string): boolean;
  211. // Check if an image file to TIFF Fax compressed B/W
  212. var
  213. ResultPChar: PChar;
  214. Compression: CompressionType;
  215. status: MagickBooleanType;
  216. wand: PMagickWand;
  217. description: PChar;
  218. severity: ExceptionType;
  219. procedure HandleError;
  220. begin
  221. description := MagickGetException(wand, @severity);
  222. try
  223. raise Exception.Create(Format('ConvertTIFFCCITT4: an error ocurred. Description: %s', [description]));
  224. finally
  225. description := MagickRelinquishMemory(description);
  226. end;
  227. end;
  228. begin
  229. wand := NewMagickWand;
  230. try
  231. status := MagickReadImage(wand,PChar(InputFile));
  232. if (status = MagickFalse) then HandleError;
  233. ResultPchar := MagickGetImageFormat(wand);
  234. Compression := UndefinedCompression;
  235. Compression := MagickGetImageCompression(wand);
  236. {$IF FPC_FULLVERSION<20701}
  237. //Group4Compression enum has the wrong number
  238. //http://mantis.freepascal.org/view.php?id=26723
  239. result := (Compression=CompressionType(7));
  240. {$ELSE}
  241. result := (Compression=CompressionType(Group4Compression));
  242. {$ENDIF}
  243. finally
  244. wand := DestroyMagickWand(wand);
  245. end;
  246. end;
  247. {$ENDIF}
  248. {$IFDEF USEMAGICK}
  249. function ConvertMemTIFFCCITTGroup4(OldImageMemoryPtr: Pointer; OldImageSize: integer;
  250. out NewImageMemoryPtr: Pointer; out NewImageSize: integer): boolean;
  251. // Let imagemagick convert a TIFF image to CCIT Group 4
  252. const
  253. CallF='ConvertMemTIFFCCITGroup4';
  254. var
  255. wand: PMagickWand;
  256. PNewImageSize: PPtrUint; //points to NewImageSize
  257. begin
  258. result:=false;
  259. wand := NewMagickWand;
  260. try
  261. //todo: debug: remove logging
  262. MagickCommand(CallF,wand,MagickReadImageBlob(wand, OldImageMemoryPtr, OldImageSize),'MagickReadImageBlob');
  263. TigerLog.WriteLog('1');
  264. // Force TIFF format so this can also be used for converting from e.g. BMP or JPG
  265. MagickCommand(CallF,wand,MagickSetImageFormat(wand,'TIFF'),'GetImageFormat');
  266. TigerLog.WriteLog('2');
  267. {$IF FPC_FULLVERSION<20701}
  268. //Group4Compression enum has the wrong number
  269. //http://mantis.freepascal.org/view.php?id=26723
  270. MagickCommand(CallF,wand,MagickSetImageCompression(wand,CompressionType(7)),'MagickSetImageCompression');
  271. {$ELSE}
  272. MagickCommand(CallF,wand,MagickSetImageCompression(wand,Group4Compression),'MagickSetImageCompression');
  273. {$ENDIF}
  274. TigerLog.WriteLog('3');
  275. // Get result into new memory segment
  276. NewImageSize:=0;
  277. PNewImageSize:=@NewImageSize;
  278. // or rather use GetImageFromMagickWand?!?! see code in client
  279. NewImageMemoryPtr:=MagickGetImageBlob(wand,PNewImageSize);
  280. TigerLog.WriteLog('4'); //Up to now it works
  281. if NewImageMemoryPtr<>nil then
  282. begin
  283. //this doesn't anymore
  284. result:=true;
  285. TigerLog.WriteLog('5');
  286. end;
  287. //Calling function should clean up original memory
  288. finally
  289. wand := DestroyMagickWand(wand); //this does get executed
  290. TigerLog.WriteLog('6');
  291. end;
  292. end;
  293. {$ENDIF USEMAGICK}
  294. function FileCopy(Source, Target: string): boolean;
  295. // Copies source to target; overwrites target.
  296. // Caches entire file content in memory.
  297. // Returns true if succeeded; false if failed
  298. var
  299. MemBuffer: TMemoryStream;
  300. begin
  301. result:=false;
  302. if not(FileExists(Source)) then
  303. begin
  304. TigerLog.WriteLog(etDebug,'FileCopy: source file '+Source+' does not exist. Arborting');
  305. exit;
  306. end;
  307. MemBuffer:=TMemoryStream.Create;
  308. try
  309. try
  310. MemBuffer.LoadFromFile(Source);
  311. MemBuffer.Position:=0;
  312. MemBuffer.SaveToFile(Target);
  313. result:=true;
  314. except
  315. on E: Exception do begin
  316. TigerLog.WriteLog(etDebug,'FileCopy: error '+E.Message);
  317. result:=false; //swallow exception; convert to error code
  318. end;
  319. end;
  320. finally
  321. MemBuffer.Free;
  322. end;
  323. end;
  324. procedure DeleteFromStream(Stream: TStream; Start, Length: Int64);
  325. // Source:
  326. // http://stackoverflow.com/questions/9598032/is-it-possible-to-delete-bytes-from-the-beginning-of-a-file
  327. var
  328. Buffer: Pointer;
  329. BufferSize: Integer;
  330. BytesToRead: Int64;
  331. BytesRemaining: Int64;
  332. SourcePos, DestPos: Int64;
  333. begin
  334. SourcePos := Start+Length;
  335. DestPos := Start;
  336. BytesRemaining := Stream.Size-SourcePos;
  337. BufferSize := Min(BytesRemaining, 1024*1024*16);//no bigger than 16MB
  338. GetMem(Buffer, BufferSize);
  339. try
  340. while BytesRemaining>0 do begin
  341. BytesToRead := Min(BufferSize, BytesRemaining);
  342. Stream.Position := SourcePos;
  343. Stream.ReadBuffer(Buffer^, BytesToRead);
  344. Stream.Position := DestPos;
  345. Stream.WriteBuffer(Buffer^, BytesToRead);
  346. inc(SourcePos, BytesToRead);
  347. inc(DestPos, BytesToRead);
  348. dec(BytesRemaining, BytesToRead);
  349. end;
  350. Stream.Size := DestPos;
  351. finally
  352. FreeMem(Buffer);
  353. end;
  354. end;
  355. function FindInStream(Stream: TStream; Start: int64; SearchFor: string): int64;
  356. // Adapted from
  357. // http://wiki.lazarus.freepascal.org/Rosetta_Stone#Finding_all_occurrences_of_some_bytes_in_a_file
  358. var
  359. a: array of byte;
  360. BlockArray: array of byte; //Gets a block of bytes from the stream
  361. BlockSize:integer = 1024*1024;
  362. ReadSize:integer;
  363. fPos:Int64;
  364. FifoBuff:array of byte; //Window into blockarray, used to match SearchFor
  365. FifoStart,FifoEnd,SearchLen,lpbyte:integer;
  366. function CheckPos: int64;
  367. var
  368. l,p:integer;
  369. begin
  370. result:=-1;
  371. p := FifoStart;
  372. for l := 0 to pred(SearchLen) do
  373. begin
  374. if a[l] <> FifoBuff[p] then exit; //match broken off
  375. //p := (p+1) mod SearchLen, the if seems quicker
  376. inc(p);
  377. if p >= SearchLen then p := 0;
  378. end;
  379. result:=(fpos-SearchLen);
  380. end;
  381. begin
  382. SetLength(a,length(SearchFor));
  383. Move(Searchfor[1], a[0], Length(Searchfor)); //todo check if this shouldn't be a^
  384. setlength(BlockArray,BlockSize);
  385. Stream.Position:=Start;
  386. ReadSize := Stream.Read(BlockArray[0],Length(BlockArray));
  387. SearchLen := length(a);
  388. if SearchLen > length(BlockArray) then
  389. raise Exception.CreateFmt('FindInStream: search term %s larger than blocksize',[SearchFor]);
  390. if ReadSize < SearchLen then exit; //can't be in there so quit
  391. setlength(FifoBuff,SearchLen);
  392. move(BlockArray[0],FifoBuff[0],SearchLen);
  393. fPos:=0;
  394. FifoStart:=0;
  395. FifoEnd:=SearchLen-1;
  396. result:=CheckPos;
  397. if result>-1 then
  398. exit; //found it
  399. while ReadSize > 0 do
  400. begin
  401. for lpByte := 0 to pred(ReadSize) do
  402. begin
  403. inc(FifoStart); if FifoStart>=SearchLen then FifoStart := 0;
  404. inc(FifoEnd); if FifoEnd>=SearchLen then FifoEnd := 0;
  405. FifoBuff[FifoEnd] := BlockArray[lpByte];
  406. inc(fPos);
  407. result:=CheckPos;
  408. if result>-1 then
  409. exit; //found it
  410. end;
  411. ReadSize := Stream.Read(BlockArray[0],Length(BlockArray));
  412. end;
  413. end;
  414. procedure infoln(Message: string; Level: TEventType);
  415. var
  416. Seriousness: string;
  417. begin
  418. case Level of
  419. etCustom: Seriousness := 'Custom:';
  420. etDebug: Seriousness := 'Debug:';
  421. etInfo: Seriousness := 'Info:';
  422. etWarning: Seriousness := 'WARNING:';
  423. etError: Seriousness := 'ERROR:';
  424. else
  425. Seriousness := 'UNKNOWN CATEGORY!!:'
  426. end;
  427. if (Level <> etDebug) then
  428. begin
  429. if AnsiPos(LineEnding, Message) > 0 then
  430. writeln(''); //Write an empty line before multiline messagse
  431. writeln(Seriousness + ' ' + Message); //we misuse this for info output
  432. sleep(200); //hopefully allow output to be written without interfering with other output
  433. end
  434. else
  435. begin
  436. {$IFDEF DEBUG}
  437. {DEBUG conditional symbol is defined using e.g.
  438. Project Options/Other/Custom Options using -dDEBUG}
  439. if AnsiPos(LineEnding, Message) > 0 then
  440. writeln(''); //Write an empty line before multiline messagse
  441. writeln(Seriousness + ' ' + Message); //we misuse this for info output
  442. sleep(200); //hopefully allow output to be written without interfering with other output
  443. {$ENDIF DEBUG}
  444. end;
  445. end;
  446. { TLogger }
  447. procedure TLogger.WriteLog(Message: string; ToConsole: boolean = False);
  448. begin
  449. FLog.Log(etInfo, Message);
  450. if ToConsole then
  451. infoln(Message, etinfo);
  452. end;
  453. procedure TLogger.WriteLog(EventType: TEventType; Message: string;
  454. ToConsole: boolean = False);
  455. begin
  456. // Only log debug level if compiled as a debug build in order to cut down on logging
  457. {$IFDEF DEBUG}
  458. if 1 = 1 then
  459. {$ELSE}
  460. if EventType <> etDebug then
  461. {$ENDIF}
  462. begin
  463. FLog.Log(EventType, Message);
  464. if ToConsole then
  465. infoln(Message, etinfo);
  466. end;
  467. {$IFDEF DEBUG}
  468. // By setting active to false, we try to force a log write. Next log attempt will set active to true again
  469. FLog.Active := False;
  470. {$ENDIF}
  471. end;
  472. constructor TLogger.Create;
  473. begin
  474. FLog := TEventLog.Create(nil);
  475. FLog.LogType := ltSystem; //eventlog/syslog, not log to file
  476. FLog.RegisterMessageFile('');
  477. //specify Windows should use the binary to look up formatting strings
  478. FLog.RaiseExceptionOnError := False; //Don't throw exceptions on log errors.
  479. FLog.Active := True;
  480. end;
  481. destructor TLogger.Destroy;
  482. begin
  483. FLog.Active := False; //save WriteLog text
  484. FLog.Free;
  485. inherited Destroy;
  486. end;
  487. initialization
  488. begin
  489. TigerLog := TLogger.Create;
  490. {$IFDEF USEMAGICK}
  491. MagickWandGenesis;
  492. {$ENDIF}
  493. end;
  494. finalization
  495. begin
  496. {$IFDEF USEMAGICK}
  497. MagickWandTerminus;
  498. {$ENDIF}
  499. TigerLog.Free;
  500. end;
  501. end.