H5移动端调用摄像头拍照、压缩上传图片

这周产品提出了新的需求,要求前端H5页面调起移动设备摄像头,并实现拍照功能。完成之后来记录一下开发经历,希望对之后遇到同样问题开发者有所帮助!

首先H5要调起设备摄像头需要使用 input 标签,借助标签的 capture 属性来完成调起操作。上代码:

<label>照相机</label>
    <input type="file" id='image' accept="image/*" capture='camera'> 
<label>图片多选</label>
    <input type="file" accept="image/*" multiple>
<label>调起前置摄像头</label>
    <input type="file" accept="image/*" capture="user">

拍照完成之后,需要读取文件,这就需要使用 FileReader 对象来完成相应操作。上代码:

// 创建 FileReader 对象
var reader = new FileReader();
    reader.onload = function() {
        that.compress(this.result, file);
    };
reader.readAsDataURL(file);
this.fileUrl = window.URL.createObjectURL(file);

// this.result 既是读取文件结果,是一个Base64形式的文件流,类似data:image/png;base64,*****
// file 是获去到的文件对象
file.png

that.compress 这个方法是用来处理图片压缩,是否旋转等功能。
this.fileUrl 是用来做 选定照片 / 拍摄照片 回显,就是 img 的 src 属性值。

到这里就可以实现简单的H5调起相册、摄像头操作。但是测试的时候会发现像素好的手机拍出来的照片非常大,就造成了上传接口相应超时问题,此时不要慌,接下来就说一说关于照片压缩问题。

这里的图片压缩就需要 canvas 来配合实现。

        let that = this;
        var width, height;
        var MAX_WH = 800;
        var image = new Image();

        image.onload = function() {
          if (image.height > MAX_WH) {
            // 宽度等比例缩放 *=
            image.width *= MAX_WH / image.height;
            image.height = MAX_WH;
          }
          if (image.width > MAX_WH) {
            // 宽度等比例缩放 *=
            image.height *= MAX_WH / image.width;
            image.width = MAX_WH;
          }
          //压缩
          var quality = 80;
          var cvs = document.createElement("canvas");
          var context = cvs.getContext("2d");
          cvs.width = width = image.width;
          cvs.height = height = image.height;

          switch (orientation) {
            case 6:
            case 8:
              cvs.width = height;
              cvs.height = width;
              break;
          }
          context.clearRect(0, 0, cvs.width, cvs.height);
          context.drawImage(image, 0, 0, image.width, image.height);
          that.readerResult = cvs.toDataURL("image/jpeg", quality / 100);
          that.getBankcardFn(); // 调用上传接口
        };
        image.src = res;

首先,创建Image对象,给imagesrc属性赋值加载完之后,调用onload。在onload中进行图片的压缩操作。
cvs.toDataURL() 方法返回的就是压缩之后的图片的 Base64 编码,这时候就可以把编码上传至服务器了。

到了这里已经就完成了一大半,功能已经基本实现,现在就可以开始考虑优化、提高用户体验了。经过测试,会发现iOS部分机型会莫名造成图片旋转,不要慌。

这里搭配EXIF对象来拿到图片的原信息。

      var orientation = 0;
      EXIF.getData(file, function() {
          orientation = EXIF.getTag(file, "Orientation");
      });
        //解决ios图片旋转问题
        switch (orientation) {
          //iphone横屏拍摄,此时home键在左侧
          case 3:
            // 180度向左旋转
            context.translate(width, height);
            context.rotate(Math.PI);
            break;
          //iphone竖屏拍摄,此时home键在下方(正常拿手机的方向)
          case 6:
             context.rotate(0.5 * Math.PI);
            context.translate(0, -height);
            break;
          //iphone竖屏拍摄,此时home键在上方
          case 8:
            // 逆时针旋转90度
            context.rotate(-0.5 * Math.PI);
             context.translate(-width, 0);
            break;
        }

这里的EXIF对象是(Exchangeable Image File)是“可交换图像文件”的缩写,当中包含了专门为数码相机的照片而定制的元数据,可以记录数码照片的拍摄参数、缩略图及其他属性信息,简单来说,Exif信息是镶嵌在 JPEG/TIFF 图像文件格式内的一组拍摄参数,需要注意的是EXIF信息是不支持png,webp等图片格式的。
可以引入CDN 也可以 npm install exif-js --save

