smart-id-scanner
Version:
A complete vanilla JavaScript ID card scanner with camera integration, auto-capture, and smart detection features
3 lines (2 loc) âĸ 34.8 kB
JavaScript
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).CardScanner=n()}(this,function(){"use strict";function e(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var n={exports:{}};return function(e){class n{constructor(e,n={}){if(this.config={showLabel:!0,label:"Upload ID Card",required:!1,disabled:!1,showImage:!0,isHover:!1,hoverValue:null,showButton:!0,labelInsideButton:!1,showIcon:!0,enableGallery:!0,enableAutoCapture:!0,themeColor:"#1976d2",maxFileSize:1e5,compressionOptions:{maxSizeMB:.05,maxWidthOrHeight:1280,useWebWorker:!0},onFileChange:null,onCancel:null,onError:null,onSuccess:null,...n},this.container="string"==typeof e?document.querySelector(e):e,!this.container)throw new Error("Container element not found");this.state={value:null,anchorEl:null,dropdownAnchorEl:null,dialogOpen:!1,cameraOpen:!1,countdown:null,feedback:null,capturedImage:null,showPreview:!1,isMobile:!1,isAnalyzing:!1,isReady:!1,stream:null},this.videoRef=null,this.canvasRef=null,this.fileInputRef=null,this.countdownTimeoutRef=null,this.detectionIntervalRef=null,this.stableFramesRef=0,this.lastCaptureTimeRef=0,this.autoCapturePendingRef=!1,this.requiredStableFrames=3,this.baseGuidance="Position your ID card within the green frame. Make sure all corners are visible and text is clear.",this.dynamicGuidance="",this.init()}init(){this.injectStyles(),this.detectMobile(),this.setupEventListeners(),this.render()}injectStyles(){if(document.getElementById("card-scanner-styles"))return;const e=document.createElement("style");e.id="card-scanner-styles",e.textContent=this.getStyles(),document.head.appendChild(e)}getStyles(){return`\n .card-scanner {\n width: 100%;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;\n }\n\n .card-scanner-label {\n color: black;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 500;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .card-scanner-required {\n color: red;\n }\n\n .card-scanner-button {\n width: 100%;\n padding: 12px 16px;\n border: 1px solid ${this.config.themeColor};\n background-color: white;\n color: ${this.config.themeColor};\n border-radius: 8px;\n font-size: 14px;\n cursor: pointer;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n }\n\n .card-scanner-button:hover:not(:disabled) {\n background-color: #f1f1f1;\n }\n\n .card-scanner-button:disabled {\n background-color: white;\n border-color: rgba(0, 0, 0, 0.26);\n cursor: not-allowed;\n opacity: 0.6;\n }\n\n .card-scanner-image-container {\n position: relative;\n margin-top: 5px;\n }\n\n .card-scanner-image {\n width: 100%;\n height: auto;\n cursor: pointer;\n border-radius: 4px;\n }\n\n .card-scanner-remove {\n position: absolute;\n top: -10px;\n right: -10px;\n background: white;\n border: 2px solid #ff4444;\n border-radius: 50%;\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n font-size: 12px;\n color: #ff4444;\n font-weight: bold;\n }\n\n .card-scanner-dropdown {\n position: absolute;\n background: white;\n border-radius: 12px;\n box-shadow: 0 8px 24px rgba(0,0,0,0.15);\n min-width: 200px;\n z-index: 1000;\n overflow: hidden;\n }\n\n .card-scanner-dropdown-option {\n padding: 12px;\n cursor: pointer;\n font-size: 16px;\n transition: background-color 0.2s ease;\n }\n\n .card-scanner-dropdown-option:hover {\n background-color: #f5f5f5;\n }\n\n .card-scanner-dropdown-option:first-child {\n border-bottom: 1px solid #f0f0f0;\n }\n\n .card-scanner-dialog {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.8);\n z-index: 9998;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n\n .card-scanner-dialog-content {\n background: white;\n border-radius: 8px;\n max-width: 90vw;\n max-height: 90vh;\n position: relative;\n overflow: hidden;\n }\n\n .card-scanner-dialog-close {\n position: absolute;\n top: 8px;\n right: 8px;\n background: white;\n border: none;\n border-radius: 50%;\n width: 32px;\n height: 32px;\n cursor: pointer;\n font-size: 18px;\n z-index: 1;\n }\n\n .card-scanner-camera-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.9);\n z-index: 9999;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n }\n\n .card-scanner-video-container {\n position: relative;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n }\n\n .card-scanner-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.3s ease;\n }\n\n .card-scanner-overlay {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 85%;\n max-width: 350px;\n aspect-ratio: 1.6;\n border: 3px solid #00ff00;\n border-radius: 12px;\n background: transparent;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 255, 0, 0.3);\n z-index: 1;\n transition: all 0.3s ease;\n }\n\n .card-scanner-overlay.analyzing {\n border-color: #ffaa00;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6), inset 0 0 20px rgba(255, 170, 0, 0.3), 0 0 15px #ffaa00;\n }\n\n .card-scanner-overlay.ready {\n border-color: #00ff00;\n box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7), inset 0 0 25px rgba(0, 255, 0, 0.4), 0 0 25px #00ff00;\n }\n\n .card-scanner-guidance {\n position: absolute;\n top: 8%;\n left: 50%;\n transform: translateX(-50%);\n color: white;\n text-align: center;\n padding: 16px 20px;\n background-color: rgba(0, 0, 0, 0.8);\n border-radius: 12px;\n font-size: 13px;\n font-weight: 500;\n width: 90%;\n max-width: 320px;\n transition: all 0.3s ease;\n backdrop-filter: blur(10px);\n line-height: 1.4;\n }\n\n .card-scanner-controls {\n position: absolute;\n bottom: 60px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n justify-content: center;\n align-items: center;\n }\n\n .card-scanner-capture-button {\n width: 75px;\n height: 75px;\n border-radius: 50%;\n border: 4px solid white;\n background-color: transparent;\n cursor: pointer;\n display: flex;\n justify-content: center;\n align-items: center;\n transition: all 0.3s ease;\n }\n\n .card-scanner-capture-button:active {\n transform: scale(0.92);\n }\n\n .card-scanner-capture-button.active {\n background-color: #00aa00;\n border-color: #00ff00;\n box-shadow: 0 0 20px rgba(0, 255, 0, 0.6);\n }\n\n .card-scanner-capture-inner {\n width: 55px;\n height: 55px;\n border-radius: 50%;\n background-color: white;\n transition: all 0.3s ease;\n }\n\n .card-scanner-countdown {\n position: absolute;\n top: 45%;\n left: 50%;\n transform: translate(-50%, -50%);\n font-size: 64px;\n font-weight: bold;\n color: #fff;\n text-shadow: 0 0 30px rgba(255,255,255,0.9);\n pointer-events: none;\n opacity: 0;\n transition: all 0.4s ease;\n }\n\n .card-scanner-countdown.show {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1.1);\n }\n\n .card-scanner-feedback {\n position: absolute;\n top: 18%;\n left: 5%;\n right: 5%;\n text-align: center;\n color: #fff;\n font-size: 14px;\n font-weight: 600;\n padding: 12px 16px;\n border-radius: 10px;\n backdrop-filter: blur(15px);\n z-index: 10;\n opacity: 0;\n transform: translateY(-30px);\n transition: all 0.4s ease;\n }\n\n .card-scanner-feedback.show {\n opacity: 1;\n transform: translateY(0);\n }\n\n .card-scanner-feedback.success {\n background-color: rgba(34, 197, 94, 0.9);\n border: 2px solid rgba(34, 197, 94, 0.8);\n }\n\n .card-scanner-feedback.warning {\n background-color: rgba(251, 146, 60, 0.9);\n border: 2px solid rgba(251, 146, 60, 0.8);\n }\n\n .card-scanner-feedback.error {\n background-color: rgba(239, 68, 68, 0.9);\n border: 2px solid rgba(239, 68, 68, 0.8);\n }\n\n .card-scanner-feedback.info {\n background-color: rgba(59, 130, 246, 0.9);\n border: 2px solid rgba(59, 130, 246, 0.8);\n }\n\n .card-scanner-close-button {\n position: absolute;\n top: 40px;\n right: 20px;\n color: white;\n font-size: 24px;\n cursor: pointer;\n z-index: 2;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background-color: rgba(0,0,0,0.7);\n display: flex;\n align-items: center;\n justify-content: center;\n backdrop-filter: blur(10px);\n border: none;\n }\n\n .card-scanner-preview {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.95);\n z-index: 10000;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n padding: 20px;\n }\n\n .card-scanner-preview-title {\n position: absolute;\n top: 40px;\n left: 50%;\n transform: translateX(-50%);\n color: white;\n font-size: 20px;\n font-weight: bold;\n text-align: center;\n }\n\n .card-scanner-preview-image {\n width: 90%;\n max-width: 400px;\n aspect-ratio: 1.6;\n background-color: white;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 10px 30px rgba(0,0,0,0.5);\n margin-bottom: 40px;\n }\n\n .card-scanner-preview-image img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .card-scanner-preview-buttons {\n display: flex;\n gap: 20px;\n width: 100%;\n max-width: 400px;\n }\n\n .card-scanner-preview-button {\n flex: 1;\n padding: 15px 20px;\n font-size: 16px;\n font-weight: bold;\n border-radius: 12px;\n cursor: pointer;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n }\n\n .card-scanner-preview-button.retake {\n border: 2px solid white;\n background-color: transparent;\n color: white;\n }\n\n .card-scanner-preview-button.retake:hover {\n background-color: rgba(255,255,255,0.1);\n }\n\n .card-scanner-preview-button.upload {\n border: none;\n background-color: #22c55e;\n color: white;\n box-shadow: 0 4px 15px rgba(34, 197, 94, 0.4);\n }\n\n .card-scanner-preview-button.upload:hover {\n background-color: #16a34a;\n transform: translateY(-1px);\n }\n\n .card-scanner-hidden {\n position: absolute;\n opacity: 0;\n pointer-events: none;\n width: 1px;\n height: 1px;\n }\n\n /* Responsive styles */\n (max-width: 480px) {\n .card-scanner-guidance {\n width: 95%;\n max-width: 260px;\n font-size: 12px;\n padding: 12px 16px;\n }\n \n .card-scanner-capture-button {\n width: 65px;\n height: 65px;\n }\n \n .card-scanner-capture-inner {\n width: 45px;\n height: 45px;\n }\n \n .card-scanner-overlay {\n width: 80%;\n max-width: 280px;\n }\n \n .card-scanner-controls {\n bottom: 40px;\n }\n }\n\n (max-width: 360px) {\n .card-scanner-guidance {\n width: 95%;\n max-width: 240px;\n font-size: 11px;\n padding: 10px 14px;\n }\n \n .card-scanner-overlay {\n width: 82%;\n max-width: 260px;\n }\n }\n\n (min-width: 768px) {\n .card-scanner-guidance {\n max-width: 320px;\n font-size: 14px;\n }\n \n .card-scanner-overlay {\n max-width: 320px;\n }\n }\n `}detectMobile(){const e=()=>{const e=navigator.userAgent,n=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e)||window.innerWidth<=768;this.state.isMobile=n};e(),window.addEventListener("resize",e)}setupEventListeners(){this.handleCountdown=e=>{this.countdownTimeoutRef&&(clearTimeout(this.countdownTimeoutRef),this.countdownTimeoutRef=null),this.state.countdown=e.detail.count,this.updateCountdownDisplay(),this.countdownTimeoutRef=setTimeout(()=>{this.state.countdown=null,this.updateCountdownDisplay(),this.countdownTimeoutRef=null},1e3)},this.handleHideCountdown=()=>{this.countdownTimeoutRef&&(clearTimeout(this.countdownTimeoutRef),this.countdownTimeoutRef=null),this.state.countdown=null,this.updateCountdownDisplay()},this.handleFeedback=e=>{this.state.feedback={message:e.detail.message,type:e.detail.type},this.updateFeedbackDisplay(),setTimeout(()=>{this.state.feedback=null,this.updateFeedbackDisplay()},3e3)},window.addEventListener("showCountdown",this.handleCountdown),window.addEventListener("hideCountdown",this.handleHideCountdown),window.addEventListener("showCameraFeedback",this.handleFeedback)}render(){this.container.innerHTML="",this.container.className="card-scanner",this.config.showLabel&&this.renderLabel(),this.renderUploadButton(),this.renderImage(),this.createHiddenElements()}renderLabel(){const e=document.createElement("div");e.className="card-scanner-label";const n=document.createElement("span");if(n.textContent=this.config.label,this.config.required){const e=document.createElement("span");e.className="card-scanner-required",e.textContent="*",n.appendChild(e)}if(e.appendChild(n),this.config.isHover&&this.config.hoverValue){const n=document.createElement("span");n.textContent="âšī¸",n.style.cursor="pointer",n.title=this.config.hoverValue,e.appendChild(n)}this.container.appendChild(e)}renderUploadButton(){if(!this.config.showButton&&this.state.value?.file)return;const e=document.createElement("button");e.className="card-scanner-button",e.disabled=this.config.disabled,this.config.labelInsideButton?e.textContent=this.config.label:this.config.showIcon?e.innerHTML="đ Choose File":e.textContent="Upload",this.state.isMobile&&this.config.enableGallery?e.addEventListener("click",e=>this.handleDropdownToggle(e)):e.addEventListener("click",()=>this.handleChooseFromGallery()),this.container.appendChild(e)}renderImage(){if(!this.state.value?.file||!this.config.showImage)return;const e=document.createElement("div");e.className="card-scanner-image-container";const n=document.createElement("img");n.className="card-scanner-image",n.src=`data:image/png;base64,${this.state.value.file}`,n.alt=this.config.label,n.addEventListener("click",()=>this.openDialog());const t=document.createElement("div");t.className="card-scanner-remove",t.textContent="â",t.addEventListener("click",()=>this.handleRemoveFile()),e.appendChild(n),e.appendChild(t),this.container.appendChild(e)}createHiddenElements(){this.fileInputRef=document.createElement("input"),this.fileInputRef.type="file",this.fileInputRef.accept="image/*",this.fileInputRef.className="card-scanner-hidden",this.fileInputRef.addEventListener("change",e=>this.handleFileChange(e)),this.container.appendChild(this.fileInputRef),this.videoRef=document.createElement("video"),this.videoRef.autoplay=!0,this.videoRef.playsInline=!0,this.videoRef.muted=!0,this.videoRef.className="card-scanner-hidden",this.canvasRef=document.createElement("canvas"),this.canvasRef.className="card-scanner-hidden",this.container.appendChild(this.videoRef),this.container.appendChild(this.canvasRef)}handleDropdownToggle(e){if(this.state.dropdownAnchorEl)return void this.closeDropdown();const n=document.createElement("div");n.className="card-scanner-dropdown";const t=e.target.getBoundingClientRect();n.style.top=`${t.bottom+5}px`,n.style.left=`${t.left}px`;const a=document.createElement("div");a.className="card-scanner-dropdown-option",a.textContent="Take Photo",a.addEventListener("click",()=>{this.closeDropdown(),this.handleTakePhoto()});const i=document.createElement("div");i.className="card-scanner-dropdown-option",i.textContent="Choose from Gallery",i.addEventListener("click",()=>{this.closeDropdown(),this.handleChooseFromGallery()}),n.appendChild(a),n.appendChild(i),document.body.appendChild(n),this.state.dropdownAnchorEl=n,setTimeout(()=>{document.addEventListener("click",this.handleOutsideClick.bind(this),{once:!0})},0)}handleOutsideClick(e){this.state.dropdownAnchorEl&&!this.state.dropdownAnchorEl.contains(e.target)&&this.closeDropdown()}closeDropdown(){this.state.dropdownAnchorEl&&(this.state.dropdownAnchorEl.remove(),this.state.dropdownAnchorEl=null)}async handleTakePhoto(){this.state.cameraOpen=!0,this.renderCamera();try{await this.startCamera()}catch(e){this.showFeedback("Camera access failed. Please check permissions.","error"),this.config.onError?.(e)}}handleChooseFromGallery(){this.fileInputRef.click()}async handleFileChange(e){const n=e.target.files?.[0];if(n)try{await this.handleImageUpload(n)}catch(e){this.config.onError?.(e)}e.target.value=""}async handleImageUpload(e){try{let n=e;e.size>this.config.maxFileSize&&(n=await this.compressImage(e));const t=await this.processImage(n);this.state.value=t,this.config.onFileChange?.(t),this.config.onSuccess?.("File uploaded successfully"),this.render()}catch(e){this.config.onError?.(e)}}async compressImage(e){return new Promise(n=>{const t=document.createElement("canvas"),a=t.getContext("2d"),i=new Image;i.onload=()=>{const o=this.config.compressionOptions.maxWidthOrHeight,s=this.config.compressionOptions.maxWidthOrHeight;let{width:r,height:d}=i;r>d?r>o&&(d=d*o/r,r=o):d>s&&(r=r*s/d,d=s),t.width=r,t.height=d,a.drawImage(i,0,0,r,d),t.toBlob(t=>{const a=new File([t],e.name,{type:"image/jpeg",lastModified:Date.now()});n(a)},"image/jpeg",.8)},i.src=URL.createObjectURL(e)})}async processImage(e){return new Promise(n=>{const t=new FileReader;t.onloadend=()=>{const a=t.result.split(",")[1];n({file:a,name:e.name,type:e.type})},t.readAsDataURL(e)})}handleRemoveFile(){this.state.value=null,this.config.onCancel?.(),this.config.onFileChange?.(null),this.render()}openDialog(){if(!this.state.value?.file)return;const e=document.createElement("div");e.className="card-scanner-dialog";const n=document.createElement("div");n.className="card-scanner-dialog-content";const t=document.createElement("button");t.className="card-scanner-dialog-close",t.textContent="â",t.addEventListener("click",()=>{e.remove()});const a=document.createElement("img");a.src=`data:image/png;base64,${this.state.value.file}`,a.alt=this.config.label,a.style.width="100%",a.style.height="auto",n.appendChild(t),n.appendChild(a),e.appendChild(n),e.addEventListener("click",n=>{n.target===e&&e.remove()}),document.body.appendChild(e)}renderCamera(){if(!this.state.cameraOpen)return;const e=document.createElement("div");e.className="card-scanner-camera-overlay";const n=document.createElement("button");n.className="card-scanner-close-button",n.textContent="â",n.addEventListener("click",()=>this.handleCloseCamera());const t=document.createElement("div");t.className="card-scanner-video-container",this.videoRef.className="card-scanner-video";const a=document.createElement("div");a.className="card-scanner-overlay",this.overlayElement=a;const i=document.createElement("div");i.className="card-scanner-guidance",i.textContent=this.baseGuidance,this.guidanceElement=i;const o=document.createElement("div");o.className="card-scanner-countdown",this.countdownElement=o;const s=document.createElement("div");s.className="card-scanner-feedback",this.feedbackElement=s;const r=document.createElement("div");r.className="card-scanner-controls";const d=document.createElement("div");d.className="card-scanner-capture-button",d.addEventListener("click",()=>this.handleManualCapture());const c=document.createElement("div");c.className="card-scanner-capture-inner",d.appendChild(c),r.appendChild(d),this.captureButtonElement=d,t.appendChild(this.videoRef),t.appendChild(a),t.appendChild(i),t.appendChild(o),t.appendChild(s),t.appendChild(r),e.appendChild(n),e.appendChild(t),e.appendChild(this.canvasRef),document.body.appendChild(e),this.cameraOverlayElement=e}async startCamera(){try{this.state.stream&&this.state.stream.getTracks().forEach(e=>e.stop());const e={video:{facingMode:"environment",width:{ideal:1920,min:1280},height:{ideal:1080,min:720}}},n=await navigator.mediaDevices.getUserMedia(e);this.videoRef.srcObject=n,this.state.stream=n,this.videoRef.addEventListener("loadedmetadata",()=>{this.canvasRef.width=this.videoRef.videoWidth,this.canvasRef.height=this.videoRef.videoHeight,this.config.enableAutoCapture&&setTimeout(()=>this.startDetection(),2e3)})}catch(e){throw e}}stopCamera(){this.state.stream&&(this.state.stream.getTracks().forEach(e=>e.stop()),this.state.stream=null),this.stopDetection()}startDetection(){this.config.enableAutoCapture&&(this.stopDetection(),this.videoRef.videoWidth&&this.videoRef.videoHeight&&(this.detectionIntervalRef=setInterval(()=>{this.checkForIDCard()},300)))}stopDetection(){this.detectionIntervalRef&&(clearInterval(this.detectionIntervalRef),this.detectionIntervalRef=null),this.resetDetectionState()}resetDetectionState(){this.stableFramesRef=0,this.autoCapturePendingRef=!1,this.state.isAnalyzing=!1,this.state.isReady=!1,this.dynamicGuidance="",this.updateOverlayState()}checkForIDCard(){const e=this.videoRef,n=this.canvasRef;if(e&&n&&!this.autoCapturePendingRef&&e.videoWidth&&e.videoHeight)try{const t=n.getContext("2d");if(!t)return;t.drawImage(e,0,0,n.width,n.height);const a=this.smartIDDetection(t,n);a.cardDetected?(this.stableFramesRef++,this.state.isAnalyzing=!0,this.dynamicGuidance=`ID detected! Hold steady... (${this.requiredStableFrames-this.stableFramesRef} more frames)`,this.stableFramesRef>=this.requiredStableFrames&&this.triggerAutoCapture()):(this.stableFramesRef>0&&this.resetDetectionState(),this.dynamicGuidance=a.guidance||""),this.updateGuidanceText(),this.updateOverlayState()}catch(e){this.resetDetectionState()}}smartIDDetection(e,n){const t=Math.floor(.1*n.width),a=Math.floor(.3*n.height),i=Math.floor(.8*n.width),o=Math.floor(i/1.6);if(i<=0||o<=0)return{cardDetected:!1,quality:"Invalid area",guidance:"Camera not ready",detectionScore:0};try{const n=e.getImageData(t,a,i,o).data;let s=0,r="";const d=this.analyzeTextPatterns(n,i,o),c=this.detectOfficialColors(n,i,o);s=(d+c+this.analyzeIDLayout(n,i,o))/3;const l=s>=50;return r=l?"ID card detected! Hold steady for capture...":d<30?"Ensure ID text and details are clear":"Adjust position for clearer ID features",{cardDetected:l,quality:l?"ID detected":"Not an ID card",guidance:r,detectionScore:s}}catch(e){return{cardDetected:!1,quality:"Detection error",guidance:"Please try again",detectionScore:0}}}analyzeTextPatterns(e,n,t){let a=0;for(let i=0;i<t-2;i+=2){let t=0;for(let a=0;a<n-2;a+=2){const o=4*(i*n+a),s=(e[o]+e[o+1]+e[o+2])/3,r=o+8;if(r<e.length){const n=(e[r]+e[r+1]+e[r+2])/3;Math.abs(s-n)>50&&t++}}t>n/20&&a++}return Math.min(a/10*100,100)}detectOfficialColors(e,n,t){let a=0,i=0;for(let n=0;n<e.length;n+=16){const t=e[n],o=e[n+1],s=e[n+2];i++,(s>t&&s>o&&s>100||t>150&&o<100&&s<100)&&a++}return Math.min(a/i*200,100)}analyzeIDLayout(e,n,t){let a=0,i=0;const o=Math.floor(n/2);for(let s=0;s<t;s+=4)for(let t=0;t<n;t+=4){const r=4*(s*n+t),d=(e[r]+e[r+1]+e[r+2])/3,c=Math.abs(d-128);t<o?a+=c:i+=c}return Math.min((a+i)/2e3,100)}async triggerAutoCapture(){if(this.autoCapturePendingRef)return;if(Date.now()-this.lastCaptureTimeRef<3e3)return;this.autoCapturePendingRef=!0,this.state.isReady=!0,this.dynamicGuidance="Perfect! Auto-capturing...",this.updateGuidanceText(),this.updateOverlayState();if(!await this.showCountdownWithDetection(3))return this.autoCapturePendingRef=!1,this.resetDetectionState(),window.dispatchEvent(new CustomEvent("hideCountdown")),void(this.state.stream&&this.config.enableAutoCapture&&setTimeout(()=>this.startDetection(),100));this.stopDetection();try{const e=await this.captureImage();e&&this.handleCapturedImage(e)}catch(e){this.showFeedback("Auto-capture failed","error")}finally{this.lastCaptureTimeRef=Date.now(),this.autoCapturePendingRef=!1,this.resetDetectionState()}}showCountdownWithDetection(e){return new Promise(n=>{let t,a,i=e,o=!1,s=0;t=setInterval(()=>{const e=this.videoRef,i=this.canvasRef;if(!e||!i||o)return clearInterval(t),void n(!1);try{const r=i.getContext("2d");if(!r)return;r.drawImage(e,0,0,i.width,i.height);this.smartIDDetection(r,i).cardDetected?s=0:(s++,s>=3&&(o=!0,clearInterval(t),a&&clearTimeout(a),window.dispatchEvent(new CustomEvent("hideCountdown")),n(!1)))}catch(e){s++,s>=3&&(o=!0,clearInterval(t),a&&clearTimeout(a),window.dispatchEvent(new CustomEvent("hideCountdown")),n(!1))}},300);const r=()=>{if(o)return clearInterval(t),void n(!1);i>0?(window.dispatchEvent(new CustomEvent("showCountdown",{detail:{count:i}})),a=setTimeout(()=>{i--,o||r()},1e3)):(clearInterval(t),n(!0))};r()})}async captureImage(){return new Promise(e=>{if(!this.videoRef||!this.canvasRef)return void e(null);const n=this.canvasRef,t=this.videoRef;if(!n.getContext("2d"))return void e(null);navigator.vibrate&&navigator.vibrate(100);const a=t.videoWidth,i=.85*a,o=i/1.6,s=(a-i)/2,r=(t.videoHeight-o)/2,d=document.createElement("canvas");d.width=i,d.height=o;const c=d.getContext("2d");c?(c.drawImage(t,s,r,i,o,0,0,i,o),d.toBlob(n=>{if(n){const t=new File([n],"camera-capture.jpg",{type:"image/jpeg"});e(t)}else e(null)},"image/jpeg",.92)):e(null)})}async handleManualCapture(){try{const e=await this.captureImage();e&&this.handleCapturedImage(e)}catch(e){this.showFeedback("Capture failed","error")}}handleCapturedImage(e){const n=new FileReader;n.onloadend=()=>{const e=n.result;this.state.capturedImage=e,this.state.showPreview=!0,this.handleCloseCamera(),this.renderPreview()},n.readAsDataURL(e)}handleCloseCamera(){this.state.cameraOpen=!1,this.stopCamera(),this.cameraOverlayElement&&(this.cameraOverlayElement.remove(),this.cameraOverlayElement=null)}renderPreview(){if(!this.state.showPreview||!this.state.capturedImage)return;const e=document.createElement("div");e.className="card-scanner-preview";const n=document.createElement("div");n.className="card-scanner-preview-title",n.textContent="ID Card Captured";const t=document.createElement("div");t.className="card-scanner-preview-image";const a=document.createElement("img");a.src=this.state.capturedImage,a.alt="Captured ID",t.appendChild(a);const i=document.createElement("div");i.className="card-scanner-preview-buttons";const o=document.createElement("button");o.className="card-scanner-preview-button retake",o.textContent="Retake",o.addEventListener("click",()=>this.handleRetake());const s=document.createElement("button");s.className="card-scanner-preview-button upload",s.textContent="Upload ID",s.addEventListener("click",()=>this.handleUploadCaptured()),i.appendChild(o),i.appendChild(s),e.appendChild(n),e.appendChild(t),e.appendChild(i),document.body.appendChild(e),this.previewElement=e}handleRetake(){this.state.capturedImage=null,this.state.showPreview=!1,this.previewElement&&(this.previewElement.remove(),this.previewElement=null),setTimeout(()=>{this.handleTakePhoto()},100)}async handleUploadCaptured(){if(this.state.capturedImage)try{const e=await fetch(this.state.capturedImage),n=await e.blob(),t=new File([n],"captured-id.jpg",{type:"image/jpeg"});await this.handleImageUpload(t),this.state.capturedImage=null,this.state.showPreview=!1,this.previewElement&&(this.previewElement.remove(),this.previewElement=null),this.showFeedback("ID uploaded successfully!","success")}catch(e){this.showFeedback("Upload failed","error"),this.config.onError?.(e)}}showFeedback(e,n){window.dispatchEvent(new CustomEvent("showCameraFeedback",{detail:{message:e,type:n}}))}updateCountdownDisplay(){this.countdownElement&&(this.state.countdown?(this.countdownElement.textContent=this.state.countdown,this.countdownElement.classList.add("show")):this.countdownElement.classList.remove("show"))}updateFeedbackDisplay(){this.feedbackElement&&(this.state.feedback?(this.feedbackElement.textContent=this.state.feedback.message,this.feedbackElement.className=`card-scanner-feedback show ${this.state.feedback.type}`):this.feedbackElement.className="card-scanner-feedback")}updateGuidanceText(){this.guidanceElement&&(this.guidanceElement.textContent=this.dynamicGuidance||this.baseGuidance)}updateOverlayState(){this.overlayElement&&(this.overlayElement.className="card-scanner-overlay",this.state.isAnalyzing&&this.overlayElement.classList.add("analyzing"),this.state.isReady&&this.overlayElement.classList.add("ready")),this.captureButtonElement&&(this.state.isReady?this.captureButtonElement.classList.add("active"):this.captureButtonElement.classList.remove("active"))}getValue(){return this.state.value}setValue(e){this.state.value=e,this.render()}reset(){this.state.value=null,this.render()}disable(){this.config.disabled=!0,this.render()}enable(){this.config.disabled=!1,this.render()}destroy(){window.removeEventListener("showCountdown",this.handleCountdown),window.removeEventListener("hideCountdown",this.handleHideCountdown),window.removeEventListener("showCameraFeedback",this.handleFeedback),this.stopCamera(),this.countdownTimeoutRef&&clearTimeout(this.countdownTimeoutRef),this.cameraOverlayElement&&this.cameraOverlayElement.remove(),this.previewElement&&this.previewElement.remove(),this.state.dropdownAnchorEl&&this.state.dropdownAnchorEl.remove(),this.container.innerHTML=""}}e.exports&&(e.exports=n),"undefined"!=typeof window&&(window.CardScanner=n)}(n),e(n.exports)});
//# sourceMappingURL=index.js.map