338 lines
8.2 KiB
Go
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
|
|
}
|