基於 java.awt 原生 API 實現的企業級圖片處理工具類,無第三方依賴,支援 JPEG/PNG/GIF/BMP 等主流格式,適用於內容審核、版權保護、縮圖生成、報表導出等場景。
| 功能 | 方法簽名 | 關鍵參數 | 應用場景 |
|---|---|---|---|
| 文字水印 | pressText(target, text, font, style, size, color, x, y, alpha) | 字體、顏色、透明度、位置 | 版權聲明、機密標識、用戶水印 |
| 圖片水印 | pressImage(target, waterImg, x, y, alpha) | 水印圖、偏移量、透明度 | Logo 品牌、QR Code、簽名圖 |
| 智能縮放 | resize(file, height, width, needPadding) | 目標尺寸、是否補白 | 縮圖生成、統一規格、防變形 |
| 格式轉換 | 內建輸出 JPEG | 質量參數可擴展 | 統一存儲格式、減小體積 |
package com.example.image;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public final class ImageUtils {
private static final String DEFAULT_FORMAT = "jpg";
private ImageUtils() {}
/**
* 添加圖片水印
* @param targetImg 目標圖片路徑
* @param waterImg 水印圖片路徑 (支援透明通道 PNG)
* @param x 水平偏移,負值=居中,超出寬度=靠右
* @param y 垂直偏移,負值=居中,超出高度=靠下
* @param alpha 透明度 0.0f(全透明) ~ 1.0f(不透明)
*/
public static void pressImage(String targetImg, String waterImg, int x, int y, float alpha) {
try {
File file = new File(targetImg);
BufferedImage src = ImageIO.read(file);
int w = src.getWidth(), h = src.getHeight();
BufferedImage canvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = canvas.createGraphics();
g.drawImage(src, 0, 0, w, h, null);
BufferedImage water = ImageIO.read(new File(waterImg));
int ww = water.getWidth(), wh = water.getHeight();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
// 位置計算:負值居中,超出邊界緊貼邊緣
int dx = (x < 0) ? (w - ww) / 2 : Math.min(x, w - ww);
int dy = (y < 0) ? (h - wh) / 2 : Math.min(y, h - wh);
g.drawImage(water, dx, dy, ww, wh, null);
g.dispose();
ImageIO.write(canvas, DEFAULT_FORMAT, file);
} catch (IOException e) {
throw new RuntimeException("圖片水印失敗: " + targetImg, e);
}
}
/**
* 添加文字水印
* @param fontName 字體名稱 (如 "微軟雅黑", "宋體")
* @param fontStyle Font.PLAIN/BOLD/ITALIC/BOLD|ITALIC
* @param fontSize 字體大小 (像素)
* @param color 字體顏色
* @param x/y/alpha 同圖片水印
*/
public static void pressText(String targetImg, String text, String fontName,
int fontStyle, int fontSize, Color color,
int x, int y, float alpha) {
try {
File file = new File(targetImg);
BufferedImage src = ImageIO.read(file);
int w = src.getWidth(), h = src.getHeight();
BufferedImage canvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = canvas.createGraphics();
g.drawImage(src, 0, 0, w, h, null);
g.setFont(new Font(fontName, fontStyle, fontSize));
g.setColor(color);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
// 估算文字寬度:中文按 fontSize,英文按 fontSize/2
int textWidth = estimateTextWidth(text, fontSize);
int textHeight = fontSize;
int dx = (x < 0) ? (w - textWidth) / 2 : Math.min(x, w - textWidth);
int dy = (y < 0) ? (h - textHeight) / 2 : Math.min(y, h - textHeight);
// 繪製文字 (y 基線需加上字體高度)
g.drawString(text, dx, dy + textHeight);
g.dispose();
ImageIO.write(canvas, DEFAULT_FORMAT, file);
} catch (Exception e) {
throw new RuntimeException("文字水印失敗: " + targetImg, e);
}
}
/** 估算文字像素寬度:中文字符計 1,英文計 0.5 */
private static int estimateTextWidth(String text, int fontSize) {
int len = 0;
for (char c : text.toCharArray()) {
len += (String.valueOf(c).getBytes().length > 1) ? 1 : 0.5;
}
return (int) Math.ceil(len * fontSize);
}
/**
* 智能縮放圖片
* @param filePath 圖片路徑 (覆蓋原文件)
* @param targetHeight 目標高度
* @param targetWidth 目標寬度
* @param needPadding true=等比縮放後不足部分補白,false=強制拉伸
*/
public static void resize(String filePath, int targetHeight, int targetWidth, boolean needPadding) {
try {
File file = new File(filePath);
BufferedImage src = ImageIO.read(file);
int srcW = src.getWidth(), srcH = src.getHeight();
// 計算縮放比例 (取較小者保證完整顯示)
double ratio = Math.min((double) targetWidth / srcW, (double) targetHeight / srcH);
AffineTransformOp op = new AffineTransformOp(
AffineTransform.getScaleInstance(ratio, ratio),
AffineTransformOp.TYPE_BILINEAR); // 雙線性插值,質量較好
BufferedImage scaled = op.filter(src, null);
BufferedImage result;
if (needPadding) {
// 建立目標畫布,居中貼圖,背景白色
result = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = result.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, targetWidth, targetHeight);
int dx = (targetWidth - scaled.getWidth()) / 2;
int dy = (targetHeight - scaled.getHeight()) / 2;
g.drawImage(scaled, dx, dy, null);
g.dispose();
} else {
result = scaled;
}
ImageIO.write(result, DEFAULT_FORMAT, file);
} catch (IOException e) {
throw new RuntimeException("圖片縮放失敗: " + filePath, e);
}
}
// 測試主方法
public static void main(String[] args) {
pressImage("photo.jpg", "logo.png", -1, -1, 0.5f); // 居中半透明水印
pressText("photo.jpg", "© 2024 MyCompany", "微軟雅黑", Font.BOLD, 24, Color.WHITE, -1, -1, 0.8f);
resize("photo.jpg", 800, 600, true); // 等比縮放至 800x600 內,補白
}
}
AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)
// SRC_ATOP: 只在目標不透明區域顯示源圖像,配合 alpha 實現半透明疊加
TYPE_NEAREST_NEIGHBOR:最鄰近插值,速度最快,質量最差TYPE_BILINEAR:雙線性插值,速度質量平衡 (推薦)TYPE_BICUBIC:雙三次插值,質量最好,速度最慢Java 原生 FontMetrics 需要 Graphics 上下文,工具類採用啟發式估算:中文字符按字體大小計算,英文按一半,足夠滿足水印定位需求,避免創建臨時 Graphics 開銷。
TYPE_INT_RGB:無透明通道,輸出 JPEG 適合,記憶體佔用較小TYPE_INT_ARGB:含透明通道,處理 PNG 水印時需使用,否則透明區變黑| 需求 | 解決方案 | 關鍵技術 |
|---|---|---|
| 輸出質量控制 | JPEG 壓縮質量參數 | ImageWriteParam + JPEGImageWriteParam |
| 大圖處理不 OOM | 分塊處理/流式寫入 | ImageReader.readAsRenderedImage + ImageWriter.writeInsert |
| 圖片旋轉/翻轉 | AffineTransform 旋轉實例 | AffineTransform.getRotateInstance(Math.PI/2) |
| EXIF 方向修正 | 讀取 Orientation 標籤自動旋轉 | Metadata-Extractor 庫或 Sanselan |
| 高併發處理 | 線程池 + 異步任務 | CompletableFuture + 自定義執行器 |
Thumbnails.of(...).size(...).toFile(...)),內部封裝 AWT,適合簡單縮放水印