自定义webIpad证件相机

该技术方案可用于各浏览器自定义相机开发

相机UI(index.html)

<!DOCTYPE html>
<html lang="zh" prew="-1">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="user-scalable=no, initial-scale=1, maximum-scale=5, minimum-scale=1, width=device-width" />
    <title>自定义相机</title>
    <link rel="stylesheet" href="./style.css">
    <script src="./tools.js"></script>
    <script src="./index.js"></script>
</head>

<body>
    <div class="errTip">
        <p>Failed to obtain the rear camera of the device. Please try another solution to obtain resources!</p>
        <button class="errBtn">GO Back</button>
    </div>
    <div class="takeOffTip"></div>
    <div class="imgBoxDom">
        <div class="imgBox">
            <img src="./center.png" style="width: 4vw;">
        </div>
    </div>
    <div class="rightBtnBox">
        <div class="takeBtn"></div>
        <div class="cancleBtn btn"></div>
    </div>
    <div class="bottomBtnBox">
        <div class="reTakeBtn btn bottonSize"></div>
        <div class="nextBtn btn bottonSize"></div>
    </div>
    <div class="loading-css">
        Loading...
    </div>
</body>

</html>

 相机UI样式(style.css)

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    border: 0;
}

html,
body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: #000;
    color: #fff;
}

.cancleBtn {
    padding: 2vw 0;
    width: 100%;
}

.takeOffTip {
    position: fixed;
    padding-top: 2vw;
    top: 0;
    left: 0;
    width: 100%;
    font-size: 1.8vw;
    text-align: center;
    color: #fff;
}

.bottonSize {
    height: 100%;
    line-height: 6vw;
    line-height: 6dvw;
    padding: 0 1.5vw;
}

.bottomBtnBox,
.rightBtnBox {
    position: fixed;
    right: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #000;
    z-index: 10;
}

.bottomBtnBox {
    bottom: 0;
    width: 100%;
    height: 6vw;
    height: 6dvw;
}

.rightBtnBox {
    flex-direction: column;
    top: 0;
    height: 100%;
    width: 6vw;
    width: 6dvw;
}

html[prew='-1'] .bottomBtnBox,
html[prew='0'] .bottomBtnBox,
html[prew='-1'] .rightBtnBox,
html[prew='1'] .rightBtnBox,
html[prew='1'] .customer_carema {
    display: none;
}

html[prew='1'] .imgBox {
    border: 0;
    font-size: 0;
    opacity: 0;
}

.takeBtn {
    padding: 4px;
    width: 5vw;
    width: 5dvw;
    height: 5vw;
    height: 5dvw;
    background-color: #fff;
    border-radius: 50%;
}

.takeBtn::before {
    content: '';
    display: block;
    width: 100%;
    height: 100%;
    border: 5px solid #000;
    background-color: #fff;
    border-radius: 50%;
    box-sizing: border-box;
}

.rightBtnBox::before {
    content: '';
    display: block;
}

.btn {
    background-color: #000;
    text-align: center;
    font-size: 1.5vw;
    color: #fff;
}

