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 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.
Online Demo: Web Frontend Image Editor and PDF Converter
https://yushulx.me/Razor-Image-Editor-Library/
NuGet Package
https://www.nuget.org/packages/RazorImageEditorLibrary/
Download Dynamsoft Document Viewer JavaScript SDK
The SDK can be downloaded from npm using the following command:
npm i dynamsoft-document-viewer
The node package primarily contains the following files:
|-- 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.
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.
public async Task LoadJS() { var module = await moduleTask.Value; await module.InvokeAsync<object>("init"); }
The resource files include
ddv.js
andddv.css
. Additionally, the third-party libraryjszip
is required for compressing images into a zip file.
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.
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.
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 aDocumentManager
object responsible for handling image operations such as viewing, editing, and conversion.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 ifisZip
is true.
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:
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.
public async Task SelectAll() { await _module.InvokeVoidAsync("selectAll", browseViewer); }
JavaScript
export function selectAll(viewer) { if (viewer != null) { viewer.selectAllPages(); } }
-
Task UnselectAll()
: Unselects all files currently managed by the document manager.
public async Task UnselectAll() { await _module.InvokeVoidAsync("unselectAll", browseViewer); }
JavaScript
export function unselectAll(viewer) { if (viewer != null) { viewer.selectPages([]); } }
-
Task RemoveSelected()
: Removes the files currently selected in the document manager.
public async Task RemoveSelected() { await _module.InvokeVoidAsync("removeSelected", _jsObjectReference, browseViewer); }
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.
public async Task RemoveAll() { await _module.InvokeVoidAsync("removeAll", _jsObjectReference); }
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.
- Request a free trial license for Dynamsoft Document Viewer.
- Install RazorCameraLibrary and RazorImageEditorLibrary via NuGet package manager into your project. These libraries allow the input source to be either a local file or a camera.
-
Import the dependencies in the
Pages/Index.razor
file.
@page "/" @inject IJSRuntime JSRuntime @using RazorImageEditorLibrary @using RazorCameraLibrary @using Camera = RazorCameraLibrary.Camera
-
Create three div elements for the browse viewer, edit viewer, and camera preview, respectively.
<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>
-
Load the image editor and camera libraries.
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"); }
-
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.
<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; } }
-
Create a button to open a camera.
<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); } } }
-
Load an image into the browse viewer from either a local file or the camera.
<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); } } }
-
Add a file conversion button to convert selected images to a specified format.
<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; } }
-
Run the Blazor app in a web browser.