iOS开发tip-图片方向

image_meta_orientqtion

概述

相信稍微接触过iOS图片相关操作的同学都遇到过图片旋转的问题,另外使用AVFoundation进行拍照的话就会遇到前后摄像头切换mirror问题就让人更摸不着头脑了。今天就简单和大家聊一下iOS的图片方向问题。

元数据Meta

在拍照过程中相机可以旋转到各个方向拍摄,但是最终展示的照片应该都是符合我们查看习惯的,比如你拿起手机不管竖着拍、横着拍还是倒着拍最后查看的时候都是正过来的图片,这才符合我们的习惯。但是无论是相机还是手机光学元件都是固定的,不可能镜头和传感器真正的旋转,要是要实现这个依靠的是相机的传感器并且将方向信息写入图片的Meta数据中(有些文章会描述为Exif,其实Meta中还有其他信息,本文全部描述为Meta),并且在真正展示时纠正过来。当然展示一张照片通常不用我们自己处理但是一旦不了解这个信息在处理一张照片后可能就出问题了,比如说常见的Meta丢失。

先看一下UIImage.imageOrientation枚举值:

public enum Orientation : Int {
    case up // 图片方向朝上,如果iPhone拍摄手机需要逆时针旋转90度(前置摄像头的话则顺时针旋转90度)
    case down // 图片旋转180度,如果iPhone拍摄手机需要顺时针旋转90度(前置摄像头的话则逆时针90度)
    case left // 图片顺时针旋转90度,如果iPhone拍摄手机需要旋转180度(前置摄像头的话也是如此)
    case right // 图片逆时针旋转90度,如果iPhone拍摄手机竖屏即可(前置摄像头的话也是如此)
    case upMirrored // 图片水平镜像
    case downMirrored // 图片旋转180度后水平镜像 
    case leftMirrored // 图片逆时针旋转90度后垂直镜像
    case rightMirrored // 图片顺时针旋转90度后垂直镜像 
}

关于UIImage.imageOrientation可以使用图片说明更加详细:

-w263

注意up并非手机的竖屏UIDeviceOrientation.portrait模式拍摄的,因为这些参数其实都是相对相机传感器而来的。另外图片的方向和手机拍摄对应关系上面已经注释清楚了,值得一提的是并非前置摄像头就对应下面记得带mirrored方向。
比如一张图UIImage.imageOrientation == UIImage.Orientation.right说明本身逆时针旋转了90度,自然显示时需要顺时针旋转90度。

iPhone11ProMax_Sample

首先看一张iPhone 11 Pro Max拍摄的样张(注意不要压缩,话说在互联网找到这样一张带有正确方向的图片还真不容易,这里借用一张网上的图片,如果有版权问题作者请留言必删),然后我们可以使用下面的代码读取到Meta(或Exif)和imageOrientation信息如下:

