diff --git a/samples/capture-single-page-and-then-crop/index.css b/samples/capture-single-page-and-then-crop/index.css
new file mode 100644
index 0000000..2050ec1
--- /dev/null
+++ b/samples/capture-single-page-and-then-crop/index.css
@@ -0,0 +1,73 @@
+html,body {
+    width: 100%;
+    height: 100%;
+    margin:0;
+    padding:0;
+    overscroll-behavior-y: none;
+    overflow: hidden;
+}
+
+#container {
+    width: 100%;
+    height: 100%;
+}
+
+#imageContainer {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    box-sizing: border-box;
+    align-items: center;
+    flex-direction: column;
+    padding: 20px 0px;
+    justify-content: flex-start
+}
+
+#imageContainer img {
+    width: 90%;
+    height: 60%;
+    object-fit: contain;
+    border:none;
+}
+
+#restore {
+    display: flex;
+    width: 80px;
+    height: 40px;
+    align-items: center;
+    background: #fe8e14;
+    justify-content: center;
+    color: white;
+    cursor: pointer;
+    user-select: none;
+}
+
+.mwc-loading-bar {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 999;
+    background-color: white;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.mwc-loading-bar span {
+    margin-top: 10px;
+    color: #535251;
+}
+
+.loader {
+    width: 50px;
+    aspect-ratio: 1;
+    border-radius: 50%;
+    border: 8px solid #d9d9d9;
+    border-right-color: #535251;
+    animation: l2 1s infinite linear;
+}
+
+@keyframes l2 {to{transform: rotate(1turn)}}
diff --git a/samples/capture-single-page-and-then-crop/index.html b/samples/capture-single-page-and-then-crop/index.html
new file mode 100644
index 0000000..6e92156
--- /dev/null
+++ b/samples/capture-single-page-and-then-crop/index.html
@@ -0,0 +1,160 @@
+<!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 - Capture Single Page and Then Crop</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>
+    <div id="imageContainer">
+        <div id="restore">Restore</div>
+        <span>Normalized Image:</span>
+        <img id="normalized">
+    </div>
+</body>
+<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 src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.js"></script>
+<script>
+    if(location.protocol === "file:") {
+        const message = `Please open the page via https:// or host it on "http://localhost/".`;
+        console.warn(message);
+        alert(message);
+    };
+</script>
+<script type="module">
+    import { 
+        isMobile,
+        initDocDetectModule,
+        startLoading,
+        updateLoadingText,
+        stopLoading
+    } from "./utils.js";
+    import { mobilePerspectiveUiConfig, pcPerspectiveUiConfig } from "./uiConfig.js";
+    // Writing style of 'Top-level await' to be compatible with older versions of browsers
+    (async () => {
+        startLoading("Waiting for authorization...");
+        // Preload DDV Resource
+        Dynamsoft.DDV.Core.loadWasm();
+        
+        /** LICENSE ALERT - README
+         * To use the library, you need to first specify a license key using the API "initLicense()" as shown below.
+         */
+
+        // Initialize DDN license
+        await Dynamsoft.License.LicenseManager.initLicense(
+            "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLXIxNzAzODM5ODkwIiwibWFpblNlcnZlclVSTCI6Imh0dHBzOi8vbWx0cy5keW5hbXNvZnQuY29tLyIsIm9yZ2FuaXphdGlvbklEIjoiMjAwMDAwIiwic3RhbmRieVNlcnZlclVSTCI6Imh0dHBzOi8vc2x0cy5keW5hbXNvZnQuY29tLyIsImNoZWNrQ29kZSI6MTgyNTQ5Njk4NH0=",
+            true
+        );
+
+        /**
+         * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mwc to get your own trial license good for 30 days.
+         * Note that if you downloaded this sample from Dynamsoft while logged in, the above license key may already be your own 30-day trial license.
+         * For more information, see https://www.dynamsoft.com/mobile-web-capture/docs/gettingstarted/license.html or contact support@dynamsoft.com.
+         * LICENSE ALERT - THE END
+         */
+
+        updateLoadingText("Loading DDV library...");
+        // Preload DDN Resource
+        Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
+
+        // Initialize DDV
+        await Dynamsoft.DDV.Core.init();
+
+        updateLoadingText("Loading DDN library...");
+        // Configure document boundaries function
+        await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);
+        stopLoading();
+
+        //Create a capture viewer
+        const captureViewer = new Dynamsoft.DDV.CaptureViewer({
+            container: "container",
+            viewerConfig: {
+                acceptedPolygonConfidence: 60,
+                enableAutoDetect: true,
+            }
+        });
+
+        // Play video stream in 1080P
+        captureViewer.play({
+            resolution: [1920,1080],
+        }).catch(err => {
+            alert(err.message)
+        });
+
+        // Register captured event
+        captureViewer.on("captured", () => {
+            switchViewer(false, true);
+        });
+
+        // Create a perspective viewer
+        const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
+            container: "container",
+            groupUid: captureViewer.groupUid,
+            uiConfig: isMobile()? mobilePerspectiveUiConfig : pcPerspectiveUiConfig,
+            viewerConfig:{
+                scrollToLatest: true,
+            }
+        });
+        perspectiveViewer.hide();
+
+        // Register the event for "PerspectiveAll" button to display the result image
+        perspectiveViewer.on("done", async () => {
+            switchViewer(false, false);
+            document.getElementById("container").style.display = "none";
+
+            const pageUid = perspectiveViewer.getCurrentPageUid();
+            const pageData =  await captureViewer.currentDocument.getPageData(pageUid);
+            // Normalized image
+            document.getElementById("normalized").src = URL.createObjectURL(pageData.display.data);
+        });
+
+        // Register the event for "Back" button
+        perspectiveViewer.on("backToCaptureViewer", () => {
+            switchViewer(true, false);
+            perspectiveViewer.currentDocument.deleteAllPages();
+        });
+
+        // Register the event for "DeleteCurrent" & "DeletedAll" buttons
+        perspectiveViewer.on("noImageBack", () => {
+            // Determine if there are no images in the viewer
+            const count = perspectiveViewer.currentDocument.pages.length;
+
+            if(count === 0) {
+                switchViewer(true,false);
+            }
+        });
+
+        // Define a function to control the viewers' visibility.
+        function switchViewer(capture, perspective){
+            if(capture) {
+                captureViewer.show();
+                captureViewer.play().catch(err => {alert(err)});
+            } else {
+                captureViewer.hide();
+                captureViewer.stop();
+            }
+
+            if(perspective) {
+                perspectiveViewer.show();
+            } else {
+                perspectiveViewer.hide();
+            }
+        };
+
+        // Restore Button function
+        document.getElementById("restore").onclick = () => {
+            perspectiveViewer.currentDocument.deleteAllPages();
+
+            document.getElementById("container").style.display = "";
+            switchViewer(true, false);
+        };
+    })();
+</script>
+</html>
\ No newline at end of file
diff --git a/samples/capture-single-page-and-then-crop/uiConfig.js b/samples/capture-single-page-and-then-crop/uiConfig.js
new file mode 100644
index 0000000..39690fa
--- /dev/null
+++ b/samples/capture-single-page-and-then-crop/uiConfig.js
@@ -0,0 +1,117 @@
+//Mobile PerspectiveViewer
+export const mobilePerspectiveUiConfig = {
+    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.
+                    // 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
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.PerspectiveAll,
+                    events:{
+                        click: "done"
+                    }
+                },
+            ],
+        },
+        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,
+                {   
+                    // Bind event for "DeleteCurrent" button
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.DeleteCurrent,
+                    events: {
+                        click: "noImageBack"
+                    },
+                },
+                {   
+                    // Bind event for "DeleteAll" button
+                    // The event will be registered later.
+                    type:Dynamsoft.DDV.Elements.DeleteAll,
+                    events: {
+                        click: "noImageBack"
+                    },
+                }
+            ],
+        },
+    ],
+};
+
+//Pc PerspectiveViewer
+export const pcPerspectiveUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-header-desktop",
+            children: [
+                {   
+                    // Add a "Back" button in perspective viewer's header and bind the event.
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.Button,
+                    className: "ddv-button-back",
+                    style: {
+                        position: "absolute",
+                        left: "0px",
+                    },
+                    events:{
+                        click: "backToCaptureViewer"
+                    }
+                },
+                Dynamsoft.DDV.Elements.FullQuad,
+                Dynamsoft.DDV.Elements.RotateLeft,
+                Dynamsoft.DDV.Elements.RotateRight,
+                {
+                    // Bind event for "DeleteCurrent" button
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.DeleteCurrent,
+                    events: {
+                        click: "noImageBack"
+                    },
+                },
+                {
+                    // Bind event for "DeleteAll" button
+                    // The event will be registered later.
+                    type:Dynamsoft.DDV.Elements.DeleteAll,
+                    events: {
+                        click: "noImageBack"
+                    },
+                },
+                {
+                    type: Dynamsoft.DDV.Elements.Pagination,
+                    className: "ddv-perspective-viewer-pagination-desktop",
+                },
+                {   
+                    // Bind event for "PerspectiveAll" button
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.PerspectiveAll,
+                    className: "ddv-perspective-viewer-perspective-desktop",
+                    events:{
+                        click: "done"
+                    }
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+    ],
+};
\ No newline at end of file
diff --git a/samples/capture-single-page-and-then-crop/utils.js b/samples/capture-single-page-and-then-crop/utils.js
new file mode 100644
index 0000000..182cedb
--- /dev/null
+++ b/samples/capture-single-page-and-then-crop/utils.js
@@ -0,0 +1,147 @@
+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())
+}
+
+export function startLoading(text){
+    const loadingBar = document.createElement('div');
+    loadingBar.className = "mwc-loading-bar";
+
+    loadingBar.innerHTML = [
+        `<div class='loader'></div>`,
+        `<span id='mwcLoadingText'>${text}</span>`
+    ].join('')
+
+    document.body.appendChild(loadingBar);
+}
+
+export function updateLoadingText(text){
+    const loadingText = document.getElementById("mwcLoadingText");
+
+    if(loadingText){
+        loadingText.innerHTML = text;
+    }
+}
+
+export function stopLoading(){
+    const loadingBar = document.getElementsByClassName("mwc-loading-bar");
+
+    if(loadingBar.length > 0){
+        loadingBar[0].remove();
+    }
+}
+
+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;
+}
diff --git a/samples/complete-document-capturing-workflow/index.css b/samples/complete-document-capturing-workflow/index.css
new file mode 100644
index 0000000..96291bb
--- /dev/null
+++ b/samples/complete-document-capturing-workflow/index.css
@@ -0,0 +1,42 @@
+html,body {
+    width: 100%;
+    height: 100%;
+    margin:0;
+    padding:0;
+    overscroll-behavior-y: none;
+}
+
+#container {
+    width: 100%;
+    height: 100%;
+}
+
+.mwc-loading-bar {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 999;
+    background-color: white;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.mwc-loading-bar span {
+    margin-top: 10px;
+    color: #535251;
+}
+
+.loader {
+    width: 50px;
+    aspect-ratio: 1;
+    border-radius: 50%;
+    border: 8px solid #d9d9d9;
+    border-right-color: #535251;
+    animation: l2 1s infinite linear;
+}
+
+@keyframes l2 {to{transform: rotate(1turn)}}
\ No newline at end of file
diff --git a/samples/complete-document-capturing-workflow/index.html b/samples/complete-document-capturing-workflow/index.html
new file mode 100644
index 0000000..4f10c9c
--- /dev/null
+++ b/samples/complete-document-capturing-workflow/index.html
@@ -0,0 +1,158 @@
+<!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-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 src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.js"></script>
+
+<script>
+    if(location.protocol === "file:") {
+        const message = `Please open the page via https:// or host it on "http://localhost/".`;
+        console.warn(message);
+        alert(message);
+    };
+</script>
+<script type="module">
+    import { 
+        isMobile,
+        initDocDetectModule,
+        startLoading,
+        updateLoadingText,
+        stopLoading
+    } from "./utils.js";
+    import { 
+        mobileCaptureViewerUiConfig,
+        mobilePerspectiveUiConfig,
+        mobileEditViewerUiConfig,
+        pcCaptureViewerUiConfig,
+        pcPerspectiveUiConfig,
+        pcEditViewerUiConfig
+    } from "./uiConfig.js";
+    
+    // Writing style of 'Top-level await' to be compatible with older versions of browsers
+    (async () => {
+        startLoading("Waiting for authorization...");
+        // Preload DDV Resource
+        Dynamsoft.DDV.Core.loadWasm();
+        
+        /** LICENSE ALERT - README
+         * To use the library, you need to first specify a license key using the API "initLicense()" as shown below.
+         */
+
+        // Initialize DDN license
+        await Dynamsoft.License.LicenseManager.initLicense(
+            "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLXIxNzAzODM5ODkwIiwibWFpblNlcnZlclVSTCI6Imh0dHBzOi8vbWx0cy5keW5hbXNvZnQuY29tLyIsIm9yZ2FuaXphdGlvbklEIjoiMjAwMDAwIiwic3RhbmRieVNlcnZlclVSTCI6Imh0dHBzOi8vc2x0cy5keW5hbXNvZnQuY29tLyIsImNoZWNrQ29kZSI6MTgyNTQ5Njk4NH0=",
+            true
+        );
+
+        /**
+         * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mwc to get your own trial license good for 30 days.
+         * Note that if you downloaded this sample from Dynamsoft while logged in, the above license key may already be your own 30-day trial license.
+         * For more information, see https://www.dynamsoft.com/mobile-web-capture/docs/gettingstarted/license.html or contact support@dynamsoft.com.
+         * LICENSE ALERT - THE END
+         */
+
+        updateLoadingText("Loading DDV library...");
+        // Preload DDN Resource
+        Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
+
+        // Initialize DDV
+        await Dynamsoft.DDV.Core.init();
+
+        updateLoadingText("Loading DDN library...");
+        // Configure document boundaries function
+        await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);
+        stopLoading();
+
+        // Configure image filter feature which is in edit viewer
+        Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
+
+        // Create a capture viewer
+        const captureViewer = new Dynamsoft.DDV.CaptureViewer({
+            container: "container",
+            uiConfig: isMobile() ? mobileCaptureViewerUiConfig : pcCaptureViewerUiConfig,
+            viewerConfig: {
+                acceptedPolygonConfidence: 60,
+                enableAutoDetect: true,
+            }
+        });
+
+        // Play video stream in 1080P
+        captureViewer.play({
+            resolution: [1920,1080],
+        }).catch(err => {
+            alert(err.message)
+        });
+
+        // Register an event in `captureViewer` to show the perspective viewer
+        captureViewer.on("showPerspectiveViewer",() => {
+            switchViewer(0,1,0);
+        });
+
+        // Create a perspective viewer
+        const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
+            container: "container",
+            groupUid: captureViewer.groupUid,
+            uiConfig: isMobile() ? mobilePerspectiveUiConfig : pcPerspectiveUiConfig,
+            viewerConfig: {
+                scrollToLatest: true,
+            }
+        });
+
+        perspectiveViewer.hide();
+
+        // Register an event in `perspectiveViewer` to go back the capture viewer
+        perspectiveViewer.on("backToCaptureViewer",() => {
+            switchViewer(1,0,0);
+            captureViewer.play().catch(err => {alert(err.message)});
+        });
+
+        // Register an event in `perspectiveViewer` to show the edit viewer
+        perspectiveViewer.on("showEditViewer",() => {
+            switchViewer(0,0,1)
+        });
+        
+        // Create an edit viewer
+        const editViewer = new Dynamsoft.DDV.EditViewer({
+            container: "container",
+            groupUid: captureViewer.groupUid,
+            uiConfig: isMobile() ? mobileEditViewerUiConfig : pcEditViewerUiConfig
+        });
+
+        editViewer.hide();
+
+        // Register an event in `editViewer` to go back the perspective viewer
+        editViewer.on("backToPerspectiveViewer",() => {
+            switchViewer(0,1,0);
+        });
+        
+        // Define a function to control the viewers' visibility
+        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();
+        };
+    })();
+</script>
+</html>
\ No newline at end of file
diff --git a/samples/complete-document-capturing-workflow/uiConfig.js b/samples/complete-document-capturing-workflow/uiConfig.js
new file mode 100644
index 0000000..afc80d8
--- /dev/null
+++ b/samples/complete-document-capturing-workflow/uiConfig.js
@@ -0,0 +1,262 @@
+// Mobile CaptureViewer
+export const mobileCaptureViewerUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-capture-viewer-header-mobile",
+            children: [
+                {
+                    type: "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: "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,
+            ],
+        },
+    ],
+};
+
+// Mobile PerspectiveViewer
+export const mobilePerspectiveUiConfig = {
+    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,
+            ],
+        },
+    ],
+};
+
+// Mobile EditViewer
+export const mobileEditViewerUiConfig = {
+    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,
+            ],
+        },
+    ],
+};
+
+// Pc CaptureViewer
+export const pcCaptureViewerUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    className: "ddv-capture-viewer-desktop",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-capture-viewer-header-desktop",
+            children: [
+                {
+                    type: Dynamsoft.DDV.Elements.CameraResolution,
+                    className: "ddv-capture-viewer-resolution-desktop",
+                },
+                Dynamsoft.DDV.Elements.AutoDetect,
+                {
+                    type: Dynamsoft.DDV.Elements.Capture,
+                    className: "ddv-capture-viewer-capture-desktop",
+                },
+                Dynamsoft.DDV.Elements.AutoCapture,
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+        {
+            // Bind click event to "ImagePreview" element
+            // The event will be registered later.
+            type: Dynamsoft.DDV.Elements.ImagePreview,
+            className: "ddv-capture-viewer-image-preview-desktop",
+            events:{
+                click: "showPerspectiveViewer" 
+            }
+        },
+    ],
+};
+
+// Pc PerspectiveViewer
+export const pcPerspectiveUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-header-desktop",
+            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",
+                    style:{
+                        position: "absolute",
+                        left: "0px",
+                    },
+                    events:{
+                        click: "backToCaptureViewer"
+                    }
+                },
+                Dynamsoft.DDV.Elements.FullQuad,
+                Dynamsoft.DDV.Elements.RotateLeft,
+                Dynamsoft.DDV.Elements.RotateRight,
+                Dynamsoft.DDV.Elements.DeleteCurrent,
+                Dynamsoft.DDV.Elements.DeleteAll,
+                {
+                    type: Dynamsoft.DDV.Elements.Pagination,
+                    className: "ddv-perspective-viewer-pagination-desktop",
+                },
+                {
+                    // Bind event for "PerspectiveAll" button to show the edit viewer
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.PerspectiveAll,
+                    className: "ddv-perspective-viewer-perspective-desktop",
+                    events:{
+                        click: "showEditViewer"
+                    }
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+    ],
+};
+
+// Pc EditViewer
+export const pcEditViewerUiConfig = {
+    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: [
+                        {
+                            // Add a "Back" button 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.ThumbnailSwitch,
+                        Dynamsoft.DDV.Elements.Zoom,
+                        Dynamsoft.DDV.Elements.FitMode,
+                        Dynamsoft.DDV.Elements.DisplayMode,
+                        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,
+                        Dynamsoft.DDV.Elements.Pan,
+						Dynamsoft.DDV.Elements.AnnotationSet,		
+                    ],
+                },
+                {
+                    type: Dynamsoft.DDV.Elements.Layout,
+                    children: [
+                        {
+                            type: Dynamsoft.DDV.Elements.Pagination,
+                            className: "ddv-edit-viewer-pagination-desktop",
+                        },
+                        Dynamsoft.DDV.Elements.Load,
+                        Dynamsoft.DDV.Elements.Download,
+                        Dynamsoft.DDV.Elements.Print,
+                    ],
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+    ],
+};
diff --git a/samples/complete-document-capturing-workflow/utils.js b/samples/complete-document-capturing-workflow/utils.js
new file mode 100644
index 0000000..1c2bd74
--- /dev/null
+++ b/samples/complete-document-capturing-workflow/utils.js
@@ -0,0 +1,147 @@
+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())
+}
+
+export function startLoading(text){
+    const loadingBar = document.createElement('div');
+    loadingBar.className = "mwc-loading-bar";
+
+    loadingBar.innerHTML = [
+        `<div class='loader'></div>`,
+        `<span id='mwcLoadingText'>${text}</span>`
+    ].join('')
+
+    document.body.appendChild(loadingBar);
+}
+
+export function updateLoadingText(text){
+    const loadingText = document.getElementById("mwcLoadingText");
+
+    if(loadingText){
+        loadingText.innerHTML = text;
+    }
+}
+
+export function stopLoading(){
+    const loadingBar = document.getElementsByClassName("mwc-loading-bar");
+
+    if(loadingBar.length > 0){
+        loadingBar[0].remove();
+    }
+}
+
+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;
+}
\ No newline at end of file
diff --git a/samples/detect-boundaries-on-existing-image/index.css b/samples/detect-boundaries-on-existing-image/index.css
new file mode 100644
index 0000000..0f882a1
--- /dev/null
+++ b/samples/detect-boundaries-on-existing-image/index.css
@@ -0,0 +1,72 @@
+html,body {
+    width: 100%;
+    height: 100%;
+    margin:0;
+    padding:0;
+    overscroll-behavior-y: none;
+}
+
+#container {
+    width: 100%;
+    height: 100%;
+}
+
+#imageContainer {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: space-around;
+    box-sizing: border-box;
+    align-items: center;
+    flex-direction: column;
+    padding: 10px 0px;
+}
+
+#imageContainer img {
+    width: 80%;
+    height: 40%;
+    object-fit: contain;
+    border:none;
+}
+
+#restore {
+    display: flex;
+    width: 80px;
+    height: 40px;
+    align-items: center;
+    background: #fe8e14;
+    justify-content: center;
+    color: white;
+    cursor: pointer;
+    user-select: none;
+}
+
+.mwc-loading-bar {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 999;
+    background-color: white;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.mwc-loading-bar span {
+    margin-top: 10px;
+    color: #535251;
+}
+
+.loader {
+    width: 50px;
+    aspect-ratio: 1;
+    border-radius: 50%;
+    border: 8px solid #d9d9d9;
+    border-right-color: #535251;
+    animation: l2 1s infinite linear;
+}
+
+@keyframes l2 {to{transform: rotate(1turn)}}
\ No newline at end of file
diff --git a/samples/detect-boundaries-on-existing-image/index.html b/samples/detect-boundaries-on-existing-image/index.html
new file mode 100644
index 0000000..779aae9
--- /dev/null
+++ b/samples/detect-boundaries-on-existing-image/index.html
@@ -0,0 +1,119 @@
+<!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 - Detect Boundaries on the Existing Image</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>
+    <div id="imageContainer" style="display: none;">
+        <div id="restore">Restore</div>
+        <span>Original Image:</span>
+        <img id="original">
+        <span>Normalized Image:</span>
+        <img id="normalized">
+    </div>
+</body>
+<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 src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.js"></script>
+<script>
+    if(location.protocol === "file:") {
+        const message = `Please open the page via https:// or host it on "http://localhost/".`;
+        console.warn(message);
+        alert(message);
+    };
+</script>
+<script type="module">
+    import { 
+        isMobile,
+        createFileInput,
+        startLoading,
+        updateLoadingText,
+        stopLoading
+    } from "./utils.js";
+    import { mobilePerspectiveUiConfig, pcPerspectiveUiConfig} from "./uiConfig.js"
+    
+    // Writing style of 'Top-level await' to be compatible with older versions of browsers
+    (async () => {
+        startLoading("Waiting for authorization...");
+        // Preload DDV Resource
+        Dynamsoft.DDV.Core.loadWasm();
+        
+        /** LICENSE ALERT - README
+         * To use the library, you need to first specify a license key using the API "initLicense()" as shown below.
+         */
+
+        // Initialize DDN license
+        await Dynamsoft.License.LicenseManager.initLicense(
+            "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLXIxNzAzODM5ODkwIiwibWFpblNlcnZlclVSTCI6Imh0dHBzOi8vbWx0cy5keW5hbXNvZnQuY29tLyIsIm9yZ2FuaXphdGlvbklEIjoiMjAwMDAwIiwic3RhbmRieVNlcnZlclVSTCI6Imh0dHBzOi8vc2x0cy5keW5hbXNvZnQuY29tLyIsImNoZWNrQ29kZSI6MTgyNTQ5Njk4NH0=",
+            true
+        );
+
+        /**
+         * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mwc to get your own trial license good for 30 days.
+         * Note that if you downloaded this sample from Dynamsoft while logged in, the above license key may already be your own 30-day trial license.
+         * For more information, see https://www.dynamsoft.com/mobile-web-capture/docs/gettingstarted/license.html or contact support@dynamsoft.com.
+         * LICENSE ALERT - THE END
+         */
+
+        updateLoadingText("Loading DDV library...");
+        // Preload DDN Resource
+        Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
+
+        // Initialize DDV
+        await Dynamsoft.DDV.Core.init();
+
+        updateLoadingText("Loading DDN library...");
+        const router = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
+        router.maxCvsSideLength = 99999;
+        stopLoading();
+
+        // Create a perspective viewer
+        const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
+            container: "container",
+            uiConfig: isMobile() ? mobilePerspectiveUiConfig : pcPerspectiveUiConfig,
+            viewerConfig: {
+                scrollToLatest: true,
+            }
+        });
+
+        // Create a document and open it in the perspectiveViewer
+        const doc = Dynamsoft.DDV.documentManager.createDocument();
+        perspectiveViewer.openDocument(doc.uid);
+
+        // Create an image input with document boundaries detection
+        const loadImageInput = createFileInput(perspectiveViewer, router);
+
+        // Register an event in `perspectiveViewer` to add existing image(s)
+        perspectiveViewer.on("addNew",() => {
+            delete loadImageInput.files;
+            loadImageInput.click();
+        });
+
+        // Register an event in `perspectiveViewer` to display the result image
+        perspectiveViewer.on("showPerspectiveResult", async () => {
+            document.getElementById("container").style.display = "none";
+            document.getElementById("imageContainer").style.display = "flex";
+
+            const pageData =  await perspectiveViewer.currentDocument.getPageData(perspectiveViewer.getCurrentPageUid());
+            // Original image
+            document.getElementById("original").src = URL.createObjectURL(pageData.raw.data);
+            // Normalized image
+            document.getElementById("normalized").src = URL.createObjectURL(pageData.display.data);
+        });
+
+        // Restore Button function
+        document.getElementById("restore").onclick = () => {
+            document.getElementById("container").style.display = "";
+            document.getElementById("imageContainer").style.display = "none";
+        };
+    })()
+</script>
+</html>
\ No newline at end of file
diff --git a/samples/detect-boundaries-on-existing-image/uiConfig.js b/samples/detect-boundaries-on-existing-image/uiConfig.js
new file mode 100644
index 0000000..f15dc24
--- /dev/null
+++ b/samples/detect-boundaries-on-existing-image/uiConfig.js
@@ -0,0 +1,84 @@
+// Create a "LoadImage" button bind click event to load image
+// The event will be registered later.
+const AddNewButton = {
+    type: Dynamsoft.DDV.Elements.Button,
+    className: "ddv-load-image2",
+    style: {
+        background: "#fe8e14"
+    },
+    events: {
+        click: "addNew"
+    }
+};
+
+export const mobilePerspectiveUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-header-mobile",
+            children: [
+                Dynamsoft.DDV.Elements.Blank,
+                Dynamsoft.DDV.Elements.Pagination,
+                {
+                    // Bind event for "PerspectiveAll" button to download pdf
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.PerspectiveAll,
+                    events: {
+                        click: "showPerspectiveResult"
+                    }
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-footer-mobile",
+            children: [
+                Dynamsoft.DDV.Elements.FullQuad,
+                Dynamsoft.DDV.Elements.RotateLeft,
+                // Replace the default "RotateRight" button with an "AddNew" button in perspective viewer's footer and bind event to the new button.
+                // The event will be registered later.
+                AddNewButton,
+                Dynamsoft.DDV.Elements.DeleteCurrent,
+                Dynamsoft.DDV.Elements.DeleteAll,
+            ],
+        },
+    ],
+};
+
+// PC Perspective Viewer
+export const pcPerspectiveUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-header-desktop",
+            children: [
+                Dynamsoft.DDV.Elements.FullQuad,
+                Dynamsoft.DDV.Elements.RotateLeft,
+                // Replace the default "RotateRight" button with an "AddNew" button in perspective viewer's header and bind event to the new button.
+                // The event will be registered later.
+                AddNewButton,
+                Dynamsoft.DDV.Elements.DeleteCurrent,
+                Dynamsoft.DDV.Elements.DeleteAll,
+                {
+                    type: Dynamsoft.DDV.Elements.Pagination,
+                    className: "ddv-perspective-viewer-pagination-desktop",
+                },
+                {
+                    // Bind event for "PerspectiveAll" button to download pdf
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.PerspectiveAll,
+                    className: "ddv-perspective-viewer-perspective-desktop",
+                    events: {
+                        click: "showPerspectiveResult"
+                    }
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+    ],
+};
\ No newline at end of file
diff --git a/samples/detect-boundaries-on-existing-image/utils.js b/samples/detect-boundaries-on-existing-image/utils.js
new file mode 100644
index 0000000..61bb49d
--- /dev/null
+++ b/samples/detect-boundaries-on-existing-image/utils.js
@@ -0,0 +1,80 @@
+export function isMobile(){
+    return "ontouchstart" in document.documentElement;
+}
+
+export function createFileInput(viewer, router){
+    const input = document.createElement("input");
+    input.accept = "image/png,image/jpeg,image/bmp";
+    input.type = "file";
+    input.multiple = true;
+
+    input.addEventListener("change", async () => {
+        const { files } = input;
+        const len = files.length;
+        const sourceArray = [];
+
+        for (let i = 0; i < len; i++) {
+            const blob = new Blob([files[i]], {
+                type: files[i].type,
+            });
+            const detectResult = await router.capture(blob, "detect-document-boundaries"); 
+
+            if(detectResult.items.length >0) {
+                const quad = [];
+                detectResult.items[0].location.points.forEach(p => {
+                    quad.push([p.x, p.y]);
+                });
+                
+                sourceArray.push({
+                    fileData: blob,
+                    extraPageData:[{
+                        index: 0,
+                        perspectiveQuad: quad
+                    }]
+                })
+            } else {
+                sourceArray.push({
+                    fileData: blob,
+                })
+            }
+        }
+
+        if(sourceArray.length > 0) {
+            viewer.currentDocument.deleteAllPages();
+            viewer.currentDocument.loadSource(sourceArray);
+        }
+
+        input.value = null;
+        input.files = null;
+    },true)
+
+    return input;
+}
+
+export function startLoading(text){
+    const loadingBar = document.createElement('div');
+    loadingBar.className = "mwc-loading-bar";
+
+    loadingBar.innerHTML = [
+        `<div class='loader'></div>`,
+        `<span id='mwcLoadingText'>${text}</span>`
+    ].join('')
+
+    document.body.appendChild(loadingBar);
+}
+
+export function updateLoadingText(text){
+    const loadingText = document.getElementById("mwcLoadingText");
+
+    if(loadingText){
+        loadingText.innerHTML = text;
+    }
+}
+
+export function stopLoading(){
+    const loadingBar = document.getElementsByClassName("mwc-loading-bar");
+
+    if(loadingBar.length > 0){
+        loadingBar[0].remove();
+    }
+}
\ No newline at end of file
diff --git a/samples/hello-world-continuous-mode/css/iconfont.css b/samples/hello-world-continuous-mode/css/iconfont.css
new file mode 100644
index 0000000..33004b8
--- /dev/null
+++ b/samples/hello-world-continuous-mode/css/iconfont.css
@@ -0,0 +1,17 @@
+@font-face {
+  font-family: "iconfont"; /* Project id  */
+  src: url('iconfont.ttf?t=1724659624433') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 22px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-perspective:before {
+  content: "\e6a2";
+}
+
diff --git a/samples/hello-world-continuous-mode/css/iconfont.ttf b/samples/hello-world-continuous-mode/css/iconfont.ttf
new file mode 100644
index 0000000..019557f
Binary files /dev/null and b/samples/hello-world-continuous-mode/css/iconfont.ttf differ
diff --git a/samples/hello-world-continuous-mode/css/index.css b/samples/hello-world-continuous-mode/css/index.css
new file mode 100644
index 0000000..96291bb
--- /dev/null
+++ b/samples/hello-world-continuous-mode/css/index.css
@@ -0,0 +1,42 @@
+html,body {
+    width: 100%;
+    height: 100%;
+    margin:0;
+    padding:0;
+    overscroll-behavior-y: none;
+}
+
+#container {
+    width: 100%;
+    height: 100%;
+}
+
+.mwc-loading-bar {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 999;
+    background-color: white;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.mwc-loading-bar span {
+    margin-top: 10px;
+    color: #535251;
+}
+
+.loader {
+    width: 50px;
+    aspect-ratio: 1;
+    border-radius: 50%;
+    border: 8px solid #d9d9d9;
+    border-right-color: #535251;
+    animation: l2 1s infinite linear;
+}
+
+@keyframes l2 {to{transform: rotate(1turn)}}
\ No newline at end of file
diff --git a/samples/hello-world-continuous-mode/index.css b/samples/hello-world-continuous-mode/index.css
new file mode 100644
index 0000000..96291bb
--- /dev/null
+++ b/samples/hello-world-continuous-mode/index.css
@@ -0,0 +1,42 @@
+html,body {
+    width: 100%;
+    height: 100%;
+    margin:0;
+    padding:0;
+    overscroll-behavior-y: none;
+}
+
+#container {
+    width: 100%;
+    height: 100%;
+}
+
+.mwc-loading-bar {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 999;
+    background-color: white;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.mwc-loading-bar span {
+    margin-top: 10px;
+    color: #535251;
+}
+
+.loader {
+    width: 50px;
+    aspect-ratio: 1;
+    border-radius: 50%;
+    border: 8px solid #d9d9d9;
+    border-right-color: #535251;
+    animation: l2 1s infinite linear;
+}
+
+@keyframes l2 {to{transform: rotate(1turn)}}
\ No newline at end of file
diff --git a/samples/hello-world-continuous-mode/index.html b/samples/hello-world-continuous-mode/index.html
new file mode 100644
index 0000000..52be7f5
--- /dev/null
+++ b/samples/hello-world-continuous-mode/index.html
@@ -0,0 +1,165 @@
+<!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 - HelloWorld - Continuous Mode</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.css">
+    <link rel="stylesheet" href="./css/index.css">
+    <link rel="stylesheet" href="./css/iconfont.css">
+</head>
+<body>
+    <div id="container"></div>
+</body>
+<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 src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@2.0.0/dist/ddv.js"></script>
+<script>
+    if(location.protocol === "file:") {
+        const message = `Please open the page via https:// or host it on "http://localhost/".`;
+        console.warn(message);
+        alert(message);
+    };
+</script>
+<script type="module">
+    import { 
+        isMobile,
+        initDocDetectModule,
+        startLoading,
+        updateLoadingText,
+        stopLoading
+     } from "./utils.js";
+    import { 
+        mobileCaptureViewerUiConfig,
+        mobileEditViewerUiConfig,
+        mobilePerspectiveUiConfig,
+        pcCaptureViewerUiConfig,
+        pcEditViewerUiConfig,
+        pcPerspectiveUiConfig
+    } from "./uiConfig.js";
+
+    // Writing style of 'Top-level await' to be compatible with older versions of browsers
+    (async () => {
+        startLoading("Waiting for authorization...");
+        // Preload DDV Resource
+        Dynamsoft.DDV.Core.loadWasm();
+        
+        /** LICENSE ALERT - README
+         * To use the library, you need to first specify a license key using the API "initLicense()" as shown below.
+         */
+
+        // Initialize DDN license
+        await Dynamsoft.License.LicenseManager.initLicense(
+            "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLXIxNzAzODM5ODkwIiwibWFpblNlcnZlclVSTCI6Imh0dHBzOi8vbWx0cy5keW5hbXNvZnQuY29tLyIsIm9yZ2FuaXphdGlvbklEIjoiMjAwMDAwIiwic3RhbmRieVNlcnZlclVSTCI6Imh0dHBzOi8vc2x0cy5keW5hbXNvZnQuY29tLyIsImNoZWNrQ29kZSI6MTgyNTQ5Njk4NH0=",
+            true
+        );
+
+        /**
+         * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mwc to get your own trial license good for 30 days.
+         * Note that if you downloaded this sample from Dynamsoft while logged in, the above license key may already be your own 30-day trial license.
+         * For more information, see https://www.dynamsoft.com/mobile-web-capture/docs/gettingstarted/license.html or contact support@dynamsoft.com.
+         * LICENSE ALERT - THE END
+         */
+
+        updateLoadingText("Loading DDV library...");
+        // Preload DDN Resource
+        Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
+
+        // Initialize DDV
+        await Dynamsoft.DDV.Core.init();
+
+        updateLoadingText("Loading DDN library...");
+        // Configure document boundaries function
+        await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);
+        stopLoading();
+
+        // Configure image filter feature which is in edit viewer
+        Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
+        
+        // Create a capture viewer
+        const captureViewer = new Dynamsoft.DDV.CaptureViewer({
+            container: "container",
+            uiConfig: isMobile() ? mobileCaptureViewerUiConfig : pcCaptureViewerUiConfig,
+            viewerConfig: {
+                acceptedPolygonConfidence: 60,
+                enableAutoDetect: true,
+            }
+        });
+
+        // Play video stream in 1080P
+        captureViewer.play({
+            resolution: [1920,1080],
+        }).catch(err => {
+            alert(err.message)
+        });
+
+        // Create an edit viewer
+        const editViewer = new Dynamsoft.DDV.EditViewer({
+            container: "container",
+            groupUid: captureViewer.groupUid, // Data sync with the capture viewer 
+            uiConfig: isMobile() ? mobileEditViewerUiConfig : pcEditViewerUiConfig,
+            viewerConfig: {
+                scrollToLatest: true // Navigate to the latest image automatically
+            }
+        });
+
+        editViewer.hide();
+
+        // Create an perspective viewer
+        const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
+            container: "container",
+            groupUid: captureViewer.groupUid, // Data sync with the capture viewer 
+            uiConfig: isMobile() ? mobilePerspectiveUiConfig : pcPerspectiveUiConfig,
+        });
+
+        perspectiveViewer.hide();
+
+        
+        Dynamsoft.DDV.documentManager.on("pagesAdded", () => {
+            updateShowPerspectiveViewerButton();
+        })
+
+        Dynamsoft.DDV.documentManager.on("pagesDeleted", () => {
+            updateShowPerspectiveViewerButton();
+        })
+
+        function updateShowPerspectiveViewerButton(){
+            const pageCount = perspectiveViewer.currentDocument.pages.length;
+            const button = document.querySelector(".icon-perspective");
+            if(pageCount) {
+                button.classList.remove("ddv-button-disabled");
+            } else {
+                button.classList.add("ddv-button-disabled");
+            }
+        };
+
+        // Register an event in `captureViewer` to show the edit viewer
+        captureViewer.on("showEditViewer",() => {
+            captureViewer.hide();
+            captureViewer.stop();
+            editViewer.show();
+        });
+
+        // Register an event in `editViewer` to go back the capture viewer
+        editViewer.on("backToCaptureViewer",() => {
+            captureViewer.show();
+            editViewer.hide();
+            captureViewer.play();
+        });
+
+        editViewer.on("showPerspectiveViewer",() => {
+            editViewer.hide();
+            perspectiveViewer.show();
+        });
+
+        // Register an event in `perspective` to show the edit viewer
+        perspectiveViewer.on("showEditViewer",() => {
+            perspectiveViewer.hide();
+            editViewer.show();
+        });
+    })();
+</script>
+</html>
\ No newline at end of file
diff --git a/samples/hello-world-continuous-mode/uiConfig.js b/samples/hello-world-continuous-mode/uiConfig.js
new file mode 100644
index 0000000..01ae75c
--- /dev/null
+++ b/samples/hello-world-continuous-mode/uiConfig.js
@@ -0,0 +1,275 @@
+// Define new UiConfig for mobile capture viewer
+export const mobileCaptureViewerUiConfig = {
+    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: "showEditViewer",
+                    }
+                },
+                Dynamsoft.DDV.Elements.CameraConvert,
+            ],
+        },
+    ],
+};
+
+// Define new UiConfig for mobile edit viewer
+export const mobileEditViewerUiConfig = {
+    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" button to header and bind click event to go back the capture viewer
+                    // The event will be registered later
+                    type: Dynamsoft.DDV.Elements.Button,
+                    className: "ddv-button-back",
+                    events:{
+                        click: "backToCaptureViewer"
+                    }
+                },
+                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,
+                {
+                    type: Dynamsoft.DDV.Elements.Button,
+                    className: "iconfont icon-perspective",
+                    events:{
+                        click: "showPerspectiveViewer"
+                    }
+                },
+                Dynamsoft.DDV.Elements.Filter,
+                Dynamsoft.DDV.Elements.Undo,
+                Dynamsoft.DDV.Elements.Delete,
+                Dynamsoft.DDV.Elements.AnnotationSet,
+            ],
+        },
+    ],
+};
+
+// Define new UiConfig for mobile perspective viewer
+export const mobilePerspectiveUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-header-mobile",
+            children: [
+                {   
+                    // Add a "Back" button to header and bind click event to go back the edit viewer
+                    // The event will be registered later
+                    type: Dynamsoft.DDV.Elements.Button,
+                    className: "ddv-button-back",
+                    events:{
+                        click: "showEditViewer"
+                    }
+                },
+                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,
+            ],
+        },
+    ],
+};
+
+// Define new UiConfig for pc capture viewer
+export const pcCaptureViewerUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    className: "ddv-capture-viewer-desktop",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-capture-viewer-header-desktop",
+            children: [
+                {
+                    type: Dynamsoft.DDV.Elements.CameraResolution,
+                    className: "ddv-capture-viewer-resolution-desktop",
+                },
+                Dynamsoft.DDV.Elements.AutoDetect,
+                {
+                    type: Dynamsoft.DDV.Elements.Capture,
+                    className: "ddv-capture-viewer-capture-desktop",
+                },
+                Dynamsoft.DDV.Elements.AutoCapture,
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+        {
+            // Bind click event to "ImagePreview" element
+            // The event will be registered later.
+            type: Dynamsoft.DDV.Elements.ImagePreview,
+            className: "ddv-capture-viewer-image-preview-desktop",
+            events:{ 
+                click: "showEditViewer",
+            }
+        },
+    ],
+};
+
+// Define new UiConfig for pc edit viewer
+export const pcEditViewerUiConfig = {
+    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: [
+                        {
+                            // Add a "Back" button to header and bind click event to go back the capture viewer
+                            // The event will be registered later
+                            type: Dynamsoft.DDV.Elements.Button,
+                            className: "ddv-button-back",
+                            events:{
+                                click: "backToCaptureViewer"
+                            }
+                        },
+                        Dynamsoft.DDV.Elements.ThumbnailSwitch,
+                        Dynamsoft.DDV.Elements.Zoom,
+                        Dynamsoft.DDV.Elements.FitMode,
+                        Dynamsoft.DDV.Elements.DisplayMode,
+                        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,
+                        Dynamsoft.DDV.Elements.Pan,
+						Dynamsoft.DDV.Elements.AnnotationSet,
+                        {
+                            type: Dynamsoft.DDV.Elements.Button,
+                            className: "iconfont icon-perspective",
+                            events:{
+                                click: "showPerspectiveViewer"
+                            }
+                        },
+                    ],
+                },
+                {
+                    type: Dynamsoft.DDV.Elements.Layout,
+                    children: [
+                        {
+                            type: Dynamsoft.DDV.Elements.Pagination,
+                            className: "ddv-edit-viewer-pagination-desktop",
+                        },
+                        Dynamsoft.DDV.Elements.Load,
+                        Dynamsoft.DDV.Elements.Download,
+                        Dynamsoft.DDV.Elements.Print,
+                    ],
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+    ],
+};
+
+// Define new UiConfig for pc perspective viewer
+export const pcPerspectiveUiConfig = {
+    type: Dynamsoft.DDV.Elements.Layout,
+    flexDirection: "column",
+    children: [
+        {
+            type: Dynamsoft.DDV.Elements.Layout,
+            className: "ddv-perspective-viewer-header-desktop",
+            children: [
+                {   
+                    // Add a "Back" button to header and bind click event to go back the edit viewer
+                    // The event will be registered later
+                    type: Dynamsoft.DDV.Elements.Button,
+                    className: "ddv-button-back",
+                    style:{
+                      position: "absolute",
+                      left: "0px",  
+                    },
+                    events:{
+                        click: "showEditViewer"
+                    }
+                },
+                Dynamsoft.DDV.Elements.FullQuad,
+                Dynamsoft.DDV.Elements.RotateLeft,
+                Dynamsoft.DDV.Elements.RotateRight,
+                Dynamsoft.DDV.Elements.DeleteCurrent,
+                Dynamsoft.DDV.Elements.DeleteAll,
+                {
+                    type: Dynamsoft.DDV.Elements.Pagination,
+                    className: "ddv-perspective-viewer-pagination-desktop",
+                },
+                {
+                    // Bind event for "PerspectiveAll" button to show the edit viewer
+                    // The event will be registered later.
+                    type: Dynamsoft.DDV.Elements.PerspectiveAll,
+                    className: "ddv-perspective-viewer-perspective-desktop",
+                    events:{
+                        click: "showEditViewer"
+                    }
+                },
+            ],
+        },
+        Dynamsoft.DDV.Elements.MainView,
+    ],
+};
diff --git a/samples/hello-world-continuous-mode/utils.js b/samples/hello-world-continuous-mode/utils.js
new file mode 100644
index 0000000..1c2bd74
--- /dev/null
+++ b/samples/hello-world-continuous-mode/utils.js
@@ -0,0 +1,147 @@
+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())
+}
+
+export function startLoading(text){
+    const loadingBar = document.createElement('div');
+    loadingBar.className = "mwc-loading-bar";
+
+    loadingBar.innerHTML = [
+        `<div class='loader'></div>`,
+        `<span id='mwcLoadingText'>${text}</span>`
+    ].join('')
+
+    document.body.appendChild(loadingBar);
+}
+
+export function updateLoadingText(text){
+    const loadingText = document.getElementById("mwcLoadingText");
+
+    if(loadingText){
+        loadingText.innerHTML = text;
+    }
+}
+
+export function stopLoading(){
+    const loadingBar = document.getElementsByClassName("mwc-loading-bar");
+
+    if(loadingBar.length > 0){
+        loadingBar[0].remove();
+    }
+}
+
+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;
+}
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-angular/.editorconfig b/samples/hello-world-singlepage/hello-world-angular/.editorconfig
new file mode 100644
index 0000000..59d9a3a
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/.editorconfig
@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/samples/hello-world-singlepage/hello-world-angular/.gitignore b/samples/hello-world-singlepage/hello-world-angular/.gitignore
new file mode 100644
index 0000000..1f4031f
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/.gitignore
@@ -0,0 +1,44 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
+
+package-lock.json
diff --git a/samples/hello-world-singlepage/hello-world-angular/.npmrc b/samples/hello-world-singlepage/hello-world-angular/.npmrc
new file mode 100644
index 0000000..3a55404
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/.npmrc
@@ -0,0 +1,2 @@
+@scannerproxy:registry=http://npm.scannerproxy.com/
+@dynamsoft:registry=http://npm.scannerproxy.com/
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-angular/README.md b/samples/hello-world-singlepage/hello-world-angular/README.md
new file mode 100644
index 0000000..2753f94
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/README.md
@@ -0,0 +1,27 @@
+# MyApp
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.4.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
+
+## Code scaffolding
+
+Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
+
+## Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
+
+## Running unit tests
+
+Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Running end-to-end tests
+
+Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
diff --git a/samples/hello-world-singlepage/hello-world-angular/angular.json b/samples/hello-world-singlepage/hello-world-angular/angular.json
new file mode 100644
index 0000000..6f29839
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/angular.json
@@ -0,0 +1,101 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "newProjectRoot": "projects",
+  "projects": {
+    "my-app": {
+      "projectType": "application",
+      "schematics": {},
+      "root": "",
+      "sourceRoot": "src",
+      "prefix": "app",
+      "architect": {
+        "build": {
+          "builder": "@angular-devkit/build-angular:browser",
+          "options": {
+            "outputPath": "dist",
+            "index": "src/index.html",
+            "main": "src/main.ts",
+            "polyfills": [
+              "zone.js"
+            ],
+            "tsConfig": "tsconfig.app.json",
+            "assets": [
+              "src/favicon.ico",
+              "src/assets"
+            ],
+            "styles": [
+              "src/styles.css"
+            ],
+            "scripts": []
+          },
+          "configurations": {
+            "production": {
+              "budgets": [
+                {
+                  "type": "initial",
+                  "maximumWarning": "500kb",
+                  "maximumError": "1mb"
+                },
+                {
+                  "type": "anyComponentStyle",
+                  "maximumWarning": "2kb",
+                  "maximumError": "4kb"
+                }
+              ],
+              "outputHashing": "all"
+            },
+            "development": {
+              "buildOptimizer": false,
+              "optimization": false,
+              "vendorChunk": true,
+              "extractLicenses": false,
+              "sourceMap": true,
+              "namedChunks": true
+            }
+          },
+          "defaultConfiguration": "production"
+        },
+        "serve": {
+          "builder": "@angular-devkit/build-angular:dev-server",
+          "configurations": {
+            "production": {
+              "browserTarget": "my-app:build:production"
+            },
+            "development": {
+              "browserTarget": "my-app:build:development"
+            }
+          },
+          "defaultConfiguration": "development"
+        },
+        "extract-i18n": {
+          "builder": "@angular-devkit/build-angular:extract-i18n",
+          "options": {
+            "browserTarget": "my-app:build"
+          }
+        },
+        "test": {
+          "builder": "@angular-devkit/build-angular:karma",
+          "options": {
+            "polyfills": [
+              "zone.js",
+              "zone.js/testing"
+            ],
+            "tsConfig": "tsconfig.spec.json",
+            "assets": [
+              "src/favicon.ico",
+              "src/assets"
+            ],
+            "styles": [
+              "src/styles.css"
+            ],
+            "scripts": []
+          }
+        }
+      }
+    }
+  },
+  "cli": {
+    "analytics": false
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/package.json b/samples/hello-world-singlepage/hello-world-angular/package.json
new file mode 100644
index 0000000..a8f924c
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/package.json
@@ -0,0 +1,43 @@
+{
+  "name": "ddnjs-angular-sample",
+  "version": "0.0.0",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve",
+    "build": "ng build",
+    "watch": "ng build --watch --configuration development",
+    "test": "ng test"
+  },
+  "private": true,
+  "dependencies": {
+    "@angular/animations": "^15.1.0",
+    "@angular/common": "^15.1.0",
+    "@angular/compiler": "^15.1.0",
+    "@angular/core": "^15.1.0",
+    "@angular/forms": "^15.1.0",
+    "@angular/platform-browser": "^15.1.0",
+    "@angular/platform-browser-dynamic": "^15.1.0",
+    "@angular/router": "^15.1.0",
+    "dynamsoft-camera-enhancer": "4.0.2",
+    "dynamsoft-capture-vision-router": "2.2.10",
+    "dynamsoft-core": "3.2.10",
+    "dynamsoft-document-normalizer": "2.2.10",
+    "dynamsoft-license": "3.2.10",
+    "rxjs": "~7.8.0",
+    "tslib": "^2.3.0",
+    "zone.js": "~0.12.0"
+  },
+  "devDependencies": {
+    "@angular-devkit/build-angular": "^15.1.4",
+    "@angular/cli": "~15.1.4",
+    "@angular/compiler-cli": "^15.1.0",
+    "@types/jasmine": "~4.3.0",
+    "jasmine-core": "~4.5.0",
+    "karma": "~6.4.0",
+    "karma-chrome-launcher": "~3.1.0",
+    "karma-coverage": "~2.2.0",
+    "karma-jasmine": "~5.1.0",
+    "karma-jasmine-html-reporter": "~2.0.0",
+    "typescript": "~4.9.4"
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.css b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.css
new file mode 100644
index 0000000..b34ff43
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.css
@@ -0,0 +1,35 @@
+.title {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 50px;
+}
+.title .title-logo {
+  width: 30px;
+  height: 30px;
+  margin-left: 10px;
+}
+
+.top-btns {
+  width: 100%;
+  margin: 20px auto;
+}
+
+.top-btns button {
+  display: inline-block;
+  border: 1px solid black;
+  padding: 5px 15px;
+  background-color: transparent;
+  cursor: pointer;
+}
+
+.top-btns button:first-child {
+  border-top-left-radius: 10px;
+  border-bottom-left-radius: 10px;
+  border-right: transparent;
+}
+.top-btns button:nth-child(2) {
+  border-top-right-radius: 10px;
+  border-bottom-right-radius: 10px;
+  border-left: transparent;
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.html b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.html
new file mode 100644
index 0000000..3d47ec1
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.html
@@ -0,0 +1,12 @@
+<div class='App'>
+  <div class='title'>
+    <h2 class='title-text'>Hello World for Angular</h2>
+    <img class='title-logo' src="./assets/logo.svg" alt="logo" />
+  </div>
+  <div class='top-btns'>
+    <button (click)="switchMode('video')" [ngStyle]="{'background-color': mode === 'video' ? 'rgb(255,174,55)' : 'white'}">VideoNormalizer</button>
+    <button (click)="switchMode('image')" [ngStyle]="{'background-color': mode === 'image' ? 'rgb(255,174,55)' : 'white'}">ImageNormalizer</button>
+  </div>
+  <app-video-normalizer *ngIf="mode === 'video'"></app-video-normalizer>
+  <app-image-normalizer *ngIf="mode === 'image'"></app-image-normalizer>
+</div>
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.spec.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.spec.ts
new file mode 100644
index 0000000..03e0fd7
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.spec.ts
@@ -0,0 +1,31 @@
+import { TestBed } from '@angular/core/testing';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [
+        AppComponent
+      ],
+    }).compileComponents();
+  });
+
+  it('should create the app', () => {
+    const fixture = TestBed.createComponent(AppComponent);
+    const app = fixture.componentInstance;
+    expect(app).toBeTruthy();
+  });
+
+  it(`should have as title 'my-app'`, () => {
+    const fixture = TestBed.createComponent(AppComponent);
+    const app = fixture.componentInstance;
+    expect(app.title).toEqual('my-app');
+  });
+
+  it('should render title', () => {
+    const fixture = TestBed.createComponent(AppComponent);
+    fixture.detectChanges();
+    const compiled = fixture.nativeElement as HTMLElement;
+    expect(compiled.querySelector('.content span')?.textContent).toContain('my-app app is running!');
+  });
+});
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.ts
new file mode 100644
index 0000000..2c1e124
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/app.component.ts
@@ -0,0 +1,16 @@
+import { Component } from '@angular/core';
+
+@Component({
+  selector: 'app-root',
+  templateUrl: './app.component.html',
+  styleUrls: ['./app.component.css']
+})
+export class AppComponent {
+  title = 'my-app';
+
+  mode: string = "video";
+
+  switchMode(value: string) {
+    this.mode = value;
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/app.module.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/app.module.ts
new file mode 100644
index 0000000..ef928f9
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/app.module.ts
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+
+import { AppComponent } from './app.component';
+import { ImageNormalizerComponent } from './image-normalizer/image-normalizer.component';
+import { VideoNormalizerComponent } from './video-normalizer/video-normalizer.component';
+
+@NgModule({
+  declarations: [
+    AppComponent,
+    ImageNormalizerComponent,
+    VideoNormalizerComponent
+  ],
+  imports: [
+    BrowserModule
+  ],
+  providers: [],
+  bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.css b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.css
new file mode 100644
index 0000000..8a82f71
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.css
@@ -0,0 +1,14 @@
+.recognize-img {
+  width: 100%;
+  height: 100%;
+  font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
+}
+
+.recognize-img .img-ipt {
+  width: 80%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  border: 1px solid black;
+  margin: 0 auto;
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.html b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.html
new file mode 100644
index 0000000..db504de
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.html
@@ -0,0 +1,4 @@
+<div class="recognize-img">
+  <div class="img-ipt"><input type="file" (change)="captureImg($event)"/></div>
+  <div class="result-area" #elInr></div>
+</div>
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.spec.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.spec.ts
new file mode 100644
index 0000000..2fdf4b4
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ImageNormalizerComponent } from './image-normalizer.component';
+
+describe('ImageNormalizerComponent', () => {
+  let component: ImageNormalizerComponent;
+  let fixture: ComponentFixture<ImageNormalizerComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ ImageNormalizerComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(ImageNormalizerComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.ts
new file mode 100644
index 0000000..9ae67f7
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/image-normalizer/image-normalizer.component.ts
@@ -0,0 +1,43 @@
+import { Component, ViewChild } from '@angular/core';
+import { NormalizedImageResultItem } from "dynamsoft-document-normalizer";
+import { CaptureVisionRouter } from "dynamsoft-capture-vision-router";
+
+@Component({
+  selector: 'app-image-normalizer',
+  templateUrl: './image-normalizer.component.html',
+  styleUrls: ['./image-normalizer.component.css']
+})
+export class ImageNormalizerComponent {
+  router: Promise<CaptureVisionRouter> | null = null;
+
+  @ViewChild('elInr') elInr: any;
+
+  ngOnInit(): void {
+    this.router = CaptureVisionRouter.createInstance();
+  }
+
+  captureImg = async (e: any) => {
+    try {
+      this.elInr.nativeElement!.innerHTML = "";
+      const normalizer = await this.router;
+      const results = await normalizer!.capture(e.target.files[0], "DetectAndNormalizeDocument_Default");
+      if (results.items.length) {
+        const cvs = (results.items[0] as NormalizedImageResultItem).toCanvas();
+        if (document.body.clientWidth < 600) {
+          cvs.style.width = "90%";
+        }
+        this.elInr.nativeElement!.append(cvs);
+      }
+      console.log(results);
+    } catch (ex: any) {
+      let errMsg = ex.message || ex;
+      console.error(errMsg);
+      alert(errMsg);
+    }
+  }
+
+  async ngOnDestroy() {
+    (await this.router)?.dispose();
+    console.log('ImageNormalizer Component Unmount');
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.css b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.css
new file mode 100644
index 0000000..25d074d
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.css
@@ -0,0 +1,3 @@
+#div-video-btns {
+  width: 75%;margin: 0 auto;margin-bottom: 10px; display: flex;justify-content: space-around;
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.html b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.html
new file mode 100644
index 0000000..fc44be3
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.html
@@ -0,0 +1,8 @@
+<div id="div-loading" [style.display]="bShowLoading ? 'block' : 'none'">Loading...</div>
+<div id="div-video-btns">
+  <button id="confirm-quad-for-normalization" (click)="confirmTheBoundary()" [disabled]="bDisabledBtnEdit">Confirm the Boundary</button>
+  <button id="normalize-with-confirmed-quad" (click)="normalze()" [disabled]="bDisabledBtnNor">Normalize</button>
+</div>
+<div id="div-ui-container" [style.display]="bShowUiContainer ? 'block' : 'none'" style="margin-top: 10px; height: 500px;" #cameraViewContainerRef></div>
+<div id="div-image-container" [style.display]="bShowImageContainer ? 'block' : 'none'" style="width: 100vw; height: 70vh;" #imageEditorViewContainerRef></div>
+<div id="normalized-result" #normalizedImageContainerRef></div>
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.spec.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.spec.ts
new file mode 100644
index 0000000..92fa96c
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { VideoNormalizerComponent } from './video-normalizer.component';
+
+describe('VideoNormalizerComponent', () => {
+  let component: VideoNormalizerComponent;
+  let fixture: ComponentFixture<VideoNormalizerComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ VideoNormalizerComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(VideoNormalizerComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.ts b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.ts
new file mode 100644
index 0000000..457794e
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/app/video-normalizer/video-normalizer.component.ts
@@ -0,0 +1,156 @@
+import { Component, ViewChild } from '@angular/core';
+import { EnumCapturedResultItemType, type DSImageData } from "dynamsoft-core";
+import { type NormalizedImageResultItem } from "dynamsoft-document-normalizer";
+import { CameraEnhancer, CameraView, QuadDrawingItem, ImageEditorView } from "dynamsoft-camera-enhancer";
+import { CapturedResultReceiver, CaptureVisionRouter, type SimplifiedCaptureVisionSettings } from "dynamsoft-capture-vision-router";
+import { CapturedResultItem, OriginalImageResultItem, Point } from 'dynamsoft-core';
+
+@Component({
+  selector: 'app-video-normalizer',
+  templateUrl: './video-normalizer.component.html',
+  styleUrls: ['./video-normalizer.component.css']
+})
+export class VideoNormalizerComponent {
+  cameraEnhancer: Promise<CameraEnhancer> | null = null;
+  router: Promise<CaptureVisionRouter> | null = null;
+  bShowUiContainer = true;
+  bShowImageContainer = false;
+  bDisabledBtnEdit = false;
+  bDisabledBtnNor = true;
+  bShowLoading = true;
+
+  items: Array<any> = [];
+  quads: Array<any> = [];
+  image: DSImageData | null = null;
+  confirmTheBoundary: () => void = () => { };
+  normalze: () => void = () => { };
+
+  @ViewChild('imageEditorViewContainerRef') imageEditorViewContainerRef: any;
+  @ViewChild('normalizedImageContainerRef') normalizedImageContainerRef: any;
+  @ViewChild('cameraViewContainerRef') cameraViewContainerRef: any;
+
+  async ngOnInit(): Promise<void> {
+    try {
+      const view = await CameraView.createInstance();
+      const dce = await (this.cameraEnhancer = CameraEnhancer.createInstance(view));
+      const imageEditorView = await ImageEditorView.createInstance();
+      /* Creates an image editing layer for drawing found document boundaries. */
+      const layer = imageEditorView.createDrawingLayer();
+
+      /**
+      * Creates a CaptureVisionRouter instance and configure the task to detect document boundaries.
+      * Also, make sure the original image is returned after it has been processed.
+      */
+      const normalizer = await (this.router = CaptureVisionRouter.createInstance());
+      normalizer.setInput(dce as any);
+      /**
+      * Sets the result types to be returned.
+      * Because we need to normalize the original image later, here we set the return result type to
+      * include both the quadrilateral and original image data.
+      */
+      let newSettings = await normalizer.getSimplifiedSettings("DetectDocumentBoundaries_Default");
+      newSettings.capturedResultItemTypes |= EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE;
+      await normalizer.updateSettings("DetectDocumentBoundaries_Default", newSettings);
+      this.cameraViewContainerRef.nativeElement!.append(view.getUIElement());
+      this.imageEditorViewContainerRef.nativeElement!.append(imageEditorView.getUIElement());
+
+      /* Defines the result receiver for the task.*/
+      const resultReceiver = new CapturedResultReceiver();
+      resultReceiver.onCapturedResultReceived = (result) => {
+        const originalImage = result.items.filter((item: CapturedResultItem) => { return item.type === EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE });
+        if (originalImage.length) {
+          this.image = (originalImage[0] as OriginalImageResultItem).imageData;
+        }
+        this.items = result.items.filter((item: CapturedResultItem) => { return item.type === EnumCapturedResultItemType.CRIT_DETECTED_QUAD });
+      }
+      normalizer.addResultReceiver(resultReceiver);
+
+      this.confirmTheBoundary = () => {
+        if (!dce.isOpen() || !this.items.length) return;
+        /* Hides the cameraView and shows the imageEditorView. */
+        this.bShowUiContainer = false
+        this.bShowImageContainer = true;
+        /* Draws the image on the imageEditorView first. */
+        imageEditorView.setOriginalImage(this.image!);
+        this.quads = [];
+        /* Draws the document boundary (quad) over the image. */
+        for (let i = 0; i < this.items.length; i++) {
+          if (this.items[i].type === EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE) continue;
+          const points = this.items[i].location.points;
+          const quad = new QuadDrawingItem({ points });
+          this.quads.push(quad);
+          layer.addDrawingItems(this.quads);
+        }
+        this.bDisabledBtnNor = false;
+        this.bDisabledBtnEdit = true;
+        normalizer.stopCapturing();
+      }
+
+      this.normalze = async () => {
+        /* Get the selected quadrilateral */
+        let seletedItems = imageEditorView.getSelectedDrawingItems();
+        let quad;
+        if (seletedItems.length) {
+          quad = (seletedItems[0] as QuadDrawingItem).getQuad();
+        } else {
+          quad = this.items[0].location;
+        }
+        const isPointOverBoundary = (point: Point) => {
+          if (point.x < 0 ||
+            point.x > this.image!.width ||
+            point.y < 0 ||
+            point.y > this.image!.height) {
+            return true;
+          } else {
+            return false;
+          }
+        };
+        /* Check if the points beyond the boundaries of the image. */
+        if (quad.points.some((point: Point) => isPointOverBoundary(point))) {
+          alert("The document boundaries extend beyond the boundaries of the image and cannot be used to normalize the document.");
+          return;
+        }
+
+        /* Hides the imageEditorView. */
+        this.bShowImageContainer = false;
+        /* Removes the old normalized image if any. */
+        this.normalizedImageContainerRef.nativeElement!.innerHTML = "";
+        /**
+         * Sets the coordinates of the ROI (region of interest)
+         * in the built-in template "normalize-document".
+         */
+        let newSettings = await normalizer.getSimplifiedSettings("normalize-document") as SimplifiedCaptureVisionSettings;
+        newSettings.roiMeasuredInPercentage = false;
+        newSettings.roi.points = quad.points;
+        await normalizer.updateSettings("normalize-document", newSettings);
+        /* Executes the normalization and shows the result on the page */
+        let norRes = await normalizer.capture(this.image!, "normalize-document");
+        if (norRes.items[0]) {
+          this.normalizedImageContainerRef.nativeElement!.append((norRes.items[0] as NormalizedImageResultItem).toCanvas());
+        }
+        layer.clearDrawingItems();
+        this.bDisabledBtnNor = true;
+        this.bDisabledBtnEdit = false;
+        /* show video view */
+        this.bShowUiContainer = true
+        view.getUIElement().style.display = "";
+        await normalizer.startCapturing("DetectDocumentBoundaries_Default");
+      }
+
+      await dce.open();
+      /* Uses the built-in template "DetectDocumentBoundaries_Default" to start a continuous boundary detection task. */
+      await normalizer.startCapturing("DetectDocumentBoundaries_Default");
+      this.bShowLoading = false;
+    } catch (ex: any) {
+      let errMsg = ex.message || ex;
+      console.error(errMsg);
+      alert(errMsg);
+    }
+  }
+
+  async ngOnDestroy() {
+    (await this.router)?.dispose();
+    (await this.cameraEnhancer)?.dispose();
+    console.log('VideoNormalizer Component Unmount');
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/assets/.gitkeep b/samples/hello-world-singlepage/hello-world-angular/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/assets/logo.svg b/samples/hello-world-singlepage/hello-world-angular/src/assets/logo.svg
new file mode 100644
index 0000000..7f7d73a
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/assets/logo.svg
@@ -0,0 +1 @@
+<svg t="1675666044496" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2961" width="200" height="200"><path d="M435.8 536.2H512V353z" fill="#DD0031" p-id="2962"></path><path d="M400.9 616.8l-52.4 130.8h-97.2L512 163V64L94.9 212.7l63.6 551.5L512 960V616.8z" fill="#DD0031" p-id="2963"></path><path d="M512 353v183.2h76.2z" fill="#C3002F" p-id="2964"></path><path d="M512 64v99l259.8 584.6h-97.2l-52.4-130.8H512V960l353.5-195.8 63.6-551.5z" fill="#C3002F" p-id="2965"></path></svg>
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/cvr.ts b/samples/hello-world-singlepage/hello-world-angular/src/cvr.ts
new file mode 100644
index 0000000..b372ff8
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/cvr.ts
@@ -0,0 +1,32 @@
+import "dynamsoft-license";
+import "dynamsoft-document-normalizer";
+import "dynamsoft-capture-vision-router";
+
+import { CoreModule } from "dynamsoft-core";
+import { LicenseManager } from "dynamsoft-license";
+
+/** LICENSE ALERT - README
+ * To use the library, you need to first call the method initLicense() to initialize the license using a license key string.
+ */
+LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9");
+/**
+ * The license "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9" is a temporary license for testing good for 24 hours.
+ * You can visit https://www.dynamsoft.com/customer/license/trialLicense?utm_source=github&architecture=dcv&product=ddn&package=js to get your own trial license good for 30 days.
+ * LICENSE ALERT - THE END
+ */
+
+CoreModule.engineResourcePaths = {
+  std: "https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-std@1.2.0/dist/",
+  dip: "https://cdn.jsdelivr.net/npm/dynamsoft-image-processing@2.2.10/dist/",
+  core: "https://cdn.jsdelivr.net/npm/dynamsoft-core@3.2.10/dist/",
+  license: "https://cdn.jsdelivr.net/npm/dynamsoft-license@3.2.10/dist/",
+  cvr: "https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.2.10/dist/",
+  ddn: "https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.2.10/dist/",
+  dce: "https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@4.0.2/dist/"
+};
+
+CoreModule.loadWasm(["DDN"]).catch((ex: any) => {
+  let errMsg = ex.message || ex;
+  console.error(errMsg);
+  alert(errMsg);
+});
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/favicon.ico b/samples/hello-world-singlepage/hello-world-angular/src/favicon.ico
new file mode 100644
index 0000000..997406a
Binary files /dev/null and b/samples/hello-world-singlepage/hello-world-angular/src/favicon.ico differ
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/index.html b/samples/hello-world-singlepage/hello-world-angular/src/index.html
new file mode 100644
index 0000000..899ba28
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>read-video-angular</title>
+  <base href="./">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+</head>
+<body>
+  <app-root></app-root>
+</body>
+</html>
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/main.ts b/samples/hello-world-singlepage/hello-world-angular/src/main.ts
new file mode 100644
index 0000000..d6f0547
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/main.ts
@@ -0,0 +1,9 @@
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+import "./cvr";
+
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+  .catch(err => console.error(err));
diff --git a/samples/hello-world-singlepage/hello-world-angular/src/styles.css b/samples/hello-world-singlepage/hello-world-angular/src/styles.css
new file mode 100644
index 0000000..d929588
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/src/styles.css
@@ -0,0 +1,7 @@
+/* You can add global styles to this file, and also import other style files */
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  text-align: center;
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/tsconfig.app.json b/samples/hello-world-singlepage/hello-world-angular/tsconfig.app.json
new file mode 100644
index 0000000..374cc9d
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/tsconfig.app.json
@@ -0,0 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./out-tsc/app",
+    "types": []
+  },
+  "files": [
+    "src/main.ts"
+  ],
+  "include": [
+    "src/**/*.d.ts"
+  ]
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/tsconfig.json b/samples/hello-world-singlepage/hello-world-angular/tsconfig.json
new file mode 100644
index 0000000..ed966d4
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/tsconfig.json
@@ -0,0 +1,33 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+  "compileOnSave": false,
+  "compilerOptions": {
+    "baseUrl": "./",
+    "outDir": "./dist/out-tsc",
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "sourceMap": true,
+    "declaration": false,
+    "downlevelIteration": true,
+    "experimentalDecorators": true,
+    "moduleResolution": "node",
+    "importHelpers": true,
+    "target": "ES2022",
+    "module": "ES2022",
+    "useDefineForClassFields": false,
+    "lib": [
+      "ES2022",
+      "dom"
+    ]
+  },
+  "angularCompilerOptions": {
+    "enableI18nLegacyMessageIdFormat": false,
+    "strictInjectionParameters": true,
+    "strictInputAccessModifiers": true,
+    "strictTemplates": true
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-angular/tsconfig.spec.json b/samples/hello-world-singlepage/hello-world-angular/tsconfig.spec.json
new file mode 100644
index 0000000..be7e9da
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-angular/tsconfig.spec.json
@@ -0,0 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./out-tsc/spec",
+    "types": [
+      "jasmine"
+    ]
+  },
+  "include": [
+    "src/**/*.spec.ts",
+    "src/**/*.d.ts"
+  ]
+}
diff --git a/samples/hello-world-singlepage/hello-world-react/.gitignore b/samples/hello-world-singlepage/hello-world-react/.gitignore
new file mode 100644
index 0000000..d3ff5fc
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/.gitignore
@@ -0,0 +1,25 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+package-lock.json
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/.npmrc b/samples/hello-world-singlepage/hello-world-react/.npmrc
new file mode 100644
index 0000000..3a55404
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/.npmrc
@@ -0,0 +1,2 @@
+@scannerproxy:registry=http://npm.scannerproxy.com/
+@dynamsoft:registry=http://npm.scannerproxy.com/
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/README.md b/samples/hello-world-singlepage/hello-world-react/README.md
new file mode 100644
index 0000000..75432dc
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/README.md
@@ -0,0 +1,46 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+
+If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+
+You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
diff --git a/samples/hello-world-singlepage/hello-world-react/package.json b/samples/hello-world-singlepage/hello-world-react/package.json
new file mode 100644
index 0000000..0097fd0
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/package.json
@@ -0,0 +1,49 @@
+{
+  "name": "ddnjs-react-sample",
+  "version": "0.0.0",
+  "private": true,
+  "homepage": "./",
+  "dependencies": {
+    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/user-event": "^13.5.0",
+    "@types/jest": "^27.5.2",
+    "@types/node": "^16.18.12",
+    "@types/react": "^18.0.27",
+    "@types/react-dom": "^18.0.10",
+    "dynamsoft-camera-enhancer": "4.0.2",
+    "dynamsoft-capture-vision-router": "2.2.10",
+    "dynamsoft-core": "3.2.10",
+    "dynamsoft-document-normalizer": "2.2.10",
+    "dynamsoft-license": "3.2.10",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-scripts": "5.0.1",
+    "typescript": "^4.9.5",
+    "web-vitals": "^2.1.4"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-react/public/favicon.ico b/samples/hello-world-singlepage/hello-world-react/public/favicon.ico
new file mode 100644
index 0000000..a11777c
Binary files /dev/null and b/samples/hello-world-singlepage/hello-world-react/public/favicon.ico differ
diff --git a/samples/hello-world-singlepage/hello-world-react/public/index.html b/samples/hello-world-singlepage/hello-world-react/public/index.html
new file mode 100644
index 0000000..c9a80cd
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/public/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>read-video-react</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>
diff --git a/samples/hello-world-singlepage/hello-world-react/src/App.css b/samples/hello-world-singlepage/hello-world-react/src/App.css
new file mode 100644
index 0000000..3b605c0
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/App.css
@@ -0,0 +1,35 @@
+.title {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 50px;
+}
+.title .title-logo {
+  width: 30px;
+  height: 30px;
+  margin-left: 10px;
+}
+
+.top-btns {
+  width: 100%;
+  margin: 20px auto;
+}
+
+.top-btns button {
+  display: inline-block;
+  border: 1px solid black;
+  padding: 5px 15px;
+  background-color: transparent;
+  cursor: pointer;
+}
+
+.top-btns button:first-child {
+  border-top-left-radius: 10px;
+  border-bottom-left-radius: 10px;
+  border-right: transparent;
+}
+.top-btns button:nth-child(2) {
+  border-top-right-radius: 10px;
+  border-bottom-right-radius: 10px;
+  border-left: transparent;
+}
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/App.tsx b/samples/hello-world-singlepage/hello-world-react/src/App.tsx
new file mode 100644
index 0000000..eb2d881
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/App.tsx
@@ -0,0 +1,24 @@
+import { useState } from 'react';
+import './App.css';
+import reactLogo from './assets/logo.svg';
+import VideoNormalizer from './components/VideoNormalizer/VideoNormalizer';
+import ImageNormalizer from './components/ImageNormalizer/ImageNormazlier';
+
+function App() {
+  const [mode, setMode] = useState("video");
+  return (
+    <div className='App'>
+      <div className='title'>
+        <h2 className='title-text'>Hello World for React</h2>
+        <img className='title-logo' src={reactLogo} alt="logo"></img>
+      </div>
+      <div className='top-btns'>
+        <button onClick={() => { setMode("video") }} style={{ backgroundColor: mode === "video" ? "rgb(255, 174, 55)" : "#fff" }}>VideoNormalizer</button>
+        <button onClick={() => { setMode("image") }} style={{ backgroundColor: mode === "image" ? "rgb(255, 174, 55)" : "#fff" }}>ImageNormalizer</button>
+      </div>
+      {mode === "video" ? <VideoNormalizer /> : <ImageNormalizer />}
+    </div>
+  );
+}
+
+export default App;
diff --git a/samples/hello-world-singlepage/hello-world-react/src/assets/logo.svg b/samples/hello-world-singlepage/hello-world-react/src/assets/logo.svg
new file mode 100644
index 0000000..9dfc1c0
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/assets/logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/components/ImageNormalizer/ImageNormalizer.css b/samples/hello-world-singlepage/hello-world-react/src/components/ImageNormalizer/ImageNormalizer.css
new file mode 100644
index 0000000..e5cd90a
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/components/ImageNormalizer/ImageNormalizer.css
@@ -0,0 +1,14 @@
+.recognize-img {
+  width: 100%;
+  height: 100%;
+  font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
+}
+
+.recognize-img .img-ipt {
+  width: 80%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  border: 1px solid black;
+  margin: 0 auto;
+}
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/components/ImageNormalizer/ImageNormazlier.tsx b/samples/hello-world-singlepage/hello-world-react/src/components/ImageNormalizer/ImageNormazlier.tsx
new file mode 100644
index 0000000..fade31e
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/components/ImageNormalizer/ImageNormazlier.tsx
@@ -0,0 +1,48 @@
+import { useEffect, useRef, MutableRefObject, ChangeEvent } from "react";
+import { CaptureVisionRouter } from "dynamsoft-capture-vision-router";
+import { NormalizedImageResultItem } from "dynamsoft-document-normalizer";
+import "./ImageNormalizer.css";
+
+function ImageNormalizer() {
+    const iptRef: MutableRefObject<HTMLInputElement | null> = useRef(null);
+    const elInr: MutableRefObject<HTMLDivElement | null> = useRef(null);
+    const router: MutableRefObject<Promise<CaptureVisionRouter> | null> = useRef(null);
+
+    useEffect((): any => {
+        router.current = CaptureVisionRouter.createInstance();
+
+        return async () => {
+            (await router.current)?.dispose();
+            console.log('ImageNormalizer Component Unmount');
+        }
+    }, []);
+
+    const captureImg = async (e: ChangeEvent<HTMLInputElement>) => {
+        try {
+            elInr.current!.innerHTML = "";
+            const normalizer = await router.current;
+            const results = await normalizer!.capture(e.target.files![0], "DetectAndNormalizeDocument_Default");
+            if (results.items.length) {
+                const cvs = (results.items[0] as NormalizedImageResultItem).toCanvas();
+                if (document.body.clientWidth < 600) {
+                    cvs.style.width = "90%";
+                }
+                elInr.current!.append(cvs);
+            }
+            console.log(results);
+        } catch (ex: any) {
+            let errMsg = ex.message || ex;
+            console.error(errMsg);
+            alert(errMsg);
+        }
+    }
+
+    return (
+        <div className="recognize-img">
+            <div className="img-ipt"><input type="file" ref={iptRef} onChange={captureImg} /></div>
+            <div className="result-area" ref={elInr}></div>
+        </div>
+    )
+}
+
+export default ImageNormalizer;
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/components/VideoNormalizer/VideoNormalizer.css b/samples/hello-world-singlepage/hello-world-react/src/components/VideoNormalizer/VideoNormalizer.css
new file mode 100644
index 0000000..1198472
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/components/VideoNormalizer/VideoNormalizer.css
@@ -0,0 +1,3 @@
+#div-video-btns {
+    width: 75%;margin: 0 auto;margin-bottom: 10px; display: flex;justify-content: space-around;
+}
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/components/VideoNormalizer/VideoNormalizer.tsx b/samples/hello-world-singlepage/hello-world-react/src/components/VideoNormalizer/VideoNormalizer.tsx
new file mode 100644
index 0000000..70db344
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/components/VideoNormalizer/VideoNormalizer.tsx
@@ -0,0 +1,172 @@
+import { useEffect, useRef, MutableRefObject, useState } from 'react';
+import "./VideoNormalizer.css";
+import { EnumCapturedResultItemType, DSImageData, OriginalImageResultItem, CapturedResultItem, Point } from "dynamsoft-core";
+import { CameraEnhancer, CameraView, QuadDrawingItem, ImageEditorView } from "dynamsoft-camera-enhancer";
+import { CapturedResultReceiver, CaptureVisionRouter, type SimplifiedCaptureVisionSettings } from "dynamsoft-capture-vision-router";
+import { NormalizedImageResultItem } from "dynamsoft-document-normalizer";
+
+function VideoNormalizer() {
+    let imageEditorViewContainerRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
+    let cameraViewContainerRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
+    let normalizedImageContainer: MutableRefObject<HTMLDivElement | null> = useRef(null);
+    let cameraEnhancer: MutableRefObject<Promise<CameraEnhancer> | null> = useRef(null);
+    let router: MutableRefObject<Promise<CaptureVisionRouter> | null> = useRef(null);
+    let [bShowUiContainer, setShowUiContainer] = useState(true);
+    let [bShowImageContainer, setShowImageContainer] = useState(false);
+    let [bDisabledBtnEdit, setDisabledBtnEdit] = useState(false);
+    let [bDisabledBtnNor, setDisabledBtnNor] = useState(true);
+    let [bShowLoading, setShowLoading] = useState(true);
+
+    let normalizer: MutableRefObject<CaptureVisionRouter | null> = useRef(null);
+    let dce: MutableRefObject<CameraEnhancer | null> = useRef(null);
+    let imageEditorView: MutableRefObject<ImageEditorView | null> = useRef(null);
+    let layer: MutableRefObject<any> = useRef(null);
+    let view: MutableRefObject<CameraView | null> = useRef(null);
+    let items: MutableRefObject<Array<any>> = useRef([]);
+    let image: MutableRefObject<DSImageData | null> = useRef(null);
+    let quads: Array<any> = [];
+
+    useEffect((): any => {
+        const init = async () => {
+            try {
+                view.current = await CameraView.createInstance();
+                dce.current = await (cameraEnhancer.current = CameraEnhancer.createInstance(view.current));
+                imageEditorView.current = await ImageEditorView.createInstance(imageEditorViewContainerRef.current as HTMLDivElement);
+                /* Creates an image editing layer for drawing found document boundaries. */
+                layer.current = imageEditorView.current.createDrawingLayer();
+
+                /**
+                 * Creates a CaptureVisionRouter instance and configure the task to detect document boundaries.
+                 * Also, make sure the original image is returned after it has been processed.
+                 */
+                normalizer.current = await (router.current = CaptureVisionRouter.createInstance());
+                normalizer.current.setInput(dce.current);
+                /**
+                 * Sets the result types to be returned.
+                 * Because we need to normalize the original image later, here we set the return result type to
+                 * include both the quadrilateral and original image data.
+                 */
+                let newSettings = await normalizer.current.getSimplifiedSettings("DetectDocumentBoundaries_Default");
+                newSettings.capturedResultItemTypes |= EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE;
+                await normalizer.current.updateSettings("DetectDocumentBoundaries_Default", newSettings);
+                cameraViewContainerRef.current!.append(view.current.getUIElement());
+
+                /* Defines the result receiver for the task.*/
+                const resultReceiver = new CapturedResultReceiver();
+                resultReceiver.onCapturedResultReceived = (result) => {
+                    const originalImage = result.items.filter((item: CapturedResultItem) => { return item.type === EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE });
+                    if (originalImage.length) {
+                        image.current = (originalImage[0] as OriginalImageResultItem).imageData;
+                    }
+                    items.current = result.items.filter((item: CapturedResultItem) => { return item.type === EnumCapturedResultItemType.CRIT_DETECTED_QUAD });
+                }
+                normalizer.current.addResultReceiver(resultReceiver);
+
+                await dce.current.open();
+                /* Uses the built-in template "DetectDocumentBoundaries_Default" to start a continuous boundary detection task. */
+                await normalizer.current.startCapturing("DetectDocumentBoundaries_Default");
+                setShowLoading(false);
+            } catch (ex: any) {
+                let errMsg = ex.message || ex;
+                console.error(errMsg);
+                alert(errMsg);
+            }
+        }
+        init();
+
+        return async () => {
+            (await router.current)?.dispose();
+            (await cameraEnhancer.current)?.dispose();
+            console.log('VideoNormalizer Component Unmount');
+        }
+    }, []);
+
+    const confirmTheBoundary = () => {
+        if (!dce.current!.isOpen() || !items.current.length) return;
+        /* Hides the cameraView and shows the imageEditorView. */
+        setShowUiContainer(false);
+        setShowImageContainer(true);
+        /* Draws the image on the imageEditorView first. */
+        imageEditorView.current!.setOriginalImage(image.current!);
+        quads = [];
+        /* Draws the document boundary (quad) over the image. */
+        for (let i = 0; i < items.current.length; i++) {
+            if (items.current[i].type === EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE) continue;
+            const points = items.current[i].location.points;
+            const quad = new QuadDrawingItem({ points });
+            quads.push(quad);
+            layer.current!.addDrawingItems(quads);
+        }
+        setDisabledBtnEdit(true);
+        setDisabledBtnNor(false);
+        normalizer.current!.stopCapturing();
+    }
+
+    const normalize = async () => {
+        /* Get the selected quadrilateral */
+        let seletedItems = imageEditorView.current!.getSelectedDrawingItems();
+        let quad;
+        if (seletedItems.length) {
+            quad = (seletedItems[0] as QuadDrawingItem).getQuad();
+        } else {
+            quad = items.current[0].location;
+        }
+        const isPointOverBoundary = (point: Point) => {
+            if(point.x < 0 || 
+            point.x > image.current!.width || 
+            point.y < 0 ||
+            point.y > image.current!.height) {
+                return true;
+            } else {
+                return false;
+            }
+        };
+        /* Check if the points beyond the boundaries of the image. */
+        if (quad.points.some((point: Point) => isPointOverBoundary(point))) {
+            alert("The document boundaries extend beyond the boundaries of the image and cannot be used to normalize the document.");
+            return;
+        }
+
+        /* Hides the imageEditorView. */
+        setShowImageContainer(false);
+        /* Removes the old normalized image if any. */
+        normalizedImageContainer.current!.innerHTML = "";
+        /**
+         * Sets the coordinates of the ROI (region of interest)
+         * in the built-in template "normalize-document".
+         */
+        let newSettings = await normalizer.current!.getSimplifiedSettings("normalize-document") as SimplifiedCaptureVisionSettings;
+        newSettings.roiMeasuredInPercentage = false;
+        newSettings.roi.points = quad.points;
+        await normalizer.current!.updateSettings("normalize-document", newSettings);
+        /* Executes the normalization and shows the result on the page */
+        let norRes = await normalizer.current!.capture(image.current!, "normalize-document");
+        if (norRes.items[0]) {
+            normalizedImageContainer.current!.append((norRes.items[0] as NormalizedImageResultItem).toCanvas());
+        }
+        layer.current!.clearDrawingItems();
+        setDisabledBtnEdit(false);
+        setDisabledBtnNor(true);
+        /* show video view */
+        setShowUiContainer(true);
+        view.current!.getUIElement().style.display = "";
+        await normalizer.current!.startCapturing("DetectDocumentBoundaries_Default");
+    }
+
+    return (
+        <div className="container">
+            <div id="div-loading" style={{ display: bShowLoading ? "block" : "none" }}>Loading...</div>
+            <div id="div-video-btns">
+                <button id="confirm-quad-for-normalization" onClick={confirmTheBoundary} disabled={bDisabledBtnEdit}>Confirm the Boundary</button>
+                <button id="normalize-with-confirmed-quad" onClick={normalize} disabled={bDisabledBtnNor}>Normalize</button>
+            </div >
+            <div id="div-ui-container" style={{ display: bShowUiContainer ? "block" : "none", marginTop: "10px", height: "500px" }} ref={cameraViewContainerRef}></div>
+            <div id="div-image-container" style={{ display: bShowImageContainer ? "block" : "none", width: "100vw", height: "70vh" }} ref={imageEditorViewContainerRef}>
+                <div className="dce-image-container" style={{ width: "100%", height: "100%" }}></div>
+            </div>
+            <div id="normalized-result" ref={normalizedImageContainer}></div>
+        </div >
+    );
+}
+
+export default VideoNormalizer;
diff --git a/samples/hello-world-singlepage/hello-world-react/src/cvr.ts b/samples/hello-world-singlepage/hello-world-react/src/cvr.ts
new file mode 100644
index 0000000..5d3df17
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/cvr.ts
@@ -0,0 +1,32 @@
+import "dynamsoft-license";
+import "dynamsoft-document-normalizer";
+import "dynamsoft-capture-vision-router";
+
+import { CoreModule } from "dynamsoft-core";
+import { LicenseManager } from "dynamsoft-license";
+
+/** LICENSE ALERT - README 
+ * To use the library, you need to first call the method initLicense() to initialize the license using a license key string.
+ */
+LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9");
+/**
+ * The license "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9" is a temporary license for testing good for 24 hours.
+ * You can visit https://www.dynamsoft.com/customer/license/trialLicense?utm_source=github&architecture=dcv&product=ddn&package=js to get your own trial license good for 30 days.
+ * LICENSE ALERT - THE END
+ */
+
+CoreModule.engineResourcePaths = {
+  std: "https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-std@1.2.0/dist/",
+  dip: "https://cdn.jsdelivr.net/npm/dynamsoft-image-processing@2.2.10/dist/",
+  core: "https://cdn.jsdelivr.net/npm/dynamsoft-core@3.2.10/dist/",
+  license: "https://cdn.jsdelivr.net/npm/dynamsoft-license@3.2.10/dist/",
+  cvr: "https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.2.10/dist/",
+  ddn: "https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.2.10/dist/",
+  dce: "https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@4.0.2/dist/"
+};
+
+CoreModule.loadWasm(["DDN"]).catch((ex: any) => {
+  let errMsg = ex.message || ex;
+  console.error(errMsg);
+  alert(errMsg);
+});
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/index.css b/samples/hello-world-singlepage/hello-world-react/src/index.css
new file mode 100644
index 0000000..5cf7344
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/index.css
@@ -0,0 +1,10 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html, body {
+  text-align: center;
+  min-width: 350px;
+}
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-react/src/index.tsx b/samples/hello-world-singlepage/hello-world-react/src/index.tsx
new file mode 100644
index 0000000..e81baa1
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/index.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+import "./cvr";
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+root.render(<App />);
diff --git a/samples/hello-world-singlepage/hello-world-react/src/react-app-env.d.ts b/samples/hello-world-singlepage/hello-world-react/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/samples/hello-world-singlepage/hello-world-react/tsconfig.json b/samples/hello-world-singlepage/hello-world-react/tsconfig.json
new file mode 100644
index 0000000..a273b0c
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-react/tsconfig.json
@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx"
+  },
+  "include": [
+    "src"
+  ]
+}
diff --git a/samples/hello-world-singlepage/hello-world-vue3/.gitignore b/samples/hello-world-singlepage/hello-world-vue3/.gitignore
new file mode 100644
index 0000000..e1f03e1
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+package-lock.json
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-vue3/.npmrc b/samples/hello-world-singlepage/hello-world-vue3/.npmrc
new file mode 100644
index 0000000..3a55404
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/.npmrc
@@ -0,0 +1,2 @@
+@scannerproxy:registry=http://npm.scannerproxy.com/
+@dynamsoft:registry=http://npm.scannerproxy.com/
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-vue3/README.md b/samples/hello-world-singlepage/hello-world-vue3/README.md
new file mode 100644
index 0000000..5bc8e46
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/README.md
@@ -0,0 +1,40 @@
+# vue3-ts
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
diff --git a/samples/hello-world-singlepage/hello-world-vue3/env.d.ts b/samples/hello-world-singlepage/hello-world-vue3/env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />
diff --git a/samples/hello-world-singlepage/hello-world-vue3/index.html b/samples/hello-world-singlepage/hello-world-vue3/index.html
new file mode 100644
index 0000000..962d379
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>read-video-vue3</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>
diff --git a/samples/hello-world-singlepage/hello-world-vue3/package.json b/samples/hello-world-singlepage/hello-world-vue3/package.json
new file mode 100644
index 0000000..54da1cf
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/package.json
@@ -0,0 +1,30 @@
+{
+  "name": "ddnjs-vue3-samples",
+  "version": "0.0.0",
+  "homepage": "./",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check build-only",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --noEmit"
+  },
+  "dependencies": {
+    "dynamsoft-camera-enhancer": "4.0.2",
+    "dynamsoft-capture-vision-router": "2.2.10",
+    "dynamsoft-core": "3.2.10",
+    "dynamsoft-document-normalizer": "2.2.10",
+    "dynamsoft-license": "3.2.10",
+    "vue": "^3.2.45"
+  },
+  "devDependencies": {
+    "@types/node": "^18.11.12",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vue/tsconfig": "^0.1.3",
+    "npm-run-all": "^4.1.5",
+    "typescript": "~4.7.4",
+    "vite": "^4.0.0",
+    "vue-tsc": "^1.0.12"
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-vue3/public/favicon.ico b/samples/hello-world-singlepage/hello-world-vue3/public/favicon.ico
new file mode 100644
index 0000000..df36fcf
Binary files /dev/null and b/samples/hello-world-singlepage/hello-world-vue3/public/favicon.ico differ
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/App.vue b/samples/hello-world-singlepage/hello-world-vue3/src/App.vue
new file mode 100644
index 0000000..cde5b34
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/App.vue
@@ -0,0 +1,61 @@
+<script setup lang="ts">
+import { ref, type Ref } from "vue";
+import vueLogo from "./assets/logo.svg";
+import VideoNormalizer from "./components/VideoNormalizer.vue";
+import ImageNormalizer from "./components/ImageNormalizer.vue";
+
+const mode: Ref<string> = ref("video");
+</script>
+
+<template>
+  <div class='App'>
+    <div class='title'>
+      <h2 class='title-text'>Hello World for Vue</h2>
+      <img class='title-logo' :src="vueLogo" alt="logo" />
+    </div>
+    <div class='top-btns'>
+      <button @click="mode = 'video'" :style="{ backgroundColor: mode === 'video' ? 'rgb(255, 174, 55)' : '#FFFFFF' }">VideoNormalizer</button>
+      <button @click="mode = 'image'" :style="{ backgroundColor: mode === 'image' ? 'rgb(255, 174, 55)' : '#FFFFFF' }">ImageNormalizer</button>
+    </div>
+    <VideoNormalizer v-if="mode === 'video'"/> 
+    <ImageNormalizer v-else/>
+  </div>
+</template>
+
+<style scoped>
+.title {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 50px;
+}
+.title .title-logo {
+  width: 30px;
+  height: 30px;
+  margin-left: 10px;
+}
+
+.top-btns {
+  width: 100%;
+  margin: 20px auto;
+}
+
+.top-btns button {
+  display: inline-block;
+  border: 1px solid black;
+  padding: 5px 15px;
+  background-color: transparent;
+  cursor: pointer;
+}
+
+.top-btns button:first-child {
+  border-top-left-radius: 10px;
+  border-bottom-left-radius: 10px;
+  border-right: transparent;
+}
+.top-btns button:nth-child(2) {
+  border-top-right-radius: 10px;
+  border-bottom-right-radius: 10px;
+  border-left: transparent;
+}
+</style>
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/assets/logo.svg b/samples/hello-world-singlepage/hello-world-vue3/src/assets/logo.svg
new file mode 100644
index 0000000..bc826fe
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/assets/logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"  xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/assets/main.css b/samples/hello-world-singlepage/hello-world-vue3/src/assets/main.css
new file mode 100644
index 0000000..77f7db1
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/assets/main.css
@@ -0,0 +1,6 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  text-align: center;
+}
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/components/ImageNormalizer.vue b/samples/hello-world-singlepage/hello-world-vue3/src/components/ImageNormalizer.vue
new file mode 100644
index 0000000..31379e2
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/components/ImageNormalizer.vue
@@ -0,0 +1,62 @@
+<script setup lang="ts">
+import { type NormalizedImageResultItem } from "dynamsoft-document-normalizer";
+import { CaptureVisionRouter } from "dynamsoft-capture-vision-router";
+import { onMounted, onUnmounted, ref, type Ref } from "vue";
+
+const iptRef: Ref<HTMLInputElement | null> = ref(null);
+const elInr: Ref<HTMLDivElement | null> = ref(null);
+const router: Ref<Promise<CaptureVisionRouter> | null> = ref(null);
+
+onMounted(() => {
+    router.value = CaptureVisionRouter.createInstance();
+})
+
+onUnmounted(async () => {
+    (await router.value)?.dispose();
+    console.log('ImageNormalizer Component Unmount');
+})
+
+const captureImg = async (e: any) => {
+    try {
+        elInr.value!.innerHTML = "";
+        const normalizer = await router.value;
+        const results = await normalizer!.capture(e.target.files[0], "DetectAndNormalizeDocument_Default");
+        if(results.items.length) {
+            const cvs = (results.items[0] as NormalizedImageResultItem).toCanvas();
+            if (document.body.clientWidth < 600) {
+                cvs.style.width = "90%";
+            }
+            elInr.value!.append(cvs);
+        }
+        console.log(results);
+    } catch (ex: any) {
+        let errMsg = ex.message || ex;
+        console.error(errMsg);
+        alert(errMsg);
+    }
+}
+</script>
+
+<template>
+    <div class="recognize-img">
+        <div class="img-ipt"><input type="file" ref="iptRef" @change="captureImg" /></div>
+    </div>
+    <div class="img-normalized-result" ref="elInr"></div>
+</template>
+    
+<style scoped>
+.recognize-img {
+    width: 100%;
+    height: 100%;
+    font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
+}
+
+.recognize-img .img-ipt {
+    width: 80%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    border: 1px solid black;
+    margin: 0 auto;
+}
+</style>
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/components/VideoNormalizer.vue b/samples/hello-world-singlepage/hello-world-vue3/src/components/VideoNormalizer.vue
new file mode 100644
index 0000000..c709b58
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/components/VideoNormalizer.vue
@@ -0,0 +1,175 @@
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref, type Ref } from "vue";
+import { EnumCapturedResultItemType, type DSImageData, type OriginalImageResultItem, type Point } from "dynamsoft-core";
+import { type NormalizedImageResultItem } from "dynamsoft-document-normalizer";
+import { CameraEnhancer, CameraView, QuadDrawingItem, ImageEditorView } from "dynamsoft-camera-enhancer";
+import { CapturedResultReceiver, CaptureVisionRouter, type SimplifiedCaptureVisionSettings } from "dynamsoft-capture-vision-router";
+
+let imageEditorViewContainerRef: Ref<HTMLDivElement | null> = ref(null);
+let cameraViewContainerRef: Ref<HTMLDivElement | null> = ref(null);
+let normalizedImageContainer: Ref<HTMLDivElement | null> = ref(null);
+let cameraEnhancer: Ref<Promise<CameraEnhancer> | null> = ref(null);
+let router: Ref<Promise<CaptureVisionRouter> | null> = ref(null);
+let bShowUiContainer = ref(true);
+let bShowImageContainer = ref(false);
+let bDisabledBtnEdit = ref(false);
+let bDisabledBtnNor = ref(true);
+let bShowLoading = ref(true);
+
+let items: Array<any> = [];
+let quads: Array<any> = [];
+let image: DSImageData;
+let confirmTheBoundary: () => void;
+let normalize: () => void;
+
+onMounted(async () => {
+    try {
+        const view = await CameraView.createInstance();
+        const dce = await (cameraEnhancer.value = CameraEnhancer.createInstance(view));
+        const imageEditorView = await ImageEditorView.createInstance(imageEditorViewContainerRef.value as HTMLDivElement);
+        /* Creates an image editing layer for drawing found document boundaries. */
+        const layer = imageEditorView.createDrawingLayer();
+
+        /**
+         * Creates a CaptureVisionRouter instance and configure the task to detect document boundaries.
+         * Also, make sure the original image is returned after it has been processed.
+         */
+        const normalizer = await (router.value = CaptureVisionRouter.createInstance());
+
+        normalizer.setInput(dce);
+        /**
+         * Sets the result types to be returned.
+         * Because we need to normalize the original image later, here we set the return result type to
+         * include both the quadrilateral and original image data.
+         */
+        let newSettings = await normalizer.getSimplifiedSettings("DetectDocumentBoundaries_Default");
+        newSettings.capturedResultItemTypes |= EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE;
+        await normalizer.updateSettings("DetectDocumentBoundaries_Default", newSettings);
+        cameraViewContainerRef.value!.append(view.getUIElement());
+
+        /* Defines the result receiver for the task.*/
+        const resultReceiver = new CapturedResultReceiver();
+        resultReceiver.onCapturedResultReceived = (result) => {
+            const originalImage = result.items.filter(item => item.type === EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE);
+            if (originalImage.length) {
+                image = (originalImage[0] as OriginalImageResultItem).imageData;
+            }
+            items = result.items.filter(item => item.type === EnumCapturedResultItemType.CRIT_DETECTED_QUAD);
+        }
+        normalizer.addResultReceiver(resultReceiver);
+
+        confirmTheBoundary = () => {
+            if (!dce.isOpen() || !items.length) return;
+            /* Hides the cameraView and shows the imageEditorView. */
+            bShowUiContainer.value = false
+            bShowImageContainer.value = true;
+            /* Draws the image on the imageEditorView first. */
+            imageEditorView.setOriginalImage(image);
+            quads = [];
+            /* Draws the document boundary (quad) over the image. */
+            for (let i = 0; i < items.length; i++) {
+                if (items[i].type === EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE) continue;
+                const points = items[i].location.points;
+                const quad = new QuadDrawingItem({ points });
+                quads.push(quad);
+                layer.addDrawingItems(quads);
+            }
+            bDisabledBtnNor.value = false;
+            bDisabledBtnEdit.value = true;
+            normalizer.stopCapturing();
+        }
+
+        normalize = async () => {
+            /* Get the selected quadrilateral */
+            let seletedItems = imageEditorView.getSelectedDrawingItems();
+            let quad;
+            if (seletedItems.length) {
+                quad = (seletedItems[0] as QuadDrawingItem).getQuad();
+            } else {
+                quad = items[0].location;
+            }
+            const isPointOverBoundary = (point: Point) => {
+                if(point.x < 0 || 
+                point.x > image.width || 
+                point.y < 0 ||
+                point.y > image.height) {
+                    return true;
+                } else {
+                    return false;
+                }
+            };
+            /* Check if the points beyond the boundaries of the image. */
+            if (quad.points.some((point: Point) => isPointOverBoundary(point))) {
+                alert("The document boundaries extend beyond the boundaries of the image and cannot be used to normalize the document.");
+                return;
+            }
+            
+            /* Hides the imageEditorView. */
+            bShowImageContainer.value = false;
+            /* Removes the old normalized image if any. */
+            normalizedImageContainer.value!.innerHTML = "";
+            /**
+             * Sets the coordinates of the ROI (region of interest)
+             * in the built-in template "normalize-document".
+             */
+            let newSettings = await normalizer.getSimplifiedSettings("normalize-document") as SimplifiedCaptureVisionSettings;
+            newSettings.roiMeasuredInPercentage = false;
+            newSettings.roi.points = quad.points;
+            await normalizer.updateSettings("normalize-document", newSettings);
+            /* Executes the normalization and shows the result on the page */
+            let normalizeResult = await normalizer.capture(image, "normalize-document");
+            if (normalizeResult.items[0]) {
+                normalizedImageContainer.value!.append((normalizeResult.items[0] as NormalizedImageResultItem).toCanvas());
+            }
+            layer.clearDrawingItems();
+            bDisabledBtnNor.value = true;
+            bDisabledBtnEdit.value = false;
+            /* show video view */
+            bShowUiContainer.value = true
+            view.getUIElement().style.display = "";
+            await normalizer.startCapturing("DetectDocumentBoundaries_Default");
+        }
+
+        await dce.open();
+        /* Uses the built-in template "DetectDocumentBoundaries_Default" to start a continuous boundary detection task. */
+        await normalizer.startCapturing("DetectDocumentBoundaries_Default");
+        bShowLoading.value = false;
+    } catch (ex: any) {
+        let errMsg = ex.message || ex;
+        console.error(errMsg);
+        alert(errMsg);
+    }
+})
+
+onUnmounted(async () => {
+    (await router.value)?.dispose();
+    (await cameraEnhancer.value)?.dispose();
+    console.log('VideoNormalizer Component Unmount');
+})
+</script>
+
+<template>
+    <div id="div-loading" v-show="bShowLoading">Loading...</div>
+    <div id="div-video-btns">
+        <button id="confirm-quad-for-normalization" @click="confirmTheBoundary" :disabled="bDisabledBtnEdit">Confirm the
+            Boundary</button>
+        <button id="normalize-with-confirmed-quad" @click="normalize" :disabled="bDisabledBtnNor">Normalize</button>
+    </div>
+    <div id="div-ui-container" style="margin-top: 10px;height: 500px;" ref="cameraViewContainerRef"
+        v-show="bShowUiContainer"></div>
+    <div id="div-image-container" style="display:none; width: 100vw; height: 70vh" ref="imageEditorViewContainerRef"
+        v-show="bShowImageContainer">
+        <div class="dce-image-container" style="width: 100%; height: 100%"></div>
+    </div>
+    <div id="normalized-result" ref="normalizedImageContainer"></div>
+</template>
+    
+<style scoped>
+#div-video-btns {
+    width: 75%;
+    margin: 0 auto;
+    margin-bottom: 10px;
+    display: flex;
+    justify-content: space-around;
+}
+</style>
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/cvr.ts b/samples/hello-world-singlepage/hello-world-vue3/src/cvr.ts
new file mode 100644
index 0000000..141a0c6
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/cvr.ts
@@ -0,0 +1,32 @@
+import "dynamsoft-license";
+import "dynamsoft-document-normalizer";
+import "dynamsoft-capture-vision-router";
+
+import { CoreModule } from "dynamsoft-core";
+import { LicenseManager } from "dynamsoft-license";
+
+/** LICENSE ALERT - README 
+ * To use the library, you need to first call the method initLicense() to initialize the license using a license key string.
+ */
+LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9");
+/**
+ * The license "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9" is a temporary license for testing good for 24 hours.
+ * You can visit https://www.dynamsoft.com/customer/license/trialLicense?utm_source=github&architecture=dcv&product=ddn&package=js to get your own trial license good for 30 days.
+ * LICENSE ALERT - THE END
+ */
+
+CoreModule.engineResourcePaths = {
+  std: "https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-std@1.2.0/dist/",
+  dip: "https://cdn.jsdelivr.net/npm/dynamsoft-image-processing@2.2.10/dist/",
+  core: "https://cdn.jsdelivr.net/npm/dynamsoft-core@3.2.10/dist/",
+  license: "https://cdn.jsdelivr.net/npm/dynamsoft-license@3.2.10/dist/",
+  cvr: "https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.2.10/dist/",
+  ddn: "https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.2.10/dist/",
+  dce: "https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@4.0.2/dist/"
+};
+
+CoreModule.loadWasm(["DDN"]).catch((ex:any) => {
+  let errMsg = ex.message || ex;
+  console.error(errMsg);
+  alert(errMsg);
+});
\ No newline at end of file
diff --git a/samples/hello-world-singlepage/hello-world-vue3/src/main.ts b/samples/hello-world-singlepage/hello-world-vue3/src/main.ts
new file mode 100644
index 0000000..0c27b32
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/src/main.ts
@@ -0,0 +1,7 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import "./cvr"
+
+import './assets/main.css'
+
+createApp(App).mount('#app')
diff --git a/samples/hello-world-singlepage/hello-world-vue3/tsconfig.config.json b/samples/hello-world-singlepage/hello-world-vue3/tsconfig.config.json
new file mode 100644
index 0000000..424084a
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/tsconfig.config.json
@@ -0,0 +1,8 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.node.json",
+  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
+  "compilerOptions": {
+    "composite": true,
+    "types": ["node"]
+  }
+}
diff --git a/samples/hello-world-singlepage/hello-world-vue3/tsconfig.json b/samples/hello-world-singlepage/hello-world-vue3/tsconfig.json
new file mode 100644
index 0000000..b735ce7
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/tsconfig.json
@@ -0,0 +1,20 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.web.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "allowJs": true,
+  "compilerOptions": {
+    "importsNotUsedAsValues": "remove",
+    "verbatimModuleSyntax": true,
+    "ignoreDeprecations": "5.0",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+
+  "references": [
+    {
+      "path": "./tsconfig.config.json"
+    }
+  ]
+}
diff --git a/samples/hello-world-singlepage/hello-world-vue3/vite.config.ts b/samples/hello-world-singlepage/hello-world-vue3/vite.config.ts
new file mode 100644
index 0000000..7677e96
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world-vue3/vite.config.ts
@@ -0,0 +1,15 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    },
+  },
+  base: "./"
+})
diff --git a/samples/hello-world-singlepage/hello-world/hello-world.html b/samples/hello-world-singlepage/hello-world/hello-world.html
new file mode 100644
index 0000000..001e43c
--- /dev/null
+++ b/samples/hello-world-singlepage/hello-world/hello-world.html
@@ -0,0 +1,262 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta name="description" content="Quickly detect document boundaries from a live camera stream and crop the document out before normalizing it further through perspective correction, deskewing, and more." />
+  <meta name="keywords" content="camera based quadrilateral detection and normalization" />
+  <title>Mobile Web Capture - HelloWorld - Single Page</title>
+  <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 src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@4.0.2/dist/dce.js"></script>
+</head>
+
+<body>
+  <h1 style="font-size: 1.5em">
+    Detect the boundary of a document and normalize it
+  </h1>
+  <button id="start-detecting">Start Detecting</button>
+  <button id="restart-detecting" style="display: none">Restart Detecting</button>
+  <button id="confirm-quad-for-normalization">Confirm the Boundary</button>
+  <button id="normalize-with-confirmed-quad" disabled>Normalize</button><br />
+  <input type="checkbox" style="vertical-align: middle" id="auto-normalize" /><label style="vertical-align: middle" for="auto-normalize">Normalize Automatically</label>
+  <div id="div-ui-container" style="margin-top: 10px; height: 450px"></div>
+  <div id="div-image-container" style="display: none; width: 100%; height: 70vh"></div>
+  <div id="normalized-result"></div>
+  <script>
+    let quads = [];
+    let cameraEnhancer = null;
+    let router = null;
+    let items;
+    let layer;
+    let originalImage;
+    let imageEditorView;
+    let promiseCVRReady;
+    let frameCount = 0;
+
+    const btnStart = document.querySelector("#start-detecting");
+    const btnRestart = document.querySelector("#restart-detecting");
+    const cameraViewContainer = document.querySelector("#div-ui-container");
+    const normalizedImageContainer = document.querySelector("#normalized-result");
+    const btnEdit = document.querySelector("#confirm-quad-for-normalization");
+    const btnNormalize = document.querySelector("#normalize-with-confirmed-quad");
+    const imageEditorViewContainer = document.querySelector("#div-image-container");
+    const autoNormalize = document.querySelector("#auto-normalize");
+
+    /** LICENSE ALERT - README
+     * To use the library, you need to first call the method initLicense() to initialize the license using a license key string.
+     */
+    Dynamsoft.License.LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLXIxNzAzODM5ODkwIiwibWFpblNlcnZlclVSTCI6Imh0dHBzOi8vbWx0cy5keW5hbXNvZnQuY29tLyIsIm9yZ2FuaXphdGlvbklEIjoiMjAwMDAwIiwic3RhbmRieVNlcnZlclVSTCI6Imh0dHBzOi8vc2x0cy5keW5hbXNvZnQuY29tLyIsImNoZWNrQ29kZSI6MTgyNTQ5Njk4NH0=");
+    /**
+     * The license "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9" is a temporary license for testing good for 24 hours.
+     * You can visit https://www.dynamsoft.com/customer/license/trialLicense?utm_source=github&architecture=dcv&product=ddn&package=js to get your own trial license good for 30 days.
+     * LICENSE ALERT - THE END
+     */
+
+    /**
+     * Preloads the `DocumentNormalizer` module, saving time in preparing for document border detection and image normalization.
+     */
+    Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
+
+    /**
+     * Creates a CameraEnhancer instance and prepares an ImageEditorView instance for later use.
+     */
+    async function initDCE() {
+      const view = await Dynamsoft.DCE.CameraView.createInstance();
+      cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(view);
+      imageEditorView = await Dynamsoft.DCE.ImageEditorView.createInstance(imageEditorViewContainer);
+      /* Creates an image editing layer for drawing found document boundaries. */
+      layer = imageEditorView.createDrawingLayer();
+      cameraViewContainer.append(view.getUIElement());
+    }
+
+    /**
+     * Creates a CaptureVisionRouter instance and configure the task to detect document boundaries.
+     * Also, make sure the original image is returned after it has been processed.
+     */
+    let cvrReady = (async function initCVR() {
+      await initDCE();
+      router = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
+      router.setInput(cameraEnhancer);
+      /**
+       * Sets the result types to be returned.
+       * Because we need to normalize the original image later, here we set the return result type to
+       * include both the quadrilateral and original image data.
+       */
+      let newSettings = await router.getSimplifiedSettings("DetectDocumentBoundaries_Default");
+      newSettings.capturedResultItemTypes |= Dynamsoft.Core.EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE;
+      await router.updateSettings("DetectDocumentBoundaries_Default", newSettings);
+
+      /* Defines the result receiver for the task.*/
+      const resultReceiver = new Dynamsoft.CVR.CapturedResultReceiver();
+      resultReceiver.onCapturedResultReceived = handleCapturedResult;
+      router.addResultReceiver(resultReceiver);
+    })();
+
+    /**
+     * Defines the callback function that is executed after each image has been processed.
+     */
+    async function handleCapturedResult(result) {
+      /* Do something with the result */
+      /* Saves the image data of the current frame for subsequent image editing. */
+      const originalImage = result.items.filter((item) => item.type === 1);
+      originalImageData = originalImage.length && originalImage[0].imageData;
+      if (!autoNormalize.checked) {
+        /* why > 1? Because the "result items" always contain a result of the original image. */
+        if (result.items.length > 1) {
+          items = result.items;
+        }
+      } else if (originalImageData) {
+        /** If "Normalize Automatically" is checked, the library uses the document boundaries found in consecutive
+         * image frames to decide whether conditions are suitable for automatic normalization.
+         */
+        if (result.items.length <= 1) {
+          frameCount = 0;
+          return;
+        }
+        frameCount++;
+        /**
+         * In our case, we determine a good condition for "automatic normalization" to be
+         * "getting document boundary detected for 30 consecutive frames".
+         *
+         * NOTE that this condition will not be valid should you add a CapturedResultFilter
+         * with ResultDeduplication enabled.
+         */
+        if (frameCount === 30) {
+          frameCount = 0;
+          normalizedImageContainer.innerHTML = "";
+          /**
+           * When the condition is met, we use the document boundary found in this image frame
+           * to normalize the document by setting the coordinates of the ROI (region of interest)
+           * in the built-in template "NormalizeDocument_Default".
+           */
+          let settings = await router.getSimplifiedSettings("NormalizeDocument_Default");
+          settings.roiMeasuredInPercentage = 0;
+          settings.roi.points = result.items[1].location.points;
+          await router.updateSettings("NormalizeDocument_Default", settings);
+          /**
+           * After that, executes the normalization and shows the result on the page.
+           */
+          let normalizeResult = await router.capture(originalImageData, "NormalizeDocument_Default");
+          normalizedImageContainer.append(normalizeResult.items[0].toCanvas());
+          cameraViewContainer.style.display = "none";
+          btnStart.style.display = "none";
+          btnRestart.style.display = "inline";
+          btnEdit.disabled = true;
+          await router.stopCapturing();
+        }
+      }
+    }
+
+    btnStart.addEventListener("click", async () => {
+      try {
+        await (promiseCVRReady = promiseCVRReady || (async () => {
+          await cvrReady;
+          /* Starts streaming the video. */
+          await cameraEnhancer.open();
+          /* Uses the built-in template "DetectDocumentBoundaries_Default" to start a continuous boundary detection task. */
+          await router.startCapturing("DetectDocumentBoundaries_Default");
+        })());
+      } catch (ex) {
+        let errMsg = ex.message || ex;
+        console.error(errMsg);
+        alert(errMsg);
+      }
+    })
+
+    btnRestart.addEventListener("click", async () => {
+      /* Reset the UI elements and restart the detection task. */
+      imageEditorViewContainer.style.display = "none";
+      normalizedImageContainer.innerHTML = "";
+      cameraViewContainer.style.display = "block";
+      btnStart.style.display = "inline";
+      btnRestart.style.display = "none";
+      btnNormalize.disabled = true;
+      btnEdit.disabled = false;
+      layer.clearDrawingItems();
+
+      await router.startCapturing("DetectDocumentBoundaries_Default");
+    })
+
+    autoNormalize.addEventListener("change", () => {
+      btnEdit.style.display = autoNormalize.checked ? "none" : "inline";
+      btnNormalize.style.display = autoNormalize.checked ? "none" : "inline";
+    });
+
+    btnEdit.addEventListener("click", async () => {
+      if (!cameraEnhancer.isOpen() || items.length <= 1) return;
+      /* Stops the detection task since we assume we have found a good boundary. */
+      router.stopCapturing();
+      /* Hides the cameraView and shows the imageEditorView. */
+      cameraViewContainer.style.display = "none";
+      imageEditorViewContainer.style.display = "block";
+      /* Draws the image on the imageEditorView first. */
+      imageEditorView.setOriginalImage(originalImageData);
+      quads = [];
+      /* Draws the document boundary (quad) over the image. */
+      for (let i = 0; i < items.length; i++) {
+        if (items[i].type === Dynamsoft.Core.EnumCapturedResultItemType.CRIT_ORIGINAL_IMAGE) continue;
+        const points = items[i].location.points;
+        const quad = new Dynamsoft.DCE.QuadDrawingItem({ points });
+        quads.push(quad);
+        layer.addDrawingItems(quads);
+      }
+      btnStart.style.display = "none";
+      btnRestart.style.display = "inline";
+      btnNormalize.disabled = false;
+      btnEdit.disabled = true;
+    });
+
+    btnNormalize.addEventListener("click", async () => {
+      /* Gets the selected quadrilateral. */
+      let seletedItems = imageEditorView.getSelectedDrawingItems();
+      let quad;
+      if (seletedItems.length) {
+        quad = seletedItems[0].getQuad();
+      } else {
+        quad = items[1].location;
+      }
+      const isPointOverBoundary = (point) => {
+        if (point.x < 0 ||
+          point.x > originalImageData.width ||
+          point.y < 0 ||
+          point.y > originalImageData.height) {
+          return true;
+        } else {
+          return false;
+        }
+      };
+      /* Check if the points beyond the boundaries of the image. */
+      if (quad.points.some(point => isPointOverBoundary(point))) {
+        alert("The document boundaries extend beyond the boundaries of the image and cannot be used to normalize the document.");
+        return;
+      }
+
+      /* Hides the imageEditorView. */
+      imageEditorViewContainer.style.display = "none";
+      /* Removes the old normalized image if any. */
+      normalizedImageContainer.innerHTML = "";
+      /**
+       * Sets the coordinates of the ROI (region of interest)
+       * in the built-in template "NormalizeDocument_Default".
+       */
+      let newSettings = await router.getSimplifiedSettings("NormalizeDocument_Default");
+      newSettings.roiMeasuredInPercentage = 0;
+      newSettings.roi.points = quad.points;
+      await router.updateSettings("NormalizeDocument_Default", newSettings);
+      /* Executes the normalization and shows the result on the page. */
+      let normalizeResult = await router.capture(originalImageData, "NormalizeDocument_Default");
+      if (normalizeResult.items[0]) {
+        normalizedImageContainer.append(normalizeResult.items[0].toCanvas());
+      }
+      layer.clearDrawingItems();
+      btnNormalize.disabled = true;
+      btnEdit.disabled = true;
+    });
+  </script>
+</body>
+
+</html>
\ No newline at end of file