- 新增 imglib 包,支持 FilePath/URL/Base64 三种图片输入方式 - 纯白占比检测、白底居中合成、等比缩放、去白边、裁切 - 二维码生成与识别、条形码生成(Code128/EAN13/Code39) - 中文文字图片、书籍信息水印、通用水印叠加 - 输出辅助:EncodeToBytes/EncodeToBase64/SaveToFile/SaveJPEG/SavePNG - 字体缓存,避免重复加载 - 完整测试覆盖(23个测试用例)
446 lines
11 KiB
Go
446 lines
11 KiB
Go
package imglib
|
||
|
||
import (
|
||
"image"
|
||
"image/color"
|
||
"image/png"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// createTestPNG 创建一个纯色测试 PNG 图片,返回文件路径
|
||
func createTestPNG(t *testing.T, width, height int, fillColor color.RGBA) string {
|
||
t.Helper()
|
||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
for y := 0; y < height; y++ {
|
||
for x := 0; x < width; x++ {
|
||
img.Set(x, y, fillColor)
|
||
}
|
||
}
|
||
dir := t.TempDir()
|
||
path := filepath.Join(dir, "test.png")
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := png.Encode(f, img); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return path
|
||
}
|
||
|
||
// createTestPNGWithPattern 创建一个带内容的测试图片
|
||
func createTestPNGWithPattern(t *testing.T) string {
|
||
t.Helper()
|
||
img := image.NewRGBA(image.Rect(0, 0, 100, 50))
|
||
// 白色背景
|
||
for y := 0; y < 50; y++ {
|
||
for x := 0; x < 100; x++ {
|
||
img.Set(x, y, color.White)
|
||
}
|
||
}
|
||
// 黑色方块(非白色像素)
|
||
for y := 10; y < 30; y++ {
|
||
for x := 20; x < 40; x++ {
|
||
img.Set(x, y, color.Black)
|
||
}
|
||
}
|
||
dir := t.TempDir()
|
||
path := filepath.Join(dir, "pattern.png")
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := png.Encode(f, img); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return path
|
||
}
|
||
|
||
// ===================== ImageInput 测试 =====================
|
||
|
||
func TestImageInput_FromFile(t *testing.T) {
|
||
path := createTestPNG(t, 10, 10, color.RGBA{255, 0, 0, 255})
|
||
input := NewImageInputFromFile(path)
|
||
img, format, err := input.Load()
|
||
if err != nil {
|
||
t.Fatalf("从文件加载图片失败: %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
if format != "png" {
|
||
t.Errorf("期望格式 png,得到 %s", format)
|
||
}
|
||
}
|
||
|
||
func TestImageInput_FromBase64(t *testing.T) {
|
||
path := createTestPNG(t, 5, 5, color.RGBA{0, 255, 0, 255})
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b64 := base64Encode(data)
|
||
|
||
input := NewImageInputFromBase64(b64)
|
||
img, format, err := input.Load()
|
||
if err != nil {
|
||
t.Fatalf("从 base64 加载图片失败: %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
if format != "png" {
|
||
t.Errorf("期望格式 png,得到 %s", format)
|
||
}
|
||
}
|
||
|
||
func TestImageInput_FromBase64_WithPrefix(t *testing.T) {
|
||
path := createTestPNG(t, 5, 5, color.RGBA{0, 0, 255, 255})
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b64WithPrefix := "data:image/png;base64," + base64Encode(data)
|
||
|
||
input := NewImageInputFromBase64(b64WithPrefix)
|
||
img, format, err := input.Load()
|
||
if err != nil {
|
||
t.Fatalf("从带前缀的 base64 加载图片失败: %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
_ = format
|
||
}
|
||
|
||
func TestImageInput_Empty(t *testing.T) {
|
||
input := ImageInput{}
|
||
_, _, err := input.Load()
|
||
if err == nil {
|
||
t.Fatal("空的 ImageInput 应该返回错误")
|
||
}
|
||
}
|
||
|
||
// ===================== 纯白占比检测 =====================
|
||
|
||
func TestCalculateWhitePercentage_FullWhite(t *testing.T) {
|
||
path := createTestPNG(t, 50, 50, color.RGBA{255, 255, 255, 255})
|
||
pct, err := CalculateWhitePercentage(NewImageInputFromFile(path))
|
||
if err != nil {
|
||
t.Fatalf("CalculateWhitePercentage 失败: %v", err)
|
||
}
|
||
if pct != 1.0 {
|
||
t.Errorf("全白图片期望占比 1.0,得到 %f", pct)
|
||
}
|
||
}
|
||
|
||
func TestCalculateWhitePercentage_PartialWhite(t *testing.T) {
|
||
path := createTestPNGWithPattern(t)
|
||
pct, err := CalculateWhitePercentage(NewImageInputFromFile(path))
|
||
if err != nil {
|
||
t.Fatalf("CalculateWhitePercentage 失败: %v", err)
|
||
}
|
||
if pct >= 1.0 || pct <= 0 {
|
||
t.Errorf("部分白色图片期望占比在 0~1 之间,得到 %f", pct)
|
||
}
|
||
}
|
||
|
||
func TestCalculateWhitePercentage_AllBlack(t *testing.T) {
|
||
path := createTestPNG(t, 20, 20, color.RGBA{0, 0, 0, 255})
|
||
pct, err := CalculateWhitePercentage(NewImageInputFromFile(path))
|
||
if err != nil {
|
||
t.Fatalf("CalculateWhitePercentage 失败: %v", err)
|
||
}
|
||
if pct != 0.0 {
|
||
t.Errorf("全黑图片期望占比 0.0,得到 %f", pct)
|
||
}
|
||
}
|
||
|
||
// ===================== 白底居中合成 =====================
|
||
|
||
func TestCreateWhiteBottomCenteredImage(t *testing.T) {
|
||
path := createTestPNG(t, 20, 20, color.RGBA{255, 0, 0, 255})
|
||
result, err := CreateWhiteBottomCenteredImage(NewImageInputFromFile(path), 100, 100)
|
||
if err != nil {
|
||
t.Fatalf("CreateWhiteBottomCenteredImage 失败: %v", err)
|
||
}
|
||
bounds := result.Bounds()
|
||
if bounds.Dx() != 100 || bounds.Dy() != 100 {
|
||
t.Errorf("期望尺寸 100x100,得到 %dx%d", bounds.Dx(), bounds.Dy())
|
||
}
|
||
}
|
||
|
||
// ===================== 缩放 =====================
|
||
|
||
func TestResizeToHeight(t *testing.T) {
|
||
path := createTestPNG(t, 100, 50, color.RGBA{255, 255, 255, 255})
|
||
result, err := ResizeToHeight(NewImageInputFromFile(path), 100)
|
||
if err != nil {
|
||
t.Fatalf("ResizeToHeight 失败: %v", err)
|
||
}
|
||
bounds := result.Bounds()
|
||
if bounds.Dy() != 100 {
|
||
t.Errorf("目标高度 100,得到 %d", bounds.Dy())
|
||
}
|
||
// 宽度应按比例缩放: 100 * (100/50) = 200
|
||
if bounds.Dx() != 200 {
|
||
t.Errorf("期望宽度 200(等比例),得到 %d", bounds.Dx())
|
||
}
|
||
}
|
||
|
||
func TestResizeToDimensions(t *testing.T) {
|
||
path := createTestPNG(t, 80, 60, color.RGBA{255, 255, 255, 255})
|
||
result, err := ResizeToDimensions(NewImageInputFromFile(path), 40, 30)
|
||
if err != nil {
|
||
t.Fatalf("ResizeToDimensions 失败: %v", err)
|
||
}
|
||
bounds := result.Bounds()
|
||
if bounds.Dx() != 40 || bounds.Dy() != 30 {
|
||
t.Errorf("期望尺寸 40x30,得到 %dx%d", bounds.Dx(), bounds.Dy())
|
||
}
|
||
}
|
||
|
||
// ===================== 裁切 =====================
|
||
|
||
func TestCrop(t *testing.T) {
|
||
path := createTestPNG(t, 100, 100, color.RGBA{255, 255, 255, 255})
|
||
result, err := Crop(NewImageInputFromFile(path), 10, 10, 50, 50)
|
||
if err != nil {
|
||
t.Fatalf("Crop 失败: %v", err)
|
||
}
|
||
bounds := result.Bounds()
|
||
if bounds.Dx() != 50 || bounds.Dy() != 50 {
|
||
t.Errorf("期望裁切尺寸 50x50,得到 %dx%d", bounds.Dx(), bounds.Dy())
|
||
}
|
||
}
|
||
|
||
func TestCrop_OutOfBounds(t *testing.T) {
|
||
path := createTestPNG(t, 30, 30, color.RGBA{255, 255, 255, 255})
|
||
_, err := Crop(NewImageInputFromFile(path), 100, 0, 10, 10)
|
||
if err == nil {
|
||
t.Fatal("越界裁切应该返回错误")
|
||
}
|
||
}
|
||
|
||
// ===================== 去白边 =====================
|
||
|
||
func TestRemoveWhiteBorder(t *testing.T) {
|
||
path := createTestPNGWithPattern(t)
|
||
result, err := RemoveWhiteBorder(NewImageInputFromFile(path))
|
||
if err != nil {
|
||
t.Fatalf("RemoveWhiteBorder 失败: %v", err)
|
||
}
|
||
if result == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
}
|
||
|
||
// ===================== 二维码生成 =====================
|
||
|
||
func TestGenerateQRCode(t *testing.T) {
|
||
qr, err := GenerateQRCode("https://example.com", 100, 100)
|
||
if err != nil {
|
||
t.Fatalf("GenerateQRCode 失败: %v", err)
|
||
}
|
||
bounds := qr.Bounds()
|
||
if bounds.Dx() != 100 || bounds.Dy() != 100 {
|
||
t.Errorf("期望尺寸 100x100,得到 %dx%d", bounds.Dx(), bounds.Dy())
|
||
}
|
||
}
|
||
|
||
// ===================== 条形码生成 =====================
|
||
|
||
func TestGenerateBarcode_Code128(t *testing.T) {
|
||
img, err := GenerateBarcode(Code128, "1234567890")
|
||
if err != nil {
|
||
t.Fatalf("GenerateBarcode Code128 失败: %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
}
|
||
|
||
func TestGenerateBarcode_EAN13(t *testing.T) {
|
||
img, err := GenerateBarcode(EAN13, "5901234123457")
|
||
if err != nil {
|
||
t.Fatalf("GenerateBarcode EAN13 失败: %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
}
|
||
|
||
func TestGenerateBarcode_Code39(t *testing.T) {
|
||
img, err := GenerateBarcode(Code39, "ABC-123")
|
||
if err != nil {
|
||
t.Fatalf("GenerateBarcode Code39 失败: %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
}
|
||
|
||
func TestGenerateBarcode_InvalidType(t *testing.T) {
|
||
_, err := GenerateBarcode("invalid", "test")
|
||
if err == nil {
|
||
t.Fatal("无效的条形码类型应该返回错误")
|
||
}
|
||
}
|
||
|
||
// ===================== 中文文字图片 =====================
|
||
|
||
func TestCreateChineseTextImage(t *testing.T) {
|
||
// 这个测试在无中文字体的环境中可能跳过
|
||
img, err := CreateChineseTextImage("测试中文文本", 300, 200, 24)
|
||
if err != nil {
|
||
t.Skipf("跳过中文文字图片测试(可能无中文字体): %v", err)
|
||
}
|
||
if img == nil {
|
||
t.Fatal("返回的图片为 nil")
|
||
}
|
||
bounds := img.Bounds()
|
||
if bounds.Dx() != 300 || bounds.Dy() != 200 {
|
||
t.Errorf("期望尺寸 300x200,得到 %dx%d", bounds.Dx(), bounds.Dy())
|
||
}
|
||
}
|
||
|
||
// ===================== 输出辅助 =====================
|
||
|
||
func TestEncodeToBytes_PNG(t *testing.T) {
|
||
img, err := GenerateQRCode("test", 50, 50)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
data, err := EncodeToBytes(img, "png", 0)
|
||
if err != nil {
|
||
t.Fatalf("EncodeToBytes PNG 失败: %v", err)
|
||
}
|
||
if len(data) == 0 {
|
||
t.Fatal("编码后的数据为空")
|
||
}
|
||
}
|
||
|
||
func TestEncodeToBytes_JPEG(t *testing.T) {
|
||
img, err := GenerateQRCode("test", 50, 50)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
data, err := EncodeToBytes(img, "jpeg", 85)
|
||
if err != nil {
|
||
t.Fatalf("EncodeToBytes JPEG 失败: %v", err)
|
||
}
|
||
if len(data) == 0 {
|
||
t.Fatal("编码后的数据为空")
|
||
}
|
||
}
|
||
|
||
func TestEncodeToBase64_PNG(t *testing.T) {
|
||
img, err := GenerateQRCode("test", 20, 20)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b64, err := EncodeToBase64(img, "png", 0)
|
||
if err != nil {
|
||
t.Fatalf("EncodeToBase64 失败: %v", err)
|
||
}
|
||
if !strings.HasPrefix(b64, "data:image/png;base64,") {
|
||
t.Errorf("期望以 'data:image/png;base64,' 开头,得到 %s", b64[:30])
|
||
}
|
||
}
|
||
|
||
func TestSaveToFile(t *testing.T) {
|
||
img, err := GenerateQRCode("test", 20, 20)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
dir := t.TempDir()
|
||
path := filepath.Join(dir, "output.png")
|
||
if err := SaveToFile(img, path); err != nil {
|
||
t.Fatalf("SaveToFile 失败: %v", err)
|
||
}
|
||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||
t.Fatal("保存的文件不存在")
|
||
}
|
||
}
|
||
|
||
func TestSaveJPEG(t *testing.T) {
|
||
img, _ := GenerateQRCode("test", 20, 20)
|
||
dir := t.TempDir()
|
||
path := filepath.Join(dir, "test.jpg")
|
||
if err := SaveJPEG(img, path, 90); err != nil {
|
||
t.Fatalf("SaveJPEG 失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestSavePNG(t *testing.T) {
|
||
img, _ := GenerateQRCode("test", 20, 20)
|
||
dir := t.TempDir()
|
||
path := filepath.Join(dir, "test.png")
|
||
if err := SavePNG(img, path); err != nil {
|
||
t.Fatalf("SavePNG 失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// ===================== 综合:ImageInput 三种方式 =====================
|
||
|
||
func TestImageInput_ThreeInputMethods(t *testing.T) {
|
||
// 1. 从文件加载
|
||
path := createTestPNGWithPattern(t)
|
||
pctFile, err := CalculateWhitePercentage(NewImageInputFromFile(path))
|
||
if err != nil {
|
||
t.Fatalf("文件方式加载失败: %v", err)
|
||
}
|
||
|
||
// 2. 从 Base64 加载(和文件加载的结果应一致)
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
pctBase64, err := CalculateWhitePercentage(NewImageInputFromBase64(base64Encode(data)))
|
||
if err != nil {
|
||
t.Fatalf("Base64方式加载失败: %v", err)
|
||
}
|
||
|
||
if pctFile != pctBase64 {
|
||
t.Errorf("文件加载(%f) 和 Base64 加载(%f) 结果不一致", pctFile, pctBase64)
|
||
}
|
||
}
|
||
|
||
// base64Encode 辅助函数
|
||
func base64Encode(data []byte) string {
|
||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||
var result strings.Builder
|
||
result.Grow((len(data) + 2) / 3 * 4)
|
||
|
||
for i := 0; i < len(data); i += 3 {
|
||
var b [3]byte
|
||
for j := 0; j < 3 && i+j < len(data); j++ {
|
||
b[j] = data[i+j]
|
||
}
|
||
n := 3
|
||
if i+3 > len(data) {
|
||
n = len(data) - i
|
||
}
|
||
|
||
val := uint(b[0])<<16 | uint(b[1])<<8 | uint(b[2])
|
||
result.WriteByte(alphabet[(val>>18)&0x3F])
|
||
result.WriteByte(alphabet[(val>>12)&0x3F])
|
||
if n >= 2 {
|
||
result.WriteByte(alphabet[(val>>6)&0x3F])
|
||
} else {
|
||
result.WriteByte('=')
|
||
}
|
||
if n >= 3 {
|
||
result.WriteByte(alphabet[val&0x3F])
|
||
} else {
|
||
result.WriteByte('=')
|
||
}
|
||
}
|
||
return result.String()
|
||
}
|