From ab39f64ad5b7faa0f30670a982732f0c54f634a8 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 30 Mar 2023 14:44:32 +0200 Subject: [PATCH] feat: switch to Go libraries for avatar conversion instead of ImageMagick --- Makefile | 2 +- backend/db/avatars.go | 121 +++++++++--------------------------------- go.mod | 3 ++ go.sum | 7 +++ 4 files changed, 36 insertions(+), 97 deletions(-) diff --git a/Makefile b/Makefile index 475b3bc..8bbfdd4 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ all: generate backend frontend .PHONY: backend backend: - CGO_ENABLED=0 go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/u1f320/pronouns.cc/backend/server.Tag=`git describe --tags --long`" . + go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/u1f320/pronouns.cc/backend/server.Tag=`git describe --tags --long`" . .PHONY: generate generate: diff --git a/backend/db/avatars.go b/backend/db/avatars.go index f268d7b..f28059e 100644 --- a/backend/db/avatars.go +++ b/backend/db/avatars.go @@ -6,13 +6,19 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "image" + _ "image/gif" + "image/jpeg" + _ "image/png" "io" - "os/exec" "strings" "emperror.dev/errors" + "github.com/disintegration/imaging" "github.com/minio/minio-go/v7" "github.com/rs/xid" + + "github.com/chai2010/webp" ) var ( @@ -25,8 +31,8 @@ const ErrInvalidContentType = errors.Sentinel("invalid avatar content type") // ConvertAvatar parses an avatar from a data URI, converts it to WebP and JPEG, and returns the results. func (db *DB) ConvertAvatar(data string) ( - webp *bytes.Buffer, - jpg *bytes.Buffer, + webpOut *bytes.Buffer, + jpgOut *bytes.Buffer, err error, ) { data = strings.TrimSpace(data) @@ -34,113 +40,36 @@ func (db *DB) ConvertAvatar(data string) ( return nil, nil, ErrInvalidDataURI } split := strings.Split(data, ",") - rest, b64 := split[0], split[1] - rest = strings.Split(rest, ":")[1] - contentType := strings.Split(rest, ";")[0] - - var contentArg []string - switch contentType { - case "image/png": - contentArg = []string{"png:-"} - case "image/jpeg": - contentArg = []string{"jpg:-"} - case "image/gif": - contentArg = []string{"gif:-"} - case "image/webp": - contentArg = []string{"webp:-"} - default: - return nil, nil, ErrInvalidContentType - } - - rawData, err := base64.StdEncoding.DecodeString(b64) + rawData, err := base64.StdEncoding.DecodeString(split[1]) if err != nil { return nil, nil, errors.Wrap(err, "invalid base64 data") } - // create webp convert command and get its pipes - webpConvert := exec.Command("convert", append(contentArg, webpArgs...)...) - stdIn, err := webpConvert.StdinPipe() + img, _, err := image.Decode(bytes.NewReader(rawData)) if err != nil { - return nil, nil, errors.Wrap(err, "getting webp stdin") - } - stdOut, err := webpConvert.StdoutPipe() - if err != nil { - return nil, nil, errors.Wrap(err, "getting webp stdout") + return nil, nil, errors.Wrap(err, "decodign image") } - // start webp command - err = webpConvert.Start() + resized := imaging.Fill(img, 512, 512, imaging.Center, imaging.Linear) + + webpOut = new(bytes.Buffer) + err = webp.Encode(webpOut, resized, &webp.Options{ + Quality: 90, + }) if err != nil { - return nil, nil, errors.Wrap(err, "starting webp command") + return nil, nil, errors.Wrap(err, "encoding WebP image") } - // write data - _, err = stdIn.Write(rawData) + jpgOut = new(bytes.Buffer) + err = jpeg.Encode(jpgOut, resized, &jpeg.Options{ + Quality: 80, + }) if err != nil { - return nil, nil, errors.Wrap(err, "writing webp data") - } - err = stdIn.Close() - if err != nil { - return nil, nil, errors.Wrap(err, "closing webp stdin") + return nil, nil, errors.Wrap(err, "encoding JPEG image") } - // read webp output - webpBuffer := new(bytes.Buffer) - _, err = io.Copy(webpBuffer, stdOut) - if err != nil { - return nil, nil, errors.Wrap(err, "reading webp data") - } - webp = webpBuffer - - // finish webp command - err = webpConvert.Wait() - if err != nil { - return nil, nil, errors.Wrap(err, "running webp command") - } - - // create jpg convert command and get its pipes - jpgConvert := exec.Command("convert", append(contentArg, jpgArgs...)...) - stdIn, err = jpgConvert.StdinPipe() - if err != nil { - return nil, nil, errors.Wrap(err, "getting jpg stdin") - } - stdOut, err = jpgConvert.StdoutPipe() - if err != nil { - return nil, nil, errors.Wrap(err, "getting jpg stdout") - } - - // start jpg command - err = jpgConvert.Start() - if err != nil { - return nil, nil, errors.Wrap(err, "starting jpg command") - } - - // write data - _, err = stdIn.Write(rawData) - if err != nil { - return nil, nil, errors.Wrap(err, "writing jpg data") - } - err = stdIn.Close() - if err != nil { - return nil, nil, errors.Wrap(err, "closing jpg stdin") - } - - // read jpg output - jpgBuffer := new(bytes.Buffer) - _, err = io.Copy(jpgBuffer, stdOut) - if err != nil { - return nil, nil, errors.Wrap(err, "reading jpg data") - } - jpg = jpgBuffer - - // finish jpg command - err = jpgConvert.Wait() - if err != nil { - return nil, nil, errors.Wrap(err, "running jpg command") - } - - return webp, jpg, nil + return webpOut, jpgOut, nil } func (db *DB) WriteUserAvatar(ctx context.Context, diff --git a/go.mod b/go.mod index 0723ecc..bddd8ed 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( emperror.dev/errors v0.8.1 github.com/Masterminds/squirrel v1.5.2 github.com/bwmarrin/discordgo v0.25.0 + github.com/chai2010/webp v1.1.1 + github.com/disintegration/imaging v1.6.2 github.com/georgysavva/scany v0.3.0 github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/httprate v0.5.3 @@ -56,6 +58,7 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index 4b47185..ccf53be 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -85,6 +87,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -520,6 +524,9 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=