圖片裁剪是頭像上傳、文檔處理、縮圖生成等場景的高頻需求。Java 標準庫 javax.imageio 提供零依賴的圖片讀寫與區域裁剪能力。本文封裝通用工具類,支援任意矩形區域裁剪、多格式輸出、記憶體安全與異常處理。
ImageReadParam param = reader.getDefaultReadParam();
Rectangle rect = new Rectangle(x, y, width, height); // 裁剪區域
param.setSourceRegion(rect); // 僅讀取指定區域,節省記憶體
BufferedImage cropped = reader.read(0, param);
優勢:不需載入完整圖片到記憶體,直接解碼目標區域,大圖處理效能優異。
package com.example.util;
import javax.imageio.*;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Iterator;
public final class ImageCropUtil {
private ImageCropUtil() {}
/**
* 裁剪圖片並保存
* @param x 左上角 X 座標
* @param y 左上角 Y 座標
* @param width 裁剪寬度
* @param height 裁剪高度
* @param srcPath 源圖片路徑
* @param destPath 目標保存路徑
* @throws IOException 讀寫異常
*/
public static void crop(int x, int y, int width, int height,
String srcPath, String destPath) throws IOException {
File srcFile = new File(srcPath);
if (!srcFile.exists()) throw new FileNotFoundException("源文件不存在: " + srcPath);
String format = getFormatName(srcPath);
if (format == null) throw new IOException("無法識別圖片格式: " + srcPath);
try (FileInputStream fis = new FileInputStream(srcFile);
ImageInputStream iis = ImageIO.createImageInputStream(fis)) {
// 1. 獲取對應格式的 Reader
Iterator readers = ImageIO.getImageReadersByFormatName(format);
if (!readers.hasNext()) throw new IOException("無可用解碼器: " + format);
ImageReader reader = readers.next();
try {
reader.setInput(iis, true); // true = 僅向前讀取,節省記憶體
// 2. 設定裁剪區域
ImageReadParam param = reader.getDefaultReadParam();
Rectangle rect = new Rectangle(x, y, width, height);
// 邊界檢查:防止越界導致異常
int imgWidth = reader.getWidth(0);
int imgHeight = reader.getHeight(0);
if (x < 0 || y < 0 || x + width > imgWidth || y + height > imgHeight) {
throw new IllegalArgumentException(
String.format("裁剪區域超出圖片邊界: 圖片[%dx%d] 裁剪[%d,%d,%dx%d]",
imgWidth, imgHeight, x, y, width, height));
}
param.setSourceRegion(rect);
// 3. 讀取裁剪後的圖片
BufferedImage cropped = reader.read(0, param);
if (cropped == null) throw new IOException("裁剪結果為空");
// 4. 寫入目標文件
File destFile = new File(destPath);
destFile.getParentFile().mkdirs(); // 確保目錄存在
boolean written = ImageIO.write(cropped, format, destFile);
if (!written) throw new IOException("無對應編碼器寫入格式: " + format);
} finally {
reader.dispose(); // 釋放原生資源
}
}
}
/**
* 裁剪並返回 BufferedImage(不寫文件,供後續處理)
*/
public static BufferedImage cropToImage(int x, int y, int width, int height, String srcPath)
throws IOException {
File srcFile = new File(srcPath);
String format = getFormatName(srcPath);
try (FileInputStream fis = new FileInputStream(srcFile);
ImageInputStream iis = ImageIO.createImageInputStream(fis)) {
Iterator readers = ImageIO.getImageReadersByFormatName(format);
ImageReader reader = readers.next();
try {
reader.setInput(iis, true);
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(x, y, width, height));
return reader.read(0, param);
} finally {
reader.dispose();
}
}
}
/** 從文件名/路徑推斷格式名 */
private static String getFormatName(String path) {
int dot = path.lastIndexOf('.');
if (dot > 0 && dot < path.length() - 1) {
return path.substring(dot + 1).toLowerCase();
}
return null;
}
// --- 使用示例 ---
public static void main(String[] args) {
try {
// 裁剪並保存
ImageCropUtil.crop(100, 50, 400, 300, "input.jpg", "output_crop.jpg");
System.out.println("裁剪完成");
// 裁剪後進一步處理(如縮放、水印)
BufferedImage img = ImageCropUtil.cropToImage(0, 0, 200, 200, "logo.png");
// ImageIO.write(img, "png", new File("logo_200.png"));
} catch (IOException e) {
System.err.println("裁剪失敗: " + e.getMessage());
e.printStackTrace();
}
}
}
public static BufferedImage cropCircle(BufferedImage src, int x, int y, int diameter) {
BufferedImage output = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = output.createGraphics();
// 抗鋸齒
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 繪製圓形裁剪區域
Shape circle = new Ellipse2D.Double(0, 0, diameter, diameter);
g2.setClip(circle);
g2.drawImage(src, -x, -y, null);
g2.dispose();
return output;
}
public static BufferedImage resizeAndCropCenter(BufferedImage src, int targetW, int targetH) {
double srcRatio = (double) src.getWidth() / src.getHeight();
double dstRatio = (double) targetW / targetH;
int cropW, cropH, cropX, cropY;
if (srcRatio > dstRatio) { // 源圖較寬,上下裁剪
cropH = src.getHeight();
cropW = (int) (cropH * dstRatio);
cropX = (src.getWidth() - cropW) / 2;
cropY = 0;
} else { // 源圖較高,左右裁剪
cropW = src.getWidth();
cropH = (int) (cropW / dstRatio);
cropX = 0;
cropY = (src.getHeight() - cropH) / 2;
}
BufferedImage cropped = src.getSubimage(cropX, cropY, cropW, cropH);
return scaleImage(cropped, targetW, targetH);
}
private static BufferedImage scaleImage(BufferedImage src, int w, int h) {
BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = dst.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(src, 0, 0, w, h, null);
g2.dispose();
return dst;
}
| 場景 | 建議 |
|---|---|
| 大圖裁剪(>50MB) | 使用 ImageReadParam.setSourceRegion 僅解碼目標區域,避免全圖載入 |
| 批量處理 | 複用 ImageReader、ImageInputStream;處理完立即 dispose() |
| 高併發服務 | 引入 TwelveMonkeys ImageIO 插件支援更多格式;考慮異步隊列削峰 |
| 記憶體受限環境 | 設置 -Djavax.imageio.maxPixels=... 限制最大像素;使用 DiskCacheImageInputStream |
基於 ImageIO + ImageReadParam 的裁剪方案零依賴、記憶體友好、支援主流格式。工具類封裝了邊界檢查、資源管理、格式自動識別,可直接用於生產。進階場景(圓形裁剪、智能縮圖、濾鏡)可組合 Graphics2D 實現。超大圖/高併發建議引入 TwelveMonkeys 或遷移至 Thumbnailator/ImgLib2 等專業庫。