From 0bd2a9bf0bb0145230a5767b4f42e52ffe34c70f Mon Sep 17 00:00:00 2001 From: matikkaeditorinkaantaja Date: Mon, 19 Feb 2024 14:30:00 +0200 Subject: [PATCH 1/3] Fix #38 improve error handling - Add new Tesseract Api methods - Make error handling more clear - Add new Exception types to separate them from other exceptions --- TesseractOcrMaui.IOS/TesseractApi.cs | 21 ++++- .../TesseractOcrMaui.IOS.csproj | 2 +- TesseractOcrMaui/Enums/RecognizionStatus.cs | 3 +- .../Exceptions/PageNotDisposedException.cs | 36 +++++++ .../Exceptions/StringMarshallingException.cs | 29 ++++++ TesseractOcrMaui/ImportApis/TesseractApi.cs | 14 ++- TesseractOcrMaui/Results/RecognizionResult.cs | 5 + TesseractOcrMaui/TessEngine.cs | 17 ++-- TesseractOcrMaui/TessPage.cs | 43 ++++----- TesseractOcrMaui/Tesseract.cs | 93 +++++++------------ TesseractOcrMaui/TesseractOcrMaui.csproj | 2 +- 11 files changed, 162 insertions(+), 103 deletions(-) create mode 100644 TesseractOcrMaui/Exceptions/PageNotDisposedException.cs create mode 100644 TesseractOcrMaui/Exceptions/StringMarshallingException.cs diff --git a/TesseractOcrMaui.IOS/TesseractApi.cs b/TesseractOcrMaui.IOS/TesseractApi.cs index a2f0e9b..3edcf1e 100644 --- a/TesseractOcrMaui.IOS/TesseractApi.cs +++ b/TesseractOcrMaui.IOS/TesseractApi.cs @@ -11,6 +11,18 @@ public sealed partial class TesseractApi const CharSet StrEncoding = CharSet.Ansi; + [LibraryImport(DllName, EntryPoint = "TessDeleteTextArray")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static unsafe partial void DeleteStringArray(char** ptr); + + [LibraryImport(DllName, EntryPoint = "TessDeleteIntArray")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial void DeleteIntArray(IntPtr ptr); + + [LibraryImport(DllName, EntryPoint = "TessDeleteText")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial void DeleteString(IntPtr ptr); + [LibraryImport(DllName, EntryPoint = "TessBaseAPICreate")] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] public static partial IntPtr CreateApi(); @@ -36,9 +48,14 @@ public extern static int BaseApi5Init(HandleRef handle, string datapath, int dat [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPISetImage2")] public static extern void SetImage(HandleRef handle, HandleRef pixHandle); - + + // This does not work with non acsii characters, use GetUTF8Text_Ptr instead [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPIGetUTF8Text", CharSet = StrEncoding)] - public static extern string GetUTF8Text(HandleRef handle); + public static extern string GetUTF8Text(HandleRef handle); + + // Remember to delete string after copying, use DeleteString(IntPtr ptr) + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPIGetUTF8Text")] + public static extern IntPtr GetUTF8Text_Ptr(HandleRef handle); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPIAllWordConfidences")] public static extern int[] GetConfidences(HandleRef handle); diff --git a/TesseractOcrMaui.IOS/TesseractOcrMaui.IOS.csproj b/TesseractOcrMaui.IOS/TesseractOcrMaui.IOS.csproj index 58cca06..666c7ab 100644 --- a/TesseractOcrMaui.IOS/TesseractOcrMaui.IOS.csproj +++ b/TesseractOcrMaui.IOS/TesseractOcrMaui.IOS.csproj @@ -50,7 +50,7 @@ - 1.0.5 + 1.0.6 Maui Tesseract ocr iOS bindings TesseractOcrMaui.IOS henrivain diff --git a/TesseractOcrMaui/Enums/RecognizionStatus.cs b/TesseractOcrMaui/Enums/RecognizionStatus.cs index 5e7e2f7..733843d 100644 --- a/TesseractOcrMaui/Enums/RecognizionStatus.cs +++ b/TesseractOcrMaui/Enums/RecognizionStatus.cs @@ -19,5 +19,6 @@ public enum RecognizionStatus InvalidImage, ImageAlredyProcessed, CannotRecognizeText, - TessDataFolderNotProvided + TessDataFolderNotProvided, + InvalidResultString } diff --git a/TesseractOcrMaui/Exceptions/PageNotDisposedException.cs b/TesseractOcrMaui/Exceptions/PageNotDisposedException.cs new file mode 100644 index 0000000..b0acbc7 --- /dev/null +++ b/TesseractOcrMaui/Exceptions/PageNotDisposedException.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TesseractOcrMaui.Exceptions; + +/// +/// Exception thrown when multiple images are given to TessEngine at the same time. +/// Old TessPage must be disposed before trying to process new image. +/// +public class PageNotDisposedException : TesseractException +{ + /// + /// New exception thrown when multiple images are given to TessEngine at the same time. + /// Old TessPage must be disposed before trying to process new image. + /// + public PageNotDisposedException() { } + + /// + /// New exception with message thrown when multiple images are given to TessEngine at the same time. + /// Old TessPage must be disposed before trying to process new image. + /// + /// Error reason + public PageNotDisposedException(string message) : base(message) { } + + /// + /// New exception with message and inner exception thrown when multiple images are given to TessEngine at the same time. + /// Old TessPage must be disposed before trying to process new image. + /// + /// Error reason + /// Exception that caused this exception + public PageNotDisposedException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/TesseractOcrMaui/Exceptions/StringMarshallingException.cs b/TesseractOcrMaui/Exceptions/StringMarshallingException.cs new file mode 100644 index 0000000..c3af90c --- /dev/null +++ b/TesseractOcrMaui/Exceptions/StringMarshallingException.cs @@ -0,0 +1,29 @@ +namespace TesseractOcrMaui.Exceptions; + +/// +/// Exception thrown when library cannot marshal string returned from native library correctly. +/// +public class StringMarshallingException : TesseractException +{ + /// + /// New Exception thrown when library cannot marshal string returned from native library correctly. + /// + public StringMarshallingException() : base() { } + + /// + /// New Exception with message thrown when library cannot marshal string returned from native library correctly. + /// + /// Error reason. + public StringMarshallingException(string? message) : base(message) { } + + /// + /// New Exception with message and inner exception. + /// Thrown when library cannot marshal string returned from native library correctly. + /// + /// Error reason. + /// Exception that caused this error. + public StringMarshallingException(string? message, Exception? innerException) : base(message, innerException) + { + } + +} diff --git a/TesseractOcrMaui/ImportApis/TesseractApi.cs b/TesseractOcrMaui/ImportApis/TesseractApi.cs index 3a64427..7f2e301 100644 --- a/TesseractOcrMaui/ImportApis/TesseractApi.cs +++ b/TesseractOcrMaui/ImportApis/TesseractApi.cs @@ -19,9 +19,14 @@ internal sealed partial class TesseractApi const string DllName = "Use Windows, Android or iOS Platform"; #endif - const CharSet StrEncoding = CharSet.Ansi; + + + [LibraryImport(DllName, EntryPoint = "TessDeleteText")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + public static partial void DeleteString(IntPtr ptr); + [LibraryImport(DllName, EntryPoint = "TessBaseAPICreate")] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] public static partial IntPtr CreateApi(); @@ -47,10 +52,15 @@ public extern static int BaseApi5Init(HandleRef handle, string datapath, int dat [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPISetImage2")] public static extern void SetImage(HandleRef handle, HandleRef pixHandle); - + + // This does not work with non acsii characters, use GetUTF8Text_Ptr instead [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPIGetUTF8Text", CharSet = StrEncoding)] public static extern string GetUTF8Text(HandleRef handle); + // Remember to delete string after copying, use DeleteString(IntPtr ptr) + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPIGetUTF8Text")] + public static extern IntPtr GetUTF8Text_Ptr(HandleRef handle); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "TessBaseAPIAllWordConfidences")] public static extern int[] GetConfidences(HandleRef handle); diff --git a/TesseractOcrMaui/Results/RecognizionResult.cs b/TesseractOcrMaui/Results/RecognizionResult.cs index 006c6ae..6dc3cd1 100644 --- a/TesseractOcrMaui/Results/RecognizionResult.cs +++ b/TesseractOcrMaui/Results/RecognizionResult.cs @@ -20,6 +20,11 @@ public RecognizionResult() { } /// public string? Message { get; init; } + /// + /// Optional exception that was thrown if failed. + /// + public Exception? Exception { get; init; } + /// /// Recognized text from the image. /// diff --git a/TesseractOcrMaui/TessEngine.cs b/TesseractOcrMaui/TessEngine.cs index 04f0351..7daf669 100644 --- a/TesseractOcrMaui/TessEngine.cs +++ b/TesseractOcrMaui/TessEngine.cs @@ -98,11 +98,9 @@ public TessEngine(string languages, string traineddataPath, EngineMode mode, /// New Tess page containing information for recognizion. /// image is null. /// Image width or height has invalid values. - /// Image already processed. You must dispose page after using. - public TessPage ProcessImage(Pix image, PageSegmentationMode? mode = null) - { - return ProcessImage(image, null, new Rect(0, 0, image.Width, image.Height), mode); - } + /// Image already processed. You must dispose page after using. + public TessPage ProcessImage(Pix image, PageSegmentationMode? mode = null) + => ProcessImage(image, null, new Rect(0, 0, image.Width, image.Height), mode); /// /// Process image to TessPage. @@ -114,7 +112,7 @@ public TessPage ProcessImage(Pix image, PageSegmentationMode? mode = null) /// New Tess page containing information for recognizion. /// image is null. /// Region is out of bounds. - /// Image already processed. You must dispose page after using. + /// Image already processed. You must dispose page after using. public TessPage ProcessImage(Pix image, string? inputName, Rect region, PageSegmentationMode? mode) { if (image is null) @@ -130,10 +128,9 @@ public TessPage ProcessImage(Pix image, string? inputName, Rect region, PageSegm } if (_processCount > 0) { - _logger.LogError("{cls}: Tried to process image with engine that already has one. " + - "You must dispose image after using.", nameof(TessEngine)); - throw new InvalidOperationException("One image already set. " + - "You must dispose page after using."); + _logger.LogError("{cls}: Already has one image process. You must dispose {page} after using it.", + nameof(TessEngine), nameof(TessPage)); + throw new PageNotDisposedException("You must dispose old TessPage after using it."); } _processCount++; diff --git a/TesseractOcrMaui/TessPage.cs b/TesseractOcrMaui/TessPage.cs index debd1fb..97cfcc5 100644 --- a/TesseractOcrMaui/TessPage.cs +++ b/TesseractOcrMaui/TessPage.cs @@ -75,44 +75,39 @@ public TessPage(TessEngine engine, Pix image, string? inputName, Rect region, Pa /// /// Get text from image. Runs recognizion if it is not already done. Uses UTF-8. /// - /// Text that apprears in image. + /// Recognized text as UTF-8 string /// PageSegmentationMode is OsdOnly when recognizing. /// Native Library call returns failed status when recognizing. /// Can't get thresholded image when recognizing. - /// [WINDOWS] Invalid byte sequence in string. + /// + /// When recognizion result string pointer is nullpointer or the pointer cannot + /// be marshalled into UTF-8 string. + /// public string GetText() { Logger.LogInformation("Try to get text from image."); Recognize(); - string result = TesseractApi.GetUTF8Text(Engine.Handle); - - Logger.LogInformation("Found '{count}' characters in image.", result.Length); - - // My Windows seems to use different encoding than UTF-8 by default, so this should help. - // Android uses UTF-8 as default so all good. -#if WINDOWS - var bytes = new byte[result.Length]; - for (int i = 0; i < result.Length; i++) - { - bytes[i] = (byte)result[i]; - } - if (bytes is null) - { - return string.Empty; - } - try + IntPtr ptr = TesseractApi.GetUTF8Text_Ptr(Engine.Handle); + if (ptr == IntPtr.Zero) { - return Encoding.UTF8.GetString(bytes); + Logger.LogError("Recognizion result string cannot be marshalled from null pointer."); + throw new StringMarshallingException("String cannot be marshalled from null pointer."); } - catch (Exception ex) + + string? result = Marshal.PtrToStringUTF8(ptr); + TesseractApi.DeleteString(ptr); + + if (result is null) { - throw new InvalidBytesException("Cannot encode current byte array, because it contains invalid bytes.", ex); + Logger.LogError("Cannot encode char* to UTF-8 string."); + throw new StringMarshallingException("Could not encode recognizion result string to UTF-8."); } -#else + + Logger.LogInformation("Found '{count}' characters in image.", result.Length); + return result; -#endif } /// diff --git a/TesseractOcrMaui/Tesseract.cs b/TesseractOcrMaui/Tesseract.cs index 8fb820e..b4e6d6b 100644 --- a/TesseractOcrMaui/Tesseract.cs +++ b/TesseractOcrMaui/Tesseract.cs @@ -1,4 +1,5 @@ using System.Runtime.Versioning; +using TesseractOcrMaui.Exceptions; using TesseractOcrMaui.Results; using TesseractOcrMaui.Tessdata; @@ -194,7 +195,7 @@ public async Task RecognizeTextAsync(byte[] imageBytes) Message = "Invalid image, cannot be loaded", }; } - catch (KnownIssueException ex) + catch (KnownIssueException ex) { _logger.LogWarning("Cannot load pix from memory. '{ex}'", ex); return new RecognizionResult @@ -222,12 +223,12 @@ public async Task RecognizeTextAsync(Pix image) internal RecognizionResult Recognize(Pix pix, string tessDataFolder, string[] traineddataFileNames) { - var (status, languages) = TrainedDataToLanguage(tessDataFolder, traineddataFileNames); - if (status.NotSuccess()) + var (traineddataStatus, languages) = TraineddataToLanguage(tessDataFolder, traineddataFileNames); + if (traineddataStatus.NotSuccess()) { return new RecognizionResult { - Status = status, + Status = traineddataStatus, Message = "Failed to load traineddata files. See status to find reason." }; } @@ -259,7 +260,7 @@ internal RecognizionResult Recognize(Pix pix, string tessDataFolder, string[] tr try { // nulls are alredy checked, can't throw. - using var engine = new TessEngine(languages, tessDataFolder, EngineMode, + using var engine = new TessEngine(languages, tessDataFolder, EngineMode, new Dictionary(), _logger); if (EngineConfiguration is not null) @@ -275,71 +276,39 @@ internal RecognizionResult Recognize(Pix pix, string tessDataFolder, string[] tr // SegMode can't be OsdOnly in here. text = page.GetText(); } - - catch (ArgumentException) - { - return new() - { - Status = RecognizionStatus.InvalidImage, - Message = "Cannot process Pix image, height or width has invalid value." - }; - } - catch (InvalidOperationException) - { - return new() - { - Status = RecognizionStatus.ImageAlredyProcessed, - Message = "Image must be disposed after one (1) use." - }; - } - catch (ImageRecognizionException) - { - return new() - { - Status = RecognizionStatus.CannotRecognizeText, - Message = "Library cannot recognize image for some reason." - }; - } - catch (InvalidBytesException ex) - { - return new() - { - Status = RecognizionStatus.CannotRecognizeText, - Message = $"Recognized text contained invalid bytes. " + - $"See inner exception '{ex.GetType().Name}', '{ex.Message}'." - }; - } - catch (TesseractInitException ex) - { - return new() - { - Status = RecognizionStatus.Failed, - Message = "Cannot initialize instance of Tesseract engine, because of invalid data. " - + ex.InnerException is null ? ex.Message - : $"'{ex.Message}': '{ex.InnerException?.Message}'" - }; - } - catch (TesseractException) - { - return new() - { - Status = RecognizionStatus.CannotRecognizeText, - Message = "Library cannot thresholded image." - }; - } catch (DllNotFoundException) { throw; } catch (Exception ex) { - return new() + (RecognizionStatus status, string message) = ex switch { - Status = RecognizionStatus.UnknowError, - Message = $"Failed to ocr for unknown reason '{ex.GetType().Name}': '{ex.Message}'." + PageNotDisposedException => (RecognizionStatus.ImageAlredyProcessed, + "Old image TessPage must be disposed after one (1) use"), + ImageRecognizionException => (RecognizionStatus.CannotRecognizeText, + "Native library failed to recognize image"), + InvalidBytesException => (RecognizionStatus.CannotRecognizeText, + "Invalid bytes in recognized text, see inner exception"), + TesseractInitException => (RecognizionStatus.Failed, + "Invalid data to init Tesseract Engine, see exception"), + StringMarshallingException => (RecognizionStatus.InvalidResultString, + "Native library returned invalid string, please file bug report with input image as attachment"), + TesseractException => (RecognizionStatus.CannotRecognizeText, + "Library cannot get thresholded image when recognizing"), + ArgumentException => (RecognizionStatus.InvalidImage, + "Cannot process Pix image, height or width has invalid values."), + Exception => (RecognizionStatus.UnknowError, + $"Failed to ocr for unknown reason '{ex.GetType().Name}': '{ex.Message}'.") + }; + return new RecognizionResult + { + Status = status, + Message = message, + Exception = ex }; } - + _logger.LogInformation("Recognized image with confidence '{value}'", confidence); _logger.LogInformation("Image contained text with length '{value}'", text?.Length ?? 0); @@ -375,7 +344,7 @@ private async Task LoadTraineddataIfNotLoadedAsync() /// /// /// (null, ErrorResult) if failed, othewise (lang, null). - private static (RecognizionStatus, string?) TrainedDataToLanguage(in string tessdataFolder, params string[] traineddataFileNames) + private static (RecognizionStatus, string?) TraineddataToLanguage(in string tessdataFolder, params string[] traineddataFileNames) { if (string.IsNullOrWhiteSpace(tessdataFolder)) { diff --git a/TesseractOcrMaui/TesseractOcrMaui.csproj b/TesseractOcrMaui/TesseractOcrMaui.csproj index 3e591df..cb859f0 100644 --- a/TesseractOcrMaui/TesseractOcrMaui.csproj +++ b/TesseractOcrMaui/TesseractOcrMaui.csproj @@ -87,7 +87,7 @@ - 1.0.5 + 1.0.6 From 2eb680c4d3e9cf8c1212bbcfe19407089754e484 Mon Sep 17 00:00:00 2001 From: matikkaeditorinkaantaja Date: Mon, 19 Feb 2024 14:32:46 +0200 Subject: [PATCH 2/3] Bump up TesseractOcrMaui version to 1.1.6 --- TesseractOcrMaui/TesseractOcrMaui.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TesseractOcrMaui/TesseractOcrMaui.csproj b/TesseractOcrMaui/TesseractOcrMaui.csproj index cb859f0..f702245 100644 --- a/TesseractOcrMaui/TesseractOcrMaui.csproj +++ b/TesseractOcrMaui/TesseractOcrMaui.csproj @@ -25,7 +25,7 @@ - 1.1.5 + 1.1.6 Tesseract Ocr Maui TesseractOcrMaui henrivain From 80ae51e5a62a5f4ad1fafbef45db88bda6c4c470 Mon Sep 17 00:00:00 2001 From: matikkaeditorinkaantaja Date: Tue, 20 Feb 2024 12:35:38 +0200 Subject: [PATCH 3/3] Update nuget ref versions on readme files --- README.md | 2 +- TesseractOcrMaui.IOS/Properties/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6cec10..37f4fc0 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ dotnet add package TesseractOcrMaui 3. By package reference ```xml - + ``` ### 2. Add package to dependency injection (see TesseractOcrMauiTestApp) diff --git a/TesseractOcrMaui.IOS/Properties/README.md b/TesseractOcrMaui.IOS/Properties/README.md index 0c8aeea..e1f23b9 100644 --- a/TesseractOcrMaui.IOS/Properties/README.md +++ b/TesseractOcrMaui.IOS/Properties/README.md @@ -13,13 +13,13 @@ This package `TesseractOcrMaui.IOS` only includes bindings for iOS and should no 2. Package reference ```xml - + ``` 3. Dotnet CLI ```ps -dotnet add package TesseractOcrMaui --version 1.1.0 +dotnet add package TesseractOcrMaui --version 1.1.6 ``` Note that you should check what the current package version is and use it in your command.