Browse Source

Added Barcode scanner for Android

Kenric Nugteren 1 month ago
parent
commit
216c93871d

+ 39 - 0
InABox.Avalonia.Platform.Android/Barcodes/Android_CameraViewControl.cs

@@ -0,0 +1,39 @@
+using Avalonia;
+using Avalonia.Android;
+using Avalonia.Platform;
+using InABox.Avalonia.Platform.Barcodes;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reactive;
+
+namespace InABox.Avalonia.Platform.Android.Barcodes;
+
+public class Android_CameraViewControl : ICameraViewControl
+{
+    public Logger? Logger { get; set; }
+
+    public IPlatformHandle CreateControl(CameraView view, IPlatformHandle parent)
+    {
+        var parentContext = (parent as AndroidViewControlHandle)?.View.Context
+            ?? global::Android.App.Application.Context;
+        var cameraManager = new CameraManager(view, parentContext);
+
+        view.GetPropertyChangedObservable(CameraView.CameraEnabledProperty).Subscribe(args => cameraManager.UpdateCameraEnabled());
+        view.GetPropertyChangedObservable(CameraView.PauseScanningProperty).Subscribe(args => cameraManager._pauseScanning = args.GetNewValue<bool>());
+        view.GetPropertyChangedObservable(CameraView.ForceInvertedProperty).Subscribe(args => cameraManager._forceInverted = args.GetNewValue<bool>());
+        view.GetPropertyChangedObservable(CameraView.ForceFrameCaptureProperty).Subscribe(args => cameraManager._forceFrameCapture = args.GetNewValue<bool>());
+        view.GetPropertyChangedObservable(CameraView.CaptureNextFrameProperty).Subscribe(args => cameraManager._captureNextFrame = args.GetNewValue<bool>());
+        view.GetPropertyChangedObservable(CameraView.AimModeProperty).Subscribe(args => cameraManager._aimMode = args.GetNewValue<bool>());
+        view.GetPropertyChangedObservable(CameraView.ViewfinderModeProperty).Subscribe(args => cameraManager._viewFinderMode = args.GetNewValue<bool>());
+
+        if (view.CameraEnabled)
+        {
+            cameraManager.UpdateCameraEnabled();
+        }
+        return new AndroidViewControlHandle(cameraManager.BarcodeView);
+    }
+}

+ 167 - 0
InABox.Avalonia.Platform.Android/Barcodes/BarcodeAnalayzer.cs

@@ -0,0 +1,167 @@
+
+using Android.Gms.Tasks;
+using Android.Runtime;
+using AndroidX.Camera.Core;
+using AndroidX.Camera.View.Transform;
+using Avalonia.Threading;
+using InABox.Avalonia.Platform.Barcodes;
+using Microsoft.Maui.Graphics.Platform;
+using System.Diagnostics;
+using Xamarin.Google.MLKit.Vision.Barcode.Common;
+using Xamarin.Google.MLKit.Vision.BarCode;
+using Xamarin.Google.MLKit.Vision.Common;
+
+using Scanner = Xamarin.Google.MLKit.Vision.BarCode.BarcodeScanning;
+using Size = Android.Util.Size;
+using Point = Microsoft.Maui.Graphics.Point;
+using Rect = Microsoft.Maui.Graphics.Rect;
+using Android.Graphics;
+
+namespace InABox.Avalonia.Platform.Android.Barcodes;
+
+internal class BarcodeAnalyzer : Java.Lang.Object, ImageAnalysis.IAnalyzer
+{
+    public Size DefaultTargetResolution => Methods.TargetResolution(null);
+    public int TargetCoordinateSystem => ImageAnalysis.CoordinateSystemViewReferenced;
+
+    private readonly HashSet<BarcodeResult> _barcodeResults;
+    private readonly CameraManager _cameraManager;
+    private readonly object _resultsLock;
+
+    private IBarcodeScanner? _barcodeScanner;
+    private CoordinateTransform? _coordinateTransform;
+
+    private bool _updateCoordinateTransform = false;
+    private Point _previewViewCenter = new();
+    private Rect _previewViewRect = new();
+
+    internal BarcodeAnalyzer(CameraManager cameraManager)
+    {
+        _barcodeResults = [];
+        _cameraManager = cameraManager;
+        _resultsLock = new();
+
+        _previewViewRect.X = 0;
+        _previewViewRect.Y = 0;
+    }
+
+    internal void UpdateSymbologies()
+    {
+        _barcodeScanner?.Dispose();
+        _barcodeScanner = Scanner.GetClient(new BarcodeScannerOptions.Builder()
+            .SetBarcodeFormats(Methods.ConvertBarcodeFormats(_cameraManager?.CameraView?.BarcodeSymbologies ?? BarcodeFormats.All))
+            .Build());
+    }
+
+    public void Analyze(IImageProxy proxy)
+    {
+        try
+        {
+            ArgumentNullException.ThrowIfNull(proxy?.Image);
+            ArgumentNullException.ThrowIfNull(_cameraManager?.CameraView);
+            ArgumentNullException.ThrowIfNull(_barcodeScanner);
+
+            if (_cameraManager._pauseScanning)
+                return;
+
+            if (_updateCoordinateTransform)
+            {
+                _coordinateTransform?.Dispose();
+                _coordinateTransform = _cameraManager.GetCoordinateTransform(proxy);
+
+                _previewViewCenter.X = _cameraManager.PreviewView.Width * 0.5;
+                _previewViewCenter.Y = _cameraManager.PreviewView.Height * 0.5;
+                _previewViewRect.Width = _cameraManager.PreviewView.Width;
+                _previewViewRect.Height = _cameraManager.PreviewView.Height;
+
+                _updateCoordinateTransform = false;
+            }
+
+            using var inputImage = InputImage.FromMediaImage(proxy.Image, 0);
+            using var task  = _barcodeScanner.Process(inputImage);
+            var result = TasksClass.Await(task);
+
+            Java.Lang.Object? invertedResult = null;
+            if (_cameraManager._forceInverted)
+            {
+                Methods.InvertLuminance(proxy.Image);
+                using var invertedImage = InputImage.FromMediaImage(proxy.Image, 0);
+                using var invertedTask = _barcodeScanner.Process(invertedImage);
+                invertedResult = TasksClass.Await(invertedTask);
+            }
+
+            lock (_resultsLock)
+            {
+                _barcodeResults.Clear();
+                AddResultToSet(result);
+                AddResultToSet(invertedResult);
+
+                _cameraManager.CameraView.DetectionFinished(_barcodeResults);
+            }
+
+            if (_cameraManager._forceFrameCapture || (_cameraManager._captureNextFrame && _barcodeResults.Count > 0))
+            {
+                // using var stream = new MemoryStream();
+                // var bitmap = proxy.ToBitmap();
+                // bitmap.Compress(Bitmap.CompressFormat.Png, 0, stream);
+                // var image = new PlatformImage(stream.ToArray());
+                // _cameraManager.CameraView.TriggerOnImageCaptured(image);
+            }
+
+            result?.Dispose();
+            invertedResult?.Dispose();
+        }
+        catch (Exception ex)
+        {
+            Debug.WriteLine(ex);
+        }
+        finally
+        {
+            try
+            {
+                proxy?.Close();
+            }
+            catch (Exception ex)
+            {
+                Debug.WriteLine(ex);
+                Dispatcher.UIThread.InvokeAsync(() => _cameraManager?.Start());
+            }
+        }
+    }
+
+    private void AddResultToSet(Java.Lang.Object? result)
+    {
+        if (result is not JavaList javaList)
+            return;
+
+        foreach (Barcode barcode in javaList)
+        {
+            if (barcode is null)
+                continue;
+            if (string.IsNullOrEmpty(barcode.DisplayValue) && string.IsNullOrEmpty(barcode.RawValue))
+                continue;
+
+            var barcodeResult = barcode.AsBarcodeResult(_coordinateTransform);
+
+            if ((_cameraManager?._aimMode ?? false) && !barcodeResult.PreviewBoundingBox.Contains(_previewViewCenter))
+                continue;
+            if ((_cameraManager?._viewFinderMode ?? false) && !_previewViewRect.Contains(barcodeResult.PreviewBoundingBox))
+                continue;
+
+            _barcodeResults.Add(barcodeResult);
+        }
+    }
+
+    public void UpdateTransform(Matrix? matrix)
+    {
+        _updateCoordinateTransform = true;
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        _coordinateTransform?.Dispose();
+        _barcodeScanner?.Dispose();
+        
+        base.Dispose(disposing);
+    }
+}