.customer_video,
.carema_img,
.cuteImg {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.imgBoxDom {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9;
}

.imgBox {
    width: var(--carema-box-width);
    height: var(--carema-box-height);
    border: 2px solid #fff;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 10vw;
    z-index: 10;
    border-radius: 2vw;
}

.errTip {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 8888;
    display: none;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #000;
}

.errTip>p {
    padding-bottom: 20px;
    color: #fff;
}

.errTip button {
    padding: 10px 30px;
}

html[prew='2'] .errTip {
    display: flex;
}

html[loaded='1'] .loading-css {
    display: none;
}

.loading-css {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #000;
    z-index: 9999;
}

.loading-css::before {
    margin-bottom: 10px;
    content: '';
    width: 50px;
    height: 50px;
    display: inline-block;
    border: 3px solid #f3f3f3;
    border-top: 3px solid rgb(160, 155, 155);
    border-radius: 50%;
    animation: loading-360 0.8s infinite linear;
}

@keyframes loading-360 {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

调试UI(carema.html)
 

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="user-scalable=no, initial-scale=1, maximum-scale=5, minimum-scale=1, width=device-width" />
    <title>调试相机</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            border: 0;
        }

        img {
            max-width: 100%;
        }

        .btnList {
            padding: 10px;
        }

        label[type='file'],
        button {
            padding: 0 10px;
            height: 32px;
            line-height: 32px;
            display: inline-block;
            font-size: 14px;
            appearance: auto;
            border: 1px solid #999;
            background-color: #dcdcdc;
        }

        label>input {
            font-size: 0;
            width: 0;
            height: 0;
            overflow: hidden;
        }

        .showImg {
            padding: 5px;
            display: flex;
            flex-wrap: wrap;

        }

        .showImg>.box {
            width: 33.33%;
            padding: 5px;
        }

        .showImg>.box>.img {
            width: 100%;
            height: 20vw;
            overflow: hidden;
            border-radius: 10px;
            border: 2px solid #888;
        }

        .showImg>.box>.img>img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        html,
        body {
            height: 100%;
            height: 100%;
        }

        body {
            display: flex;
            flex-direction: column;
        }

        .showImg {
            flex: 1;
            overflow-x: hidden;
        }
    </style>
</head>

<body>
    <div class="btnList">
        <button onclick="openCarema('HK_ID')">COMM_ID_IMG</button>
        <button onclick="openCarema('LANDING')">LANDING_IMG</button>
        <label name="upload" type="file">
            LOCAL_IMG
            <input type="file" id="upload">
        </label>
    </div>
    <div class="showImg" id="showImg"></div>
</body>
<script>
    function fileToBase64(file) {
        return new Promise((resolve, reject) => {
            // 创建一个新的 FileReader 对象
            var reader = new FileReader();
            // 读取 File 对象
            reader.readAsDataURL(file);
            // 加载完成后
            reader.onload = function () {
                // 将读取的数据转换为 base64 编码的字符串
                var base64String = reader.result.split(",")[1];
                // 解析为 Promise 对象,并返回 base64 编码的字符串
                resolve(base64String);
            };

            // 加载失败时
            reader.onerror = function () {
                reject(new Error("Failed to load file"));
            };
        });
    }
    function showImg(url) {
        var showImgDom = document.getElementById('showImg');
        var img = document.createElement('img');
        img.src = `data:image/jpeg;base64,${url}`;
        var div = document.createElement('div');
        var cDiv = document.createElement('div');
        div.append(cDiv);
        cDiv.append(img);
        div.className = 'box';
        cDiv.className = "img";
        showImgDom.insertBefore(div, showImgDom.firstChild);
    }
    document.getElementById('upload').addEventListener('change', function ($event) {
        var file = $event.target.files[0];
        fileToBase64(file).then(showImg);
    })
    function openCarema(idType) {
        var openId = Date.now() + '';
        window.open(`./index.html?openId=${openId}&idType=${idType}&isDev=1`);
        window.addEventListener('message', function (res) {
            var resOpenId = res.data.openId;
            var mothod = res.data.mothod;
            var file = res.data.imgUrl;
            console.log(resOpenId, mothod, file);
            if (mothod === "success_file" && openId === resOpenId) fileToBase64(file).then(showImg);
        })
    }
</script>

</html>

相机逻辑基础(index.js)

function WbCRM() {
    this.body = document.body;
    this.html = document.documentElement;
    this.takeBtn = document.querySelector('.takeBtn');
    this.imgBox = document.querySelector('.imgBox');
    this.reTakeBtn = document.querySelector('.reTakeBtn');
    this.cancleBtn = document.querySelector('.cancleBtn');
    this.nextBtn = document.querySelector('.nextBtn');
    var errBtn = document.querySelector('.errBtn');
    this.video = null;
    this.err = null;
    this.fullImg = null;
    this.file = '';
    this.idType = '';
    this.isDev = false;

    this.stream = null;
    this.openId = '';

    this.ratio = window.devicePixelRatio || 1;
    this.videoWidth = this.body.clientWidth * this.ratio;
    this.videoHeight = this.body.clientHeight * this.ratio;

    this.html.setAttribute('prew', '-1');
    var isMp3 = !(navigator.userAgent.match(/Firefox/));
    var audio = new Audio();
    audio.autoplay = isMp3 ? './shutter.mp3' : './shutter.ogg';
    this.audio = audio;
    console.log(isMp3,audio);

    this.mediaDevices = (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) ?
        navigator.mediaDevices : ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia) ? {
            getUserMedia: function (c) {
                return new Promise(function (y, n) {
                    (navigator.mozGetUserMedia ||
                        navigator.webkitGetUserMedia).call(navigator, c, y, n);
                });
            }
        } : null);
    this.setDom();
    this.setCarema();
    this.takeBtn.addEventListener('click', this.takePhoto.bind(this));
    this.nextBtn.addEventListener('click', this.next.bind(this));
    this.reTakeBtn.addEventListener('click', this.reTake.bind(this));
    this.cancleBtn.addEventListener('click', this.cancle.bind(this));
    errBtn.addEventListener('click', this.openErro.bind(this));
}
WbCRM.prototype.openErro = function () {
    this.sendMsg('open_erro');
}
WbCRM.prototype.cancle = function () {
    this.removeStream();
    this.sendMsg('off_carema');
}
WbCRM.prototype.next = function () {
    if (this.fullImg) this.fullImg.remove();
    this.removeStream();
    this.sendMsg('success_file');
}
WbCRM.prototype.reTake = function () {
    this.file = null;
    this.err = null;
    if (this.fullImg) this.fullImg.remove();
    this.html.setAttribute('loaded', 0);
    this.removeStream();
    this.setCarema();
}
WbCRM.prototype.cutImage = function () {
    var boxWidth = this.imgBox.clientWidth * this.ratio;
    var boxHeight = this.imgBox.clientHeight * this.ratio;
    var vLeft = (this.videoWidth - boxWidth) / 2;
    var vTop = (this.videoHeight - boxHeight) / 2;
    var nCanvas = wbCRMTools.drawHighDefinitionImg(boxWidth, boxHeight);
    var nCtx = nCanvas.getContext('2d');
    nCtx.drawImage(this.fullImg, -vLeft, -vTop);
    var cutImage = nCtx.getImageData(0, 0, boxWidth, boxHeight);
    wbCRMTools.changeImgData(cutImage?.data || [], this.idType || '');
    nCtx.putImageData(cutImage, 0, 0);
    reImgUrl = nCanvas.toDataURL('image/jpeg');
    var cImg = document.createElement('img');
    cImg.src = reImgUrl;
    this.file = wbCRMTools.canvas2File(reImgUrl);
    wbCRMTools.clearCanvas(nCtx, nCanvas);
    cImg.className = "cuteImg";
    this.imgBox.append(cImg);
    this.html.setAttribute('prew', '1');
    this.removeStream();
}
WbCRM.prototype.takePhoto = function () {
    var gCanvas = wbCRMTools.drawHighDefinitionImg(this.videoWidth, this.videoHeight);
    var originalCtx = gCanvas.getContext('2d');
    originalCtx.drawImage(this.video, 0, 0, this.videoWidth, this.videoHeight);

    var imgUrl = gCanvas.toDataURL('image/jpeg');
    var fullImg = document.createElement("img");
    fullImg.className = "carema_img";
    fullImg.src = imgUrl;
    this.fullImg = fullImg;
    this.body.append(fullImg);
    wbCRMTools.clearCanvas(originalCtx, gCanvas);
    this.audio.play();
    fullImg.onload = this.cutImage.bind(this);
}

