How to Build Xamarin.Forms Barcode QR Code Scanner

Xamarin.Forms lets .NET developers create cross-platform mobile apps for Android and iOS in C#. This article demonstrates how to scan barcode and QR code from image file and live video stream using Xamarin.Forms Custom Renderers and Dynamsoft Barcode Reader SDK. In the end of this article, we will also demonstrate how to use Dynamsoft.CaptureVision.Xamarin.Forms to simplify the development process. If you are only interested in how to quickly build a barcode scanner app using Xamarin.Forms, you can skip to the last section.

Getting Started with Xamarin.Forms Custom Renderers

Our goal is to create camera preview interface and invoke Dynamsoft Barcode Reader SDK for decoding barcode and QR code, thus it is inevitable to put lots of effort into platform-specific code. Fortunately, it is not necessary to reinvent the wheel. There are some official samples demonstrating how to use the custom renders to bridge the shared code and platform-specific code.

The camera preview samples include:

  • ContentPage
    • Camera API for Android
    • Native code takes over the whole content page rendering
  • View
    • Camera2 API for Android
    • Native code renders the custom view

Although Android camera API is out of date, it is still a good choice due to its simplicity. Therefore, we pick the view example as our codebase and replace the camera2 API with camera API for Android.

Get the source code via svn command in terminal:

svn checkout https://github.com/xamarin/xamarin-forms-samples/trunk/CustomRenderers/View

Implementing Xamarin.Forms Barcode QR Code Scanner

In the following paragraphs, we will show how to implement the barcode and QR code scanning feature in Xamarin.Forms. The steps include installing Dynamsoft Barcode Reader SDK, reading barcode and QR code from image file and live video stream, and drawing the results on overlay.

Install Dynamsoft Xamarin Barcode SDK for Android and iOS

Let’s open nuget package manager to search for dynamsoft xamarin barcode in Visual Studio.

Xamarin barcode QR code SDK

There are two search results: one for Android and one for iOS. Xamarin SDK is not Xamarin.Forms-compatible, so you have to install them separately for Android and iOS projects.

Xamarin SDK installation

Content Pages

The projects consists of three content pages: main page, picture page and camera page.

The MainPage.xaml includes two buttons for page navigation.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
			 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
			 x:Class="BarcodeQrScanner.MainPage"
			 Title="Main Page">
	<ContentPage.Content>
		<StackLayout>
			<Button x:Name="takePhotoButton" Text="Take Photo" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnTakePhotoButtonClicked" />
            <Button x:Name="takeVideoButton" Text="Take Video" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnTakeVideoButtonClicked" />
		</StackLayout>
	</ContentPage.Content>
</ContentPage>

The PicturePage.xaml includes a label for displaying the barcode QR code scanning results, and a SKCanvasView for drawing the loaded image and overlay.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="BarcodeQrScanner.PicturePage"
             Title="Picture Page">
    <ContentPage.Content>
        <StackLayout>
            <Label FontSize="18"
                FontAttributes="Bold"
                x:Name="ResultLabel"
                   Text="Results"
                HorizontalOptions="Start"/>
            <skia:SKCanvasView x:Name="canvasView"
                           WidthRequest="640" HeightRequest="640"
                           PaintSurface="OnCanvasViewPaintSurface" />
            
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

The CameraPage.xaml includes a custom camera view for live video stream, a label for barcode QR code results, and a SKCanvasView for overlay. The layout here is Grid rather than StackLayout because the camera view is a full screen view and other elements are placed on top of it.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
			 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:local="clr-namespace:BarcodeQrScanner;assembly=BarcodeQrScanner"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
			 x:Class="BarcodeQrScanner.CameraPage"
			 Title="Camera Page">
    <Grid x:Name="scannerView" Margin="0">
        <local:CameraPreview 
            x:Name="cameraView"
            Camera="Rear"
            ScanMode="Multiple"
			HorizontalOptions="FillAndExpand"
			VerticalOptions="FillAndExpand" 
            ResultReady="CameraPreview_ResultReady"/>
        <Label FontSize="18"
                FontAttributes="Bold"
               TextColor="Blue"
                x:Name="ResultLabel"
                   Text="Results"
                HorizontalOptions="Center"
               VerticalOptions="Center" />
        <skia:SKCanvasView x:Name="canvasView"
                           Margin="0"
                           HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

    </Grid>
</ContentPage>

Xamarin.Forms DependencyService

