From 02464e91c9965402d2b8488c08b0ef78cccf4c5b Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 12 Nov 2024 11:12:13 -0800 Subject: [PATCH 01/21] feat: update homepage for desktop --- assets/qr-code.svg | 181 +++++++++++++++++++++++++++++++++++++++++++++ css/index.css | 58 ++++++++++++--- index.html | 18 +++++ js/const.js | 2 +- js/index.js | 2 +- 5 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 assets/qr-code.svg diff --git a/assets/qr-code.svg b/assets/qr-code.svg new file mode 100644 index 0000000..4bdc98f --- /dev/null +++ b/assets/qr-code.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/css/index.css b/css/index.css index e8860c0..99b74f6 100644 --- a/css/index.css +++ b/css/index.css @@ -57,21 +57,21 @@ img { } .home-page .logo { - width: 150px; - height: 36px; + width: 112.5px; + height: 27px; } .home-page .description { - width: 50%; + width: 85%; text-align: center; background-color: #323234; margin: 0 auto; - padding: 2rem; + padding: 1rem 2rem 2rem; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .home-page .description .title { - font-size: 30px; + font-size: 26px; font-family: Oswald-Regular; } @@ -79,22 +79,52 @@ img { font-family: OpenSans-Regular; font-size: 16px; line-height: 26px; - margin: 16px 0 25px; + margin: 1rem 0 1rem; +} + +.home-page .description .desktop-qr-container { + font-family: OpenSans-Regular; + background-color: #2b2b2b; + padding: 2rem; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 0.5rem; + justify-content: center; + align-items: center; +} + +.home-page .description .desktop-start-btn { + font-family: OpenSans-Regular; + background-color: #2b2b2b; + margin-top: 1rem; + padding: 2rem; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: start; + gap: 1rem; + text-align: start; +} + +.home-page .description .desktop-start-btn .start-btn { + display: flex; + width: 100%; } .home-page .description .start-btn { - width: 180px; height: 6vh; min-height: 40px; max-height: 60px; background-color: #fe8e14; font-family: Oswald-Regular; - font-size: 20px; + font-size: 16px; margin: 0 auto; - display: flex; + display: none; justify-content: center; align-items: center; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + padding: 2rem; } .home-page .description .start-btn:hover { @@ -462,8 +492,18 @@ img { box-shadow: none; } + .home-page .description .desktop-qr-container { + display: none; + } + + .home-page .description .desktop-start-btn { + display: none; + } + .home-page .description .start-btn { + display: flex; font-size: 20px; + padding: 1.5rem; } } diff --git a/index.html b/index.html index 53cd513..4617029 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,24 @@ Dynamsoft MRZ Scanner recognizes the Machine-Readable Zone (MRZ) on a passport or ID card and converts the encoded strings into human-readable fields +
+ https://demo.dynamsoft.com/solutions/mrz-scanner/ +

Scan to Open on Mobile

+

+ For optimal performance, scan this QR code to open the scanner on your mobile device +

+
+
+

Quick Start Options

+ +

+ Note: Desktop cameras may have limited performance. Mobile scanning is recommended for best results. +

+
Powered by Dynamsoft
diff --git a/js/const.js b/js/const.js index 721cdea..b613972 100644 --- a/js/const.js +++ b/js/const.js @@ -41,7 +41,7 @@ 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 startScaningBtn = document.querySelectorAll(".start-btn"); const scanModeContainer = document.querySelector(".scan-mode-container"); const scanBothBtn = document.querySelector("#scan-both-btn"); diff --git a/js/index.js b/js/index.js index ba61235..52350d6 100644 --- a/js/index.js +++ b/js/index.js @@ -134,7 +134,7 @@ window.addEventListener("resize", () => { }); // Add click events to buttons -startScaningBtn.addEventListener("click", () => scanBothBtn.click()); +startScaningBtn.forEach((btn) => btn.addEventListener("click", () => scanBothBtn.click())); const restartVideo = async () => { resultContainer.style.display = "none"; document.querySelector(`#scan-${currentMode}-btn`).click(); From cafaa27e5f5464f1ca99b7597e3a5430774b7d39 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 12 Nov 2024 11:56:12 -0800 Subject: [PATCH 02/21] feat: update template --- template.json | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/template.json b/template.json index 2933d2e..62e1f3a 100644 --- a/template.json +++ b/template.json @@ -38,70 +38,70 @@ ], "TextLineSpecificationOptions": [ { - "Name": "tls-mrz-passport", - "BaseTextLineSpecificationName": "tls-base", + "Name": "tls_mrz_passport", + "BaseTextLineSpecificationName": "tls_base", "StringLengthRange": [44, 44], "OutputResults": 1, "ExpectedGroupsCount": 1, "ConcatResults": 1, - "ConcatSeparator": "", + "ConcatSeparator": "\n", "SubGroups": [ { "StringRegExPattern": "(P[A-Z<][A-Z<]{3}[A-Z<]{39}){(44)}", "StringLengthRange": [44, 44], - "BaseTextLineSpecificationName": "tls-base" + "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)}", + "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[0-9<]{4}[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" + "BaseTextLineSpecificationName": "tls_base" } ] }, { - "Name": "tls-mrz-id-td2", - "BaseTextLineSpecificationName": "tls-base", + "Name": "tls_mrz_id_td2", + "BaseTextLineSpecificationName": "tls_base", "StringLengthRange": [36, 36], "OutputResults": 1, "ExpectedGroupsCount": 1, "ConcatResults": 1, - "ConcatSeparator": "", + "ConcatSeparator": "\n", "SubGroups": [ { "StringRegExPattern": "([ACI][A-Z<][A-Z<]{3}[A-Z<]{31}){(36)}", "StringLengthRange": [36, 36], - "BaseTextLineSpecificationName": "tls-base" + "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)}", + "StringRegExPattern": "([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[0-9<]{4}[0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{8}){(36)}", "StringLengthRange": [36, 36], - "BaseTextLineSpecificationName": "tls-base" + "BaseTextLineSpecificationName": "tls_base" } ] }, { - "Name": "tls-mrz-id-td1", - "BaseTextLineSpecificationName": "tls-base", + "Name": "tls_mrz_id_td1", + "BaseTextLineSpecificationName": "tls_base", "StringLengthRange": [30, 30], "OutputResults": 1, "ExpectedGroupsCount": 1, "ConcatResults": 1, - "ConcatSeparator": "", + "ConcatSeparator": "\n", "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" + "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)}", + "StringRegExPattern": "([0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[0-9<]{4}[0-9][A-Z<]{3}[A-Z0-9<]{11}[0-9]){(30)}", "StringLengthRange": [30, 30], - "BaseTextLineSpecificationName": "tls-base" + "BaseTextLineSpecificationName": "tls_base" }, { "StringRegExPattern": "([A-Z<]{30}){(30)}", "StringLengthRange": [30, 30], - "BaseTextLineSpecificationName": "tls-base" + "BaseTextLineSpecificationName": "tls_base" } ] }, @@ -132,7 +132,7 @@ { "Name": "task-passport", "ConfusableCharactersPath": "ConfusableChars.data", - "TextLineSpecificationNameArray": ["tls-mrz-passport"], + "TextLineSpecificationNameArray": ["tls_mrz_passport"], "SectionImageParameterArray": [ { "Section": "ST_REGION_PREDETECTION", @@ -151,7 +151,7 @@ { "Name": "task-id", "ConfusableCharactersPath": "ConfusableChars.data", - "TextLineSpecificationNameArray": ["tls-mrz-id-td1", "tls-mrz-id-td2"], + "TextLineSpecificationNameArray": ["tls_mrz_id_td1", "tls_mrz_id_td2"], "SectionImageParameterArray": [ { "Section": "ST_REGION_PREDETECTION", @@ -170,7 +170,7 @@ { "Name": "task-passport-and-id", "ConfusableCharactersPath": "ConfusableChars.data", - "TextLineSpecificationNameArray": ["tls-mrz-passport", "tls-mrz-id-td1", "tls-mrz-id-td2"], + "TextLineSpecificationNameArray": ["tls_mrz_passport", "tls_mrz_id_td1", "tls_mrz_id_td2"], "SectionImageParameterArray": [ { "Section": "ST_REGION_PREDETECTION", From e67df2830ea4c2c128ac677b498c8f8de446dedd Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 12 Nov 2024 11:58:32 -0800 Subject: [PATCH 03/21] chore: update dcv bundle to 2.4.2000 --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 4617029..903c37a 100644 --- a/index.html +++ b/index.html @@ -29,7 +29,7 @@ /> - +
From e7fb0ec0504e39157c9d3c99dc83830c15c5e974 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 12 Nov 2024 14:23:32 -0800 Subject: [PATCH 04/21] fix: invalid template name --- template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.json b/template.json index 62e1f3a..5e38944 100644 --- a/template.json +++ b/template.json @@ -106,7 +106,7 @@ ] }, { - "Name": "tls-base", + "Name": "tls_base", "CharacterModelName": "MRZ", "CharHeightRange": [5, 1000, 1], "BinarizationModes": [ From 7e804ca3407d8bb372767dfcb96bb8024670d917 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 12 Nov 2024 22:11:29 -0800 Subject: [PATCH 05/21] feat: add image preview --- assets/upload-image.svg | 14 ++++++ css/index.css | 33 +++++++++++++- index.html | 13 ++++-- js/const.js | 5 ++- js/index.js | 26 ++++++++++- js/init.js | 96 ++++++++++++++++++++++++++--------------- js/util.js | 4 +- template.json | 6 +-- 8 files changed, 149 insertions(+), 48 deletions(-) create mode 100644 assets/upload-image.svg diff --git a/assets/upload-image.svg b/assets/upload-image.svg new file mode 100644 index 0000000..fbb3d38 --- /dev/null +++ b/assets/upload-image.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/css/index.css b/css/index.css index 99b74f6..070ae9b 100644 --- a/css/index.css +++ b/css/index.css @@ -238,6 +238,24 @@ img { display: none; } +.scanner-container .header .upload-image-btn { + width: 30px; + height: 30px; + cursor: pointer; + display: flex; + align-items: center; + cursor: pointer; +} + +.scanner-container .header .upload-image-icon { + width: 24px; + height: 24px; +} + +.scanner-container .header .upload-image-icon:hover { + opacity: 0.8; +} + .result-container { position: absolute; width: 100%; @@ -269,6 +287,17 @@ img { color: #aaaaaa; } +.result-container .scanned-image { + max-height: 200px; +} + +.result-container .scanned-image img, +.result-container .scanned-image canvas { + object-fit: contain; + width: 100%; + height: 100%; +} + .result-container .parsed-result-area { width: 100%; height: 84%; @@ -292,7 +321,7 @@ img { font-family: monospace; } -.result-container .restart-video { +.result-container .scan-again { width: 100%; height: 10%; min-height: 60px; @@ -304,7 +333,7 @@ img { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } -.result-container .restart-video .btn-restart-video { +.result-container .scan-again .btn-scan-again { width: 160px; height: 60%; border: 0; diff --git a/index.html b/index.html index 903c37a..199b19e 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,10 @@

Quick Start Options

up
+ +
+ upload-image +
music no-music @@ -213,8 +217,8 @@

Quick Start Options

right: 0; bottom: 0; margin: auto; - width: 40%; - height: 40%; + width: 20%; + height: 20%; fill: #aaa; animation: 1s linear infinite dce-rotate; " @@ -407,9 +411,10 @@

Quick Start Options

+
-
- +
+
diff --git a/js/const.js b/js/const.js index b613972..bf96991 100644 --- a/js/const.js +++ b/js/const.js @@ -28,6 +28,8 @@ const homePage = document.querySelector(".home-page"); const cameraViewContainer = document.querySelector(".camera-view-container"); +const uploadImageInput = document.querySelector(".upload-image-input"); + const cameraListContainer = document.querySelector(".camera-list"); const cameraSelector = document.querySelector(".camera-selector"); @@ -39,6 +41,7 @@ const scannerContainer = document.querySelector(".scanner-container"); const mrzGuideFrame = document.querySelector(".mrz-frame"); const resultContainer = document.querySelector(".result-container"); +const scannedImage = document.querySelector(".scanned-image"); const parsedResultArea = document.querySelector(".parsed-result-area"); const startScaningBtn = document.querySelectorAll(".start-btn"); @@ -46,7 +49,7 @@ const startScaningBtn = document.querySelectorAll(".start-btn"); const scanModeContainer = document.querySelector(".scan-mode-container"); const scanBothBtn = document.querySelector("#scan-both-btn"); -const restartVideoBtn = document.querySelector(".btn-restart-video"); +const scanAgainBtn = document.querySelector(".btn-scan-again"); const playSoundBtn = document.querySelector(".music"); const closeSoundBtn = document.querySelector(".no-music"); diff --git a/js/index.js b/js/index.js index 52350d6..ad166f0 100644 --- a/js/index.js +++ b/js/index.js @@ -1,4 +1,4 @@ -import { init, pDataLoad } from "./init.js"; +import { handleCapturedResult, init, pDataLoad } from "./init.js"; import { judgeCurResolution, shouldShowScanModeContainer, showNotification } from "./util.js"; import { checkOrientation, getVisibleRegionOfVideo } from "./util.js"; @@ -139,7 +139,29 @@ const restartVideo = async () => { resultContainer.style.display = "none"; document.querySelector(`#scan-${currentMode}-btn`).click(); }; -restartVideoBtn.addEventListener("click", restartVideo); +scanAgainBtn.addEventListener("click", restartVideo); + +uploadImageInput.addEventListener("change", async (event) => { + try { + const file = event.target.files[0]; + + if (file) { + // Open the camera after the model and .wasm files have loaded + pInit = pInit || (await init); + await pDataLoad.promise; + + event.target.value = ""; + + // Decode selected image with 'both' template. + const result = await cvRouter.capture(file, SCAN_TEMPLATES.both); + handleCapturedResult(result, file); + } + } catch (ex) { + let errMsg = ex.message || ex; + alert(errMsg); + console.error(errMsg); + } +}); cameraSelector.addEventListener("click", (e) => { e.stopPropagation(); diff --git a/js/init.js b/js/init.js index bbddd60..02529dd 100644 --- a/js/init.js +++ b/js/init.js @@ -100,42 +100,70 @@ let init = (async () => { /* Defines the result receiver for the solution.*/ const resultReceiver = new Dynamsoft.CVR.CapturedResultReceiver(); - resultReceiver.onCapturedResultReceived = (result) => { - const recognizedResults = result.textLineResultItems; - const parsedResults = result.parsedResultItems; - - 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 = extractDocumentFields(parsedResults[0]); - Object.entries(parseResultInfo).map(([field, value]) => { - const resultElement = resultToHTMLElement(field, value); - parsedResultArea.appendChild(resultElement); - }); - } else { - alert(`Failed to parse the content.`); - parsedResultArea.style.justifyContent = "flex-start"; - } - resultContainer.style.display = "flex"; - cameraListContainer.style.display = "none"; - informationListContainer.style.display = "none"; - scanModeContainer.style.display = "none"; + resultReceiver.onCapturedResultReceived = handleCapturedResult; - cvRouter.stopCapturing(); - cameraView.clearAllInnerDrawingItems(); - } - }; await cvRouter.addResultReceiver(resultReceiver); })(); +export const handleCapturedResult = (result, uploadedImage = null) => { + const recognizedResults = result.textLineResultItems; + const parsedResults = result.parsedResultItems; + const originalImage = result.items?.[0]?.imageData; + + 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 = extractDocumentFields(parsedResults[0]); + Object.entries(parseResultInfo).map(([field, value]) => { + const resultElement = resultToHTMLElement(field, value); + parsedResultArea.appendChild(resultElement); + }); + + if (uploadedImage && uploadedImage.type.startsWith("image/")) { + handleUploadedImage(uploadedImage); + } else if (originalImage) { + scannedImage.innerHTML = ""; + scannedImage.append(originalImage.toCanvas()); + } + } else { + alert(`Failed to parse the content.`); + parsedResultArea.style.justifyContent = "flex-start"; + } + resultContainer.style.display = "flex"; + cameraListContainer.style.display = "none"; + informationListContainer.style.display = "none"; + scanModeContainer.style.display = "none"; + + cameraEnhancer.close(); + cvRouter.stopCapturing(); + cameraView.clearAllInnerDrawingItems(); + } +}; + +function handleUploadedImage(file) { + const img = document.createElement("img"); + const imageUrl = URL.createObjectURL(file); + + img.src = imageUrl; + img.className = "uploaded-image"; + + // Append the image to the div + scannedImage.innerHTMl = ""; + scannedImage.append(img); + + img.onload = () => { + URL.revokeObjectURL(imageUrl); + }; +} + export { pDataLoad, init }; diff --git a/js/util.js b/js/util.js index 1dc0a1b..403fe2d 100644 --- a/js/util.js +++ b/js/util.js @@ -33,12 +33,12 @@ export function extractDocumentFields(result) { 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["Nationality"] = result.getFieldValue("nationality"); parseResultInfo["Document Number"] = type === "P" ? result.getFieldValue("passportNumber") : result.getFieldValue("documentNumber"); - parseResultInfo["Nationality"] = result.getFieldValue("nationality"); + parseResultInfo["Issuing State"] = result.getFieldValue("issuingState"); parseResultInfo["Sex"] = result.getFieldValue("sex"); parseResultInfo["Date of Birth (YYYY-MM-DD)"] = fullBirthYear + "-" + result.getFieldValue("birthMonth") + "-" + result.getFieldValue("birthDay"); diff --git a/template.json b/template.json index 5e38944..b2ac8fb 100644 --- a/template.json +++ b/template.json @@ -2,21 +2,21 @@ "CaptureVisionTemplates": [ { "Name": "ReadPassportAndId", - "OutputOriginalImage": 0, + "OutputOriginalImage": 1, "ImageROIProcessingNameArray": ["roi-passport-and-id"], "SemanticProcessingNameArray": ["sp-passport-and-id"], "Timeout": 2000 }, { "Name": "ReadPassport", - "OutputOriginalImage": 0, + "OutputOriginalImage": 1, "ImageROIProcessingNameArray": ["roi-passport"], "SemanticProcessingNameArray": ["sp-passport"], "Timeout": 2000 }, { "Name": "ReadId", - "OutputOriginalImage": 0, + "OutputOriginalImage": 1, "ImageROIProcessingNameArray": ["roi-id"], "SemanticProcessingNameArray": ["sp-id"], "Timeout": 2000 From 6bfada1852ff0e27b4596992f6573a17f28e129d Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Wed, 13 Nov 2024 00:19:39 -0800 Subject: [PATCH 06/21] fix: remove redundant MRZ formatter --- js/init.js | 4 ++-- js/util.js | 24 ------------------------ 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/js/init.js b/js/init.js index 02529dd..fac3772 100644 --- a/js/init.js +++ b/js/init.js @@ -1,5 +1,5 @@ import { judgeCurResolution, showNotification } from "./util.js"; -import { createPendingPromise, extractDocumentFields, resultToHTMLElement, formatMRZ } from "./util.js"; +import { createPendingPromise, extractDocumentFields, resultToHTMLElement } from "./util.js"; // Promise variable used to control model loading state const pDataLoad = createPendingPromise(); @@ -117,7 +117,7 @@ export const handleCapturedResult = (result, uploadedImage = null) => { parsedResultArea.innerText = ""; // Add MRZ Text to Result - const mrzElement = resultToHTMLElement("MRZ String", formatMRZ(recognizedResults[0]?.text)); + const mrzElement = resultToHTMLElement("MRZ String", recognizedResults[0]?.text); mrzElement.classList.add("code"); parsedResultArea.appendChild(mrzElement); diff --git a/js/util.js b/js/util.js index 403fe2d..fb9c28a 100644 --- a/js/util.js +++ b/js/util.js @@ -135,30 +135,6 @@ export function resultToHTMLElement(field, value) { 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 From 20316f0d05b49208974fd0838aa65db8cc7235a3 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Wed, 13 Nov 2024 09:06:04 -0800 Subject: [PATCH 07/21] feat: add validation status to result fields --- css/index.css | 23 +++++++++++++++++ index.html | 14 +++++++---- js/util.js | 68 ++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/css/index.css b/css/index.css index 070ae9b..a4205e2 100644 --- a/css/index.css +++ b/css/index.css @@ -313,14 +313,37 @@ img { .result-container .parsed-result-area .parsed-filed .field-name { color: #aaaaaa; + display: flex; + align-items: center; + gap: 0.5rem; } .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 .parsed-result-area .parsed-filed .status-icon { + display: inline-flex; + align-items: center; + cursor: help; /* Indicates there's a tooltip */ +} + +.result-container .parsed-result-area .parsed-filed .status-icon svg { + width: 16px; + height: 16px; +} + +.result-container .parsed-result-area .parsed-filed .status-success { + color: #22c55e; /* Green */ +} + +.result-container .parsed-result-area .parsed-filed .status-failed { + color: #ef4444; /* Red */ +} + .result-container .scan-again { width: 100%; height: 10%; diff --git a/index.html b/index.html index 199b19e..18c902b 100644 --- a/index.html +++ b/index.html @@ -64,7 +64,7 @@

Quick Start Options

-
+
@@ -98,14 +98,18 @@

Quick Start Options

-
+
upload-image
- music - no-music + music + no-music
-
+
new Date().getFullYear() % 100 ? "19" : "20"; @@ -33,13 +34,30 @@ export function extractDocumentFields(result) { const fullExpiryYear = `${expiryYearBase}${expiryYear}`; parseResultInfo["Document Type"] = documentType; - parseResultInfo["Surname"] = result.getFieldValue("primaryIdentifier"); - parseResultInfo["Given Name"] = result.getFieldValue("secondaryIdentifier"); - parseResultInfo["Nationality"] = result.getFieldValue("nationality"); - parseResultInfo["Document Number"] = - type === "P" ? result.getFieldValue("passportNumber") : result.getFieldValue("documentNumber"); - parseResultInfo["Issuing State"] = result.getFieldValue("issuingState"); - parseResultInfo["Sex"] = result.getFieldValue("sex"); + parseResultInfo["Surname"] = { + text: result.getFieldValue("primaryIdentifier"), + status: result.getFieldValidationStatus("primaryIdentifier"), + }; + parseResultInfo["Given Name"] = { + text: result.getFieldValue("secondaryIdentifier"), + status: result.getFieldValidationStatus("secondaryIdentifier"), + }; + parseResultInfo["Nationality"] = { + text: result.getFieldValue("nationality"), + status: result.getFieldValidationStatus("nationality"), + }; + parseResultInfo["Document Number"] = { + text: result.getFieldValue(documentNumber), + status: result.getFieldValidationStatus(documentNumber), + }; + parseResultInfo["Issuing State"] = { + text: result.getFieldValue("issuingState"), + status: result.getFieldValidationStatus("issuingState"), + }; + parseResultInfo["Sex"] = { + text: result.getFieldValue("sex"), + status: result.getFieldValidationStatus("sex"), + }; parseResultInfo["Date of Birth (YYYY-MM-DD)"] = fullBirthYear + "-" + result.getFieldValue("birthMonth") + "-" + result.getFieldValue("birthDay"); parseResultInfo["Date of Expiry (YYYY-MM-DD)"] = @@ -125,9 +143,41 @@ export function resultToHTMLElement(field, value) { spanFieldName.className = "field-name"; const spanValue = document.createElement("span"); spanValue.className = "field-value"; + const statusIcon = document.createElement("span"); + statusIcon.className = "status-icon"; + + // Define success and failed icons + const icons = { + success: ` + + `, + failed: ` + + `, + }; + + // Handle validation status based on EnumValidationStatus + switch (value?.status) { + case Dynamsoft.DCP.EnumValidationStatus.VS_SUCCEEDED: + statusIcon.innerHTML = icons.success; + statusIcon.className += " status-success"; + statusIcon.title = "Validation passed"; + break; + case Dynamsoft.DCP.EnumValidationStatus.VS_FAILED: + statusIcon.innerHTML = icons.failed; + statusIcon.className += " status-failed"; + statusIcon.title = "Validation failed"; + break; + case Dynamsoft.DCP.EnumValidationStatus.VS_NONE: + default: + // Don't add any icon for VS_NONE + statusIcon.style.display = "none"; + break; + } - spanFieldName.innerText = `${field} : `; - spanValue.innerText = `${value || "Not detected"}`; + spanFieldName.innerText = `${field}`; + spanFieldName.append(statusIcon); + spanValue.innerText = `${value?.text || value || "Not detected"}`; p.appendChild(spanFieldName); p.appendChild(spanValue); From 9eeb40a2547204a76351089ea203605ff6092f3d Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Wed, 13 Nov 2024 13:48:31 -0800 Subject: [PATCH 08/21] feat: add option to take a photo or upload an image --- assets/{arrow-down 1.svg => arrow-down.svg} | 0 assets/{arrow-up 1.svg => arrow-up.svg} | 0 assets/external-link.svg | 17 +++ assets/photo-camera.svg | 1 + assets/{upload-image.svg => upload-menu.svg} | 0 assets/upload.svg | 1 + assets/video-camera.svg | 28 +++++ css/index.css | 50 ++++++++- index.html | 107 ++++--------------- js/const.js | 8 +- js/index.js | 39 ++++++- js/init.js | 86 +++++++++------ 12 files changed, 209 insertions(+), 128 deletions(-) rename assets/{arrow-down 1.svg => arrow-down.svg} (100%) rename assets/{arrow-up 1.svg => arrow-up.svg} (100%) create mode 100644 assets/external-link.svg create mode 100644 assets/photo-camera.svg rename assets/{upload-image.svg => upload-menu.svg} (100%) create mode 100644 assets/upload.svg create mode 100644 assets/video-camera.svg diff --git a/assets/arrow-down 1.svg b/assets/arrow-down.svg similarity index 100% rename from assets/arrow-down 1.svg rename to assets/arrow-down.svg diff --git a/assets/arrow-up 1.svg b/assets/arrow-up.svg similarity index 100% rename from assets/arrow-up 1.svg rename to assets/arrow-up.svg diff --git a/assets/external-link.svg b/assets/external-link.svg new file mode 100644 index 0000000..55c5f58 --- /dev/null +++ b/assets/external-link.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/photo-camera.svg b/assets/photo-camera.svg new file mode 100644 index 0000000..f603bed --- /dev/null +++ b/assets/photo-camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/upload-image.svg b/assets/upload-menu.svg similarity index 100% rename from assets/upload-image.svg rename to assets/upload-menu.svg diff --git a/assets/upload.svg b/assets/upload.svg new file mode 100644 index 0000000..7a89991 --- /dev/null +++ b/assets/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/video-camera.svg b/assets/video-camera.svg new file mode 100644 index 0000000..d68e94c --- /dev/null +++ b/assets/video-camera.svg @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/css/index.css b/css/index.css index a4205e2..724a327 100644 --- a/css/index.css +++ b/css/index.css @@ -238,7 +238,7 @@ img { display: none; } -.scanner-container .header .upload-image-btn { +.scanner-container .header .upload-menu-btn { width: 30px; height: 30px; cursor: pointer; @@ -247,15 +247,59 @@ img { cursor: pointer; } -.scanner-container .header .upload-image-icon { +.scanner-container .header .upload-menu-icon { width: 24px; height: 24px; } -.scanner-container .header .upload-image-icon:hover { +.scanner-container .header .upload-menu-list { + position: absolute; + top: 54px; /* Header height 46px + triangle 8px*/ + left: 70px; + background-color: #000000; + z-index: 3; + display: none; + border-right: #aaaaaa; +} + +/* Tooltip Triangle */ +.scanner-container .header .upload-menu-list::after { + content: ""; + position: absolute; + top: -16px; /* At the top of the information list */ + left: 1rem; /* 1rem margin */ + /* margin-left: -5px; */ + border-width: 8px; + border-style: solid; + border-color: transparent transparent #2b2b2b transparent; /* triangle */ +} + +.scanner-container .header .upload-menu-list .menu-item { + width: 100%; + border-bottom: 1px solid rgb(55, 55, 55); + padding: 10px; + cursor: pointer; + background-color: #2b2b2b; + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; + overflow: hidden; + user-select: none; + color: #aaaaaa; + font-size: 12px; + font-family: OpenSans-Regular; + text-decoration: none; +} + +.scanner-container .header .upload-menu-list .menu-item:hover { opacity: 0.8; } +.scanner-container .header .upload-menu-list .menu-item:last-child { + border: unset; +} + .result-container { position: absolute; width: 100%; diff --git a/index.html b/index.html index 18c902b..ec1409a 100644 --- a/index.html +++ b/index.html @@ -65,45 +65,24 @@

Quick Start Options

- - - - - - - - - - - down - up + camera + down + up
- -
- upload-image +
+ upload-menu +
+
+ + +
music @@ -135,43 +114,11 @@

Quick Start Options

target="_blank" > About - - - - - - + link-icon Github Projects - - - - - - + link-icon Quick Start Options target="_blank" > Contact Us - - - - - - + link-icon
diff --git a/js/const.js b/js/const.js index bf96991..4af569f 100644 --- a/js/const.js +++ b/js/const.js @@ -28,11 +28,15 @@ const homePage = document.querySelector(".home-page"); const cameraViewContainer = document.querySelector(".camera-view-container"); -const uploadImageInput = document.querySelector(".upload-image-input"); - const cameraListContainer = document.querySelector(".camera-list"); const cameraSelector = document.querySelector(".camera-selector"); +const uploadMenuBtn = document.querySelector(".upload-menu-btn"); +const uploadMenuList = document.querySelector(".upload-menu-list"); +const uploadImageBtn = document.querySelector(".upload-image-btn"); +const uploadImageInput = document.querySelector(".upload-image-input"); +const takePhotoBtn = document.querySelector(".take-photo-btn"); + const informationBtn = document.querySelectorAll(".information-btn"); const informationListContainer = document.querySelector(".information-list"); diff --git a/js/index.js b/js/index.js index ad166f0..7d7a828 100644 --- a/js/index.js +++ b/js/index.js @@ -115,10 +115,11 @@ const region = () => { // -----------Logic for calculating scan region ↑------------ window.addEventListener("click", () => { - cameraListContainer.style.display = "none"; + cameraListContainer.style.display = "none"; // hide camera list and reset arrow indicator up.style.display = "none"; down.style.display = "inline-block"; - informationListContainer.style.display = "none"; + informationListContainer.style.display = "none"; // hide information menu + uploadMenuList.style.display = "none"; // hide upload image menu }); // Recalculate the scan region when the window size changes @@ -162,8 +163,30 @@ uploadImageInput.addEventListener("change", async (event) => { console.error(errMsg); } }); +takePhotoBtn.addEventListener("click", async (event) => { + try { + const image = cameraEnhancer.fetchImage(); + const imageCvs = image.toCanvas(); + + // Open the camera after the model and .wasm files have loaded + pInit = pInit || (await init); + await pDataLoad.promise; + + // Decode selected image with 'both' template. + const result = await cvRouter.capture(imageCvs, SCAN_TEMPLATES.both); + console.log(result); + handleCapturedResult(result, image); + } catch (ex) { + let errMsg = ex.message || ex; + alert(errMsg); + console.error(errMsg); + } +}); cameraSelector.addEventListener("click", (e) => { + informationListContainer.style.display = "none"; // hide information menu + uploadMenuList.style.display = "none"; // hide upload image menu + e.stopPropagation(); const isShow = cameraListContainer.style.display === "block"; cameraListContainer.style.display = isShow ? "none" : "block"; @@ -187,8 +210,20 @@ closeSoundBtn.addEventListener("click", () => { informationBtn.forEach((infoBtn) => infoBtn.addEventListener("click", (e) => { + uploadMenuList.style.display = "none"; // hide upload image menu + cameraListContainer.style.display = "none"; // hide camera list + e.stopPropagation(); const isShow = informationListContainer.style.display === "block"; informationListContainer.style.display = isShow ? "none" : "block"; }) ); + +uploadMenuBtn.addEventListener("click", (e) => { + informationListContainer.style.display = "none"; // hide information menu + cameraListContainer.style.display = "none"; // hide camera list + + e.stopPropagation(); + const isShow = uploadMenuList.style.display === "block"; + uploadMenuList.style.display = isShow ? "none" : "block"; +}); diff --git a/js/init.js b/js/init.js index fac3772..4c438af 100644 --- a/js/init.js +++ b/js/init.js @@ -121,49 +121,69 @@ export const handleCapturedResult = (result, uploadedImage = null) => { mrzElement.classList.add("code"); parsedResultArea.appendChild(mrzElement); - // If a parsed result is obtained, use it to render the result page - if (parsedResults) { - const parseResultInfo = extractDocumentFields(parsedResults[0]); - Object.entries(parseResultInfo).map(([field, value]) => { - const resultElement = resultToHTMLElement(field, value); - parsedResultArea.appendChild(resultElement); - }); + const parseSuccess = displayResults(recognizedResults[0]?.text, parsedResults?.[0]); - if (uploadedImage && uploadedImage.type.startsWith("image/")) { - handleUploadedImage(uploadedImage); - } else if (originalImage) { - scannedImage.innerHTML = ""; - scannedImage.append(originalImage.toCanvas()); - } - } else { + if (!parseSuccess) { alert(`Failed to parse the content.`); parsedResultArea.style.justifyContent = "flex-start"; } - resultContainer.style.display = "flex"; - cameraListContainer.style.display = "none"; - informationListContainer.style.display = "none"; - scanModeContainer.style.display = "none"; - - cameraEnhancer.close(); - cvRouter.stopCapturing(); - cameraView.clearAllInnerDrawingItems(); + displayImage(uploadedImage || originalImage); + + dispose(); + } else if (uploadedImage) { + parsedResultArea.innerText = "No results found"; + displayImage(uploadedImage); + dispose(); + } +}; + +const displayResults = (recognizedText, parsedResult) => { + parsedResultArea.innerText = ""; + + // Display MRZ text + const mrzElement = resultToHTMLElement("MRZ String", recognizedText); + mrzElement.classList.add("code"); + parsedResultArea.appendChild(mrzElement); + + // Display parsed fields + if (parsedResult) { + const fields = extractDocumentFields(parsedResult); + Object.entries(fields).forEach(([field, value]) => { + parsedResultArea.appendChild(resultToHTMLElement(field, value)); + }); + return true; } + + return false; }; -function handleUploadedImage(file) { - const img = document.createElement("img"); - const imageUrl = URL.createObjectURL(file); +function displayImage(image) { + scannedImage.textContent = ""; + + if (image.type?.startsWith("image/")) { + const img = document.createElement("img"); + const imageUrl = URL.createObjectURL(image); + + img.src = imageUrl; + img.className = "uploaded-image"; + img.onload = () => URL.revokeObjectURL(imageUrl); - img.src = imageUrl; - img.className = "uploaded-image"; + scannedImage.append(img); + } else if (image.toCanvas) { + scannedImage.append(image.toCanvas()); + } +} - // Append the image to the div - scannedImage.innerHTMl = ""; - scannedImage.append(img); +function dispose() { + resultContainer.style.display = "flex"; // Show result container + cameraListContainer.style.display = "none"; // hide header menu windows + informationListContainer.style.display = "none"; + uploadMenuList.style.display = "none"; + scanModeContainer.style.display = "none"; // hide scan mode buttons - img.onload = () => { - URL.revokeObjectURL(imageUrl); - }; + cameraEnhancer.close(); + cvRouter.stopCapturing(); + cameraView.clearAllInnerDrawingItems(); } export { pDataLoad, init }; From e069949f3b58125e0cb6d1b68a4ec60fa80d2d90 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Wed, 13 Nov 2024 14:02:27 -0800 Subject: [PATCH 09/21] fix: remove console.log --- js/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/index.js b/js/index.js index 7d7a828..181110a 100644 --- a/js/index.js +++ b/js/index.js @@ -174,7 +174,6 @@ takePhotoBtn.addEventListener("click", async (event) => { // Decode selected image with 'both' template. const result = await cvRouter.capture(imageCvs, SCAN_TEMPLATES.both); - console.log(result); handleCapturedResult(result, image); } catch (ex) { let errMsg = ex.message || ex; From 31f08a965e46a20741b6d65ec93b985de7aab7ff Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 10:24:05 -0800 Subject: [PATCH 10/21] feat: add verification for dates --- js/util.js | 189 ++++++++++++++++++++++++++--------------------------- 1 file changed, 91 insertions(+), 98 deletions(-) diff --git a/js/util.js b/js/util.js index 1df72ba..577f5bb 100644 --- a/js/util.js +++ b/js/util.js @@ -20,50 +20,99 @@ export function createPendingPromise() { * @returns {Object} An object with key-value pairs of the extracted fields. */ export function extractDocumentFields(result) { - const parseResultInfo = {}; - if (!result.exception) { - const type = result.getFieldValue("documentCode"); - const documentNumber = type === "P" ? "passportNumber" : "documentNumber"; - 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["Surname"] = { - text: result.getFieldValue("primaryIdentifier"), - status: result.getFieldValidationStatus("primaryIdentifier"), - }; - parseResultInfo["Given Name"] = { - text: result.getFieldValue("secondaryIdentifier"), - status: result.getFieldValidationStatus("secondaryIdentifier"), - }; - parseResultInfo["Nationality"] = { - text: result.getFieldValue("nationality"), - status: result.getFieldValidationStatus("nationality"), - }; - parseResultInfo["Document Number"] = { - text: result.getFieldValue(documentNumber), - status: result.getFieldValidationStatus(documentNumber), - }; - parseResultInfo["Issuing State"] = { - text: result.getFieldValue("issuingState"), - status: result.getFieldValidationStatus("issuingState"), - }; - parseResultInfo["Sex"] = { - text: result.getFieldValue("sex"), - status: result.getFieldValidationStatus("sex"), + if (!result || result.exception) { + return {}; + } + + const fieldWithStatus = (fieldName) => ({ + text: result.getFieldValue(fieldName), + status: result.getFieldValidationStatus(fieldName), + }); + + const parseDate = (yearField, monthField, dayField) => { + const year = result.getFieldValue(yearField); + const currentYear = new Date().getFullYear() % 100; + const baseYear = + yearField === "expiryYear" ? (parseInt(year) >= 60 ? "19" : "20") : parseInt(year) > currentYear ? "19" : "20"; + + return { + text: `${baseYear}${year}-${result.getFieldValue(monthField)}-${result.getFieldValue(dayField)}`, + status: [yearField, monthField, dayField].every((field) => result.getFieldValidationStatus(field)) + ? Dynamsoft.DCP.EnumValidationStatus.VS_SUCCEEDED + : Dynamsoft.DCP.EnumValidationStatus.VS_FAILED, }; - 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"); + }; + + const documentType = result.getFieldValue("documentCode"); + const documentNumberField = documentType === "P" ? "passportNumber" : "documentNumber"; + + return { + Surname: fieldWithStatus("primaryIdentifier"), + "Given Name": fieldWithStatus("secondaryIdentifier"), + Nationality: fieldWithStatus("nationality"), + "Document Number": fieldWithStatus(documentNumberField), + "Issuing State": fieldWithStatus("issuingState"), + Sex: fieldWithStatus("sex"), + "Date of Birth (YYYY-MM-DD)": parseDate("birthYear", "birthMonth", "birthDay"), + "Date of Expiry (YYYY-MM-DD)": parseDate("expiryYear", "expiryMonth", "expiryDay"), + "Document Type": JSON.parse(result.jsonString).CodeType, + }; +} + +/** + * 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"; + const statusIcon = document.createElement("span"); + statusIcon.className = "status-icon"; + + // Define success and failed icons + const icons = { + success: ` + + `, + failed: ` + + `, + }; + + // Handle validation status based on EnumValidationStatus + switch (value?.status) { + case Dynamsoft.DCP.EnumValidationStatus.VS_SUCCEEDED: + statusIcon.innerHTML = icons.success; + statusIcon.className += " status-success"; + statusIcon.title = "Validation passed"; + break; + case Dynamsoft.DCP.EnumValidationStatus.VS_FAILED: + statusIcon.innerHTML = icons.failed; + statusIcon.className += " status-failed"; + statusIcon.title = "Validation failed"; + break; + case Dynamsoft.DCP.EnumValidationStatus.VS_NONE: + default: + // Don't add any icon for VS_NONE + statusIcon.style.display = "none"; + break; } - return parseResultInfo; + + spanFieldName.innerText = `${field}`; + spanFieldName.append(statusIcon); + spanValue.innerText = `${value?.text || value || "Not detected"}`; + + p.appendChild(spanFieldName); + p.appendChild(spanValue); + + return p; } /** @@ -129,62 +178,6 @@ export function getVisibleRegionOfVideo() { return regionInPixels; } -/** - * 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"; - const statusIcon = document.createElement("span"); - statusIcon.className = "status-icon"; - - // Define success and failed icons - const icons = { - success: ` - - `, - failed: ` - - `, - }; - - // Handle validation status based on EnumValidationStatus - switch (value?.status) { - case Dynamsoft.DCP.EnumValidationStatus.VS_SUCCEEDED: - statusIcon.innerHTML = icons.success; - statusIcon.className += " status-success"; - statusIcon.title = "Validation passed"; - break; - case Dynamsoft.DCP.EnumValidationStatus.VS_FAILED: - statusIcon.innerHTML = icons.failed; - statusIcon.className += " status-failed"; - statusIcon.title = "Validation failed"; - break; - case Dynamsoft.DCP.EnumValidationStatus.VS_NONE: - default: - // Don't add any icon for VS_NONE - statusIcon.style.display = "none"; - break; - } - - spanFieldName.innerText = `${field}`; - spanFieldName.append(statusIcon); - spanValue.innerText = `${value?.text || value || "Not detected"}`; - - p.appendChild(spanFieldName); - p.appendChild(spanValue); - - return p; -} - /** 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 From a173f4becc9bebf3f65aa39f266a1b001545a4d1 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 11:14:59 -0800 Subject: [PATCH 11/21] feat: crop uploaded images based on text location --- css/index.css | 52 ++------------------------------------------------- index.html | 20 +++++++------------- js/const.js | 4 ---- js/index.js | 30 ----------------------------- js/init.js | 39 ++++++++++++++++++++++++++++++++------ 5 files changed, 42 insertions(+), 103 deletions(-) diff --git a/css/index.css b/css/index.css index 724a327..58bdfbb 100644 --- a/css/index.css +++ b/css/index.css @@ -238,7 +238,7 @@ img { display: none; } -.scanner-container .header .upload-menu-btn { +.scanner-container .header .upload-img-btn { width: 30px; height: 30px; cursor: pointer; @@ -247,59 +247,11 @@ img { cursor: pointer; } -.scanner-container .header .upload-menu-icon { +.scanner-container .header .upload-img-icon { width: 24px; height: 24px; } -.scanner-container .header .upload-menu-list { - position: absolute; - top: 54px; /* Header height 46px + triangle 8px*/ - left: 70px; - background-color: #000000; - z-index: 3; - display: none; - border-right: #aaaaaa; -} - -/* Tooltip Triangle */ -.scanner-container .header .upload-menu-list::after { - content: ""; - position: absolute; - top: -16px; /* At the top of the information list */ - left: 1rem; /* 1rem margin */ - /* margin-left: -5px; */ - border-width: 8px; - border-style: solid; - border-color: transparent transparent #2b2b2b transparent; /* triangle */ -} - -.scanner-container .header .upload-menu-list .menu-item { - width: 100%; - border-bottom: 1px solid rgb(55, 55, 55); - padding: 10px; - cursor: pointer; - background-color: #2b2b2b; - display: flex; - align-items: center; - justify-content: space-between; - gap: 5px; - overflow: hidden; - user-select: none; - color: #aaaaaa; - font-size: 12px; - font-family: OpenSans-Regular; - text-decoration: none; -} - -.scanner-container .header .upload-menu-list .menu-item:hover { - opacity: 0.8; -} - -.scanner-container .header .upload-menu-list .menu-item:last-child { - border: unset; -} - .result-container { position: absolute; width: 100%; diff --git a/index.html b/index.html index ec1409a..0f3cc2d 100644 --- a/index.html +++ b/index.html @@ -70,19 +70,13 @@

Quick Start Options

up
-
- upload-menu -
-
- - - + +
+ upload-menu
music diff --git a/js/const.js b/js/const.js index 4af569f..3a12a6e 100644 --- a/js/const.js +++ b/js/const.js @@ -31,11 +31,7 @@ const cameraViewContainer = document.querySelector(".camera-view-container"); const cameraListContainer = document.querySelector(".camera-list"); const cameraSelector = document.querySelector(".camera-selector"); -const uploadMenuBtn = document.querySelector(".upload-menu-btn"); -const uploadMenuList = document.querySelector(".upload-menu-list"); -const uploadImageBtn = document.querySelector(".upload-image-btn"); const uploadImageInput = document.querySelector(".upload-image-input"); -const takePhotoBtn = document.querySelector(".take-photo-btn"); const informationBtn = document.querySelectorAll(".information-btn"); const informationListContainer = document.querySelector(".information-list"); diff --git a/js/index.js b/js/index.js index 181110a..0127ed7 100644 --- a/js/index.js +++ b/js/index.js @@ -119,7 +119,6 @@ window.addEventListener("click", () => { up.style.display = "none"; down.style.display = "inline-block"; informationListContainer.style.display = "none"; // hide information menu - uploadMenuList.style.display = "none"; // hide upload image menu }); // Recalculate the scan region when the window size changes @@ -163,28 +162,9 @@ uploadImageInput.addEventListener("change", async (event) => { console.error(errMsg); } }); -takePhotoBtn.addEventListener("click", async (event) => { - try { - const image = cameraEnhancer.fetchImage(); - const imageCvs = image.toCanvas(); - - // Open the camera after the model and .wasm files have loaded - pInit = pInit || (await init); - await pDataLoad.promise; - - // Decode selected image with 'both' template. - const result = await cvRouter.capture(imageCvs, SCAN_TEMPLATES.both); - handleCapturedResult(result, image); - } catch (ex) { - let errMsg = ex.message || ex; - alert(errMsg); - console.error(errMsg); - } -}); cameraSelector.addEventListener("click", (e) => { informationListContainer.style.display = "none"; // hide information menu - uploadMenuList.style.display = "none"; // hide upload image menu e.stopPropagation(); const isShow = cameraListContainer.style.display === "block"; @@ -209,7 +189,6 @@ closeSoundBtn.addEventListener("click", () => { informationBtn.forEach((infoBtn) => infoBtn.addEventListener("click", (e) => { - uploadMenuList.style.display = "none"; // hide upload image menu cameraListContainer.style.display = "none"; // hide camera list e.stopPropagation(); @@ -217,12 +196,3 @@ informationBtn.forEach((infoBtn) => informationListContainer.style.display = isShow ? "none" : "block"; }) ); - -uploadMenuBtn.addEventListener("click", (e) => { - informationListContainer.style.display = "none"; // hide information menu - cameraListContainer.style.display = "none"; // hide camera list - - e.stopPropagation(); - const isShow = uploadMenuList.style.display === "block"; - uploadMenuList.style.display = isShow ? "none" : "block"; -}); diff --git a/js/init.js b/js/init.js index 4c438af..7e9cdc7 100644 --- a/js/init.js +++ b/js/init.js @@ -106,6 +106,7 @@ let init = (async () => { })(); export const handleCapturedResult = (result, uploadedImage = null) => { + console.log(result); const recognizedResults = result.textLineResultItems; const parsedResults = result.parsedResultItems; const originalImage = result.items?.[0]?.imageData; @@ -127,7 +128,7 @@ export const handleCapturedResult = (result, uploadedImage = null) => { alert(`Failed to parse the content.`); parsedResultArea.style.justifyContent = "flex-start"; } - displayImage(uploadedImage || originalImage); + displayImage(uploadedImage || originalImage, recognizedResults[0].location.points); dispose(); } else if (uploadedImage) { @@ -157,7 +158,7 @@ const displayResults = (recognizedText, parsedResult) => { return false; }; -function displayImage(image) { +function displayImage(image, points) { scannedImage.textContent = ""; if (image.type?.startsWith("image/")) { @@ -166,9 +167,36 @@ function displayImage(image) { img.src = imageUrl; img.className = "uploaded-image"; - img.onload = () => URL.revokeObjectURL(imageUrl); - - scannedImage.append(img); + img.onload = () => { + URL.revokeObjectURL(imageUrl); + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + const width = points[1].x - points[0].x; + const height = points[2].y - points[1].y; + + canvas.width = width; + canvas.height = height; + + ctx.drawImage( + img, + points[0].x, + points[0].y, + width, + height, // Source coordinates + 0, + 0, + width, + height // Destination coordinates + ); + + const croppedImage = new Image(); + croppedImage.src = canvas.toDataURL(); + croppedImage.className = "uploaded-image"; + + scannedImage.append(croppedImage); + }; } else if (image.toCanvas) { scannedImage.append(image.toCanvas()); } @@ -178,7 +206,6 @@ function dispose() { resultContainer.style.display = "flex"; // Show result container cameraListContainer.style.display = "none"; // hide header menu windows informationListContainer.style.display = "none"; - uploadMenuList.style.display = "none"; scanModeContainer.style.display = "none"; // hide scan mode buttons cameraEnhancer.close(); From 2b369ef29854ca7654b45a6744a122f0fc0c746a Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 11:16:39 -0800 Subject: [PATCH 12/21] fix: add image even if points not provided --- js/init.js | 57 +++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/js/init.js b/js/init.js index 7e9cdc7..8a8dde1 100644 --- a/js/init.js +++ b/js/init.js @@ -167,35 +167,40 @@ function displayImage(image, points) { img.src = imageUrl; img.className = "uploaded-image"; + // Crop image based on points img.onload = () => { URL.revokeObjectURL(imageUrl); - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - - const width = points[1].x - points[0].x; - const height = points[2].y - points[1].y; - - canvas.width = width; - canvas.height = height; - - ctx.drawImage( - img, - points[0].x, - points[0].y, - width, - height, // Source coordinates - 0, - 0, - width, - height // Destination coordinates - ); - - const croppedImage = new Image(); - croppedImage.src = canvas.toDataURL(); - croppedImage.className = "uploaded-image"; - - scannedImage.append(croppedImage); + if (points) { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + const width = points[1].x - points[0].x; + const height = points[2].y - points[1].y; + + canvas.width = width; + canvas.height = height; + + ctx.drawImage( + img, + points[0].x, + points[0].y, + width, + height, // Source coordinates + 0, + 0, + width, + height // Destination coordinates + ); + + const croppedImage = new Image(); + croppedImage.src = canvas.toDataURL(); + croppedImage.className = "uploaded-image"; + + scannedImage.append(croppedImage); + } else { + scannedImage.append(img); + } }; } else if (image.toCanvas) { scannedImage.append(image.toCanvas()); From 9e14cd059067eb591a074c78abb616d03732bdcb Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 11:16:59 -0800 Subject: [PATCH 13/21] fix: remove console.log --- js/init.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/init.js b/js/init.js index 8a8dde1..76c0e73 100644 --- a/js/init.js +++ b/js/init.js @@ -106,7 +106,6 @@ let init = (async () => { })(); export const handleCapturedResult = (result, uploadedImage = null) => { - console.log(result); const recognizedResults = result.textLineResultItems; const parsedResults = result.parsedResultItems; const originalImage = result.items?.[0]?.imageData; From 79ff1fb38cc34e85cb435dded167b97fa0c5902e Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 11:26:49 -0800 Subject: [PATCH 14/21] fix: remove unused icons --- assets/photo-camera.svg | 1 - assets/upload.svg | 1 - 2 files changed, 2 deletions(-) delete mode 100644 assets/photo-camera.svg delete mode 100644 assets/upload.svg diff --git a/assets/photo-camera.svg b/assets/photo-camera.svg deleted file mode 100644 index f603bed..0000000 --- a/assets/photo-camera.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/upload.svg b/assets/upload.svg deleted file mode 100644 index 7a89991..0000000 --- a/assets/upload.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From a4841672cd3a2f85127d910c5cbe0276f461031f Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 11:35:16 -0800 Subject: [PATCH 15/21] feat: add comm100 livechat --- index.html | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/index.html b/index.html index 0f3cc2d..c3df411 100644 --- a/index.html +++ b/index.html @@ -366,5 +366,31 @@

Quick Start Options

> +
+ +
+ + +
From eaf2e959bd3e9b34a22c7558bf8ed898c92bc7a3 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Mon, 18 Nov 2024 11:55:37 -0800 Subject: [PATCH 16/21] fix: rename --- assets/{Music-selected.svg => music-selected.svg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename assets/{Music-selected.svg => music-selected.svg} (100%) diff --git a/assets/Music-selected.svg b/assets/music-selected.svg similarity index 100% rename from assets/Music-selected.svg rename to assets/music-selected.svg From 33274a2bfbfb012b8070402509c150c44db787dc Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 19 Nov 2024 00:24:32 -0800 Subject: [PATCH 17/21] fix: dcp validation for date --- js/util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/util.js b/js/util.js index 577f5bb..d2d8da2 100644 --- a/js/util.js +++ b/js/util.js @@ -37,7 +37,9 @@ export function extractDocumentFields(result) { return { text: `${baseYear}${year}-${result.getFieldValue(monthField)}-${result.getFieldValue(dayField)}`, - status: [yearField, monthField, dayField].every((field) => result.getFieldValidationStatus(field)) + status: [yearField, monthField, dayField].every( + (field) => result.getFieldValidationStatus(field) === Dynamsoft.DCP.EnumValidationStatus.VS_SUCCEEDED + ) ? Dynamsoft.DCP.EnumValidationStatus.VS_SUCCEEDED : Dynamsoft.DCP.EnumValidationStatus.VS_FAILED, }; From c937e55604a157fdb1bbd88304ce6e43c0e928d7 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 19 Nov 2024 09:23:01 -0800 Subject: [PATCH 18/21] fix: show full image when uploading photo --- js/index.js | 2 ++ js/init.js | 42 ++++++------------------------------------ 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/js/index.js b/js/index.js index 0127ed7..580d0b2 100644 --- a/js/index.js +++ b/js/index.js @@ -145,6 +145,8 @@ uploadImageInput.addEventListener("change", async (event) => { try { const file = event.target.files[0]; + console.log(event); + if (file) { // Open the camera after the model and .wasm files have loaded pInit = pInit || (await init); diff --git a/js/init.js b/js/init.js index 76c0e73..934f478 100644 --- a/js/init.js +++ b/js/init.js @@ -7,9 +7,7 @@ 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( - "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLTEwMzAwNjk2NyIsIm1haW5TZXJ2ZXJVUkwiOiJodHRwczovL21sdHMuZHluYW1zb2Z0LmNvbS8iLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMCIsInNlc3Npb25QYXNzd29yZCI6IkVUSHZVNlNPV3F3ZiIsInN0YW5kYnlTZXJ2ZXJVUkwiOiJodHRwczovL3NsdHMuZHluYW1zb2Z0LmNvbS8iLCJjaGVja0NvZGUiOjM5OTMzODU2Nn0=" -); +Dynamsoft.License.LicenseManager.initLicense(""); /** * 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. @@ -106,6 +104,7 @@ let init = (async () => { })(); export const handleCapturedResult = (result, uploadedImage = null) => { + console.log(result); const recognizedResults = result.textLineResultItems; const parsedResults = result.parsedResultItems; const originalImage = result.items?.[0]?.imageData; @@ -127,7 +126,7 @@ export const handleCapturedResult = (result, uploadedImage = null) => { alert(`Failed to parse the content.`); parsedResultArea.style.justifyContent = "flex-start"; } - displayImage(uploadedImage || originalImage, recognizedResults[0].location.points); + displayImage(uploadedImage || originalImage); dispose(); } else if (uploadedImage) { @@ -157,7 +156,7 @@ const displayResults = (recognizedText, parsedResult) => { return false; }; -function displayImage(image, points) { +function displayImage(image) { scannedImage.textContent = ""; if (image.type?.startsWith("image/")) { @@ -166,40 +165,11 @@ function displayImage(image, points) { img.src = imageUrl; img.className = "uploaded-image"; - // Crop image based on points + img.onload = () => { URL.revokeObjectURL(imageUrl); - if (points) { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - - const width = points[1].x - points[0].x; - const height = points[2].y - points[1].y; - - canvas.width = width; - canvas.height = height; - - ctx.drawImage( - img, - points[0].x, - points[0].y, - width, - height, // Source coordinates - 0, - 0, - width, - height // Destination coordinates - ); - - const croppedImage = new Image(); - croppedImage.src = canvas.toDataURL(); - croppedImage.className = "uploaded-image"; - - scannedImage.append(croppedImage); - } else { - scannedImage.append(img); - } + scannedImage.append(img); }; } else if (image.toCanvas) { scannedImage.append(image.toCanvas()); From 33dcd649c3b083e07cbc59a92e8ab25b269c4795 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 19 Nov 2024 12:11:19 -0800 Subject: [PATCH 19/21] update license msg --- js/init.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/js/init.js b/js/init.js index 934f478..678e1a0 100644 --- a/js/init.js +++ b/js/init.js @@ -7,11 +7,12 @@ 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(""); +Dynamsoft.License.LicenseManager.initLicense( + "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLTEwMzAwNjk2NyIsIm1haW5TZXJ2ZXJVUkwiOiJodHRwczovL21sdHMuZHluYW1zb2Z0LmNvbS8iLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMCIsInNlc3Npb25QYXNzd29yZCI6IkVUSHZVNlNPV3F3ZiIsInN0YW5kYnlTZXJ2ZXJVUkwiOiJodHRwczovL3NsdHMuZHluYW1zb2Z0LmNvbS8iLCJjaGVja0NvZGUiOjM5OTMzODU2Nn0=" +); /** - * 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. + * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mrz&utm_source=github&package=js to get your own trial license good for 30 days. + * For more information, see https://www.dynamsoft.com/capture-vision/docs/web/programming/javascript/user-guide/mrz-scanner.html#specify-the-license or contact support@dynamsoft.com. * LICENSE ALERT - THE END */ From a9c08a456a49f7660a8e3db0ec5f301ab58c3fe3 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 19 Nov 2024 23:25:35 -0800 Subject: [PATCH 20/21] update readme --- README.md | 2 +- js/init.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7491c7b..f3c92e4 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ git clone https://github.com/Dynamsoft/mrz-scanner-javascript ## Request a Trial License -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. +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. For more information, see [Specify the License](https://www.dynamsoft.com/capture-vision/docs/web/programming/javascript/user-guide/mrz-scanner.html#specify-the-license) or contact [support@dynamsoft.com](mailto:support@dynamsoft.com). ## Project Structure diff --git a/js/init.js b/js/init.js index 678e1a0..2edd035 100644 --- a/js/init.js +++ b/js/init.js @@ -11,7 +11,7 @@ Dynamsoft.License.LicenseManager.initLicense( "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAwLTEwMzAwNjk2NyIsIm1haW5TZXJ2ZXJVUkwiOiJodHRwczovL21sdHMuZHluYW1zb2Z0LmNvbS8iLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMCIsInNlc3Npb25QYXNzd29yZCI6IkVUSHZVNlNPV3F3ZiIsInN0YW5kYnlTZXJ2ZXJVUkwiOiJodHRwczovL3NsdHMuZHluYW1zb2Z0LmNvbS8iLCJjaGVja0NvZGUiOjM5OTMzODU2Nn0=" ); /** - * You can visit https://www.dynamsoft.com/customer/license/trialLicense/?product=mrz&utm_source=github&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=samples&package=js to get your own trial license good for 30 days. * For more information, see https://www.dynamsoft.com/capture-vision/docs/web/programming/javascript/user-guide/mrz-scanner.html#specify-the-license or contact support@dynamsoft.com. * LICENSE ALERT - THE END */ From 4682c23c7cc9be2e2fb1200457843d8ea11c0c23 Mon Sep 17 00:00:00 2001 From: felixindynamsoft Date: Tue, 19 Nov 2024 23:41:52 -0800 Subject: [PATCH 21/21] feat: add chat icon --- css/index.css | 31 +++++++++++++++++++++++++++---- index.html | 35 +++++++++++++++++++++++++++++++++++ js/init.js | 4 +--- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/css/index.css b/css/index.css index 58bdfbb..bbce8ba 100644 --- a/css/index.css +++ b/css/index.css @@ -215,8 +215,8 @@ img { } .scanner-container .header .music-container { - width: 30px; - height: 30px; + width: 24px; + height: 24px; cursor: pointer; display: flex; align-items: center; @@ -239,8 +239,8 @@ img { } .scanner-container .header .upload-img-btn { - width: 30px; - height: 30px; + width: 24px; + height: 24px; cursor: pointer; display: flex; align-items: center; @@ -528,6 +528,20 @@ img { } } +/* LIVE CHAT */ +.scanner-container .header .live-chat { + width: 24px; + height: 24px; + cursor: pointer; + display: none; + align-items: center; + cursor: pointer; +} + +.scanner-container .header .live-chat:hover { + opacity: 0.8; +} + @media screen and (max-width: 800px) { html, body, @@ -553,6 +567,15 @@ img { font-size: 20px; padding: 1.5rem; } + + /* LIVE CHAT CSS */ + .live-chat { + display: flex !important; + } + + #comm100-float-button-20242b05-3781-4d86-9b7f-fab63ddcdde3-2 { + display: none !important; + } } @media screen and (max-width: 800px) and (orientation: landscape) { diff --git a/index.html b/index.html index c3df411..7e85d12 100644 --- a/index.html +++ b/index.html @@ -82,6 +82,41 @@

Quick Start Options

music no-music
+
+ + + + + + + + + + +