写点什么

Java 图片压缩:thumbnailator 与 commons-imaging

  • 2024-10-13
    吉林
  • 本文字数:3370 字

    阅读完需:约 11 分钟

Java图片压缩:thumbnailator与commons-imaging

一 图片压缩场景

由于现在手机、相机设备越来越好,拍出来的照片清晰度也越来越高。以我的手机为例,日常排除的照片都在 2-5M 左右,这还是很一般的拍照效果。

图片越清晰,对我们的系统压力可能也就越大。考虑需要加载大量图片的场景,例如某社区/微博的评论列表,如果大家每个评论都包含 1 张以上的 5M 图片,那么仅 20 张图片就要 100M 以上,用户的流量、服务器的带宽、图片服务器的压力都可能导致加载速度过慢,影响用户体验。考虑到用户并不需要每张图片都要马上看到高清版本,那么先加载缩略图,感兴趣时再点开查看高清图片就是个比较合理的设想,从而也就需要对原始图片进行压缩。

二 常用图片操作工具

1、JDK 自带的图片工具,ImageIO、BufferedImageGraphics2D

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 来操作图片。大家如果感兴趣后续可以继续展开介绍,本篇暂不赘述。


发布于: 刚刚阅读数: 5
用户头像

磨炼中成长,痛苦中前行 2017-10-22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
Java图片压缩:thumbnailator与commons-imaging_架构_程序员架构进阶_InfoQ写作社区