+ 17 - 0
InABox.Avalonia.Platform.Android/Barcodes/BarcodeView.cs

@@ -0,0 +1,17 @@
+using Android.Content;
+using Android.Views;
+using AndroidX.CoordinatorLayout.Widget;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Platform.Android.Barcodes;
+
+public class BarcodeView : FrameLayout
+{
+    public BarcodeView(Context context) : base(context)
+    {
+    }
+}

+ 316 - 0
InABox.Avalonia.Platform.Android/Barcodes/CameraManager.cs

@@ -0,0 +1,316 @@
+using Android;
+using Android.Content;
+using Android.Graphics;
+using AndroidX.Camera.Core;
+using AndroidX.Camera.Core.ResolutionSelector;
+using AndroidX.Camera.View;
+using AndroidX.Camera.View.Transform;
+using AndroidX.Core.Content;
+using AndroidX.Lifecycle;
+using Avalonia.Media;
+using Avalonia.Threading;
+using InABox.Avalonia.Platform.Barcodes;
+using Java.Util.Concurrent;
+using Microsoft.Maui.Devices;
+
+using static Android.Views.ViewGroup;
+using Color = Android.Graphics.Color;
+using AvColor = Avalonia.Media.Color;
+using Avalonia;
+
+namespace InABox.Avalonia.Platform.Android.Barcodes;
+
+internal class CameraManager : IDisposable
+{
+    internal BarcodeView BarcodeView { get => _barcodeView; }
+    internal CameraView? CameraView { get => _cameraView; }
+    internal PreviewView PreviewView { get => _previewView; }
+
+    internal CameraState? OpenedCameraState { get; set; }
+
+    private readonly BarcodeAnalyzer _barcodeAnalyzer;
+    private readonly BarcodeView _barcodeView;
+    private readonly Context _context;
+    private readonly IExecutorService _analyzerExecutor;
+    private readonly ImageView _imageView;
+    private readonly LifecycleCameraController _cameraController;
+    private readonly ILifecycleOwner _lifecycleOwner;
+    private readonly PreviewView _previewView;
+    private readonly RelativeLayout _relativeLayout;
+    private readonly CameraStateObserver _cameraStateObserver;
+
+    private readonly CameraView? _cameraView;
+
+    // Caches of CameraView properties to avoid calling on non-main threads.
+    internal bool _pauseScanning;
+    internal bool _forceInverted;
+    internal bool _forceFrameCapture;
+    internal bool _captureNextFrame;
+    internal bool _aimMode;
+    internal bool _viewFinderMode;
+
+    private ICameraInfo? _currentCameraInfo;
+
+    private const int aimRadius = 25;
+
+    static CameraManager()
+    {
+    }
+
+    internal CameraManager(CameraView cameraView, Context context)
+    {
+        _context = context;
+        _cameraView = cameraView;
+
+        ILifecycleOwner? owner = null;
+        if (_context is ILifecycleOwner)
+            owner = _context as ILifecycleOwner;
+        else if ((_context as ContextWrapper)?.BaseContext is ILifecycleOwner)
+            owner = (_context as ContextWrapper)?.BaseContext as ILifecycleOwner;
+        // else if (Platform.CurrentActivity is ILifecycleOwner)
+        //     owner = Platform.CurrentActivity as ILifecycleOwner;
+        
+        var executor = Executors.NewSingleThreadExecutor();
+
+        ArgumentNullException.ThrowIfNull(owner);
+        ArgumentNullException.ThrowIfNull(executor);
+        _lifecycleOwner = owner;
+        _analyzerExecutor = executor;
+
+        _barcodeAnalyzer = new BarcodeAnalyzer(this);
+
+        _cameraStateObserver = new CameraStateObserver(this, _cameraView);
+        _cameraController = new LifecycleCameraController(_context)
+        {
+            TapToFocusEnabled = _cameraView?.TapToFocusEnabled ?? false,
+            ImageAnalysisBackpressureStrategy = ImageAnalysis.StrategyKeepOnlyLatest
+        };
+        _cameraController.SetEnabledUseCases(CameraController.ImageAnalysis);
+        _cameraController.ZoomState.ObserveForever(_cameraStateObserver);
+        _cameraController.InitializationFuture.AddListener(new Java.Lang.Runnable(() => 
+        {
+            _currentCameraInfo?.CameraState.RemoveObserver(_cameraStateObserver);
+            _currentCameraInfo = _cameraController.CameraInfo;
+            _currentCameraInfo?.CameraState.ObserveForever(_cameraStateObserver);
+        }), ContextCompat.GetMainExecutor(_context));
+
+        ArgumentNullException.ThrowIfNull(PreviewView.ImplementationMode.Compatible);
+        ArgumentNullException.ThrowIfNull(PreviewView.ScaleType.FillCenter);
+        ArgumentNullException.ThrowIfNull(Bitmap.Config.Argb8888);
+
+        _previewView = new PreviewView(_context)
+        {
+            LayoutParameters = new RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
+        };
+        var background = (_cameraView?.Background as SolidColorBrush)?.Color ?? Colors.Transparent;
+        _previewView.SetBackgroundColor(new Color(background.R, background.G, background.B, background.A));
+        _previewView.SetImplementationMode(PreviewView.ImplementationMode.Compatible);
+        _previewView.SetScaleType(PreviewView.ScaleType.FillCenter);
+        
+        using var layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.WrapContent);
+        layoutParams.AddRule(LayoutRules.CenterInParent);
+        using var circleBitmap = Bitmap.CreateBitmap(2 * aimRadius, 2 * aimRadius, Bitmap.Config.Argb8888);
+        using var canvas = new Canvas(circleBitmap);
+        canvas.DrawCircle(aimRadius, aimRadius, aimRadius, new Paint
+        {
+            AntiAlias = true,
+            Color = Color.Red,
+            Alpha = 150
+        }); 
+        _imageView = new ImageView(_context)
+        {
+            LayoutParameters = layoutParams
+        };
+        _imageView.SetImageBitmap(circleBitmap);
+
+        _relativeLayout = new RelativeLayout(_context)
+        {
+            LayoutParameters = new RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
+        };
+        _relativeLayout.AddView(_previewView);
+
+        _barcodeView = new BarcodeView(_context);
+        _barcodeView.AddView(_relativeLayout);
+
+        DeviceDisplay.Current.MainDisplayInfoChanged += MainDisplayInfoChangedAsync;
+    }
+
+    internal void Start()
+    { 
+        if (_previewView is not null)
+            _previewView.Controller = null;
+
+        if (OpenedCameraState?.GetType() != CameraState.Type.Closed)
+            _cameraController?.Unbind();
+        
+        _cameraController?.ClearImageAnalysisAnalyzer();
+        if (_barcodeAnalyzer is not null && _analyzerExecutor is not null)
+            _cameraController?.SetImageAnalysisAnalyzer(_analyzerExecutor, _barcodeAnalyzer);
+
+        UpdateResolution();
+        UpdateCamera();
+        UpdateSymbologies();
+        UpdateTorch();
+
+        if (_lifecycleOwner is not null)
+            _cameraController?.BindToLifecycle(_lifecycleOwner);
+        
+        if (_previewView is not null && _cameraController is not null)
+            _previewView.Controller = _cameraController;
+    }
+
+    internal void Stop()
+    {
+        _cameraController?.Unbind();
+    }
+
+    internal void UpdateAimMode()
+    {
+        if (_aimMode)
+            _relativeLayout?.AddView(_imageView);
+        else
+            _relativeLayout?.RemoveView(_imageView);
+    }
+
+    internal void UpdateBackgroundColor()
+    {
+        var background = (_cameraView?.Background as SolidColorBrush)?.Color ?? Colors.Transparent;
+        _previewView.SetBackgroundColor(new Color(background.R, background.G, background.B, background.A));
+    }
+
+    internal void UpdateCamera()
+    {
+        if (_cameraController is not null)
+        {
+            if (_cameraView?.CameraFacing == CameraFacing.Front)
+                _cameraController.CameraSelector = CameraSelector.DefaultFrontCamera;
+            else
+                _cameraController.CameraSelector = CameraSelector.DefaultBackCamera;
+        }
+    }
+
+    internal void UpdateCameraEnabled()
+    {
+        if (_cameraView?.CameraEnabled ?? false)
+            Start();
+        else
+            Stop();
+    }
+
+    internal void UpdateResolution()
+    {
+        using var analysisStrategy = new ResolutionStrategy(Methods.TargetResolution(_cameraView?.CaptureQuality), ResolutionStrategy.FallbackRuleClosestHigherThenLower);
+        using var resolutionBuilder = new ResolutionSelector.Builder();   
+        resolutionBuilder.SetAllowedResolutionMode(ResolutionSelector.PreferHigherResolutionOverCaptureRate);
+        resolutionBuilder.SetResolutionStrategy(analysisStrategy);
+        resolutionBuilder.SetAspectRatioStrategy(AspectRatioStrategy.Ratio169FallbackAutoStrategy);
+        var selector = resolutionBuilder.Build();
+
+        if (_cameraController is not null)
+        {
+            _cameraController.ImageAnalysisResolutionSelector = selector;
+            _cameraController.PreviewResolutionSelector = selector;
+        }
+    }
+
+    internal void UpdateSymbologies()
+    {
+        _barcodeAnalyzer?.UpdateSymbologies();
+    }
+
+    internal void UpdateTapToFocus() 
+    {
+        if (_cameraController is not null)
+            _cameraController.TapToFocusEnabled = _cameraView?.TapToFocusEnabled ?? false;
+    }
+
+    internal void UpdateTorch()
+    {
+        _cameraController?.EnableTorch(_cameraView?.TorchOn ?? false);
+    }
+
+    internal void UpdateVibration()
+    {
+        // if ((_cameraView?.VibrationOnDetected ?? false) &&
+        //     !Permissions.IsDeclaredInManifest(Manifest.Permission.Vibrate))
+        //     _cameraView.VibrationOnDetected = false;
+    }
+
+    internal void UpdateZoomFactor()
+    {
+        if (_cameraView is not null && (_cameraController?.ZoomState?.IsInitialized ?? false))
+        {
+            var factor = _cameraView.RequestZoomFactor;
+            if (factor > 0)
+            {
+                factor = Math.Max(factor, _cameraView.MinZoomFactor);
+                factor = Math.Min(factor, _cameraView.MaxZoomFactor);
+
+                if (factor != _cameraView.CurrentZoomFactor)
+                    _cameraController.SetZoomRatio(factor);
+            }
+        }
+    }
+
+    internal CoordinateTransform? GetCoordinateTransform(IImageProxy proxy)
+    {
+        var imageOutputTransform = new ImageProxyTransformFactory().GetOutputTransform(proxy);
+        var previewOutputTransform = Dispatcher.UIThread.InvokeAsync(() => _previewView?.OutputTransform).GetAwaiter().GetResult();
+
+        if (imageOutputTransform is not null && previewOutputTransform is not null)
+            return new CoordinateTransform(imageOutputTransform, previewOutputTransform);
+        else
+            return null;
+    }
+
+    private async void MainDisplayInfoChangedAsync(object? sender, DisplayInfoChangedEventArgs e)
+    {
+        if (OpenedCameraState?.GetType() == CameraState.Type.Open)
+        {
+            if (_previewView is not null)
+                _previewView.Controller = null;
+
+            await Task.Delay(100);
+
+            if (_previewView is not null)
+                _previewView.Controller = _cameraController;
+        }
+    }
+    
+    public void Dispose()
+    {
+        Dispose(true);
+        GC.SuppressFinalize(this);
+    }
+
+    protected virtual void Dispose(bool disposing)
+    {
+        if (disposing)
+        {
+            Stop();
+
+            DeviceDisplay.Current.MainDisplayInfoChanged -= MainDisplayInfoChangedAsync;
+            
+            if (_cameraStateObserver is not null)
+            {
+                _cameraController?.ZoomState.RemoveObserver(_cameraStateObserver);
+                _currentCameraInfo?.CameraState.RemoveObserver(_cameraStateObserver);
+            }
+
+            _cameraController?.ClearImageAnalysisAnalyzer();
+
+            _barcodeView?.RemoveAllViews();
+            _relativeLayout?.RemoveAllViews();
+            
+            _barcodeView?.Dispose();
+            _relativeLayout?.Dispose();
+            _imageView?.Dispose();
+            _previewView?.Dispose();
+            _cameraController?.Dispose();
+            _currentCameraInfo?.Dispose();
+            _cameraStateObserver?.Dispose();
+            _barcodeAnalyzer?.Dispose();
+            _analyzerExecutor?.Dispose();
+        }
+    }
+}