if let url = Bundle.main.url(forResource: "iPhoneXR_Portrait", withExtension: "jpg") {
    do {
        let data = try Data(contentsOf: url)
        if let cgimage = CGImageSourceCreateWithData(data as CFData, nil) {
            if let attr = CGImageSourceCopyPropertiesAtIndex(cgimage, 0, nil) {
                if let image = UIImage(data:data) {
                    debugPrint("ImageOrientation:\(String(describing: image.imageOrientation.rawValue))")
                }
                debugPrint("MetaInfo:")
                debugPrint(attr as NSDictionary)
            }
        }
    } catch {
        print("error:\(error.localizedDescription)")
    }
}
打印内容比较长,点击查看打印结果 ``` "ImageOrientation:3" "MetaInfo:" { ColorModel = RGB; DPIHeight = 72; DPIWidth = 72; Depth = 8; Orientation = 6; PixelHeight = 3024; PixelWidth = 4032; ProfileName = "Display P3"; "{Exif}" = { ApertureValue = "1.69599381283836"; BrightnessValue = "9.252236963900032"; ComponentsConfiguration = ( 1, 2, 3, 0 ); DateTimeDigitized = "2019:06:28 18:45:43"; DateTimeOriginal = "2019:06:28 18:45:43"; ExifVersion = ( 2, 2, 1 ); ExposureBiasValue = 0; ExposureMode = 0; ExposureProgram = 2; ExposureTime = "0.00103950103950104"; FNumber = "1.8"; Flash = 16; FlashPixVersion = ( 1, 0 ); FocalLenIn35mmFilm = 26; FocalLength = "4.25"; ISOSpeedRatings = ( 25 ); LensMake = ; LensModel = "iPhone XR back camera 4.25mm f/1.8"; LensSpecification = ( "4.25", "4.25", "1.8", "1.8" ); MeteringMode = 5; PixelXDimension = 4032; PixelYDimension = 3024; SceneCaptureType = 0; SceneType = 1; SensingMethod = 2; ShutterSpeedValue = "9.910588639093874"; SubjectArea = ( 2013, 1511, 2116, 1270 ); SubsecTimeDigitized = 354; SubsecTimeOriginal = 354; WhiteBalance = 0; }; "{GPS}" = { Altitude = "14.96670574443142"; AltitudeRef = 0; DateStamp = "2019:06:28"; DestBearing = "275.3164977571025"; DestBearingRef = T; HPositioningError = "6.852588686481304"; ImgDirection = "275.3164977571025"; ImgDirectionRef = T; Latitude = "24.25116166666667"; LatitudeRef = N; Longitude = "118.0952083333333"; LongitudeRef = E; Speed = "0.110432714091527"; SpeedRef = K; TimeStamp = "10:45:42"; }; "{JFIF}" = { DensityUnit = 0; JFIFVersion = ( 1, 0, 1 ); XDensity = 72; YDensity = 72; }; "{MakerApple}" = { 1 = 10; 14 = 4; 2 = {length = 512, bytes = 0x4e005100 5d006700 73007800 9800f800 ... c500c000 a0005f00 }; 20 = 10; 23 = 0; 25 = 0; 26 = q825s; 3 = { epoch = 0; flags = 1; timescale = 1000000000; value = 315296098277500; }; 31 = 0; 33 = 0; 35 = ( 571, 268435846 ); 37 = 386; 38 = 3; 39 = "56.35717"; 4 = 1; 40 = 1; 5 = 184; 6 = 189; 7 = 1; 8 = ( "0.001883197", "-0.8499792", "0.5379266" ); }; "{TIFF}" = { DateTime = "2019:06:28 18:45:43"; Make = ; Model = "iPhone XR"; Orientation = 6; ResolutionUnit = 2; Software = "12.3.1"; TileLength = 512; TileWidth = 512; XResolution = 72; YResolution = 72; }; } ```

首先上面的照片的imageOrientation=3也就是right(逆时针旋转90度),可以计算出拍摄时手机是portraint竖屏拍摄的(哈哈,不是手机倒过来啊,可以测试)。如果说要正确展示其实应该顺时针旋转90度就可以了,浏览器本身是做了处理的,当然也有软件没有处理,比如当前博主的编辑器预览界面是这样的(如下:这里是截图),这是因为编辑器预览界面并没有正确处理造成的:首先编辑器并没有读取图片方向信息,而是按照图片的真实像素展示,理论上它应该读取图片方向然后顺时针旋转90度,但是因为并没有那么做而造成的。

iPhonePortrait_inEditor_screenshot

尽管如此,上面的图片虽然imageOrientation=3,可是为什么TIFF中的meta信息为什么是Orientation = 6呢?两者又有什么关系呢?首先看一下Exif中的信息:

exif_orientation

其正确的方向可以通过上图看到,当然上面也少了imageOrientation中所得mirred方向,其实这个是通过翻转而来:

exif_orientation_flip

关于imageOrientation和exif中的orientation flag两者有着一一对应的关系,但是值又是不同的,记住即可:

            UIImage.imageOrientation     TIFF/IPTC kCGImagePropertyOrientation

