iOS开发tip-图片方向
概述
相信稍微接触过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都应该考虑图片方向问题。
iOS开发tip-图片方向的更多相关文章
- iOS开发中图片方向的获取与更改
iOS开发中 再用到照片的时候 或多或少遇到过这样的问题 就是我想用的照片有横着拍的有竖着排的 所以导致我选取图片后的效果也横七竖八的 显示效果不好 比如: 图中红圈选中的图片选取的是横着拍 ...
- iOS开发基础-图片切换(4)之懒加载
延续:iOS开发基础-图片切换(3),对(3)里面的代码用懒加载进行改善. 一.懒加载基本内容 懒加载(延迟加载):即在需要的时候才加载,修改属性的 getter 方法. 注意:懒加载时一定要先判断该 ...
- iOS开发基础-图片切换(3)之属性列表
延续:iOS开发基础-图片切换(2),对(2)里面的代码用属性列表plist进行改善. 新建 Property List 命名为 Data 获得一个后缀为 .plist 的文件. 按如图修改刚创建的文 ...
- iOS开发基础-图片切换(2)之懒加载
延续:iOS开发基础-图片切换(1),对(1)里面的代码进行改善. 在 ViewController 类中添加新的数组属性: @property (nonatomic, strong) NSArra ...
- iOS开发基础-图片切换(1)
一.程序功能分析 1)点击左右箭头切换图片.序号.描述: 2)如果是首张图片,左边箭头失效: 3)如果是最后一张图片,右边箭头失效. 二.程序实现 定义确定图片位置.大小的常量: //ViewCont ...
- 李洪强iOS开发之图片拉伸技巧
纵观移动市场,一款移动app,要想长期在移动市场立足,最起码要包含以下几个要素:实用的功能.极强的用户体验.华丽简洁的外观.华丽外观的背后,少不了美工的辛苦设计,但如果开发人员不懂得怎么合理展示这些设 ...
- 【ios开发】图片拉伸
最近在做一个项目 其中要自己定制一个View 如图: 但是美工给了我的图片尺寸却是不一样的. 分别是599*80 26*61 于是就成了这样的效果. 很明显的发现取消四周不对劲. 于是我就去找美工姐 ...
- iOS开发之图片分辨率与像素对齐
像素对齐的概念 在iOS中,有一个概念叫做像素对齐,如果像素不对齐,那么在GPU渲染时,需要进行插值计算,这个插值计算的过程会有性能损耗. 在模拟器上,有一个选项可以把像素不对齐的部分显示出来.  ...
- iOS 开发--开源图片处理圆角
概述 开源项目名称:HYBImageCliped 当前版本:2.0.0 项目用途:可给任意继承UIView的控件添加任意多个圆角.可根据颜色生成图片且可带任意个圆角.给UIButton设置不同状态下的 ...
随机推荐
- keil中使用_at_绝对地址定位问题
最近在做51单片机的时候,看到程序中某头文件有这样一段: 其中,_at_的作用就是将变量限定存放在指定的RAM空间.比如在这个单片机头文件中,就是将变量P00F,P01F分别存到Addr(0x8000 ...
- Google 开源的 Python 命令行库:深入 fire(一)
作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...
- 工具系列 | Docker基本概念小结
▍什么是Docker? Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows 机器上,也可以实现虚拟化.容 ...
- 【题解】CF741D(DSU on TREE)
[题解]CF741D(DSU on TREE) 写一写这道题来学习学习模板 用二进制来转换一下条件,现在就是要求一下\(lowbit(x)=x\)的那些路径了. DSU on TREE 是这样一种算法 ...
- 高阶函数HOF和高阶组件HOC(Higher Order Func/Comp)
一.什么是高阶函数(组件),作用是什么? 子类使用父类的方法可以通过继承的方式实现,那无关联组件通信(redux).父类使用子类方法(反向继承)呢 为了解决类(函数)功能交叉/功能复用等问题,通过传入 ...
- 菜鸟学习Fabric源码学习 — Endorser背书节点
Fabric 1.4 源码分析 Endorser背书节点 本文档主要介绍fabric背书节点的主要功能及其实现. 1. 简介 Endorser节点是peer节点所扮演的一种角色,在peer启动时会创建 ...
- 27.openpyxl 向指定单元格添加图片并修改图片大小 以及修改单元格行高列宽
openpyxl 向指定单元格添加图片并修改图片大小 以及修改单元格行高列宽 from openpyxl import Workbook,load_workbook from openpyxl.dra ...
- matplotlib绘制符合论文要求的图片
最近需要将实验数据画图出来,由于使用python进行实验,自然使用到了matplotlib来作图. 下面的代码可以作为画图的模板代码,代码中有详细注释,可根据需要进行更改. # -*- coding: ...
- 【一起学源码-微服务】Feign 源码三:Feign结合Ribbon实现负载均衡的原理分析
前言 前情回顾 上一讲我们已经知道了Feign的工作原理其实是在项目启动的时候,通过JDK动态代理为每个FeignClinent生成一个动态代理. 动态代理的数据结构是:ReflectiveFeign ...
- C++ | C++ 基础知识 | 类型与声明
一.类型 C++ 包含一整套基本类型,这些类型对应计算机最基本的存储单元并且展现 1.0 布尔值 一个布尔变量(bool)的取值或者是 true 或者是 false,布尔变量常用于表达逻辑运算结果. ...