Friday, 1 August 2025

NET MAUI app with camera-based OCR for Android and iOS using Tesseract OCR.

 Goal

Build a .NET MAUI app that:

✅ Captures an image using the camera
✅ Runs OCR using Tesseract OCR
✅ Displays extracted text on screen


✅ Prerequisites


🧩 Key Libraries


dotnet add package Tesseract.Net.SDK dotnet add package SkiaSharp

SkiaSharp is used to handle and convert images into a format Tesseract can understand.

Step-by-Step Guide


1. Create the MAUI Project


dotnet new maui -n MauiCameraOCR cd MauiCameraOCR

2. Add Permissions

✅ Android: Platforms/Android/AndroidManifest.xml

Add inside <manifest><application>:


<uses-permission android:name="android.permission.CAMERA" />

And enable camera in capabilities:


<uses-feature android:name="android.hardware.camera" android:required="true" />

✅ iOS: Platforms/iOS/Info.plist

Add:


<key>NSCameraUsageDescription</key> <string>This app uses the camera for OCR scanning</string>

3. Add NuGet Packages


dotnet add package Tesseract.Net.SDK dotnet add package SkiaSharp

4. Download Tesseract Language Files

Download from: https://github.com/tesseract-ocr/tessdata

  1. Download eng.traineddata

  2. Create a folder tessdata in your project root

  3. Place eng.traineddata inside

  4. Set properties:

    • Build Action: Content

    • Copy to Output Directory: Copy always


5. Main UI (MainPage.xaml)


<VerticalStackLayout Padding="20" Spacing="15"> <Label Text="Camera OCR App" FontSize="24" HorizontalOptions="Center"/> <Button Text="Capture Image" Clicked="OnCaptureImage"/> <Image x:Name="CapturedImage" HeightRequest="200"/> <ScrollView HeightRequest="300"> <Label x:Name="ResultLabel" FontSize="16"/> </ScrollView> </VerticalStackLayout>

6. Main Logic (MainPage.xaml.cs)

