TKK_E32230273/scanning.php

428 lines
19 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, user-scalable=0">
<title>Door Scanner - Identia</title>
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
<link rel="shortcut icon" href="images/myn.png" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #0f0f1a; color: #fff;
min-height: 100vh; overflow: hidden; position: relative;
}
.camera-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
.camera-container video { width: 100%; height: 100%; object-fit: cover; }
.scan-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
z-index: 10; pointer-events: none;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.top-bar {
position: fixed; top: 0; left: 0; right: 0; z-index: 20;
padding: 16px 20px; display: flex; align-items: center; justify-content: center;
background: linear-gradient(to bottom, rgba(0,0,0,0.7) 0%, transparent 100%);
}
.top-bar-title {
font-size: 15px; font-weight: 700; color: #fff;
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
display: flex; align-items: center; gap: 8px;
}
.top-bar-title i { font-size: 20px; color: #a78bfa; }
.scan-frame { width: 280px; height: 180px; position: relative; border-radius: 20px; }
.scan-frame .corner { position: absolute; width: 36px; height: 36px; border: 4px solid #a78bfa; }
.scan-frame .corner.tl { top: 0; left: 0; border-right: none; border-bottom: none; border-radius: 12px 0 0 0; }
.scan-frame .corner.tr { top: 0; right: 0; border-left: none; border-bottom: none; border-radius: 0 12px 0 0; }
.scan-frame .corner.bl { bottom: 0; left: 0; border-right: none; border-top: none; border-radius: 0 0 0 12px; }
.scan-frame .corner.br { bottom: 0; right: 0; border-left: none; border-top: none; border-radius: 0 0 12px 0; }
.scan-line {
position: absolute; left: 8px; right: 8px; height: 3px;
background: linear-gradient(90deg, transparent, #a78bfa, #7c3aed, #a78bfa, transparent);
border-radius: 2px; animation: scanMove 2s ease-in-out infinite;
box-shadow: 0 0 15px rgba(167, 139, 250, 0.6);
}
@keyframes scanMove { 0%, 100% { top: 8px; } 50% { top: calc(100% - 11px); } }
.status-pill {
position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%);
z-index: 20; display: flex; align-items: center; gap: 10px;
padding: 12px 24px; border-radius: 50px;
background: rgba(255,255,255,0.12); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.15);
font-size: 13px; font-weight: 600; color: #fff;
transition: all 0.4s ease; white-space: nowrap;
}
.status-pill i { font-size: 18px; }
.status-pill.scanning i { color: #a78bfa; animation: pulse 1.5s infinite; }
.status-pill.success { background: rgba(16,185,129,0.3); border-color: rgba(16,185,129,0.4); }
.status-pill.success i { color: #34d399; animation: none; }
.status-pill.error { background: rgba(239,68,68,0.3); border-color: rgba(239,68,68,0.4); }
.status-pill.error i { color: #f87171; animation: none; }
.status-pill.warning { background: rgba(245,158,11,0.3); border-color: rgba(245,158,11,0.4); }
.status-pill.warning i { color: #fbbf24; animation: none; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.bottom-hint {
position: fixed; bottom: 40px; left: 50%; transform: translateX(-50%);
z-index: 20; font-size: 12px; color: rgba(255,255,255,0.5); text-align: center;
}
.loading-overlay {
position: fixed; inset: 0; z-index: 50; background: #0f0f1a;
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 20px; transition: opacity 0.5s ease;
}
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
.loading-spinner {
width: 48px; height: 48px; border-radius: 50%;
border: 3px solid rgba(167,139,250,0.2); border-top-color: #a78bfa;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { font-size: 14px; color: rgba(255,255,255,0.6); font-weight: 600; }
.error-overlay {
position: fixed; inset: 0; z-index: 50; background: #f8f9fa;
display: none; flex-direction: column; align-items: center;
justify-content: center; text-align: center; padding: 40px;
}
.error-overlay .err-icon {
width: 80px; height: 80px; border-radius: 50%;
background: #fef2f2; border: 1px solid #fecaca;
display: flex; align-items: center; justify-content: center; margin-bottom: 20px;
}
.error-overlay .err-icon i { font-size: 40px; color: #ef4444; }
.error-overlay h3 { font-size: 20px; font-weight: 800; color: #1e293b; margin-bottom: 8px; }
.error-overlay p { font-size: 14px; color: #64748b; line-height: 1.6; max-width: 300px; }
#scanCanvas { display: none; }
</style>
</head>
<body>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
<div class="loading-text">Initializing camera...</div>
</div>
<div class="error-overlay" id="errorOverlay">
<div class="err-icon"><i class="mdi mdi-camera-off"></i></div>
<h3>Camera Access Failed</h3>
<p id="errorMsg">Unable to access the camera. Please allow camera permissions.</p>
</div>
<div class="top-bar">
<div class="top-bar-title">
<i class="mdi mdi-shield-lock-outline"></i>
Identia Door Scanner
</div>
</div>
<div class="camera-container">
<video id="cameraVideo" autoplay playsinline muted></video>
</div>
<div class="scan-overlay">
<div class="scan-frame">
<div class="corner tl"></div>
<div class="corner tr"></div>
<div class="corner bl"></div>
<div class="corner br"></div>
<div class="scan-line"></div>
</div>
</div>
<div class="status-pill scanning" id="statusPill">
<i class="mdi mdi-line-scan" id="statusIcon"></i>
<span id="statusText">Point camera at door code...</span>
</div>
<div class="bottom-hint">Only works with original printed Identia Door Code</div>
<canvas id="scanCanvas"></canvas>
<script>
(function() {
'use strict';
const video = document.getElementById('cameraVideo');
const canvas = document.getElementById('scanCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const loadingOverlay = document.getElementById('loadingOverlay');
const errorOverlay = document.getElementById('errorOverlay');
const errorMsg = document.getElementById('errorMsg');
const statusPill = document.getElementById('statusPill');
const statusIcon = document.getElementById('statusIcon');
const statusText = document.getElementById('statusText');
let scanning = true;
let detected = false;
let consecutiveDetections = 0;
const REQUIRED_DETECTIONS = 6;
let lastTokenTime = 0;
const RATE_LIMIT_MS = 10000; // 10 sec between scans
// ═══════════════════════════════════════════════════════════
// ULTRA-COMPLEX 61-ELEMENT BAR-WIDTH SIGNATURE
// 31 black bars + 30 white gaps
// Many very thin bars (1-unit) mixed with thick bars
// Photographing this code causes thin bars to blur/merge,
// breaking the precise ratio pattern
// ═══════════════════════════════════════════════════════════
const SIGNATURE = [
3,1,1,1,4,1,1,2,2,1,1,1,3,1,1,1,2,1,1,3,
5,1,1,1,1,2,3,1,1,1,2,1,1,1,4,1,1,2,1,1,
3,1,1,1,2,1,1,1,1,2,4,1,1,1,1,1,2,1,1,1,3
];
const SIG_LEN = SIGNATURE.length; // 61
const SIG_TOTAL = SIGNATURE.reduce((a, b) => a + b, 0); // 97
const SIG_RATIOS = SIGNATURE.map(v => v / SIG_TOTAL);
// ═══════════════════════════════════════════════════════════
// ANTI-PHOTO SECURITY
// 1. Precision matching: requires very accurate ratios
// (photo-of-photo degrades thin bars → ratios break)
// 2. Sharpness check: real printed code has sharp edges,
// photo-of-code has softer transitions
// 3. Rate limiting: max 1 scan per 10 seconds
// ═══════════════════════════════════════════════════════════
/**
* Scan one horizontal line, return array of run widths
*/
function scanLine(data, y, w) {
const runs = [];
let inBar = false;
let runStart = -1;
for (let x = 0; x < w; x++) {
const idx = (y * w + x) * 4;
const lum = data[idx] * 0.299 + data[idx+1] * 0.587 + data[idx+2] * 0.114;
const black = lum < 128;
if (!inBar && !black) continue; // skip leading white
if (!inBar && black) { inBar = true; runStart = x; runs.push(0); continue; }
if (runs.length === 0) continue;
const wasBlack = (runs.length % 2 === 1); // odd count = currently in bar
if (black === wasBlack) {
// same color, extend run
} else {
// transition
runs[runs.length - 1] = x - runStart;
runs.push(0);
runStart = x;
}
}
// close last run
if (runs.length > 0 && runStart >= 0) {
runs[runs.length - 1] = canvas.width - runStart;
}
// Trim trailing white gaps
while (runs.length > 0 && runs.length % 2 === 0) {
runs.pop();
}
return runs;
}
/**
* Match runs against signature with STRICT precision
* Returns score 0.0 - 1.0
*/
function matchSignature(runs) {
if (runs.length < SIG_LEN) return 0;
let bestScore = 0;
for (let off = 0; off <= runs.length - SIG_LEN; off++) {
const sub = runs.slice(off, off + SIG_LEN);
const total = sub.reduce((a, b) => a + b, 0);
if (total < 40) continue; // too small, noise
const ratios = sub.map(v => v / total);
let sumSqDiff = 0;
for (let i = 0; i < SIG_LEN; i++) {
const diff = ratios[i] - SIG_RATIOS[i];
sumSqDiff += diff * diff;
}
// STRICT threshold: multiply by 80 (stricter than before)
// Photo-of-photo typically scores < 0.5 due to blur
const score = Math.max(0, 1 - (sumSqDiff * 80));
bestScore = Math.max(bestScore, score);
}
return bestScore;
}
/**
* ANTI-PHOTO: Measure edge sharpness of black-to-white transitions.
* Real printed codes have sharp edges; photos-of-codes have gradual transitions.
* Returns sharpness score 0.0 - 1.0
*/
function measureSharpness(data, y, w) {
let sharpTransitions = 0;
let totalTransitions = 0;
for (let x = 1; x < w - 1; x++) {
const idx = (y * w + x) * 4;
const idxPrev = (y * w + (x - 1)) * 4;
const lumCur = data[idx] * 0.299 + data[idx+1] * 0.587 + data[idx+2] * 0.114;
const lumPrev = data[idxPrev] * 0.299 + data[idxPrev+1] * 0.587 + data[idxPrev+2] * 0.114;
const diff = Math.abs(lumCur - lumPrev);
if (diff > 30) { // significant change
totalTransitions++;
if (diff > 100) { // sharp change (>100 lum jump in 1 pixel)
sharpTransitions++;
}
}
}
if (totalTransitions === 0) return 0;
return sharpTransitions / totalTransitions;
}
/**
* Main frame analysis
*/
function analyzeFrame() {
if (!scanning || detected) return;
if (video.readyState < video.HAVE_ENOUGH_DATA) {
requestAnimationFrame(analyzeFrame);
return;
}
const vw = video.videoWidth, vh = video.videoHeight;
if (!vw || !vh) { requestAnimationFrame(analyzeFrame); return; }
const scale = 0.5;
canvas.width = Math.floor(vw * scale);
canvas.height = Math.floor(vh * scale);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const cw = canvas.width, ch = canvas.height;
const imgData = ctx.getImageData(0, 0, cw, ch).data;
let highestScore = 0;
let avgSharpness = 0;
let sharpCount = 0;
const scanCount = 20;
const centerY = Math.floor(ch / 2);
const range = Math.floor(ch * 0.25);
for (let i = 0; i < scanCount; i++) {
const y = centerY - range + Math.floor((2 * range * i) / (scanCount - 1));
if (y < 0 || y >= ch) continue;
const runs = scanLine(imgData, y, cw);
const score = matchSignature(runs);
highestScore = Math.max(highestScore, score);
// Also measure sharpness on lines with decent scores
if (score > 0.4) {
avgSharpness += measureSharpness(imgData, y, cw);
sharpCount++;
}
}
// Calculate average sharpness
if (sharpCount > 0) avgSharpness /= sharpCount;
// DETECTION LOGIC
if (highestScore > 0.70) {
// Check anti-photo: sharpness must be > 0.35
// Real printed code ≈ 0.5-0.9, photo-of-code ≈ 0.1-0.3
if (sharpCount > 0 && avgSharpness < 0.25) {
// Likely a photo of the code, not the real thing
setStatus('warning', 'mdi mdi-alert', 'Scan original printed code only');
consecutiveDetections = 0;
} else {
consecutiveDetections++;
if (consecutiveDetections >= REQUIRED_DETECTIONS) {
onCodeDetected();
return;
}
}
} else {
consecutiveDetections = Math.max(0, consecutiveDetections - 1);
}
requestAnimationFrame(analyzeFrame);
}
function setStatus(type, icon, text) {
statusPill.className = 'status-pill ' + type;
statusIcon.className = icon;
statusText.textContent = text;
}
/**
* Code detected → generate token → redirect
*/
async function onCodeDetected() {
// Rate limiting
const now = Date.now();
if (now - lastTokenTime < RATE_LIMIT_MS) {
setStatus('warning', 'mdi mdi-clock-fast', 'Please wait before scanning again...');
resetScanner();
return;
}
lastTokenTime = now;
detected = true;
scanning = false;
setStatus('success', 'mdi mdi-check-circle', 'Code detected! Generating access...');
if (navigator.vibrate) navigator.vibrate(200);
try {
const resp = await fetch('generate_door_token.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=generate'
});
const data = await resp.json();
if (data.status === 'success' && data.token) {
setStatus('success', 'mdi mdi-check-circle', 'Redirecting...');
setTimeout(() => {
window.location.href = 'openning.php?token=' + encodeURIComponent(data.token);
}, 600);
} else {
setStatus('error', 'mdi mdi-alert-circle', data.message || 'Token generation failed');
resetScanner();
}
} catch (err) {
setStatus('error', 'mdi mdi-alert-circle', 'Network error');
resetScanner();
}
}
function resetScanner() {
setTimeout(() => {
detected = false;
scanning = true;
consecutiveDetections = 0;
setStatus('scanning', 'mdi mdi-line-scan', 'Point camera at door code...');
requestAnimationFrame(analyzeFrame);
}, 2500);
}
async function initCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false
});
video.srcObject = stream;
await new Promise(r => { video.onloadedmetadata = () => { video.play().then(r).catch(r); }; setTimeout(r, 5000); });
setTimeout(() => { loadingOverlay.classList.add('hidden'); requestAnimationFrame(analyzeFrame); }, 500);
} catch (err) {
loadingOverlay.style.display = 'none';
errorOverlay.style.display = 'flex';
errorMsg.textContent = err.name === 'NotAllowedError' ? 'Camera access denied.' :
err.name === 'NotFoundError' ? 'No camera found.' : 'Error: ' + err.message;
}
}
initCamera();
})();
</script>
</body>
</html>