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/README.md b/README.md index 244cff1..c437e7c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,22 @@ # 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. +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 +1. Clone the repository to a working directory or download the code as a ZIP file: ```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 +2. Deploy the files to a directory hosted on an HTTPS server. -```sh -cd passport-MRZ-scanner-javascript -``` +3. Open the "index.html" file in your browser. > Basic Requirements > @@ -30,12 +28,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 (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 ```text -Passport MRZ Scanner +MRZ Scanner ├── assets │ ├── ... │ ├── ... @@ -47,7 +45,7 @@ Passport MRZ Scanner │ ├── ... │ └── ... ├── js -│ ├── define.js +│ ├── const.js │ ├── index.js │ ├── init.js │ └── util.js @@ -59,7 +57,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..c5702ef 100644 --- a/assets/passport frame.svg +++ b/assets/mrz-guide-box.svg @@ -7,7 +7,7 @@ .st1{enable-background:new ;} .st2{fill:#AAAAAA;} - + @@ -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..72356f9 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 .power { +.home-page .description .start-btn:hover { + background-color: #fea543; +} + +.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; + z-index: 3; 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 { @@ -223,9 +266,11 @@ body { height: 10%; min-height: 60px; max-height: 100px; + background-color: #2b2b2b; 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 +283,99 @@ 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); +} + +#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; +} + +.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); @@ -258,8 +396,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..b42e852 100644 --- a/index.html +++ b/index.html @@ -1,160 +1,319 @@ - - - - - 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 -
-
- music - no-music -
-
- - - - -
-
- + + down + up +
+
+
+ music + no-music +
+ + GET DEMO CODE + + + + + + + + + + +
- -
- - - + + + + + + +
+
+ mrz-guide +
+ +
+ + + - + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + C92.9,14.1,92.8,14.1,92.7,14.1z" + /> + + +
-
-
-
-
Passport Scan Results:
-
Restart >
-
-
-
-
-
+
+
+
Scan Results
+
+
+
+
-
-
- +
+
+ + + +
-
- - - - - - \ No newline at end of file + + + + + diff --git a/js/const.js b/js/const.js new file mode 100644 index 0000000..2174310 --- /dev/null +++ b/js/const.js @@ -0,0 +1,53 @@ +// 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"); + +const notification = document.querySelector("#notification"); 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..78e17c9 100644 --- a/js/index.js +++ b/js/index.js @@ -1,29 +1,51 @@ -import { pDataLoad, cvrReady } from "./init.js"; -import { checkOrientation, getVisibleRegionOfVideo } from "./util.js" +import { init, pDataLoad } from "./init.js"; +import { judgeCurResolution, shouldShowScanModeContainer, showNotification } 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"); - })()); + + await cvRouter.startCapturing(SCAN_TEMPLATES[mode]); + + // Show MRZ guide frame + mrzGuideFrame.style.display = "inline-block"; + + // 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"); + showNotification(`Scan mode switched successfully`, "banner-success"); + + currentMode = mode; + scanModeContainer.style.display = "flex"; + })(); } catch (ex) { let errMsg = ex.message || ex; console.error(errMsg); @@ -31,60 +53,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 +113,53 @@ 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; -}) + showNotification("Sound feedback off", "banner-default"); + isSoundOn = false; +}); closeSoundBtn.addEventListener("click", () => { playSoundBtn.style.display = "block"; closeSoundBtn.style.display = "none"; - isPlaySound = true; -}) \ No newline at end of file + showNotification("Sound feedback on", "banner-default"); + isSoundOn = true; +}); diff --git a/js/init.js b/js/init.js index 4508f10..7e5db8a 100644 --- a/js/init.js +++ b/js/init.js @@ -1,31 +1,33 @@ - -import { createPendingPromise, getNeedShowFields } 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 -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=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 + * LICENSE ALERT - THE END */ Dynamsoft.DLR.LabelRecognizerModule.onDataLoadProgressChanged = (modelPath, tag, progress) => { if (tag === "completed") { pDataLoad.resolve(); } -} +}; /** * Preloads the resources */ 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"); /** @@ -38,29 +40,57 @@ 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 (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"; + if (currentCamera.deviceId === child.deviceId && currentResolution === child.resolution) { + child.className = "camera-item camera-selected"; + } + } + } else { + showNotification(`Camera switch failed!`, "banner-error"); + } + + // 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 +102,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 +135,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..1dc0a1b 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,99 @@ 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 = 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) { + 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"; +} + +/** 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); +} 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