using Microsoft.Maui.Media; using Tesseract; using SkiaSharp; public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); } async void OnCaptureImage(object sender, EventArgs e) { try { var photo = await MediaPicker.CapturePhotoAsync(); if (photo == null) return; var stream = await photo.OpenReadAsync(); CapturedImage.Source = ImageSource.FromStream(() => stream); stream.Seek(0, SeekOrigin.Begin); var text = await ExtractTextFromImage(stream); ResultLabel.Text = text; } catch (Exception ex) { await DisplayAlert("Error", ex.Message, "OK"); } } async Task<string> ExtractTextFromImage(Stream stream) { using var engine = new TesseractEngine("./tessdata", "eng", EngineMode.Default); // Convert stream to byte[] using var ms = new MemoryStream(); await stream.CopyToAsync(ms); var bytes = ms.ToArray(); using var pix = Pix.LoadFromMemory(bytes); using var page = engine.Process(pix); return page.GetText(); } }

πŸ“± Platform Notes

Android

  • MediaPicker will use the device camera.

  • Ensure emulator supports camera or test on device.

iOS

  • MediaPicker uses the system camera picker.

  • Permissions must be correctly set in Info.plist.


✅ Build and Run


dotnet build dotnet maui run -f:net8.0-android

Or:

dotnet maui run -f:net8.0-ios

live camera preview in a .NET MAUI app requires platform-specific code because .NET MAUI does not yet provide built-in camera preview controls as of .NET 8.

Here’s how you can implement live camera preview in Android and iOS using Handlers and native views, and integrate it into a .NET MAUI project.

🎯 Goal

  • Show live camera preview (not just capture)

  • Platform-specific implementations:

    • πŸ“± Android: Use CameraX or CameraPreview

    • 🍎 iOS: Use AVCaptureSession and AVCaptureVideoPreviewLayer

✅ Approach Overview

  1. Create a CameraPreview control in .NET MAUI.

  2. Add native handlers for Android and iOS.

  3. Embed native preview views.

  4. Add a "Capture" button to trigger OCR.

πŸ“¦ Step-by-Step Guide

πŸ”§ Step 1: Create the MAUI CameraPreview Control

πŸ“„ Controls/CameraPreview.cs

namespace MauiCameraOCR.Controls; public class CameraPreview : View { public event EventHandler<byte[]> PhotoCaptured; public void OnPhotoCaptured(byte[] imageData) => PhotoCaptured?.Invoke(this, imageData); public void CapturePhoto() { // This will be handled by the native handler } }

🎨 Step 2: Use It in XAML

πŸ“„ MainPage.xaml


<VerticalStackLayout Padding="20"> <Label Text="Live Camera OCR" FontSize="24" HorizontalOptions="Center" /> <ContentView HeightRequest="300"> <controls:CameraPreview x:Name="Camera" /> </ContentView> <Button Text="Capture and OCR" Clicked="OnCaptureClicked" /> <ScrollView HeightRequest="300"> <Label x:Name="ResultLabel" FontSize="16" /> </ScrollView> </VerticalStackLayout>

In code-behind:


private void OnCaptureClicked(object sender, EventArgs e) { Camera.CapturePhoto(); }

And subscribe to the PhotoCaptured event in OnAppearing.

πŸ€– Step 3: Android Handler

πŸ“„ Platforms/Android/Handlers/CameraPreviewHandler.cs


using Android.Content; using Android.Views; using AndroidX.Camera.Core; using AndroidX.Camera.Lifecycle; using AndroidX.Camera.View; using AndroidX.Lifecycle; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using MauiCameraOCR.Controls; [assembly: ExportHandler(typeof(CameraPreview), typeof(CameraPreviewHandler))] namespace MauiCameraOCR.Platforms.Android.Handlers; public class CameraPreviewHandler : ViewHandler<CameraPreview, PreviewView> { PreviewView _previewView; ImageCapture _imageCapture; protected override PreviewView CreatePlatformView() { _previewView = new PreviewView(Context); var cameraProviderFuture = ProcessCameraProvider.GetInstance(Context); cameraProviderFuture.AddListener(() => { var cameraProvider = (ProcessCameraProvider)cameraProviderFuture.Get(); var preview = new Preview.Builder().Build(); _imageCapture = new ImageCapture.Builder().Build(); preview.SetSurfaceProvider(_previewView.SurfaceProvider); var cameraSelector = CameraSelector.DefaultBackCamera; cameraProvider.UnbindAll(); cameraProvider.BindToLifecycle((ILifecycleOwner)Context, cameraSelector, preview, _imageCapture); }, Context.MainExecutor); return _previewView; } public override void ConnectHandler(PreviewView platformView) { base.ConnectHandler(platformView); VirtualView.CapturePhoto = async () => { var file = Java.IO.File.CreateTempFile("ocr_image", ".jpg"); var outputOptions = new ImageCapture.OutputFileOptions.Builder(file).Build(); _imageCapture.TakePicture(outputOptions, Context.MainExecutor, new ImageCaptureOnImageSavedCallback((bytes) => { (VirtualView as CameraPreview)?.OnPhotoCaptured(bytes); })); }; } class ImageCaptureOnImageSavedCallback : Java.Lang.Object, ImageCapture.IOnImageSavedCallback { readonly Action<byte[]> _onSaved; public ImageCaptureOnImageSavedCallback(Action<byte[]> onSaved) { _onSaved = onSaved; } public void OnImageSaved(ImageCapture.OutputFileResults output) { var path = output.SavedUri?.Path; var bytes = File.ReadAllBytes(path); _onSaved(bytes); } public void OnError(ImageCaptureException exc) { Console.WriteLine("Error capturing image: " + exc.Message); } } }

🍎 Step 4: iOS Handler

πŸ“„ Platforms/iOS/Handlers/CameraPreviewHandler.cs


using AVFoundation; using CoreGraphics; using Foundation; using Microsoft.Maui.Handlers; using UIKit; using MauiCameraOCR.Controls; [assembly: ExportHandler(typeof(CameraPreview), typeof(CameraPreviewHandler))] namespace MauiCameraOCR.Platforms.iOS.Handlers; public class CameraPreviewHandler : ViewHandler<CameraPreview, UIView> { AVCaptureSession _session; AVCapturePhotoOutput _photoOutput; UIView _nativeView; protected override UIView CreatePlatformView() { _session = new AVCaptureSession(); _photoOutput = new AVCapturePhotoOutput(); _session.AddOutput(_photoOutput); var device = AVCaptureDevice.GetDefaultDevice(AVMediaTypes.Video); var input = AVCaptureDeviceInput.FromDevice(device, out var error); if (input != null) _session.AddInput(input); var previewLayer = new AVCaptureVideoPreviewLayer(_session) { Frame = new CGRect(0, 0, 300, 300), VideoGravity = AVLayerVideoGravity.ResizeAspectFill }; _nativeView = new UIView(); _nativeView.Layer.AddSublayer(previewLayer); _session.StartRunning(); VirtualView.CapturePhoto = () => { var settings = AVCapturePhotoSettings.Create(); _photoOutput.CapturePhoto(settings, new PhotoCaptureDelegate((bytes) => { VirtualView.OnPhotoCaptured(bytes); })); }; return _nativeView; } class PhotoCaptureDelegate : AVCapturePhotoCaptureDelegate { private readonly Action<byte[]> _callback; public PhotoCaptureDelegate(Action<byte[]> callback) => _callback = callback; public override void DidFinishProcessingPhoto(AVCapturePhotoOutput output, AVCapturePhoto photo, NSError error) { if (photo.FileDataRepresentation != null) { _callback(photo.FileDataRepresentation.ToArray()); } } } }

🧠 OCR Integration (Shared Code)

In your MainPage.xaml.cs, handle:


protected override void OnAppearing() { base.OnAppearing(); Camera.PhotoCaptured += async (s, imgBytes) => { using var engine = new TesseractEngine("./tessdata", "eng", EngineMode.Default); using var pix = Pix.LoadFromMemory(imgBytes); using var page = engine.Process(pix); ResultLabel.Text = page.GetText(); }; }

✅ Final Touches

πŸ›‘ Permissions

Ensure you request camera permissions at runtime (especially on Android 13+).

var status = await Permissions.RequestAsync<Permissions.Camera>();

πŸ“¦ Summary

PlatformCameraLive PreviewCaptureOCR
Android✅ (CameraX)
iOS✅ (AVFoundation)



No comments:

Post a Comment

Cricket Streaming MAUI App: Complete Solution

This document outlines a comprehensive solution for building a cricket streaming MAUI app with subscription plans, geo-restrictions, and pre...

Ads2