Building Document PDF Scanner for Windows, Android and iOS with .NET MAUI Blazor

Xiao Ling - Aug 12 - - Dev Community

In today's fast-paced digital environment, efficient document management is vital across industries such as finance, healthcare, legal, and education. Traditional paper handling methods are not only slow but also susceptible to errors and inefficiencies. Document PDF scanner apps offer a modern solution by streamlining workflows and significantly boosting productivity. This tutorial will show you how to quickly build a cross-platform Document PDF Scanner app for Windows, Android, and iOS using .NET MAUI Blazor and Dynamsoft Mobile Web Capture SDK.

.NET MAUI Blazor Document PDF Scanner

Prerequisites

Mobile Web Capture Online Demo

Experience the Dynamsoft Mobile Web Capture SDK in action by trying the online demo. It works seamlessly across desktop and mobile browsers.
Try the Demo

Getting Started with Mobile Web Capture Sample Code

Explore the Mobile Web Capture GitHub Repository for a hands-on introduction to the Dynamsoft Mobile Web Capture SDK. The repository features five sample projects designed to help you quickly grasp the SDK’s capabilities:

mobile web capture sample code

These samples are deployed on GitHub Pages for easy access:

Sample Name Description Live Demo
hello-world Capture images directly from the camera. Run
detect-boundaries-on-existing-images Detect, crop, and rectify documents in an image file. Run
review-adjust-detected-boundaries Detect, crop and rectify a document captured from a camera stream. Run
capture-continuously-edit-result-images Capture and edit multiple documents from a camera stream. Run
relatively-complete-doc-capturing-workflow A comprehensive workflow demonstrating all supported functionalities. Run

In the following sections, we will integrate the relatively-complete-doc-capturing-workflow sample code into a .NET MAUI Blazor project, demonstrating how to harness the power of this SDK in a cross-platform environment.

Setting Up a .NET MAUI Blazor Project with the Mobile Web Capture SDK

Scaffold a New Project

Start by creating a new project using the .NET MAUI Blazor Hybrid App template.

Add HTML, CSS, and JavaScript Files

Next, copy the source code and static resource files from the relatively-complete-doc-capturing-workflow sample into your project's wwwroot folder.

The sample index.html references JavaScript and CSS files hosted on a CDN:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/ddv.css">
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-core@3.0.30/dist/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-license@3.0.20/dist/license.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.0.20/dist/ddn.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.0.30/dist/cvr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/ddv.js"></script>
Enter fullscreen mode Exit fullscreen mode

To improve load times, download these files and reference them locally. Place the CSS files in the wwwroot/css folder and the JavaScript files in the wwwroot/js folder.

mobile web capture css and js files

Update the index.html file in your .NET MAUI Blazor project to reference these local files:

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
    <title>ScanToPDF</title>
    <base href="/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="css/app.css" />
    <link rel="stylesheet" href="css/ddv.css">
    <link rel="stylesheet" href="ScanToPDF.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />

    <script src="js/dynamsoft-core/dist/core.js"></script>
    <script src="js/dynamsoft-license/dist/license.js"></script>
    <script src="js/dynamsoft-document-normalizer/dist/ddn.js"></script>
    <script src="js/dynamsoft-capture-vision-router/dist/cvr.js"></script>
    <script src="js/dynamsoft-document-viewer/dist/ddv.js"></script>
</head>
Enter fullscreen mode Exit fullscreen mode

Create an interop.js File

For interoperation between C# and JavaScript, create an interop.js file and include it in the index.html file.

<script src="_framework/blazor.webview.js" autostart="false"></script>
<script type="module" src="interop.js"></script>
Enter fullscreen mode Exit fullscreen mode

Move the JavaScript code from the sample index.html into this interop.js file.