iPhone native     UIImageOrientationUp    = 0  =  Landscape left  = 1  
rotate 180deg     UIImageOrientationDown  = 1  =  Landscape right = 3  
rotate 90CCW      UIImageOrientationLeft  = 2  =  Portrait  down  = 8  
rotate 90CW       UIImageOrientationRight = 3  =  Portrait  up    = 6  

需要指出的是,无论是CGImage(这里并不是CGImageSource)、CIImage都是没有Meta的,UIImage可能有,但是即使有也是不全的。了解这个一点很重要,不然转化或者保存时Meta就丢失了。就拿上面的例子来说,我们打印Meta信息其实使用的是Data类型,这个Data是直接从文件(也可以是相册)读取的,如果你读取到的是UIImage然后转化成Data(比如说UIImage.pngData)此时查看Exif将会打印如下信息:

 {
    ColorModel = RGB;
    Depth = 8;
    PixelHeight = 3024;
    PixelWidth = 4032;
    ProfileName = "Display P3";
    "{Exif}" =     {
        PixelXDimension = 4032;
        PixelYDimension = 3024;
    };
    "{PNG}" =     {
        InterlaceType = 0;
    };
}

如果换成UIImage.jpegData(compressionQuality: 1.0)再打印可以看到:

{
    ColorModel = RGB;
    Depth = 8;
    Orientation = 6;
    PixelHeight = 3024;
    PixelWidth = 4032;
    ProfileName = "Display P3";
    "{Exif}" =     {
        ColorSpace = 65535;
        PixelXDimension = 4032;
        PixelYDimension = 3024;
    };
    "{JFIF}" =     {
        DensityUnit = 0;
        JFIFVersion =         (
            1,
            0,
            1
        );
        XDensity = 72;
        YDensity = 72;
    };
    "{TIFF}" =     {
        Orientation = 6;
    };
}

也就是说UIImage本身可能包含Exif但是不一定齐全,如果是pngData也并不会包含方向信息。但是还要提的是上面的UIImage是通过UIImage(data:XXX)创建的,如果通过CGImage或者CIImage创建则情况又不一样,不如说通过CGImage创建然后同样的方法打印(转化成UIImage.jpegData(compressionQuality: 1.0)),可以看到下面的Exif信息,方向已经不对了(注意如果保存这个图片方向是错误的):

{
    ColorModel = RGB;
    Depth = 8;
    Orientation = 1;
    PixelHeight = 3024;
    PixelWidth = 4032;
    ProfileName = "Display P3";
    "{Exif}" =     {
        ColorSpace = 65535;
        PixelXDimension = 4032;
        PixelYDimension = 3024;
    };
    "{JFIF}" =     {
        DensityUnit = 0;
        JFIFVersion =         (
            1,
            0,
            1
        );
        XDensity = 72;
        YDensity = 72;
    };
    "{TIFF}" =     {
        Orientation = 1;
    };
}

所以总结起来Data、UIImage、CGImage、CIImage之间方向的传递并非对等,只有Data以及从Data创建的UIImage才能正确处理图片方向,其他情况均需要考虑方向问题。

操作Meta

既然搞清楚了图片方向的控制属性,那么其实要正确处理图片方向就不难了,当然你不要试图操作imageOrientation这个属性是readonly,正确的操作方式就是操作orientation flag。通常我们遇到图片不正确的情况多数是因为你编辑了图片没有正确的还原造成orientation flag的值和图片实际的像素排布不符造成的(人眼视觉认为图片像素起始行应该在上面,也就是up是正确的),比如下图中的F字样的图像,首先我们认为第一篇F型展示才是正确的而旋转倒过来都是不对的(比如看到 F 我们就认为显示有问题),这样配合orientation flag才能正确展示。

-w605

了解了视觉up正确性我们要解决拍照后由于使用滤镜或者编辑了图片后造成的图片方向问题就可以迎刃而解了。比如就拿上面的iPhone拍摄的照片来说,比如说你想加一个滤镜然后保存通常的处理方法可能是这样:

