概述
相信稍微接觸過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可以使用圖片說明更加詳細:
注意up
並非手機的豎屏UIDeviceOrientation.portrait
模式拍攝的,因為這些參數其實都是相對相機傳感器而來的。另外圖片的方向和手機拍攝對應關系上面已經注釋清楚了,值得一提的是並非前置攝像頭就對應下面記得帶mirrored
方向。
比如一張圖UIImage.imageOrientation == UIImage.Orientation.right
說明本身逆時針旋轉了90度,自然顯示時需要順時針旋轉90度。
首先看一張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度,但是因為並沒有那么做而造成的。
盡管如此,上面的圖片雖然imageOrientation=3
,可是為什么TIFF
中的meta
信息為什么是Orientation = 6
呢?兩者又有什么關系呢?首先看一下Exif
中的信息:
其正確的方向可以通過上圖看到,當然上面也少了imageOrientation中所得mirred方向,其實這個是通過翻轉而來:
關於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
才能正確展示。
了解了視覺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
可以看是一張天空在左邊的旋轉圖片,類似於上文提到的編輯器預覽效果一樣:
原因上面也提到過,這個因為CGImage沒有exif信息,而視覺up
和相機保留的信息不同造成看起來出錯。繼續應用濾鏡之后會發現保存起來的效果也是錯誤的:
解決這個問題其實並不復雜,因為肯定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都應該考慮圖片方向問題。