跳至主要内容

圖像處理模組

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_DOWNcontain 和 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
Twip1/20 點1 pt = 20 twip
Pixel (px)取決於 PPI1 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簡單動畫、少色彩圖片

下一步