if let path = Bundle.main.path(forResource: "iPhoneXR_Portrait", ofType: "jpg") {
    guard let originImage = UIImage(contentsOfFile: path), let cgImage = originImage.cgImage else { return }
    let ciImage = CIImage(cgImage: cgImage)
    let outputImage = ciImage.applyingFilter("CIExposureAdjust", parameters: ["inputEV":0.6])
    let context = CIContext()
    if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
        let image = UIImage(cgImage: cgImage)
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
    }
}

如果方便运行代码可以在cgImage后面打个断点使用Xcode查看一下cgImage可以看是一张天空在左边的旋转图片,类似于上文提到的编辑器预览效果一样:

iPhonePortrait_inEditor_screenshot

原因上面也提到过,这个因为CGImage没有exif信息,而视觉up和相机保留的信息不同造成看起来出错。继续应用滤镜之后会发现保存起来的效果也是错误的:

filter_image_inalbu

解决这个问题其实并不复杂,因为肯定Core Image框架开发者已经想到这个问题了,只需要在创建会UIImage时传入原图imageOrientation即可:

let image = UIImage(cgImage: cgImage, scale: 1.0, orientation: originImage.imageOrientation)

不过必须强调的是这种方式并非修改了orientation flag,还是没有exif信息的,只是把图片旋转过来达到视觉up的效果。

那么有没有方式可以既保存修改后的图片又保存原始Exif呢?当然解决方式就是处理后再生成UIImage时不用传递originImage.imageOrientation,而是在生成后重新写入原始Meta信息即可。

if let url = Bundle.main.url(forResource: "iPhoneXR_Portrait", withExtension: "jpg") {
    guard let originImageData = try? Data(contentsOf: url), let originImage = UIImage(data: originImageData), let originCGImage = originImage.cgImage else { return }
                
    let newData = UIImage(cgImage: originCGImage).jpegData(compressionQuality: 1.0)
    
    var metaInfo:NSDictionary?
    if let image = CGImageSourceCreateWithData(newData! as CFData, nil) {
        if let attr = CGImageSourceCopyPropertiesAtIndex(image, 0, nil) {
            metaInfo = attr as NSDictionary
            print(metaInfo)
        }
    }
        
    let ciImage = CIImage(cgImage: originCGImage)
    let outputImage = ciImage.applyingFilter("CIExposureAdjust", parameters: ["inputEV":0.6])
    let context = CIContext()
    if let filterCGImage = context.createCGImage(outputImage, from: outputImage.extent){
        let filterImage = UIImage(cgImage: filterCGImage)
        if let filterImageData = filterImage.jpegData(compressionQuality: 0.8),let compressImage = UIImage(data: filterImageData) { // 压缩图片
            let data = NSMutableData()
            if let imageDest = CGImageDestinationCreateWithData(data as CFMutableData, kUTTypeJPEG, 1, nil),let metaInfo = metaInfo {
                CGImageDestinationAddImage(imageDest, compressImage.cgImage!, metaInfo)
                CGImageDestinationFinalize(imageDest)
                PHPhotoLibrary.shared().performChanges({
                    let creationRequest = PHAssetCreationRequest.forAsset()
                    creationRequest.addResource(with: PHAssetResourceType.photo, data: newData as! Data, options: nil)
                }) { (isSuccess, error) in
                    if isSuccess {
                        print("Save success...")
                    }
                }
                
            }
        }
    }
}

上面的代码首先读取Meta保存到MetaInfo,然后给图片应用滤镜,最后通过CGImageDestinationCreateWithData将应用滤镜后的图片写入Meta信息,最后使用PHPhotoLibrary保存到相册。这里着重说一下保存时不要使用UIImage,比如上面UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil),因为上面说过UIImage并不包含完整的Exif。

试一下下面的代码(修改PHPhotoLibrary保存照片的方式,直接保存UIImage):

