最近在处理移动端上传图片遇到一个问题:有时会出现图片翻转的问题,一般是翻转 90 度。后经一翻研究找到了问题所在,特在此记录一下。
问题描述
经过测试发现:webapp在iPhone手机在竖屏下拍摄图片,上传后会出现图片翻转;横屏不会出现这样的问题。部分Android手机也会出现类似的问题。原生的没有测试过。
问题分析
在这里必须要知道可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。
利用exif.js读取照片的拍摄信息,详见 exif-js
这里主要用到Orientation属性。 说明如下:
旋转角度 | 参数 |
---|---|
0° | 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 参数:
参数 | 类型 | 说明 |
---|---|---|
compress | Boolean / Object | 压缩参数,为false时,不压缩 |
compress.width | Int | 压缩时的最大宽度,超过即压缩 |
compress.height | Int | 压缩时的最大高,超过即压缩 |
compress.quality | Int | 压缩质量,默认0.9 |
type | String | 返回结果类型,file时返回blob二进制流。默认返回base64 |
调用示例:
compress(file, {
compress: {
width: 1200,
height: 1600,
quality: 0.8
},
type: 'file'
}, (resFile) => {
// 上传图片
})