From a4914b3dfa53446590636d3079d599b5281a2f69 Mon Sep 17 00:00:00 2001 From: Fdvkey Date: Sat, 18 Jul 2026 12:08:19 +0700 Subject: [PATCH] feat: integrate govips for image processing and enhance image conversion to WebP --- Dockerfile | 9 ++++---- go.mod | 14 ++++++----- go.sum | 16 +++++++++++++ helper/media_convert.go | 51 +++++++++++++++++++++++++++-------------- main.go | 4 ++++ 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7d26e67..d78a5eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Stage: build FROM golang:1.25.5-alpine AS build -# Install build dependencies (CGO often needed for go-sqlite3) -RUN apk add --no-cache gcc musl-dev +# Install build dependencies (CGO for go-sqlite3 + govips) +RUN apk add --no-cache gcc musl-dev vips-dev WORKDIR /app @@ -17,11 +17,10 @@ RUN CGO_ENABLED=1 GOOS=linux go build -trimpath -ldflags="-w -s" -o /app/neo . # Stage: prod FROM alpine:latest AS prod -# Install required tools (cwebp/gif2webp via libwebp-tools, ffmpeg, imagemagick) +# Install runtime dependencies (vips for image processing, ffmpeg for video) RUN apk add --no-cache \ ffmpeg \ - libwebp-tools \ - imagemagick \ + vips \ ca-certificates \ tzdata diff --git a/go.mod b/go.mod index 08eda17..6b760f1 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/beeper/argo-go v1.1.2 // indirect github.com/coder/websocket v1.8.15 // indirect + github.com/davidbyttow/govips/v2 v2.18.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -24,12 +25,13 @@ require ( github.com/vektah/gqlparser/v2 v2.5.27 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/util v0.9.10 // indirect - golang.org/x/crypto v0.53.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/image v0.44.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 81afb20..e1c6d00 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNU github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidbyttow/govips/v2 v2.18.0 h1:pZRshWVYvewP/TZx3yZ7YeC42WyLXg53tHy5Qt8nT9E= +github.com/davidbyttow/govips/v2 v2.18.0/go.mod h1:8+nst5zfMoats12PgmmAPh6p5OfjDaXK0BXMFl/vOcM= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -50,19 +52,33 @@ go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/helper/media_convert.go b/helper/media_convert.go index 8fdfa16..ff74301 100644 --- a/helper/media_convert.go +++ b/helper/media_convert.go @@ -4,30 +4,47 @@ import ( "bytes" "fmt" "os/exec" + + "github.com/davidbyttow/govips/v2/vips" ) func ConvertImageToWebp(imgData []byte) ([]byte, error) { - convertCmd := exec.Command("convert", "-", "-resize", "512x512", "-background", "none", "-compose", "Copy", "-gravity", "center", "-extent", "512x512", "-quality", "100", "png:-") - convertCmd.Stdin = bytes.NewReader(imgData) - var convertBuf bytes.Buffer - var stderr bytes.Buffer - convertCmd.Stdout = &convertBuf - convertCmd.Stderr = &stderr - if err := convertCmd.Run(); err != nil { - return nil, fmt.Errorf("convert failed: %w, stderr: %s", err, stderr.String()) + img, err := vips.NewImageFromBuffer(imgData) + if err != nil { + return nil, fmt.Errorf("failed to load image: %w", err) + } + defer img.Close() + + if !img.HasAlpha() { + if err := img.AddAlpha(); err != nil { + return nil, fmt.Errorf("failed to add alpha: %w", err) + } } - cwebpCmd := exec.Command("cwebp", "-quiet", "-mt", "-exact", "-q", "100", "-m", "6", "-alpha_q", "100", "-o", "-", "--", "-") - cwebpCmd.Stdin = &convertBuf - var cwebpBuf bytes.Buffer - var cwebStderr bytes.Buffer - cwebpCmd.Stdout = &cwebpBuf - cwebpCmd.Stderr = &cwebStderr - if err := cwebpCmd.Run(); err != nil { - return nil, fmt.Errorf("cwebp failed: %w, stderr: %s", err, cwebStderr.String()) + if err := img.ThumbnailWithSize(512, 512, vips.InterestingNone, vips.SizeBoth); err != nil { + return nil, fmt.Errorf("failed to resize: %w", err) } - return cwebpBuf.Bytes(), nil + // Center in 512x512 canvas with transparent background (fit: contain) + w, h := img.Width(), img.Height() + if w != 512 || h != 512 { + left := (512 - w) / 2 + top := (512 - h) / 2 + if err := img.EmbedBackgroundRGBA(left, top, 512, 512, &vips.ColorRGBA{R: 0, G: 0, B: 0, A: 0}); err != nil { + return nil, fmt.Errorf("failed to embed: %w", err) + } + } + + webpBuf, _, err := img.ExportWebp(&vips.WebpExportParams{ + Quality: 100, + Lossless: false, + ReductionEffort: 6, + }) + if err != nil { + return nil, fmt.Errorf("failed to export webp: %w", err) + } + + return webpBuf, nil } func ConvertVideoToWebp(vidData []byte) ([]byte, error) { diff --git a/main.go b/main.go index 53847c3..8611cb5 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( _ "neo/cmds" "neo/core" + "github.com/davidbyttow/govips/v2/vips" "github.com/joho/godotenv" _ "github.com/mattn/go-sqlite3" "github.com/mdp/qrterminal/v3" @@ -33,6 +34,9 @@ func init() { } func main() { + vips.Startup(nil) + defer vips.Shutdown() + dbLog := waLog.Stdout("Database", "DEBUG", true) ctx := context.Background() container, err := sqlstore.New(ctx, "sqlite3", fmt.Sprintf("file:%s?_foreign_keys=on", os.Getenv("SESSION_PATH")), dbLog)