if let newImage = UIImage(data: data.copy() as! Data) {
    UIImageWriteToSavedPhotosAlbum(newImage, nil, nil, nil)
}

可以发现保存之后没有Meta信息,当然这并不是因为data中没有而是转化成UIImage以后丢失了,而UIImageWriteToSavedPhotosAlbum(xxx)并没有一个可以传Data类型的重载。比如可以试一下下面的方式应该可以正确保存Meta:

if let newImage = UIImage(data: data.copy() as! Data) {
    let path = NSTemporaryDirectory() + "1.jpg"
    let url = URL(fileURLWithPath: path)
    do {
        try (data.copy() as? Data)?.write(to: url)
    } catch {
        print("error:\(error.localizedDescription)")
    }
}

常用的fixOrientation

相信大家遇到图片旋转问题一搜索就会有下面一段代码出现(当然可能是OC版本):

public func fixOrientation() -> UIImage {
    if imageOrientation == .up {
        return self
    }
        
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    draw(in: CGRect(origin: CGPoint.zero, size: size))
    let processdImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
        
    return processdImage ?? self
}

首先说明这种方式并不能保存Exif信息,如果没有保存Meta的需求通常可以直接解决问题,之所以可以修正图片的方向本质是什么呢?了解这些才能正确的运用这个方法。比如下面的代码其实是不能正确修复方向信息的:

if let path = Bundle.main.path(forResource: "iPhoneXR_Portrait", ofType: "jpg") {
    guard let originImage = UIImage(contentsOfFile: path), let cgImage = originImage.cgImage else { return }
    let ciImage = CIImage(cgImage: cgImage)
    let outputImage = ciImage.applyingFilter("CIExposureAdjust", parameters: ["inputEV":0.6])
    let context = CIContext()
    if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
        let image = UIImage(cgImage: cgImage)
        let newImage = image.fixOrientation()
        UIImageWriteToSavedPhotosAlbum(newImage, nil, nil, nil)
    }
}

那么正确的用法是什么呢?

if let path = Bundle.main.path(forResource: "iPhoneXR_Portrait", ofType: "jpg") {
    guard let originImage = UIImage(contentsOfFile: path)?.fixOrientation(), let cgImage = originImage.cgImage else { return }
    let ciImage = CIImage(cgImage: cgImage)
    let outputImage = ciImage.applyingFilter("CIExposureAdjust", parameters: ["inputEV":0.6])
    let context = CIContext()
    if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
        let image = UIImage(cgImage: cgImage)
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
    }
}

因为fixOrientation()方法本身并非修改了图片信息而是将图片修改为视觉up并且移除Meta中的方向信息,前面的代码不能正确修复的原因是图片已经没有Meta信息了,它也就不能正确修正了。

还有另一个版本的fixOrientation是通过transform旋转修正,了解了imageOrientation或者Meta的orientation这么做也是可以的比如示例图片中的imageOrientation == .right只需要使用CGAffineTransform顺时针旋转90度即可正确展示,这里不再赘述。

总结

  • 先明确一个概念就是图片的真实存储信息是以相机传感器正向(相机正向就是横向模式,手机的话就是,竖平逆时针旋转90度),图片实际存储就是以传感器拍摄来存储的(传感器的物理上方就是图片的首行像素存储位置),然后通过读取Meta中的方向信息(和imageOrientation有一一对应关系)通过transform正确展示。所谓正确展示是让传感器拍摄的图片的首行像素展示在上面。
  • 正确操作Meta的方向信息应该使用Data方式来读取图片,而不是UIImage、CGImage或者CIImage,UIImage中具体是否包含正确的方向信息要看是通过何种方式创建的比如通过UIImage(data:xxx)是包含方向信息的,CGImage和CIImage都不包含正确的方向信息,通过其转化都会丢失正确的方向信息,也就是说通过CGImage、CIImage处理的图片或者非Data创建的UIImage都应该考虑图片方向问题。
posted @ 2020-02-03 16:17  KenshinCui  阅读(1762)  评论(0编辑  收藏  举报