本文詳細講解使用 Java 2D API 實現可見數字水印,核心利用 AlphaComposite 類實現圖層疊加透明度控制。支援文字水印與圖片水印,提供完整可運行代碼與參數詳細說明。
AlphaComposite 實現 Porter-Duff 混合規則,定義源圖與目標圖像素如何混合。
AlphaComposite.getInstance(int rule, float alpha)
rule:混合規則,常用 AlphaComposite.SRC_OVER (源覆蓋目標)alpha:0.0f~1.0f,源圖透明度乘數
Ar = As + Ad * (1 - As)
Cr = Cs + Cd * (1 - As)
// Ar: 結果 Alpha, As: 源 Alpha, Ad: 目標 Alpha
// Cr: 結果顏色, Cs: 源顏色, Cd: 目標顏色
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class WatermarkUtil {
/**
* 添加文字水印
* @param srcPath 源圖片路徑
* @param text 水印文字
* @param fontName 字體名稱 (如 "微軟雅黑", "SansSerif")
* @param fontStyle 字體樣式 (Font.PLAIN/BOLD/ITALIC)
* @param fontSize 字體大小
* @param color 字體顏色
* @param alpha 透明度 0.0f-1.0f
* @param x X 座標 (文字基線左邊)
* @param y Y 座標 (文字基線底部)
* @param outputPath 輸出路徑
* @param formatName 輸出格式 (jpg, png)
*/
public static void addTextWatermark(String srcPath, String text,
String fontName, int fontStyle, int fontSize,
Color color, float alpha, int x, int y,
String outputPath, String formatName) throws IOException {
BufferedImage src = ImageIO.read(new File(srcPath));
Graphics2D g2d = src.createGraphics();
// 抗鋸齒優化
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// 字體設置
Font font = new Font(fontName, fontStyle, fontSize);
g2d.setFont(font);
// 透明度混合模式
AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
g2d.setComposite(composite);
// 繪製文字
g2d.setColor(color);
g2d.drawString(text, x, y);
g2d.dispose();
ImageIO.write(src, formatName, new File(outputPath));
}
/**
* 添加圖片水印
* @param srcPath 源圖片路徑
* @param watermarkPath 水印圖片路徑 (支援透明 PNG)
* @param alpha 透明度 0.0f-1.0f
* @param x 水印左上角 X
* @param y 水印左上角 Y
* @param width 水印寬度 (-1 保持原尺寸)
* @param height 水印高度 (-1 保持原尺寸)
* @param outputPath 輸出路徑
* @param formatName 輸出格式
*/
public static void addImageWatermark(String srcPath, String watermarkPath,
float alpha, int x, int y, int width, int height,
String outputPath, String formatName) throws IOException {
BufferedImage src = ImageIO.read(new File(srcPath));
BufferedImage watermark = ImageIO.read(new File(watermarkPath));
// 縮放水印 (如指定尺寸)
if (width > 0 && height > 0 &&
(watermark.getWidth() != width || watermark.getHeight() != height)) {
watermark = resizeImage(watermark, width, height);
}
Graphics2D g2d = src.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
g2d.setComposite(composite);
g2d.drawImage(watermark, x, y, null);
g2d.dispose();
ImageIO.write(src, formatName, new File(outputPath));
}
/**
* 圖片縮放 (雙線性插值)
*/
private static BufferedImage resizeImage(BufferedImage src, int width, int height) {
BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = dest.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(src, 0, 0, width, height, null);
g2d.dispose();
return dest;
}
/**
* 九大預設位置計算
*/
public static int[] calcPosition(Position pos, int imgW, int imgH, int wmW, int wmH, int margin) {
return switch (pos) {
case TOP_LEFT -> new int[]{margin, margin + wmH}; // 文字基線
case TOP_CENTER -> new int[]{(imgW - wmW) / 2, margin + wmH};
case TOP_RIGHT -> new int[]{imgW - wmW - margin, margin + wmH};
case MIDDLE_LEFT -> new int[]{margin, (imgH + wmH) / 2};
case CENTER -> new int[]{(imgW - wmW) / 2, (imgH + wmH) / 2};
case MIDDLE_RIGHT -> new int[]{imgW - wmW - margin, (imgH + wmH) / 2};
case BOTTOM_LEFT -> new int[]{margin, imgH - margin};
case BOTTOM_CENTER -> new int[]{(imgW - wmW) / 2, imgH - margin};
case BOTTOM_RIGHT -> new int[]{imgW - wmW - margin, imgH - margin};
};
}
public enum Position {
TOP_LEFT, TOP_CENTER, TOP_RIGHT,
MIDDLE_LEFT, CENTER, MIDDLE_RIGHT,
BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT
}
// 測試主方法
public static void main(String[] args) {
try {
String src = "F:/photo.jpg";
String logo = "F:/logo.png"; // 建議透明 PNG
String out1 = "F:/watermark_text.jpg";
String out2 = "F:/watermark_image.jpg";
// 文字水印:右下角,紅色,40% 透明
addTextWatermark(src, "版權所有 @MyCompany",
"微軟雅黑", Font.BOLD, 36,
Color.RED, 0.4f, 50, 50, out1, "jpg");
// 圖片水印:右下角,100x100,50% 透明
addImageWatermark(src, logo, 0.5f,
800, 600, 100, 100, out2, "jpg");
System.out.println("水印生成完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
| 參數 | 類型 | 範圍/建議值 | 說明 |
|---|---|---|---|
| alpha | float | 0.1f~0.5f | 0.3-0.4 較適中,過高遮擋內容,過低難識別 |
| fontName | String | 系統字體 | 跨平台建議 "SansSerif", "Serif", "Monospaced" 或註冊 TTF |
| color | Color | Color.WHITE/BLACK/RED 等 | 根據背景選擇對比色 |
| x, y | int | 像素座標 | 文字基線 / 圖片左上角 |
| formatName | String | "jpg", "png" | 需透明背景用 png |
原因:JVM 未找到指定字體文件
解決:
new Font("SansSerif", Font.PLAIN, 24)
Font font = Font.createFont(Font.TRUETYPE_FONT, new File("msyh.ttc"));
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
ge.registerFont(font);
// 之後可用 "Microsoft YaHei"
# Debian/Ubuntu
apt-get update && apt-get install -y fontconfig fonts-noto-cjk
# CentOS
yum install -y fontconfig google-noto-cjk-fonts
fc-cache -fv
原因:JPEG 不支援 Alpha 通道
解決:輸出格式用 png,讀取時確保 BufferedImage.TYPE_INT_ARGB
原因:ImageIO.read 將整圖載入堆內存
解決:
-Xmx2g本文實現的是可見水印。不可見水印需頻域嵌入:
推薦開源庫:Java-Watermark、Steganography
Java 2D API 配合 AlphaComposite.SRC_OVER 可輕鬆實現靈活的可見水印。關鍵點:正確設置字體、透明度、座標、輸出格式。生產環境需解決字體部署、大圖記憶體、高併發處理等工程化問題。不可見水印屬於資訊隱藏範疇,算法複雜度較高,建議使用成熟庫。