+ 37 - 0
InABox.Avalonia.Platform.Android/Barcodes/CameraStateObserver.cs

@@ -0,0 +1,37 @@
+using AndroidX.Camera.Core;
+using AndroidX.Lifecycle;
+using InABox.Avalonia.Platform.Barcodes;
+
+namespace InABox.Avalonia.Platform.Android.Barcodes;
+
+internal class CameraStateObserver : Java.Lang.Object, IObserver
+{
+    private readonly CameraManager _cameraManager;
+    private readonly CameraView _cameraView;
+
+    internal CameraStateObserver(CameraManager cameraManager, CameraView cameraView)
+    {
+        _cameraManager = cameraManager;
+        _cameraView = cameraView;
+    }
+
+    public void OnChanged(Java.Lang.Object? value)
+    {
+        if (value is not null && _cameraView is not null && _cameraManager is not null)
+        {
+            if (value is IZoomState zoomState)
+            {
+                _cameraView.CurrentZoomFactor = zoomState.ZoomRatio;
+                _cameraView.MinZoomFactor = zoomState.MinZoomRatio;
+                _cameraView.MaxZoomFactor = zoomState.MaxZoomRatio;
+
+                _cameraManager.UpdateZoomFactor();
+            }
+
+            if (value is CameraState cameraState)
+            {
+                _cameraManager.OpenedCameraState = cameraState;
+            }
+        }
+    }
+}

