A:更新工具类
This commit is contained in:
1 parent
39823db70a
commit
bae2721935
14 files changed
+598
-26
No files matched your search
@@ -0,0 +1,337 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user