Repo for the search and displace ingest module that takes odf, docx and pdf and transforms it into .md to be used with search and displace operations
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
13 KiB

3 years ago
  1. {
  2. Deskew
  3. by Marek Mauder
  4. http://galfar.vevb.net/deskew
  5. The contents of this file are used with permission, subject to the Mozilla
  6. Public License Version 1.1 (the "License"); you may not use this file except
  7. in compliance with the License. You may obtain a copy of the License at
  8. http://www.mozilla.org/MPL/MPL-1.1.html
  9. Software distributed under the License is distributed on an "AS IS" basis,
  10. WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  11. the specific language governing rights and limitations under the License.
  12. Alternatively, the contents of this file may be used under the terms of the
  13. GNU Lesser General Public License (the "LGPL License"), in which case the
  14. provisions of the LGPL License are applicable instead of those above.
  15. If you wish to allow use of your version of this file only under the terms
  16. of the LGPL License and not to allow others to use your version of this file
  17. under the MPL, indicate your decision by deleting the provisions above and
  18. replace them with the notice and other provisions required by the LGPL
  19. License. If you do not delete the provisions above, a recipient may use
  20. your version of this file under either the MPL or the LGPL License.
  21. For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html
  22. }
  23. unit MainUnit;
  24. {$I ImagingOptions.inc}
  25. interface
  26. procedure RunDeskew;
  27. implementation
  28. uses
  29. Types,
  30. SysUtils,
  31. Classes,
  32. ImagingTypes,
  33. Imaging,
  34. ImagingClasses,
  35. ImagingFormats,
  36. ImagingUtility,
  37. ImagingExtras,
  38. // Project units
  39. CmdLineOptions,
  40. ImageUtils,
  41. RotationDetector;
  42. const
  43. SAppTitle = 'Deskew 1.30 (2019-06-07)'
  44. {$IF Defined(CPUX64)} + ' x64'
  45. {$ELSEIF Defined(CPUX86)} + ' x86'
  46. {$ELSEIF Defined(CPUARM)} + ' ARM'
  47. {$IFEND}
  48. {$IFDEF DEBUG} + ' (DEBUG)'{$ENDIF}
  49. + ' by Marek Mauder';
  50. SAppHome = 'http://galfar.vevb.net/deskew/';
  51. var
  52. // Program options
  53. Options: TCmdLineOptions;
  54. // Input and output image
  55. InputImage, OutputImage: TSingleImage;
  56. procedure WriteUsage;
  57. var
  58. InFilter, OutFilter: string;
  59. I, Count: Integer;
  60. Fmt: TImageFileFormat;
  61. begin
  62. InFilter := '';
  63. OutFilter := '';
  64. WriteLn('Usage:');
  65. WriteLn('deskew [-o output] [-a angle] [-b color] [..] input');
  66. WriteLn(' input: Input image file');
  67. WriteLn(' Options:');
  68. WriteLn(' -o output: Output image file (default: out.png)');
  69. WriteLn(' -a angle: Maximal expected skew angle (both directions) in degrees (default: 10)');
  70. WriteLn(' -b color: Background color in hex format RRGGBB|LL|AARRGGBB (default: black)');
  71. WriteLn(' Ext. options:');
  72. WriteLn(' -q filter: Resampling filter used for rotations (default: linear,');
  73. WriteLn(' values: nearest|linear|cubic|lanczos)');
  74. WriteLn(' -t a|treshold: Auto threshold or value in 0..255 (default: a)');
  75. WriteLn(' -r rect: Skew detection only in content rectangle (pixels):');
  76. WriteLn(' left,top,right,bottom (default: whole page)');
  77. WriteLn(' -f format: Force output pixel format (values: b1|g8|rgb24|rgba32)');
  78. WriteLn(' -l angle: Skip deskewing step if skew angle is smaller (default: 0.01)');
  79. WriteLn(' -g flags: Operational flags (any combination of):');
  80. WriteLn(' c - auto crop, d - detect only (no output to file)');
  81. WriteLn(' -s info: Info dump (any combination of):');
  82. WriteLn(' s - skew detection stats, p - program parameters, t - timings');
  83. WriteLn(' -c specs: Output compression specs for some file formats. Several specs');
  84. WriteLn(' can be defined - delimited by commas. Supported specs:');
  85. WriteLn(' jXX - JPEG compression quality, XX is in range [1,100(best)]');
  86. WriteLn(' tSCHEME - TIFF compression scheme: none|lzw|rle|deflate|jpeg|g4');
  87. Count := GetFileFormatCount;
  88. for I := 0 to Count - 1 do
  89. begin
  90. Fmt := GetFileFormatAtIndex(I);
  91. if Fmt.CanLoad then
  92. InFilter := InFilter + Fmt.Extensions[0] + Iff(I < Count - 1, ', ', '');
  93. if Fmt.CanSave then
  94. OutFilter := OutFilter + Fmt.Extensions[0] + Iff(I < Count - 1, ', ', '');
  95. end;
  96. WriteLn;
  97. WriteLn(' Supported file formats');
  98. WriteLn(' Input: ', UpperCase(InFilter));
  99. WriteLn(' Output: ', UpperCase(OutFilter));
  100. end;
  101. procedure ReportBadInput(const Msg: string; ShowUsage: Boolean = True);
  102. begin
  103. WriteLn;
  104. WriteLn('Error: ' + Msg);
  105. if Options.ErrorMessage <> '' then
  106. WriteLn(Options.ErrorMessage);
  107. WriteLn;
  108. if ShowUsage then
  109. WriteUsage;
  110. ExitCode := 1;
  111. end;
  112. function FormatNiceNumber(const X: Int64; Width : Integer = 16): string;
  113. var
  114. FmtStr: string;
  115. begin
  116. if Width = 0 then
  117. FmtStr := '%.0n'
  118. else
  119. FmtStr := '%' + IntToStr(Width) + '.0n';
  120. Result := Format(FmtStr, [X * 1.0], GetFormatSettingsForFloats);
  121. end;
  122. var
  123. Time: Int64;
  124. procedure WriteTiming(const StepName: string);
  125. begin
  126. if Options.ShowTimings then
  127. WriteLn(StepName + ' - time taken: ' + FormatNiceNumber(GetTimeMicroseconds - Time, 0) + ' us');
  128. end;
  129. function DoDeskew: Boolean;
  130. var
  131. SkewAngle: Double;
  132. Threshold: Integer;
  133. ContentRect: TRect;
  134. Stats: TCalcSkewAngleStats;
  135. procedure WriteStats;
  136. begin
  137. WriteLn('Skew detection stats:');
  138. WriteLn(' pixel count: ', FormatNiceNumber(Stats.PixelCount));
  139. WriteLn(' tested pixels: ', FormatNiceNumber(Stats.TestedPixels));
  140. WriteLn(' accumulator size: ', FormatNiceNumber(Stats.AccumulatorSize));
  141. WriteLn(' accumulated counts: ', FormatNiceNumber(Stats.AccumulatedCounts));
  142. WriteLn(' best count: ', FormatNiceNumber(Stats.BestCount));
  143. end;
  144. begin
  145. Result := False;
  146. Threshold := 0;
  147. WriteLn('Preparing input image (', ExtractFileName(Options.InputFile), ' [',
  148. InputImage.Width, 'x', InputImage.Height, '/', string(InputImage.FormatInfo.Name), ']) ...');
  149. // Clone input image and convert it to 8bit grayscale. This will be our
  150. // working image.
  151. OutputImage.Assign(InputImage);
  152. InputImage.Format := ifGray8;
  153. // Determine threshold level for black/white pixel classification during skew detection
  154. case Options.ThresholdingMethod of
  155. tmExplicit:
  156. begin
  157. // Use explicit threshold
  158. Threshold := Options.ThresholdLevel;
  159. end;
  160. tmOtsu:
  161. begin
  162. // Determine the threshold automatically
  163. Time := GetTimeMicroseconds;
  164. Threshold := OtsuThresholding(InputImage.ImageDataPointer^);
  165. WriteTiming('Auto thresholding');
  166. end;
  167. end;
  168. // Determine the content rect - where exactly to detect rotated text
  169. ContentRect := InputImage.BoundsRect;
  170. if not IsRectEmpty(Options.ContentRect) then
  171. begin
  172. if not IntersectRect(ContentRect, Options.ContentRect, InputImage.BoundsRect) then
  173. ContentRect := InputImage.BoundsRect;
  174. end;
  175. // Main step - calculate image rotation SkewAngle
  176. WriteLn('Calculating skew angle...');
  177. Time := GetTimeMicroseconds;
  178. SkewAngle := CalcRotationAngle(Options.MaxAngle, Threshold,
  179. InputImage.Width, InputImage.Height, InputImage.Bits,
  180. @ContentRect, @Stats);
  181. WriteTiming('Skew detection');
  182. WriteLn('Skew angle found [deg]: ', SkewAngle:4:3);
  183. if Options.ShowStats then
  184. WriteStats;
  185. if ofDetectOnly in Options.OperationalFlags then
  186. Exit;
  187. // Check if detected skew angle is higher than "skip" threshold - may not
  188. // want to do rotation needlessly.
  189. if Abs(SkewAngle) >= Options.SkipAngle then
  190. begin
  191. Result := True;
  192. // Finally, rotate the image. We rotate the original input image, not the working
  193. // one so the color space is preserved if possible.
  194. WriteLn('Rotating image...');
  195. // Rotation is optimized for Gray8, RGB24, and ARGB32 formats at this time
  196. if not (OutputImage.Format in ImageUtils.SupportedRotationFormats) then
  197. begin
  198. if OutputImage.Format = ifIndex8 then
  199. begin
  200. if PaletteHasAlpha(OutputImage.Palette, OutputImage.PaletteEntries) then
  201. OutputImage.Format := ifA8R8G8B8
  202. else if PaletteIsGrayScale(OutputImage.Palette, OutputImage.PaletteEntries) then
  203. OutputImage.Format := ifGray8
  204. else
  205. OutputImage.Format := ifR8G8B8;
  206. end
  207. else if OutputImage.FormatInfo.HasAlphaChannel then
  208. OutputImage.Format := ifA8R8G8B8
  209. else if (OutputImage.Format = ifBinary) or OutputImage.FormatInfo.HasGrayChannel then
  210. OutputImage.Format := ifGray8
  211. else
  212. OutputImage.Format := ifR8G8B8;
  213. end;
  214. if (Options.BackgroundColor and $FF000000) <> $FF000000 then
  215. begin
  216. // User explicitly requested some alpha in background color
  217. OutputImage.Format := ifA8R8G8B8;
  218. end
  219. else if (OutputImage.Format = ifGray8) and not (
  220. (GetRedValue(Options.BackgroundColor) = GetGreenValue(Options.BackgroundColor)) and
  221. (GetBlueValue(Options.BackgroundColor) = GetGreenValue(Options.BackgroundColor))) then
  222. begin
  223. // Some non-grayscale background for gray image was requested
  224. OutputImage.Format := ifR8G8B8;
  225. end;
  226. Time := GetTimeMicroseconds;
  227. ImageUtils.RotateImage(OutputImage.ImageDataPointer^, SkewAngle, Options.BackgroundColor,
  228. Options.ResamplingFilter, not (ofAutoCrop in Options.OperationalFlags));
  229. WriteTiming('Rotate image');
  230. end
  231. else
  232. WriteLn('Skipping deskewing step, skew angle lower than threshold of ', Options.SkipAngle:4:2);
  233. if (Options.ForcedOutputFormat <> ifUnknown) and (OutputImage.Format <> Options.ForcedOutputFormat) then
  234. begin
  235. // Force output format. For example Deskew won't automatically
  236. // save image as binary if the input was binary since it
  237. // might degrade the output a lot (rotation adds a lot of colors to image).
  238. OutputImage.Format := Options.ForcedOutputFormat;
  239. Result := True;
  240. end;
  241. end;
  242. procedure RunDeskew;
  243. procedure EnsureOutputLocation(const FileName: string);
  244. var
  245. Dir, Path: string;
  246. begin
  247. Path := ExpandFileName(FileName);
  248. Dir := GetFileDir(Path);
  249. if Dir <> '' then
  250. ForceDirectories(Dir);
  251. end;
  252. procedure CopyFile(const SrcPath, DestPath: string);
  253. var
  254. SrcStream, DestStream: TFileStream;
  255. begin
  256. if SameText(SrcPath, DestPath) then
  257. Exit; // No need to copy anything
  258. SrcStream := TFileStream.Create(SrcPath, fmOpenRead);
  259. DestStream := TFileStream.Create(DestPath, fmCreate);
  260. DestStream.CopyFrom(SrcStream, SrcStream.Size);
  261. DestStream.Free;
  262. SrcStream.Free;
  263. end;
  264. procedure SetImagingOptions;
  265. begin
  266. if Options.JpegCompressionQuality <> -1 then
  267. begin
  268. Imaging.SetOption(ImagingJpegQuality, Options.JpegCompressionQuality);
  269. Imaging.SetOption(ImagingTiffJpegQuality, Options.JpegCompressionQuality);
  270. Imaging.SetOption(ImagingJNGQuality, Options.JpegCompressionQuality);
  271. end;
  272. if Options.TiffCompressionScheme <> -1 then
  273. Imaging.SetOption(ImagingTiffCompression, Options.TiffCompressionScheme);
  274. end;
  275. var
  276. Changed: Boolean;
  277. begin
  278. {$IF Defined(FPC) and not Defined(MSWINDOWS)}
  279. // Flush after WriteLn also when output is redirected to file/pipe
  280. if Textrec(Output).FlushFunc = nil then
  281. Textrec(Output).FlushFunc := Textrec(Output).InOutFunc;
  282. {$IFEND}
  283. WriteLn(SAppTitle);
  284. WriteLn(SAppHome);
  285. Options := TCmdLineOptions.Create;
  286. InputImage := TSingleImage.Create;
  287. OutputImage := TSingleImage.Create;
  288. try
  289. try
  290. if Options.ParseCommnadLine and Options.IsValid then
  291. begin
  292. SetImagingOptions;
  293. if Options.ShowParams then
  294. WriteLn(Options.OptionsToString);
  295. if not IsFileFormatSupported(Options.InputFile) then
  296. begin
  297. ReportBadInput('File format not supported: ' + Options.InputFile);
  298. Exit;
  299. end;
  300. // Load input image
  301. Time := GetTimeMicroseconds;
  302. InputImage.LoadFromFile(Options.InputFile);
  303. WriteTiming('Load input file');
  304. if not InputImage.Valid then
  305. begin
  306. ReportBadInput('Loaded input image is not valid: ' + Options.InputFile, False);
  307. Exit;
  308. end;
  309. // Do the magic
  310. Changed := DoDeskew();
  311. if not (ofDetectOnly in Options.OperationalFlags) then
  312. begin
  313. WriteLn('Saving output (', ExpandFileName(Options.OutputFile), ' [',
  314. OutputImage.Width, 'x', OutputImage.Height, '/', string(OutputImage.FormatInfo.Name), ']) ...');
  315. // Make sure output folders are ready
  316. EnsureOutputLocation(Options.OutputFile);
  317. // In case no change to image was done by deskewing we still need to resave if requested file format differs from input
  318. Changed := Changed or not SameText(GetFileExt(Options.InputFile), GetFileExt(Options.OutputFile));
  319. Time := GetTimeMicroseconds;
  320. if Changed then
  321. begin
  322. // Make sure recognized metadata stays (like scanning DPI info)
  323. GlobalMetadata.CopyLoadedMetaItemsForSaving;
  324. // Save the output
  325. OutputImage.SaveToFile(Options.OutputFile);
  326. end
  327. else
  328. begin
  329. // No change to image made, just copy it to the desired destination
  330. CopyFile(Options.InputFile, Options.OutputFile);
  331. end;
  332. WriteTiming('Save output file');
  333. end;
  334. WriteLn('Done!');
  335. end
  336. else
  337. begin
  338. // Bad input
  339. ReportBadInput('Invalid parameters!');
  340. end;
  341. except
  342. on E: Exception do
  343. begin
  344. WriteLn;
  345. WriteLn(E.ClassName, ': ', E.Message);
  346. ExitCode := 1;
  347. end;
  348. end;
  349. finally
  350. Options.Free;
  351. InputImage.Free;
  352. OutputImage.Free;
  353. {$IFDEF DEBUG}
  354. ReadLn;
  355. {$ENDIF}
  356. end;
  357. end;
  358. end.