圖像處理模組
Package:
io.leandev.appfuse.image.*,io.leandev.appfuse.measure.*
AppFuse Server 提供圖像處理工具集,支援圖片縮放、格式轉換、畫布繪圖,以及物理單位(公分、英吋)到像素的轉換。
核心特色
1. 工具類別總覽
| 類別 | 用途 |
|---|---|
| ImageReader | 讀取圖片(InputStream、byte[]) |
| ImageWriter | 寫入圖片(OutputStream、byte[]、Data URL) |
| ImageScaler | 圖片縮放(等比縮放、只縮小、填滿) |
| ImageProcessor | 有狀態的圖片處理器 |
| Canvas | 繪圖畫布(繪製圖形、文字、圖片) |
| Length | 物理單位轉換(公分、毫米、英吋、點數) |
2. 縮放模式
ImageReader 讀取圖片
靜態方法(簡單場景)
import io.leandev.appfuse.image.ImageReader;
import java.awt.image.BufferedImage;
// 從 InputStream 讀取
try (InputStream inputStream = new FileInputStream("image.png")) {
BufferedImage image = ImageReader.getBufferedImage(inputStream);
}
// 從 byte[] 讀取
byte[] imageData = Files.readAllBytes(Path.of("image.png"));
BufferedImage image = ImageReader.getBufferedImage(imageData);
實例方法(自動資源管理)
// 使用 try-with-resources
try (ImageReader reader = new ImageReader(inputStream)) {
BufferedImage image = reader.read();
// 處理圖片...
} // 自動關閉 stream
ImageWriter 寫入圖片
寫入到 OutputStream
import io.leandev.appfuse.image.ImageWriter;
// 寫入檔案(預設 PNG)
try (FileOutputStream fos = new FileOutputStream("output.png")) {
ImageWriter.writeBufferedImage(fos, bufferedImage);
}
// 指定格式
try (FileOutputStream fos = new FileOutputStream("output.jpg")) {
ImageWriter.writeBufferedImage(fos, bufferedImage, "jpg");
}
// 使用 MediaType
ImageWriter.writeBufferedImage(fos, bufferedImage, MediaType.IMAGE_JPEG);
轉換為 byte[]
// 預設 PNG 格式
byte[] pngData = ImageWriter.writeBufferedImage(bufferedImage);
// 指定格式
byte[] jpgData = ImageWriter.writeBufferedImage(bufferedImage, "jpg");
產生 Data URL(用於 HTML)
// 產生 Base64 編碼的 Data URL
String dataUrl = ImageWriter.encodeBufferedImageToDataUrl(bufferedImage);
// 結果: "data:image/png;base64,iVBORw0KG..."
// 可直接用於 HTML
// <img src="data:image/png;base64,iVBORw0KG..." />
// 指定格式
String jpgDataUrl = ImageWriter.encodeBufferedImageToDataUrl(bufferedImage, "jpg");
支援格式
常見支援格式:png, jpg, jpeg, gif, bmp, wbmp
格式支援取決於 Java ImageIO 的註冊編碼器。
ImageScaler 圖片縮放
等比縮放(scale)
import io.leandev.appfuse.image.ImageScaler;
// 縮放到指定範圍內(保持比例,類似 CSS object-fit: contain)
BufferedImage scaled = ImageScaler.scale(image, 800, 600);
// 依寬度等比縮放
BufferedImage scaledByWidth = ImageScaler.scaleByWidth(image, 400);
// 依高度等比縮放
BufferedImage scaledByHeight = ImageScaler.scaleByHeight(image, 300);
// 只指定一個維度
BufferedImage scaled = ImageScaler.scale(image, 800, null); // 只限制寬度
BufferedImage scaled = ImageScaler.scale(image, null, 600); // 只限制高度
只縮小不放大(shrink)
適合處理用戶上傳的圖片,避免小圖被放大而模糊:
// 如果原圖 > 800x600,縮小到範圍內
// 如果原圖 <= 800x600,返回原圖
BufferedImage shrunk = ImageScaler.shrink(image, 800, 600);
// 依寬度縮小
BufferedImage shrunkByWidth = ImageScaler.shrinkByWidth(image, 400);
// 依高度縮小
BufferedImage shrunkByHeight = ImageScaler.shrinkByHeight(image, 300);
填滿模式(fill)
拉伸圖片填滿指定尺寸,不保持比例:
// 類似 CSS object-fit: fill
BufferedImage filled = ImageScaler.fill(image, 800, 600);
使用物理單位
搭配 Length 類別使用物理單位:
import io.leandev.appfuse.measure.Length;
// 縮放到 5 公分寬(使用預設 300 PPI)
BufferedImage scaled = ImageScaler.scaleByWidth(image, Length.ofCentimeter(5));
// 指定 PPI
BufferedImage scaled = ImageScaler.scaleByWidth(image, Length.ofCentimeter(5), 150);
// 縮放到 10x8 公分
BufferedImage scaled = ImageScaler.scale(image,
Length.ofCentimeter(10),
Length.ofCentimeter(8),
300); // 300 PPI
ImageProcessor 圖片處理器
有狀態的處理器,適合需要固定 PPI 的場景:
import io.leandev.appfuse.image.ImageProcessor;
// 建立處理器(預設 300 PPI)
ImageProcessor processor = new ImageProcessor();
// 或指定 PPI
ImageProcessor processor = new ImageProcessor(150);
// 縮放
BufferedImage scaled = processor.scale(image, 800, 600);
// 使用物理單位
BufferedImage scaled = processor.scaleByWidth(image, Length.ofCentimeter(5));
Canvas 繪圖畫布
Canvas 封裝 Java 2D Graphics API,提供便利的繪圖方法。
建立畫布
import io.leandev.appfuse.image.Canvas;
import io.leandev.appfuse.image.Scaling;
import io.leandev.appfuse.image.Position;
// 以像素建立
Canvas canvas = new Canvas(800, 600);
// 以物理單位建立(使用預設 300 PPI)
Canvas canvas = new Canvas(Length.ofCentimeter(10), Length.ofCentimeter(8));
// 指定 PPI
Canvas canvas = new Canvas(Length.ofCentimeter(10), Length.ofCentimeter(8), 150);
// 從現有圖片建立
Canvas canvas = new Canvas(existingImage);
繪製圖形
import java.awt.Color;
import java.awt.Rectangle;
// 設定背景
canvas.setBackground(Color.WHITE);
// 設定繪圖顏色
canvas.setColor(Color.RED);
// 繪製矩形邊框
canvas.drawRect(10, 10, 100, 50);
// 填滿矩形
canvas.fillRect(10, 70, 100, 50);
// 設定邊框
canvas.setBorder(Color.BLACK);
// 設定筆觸寬度
canvas.setStroke(2.0f);
繪製圖片
// 定義繪製區域
Rectangle rect = new Rectangle(100, 100, 200, 150);
// 繪製圖片(各種模式)
canvas.drawImage(image, rect); // 原尺寸,置中
canvas.drawImage(image, rect, Scaling.CONTAIN); // 等比縮放,完整顯示
canvas.drawImage(image, rect, Scaling.FIT); // 填滿(可能變形)
canvas.drawImage(image, rect, Position.TOP_LEFT); // 原尺寸,左上角
canvas.drawImage(image, rect, Scaling.CONTAIN, Position.BOTTOM_RIGHT); // 等比縮放,右下角
// 直接指定位置
canvas.drawImage(image, 50, 50); // 左上角在 (50, 50)
繪製文字
import java.awt.Font;
// 建立字體(自動根據 PPI 轉換大小)
Font font = canvas.createFont(Font.SANS_SERIF, Font.BOLD, 12); // 12pt
canvas.setFont(font);
// 設定顏色
canvas.setColor(Color.BLACK);
// 繪製文字
Rectangle textRect = new Rectangle(100, 200, 300, 50);
canvas.drawText("Hello World", textRect); // 置中
canvas.drawText("Left aligned", textRect, Position.LEFT); // 左對齊
canvas.drawText("Top right", textRect, Position.TOP_RIGHT); // 右上角
使用物理單位
// 使用 Length 建立矩形
Rectangle rect = canvas.createRectangle(
Length.ofCentimeter(1), // x
Length.ofCentimeter(1), // y
Length.ofCentimeter(5), // width
Length.ofCentimeter(3) // height
);
// 使用 Length 建立字體
Font font = canvas.createFont(Font.SERIF, Font.PLAIN, Length.ofPoint(14));
取得結果並釋放資源
// 取得繪製結果
BufferedImage result = canvas.getImage();
// 釋放資源
canvas.dispose();
Scaling 縮放模式
| 模式 | 說明 | CSS 對應 |
|---|---|---|
FIT | 填滿區域,可能變形 | object-fit: fill |
CONTAIN | 完整顯示,保持比例 | object-fit: contain |
COVER | 填滿並裁切,保持比例 | object-fit: cover |
NONE | 保持原尺寸 | object-fit: none |
SCALE_DOWN | contain 和 none 中較小者 | object-fit: scale-down |
Position 定位
支援 9 宮格定位:
┌─────────────┬─────────────┬─────────────┐
│ TOP_LEFT │ TOP │ TOP_RIGHT │
├─────────────┼─────────────┼─────────────┤
│ LEFT │ CENTER │ RIGHT │
├─────────────┼─────────────┼─────────────┤
│ BOTTOM_LEFT │ BOTTOM │ BOTTOM_RIGHT│
└─────────────┴─────────────┴─────────────┘
Length 物理單位
Length 類別支援物理單位與像素的轉換。
建立 Length
import io.leandev.appfuse.measure.Length;
Length cm = Length.ofCentimeter(5); // 5 公分
Length mm = Length.ofMillimeter(50); // 50 毫米
Length inch = Length.ofInch(2); // 2 英吋
Length pt = Length.ofPoint(72); // 72 點(1 英吋)
Length twip = Length.ofTwip(1440); // 1440 twip(1 英吋)
單位轉換
Length cm = Length.ofCentimeter(2.54);
double mm = cm.toMillimeter(); // 25.4
double inch = cm.toInch(); // 1.0
double pt = cm.toPoint(); // 72.0
int pixel = cm.toPixel(300); // 300(以 300 PPI 計算)
運算
Length a = Length.ofCentimeter(5);
Length b = Length.ofCentimeter(3);
Length sum = a.add(b); // 8 cm
Length diff = a.subtract(b); // 2 cm
Length doubled = a.multiply(2); // 10 cm
Length half = a.divide(2); // 2.5 cm
單位常見換算
| 單位 | 說明 | 換算 |
|---|---|---|
| Point (pt) | 印刷點數 | 1 inch = 72 pt |
| Twip | 1/20 點 | 1 pt = 20 twip |
| Pixel (px) | 取決於 PPI | 1 inch = PPI pixels |
完整範例
縮圖產生器
@Service
public class ThumbnailService {
public byte[] generateThumbnail(byte[] originalImage, int maxWidth, int maxHeight)
throws IOException {
// 讀取原圖
BufferedImage image = ImageReader.getBufferedImage(originalImage);
// 縮小到指定範圍(只縮小不放大)
BufferedImage thumbnail = ImageScaler.shrink(image, maxWidth, maxHeight);
// 轉換為 byte[]
return ImageWriter.writeBufferedImage(thumbnail, "jpg");
}
}
浮水印產生器
@Service
public class WatermarkService {
public byte[] addWatermark(byte[] originalImage, String watermarkText)
throws IOException {
// 讀取原圖
BufferedImage image = ImageReader.getBufferedImage(originalImage);
// 建立畫布
Canvas canvas = new Canvas(image);
// 設定半透明白色
canvas.setColor(new Color(255, 255, 255, 128));
// 設定字體
Font font = new Font(Font.SANS_SERIF, Font.BOLD, 24);
canvas.setFont(font);
// 在右下角繪製浮水印
Rectangle textArea = new Rectangle(
image.getWidth() - 200,
image.getHeight() - 50,
180, 40
);
canvas.drawText(watermarkText, textArea, Position.BOTTOM_RIGHT);
// 取得結果
BufferedImage result = canvas.getImage();
canvas.dispose();
return ImageWriter.writeBufferedImage(result, "png");
}
}
產品圖片處理
@Service
public class ProductImageService {
private final FileStorage fileStorage;
public String processAndSaveProductImage(String tenantId, InputStream imageStream)
throws IOException {
// 讀取上傳的圖片
BufferedImage original = ImageReader.getBufferedImage(imageStream);
// 產生不同尺寸
BufferedImage large = ImageScaler.shrink(original, 1200, 1200);
BufferedImage medium = ImageScaler.shrink(original, 600, 600);
BufferedImage thumb = ImageScaler.shrink(original, 150, 150);
// 儲存各尺寸
String largeId = saveImage(tenantId, large, "large.jpg");
String mediumId = saveImage(tenantId, medium, "medium.jpg");
String thumbId = saveImage(tenantId, thumb, "thumb.jpg");
return largeId; // 返回主圖 ID
}
private String saveImage(String tenantId, BufferedImage image, String filename)
throws IOException {
byte[] data = ImageWriter.writeBufferedImage(image, "jpg");
return fileStorage.store(tenantId, filename,
new ByteArrayInputStream(data),
"image/jpeg",
data.length);
}
}
列印用圖片產生
public class PrintImageGenerator {
public byte[] generatePrintImage() throws IOException {
// 建立 A4 大小畫布(300 PPI)
Canvas canvas = new Canvas(
Length.ofCentimeter(21), // A4 寬度
Length.ofCentimeter(29.7), // A4 高度
300 // 列印品質 PPI
);
canvas.setBackground(Color.WHITE);
// 繪製標題
Font titleFont = canvas.createFont(Font.SERIF, Font.BOLD, 24);
canvas.setFont(titleFont);
canvas.setColor(Color.BLACK);
Rectangle titleRect = canvas.createRectangle(
Length.ofCentimeter(1),
Length.ofCentimeter(1),
Length.ofCentimeter(19),
Length.ofCentimeter(2)
);
canvas.drawText("產品報告", titleRect, Position.CENTER);
// 繪製產品圖片
BufferedImage productImage = ImageReader.getBufferedImage(
getClass().getResourceAsStream("/images/product.png")
);
Rectangle imageRect = canvas.createRectangle(
Length.ofCentimeter(1),
Length.ofCentimeter(4),
Length.ofCentimeter(10),
Length.ofCentimeter(10)
);
canvas.drawImage(productImage, imageRect, Scaling.CONTAIN);
// 取得結果
BufferedImage result = canvas.getImage();
canvas.dispose();
return ImageWriter.writeBufferedImage(result, "png");
}
}
最佳實踐
1. 資源管理
// 使用 try-with-resources
try (ImageReader reader = new ImageReader(inputStream)) {
BufferedImage image = reader.read();
// 處理...
}
// Canvas 記得 dispose
Canvas canvas = new Canvas(800, 600);
try {
// 繪製...
} finally {
canvas.dispose();
}
2. 上傳圖片處理
// 限制上傳圖片大小,避免記憶體問題
BufferedImage uploaded = ImageReader.getBufferedImage(inputStream);
BufferedImage processed = ImageScaler.shrink(uploaded, 2000, 2000); // 最大 2000x2000
3. 選擇適當的輸出格式
| 格式 | 適用場景 |
|---|---|
| PNG | 需要透明背景、圖示、截圖 |
| JPEG | 照片、不需透明的大圖 |
| GIF | 簡單動畫、少色彩圖片 |
下一步
- File 模組 - 檔案存儲,搭配圖片上傳
- Content 模組 - MIME 類型檢測
- HTTP 模組 - HTTP 客戶端,下載遠端圖片