Barcode reader object needs to be created in native code. To invoke native platform functionality from shared code, we use the DependencyService class.

  1. In IBarcodeQRCodeService.cs, define an IBarcodeQRCodeService interface and a BarcodeQrData structure. The IBarcodeQRCodeService interface contains a method for initializing the SDK license and a method for decoding barcode and QR code from a file. The BarcodeQrData structure contains the barcode and QR code results.

     using SkiaSharp;
     using System;
     using System.Collections.Generic;
     using System.IO;
     using System.Text;
     using System.Threading.Tasks;
     using Xamarin.Forms;
    
     namespace BarcodeQrScanner.Services
     {
         public class BarcodeQrData
         {
             public string text;
             public string format;
             public SKPoint[] points;
         }
    
         public interface IBarcodeQRCodeService
         {
             Task<int> InitSDK(string license);
             Task<BarcodeQrData[]> DecodeFile(string filePath);
         }
     }
    
  2. Implement the interface in native BarcodeQRCodeService.cs file.

    Android

     using Android.App;
     using Android.Content;
     using Android.OS;
     using Android.Runtime;
     using Android.Views;
     using Android.Widget;
     using Com.Dynamsoft.Dbr;
     using BarcodeQrScanner.Droid.Services;
     using BarcodeQrScanner.Services;
     using SkiaSharp;
     using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Text;
     using System.Threading.Tasks;
     using Xamarin.Forms;
    
     [assembly: Dependency(typeof(BarcodeQRCodeService))]
     namespace BarcodeQrScanner.Droid.Services
     {
         public class DBRLicenseVerificationListener : Java.Lang.Object, IDBRLicenseVerificationListener
         {
             public void DBRLicenseVerificationCallback(bool isSuccess, Java.Lang.Exception error)
             {
                 if (!isSuccess)
                 {
                     System.Console.WriteLine(error.Message);
                 }
             }
         }
    
         public class BarcodeQRCodeService: IBarcodeQRCodeService
         {
             BarcodeReader reader;
    
             Task<int> IBarcodeQRCodeService.InitSDK(string license)
             {
                 BarcodeReader.InitLicense(license, new DBRLicenseVerificationListener());
                 reader = new BarcodeReader();
                 TaskCompletionSource<int> taskCompletionSource = new TaskCompletionSource<int>();
                 taskCompletionSource.SetResult(0);
                 return taskCompletionSource.Task;
             }
    
             Task<BarcodeQrData[]> IBarcodeQRCodeService.DecodeFile(string filePath)
             {
                 BarcodeQrData[] output = null;
                 try
                 {
                     PublicRuntimeSettings settings = reader.RuntimeSettings;
                     settings.ExpectedBarcodesCount = 0;
                     reader.UpdateRuntimeSettings(settings);
                     TextResult[] results = reader.DecodeFile(filePath);
                     if (results != null)
                     {
                         output = new BarcodeQrData[results.Length];
                         int index = 0;
                         foreach (TextResult result in results)
                         {
                             BarcodeQrData data = new BarcodeQrData();
                             data.text = result.BarcodeText;
                             data.format = result.BarcodeFormatString;
                             LocalizationResult localizationResult = result.LocalizationResult;
                             data.points = new SKPoint[localizationResult.ResultPoints.Count];
                             int pointsIndex = 0;
                             foreach (Com.Dynamsoft.Dbr.Point point in localizationResult.ResultPoints)
                             {
                                 SKPoint p = new SKPoint();
                                 p.X = point.X;
                                 p.Y = point.Y;
                                 data.points[pointsIndex++] = p;
                             }
                             output[index++] = data;
                         }
                     }
                 }
                 catch (Exception e)
                 {
                 }
    
                 TaskCompletionSource<BarcodeQrData[]> taskCompletionSource = new TaskCompletionSource<BarcodeQrData[]>();
                 taskCompletionSource.SetResult(output);
                 return taskCompletionSource.Task;
             }
         }
     }
    

    iOS

     using System;
     using Xamarin.Forms;
     using BarcodeQrScanner.Services;
     using DBRiOS;
     using BarcodeQrScanner.iOS.Services;
     using System.Threading.Tasks;
     using Foundation;
     using SkiaSharp;
    
     [assembly: Dependency(typeof(BarcodeQRCodeService))]
     namespace BarcodeQrScanner.iOS.Services
     {
         public class DBRLicenseVerificationListener : NSObject, IDBRLicenseVerificationListener
         {
             public void DBRLicenseVerificationCallback(bool isSuccess, NSError error)
             {
                 if (error != null)
                 {
                     System.Console.WriteLine(error.UserInfo);
                 }
             }
         }
    
         public class BarcodeQRCodeService: IBarcodeQRCodeService
         {
             DynamsoftBarcodeReader reader;
    
                
    
             Task<int> IBarcodeQRCodeService.InitSDK(string license)
             {
                 DynamsoftBarcodeReader.InitLicense(license, new DBRLicenseVerificationListener());
                 reader = new DynamsoftBarcodeReader();
                 TaskCompletionSource<int> taskCompletionSource = new TaskCompletionSource<int>();
                 taskCompletionSource.SetResult(0);
                 return taskCompletionSource.Task;
             }
    
             Task<BarcodeQrData[]> IBarcodeQRCodeService.DecodeFile(string filePath)
             {
                 BarcodeQrData[] output = null;
                 try
                 {
                     NSError error;
    
                     iPublicRuntimeSettings settings = reader.GetRuntimeSettings(out error);
                     settings.ExpectedBarcodesCount = 0;
                     reader.UpdateRuntimeSettings(settings, out error);
                        
                     iTextResult[] results = reader.DecodeFileWithName(filePath, "", out error);
                     if (results != null)
                     {
                         output = new BarcodeQrData[results.Length];
                         int index = 0;
                         foreach (iTextResult result in results)
                         {
                             BarcodeQrData data = new BarcodeQrData();
                             data.text = result.BarcodeText;
                             data.format = result.BarcodeFormatString;
                             iLocalizationResult localizationResult = result.LocalizationResult;
                             data.points = new SKPoint[localizationResult.ResultPoints.Length];
                             int pointsIndex = 0;
                             foreach (NSObject point in localizationResult.ResultPoints)
                             {
                                 SKPoint p = new SKPoint();
                                 p.X = (float)((NSValue)point).CGPointValue.X;
                                 p.Y = (float)((NSValue)point).CGPointValue.Y;
                                 data.points[pointsIndex++] = p;
                             }
                             output[index++] = data;
                         }
                     }
                 }
                 catch (Exception e)
                 {
                 }
    
                 TaskCompletionSource<BarcodeQrData[]> taskCompletionSource = new TaskCompletionSource<BarcodeQrData[]>();
                 taskCompletionSource.SetResult(output);
                 return taskCompletionSource.Task;
             }
    
         }
     }
    
  3. Use the DependencyService.Get<T> method to resolve the native implementation in MainPage.xaml.cs.

     _barcodeQRCodeService = DependencyService.Get<IBarcodeQRCodeService>();
     await Task.Run(() =>
     {
         try
         {
             _barcodeQRCodeService.InitSDK("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==");
         }
         catch (Exception ex)
         {
             DisplayAlert("Error", ex.Message, "OK");
         }
    
         return Task.CompletedTask;
     });
    

    You can get the license key from here.

