diff --git a/NAPS2.Lib.Common/Modules/CommonModule.cs b/NAPS2.Lib.Common/Modules/CommonModule.cs index 8669b0eb5..8b85b1861 100644 --- a/NAPS2.Lib.Common/Modules/CommonModule.cs +++ b/NAPS2.Lib.Common/Modules/CommonModule.cs @@ -31,7 +31,7 @@ public class CommonModule : NinjectModule Bind().To(); Bind().To(); Bind().To(); - Bind().ToMethod(ctx => OcrEngineManager.Default); + // TODO: Bind TesseractLanguageManager Bind().ToSelf().InSingletonScope(); // Scan diff --git a/NAPS2.Lib.Common/StaticConfiguration.cs b/NAPS2.Lib.Common/StaticConfiguration.cs index f2c51cea7..13564db10 100644 --- a/NAPS2.Lib.Common/StaticConfiguration.cs +++ b/NAPS2.Lib.Common/StaticConfiguration.cs @@ -19,11 +19,10 @@ public static class StaticConfiguration Debug.Listeners.Add(new NLogTraceListener()); #endif + // TODO: Initialize TesseractLanguageManager here? var customPath = config.Get(c => c.ComponentsPath); var basePath = string.IsNullOrWhiteSpace(customPath) ? Paths.Components : Environment.ExpandEnvironmentVariables(customPath); - - OcrEngineManager.Default = new OcrEngineManager(basePath); } } \ No newline at end of file diff --git a/NAPS2.Lib.Console/Automation/AutomatedScanning.cs b/NAPS2.Lib.Console/Automation/AutomatedScanning.cs index ea4b7ad91..fd2f9aa3e 100644 --- a/NAPS2.Lib.Console/Automation/AutomatedScanning.cs +++ b/NAPS2.Lib.Console/Automation/AutomatedScanning.cs @@ -20,7 +20,7 @@ public class AutomatedScanning private readonly ErrorOutput _errorOutput; private readonly IScannedImageImporter _scannedImageImporter; private readonly IOperationFactory _operationFactory; - private readonly OcrEngineManager _ocrEngineManager; + private readonly TesseractLanguageManager _tesseractLanguageManager; private readonly IFormFactory _formFactory; private readonly ScopedConfig _config; private readonly TransactionConfigScope _userTransact; @@ -43,7 +43,7 @@ public class AutomatedScanning public AutomatedScanning(ConsoleOutput output, AutomatedScanningOptions options, ImageContext imageContext, IScanPerformer scanPerformer, ErrorOutput errorOutput, IEmailProviderFactory emailProviderFactory, IScannedImageImporter scannedImageImporter, IOperationFactory operationFactory, - OcrEngineManager ocrEngineManager, IFormFactory formFactory, ScopedConfig config, + TesseractLanguageManager tesseractLanguageManager, IFormFactory formFactory, ScopedConfig config, IProfileManager profileManager, RecoveryStorageManager recoveryStorageManager, ScanningContext scanningContext) { _output = output; @@ -54,7 +54,7 @@ public class AutomatedScanning _emailProviderFactory = emailProviderFactory; _scannedImageImporter = scannedImageImporter; _operationFactory = operationFactory; - _ocrEngineManager = ocrEngineManager; + _tesseractLanguageManager = tesseractLanguageManager; _formFactory = formFactory; _config = config; _profileManager = profileManager; @@ -173,12 +173,7 @@ public class AutomatedScanning private void InstallComponents() { var availableComponents = new List(); - var ocrEngine = _ocrEngineManager.EngineToInstall; - if (ocrEngine != null) - { - availableComponents.Add(ocrEngine.Component); - availableComponents.AddRange(ocrEngine.LanguageComponents); - } + availableComponents.AddRange(_tesseractLanguageManager.LanguageComponents); var componentDict = availableComponents.ToDictionary(x => x.Id.ToLowerInvariant()); var installId = _options.Install.ToLowerInvariant(); diff --git a/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs b/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs index b98950be6..7f79b23e1 100644 --- a/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs +++ b/NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs @@ -100,7 +100,7 @@ public class CommandLineIntegrationTests : ContextualTexts public override void Load() { Rebind().ToConstant(_imageContext); - Rebind().ToConstant(new OcrEngineManager()); + // TODO: Bind TesseractLanguageManager Rebind().ToConstant(_scanDriverFactory); Rebind().To(); Rebind().ToSelf().WithConstructorArgument("writer", new TestOutputTextWriter(_testOutputHelper)); diff --git a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj index 7cd89b850..fefb25e92 100644 --- a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj +++ b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj @@ -38,6 +38,10 @@ ResXFileCodeGenerator ImageImporterTestsData.Designer.cs + + ResXFileCodeGenerator + TesseractResources.Designer.cs + @@ -46,6 +50,20 @@ True ImageImporterTestsData.resx + + True + True + TesseractTestResources.resx + + + + + + PreserveNewest + + + PreserveNewest + \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Ocr/TesseractEngineTests.cs b/NAPS2.Sdk.Tests/Ocr/TesseractEngineTests.cs deleted file mode 100644 index 010dd0698..000000000 --- a/NAPS2.Sdk.Tests/Ocr/TesseractEngineTests.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NAPS2.Sdk.Tests.Ocr; - -public class TesseractEngineTests -{ - // TODO: Ocr tests - // (1) Integration test that runs a real tesseract, produces a real pdf, and parses the pdf - // (2) Unit test for hocr parsing - // (3) Unit tests for OcrEngineManager - // (4) ? -} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs b/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs new file mode 100644 index 000000000..4708533db --- /dev/null +++ b/NAPS2.Sdk.Tests/Ocr/TesseractOcrEngineTests.cs @@ -0,0 +1,34 @@ +using System.Threading; +using NAPS2.Ocr; +using Xunit; + +namespace NAPS2.Sdk.Tests.Ocr; + +public class TesseractOcrEngineTests : ContextualTexts +{ + private readonly TesseractOcrEngine _engine; + private readonly string _testImagePath; + + public TesseractOcrEngineTests() + { + var exePath = Path.Combine(FolderPath, "tesseract.exe"); + File.WriteAllBytes(exePath, TesseractResources.tesseract_x64); + + var tessdataPath = Path.Combine(FolderPath, "fast"); + Directory.CreateDirectory(tessdataPath); + var engDataPath = Path.Combine(tessdataPath, "eng.traineddata"); + File.WriteAllBytes(engDataPath, TesseractResources.eng_traineddata); + + _testImagePath = Path.Combine(FolderPath, "ocr_test.jpg"); + File.WriteAllBytes(_testImagePath, TesseractResources.ocr_test); + + _engine = new TesseractOcrEngine(exePath, FolderPath); + } + + [Fact] + public async Task RunTesseract() + { + var result = await _engine.ProcessImage(_testImagePath, new OcrParams("eng", OcrMode.Fast, 0), CancellationToken.None); + Assert.NotNull(result); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Ocr/TesseractResources.Designer.cs b/NAPS2.Sdk.Tests/Ocr/TesseractResources.Designer.cs new file mode 100644 index 000000000..87ab66dd6 --- /dev/null +++ b/NAPS2.Sdk.Tests/Ocr/TesseractResources.Designer.cs @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace NAPS2.Sdk.Tests.Ocr { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TesseractResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TesseractResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NAPS2.Sdk.Tests.Ocr.TesseractResources", typeof(TesseractResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] eng_traineddata { + get { + object obj = ResourceManager.GetObject("eng_traineddata", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ocr_test { + get { + object obj = ResourceManager.GetObject("ocr_test", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] tesseract_x64 { + get { + object obj = ResourceManager.GetObject("tesseract_x64", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/NAPS2.Sdk.Tests/Ocr/TesseractResources.resx b/NAPS2.Sdk.Tests/Ocr/TesseractResources.resx new file mode 100644 index 000000000..1e36c1b91 --- /dev/null +++ b/NAPS2.Sdk.Tests/Ocr/TesseractResources.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\tesseract-5.1.0-x64.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\eng.traineddata;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\ocr_test.jpg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Resources/eng.traineddata b/NAPS2.Sdk.Tests/Resources/eng.traineddata new file mode 100644 index 000000000..bbef46750 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/eng.traineddata differ diff --git a/NAPS2.Sdk.Tests/Resources/ocr_test.jpg b/NAPS2.Sdk.Tests/Resources/ocr_test.jpg new file mode 100644 index 000000000..aea89cb86 Binary files /dev/null and b/NAPS2.Sdk.Tests/Resources/ocr_test.jpg differ diff --git a/NAPS2.Sdk/ImportExport/AutoSaver.cs b/NAPS2.Sdk/ImportExport/AutoSaver.cs index 70961744c..47677190f 100644 --- a/NAPS2.Sdk/ImportExport/AutoSaver.cs +++ b/NAPS2.Sdk/ImportExport/AutoSaver.cs @@ -20,7 +20,7 @@ public class AutoSaver private readonly TiffHelper _tiffHelper; private readonly ImageContext _imageContext; - public AutoSaver(IConfigProvider pdfSettingsProvider, IConfigProvider imageSettingsProvider, OcrEngineManager ocrEngineManager, OcrRequestQueue ocrRequestQueue, ErrorOutput errorOutput, DialogHelper dialogHelper, OperationProgress operationProgress, ISaveNotify notify, PdfExporter pdfExporter, OverwritePrompt overwritePrompt, ScopedConfig config, TiffHelper tiffHelper, ImageContext imageContext) + public AutoSaver(IConfigProvider pdfSettingsProvider, IConfigProvider imageSettingsProvider, ErrorOutput errorOutput, DialogHelper dialogHelper, OperationProgress operationProgress, ISaveNotify notify, PdfExporter pdfExporter, OverwritePrompt overwritePrompt, ScopedConfig config, TiffHelper tiffHelper, ImageContext imageContext) { _pdfSettingsProvider = pdfSettingsProvider; _imageSettingsProvider = imageSettingsProvider; diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfSharpExporter.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfSharpExporter.cs index 66e9a8e53..31d11b10d 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfSharpExporter.cs +++ b/NAPS2.Sdk/ImportExport/Pdf/PdfSharpExporter.cs @@ -74,10 +74,6 @@ public class PdfSharpExporter : PdfExporter { Log.Error("Supported OCR engine not installed.", ocrParams.LanguageCode); } - else if (!activeEngine.CanProcess(ocrParams.LanguageCode)) - { - Log.Error("OCR files not available for '{0}'.", ocrParams.LanguageCode); - } else { ocrEngine = activeEngine; diff --git a/NAPS2.Sdk/Ocr/IOcrEngine.cs b/NAPS2.Sdk/Ocr/IOcrEngine.cs index 792d18fa3..36c45d13a 100644 --- a/NAPS2.Sdk/Ocr/IOcrEngine.cs +++ b/NAPS2.Sdk/Ocr/IOcrEngine.cs @@ -1,27 +1,8 @@ using System.Threading; -using NAPS2.Dependencies; namespace NAPS2.Ocr; public interface IOcrEngine { - bool CanProcess(string langCode); - Task ProcessImage(string imagePath, OcrParams ocrParams, CancellationToken cancelToken); - - bool IsSupported { get; } - - bool IsInstalled { get; } - - bool CanInstall { get; } - - IEnumerable InstalledLanguages { get; } - - IEnumerable NotInstalledLanguages { get; } - - IExternalComponent Component { get; } - - IEnumerable LanguageComponents { get; } - - IEnumerable SupportedModes { get; } } \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/OcrController.cs b/NAPS2.Sdk/Ocr/OcrController.cs index afdc5d783..a74dc80d5 100644 --- a/NAPS2.Sdk/Ocr/OcrController.cs +++ b/NAPS2.Sdk/Ocr/OcrController.cs @@ -50,10 +50,6 @@ public class OcrController { throw new InvalidOperationException("OCR is enabled but no language code is specified."); } - if (!Engine.CanProcess(OcrParams.LanguageCode)) - { - throw new InvalidOperationException("OCR is enabled but the engine can't handle the specified language."); - } CancellationTokenSource cts = new CancellationTokenSource(); _cancellationTokenSources.Add(image.GetWeakReference(), cts); diff --git a/NAPS2.Sdk/Ocr/OcrEngineManager.cs b/NAPS2.Sdk/Ocr/OcrEngineManager.cs deleted file mode 100644 index cda58b4e1..000000000 --- a/NAPS2.Sdk/Ocr/OcrEngineManager.cs +++ /dev/null @@ -1,82 +0,0 @@ -using NAPS2.Scan; - -namespace NAPS2.Ocr; - -public class OcrEngineManager : IOcrEngineProvider -{ - private static OcrEngineManager _default = new OcrEngineManager(); - - public static OcrEngineManager Default - { - get - { - TestingContext.NoStaticDefaults(); - return _default; - } - set => _default = value ?? throw new ArgumentNullException(nameof(value)); - } - - private readonly List _engines; - - /// - /// Creates a new instance of OcrEngineManager that only looks for Tesseract on the system path. - /// - public OcrEngineManager() - { - _engines = new List - { - new TesseractSystemEngine() - }; - } - - /// - /// Creates a new instance of OcrEngineManager with the specified engines. The order of engines is important; preferred/newer first. - /// - /// - public OcrEngineManager(IEnumerable orderedEngineList) - { - _engines = orderedEngineList.ToList(); - } - - /// - /// Creates a new instance of OcrEngineManager with the default set of engines. - /// The base path for installed engines. - /// - public OcrEngineManager(string basePath) - { - _engines = new List - { - new Tesseract400Beta4Engine(basePath), - new Tesseract304Engine(basePath), - new Tesseract304XpEngine(basePath), - new Tesseract302Engine(basePath), - new TesseractSystemEngine() - }; - } - - public IEnumerable Engines => _engines; - - public bool IsReady => _engines.Any(x => x.IsSupported && x.IsInstalled && x.InstalledLanguages.Any()); - - public bool IsNewestReady - { - get - { - var latest = _engines.FirstOrDefault(x => x.IsSupported); - if (latest == null) return false; - return latest.IsInstalled && latest.InstalledLanguages.Any(); - } - } - - public bool CanUpgrade => !IsNewestReady && _engines.Any(x => x.IsInstalled); - - public bool MustUpgrade => !IsReady && _engines.Any(x => x.IsInstalled); - - public bool MustInstallPackage => _engines.All(x => (!x.IsSupported || !x.CanInstall) && !x.IsInstalled); - - public IOcrEngine? ActiveEngine => _engines.FirstOrDefault(x => x.IsSupported && x.IsInstalled && x.InstalledLanguages.Any()); - - public IOcrEngine? InstalledEngine => _engines.FirstOrDefault(x => x.IsInstalled && x.InstalledLanguages.Any()); - - public IOcrEngine? EngineToInstall => _engines.FirstOrDefault(x => x.IsSupported && x.CanInstall); -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/Tesseract302Engine.cs b/NAPS2.Sdk/Ocr/Tesseract302Engine.cs deleted file mode 100644 index 3952ca503..000000000 --- a/NAPS2.Sdk/Ocr/Tesseract302Engine.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NAPS2.Dependencies; - -namespace NAPS2.Ocr; - -public class Tesseract302Engine : TesseractBaseEngine -{ - public Tesseract302Engine(string basePath) - { - // Using the newer data since we just need the 302 engine for backwards compatibility - LanguageData = TesseractLanguageData.V304; - TesseractBasePath = Path.Combine(basePath, "tesseract-3.0.2"); - TesseractExePath = "tesseract.exe"; - TesseractHocrExtension = ".html"; - PlatformSupport = PlatformSupport.Windows; - CanInstall = false; - - Component = new ExternalComponent("ocr", Path.Combine(TesseractBasePath, TesseractExePath), null); - - LanguageComponents = LanguageData.Data.Select(x => - new ExternalComponent($"ocr-{x.Code}", Path.Combine(TesseractBasePath, "tessdata", x.Filename.Replace(".gz", "")), null)); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/Tesseract304Engine.cs b/NAPS2.Sdk/Ocr/Tesseract304Engine.cs deleted file mode 100644 index 529f1db68..000000000 --- a/NAPS2.Sdk/Ocr/Tesseract304Engine.cs +++ /dev/null @@ -1,29 +0,0 @@ -using NAPS2.Dependencies; - -namespace NAPS2.Ocr; - -public class Tesseract304Engine : TesseractBaseEngine -{ - protected static readonly List Mirrors = new List - { - new DownloadMirror(PlatformSupport.ModernWindows.Or(PlatformSupport.Linux), @"https://github.com/cyanfish/naps2-components/releases/download/tesseract-3.04/{0}"), - new DownloadMirror(PlatformSupport.ModernWindows.Or(PlatformSupport.Linux), @"https://sourceforge.net/projects/naps2/files/components/tesseract-3.04/{0}/download"), - new DownloadMirror(PlatformSupport.WindowsXp, @"http://xp-mirror.naps2.com/tesseract-3.04/{0}") - }; - - public Tesseract304Engine(string basePath) - { - LanguageData = TesseractLanguageData.V304; - TesseractBasePath = Path.Combine(basePath, "tesseract-3.0.4"); - TesseractExePath = "tesseract.exe"; - PlatformSupport = PlatformSupport.ModernWindows; - CanInstall = true; - - Component = new ExternalComponent("ocr", Path.Combine(TesseractBasePath, TesseractExePath), - new DownloadInfo("tesseract.exe.gz", Mirrors, 1.32, "0b0fd21cd886c04c60ed5c3f38b9120b408139b3", DownloadFormat.Gzip)); - - LanguageComponents = LanguageData.Data.Select(x => - new ExternalComponent($"ocr-{x.Code}", Path.Combine(TesseractBasePath, "tessdata", x.Filename.Replace(".gz", "")), - new DownloadInfo(x.Filename, Mirrors, x.Size, x.Sha1, DownloadFormat.Gzip))); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/Tesseract304XpEngine.cs b/NAPS2.Sdk/Ocr/Tesseract304XpEngine.cs deleted file mode 100644 index 3bb7fa089..000000000 --- a/NAPS2.Sdk/Ocr/Tesseract304XpEngine.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NAPS2.Dependencies; - -namespace NAPS2.Ocr; - -public class Tesseract304XpEngine : Tesseract304Engine -{ - public Tesseract304XpEngine(string basePath) : base(basePath) - { - TesseractExePath = "tesseract_xp.exe"; - PlatformSupport = PlatformSupport.Windows; - - Component = new ExternalComponent("ocr", Path.Combine(TesseractBasePath, TesseractExePath), - new DownloadInfo("tesseract_xp.exe.gz", Mirrors, 1.32, "98d15e4765caae864f16fa2ab106e3fd6adbe8c3", DownloadFormat.Gzip)); - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/Tesseract400Beta4Engine.cs b/NAPS2.Sdk/Ocr/Tesseract400Beta4Engine.cs deleted file mode 100644 index fc01611e4..000000000 --- a/NAPS2.Sdk/Ocr/Tesseract400Beta4Engine.cs +++ /dev/null @@ -1,51 +0,0 @@ -using NAPS2.Dependencies; - -namespace NAPS2.Ocr; - -public class Tesseract400Beta4Engine : TesseractBaseEngine -{ - protected static readonly List Mirrors = new List - { - new DownloadMirror(PlatformSupport.ModernWindows.Or(PlatformSupport.Linux), @"https://github.com/cyanfish/naps2-components/releases/download/tesseract-4.0.0b4/{0}"), - new DownloadMirror(PlatformSupport.ModernWindows.Or(PlatformSupport.Linux), @"https://sourceforge.net/projects/naps2/files/components/tesseract-4.0.0b4/{0}/download") - }; - - public Tesseract400Beta4Engine(string basePath) - { - string exeFolder = Environment.Is64BitProcess ? "w64" : "w32"; - LanguageData = TesseractLanguageData.V400B4; - TesseractBasePath = Path.Combine(basePath, "tesseract-4.0.0b4"); - TesseractExePath = Path.Combine(exeFolder, "tesseract.exe"); - PlatformSupport = PlatformSupport.ModernWindows; - CanInstall = true; - SupportedModes = new[] { OcrMode.Fast, OcrMode.Best, OcrMode.Legacy }; - - var download = Environment.Is64BitProcess - ? new DownloadInfo("tesseract.exe.w64.zip", Mirrors, 1.83, "4eba9aaf8800a100ef059c512be572e39ae72f4d", DownloadFormat.Zip) - : new DownloadInfo("tesseract.exe.w32.zip", Mirrors, 1.56, "300ad281a5fa1c734dbb4a8a4dd49e3a8ab921a4", DownloadFormat.Zip); - Component = new MultiFileExternalComponent("ocr", Path.Combine(TesseractBasePath, exeFolder), new[] { "tesseract.exe" }, download); - - LanguageComponents = LanguageData.Data.Select(x => - new MultiFileExternalComponent($"ocr-{x.Code}", TesseractBasePath, new[] { $"best/{x.Code}.traineddata", $"fast/{x.Code}.traineddata" }, - new DownloadInfo(x.Filename, Mirrors, x.Size, x.Sha1, DownloadFormat.Zip))); - } - - protected override RunInfo TesseractRunInfo(OcrParams ocrParams) - { - OcrMode mode = ocrParams.Mode; - string folder = mode == OcrMode.Fast || mode == OcrMode.Default ? "fast" : "best"; - if (ocrParams.LanguageCode.Split('+').All(code => !File.Exists(Path.Combine(TesseractBasePath, folder, $"{code.ToLowerInvariant()}.traineddata")))) - { - // Use the other source if the selected one doesn't exist - folder = folder == "fast" ? "best" : "fast"; - mode = folder == "fast" ? OcrMode.Fast : OcrMode.Best; - } - - return new RunInfo - { - Arguments = mode == OcrMode.Best ? "--oem 1" : mode == OcrMode.Legacy ? "--oem 0" : "", - DataPath = folder, - PrefixPath = folder - }; - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/TesseractLanguageData.cs b/NAPS2.Sdk/Ocr/TesseractLanguageData.cs index 8df23d25b..8c8c05d88 100644 --- a/NAPS2.Sdk/Ocr/TesseractLanguageData.cs +++ b/NAPS2.Sdk/Ocr/TesseractLanguageData.cs @@ -37,124 +37,9 @@ public class TesseractLanguageData public TesseractLanguage[] Data { get; set; } - #region Tesseract 3.04 Language Data (auto-generated) + #region Tesseract Language Data (auto-generated) - public static readonly TesseractLanguageData V304 = new TesseractLanguageData(new [] - { - new TesseractLanguage("afr.traineddata.gz", "afr", "Afrikaans", 1.93, "a669186130bf1fc6c78226ac868c82b70a44c70b"), - new TesseractLanguage("amh.traineddata.gz", "amh", "Amharic", 1.03, "1153cbbac7306d42e72ca639ff3f36f45dcb15a2"), - new TesseractLanguage("ara.traineddata.gz", "ara", "Arabic", 1.62, "87b76c73fdcc4c54ec1f03d83b6df665430c2b06", true), - new TesseractLanguage("asm.traineddata.gz", "asm", "Assamese", 6.56, "223900790d10f638b7dca2a8b8e8a15295d1f19c"), - new TesseractLanguage("aze.traineddata.gz", "aze", "Azerbaijani", 2.54, "01607e49fe6ba6604f65d9b57c77b403ab74040a", true), - new TesseractLanguage("aze_cyrl.traineddata.gz", "aze_cyrl", "Azerbaijani (Cyrillic)", 0.97, "f9c9b153e8825bb92d9c8005342ac3d5ea81d0bc"), - new TesseractLanguage("bel.traineddata.gz", "bel", "Belarusian", 2.43, "3ac0935dd22f4f2730286d5cb127324d27718410"), - new TesseractLanguage("ben.traineddata.gz", "ben", "Bengali", 6.45, "479674b283db6e84fdfb17386056f2e9a5b41b9c"), - new TesseractLanguage("bod.traineddata.gz", "bod", "Tibetan", 10.74, "3ff199544dc9e7994658231cbc999878e23463db"), - new TesseractLanguage("bos.traineddata.gz", "bos", "Bosnian", 1.87, "9d0bb89c53251789bba06de1452cf1a74d978f35"), - new TesseractLanguage("bul.traineddata.gz", "bul", "Bulgarian", 2.20, "ac0481cc1fe62c3af5a34d57fa1571dfd2a95865"), - new TesseractLanguage("cat.traineddata.gz", "cat", "Catalan", 1.97, "e1e1dc2e37f6b085bdefdb9d0d63d3ad086ef1f4"), - new TesseractLanguage("ceb.traineddata.gz", "ceb", "Cebuano", 0.58, "f867102f828b6495996370eea6ed8688af219b17"), - new TesseractLanguage("ces.traineddata.gz", "ces", "Czech", 4.65, "155f60a0994f1590d3d3ba29ec1a5bca3f16efdd"), - new TesseractLanguage("chi_sim.traineddata.gz", "chi_sim", "Chinese (Simplified)", 17.60, "9bd65dcecd2581e8f588cec11cd1e2f754885fcb"), - new TesseractLanguage("chi_tra.traineddata.gz", "chi_tra", "Chinese (Traditional)", 24.11, "5abef9af8a4fd83a0d156ee2e1d5234c80bb836b"), - new TesseractLanguage("chr.traineddata.gz", "chr", "Cherokee", 0.36, "d3677cb6c57ec1b14625a5594dad159a1ad9ec93"), - new TesseractLanguage("cym.traineddata.gz", "cym", "Welsh", 1.36, "a5d5733d45710f6da1c4b19f0903bf5edb10a484"), - new TesseractLanguage("dan.traineddata.gz", "dan", "Danish", 2.76, "eb813b0c299261b9535a2c684e51f159f05ae8ea"), - new TesseractLanguage("dan_frak.traineddata.gz", "dan_frak", "Danish (Fraktur)", 0.65, "dcb540024688da096399e52ff9826aad1d71479c"), - new TesseractLanguage("deu.traineddata.gz", "deu", "German", 5.48, "f575f3fcb554077b906aaaac8850d5bd56967cbd"), - new TesseractLanguage("deu_frak.traineddata.gz", "deu_frak", "German (Fraktur)", 0.78, "28ac257129f881b3a09c099004048bf6de4bc952"), - new TesseractLanguage("dzo.traineddata.gz", "dzo", "Dzongkha", 1.32, "6eb0c943242e4d906cbebec2cf43b2ca63979424"), - new TesseractLanguage("ell.traineddata.gz", "ell", "Greek", 2.00, "e54ab7455c1d4715652253321f693e221b61ac8b"), - new TesseractLanguage("eng.traineddata.gz", "eng", "English", 9.02, "36bfd5953540b3c294c62402e303f381cee156f3"), - new TesseractLanguage("enm.traineddata.gz", "enm", "Middle English (1100-1500)", 0.77, "02486b802f4f83b5d9198309955cbf4aa38e5e05"), - new TesseractLanguage("epo.traineddata.gz", "epo", "Esperanto", 2.42, "465dfb934eb45116ebe7f3c4e3adf28826e49dca"), - new TesseractLanguage("equ.traineddata.gz", "equ", "Math / equation detection", 0.78, "c9bc582875cf7c7903b529a9cdb0b9f4669b840d"), - new TesseractLanguage("est.traineddata.gz", "est", "Estonian", 3.62, "d743f2456fa32ce7bbbb80cb40951eb742692596"), - new TesseractLanguage("eus.traineddata.gz", "eus", "Basque", 1.83, "d991552b861e5ea1dca59ffca7e295b323e62bbf"), - new TesseractLanguage("fas.traineddata.gz", "fas", "Persian", 1.75, "c8a7a6b11c3f455b07a397af2e51705a68ff5f77", true), - new TesseractLanguage("fin.traineddata.gz", "fin", "Finnish", 4.98, "90232ad3572901a35bd4bbc736d47184171fa0fd"), - new TesseractLanguage("fra.traineddata.gz", "fra", "French", 5.65, "2bebc5a4c981443c1cbff254e0ca3120004a6c7b"), - new TesseractLanguage("frk.traineddata.gz", "frk", "Frankish", 6.64, "1a6984f8b5768ae663f293ea04594fca229bdb16"), - new TesseractLanguage("frm.traineddata.gz", "frm", "Middle French (ca. 1400-1600)", 6.34, "64e0c6e00352833b206f8b26b6410d0d544b798d"), - new TesseractLanguage("gle.traineddata.gz", "gle", "Irish", 1.25, "994c111e9c24e74bf7105f42a3e39d87ea24f258"), - new TesseractLanguage("glg.traineddata.gz", "glg", "Galician", 2.04, "201c627e518099c15dbbecd72e6e4782e389f619"), - new TesseractLanguage("grc.traineddata.gz", "grc", "Ancient Greek", 1.88, "ae58a943620c485d33ba95b3fcaca79314105d56"), - new TesseractLanguage("guj.traineddata.gz", "guj", "Gujarati", 4.39, "f469d7257f39dcdd0668d768886f19084816b10e"), - new TesseractLanguage("hat.traineddata.gz", "hat", "Haitian", 0.49, "1667e25ebfe6dc74695af413f291e20f1eec552a"), - new TesseractLanguage("heb.traineddata.gz", "heb", "Hebrew", 1.51, "64401c999ef08d6190a11a4347c8f9acf40a8e50", true), - new TesseractLanguage("hin.traineddata.gz", "hin", "Hindi", 6.28, "dae6a9a729ad84eded87fef69004d89249170d44"), - new TesseractLanguage("hrv.traineddata.gz", "hrv", "Croatian", 3.33, "b05db705553607afe3d3f2385dc7f272f348a59c"), - new TesseractLanguage("hun.traineddata.gz", "hun", "Hungarian", 4.62, "250f8b5ad6464e3f0ad8694c0b54392cf6c9d73b"), - new TesseractLanguage("iku.traineddata.gz", "iku", "Inuktitut", 0.30, "119af8b174547aa9cb00f04512d4960d523863ad"), - new TesseractLanguage("ind.traineddata.gz", "ind", "Indonesian", 2.51, "f46f56473ba850408499678c349bdb6dc544dc67"), - new TesseractLanguage("isl.traineddata.gz", "isl", "Icelandic", 2.28, "54004c851361c36ddf48b4443caf79188fa757b6"), - new TesseractLanguage("ita.traineddata.gz", "ita", "Italian", 5.40, "1730f0e32cad3bd76a4f58de67d7c8e2cde17b51"), - new TesseractLanguage("ita_old.traineddata.gz", "ita_old", "Italian (Old)", 5.35, "b7a4293b464cbcce08fd5dc15a9831cff888cdf0"), - new TesseractLanguage("jav.traineddata.gz", "jav", "Javanese", 1.60, "3caa600f063705a2649be289038f381ecdaa8989"), - new TesseractLanguage("jpn.traineddata.gz", "jpn", "Japanese", 13.65, "7545927e6c60888a61556af4247e81c7a08cc17d"), - new TesseractLanguage("kan.traineddata.gz", "kan", "Kannada", 15.12, "53d26da4fde19b5663f4e7748809ba4baf12fe96"), - new TesseractLanguage("kat.traineddata.gz", "kat", "Georgian", 2.23, "8c48267883781ad2278f052259fe4094c64ef9bb"), - new TesseractLanguage("kat_old.traineddata.gz", "kat_old", "Georgian (Old)", 0.19, "88e8312c3fc30ba03811d5d571e44158bc0ab5bf"), - new TesseractLanguage("kaz.traineddata.gz", "kaz", "Kazakh", 1.65, "45c6603afcfe4d81990439df3bed13dd1b4c654b"), - new TesseractLanguage("khm.traineddata.gz", "khm", "Central Khmer", 20.96, "d5a542959114b154db4db61419cd57aba1e3cf5a"), - new TesseractLanguage("kir.traineddata.gz", "kir", "Kirghiz", 2.02, "ee9ba20cde7597688140fc43b14e49417d1052b7"), - new TesseractLanguage("kor.traineddata.gz", "kor", "Korean", 5.11, "39b452ede31b196c66442ea580b5664377eabdab"), - new TesseractLanguage("kur.traineddata.gz", "kur", "Kurdish", 0.73, "a36683c3f62415e1d12529b7642b9463c880db0c", true), - new TesseractLanguage("lao.traineddata.gz", "lao", "Lao", 8.70, "95dbad397571d2d2c13ed63ddc16a51fca343cfb"), - new TesseractLanguage("lat.traineddata.gz", "lat", "Latin", 2.04, "43dc27088ecce88915f6de15c7f6ec9037eebfee"), - new TesseractLanguage("lav.traineddata.gz", "lav", "Latvian", 2.91, "db4e13d875a4c88bd6d8873a7db95fcbd7f9114b"), - new TesseractLanguage("lit.traineddata.gz", "lit", "Lithuanian", 3.28, "fae20b8933a2c49fb9d98539299c7452d530514a"), - new TesseractLanguage("mal.traineddata.gz", "mal", "Malayalam", 3.49, "77a6553e0a37ddf5935a4e81b918850b8babb379"), - new TesseractLanguage("mar.traineddata.gz", "mar", "Marathi", 5.85, "36297ba7adad4e476815a1ab962b556994e85196"), - new TesseractLanguage("mkd.traineddata.gz", "mkd", "Macedonian", 1.36, "63a9ce25d9e2ce9e169ac17e422564809be21fb2"), - new TesseractLanguage("mlt.traineddata.gz", "mlt", "Maltese", 1.96, "18cb93ee612c4c7989c005cdf3a228c4e524db67"), - new TesseractLanguage("msa.traineddata.gz", "msa", "Malay", 2.47, "a40a2af1a06db7cbf4ecef903bff645d7ee3cfc3"), - new TesseractLanguage("mya.traineddata.gz", "mya", "Burmese", 29.36, "f5875d22dc164da4176856ced8521790dfa986a8"), - new TesseractLanguage("nep.traineddata.gz", "nep", "Nepali", 6.53, "55940992c6269123a49c0f0f616d766f9cb3aa4c"), - new TesseractLanguage("nld.traineddata.gz", "nld", "Dutch", 6.83, "7a19402e128c97ffb5044780c055344e4b92cceb"), - new TesseractLanguage("nor.traineddata.gz", "nor", "Norwegian", 3.14, "33fd288a93a5260954b0fca37894ce50d8872971"), - new TesseractLanguage("ori.traineddata.gz", "ori", "Oriya", 3.06, "cc4951bf162f3e06f83a7f63868dc0ba2a86c83c"), -// new TesseractLanguage { Filename = "osd.traineddata.gz", Code = "osd", LangName = "", Size = 4.08, Sha1 = "d8c10c1fca9b954ca2500e6abeee94b50329f486" }, - new TesseractLanguage("pan.traineddata.gz", "pan", "Panjabi", 4.06, "ec846c1a93576f85878de4b06fa82241782cf2a4"), - new TesseractLanguage("pol.traineddata.gz", "pol", "Polish", 5.41, "55a31b8724722219ce80f0a75685f267ae221d3d"), - new TesseractLanguage("por.traineddata.gz", "por", "Portuguese", 5.06, "c486d3ba8ad2d7555f894352313f4c5cfb287dca"), - new TesseractLanguage("pus.traineddata.gz", "pus", "Pushto", 0.88, "c45f471412ae0a7b4ed92141c828963911fa5f15"), - new TesseractLanguage("ron.traineddata.gz", "ron", "Romanian", 2.99, "e21ef667ff7bb90904cf0d731ebe184854cde616"), - new TesseractLanguage("rus.traineddata.gz", "rus", "Russian", 6.05, "96d7897ddecc7f944b5c1751e9ff44416cc3ee21"), - new TesseractLanguage("san.traineddata.gz", "san", "Sanskrit", 9.52, "c324b96fc4f1dcd2295329081f18be98e1c71053"), - new TesseractLanguage("sin.traineddata.gz", "sin", "Sinhala", 2.60, "145f8b7da56fe12340d4a0ce3f0c1385e437398c"), - new TesseractLanguage("slk.traineddata.gz", "slk", "Slovakian", 3.45, "abe9737fb49c9284a10cbb87b9efa773234af5c3"), - new TesseractLanguage("slk_frak.traineddata.gz", "slk_frak", "Slovakian (Fraktur)", 0.28, "e12b4fd2b4d2739656ed28142ba5db081d49fce2"), - new TesseractLanguage("slv.traineddata.gz", "slv", "Slovenian", 2.47, "d94468d01fec2bbcb8be23e97ec5329ef58c541f"), - new TesseractLanguage("spa.traineddata.gz", "spa", "Spanish", 6.31, "89160dbb92dbb5bcd6c48237315f6aa892450ef1"), - new TesseractLanguage("spa_old.traineddata.gz", "spa_old", "Spanish (Old)", 6.57, "9d13656da6a91ca4717f9235340f0304c7f77110"), - new TesseractLanguage("sqi.traineddata.gz", "sqi", "Albanian", 2.40, "30957e11c55610634dfdd2704ff0d6036c2e4ca5"), - new TesseractLanguage("srp.traineddata.gz", "srp", "Serbian", 1.56, "5a7ef0c3c37d7f1891bde5a96b92b2fd3e48783a"), - new TesseractLanguage("srp_latn.traineddata.gz", "srp_latn", "Serbian (Latin)", 2.27, "2aa8ff0e22440d3aab1a59e47b416bcd7ab2e7ae"), - new TesseractLanguage("swa.traineddata.gz", "swa", "Swahili", 1.43, "6010b9255c1cd98c8bda39cd18904bf7782942e1"), - new TesseractLanguage("swe.traineddata.gz", "swe", "Swedish", 3.64, "1bd6fd11f36b3ca04342a521773179269c5410e3"), - new TesseractLanguage("syr.traineddata.gz", "syr", "Syriac", 1.06, "01aa53fd62897bcbfc053401405485d6f6aa9df9"), - new TesseractLanguage("tam.traineddata.gz", "tam", "Tamil", 1.99, "eaca5e8c91d7995894ff2dafc4b824f305d6fff0"), - new TesseractLanguage("tel.traineddata.gz", "tel", "Telugu", 16.81, "1f5b1e2f3d8a772b406e4a2b9d8ec38f1eec4cc6"), - new TesseractLanguage("tgk.traineddata.gz", "tgk", "Tajik", 0.40, "b839d70a88e1dc2a019d1b7e76b83e5dcb0df440"), - new TesseractLanguage("tgl.traineddata.gz", "tgl", "Tagalog", 1.56, "0bdbb9e5f763ebfeef8fc9cd0ba1913bd7309755"), - new TesseractLanguage("tha.traineddata.gz", "tha", "Thai", 5.61, "7a171182716c99c19c1cc9b934a70ef5bee7893a"), - new TesseractLanguage("tir.traineddata.gz", "tir", "Tigrinya", 0.60, "4292700b180a505c4a45666a13eac6e144b48615"), - new TesseractLanguage("tur.traineddata.gz", "tur", "Turkish", 5.61, "8d72dc5ec5f22073f6b3ae2f79534e36aa8f63e8"), - new TesseractLanguage("uig.traineddata.gz", "uig", "Uighur", 0.72, "d20262f24476229539b4b87efa9327428052b241"), - new TesseractLanguage("ukr.traineddata.gz", "ukr", "Ukrainian", 2.92, "0871744dfacfa446e212e5c7e671c790b5fdd2f0"), - new TesseractLanguage("urd.traineddata.gz", "urd", "Urdu", 1.83, "be2964ca83114ee04b3a258e71525b8a1a670c97", true), - new TesseractLanguage("uzb.traineddata.gz", "uzb", "Uzbek", 1.55, "8de3127c90628514d61c0ded9510d4b2728f4b69"), - new TesseractLanguage("uzb_cyrl.traineddata.gz", "uzb_cyrl", "Uzbek (Cyrillic)", 1.19, "e1190d147d6ce3770d768724c82e103b06c93061"), - new TesseractLanguage("vie.traineddata.gz", "vie", "Vietnamese", 2.27, "571e132cd3ed26f5c33943efe7aa17835d277a15"), - new TesseractLanguage("yid.traineddata.gz", "yid", "Yiddish", 1.60, "0dbb6e19b660b57283f954eb5183cc2f3677fdda"), - }); - - #endregion - - #region Tesseract 4.00Beta4 Language Data (auto-generated) - - public static readonly TesseractLanguageData V400B4 = new TesseractLanguageData(new[] + public static readonly TesseractLanguageData Latest = new(new[] { new TesseractLanguage("afr.traineddata.zip", "afr", "Afrikaans", 5.44, "4278120a18e3464194df302f55417afc35415af7"), new TesseractLanguage("amh.traineddata.zip", "amh", "Amharic", 5.55, "166219c79a3c92775ac8cc987fba91899dc63f7d"), diff --git a/NAPS2.Sdk/Ocr/TesseractLanguageManager.cs b/NAPS2.Sdk/Ocr/TesseractLanguageManager.cs new file mode 100644 index 000000000..1ecedaacf --- /dev/null +++ b/NAPS2.Sdk/Ocr/TesseractLanguageManager.cs @@ -0,0 +1,34 @@ +using NAPS2.Dependencies; + +namespace NAPS2.Ocr; + +public class TesseractLanguageManager +{ + private static readonly List Mirrors = new() + { + new(PlatformSupport.ModernWindows.Or(PlatformSupport.Linux), @"https://github.com/cyanfish/naps2-components/releases/download/tesseract-4.0.0b4/{0}"), + new(PlatformSupport.ModernWindows.Or(PlatformSupport.Linux), @"https://sourceforge.net/projects/naps2/files/components/tesseract-4.0.0b4/{0}/download") + }; + + public TesseractLanguageManager(string basePath) + { + LanguageData = TesseractLanguageData.Latest; + TesseractBasePath = Path.Combine(basePath, "tesseract-4.0.0b4"); + + LanguageComponents = LanguageData.Data.Select(x => + new MultiFileExternalComponent($"ocr-{x.Code}", TesseractBasePath, new[] { $"best/{x.Code}.traineddata", $"fast/{x.Code}.traineddata" }, + new DownloadInfo(x.Filename, Mirrors, x.Size, x.Sha1, DownloadFormat.Zip))); + } + + public virtual IEnumerable InstalledLanguages => + LanguageComponents.Where(x => x.IsInstalled).Select(x => LanguageData.LanguageMap[x.Id]); + + public virtual IEnumerable NotInstalledLanguages => + LanguageComponents.Where(x => !x.IsInstalled).Select(x => LanguageData.LanguageMap[x.Id]); + + public string TesseractBasePath { get; protected init; } + + public TesseractLanguageData LanguageData { get; protected init; } + + public IEnumerable LanguageComponents { get; protected init; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/TesseractBaseEngine.cs b/NAPS2.Sdk/Ocr/TesseractOcrEngine.cs similarity index 66% rename from NAPS2.Sdk/Ocr/TesseractBaseEngine.cs rename to NAPS2.Sdk/Ocr/TesseractOcrEngine.cs index a0142a4f1..c209ecef3 100644 --- a/NAPS2.Sdk/Ocr/TesseractBaseEngine.cs +++ b/NAPS2.Sdk/Ocr/TesseractOcrEngine.cs @@ -1,45 +1,39 @@ -using System.Threading; -using NAPS2.Dependencies; +using System.Threading; namespace NAPS2.Ocr; -public abstract class TesseractBaseEngine : IOcrEngine +public class TesseractOcrEngine : IOcrEngine { - private const int CHECK_INTERVAL = 500; + private readonly string _exePath; + private readonly string? _languageDataBasePath; - public bool CanProcess(string langCode) + public TesseractOcrEngine(string exePath, string? languageDataBasePath) { - if (string.IsNullOrEmpty(langCode) || !IsInstalled || !IsSupported) - { - return false; - } - // Support multiple specified languages (e.g. "eng+fra") - return langCode.Split('+').All(code => InstalledLanguages.Any(x => x.Code == code)); + _exePath = exePath; + _languageDataBasePath = languageDataBasePath; } - + public async Task ProcessImage(string imagePath, OcrParams ocrParams, CancellationToken cancelToken) { string tempHocrFilePath = Path.Combine(Paths.Temp, Path.GetRandomFileName()); - string tempHocrFilePathWithExt = tempHocrFilePath + TesseractHocrExtension; + string tempHocrFilePathWithExt = tempHocrFilePath + ".hocr"; try { - var runInfo = TesseractRunInfo(ocrParams); var startInfo = new ProcessStartInfo { - FileName = Path.Combine(TesseractBasePath, TesseractExePath), - Arguments = $"\"{imagePath}\" \"{tempHocrFilePath}\" -l {ocrParams.LanguageCode} {runInfo.Arguments} hocr", + FileName = _exePath, + Arguments = $"\"{imagePath}\" \"{tempHocrFilePath}\" -l {ocrParams.LanguageCode} hocr", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; - if (runInfo.PrefixPath != null) + if (_languageDataBasePath != null) { - startInfo.EnvironmentVariables["TESSDATA_PREFIX"] = Path.Combine(TesseractBasePath, runInfo.PrefixPath); - } - if (runInfo.DataPath != null) - { - var tessdata = new DirectoryInfo(Path.Combine(TesseractBasePath, runInfo.DataPath)); + string subfolder = ocrParams.Mode == OcrMode.Best ? "best" : "fast"; + string languageDataPath = Path.Combine(_languageDataBasePath, subfolder); + startInfo.EnvironmentVariables["TESSDATA_PREFIX"] = languageDataPath; + var tessdata = new DirectoryInfo(languageDataPath); EnsureHocrConfigExists(tessdata); } var tesseractProcess = Process.Start(startInfo); @@ -107,8 +101,10 @@ public abstract class TesseractBaseEngine : IOcrEngine var elements = hocrDocument.Descendants() .Where(x => x.Attributes("class").Any(y => y.Value == "ocrx_word")) .Select(x => new OcrResultElement(x.Value, GetBounds(x.Attribute("title")))); - var rtl = InstalledLanguages.Where(x => x.Code == ocrParams.LanguageCode).Select(x => x.RTL) - .FirstOrDefault(); + var rtl = false; + // TODO: Can we detect rtl from the hocr file? + // var rtl = _data.InstalledLanguages.Where(x => x.Code == ocrParams.LanguageCode).Select(x => x.RTL) + // .FirstOrDefault(); return new OcrResult(pageBounds, elements, rtl); } catch (Exception e) @@ -162,51 +158,32 @@ public abstract class TesseractBaseEngine : IOcrEngine } return bounds; } - - public bool CanInstall { get; protected set; } - - public IEnumerable SupportedModes { get; protected set; } - - public IExternalComponent Component { get; protected set; } - - public IEnumerable LanguageComponents { get; protected set; } - - public virtual bool IsSupported => PlatformSupport.Validate(); - - public virtual bool IsInstalled => Component.IsInstalled; - - public virtual IEnumerable InstalledLanguages => - LanguageComponents.Where(x => x.IsInstalled).Select(x => LanguageData.LanguageMap[x.Id]); - - public virtual IEnumerable NotInstalledLanguages => - LanguageComponents.Where(x => !x.IsInstalled).Select(x => LanguageData.LanguageMap[x.Id]); - - protected string TesseractBasePath { get; set; } - - protected string TesseractExePath { get; set; } - - protected string TesseractHocrExtension { get; set; } = ".hocr"; - - protected DownloadInfo DownloadInfo { get; set; } - - protected PlatformSupport PlatformSupport { get; set; } - - protected TesseractLanguageData LanguageData { get; set; } - - protected virtual RunInfo TesseractRunInfo(OcrParams ocrParams) => new RunInfo - { - Arguments = "", - DataPath = "tessdata", - PrefixPath = "" - }; - - - protected class RunInfo - { - public string Arguments { get; set; } - - public string? PrefixPath { get; set; } - - public string? DataPath { get; set; } - } + + + // TODO: For local engine (where we need to manually direct to the language data) + // public override TesseractRunInfo TesseractRunInfo(OcrParams ocrParams) + // { + // OcrMode mode = ocrParams.Mode; + // string folder = mode == OcrMode.Fast || mode == OcrMode.Default ? "fast" : "best"; + // if (ocrParams.LanguageCode.Split('+').All(code => !File.Exists(Path.Combine(TesseractBasePath, folder, $"{code.ToLowerInvariant()}.traineddata")))) + // { + // // Use the other source if the selected one doesn't exist + // folder = folder == "fast" ? "best" : "fast"; + // mode = folder == "fast" ? OcrMode.Fast : OcrMode.Best; + // } + // + // return new() + // { + // Arguments = mode == OcrMode.Best ? "--oem 1" : mode == OcrMode.Legacy ? "--oem 0" : "", + // DataPath = folder, + // PrefixPath = folder + // }; + + // TODO: For system engine (where the language data is externally managed) + // public override TesseractRunInfo TesseractRunInfo(OcrParams ocrParams) => new() + // { + // Arguments = "", + // DataPath = null, + // PrefixPath = null + // }; } \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/TesseractRunInfo.cs b/NAPS2.Sdk/Ocr/TesseractRunInfo.cs new file mode 100644 index 000000000..a2fcba346 --- /dev/null +++ b/NAPS2.Sdk/Ocr/TesseractRunInfo.cs @@ -0,0 +1,10 @@ +namespace NAPS2.Ocr; + +public class TesseractRunInfo +{ + public string Arguments { get; set; } + + public string? PrefixPath { get; set; } + + public string? DataPath { get; set; } +} \ No newline at end of file diff --git a/NAPS2.Sdk/Ocr/TesseractSystemEngine.cs b/NAPS2.Sdk/Ocr/TesseractSystemEngine.cs index 297e32717..f3e1af4dc 100644 --- a/NAPS2.Sdk/Ocr/TesseractSystemEngine.cs +++ b/NAPS2.Sdk/Ocr/TesseractSystemEngine.cs @@ -1,77 +1,77 @@ -using NAPS2.Dependencies; - -namespace NAPS2.Ocr; - -public class TesseractSystemEngine : TesseractBaseEngine -{ - private bool _isInstalled; - private DateTime? _installCheckTime; - private List? _installedLanguages; - - public TesseractSystemEngine() - { - // Use the most complete set of language mappings - LanguageData = TesseractLanguageData.V400B4; - TesseractBasePath = ""; - TesseractExePath = "tesseract"; - PlatformSupport = PlatformSupport.Linux; - CanInstall = false; - } - - protected override RunInfo TesseractRunInfo(OcrParams ocrParams) => new RunInfo - { - Arguments = "", - DataPath = null, - PrefixPath = null - }; - - public override bool IsInstalled - { - get - { - CheckIfInstalled(); - return _isInstalled; - } - } - - public override IEnumerable InstalledLanguages - { - get - { - CheckIfInstalled(); - return _installedLanguages ?? Enumerable.Empty(); - } - } - - public override IEnumerable NotInstalledLanguages => Enumerable.Empty(); - - private void CheckIfInstalled() - { - if (IsSupported && (_installCheckTime == null || _installCheckTime < DateTime.Now - TimeSpan.FromSeconds(2))) - { - try - { - var process = Process.Start(new ProcessStartInfo - { - FileName = TesseractExePath, - Arguments = "--list-langs", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }); - if (process != null && process.Id != 0) - { - var codes = process.StandardError.ReadToEnd().Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries).Where(x => x.Length == 3); - _installedLanguages = codes.Select(code => LanguageData.LanguageMap.Get($"ocr-{code}")).WhereNotNull().ToList(); - _isInstalled = true; - process.Kill(); - } - } - catch (Exception) - { - // Component is not installed on the system path (or had an error) - } - _installCheckTime = DateTime.Now; - } - } -} \ No newline at end of file +// using NAPS2.Dependencies; +// +// namespace NAPS2.Ocr; +// +// public class TesseractSystemData : TesseractOcrEngineData +// { +// private bool _isInstalled; +// private DateTime? _installCheckTime; +// private List? _installedLanguages; +// +// public TesseractSystemData() +// { +// // Use the most complete set of language mappings +// LanguageData = TesseractLanguageData.V400B4; +// TesseractBasePath = ""; +// TesseractExePath = "tesseract"; +// PlatformSupport = PlatformSupport.Linux; +// CanInstall = false; +// } +// +// public override TesseractRunInfo TesseractRunInfo(OcrParams ocrParams) => new() +// { +// Arguments = "", +// DataPath = null, +// PrefixPath = null +// }; +// +// public override bool IsInstalled +// { +// get +// { +// CheckIfInstalled(); +// return _isInstalled; +// } +// } +// +// public override IEnumerable InstalledLanguages +// { +// get +// { +// CheckIfInstalled(); +// return _installedLanguages ?? Enumerable.Empty(); +// } +// } +// +// public override IEnumerable NotInstalledLanguages => Enumerable.Empty(); +// +// private void CheckIfInstalled() +// { +// if (IsSupported && (_installCheckTime == null || _installCheckTime < DateTime.Now - TimeSpan.FromSeconds(2))) +// { +// try +// { +// var process = Process.Start(new ProcessStartInfo +// { +// FileName = TesseractExePath, +// Arguments = "--list-langs", +// UseShellExecute = false, +// RedirectStandardOutput = true, +// RedirectStandardError = true +// }); +// if (process != null && process.Id != 0) +// { +// var codes = process.StandardError.ReadToEnd().Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries).Where(x => x.Length == 3); +// _installedLanguages = codes.Select(code => LanguageData.LanguageMap.Get($"ocr-{code}")).WhereNotNull().ToList(); +// _isInstalled = true; +// process.Kill(); +// } +// } +// catch (Exception) +// { +// // Component is not installed on the system path (or had an error) +// } +// _installCheckTime = DateTime.Now; +// } +// } +// } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Batch/BatchScanPerformer.cs b/NAPS2.Sdk/Scan/Batch/BatchScanPerformer.cs index d8411e18e..3869c5f58 100644 --- a/NAPS2.Sdk/Scan/Batch/BatchScanPerformer.cs +++ b/NAPS2.Sdk/Scan/Batch/BatchScanPerformer.cs @@ -15,18 +15,16 @@ public class BatchScanPerformer : IBatchScanPerformer private readonly PdfExporter _pdfExporter; private readonly IOperationFactory _operationFactory; private readonly IConfigProvider _pdfSettingsProvider; - private readonly OcrEngineManager _ocrEngineManager; private readonly IFormFactory _formFactory; private readonly ScopedConfig _config; private readonly IProfileManager _profileManager; - public BatchScanPerformer(IScanPerformer scanPerformer, PdfExporter pdfExporter, IOperationFactory operationFactory, IConfigProvider pdfSettingsProvider, OcrEngineManager ocrEngineManager, IFormFactory formFactory, ScopedConfig config, IProfileManager profileManager) + public BatchScanPerformer(IScanPerformer scanPerformer, PdfExporter pdfExporter, IOperationFactory operationFactory, IConfigProvider pdfSettingsProvider, IFormFactory formFactory, ScopedConfig config, IProfileManager profileManager) { _scanPerformer = scanPerformer; _pdfExporter = pdfExporter; _operationFactory = operationFactory; _pdfSettingsProvider = pdfSettingsProvider; - _ocrEngineManager = ocrEngineManager; _formFactory = formFactory; _config = config; _profileManager = profileManager; @@ -34,7 +32,7 @@ public class BatchScanPerformer : IBatchScanPerformer public async Task PerformBatchScan(IConfigProvider settings, FormBase batchForm, Action imageCallback, Action progressCallback, CancellationToken cancelToken) { - var state = new BatchState(_scanPerformer, _pdfExporter, _operationFactory, _pdfSettingsProvider, _ocrEngineManager, _formFactory, _config, _profileManager) + var state = new BatchState(_scanPerformer, _pdfExporter, _operationFactory, _pdfSettingsProvider, _formFactory, _config, _profileManager) { Settings = settings, ProgressCallback = progressCallback, @@ -51,7 +49,6 @@ public class BatchScanPerformer : IBatchScanPerformer private readonly PdfExporter _pdfExporter; private readonly IOperationFactory _operationFactory; private readonly IConfigProvider _pdfSettingsProvider; - private readonly OcrEngineManager _ocrEngineManager; private readonly IFormFactory _formFactory; private readonly ScopedConfig _config; private readonly IProfileManager _profileManager; @@ -61,13 +58,12 @@ public class BatchScanPerformer : IBatchScanPerformer private List> _scans; public BatchState(IScanPerformer scanPerformer, PdfExporter pdfExporter, IOperationFactory operationFactory, - IConfigProvider pdfSettingsProvider, OcrEngineManager ocrEngineManager, IFormFactory formFactory, ScopedConfig config, IProfileManager profileManager) + IConfigProvider pdfSettingsProvider, IFormFactory formFactory, ScopedConfig config, IProfileManager profileManager) { _scanPerformer = scanPerformer; _pdfExporter = pdfExporter; _operationFactory = operationFactory; _pdfSettingsProvider = pdfSettingsProvider; - _ocrEngineManager = ocrEngineManager; _formFactory = formFactory; _config = config; _profileManager = profileManager; diff --git a/NAPS2.Sdk/Scan/ScanPerformer.cs b/NAPS2.Sdk/Scan/ScanPerformer.cs index 99cb26adc..a6ff25724 100644 --- a/NAPS2.Sdk/Scan/ScanPerformer.cs +++ b/NAPS2.Sdk/Scan/ScanPerformer.cs @@ -20,12 +20,12 @@ internal class ScanPerformer : IScanPerformer private readonly ErrorOutput _errorOutput; private readonly ScanOptionsValidator _scanOptionsValidator; private readonly IScanBridgeFactory _scanBridgeFactory; - private readonly OcrEngineManager _ocrEngineManager; + private readonly IOcrEngine _ocrEngine; public ScanPerformer(IFormFactory formFactory, ScopedConfig config, OperationProgress operationProgress, AutoSaver autoSaver, IProfileManager profileManager, ErrorOutput errorOutput, ScanOptionsValidator scanOptionsValidator, IScanBridgeFactory scanBridgeFactory, - ScanningContext scanningContext, OcrEngineManager ocrEngineManager) + ScanningContext scanningContext, IOcrEngine ocrEngine) { _formFactory = formFactory; _config = config; @@ -36,7 +36,7 @@ internal class ScanPerformer : IScanPerformer _scanOptionsValidator = scanOptionsValidator; _scanBridgeFactory = scanBridgeFactory; _scanningContext = scanningContext; - _ocrEngineManager = ocrEngineManager; + _ocrEngine = ocrEngine; } public async Task PromptForDevice(ScanProfile scanProfile, IntPtr dialogParent = default) @@ -101,7 +101,7 @@ internal class ScanPerformer : IScanPerformer OcrController ocrController = new OcrController(_scanningContext); if (scanParams.DoOcr) { - ocrController.Engine = _ocrEngineManager.ActiveEngine; + ocrController.Engine = _ocrEngine; if (ocrController.Engine == null) { Log.Error("OCR is enabled but no OCR engine is available."); diff --git a/NAPS2.Sdk/WinForms/FDesktop.cs b/NAPS2.Sdk/WinForms/FDesktop.cs index b0e5ca88d..72902b272 100644 --- a/NAPS2.Sdk/WinForms/FDesktop.cs +++ b/NAPS2.Sdk/WinForms/FDesktop.cs @@ -34,7 +34,7 @@ namespace NAPS2.WinForms private readonly ImageContext _imageContext; private readonly StringWrapper _stringWrapper; private readonly RecoveryManager _recoveryManager; - private readonly OcrEngineManager _ocrEngineManager; + private readonly TesseractLanguageManager _tesseractLanguageManager; private readonly IScanPerformer _scanPerformer; private readonly IScannedImagePrinter _scannedImagePrinter; private readonly StillImage _stillImage; @@ -70,7 +70,7 @@ namespace NAPS2.WinForms #region Initialization and Culture - public FDesktop(ImageContext imageContext, StringWrapper stringWrapper, RecoveryManager recoveryManager, OcrEngineManager ocrEngineManager, + public FDesktop(ImageContext imageContext, StringWrapper stringWrapper, RecoveryManager recoveryManager, TesseractLanguageManager tesseractLanguageManager, IScanPerformer scanPerformer, IScannedImagePrinter scannedImagePrinter, StillImage stillImage, IOperationFactory operationFactory, KeyboardShortcutManager ksm, ThumbnailRenderer thumbnailRenderer, WinFormsExportHelper exportHelper, ImageClipboard imageClipboard, NotificationManager notify, CultureInitializer cultureInitializer, IWorkerFactory workerFactory, OperationProgress operationProgress, @@ -80,7 +80,7 @@ namespace NAPS2.WinForms _imageContext = imageContext; _stringWrapper = stringWrapper; _recoveryManager = recoveryManager; - _ocrEngineManager = ocrEngineManager; + _tesseractLanguageManager = tesseractLanguageManager; _scanPerformer = scanPerformer; _scannedImagePrinter = scannedImagePrinter; _stillImage = stillImage; @@ -1165,33 +1165,14 @@ namespace NAPS2.WinForms private void tsOcr_Click(object sender, EventArgs e) { - if (_ocrEngineManager.MustUpgrade && !Config.Get(c => c.NoUpdatePrompt)) + if (_tesseractLanguageManager.InstalledLanguages.Any()) { - // Re-download a fixed version on Windows XP if needed - MessageBox.Show(MiscResources.OcrUpdateAvailable, "", MessageBoxButtons.OK, MessageBoxIcon.Information); - var progressForm = FormFactory.Create(); - progressForm.QueueFile(_ocrEngineManager.EngineToInstall.Component); - progressForm.ShowDialog(); - } - - if (_ocrEngineManager.MustInstallPackage) - { - const string packages = "\ntesseract-ocr"; - MessageBox.Show(MiscResources.TesseractNotAvailable + packages, MiscResources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); - } - else if (_ocrEngineManager.IsReady) - { - if (_ocrEngineManager.CanUpgrade && !Config.Get(c => c.NoUpdatePrompt)) - { - MessageBox.Show(MiscResources.OcrUpdateAvailable, "", MessageBoxButtons.OK, MessageBoxIcon.Information); - FormFactory.Create().ShowDialog(); - } FormFactory.Create().ShowDialog(); } else { FormFactory.Create().ShowDialog(); - if (_ocrEngineManager.IsReady) + if (_tesseractLanguageManager.InstalledLanguages.Any()) { FormFactory.Create().ShowDialog(); } diff --git a/NAPS2.Sdk/WinForms/FOcrLanguageDownload.cs b/NAPS2.Sdk/WinForms/FOcrLanguageDownload.cs index c3f763b48..ea3b5e551 100644 --- a/NAPS2.Sdk/WinForms/FOcrLanguageDownload.cs +++ b/NAPS2.Sdk/WinForms/FOcrLanguageDownload.cs @@ -5,26 +5,16 @@ namespace NAPS2.WinForms { public partial class FOcrLanguageDownload : FormBase { - private readonly OcrEngineManager _ocrEngineManager; - private readonly IOcrEngine _engineToInstall; + private readonly TesseractLanguageManager _tesseractLanguageManager; - public FOcrLanguageDownload(OcrEngineManager ocrEngineManager) + public FOcrLanguageDownload(TesseractLanguageManager tesseractLanguageManager) { - _ocrEngineManager = ocrEngineManager; - _engineToInstall = ocrEngineManager.EngineToInstall; + _tesseractLanguageManager = tesseractLanguageManager; InitializeComponent(); var initialSelection = new HashSet(); - if (ocrEngineManager.InstalledEngine != null && ocrEngineManager.InstalledEngine != _engineToInstall) - { - // Upgrading from an old version, so pre-select previously used languages - foreach (var lang in ocrEngineManager.InstalledEngine.LanguageComponents.Where(x => x.IsInstalled)) - { - initialSelection.Add(lang.Id); - } - } - - if (!_engineToInstall.InstalledLanguages.Any()) + // TODO: We used to select old installed languages here, maybe we could do it again if we get new lang data + if (!_tesseractLanguageManager.InstalledLanguages.Any()) { // Fresh install, so pre-select English as a sensible default initialSelection.Add("ocr-eng"); @@ -32,7 +22,7 @@ namespace NAPS2.WinForms // Populate the list of language options // Special case for English: sorted to the top of the list - var languageOptions = _engineToInstall.NotInstalledLanguages + var languageOptions = _tesseractLanguageManager.NotInstalledLanguages .OrderBy(x => x.Code == "eng" ? "AAA" : x.Name); foreach (var languageOption in languageOptions) { @@ -63,16 +53,11 @@ namespace NAPS2.WinForms private void UpdateView() { var selectedLanguages = SelectedLanguages; - double downloadSize = _engineToInstall.LanguageComponents.Where(x => selectedLanguages.Contains(x.Id)).Select(x => x.DownloadInfo.Size).Sum(); - - if (!_engineToInstall.IsInstalled) - { - downloadSize += _engineToInstall.Component.DownloadInfo.Size; - } + double downloadSize = _tesseractLanguageManager.LanguageComponents.Where(x => selectedLanguages.Contains(x.Id)).Select(x => x.DownloadInfo.Size).Sum(); labelSizeEstimate.Text = string.Format(MiscResources.EstimatedDownloadSize, downloadSize.ToString("f1")); - btnDownload.Enabled = lvLanguages.Items.Cast().Any(x => x.Checked) || _engineToInstall.InstalledLanguages.Any() && !_engineToInstall.IsInstalled; + btnDownload.Enabled = lvLanguages.Items.Cast().Any(x => x.Checked); } private HashSet SelectedLanguages @@ -94,13 +79,8 @@ namespace NAPS2.WinForms { var progressForm = FormFactory.Create(); - if (!_engineToInstall.IsInstalled) - { - progressForm.QueueFile(_engineToInstall.Component); - } - var selectedLanguages = SelectedLanguages; - foreach (var langComponent in _engineToInstall.LanguageComponents.Where(x => selectedLanguages.Contains(x.Id))) + foreach (var langComponent in _tesseractLanguageManager.LanguageComponents.Where(x => selectedLanguages.Contains(x.Id))) { progressForm.QueueFile(langComponent); } diff --git a/NAPS2.Sdk/WinForms/FOcrSetup.cs b/NAPS2.Sdk/WinForms/FOcrSetup.cs index 77b5ec8e9..261c1fb9a 100644 --- a/NAPS2.Sdk/WinForms/FOcrSetup.cs +++ b/NAPS2.Sdk/WinForms/FOcrSetup.cs @@ -6,22 +6,18 @@ namespace NAPS2.WinForms; public partial class FOcrSetup : FormBase { - private readonly OcrEngineManager _ocrEngineManager; - private readonly List _availableModes; + private readonly TesseractLanguageManager _tesseractLanguageManager; + private readonly List _availableModes = new() { OcrMode.Fast, OcrMode.Best }; - public FOcrSetup(OcrEngineManager ocrEngineManager) + public FOcrSetup(TesseractLanguageManager tesseractLanguageManager) { - _ocrEngineManager = ocrEngineManager; + _tesseractLanguageManager = tesseractLanguageManager; InitializeComponent(); comboOcrMode.Format += (sender, e) => e.Value = ((Enum)e.ListItem).Description(); - _availableModes = ocrEngineManager.ActiveEngine?.SupportedModes?.ToList(); - if (_availableModes != null) + foreach (var mode in _availableModes) { - foreach (var mode in _availableModes) - { - comboOcrMode.Items.Add(mode); - } + comboOcrMode.Items.Add(mode); } } @@ -38,11 +34,6 @@ public partial class FOcrSetup : FormBase comboLanguages.DisplayMember = "Name"; comboLanguages.ValueMember = "Code"; - ConditionalControls.UnlockHeight(this); - ConditionalControls.SetVisible(comboOcrMode, _availableModes != null, 8); - labelOcrMode.Visible = _availableModes != null; - ConditionalControls.LockHeight(this); - checkBoxEnableOcr.Checked = Config.Get(c => c.EnableOcr); SetSelectedValue(comboLanguages, Config.Get(c => c.OcrLanguageCode)); SetSelectedItem(comboOcrMode, Config.Get(c => c.OcrMode)); @@ -73,12 +64,10 @@ public partial class FOcrSetup : FormBase private void LoadLanguages() { - var languages = _ocrEngineManager.ActiveEngine?.InstalledLanguages + var languages = _tesseractLanguageManager.InstalledLanguages .OrderBy(x => x.Name) .ToList(); comboLanguages.DataSource = languages; - - linkGetLanguages.Visible = _ocrEngineManager.EngineToInstall != null; } private void UpdateView() diff --git a/NAPS2.sln.DotSettings b/NAPS2.sln.DotSettings index b70d9c6cb..1758aca46 100644 --- a/NAPS2.sln.DotSettings +++ b/NAPS2.sln.DotSettings @@ -17,4 +17,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file