From d8484bb7e0700300063792d6792d5ca7a8b3e3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Sala=C3=BCn?= <1910607+yansal@users.noreply.github.com> Date: Thu, 10 Sep 2020 14:01:33 +0200 Subject: [PATCH] feat: add gifsicle backend (#134) --- engine/backend/gifsicle.go | 112 +++++++++++++++++++++++++++++++++++++ engine/config/config.go | 11 +++- engine/engine.go | 28 +++++++--- 3 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 engine/backend/gifsicle.go diff --git a/engine/backend/gifsicle.go b/engine/backend/gifsicle.go new file mode 100644 index 00000000..deccee51 --- /dev/null +++ b/engine/backend/gifsicle.go @@ -0,0 +1,112 @@ +package backend + +import ( + "bytes" + "errors" + "fmt" + "image/gif" + "os/exec" + + "github.com/thoas/picfit/image" +) + +// Gifsicle is the gifsicle backend. +type Gifsicle struct { + Path string +} + +func (b *Gifsicle) String() string { + return "gifsicle" +} + +// Fit implements Backend. +func (b *Gifsicle) Fit(*image.ImageFile, *Options) ([]byte, error) { + return nil, MethodNotImplementedError +} + +// Flat implements Backend. +func (b *Gifsicle) Flat(*image.ImageFile, *Options) ([]byte, error) { + return nil, MethodNotImplementedError +} + +// Flip implements Backend. +func (b *Gifsicle) Flip(*image.ImageFile, *Options) ([]byte, error) { + return nil, MethodNotImplementedError +} + +// Resize implements Backend. +func (b *Gifsicle) Resize(imgfile *image.ImageFile, opts *Options) ([]byte, error) { + cmd := exec.Command(b.Path, + "--resize", fmt.Sprintf("%dx%d", opts.Width, opts.Height), + ) + cmd.Stdin = bytes.NewReader(imgfile.Source) + stdout := new(bytes.Buffer) + cmd.Stdout = stdout + stderr := new(bytes.Buffer) + cmd.Stderr = stderr + + var target *exec.ExitError + if err := cmd.Run(); errors.As(err, &target) && target.Exited() { + return nil, errors.New(stderr.String()) + } else if err != nil { + return nil, err + } + return stdout.Bytes(), nil +} + +// Rotate implements Backend. +func (b *Gifsicle) Rotate(*image.ImageFile, *Options) ([]byte, error) { + return nil, MethodNotImplementedError +} + +// Thumbnail implements Backend. +func (b *Gifsicle) Thumbnail(imgfile *image.ImageFile, opts *Options) ([]byte, error) { + img, err := gif.Decode(bytes.NewReader(imgfile.Source)) + if err != nil { + return nil, err + } + bounds := img.Bounds() + left, top, cropw, croph := computecrop(bounds.Dx(), bounds.Dy(), opts.Width, opts.Height) + + cmd := exec.Command(b.Path, + "--crop", fmt.Sprintf("%d,%d+%dx%d", left, top, cropw, croph), + "--resize", fmt.Sprintf("%dx%d", opts.Width, opts.Height), + ) + cmd.Stdin = bytes.NewReader(imgfile.Source) + stdout := new(bytes.Buffer) + cmd.Stdout = stdout + stderr := new(bytes.Buffer) + cmd.Stderr = stderr + + var target *exec.ExitError + if err := cmd.Run(); errors.As(err, &target) && target.Exited() { + return nil, errors.New(stderr.String()) + } else if err != nil { + return nil, err + } + return stdout.Bytes(), nil +} + +func computecrop(srcw, srch, destw, desth int) (left, top, cropw, croph int) { + srcratio := float64(srcw) / float64(srch) + destratio := float64(destw) / float64(desth) + + if srcratio > destratio { + cropw = int((destratio * float64(srch)) + 0.5) + croph = srch + } else { + croph = int((float64(srcw) / destratio) + 0.5) + cropw = srcw + } + + left = int(float64(srcw-cropw) * 0.5) + if left < 0 { + left = 0 + } + + top = int(float64(srch-croph) * 0.5) + if top < 0 { + top = 0 + } + return +} diff --git a/engine/config/config.go b/engine/config/config.go index 349d695d..747252ad 100644 --- a/engine/config/config.go +++ b/engine/config/config.go @@ -1,8 +1,9 @@ package config type Backends struct { - Lilliput *Backend `mapstructure:"lilliput"` - GoImage *Backend `mapstructure:"goimage"` + Gifsicle *CommandBackend `mapstructure:"gifsicle"` + GoImage *Backend `mapstructure:"goimage"` + Lilliput *Backend `mapstructure:"lilliput"` } type Backend struct { @@ -10,6 +11,12 @@ type Backend struct { Mimetypes []string } +type CommandBackend struct { + Path string + Mimetypes []string + Weight int +} + // Config is the engine config type Config struct { Backends *Backends `mapstructure:"backends"` diff --git a/engine/engine.go b/engine/engine.go index 058b4938..da3b3b1a 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "os/exec" "sort" "strings" @@ -34,14 +35,20 @@ func New(cfg config.Config) *Engine { mimetypes: MimeTypes, }) } else { - if cfg.Backends.Lilliput != nil { - b = append(b, Backend{ - Backend: backend.NewLilliput(cfg), - mimetypes: cfg.Backends.Lilliput.Mimetypes, - weight: cfg.Backends.Lilliput.Weight, - }) - } + if cfg.Backends.Gifsicle != nil { + path := cfg.Backends.Gifsicle.Path + if path == "" { + path = "gifsicle" + } + if _, err := exec.LookPath(path); err == nil { + b = append(b, Backend{ + Backend: &backend.Gifsicle{Path: path}, + mimetypes: cfg.Backends.Gifsicle.Mimetypes, + weight: cfg.Backends.Gifsicle.Weight, + }) + } + } if cfg.Backends.GoImage != nil { b = append(b, Backend{ Backend: &backend.GoImage{}, @@ -49,6 +56,13 @@ func New(cfg config.Config) *Engine { weight: cfg.Backends.GoImage.Weight, }) } + if cfg.Backends.Lilliput != nil { + b = append(b, Backend{ + Backend: backend.NewLilliput(cfg), + mimetypes: cfg.Backends.Lilliput.Mimetypes, + weight: cfg.Backends.Lilliput.Weight, + }) + } } sort.Slice(b, func(i, j int) bool {