js 图片压缩处理、旋转矫正

最近在处理移动端上传图片遇到一个问题:有时会出现图片翻转的问题,一般是翻转 90 度。后经一翻研究找到了问题所在,特在此记录一下。

问题描述

经过测试发现:webapp在iPhone手机在竖屏下拍摄图片,上传后会出现图片翻转;横屏不会出现这样的问题。部分Android手机也会出现类似的问题。原生的没有测试过。

问题分析

在这里必须要知道可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。

利用exif.js读取照片的拍摄信息,详见 exif-js

这里主要用到Orientation属性。 说明如下:

旋转角度参数
1
顺时针90°6
逆时针90°8
180°3

通过Orientation属性判断图片是否翻转后,再用canvas把图片转正。

解决问题

引入compress.js工具方法

// clone https://github.com/Tencent/weui.js/blob/master/src/uploader/image.js
// updated by cube-ui

/*
 * Tencent is pleased to support the open source community by making WeUI.js available.
 *
 * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
 *
 * Licensed under the MIT License (the "License") you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *       http://opensource.org/licenses/MIT
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * 检查图片是否有被压扁,如果有,返回比率
 * ref to http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios
 */
function detectVerticalSquash(img) {
  // 拍照在IOS7或以下的机型会出现照片被压扁的bug
  var data
  var ih = img.naturalHeight
  var canvas = document.createElement('canvas')
  canvas.width = 1
  canvas.height = ih
  var ctx = canvas.getContext('2d')
  ctx.drawImage(img, 0, 0)
  try {
    data = ctx.getImageData(0, 0, 1, ih).data
  } catch (err) {
    console.warn('Cannot check verticalSquash: CORS?')
    return 1
  }
  var sy = 0
  var ey = ih
  var py = ih
  while (py > sy) {
    var alpha = data[(py - 1) * 4 + 3]
    if (alpha === 0) {
      ey = py
    } else {
      sy = py
    }
    py = (ey + sy) >> 1 // py = parseInt((ey + sy) / 2)
  }
  var ratio = (py / ih)
  return (ratio === 0) ? 1 : ratio
}

/**
 * dataURI to blob, ref to https://gist.github.com/fupslot/5015897
 * @param dataURI
 */
function dataURItoBuffer(dataURI) {
  var byteString = atob(dataURI.split(',')[1])
  var buffer = new ArrayBuffer(byteString.length)
  var view = new Uint8Array(buffer)
  for (var i = 0; i < byteString.length; i++) {
    view[i] = byteString.charCodeAt(i)
  }
  return buffer
}

function dataURItoBlob(dataURI) {
  var mimeString = dataURI.split(',')[0].split(':')[1].split('')[0]
  var buffer = dataURItoBuffer(dataURI)
  return new Blob([buffer], {
    type: mimeString
  })
}

/**
 * 获取图片的旋转方向orientation
 * ref to http://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side
 */
function getOrientation(buffer) {
  var view = new DataView(buffer)
  if (view.getUint16(0, false) !== 0xFFD8) return -2
  var length = view.byteLength
  var offset = 2
  while (offset < length) {
    var marker = view.getUint16(offset, false)
    offset += 2
    if (marker === 0xFFE1) {
      if (view.getUint32(offset += 2, false) !== 0x45786966) return -1
      var little = view.getUint16(offset += 6, false) === 0x4949
      offset += view.getUint32(offset + 4, little)
      var tags = view.getUint16(offset, little)
      offset += 2
      for (var i = 0; i < tags; i++) {
        if (view.getUint16(offset + (i * 12), little) === 0x0112) {
          return view.getUint16(offset + (i * 12) + 8, little)
        }
      }
    } else if ((marker & 0xFF00) !== 0xFF00) break
    else offset += view.getUint16(offset, false)
  }
  return -1
}

/**
 * 修正拍照时图片的方向
 * ref to http://stackoverflow.com/questions/19463126/how-to-draw-photo-with-correct-orientation-in-canvas-after-capture-photo-by-usin
 */
