我与照片之乾坤大挪移-瞬间旋转你的照片

3,276 阅读7分钟

前言

公司项目中有一个祖传的自定义相机,之前发现它在横拍的时候没有对图片进行旋转,对其进行一番修改之后在测试机上面测试成功。上线后又发现只有部分手机正确旋转了,经过一番努力之后,终于解决。并且在文末给出一个与网上千篇一律的照片旋转不同的比较少见的快速旋转方案

写下此文章记录下问题的解决经过,一步步分析里面的问题。

只有部分手机正确旋转

这种做法只有部分手机获得了正确旋转的照片, 直接来看一下代码里面相机拍照回调方法onPictureTaken()的处理:

 public void onPictureTaken(byte[] data, Camera camera) {
                camera.stopPreview();
                //是否需要旋转
                boolean isOrientation = false;
                //用户是否横屏拍摄,true为横屏 0度左旋 90不旋 180右旋 270度旋
                if (CameraActivity.this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE
                        && (takePhotoOrientation <= 90+45)&&(takePhotoOrientation>=90-45)) {//不旋转
                    isOrientation = false;
                } else {
                    isOrientation = true;
                }
               //旋转太过消耗时间,是储存的40倍时间左右,考虑后去掉旋转。
               //---反注释start---
                if (isOrientation) {
                    //横屏拍摄,需要旋转90度
                   byte[] bytes = ImageUtils.rotatePic(data,takePhotoOrientation-90);
                    if (bytes != null) {
                        data = bytes;
                    }
                }
                //---反注释end---
                mPicPath = String.format("%s%s%s", AppConfig.getCarTradeFileDir("Camera"),
                            System.currentTimeMillis() + ".jpg", tempFileSuffex);
                FileSaveUtils.saveFile(data, mPicPath, new FileSaveUtils.SaveListener() {
                    @Override
                    public void saveComplete() {
                        finish();
                    }
                });
    }

其实这里我做的就只是把作者关于旋转的代码反注释了。

分析一下里面做了些什么:

  1. takePhotoOrientation这个变量是由一个重力监听器提供的,用它判断当前手机是否处于横拍状态,横拍需要旋转;
  2. 然后就可以看到原代码作者非常贴心地在这里注释了旋转非常耗时,告诫我们不要去旋转它,然后注释掉了旋转代码,我又把他的注释解开了;
  3. ImageUtils.rotatePic()旋转图片,里面走的核心代码就是网上一搜就是的旋转方案:
public static byte[] rotatePic(byte[] data, int degree) {
        byte[] bytes = null;
        try {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            options.inJustDecodeBounds = false;
            options.inPurgeable = true;
            options.inInputShareable = true;
            Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);// data是字节数据,将其解析成位图
            bitmap = rotateBitmap(bitmap, degree);
            bytes = bitmap2Bytes(bitmap);
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
        } catch (Error e) {
            e.printStackTrace();
        }
        return bytes;
    }
    /**
    * 网上一搜就有的旋转代码
    */
    public static Bitmap rotateBitmap(Bitmap bitmap, int degree) {
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        Bitmap bm = null;
        try {
            bm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        } catch (Exception e) {
            bm = bitmap;
        }
        return bm;
    }
    
    public static byte[] bitmap2Bytes(Bitmap bm) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        return baos.toByteArray();
    }
  1. 保存byte[]数据。这里的照片保存方法内部实现也有问题,但是这里不是我们这篇文章的重点,所以忽略它先。

看到这里,一些明眼的同学可能已经看出为什么作者说旋转用了40倍时间了,是的呢~

ImageUtils.rotatePic()里面出了问题: 它先把byte[]解析成bitmap,然后旋转又create了一个bitmap,再把bitmap转换成byte[]来保存。这个过程有没有40倍我没有实践过,不过可以想象这耗时很长。留待优化。

获取EXIF

优化的事情先放下,先解决业务上的问题-旋转。

那怎么才能知道照片的方向呢?小case,难不倒玩摄影的我,相机在拍照的时候都会有保存提个叫EXIF的照片信息,它里面存放着这张照片的诸多信息,例如光圈、焦距、快门时间等等,最重要的还有我们需要的旋转方向-orientation。有了它我们不就可以轻轻松松判断方向了!?

嘿嘿~我读:

    ExifInterface exifInterface = new ExifInterface(filepath);
    int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                            ExifInterface.ORIENTATION_NORMAL);
    ...

emmmm... 这,这里我们不能马上用这个代码来读,如果你用了这个项目的代码,去读exif,你可能就要进入死胡同里面出不来了!

先下载一个能看exif的app,我用的是photo exif editor,它打开是这样的:

我波波可爱吗?

如图exif一目了然,然而用photo exif editor去查看项目相机拍出来的图,所有exif数据都是空的,所以如果你用代码来读,永远都只会读出你填写的那个默认值。

再去探究一下exif丢失的原因吧,开始我以为是Carema Api的问题,但在onPictureTaken()里面一回调就直接保存图片,exif是存在的,明显问题出在旋转代码里面,可以猜想是转换成bitmap的时候丢失了exif信息,经过一番资料查验,的确如此。

什么是Exif,为什么会丢失Exif信息?

从Exif2.2规范里面可以找到关于压缩图像文件数据的描述,其中有这么一幅图:

