一、核心需求分析
我們希望工具類支持兩種主流模式:
模式 1:等比縮放(Fit)
在不超過目標寬高的前提下,按原圖比例縮放,空白處留白或透明。
適用:商品圖、文章封面預覽。
模式 2:中心裁剪(Crop)
先等比縮放使圖像覆蓋整個目標區域,再從中心裁剪出指定尺寸。
適用:用户頭像、方形圖標。
二、技術選型:為什麼不用 Thumbnails(Google)?
- Thumbnails of the Java (by coobird) 是優秀庫,但:
- 需引入第三方依賴
- 在高併發場景下可能有內存泄漏風險(歷史 issue)
- 企業項目常要求“零外部依賴”
✅ 本文方案:僅使用 JDK 標準庫(java.awt, javax.imageio),輕量、可控、無許可風險。
三、完整工具類實現
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
/**
* 縮略圖生成工具類(純 JDK 實現)
*/
public class ThumbnailUtils {
/**
* 生成等比縮放縮略圖(Fit 模式)
*
* @param srcImageFile 源圖片文件
* @param destFile 目標文件(如 thumb.jpg)
* @param maxWidth 最大寬度
* @param maxHeight 最大高度
* @param formatName 輸出格式:"jpg", "png", "webp" 等
* @param quality JPEG 質量(0.0 ~ 1.0),僅對 jpg/jpeg 生效
* @throws IOException
*/
public static void createThumbnailFit(
File srcImageFile,
File destFile,
int maxWidth,
int maxHeight,
String formatName,
float quality) throws IOException {
BufferedImage originalImage = ImageIO.read(srcImageFile);
if (originalImage == null) {
throw new IOException("無法讀取圖片: " + srcImageFile.getAbsolutePath());
}
int srcWidth = originalImage.getWidth();
int srcHeight = originalImage.getHeight();
// 計算縮放比例
double ratio = Math.min((double) maxWidth / srcWidth, (double) maxHeight / srcHeight);
if (ratio >= 1.0) {
// 原圖比目標小,直接複製
ImageIO.write(originalImage, formatName, destFile);
return;
}
int destWidth = (int) (srcWidth * ratio);
int destHeight = (int) (srcHeight * ratio);
BufferedImage thumbnail = scaleImage(originalImage, destWidth, destHeight);
writeImage(thumbnail, destFile, formatName, quality);
}
/**
* 生成中心裁剪縮略圖(Crop 模式)
*/
public static void createThumbnailCrop(
File srcImageFile,
File destFile,
int targetWidth,
int targetHeight,
String formatName,
float quality) throws IOException {
BufferedImage originalImage = ImageIO.read(srcImageFile);
if (originalImage == null) {
throw new IOException("無法讀取圖片: " + srcImageFile.getAbsolutePath());
}
int srcWidth = originalImage.getWidth();
int srcHeight = originalImage.getHeight();
// 計算裁剪前的縮放比例(確保覆蓋目標區域)
double ratio = Math.max((double) targetWidth / srcWidth, (double) targetHeight / srcHeight);
int scaledWidth = (int) (srcWidth * ratio);
int scaledHeight = (int) (srcHeight * ratio);
// 先縮放
BufferedImage scaledImage = scaleImage(originalImage, scaledWidth, scaledHeight);
// 再裁剪中心區域
int x = (scaledWidth - targetWidth) / 2;
int y = (scaledHeight - targetHeight) / 2;
BufferedImage cropped = scaledImage.getSubimage(x, y, targetWidth, targetHeight);
writeImage(cropped, destFile, formatName, quality);
}
/**
* 高質量縮放圖像(使用雙線性插值)
*/
private static BufferedImage scaleImage(BufferedImage source, int targetWidth, int targetHeight) {
BufferedImage dest = new BufferedImage(targetWidth, targetHeight, source.getType());
Graphics2D g2d = dest.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(source, 0, 0, targetWidth, targetHeight, null);
g2d.dispose();
return dest;
}
/**
* 寫入圖像(支持 JPEG 質量控制)
*/
private static void writeImage(BufferedImage image, File destFile, String formatName, float quality) throws IOException {
// 確保父目錄存在
if (destFile.getParentFile() != null) {
destFile.getParentFile().mkdirs();
}
String format = formatName.toLowerCase();
if ("jpg".equals(format) || "jpeg".equals(format)) {
// 使用 ImageWriter 控制 JPEG 質量
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
if (!writers.hasNext()) {
throw new IllegalStateException("No JPEG ImageWriter found");
}
ImageWriter writer = writers.next();
try (OutputStream os = new FileOutputStream(destFile);
ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality); // 0.0 ~ 1.0
writer.write(null, new javax.imageio.IIOImage(image, null, null), param);
writer.dispose();
}
} else {
// PNG/WebP 等格式直接寫入
ImageIO.write(image, format, destFile);
}
}
// ==================== 便捷方法 ====================
public static void createThumbnailFit(File src, File dest, int size) throws IOException {
createThumbnailFit(src, dest, size, size, "jpg", 0.85f);
}
public static void createThumbnailCrop(File src, File dest, int size) throws IOException {
createThumbnailCrop(src, dest, size, size, "jpg", 0.85f);
}
}
四、使用示例
示例 1:等比縮放(不超過 300×300)
File src = new File("/uploads/avatar.png");
File thumb = new File("/thumbs/avatar_300.jpg");
ThumbnailUtils.createThumbnailFit(src, thumb, 300, 300, "jpg", 0.9f);
示例 2:頭像中心裁剪為 100×100
ThumbnailUtils.createThumbnailCrop(
new File("/uploads/photo.jpg"),
new File("/avatars/user_100.jpg"),
100, 100, "jpg", 0.85f
);
示例 3:快捷方法(正方形縮略圖)
// 生成 150x150 等比縮放圖
ThumbnailUtils.createThumbnailFit(src, dest, 150);
// 生成 150x150 裁剪圖
ThumbnailUtils.createThumbnailCrop(src, dest, 150);