Scan Barcode and QR Code from Image File

To take pictures with the camera, we use MediaPicker, which is provided by Xamarin.Essentials.

The following configurations are required for camera access on Android and iOS:

  • Android AndroidManifest.xml:

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.BarcodeQrScanner" android:installLocation="auto">
          <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
          <application android:label="BarcodeQrScanner"></application>
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
      <uses-permission android:name="android.permission.CAMERA" />
      <queries>
          <intent>
          <action android:name="android.media.action.IMAGE_CAPTURE" />
          </intent>
      </queries>
      </manifest>
    
  • iOS Info.plist:

      <key>UILaunchStoryboardName</key>
      <string>LaunchScreen</string>
      <key>CFBundleName</key>
      <string>BarcodeQrScanner</string>
      <key>NSCameraUsageDescription</key>
      <string>This app is using the camera</string>
      <key>NSPhotoLibraryAddUsageDescription</key>
      <string>This app is saving photo to the library</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>
    

We are going to call the MediaPicker.PickPhotoAsync method to take a photo and save then it to local disk.

async void OnTakePhotoButtonClicked (object sender, EventArgs e)
{
    try
    {
        var photo = await MediaPicker.CapturePhotoAsync();
        await LoadPhotoAsync(photo);
        Console.WriteLine($"CapturePhotoAsync COMPLETED: {PhotoPath}");
    }
    catch (FeatureNotSupportedException fnsEx)
    {
        // Feature is not supported on the device
    }
    catch (PermissionException pEx)
    {
        // Permissions not granted
    }
    catch (Exception ex)
    {
        Console.WriteLine($"CapturePhotoAsync THREW: {ex.Message}");
    }
}

