Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
150d398db6
5 files changed
+523
No files matched your search
@@ -0,0 +1,158 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 全局变量存储工具路径
|
||||
var (
|
||||
ImageMagickPath string
|
||||
ExifToolPath string
|
||||
toolsInitOnce sync.Once
|
||||
toolsInitErr error
|
||||
)
|
||||
|
||||
// Config 初始化配置
|
||||
type Config struct {
|
||||
// 工具路径(如果为空,会自动检测)
|
||||
ImageMagickPath string
|
||||
ExifToolPath string
|
||||
}
|
||||
|
||||
// Initialize 初始化工具路径(可选调用,如果不调用会在第一次使用时自动初始化)
|
||||
func Initialize(config *Config) error {
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
|
||||
toolsInitOnce.Do(func() {
|
||||
ImageMagickPath = config.ImageMagickPath
|
||||
ExifToolPath = config.ExifToolPath
|
||||
toolsInitErr = detectTools()
|
||||
})
|
||||
|
||||
return toolsInitErr
|
||||
}
|
||||
|
||||
// detectTools 检测工具路径
|
||||
func detectTools() error {
|
||||
// 获取可执行文件所在目录
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
execDir := filepath.Dir(execPath)
|
||||
|
||||
// 检测 ImageMagick
|
||||
if ImageMagickPath == "" {
|
||||
ImageMagickPath = findTool("convert", execDir)
|
||||
if ImageMagickPath == "" {
|
||||
ImageMagickPath = findTool("magick", execDir)
|
||||
}
|
||||
}
|
||||
if ImageMagickPath == "" {
|
||||
return fmt.Errorf("ImageMagick not found")
|
||||
}
|
||||
|
||||
// 检测 ExifTool
|
||||
if ExifToolPath == "" {
|
||||
ExifToolPath = findTool("exiftool", execDir)
|
||||
}
|
||||
if ExifToolPath == "" {
|
||||
return fmt.Errorf("ExifTool not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findTool 查找工具
|
||||
func findTool(name string, execDir string) string {
|
||||
// Windows 下添加 .exe 后缀
|
||||
if runtime.GOOS == "windows" {
|
||||
name = name + ".exe"
|
||||
}
|
||||
|
||||
// 查找顺序:
|
||||
// 1. 执行文件同目录
|
||||
// 2. 执行文件同目录的 bin 子目录
|
||||
// 3. 执行文件同目录的 tools 子目录
|
||||
// 4. 系统 PATH
|
||||
|
||||
searchPaths := []string{
|
||||
filepath.Join(execDir, name),
|
||||
filepath.Join(execDir, "bin", name),
|
||||
filepath.Join(execDir, "tools", name),
|
||||
}
|
||||
|
||||
for _, path := range searchPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从 PATH 中查找
|
||||
if path, err := exec.LookPath(name); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// EnsureInitialized 确保工具已初始化
|
||||
func EnsureInitialized() error {
|
||||
if toolsInitErr == nil && ImageMagickPath == "" && ExifToolPath == "" {
|
||||
return Initialize(nil)
|
||||
}
|
||||
return toolsInitErr
|
||||
}
|
||||
|
||||
// CommandResult 命令执行结果
|
||||
type CommandResult struct {
|
||||
Stdout []byte
|
||||
Stderr []byte
|
||||
ExitCode int
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// ExecuteCommand 执行命令的通用函数
|
||||
func ExecuteCommand(ctx context.Context, program string, args ...string) (*CommandResult, error) {
|
||||
// 创建命令
|
||||
cmd := exec.CommandContext(ctx, program, args...)
|
||||
|
||||
// 准备输出缓冲区
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// 记录开始时间
|
||||
start := time.Now()
|
||||
|
||||
// 执行命令
|
||||
err := cmd.Run()
|
||||
duration := time.Since(start)
|
||||
|
||||
result := &CommandResult{
|
||||
Stdout: stdout.Bytes(),
|
||||
Stderr: stderr.Bytes(),
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
// 获取退出码
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
result.ExitCode = exitErr.ExitCode()
|
||||
} else if err == nil {
|
||||
result.ExitCode = 0
|
||||
} else {
|
||||
result.ExitCode = -1
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"rear/internal/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExifData EXIF 数据
|
||||
type ExifData map[string]interface{}
|
||||
|
||||
// GetExifData 获取 EXIF 数据
|
||||
func GetExifData(ctx context.Context, input string) (ExifData, error) {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := utils.ExecuteCommand(ctx, utils.ExifToolPath, "-j", "-n", input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("exiftool failed: %w, stderr: %s", err, string(result.Stderr))
|
||||
}
|
||||
|
||||
// 解析 JSON 输出
|
||||
var data []ExifData
|
||||
if err := json.Unmarshal(result.Stdout, &data); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse exif data: %w", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("no exif data found")
|
||||
}
|
||||
|
||||
return data[0], nil
|
||||
}
|
||||
|
||||
// GetExifField 获取特定的 EXIF 字段
|
||||
func GetExifField(ctx context.Context, input string, fields ...string) (map[string]string, error) {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []string{"-s", "-s", "-s"}
|
||||
for _, field := range fields {
|
||||
args = append(args, "-"+field)
|
||||
}
|
||||
args = append(args, input)
|
||||
|
||||
result, err := utils.ExecuteCommand(ctx, utils.ExifToolPath, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("exiftool failed: %w, stderr: %s", err, string(result.Stderr))
|
||||
}
|
||||
|
||||
// 解析输出
|
||||
data := make(map[string]string)
|
||||
lines := strings.Split(string(result.Stdout), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
// 简单解析,假设最后一个字段是值
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 0 {
|
||||
field := fields[len(data)]
|
||||
data[field] = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// RemoveExifData 移除 EXIF 数据
|
||||
func RemoveExifData(ctx context.Context, input string, backup bool) error {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{"-all="}
|
||||
if !backup {
|
||||
args = append(args, "-overwrite_original")
|
||||
}
|
||||
args = append(args, input)
|
||||
|
||||
result, err := utils.ExecuteCommand(ctx, utils.ExifToolPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove exif failed: %w, stderr: %s", err, string(result.Stderr))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyExifData 复制 EXIF 数据从一个文件到另一个文件
|
||||
func CopyExifData(ctx context.Context, source, target string) error {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-TagsFromFile", source,
|
||||
"-all:all",
|
||||
"-overwrite_original",
|
||||
target,
|
||||
}
|
||||
|
||||
result, err := utils.ExecuteCommand(ctx, utils.ExifToolPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy exif failed: %w, stderr: %s", err, string(result.Stderr))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetExifField 设置 EXIF 字段
|
||||
func SetExifField(ctx context.Context, input string, fields map[string]string) error {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{}
|
||||
for key, value := range fields {
|
||||
args = append(args, fmt.Sprintf("-%s=%s", key, value))
|
||||
}
|
||||
args = append(args, "-overwrite_original", input)
|
||||
|
||||
result, err := utils.ExecuteCommand(ctx, utils.ExifToolPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set exif failed: %w, stderr: %s", err, string(result.Stderr))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExifToolAvailable 检查 ExifTool 是否可用
|
||||
func IsExifToolAvailable() bool {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return false
|
||||
}
|
||||
return utils.ExifToolPath != ""
|
||||
}
|
||||
|
||||
// GetToolPaths 获取工具路径(用于调试)
|
||||
func GetToolPaths() (imageMagick, exifTool string) {
|
||||
utils.EnsureInitialized()
|
||||
return utils.ImageMagickPath, utils.ExifToolPath
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"rear/internal/utils"
|
||||
)
|
||||
|
||||
// ImageInfo 图片信息
|
||||
type ImageInfo struct {
|
||||
Format string
|
||||
Width int
|
||||
Height int
|
||||
ColorSpace string
|
||||
FileSize string
|
||||
}
|
||||
|
||||
// ConvertImage 使用 ImageMagick 转换图片
|
||||
// 示例: ConvertImage(ctx, "input.jpg", "output.png", "-quality", "90")
|
||||
func ConvertImage(ctx context.Context, input, output string, options ...string) error {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := append([]string{input}, options...)
|
||||
args = append(args, output)
|
||||
|
||||
result, err := utils.ExecuteCommand(ctx, utils.ImageMagickPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert failed: %w, stderr: %s", err, string(result.Stderr))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResizeImage 调整图片大小
|
||||
func ResizeImage(ctx context.Context, input, output string, width, height int) error {
|
||||
size := fmt.Sprintf("%dx%d", width, height)
|
||||
return ConvertImage(ctx, input, output, "-resize", size)
|
||||
}
|
||||
|
||||
// ResizeImageKeepAspect 按比例调整图片大小(保持宽高比)
|
||||
func ResizeImageKeepAspect(ctx context.Context, input, output string, maxWidth, maxHeight int) error {
|
||||
size := fmt.Sprintf("%dx%d>", maxWidth, maxHeight)
|
||||
return ConvertImage(ctx, input, output, "-resize", size)
|
||||
}
|
||||
|
||||
// CropImage 裁剪图片
|
||||
func CropImage(ctx context.Context, input, output string, width, height, x, y int) error {
|
||||
crop := fmt.Sprintf("%dx%d+%d+%d", width, height, x, y)
|
||||
return ConvertImage(ctx, input, output, "-crop", crop)
|
||||
}
|
||||
|
||||
// RotateImage 旋转图片
|
||||
func RotateImage(ctx context.Context, input, output string, degrees float64) error {
|
||||
return ConvertImage(ctx, input, output, "-rotate", fmt.Sprintf("%.2f", degrees))
|
||||
}
|
||||
|
||||
// IsImageMagickAvailable 检查 ImageMagick 是否可用
|
||||
func IsImageMagickAvailable() bool {
|
||||
if err := utils.EnsureInitialized(); err != nil {
|
||||
return false
|
||||
}
|
||||
return utils.ImageMagickPath != ""
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ========== 使用示例 ==========
|
||||
|
||||
func Example() {
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. 简单的图片处理
|
||||
err := ResizeImage(ctx, "input.jpg", "output.jpg", 800, 600)
|
||||
if err != nil {
|
||||
fmt.Printf("Resize failed: %v\n", err)
|
||||
}
|
||||
|
||||
// 2. 获取图片信息
|
||||
//info, err := GetImageInfo(ctx, "photo.jpg")
|
||||
//if err != nil {
|
||||
// fmt.Printf("Get info failed: %v\n", err)
|
||||
//}
|
||||
//fmt.Printf("Image: %dx%d, Format: %s\n", info.Width, info.Height, info.Format)
|
||||
|
||||
// 3. 处理 EXIF
|
||||
exifData, err := GetExifData(ctx, "photo.jpg")
|
||||
if err != nil {
|
||||
fmt.Printf("Get EXIF failed: %v\n", err)
|
||||
}
|
||||
fmt.Printf("EXIF: %v\n", exifData)
|
||||
|
||||
// 4. 在 goroutine 中使用(并发安全)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
// 每个 goroutine 可以安全地调用这些函数
|
||||
input := fmt.Sprintf("img%d.jpg", index)
|
||||
output := fmt.Sprintf("thumb%d.jpg", index)
|
||||
|
||||
// 使用带超时的 context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := ResizeImage(ctx, input, output, 200, 200); err != nil {
|
||||
fmt.Printf("Error processing %s: %v\n", input, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 构建和打包脚本
|
||||
set -e
|
||||
|
||||
PROJECT_NAME="image-processor"
|
||||
VERSION="1.0.0"
|
||||
BUILD_DIR="build"
|
||||
TOOLS_DIR="tools"
|
||||
|
||||
# 清理构建目录
|
||||
rm -rf $BUILD_DIR
|
||||
mkdir -p $BUILD_DIR
|
||||
|
||||
# 构建不同平台的可执行文件
|
||||
platforms=("windows/amd64" "linux/amd64" "darwin/amd64" "darwin/arm64")
|
||||
|
||||
for platform in "${platforms[@]}"
|
||||
do
|
||||
platform_split=(${platform//\// })
|
||||
GOOS=${platform_split[0]}
|
||||
GOARCH=${platform_split[1]}
|
||||
|
||||
output_name=$PROJECT_NAME
|
||||
if [ $GOOS = "windows" ]; then
|
||||
output_name+='.exe'
|
||||
fi
|
||||
|
||||
output_dir="$BUILD_DIR/${PROJECT_NAME}-${VERSION}-${GOOS}-${GOARCH}"
|
||||
mkdir -p $output_dir
|
||||
|
||||
echo "Building for $GOOS/$GOARCH..."
|
||||
|
||||
# 构建 Go 程序
|
||||
env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_dir/$output_name .
|
||||
|
||||
# 复制工具文件
|
||||
bin_dir="$output_dir/bin"
|
||||
mkdir -p $bin_dir
|
||||
|
||||
if [ $GOOS = "windows" ]; then
|
||||
# Windows 工具
|
||||
cp $TOOLS_DIR/windows/magick.exe $bin_dir/
|
||||
cp $TOOLS_DIR/windows/exiftool.exe $bin_dir/
|
||||
# 复制可能需要的 DLL 文件
|
||||
cp $TOOLS_DIR/windows/*.dll $bin_dir/ 2>/dev/null || true
|
||||
elif [ $GOOS = "linux" ]; then
|
||||
# Linux 工具
|
||||
cp $TOOLS_DIR/linux/magick $bin_dir/
|
||||
cp $TOOLS_DIR/linux/exiftool $bin_dir/
|
||||
chmod +x $bin_dir/*
|
||||
elif [ $GOOS = "darwin" ]; then
|
||||
# macOS 工具
|
||||
if [ $GOARCH = "arm64" ]; then
|
||||
cp $TOOLS_DIR/darwin-arm64/magick $bin_dir/
|
||||
cp $TOOLS_DIR/darwin-arm64/exiftool $bin_dir/
|
||||
else
|
||||
cp $TOOLS_DIR/darwin-amd64/magick $bin_dir/
|
||||
cp $TOOLS_DIR/darwin-amd64/exiftool $bin_dir/
|
||||
fi
|
||||
chmod +x $bin_dir/*
|
||||
fi
|
||||
|
||||
# 创建压缩包
|
||||
cd $BUILD_DIR
|
||||
if [ $GOOS = "windows" ]; then
|
||||
zip -r "${PROJECT_NAME}-${VERSION}-${GOOS}-${GOARCH}.zip" "${PROJECT_NAME}-${VERSION}-${GOOS}-${GOARCH}"
|
||||
else
|
||||
tar -czf "${PROJECT_NAME}-${VERSION}-${GOOS}-${GOARCH}.tar.gz" "${PROJECT_NAME}-${VERSION}-${GOOS}-${GOARCH}"
|
||||
fi
|
||||
cd ..
|
||||
|
||||
echo "Built $output_dir"
|
||||
done
|
||||
|
||||
echo "All builds completed!"
|
||||
|
||||
# 下载工具的脚本部分(单独运行)
|
||||
download_tools() {
|
||||
echo "Downloading external tools..."
|
||||
mkdir -p $TOOLS_DIR/{windows,linux,darwin-amd64,darwin-arm64}
|
||||
|
||||
# 下载 ImageMagick (示例 URL,需要根据实际情况调整)
|
||||
# Windows
|
||||
wget -O imagemagick-windows.zip "https://imagemagick.org/download/binaries/ImageMagick-7.1.0-portable-Q16-x64.zip"
|
||||
unzip imagemagick-windows.zip -d $TOOLS_DIR/windows/
|
||||
|
||||
# 下载 ExifTool
|
||||
# Windows
|
||||
wget -O exiftool-windows.zip "https://exiftool.org/exiftool-12.50.zip"
|
||||
unzip exiftool-windows.zip -d temp/
|
||||
cp temp/exiftool(-k).exe $TOOLS_DIR/windows/exiftool.exe
|
||||
|
||||
echo "Tools downloaded!"
|
||||
}
|
||||
|
||||
# 如果需要下载工具,取消注释下面这行
|
||||
# download_tools
|
||||
Reference in new issue
Block a user