目前从事主营业务为在线设计的一家公司,其中某个业务为用户在线设计,然后将用户创建的设计下载渲染成图片。前几天,我在工作中遇到了一个奇怪的问题。某张用苹果手机拍摄的图片在浏览器中使用时是正着的,但是当下载生成图片后图片便横了过来。同样的照片,我使用小米手机拍摄则可以正常使用,很好奇这是为什么,因为这确实影响到了我的正常使用,于是研究了一下其中的原因。
在浏览器中使用时 生成图片时
带着上边的问题,我们先来了解一下关于图片的一些数据信息存储方式,以及关于图片的一些知识。
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依赖
<dependency> <groupId>com.drewnoakes</groupId> <artifactId>metadata-extractor</artifactId> <version>2.14.0</version> </dependency>示例代码
@Test public void getImageInfo(){ try { // 获取图片元信息 Metadata metadata = ImageMetadataReader.readMetadata(new File("C:\\Users\\Administrator\\Desktop\\image.jpg")); // 获取图片标签库 Iterable<Directory> directories = metadata.getDirectories(); for(Directory directory : directories){ // 获取图片标签信息 Collection<Tag> 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、叫算法部门渲染引擎兼容旋转角问题。