function orientationHelper(canvas, ctx, orientation) {
  const w = canvas.width
  const h = canvas.height
  if (orientation > 4) {
    canvas.width = h
    canvas.height = w
  }
  switch (orientation) {
    case 2:
      ctx.translate(w, 0)
      ctx.scale(-1, 1)
      break
    case 3:
      ctx.translate(w, h)
      ctx.rotate(Math.PI)
      break
    case 4:
      ctx.translate(0, h)
      ctx.scale(1, -1)
      break
    case 5:
      ctx.rotate(0.5 * Math.PI)
      ctx.scale(1, -1)
      break
    case 6:
      ctx.rotate(0.5 * Math.PI)
      ctx.translate(0, -h)
      break
    case 7:
      ctx.rotate(0.5 * Math.PI)
      ctx.translate(w, -h)
      ctx.scale(-1, 1)
      break
    case 8:
      ctx.rotate(-0.5 * Math.PI)
      ctx.translate(-w, 0)
      break
  }
}

/**
 * 压缩图片入口
 * @param {File} file 图像文件
 * @param {Object} options 操作参数
 * @param {Function} callback 回调函数
 */
function compress(file, options, callback) {
  const reader = new FileReader()
  reader.onload = function (evt) {
    if (options.compress === false) {
      // 不启用压缩 & base64上传 的分支,不做任何处理,直接返回文件的base64编码
      file.base64 = evt.target.result
      callback(file)
      return
    }

    // 启用压缩的分支
    const img = new Image()
    img.onload = function () {
      const ratio = detectVerticalSquash(img)
      const orientation = getOrientation(dataURItoBuffer(img.src))
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      const maxW = options.compress.width
      const maxH = options.compress.height
      let w = img.width
      let h = img.height
      let dataURL

      if (w < h && h > maxH) {
        w = parseInt(maxH * img.width / img.height)
        h = maxH
      } else if (w >= h && w > maxW) {
        h = parseInt(maxW * img.height / img.width)
        w = maxW
      }

      canvas.width = w
      canvas.height = h

      if (orientation > 0) {
        orientationHelper(canvas, ctx, orientation)
      }
      ctx.drawImage(img, 0, 0, w, h / ratio)

      if (/image\/jpeg/.test(file.type) || /image\/jpg/.test(file.type)) {
        dataURL = canvas.toDataURL('image/jpeg', options.compress.quality)
      } else {
        dataURL = canvas.toDataURL(file.type)
      }

      if (options.type === 'file') {
        if (/base64,null/.test(dataURL) || /base64,$/.test(dataURL)) {
          // 压缩出错,以文件方式上传的,采用原文件上传
          console.warn('Compress fail, dataURL is ' + dataURL + '. Next will use origin file to upload.')
          callback(file)
        } else {
          let blob = dataURItoBlob(dataURL)
          blob.id = file.id
          blob.name = file.name
          blob.lastModified = file.lastModified
          blob.lastModifiedDate = file.lastModifiedDate
          callback(blob)
        }
      } else {
        if (/base64,null/.test(dataURL) || /base64,$/.test(dataURL)) {
          // 压缩失败,以base64上传的,直接报错不上传
          options.onError(file, new Error('Compress fail, dataURL is ' + dataURL + '.'))
          callback()
        } else {
          file.base64 = dataURL
          callback(file)
        }
      }
    }
    img.src = evt.target.result
  }
  reader.readAsDataURL(file)
}

export default compress

options 参数:

参数类型说明
compressBoolean / Object压缩参数,为false时,不压缩
compress.widthInt压缩时的最大宽度,超过即压缩
compress.heightInt压缩时的最大高,超过即压缩
compress.qualityInt压缩质量,默认0.9
typeString返回结果类型,file时返回blob二进制流。默认返回base64

调用示例:

compress(file, {
  compress: {
     width: 1200,
     height: 1600,
     quality: 0.8
   },
   type: 'file'
 }, (resFile) => {
 	// 上传图片
 })
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值