+ 246 - 0
InABox.Avalonia.Platform.Android/Barcodes/Methods.cs

@@ -0,0 +1,246 @@
+using Android.Gms.Extensions;
+using Android.Graphics;
+using Android.Runtime;
+using Android.Util;
+using AndroidX.Camera.View.Transform;
+using InABox.Avalonia.Platform.Barcodes;
+using Java.Net;
+using Java.Util;
+using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Storage;
+using Xamarin.Google.MLKit.Vision.Barcode.Common;
+using Xamarin.Google.MLKit.Vision.BarCode;
+using Xamarin.Google.MLKit.Vision.Common;
+
+using Image = Android.Media.Image;
+using Paint = Android.Graphics.Paint;
+using RectF = Microsoft.Maui.Graphics.RectF;
+using Size = Android.Util.Size;
+using ARectF = Android.Graphics.RectF;
+
+namespace InABox.Avalonia.Platform.Android.Barcodes;
+
+public static partial class Methods
+{
+    private static readonly bool neonSupported = IsNeonSupported();
+    private static readonly ParallelOptions parallelOptions = new()
+    {
+        MaxDegreeOfParallelism = Environment.ProcessorCount * 2
+    };
+    
+    public static async Task<IReadOnlySet<BarcodeResult>> ScanFromImageAsync(byte[] imageArray)
+        => await ProcessBitmapAsync(await BitmapFactory.DecodeByteArrayAsync(imageArray, 0, imageArray.Length));
+    public static async Task<IReadOnlySet<BarcodeResult>> ScanFromImageAsync(FileResult file)
+        => await ProcessBitmapAsync(await BitmapFactory.DecodeStreamAsync(await file.OpenReadAsync()));
+    public static async Task<IReadOnlySet<BarcodeResult>> ScanFromImageAsync(string url)
+        => await ProcessBitmapAsync(await BitmapFactory.DecodeStreamAsync(new URL(url).OpenStream()));
+    public static async Task<IReadOnlySet<BarcodeResult>> ScanFromImageAsync(Stream stream)
+        => await ProcessBitmapAsync(await BitmapFactory.DecodeStreamAsync(stream));
+    private static async Task<IReadOnlySet<BarcodeResult>> ProcessBitmapAsync(Bitmap? bitmap)
+    {
+        var barcodeResults = new HashSet<BarcodeResult>();
+
+        if (bitmap is null)
+            return barcodeResults;
+        
+        using var scanner = BarcodeScanning.GetClient(new BarcodeScannerOptions.Builder()
+            .SetBarcodeFormats(Barcode.FormatAllFormats)
+            .Build());
+
+        using var image = InputImage.FromBitmap(bitmap, 0);
+        using var results = await scanner.Process(image).AsAsync<Java.Lang.Object>();
+        ProcessBarcodeResult(results, barcodeResults);
+
+        using var invertedBitmap = Bitmap.CreateBitmap(bitmap.Height, bitmap.Width, bitmap.GetConfig());
+        using var canvas = new Canvas(invertedBitmap);
+        using var paint = new Paint();
+        using var matrixInvert = new ColorMatrix();
+
+        matrixInvert.Set(
+        [
+            -1.0f,  0.0f,  0.0f, 0.0f, 255.0f,
+			 0.0f, -1.0f,  0.0f, 0.0f, 255.0f,
+			 0.0f,  0.0f, -1.0f, 0.0f, 255.0f,
+			 0.0f,  0.0f,  0.0f, 1.0f, 0.0f
+        ]);
+
+        using var filter = new ColorMatrixColorFilter(matrixInvert);
+        paint.SetColorFilter(filter);
+        canvas.DrawBitmap(bitmap, 0, 0, paint);
+
+        using var invertedImage = InputImage.FromBitmap(invertedBitmap, 0);
+        using var invertedResults = await scanner.Process(invertedImage).AsAsync<Java.Lang.Object>();
+        ProcessBarcodeResult(invertedResults, barcodeResults);
+
+        return barcodeResults;
+    }
+    
+    private static void ProcessBarcodeResult(Java.Lang.Object? inputResults, HashSet<BarcodeResult> outputResults)
+    {
+        if (inputResults is not JavaList javaList)
+            return;
+
+        foreach (Barcode barcode in javaList)
+        {
+            if (barcode is null)
+                continue;
+            if (string.IsNullOrEmpty(barcode.DisplayValue) && string.IsNullOrEmpty(barcode.RawValue))
+                continue;
+
+            outputResults.Add(barcode.AsBarcodeResult());
+        }
+    }
+
+    // [LibraryImport("libInvertBytes.so")]
+    // private static partial int InvertBytes(IntPtr data, int length);
+
+    internal static void InvertLuminance(Image image)
+    {
+        var yBuffer = image.GetPlanes()?[0].Buffer;
+        if (yBuffer is null)
+            return;
+
+        if (yBuffer.IsDirect)
+        {
+            var data = yBuffer.GetDirectBufferAddress();
+            var length = yBuffer.Capacity();
+
+            if (!neonSupported/* || InvertBytes(data, length) != 0*/)
+            {
+                unsafe
+                {
+                    var dataPtr = (ulong*)data; 
+                    Parallel.For(0, length >> 3, parallelOptions, (i) => dataPtr[i] = ~dataPtr[i]);
+                }
+            }
+        }
+        else
+        {
+            using var bits = BitSet.ValueOf(yBuffer);
+            bits?.Flip(0, bits.Length());
+            yBuffer.Rewind();
+            yBuffer.Put(bits?.ToByteArray() ?? []);
+        }
+    }
+
+    internal static BarcodeTypes ConvertBarcodeResultTypes(int barcodeValueType)
+    {
+        return barcodeValueType switch
+        {
+            Barcode.TypeCalendarEvent => BarcodeTypes.CalendarEvent,
+            Barcode.TypeContactInfo => BarcodeTypes.ContactInfo,
+            Barcode.TypeDriverLicense => BarcodeTypes.DriversLicense,
+            Barcode.TypeEmail => BarcodeTypes.Email,
+            Barcode.TypeGeo => BarcodeTypes.GeographicCoordinates,
+            Barcode.TypeIsbn => BarcodeTypes.Isbn,
+            Barcode.TypePhone => BarcodeTypes.Phone,
+            Barcode.TypeProduct => BarcodeTypes.Product,
+            Barcode.TypeSms => BarcodeTypes.Sms,
+            Barcode.TypeText => BarcodeTypes.Text,
+            Barcode.TypeUrl => BarcodeTypes.Url,
+            Barcode.TypeWifi => BarcodeTypes.WiFi,
+            _ => BarcodeTypes.Unknown
+        };
+    }
+
+    internal static int ConvertBarcodeFormats(BarcodeFormats barcodeFormats)
+    {
+        var formats = Barcode.FormatAllFormats;
+
+        if (barcodeFormats.HasFlag(BarcodeFormats.Code128))
+            formats |= Barcode.FormatCode128;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Code39))
+            formats |= Barcode.FormatCode39;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Code93))
+            formats |= Barcode.FormatCode93;
+        if (barcodeFormats.HasFlag(BarcodeFormats.CodaBar))
+            formats |= Barcode.FormatCodabar;
+        if (barcodeFormats.HasFlag(BarcodeFormats.DataMatrix))
+            formats |= Barcode.FormatDataMatrix;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Ean13))
+            formats |= Barcode.FormatEan13;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Ean8))
+            formats |= Barcode.FormatEan8;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Itf))
+            formats |= Barcode.FormatItf;
+        if (barcodeFormats.HasFlag(BarcodeFormats.QRCode))
+            formats |= Barcode.FormatQrCode;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Upca))
+            formats |= Barcode.FormatUpcA;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Upce))
+            formats |= Barcode.FormatUpcE;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Pdf417))
+            formats |= Barcode.FormatPdf417;
+        if (barcodeFormats.HasFlag(BarcodeFormats.Aztec))
+            formats |= Barcode.FormatAztec;
+        if (barcodeFormats.HasFlag(BarcodeFormats.All))
+            formats = Barcode.FormatAllFormats;
+        return formats;
+    }
+
+    internal static Size TargetResolution(CaptureQuality? captureQuality)
+    {
+        return captureQuality switch
+        {
+            CaptureQuality.Low => new Size(854, 480),
+            CaptureQuality.Medium => new Size(1280, 720),
+            CaptureQuality.High => new Size(1920, 1080),
+            CaptureQuality.Highest => new Size(3840, 2160),
+            _ => new Size(1280, 720)
+        };
+    }
+
+    private static bool IsNeonSupported()
+    {
+        try
+        {
+            var info = File.ReadAllText("/proc/cpuinfo");
+            return info.Contains("neon") || info.Contains("asimd");
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+    }
+
+    internal static RectF AsRectangleF(this ARectF rect)
+    {
+        return new(rect.Left, rect.Top, rect.Width(), rect.Height());
+    }
+
+    internal static BarcodeResult AsBarcodeResult(this Barcode barcode, CoordinateTransform? coordinateTransform = null)
+    {
+        RectF imageRect, previewRect;
+        if (barcode.BoundingBox is null)
+        {
+            imageRect = RectF.Zero;
+            previewRect = RectF.Zero;
+        }
+        else
+        {
+            using var barcodeBox = new ARectF(barcode.BoundingBox);
+            imageRect = barcodeBox.AsRectangleF();
+
+            if (coordinateTransform is null)
+            {
+                previewRect = new();
+            }
+            else
+            {
+                coordinateTransform.MapRect(barcodeBox);
+                previewRect = barcodeBox.AsRectangleF();
+            }
+        }
+
+        return new BarcodeResult()
+        {
+            BarcodeType = Methods.ConvertBarcodeResultTypes(barcode.ValueType),
+            BarcodeFormat = (BarcodeFormats)barcode.Format,
+            DisplayValue = barcode.DisplayValue ?? string.Empty,
+            RawValue = barcode.RawValue ?? string.Empty,
+            RawBytes = barcode.GetRawBytes() ?? [],
+            PreviewBoundingBox = previewRect,
+            ImageBoundingBox = imageRect
+        };
+    }
+}

