Files
argus-go/rear/internal/utils/tools/libvips_tool.go
T
2025-06-29 20:33:11 +08:00

338 lines
8.2 KiB
Go

package tools
import (
"context"
"fmt"
"os"
"path/filepath"
"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图片信息
type VipsImageInfo struct {
Format string
Width int
Height int
Channels int
ColorSpace string
FileSize string
}
// 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,
}
}
// 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
}
// 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,
})
}
// 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 {
return nil, err
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, "identify", imagePath)
if err != nil {
return nil, fmt.Errorf("failed to get image info: %w", err)
}
// 解析vips identify输出
info := &VipsImageInfo{}
lines := strings.Split(string(result.Stdout), "\n")
for _, line := range lines {
if strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
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
}
}
}
}
// 获取文件大小
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 {
if options == nil {
options = DefaultOptions()
}
// 支持的图像格式
supportedExts := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
".tiff": true,
".bmp": true,
".tif": true,
}
return filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
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)
})
}
// IsVipsAvailable 检查 libvips 是否可用
func IsVipsAvailable() bool {
if err := utils.EnsureInitialized(); 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))
}
// 质量设置
if options.Quality > 0 && options.Quality <= 100 {
args = append(args, "--quality", strconv.Itoa(options.Quality))
}
// 格式转换
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
}