var isInitialized = false;
import { isMobile, initDocDetectModule } from "./utils.js";
import {
    mobileCaptureViewerUiConfig,
    mobilePerspectiveUiConfig,
    mobileEditViewerUiConfig,
    pcCaptureViewerUiConfig,
    pcPerspectiveUiConfig,
    pcEditViewerUiConfig
} from "./uiConfig.js";

window.initSDK = async function (license) {
    if (isInitialized) return true;

    let result = true;
    try {
        Dynamsoft.DDV.Core.engineResourcePath = "/js/dynamsoft-document-viewer/dist/engine";
        Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
        Dynamsoft.DDV.Core.loadWasm();
        await Dynamsoft.License.LicenseManager.initLicense(license, true);
        isInitialized = true;
    } catch (e) {
        console.log(e);
        result = false;
    }
    return result;
},

window.initializeCaptureViewer = async (dotnetRef) => {    
    try {
        await Dynamsoft.DDV.Core.init();
        await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);

        const captureViewer = new Dynamsoft.DDV.CaptureViewer({
            container: "container",
            uiConfig: isMobile() ? mobileCaptureViewerUiConfig : pcCaptureViewerUiConfig,
            viewerConfig: {
                acceptedPolygonConfidence: 60,
                enableAutoDetect: true,
            }
        });

        await captureViewer.play({ resolution: [1920, 1080] });
        captureViewer.on("showPerspectiveViewer", () => switchViewer(0, 1, 0));

        const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
            container: "container",
            groupUid: captureViewer.groupUid,
            uiConfig: isMobile() ? mobilePerspectiveUiConfig : pcPerspectiveUiConfig,
            viewerConfig: { scrollToLatest: true }
        });

        perspectiveViewer.hide();
        perspectiveViewer.on("backToCaptureViewer", () => {
            switchViewer(1, 0, 0);
            captureViewer.play();
        });

        perspectiveViewer.on("showEditViewer", () => switchViewer(0, 0, 1));

        const editViewer = new Dynamsoft.DDV.EditViewer({
            container: "container",
            groupUid: captureViewer.groupUid,
            uiConfig: isMobile() ? mobileEditViewerUiConfig : pcEditViewerUiConfig
        });

        editViewer.hide();
        editViewer.on("backToPerspectiveViewer", () => switchViewer(0, 1, 0));
        editViewer.on("save", async () => {
            let blob = await editViewer.currentDocument.saveToPdf();

            // convert blob to base64
            let reader = new FileReader();
            reader.readAsDataURL(blob);
            reader.onloadend = function () {
                let base64data = reader.result;
                if (dotnetRef) {
                    dotnetRef.invokeMethodAsync('SavePdfFromBlob', base64data.split(',')[1])
                }
            }

        });

        const switchViewer = (c, p, e) => {
            captureViewer.hide();
            perspectiveViewer.hide();
            editViewer.hide();
            if (c) captureViewer.show();
            else captureViewer.stop();
            if (p) perspectiveViewer.show();
            if (e) editViewer.show();
        };
    }
    catch (e) {
        alert(e);
    }
};

window.displayAlert = function(message) {
    alert(message);
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • Functions assigned to window are callable from C#.
  • Dynamsoft.DDV.Core.engineResourcePath sets the path to engine resources.
  • The save button click event in editViewer triggers the save process, converting the document blob to a base64 string for use by the C# method SavePdfFromBlob. The event is defined in the uiConfig.js file.

    Dynamsoft.DDV.Elements.Load,
    {
        type: Dynamsoft.DDV.Elements.Button,
        className: "ddv-button ddv-button-download",
        events:{
            click: "save",
        }
    },
    

    Call saveToPdf() to get the document blob and convert it to a base64 string.

    let blob = await editViewer.currentDocument.saveToPdf();
    
    let reader = new FileReader();
    reader.readAsDataURL(blob);
    reader.onloadend = function () {
        let base64data = reader.result;
    }
    
  • dotnetRef is a reference to the C# code, allowing JavaScript to invoke C# methods. JavaScript code cannot access local files directly.

    if (dotnetRef) {
        dotnetRef.invokeMethodAsync('SavePdfFromBlob', base64data.split(',')[1])
    }
    

Add a Razor Component

Create a ScanToPdf.razor file in the Pages folder to contain the HTML and C# code for the document PDF scanner:

@page "/scantopdf"

@inject IJSRuntime JSRuntime

<div id="container"></div>

@code {
    private DotNetObjectReference<ScanToPdf> objRef;
    private bool isGranted = false;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }
}
Enter fullscreen mode Exit fullscreen mode