+ 8 - 2
InABox.Avalonia.Platform.Android/InABox.Avalonia.Platform.Android.csproj

@@ -7,6 +7,7 @@
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <LangVersion>default</LangVersion>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     </PropertyGroup>
 
     <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -22,10 +23,15 @@
     </ItemGroup>
     
     <ItemGroup>
-      <PackageReference Include="Avalonia.Android" Version="11.2.2" />
+      <PackageReference Include="Avalonia.Android" Version="11.3.2" />
       <PackageReference Include="bblanchon.PDFium.Android" Version="136.0.7073" />
-      <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.71" />
+      <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.90" />
       <PackageReference Include="PDFtoImage" Version="4.1.1" />
+      <PackageReference Include="System.Reactive" Version="6.0.1" />
+      <PackageReference Include="Xamarin.AndroidX.Camera.Camera2" Version="1.4.2.3" />
+      <PackageReference Include="Xamarin.AndroidX.Camera.View" Version="1.4.2.3" />
+      <PackageReference Include="Xamarin.AndroidX.CoordinatorLayout" Version="1.2.0.19" />
+      <PackageReference Include="Xamarin.Google.MLKit.BarcodeScanning" Version="117.3.0.5" />
     </ItemGroup>
 
 </Project>

+ 1 - 1
InABox.Avalonia.Platform.Desktop/InABox.Avalonia.Platform.Desktop.csproj

