A:调整测试文件,并增加单独readme
This commit is contained in:
1 parent
349cfd5fa1
commit
e0484efa2e
3 files changed
+1145
-277
No files matched your search
Whitespace-only changes.
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user