Camera Access in .NET MAUI Blazor

Since a .NET MAUI Blazor app runs within a WebView and is packaged as a native app, it’s necessary to request camera access through native code. In the ScanToPdf.razor file, you can use the following code to request camera access permission:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
        if (status == PermissionStatus.Granted)
        {
            isGranted = true;
        }
        else
        {
            status = await Permissions.RequestAsync<Permissions.Camera>();
            if (status == PermissionStatus.Granted)
            {
                isGranted = true;
            }
        }

        if (isGranted)
        {
            StateHasChanged();
            await JSRuntime.InvokeVoidAsync("initializeCaptureViewer", objRef);
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

This shared code initiates the camera permission request but requires additional platform-specific implementations for Android and iOS to function correctly.

Android

  1. Update AndroidManifest.xml: Add the camera permission to your AndroidManifest.xml file:

    <uses-permission android:name="android.permission.CAMERA" />
    
  2. Create MyWebChromeClient.cs: In the Platforms/Android folder, create a MyWebChromeClient.cs file extending WebChromeClient to handle camera permission requests by overriding the OnPermissionRequest method.

    using Android.Content;
    using Android.Webkit;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using static Android.Webkit.WebChromeClient;
    
    namespace ScanToPDF.Platforms.Android
    {
        public class MyWebChromeClient : WebChromeClient
        {
            private MainActivity _activity;
    
            public MyWebChromeClient(Context context)
            {
                _activity = context as MainActivity;
            }
    
            public override void OnPermissionRequest(PermissionRequest request)
            {
                try
                {
                    request.Grant(request.GetResources());
                    base.OnPermissionRequest(request);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
    
            public override bool OnShowFileChooser(global::Android.Webkit.WebView webView, IValueCallback filePathCallback, FileChooserParams fileChooserParams)
            {
                base.OnShowFileChooser(webView, filePathCallback, fileChooserParams);
                return _activity.ChooseFile(filePathCallback, fileChooserParams.CreateIntent(), fileChooserParams.Title);
            }
        }
    }
    
    

    The OnShowFileChooser method is responsible for handling file selection events triggered by the HTML input element within the WebView. To fully implement this functionality, you need to add the following code to the MainActivity.cs file.

    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
        if (_requestCode == requestCode)
        {
            if (_filePathCallback == null)
                return;
    
            Java.Lang.Object result = FileChooserParams.ParseResult((int)resultCode, data);
            _filePathCallback.OnReceiveValue(result);
        }
    }
    
    public bool ChooseFile(IValueCallback filePathCallback, Intent intent, string title)
    {
        _filePathCallback = filePathCallback;
    
        StartActivityForResult(Intent.CreateChooser(intent, title), _requestCode);
    
        return true;
    }
    
  3. Create MauiBlazorWebViewHandler.cs: Implement a custom WebView handler for Android:

    public class MauiBlazorWebViewHandler : BlazorWebViewHandler
    {
    
        protected override global::Android.Webkit.WebView CreatePlatformView()
        {
            var view = base.CreatePlatformView();
            view.SetWebChromeClient(new MyWebChromeClient(this.Context));
            return view;
        }
    }
    
  4. Configure the WebView Handler: In MauiProgram.cs, configure the custom WebView handler for Android:

    using Microsoft.AspNetCore.Components.WebView.Maui;
    using Microsoft.Extensions.Logging;
    
    #if ANDROID
    using ScanToPDF.Platforms.Android;
    #endif
    
    namespace ScanToPDF
    {
        public static class MauiProgram
        {
            public static MauiApp CreateMauiApp()
            {
                var builder = MauiApp.CreateBuilder();
                builder
                    .UseMauiApp<App>()
                    .ConfigureFonts(fonts =>
                    {
                        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    }).ConfigureMauiHandlers(handlers =>
                    {
    #if ANDROID
                        handlers.AddHandler<BlazorWebView, MauiBlazorWebViewHandler>();
    #endif
                    });
    
                builder.Services.AddMauiBlazorWebView();
    
    #if DEBUG
                builder.Services.AddBlazorWebViewDeveloperTools();
                builder.Logging.AddDebug();
    #endif
    
                return builder.Build();
            }
        }
    }
    
    

iOS

  1. Update Info.plist: Add camera and microphone usage descriptions to your Info.plist:

    <key>NSCameraUsageDescription</key>
    <string>This app is using the camera</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>This app needs access to microphone for taking videos.</string>
    
  2. Configure the WebView: In MainPage.xaml.cs, register the BlazorWebViewInitializing event to configure the WebView on iOS:

    using Microsoft.AspNetCore.Components.WebView;
    
    namespace ScanToPDF
    {
        public partial class MainPage : ContentPage
        {
            public MainPage()
            {
                InitializeComponent();
                blazorWebView.BlazorWebViewInitializing += WebView_BlazorWebViewInitializing;
            }
    
            private void WebView_BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)
            {
    #if IOS || MACCATALYST
                e.Configuration.AllowsInlineMediaPlayback = true;
                e.Configuration.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;
    #endif
            }
    
        }
    }
    
    

Saving Scanned Documents as PDFs to Local Storage

To save scanned documents as PDFs, create the SavePdfFromBlob method in the ScanToPdf.razor file. This method converts the base64 string from JavaScript into a PDF file and saves it locally:

[JSInvokable("SavePdfFromBlob")]
public async void SavePdfFromBlob(string base64String)
{
    if (!string.IsNullOrEmpty(base64String))
    {
        byte[] imageBytes = Convert.FromBase64String(base64String);


#if WINDOWS
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#elif ANDROID
string folderPath = FileSystem.AppDataDirectory;
#elif IOS || MACCATALYST
string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
#else
        throw new PlatformNotSupportedException("Platform not supported");
#endif
        string filePath = Path.Combine(folderPath, GenerateFilename());
        try
        {
            await File.WriteAllBytesAsync(filePath, imageBytes);
            await JSRuntime.InvokeVoidAsync("displayAlert", $"Image saved to {filePath}");
            await Share.RequestAsync(new ShareFileRequest
                {
                    Title = "Share PDF File",
                    File = new ShareFile(filePath)
                });
        }
        catch (Exception ex)
        {
            await JSRuntime.InvokeVoidAsync("displayAlert", $"Error saving image: {ex.Message}");
        }
    }
    else
    {
        Console.WriteLine("Failed to fetch the image.");
    }
}

private static string GenerateFilename()
{
    DateTime now = DateTime.Now;
    string timestamp = now.ToString("yyyyMMdd_HHmmss");
    return $"{timestamp}.pdf";
}
Enter fullscreen mode Exit fullscreen mode

This method determines the correct save directory based on the platform:

  • Android: FileSystem.AppDataDirectory
  • iOS: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
  • Windows: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)

Running the Document PDF Scanner App on Windows, Android, and iOS

Windows

Windows document PDF scanner with .NET MAUI Blazor

Android

Android document PDF scanner with .NET MAUI Blazor

iPadOS

iPadOS document PDF scanner with .NET MAUI Blazor

Source Code

https://github.com/yushulx/web-twain-document-scan-management/tree/main/examples/camera_scan_pdf

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player