A:增加govips的调用
This commit is contained in:
1 parent
3c17a7fff6
commit
e5009ee984
5 files changed
+233
-3
No files matched your search
Generated
+5
-1
@@ -2,7 +2,11 @@
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.claude" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.github" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Root Go service is in `./` with `main.go`, `go.mod`, and `config.yaml`.
|
||||
- Core backend lives in `internal/` (API, handlers, services, repositories, workflows, utilities).
|
||||
- Reusable libraries are in `pkg/` (logging, SSE, system utilities, image helpers).
|
||||
- HTTP request examples live in `tests/` (`*.http`) and complement Go tests.
|
||||
- Supporting assets and tools: `tools/` (external binaries), `scripts/` (build), `db/`, `logs/`, `examples/`, `doc/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Install deps: `go mod download`.
|
||||
- Run in dev: `go run main.go` (uses `config.yaml`).
|
||||
- Build local binary: `go build -o rear.exe .`.
|
||||
- Cross-platform build: `bash scripts/build.sh` (from `rear/`, requires Bash and Go).
|
||||
- Run tests: `go test ./...` (unit/integration), plus manual API checks via `.http` files in `tests/`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Language: Go 1.24, format all code with `gofmt`/editor-on-save (`go fmt ./...` before committing).
|
||||
- Package names: short, lowercase (`filesystem`, `logger`, `sse`); files use `snake_case.go`.
|
||||
- Types and exported functions: `PascalCase`; locals and unexported symbols: `camelCase`.
|
||||
- Keep handlers thin, push business logic into `internal/service` and persistence into `internal/repositories`.
|
||||
|
||||
## Testing Guidelines
|
||||
- Add/maintain Go tests under the same package with `*_test.go` suffix (`go test ./pkg/...` for focused runs).
|
||||
- Mirror new behavior with table-driven tests where practical.
|
||||
- Prefer deterministic tests; avoid relying on real external tools or network when possible.
|
||||
- Use `.http` files in `tests/` to document and validate API flows when endpoints change.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Follow conventional commits: `feat(scope): summary`, `fix(scope): summary`, `chore(scope): summary`, `docs(scope): summary`, etc. Scopes like `sse`, `filesystem`, `repo`, `tests`.
|
||||
- Use present-tense, concise messages; keep one logical change per commit.
|
||||
- PRs should include: brief description, motivation/context, checklist of changes, how to test (commands and sample requests), and screenshots or log snippets when relevant.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Never commit real credentials or private file paths; use `config.yaml.example` as the template and keep local `config.yaml` environment-specific.
|
||||
- Treat `tools/` binaries as build/runtime dependencies; do not modify or repackage them without confirming licenses and platform impact.
|
||||
- When introducing new external tools or ports, document them in `README.md` and `doc/` and ensure they are configurable via `config.yaml`.
|
||||
|
||||
## Agent-Specific Instructions
|
||||
- Keep changes minimal and scoped; prefer extending existing modules under `internal/` or `pkg/` instead of creating new top-level directories.
|
||||
- Do not introduce new Go dependencies unless necessary; if added, group them logically in `go.mod` and explain the rationale in the PR description.
|
||||
- When modifying behavior, update or add tests in the nearest relevant package and, if API-visible, adjust `.http` examples in `tests/`.
|
||||
@@ -3,8 +3,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/h2non/filetype"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rear/internal/config"
|
||||
@@ -15,6 +13,9 @@ import (
|
||||
"rear/pkg/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/h2non/filetype"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ImageAPI struct {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"rear/internal/consts"
|
||||
"sync"
|
||||
|
||||
vips "github.com/davidbyttow/govips/v2/vips"
|
||||
)
|
||||
|
||||
var (
|
||||
goVipsInitOnce sync.Once
|
||||
goVipsInitErr error
|
||||
)
|
||||
|
||||
// ensureGoVipsInitialized 确保 govips 已初始化(仅在 Linux/macOS 下编译)
|
||||
func ensureGoVipsInitialized() error {
|
||||
goVipsInitOnce.Do(func() {
|
||||
// 使用默认配置启动 govips,具体配置可在以后按需扩展
|
||||
goVipsInitErr = vips.Startup(nil)
|
||||
})
|
||||
return goVipsInitErr
|
||||
}
|
||||
|
||||
// IsGoVipsAvailable 检查 govips 是否可用(是否能够成功初始化)
|
||||
func IsGoVipsAvailable() bool {
|
||||
return ensureGoVipsInitialized() == nil
|
||||
}
|
||||
|
||||
// ConvertVipsImageWithGoVips 使用 govips 进行格式转换
|
||||
// 签名与 ConvertVipsImage 基本一致,方便在调用侧切换实现。
|
||||
func ConvertVipsImageWithGoVips(ctx context.Context, inputPath, outputPath string, format consts.ImageFormat, options *VipsConvertOptions) error {
|
||||
if err := ensureGoVipsInitialized(); err != nil {
|
||||
return fmt.Errorf("govips 初始化失败: %w", err)
|
||||
}
|
||||
|
||||
if options == nil {
|
||||
options = &VipsConvertOptions{Quality: 85}
|
||||
}
|
||||
|
||||
image, err := vips.NewImageFromFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("govips 加载图片失败: %w", err)
|
||||
}
|
||||
defer image.Close()
|
||||
|
||||
var (
|
||||
data []byte
|
||||
)
|
||||
|
||||
switch format {
|
||||
case consts.FormatJPG:
|
||||
params := vips.JpegExportParams{
|
||||
Quality: options.Quality,
|
||||
StripMetadata: options.StripMeta,
|
||||
Interlace: options.Progressive,
|
||||
}
|
||||
data, _, err = image.ExportJpeg(¶ms)
|
||||
case consts.FormatPNG:
|
||||
compression := 0
|
||||
if options.Quality > 0 {
|
||||
// 与 CLI 方式保持一致,将 1-100 映射到 0-9 压缩级别
|
||||
compression = 9 - (options.Quality-1)*9/99
|
||||
}
|
||||
params := vips.PngExportParams{
|
||||
Compression: compression,
|
||||
StripMetadata: options.StripMeta,
|
||||
}
|
||||
data, _, err = image.ExportPng(¶ms)
|
||||
case consts.FormatWEBP:
|
||||
params := vips.WebpExportParams{
|
||||
Quality: options.Quality,
|
||||
Lossless: options.Lossless,
|
||||
StripMetadata: options.StripMeta,
|
||||
}
|
||||
data, _, err = image.ExportWebp(¶ms)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format for govips: %s", format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("govips 导出图片失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputPath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("写出图片失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompressVipsImageWithGoVips 使用 govips 进行综合压缩(缩放 + 去元数据 + 质量控制 + 格式转换)
|
||||
// 行为尽可能与 CompressVipsImage 保持一致,但采用 libvips C 接口而非 CLI。
|
||||
func CompressVipsImageWithGoVips(ctx context.Context, inputPath, outputPath string,
|
||||
targetFormat consts.ImageFormat, targetWidth, targetHeight, quality int) error {
|
||||
|
||||
if err := ensureGoVipsInitialized(); err != nil {
|
||||
return fmt.Errorf("govips 初始化失败: %w", 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")
|
||||
}
|
||||
|
||||
image, err := vips.NewImageFromFile(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("govips 加载图片失败: %w", err)
|
||||
}
|
||||
defer image.Close()
|
||||
|
||||
// 计算缩放比例(保持宽高比,且不放大)
|
||||
width := image.Width()
|
||||
height := image.Height()
|
||||
if width <= 0 || height <= 0 {
|
||||
return fmt.Errorf("invalid source dimensions: %dx%d", width, height)
|
||||
}
|
||||
|
||||
scaleX := float64(targetWidth) / float64(width)
|
||||
scaleY := float64(targetHeight) / float64(height)
|
||||
scale := scaleX
|
||||
if scaleY < scaleX {
|
||||
scale = scaleY
|
||||
}
|
||||
if scale <= 0 {
|
||||
return fmt.Errorf("invalid resize scale computed: %f", scale)
|
||||
}
|
||||
if scale > 1.0 {
|
||||
scale = 1.0
|
||||
}
|
||||
|
||||
if err := image.Resize(scale, vips.KernelLanczos3); err != nil {
|
||||
return fmt.Errorf("govips 缩放图片失败: %w", err)
|
||||
}
|
||||
|
||||
// 导出为目标格式
|
||||
var data []byte
|
||||
|
||||
switch targetFormat {
|
||||
case consts.FormatJPG:
|
||||
params := vips.JpegExportParams{
|
||||
Quality: quality,
|
||||
StripMetadata: true,
|
||||
Interlace: false,
|
||||
}
|
||||
data, _, err = image.ExportJpeg(¶ms)
|
||||
case consts.FormatPNG:
|
||||
compression := 9 - (quality-1)*9/99
|
||||
params := vips.PngExportParams{
|
||||
Compression: compression,
|
||||
StripMetadata: true,
|
||||
}
|
||||
data, _, err = image.ExportPng(¶ms)
|
||||
case consts.FormatWEBP:
|
||||
params := vips.WebpExportParams{
|
||||
Quality: quality,
|
||||
Lossless: false,
|
||||
StripMetadata: true,
|
||||
}
|
||||
data, _, err = image.ExportWebp(¶ms)
|
||||
default:
|
||||
return fmt.Errorf("unsupported target format for govips: %s", targetFormat)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("govips 导出压缩图片失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputPath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("写出压缩图片失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -154,3 +154,6 @@
|
||||
{"level":"info","time":"2025-10-06 17:28:15.373","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-10-06 17:28:49.241","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-10-06 17:38:05.412","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-11-16 14:43:16.424","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-11-16 17:26:19.223","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
{"level":"info","time":"2025-11-16 17:32:50.846","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
|
||||
Reference in new issue
Block a user