WbCRM.prototype.sendMsg = function (mothod) {
    this.audio.remove();
    const origin = this.isDev ? undefined : window.location.origin;
    window.opener.postMessage({ mothod: mothod, file: this.file, openId: this.openId, error: this.err }, origin);
    window.close();
}

WbCRM.prototype.removeStream = function () {
    var self = this;
    if (self.stream) {
        self.stream.getTracks().forEach(function (track) {
            if (track.readyState === 'live') track.stop();
            self.stream.removeTrack(track);
        });
    }
    if (this.video) this.video.remove();
    var cuteImgList = document.querySelectorAll('.cuteImg');
    cuteImgList.forEach(function (dom) {
        dom.remove();
    })
}

WbCRM.prototype.setDom = function () {
    this.openId = wbCRMTools.getUrlParam('openId');
    var okText = wbCRMTools.getUrlParam('continue');
    var cancelText = wbCRMTools.getUrlParam('cancel');
    var retakeText = wbCRMTools.getUrlParam('retake');
    var idType = wbCRMTools.getUrlParam('idType') || '';
    var takeOffTip = wbCRMTools.getUrlParam('takeOffTip');
    const isDev = wbCRMTools.getUrlParam('isDev');
    this.isDev = isDev === '1';
    this.nextBtn.innerText = okText || 'Cuntinue';
    this.cancleBtn.innerText = cancelText || 'Cancel';
    this.reTakeBtn.innerText = retakeText || 'Retake';
    document.querySelector('.takeOffTip').innerHTML = takeOffTip;
    this.html.setAttribute('loaded', 0);
    this.html.style.setProperty('--carema-box-width', '64.512vw');
    this.html.style.setProperty('--carema-box-height', '40.6789vw');
    if (idType === "LANDING") {
        this.html.style.setProperty('--carema-box-width', '51.2vw');
        this.html.style.setProperty('--carema-box-height', '44.5935vw');
    }
    this.idType = idType;
}