到了这里功能就可以交付了。下面附上完整代码粘贴即用:

<!-- 详情操作页面 -->
<template>
  <div class="wrapper Detial">
    <div class="title">
      {{ this.$route.query.group }}
    </div>
    <div class="content">
      <div v-if="!mutually" class="cont">
        <input @change="fileChoose" type="file" id="image" accept="image/*" />
        上传银行卡照片
        <img src="../assets/upload.png" alt="" />
      </div>
      <div v-if="mutually" class="conte">
        <img :src="fileUrl" alt="" />
      </div>
    </div>
    <div class="inputGroup">
      <label v-for="(item, index) in detialData" :key="index">
        <span class="text">{{ item.name }}</span>
        <span class="cardNumber">{{ item.value }}</span>
      </label>
    </div>
    <div class="button">
      <button @click="getBactFn" class="btn">返回首页</button>
    </div>
    <div v-if="maskType" class="mask box">
      <div class="loader-15"></div>
      <span class="ideng">正在识别,请稍后!</span>
    </div>
  </div>
</template>

<script>
import EXIF from "exif-js";
export default {
  name: "Detial",
  data() {
    return {
      fileUrl: null,
      mutually: false,
      readerResult: "",
      detialData: [],
      maskType: false
    };
  },
  //生命周期 - 创建完成(访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(访问DOM元素)
  mounted() {},
  //存放自定义方法
  methods: {
    // 读取图片
    fileChoose(ifile) {
      this.maskType = true;
      this.mutually = true;
      let that = this;
      let file = ifile.target.files[0];
      console.log(file);
      var reader = new FileReader();
      reader.onload = function() {
        that.compress(this.result, file);
      };
      reader.readAsDataURL(file);
      this.fileUrl = window.URL.createObjectURL(file);
    },
    // 调用OCR银行卡识别接口
    getBankcardFn() {
      const params = {
        image_data: this.readerResult,
        detect_direction: "true"
      };
      this.$api.OCRServerDetial.getBankcardPort(params).then(res => {
        if (res.code === 200) {
          this.maskType = false;
          this.detialData = [
            {
              name: "银行卡号:",
              value: res.result.bank_card_number
            },
            {
              name: "有 效 期:",
              value: res.result.valid_date
            },
            {
              name: "银行名称:",
              value: res.result.bank_name
            },
            {
              name: "卡片类型:",
              value:
                res.result.bank_card_type == 0
                  ? "不能识别"
                  : res.result.bank_card_type == 1
                  ? "借记卡"
                  : "信用卡"
            }
          ];
        } else if (res.code === -1) {
          this.maskType = false;
          this.mutually = false;
          alert("请上传银行卡照片,或将图片旋转90°重试!");
        }
      });
    },
    // 压缩图片
    compress(res, file) {
      let that = this;
      var orientation = 0;
      if (file && /^image\//i.test(file.type)) {
        EXIF.getData(file, function() {
          orientation = EXIF.getTag(file, "Orientation");
        });
        var width, height;
        var MAX_WH = 800;
        var image = new Image();

        image.onload = function() {
          if (image.height > MAX_WH) {
            // 宽度等比例缩放 *=
            image.width *= MAX_WH / image.height;
            image.height = MAX_WH;
          }
          if (image.width > MAX_WH) {
            // 宽度等比例缩放 *=
            image.height *= MAX_WH / image.width;
            image.width = MAX_WH;
          }
          //压缩
          var quality = 80;
          var cvs = document.createElement("canvas");
          var context = cvs.getContext("2d");
          cvs.width = width = image.width;
          cvs.height = height = image.height;

          switch (orientation) {
            case 6:
            case 8:
              cvs.width = height;
              cvs.height = width;
              break;
          }
          //解决ios图片旋转问题
          switch (orientation) {
            //iphone横屏拍摄,此时home键在左侧
            case 3:
              // 180度向左旋转
              context.translate(width, height);
              context.rotate(Math.PI);
              break;
            //iphone竖屏拍摄,此时home键在下方(正常拿手机的方向)
            case 6:
              context.rotate(0.5 * Math.PI);
              context.translate(0, -height);
              break;
            //iphone竖屏拍摄,此时home键在上方
            case 8:
              // 逆时针旋转90度
              context.rotate(-0.5 * Math.PI);
              context.translate(-width, 0);
              break;
          }
          context.clearRect(0, 0, cvs.width, cvs.height);
          context.drawImage(image, 0, 0, image.width, image.height);
          that.readerResult = cvs.toDataURL("image/jpeg", quality / 100);
          that.getBankcardFn(); // 调用上传接口
        };
        image.src = res;
      }
    },
    // 点击返回首页
    getBactFn() {
      this.$router.push({ path: "/ocrList" });
    }
  },
  //生命周期 - 页面销毁前
  beforeDestroy() {}
};
</script>

