Complete Document Capturing Workflow
This sample demonstrates a complete document capturing workflow: Capture continuously & Review and Adjust the detected boundaries & Edit result images.
In this sample, we would like to achieve the workflow as below.
We’ll build on this skeleton page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Web Capture - Complete Document Capturing Workflow</title>
</head>
<body>
</body>
<script type="module">
// Write your code here
</script>
</html>
Adding the dependency
This sample is using a CDN to include the SDKs. Please refer to Adding the dependency - Use a CDN.
If you would like to host the resources files on your own server. Please refer to Adding the dependency - Host yourself.
Define necessary HTML elements
For this sample, we define below element.
- Container to hold the viewer
<div id="container"></div>
Link CSS to HTML
ddv.css
is the necessary css file which defines the viewer style of Dynamsoft Document Viewer.
index.css
defines the style of elements which is in this sample.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.css">
<link rel="stylesheet" href="./index.css">
index.css
content:
html,body {
width: 100%;
height: 100%;
margin:0;
padding:0;
overscroll-behavior-y: none;
}
#container {
width: 100%;
height: 100%;
}
Related SDK initialization
//Preloads the Document Normalizer module.
Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
//Preloads the Document Viewer module.
Dynamsoft.DDV.Core.loadWasm();
// Initialize DDN
await Dynamsoft.License.LicenseManager.initLicense(
"DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9",
true
);
// Initialize DDV
await Dynamsoft.DDV.Core.init();
Configure document boundaries function
-
Step one: The related configuration code is packaged in
utils.js
, so it should be imported.import { isMobile, initDocDetectModule } from "./utils.js";
-
Step two: Call the following function.
await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);
utils.js
content:
export function isMobile(){
return "ontouchstart" in document.documentElement;
}
export async function initDocDetectModule(DDV, CVR) {
const router = await CVR.CaptureVisionRouter.createInstance();
class DDNNormalizeHandler extends DDV.DocumentDetect {
async detect(image, config) {
if (!router) {
return Promise.resolve({
success: false
});
};
let width = image.width;
let height = image.height;
let ratio = 1;
let data;
if (height > 720) {
ratio = height / 720;
height = 720;
width = Math.floor(width / ratio);
data = compress(image.data, image.width, image.height, width, height);
} else {
data = image.data.slice(0);
}
// Define DSImage according to the usage of DDN
const DSImage = {
bytes: new Uint8Array(data),
width,
height,
stride: width * 4, //RGBA
format: 10 // IPF_ABGR_8888
};
// Use DDN normalized module
const results = await router.capture(DSImage, 'detect-document-boundaries');
// Filter the results and generate corresponding return values
if (results.items.length <= 0) {
return Promise.resolve({
success: false
});
};
const quad = [];
results.items[0].location.points.forEach((p) => {
quad.push([p.x * ratio, p.y * ratio]);
});
const detectResult = this.processDetectResult({
location: quad,
width: image.width,
height: image.height,
config
});
return Promise.resolve(detectResult);
}
}
DDV.setProcessingHandler('documentBoundariesDetect', new DDNNormalizeHandler())
}
function compress(
imageData,
imageWidth,
imageHeight,
newWidth,
newHeight,
) {
let source = null;
try {
source = new Uint8ClampedArray(imageData);
} catch (error) {
source = new Uint8Array(imageData);
}
const scaleW = newWidth / imageWidth;
const scaleH = newHeight / imageHeight;
const targetSize = newWidth * newHeight * 4;
const targetMemory = new ArrayBuffer(targetSize);
let distData = null;
try {
distData = new Uint8ClampedArray(targetMemory, 0, targetSize);
} catch (error) {
distData = new Uint8Array(targetMemory, 0, targetSize);
}
const filter = (distCol, distRow) => {
const srcCol = Math.min(imageWidth - 1, distCol / scaleW);
const srcRow = Math.min(imageHeight - 1, distRow / scaleH);
const intCol = Math.floor(srcCol);
const intRow = Math.floor(srcRow);
let distI = (distRow * newWidth) + distCol;
let srcI = (intRow * imageWidth) + intCol;
distI *= 4;
srcI *= 4;
for (let j = 0; j <= 3; j += 1) {
distData[distI + j] = source[srcI + j];
}
};
for (let col = 0; col < newWidth; col += 1) {
for (let row = 0; row < newHeight; row += 1) {
filter(col, row);
}
}
return distData;
}
Configure image filter feature which is in edit viewer
Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
Create a capture viewer
To capture images, we need to create a capture viewer.
- Customize the capture viewer
UiConfig
based on the default one to implement the workflow.- Bind click event to “ImagePreview” element to show the perspective viewer
const newCaptureViewerUiConfig = { type: Dynamsoft.DDV.Elements.Layout, flexDirection: "column", children: [ { type: Dynamsoft.DDV.Elements.Layout, className: "ddv-capture-viewer-header-mobile", children: [ { type: Dynamsoft.DDV.Elements.CameraResolution, className: "ddv-capture-viewer-resolution", }, Dynamsoft.DDV.Elements.Flashlight, ], }, Dynamsoft.DDV.Elements.MainView, { type: Dynamsoft.DDV.Elements.Layout, className: "ddv-capture-viewer-footer-mobile", children: [ Dynamsoft.DDV.Elements.AutoDetect, Dynamsoft.DDV.Elements.AutoCapture, { type: Dynamsoft.DDV.Elements.Capture, className: "ddv-capture-viewer-captureButton", }, { // Bind click event to "ImagePreview" element // The event will be registered later type: Dynamsoft.DDV.Elements.ImagePreview, events: { click: "showPerspectiveViewer" }, }, Dynamsoft.DDV.Elements.CameraConvert, ], }, ], };
- Bind click event to “ImagePreview” element to show the perspective viewer
-
Create the viewer by using the new
UiConfig
.// Create a capture viewer const captureViewer = new Dynamsoft.DDV.CaptureViewer({ container: "container", uiConfig: newCaptureViewerUiConfig, // Configure the new UiConfig viewerConfig: { acceptedPolygonConfidence: 60, // Configure the accpeted confidence to 60 enableAutoCapture: true, // Enable auto capture enableAutoDetect: true, // Enable real-time detection }, }); // Play video stream in 1080P captureViewer.play({ resolution: [1920,1080], });
Create a perspective viewer
- Customize the viewer’s
UiConfig
based on the default one to implement the workflow.- Add a “Back” buttom to header and bind click event to go back to the capture viewer
- Bind click event to “PerspectiveAll” button to show the edit viewer
const newPerspectiveUiConfig = { type: Dynamsoft.DDV.Elements.Layout, flexDirection: "column", children: [ { type: Dynamsoft.DDV.Elements.Layout, className: "ddv-perspective-viewer-header-mobile", children: [ { // Add a "Back" button in perspective viewer's header and bind the event to go back to capture viewer // The event will be registered later type: Dynamsoft.DDV.Elements.Button, className: "ddv-button-back", events:{ click: "backToCaptureViewer" } }, Dynamsoft.DDV.Elements.Pagination, { // Bind event for "PerspectiveAll" button to show the edit viewer // The event will be registered later type: Dynamsoft.DDV.Elements.PerspectiveAll, events:{ click: "showEditViewer" } }, ], }, Dynamsoft.DDV.Elements.MainView, { type: Dynamsoft.DDV.Elements.Layout, className: "ddv-perspective-viewer-footer-mobile", children: [ Dynamsoft.DDV.Elements.FullQuad, Dynamsoft.DDV.Elements.RotateLeft, Dynamsoft.DDV.Elements.RotateRight, Dynamsoft.DDV.Elements.DeleteCurrent, Dynamsoft.DDV.Elements.DeleteAll, ], }, ], };
-
Create the viewer by using the new
UiConfig
.// Create a perspective viewer const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({ container: "container", groupUid: captureViewer.groupUid, // Data synchronisation with the capture viewer uiConfig: newPerspectiveUiConfig, // Configure the new UiConfig viewerConfig:{ scrollToLatest: true, // Navigate to the latest image automatically } });
-
Since this viewer only shows when clicking “ImagePreview” element in the capture viewer, it should be hidden at first.
perspectiveViewer.hide();
Create an edit viewer
To review and edit the captured images, we create an edit viewer.
- Customize the capture viewer
UiConfig
based on the default one to implement the workflow.- Add a “Back” buttom to header and bind click event to go back the perspective viewer
const newEditViewerUiConfig = { type: Dynamsoft.DDV.Elements.Layout, flexDirection: "column", className: "ddv-edit-viewer-mobile", children: [ { type: Dynamsoft.DDV.Elements.Layout, className: "ddv-edit-viewer-header-mobile", children: [ { // Add a "Back" buttom to header and bind click event to go back to the perspective viewer // The event will be registered later type: Dynamsoft.DDV.Elements.Button, className: "ddv-button-back", events: { click: "backToPerspectiveViewer" }, }, Dynamsoft.DDV.Elements.Pagination, Dynamsoft.DDV.Elements.Load, Dynamsoft.DDV.Elements.Download, ], }, Dynamsoft.DDV.Elements.MainView, { type: Dynamsoft.DDV.Elements.Layout, className: "ddv-edit-viewer-footer-mobile", children: [ Dynamsoft.DDV.Elements.DisplayMode, Dynamsoft.DDV.Elements.RotateLeft, Dynamsoft.DDV.Elements.Crop, Dynamsoft.DDV.Elements.Filter, Dynamsoft.DDV.Elements.Undo, Dynamsoft.DDV.Elements.Delete, Dynamsoft.DDV.Elements.AnnotationSet, ], }, ], };
- Add a “Back” buttom to header and bind click event to go back the perspective viewer
-
Create the viewer by using the new
UiConfig
.// Create an edit viewer const editViewer = new Dynamsoft.DDV.EditViewer({ container: "container", groupUid: captureViewer.groupUid, // Data synchronisation with the capture viewer uiConfig: newEditViewerUiConfig, // Configure the new UiConfig });
-
Since this viewer only shows when clicking “PerspectiveAll” button in the perspective viewer, it should be hidden at first.
editViewer.hide();
Configure the workflow
-
Define a function to control the viewers’ visibility.
function switchViewer(capture, perspective, edit) { captureViewer.hide(); perspectiveViewer.hide(); editViewer.hide(); if(capture) { captureViewer.show(); captureViewer.play(); } else { captureViewer.stop(); } if(perspective) perspectiveViewer.show(); if(edit) editViewer.show(); }
-
Register an event in
captureViewer
to show the perspective viewer.captureViewer.on("showPerspectiveViewer",() => { switchViewer(0,1,0); });
-
Register an event in
perspectiveViewer
to show the edit viewer.perspectiveViewer.on("showEditViewer",() => { switchViewer(0,0,1) });
-
Register an event in
perspectiveViewer
to go back the capture viewer.perspectiveViewer.on("backToCaptureViewer",() => { switchViewer(1,0,0); });
-
Register an event in
editViewer
to go back the perspective viewer.editViewer.on("backToPerspectiveViewer",() => { switchViewer(0,1,0); });
Review the complete code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Web Capture - Complete Document Capturing Workflow</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.css">
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div id="container"></div>
</body>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-core@3.2.10/dist/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-license@3.2.10/dist/license.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.2.10/dist/ddn.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.2.10/dist/cvr.js"></script>
<script type="module">
import { isMobile, initDocDetectModule } from "./utils.js";
(async () => {
//Preloads the Document Normalizer module.
Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
//Preloads the Document Viewer module.
Dynamsoft.DDV.Core.loadWasm();
// Initialize DDN
await Dynamsoft.License.LicenseManager.initLicense(
"DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9",
true
);
// Initialize DDV
await Dynamsoft.DDV.Core.init();
// Configure document boundaries function
await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);
// Configure image filter feature which is in edit viewer
Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
// Define new UiConfig for capture viewer
const newCaptureViewerUiConfig = {
type: Dynamsoft.DDV.Elements.Layout,
flexDirection: "column",
children: [
{
type: Dynamsoft.DDV.Elements.Layout,
className: "ddv-capture-viewer-header-mobile",
children: [
{
type: Dynamsoft.DDV.Elements.CameraResolution,
className: "ddv-capture-viewer-resolution",
},
Dynamsoft.DDV.Elements.Flashlight,
],
},
Dynamsoft.DDV.Elements.MainView,
{
type: Dynamsoft.DDV.Elements.Layout,
className: "ddv-capture-viewer-footer-mobile",
children: [
Dynamsoft.DDV.Elements.AutoDetect,
Dynamsoft.DDV.Elements.AutoCapture,
{
type: Dynamsoft.DDV.Elements.Capture,
className: "ddv-capture-viewer-captureButton",
},
{
// Bind click event to "ImagePreview" element
// The event will be registered later
type: Dynamsoft.DDV.Elements.ImagePreview,
events: {
click: "showPerspectiveViewer"
},
},
Dynamsoft.DDV.Elements.CameraConvert,
],
},
],
};
// Create a capture viewer
const captureViewer = new Dynamsoft.DDV.CaptureViewer({
container: "container",
uiConfig: newCaptureViewerUiConfig, // Configure the new UiConfig
viewerConfig: {
acceptedPolygonConfidence: 60, // Configure the accpeted confidence to 60
enableAutoCapture: true, // Enable auto capture
enableAutoDetect: true, // Enable real-time detection
},
});
// Play video stream in 1080P
captureViewer.play({
resolution: [1920,1080],
});
// Define new UiConfig for perspective viewer
const newPerspectiveUiConfig = {
type: Dynamsoft.DDV.Elements.Layout,
flexDirection: "column",
children: [
{
type: Dynamsoft.DDV.Elements.Layout,
className: "ddv-perspective-viewer-header-mobile",
children: [
{
// Add a "Back" button in perspective viewer's header and bind the event to go back to capture viewer
// The event will be registered later
type: Dynamsoft.DDV.Elements.Button,
className: "ddv-button-back",
events:{
click: "backToCaptureViewer"
}
},
Dynamsoft.DDV.Elements.Pagination,
{
// Bind event for "PerspectiveAll" button to show the edit viewer
// The event will be registered later
type: Dynamsoft.DDV.Elements.PerspectiveAll,
events:{
click: "showEditViewer"
}
},
],
},
Dynamsoft.DDV.Elements.MainView,
{
type: Dynamsoft.DDV.Elements.Layout,
className: "ddv-perspective-viewer-footer-mobile",
children: [
Dynamsoft.DDV.Elements.FullQuad,
Dynamsoft.DDV.Elements.RotateLeft,
Dynamsoft.DDV.Elements.RotateRight,
Dynamsoft.DDV.Elements.DeleteCurrent,
Dynamsoft.DDV.Elements.DeleteAll,
],
},
],
};
// Create a perspective viewer
const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
container: "container",
groupUid: captureViewer.groupUid, // Data synchronisation with the capture viewer
uiConfig: newPerspectiveUiConfig, // Configure the new UiConfig
viewerConfig:{
scrollToLatest: true, // Navigate to the latest image automatically
}
});
// Define new UiConfig for edit viewer
const newEditViewerUiConfig = {
type: Dynamsoft.DDV.Elements.Layout,
flexDirection: "column",
className: "ddv-edit-viewer-mobile",
children: [
{
type: Dynamsoft.DDV.Elements.Layout,
className: "ddv-edit-viewer-header-mobile",
children: [
{
// Add a "Back" buttom to header and bind click event to go back to the perspective viewer
// The event will be registered later
type: Dynamsoft.DDV.Elements.Button,
className: "ddv-button-back",
events: {
click: "backToPerspectiveViewer"
},
},
Dynamsoft.DDV.Elements.Pagination,
Dynamsoft.DDV.Elements.Load,
Dynamsoft.DDV.Elements.Download,
],
},
Dynamsoft.DDV.Elements.MainView,
{
type: Dynamsoft.DDV.Elements.Layout,
className: "ddv-edit-viewer-footer-mobile",
children: [
Dynamsoft.DDV.Elements.DisplayMode,
Dynamsoft.DDV.Elements.RotateLeft,
Dynamsoft.DDV.Elements.Crop,
Dynamsoft.DDV.Elements.Filter,
Dynamsoft.DDV.Elements.Undo,
Dynamsoft.DDV.Elements.Delete,
Dynamsoft.DDV.Elements.AnnotationSet,
],
},
],
};
// Create an edit viewer
const editViewer = new Dynamsoft.DDV.EditViewer({
container: "container",
groupUid: captureViewer.groupUid, // Data synchronisation with the capture viewer
uiConfig: newEditViewerUiConfig, // Configure the new UiConfig
});
// Register an event in `captureViewer` to show the perspective viewer
captureViewer.on("showPerspectiveViewer",() => {
switchViewer(0,1,0);
});
// Register an event in `perspectiveViewer` to show the edit viewer
perspectiveViewer.on("showEditViewer",() => {
switchViewer(0,0,1)
});
// Register an event in `perspectiveViewer` to go back the capture viewer
perspectiveViewer.on("backToCaptureViewer",() => {
switchViewer(1,0,0);
});
// Register an event in `editViewer` to go back the perspective viewer
editViewer.on("backToPerspectiveViewer",() => {
switchViewer(0,1,0);
});
// Control viewers' visibility.
function switchViewer(capture, perspective, edit) {
captureViewer.hide();
perspectiveViewer.hide();
editViewer.hide();
if(capture) {
captureViewer.show();
captureViewer.play();
} else {
captureViewer.stop();
}
if(perspective) perspectiveViewer.show();
if(edit) editViewer.show();
}
})();
</script>
Download the whole project
Please note that in order to be compatible with desktop devices as much as possible, some compatibility codes have been added to the whole project code.
UiConfig
part is organized into uiConfig.js
and referenced in the core code to minimize the length of the core code.
Add auxiliary text if necessary
Sometimes, you may want to add some auxiliary text to icons to show better user guidance.