WbCRM.prototype.setVideo = function (stream) {
    var video = document.createElement('video');
    video.setAttribute('autoplay', 'autoplay');
    video.setAttribute('playsinline', 'playsinline');
    video.className = 'customer_video';
    this.video = video;
    this.stream = stream;
    this.body.append(video);
    var self = this;
    video.onloadedmetadata = function (e) {
        self.stream = stream;
        self.loaded = true;
        self.html.setAttribute('loaded', 1);
    };
    video.onplay = function () {
        self.html.setAttribute('prew', '0');
    }
    // as window.URL.createObjectURL() is deprecated, adding a check so that it works in Safari.
    // older browsers may not have srcObject
    if ("srcObject" in video) {
        video.srcObject = stream;
    } else {
        // using URL.createObjectURL() as fallback for old browsers
        video.src = window.URL.createObjectURL(stream);
    }
}

WbCRM.prototype.setCarema = function () {
    const videoConf = this.isDev ? {} : {
        width: { min: 1024, ideal: 2360, max: 2732 },
        height: { min: 776, ideal: 1640, max: 2048 },
        facingMode: { exact: "environment" }
    }
    var self = this;
    this.mediaDevices.getUserMedia({
        audio: false,
        video: videoConf
    }).then(this.setVideo.bind(this)).catch(function (error) {
        self.err = error.toString();
        self.html.setAttribute('prew', '2');
        self.html.setAttribute('loaded', '1');
    })
}

window.addEventListener('load', function () {
    var wbCRM = new WbCRM();
    window.addEventListener('visibilitychange', function () {
        wbCRM.removeStream();
        window.close();
    });
});

图片出路和文件生成工具(tools.js) 

var wbCRMTools = {
    drawHighDefinitionImg: function (width, height) {
        const canvas = document.createElement('canvas');
        canvas.style.width = width + 'px';
        canvas.style.height = height + 'px';
        canvas.width = width;
        canvas.height = height;
        return canvas;
    },
    clearCanvas: function (ctx, canvas) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        canvas.height = 0;
        canvas.width = 0;
        canvas.remove();
        canvas.parentNode?.removeChild(canvas);
    },

    changeImgData: function (data, idType) {
        const isGrayscale = ['PASSPORT', 'LANDING', 'ENTRYPERMIT', 'SUP_LEGAL_ID'].some(imgType => idType.indexOf(imgType) !== -1);
        let contrast = 35;
        const thereshold = 20;
        if ('LANDING' === idType) contrast = 45;
        // gaussBlur will use in the feature, cancel this fun now, don`t delete please
        // this.gaussBlur(imageData, 1);
        // If MacId and HK-LANDING change cavans-img-code.
        const factor = (255 + contrast) / (255.01 - contrast);  //add .1 to avoid /0 error
        const denominator = 1 / (1 - contrast / 255) - 1;
        const setCV = cv => cv + (cv - thereshold) * denominator;
        const setCTV = cv => cv + (cv - thereshold) * contrast / 255;
        const getRGB = cv => factor * (cv - 128) + 128;
        // Data array data-length.
        const len = data?.length || 0;
        // loop value to change cavans imgData;
        for (let index = 0; index < len; index += 4) {
            let R = data[index];     //r value
            let G = data[index + 1]; //g value
            let B = data[index + 2] //b value
            if (contrast || thereshold) {
                R = getRGB(R); //r value
                G = getRGB(G); //g value
                B = getRGB(B); //b value
            }
            const isColorNum = index % 4 === 0;
            if (isColorNum) {
                R = contrast ? setCV(R) : setCTV(R);
                G = contrast ? setCV(G) : setCTV(G);
                B = contrast ? setCV(B) : setCTV(B);
                if (isGrayscale) {
                    const vNum = Math.round((R + G + B) / 3);
                    R = vNum;
                    G = vNum;
                    B = vNum;
                    data[index + 3] = 255;
                }
                data[index] = R;
                data[index + 1] = G;
                data[index + 2] = B;
            }
        }
    },
    getUrlParam: function (urlKey) {
        var url = window.location.search;
        var reg = new RegExp("(^|&)" + urlKey + "=([^&]*)(&|$)");
        var result = url.substring(1).match(reg);
        return result ? decodeURIComponent(result[2]) : null;
    },
    canvas2File: function (dataUrl) {
        let arr = dataUrl.split(','),
            mime = arr[0].match(/:(.*?);/)[1],
            bstr = atob(arr[1]),
            n = bstr.length,
            u8arr = new Uint8Array(n);
        while (n--) {
            u8arr[n] = bstr.charCodeAt(n);
        }
        const nowId = Date.now();
        const fileName = `takePhoto_${nowId}.jpeg`;
        const blob = new Blob([u8arr], { type: mime, name: fileName });
        blob.lastModifiedDate = new Date();
        return new File([blob], fileName, { type: "image/jpeg" });
    }
}

 文件目录

效果图

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值