@@ -16,7 +16,7 @@
 
     <ItemGroup>
         <PackageReference Include="bblanchon.PDFium.Win32" Version="135.0.7019" />
-        <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.71" />
+        <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.90" />
         <PackageReference Include="PDFtoImage" Version="4.1.1" />
         <PackageReference Include="SkiaSharp" Version="2.88.9" />
         <PackageReference Include="Syncfusion.Pdf.Wpf" Version="29.2.7" />

+ 35 - 0
InABox.Avalonia.Platform/Barcodes/BarcodeFormats.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Platform.Barcodes;
+
+[Flags]
+public enum BarcodeFormats
+{
+    None = 0,
+
+    Code128 = 1 << 0,
+    Code39 = 1 << 1,
+    Code93 = 1 << 2,
+    CodaBar = 1 << 3,
+    DataMatrix = 1 << 4,
+    Ean13 = 1 << 5,
+    Ean8 = 1 << 6,
+    Itf = 1 << 7,
+    QRCode = 1 << 8,
+    Upca = 1 << 9,
+    Upce = 1 << 10,
+    Pdf417 = 1 << 11,
+    Aztec = 1 << 12,
+    MicroQR = 1 << 13,
+    MicroPdf417 = 1 << 14,
+    I20F5 = 1 << 15,
+    GS1DataBar = 1 << 16,
+    MaxiCode = 1 << 17,
+    DXFilmEdge = 1 << 18,
+
+    All = ~0
+}

+ 43 - 0
InABox.Avalonia.Platform/Barcodes/BarcodeResult.cs