async Task LoadPhotoAsync(FileResult photo)
{
    // canceled
    if (photo == null)
    {
        PhotoPath = null;
        return;
    }
    // save the file into local storage
    var newFile = Path.Combine(FileSystem.CacheDirectory, photo.FileName);
    using (var stream = await photo.OpenReadAsync())
    using (var newStream = File.OpenWrite(newFile))
        await stream.CopyToAsync(newStream);

    PhotoPath = newFile;

    await Navigation.PushAsync(new PicturePage(PhotoPath, _barcodeQRCodeService));
}

Afterwards, pass the photo path and the IBarcodeQRCodeService instance to the PicturePage page for further processing.

namespace BarcodeQrScanner
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class PicturePage : ContentPage
    {
        string path;
        SKBitmap bitmap;
        IBarcodeQRCodeService _barcodeQRCodeService;
        public PicturePage(string imagepath, IBarcodeQRCodeService barcodeQRCodeService)
        {
            InitializeComponent();
            _barcodeQRCodeService = barcodeQRCodeService;
            path = imagepath;
            try
            {
                using (var stream = new SKFileStream(imagepath))
                {
                    bitmap = SKBitmap.Decode(stream);
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

The OnCanvasViewPaintSurface method will be triggered when the element is ready to be rendered. We can draw the image bitmap and corresponding barcode and QR code results using SKCanvas.

async void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    var imageCanvas = new SKCanvas(bitmap);

    SKPaint skPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
    };

    BarcodeQrData[] data = await _barcodeQRCodeService.DecodeFile(path);
    ResultLabel.Text = "";
    if (data != null)
    {
        foreach (BarcodeQrData barcodeQrData in data)
        {
            ResultLabel.Text += barcodeQrData.text + "\n";
            imageCanvas.DrawLine(barcodeQrData.points[0], barcodeQrData.points[1], skPaint);
            imageCanvas.DrawLine(barcodeQrData.points[1], barcodeQrData.points[2], skPaint);
            imageCanvas.DrawLine(barcodeQrData.points[2], barcodeQrData.points[3], skPaint);
            imageCanvas.DrawLine(barcodeQrData.points[3], barcodeQrData.points[0], skPaint);
        }
    }
    else
    {
        ResultLabel.Text = "No barcode QR code found";
    }

    float scale = Math.Min((float)info.Width / bitmap.Width,
                        (float)info.Height / bitmap.Height);
    float x = (info.Width - scale * bitmap.Width) / 2;
    float y = (info.Height - scale * bitmap.Height) / 2;
    SKRect destRect = new SKRect(x, y, x + scale * bitmap.Width,
                                        y + scale * bitmap.Height);

    canvas.DrawBitmap(bitmap, destRect);
}

Xamarin.Forms barcode QR code reader

Scan Barcode and QR Code from Live Video Stream

Dynamsoft Barcode Reader supports multiple barcode and QR code detection. So we add a new property to the pre-created CameraPreview.cs file.

public enum ScanOptions
{
    Single,
    Multiple
}

public static readonly BindableProperty ScanProperty = BindableProperty.Create(
            propertyName: "ScanMode",
            returnType: typeof(ScanOptions),
            declaringType: typeof(CameraPreview),
            defaultValue: ScanOptions.Single);

public ScanOptions ScanMode
{
    get { return (ScanOptions)GetValue(ScanProperty); }
    set { SetValue(ScanProperty, value); }
}

To pass results from native code to shared code, we create a event handler and a callback function. The video frame width and height are used to calculate the scale factor for overlay drawing.

public class ResultReadyEventArgs : EventArgs
{
    public ResultReadyEventArgs(object result, int previewWidth, int previewHeight)
    {
        Result = result;
        PreviewWidth = previewWidth;
        PreviewHeight = previewHeight;
    }

    public object Result { get; private set; }
    public int PreviewWidth { get; private set; }
    public int PreviewHeight { get; private set; }

}

public event EventHandler<ResultReadyEventArgs> ResultReady;

public void NotifyResultReady(object result, int previewWidth, int previewHeight)
{
    if (ResultReady != null)
    {
        ResultReady(this, new ResultReadyEventArgs(result, previewWidth, previewHeight));
    }
}

The CameraPreviewRenderer.cs file is the entry point of native code. We need to first add the code logic for receiving the video frames.

Android

public class CameraPreviewRenderer : FrameLayout, IVisualElementRenderer, IViewRenderer, TextureView.ISurfaceTextureListener, IPreviewCallback, Handler.ICallback
{
    public void OnPreviewFrame(byte[] data, Android.Hardware.Camera camera)
    {
        try
        {
            YuvImage yuvImage = new YuvImage(data, ImageFormatType.Nv21,
                    previewWidth, previewHeight, null);
            stride = yuvImage.GetStrides();
            try
            {
                if (isReady)
                {
                    if (backgroundHandler != null)
                    {
                        isReady = false;
                        Message msg = new Message();
                        msg.What = 100;
                        msg.Obj = yuvImage;
                        backgroundHandler.SendMessage(msg);
                    }
                }
            }
            catch (BarcodeReaderException e)
            {
                e.PrintStackTrace();
            }
        }
        catch (System.IO.IOException)
        {
        }
    }

    void PrepareAndStartCamera()
    {
        camera.SetPreviewCallback(null);
        camera.StopPreview();

        var display = activity.WindowManager.DefaultDisplay;
        if (display.Rotation == SurfaceOrientation.Rotation0)
        {
            camera.SetDisplayOrientation(90);
        }

        if (display.Rotation == SurfaceOrientation.Rotation270)
        {
            camera.SetDisplayOrientation(180);
        }

        Parameters parameters = camera.GetParameters();
        previewWidth = parameters.PreviewSize.Width;
        previewHeight = parameters.PreviewSize.Height;
        if (parameters.SupportedFocusModes.Contains(Parameters.FocusModeContinuousVideo))
        {
            parameters.FocusMode = Parameters.FocusModeContinuousVideo;
        }
        camera.SetParameters(parameters);
        camera.SetPreviewCallback(this);
        camera.StartPreview();
    }
}

iOS

class CaptureOutput : AVCaptureVideoDataOutputSampleBufferDelegate
{
    ...

    public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSampleBuffer sampleBuffer, AVCaptureConnection connection)
    {
        if (ready)
        {
            ready = false;
            CVPixelBuffer cVPixelBuffer = (CVPixelBuffer)sampleBuffer.GetImageBuffer();

            cVPixelBuffer.Lock(CVPixelBufferLock.ReadOnly);
            nint dataSize = cVPixelBuffer.DataSize;
            width = cVPixelBuffer.Width;
            height = cVPixelBuffer.Height;
            IntPtr baseAddress = cVPixelBuffer.BaseAddress;
            bpr = cVPixelBuffer.BytesPerRow;
            cVPixelBuffer.Unlock(CVPixelBufferLock.ReadOnly);
            buffer = NSData.FromBytes(baseAddress, (nuint)dataSize);
            cVPixelBuffer.Dispose();
            queue.DispatchAsync(ReadTask);
        }
        sampleBuffer.Dispose();
    }
    ...
}

void Initialize()
{
    CaptureSession = new AVCaptureSession();
    previewLayer = new AVCaptureVideoPreviewLayer(CaptureSession)
    {
        Frame = Bounds,
        VideoGravity = AVLayerVideoGravity.ResizeAspectFill
    };

    var videoDevices = AVCaptureDevice.DevicesWithMediaType(AVMediaType.Video);
    var cameraPosition = (cameraOptions == CameraOptions.Front) ? AVCaptureDevicePosition.Front : AVCaptureDevicePosition.Back;
    var device = videoDevices.FirstOrDefault(d => d.Position == cameraPosition);

    if (device == null)
    {
        return;
    }

    NSError error;

    iPublicRuntimeSettings settings = reader.GetRuntimeSettings(out error);
    settings.ExpectedBarcodesCount = (cameraPreview.ScanMode == ScanOptions.Single) ? 1 : 0;
    reader.UpdateRuntimeSettings(settings, out error);
    
    var input = new AVCaptureDeviceInput(device, out error);
    CaptureSession.AddInput(input);
    var videoDataOutput = new AVCaptureVideoDataOutput()
    {
        AlwaysDiscardsLateVideoFrames = true
    };
    if (CaptureSession.CanAddOutput(videoDataOutput))
    {
        CaptureSession.AddOutput(videoDataOutput);
        captureOutput.reader = reader;
        captureOutput.update = UpdateResults;

        DispatchQueue queue = new DispatchQueue("camera");
        videoDataOutput.SetSampleBufferDelegateQueue(captureOutput, queue);
        videoDataOutput.WeakVideoSettings = new NSDictionary<NSString, NSObject>(CVPixelBuffer.PixelFormatTypeKey, NSNumber.FromInt32((int)CVPixelFormatType.CV32BGRA));
    }
    CaptureSession.CommitConfiguration();

    Layer.AddSublayer(previewLayer);
    CaptureSession.StartRunning();
    IsPreviewing = true;
}

After receiving an video frame, we use a background thread to process the frame and get the results.

Android

BarcodeQrData[] output = null;
try
{
    YuvImage image = (YuvImage)msg.Obj;
    if (image != null)
    {
        int[] stridelist = image.GetStrides();
        TextResult[] results = barcodeReader.DecodeBuffer(image.GetYuvData(), previewWidth, previewHeight, stridelist[0], EnumImagePixelFormat.IpfNv21);
        if (results != null && results.Length > 0)
        {
            output = new BarcodeQrData[results.Length];
            int index = 0;
            foreach (TextResult result in results)
            {
                BarcodeQrData data = new BarcodeQrData();
                data.text = result.BarcodeText;
                data.format = result.BarcodeFormatString;
                LocalizationResult localizationResult = result.LocalizationResult;
                data.points = new SKPoint[localizationResult.ResultPoints.Count];
                int pointsIndex = 0;
                foreach (Com.Dynamsoft.Dbr.Point point in localizationResult.ResultPoints)
                {
                    SKPoint p = new SKPoint();
                    p.X = point.X;
                    p.Y = point.Y;
                    data.points[pointsIndex++] = p;
                }
                output[index++] = data;
            }
        }
    }
}
catch (BarcodeReaderException e)
{
    e.PrintStackTrace();
}

iOS

output = null;
if (reader != null)
{
    results = reader.DecodeBuffer(buffer,
                                width,
                                height,
                                bpr,
                                EnumImagePixelFormat.Argb8888,
                                "", out errorr);

    if (results != null && results.Length > 0)
    {
        output = new BarcodeQrData[results.Length];
        int index = 0;
        foreach (iTextResult result in results)
        {
            BarcodeQrData data = new BarcodeQrData();
            data.text = result.BarcodeText;
            data.format = result.BarcodeFormatString;
            iLocalizationResult localizationResult = result.LocalizationResult;
            data.points = new SKPoint[localizationResult.ResultPoints.Length];
            int pointsIndex = 0;
            foreach (NSObject point in localizationResult.ResultPoints)
            {
                SKPoint p = new SKPoint();
                p.X = (float)((NSValue)point).CGPointValue.X;
                p.Y = (float)((NSValue)point).CGPointValue.Y;
                data.points[pointsIndex++] = p;
            }
            output[index++] = data;
        }
    }
    else
    {
        result = "";
    }
}

Finally, call NotifyResultReady to send the results to the shared code.

Android

Element.NotifyResultReady(output, previewWidth, previewHeight);

iOS

cameraPreview.NotifyResultReady(captureOutput.output, (int)captureOutput.width, (int)captureOutput.height);

When the results reach the camera page, we draw the results on SKCanvasView. The pixel density is vital for coordinate transformation.

private void CameraPreview_ResultReady(object sender, ResultReadyEventArgs e)
{
    if (e.Result != null)
    {
        data = (BarcodeQrData[])e.Result;
    }
    else
    {
        data = null;
    }

    imageWidth = e.PreviewWidth;
    imageHeight = e.PreviewHeight;

    canvasView.InvalidateSurface();
}

public static SKPoint rotateCW90(SKPoint point, int width)
{
    SKPoint rotatedPoint = new SKPoint();
    rotatedPoint.X = width - point.Y;
    rotatedPoint.Y = point.X;
    return rotatedPoint;
}

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    double width = canvasView.Width;
    double height = canvasView.Height;

    var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
    var orientation = mainDisplayInfo.Orientation;
    var rotation = mainDisplayInfo.Rotation;
    var density = mainDisplayInfo.Density;

    width *= density;
    height *= density;

    double scale, widthScale, heightScale, scaledWidth, scaledHeight;

    if (orientation == DisplayOrientation.Portrait)
    {
        widthScale = imageHeight / width;
        heightScale = imageWidth / height;
        scale = widthScale < heightScale ? widthScale : heightScale;
        scaledWidth = imageHeight / scale;
        scaledHeight = imageWidth / scale;
    }
    else
    {
        widthScale = imageWidth / width;
        heightScale = imageHeight / height;
        scale = widthScale < heightScale ? widthScale : heightScale;
        scaledWidth = imageWidth / scale;
        scaledHeight = imageHeight / scale;
    }

    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    SKPaint skPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
    };

    SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        TextSize = (float)(18 * density),
        StrokeWidth = 4,
    };

    ResultLabel.Text = "";
    if (data != null)
    {
        foreach (BarcodeQrData barcodeQrData in data)
        {
            for (int i = 0; i < 4; i++)
            {
                if (orientation == DisplayOrientation.Portrait)
                {
                    barcodeQrData.points[i] = rotateCW90(barcodeQrData.points[i], imageHeight);
                }

                if (widthScale < heightScale)
                {
                    barcodeQrData.points[i].X = (float)(barcodeQrData.points[i].X / scale);
                    barcodeQrData.points[i].Y = (float)(barcodeQrData.points[i].Y / scale - (scaledHeight - height) / 2);
                }
                else
                {
                    barcodeQrData.points[i].X = (float)(barcodeQrData.points[i].X / scale - (scaledWidth - width) / 2);
                    barcodeQrData.points[i].Y = (float)(barcodeQrData.points[i].Y / scale);
                }
            }

            canvas.DrawText(barcodeQrData.text, barcodeQrData.points[0], textPaint);
            canvas.DrawLine(barcodeQrData.points[0], barcodeQrData.points[1], skPaint);
            canvas.DrawLine(barcodeQrData.points[1], barcodeQrData.points[2], skPaint);
            canvas.DrawLine(barcodeQrData.points[2], barcodeQrData.points[3], skPaint);
            canvas.DrawLine(barcodeQrData.points[3], barcodeQrData.points[0], skPaint);
        }
    }
    else
    {
        ResultLabel.Text = "No barcode QR code found";
    }
}

