修复 iOS 网页在拍照预览时图片被旋转的问题

前几天用 uni-app 写了个上传图片的小案例(编译到 H5 端),当在 iPhone 上拍照上传时,发现在页面上预览的图片是发生了旋转的。

图片的 Exif 信息

Exif 全称 Exchangeable image file format,中文叫可交换图像文件格式,Exif 中存储了图片的方向信息。

EXIF Orientation Value Row #0 is: Column #0 is:
1 Top Left side
2 Top Right side
3 Bottom Right side
4 Bottom Left side
5 Left side Top
6 Right side Top
7 Right side Bottom
8 Left side Bottom

看不懂没关系,网上找了张示例图(如有侵权,请联系删之),请对号入座:

获取图片的 Orientation 信息

网上都在推荐 Exif.js 这个库,有兴趣的可以自行研究,我没用过,就不多说了。

/**
 * 根据File对象获取图片的方向信息
 * @param {File} file
 * @param {Function} callback
 */
getOrientation: function(file, callback) {
    var reader = new FileReader();
    reader.onload = function(e) {
        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8)
        {
            return callback(-2);
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) 
        {
            if (view.getUint16(offset+2, false) <= 8) return callback(-1);
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) 
            {
                if (view.getUint32(offset += 2, false) != 0x45786966) 
                {
                    return callback(-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 callback(view.getUint16(offset + (i * 12) + 8, little));
                    }
                }
            }
            else if ((marker & 0xFF00) != 0xFF00)
            {
                break;
            }
            else
            { 
                offset += view.getUint16(offset, false);
            }
        }
        return callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

如果是采用 <input type="file" capture="camera" accept="image/png, image/jpeg"> 来拍照,返回的就是一个 FileList 对象,里面储存的每一个 File 对象代表用户选择的一个图片,可以直接调用上面 getOrientation 方法获取图片的方向信息

但是 uni-app 的 uni.chooseImage 接口在 H5 端返回的是一个 blob:http://xxxxxx/xxxxx 格式的路径信息,并不是一个 File 对象,所以我们需要做一些额外处理:

1、将路径转成 Blob 对象
JavaScript const xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.responseType = "blob"; xhr.onload = function() { const blobObject = this.response; // ... }; xhr.send();
2、将 Blob 转成 File
JavaScript const file = new File([blobObject], 'photo.png', {type: blobObject.type});
3、调用 getOrientation,然后做相应的旋转处理

修复预览旋转

由于我的项目是基于 uni-app 进行构建的,所以这里主要是针对 uni-app 下编译的 H5 页面进行说明。

完整示例代码:

<template>
    <view class="container">
        <view class="content">
            <view class="photo" @click="onTokePhotoTapped">
                <image src="/static/placeholder.png"></image>
                <image :src="faceImage" mode="aspectFill" :style="fixOrientationStyle"></image>
            </view>
            <view class="field-item">
                <view class="field-item-title">工号</view>
                <input class="field-item-input" placeholder="请输入工号" maxlength="30" v-model="jobNumber" />
            </view>
            <view class="submit-button" @click="onSubmitTapped">提交</view>
        </view>
    </view>
</template>

<script>
    export default {
        data() {
            return {
                jobNumber: '',
                faceImage: '',
                fixOrientationStyle: '', // 修正图片方向
            }
        },

        components: { PlaceholderImage },

        onLoad() {

        },

        methods: {

            onTokePhotoTapped: function() {
                uni.chooseImage({
                    count: 1,
                    success: (res) => {
                        // this.faceImage = res.tempFilePaths[0];
                        /**
                         1、URL => Blob => File
                         2、获取方向
                         */
                        const url = res.tempFilePaths[0];
                        const self = this;
                        let xhr  = new XMLHttpRequest();
                        xhr.open("GET", url, true);
                        xhr.responseType = "blob";
                        xhr.onload = function() {
                            self.faceImage = url;
                            self.fixOrientationStyle = '';
                            if (this.status == 200) {
                                const blob = this.response;
                                const file = new File([blob], 'photo.png', {type: blob.type});
                                self.getOrientation(file, (orientation) => {
                                    switch(orientation){  
                                        case 6://需要顺时针(向左)90度旋转
                                            self.fixOrientationStyle = 'transform: rotate(90deg);';
                                            break;
                                        case 8://需要逆时针(向右)90度旋转
                                            self.fixOrientationStyle = 'transform: rotate(-90deg);';
                                            break;
                                        case 3://需要180度旋转
                                            self.fixOrientationStyle = 'transform: rotate(180deg);';
                                            break;  
                                    }
                                });
                            }
                        };
                        xhr.send();
                    }
                });
            },

            onSubmitTapped: function() {
                const number = this.jobNumber.replace(/(^\s*)|(\s*$)/g, '');
                if (!number.length) {
                    uni.showToast({ icon: 'none', title: '请输入工号' });
                    return;
                }
                if (!this.faceImage.length) {
                    uni.showToast({ icon: 'none', title: '请选择图片' });
                    return;
                }
                uni.showLoading({ mask: true, title: '正在保存...' });
                // TODO: 这里就是图片上传的代码, 直接调用相应接口即可
                // 这里上传的图片还是旋转的,需要后台将图片方向修正过来
            },

            /**
             * 获取图片旋转方向

             * @param {Object} file            File对象
             * @param {Object} callback        回调
             */
            getOrientation: function(file, callback) {
                var reader = new FileReader();
                reader.onload = function(e) {

                    var view = new DataView(e.target.result);
                    if (view.getUint16(0, false) != 0xFFD8)
                    {
                        return callback(-2);
                    }
                    var length = view.byteLength, offset = 2;
                    while (offset < length) 
                    {
                        if (view.getUint16(offset+2, false) <= 8) return callback(-1);
                        var marker = view.getUint16(offset, false);
                        offset += 2;
                        if (marker == 0xFFE1) 
                        {
                            if (view.getUint32(offset += 2, false) != 0x45786966) 
                            {
                                return callback(-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 callback(view.getUint16(offset + (i * 12) + 8, little));
                                }
                            }
                        }
                        else if ((marker & 0xFF00) != 0xFF00)
                        {
                            break;
                        }
                        else
                        { 
                            offset += view.getUint16(offset, false);
                        }
                    }
                    return callback(-1);
                };
                reader.readAsArrayBuffer(file);
            }
        }
    }
</script>

<style>
    page {
        background-color: #EFEFF4;
        font-size: 30rpx;
    }
    .container {
        width: 100%;
        min-height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .content {
        width: 600rpx;
        margin-top: 40rpx;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
    .photo {
        width: 400rpx;
        height: 400rpx;
        border-radius: 20rpx;
        background-color: white;
        overflow: hidden;
        position: relative;
    }
    .photo image {
        width: 100%;
        height: 100%;
    }
    .photo image:last-child {
        position: absolute;
        top: 0;
        left: 0;
    }
    .field-item {
        width: calc(100% - 40rpx);
        height: 88rpx;
        padding: 0 20rpx;
        margin: 30rpx 0 50rpx;
        border-radius: 10rpx;
        overflow: hidden;
        background-color: white;
        display: flex;
        align-items: center;
    }
    .field-item-title {
        width: 100rpx;
    }
    .field-item-input {
        width: calc(100% - 100rpx);
        height: 100%;
    }
    .submit-button {
        width: 100%;
        height: 80rpx;
        border-radius: 20rpx;
        background-color: #007AFF;
        color: white;
        display: flex;
        align-items: center;
        justify-content: center;
    }
</style>

参考