How to Edit and Convert PDF, PNG, JPEG, and TIFF Files in Blazor Apps
Tags: blazor, webdev, csharp, programming
Published: 2024-01-12 07:20:25

If you ever searched for "**image converter**" or "**PDF converter**" on Google, you might have noticed most of the tools are server-side applications, which means you must upload your files to the server for further processing. This may cause an information leak and is not ideal for users who are concerned about **privacy** and **security**. [Dynamsoft Document Viewer](https://www.dynamsoft.com/document-viewer/docs/) aims to provide APIs for document image viewing, editing, and conversion on the web frontend. It is implemented in JavaScript and WebAssembly and does not collect user data. In this article, we will tailor the SDK to build a razor class library. The library can be employed to easily build web image and PDF converter applications with Blazor.


<video src="https://github.com/yushulx/Razor-Image-Editor-Library/assets/2202306/ac99fef4-d171-429a-88e4-ac4a2f1b23fb" controls="controls" muted="muted" style="max-height:640px; min-height: 200px; max-width: 100%;"></video>

## Online Demo: Web Frontend Image Editor and PDF Converter
[https://yushulx.me/Razor-Image-Editor-Library/](https://yushulx.me/Razor-Image-Editor-Library/)

## NuGet Package
[https://www.nuget.org/packages/RazorImageEditorLibrary/](https://www.nuget.org/packages/RazorImageEditorLibrary/)

## Download Dynamsoft Document Viewer JavaScript SDK
The SDK can be downloaded from [npm](https://www.npmjs.com/package/dynamsoft-document-viewer) using the following command:

```bash
npm i dynamsoft-document-viewer
```

The node package primarily contains the following files:

```bash
|-- engine
    |-- ddv-crypto.wasm
    |-- ddv-imagecore.wasm
    |-- ddv-imageio.wasm
    |-- ddv-imageio.worker.js
    |-- ddv-imageproc.wasm
    |-- ddv-imageproc-simd.wasm
    |-- ddv-pdfreader.wasm
    |-- dls.license.dialog.html
|-- ddv.css
|-- ddv.js
```

You can start a new `Razor Class Library` project in Visual Studio and copy these files into the `wwwroot` folder.

## Razor Class Library Project 
A Razor Class library project is typically scaffolded with a JavaScript interop file and a C# interop file. They can be named `imageEditorJsInterop.js` and `ImageEditorJsInterop.cs`, respectively. 

The constructor of the `ImageEditorJsInterop` class is designed to load the JavaScript interop file.

```csharp
public ImageEditorJsInterop(IJSRuntime jsRuntime)
{
    moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
        "import", "./_content/RazorImageEditorLibrary/imageEditorJsInterop.js").AsTask());
}
```

In the `ImageEditorJsInterop`, we define three C# methods:

- `Task LoadJS()`: Loads all required resource files.
    
    ```csharp
    public async Task LoadJS()
    {
        var module = await moduleTask.Value;
        await module.InvokeAsync<object>("init");
    }
    ```
    
    The resource files include `ddv.js` and `ddv.css`. Additionally, the third-party library `jszip` is required for compressing images into a zip file. 

    ```javascript
    export function init() {
        return new Promise((resolve, reject) => {
            let script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = '_content/RazorImageEditorLibrary/ddv.js';
            script.onload = async () => {
                let jszip = document.createElement('script');
                jszip.type = 'text/javascript';
                jszip.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js';
                document.head.appendChild(jszip);
    
                let css = document.createElement('link');
                css.rel = 'stylesheet';
                css.href = '_content/RazorImageEditorLibrary/ddv.css';
                document.head.appendChild(css);
                resolve();
            };
            script.onerror = () => {
                reject();
            };
            document.head.appendChild(script);
        });
    }
    ```

- `Task<int> SetLicense(string license)`: Sets the license key for the Dynamsoft Document Viewer SDK.
    
    ```csharp
    public async Task<int> SetLicense(string license)
    {
        var module = await moduleTask.Value;
        return await module.InvokeAsync<int>("setLicense", license);
    }
    ```

    Setting the license key in JavaScript triggers the download and compilation of wasm. The **PDF reader wasm file is not included** at this stage due to its large size. For a better user experience, this file should be downloaded independently.

    ```javascript
    export async function setLicense(license) {
        if (!Dynamsoft) return;
        try {
            await Dynamsoft.DDV.setConfig({
                license: license,
                engineResourcePath: "_content/RazorImageEditorLibrary/engine",
            });
            Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
            return 0;
        }
        catch (ex) {
            console.error(ex);
            return -1;
        }
    }
    ```

- `Task<DocumentManager> CreateDocumentManager()`: Creates a `DocumentManager` object responsible for handling image operations such as viewing, editing, and conversion. 

    ```csharp
    public async Task<DocumentManager> CreateDocumentManager()
    {
        var module = await moduleTask.Value;
        IJSObjectReference jsObjectReference = await module.InvokeAsync<IJSObjectReference>("createDocumentManager");
        DocumentManager docManager = new DocumentManager(module, jsObjectReference);
        return docManager;
    }
    ```

    This object is internally initialized in the previous step and is returned to the caller.

    ```javascript
    export async function createDocumentManager() {
        if (!Dynamsoft) return;
    
        try {
            let docManager = Dynamsoft.DDV.documentManager;
            docManager.createDocument();
            return docManager;
        }
        catch (ex) {
            console.error(ex);
        }
        return null;
    }
    ```


## Document Manager Class for Image Viewing, Editing and Conversion
In the `DocumentManager.cs` file, we implement the following methods:
- `Task LoadPdfWasm()`: Loads the PDF reader WebAssembly (wasm) module.
    
    ```csharp
    public async Task LoadPdfWasm()
    {
        await _module.InvokeVoidAsync("loadPdfWasm", _jsObjectReference);
    }
    ```
    
    The download of the PDF reader wasm file is triggered upon the first loading of a PDF file. To boost the loading speed, we can load a preset PDF file, thereby initiating the download earlier.

    ```javascript
    export async function loadPdfWasm(docManager) {
        if (!Dynamsoft) return;
        try {
    
            let response = await fetch('_content/RazorImageEditorLibrary/twain.pdf');
            let blob = await response.blob();  
            let doc = docManager.createDocument();
            await doc.loadSource(blob);
            await doc.saveToJpeg(0);
            docManager.deleteDocuments([doc.uid]);
        }
        catch (ex) {
            console.error(ex);
        }
    }
    ```
       
- `Task CreateBrowseViewer(string elementId)`: Creates a browse viewer instance.

    ```csharp
    public async Task CreateBrowseViewer(string elementId)
    {
        browseViewer = await _module.InvokeAsync<IJSObjectReference>("createBrowseViewer", _jsObjectReference, elementId, objRef, "OnPageIndexChanged");
    }
    ``` 

    The browse viewer is designed for viewing and selecting images and must be bound to an HTML div element.

    ```javascript
    export function openDocument(docManager, viewer) {
        if (!Dynamsoft) return;
        let docs = docManager.getAllDocuments();
        let doc = docs.length == 0 ? docManager.createDocument() : docs[0];
        viewer.openDocument(doc.uid);
    }

    export function createBrowseViewer(docManager, elementId, dotNetHelper, cbName) {
        if (!Dynamsoft) return;
    
        try {
            let browseViewer = new Dynamsoft.DDV.BrowseViewer({
                container: document.getElementById(elementId),
            });
            browseViewer.multiselectMode = true;
            browseViewer.on("currentIndexChanged", (e) => {
                dotNetHelper.invokeMethodAsync(cbName, e.newIndex);
            });
            openDocument(docManager, browseViewer);
            return browseViewer;
        }
        catch (ex) {
            console.error(ex);
        }
        return null;
    
    }
    ```

- `Task CreateEditViewer(string elementId)`: Creates an edit viewer instance.
    
    ```csharp
    public async Task CreateEditViewer(string elementId)
    {
        editViewer = await _module.InvokeAsync<IJSObjectReference>("createEditViewer", _jsObjectReference, elementId);
    }
    ```

    The edit viewer allows users to rotate, crop, and apply filters to a selected image. Its UI elements can be customized by defining a specific configuration object.

    ```javascript
    export function createEditViewer(docManager, elementId) {
        if (!Dynamsoft) return;
    
        try {
            let config = {
                type: Dynamsoft.DDV.Elements.Layout,
                    flexDirection: "column",
                        className: "ddv-edit-viewer-desktop",
                            children: [
                                {
                                    type: Dynamsoft.DDV.Elements.Layout,
                                    className: "ddv-edit-viewer-header-desktop",
                                    children: [
                                        {
                                            type: Dynamsoft.DDV.Elements.Layout,
                                            children: [
                                                Dynamsoft.DDV.Elements.Zoom,
                                                Dynamsoft.DDV.Elements.FitMode,
                                                Dynamsoft.DDV.Elements.RotateLeft,
                                                Dynamsoft.DDV.Elements.RotateRight,
                                                Dynamsoft.DDV.Elements.Crop,
                                                Dynamsoft.DDV.Elements.Filter,
                                                Dynamsoft.DDV.Elements.Undo,
                                                Dynamsoft.DDV.Elements.Redo,
                                                Dynamsoft.DDV.Elements.DeleteCurrent,
                                                Dynamsoft.DDV.Elements.DeleteAll,
                                            ],
                                        },
                                        {
                                            type: Dynamsoft.DDV.Elements.Layout,
                                            children: [
                                                {
                                                    type: Dynamsoft.DDV.Elements.Pagination,
                                                    className: "ddv-edit-viewer-pagination-desktop",
                                                },
                                            ],
                                        },
                                    ],
                                },
                                Dynamsoft.DDV.Elements.MainView,
                            ],
                        };
    
    
            let editViewer = new Dynamsoft.DDV.EditViewer({
                container: document.getElementById(elementId),
                uiConfig: config,
            });
            editViewer.displayMode = "single"
            openDocument(docManager, editViewer);
            return editViewer;
        }
        catch (ex) {
            console.error(ex);
        }
        return null;
    
    }
    ```

- `Task LoadFile(ElementReference inputFile)`: Loads a local file into the browse viewer.

    ```csharp
    public async Task LoadFile(ElementReference inputFile)
    {
        int count = await _module.InvokeAsync<int>("getFileCount", inputFile);
        if (count > 0)
        {
            for (int i = 0; i < count; i++)
            {
                IJSObjectReference blob = await _module.InvokeAsync<IJSObjectReference>("readFileData", inputFile, i);
    
                await _module.InvokeVoidAsync("loadPage", _jsObjectReference, blob, browseViewer, editViewer);
            }
        }
    }
    ```

    A reference to an input file element is passed from C# to JavaScript. The JavaScript code reads the file data and loads it to the browse viewer.

    ```javascript
    export function getFileCount(element) {

        return element.files.length;
    }

    export async function loadPage(docManager, blob, browseViewer, editViewer) {
        if (!Dynamsoft) return;
    
        try {
            let docs = docManager.getAllDocuments();
            let doc = docs.length == 0 ? docManager.createDocument() : docs[0];
            let index = doc.pages.length;
            let pages = await doc.loadSource(blob);
            
            if (browseViewer) {
                
                let selectedIndices = browseViewer.getSelectedPageIndices();
                for (let i = 0; i < pages.length; i++) {
                    selectedIndices.push(i + index);
                }
                browseViewer.selectPages(selectedIndices);
                goToPage(browseViewer, index);
    
                if (editViewer) {
                    goToPage(editViewer, index);
                }
            }
        }
        catch (ex) {
            console.error(ex);
        }
    }
    ```

- `Task LoadCanvas(IJSObjectReference canvas)`: Loads a canvas into the browse viewer. 

    ```csharp
    public async Task LoadCanvas(IJSObjectReference canvas)
    {
        IJSObjectReference blob = await _module.InvokeAsync<IJSObjectReference>("readCanvasData", canvas);
        await _module.InvokeVoidAsync("loadPage", _jsObjectReference, blob, browseViewer, editViewer);
    }
    ```

    A reference to a canvas element is passed from C# to JavaScript.

    ```javascript
    export function readCanvasData(canvas) {
        if (!Dynamsoft) return;
    
        return new Promise((resolve, reject) => {
            canvas.toBlob(function (blob) {
                resolve(blob);
            });
        });
    }
    ```


- `Task Convert(string filename, string format, bool isZip)`: Converts selected images to a specified format, optionally compressing them into a zip file if `isZip` is true.

    ```csharp
    public async Task Convert(string filename, string format, bool isZip)
    {
        await _module.InvokeVoidAsync("convert", _jsObjectReference, filename, format, isZip, browseViewer);
    }
    ```

    Based on the specified file name, output format, and the decision to compress the output or not, the file conversion can be implemented as follows:

    ```javascript
    export async function convert(docManager, filename, format, isZip, browseViewer) {
        if (!Dynamsoft || docManager.getAllDocuments().length == 0) return;
    
        let zip = null;
        if (isZip) {
            zip = new JSZip();
        }
    
        try {
            let doc = docManager.getAllDocuments()[0];
            let result = null;
            let indices = browseViewer == null ? createNumberArray(doc.pages.length) : browseViewer.getSelectedPageIndices();
            let count = indices.length;
    
            if (count == 0) return;
    
            switch (format) {
                case "pdf":
                    const pdfSettings = {
                        author: "Dynamsoft",
                        compression: "pdf/jpeg",
                        pageType: "page/a4",
                        creator: "DDV",
                        creationDate: "D:20230101085959",
                        keyWords: "samplepdf",
                        modifiedDate: "D:20230101090101",
                        producer: "Dynamsoft Document Viewer",
                        subject: "SamplePdf",
                        title: "SamplePdf",
                        version: "1.5",
                        quality: 90,
                    }
                    result = await doc.saveToPdf(indices, pdfSettings);
    
                    if (zip != null) {
                        zip.file(filename + "." + format, result);
                        zip.generateAsync({ type: "blob" })
                            .then(function (content) {
                                saveBlob(content, filename + ".zip");
                            });
                    }
                    else {
                        saveBlob(result, filename + "." + format);
                    }
                    
                    break;
                case "png":
                    {
                        if (zip != null) {
                            for (let i = 0; i < count; i++) {
                                result = await doc.saveToPng(indices[i]);
                                zip.file(filename + i + "." + format, result);
                            }
                            
                            zip.generateAsync({ type: "blob" })
                                .then(function (content) {
                                    saveBlob(content, filename + ".zip");
                                });
                        }
                        else {
                            for (let i = 0; i < count; i++) {
                                result = await doc.saveToPng(indices[i]);
                                saveBlob(result, filename + i + "." + format);
                            }
                        }
                    }
    
                    break;
                case "jpeg":
                    {
                        const settings = {
                            quality: 80,
                        };
    
                        if (zip != null) {
                            for (let i = 0; i < count; i++) {
                                result = await doc.saveToJpeg(indices[i]);
                                zip.file(filename + i + "." + format, result);
                            }
    
                            zip.generateAsync({ type: "blob" })
                                .then(function (content) {
                                    saveBlob(content, filename + ".zip");
                                });
                        }
                        else {
                            for (let i = 0; i < count; i++) {
                                result = await doc.saveToJpeg(indices[i], settings);
                                saveBlob(result, filename + i + "." + format);
                            }
                        }
                    }
    
                    break;
                case "tiff":
                    const customTag1 = {
                        id: 700,
                        content: "Created By Dynamsoft",
                        contentIsBase64: false,
                    }
    
                    const tiffSettings = {
                        customTag: [customTag1],
                        compression: "tiff/auto",
                    }
                    result = await doc.saveToTiff(indices, tiffSettings);
    
                    if (zip != null) {
                        zip.file(filename + "." + format, result);
                        zip.generateAsync({ type: "blob" })
                            .then(function (content) {
                                saveBlob(content, filename + ".zip");
                            });
                    }
                    else {
                        saveBlob(result, filename + "." + format);
                    }
                    
                    break;
            }
        }
        catch (ex) {
            console.error(ex);
        }
    }
    ```

    In cases where there are multiple images to convert, particularly when the output format is JPEG or PNG, it's possible to save them all into a single zip file.

- `Task SelectAll()`: Selects all files currently managed by the document manager.

    ```csharp
    public async Task SelectAll()
    {
        await _module.InvokeVoidAsync("selectAll", browseViewer);
    }
    ```

    **JavaScript**
    
    ```javascript
    export function selectAll(viewer) {
        if (viewer != null) {
            viewer.selectAllPages();
        }
    }

    ```

- `Task UnselectAll()`: Unselects all files currently managed by the document manager.

    ```csharp
    public async Task UnselectAll()
    {
        await _module.InvokeVoidAsync("unselectAll", browseViewer);
    }
    ```
    
    **JavaScript**

    ```javascript
    export function unselectAll(viewer) {
        if (viewer != null) {
            viewer.selectPages([]);
        }
    }

    ```

- `Task RemoveSelected()`: Removes the files currently selected in the document manager.

    ```csharp
    public async Task RemoveSelected()
    {
        await _module.InvokeVoidAsync("removeSelected", _jsObjectReference, browseViewer);
    }
    ```

    **JavaScript**

    ```javascript
    export function removeSelected(docManager, viewer) {
        if (viewer != null) {
            let selectedIndices = viewer.getSelectedPageIndices();
            let docs = docManager.getAllDocuments();
            let doc = docs.length == 0 ? docManager.createDocument() : docs[0];
            doc.deletePages(selectedIndices);
        }
    }
    
    ```

- `Task RemoveAll()`: Removes all files from the document manager.

    ```csharp
    public async Task RemoveAll()
    {
        await _module.InvokeVoidAsync("removeAll", _jsObjectReference);
    }
    ```

    **JavaScript**

    ```javascript
    export function removeAll(docManager) {
        if (docManager != null) {
            let docs = docManager.getAllDocuments();
            let doc = docs.length == 0 ? docManager.createDocument() : docs[0];
            doc.deleteAllPages();
        }
    }

    ```

## Developing an Image Editor and PDF Converter App with Blazor
Now, we can utilize the Razor class library to develop a Blazor image and PDF converter app.
1. Request a [free trial license](https://www.dynamsoft.com/customer/license/trialLicense?product=ddv) for Dynamsoft Document Viewer.
2. Install [RazorCameraLibrary](https://www.nuget.org/packages/RazorCameraLibrary) and [RazorImageEditorLibrary](https://www.nuget.org/packages/RazorImageEditorLibrary/) via NuGet package manager into your project. These libraries allow the input source to be either a local file or a camera.
3. Import the dependencies in the `Pages/Index.razor` file. 
    ```csharp
    @page "/"

    @inject IJSRuntime JSRuntime
    @using RazorImageEditorLibrary
    @using RazorCameraLibrary
    @using Camera = RazorCameraLibrary.Camera
    ```

4. Create three div elements for the browse viewer, edit viewer, and camera preview, respectively.

    ```html
    <div class="container">
        
        <div id="videoview">
            <div id="videoContainer"></div>
        </div>
    </div>
    
    <div class="container">
        <div class="document-viewer">
            <div id="browse-viewer"></div>
        </div>
    </div>

    <div class="container" style="@(showEditor ? "display: block;" : "display: none;")">
        <div class="document-viewer">
            <div id="edit-viewer"></div>
        </div>
    </div>
    ```

5. Load the image editor and camera libraries.
    
    ```csharp
    private ImageEditorJsInterop? imageEditorJsInterop;
    private CameraJsInterop? cameraJsInterop;
    private CameraEnhancer? cameraEnhancer;
    
    protected override async Task OnInitializedAsync()
    {
        imageEditorJsInterop = new ImageEditorJsInterop(JSRuntime);
        await imageEditorJsInterop.LoadJS();

        cameraJsInterop = new CameraJsInterop(JSRuntime);
        await cameraJsInterop.LoadJS();

        cameraEnhancer = await cameraJsInterop.CreateCameraEnhancer();
        await cameraEnhancer.SetVideoElement("videoContainer");
    }
    ```
    
6. Create a button to activate the image editor library using a valid license key, and set up div elements for the browse viewer and edit viewer.
    
    ```javascript
    <button @onclick="Activate">Activate SDK</button>

    @code {
        public async Task Activate()
        {
            if (imageEditorJsInterop == null) return;
            isLoading = true;
            int ret = await imageEditorJsInterop.SetLicense(licenseKey);
            if (ret == 0)
            {
                documentManager = await imageEditorJsInterop.CreateDocumentManager();
                _ = documentManager.LoadPdfWasm();
                await documentManager.CreateBrowseViewer("browse-viewer");
                await documentManager.CreateEditViewer("edit-viewer");
                isReady = true;
            }
    
            isLoading = false;
        }
    }
    ```

7. Create a button to open a camera.
    
    ```csharp
    <button @onclick="GetCameras">Get Cameras</button>

    @code {
        public async Task GetCameras()
        {
            if (cameraEnhancer == null) return;
            try
            {
                cameras = await cameraEnhancer.GetCameras();
                if (cameras.Count >= 0)
                {
                    selectedCamera = cameras[0].DeviceId;
                    await OpenCamera();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    
        public async Task OpenCamera()
        {
            if (cameraEnhancer == null) return;
            try
            {
                int selectedIndex = cameras.FindIndex(camera => camera.DeviceId == selectedCamera);
                await cameraEnhancer.SetResolution(640, 480);
                await cameraEnhancer.OpenCamera(cameras[selectedIndex]);
    
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    ```

8. Load an image into the browse viewer from either a local file or the camera.
    
    ```csharp
    
    <input type="file" @ref="inputFile" accept="application/pdf,image/png,image/jpeg,image/tiff" @onchange="LoadFile" multiple />
    <button @onclick="Capture">Capture</button>

    @code {
        private ElementReference inputFile;
        public async Task LoadFile(ChangeEventArgs e)
        {
            if (documentManager == null) return;
    
            if (await documentManager.ExistPDF(inputFile))
            {
                isLoading = true;
                while (!documentManager.IsPDFReady)
                {
                    await Task.Delay(1000);
                }
                isLoading = false;
            }
    
            await documentManager.LoadFile(inputFile);
        }
    
        public async Task Capture()
        {
            if (cameraEnhancer == null || documentManager == null) return;
    
            try
            {
                IJSObjectReference canvas = await cameraEnhancer.AcquireCameraFrame();
                await documentManager.LoadCanvas(canvas);   
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    ```

9. Add a file conversion button to convert selected images to a specified format.
    
    ```csharp
    <select id="sources" @onchange="e => OnChange(e)">
        @foreach (var format in formats)
        {
            <option value="@format">@format</option>
        }
    </select>
    <input type="checkbox" @bind="isZip" style="margin: 0 2px;" />Zip
    <button @onclick="ConvertImage">Convert</button>

    @code {
        private string selectedValue = FileFormat.PDF;
        private bool isZip = false;

        private async Task OnChange(ChangeEventArgs e)
        {
            selectedValue = e.Value.ToString();
        }

        public async Task ConvertImage()
        {
            if (documentManager == null) return;
            isLoading = true;
            await documentManager.Convert("convert", selectedValue, isZip);
            isLoading = false;
        }
    }
    ```

10. Run the Blazor app in a web browser.

    ![blazor image PDF editor converter](https://www.dynamsoft.com/codepool/img/2024/01/blazor-image-editor-converter.png)
    
## Source Code
[https://github.com/yushulx/Razor-Image-Editor-Library](https://github.com/yushulx/Razor-Image-Editor-Library)