@@ -0,0 +1,43 @@
+using Microsoft.Maui.Graphics;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Platform.Barcodes;
+
+public class BarcodeResult : IEquatable<BarcodeResult>
+{
+    public required BarcodeTypes BarcodeType { get; init; }
+    public required BarcodeFormats BarcodeFormat { get; init; }
+    public required string DisplayValue { get; init; }
+    public required string RawValue { get; init; }
+    public required byte[] RawBytes { get; init; }
+    public required RectF PreviewBoundingBox { get; init; }
+    public required RectF ImageBoundingBox { get; init; }
+
+    public bool Equals(BarcodeResult? other)
+    {
+        if (other is null)
+            return false;
+
+        if (!string.IsNullOrEmpty(RawValue))
+        {
+            return RawValue == other.RawValue && ImageBoundingBox.IntersectsWith(other.ImageBoundingBox);
+        }
+        else
+        {
+            return DisplayValue == other.DisplayValue && ImageBoundingBox.IntersectsWith(other.ImageBoundingBox);
+        }
+    }
+
+    public override bool Equals(object? obj)
+    {
+        return obj is BarcodeResult result && Equals(result);
+    }
+    public override int GetHashCode()
+    {
+        return !string.IsNullOrEmpty(RawValue) ? RawValue.GetHashCode() : DisplayValue.GetHashCode();
+    }
+}

+ 24 - 0
InABox.Avalonia.Platform/Barcodes/BarcodeTypes.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Platform.Barcodes;
+
+public enum BarcodeTypes
+{
+    Unknown = 0,
+    ContactInfo = 1,
+    Email = 2,
+    Isbn = 3,
+    Phone = 4,
+    Product = 5,
+    Sms = 6,
+    Text = 7,
+    Url = 8,
+    WiFi = 9,
+    GeographicCoordinates = 10,
+    CalendarEvent = 11,
+    DriversLicense = 12
+}

+ 13 - 0
InABox.Avalonia.Platform/Barcodes/CameraFacing.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Platform.Barcodes;
+
+public enum CameraFacing
+{
+    Back = 0,
+    Front = 1
+}

+ 9 - 0
InABox.Avalonia.Platform/Barcodes/CameraView/CameraView.axaml

@@ -0,0 +1,9 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+			 xmlns:barcodes="using:InABox.Avalonia.Platform.Barcodes"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="InABox.Avalonia.Platform.Barcodes.CameraView">
+	<barcodes:CameraViewControlHost Name="Host"/>
+</UserControl>

+ 251 - 0
InABox.Avalonia.Platform/Barcodes/CameraView/CameraView.axaml.cs

