A:调整测试文件,并增加单独readme

This commit is contained in:
jimmy committed 2025-08-02 14:27:29 +08:00
1 parent 349cfd5fa1
commit e0484efa2e
3 files changed
+1145 -277

No files matched your search

View File
Whitespace-only changes.
+401 -277
View File
@@ -5,333 +5,457 @@ import (
"fmt"
"os"
"path/filepath"
"rear/internal/consts"
"rear/internal/utils"
"strconv"
"strings"
)
// ProcessOptions 图像处理选项
type ProcessOptions struct {
// 基础选项
MaxSize int // 最长边的最大尺寸(保持比例)
Quality int // 压缩质量 (1-100)
Format string // 输出格式 (jpeg, png, webp, avif等)
// 高级选项
Strip bool // 是否移除EXIF等元数据
Interlace bool // 是否启用渐进式JPEG
Optimize bool // 是否启用优化
Background string // 背景色(处理透明图像时)
Sharpen bool // 是否启用锐化
AutoRotate bool // 是否根据EXIF自动旋转
// WebP特定选项
WebPLossless bool // WebP无损压缩
WebPEffort int // WebP压缩努力程度 (0-6)
// 尺寸控制
Width int // 固定宽度(如果设置则忽略MaxSize)
Height int // 固定高度(如果设置则忽略MaxSize)
Crop bool // 是否裁剪以填充指定尺寸
}
// VipsImageInfo vips图片信息
// VipsImageInfo VIPS 图片信息
type VipsImageInfo struct {
Format string
Width int
Height int
Channels int
ColorSpace string
FileSize string
Format string `json:"format"`
Width int `json:"width"`
Height int `json:"height"`
Bands int `json:"bands"` // 通道数
HasAlpha bool `json:"has_alpha"` // 是否有透明通道
ColorSpace string `json:"colorspace"` // 颜色空间
FileSize int64 `json:"file_size"` // 文件大小(字节)
DPI float64 `json:"dpi"` // DPI
}
// DefaultOptions 返回默认处理选项
func DefaultOptions() *ProcessOptions {
return &ProcessOptions{
MaxSize: 512,
Quality: 85,
Format: "jpeg",
Strip: true,
Interlace: false,
Optimize: true,
Background: "white",
Sharpen: false,
AutoRotate: true,
WebPLossless: false,
WebPEffort: 4,
Width: 0,
Height: 0,
Crop: false,
}
// VipsCropPosition 裁剪位置枚举
type VipsCropPosition string
const (
VipsCropTop VipsCropPosition = "top"
VipsCropCenter VipsCropPosition = "center"
VipsCropBottom VipsCropPosition = "bottom"
VipsCropSmart VipsCropPosition = "smart" // 智能裁剪
VipsCropCustom VipsCropPosition = "custom" // 自定义位置
)
// VipsConvertOptions 转换选项
type VipsConvertOptions struct {
Quality int // 质量 (1-100)
StripMeta bool // 删除元数据
Progressive bool // 渐进式 (仅JPEG)
Lossless bool // 无损压缩 (仅WEBP)
}
// ProcessImageWithVips 使用 libvips 处理图像
// 示例: ProcessImageWithVips(ctx, "input.jpg", "output.webp", DefaultOptions())
func ProcessImageWithVips(ctx context.Context, inputPath, outputPath string, options *ProcessOptions) error {
if err := utils.EnsureInitialized(); err != nil {
return err
}
if options == nil {
options = DefaultOptions()
}
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
return fmt.Errorf("input file not found: %s", inputPath)
}
// 确保输出目录存在
outputDir := filepath.Dir(outputPath)
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// 构建vips命令参数
args := buildVipsArgs(inputPath, outputPath, options)
// 执行命令
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips command failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
// VipsResizeOptions 调整大小选项
type VipsResizeOptions struct {
Width int // 目标宽度
Height int // 目标高度
KeepAspect bool // 保持宽高比
Scale float64 // 缩放比例 (优先级高于宽高)
NoEnlarge bool // 不放大图片
}
// ThumbnailImage 生成缩略图(使用vips thumbnail命令)
func ThumbnailImage(ctx context.Context, input, output string, size int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
MaxSize: size,
Quality: 85,
Strip: true,
})
// VipsCropOptions 裁剪选项
type VipsCropOptions struct {
Position VipsCropPosition // 裁剪位置
X int // 自定义裁剪起始X坐标
Y int // 自定义裁剪起始Y坐标
}
// ResizeImageWithVips 使用vips调整图片大小
func ResizeImageWithVips(ctx context.Context, input, output string, width, height int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
Width: width,
Height: height,
Quality: 85,
Strip: true,
})
}
// ResizeImageKeepAspectVips 使用vips按比例调整图片大小(保持宽高比)
func ResizeImageKeepAspectVips(ctx context.Context, input, output string, maxSize int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
MaxSize: maxSize,
Quality: 85,
Strip: true,
})
}
// ConvertImageFormat 转换图像格式
func ConvertImageFormat(ctx context.Context, input, output, format string) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
Format: format,
Quality: 85,
Strip: true,
})
}
// CompressImage 压缩图像
func CompressImage(ctx context.Context, input, output string, quality int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
Quality: quality,
Strip: true,
Optimize: true,
})
}
// GetVipsImageInfo 获取图像信息
func GetVipsImageInfo(ctx context.Context, imagePath string) (*VipsImageInfo, error) {
if err := utils.EnsureInitialized(); err != nil {
// GetVipsImageInfo 获取图片基础信息
func GetVipsImageInfo(ctx context.Context, inputPath string) (*VipsImageInfo, error) {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return nil, err
}
// 构建 vipsheader.exe 的路径
vipsHeaderPath := filepath.Join(filepath.Dir(utils.VipsPath), "vipsheader.exe")
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, "identify", imagePath)
// 执行 vipsheader -a 命令
result, err := utils.ExecuteCommand(ctx, vipsHeaderPath, "-a", inputPath)
if err != nil {
return nil, fmt.Errorf("failed to get image info: %w", err)
return nil, fmt.Errorf("failed to get image info: %w, stderr: %s", err, string(result.Stderr))
}
// 解析vips identify输出
output := string(result.Stdout)
lines := strings.Split(strings.TrimSpace(output), "\n")
info := &VipsImageInfo{}
lines := strings.Split(string(result.Stdout), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "format":
info.Format = value
case "width":
if w, err := strconv.Atoi(value); err == nil {
info.Width = w
}
case "height":
if h, err := strconv.Atoi(value); err == nil {
info.Height = h
}
case "channels":
if c, err := strconv.Atoi(value); err == nil {
info.Channels = c
}
case "interpretation":
info.ColorSpace = value
switch key {
case "width":
if w, err := strconv.Atoi(value); err == nil {
info.Width = w
}
case "height":
if h, err := strconv.Atoi(value); err == nil {
info.Height = h
}
case "bands":
if b, err := strconv.Atoi(value); err == nil {
info.Bands = b
}
case "format":
info.Format = value
case "interpretation":
info.ColorSpace = value
case "alpha":
info.HasAlpha = (value == "yes")
}
}
}
// 获取文件大小
if stat, err := os.Stat(imagePath); err == nil {
info.FileSize = fmt.Sprintf("%d bytes", stat.Size())
}
return info, nil
}
// BatchProcessImages 批量处理图像
func BatchProcessImages(ctx context.Context, inputDir, outputDir string, options *ProcessOptions) error {
// ConvertVipsImage 使用 VIPS 转换图片格式
func ConvertVipsImage(ctx context.Context, inputPath, outputPath string, format consts.ImageFormat, options *VipsConvertOptions) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
if options == nil {
options = DefaultOptions()
options = &VipsConvertOptions{Quality: 85}
}
// 支持的图像格式
supportedExts := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
".tiff": true,
".bmp": true,
".tif": true,
var args []string
switch format {
case consts.FormatJPG:
args = append(args, "jpegsave", inputPath, outputPath)
if options.Quality > 0 {
args = append(args, "--Q", strconv.Itoa(options.Quality))
}
if options.Progressive {
args = append(args, "--interlace")
}
case consts.FormatPNG:
args = append(args, "pngsave", inputPath, outputPath)
if options.Quality > 0 {
compression := 9 - (options.Quality-1)*9/99 // 转换为0-9范围
args = append(args, "--compression", strconv.Itoa(compression))
}
case consts.FormatWEBP:
args = append(args, "webpsave", inputPath, outputPath)
if options.Quality > 0 {
args = append(args, "--Q", strconv.Itoa(options.Quality))
}
if options.Lossless {
args = append(args, "--lossless")
}
default:
return fmt.Errorf("unsupported format: %s", format)
}
return filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 删除元数据
if options.StripMeta {
args = append(args, "--strip")
}
if info.IsDir() {
return nil
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips convert failed: %w, stderr: %s", err, string(result.Stderr))
}
ext := strings.ToLower(filepath.Ext(path))
if !supportedExts[ext] {
return nil
}
// 计算相对路径
relPath, err := filepath.Rel(inputDir, path)
if err != nil {
return err
}
// 构建输出路径
outputPath := filepath.Join(outputDir, relPath)
// 根据输出格式修改扩展名
if options.Format != "" {
outputPath = strings.TrimSuffix(outputPath, filepath.Ext(outputPath)) + "." + options.Format
}
return ProcessImageWithVips(ctx, path, outputPath, options)
})
return nil
}
// IsVipsAvailable 检查 libvips 是否可用
// ResizeVipsImage 调整图片大小
func ResizeVipsImage(ctx context.Context, inputPath, outputPath string, options *VipsResizeOptions) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
if options == nil {
return fmt.Errorf("resize options cannot be nil")
}
var args []string
// 如果指定了缩放比例,优先使用缩放
if options.Scale > 0 {
args = []string{"resize", inputPath, outputPath, strconv.FormatFloat(options.Scale, 'f', 2, 64)}
} else {
// 获取原图信息以计算缩放比例
info, err := GetVipsImageInfo(ctx, inputPath)
if err != nil {
return fmt.Errorf("failed to get image info: %w", err)
}
var scale float64
if options.KeepAspect {
// 保持宽高比,计算最小缩放比例
scaleX := float64(options.Width) / float64(info.Width)
scaleY := float64(options.Height) / float64(info.Height)
if scaleX > 0 && scaleY > 0 {
scale = scaleX
if scaleY < scaleX {
scale = scaleY
}
} else if scaleX > 0 {
scale = scaleX
} else if scaleY > 0 {
scale = scaleY
} else {
return fmt.Errorf("invalid resize dimensions")
}
} else {
// 直接使用指定的宽高(可能会变形)
if options.Width > 0 && options.Height > 0 {
_ = float64(options.Width) / float64(info.Width)
_ = float64(options.Height) / float64(info.Height)
// 使用 thumbnail 命令可以指定具体尺寸
args = []string{"thumbnail", inputPath, outputPath,
fmt.Sprintf("%dx%d!", options.Width, options.Height)}
} else {
return fmt.Errorf("width and height must be specified when not keeping aspect ratio")
}
}
if len(args) == 0 {
// 检查是否需要防止放大
if options.NoEnlarge && scale > 1.0 {
scale = 1.0
}
args = []string{"resize", inputPath, outputPath, strconv.FormatFloat(scale, 'f', 2, 64)}
}
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips resize failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
}
// CropVipsImage 裁剪图片
func CropVipsImage(ctx context.Context, inputPath, outputPath string, width, height int, options *VipsCropOptions) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
if width <= 0 || height <= 0 {
return fmt.Errorf("invalid crop dimensions: width and height must be positive")
}
if options == nil {
options = &VipsCropOptions{Position: VipsCropCenter}
}
var args []string
switch options.Position {
case VipsCropSmart:
// 智能裁剪,使用 smartcrop
args = []string{"smartcrop", inputPath, outputPath,
strconv.Itoa(width), strconv.Itoa(height)}
case VipsCropCustom:
// 自定义位置裁剪
args = []string{"crop", inputPath, outputPath,
strconv.Itoa(options.X), strconv.Itoa(options.Y),
strconv.Itoa(width), strconv.Itoa(height)}
default:
// 使用 thumbnail 进行位置裁剪
var gravity string
switch options.Position {
case VipsCropTop:
gravity = "attention" // top -> attention
case VipsCropBottom:
gravity = "low" // bottom -> low
default:
gravity = "centre"
}
args = []string{"thumbnail", inputPath, outputPath,
fmt.Sprintf("%dx%d^", width, height),
"--crop", gravity}
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips crop failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
}
// CompressVipsImage 综合压缩图片(删除EXIF,调整大小,质量压缩,格式转换)
func CompressVipsImage(ctx context.Context, inputPath, outputPath string,
targetFormat consts.ImageFormat, targetWidth, targetHeight, quality int) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
if targetWidth <= 0 || targetHeight <= 0 {
return fmt.Errorf("invalid target dimensions: width and height must be positive")
}
if quality < 1 || quality > 100 {
return fmt.Errorf("invalid quality: must be between 1 and 100")
}
// 创建一个中间临时文件
ext := filepath.Ext(outputPath)
tmpOutput := strings.TrimSuffix(outputPath, ext) + ".tmp" + ext
// 第一步:缩略图处理(不带 strip)
argsThumb := []string{
"thumbnail", inputPath, tmpOutput,
fmt.Sprintf("%dx%d>", targetWidth, targetHeight),
}
if result, err := utils.ExecuteCommand(ctx, utils.VipsPath, argsThumb...); err != nil {
return fmt.Errorf("vips thumbnail failed: %w, stderr: %s", err, string(result.Stderr))
}
// 第二步:拷贝并压缩,带 strip 和质量设置
var argsCopy []string
switch targetFormat {
case consts.FormatJPG:
argsCopy = append(argsCopy, "jpegsave", tmpOutput, outputPath)
if quality > 0 {
argsCopy = append(argsCopy, "--Q", strconv.Itoa(quality))
}
argsCopy = append(argsCopy, "--strip")
case consts.FormatPNG:
argsCopy = append(argsCopy, "pngsave", tmpOutput, outputPath)
compression := 9 - (quality-1)*9/99
argsCopy = append(argsCopy, "--compression", strconv.Itoa(compression))
argsCopy = append(argsCopy, "--strip")
case consts.FormatWEBP:
argsCopy = append(argsCopy, "webpsave", tmpOutput, outputPath)
if quality > 0 {
argsCopy = append(argsCopy, "--Q", strconv.Itoa(quality))
}
argsCopy = append(argsCopy, "--strip")
default:
return fmt.Errorf("unsupported target format: %s", targetFormat)
}
if result, err := utils.ExecuteCommand(ctx, utils.VipsPath, argsCopy...); err != nil {
return fmt.Errorf("vips copy failed: %w, stderr: %s", err, string(result.Stderr))
}
// 可选:删除中间文件(如果你没有自动管理临时文件)
_ = os.Remove(tmpOutput)
return nil
}
// StripVipsImageMeta 删除图片元数据
func StripVipsImageMeta(ctx context.Context, inputPath, outputPath string) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
ext := strings.ToLower(filepath.Ext(outputPath))
var args []string
switch ext {
case ".jpg", ".jpeg":
args = []string{"jpegsave", inputPath, outputPath, "--strip"}
case ".png":
args = []string{"pngsave", inputPath, outputPath, "--strip"}
case ".webp":
args = []string{"webpsave", inputPath, outputPath, "--strip"}
default:
return fmt.Errorf("unsupported output format for stripping metadata: %s", ext)
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips strip meta failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
}
// IsVipsAvailable 检查 VIPS 是否可用
func IsVipsAvailable() bool {
if err := utils.EnsureInitialized(); err != nil {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return false
}
return utils.VipsPath != ""
}
// buildVipsArgs 构建vips命令参数
func buildVipsArgs(inputPath, outputPath string, options *ProcessOptions) []string {
args := []string{"thumbnail"}
// 输入文件
args = append(args, inputPath)
// 输出文件
args = append(args, outputPath)
// 尺寸参数
if options.Width > 0 && options.Height > 0 {
if options.Crop {
// 裁剪模式:填充指定尺寸
args = append(args, fmt.Sprintf("%dx%d^", options.Width, options.Height))
args = append(args, "--crop", "centre")
} else {
// 适应模式:保持比例
args = append(args, fmt.Sprintf("%dx%d", options.Width, options.Height))
}
} else if options.Width > 0 {
args = append(args, strconv.Itoa(options.Width))
} else if options.Height > 0 {
args = append(args, fmt.Sprintf("x%d", options.Height))
} else {
// 使用MaxSize(最长边)
args = append(args, strconv.Itoa(options.MaxSize))
// VipsVersion 获取 VIPS 版本信息
func VipsVersion(ctx context.Context) (string, error) {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return "", err
}
// 质量设置
if options.Quality > 0 && options.Quality <= 100 {
args = append(args, "--quality", strconv.Itoa(options.Quality))
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, "--version")
if err != nil {
return "", fmt.Errorf("failed to get vips version: %w", err)
}
// 格式转换
if options.Format != "" {
args = append(args, "--format", options.Format)
}
// 移除元数据
if options.Strip {
args = append(args, "--strip")
}
// 渐进式JPEG
if options.Interlace {
args = append(args, "--interlace")
}
// 优化
if options.Optimize {
args = append(args, "--optimize")
}
// 背景色
if options.Background != "" {
args = append(args, "--background", options.Background)
}
// 锐化
if options.Sharpen {
args = append(args, "--sharpen", "1")
}
// 自动旋转
if options.AutoRotate {
args = append(args, "--auto-rotate")
}
// WebP特定选项
if strings.ToLower(options.Format) == "webp" {
if options.WebPLossless {
args = append(args, "--webp-lossless")
}
if options.WebPEffort >= 0 && options.WebPEffort <= 6 {
args = append(args, "--webp-effort", strconv.Itoa(options.WebPEffort))
}
}
return args
return strings.TrimSpace(string(result.Stdout)), nil
}
// 快捷函数
// QuickConvertPNGToJPEG 快速将PNG转换为JPEG
func QuickConvertPNGToJPEG(ctx context.Context, inputPath, outputPath string, quality int) error {
options := &VipsConvertOptions{
Quality: quality,
StripMeta: true,
}
return ConvertVipsImage(ctx, inputPath, outputPath, consts.FormatJPG, options)
}
// QuickConvertJPEGToPNG 快速将JPEG转换为PNG
func QuickConvertJPEGToPNG(ctx context.Context, inputPath, outputPath string) error {
options := &VipsConvertOptions{
StripMeta: true,
}
return ConvertVipsImage(ctx, inputPath, outputPath, consts.FormatPNG, options)
}
// QuickConvertToWEBP 快速转换为WEBP格式
func QuickConvertToWEBP(ctx context.Context, inputPath, outputPath string, quality int, lossless bool) error {
options := &VipsConvertOptions{
Quality: quality,
StripMeta: true,
Lossless: lossless,
}
return ConvertVipsImage(ctx, inputPath, outputPath, consts.FormatWEBP, options)
}
// QuickResizeKeepAspect 快速等比例调整大小
func QuickResizeKeepAspect(ctx context.Context, inputPath, outputPath string, maxWidth, maxHeight int) error {
options := &VipsResizeOptions{
Width: maxWidth,
Height: maxHeight,
KeepAspect: true,
NoEnlarge: true,
}
return ResizeVipsImage(ctx, inputPath, outputPath, options)
}
// QuickCropCenter 快速居中裁剪
func QuickCropCenter(ctx context.Context, inputPath, outputPath string, width, height int) error {
options := &VipsCropOptions{
Position: VipsCropCenter,
}
return CropVipsImage(ctx, inputPath, outputPath, width, height, options)
}
// QuickSmartCrop 快速智能裁剪
func QuickSmartCrop(ctx context.Context, inputPath, outputPath string, width, height int) error {
options := &VipsCropOptions{
Position: VipsCropSmart,
}
return CropVipsImage(ctx, inputPath, outputPath, width, height, options)
}
@@ -0,0 +1,744 @@
package tools
import (
"context"
"os"
"path/filepath"
"rear/internal/consts"
"rear/internal/utils"
"testing"
"time"
)
// 测试图片路径
const (
testImageDir = "D:\\go-argus\\rear\\internal\\img"
outputDir = "D:\\go-argus\\rear\\internal\\imgtest_output"
)
func TestMain(m *testing.M) {
// 创建输出目录
if err := os.MkdirAll(outputDir, 0755); err != nil {
panic("Failed to create output directory: " + err.Error())
}
// 运行测试
code := m.Run()
// 清理(可选)
// os.RemoveAll(outputDir)
os.Exit(code)
}
func getTestImagePath3(filename string) string {
return filepath.Join(testImageDir, filename)
}
func getOutputPath(filename string) string {
return filepath.Join(outputDir, filename)
}
func TestIsVipsAvailable(t *testing.T) {
available := IsVipsAvailable()
if !available {
t.Skip("VIPS is not available, skipping tests")
}
t.Log("VIPS is available")
}
func TestVipsVersion(t *testing.T) {
dir := "D:\\go-argus\\rear\\output\\tools"
err := utils.Initialize(nil, &dir)
if err != nil {
return
}
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
version, err := VipsVersion(ctx)
if err != nil {
t.Fatalf("Failed to get VIPS version: %v", err)
}
t.Logf("VIPS version: %s", version)
}
func TestGetVipsImageInfo(t *testing.T) {
dir := "D:\\go-argus\\rear\\output\\tools"
err := utils.Initialize(nil, &dir)
if err != nil {
return
}
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
tests := []struct {
name string
filename string
}{
{"WEBP Image", "cyberpunk2077_world_third@3x.webp"},
{"JPEG Image", "image-1.JPG"},
{"JPG Image", "img_1.jpg"},
{"PNG Image", "Snipaste_2022-12-17_20-13-15.png"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPath := getTestImagePath3(tt.filename)
// 检查文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skipf("Test image %s not found", inputPath)
}
info, err := GetVipsImageInfo(ctx, inputPath)
if err != nil {
t.Fatalf("Failed to get image info for %s: %v", tt.filename, err)
}
t.Logf("Image: %s", tt.filename)
t.Logf(" Format: %s", info.Format)
t.Logf(" Dimensions: %dx%d", info.Width, info.Height)
t.Logf(" Bands: %d", info.Bands)
t.Logf(" Has Alpha: %t", info.HasAlpha)
t.Logf(" Color Space: %s", info.ColorSpace)
t.Logf(" File Size: %d bytes", info.FileSize)
t.Logf(" DPI: %.2f", info.DPI)
// 验证基本信息
if info.Width <= 0 || info.Height <= 0 {
t.Errorf("Invalid dimensions: %dx%d", info.Width, info.Height)
}
if info.Bands <= 0 {
t.Errorf("Invalid bands: %d", info.Bands)
}
})
}
}
func TestConvertVipsImage(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
tests := []struct {
name string
input string
output string
format consts.ImageFormat
options *VipsConvertOptions
}{
{
name: "JPEG to PNG",
input: "image-1.JPG",
output: "converted_jpg_to_png.png",
format: consts.FormatPNG,
options: &VipsConvertOptions{
Quality: 85,
StripMeta: true,
},
},
{
name: "PNG to JPEG",
input: "Snipaste_2022-12-17_20-13-15.png",
output: "converted_png_to_jpg.jpg",
format: consts.FormatJPG,
options: &VipsConvertOptions{
Quality: 90,
StripMeta: true,
Progressive: true,
},
},
{
name: "JPEG to WEBP",
input: "img_1.jpg",
output: "converted_jpg_to_webp.webp",
format: consts.FormatWEBP,
options: &VipsConvertOptions{
Quality: 80,
StripMeta: true,
Lossless: false,
},
},
{
name: "WEBP to PNG",
input: "cyberpunk2077_world_third@3x.webp",
output: "converted_webp_to_png.png",
format: consts.FormatPNG,
options: &VipsConvertOptions{
Quality: 95,
StripMeta: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPath := getTestImagePath3(tt.input)
outputPath := getOutputPath(tt.output)
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skipf("Test image %s not found", inputPath)
}
err := ConvertVipsImage(ctx, inputPath, outputPath, tt.format, tt.options)
if err != nil {
t.Fatalf("Failed to convert image: %v", err)
}
// 验证输出文件存在
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Errorf("Output file %s was not created", outputPath)
}
t.Logf("Successfully converted %s to %s", tt.input, tt.output)
})
}
}
func TestResizeVipsImage(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
tests := []struct {
name string
input string
output string
options *VipsResizeOptions
}{
{
name: "Resize by scale",
input: "image-1.JPG",
output: "resized_scale_0.5.jpg",
options: &VipsResizeOptions{
Scale: 0.5,
},
},
{
name: "Resize keep aspect ratio",
input: "img_1.jpg",
output: "resized_keep_aspect_800x600.jpg",
options: &VipsResizeOptions{
Width: 800,
Height: 600,
KeepAspect: true,
NoEnlarge: true,
},
},
{
name: "Resize fixed dimensions",
input: "Snipaste_2022-12-17_20-13-15.png",
output: "resized_fixed_400x300.png",
options: &VipsResizeOptions{
Width: 400,
Height: 300,
KeepAspect: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPath := getTestImagePath3(tt.input)
outputPath := getOutputPath(tt.output)
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skipf("Test image %s not found", inputPath)
}
err := ResizeVipsImage(ctx, inputPath, outputPath, tt.options)
if err != nil {
t.Fatalf("Failed to resize image: %v", err)
}
// 验证输出文件存在
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Errorf("Output file %s was not created", outputPath)
}
// 验证输出图片信息
info, err := GetVipsImageInfo(ctx, outputPath)
if err != nil {
t.Errorf("Failed to get output image info: %v", err)
} else {
t.Logf("Resized image dimensions: %dx%d", info.Width, info.Height)
}
t.Logf("Successfully resized %s to %s", tt.input, tt.output)
})
}
}
func TestCropVipsImage(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
tests := []struct {
name string
input string
output string
width int
height int
options *VipsCropOptions
}{
{
name: "Center crop",
input: "image-1.JPG",
output: "cropped_center_500x500.jpg",
width: 500,
height: 500,
options: &VipsCropOptions{
Position: VipsCropCenter,
},
},
{
name: "Top crop",
input: "img_1.jpg",
output: "cropped_top_400x400.jpg",
width: 400,
height: 400,
options: &VipsCropOptions{
Position: VipsCropTop,
},
},
{
name: "Smart crop",
input: "cyberpunk2077_world_third@3x.webp",
output: "cropped_smart_600x400.webp",
width: 600,
height: 400,
options: &VipsCropOptions{
Position: VipsCropSmart,
},
},
{
name: "Custom crop",
input: "Snipaste_2022-12-17_20-13-15.png",
output: "cropped_custom_300x300.png",
width: 300,
height: 300,
options: &VipsCropOptions{
Position: VipsCropCustom,
X: 100,
Y: 50,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPath := getTestImagePath3(tt.input)
outputPath := getOutputPath(tt.output)
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skipf("Test image %s not found", inputPath)
}
err := CropVipsImage(ctx, inputPath, outputPath, tt.width, tt.height, tt.options)
if err != nil {
t.Fatalf("Failed to crop image: %v", err)
}
// 验证输出文件存在
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Errorf("Output file %s was not created", outputPath)
}
// 验证输出图片尺寸
info, err := GetVipsImageInfo(ctx, outputPath)
if err != nil {
t.Errorf("Failed to get output image info: %v", err)
} else if info.Width != tt.width || info.Height != tt.height {
t.Errorf("Expected dimensions %dx%d, got %dx%d", tt.width, tt.height, info.Width, info.Height)
}
t.Logf("Successfully cropped %s to %s (%dx%d)", tt.input, tt.output, tt.width, tt.height)
})
}
}
func TestCompressVipsImage(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
tests := []struct {
name string
input string
output string
format consts.ImageFormat
targetWidth int
targetHeight int
quality int
}{
{
name: "Compress JPEG",
input: "image-1.JPG",
output: "compressed_image-1_800x600_q75.jpg",
format: consts.FormatJPG,
targetWidth: 800,
targetHeight: 600,
quality: 75,
},
{
name: "Compress PNG",
input: "Snipaste_2022-12-17_20-13-15.png",
output: "compressed_screenshot_600x400_q80.png",
format: consts.FormatPNG,
targetWidth: 600,
targetHeight: 400,
quality: 80,
},
{
name: "Compress to WEBP",
input: "img_1.jpg",
output: "compressed_img1_500x500_q70.webp",
format: consts.FormatWEBP,
targetWidth: 500,
targetHeight: 500,
quality: 70,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPath := getTestImagePath3(tt.input)
outputPath := getOutputPath(tt.output)
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skipf("Test image %s not found", inputPath)
}
// 获取原始文件信息
originalInfo, err := os.Stat(inputPath)
if err != nil {
t.Fatalf("Failed to get original file stats: %v", err)
}
err = CompressVipsImage(ctx, inputPath, outputPath, tt.format, tt.targetWidth, tt.targetHeight, tt.quality)
if err != nil {
t.Fatalf("Failed to compress image: %v", err)
}
// 验证输出文件存在
compressedInfo, err := os.Stat(outputPath)
if err != nil {
t.Errorf("Output file %s was not created: %v", outputPath, err)
return
}
// 验证文件大小减小(通常情况下)
compressionRatio := float64(compressedInfo.Size()) / float64(originalInfo.Size())
t.Logf("Compression ratio: %.2f (original: %d bytes, compressed: %d bytes)",
compressionRatio, originalInfo.Size(), compressedInfo.Size())
t.Logf("Successfully compressed %s to %s", tt.input, tt.output)
})
}
}
func TestStripVipsImageMeta(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
tests := []struct {
name string
input string
output string
}{
{
name: "Strip JPEG metadata",
input: "image-1.JPG",
output: "stripped_image-1.jpg",
},
{
name: "Strip PNG metadata",
input: "Snipaste_2022-12-17_20-13-15.png",
output: "stripped_screenshot.png",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPath := getTestImagePath3(tt.input)
outputPath := getOutputPath(tt.output)
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skipf("Test image %s not found", inputPath)
}
err := StripVipsImageMeta(ctx, inputPath, outputPath)
if err != nil {
t.Fatalf("Failed to strip metadata: %v", err)
}
// 验证输出文件存在
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Errorf("Output file %s was not created", outputPath)
}
t.Logf("Successfully stripped metadata from %s to %s", tt.input, tt.output)
})
}
}
// 测试快捷函数
func TestQuickFunctions(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
t.Run("QuickConvertPNGToJPEG", func(t *testing.T) {
inputPath := getTestImagePath3("Snipaste_2022-12-17_20-13-15.png")
outputPath := getOutputPath("quick_png_to_jpeg.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test PNG image not found")
}
err := QuickConvertPNGToJPEG(ctx, inputPath, outputPath, 85)
if err != nil {
t.Fatalf("QuickConvertPNGToJPEG failed: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
t.Log("QuickConvertPNGToJPEG succeeded")
})
t.Run("QuickConvertJPEGToPNG", func(t *testing.T) {
inputPath := getTestImagePath3("image-1.JPG")
outputPath := getOutputPath("quick_jpeg_to_png.png")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test JPEG image not found")
}
err := QuickConvertJPEGToPNG(ctx, inputPath, outputPath)
if err != nil {
t.Fatalf("QuickConvertJPEGToPNG failed: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
t.Log("QuickConvertJPEGToPNG succeeded")
})
t.Run("QuickConvertToWEBP", func(t *testing.T) {
inputPath := getTestImagePath3("img_1.jpg")
outputPath := getOutputPath("quick_to_webp.webp")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := QuickConvertToWEBP(ctx, inputPath, outputPath, 80, false)
if err != nil {
t.Fatalf("QuickConvertToWEBP failed: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
t.Log("QuickConvertToWEBP succeeded")
})
t.Run("QuickResizeKeepAspect", func(t *testing.T) {
inputPath := getTestImagePath3("image-2.JPG")
outputPath := getOutputPath("quick_resize_keep_aspect.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := QuickResizeKeepAspect(ctx, inputPath, outputPath, 800, 600)
if err != nil {
t.Fatalf("QuickResizeKeepAspect failed: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
t.Log("QuickResizeKeepAspect succeeded")
})
t.Run("QuickCropCenter", func(t *testing.T) {
inputPath := getTestImagePath3("image-3.JPG")
outputPath := getOutputPath("quick_crop_center.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := QuickCropCenter(ctx, inputPath, outputPath, 400, 400)
if err != nil {
t.Fatalf("QuickCropCenter failed: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
t.Log("QuickCropCenter succeeded")
})
t.Run("QuickSmartCrop", func(t *testing.T) {
inputPath := getTestImagePath3("image-4.JPG")
outputPath := getOutputPath("quick_smart_crop.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := QuickSmartCrop(ctx, inputPath, outputPath, 500, 300)
if err != nil {
t.Fatalf("QuickSmartCrop failed: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
t.Log("QuickSmartCrop succeeded")
})
}
// 性能测试
func BenchmarkGetVipsImageInfo(b *testing.B) {
if !IsVipsAvailable() {
b.Skip("VIPS is not available")
}
ctx := context.Background()
inputPath := getTestImagePath3("image-1.JPG")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
b.Skip("Test image not found")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := GetVipsImageInfo(ctx, inputPath)
if err != nil {
b.Fatalf("GetVipsImageInfo failed: %v", err)
}
}
}
func BenchmarkResizeVipsImage(b *testing.B) {
if !IsVipsAvailable() {
b.Skip("VIPS is not available")
}
ctx := context.Background()
inputPath := getTestImagePath3("image-1.JPG")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
b.Skip("Test image not found")
}
options := &VipsResizeOptions{
Scale: 0.5,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
outputPath := getOutputPath("bench_resize_" + time.Now().Format("20060102150405") + ".jpg")
err := ResizeVipsImage(ctx, inputPath, outputPath, options)
if err != nil {
b.Fatalf("ResizeVipsImage failed: %v", err)
}
// 清理输出文件
os.Remove(outputPath)
}
}
// 错误处理测试
func TestErrorHandling(t *testing.T) {
if !IsVipsAvailable() {
t.Skip("VIPS is not available")
}
ctx := context.Background()
t.Run("Invalid input path", func(t *testing.T) {
_, err := GetVipsImageInfo(ctx, "nonexistent.jpg")
if err == nil {
t.Error("Expected error for nonexistent file")
}
})
t.Run("Invalid resize options", func(t *testing.T) {
inputPath := getTestImagePath3("image-1.JPG")
outputPath := getOutputPath("invalid_resize.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := ResizeVipsImage(ctx, inputPath, outputPath, nil)
if err == nil {
t.Error("Expected error for nil resize options")
}
})
t.Run("Invalid crop dimensions", func(t *testing.T) {
inputPath := getTestImagePath3("image-1.JPG")
outputPath := getOutputPath("invalid_crop.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := CropVipsImage(ctx, inputPath, outputPath, 0, 0, nil)
if err == nil {
t.Error("Expected error for invalid crop dimensions")
}
})
t.Run("Invalid compress quality", func(t *testing.T) {
inputPath := getTestImagePath3("image-1.JPG")
outputPath := getOutputPath("invalid_compress.jpg")
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
t.Skip("Test image not found")
}
err := CompressVipsImage(ctx, inputPath, outputPath, consts.FormatJPG, 100, 100, 101)
if err == nil {
t.Error("Expected error for invalid quality value")
}
})
}