一、核心需求分析

我們希望工具類支持兩種主流模式:

模式 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);