它描述了这么一个事实:压缩图像文件由标记码和压缩图像数据组成,其中标记码数据中包括Exif。很显然,bitmap作为一个图像类只包含了解压出的图像数据。

总结

照片转换成bitmap会丢失exif,所以编辑图片的时候需要把exif保存起来再修改后重新保存到照片中去。

想要深入了解Exif的话这里有一个2.2版本的规范文档 Exif2.2传送门

既然了解好了,读Exif吧!

    ExifInterface exifInterface = new ExifInterface(filepath);
    int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                            ExifInterface.ORIENTATION_NORMAL);
    ...

我读~~~~↓

小米:照片方向与手机方向一致,没有正确旋转,Exif 90°;

VIVO:照片方向与手机方向不一致,正确旋转,Exif 90°。

惊了!居然和预想的不一致。。出现这样的结果证明这又是手机系统内部出现的差异,安卓碎片化!

工作陷入了停顿。。只能搬出大牛鱼哥~

一番询问之后,鱼哥show his code解决了我的问题~再次感谢鱼哥

配置相机参数

Camera#Parameters这个类相信对于熟悉相机的同学不会陌生,它能够用来获取和配置相机参数:获取预览尺寸,设置闪光灯,对焦模式等等,都在这个通过这个类进行调节配置,我们要的修正方法竟然藏在这里面!

public class IOrientationEventListener extends OrientationEventListener {
        
        public IOrientationEventListener(Context context) {
            super(context);
        }
        
        @Override
        public void onOrientationChanged(int orientation) {
            if (ORIENTATION_UNKNOWN == orientation) {
                return;
            }
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(defaultCameraId, info);
            orientation = (orientation + 45) / 90 * 90;
            int rotation = 0;
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                rotation = (info.orientation - orientation + 360) % 360;
            } else {
                rotation = (info.orientation + orientation) % 360;
            }
            
            if (null != mCamera) {
                Camera.Parameters parameters = mCamera.getParameters();
                parameters.setRotation(rotation);//关键代码
                mCamera.setParameters(parameters);
            }
        }
    }

获取实例之后分别在SurfaceHolder#Callback#surfaceCreated()和SurfaceHolder#Callback#surfaceDestroyed()调用一下实例的enable()和disable()即可统一拍照旋转问题

由于onOrientationChanged()回调频繁,更加优化的做法可以在按下拍摄按钮之后才对相机进行设置。

通过修改rotation的值可以发现,只要rotation的值一定,所有手机的拍照方向都是统一的,意味着厂商对相机设置的方向默认值碎片化。

至此解决自定义相机拍照旋转问题。


快速旋转照片

通过上面的一大轮介绍,聪明的同学可能已经猜想出这个快速旋转的方案了--就是利用Exif。

因为照片本身是有设备生成的Exif的,里面含有标记照片方向的orientation,图片加载框架会通过读取orientation来对照片进行显示,也就是说,通过修改Exif里orientation的值,即可以达到旋转照片的效果!而且只需要操作一个小参数就能完成,瞬间完成,速度杠杠的!再也不怕旋转图片慢啦!

看一下Glide的处理: 对Exif解析确认图像方向

    //默认的图片头解析器
    public final class DefaultImageHeaderParser implements ImageHeaderParser {
        ...
        private int getOrientation(Reader reader, ArrayPool byteArrayPool) throws IOException {
            ...
            int exifSegmentLength = moveToExifSegmentAndGetLength(reader);
            if (exifSegmentLength == -1) {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Failed to parse exif segment length, or exif segment not found");
                }
                return UNKNOWN_ORIENTATION;
            }

            byte[] exifData = byteArrayPool.get(exifSegmentLength, byte[].class);
            try {
                return parseExifSegment(reader, exifData, exifSegmentLength);
            } finally {
                byteArrayPool.put(exifData);
            }
            ...
        }
        ...
    }

逆时针方向旋转的代码:

 ExifInterface exifInterface = new ExifInterface(currentPath);
                    // 获取图片的旋转信息
                    int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                            ExifInterface.ORIENTATION_NORMAL);
                    LogUtils.v("exif orientation:" + orientation);
                    //根据当前图片方向设置想要的图片方向
                    switch (orientation) {
                        case ExifInterface.ORIENTATION_ROTATE_90://正常角度
                            exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL+"");
                            break;
                        case ExifInterface.ORIENTATION_ROTATE_180:
                            exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_ROTATE_90+"");
                            break;
                        case ExifInterface.ORIENTATION_ROTATE_270:
                            exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_ROTATE_180+"");
                            break;
                        case ExifInterface.ORIENTATION_NORMAL:
                            exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_ROTATE_270+"");
                            break;
                    }
                    exifInterface.saveAttributes();

使用注意: 这种方案只能用于具有Exif的照片,旋转之后的照片显示需要注意缓存问题,每次旋转之后要更新缓存以正确显示照片。

Glide在加载图片的时候直接通过signature()方法new ObjectKey传入文件的修改时间作缓存标记即可:

Glide.with(context)
     .load(filePath)
     .signature(new ObjectKey(file.lastModified())
     .into(imageView);

其他框架的话请自行查找啦~

结尾

终于写完了,要是觉得有用或者让你拓展了知识就点个赞以示支持呗~

谢谢你的观看和学习!下回再见~