目前从事主营业务为在线设计的一家公司,其中某个业务为用户在线设计,然后将用户创建的设计下载渲染成图片。前几天,我在工作中遇到了一个奇怪的问题。某张用苹果手机拍摄的图片在浏览器中使用时是正着的,但是当下载生成图片后图片便横了过来。同样的照片,我使用小米手机拍摄则可以正常使用,很好奇这是为什么,因为这确实影响到了我的正常使用,于是研究了一下其中的原因。
带着上边的问题,我们先来了解一下关于图片的一些数据信息存储方式,以及关于图片的一些知识。
Exif(可交换图像文件格式)是一种协议,用于存储有关数码相机拍摄的图像的各种元信息。Exif与实际图像数据一起存储。Exif中的某些元信息包括相机制造商,快门速度,焦距,方向,拍摄时间等。这些元信息称为标签,每个标签都有一个由Exif格式标准决定的特定标签号。标签的完整列表及其相关信息可在此处找到。
在这里,我们对方向元信息感兴趣。用相机拍摄照片时,可能并不总是将相机保持在相机顶部与场景顶部相对应的位置。该博客下面的图片 清楚地说明了这个想法:
但是,无论如何握持相机,如果您在计算机上查看图像,图像都将以正确的方向显示。这与Exif方向标志有关。当您以非直立姿势握持照相机时,所拍摄的原始照片将存储为旋转的图像。数字设备(例如智能手机或数码相机)具有传感器,用于记录相机的方向并将该信息写入Exif中的方向标志。
Exif方向标记可以具有1到9的9个不同值。下图2显示了其中的八个:
通常,对于数码照片,您只会得到标记1、8、3、6。标志2、7、4、5代表镜像和旋转的图像版本。
因为苹果手机拍摄的照片会带有90°的旋转角,小米手机则不会(有的安卓也会,比如三星),当我们在苹果手机上或者是浏览器上查看带有exif属性的图片时,因为两者都读取了Exif信息并根据信息对图片进行了适应,它将基于方向信息自动旋转原始图像,所以我们看到的就是我们拍摄的。但是当我们使用没有兼容图片的exif信息的软件读取或者查看时,则看到的是旋转之前的图片,所以给我们的感觉看到的和拍摄的方向颠倒了。
同理,我们的图片渲染引擎不支持读取图片的这种额外信息,所以它处理的实际是图片的原图,即旋转之前的图片,所以浏览器是支持的展示没有问题,但是下载下来就有问题了。
这里推荐两款软件,自认为比较好用的
官网地址:https://www.impulseadventure.com/photo/jpeg-snoop.html
每张数码照片都包含大量隐藏的信息-JPEGsnoop的编写是为了向好奇的人公开这些细节。该软件还可以判断图片是否进行过ps(不要叫女朋友知道)。
直接导入图片就可以,图片读取完之后会自动将图片的属性输出到日志中,从上图红框里可以看到,该图片带有旋转属性,并且进行了90°的旋转,可以看到Orientation的值为6,可以在Exif方向判断哪里找到6对应的旋转方向。
官网地址:https://www.irfanview.com
IrfanView是Windows上出色的图像查看器,它也可以查看图像的Exif信息。
默认情况下,IrfanView会遵守Exif信息,并将根据其方向标记自动旋转图像。要禁用此行为,请转到Options -> Properties/Settings
,单击JPG/PCD/GIF
并取消选中该框Auto-rotate image according to EXIF info (if available)
。
我使用的是metadata-extractor,开源,很多“厂子”都在用。
项目地址:https://github.com/drewnoakes/metadata-extractor
maven依赖
com.drewnoakes
metadata-extractor
2.14.0
示例代码
@Test
public void getImageInfo(){
try {
// 获取图片元信息
Metadata metadata = ImageMetadataReader.readMetadata(new File("C:\\Users\\Administrator\\Desktop\\image.jpg"));
// 获取图片标签库
Iterable directories = metadata.getDirectories();
for(Directory directory : directories){
// 获取图片标签信息
Collection tags = directory.getTags();
for(Tag tag : tags){
System.out.println(tag.toString());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
输出信息
[File] File Name - image.jpg
[File] File Size - 2188218 bytes
[File] File Modified Date - Mon Jun 29 10:28:57 CST 2020
[ICC Profile] Profile Size - 548
[ICC Profile] CMM Type - appl
[ICC Profile] Version - 4.0.0
[ICC Profile] Class - Display Device
[ICC Profile] Color space - RGB
[ICC Profile] Profile Connection Space - XYZ
[ICC Profile] Profile Date/Time - Mon Aug 07 21:22:32 CST 2017
[ICC Profile] Signature - acsp
[ICC Profile] Primary Platform - Apple Computer, Inc.
[ICC Profile] Device manufacturer - APPL
[ICC Profile] XYZ values - 0.9642029 1.0 0.8249054
[ICC Profile] Tag Count - 10
[ICC Profile] Profile Description - Display P3
[ICC Profile] Copyright - Copyright Apple Inc., 2017
[ICC Profile] Media White Point - (0.9504547, 1.0, 1.0890503)
[ICC Profile] Red Colorant - (0.51512146, 0.24119568, 65536.0)
[ICC Profile] Green Colorant - (0.29197693, 0.6922455, 0.041885376)
[ICC Profile] Blue Colorant - (0.15710449, 0.0665741, 0.7840729)
[ICC Profile] Red TRC - para(0x70617261): 32 bytes
[ICC Profile] Chromatic Adaptation - sf32(0x73663332): 44 bytes
[ICC Profile] Blue TRC - para(0x70617261): 32 bytes
[ICC Profile] Green TRC - para(0x70617261): 32 bytes
[Exif IFD0] Orientation - Right side, top (Rotate 90 CW)
[Exif IFD0] X Resolution - 72 dots per inch
[Exif IFD0] Y Resolution - 72 dots per inch
[Exif IFD0] Resolution Unit - Inch
[Exif IFD0] YCbCr Positioning - Center of pixel array
[Exif SubIFD] Exif Version - 2.21
[Exif SubIFD] Components Configuration - YCbCr
[Exif SubIFD] FlashPix Version - 1.00
[Exif SubIFD] Color Space - sRGB
[Exif SubIFD] Exif Image Width - 4032 pixels
[Exif SubIFD] Exif Image Height - 3024 pixels
[Exif SubIFD] Scene Capture Type - Standard
[JPEG] Compression Type - Baseline
[JPEG] Data Precision - 8 bits
[JPEG] Image Height - 3024 pixels
[JPEG] Image Width - 4032 pixels
[JPEG] Number of Components - 3
[JPEG] Component 1 - Y component: Quantization table 0, Sampling factors 2 horiz/2 vert
[JPEG] Component 2 - Cb component: Quantization table 1, Sampling factors 1 horiz/1 vert
[JPEG] Component 3 - Cr component: Quantization table 1, Sampling factors 1 horiz/1 vert
[Exif Thumbnail] Thumbnail Compression - JPEG (old-style)
[Exif Thumbnail] X Resolution - 72 dots per inch
[Exif Thumbnail] Y Resolution - 72 dots per inch
[Exif Thumbnail] Resolution Unit - Inch
[Exif Thumbnail] Thumbnail Offset - 286 bytes
[Exif Thumbnail] Thumbnail Length - 9057 bytes
属性解释
1、将图片按照旋转角旋转的值再将图片转回去,应用到我们的业务中,就是保证用户在设计中图片的方向保持和下载一致,也就是说叫浏览器也不读取图片的EXIF数据,然后在通过我们设计工具自带的旋转功能手动将图片旋转回去,这样的问题就是对用户不够友好,但是保证了用户做的和渲染出来的一致。
通过Java代码将图片旋转回去,在用户上传图片时通过代码将图片旋转,但是这样实际保存的就不是用户上传的原始图片,而是处理之后的图片。
参考:https://blog.csdn.net/c20081052/article/details/89479970?utm_medium=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.compare&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.compare
public class ImageTest {
@Test
public void test(){
String fileFromPath = "C:\\Users\\Administrator\\Desktop\\image.jpg";
String fileToPath = "C:\\Users\\Administrator\\Desktop\\target_image.jpg";
String contentType = "jpg";
try {
rotate(fileFromPath,fileToPath, contentType);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @desc 旋转图片
* @author dataozi
* @date 2020/7/1 9:20
* @param fileFromPath 源图片路径
* @param fileToPath 目标图片路径
* @param contentType 图片类型
*/
private void rotate(String fileFromPath,String fileToPath, String contentType) throws Exception{
// 校验参数
if(StringUtils.isBlank(fileFromPath)){
throw new RuntimeException("file path can not be null");
}
File image = new File(fileFromPath);
if(!image.exists()){
throw new RuntimeException(String.format("%s can not be find", fileFromPath));
}
contentType = StringUtils.isBlank(contentType) ? "jpg" : contentType;
// 获取图片旋转角度
Integer angel = getImageRotateAngle(image);
if(NumberUtils.INTEGER_ZERO.equals(angel)){
return;
}
// 读取原图片的宽高
BufferedImage bufferedImage = ImageIO.read(image);
int width = bufferedImage.getWidth(null);
int height = bufferedImage.getHeight(null);
// 计算目标图片的宽高
int[] mathNewSize = mathNewSize(width, height, angel);
int targetWidth = mathNewSize[0];
int targetHeight = mathNewSize[1];
// 绘制目标图片
BufferedImage res = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = res.createGraphics();
g2.translate((targetWidth - width) / 2, (targetHeight - height) / 2);
g2.rotate(Math.toRadians(angel), width / 2.0, height / 2.0);
g2.drawImage(bufferedImage, null, null);
// 输出目标图片
ImageIO.write(res,contentType, new File(fileToPath));
}
/**
* @desc 获取旋转之后图片的宽高
* @author dataozi
* @date 2020/7/1 9:45
* @param width 原图宽
* @param height 原图高
* @param angel 旋转角度
* @return int[] arr[0]目标图片宽 arr[1]目标图片高
*/
private int[] mathNewSize(int width, int height, Integer angel) {
if (angel >= 90) {
if (angel / 90 % 2 == 1) {
int temp = height;
height = width;
width = temp;
}
angel = angel % 90;
}
// 求平方根
double r = Math.sqrt(height * height + width * width) / 2;
double len = 2 * Math.sin(Math.toRadians(angel) / 2) * r;
double angelAlpha = (Math.PI - Math.toRadians(angel)) / 2;
// 根据图片旋转中心画圆,计算目标图片宽高
double angelWidth = Math.atan((double) height / width);
double angelHeight = Math.atan((double) width / height);
int lenWidth = (int) (len * Math.cos(Math.PI - angelAlpha
- angelWidth));
int lenHeight = (int) (len * Math.cos(Math.PI - angelAlpha
- angelHeight));
// 计算新的宽高
return new int[]{width + lenWidth * 2, height + lenHeight * 2};
}
/**
* @desc 获取原图片的旋转角
* @author dataozi
* @date 2020/7/1 8:44
* @param image 图片文件
* @return 旋转角度数
*/
private Integer getImageRotateAngle(File image){
Integer angle = NumberUtils.INTEGER_ZERO;
try {
// 获取图片元信息
Metadata metadata = ImageMetadataReader.readMetadata(image);
// 这里我们直接获取图片元信息标签库
ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
// 这里我们直接获取旋转角度。注意,这里返回的是方向值(1,6,8,3)
int orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
// 这里我们将角度值转换为旋转角度
switch (orientation){
case 6:
angle = 90;
break;
case 3:
angle = 180;
break;
case 8:
angle = 270;
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
return angle;
}
}
2、通过阿里云OSS
因为我们的图片借用了阿里云的OSS,在用户上传时直接将图片上传上去,然后通过OSS的访问参数控制图片不旋转,这样保证了不会更改用户上传的原图,同时达到效果。
3、对图片进行压缩,相当于去除图片的exif属性,同时保证图片不进行回转。这样保证了用户上传的图和设计中的图一致,并且因为没有了旋转参数,下载也就一致了。
@Test
public void zipImage(){
String fromImage = "C:\\Users\\Administrator\\Desktop\\image.jpg";
String toImage = "C:\\Users\\Administrator\\Desktop\\target_image.jpg";
String contentType = "jpg";
try {
BufferedImage bufferedImage = ImageIO.read(new File(fromImage));
int width = bufferedImage.getWidth(null);
int height = bufferedImage.getHeight(null);
Thumbnails.of(fromImage).size(height, width).outputQuality(1f).outputFormat(contentType).toFile(toImage);
} catch (IOException e) {
e.printStackTrace();
}
}
4、叫算法部门渲染引擎兼容旋转角问题。