Skip to content

Commit

Permalink
tested on windows, android, ios
Browse files Browse the repository at this point in the history
  • Loading branch information
kfrancis committed Mar 27, 2024
1 parent c07755b commit 2822708
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 162 deletions.
14 changes: 7 additions & 7 deletions samples/Plugin.Maui.Feature.Sample/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace Plugin.Maui.Feature.Sample;
namespace Plugin.Maui.Feature.Sample;

public partial class App : Application
{
public App()
{
InitializeComponent();
public App()
{
InitializeComponent();

MainPage = new AppShell();
}
}
MainPage = new AppShell();
}
}
29 changes: 20 additions & 9 deletions samples/Plugin.Maui.Feature.Sample/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@
x:Class="Plugin.Maui.Feature.Sample.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Feature Plugin">

<!-- Add something that makes sense for your plugin here -->
<Grid Padding="20">
<Label Text="Hello" />
Title="OCR Plugin">
<StackLayout Padding="20" Spacing="10">
<Button
x:Name="RunOcrBtn"
Clicked="RunOcrBtn_Clicked"
x:Name="OpenFromCameraBtn"
MaximumHeightRequest="100"
MaximumWidthRequest="200"
Text="Run" />
</Grid>
Clicked="OpenFromCameraBtn_Clicked"
Text="From Camera .." />
<Button
x:Name="OpenFromFileBtn"
MaximumHeightRequest="100"
MaximumWidthRequest="200"
Clicked="OpenFromFileBtn_Clicked"
Text="From File .." />
<Label x:Name="ResultLbl" Text="Waiting for results .." HorizontalOptions="Center" />
<Button
IsEnabled="False"
x:Name="ClearBtn"
Clicked="ClearBtn_Clicked"
MaximumHeightRequest="100"
MaximumWidthRequest="200"
Text="Clear Result" />
</StackLayout>
</ContentPage>
67 changes: 57 additions & 10 deletions samples/Plugin.Maui.Feature.Sample/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,78 @@ namespace Plugin.Maui.Feature.Sample;

public partial class MainPage : ContentPage
{
readonly IOcrService _feature;
readonly IOcrService _ocr;

public MainPage(IOcrService feature)
{
InitializeComponent();

_feature = feature;
_ocr = feature;
}

protected override async void OnAppearing()
{
base.OnAppearing();

await _feature.InitAsync();
await _ocr.InitAsync();
}

private async void RunOcrBtn_Clicked(object sender, EventArgs e)
private async void OpenFromCameraBtn_Clicked(object sender, EventArgs e)
{
// load test.jpg and then run OCR on it
using var fileStream = await FileSystem.Current.OpenAppPackageFileAsync("test.jpg");
if (MediaPicker.Default.IsCaptureSupported)
{
var photo = await MediaPicker.Default.CapturePhotoAsync();

// read all bytes of the image from fileStream
var imageData = new byte[fileStream.Length];
await fileStream.ReadAsync(imageData, 0, imageData.Length);
if (photo != null)
{
// Open a stream to the photo
using var sourceStream = await photo.OpenReadAsync();

var result = await _feature.RecognizeTextAsync(imageData);
// Create a byte array to hold the image data
var imageData = new byte[sourceStream.Length];

// Read the stream into the byte array
await sourceStream.ReadAsync(imageData);

var result = await _ocr.RecognizeTextAsync(imageData);

ResultLbl.Text = result.AllText;

ClearBtn.IsEnabled = true;
}
}
else
{
await DisplayAlert(title: "Sorry", message: "Image capture is not supported on this device.", cancel: "OK");
}
}

private async void OpenFromFileBtn_Clicked(object sender, EventArgs e)
{
var photo = await MediaPicker.Default.PickPhotoAsync();

if (photo != null)
{
// Open a stream to the photo
using var sourceStream = await photo.OpenReadAsync();

// Create a byte array to hold the image data
var imageData = new byte[sourceStream.Length];

// Read the stream into the byte array
await sourceStream.ReadAsync(imageData);

var result = await _ocr.RecognizeTextAsync(imageData);

ResultLbl.Text = result.AllText;

ClearBtn.IsEnabled = true;
}
}

private void ClearBtn_Clicked(object sender, EventArgs e)
{
ResultLbl.Text = string.Empty;
ClearBtn.IsEnabled = false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
<meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="ocr" />
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
using Android.App;
using Android.App;
using Android.Content.PM;
using Android.OS;

// Needed for Picking photo/video
[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage, MaxSdkVersion = 32)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaAudio)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaImages)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaVideo)]

