Java java.awt.image 與 javax.imageio 提供完整的影像處理能力,無需外部依賴即可實現縮放、裁切、轉檔、水印等常見需求。本文整理一個生產級 ImageUtils 工具類,含完整參數說明、單元測試入口與最佳實務建議。
ColorConvertOp 一鍵轉換package com.example.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
public final class ImageUtils {
private ImageUtils() {} // 工具類禁止實例化
// ===== 常見格式常數 =====
public static final String FMT_JPG = "jpg";
public static final String FMT_JPEG = "jpeg";
public static final String FMT_PNG = "png";
public static final String FMT_GIF = "gif";
public static final String FMT_BMP = "bmp";
// ===== 1. 縮放 =====
/** 按比例縮放 */
public static void scale(String src, String dest, double ratio, boolean enlarge) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
int w = (int) (srcImg.getWidth() * (enlarge ? ratio : 1/ratio));
int h = (int) (srcImg.getHeight() * (enlarge ? ratio : 1/ratio));
resizeAndWrite(srcImg, dest, w, h, false);
}
/** 指定寬高縮放(可選補白) */
public static void scaleFixed(String src, String dest, int targetW, int targetH, boolean padWhite) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
resizeAndWrite(srcImg, dest, targetW, targetH, padWhite);
}
// ===== 2. 裁切 =====
/** 指定座標裁切 */
public static void crop(String src, String dest, int x, int y, int w, int h) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
BufferedImage cropped = srcImg.getSubimage(x, y, w, h);
ImageIO.write(cropped, getFormat(dest), new File(dest));
}
/** 等分行列裁切 */
public static void cropGrid(String src, String destDir, int rows, int cols) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
int sw = srcImg.getWidth(), sh = srcImg.getHeight();
int tileW = (int) Math.ceil((double) sw / cols);
int tileH = (int) Math.ceil((double) sh / rows);
File dir = new File(destDir);
dir.mkdirs();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int x = c * tileW;
int y = r * tileH;
int w = Math.min(tileW, sw - x);
int h = Math.min(tileH, sh - y);
BufferedImage tile = srcImg.getSubimage(x, y, w, h);
File out = new File(dir, String.format("r%d_c%d.%s", r, c, getFormat(src)));
ImageIO.write(tile, getFormat(src), out);
}
}
}
// ===== 3. 格式轉換 =====
public static void convert(String src, String dest, String formatName) throws IOException {
BufferedImage img = ImageIO.read(new File(src));
ImageIO.write(img, formatName, new File(dest));
}
// ===== 4. 灰階 =====
public static void toGray(String src, String dest) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
ColorConvertOp op = new ColorConvertOp(cs, null);
BufferedImage gray = op.filter(srcImg, null);
ImageIO.write(gray, getFormat(dest), new File(dest));
}
// ===== 5. 文字水印 =====
public static void watermarkText(String src, String dest, String text,
String fontName, int fontStyle, Color color,
int fontSize, int offsetX, int offsetY, float alpha) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
int w = srcImg.getWidth(), h = srcImg.getHeight();
BufferedImage canvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = canvas.createGraphics();
g2.drawImage(srcImg, 0, 0, null);
g2.setFont(new Font(fontName, fontStyle, fontSize));
g2.setColor(color);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
// 文字置中 + 微調
FontMetrics fm = g2.getFontMetrics();
int textW = fm.stringWidth(text);
int textH = fm.getHeight();
int x = (w - textW) / 2 + offsetX;
int y = (h + textH) / 2 + offsetY;
g2.drawString(text, x, y);
g2.dispose();
ImageIO.write(canvas, getFormat(dest), new File(dest));
}
// ===== 6. 圖片水印 =====
public static void watermarkImage(String src, String dest, String watermarkFile,
int offsetX, int offsetY, float alpha) throws IOException {
BufferedImage srcImg = ImageIO.read(new File(src));
BufferedImage markImg = ImageIO.read(new File(watermarkFile));
int w = srcImg.getWidth(), h = srcImg.getHeight();
BufferedImage canvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = canvas.createGraphics();
g2.drawImage(srcImg, 0, 0, null);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
int mw = markImg.getWidth(), mh = markImg.getHeight();
int x = (w - mw) / 2 + offsetX;
int y = (h - mh) / 2 + offsetY;
g2.drawImage(markImg, x, y, null);
g2.dispose();
ImageIO.write(canvas, getFormat(dest), new File(dest));
}
// ===== 內部輔助 =====
private static void resizeAndWrite(BufferedImage src, String dest, int w, int h, boolean padWhite) throws IOException {
BufferedImage result;
if (padWhite) {
// 保持比例縮放至目標框內,不足處補白
double ratio = Math.min((double) w / src.getWidth(), (double) h / src.getHeight());
int nw = (int) (src.getWidth() * ratio);
int nh = (int) (src.getHeight() * ratio);
BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = scaled.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(src, 0, 0, nw, nh, null);
g2.dispose();
result = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
g2 = result.createGraphics();
g2.setColor(Color.WHITE);
g2.fillRect(0, 0, w, h);
g2.drawImage(scaled, (w - nw) / 2, (h - nh) / 2, null);
g2.dispose();
} else {
// 直接拉伸填滿
result = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = result.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(src, 0, 0, w, h, null);
g2.dispose();
}
ImageIO.write(result, getFormat(dest), new File(dest));
}
private static String getFormat(String fileName) {
int dot = fileName.lastIndexOf('.');
return (dot > 0) ? fileName.substring(dot + 1).toLowerCase() : FMT_JPG;
}
// ===== 測試入口 =====
public static void main(String[] args) throws IOException {
String src = "e:/test.jpg";
scale(src, "e:/test_scale.jpg", 0.5, false);
scaleFixed(src, "e:/test_fixed.jpg", 400, 300, true);
crop(src, "e:/test_crop.jpg", 100, 100, 200, 200);
cropGrid(src, "e:/tiles", 2, 3);
convert(src, "e:/test.png", "png");
toGray(src, "e:/test_gray.jpg");
watermarkText(src, "e:/test_textwm.jpg", "版權所有", "微軟正黑體", Font.BOLD, Color.WHITE, 48, 0, 0, 0.5f);
watermarkImage(src, "e:/test_imgwm.jpg", "e:/logo.png", 10, 10, 0.7f);
}
}
| 功能 | 核心 API | 注意事項 |
|---|---|---|
| 縮放 | AffineTransformOp、getScaledInstance | 雙線性插值 RenderingHints.VALUE_INTERPOLATION_BILINEAR 品質較好 |
| 裁切 | BufferedImage.getSubimage() | 回傳視圖共用資料,寫入前建議 new BufferedImage(...) 複製 |
| 水印透明度 | AlphaComposite.SRC_ATOP | alpha 0.0~1.0,疊加模式保留原圖非透明區 |
| 文字置中 | FontMetrics.stringWidth() | 基線座標 y 需加 getAscent() 或用 getHeight() 近似 |
| PNG 透明背景 | BufferedImage.TYPE_INT_ARGB | 輸出 PNG 保留透明通道,JPG 不支援透明 |
ImageReader.setInput 分塊讀取,避免 OOM。Graphics2D 非線程安全,請勿共用實例;工具類方法內部建立即用即銷。g2.dispose() 必須呼叫,釋放原生資源。fontconfig + 字體檔,或打包 .ttf 用 Font.createFont 動態載入。CompletableFuture 或線程池並行,CPU 密集型任務核心數配置。Thumbnails.of(src).size(200,200).toFile(dest))簡化縮放代碼。