Table of contents

Capture Single Page and Then Crop

This sample adds a perspective viewer based on HelloWorld solution to review and adjust the detected boundaries on the captured image.

Check out it online

In this sample, we would like to achieve the workflow as below.

Flow chart for review-adjust-detected-boundaries

Create a perspective viewer

  • Customize the viewer’s UiConfig based on the default one to implement the workflow.

      const newPerspectiveUiConfig = {
          type: Dynamsoft.DDV.Elements.Layout,
          flexDirection: "column",
          children: [
              {
                  type: Dynamsoft.DDV.Elements.Layout,
                  className: "ddv-perspective-viewer-header-mobile",
                  children: [
                      {   
                          // Add a "Back" button in perspective viewer's header and bind the event to go back to 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,
                      {
                          // 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"
                          },
                      }
                  ],
              },
          ],
      };
    
  • Create the viewer by using the new UiConfig.

      // Create a perspective viewer
      const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
          container: "container",
          groupUid: captureViewer.groupUid, // Data synchronisation with the capture viewer
          uiConfig: newPerspectiveUiConfig, // Configure the new UiConfig
          viewerConfig:{
              scrollToLatest: true, // Navigate to the latest image automatically
          }
      });
    
  • Since this viewer only shows when captured an image, it should be hidden at first.

      perspectiveViewer.hide();
    

Configure the workflow

  • Define a function to control the viewers’ visibility.

      function switchViewer(capture, perspective){
          if(capture) {
              captureViewer.show();
              captureViewer.play();
          } else {
              captureViewer.hide();
              captureViewer.stop();
          }
    
          if(perspective) {
              perspectiveViewer.show();
          } else {
              perspectiveViewer.hide();
          }
      }
    
  • Register the relavant events to customize the interaction behavior between viewers.

    • Register captured event in captureViewer to hide the capture viewer and show the perspective viewer once an image is captured.

        captureViewer.on("captured", () => {
            switchViewer(false, true);
        });
      
    • Register an event in perspectiveViewer to delete the current image and go back the capture viewer.

        // Event for clicking "Back" button
        perspectiveViewer.on("backToCaptureViewer",() => {
            switchViewer(true, false);
            perspectiveViewer.currentDocument.deleteAllPages();
        });
      
    • Register an event in perspectiveViewer to determine if there are no images in the viewer when clicking “DeleteCurrent” & “DeletedAll” buttons.

        // Event for clicking "DeleteCurrent" & "DeletedAll" buttons
        perspectiveViewer.on("noImageBack", () => {
            // Determine if there are no images in the viewer
            const count = perspectiveViewer.currentDocument.pages.length;
            // If yes, back to the capture viewer
            if(count === 0) {
                switchViewer(true,false)
            }
        });
      
    • Register an event in perspectiveViewer to display the result image.

        perspectiveViewer.on("done", async () => {
            // hide viewers and container
            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);
        });
      

For now, we finish the main workflow for this sample, can add the restore function to capture new image additionally.

document.getElementById("restore").onclick = () => {
    perspectiveViewer.currentDocument.deleteAllPages();
    document.getElementById("container").style.display = "";
    switchViewer(true, false)
};

Review the complete code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Mobile Web Capture - 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-document-viewer@2.0.0/dist/ddv.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-core@3.2.10/dist/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-license@3.2.10/dist/license.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.2.10/dist/ddn.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.2.10/dist/cvr.js"></script>
<script type="module">

    import { isMobile, initDocDetectModule } from "./utils.js";

    (async () => {
        //Preloads the Document Normalizer module.
        Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
        //Preloads the Document Viewer module.
        Dynamsoft.DDV.Core.loadWasm();

        // Initialize DDN
        await Dynamsoft.License.LicenseManager.initLicense(
            "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTEwMjQ5NjE5NyJ9",
            true
        );
        // Initialize DDV
        await Dynamsoft.DDV.Core.init();

        // Configure document boundaries function
        await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);

        //Create a capture viewer
        const captureViewer = new Dynamsoft.DDV.CaptureViewer({
            container: "container",
            viewerConfig: {
                acceptedPolygonConfidence: 60,
                enableAutoCapture: true,
                enableAutoDetect: true
            }
        });
        // Play video stream in 1080P
        captureViewer.play({ 
            resolution: [1920,1080],
            fill: true
        });
        // Register captured event
        captureViewer.on("captured", () => {
            switchViewer(false, true);
        });

        // Define new UiConfig for perspecitve viewer
        const newPerspectiveUiConfig = {
            type: Dynamsoft.DDV.Elements.Layout,
            flexDirection: "column",
            children: [
                {
                    type: Dynamsoft.DDV.Elements.Layout,
                    className: "ddv-perspective-viewer-header-mobile",
                    children: [
                        {   
                            // Add a "Back" button in perspective viewer's header and bind the event
                            // 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 "DeleteCurrent" button
                            // The event will be registered later
                            type:Dynamsoft.DDV.Elements.DeleteAll,
                            events: {
                                click: "noImageBack"
                            },
                        }
                    ],
                },
            ],
        };

        // Create a perspective viewer
        const perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
            container: "container",
            groupUid: captureViewer.groupUid, // Data synchronisation with the capture viewer
            uiConfig: newPerspectiveUiConfig, // Configure the new UiConfig
            viewerConfig:{
                scrollToLatest: true,
            }
        }); 

        // 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)
            }
        }); 

        // Register the event for "PerspectiveAll" button to display the result image
        perspectiveViewer.on("done", async () => {
            // hide viewers and container
            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);
        });

        // Restore Button function
        document.getElementById("restore").onclick = () => {
            perspectiveViewer.currentDocument.deleteAllPages();
            document.getElementById("container").style.display = "";
            switchViewer(true, false)
        };

        // Control viewers' visibility.
        function switchViewer(capture, perspective){
            if(capture) {
                captureViewer.show();
                captureViewer.play();
            } else {
                captureViewer.hide();
                captureViewer.stop();
            }

            if(perspective) {
                perspectiveViewer.show();
            } else {
                perspectiveViewer.hide();
            }
        }
    })();
</script>
</html>

Download the whole project

Github | Run

Please note that in order to be compatible with desktop devices as much as possible, some compatibility codes have been added to the whole project code.

UiConfig part is organized into uiConfig.js and referenced in the core code to minimize the length of the core code.

Add auxiliary text if necessary

Sometimes, you may want to add some auxiliary text to icons to show better user guidance.

Refer to