// Needed for Taking photo/video
[assembly: UsesPermission(Android.Manifest.Permission.Camera)]
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage, MaxSdkVersion = 32)]

// Add these properties if you would like to filter out devices that do not have cameras, or set to false to make them optional
[assembly: UsesFeature("android.hardware.camera", Required = true)]
[assembly: UsesFeature("android.hardware.camera.autofocus", Required = true)]

namespace Plugin.Maui.Feature.Sample;

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}
}
64 changes: 36 additions & 28 deletions samples/Plugin.Maui.Feature.Sample/Platforms/iOS/Info.plist
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to the camera to take photos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for taking videos.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to the photo gallery for picking photos and videos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photos gallery for picking photos and videos.</string>
</dict>
</plist>
20 changes: 15 additions & 5 deletions samples/Plugin.Maui.Feature.Sample/Plugin.Maui.OCR.Sample.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
Expand Down Expand Up @@ -30,6 +30,11 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='net8.0-ios'">
<CodesignKey>Apple Development: Created via API (BQQPP4HLY2)</CodesignKey>
<CodesignProvision>VS: WildCard Development</CodesignProvision>
</PropertyGroup>

<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
Expand All @@ -47,13 +52,18 @@
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<None Remove="Resources\Raw\test.jpg" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Plugin.Maui.OCR\Plugin.Maui.OCR.csproj" />
<ProjectReference Include="..\..\src\Plugin.Shared.OCR\Plugin.Shared.OCR.csproj" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Xamarin.AndroidX.Fragment.Ktx" Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<Version>1.6.2.2</Version>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0-android'">
<PackageReference Include="Xamarin.GooglePlayServices.MLKit.Text.Recognition">
<Version>119.0.0.5</Version>
</PackageReference>
</ItemGroup>
</Project>
Binary file not shown.
35 changes: 34 additions & 1 deletion src/Plugin.Maui.OCR/OcrImplementation.android.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics;
using Android.Gms.Tasks;
using Android.Graphics;
using Android.Util;
using Plugin.Shared.OCR;
using Xamarin.Google.MLKit.Common;
using Xamarin.Google.MLKit.Vision.Common;
using Xamarin.Google.MLKit.Vision.Text;
using Xamarin.Google.MLKit.Vision.Text.Latin;
Expand Down Expand Up @@ -43,6 +45,8 @@ public Task InitAsync(System.Threading.CancellationToken ct = default)
{
// Initialization might not be required for ML Kit's on-device text recognition,
// but you can perform any necessary setup here.


return Task.CompletedTask;
}

Expand All @@ -53,7 +57,36 @@ public async Task<OcrResult> RecognizeTextAsync(byte[] imageData, System.Threadi
using var inputImage = InputImage.FromBitmap(image, 0);

using var textScanner = TextRecognition.GetClient(TextRecognizerOptions.DefaultOptions);
return ProcessOcrResult(await ToAwaitableTask(textScanner.Process(inputImage).AddOnSuccessListener(new OnSuccessListener()).AddOnFailureListener(new OnFailureListener())));

MlKitException? lastException = null;
var maxRetries = 5;

for (var retry = 0; retry < maxRetries; retry++)
{
try
{
// Try to perform the OCR operation
return ProcessOcrResult(await ToAwaitableTask(textScanner.Process(inputImage).AddOnSuccessListener(new OnSuccessListener()).AddOnFailureListener(new OnFailureListener())));
}
catch (MlKitException ex) when (ex.Message.Contains("Waiting for the text optional module to be downloaded"))
{
// If the specific exception is caught, log it and wait before retrying
lastException = ex;
Debug.WriteLine($"OCR model is not ready. Waiting before retrying... Attempt {retry + 1}/{maxRetries}");
await Task.Delay(5000);
}
}

// If all retries have failed, throw the last exception
if (lastException != null)
{
throw lastException;
}
else
{
throw new InvalidOperationException("OCR operation failed without an exception.");
}

}

private static Task<Java.Lang.Object> ToAwaitableTask(global::Android.Gms.Tasks.Task task)
Expand Down
Loading

0 comments on commit 2822708

Please sign in to comment.