一 图片压缩场景
由于现在手机、相机设备越来越好,拍出来的照片清晰度也越来越高。以我的手机为例,日常排除的照片都在 2-5M 左右,这还是很一般的拍照效果。
图片越清晰,对我们的系统压力可能也就越大。考虑需要加载大量图片的场景,例如某社区/微博的评论列表,如果大家每个评论都包含 1 张以上的 5M 图片,那么仅 20 张图片就要 100M 以上,用户的流量、服务器的带宽、图片服务器的压力都可能导致加载速度过慢,影响用户体验。考虑到用户并不需要每张图片都要马上看到高清版本,那么先加载缩略图,感兴趣时再点开查看高清图片就是个比较合理的设想,从而也就需要对原始图片进行压缩。
二 常用图片操作工具
1、JDK 自带的图片工具,ImageIO、BufferedImage
、Graphics2D
;
2、Java 图片库,例如 Thumbnailator、Apache 的 commons imaging 等。
区别显然,用 ImageIO 等类无需引入外部依赖,但使用上有些繁琐;引入类库能简化操作,但也需要对类库有一定了解,否则发布到生产环境可能会引入未知问题(例如后面章节将会提到的 OOM)。
三 Thumbnailator
3.1 Thumbnailator 简介
Thumbnailator 是一个用来生成图像缩略图的 Java 类库,通过很简单的代码即可生成图片缩略图,也可直接对一整个目录的图片生成缩略图。支持:图片缩放,区域裁剪,水印,旋转,保持比例。
3.2 使用示例
pom 依赖引入:
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.13</version>
</dependency>
复制代码
快速使用示例代码:
Thumbnails.of(new File("original.jpg"))
.size(160, 160) // 压缩后图片大小,注意:第二个160表示等比例缩放
.rotate(90) // 旋转角度
.watermark(Positions.BOTTOM_RIGHT, ImageIO.read(new File("watermark.png")), 0.5f) // 水印
.outputQuality(0.8f) // 对jpg图片,设置图片质量
.toFile(new File("image-with-watermark.jpg")); // 输出图片结果
复制代码
执行上述代码,我们将一张 3000x4000 的 jpg 图片(6M)压缩到 500x375 像素(37K),并且旋转 90 度的图片。
四 Thumbnailator 使用问题
4.1 奇怪的 OOM
上述代码在某个系统中运行了一段时间,最近发现存在图片无法压缩的情况。查看日志,发现报了 OOM 的错误,具体日志如下:
显然,容易定位到是 Thumbnailator 的问题,并且出在 ImageReader 这个类上,也就是说加载图片时出了问题。但已经限制了上传图片的大小不超过 5M,通过拿到引发问题的图片,确认大小为 3M 左右,符合要求。那为什么还会导致 OOM?而且通过修改 jar 包启动的 jvm 设置-Xmx 参数到 1g 也无法解决。
4.2 初步定位
再次打开图片查看信息,我们发现虽然大小没有超标,但分辨率非常高。而且对图片做调整大小/另存为操作时,提示图片大小超过 20M,那么显然就是这个超高的分辨率造成的了。
那么为什么会在加载高分辨率的图片时会 oom 呢?
4.3 罪魁祸首
根据报错日志,跟踪代码的调用链路:
继续查看:
/**
* Creates a <code>BufferedImage</code> with a given width and
* height according to the specification embodied in this object.
*
* @param width the desired width of the returned
* <code>BufferedImage</code>.
* @param height the desired height of the returned
* <code>BufferedImage</code>.
*
* @return a new <code>BufferedImage</code>
*
* @exception IllegalArgumentException if either <code>width</code> or
* <code>height</code> are negative or zero.
* @exception IllegalArgumentException if the product of
* <code>width</code> and <code>height</code> is greater than
* <code>Integer.MAX_VALUE</code>, or if the number of array
* elements needed to store the image is greater than
* <code>Integer.MAX_VALUE</code>.
*/
public BufferedImage createBufferedImage(int width, int height) {
try {
SampleModel sampleModel = getSampleModel(width, height);
WritableRaster raster =
Raster.createWritableRaster(sampleModel,
new Point(0, 0));
return new BufferedImage(colorModel, raster,
colorModel.isAlphaPremultiplied(),
new Hashtable());
} catch (NegativeArraySizeException e) {
// Exception most likely thrown from a DataBuffer constructor
throw new IllegalArgumentException
("Array size > Integer.MAX_VALUE!");
}
}
复制代码
继续 java.awt.image.Raster:
public static WritableRaster createWritableRaster(SampleModel sm,
Point location) {
if (location == null) {
location = new Point(0,0);
}
return createWritableRaster(sm, sm.createDataBuffer(), location);
}
复制代码
出在 sm.createDataBuffer(),再继续:
public abstract DataBuffer createDataBuffer();
复制代码
SampleModel 是一个抽象类,找到对应的实现类:
至此,明确了问题原因所在,DataBufferByte 初始化时报错,加载图片过大导致 OOM。
4.4 问题小结
Thumbnailator 处理图片,使用的是 Java 原生的 ImageReader 类读取原始图片,得到一个 BufferedImage 对象,该对象将图片的数据全部存放在内存中,因此当原始图片太大时,会消耗很多内存。如果机器内存较小、或 JVM 设置的内存较小时,就会出现 OOM。
可以想到的解决方案,要么对图片进行限制,在大小的基础上再加上分辨率;要么就是加大 JVM 内存(多图片并行处理或超大图片暂不考虑)。
但这里还有个问题,要获取分辨率,通过 ImageIO 加载图片的话还绕不开内存泄露的问题,这就需要另外找办法获取图片的分辨率信息。这就可以考虑 Apache Commons Imaging。
五 commons-imaging
5.1 简介
Apache Commons Imaging,以前被称为 Apache Commons Sanselan,是一个可以读写各种图像格式的库,包括快速解析图像信息(大小、色彩空间、ICC 配置文件等)和元数据。
这个库是纯 Java 的。与典型的本地代码的图像 I/O 库相比,它更具有可移植性,而且应该更可靠,对损坏/恶意的图像更安全,但仍然表现得相当好。它比 ImageIO/JAI/java.awt.Toolkit(Sun/Java 的图像支持)更容易使用,支持更多的格式(并且更正确地支持它们);也提供了对元数据的简单访问。
5.2 使用示例
5.2.1 依赖引入
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha2</version>
</dependency>
复制代码
5.2.2 代码示例
5.2.2.1 元数据读取
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.ImagingException;
import java.io.File;
import java.io.IOException;
import java.awt.image.BufferedImage;
public class CommonImagingTest {
public static void main(String[] args) {
String baseDir = "/Users/flamingskys/develop/mine/images/";
try {
File imageFile = new File(baseDir + "2961728830390_.pic_hd.jpg");
BufferedImage image = Imaging.getBufferedImage(imageFile);
// 输出基本信息
System.out.println("图像格式: " + Imaging.guessFormat(imageFile));
System.out.println("宽度: " + image.getWidth());
System.out.println("高度: " + image.getHeight());
} catch (ImagingException | IOException e) {
e.printStackTrace();
}
}
}
复制代码
通过上述代码,我们可以读取到图片的元数据信息,包括大小、分辨率等等。结合上一章的描述,就可以实现对上传图片分辨率的限制,避免压缩时再报 OOM 错误。
当然,也可以直接使用 commons-imaging 来操作图片。大家如果感兴趣后续可以继续展开介绍,本篇暂不赘述。
评论