canvas 压缩图片并保存原图片exif信息

<template>
  <div>
    <input
      type="file"
      id="avatar"
      name="avatar"
      accept="image/png, image/jpeg"
      @change="loadFile"
    />
  </div>
</template>

<script>
//@ canvas 压缩图片并保存原图片exif信息 核心思路
//@ 保存原图 Exif 信息,待图片压缩完成后,将原图 Exif 信息拼接到压缩图上。
//@ 校验图片base64图片信息   地址:  http://code.ciaoca.com/javascript/exif-js/demo/base64
//@ 参考: http://www.dirk.wang/2019/08/16/canvas%E5%8E%8B%E7%BC%A9jpg%E4%B8%A2%E5%A4%B1exif/
import EXIF from "exif-js";
var BasePage = zj.widgets.BasePage;
export default {
  mixins: [BasePage],
  components: {},
  data() {
    return {
      exifInfo: "",
    };
  },
  methods: {
    loadFile(event) {
      let file = event.target.files[0];
      //读取图片的元信息
      var orientation;
      EXIF.getData(file, function () {
        orientation = EXIF.getTag(this, "Orientation"); // 图片旋转的方向
      });

      let reader = new FileReader();
      let that = this;
      reader.onload = function () {
        let result = this.result;
        that.imgSrc = result;

        // 保存原有图片 exif信息
        let orignBuffer = that.base64ToArrayBuffer(result);
        let segments = that.getSegments(orignBuffer); //分割片段
        that.exifInfo = that.getEXIF(segments);

        //使用exif
        that.getImgData(this.result, orientation, (data) => {
          //这里可以使用校正后的图片data了
          var img = new Image();
          img.src = data;

          //图片加载完毕之后进行压缩,然后上传
          if (img.complete) {
            callback();
          } else {
            img.onload = callback;
          }

          function callback() {
            var data = that.compress(img);

            // 将原始图片exif信息拼接到压缩后的图片base64之后
            let orignBuffer = that.base64ToArrayBuffer(data);
            let newImg = that.insertEXIF(orignBuffer, that.exifInfo);
            let base64Img = that.transformArrayBufferToBase64(newImg);
            console.log("base64Img", base64Img);
            that.upload(base64Img, file.type, file.name);
          }
        });
      };

      reader.readAsDataURL(file);
      return false;
    },
    //上传图片
    upload(basestr, type, name) {
      let text = window.atob(basestr.split(",")[1]);
      let buffer = new ArrayBuffer(text.length);
      let ubuffer = new Uint8Array(buffer);

      for (let i = 0; i < text.length; i++) {
        ubuffer[i] = text.charCodeAt(i);
      }

      let Builder = window.WebKitBlobBuilder || window.MozBlobBuilder;

      let blob;
      if (Builder) {
        let builder = new Builder();
        builder.append(buffer);
        blob = builder.getBlob(type);
      } else {
        blob = new window.Blob([buffer], { type: type });
      }

      let formdata = new FormData();
      // formdata.append(model, blob, name);
    },
    //压缩图片
    compress(img) {
      //用于压缩图片的canvas
      let canvas = document.createElement("canvas");
      let ctx = canvas.getContext("2d");

      // 瓦片canvas
      var tCanvas = document.createElement("canvas");
      var tctx = tCanvas.getContext("2d");

      let initSize = img.src.length;
      let width = img.width;
      let height = img.height;

      //如果图片大于四百万像素,计算压缩比并将大小压至400万以下
      var ratio;
      if ((ratio = (width * height) / 4000000) > 1) {
        ratio = Math.sqrt(ratio);
        width /= ratio;
        height /= ratio;
      } else {
        ratio = 1;
      }
      canvas.width = width * 2;
      canvas.height = height * 2;
      //铺底色
      ctx.fillStyle = "#fff";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      //如果图片像素大于100万则使用瓦片绘制
      var count;
      if ((count = (width * height) / 4000000) > 1) {
        count = ~~(Math.sqrt(count) + 1); //计算要分成多少块瓦片
        //计算每块瓦片的宽和高
        var nw = ~~(width / count);
        var nh = ~~(height / count);
        tCanvas.width = nw;
        tCanvas.height = nh;
        for (var i = 0; i < count; i++) {
          for (var j = 0; j < count; j++) {
            tctx.drawImage(
              img,
              i * nw * ratio,
              j * nh * ratio,
              nw * ratio * 2,
              nh * ratio * 2,
              0,
              0,
              nw,
              nh
            );
            ctx.drawImage(tCanvas, i * nw, j * nh, nw * 2, nh * 2);
          }
        }
      } else {
        ctx.drawImage(img, 0, 0, width * 2, height * 2);
      }
      //进行最小压缩
      let ndata = canvas.toDataURL("image/jpeg", 1);

      console.log(
        "压缩前:" +
          initSize +
          ",压缩后:" +
          ndata.length +
          ",压缩率:" +
          ~~((100 * (initSize - ndata.length)) / initSize) +
          "%"
      );

      return ndata;
    },
    getImgData(img, dir, next) {
      // @param {string} img 图片的base64
      // @param {int} dir exif获取的方向信息
      // @param {function} next 回调方法,返回校正方向后的base64
      var image = new Image();
      image.onload = function () {
        var degree = 0,
          drawWidth,
          drawHeight,
          width,
          height;
        drawWidth = this.naturalWidth;
        drawHeight = this.naturalHeight;
        //以下改变一下图片大小
        var maxSide = Math.max(drawWidth, drawHeight);
        if (maxSide > 1024) {
          var minSide = Math.min(drawWidth, drawHeight);
          minSide = (minSide / maxSide) * 1024;
          maxSide = 1024;
          if (drawWidth > drawHeight) {
            drawWidth = maxSide;
            drawHeight = minSide;
          } else {
            drawWidth = minSide;
            drawHeight = maxSide;
          }
        }
        var canvas = document.createElement("canvas");
        canvas.width = width = drawWidth;
        canvas.height = height = drawHeight;
        var context = canvas.getContext("2d");
        //判断图片方向,重置canvas大小,确定旋转角度,iphone默认的是home键在右方的横屏拍摄方式
        switch (dir) {
          //iphone横屏拍摄,此时home键在左侧
          case 3:
            degree = 180;
            drawWidth = -width;
            drawHeight = -height;
            break;
          //iphone竖屏拍摄,此时home键在下方(正常拿手机的方向)
          case 6:
            canvas.width = height;
            canvas.height = width;
            degree = 90;
            drawWidth = width;
            drawHeight = -height;
            break;
          //iphone竖屏拍摄,此时home键在上方
          case 8:
            canvas.width = height;
            canvas.height = width;
            degree = 270;
            drawWidth = -width;
            drawHeight = height;
            break;
        }
        //使用canvas旋转校正
        context.rotate((degree * Math.PI) / 180);
        context.drawImage(this, 0, 0, drawWidth, drawHeight);
        //返回校正图片
        next(canvas.toDataURL("image/jpeg", 0.4));
      };
      image.src = img;
    },

    //@ canvas 压缩图片保存原图片exif核心代码
   
    // 工具函数 将 base64 转 ArrayBuffer
    base64ToArrayBuffer(base64, contentType) {
      contentType =
        contentType || base64.match(/^data\:([^\;]+)\;base64,/im)[1] || ""; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg'
      base64 = base64.replace(/^data\:([^\;]+)\;base64,/gim, "");
      // btoa是binary to ascii,将binary的数据用ascii码表示,即Base64的编码过程
      // atob则是ascii to binary,用于将ascii码解析成binary数据
      var binary = atob(base64);
      var len = binary.length;
      var buffer = new ArrayBuffer(len);
      var view = new Uint8Array(buffer);
      for (var i = 0; i < len; i++) {
        view[i] = binary.charCodeAt(i);
      }
      return buffer;
    },
    transformArrayBufferToBase64(buffer) {
      var binary = "";
      var bytes = new Uint8Array(buffer);
      for (var len = bytes.byteLength, i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return `data:image/jpeg;base64,${window.btoa(binary)}`;
    },

    // 获取 0xFFE0~0xFFEF 开头的应用标记片段
    getSegments(arrayBuffer) {
      var head = 0,
        segments = [];
      var length, endPoint, seg;
      var arr = [].slice.call(new Uint8Array(arrayBuffer), 0);

      while (true) {
        // SOS(Start of Scan, 由 0xff 0xda 开头)
        // 遍历到 SOS 表示已经遍历完所有标记,再往下就是图像数据流了,直接 break
        if (arr[head] === 0xff && arr[head + 1] === 0xda) {
          break;
        }

        // SOI(Start of Image)是 JPG 文件的开头内容,由 0xff 0xd8 开头
        if (arr[head] === 0xff && arr[head + 1] === 0xd8) {
          head += 2;
        }
        // 找出每个标记片段
        else {
          // 每个标记开头后跟着的两个字节记录了该标记所记录内容的长度
          length = arr[head + 2] * 256 + arr[head + 3]; // 内容长度
          endPoint = head + length + 2; // 内容结束位置
          // 从0xff开头,到标记数据内容结束全部截出来
          seg = arr.slice(head, endPoint);
          head = endPoint;
          // push整个标记信息
          segments.push(seg);
        }
        if (head > arr.length) {
          break;
        }
      }
      return segments;
    },
    // 从标记片段筛选 & 取出 exif 信息
    getEXIF(segments) {
      if (!segments.length) {
        return [];
      }
      var seg = [];
      for (var x = 0; x < segments.length; x++) {
        var s = segments[x];
        // 0xff 0xe1开头的才是 exif数据(即app1)
        if (s[0] === 0xff && s[1] === 0xe1) {
          // app1 exif 0xff 0xe1
          seg = seg.concat(s);
        }
      }
      return seg;
    },
    // 拼接 Exif 到压缩后的 base64 中:
    // 插入 Exif 信息
    insertEXIF(resizedImg, exifArr) {
      var arr = [].slice.call(new Uint8Array(resizedImg), 0);
      //不是标准的JPEG文件
      if (arr[2] !== 0xff || arr[3] !== 0xe0) {
        return resizedImg;
      }
      var app0_length = arr[4] * 256 + arr[5]; //两个字节

      // 拼接文件 SOI + EXIF + 去除APP0的图像信息
      var newImg = [0xff, 0xd8].concat(exifArr, arr.slice(4 + app0_length));
      return new Uint8Array(newImg);
    },
  },
};
</script>

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