From ee864d7a09b1bb40892afebd310882d73666abda Mon Sep 17 00:00:00 2001 From: felixindrawan Date: Wed, 28 Aug 2024 09:27:22 -0700 Subject: [PATCH 01/12] feat: General MRZ --- README.md | 10 +- assets/Music-selected.svg | 20 +- .../{passport frame.svg => mrz-guide-box.svg} | 2 +- assets/music-unselected.svg | 17 +- assets/torch-icon-close.svg | 14 + assets/torch-icon-open.svg | 16 + css/index.css | 155 ++++++-- index.html | 345 ++++++++++++------ js/const.js | 51 +++ js/define.js | 29 -- js/index.js | 149 ++++---- js/init.js | 116 +++--- js/util.js | 208 +++++++---- template.json | 287 +++++++++++---- 14 files changed, 959 insertions(+), 460 deletions(-) rename assets/{passport frame.svg => mrz-guide-box.svg} (99%) create mode 100644 assets/torch-icon-close.svg create mode 100644 assets/torch-icon-open.svg create mode 100644 js/const.js delete mode 100644 js/define.js diff --git a/README.md b/README.md index 244cff1..2af013a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # About the solution -Passport MRZ Scanner enables camera to scan the MRZ code of a passport. It will extract all data like firstname, lastname, passport number, nationality, date of birth, expiration date and personal number from the MRZ string, and converts the encoded string into human-readable fields. Welcome to visit dynamsoft's [official website](https://dynamsoft.com/capture-vision/docs/web/programming/javascript/user-guide/passport-mrz-scanner.html) to learn more about this solution. +General MRZ Scanner enables camera to scan the MRZ code of ID-cards, passports, and visas. Currently, the General MRZ Scanner supports TD-1, TD-2, TD-3, MRV-A, and MRV-B standards. It will extract all data like first name, last name, document number, nationality, date of birth, expiration date and more from the MRZ string, and converts the encoded string into human-readable fields. ## Web demo @@ -30,12 +30,12 @@ cd passport-MRZ-scanner-javascript ## Request a Trial License -The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution serves as a test license valid for 24 hours, applicable to any newly authorized browser. To test the SDK further, you can request a 30-day free trial license via the Request a Trial License link. +The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution (in the file `js/init.js`) serves as a test license valid for 24 hours, applicable to any newly authorized browser. To test the SDK further, you can request a 30-day free trial license via the Request a Trial License link. ## Project Structure ```text -Passport MRZ Scanner +General MRZ Scanner ├── assets │ ├── ... │ ├── ... @@ -47,7 +47,7 @@ Passport MRZ Scanner │ ├── ... │ └── ... ├── js -│ ├── define.js +│ ├── const.js │ ├── index.js │ ├── init.js │ └── util.js @@ -59,7 +59,7 @@ Passport MRZ Scanner * `/css` : This directory contains the CSS file(s) used for styling the project. * `/font` : This directory contains the font files used in the project. * `/js` : This directory contains all the JavaScript files used in the project. - * `define.js` : This file contains definitions of certain constants or variables used across the project. + * `const.js` : This file contains definitions of certain constants or variables used across the project. * `index.js`: This is the main JavaScript file where the core logic of the project is implemented. * `init.js` : This file is used for initialization purposes, such as initializing license, load resources, etc. * `util.js` : This file contains utility functions that are used across the project. diff --git a/assets/Music-selected.svg b/assets/Music-selected.svg index ff3d7a2..f440979 100644 --- a/assets/Music-selected.svg +++ b/assets/Music-selected.svg @@ -1,14 +1,6 @@ - - - - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/assets/passport frame.svg b/assets/mrz-guide-box.svg similarity index 99% rename from assets/passport frame.svg rename to assets/mrz-guide-box.svg index caa616c..06cdc9c 100644 --- a/assets/passport frame.svg +++ b/assets/mrz-guide-box.svg @@ -133,4 +133,4 @@ - + \ No newline at end of file diff --git a/assets/music-unselected.svg b/assets/music-unselected.svg index 4b5f4be..0edab1f 100644 --- a/assets/music-unselected.svg +++ b/assets/music-unselected.svg @@ -1,10 +1,7 @@ - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/assets/torch-icon-close.svg b/assets/torch-icon-close.svg new file mode 100644 index 0000000..6b47cce --- /dev/null +++ b/assets/torch-icon-close.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/torch-icon-open.svg b/assets/torch-icon-open.svg new file mode 100644 index 0000000..e8c0013 --- /dev/null +++ b/assets/torch-icon-open.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/css/index.css b/css/index.css index 77bbbe9..e6f065c 100644 --- a/css/index.css +++ b/css/index.css @@ -25,6 +25,18 @@ body { height: 100%; -webkit-text-size-adjust: 100%; /* Prevent font scaling in landscape while allowing user zoom */ + background-color: #2b2b2b; +} + +button { + border: none; + cursor: pointer; + color: #ffffff; + border: 0; +} + +img { + user-select: none; } .home-page { @@ -39,8 +51,9 @@ body { justify-content: space-between; align-items: center; color: #ffffff; - background-color: #323234; + background-color: #2b2b2b; padding: 30px 0; + gap: 20px; } .home-page .logo { @@ -49,8 +62,12 @@ body { } .home-page .description { - width: 80%; + width: 50%; text-align: center; + background-color: #323234; + margin: 0 auto; + padding: 2rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .home-page .description .title { @@ -62,7 +79,7 @@ body { font-family: OpenSans-Regular; font-size: 16px; line-height: 26px; - margin: 16px 0 25px 0; + margin: 16px 0 25px; } .home-page .description .start-btn { @@ -70,16 +87,21 @@ body { height: 6vh; min-height: 40px; max-height: 60px; - background-color: #FE8E14; + background-color: #fe8e14; font-family: Oswald-Regular; font-size: 20px; margin: 0 auto; display: flex; justify-content: center; align-items: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.home-page .description .start-btn:hover { + background-color: #fea543; } -.home-page .power { +.home-page .powered-by-msg { font-size: 16px; font-family: Oswald-Light; } @@ -97,6 +119,8 @@ body { background-color: rgb(55, 55, 55); display: flex; align-items: center; + gap: 15px; + position: relative; } .scanner-container .header .camera-selector { @@ -107,7 +131,11 @@ body { justify-content: space-around; align-items: center; padding: 0 10px; - margin-right: 15px; + cursor: pointer; +} + +.scanner-container .header .camera-selector:hover { + opacity: 0.8; } .scanner-container .header .camera-selector .camera-svg { @@ -120,27 +148,32 @@ body { } .scanner-container .header .camera-list { - width: 165px; position: absolute; top: 46px; left: 0; background-color: #000000; z-index: 1; display: none; + border-right: #aaaaaa; } .scanner-container .header .camera-list .camera-item { width: 100%; height: 40px; - color: #AAAAAA; + color: #aaaaaa; border-bottom: 1px solid rgb(50, 50, 52); font-size: 12px; - font-family: "OpenSans-Regular"; + font-family: OpenSans-Regular; line-height: 40px; padding: 0 10px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + user-select: none; + cursor: pointer; +} +.scanner-container .header .camera-list .camera-item:hover { + opacity: 0.8; } .scanner-container .header .camera-list .camera-selected { @@ -151,14 +184,27 @@ body { border: unset; } -.scanner-container .header .music { +.scanner-container .header .music-container { width: 30px; height: 30px; + cursor: pointer; + display: flex; + align-items: center; + cursor: pointer; +} + +.scanner-container .header .music-container:hover { + opacity: 0.8; +} + +.scanner-container .header .music { + width: 24px; + height: 24px; } .scanner-container .header .no-music { - width: 22px; - height: 22px; + width: 24px; + height: 24px; display: none; } @@ -181,19 +227,15 @@ body { height: 6%; min-height: 35px; max-height: 50px; - padding: 0 15px 0 30px; - background-color: #2B2B2B; + background-color: #2b2b2b; display: flex; justify-content: space-between; align-items: center; + padding: 0 15px 0 30px; } .result-container .result-header .result-title { - color: #AAAAAA; -} - -.result-container .result-header .result-restart { - color: #FE8E14; + color: #aaaaaa; } .result-container .parsed-result-area { @@ -203,11 +245,6 @@ body { overflow: auto; } -.result-container .parsed-result-area .parsed-result-header { - font-size: 18px; - margin-bottom: 30px; -} - .result-container .parsed-result-area .parsed-filed { display: flex; flex-direction: column; @@ -215,7 +252,13 @@ body { } .result-container .parsed-result-area .parsed-filed .field-name { - color: #AAAAAA; + color: #aaaaaa; +} +.result-container .parsed-result-area .parsed-filed .field-value { + word-wrap: break-word; +} +.result-container .parsed-result-area .code .field-value { + font-family: monospace; } .result-container .restart-video { @@ -226,6 +269,7 @@ body { display: flex; justify-content: center; align-items: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .result-container .restart-video .btn-restart-video { @@ -238,6 +282,46 @@ body { font-family: "Oswald-Regular"; } +.scan-mode-container { + display: none; + justify-content: center; + align-items: center; + position: fixed; + bottom: 15%; + left: 5%; + right: 5%; + z-index: 2; +} + +.scan-mode-container .scan-mode-content { + position: relative; + display: flex; + justify-content: center; + align-items: center; + background-color: rgb(34, 34, 34); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + border-radius: 8px; + padding: 0.5rem; + width: max-content; + opacity: 0.8; +} + +.scan-option-btn { + background-color: transparent; + padding: 0.5rem; + font-family: OpenSans-Regular; + color: white; + flex: 1; + width: 5rem; +} + +.selected { + background-color: #fe8e14; + color: white; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + @keyframes dce-rotate { from { transform: rotate(0turn); @@ -258,8 +342,25 @@ body { } } -@media screen and (max-width: 780px) and (orientation: landscape) { +@media screen and (max-width: 800px) { + html, + body, + .home-page { + background-color: #323234; + } + + .home-page .description { + width: 80%; + box-shadow: none; + } + + .home-page .description .start-btn { + font-size: 20px; + } +} + +@media screen and (max-width: 800px) and (orientation: landscape) { .result-container .parsed-result-area .parsed-filed { font-size: 14px; } -} \ No newline at end of file +} diff --git a/index.html b/index.html index 58596a3..a590649 100644 --- a/index.html +++ b/index.html @@ -1,160 +1,279 @@ - - - - - Passport MRZ Scanner - - - - - - - - - - - -
- -
-
Passport MRZ Scanner
-
Dynamsoft Passport MRZ Scanner recognizes the Machine-Readable Zone (MRZ) on the biographical page of a passport and converts the encoded strings into human-readable fields.
-
Scan a Passport
+ + + + Dynamsoft MRZ Scanner + + + + + + +
+ +
+
MRZ Scanner
+
+ Dynamsoft MRZ Scanner recognizes the Machine-Readable Zone (MRZ) on a passport or ID card and converts the + encoded strings into human-readable fields +
+ +
+
Powered by Dynamsoft
-
Powered by Dynamsoft
-
-
-
-
- - - - - - +
+
+
+ + + + + + + - - - down - up + + down + up +
+
+
+ music + no-music +
-
- music - no-music -
-
- - - - -
-
- -
- -
- - - + + + + + + + + +
+
+ mrz-guide +
+ +
+ + + - + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + C92.9,14.1,92.8,14.1,92.7,14.1z" + /> + + +
-
-
-
-
Passport Scan Results:
-
Restart >
-
-
-
-
-
+
+
+
Passport MRZ Scan Results
+
+
+
+
-
-
- +
+
+ + + +
-
- - - - - - \ No newline at end of file + + + + + diff --git a/js/const.js b/js/const.js new file mode 100644 index 0000000..915cfd1 --- /dev/null +++ b/js/const.js @@ -0,0 +1,51 @@ +// Define some global variables that will be used +let cameraList = []; +let cameraView = null; +let cameraEnhancer = null; +let cvRouter = null; +let pInit = null; // Promise of init +let isSoundOn = true; +let timer = null; + +const SCAN_MODES = ["id", "passport", "both"]; +const SCAN_TEMPLATES = { + id: "ReadId", + passport: "ReadPassport", + both: "ReadPassportAndId", +}; +let currentMode = SCAN_MODES[2]; // Set scan mode as "Scan Both" by default + +const resolutions = { + "Full HD": [1920, 1080], // Full HD + HD: [1280, 720], // HD +}; + +// Aspect Ratio of MRZ Guide Box +const MRZ_GUIDEBOX_ASPECT_RATIO = 6.73; + +// Get the UI element +const homePage = document.querySelector(".home-page"); + +const cameraViewContainer = document.querySelector(".camera-view-container"); + +const cameraListContainer = document.querySelector(".camera-list"); +const cameraSelector = document.querySelector(".camera-selector"); + +const scannerContainer = document.querySelector(".scanner-container"); + +const mrzGuideFrame = document.querySelector(".mrz-frame"); + +const resultContainer = document.querySelector(".result-container"); +const parsedResultArea = document.querySelector(".parsed-result-area"); + +const startScaningBtn = document.querySelector(".start-btn"); + +const scanModeContainer = document.querySelector(".scan-mode-container"); +const scanBothBtn = document.querySelector("#scan-both-btn"); + +const restartVideoBtn = document.querySelector(".btn-restart-video"); + +const playSoundBtn = document.querySelector(".music"); +const closeSoundBtn = document.querySelector(".no-music"); +const down = document.querySelector(".down"); +const up = document.querySelector(".up"); diff --git a/js/define.js b/js/define.js deleted file mode 100644 index e9781f3..0000000 --- a/js/define.js +++ /dev/null @@ -1,29 +0,0 @@ -// Define some global variables that will be used -let cameraList = []; -let cameraView = null; -let cvRouter = null; -let cameraEnhancer = null; -let promiseCVRReady = null; -let isPlaySound = true; -let timer = null; - -// Get the UI element -const homePage = document.querySelector(".home-page"); -const scannerContainer = document.querySelector(".scanner-container"); -const startScaningBtn = document.querySelector(".start-btn"); -const resultRestartBtn = document.querySelector(".result-restart"); -const passportFrame = document.querySelector(".passport-frame"); -const restartVideoBtn = document.querySelector(".btn-restart-video"); -const resultContainer = document.querySelector(".result-container"); -const cameraViewContainer = document.querySelector(".div-ui-container"); -const parsedResultArea = document.querySelector(".parsed-result-area"); -const parsedResultHeader = document.querySelector(".parsed-result-header"); -const parsedResultName = document.querySelector(".name"); -const parsedResultSexAndAge = document.querySelector(".sex-and-age"); -const parsedResultMain = document.querySelector(".parsed-result-main"); -const cameraListDiv = document.querySelector(".camera-list") -const cameraSelector = document.querySelector(".camera-selector"); -const playSoundBtn = document.querySelector(".music"); -const closeSoundBtn = document.querySelector(".no-music"); -const down = document.querySelector(".down"); -const up = document.querySelector(".up"); \ No newline at end of file diff --git a/js/index.js b/js/index.js index 6c2b1d7..f2f0023 100644 --- a/js/index.js +++ b/js/index.js @@ -1,29 +1,50 @@ -import { pDataLoad, cvrReady } from "./init.js"; -import { checkOrientation, getVisibleRegionOfVideo } from "./util.js" +import { init, pDataLoad } from "./init.js"; +import { judgeCurResolution, shouldShowScanModeContainer } from "./util.js"; +import { checkOrientation, getVisibleRegionOfVideo } from "./util.js"; -async function startCapturing() { +function startCapturing(mode) { try { - await (promiseCVRReady = promiseCVRReady || (async () => { + (async () => { homePage.style.display = "none"; scannerContainer.style.display = "block"; - // Open the camera after the model and .wasm files have loaded - await cvrReady; + // Open the camera after the model and .wasm files have loaded + pInit = pInit || (await init); await pDataLoad.promise; // Starts streaming the video - await cameraEnhancer.open(); + if (cameraEnhancer.isOpen()) { + await cvRouter.stopCapturing(); + await cameraView.clearAllInnerDrawingItems(); + } else { + await cameraEnhancer.open(); + } + + // Highlight the selected camera in the camera list container const currentCamera = cameraEnhancer.getSelectedCamera(); - for (let child of cameraListDiv.childNodes) { - if (currentCamera.deviceId === child.deviceId) { + const currentResolution = judgeCurResolution(cameraEnhancer.getResolution()); + cameraListContainer.childNodes.forEach((child) => { + if (currentCamera.deviceId === child.deviceId && currentResolution === child.resolution) { child.className = "camera-item camera-selected"; } - } - passportFrame.style.display = "inline-block"; + }); cameraEnhancer.setScanRegion(region()); cameraView.setScanRegionMaskVisible(false); - await cvRouter.startCapturing("ReadPassport"); - })()); + + // Show MRZ guide frame + mrzGuideFrame.style.display = "inline-block"; + + await cvRouter.startCapturing(SCAN_TEMPLATES[mode]); + + // Update button styles to show selected scan mode + document.querySelectorAll(".scan-option-btn").forEach((button) => { + button.classList.remove("selected"); + }); + document.querySelector(`#scan-${mode}-btn`).classList.add("selected"); + + currentMode = mode; + scanModeContainer.style.display = "flex"; + })(); } catch (ex) { let errMsg = ex.message || ex; console.error(errMsg); @@ -31,60 +52,55 @@ async function startCapturing() { } } -// -----------Logic for calculating scan region ↓------------ -const regionEdgeLength = () => { - if (!cameraEnhancer || !cameraEnhancer.isOpen()) return 0; - const visibleRegionInPixels = getVisibleRegionOfVideo(); - const visibleRegionWidth = visibleRegionInPixels.width; - const visibleRegionHeight = visibleRegionInPixels.height; - const regionEdgeLength = 0.4 * Math.min(visibleRegionWidth, visibleRegionHeight); - return Math.round(regionEdgeLength); -}; +SCAN_MODES.forEach((mode) => + document.querySelector(`#scan-${mode}-btn`).addEventListener("click", () => startCapturing(mode)) +); +// -----------Logic for calculating scan region ↓------------ const regionLeft = () => { if (!cameraEnhancer || !cameraEnhancer.isOpen()) return 0; const visibleRegionInPixels = getVisibleRegionOfVideo(); const currentResolution = cameraEnhancer.getResolution(); - let vw = currentResolution.width; - if (checkOrientation() === "portrait") { - vw = Math.min(currentResolution.width, currentResolution.height); - } else { - vw = Math.max(currentResolution.width, currentResolution.height); - } + + const vw = + checkOrientation() === "portrait" + ? Math.min(currentResolution.width, currentResolution.height) + : Math.max(currentResolution.width, currentResolution.height); const visibleRegionWidth = visibleRegionInPixels.width; - let left = 0.5 - regionEdgeLength() / vw / 2; + let regionCssW; - if (document.body.clientWidth > document.body.clientHeight * 6.73) { + if (document.body.clientWidth > document.body.clientHeight * MRZ_GUIDEBOX_ASPECT_RATIO) { let regionCssH = document.body.clientHeight * 0.75; - regionCssW = regionCssH * 6.73; + regionCssW = regionCssH * MRZ_GUIDEBOX_ASPECT_RATIO; } else { regionCssW = document.body.clientWidth * 0.9; } regionCssW = Math.min(regionCssW, 600); + const regionWidthInPixel = (visibleRegionWidth / document.body.clientWidth) * regionCssW; - left = ((vw - regionWidthInPixel) / 2 / vw) * 100; - left = Math.round(left); - return left; + const left = ((vw - regionWidthInPixel) / 2 / vw) * 100; + + return Math.round(left); }; const regionTop = () => { if (!cameraEnhancer || !cameraEnhancer.isOpen()) return 0; + const currentResolution = cameraEnhancer.getResolution(); - let vw = currentResolution.width; - let vh = currentResolution.height; - if (checkOrientation() === "portrait") { - vw = Math.min(currentResolution.width, currentResolution.height); - vh = Math.max(currentResolution.width, currentResolution.height); - } else { - vw = Math.max(currentResolution.width, currentResolution.height); - vh = Math.min(currentResolution.width, currentResolution.height); - } - let top = 0.5 - regionEdgeLength() / vh / 2; + + const vw = + checkOrientation() === "portrait" + ? Math.min(currentResolution.width, currentResolution.height) + : Math.max(currentResolution.width, currentResolution.height); + const vh = + checkOrientation() === "portrait" + ? Math.max(currentResolution.width, currentResolution.height) + : Math.min(currentResolution.width, currentResolution.height); + const regionWidthInPixel = vw - (regionLeft() * 2 * vw) / 100; const regionHeightInPixel = regionWidthInPixel / 4; - top = ((vh - regionHeightInPixel) / 2 / vh) * 100; - top = Math.round(top); - return top; + const top = ((vh - regionHeightInPixel) / 2 / vh) * 100; + return Math.round(top); }; const region = () => { @@ -96,52 +112,51 @@ const region = () => { isMeasuredInPercentage: true, }; return region; -} +}; // -----------Logic for calculating scan region ↑------------ -const restartVideo = async () => { - resultContainer.style.display = "none"; - await cvRouter.startCapturing("ReadPassport"); -} - window.addEventListener("click", () => { - cameraListDiv.style.display = "none"; + cameraListContainer.style.display = "none"; up.style.display = "none"; down.style.display = "inline-block"; -}) +}); // Recalculate the scan region when the window size changes window.addEventListener("resize", () => { - passportFrame.style.display = "none"; + mrzGuideFrame.style.display = "none"; timer && clearTimeout(timer); timer = setTimeout(() => { - passportFrame.style.display = "inline-block"; + shouldShowScanModeContainer(); + mrzGuideFrame.style.display = "inline-block"; cameraEnhancer.setScanRegion(region()); cameraView.setScanRegionMaskVisible(false); }, 500); -}) +}); // Add click events to buttons -startScaningBtn.addEventListener("click", startCapturing); +startScaningBtn.addEventListener("click", () => scanBothBtn.click()); +const restartVideo = async () => { + resultContainer.style.display = "none"; + document.querySelector(`#scan-${currentMode}-btn`).click(); +}; restartVideoBtn.addEventListener("click", restartVideo); -resultRestartBtn.addEventListener("click", restartVideo); cameraSelector.addEventListener("click", (e) => { e.stopPropagation(); - const isShow = cameraListDiv.style.display === "block"; - cameraListDiv.style.display = isShow ? "none" : "block"; + const isShow = cameraListContainer.style.display === "block"; + cameraListContainer.style.display = isShow ? "none" : "block"; up.style.display = !isShow ? "inline-block" : "none"; down.style.display = isShow ? "inline-block" : "none"; -}) +}); playSoundBtn.addEventListener("click", () => { playSoundBtn.style.display = "none"; closeSoundBtn.style.display = "block"; - isPlaySound = false; -}) + isSoundOn = false; +}); closeSoundBtn.addEventListener("click", () => { playSoundBtn.style.display = "block"; closeSoundBtn.style.display = "none"; - isPlaySound = true; -}) \ No newline at end of file + isSoundOn = true; +}); diff --git a/js/init.js b/js/init.js index 4508f10..271301e 100644 --- a/js/init.js +++ b/js/init.js @@ -1,25 +1,25 @@ - -import { createPendingPromise, getNeedShowFields } from "./util.js"; +import { judgeCurResolution } from "./util.js"; +import { createPendingPromise, extractDocumentFields, resultToHTMLElement, formatMRZ } from "./util.js"; // Promise variable used to control model loading state -let pDataLoad = createPendingPromise(); +const pDataLoad = createPendingPromise(); /** LICENSE ALERT - README * To use the library, you need to first specify a license key using the API "initLicense" as shown below. */ Dynamsoft.License.LicenseManager.initLicense("DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9"); -/** - * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=passport&utm_source=docs&package=js to get your own trial license good for 30 days. +/** + * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=passport&utm_source=docs&package=js 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/label-recognition/programming/javascript/user-guide.html?ver=latest#specify-the-license or contact support@dynamsoft.com. - * LICENSE ALERT - THE END + * LICENSE ALERT - THE END */ Dynamsoft.DLR.LabelRecognizerModule.onDataLoadProgressChanged = (modelPath, tag, progress) => { if (tag === "completed") { pDataLoad.resolve(); } -} +}; /** * Preloads the resources @@ -38,29 +38,50 @@ async function initDCE() { // Get the camera information of the device and render the camera list cameraList = await cameraEnhancer.getAllCameras(); for (let camera of cameraList) { - const cameraItem = document.createElement("div"); - cameraItem.className = "camera-item"; - cameraItem.innerText = camera.label; - cameraItem.deviceId = camera.deviceId; - // - cameraItem.addEventListener("click", (e) => { - e.stopPropagation(); - for (let child of cameraListDiv.childNodes) { - child.className = "camera-item"; - } - cameraItem.className = "camera-item camera-selected"; - cameraEnhancer.selectCamera(camera); - }) - cameraListDiv.appendChild(cameraItem); + for (let res of Object.keys(resolutions)) { + const cameraItem = document.createElement("div"); + cameraItem.className = "camera-item"; + cameraItem.innerText = `${camera.label} (${res})`; + cameraItem.deviceId = camera.deviceId; + cameraItem.resolution = res; + + cameraItem.addEventListener("click", async (e) => { + e.stopPropagation(); + for (let child of cameraListContainer.childNodes) { + child.className = "camera-item"; + } + cameraItem.className = "camera-item camera-selected"; + await cameraEnhancer.selectCamera(camera); + await cameraEnhancer.setResolution({ + width: resolutions[res][0], + height: resolutions[res][1], + }); + + const currentCamera = await cameraEnhancer.getSelectedCamera(); + const currentResolution = judgeCurResolution(await cameraEnhancer.getResolution()); + if (judgeCurResolution(currentResolution) !== res) { + // Update resolution to the current resolution that is supported + for (let child of cameraListContainer.childNodes) { + child.className = "camera-item"; + if (currentCamera.deviceId === child.deviceId && currentResolution === child.resolution) { + child.className = "camera-item camera-selected"; + } + } + } + // Hide options after user clicks an option + cameraSelector.click(); + }); + cameraListContainer.appendChild(cameraItem); + } } cameraView.setVideoFit("cover"); await cameraEnhancer.setResolution({ width: 1920, height: 1080 }); } /** - * Creates a CaptureVisionRouter instance and configure the task setting. + * Initialize CaptureVisionRouter, CameraEnhancer, and CameraView instance */ -let cvrReady = (async function initCVR() { +let init = (async () => { await initDCE(); cvRouter = await Dynamsoft.CVR.CaptureVisionRouter.createInstance(); await cvRouter.initSettings("./template.json"); @@ -72,39 +93,32 @@ let cvrReady = (async function initCVR() { const recognizedResults = result.textLineResultItems; const parsedResults = result.parsedResultItems; - if (recognizedResults) { - parsedResultName.innerText = ""; - parsedResultSexAndAge.innerText = ""; - parsedResultMain.innerText = ""; + if (recognizedResults?.length) { + // Play sound feedback if enabled + isSoundOn ? Dynamsoft.DCE.Feedback.beep() : null; + + parsedResultArea.innerText = ""; + + // Add MRZ Text to Result + const mrzElement = resultToHTMLElement("MRZ String", formatMRZ(recognizedResults[0]?.text)); + mrzElement.classList.add("code"); + parsedResultArea.appendChild(mrzElement); + // If a parsed result is obtained, use it to render the result page if (parsedResults) { - const parseResultInfo = getNeedShowFields(parsedResults[0]); - parsedResultName.innerText = parseResultInfo["Name"] || "Name not detected"; - const sex = parseResultInfo["Gender"] || "Sex not detected"; - const age = parseResultInfo["Age"] || "Age not detected"; - parsedResultSexAndAge.innerText = sex + ", Age: " + age; - - for (let field in parseResultInfo) { - if(["Name", "Gender", "Age"].includes(field)) continue; - const p = document.createElement("p"); - p.className = "parsed-filed"; - const spanFieldName = document.createElement("span"); - spanFieldName.className = "field-name"; - const spanValue = document.createElement("span"); - spanValue.className = "field-value"; - spanFieldName.innerText = `${field} : `; - spanValue.innerText = `${parseResultInfo[field] || 'not detected'}`; - p.appendChild(spanFieldName); - p.appendChild(spanValue); - parsedResultMain.appendChild(p); - } + const parseResultInfo = extractDocumentFields(parsedResults[0]); + Object.entries(parseResultInfo).map(([field, value]) => { + const resultElement = resultToHTMLElement(field, value); + parsedResultArea.appendChild(resultElement); + }); } else { - alert(`Failed to parse the content. The MRZ text ${needShowTextLines}.`); + alert(`Failed to parse the content.`); parsedResultArea.style.justifyContent = "flex-start"; } - isPlaySound ? Dynamsoft.DCE.Feedback.beep() : null; resultContainer.style.display = "flex"; - cameraListDiv.style.display = "none"; + cameraListContainer.style.display = "none"; + scanModeContainer.style.display = "none"; + cvRouter.stopCapturing(); cameraView.clearAllInnerDrawingItems(); } @@ -112,4 +126,4 @@ let cvrReady = (async function initCVR() { await cvRouter.addResultReceiver(resultReceiver); })(); -export { pDataLoad, cvrReady } +export { pDataLoad, init }; diff --git a/js/util.js b/js/util.js index bd3e6f2..8dee534 100644 --- a/js/util.js +++ b/js/util.js @@ -1,3 +1,8 @@ +/** + * Creates a pending promise. Used to keep track of library loading progress + * + * @returns {Object} An object containing the promise, resolve, and reject functions. + */ export function createPendingPromise() { let resolve, reject; const promise = new Promise((res, rej) => { @@ -8,101 +13,76 @@ export function createPendingPromise() { return { promise, resolve, reject }; } -export function getNeedShowFields(result) { +/** + * Extracts and returns document fields from the parsed MRZ result + * + * @param {Object} result - The parsed result object containing document fields. + * @returns {Object} An object with key-value pairs of the extracted fields. + */ +export function extractDocumentFields(result) { const parseResultInfo = {}; if (!result.exception) { - let name = result.getFieldValue("name"); - parseResultInfo['Name'] = name; - - let gender = result.getFieldValue("sex"); - parseResultInfo["Gender"] = gender; - - let birthYear = result.getFieldValue("birthYear"); - let birthMonth = result.getFieldValue("birthMonth"); - let birthDay = result.getFieldValue("birthDay"); - if (parseInt(birthYear) > (new Date().getFullYear() % 100)) { - birthYear = "19" + birthYear; - } else { - birthYear = "20" + birthYear; - } - if(isNaN(parseInt(birthYear))) { - parseResultInfo["Age"] = undefined; - } else { - let age = new Date().getUTCFullYear() - parseInt(birthYear); - parseResultInfo["Age"] = age; - } - let documentNumber = result.getFieldValue("passportNumber"); - parseResultInfo['Document Number'] = documentNumber; - - let issuingState = result.getFieldValue("issuingState"); - parseResultInfo['Issuing State'] = issuingState; - - let nationality = result.getFieldValue("nationality"); - parseResultInfo['Nationality'] = nationality; - - parseResultInfo['Date of Birth (YYYY-MM-DD)'] = birthYear + "-" + birthMonth + "-" + birthDay; - - let expiryYear = result.getFieldValue("expiryYear"); - let expiryMonth = result.getFieldValue("expiryMonth"); - let expiryDay = result.getFieldValue("expiryDay"); - if (parseInt(expiryYear) >= 60) { - expiryYear = "19" + expiryYear; - } else { - expiryYear = "20" + expiryYear; - } - parseResultInfo["Date of Expiry (YYYY-MM-DD)"] = expiryYear + "-" + expiryMonth + "-" + expiryDay; - - let personalNumber = result.getFieldValue("personalNumber"); - parseResultInfo["Personal Number"] = personalNumber; - - let primaryIdentifier = result.getFieldValue("primaryIdentifier"); - parseResultInfo["Primary Identifier(s)"] = primaryIdentifier; - - let secondaryIdentifier = result.getFieldValue("secondaryIdentifier"); - parseResultInfo["Secondary Identifier(s)"] = secondaryIdentifier; + const type = result.getFieldValue("documentCode"); + const documentType = JSON.parse(result.jsonString).CodeType; + const birthYear = result.getFieldValue("birthYear"); + const birthYearBase = parseInt(birthYear) > new Date().getFullYear() % 100 ? "19" : "20"; + const fullBirthYear = `${birthYearBase}${birthYear}`; + + const expiryYear = result.getFieldValue("expiryYear"); + const expiryYearBase = parseInt(expiryYear) >= 60 ? "19" : "20"; + const fullExpiryYear = `${expiryYearBase}${expiryYear}`; + + parseResultInfo["Document Type"] = documentType; + parseResultInfo["Issuing State"] = result.getFieldValue("issuingState"); + parseResultInfo["Surname"] = result.getFieldValue("primaryIdentifier"); + parseResultInfo["Given Name"] = result.getFieldValue("secondaryIdentifier"); + parseResultInfo["Document Number"] = + type === "P" ? result.getFieldValue("passportNumber") : result.getFieldValue("documentNumber"); + parseResultInfo["Nationality"] = result.getFieldValue("nationality"); + parseResultInfo["Sex"] = result.getFieldValue("sex"); + parseResultInfo["Date of Birth (YYYY-MM-DD)"] = + fullBirthYear + "-" + result.getFieldValue("birthMonth") + "-" + result.getFieldValue("birthDay"); + parseResultInfo["Date of Expiry (YYYY-MM-DD)"] = + fullExpiryYear + "-" + result.getFieldValue("expiryMonth") + "-" + result.getFieldValue("expiryDay"); } return parseResultInfo; } +/** + * Checks and returns the current screen orientation. + * + * @returns {string} The current screen orientation ('portrait' or 'landscape'). + */ export function checkOrientation() { if (window.matchMedia("(orientation: portrait)").matches) { - return 'portrait'; + return "portrait"; } else if (window.matchMedia("(orientation: landscape)").matches) { - return 'landscape'; + return "landscape"; } } export function getVisibleRegionOfVideo() { - if(!cameraView || !cameraView.getVideoElement()) return; + if (!cameraView || !cameraView.getVideoElement()) return; const video = cameraView.getVideoElement(); - let width = video.videoWidth; - let height = video.videoHeight; - let objectFit = cameraView.getVideoFit(); + const { videoWidth, videoHeight } = video; + const objectFit = cameraView.getVideoFit(); + // Adjust dimensions based on orientation const isPortrait = checkOrientation() === "portrait"; - let _width = width; - let _height = height; - if (isPortrait) { - _width = Math.min(width, height); - _height = Math.max(width, height); - } else { - _width = Math.max(width, height); - _height = Math.min(width, height); - } - width = _width; - height = _height; + const width = isPortrait ? Math.min(videoWidth, videoHeight) : Math.max(videoWidth, videoHeight); + const height = isPortrait ? Math.max(videoWidth, videoHeight) : Math.min(videoWidth, videoHeight); - const { width: videoCSSWidth, height: videoCSSHeight } = - cameraView._innerComponent.getBoundingClientRect(); + // Get the CSS dimensions of the video element + const { width: videoCSSWidth, height: videoCSSHeight } = cameraView._innerComponent.getBoundingClientRect(); if (videoCSSWidth <= 0 || videoCSSHeight <= 0) { - throw new Error( - `Unable to get video dimensions. Video may not be rendered on the page.` - ); + throw new Error(`Unable to get video dimensions. Video may not be rendered on the page.`); } const videoCSSWHRatio = videoCSSWidth / videoCSSHeight, videoWHRatio = width / height; let cssScaleRatio; + + // Set visible region in pixels const regionInPixels = { x: 0, y: 0, @@ -115,9 +95,7 @@ export function getVisibleRegionOfVideo() { if (videoCSSWHRatio < videoWHRatio) { // a part of length is invisible cssScaleRatio = videoCSSHeight / height; - regionInPixels.x = Math.floor( - (width - videoCSSWidth / cssScaleRatio) / 2 - ); + regionInPixels.x = Math.floor((width - videoCSSWidth / cssScaleRatio) / 2); regionInPixels.y = 0; regionInPixels.width = Math.ceil(videoCSSWidth / cssScaleRatio); regionInPixels.height = height; @@ -125,12 +103,84 @@ export function getVisibleRegionOfVideo() { // a part of height is invisible cssScaleRatio = videoCSSWidth / width; regionInPixels.x = 0; - regionInPixels.y = Math.floor( - (height - videoCSSHeight / cssScaleRatio) / 2 - ); + regionInPixels.y = Math.floor((height - videoCSSHeight / cssScaleRatio) / 2); regionInPixels.width = width; regionInPixels.height = Math.ceil(videoCSSHeight / cssScaleRatio); } } return regionInPixels; -} \ No newline at end of file +} + +/** + * Create an HTML paragraph element containing the document field name and value. + * + * @param {string} field - The document field name. + * @param {string} value - The document field value. + * @returns {HTMLElement} The paragraph element containing the formatted document field name and value. + */ +export function resultToHTMLElement(field, value) { + const p = document.createElement("p"); + p.className = "parsed-filed"; + const spanFieldName = document.createElement("span"); + spanFieldName.className = "field-name"; + const spanValue = document.createElement("span"); + spanValue.className = "field-value"; + + spanFieldName.innerText = `${field} : `; + spanValue.innerText = `${value || "Not detected"}`; + + p.appendChild(spanFieldName); + p.appendChild(spanValue); + + return p; +} + +/** + * Formats a Machine Readable Zone (MRZ) string by adding line breaks based on its length. + * + * @param {string} [mrzString=""] - The MRZ string to format. + * @returns {string} The formatted MRZ string with appropriate line breaks or the original string + */ +export function formatMRZ(mrzString = "") { + let formattedMRZ = mrzString; + + // Check if the length matches any known MRZ format + if (mrzString.length === 88) { + // Passport (TD3 format) + formattedMRZ = mrzString.slice(0, 44) + "\n" + mrzString.slice(44); + } else if (mrzString.length === 90) { + // ID card (TD1 format) + formattedMRZ = mrzString.slice(0, 30) + "\n" + mrzString.slice(30, 60) + "\n" + mrzString.slice(60); + } else if (mrzString.length === 72) { + // Visa (TD2 format) + formattedMRZ = mrzString.slice(0, 36) + "\n" + mrzString.slice(36); + } + + return formattedMRZ; +} + +/** Check if current resolution is Full HD or HD + * @params {Object} currentResolution - an object with `width` and `height` to represent the current resolution of the camera + * @returns {string} Either "HD" or "Full HD" depending of the resolution of the screen + */ +export const judgeCurResolution = (currentResolution) => { + const { width, height } = currentResolution; + const minValue = Math.min(width, height); + const maxValue = Math.max(width, height); + + if (minValue > 480 && minValue < 960 && maxValue > 960 && maxValue < 1440) { + return "HD"; + } else if (minValue > 900 && minValue < 1440 && maxValue > 1400 && maxValue < 2160) { + return "Full HD"; + } +}; + +/** + * Checks if we should show the switch scan mode buttons + * @returns true if cameraEnhancer is open, false otherwise + */ +export function shouldShowScanModeContainer() { + const isHomepageClosed = homePage.style.display === "none"; + const isResultClosed = resultContainer.style.display === "none" || resultContainer.style.display === ""; + scanModeContainer.style.display = isHomepageClosed && isResultClosed ? "flex" : "none"; +} diff --git a/template.json b/template.json index 54a174d..2704d9c 100644 --- a/template.json +++ b/template.json @@ -1,79 +1,226 @@ { - "CaptureVisionTemplates" : - [ + "CaptureVisionTemplates": [ { - "Name" : "ReadPassport", - "ImageROIProcessingNameArray" : - [ - "ROI_OriginalImage" + "Name": "ReadPassportAndId", + "OutputOriginalImage": 0, + "ImageROIProcessingNameArray": [ + "roi-passport-and-id" ], - "SemanticProcessingNameArray": [ "SP_Passport" ], - "OutputOriginalImage": 1 + "SemanticProcessingNameArray": ["sp-passport-and-id"], + "Timeout": 2000 + }, + { + "Name": "ReadPassport", + "OutputOriginalImage": 0, + "ImageROIProcessingNameArray": [ + "roi-passport" + ], + "SemanticProcessingNameArray": ["sp-passport"], + "Timeout": 2000 + }, + { + "Name": "ReadId", + "OutputOriginalImage": 0, + "ImageROIProcessingNameArray": [ + "roi-id" + ], + "SemanticProcessingNameArray": ["sp-id"], + "Timeout": 2000 } ], - "LabelRecognizerTaskSettingOptions": [ + "TargetROIDefOptions": [ { - "Name": "Task_RecognizeMRZonPassport", - "TextLineSpecificationNameArray": [ - "TLS_Passport" - ], - "SectionImageParameterArray": [ - { - "Section": "ST_TEXT_LINE_LOCALIZATION", - "ImageParameterName": "IP_RecognizePassport" - } + "Name": "roi-passport-and-id", + "TaskSettingNameArray": [ + "task-passport-and-id" + ] + }, + { + "Name": "roi-passport", + "TaskSettingNameArray": [ + "task-passport" + ] + }, + { + "Name": "roi-id", + "TaskSettingNameArray": [ + "task-id" ] } ], "TextLineSpecificationOptions": [ { - "Name": "TLS_Template", + "Name": "tls-mrz-passport", + "BaseTextLineSpecificationName": "tls-base", + "StringLengthRange": [ 44, 44 ], + "OutputResults": 1, + "ExpectedGroupsCount": 1, + "ConcatResults": 1, + "ConcatSeparator": "", + "SubGroups": [ + { + "StringRegExPattern": "(P[A-Z<][A-Z<]{3}[A-Z<]{39}){(44)}", + "StringLengthRange": [ 44, 44 ], + "BaseTextLineSpecificationName": "tls-base" + }, + { + "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{14}[0-9<][0-9]){(44)}", + "StringLengthRange": [ 44, 44 ], + "BaseTextLineSpecificationName": "tls-base" + } + ] + }, + { + "Name": "tls-mrz-id-td2", + "BaseTextLineSpecificationName": "tls-base", + "StringLengthRange": [ 36, 36 ], + "OutputResults": 1, + "ExpectedGroupsCount": 1, + "ConcatResults": 1, + "ConcatSeparator": "", + "SubGroups": [ + { + "StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z<]{31}){(36)}", + "StringLengthRange": [ 36, 36 ], + "BaseTextLineSpecificationName": "tls-base" + }, + { + "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{8}){(36)}", + "StringLengthRange": [ 36, 36 ], + "BaseTextLineSpecificationName": "tls-base" + } + ] + }, + { + "Name": "tls-mrz-id-td1", + "BaseTextLineSpecificationName": "tls-base", + "StringLengthRange": [ 30, 30 ], + "OutputResults": 1, + "ExpectedGroupsCount": 1, + "ConcatResults": 1, + "ConcatSeparator": "", + "SubGroups": [ + { + "StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z0-9<]{9}[0-9][A-Z0-9<]{15}){(30)}", + "StringLengthRange": [ 30, 30 ], + "BaseTextLineSpecificationName": "tls-base" + }, + { + "StringRegExPattern": "([0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z<]{3}[A-Z0-9<]{11}[0-9]){(30)}", + "StringLengthRange": [ 30, 30 ], + "BaseTextLineSpecificationName": "tls-base" + }, + { + "StringRegExPattern": "([A-Z<]{30}){(30)}", + "StringLengthRange": [ 30, 30 ], + "BaseTextLineSpecificationName": "tls-base" + } + ] + }, + { + "Name": "tls-base", "CharacterModelName": "MRZ", "CharHeightRange": [ 5, 1000, 1 ], - "ConfusableCharactersCorrection":{ - "ConfusableCharacters":[["0","O"],["1","I"],["5","S"]], - "FontNameArray":["OCR_B"] - }, "BinarizationModes": [ { "BlockSizeX": 30, "BlockSizeY": 30, "Mode": "BM_LOCAL_BLOCK", - "MorphOperation": "Close" + "EnableFillBinaryVacancy": 0, + "ThresholdCompensation": 15 + } + ], + "ConfusableCharactersCorrection": { + "ConfusableCharacters": [ + [ "0", "O" ], + [ "1", "I" ], + [ "5", "S" ] + ], + "FontNameArray": [ "OCR_B" ] + } + } + ], + "LabelRecognizerTaskSettingOptions": [ + { + "Name": "task-passport", + "ConfusableCharactersPath": "ConfusableChars.data", + "TextLineSpecificationNameArray": ["tls-mrz-passport"], + "SectionImageParameterArray": [ + { + "Section": "ST_REGION_PREDETECTION", + "ImageParameterName": "ip-mrz" + }, + { + "Section": "ST_TEXT_LINE_LOCALIZATION", + "ImageParameterName": "ip-mrz" + }, + { + "Section": "ST_TEXT_LINE_RECOGNITION", + "ImageParameterName": "ip-mrz" } ] }, { - "Name": "TLS_Passport", - "BaseTextLineSpecificationName": "TLS_Template", - "OutputResults": 1, - "ConcatResults": 1, - "SubGroups": [ + "Name": "task-id", + "ConfusableCharactersPath": "ConfusableChars.data", + "TextLineSpecificationNameArray": ["tls-mrz-id-td1", "tls-mrz-id-td2"], + "SectionImageParameterArray": [ { - "StringRegExPattern": "(P[A-Z<][A-Z<]{3}[A-Z<]{39}){(44)}", - "StringLengthRange": [ 44, 44 ], - "BaseTextLineSpecificationName": "TLS_Template", - "TextLinesCount": 1 + "Section": "ST_REGION_PREDETECTION", + "ImageParameterName": "ip-mrz" }, { - "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{14}[0-9<][0-9]){(44)}", - "StringLengthRange": [ 44, 44 ], - "BaseTextLineSpecificationName": "TLS_Template", - "TextLinesCount": 1 + "Section": "ST_TEXT_LINE_LOCALIZATION", + "ImageParameterName": "ip-mrz" + }, + { + "Section": "ST_TEXT_LINE_RECOGNITION", + "ImageParameterName": "ip-mrz" } ] + }, + { + "Name": "task-passport-and-id", + "ConfusableCharactersPath": "ConfusableChars.data", + "TextLineSpecificationNameArray": ["tls-mrz-passport", "tls-mrz-id-td1", "tls-mrz-id-td2"], + "SectionImageParameterArray": [ + { + "Section": "ST_REGION_PREDETECTION", + "ImageParameterName": "ip-mrz" + }, + { + "Section": "ST_TEXT_LINE_LOCALIZATION", + "ImageParameterName": "ip-mrz" + }, + { + "Section": "ST_TEXT_LINE_RECOGNITION", + "ImageParameterName": "ip-mrz" + } + ] + } + ], + "CharacterModelOptions": [ + { + "DirectoryPath": "", + "Name": "MRZ" } ], - "ImageParameterOptions" : - [ + "ImageParameterOptions": [ { - "Name" : "IP_RecognizePassport", + "Name": "ip-mrz", "TextureDetectionModes": [ { "Mode": "TDM_GENERAL_WIDTH_CONCENTRATION", "Sensitivity": 8 } ], + "BinarizationModes": [ + { + "EnableFillBinaryVacancy": 0, + "ThresholdCompensation": 21, + "Mode": "BM_LOCAL_BLOCK" + } + ], "TextDetectionMode": { "Mode": "TTDM_LINE", "CharHeightRange": [ 5, 1000, 1 ], @@ -82,41 +229,53 @@ } } ], - "TargetROIDefOptions" : - [ + "SemanticProcessingOptions": [ { - "Name" : "ROI_OriginalImage", - "TaskSettingNameArray" : - [ - "Task_RecognizeMRZonPassport" + "Name": "sp-passport-and-id", + "ReferenceObjectFilter": { + "ReferenceTargetROIDefNameArray": [ + "roi-passport-and-id" + ] + }, + "TaskSettingNameArray": [ + "dcp-passport-and-id" ] - } - ], - "CharacterModelOptions": [ + }, { - "Name" : "MRZ" - } - ], - "SemanticProcessingOptions": [ + "Name": "sp-passport", + "ReferenceObjectFilter": { + "ReferenceTargetROIDefNameArray": [ + "roi-passport" + ] + }, + "TaskSettingNameArray": [ + "dcp-passport" + ] + }, { - "Name": "SP_Passport", + "Name": "sp-id", "ReferenceObjectFilter": { - "ReferenceTargetROIDefNameArray": ["ROI_OriginalImage"], - "AtomicResultTypeArray" : ["ART_TEXT_LINE"] + "ReferenceTargetROIDefNameArray": [ + "roi-id" + ] }, "TaskSettingNameArray": [ - "ParsePassport" + "dcp-id" ] } ], "CodeParserTaskSettingOptions": [ { - "Name": "ParsePassport", - "CodeSpecifications": ["MRTD_TD3_PASSPORT"] + "Name": "dcp-passport", + "CodeSpecifications": [ "MRTD_TD3_PASSPORT" ] + }, + { + "Name": "dcp-id", + "CodeSpecifications": [ "MRTD_TD1_ID", "MRTD_TD2_ID" ] + }, + { + "Name": "dcp-passport-and-id", + "CodeSpecifications": [ "MRTD_TD3_PASSPORT", "MRTD_TD1_ID", "MRTD_TD2_ID" ] } - ], - "GlobalParameter" : - { - "MaxTotalImageDimension" : 0 - } + ] } \ No newline at end of file From aa3a38ac60de2cf2d0bd2ffccf8547956c237c12 Mon Sep 17 00:00:00 2001 From: felixindrawan Date: Wed, 28 Aug 2024 09:30:33 -0700 Subject: [PATCH 02/12] fix: show guidebox after cvRouter starts capturing --- js/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/index.js b/js/index.js index f2f0023..919a251 100644 --- a/js/index.js +++ b/js/index.js @@ -31,11 +31,11 @@ function startCapturing(mode) { cameraEnhancer.setScanRegion(region()); cameraView.setScanRegionMaskVisible(false); + await cvRouter.startCapturing(SCAN_TEMPLATES[mode]); + // Show MRZ guide frame mrzGuideFrame.style.display = "inline-block"; - await cvRouter.startCapturing(SCAN_TEMPLATES[mode]); - // Update button styles to show selected scan mode document.querySelectorAll(".scan-option-btn").forEach((button) => { button.classList.remove("selected"); From 5f2175662d48097ce55597b91074105f8b1131b1 Mon Sep 17 00:00:00 2001 From: Tom Kent Date: Tue, 3 Sep 2024 15:39:22 -0700 Subject: [PATCH 03/12] fix: remove visa mrz from description in meta, renamed an image name to lower-case --- index.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index a590649..9a8dc7a 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,9 @@ Dynamsoft MRZ Scanner - - + + From f65c112f51d33f1a43621f378a65ea2a625405cd Mon Sep 17 00:00:00 2001 From: Tom Kent Date: Tue, 3 Sep 2024 20:28:29 -0700 Subject: [PATCH 04/12] fix: passport => mrz to be more accurate --- README.md | 10 +++++----- assets/mrz-guide-box.svg | 2 +- css/index.css | 1 + index.html | 2 +- js/init.js | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2af013a..d5c79ea 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ # About the solution -General MRZ Scanner enables camera to scan the MRZ code of ID-cards, passports, and visas. Currently, the General MRZ Scanner supports TD-1, TD-2, TD-3, MRV-A, and MRV-B standards. It will extract all data like first name, last name, document number, nationality, date of birth, expiration date and more from the MRZ string, and converts the encoded string into human-readable fields. +The MRZ Scanner enables camera to scan the MRZ code of ID-cards and passports. It will extract all data like first name, last name, document number, nationality, date of birth, expiration date and more from the MRZ string, and converts the encoded string into human-readable fields. ## Web demo -The web demo is available at [https://demo.dynamsoft.com/solutions/passport-scanner/index.html](https://demo.dynamsoft.com/solutions/passport-scanner/index.html) (nothing will be uploaded). +The web demo is available at [https://demo.dynamsoft.com/solutions/mrz-scanner/index.html](https://demo.dynamsoft.com/solutions/mrz-scanner/index.html) (nothing will be uploaded). ## Run this Solution 1. Clone the repo to a working directory ```sh -git clone https://github.com/Dynamsoft/passport-MRZ-scanner-javascript +git clone https://github.com/Dynamsoft/MRZ-scanner-javascript ``` 2. CD to the folder and run an https server ```sh -cd passport-MRZ-scanner-javascript +cd MRZ-scanner-javascript ``` > Basic Requirements @@ -30,7 +30,7 @@ cd passport-MRZ-scanner-javascript ## Request a Trial License -The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution (in the file `js/init.js`) serves as a test license valid for 24 hours, applicable to any newly authorized browser. To test the SDK further, you can request a 30-day free trial license via the Request a Trial License link. +The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution (in the file `js/init.js`) serves as a test license valid for 24 hours, applicable to any newly authorized browser. To test the SDK further, you can request a 30-day free trial license via the Request a Trial License link. ## Project Structure diff --git a/assets/mrz-guide-box.svg b/assets/mrz-guide-box.svg index 06cdc9c..c5702ef 100644 --- a/assets/mrz-guide-box.svg +++ b/assets/mrz-guide-box.svg @@ -7,7 +7,7 @@ .st1{enable-background:new ;} .st2{fill:#AAAAAA;} - + diff --git a/css/index.css b/css/index.css index e6f065c..878a568 100644 --- a/css/index.css +++ b/css/index.css @@ -266,6 +266,7 @@ img { height: 10%; min-height: 60px; max-height: 100px; + background-color: #2b2b2b; display: flex; justify-content: center; align-items: center; diff --git a/index.html b/index.html index 9a8dc7a..9fa3400 100644 --- a/index.html +++ b/index.html @@ -259,7 +259,7 @@
-
Passport MRZ Scan Results
+
Scan Results
diff --git a/js/init.js b/js/init.js index 271301e..fd3af65 100644 --- a/js/init.js +++ b/js/init.js @@ -9,7 +9,7 @@ const pDataLoad = createPendingPromise(); */ Dynamsoft.License.LicenseManager.initLicense("DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9"); /** - * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=passport&utm_source=docs&package=js to get your own trial license good for 30 days. + * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mrz&utm_source=docs&package=js 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/label-recognition/programming/javascript/user-guide.html?ver=latest#specify-the-license or contact support@dynamsoft.com. * LICENSE ALERT - THE END From 6a3dc13272c2b259ee813f7508ca7d5725b3250c Mon Sep 17 00:00:00 2001 From: Tom Kent Date: Tue, 3 Sep 2024 20:59:16 -0700 Subject: [PATCH 05/12] feat: preload all data files for TD1/2/3 --- js/init.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/init.js b/js/init.js index fd3af65..7af1525 100644 --- a/js/init.js +++ b/js/init.js @@ -26,6 +26,8 @@ Dynamsoft.DLR.LabelRecognizerModule.onDataLoadProgressChanged = (modelPath, tag, */ Dynamsoft.Core.CoreModule.loadWasm(["DLR", "DCP"]); Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD3_PASSPORT"); +Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD1_ID"); +Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_ID"); Dynamsoft.DLR.LabelRecognizerModule.loadRecognitionData("MRZ"); /** From 159a1fd50d72929dafb28bee0082876c6144b39d Mon Sep 17 00:00:00 2001 From: Tom Kent Date: Tue, 3 Sep 2024 21:14:04 -0700 Subject: [PATCH 06/12] fix: no need to say general in the name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5c79ea..58bcf08 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution (in the ## Project Structure ```text -General MRZ Scanner +MRZ Scanner ├── assets │ ├── ... │ ├── ... From 3b1d874e85c3dfa265edc7b8c5400bbe6f1ec486 Mon Sep 17 00:00:00 2001 From: felixindrawan Date: Wed, 4 Sep 2024 09:17:06 -0700 Subject: [PATCH 07/12] fix: changed the position of camera above the guide box --- .vscode/settings.json | 3 +++ css/index.css | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0f9429a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5503 +} \ No newline at end of file diff --git a/css/index.css b/css/index.css index 878a568..16ac656 100644 --- a/css/index.css +++ b/css/index.css @@ -152,7 +152,7 @@ img { top: 46px; left: 0; background-color: #000000; - z-index: 1; + z-index: 2; display: none; border-right: #aaaaaa; } From 519d667f2f1944dc9a5fd80196b7c6cd6104bf4d Mon Sep 17 00:00:00 2001 From: felixindrawan Date: Wed, 4 Sep 2024 13:14:58 -0700 Subject: [PATCH 08/12] fix: changed zindex of cameralist vs scan mode container --- css/index.css | 2 +- index.html | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/css/index.css b/css/index.css index 16ac656..2a097dd 100644 --- a/css/index.css +++ b/css/index.css @@ -152,7 +152,7 @@ img { top: 46px; left: 0; background-color: #000000; - z-index: 2; + z-index: 3; display: none; border-right: #aaaaaa; } diff --git a/index.html b/index.html index 9fa3400..253cd08 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,11 @@ Dynamsoft MRZ Scanner - + @@ -113,10 +116,6 @@ - -
Date: Wed, 4 Sep 2024 13:38:27 -0700 Subject: [PATCH 09/12] fix: judge resolution --- js/util.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/util.js b/js/util.js index 8dee534..ee94dd3 100644 --- a/js/util.js +++ b/js/util.js @@ -164,13 +164,13 @@ export function formatMRZ(mrzString = "") { * @returns {string} Either "HD" or "Full HD" depending of the resolution of the screen */ export const judgeCurResolution = (currentResolution) => { - const { width, height } = currentResolution; + const width = currentResolution?.width ?? 0; + const height = currentResolution?.height ?? 0; const minValue = Math.min(width, height); const maxValue = Math.max(width, height); - - if (minValue > 480 && minValue < 960 && maxValue > 960 && maxValue < 1440) { + if (minValue >= 480 && minValue <= 960 && maxValue >= 960 && maxValue <= 1440) { return "HD"; - } else if (minValue > 900 && minValue < 1440 && maxValue > 1400 && maxValue < 2160) { + } else if (minValue >= 900 && minValue <= 1440 && maxValue >= 1400 && maxValue <= 2160) { return "Full HD"; } }; From c1069de03389d507e0d2605042c6a323fd32cb3b Mon Sep 17 00:00:00 2001 From: felixindrawan Date: Wed, 4 Sep 2024 13:59:30 -0700 Subject: [PATCH 10/12] feaT: add notification --- css/index.css | 35 +++++++++++++++++++++++++++++++++++ index.html | 1 + js/const.js | 2 ++ js/index.js | 5 ++++- js/init.js | 11 +++++++++-- js/util.js | 15 +++++++++++++++ 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/css/index.css b/css/index.css index 2a097dd..1f738d1 100644 --- a/css/index.css +++ b/css/index.css @@ -323,6 +323,41 @@ img { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } +#notification { + text-align: center; + text-align: center; + padding: 0.5rem; + width: -moz-fit-content; + width: fit-content; + position: absolute; + z-index: 3; + bottom: -200%; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + font-family: Oswald-Light; + color: #fff; + + /* Fade in animation */ + opacity: 0; + display: none; + transition: opacity 300ms; +} + +.banner-default { + background-color: rgb(254, 142, 20, 0.4); + border: 1px solid #fe8e14; +} +.banner-error { + background-color: rgb(252, 2, 0, 0.4); + border: 1px solid #fc0200; +} +.banner-success { + background-color: rgb(124, 252, 0, 0.4); + border: 1px solid #00fc15; +} + @keyframes dce-rotate { from { transform: rotate(0turn); diff --git a/index.html b/index.html index 253cd08..adf5ef6 100644 --- a/index.html +++ b/index.html @@ -65,6 +65,7 @@ music no-music
+
{ playSoundBtn.addEventListener("click", () => { playSoundBtn.style.display = "none"; closeSoundBtn.style.display = "block"; + showNotification("Sound feedback off", "banner-default"); isSoundOn = false; }); closeSoundBtn.addEventListener("click", () => { playSoundBtn.style.display = "block"; closeSoundBtn.style.display = "none"; + showNotification("Sound feedback on", "banner-default"); isSoundOn = true; }); diff --git a/js/init.js b/js/init.js index 7af1525..7e5db8a 100644 --- a/js/init.js +++ b/js/init.js @@ -1,4 +1,4 @@ -import { judgeCurResolution } from "./util.js"; +import { judgeCurResolution, showNotification } from "./util.js"; import { createPendingPromise, extractDocumentFields, resultToHTMLElement, formatMRZ } from "./util.js"; // Promise variable used to control model loading state @@ -61,7 +61,11 @@ async function initDCE() { const currentCamera = await cameraEnhancer.getSelectedCamera(); const currentResolution = judgeCurResolution(await cameraEnhancer.getResolution()); - if (judgeCurResolution(currentResolution) !== res) { + if (currentCamera.deviceId === camera.deviceId && currentResolution === res) { + showNotification("Camera and resolution switched successfully!", "banner-success"); + } else if (judgeCurResolution(currentResolution) !== res) { + showNotification(`Resolution switch failed! ${res} is not supported.`, "banner-default"); + // Update resolution to the current resolution that is supported for (let child of cameraListContainer.childNodes) { child.className = "camera-item"; @@ -69,7 +73,10 @@ async function initDCE() { child.className = "camera-item camera-selected"; } } + } else { + showNotification(`Camera switch failed!`, "banner-error"); } + // Hide options after user clicks an option cameraSelector.click(); }); diff --git a/js/util.js b/js/util.js index ee94dd3..1dc0a1b 100644 --- a/js/util.js +++ b/js/util.js @@ -184,3 +184,18 @@ export function shouldShowScanModeContainer() { const isResultClosed = resultContainer.style.display === "none" || resultContainer.style.display === ""; scanModeContainer.style.display = isHomepageClosed && isResultClosed ? "flex" : "none"; } + +/** Show notification banner to users + * @params {string} message - noficiation message + * @params {string} className - CSS class name to show notification status + */ +export function showNotification(message, className) { + notification.className = className; + notification.innerText = message; + notification.style.display = "block"; + notification.style.opacity = 1; + setTimeout(() => { + notification.style.opacity = 0; + notification.style.display = "none"; + }, 2000); +} From fb41f545fefbb1e4df840197862f681b5c7e7bba Mon Sep 17 00:00:00 2001 From: felixindrawan Date: Wed, 4 Sep 2024 14:29:14 -0700 Subject: [PATCH 11/12] add demo code --- css/index.css | 18 ++++++++++++++++++ index.html | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/css/index.css b/css/index.css index 1f738d1..72356f9 100644 --- a/css/index.css +++ b/css/index.css @@ -358,6 +358,24 @@ img { border: 1px solid #00fc15; } +.get-demo-code { + display: flex; + justify-content: center; + align-items: center; + color: #ffae38; + margin-left: auto; + margin-right: 1rem; + font-family: Oswald-Regular; + text-decoration: none; +} + +.dbr-download-code-icon { + width: 16px; + height: 16px; + stroke: #ffae38; + margin-left: 5px; +} + @keyframes dce-rotate { from { transform: rotate(0turn); diff --git a/index.html b/index.html index adf5ef6..b42e852 100644 --- a/index.html +++ b/index.html @@ -65,6 +65,45 @@ music no-music
+ + GET DEMO CODE + + + + + + + + + +
Date: Wed, 4 Sep 2024 14:59:46 -0700 Subject: [PATCH 12/12] Update the readme to emphasize the need to deploy the files to an HTTPS server before running it --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 58bcf08..c437e7c 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,15 @@ The web demo is available at [https://demo.dynamsoft.com/solutions/mrz-scanner/i ## Run this Solution -1. Clone the repo to a working directory +1. Clone the repository to a working directory or download the code as a ZIP file: ```sh git clone https://github.com/Dynamsoft/MRZ-scanner-javascript ``` -2. CD to the folder and run an https server +2. Deploy the files to a directory hosted on an HTTPS server. -```sh -cd MRZ-scanner-javascript -``` +3. Open the "index.html" file in your browser. > Basic Requirements > @@ -30,7 +28,7 @@ cd MRZ-scanner-javascript ## Request a Trial License -The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution (in the file `js/init.js`) serves as a test license valid for 24 hours, applicable to any newly authorized browser. To test the SDK further, you can request a 30-day free trial license via the Request a Trial License link. +The key "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9" used in this solution (found in the js/init.js file) is a test license valid for 24 hours for any newly authorized browser. If you wish to test the SDK further, you can request a 30-day free trial license through the Request a Trial License link. ## Project Structure