將水印圖片按指定角度旋轉,以網格狀鋪滿整張目標圖片,支援透明度、間距、角度配置。
public class ImageMarkUtil {
/** 水印透明度 0.0-1.0 */
private static float alpha = 0.5f;
/** 水印旋轉角度 */
private static double degree = 30.0;
/** 水印間距 (像素) */
private static int interval = 50;
/** 統一配置入口 */
public static void setImageMarkOptions(float alpha, int degree, int interval) {
if (alpha > 0) ImageMarkUtil.alpha = alpha;
if (degree != 0) ImageMarkUtil.degree = degree;
if (interval > 0) ImageMarkUtil.interval = interval;
}
/** 主入口:給圖片添加水印
* @param waterImgPath 水印圖片路徑 (建議 PNG/GIF 支援透明)
* @param srcImgPath 源圖片路徑
* @param targetPath 輸出路徑 (可覆蓋原圖)
*/
public static void waterMarkByImg(String waterImgPath, String srcImgPath, String targetPath)
throws Exception {
waterMarkByImg(waterImgPath, srcImgPath, targetPath, degree);
}
/** 完整參數版本 */
public static void waterMarkByImg(String waterImgPath, String srcImgPath,
String targetPath, double degree) throws Exception {
OutputStream os = null;
try {
// 1. 讀取源圖
Image srcImg = ImageIO.read(new File(srcImgPath));
BufferedImage buffImg = new BufferedImage(
srcImg.getWidth(null), srcImg.getHeight(null),
BufferedImage.TYPE_INT_RGB);
// 2. 繪圖上下文
Graphics2D g = buffImg.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(srcImg.getScaledInstance(
srcImg.getWidth(null), srcImg.getHeight(null),
Image.SCALE_SMOOTH), 0, 0, null);
// 3. 旋轉變換 (以圖片中心為原點)
if (degree != 0) {
g.rotate(Math.toRadians(degree),
buffImg.getWidth() / 2.0, buffImg.getHeight() / 2.0);
}
// 4. 載入水印圖片
ImageIcon imgIcon = new ImageIcon(waterImgPath);
Image waterImg = imgIcon.getImage();
int wWidth = imgIcon.getIconWidth();
int wHeight = imgIcon.getIconHeight();
// 5. 設定透明度合成規則
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
// 6. 網格鋪滿繪製
for (int y = interval + wHeight; y < buffImg.getHeight();
y += interval + wHeight) {
for (int x = interval + wWidth; x < buffImg.getWidth();
x += interval + wWidth) {
g.drawImage(waterImg, x - wWidth, y - wHeight, null);
}
}
// 7. 恢復合成模式並釋放
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
g.dispose();
// 8. 輸出文件
os = new FileOutputStream(targetPath);
ImageIO.write(buffImg, "JPG", os);
System.out.println("水印添加完成: " + targetPath);
} finally {
if (os != null) try { os.close(); } catch (IOException ignored) {}
}
}
}
String watermarkPath = "/path/to/watermark.png"; String uploadPath = "/path/to/uploaded/photo.jpg"; ImageMarkUtil.waterMarkByImg(watermarkPath, uploadPath); // 覆蓋原圖
// 透明度 0.3、角度 45°、間距 30px ImageMarkUtil.setImageMarkOptions(0.3f, 45, 30); ImageMarkUtil.waterMarkByImg(watermarkPath, uploadPath, uploadPath);
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
String savePath = saveFile(file); // 你的保存邏輯
// 異步添加水印,不阻塞響應
CompletableFuture.runAsync(() -> {
try {
ImageMarkUtil.setImageMarkOptions(0.4f, 30, 40);
ImageMarkUtil.waterMarkByImg(
"/static/watermark/logo.png", savePath, savePath);
} catch (Exception e) {
log.error("水印添加失敗", e);
}
});
return Result.ok(savePath);
}
| 技術點 | 實現細節 |
|---|---|
| 旋轉中心 | g.rotate(angle, width/2, height/2) 以圖片中心旋轉,避免邊緣截斷 |
| 透明度合成 | AlphaComposite.SRC_ATOP 只在目標像素非透明處顯示水印,配合 alpha 參數 |
| 網格計算 | 雙層迴圈步長 = 水印尺寸 + interval,確保均勻分佈 |
| 圖片類型 | 源圖用 TYPE_INT_RGB,水印建議 PNG 保留 Alpha 通道 |
| 資源釋放 | Graphics2D.dispose() + 流關閉,防止記憶體洩漏 |
ImageIO.read 時配合 ImageReader 分塊處理,或縮放後再加水印BufferedImage,避免重複 ImageIO.readsetImageMarkOptions 修改靜態變數,高併發建議改為參數傳遞g.drawString() + 字體/顏色/斜體設置@EnableWatermark 自動配置