@@ -0,0 +1,251 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using InABox.Core;
+using Microsoft.Maui.Graphics.Platform;
+using System.Timers;
+using System.Windows.Input;
+using Timer = System.Timers.Timer;
+
+namespace InABox.Avalonia.Platform.Barcodes;
+
+public interface ICameraViewControl : ILoggable
+{
+    IPlatformHandle CreateControl(CameraView view, IPlatformHandle parent);
+}
+public class DefaultCameraViewControl : ICameraViewControl
+{
+    public Logger? Logger { get; set; }
+
+    public IPlatformHandle CreateControl(CameraView view, IPlatformHandle parent)
+    {
+        return null;
+    }
+}
+
+public partial class CameraView : UserControl
+{
+    public static readonly StyledProperty<ICommand> OnDetectionFinishedCommandProperty = 
+        AvaloniaProperty.Register<CameraView, ICommand>(nameof(OnDetectionFinishedCommand));
+    public ICommand OnDetectionFinishedCommand
+    {
+        get => GetValue(OnDetectionFinishedCommandProperty);
+        set => SetValue(OnDetectionFinishedCommandProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand> OnImageCapturedCommandProperty = 
+        AvaloniaProperty.Register<CameraView, ICommand>(nameof(OnImageCapturedCommand));
+    public ICommand OnImageCapturedCommand
+    {
+        get => GetValue(OnImageCapturedCommandProperty);
+        set => SetValue(OnImageCapturedCommandProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> VibrationOnDetectedProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(VibrationOnDetected), false);
+    public bool VibrationOnDetected
+    {
+        get => GetValue(VibrationOnDetectedProperty);
+        set => SetValue(VibrationOnDetectedProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> CameraEnabledProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(CameraEnabled), false);
+    public bool CameraEnabled
+    {
+        get => GetValue(CameraEnabledProperty);
+        set => SetValue(CameraEnabledProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> PauseScanningProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(PauseScanning), false);
+    public bool PauseScanning
+    {
+        get => GetValue(PauseScanningProperty);
+        set => SetValue(PauseScanningProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> ForceInvertedProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(ForceInverted), false);
+    public bool ForceInverted
+    {
+        get => GetValue(ForceInvertedProperty);
+        set => SetValue(ForceInvertedProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> TorchOnProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(TorchOn), false);
+    public bool TorchOn
+    {
+        get => GetValue(TorchOnProperty);
+        set => SetValue(TorchOnProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> TapToFocusEnabledProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(TapToFocusEnabled), false);
+    public bool TapToFocusEnabled
+    {
+        get => GetValue(TapToFocusEnabledProperty);
+        set => SetValue(TapToFocusEnabledProperty, value);
+    }
+
+    public static readonly StyledProperty<CameraFacing> CameraFacingProperty = 
+        AvaloniaProperty.Register<CameraView, CameraFacing>(nameof(CameraFacing), CameraFacing.Back);
+    public CameraFacing CameraFacing
+    {
+        get => GetValue(CameraFacingProperty);
+        set => SetValue(CameraFacingProperty, value);
+    }
+
+    public static readonly StyledProperty<CaptureQuality> CaptureQualityProperty = 
+        AvaloniaProperty.Register<CameraView, CaptureQuality>(nameof(CaptureQuality), CaptureQuality.Medium);
+    public CaptureQuality CaptureQuality
+    {
+        get => GetValue(CaptureQualityProperty);
+        set => SetValue(CaptureQualityProperty, value);
+    }
+
+    public static readonly StyledProperty<BarcodeFormats> BarcodeSymbologiesProperty = 
+        AvaloniaProperty.Register<CameraView, BarcodeFormats>(nameof(BarcodeSymbologies), BarcodeFormats.All);
+    public BarcodeFormats BarcodeSymbologies
+    {
+        get => GetValue(BarcodeSymbologiesProperty);
+        set => SetValue(BarcodeSymbologiesProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> AimModeProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(AimMode), false);
+    public bool AimMode
+    {
+        get => GetValue(AimModeProperty);
+        set => SetValue(AimModeProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> ViewfinderModeProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(ViewfinderMode), false);
+    public bool ViewfinderMode
+    {
+        get => GetValue(ViewfinderModeProperty);
+        set => SetValue(ViewfinderModeProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> CaptureNextFrameProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(CaptureNextFrame), false);
+    public bool CaptureNextFrame
+    {
+        get => GetValue(CaptureNextFrameProperty);
+        set => SetValue(CaptureNextFrameProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> ForceFrameCaptureProperty = 
+        AvaloniaProperty.Register<CameraView, bool>(nameof(ForceFrameCapture), false);
+    public bool ForceFrameCapture
+    {
+        get => GetValue(ForceFrameCaptureProperty);
+        set => SetValue(ForceFrameCaptureProperty, value);
+    }
+
+    public static readonly StyledProperty<float> RequestZoomFactorProperty = 
+        AvaloniaProperty.Register<CameraView, float>(nameof(RequestZoomFactor), -1f);
+    public float RequestZoomFactor
+    {
+        get => GetValue(RequestZoomFactorProperty);
+        set => SetValue(RequestZoomFactorProperty, value);
+    }
+
+    public static readonly StyledProperty<float> CurrentZoomFactorProperty = 
+        AvaloniaProperty.Register<CameraView, float>(nameof(CurrentZoomFactor), -1f);
+    public float CurrentZoomFactor
+    {
+        get => GetValue(CurrentZoomFactorProperty);
+        set => SetValue(CurrentZoomFactorProperty, value);
+    }
+
+    public static readonly StyledProperty<float> MinZoomFactorProperty = 
+        AvaloniaProperty.Register<CameraView, float>(nameof(MinZoomFactor), -1f);
+    public float MinZoomFactor
+    {
+        get => GetValue(MinZoomFactorProperty);
+        set => SetValue(MinZoomFactorProperty, value);
+    }
+
+    public static readonly StyledProperty<float> MaxZoomFactorProperty = 
+        AvaloniaProperty.Register<CameraView, float>(nameof(MaxZoomFactor), -1f);
+    public float MaxZoomFactor
+    {
+        get => GetValue(MaxZoomFactorProperty);
+        set => SetValue(MaxZoomFactorProperty, value);
+    }
+
+    public static readonly StyledProperty<float[]> DeviceSwitchZoomFactorProperty = 
+        AvaloniaProperty.Register<CameraView, float[]>(nameof(DeviceSwitchZoomFactor), []);
+    public float[] DeviceSwitchZoomFactor
+    {
+        get => GetValue(DeviceSwitchZoomFactorProperty);
+        set => SetValue(DeviceSwitchZoomFactorProperty, value);
+    }
+
+    public CameraView()
+    {
+        InitializeComponent();
+
+        LayoutUpdated += CameraView_LayoutUpdated;
+    }
+
+    private void CameraView_LayoutUpdated(object? sender, EventArgs e)
+    {
+        Host.TryUpdateNativeControlPosition();
+    }
+
+    public void DetectionFinished(HashSet<BarcodeResult> barCodeResults)
+    {
+        // if (_poolingTimer.Enabled)
+        // {
+        //     _poolingTimer.Stop();
+        // }
+        TriggerOnDetectionFinished(barCodeResults);
+    }
+
+    private void TriggerOnDetectionFinished(HashSet<BarcodeResult> barCodeResults)
+    {
+        Dispatcher.UIThread.InvokeAsync(() =>
+        {
+            if(OnDetectionFinishedCommand?.CanExecute(barCodeResults) ?? false)
+            {
+                OnDetectionFinishedCommand?.Execute(barCodeResults);
+            }
+        });
+    }
+    public void TriggerOnImageCaptured(PlatformImage image)
+    {
+        Dispatcher.UIThread.InvokeAsync(() =>
+        {
+            CaptureNextFrame = false;
+
+            if (PauseScanning)
+                return;
+
+            // OnImageCaptured?.Invoke(this, new OnImageCapturedEventArg { Image = image });
+            if (OnImageCapturedCommand?.CanExecute(image) ?? false)
+                OnImageCapturedCommand?.Execute(image);
+        });
+    }
+}
+
+internal class CameraViewControlHost : NativeControlHost
+{
+    protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
+    {
+        if(Parent is CameraView cameraView)
+        {
+            return PlatformTools.CameraViewControl.CreateControl(cameraView, parent)
+                ?? base.CreateNativeControlCore(parent);
+        }
+        else
+        {
+            return base.CreateNativeControlCore(parent);
+        }
+    }
+}

+ 15 - 0
InABox.Avalonia.Platform/Barcodes/CaptureQuality.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Platform.Barcodes;
+
+public enum CaptureQuality
+{
+    Low = 0,
+    Medium = 1,
+    High = 2,
+    Highest = 3
+}

+ 1 - 1
InABox.Avalonia.Platform/InABox.Avalonia.Platform.csproj

@@ -11,7 +11,7 @@
 
     <ItemGroup>
       <PackageReference Include="Autofac" Version="8.4.0" />
-      <PackageReference Include="Avalonia" Version="11.3.2" />
+      <PackageReference Include="Avalonia" Version="11.2.2" />
       <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.90" />
     </ItemGroup>
 

+ 11 - 0
InABox.Avalonia.Platform/PlatformTools.cs

@@ -1,5 +1,6 @@
 using Autofac;
 using InABox.Avalonia.Platform;
+using InABox.Avalonia.Platform.Barcodes;
 using InABox.Core;
 using Exception = System.Exception;
 
@@ -50,6 +51,16 @@ public static class PlatformTools
             return _pdfRenderer;
         }
     }
+
+    private static ICameraViewControl? _cameraViewControl;
+    public static ICameraViewControl CameraViewControl
+    {
+        get
+        {
+            _cameraViewControl ??= Resolve<ICameraViewControl, DefaultCameraViewControl>();
+            return _cameraViewControl;
+        }
+    }
     
     public static Guid DigitalKeyServiceId = Guid.Parse("ce6c0b18-0000-1000-8000-00805F9B34FB");
     public static Guid DigitalKeyConfigId = Guid.Parse("447c1982-77ef-49be-a39a-2920f33c31e5");