Now the Xamarin.Forms barcode QR code scanner is ready to use.

Xamarin.Forms barcode QR code scanner

Source Code

https://github.com/yushulx/xamarin-forms-barcode-qrcode-scanner

How to Build QR Code Scanner with Dynamsoft.DocumentNormalizer.Xamarin.Forms

Dynamsoft.DocumentNormalizer.Xamarin.Forms is available on NuGet. With this package, you can build a QR code scanner in Xamarin.Forms within 5 minutes.

Here are the steps:

  1. Create a blank Xamarin.Forms project in Visual Studio.

    Xamarin.Forms project

  2. Install Dynamsoft Dynamsoft.CaptureVision.Xamarin.Forms via NuGet.

    Dynamsoft Barcode Qr SDK for Xamarin.Forms

  3. Update the version of Xamarin.Forms to 5.0.0.2478. Xamarin.Forms version

    The default version of Xamarin.Forms may cause build errors. Xamarin.Forms version issue

  4. We will navigate to another content page from the main page. So change MainPage to NavigationPage in App.xaml.cs.

     - MainPage = new MainPage();
     + MainPage = new NavigationPage(new MainPage());
    
  5. Initialize Dynamsoft Camera Enhancer and Dynamsoft Barcode Reader in native projects.

    Android MainActivity.cs:

     protected override void OnCreate(Bundle savedInstanceState)
     {
         base.OnCreate(savedInstanceState);
    
         Xamarin.Essentials.Platform.Init(this, savedInstanceState);
         global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    
         global::DCVXamarin.Droid.DCVCameraEnhancer dce = new global::DCVXamarin.Droid.DCVCameraEnhancer(context: this);
         global::DCVXamarin.Droid.DCVBarcodeReader dbr = new global::DCVXamarin.Droid.DCVBarcodeReader();
         LoadApplication(new App(dce, dbr));
     }
    

    iOS AppDelegate.cs:

     public override bool FinishedLaunching(UIApplication app, NSDictionary options)
     {
         global::Xamarin.Forms.Forms.Init();
         global::DCVXamarin.iOS.DCVCameraEnhancer dce = new global::DCVXamarin.iOS.DCVCameraEnhancer();
         global::DCVXamarin.iOS.DCVBarcodeReader dbr = new global::DCVXamarin.iOS.DCVBarcodeReader();
         LoadApplication(new App(dce, dbr));
    
         return base.FinishedLaunching(app, options);
     }
    
  6. We pass the references of Dynamsoft Camera Enhancer and Dynamsoft Barcode Reader to the main page.

     using DCVXamarin;
     using System;
     using Xamarin.Forms;
    
     namespace QrScanner
     {
         public partial class MainPage : ContentPage, ILicenseVerificationListener
         {
             private IDCVCameraEnhancer camera;
             private IDCVBarcodeReader barcodeReader;
             public MainPage(IDCVCameraEnhancer dce, IDCVBarcodeReader dbr)
             {
                 camera = dce;
                 barcodeReader = dbr;
                 barcodeReader.InitLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", this);
    
                 InitializeComponent();
             }
    
             async void OnStartScanningButtonClicked(object sender, EventArgs e)
             {
                 await Navigation.PushAsync(new ScanningPage(camera, barcodeReader));
             }
    
             public void LicenseVerificationCallback(bool isSuccess, string msg)
             {
                 if (!isSuccess)
                 {
                     System.Console.WriteLine(msg);
                 }
             }
         }
     }
    
  7. Create a new content page ScanningPage and add a navigation button in MainPage.xaml to.

     <?xml version="1.0" encoding="utf-8" ?>
     <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="QrScanner.MainPage">
    
         <StackLayout>
             <Button x:Name="startScanning" Text="Start Scanning" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnStartScanningButtonClicked" />
         </StackLayout>
    
     </ContentPage>
    
  8. Import DCVXamarin namespace and add a CameraView in ScanningPage.xaml. We use grid view to make result hover on the camera view.

     <?xml version="1.0" encoding="utf-8" ?>
     <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:dynamsoft = "clr-namespace:DCVXamarin;assembly=DCVXamarin"
                 x:Class="QrScanner.ScanningPage">
         <ContentPage.Content>
             <Grid x:Name="scannerView" Margin="0">
                    
                 <!-- Add DCECameraView. -->
                 <dynamsoft:DCVCameraView OverlayVisible="True"
                                 HorizontalOptions="FillAndExpand"
                                 VerticalOptions="FillAndExpand" >
                 </dynamsoft:DCVCameraView>
    
                 <Label x:Name="barcodeResultLabel" Text="No Barcode detected" FontSize="18"
                     FontAttributes="Bold"
                 TextColor="Blue"
                     HorizontalOptions="Center"
                 VerticalOptions="Center"></Label>
    
             </Grid>
         </ContentPage.Content>
     </ContentPage>
    
  9. Add the camera control code and set barcode detection callback function in ScanningPage.xaml.cs.

     using DCVXamarin;
     using System;
     using Xamarin.Forms;
     using Xamarin.Forms.Xaml;
    
     namespace QrScanner
     {
         [XamlCompilation(XamlCompilationOptions.Compile)]
         public partial class ScanningPage : ContentPage, IBarcodeResultListener
         {
             private IDCVCameraEnhancer camera;
             private IDCVBarcodeReader barcodeReader;
    
             public ScanningPage(IDCVCameraEnhancer dce, IDCVBarcodeReader dbr)
             {
                 camera = dce;
                 barcodeReader = dbr;
    
                 InitializeComponent();
    
                 barcodeReader.SetCameraEnhancer(camera);
                 barcodeReader.AddResultListener(this);
             }
    
             protected override void OnAppearing()
             {
                 base.OnAppearing();
                 camera.Open();
                 barcodeReader.StartScanning();
             }
    
             protected override void OnDisappearing()
             {
                 base.OnDisappearing();
                 camera.Close();
                 barcodeReader.StopScanning();
             }
    
             public void BarcodeResultCallback(int frameID, BarcodeResult[] barcodeResults)
             {
                 string newBarcodeText = "";
                 if (barcodeResults != null && barcodeResults.Length > 0)
                 {
                     for (int i = 0; i < barcodeResults.Length; i++)
                     {
                         {
                             newBarcodeText += barcodeResults[i].BarcodeText;
                             newBarcodeText += "\n ";
                         }
                     }
                 }
                 else
                 {
                     Console.WriteLine("test");
                 }
    
                 Device.BeginInvokeOnMainThread(() => {
                     barcodeResultLabel.Text = newBarcodeText;
                 });
             }
         }
     }
    

The Xamarin.Forms QR code detection app is done. You can download the full sample code from https://github.com/Dynamsoft/capture-vision-xamarin-forms-samples.