<style lang="scss" scoped>
/* @import url(); 引入css类 */
.mask {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.6);
  position: absolute;
  top: 0;
  left: 0;
}
.box {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  font-size: 30px;
  color: rgba(200, 200, 200, 1);
  padding: 0 1em;
  -webkit-transition: 0.3s color, 0.3s border;
  transition: 0.3s color, 0.3s border;
  box-sizing: border-box;
}
[class*="loader-"] {
  display: inline-block;
  width: 1em;
  height: 1em;
  color: inherit;
  vertical-align: middle;
  pointer-events: none;
}
.ideng {
  font-size: 0.3rem;
  margin-top: 0.5rem;
}
.loader-15 {
  background: currentcolor;
  position: relative;
  -webkit-animation: loader-15 1s ease-in-out infinite;
  animation: loader-15 1s ease-in-out infinite;
  -webkit-animation-delay: 0.4s;
  animation-delay: 0.4s;
  width: 0.25em;
  height: 0.5em;
}
.loader-15:after,
.loader-15:before {
  content: "";
  position: absolute;
  width: inherit;
  height: inherit;
  background: inherit;
  -webkit-animation: inherit;
  animation: inherit;
}
.loader-15:before {
  right: 0.5em;
  -webkit-animation-delay: 0.2s;
  animation-delay: 0.2s;
}
.loader-15:after {
  left: 0.5em;
  -webkit-animation-delay: 0.6s;
  animation-delay: 0.6s;
}
@-webkit-keyframes loader-15 {
  0%,
  100% {
    box-shadow: 0 0 0 currentcolor, 0 0 0 currentcolor;
  }
  50% {
    box-shadow: 0 -0.25em 0 currentcolor, 0 0.25em 0 currentcolor;
  }
}
@keyframes loader-15 {
  0%,
  100% {
    box-shadow: 0 0 0 currentcolor, 0 0 0 currentcolor;
  }
  50% {
    box-shadow: 0 -0.25em 0 currentcolor, 0 0.25em 0 currentcolor;
  }
}
.button {
  margin-top: 0.5rem;
  text-align: center;
  .btn {
    color: #39a1ec;
    background: none;
    border: 1px solid #ccc;
  }
}
.title {
  font-size: 0.4rem;
  color: #333333;
  padding: 0.2rem;
}
.content {
  width: 100%;
  height: 4.5rem;
  position: relative;
  box-sizing: border-box;
  padding: 0 0.2rem 0.6rem;
  border-bottom: 0.3rem solid #f5f5f5;
  .conte {
    width: 100%;
    height: 100%;
    border-radius: 8px;
    overflow: hidden;
    img {
      width: 100%;
      height: 100%;
      display: block;
    }
  }
  .cont {
    width: 100%;
    height: 100%;
    color: #333;
    text-align: center;
    line-height: 3.6rem;
    font-size: 0.4rem;
    background: url("../assets/5.png") no-repeat;
    background-size: 100% 100%;
    img {
      width: 0.7rem;
      height: 0.5rem;
    }
    input {
      width: 100%;
      height: 100%;
      outline: none;
      opacity: 0;
      position: absolute;
      top: 0;
      left: 0;
    }
  }
}
.inputGroup {
  padding: 0 0.2rem;
  label {
    display: flex;
    padding: 0.24rem 0;
    align-items: center;
    justify-content: center;
    border-bottom: 1px solid #eeeeee;
    span {
      font-size: 0.3rem;
      color: #333333;
      font-family: STHeitiSC-Medium;
    }
    .text {
      width: 1.6rem;
      color: #999999;
      margin-right: 0.1rem;
    }
    .cardNumber {
      flex: 1;
      padding-left: 0.3rem;
    }
  }
}
</style>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容