diff --git a/constants/constants.go b/constants/constants.go index 9fa4a9ac..7c3fc459 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -18,3 +18,17 @@ var ( // Compiler is the compiler used during build Compiler string ) + +const ( + TopRight = "top-right" + TopLeft = "top-left" + BottomRight = "bottom-right" + BottomLeft = "bottom-left" +) + +var StickPositions = []string{ + TopRight, + TopLeft, + BottomRight, + BottomLeft, +} diff --git a/engine/backend/backend.go b/engine/backend/backend.go index 2f3f5df4..6a49908b 100644 --- a/engine/backend/backend.go +++ b/engine/backend/backend.go @@ -17,6 +17,7 @@ type Options struct { Width int Height int Position string + Stick string Color string Degree int Images []image.ImageFile diff --git a/engine/backend/goimage_flat.go b/engine/backend/goimage_flat.go index 27fcb81f..4df80376 100644 --- a/engine/backend/goimage_flat.go +++ b/engine/backend/goimage_flat.go @@ -1,21 +1,52 @@ package backend import ( + "bytes" "fmt" "image" "image/draw" + "image/gif" "strconv" "strings" "github.com/disintegration/imaging" colorful "github.com/lucasb-eyer/go-colorful" + "github.com/thoas/picfit/constants" imagefile "github.com/thoas/picfit/image" ) func (e *GoImage) Flat(backgroundFile *imagefile.ImageFile, options *Options) ([]byte, error) { + var err error + images := make([]image.Image, len(options.Images)) + for i := range options.Images { + images[i], err = e.Source(&options.Images[i]) + if err != nil { + return nil, err + } + } + if options.Format == imaging.GIF { - return e.TransformGIF(backgroundFile, options, imaging.Resize) + g, err := gif.DecodeAll(bytes.NewReader(backgroundFile.Source)) + if err != nil { + return nil, err + } + + for i := range g.Image { + if options.Stick != "" { + drawStickForeground(g.Image[i], images, options) + } else { + drawPosForeground(g.Image[i], images, options) + } + } + buf := bytes.Buffer{} + + err = gif.EncodeAll(&buf, g) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil } background, err := e.Source(backgroundFile) @@ -23,26 +54,65 @@ func (e *GoImage) Flat(backgroundFile *imagefile.ImageFile, options *Options) ([ return nil, err } - images := make([]image.Image, len(options.Images)) - for i := range options.Images { - images[i], err = e.Source(&options.Images[i]) - if err != nil { - return nil, err - } + bg, ok := background.(draw.Image) + if !ok { + bg = image.NewRGBA(image.Rectangle{image.Point{}, background.Bounds().Size()}) + draw.Draw(bg, background.Bounds(), background, image.Point{}, draw.Src) + } + + if options.Stick != "" { + drawStickForeground(bg, images, options) + } else { + drawPosForeground(bg, images, options) } - bg := image.NewRGBA(image.Rectangle{image.Point{}, background.Bounds().Size()}) - draw.Draw(bg, background.Bounds(), background, image.Point{}, draw.Src) + return e.ToBytes(bg, options.Format, options.Quality) +} + +func drawStickForeground(bg draw.Image, images []image.Image, options *Options) { + for i := range images { + opts := &Options{ + Upscale: true, + Width: options.Width, + Height: options.Height, + } + images[i] = scale(images[i], opts, imaging.Resize) + + bounds := images[i].Bounds() + var position image.Point + switch options.Stick { + case constants.TopLeft: + position = bounds.Min + case constants.TopRight: + position = image.Point{X: bg.Bounds().Dx() - bounds.Dx(), Y: 0} + case constants.BottomLeft: + position = image.Point{X: 0, Y: bg.Bounds().Dy() - bounds.Dy()} + case constants.BottomRight: + position = image.Point{ + X: bg.Bounds().Dx() - bounds.Dx(), + Y: bg.Bounds().Dy() - bounds.Dy(), + } + } + + draw.Draw(bg, image.Rectangle{ + position, + position.Add(bounds.Size()), + }, images[i], bounds.Min, draw.Over) + } +} + +// drawPosForeground draw the given images on the given background inside the +// section delimited by the options position. +func drawPosForeground(bg draw.Image, images []image.Image, options *Options) { dst := positionForeground(bg, options.Position) fg := foregroundImage(dst, options.Color) fg = drawForeground(fg, images, options) draw.Draw(bg, dst, fg, fg.Bounds().Min, draw.Over) - - return e.ToBytes(bg, options.Format, options.Quality) } +// positionForeground creates a mask with the given position. func positionForeground(bg image.Image, pos string) image.Rectangle { ratios := []int{100, 100, 100, 100} val := strings.Split(pos, ".") @@ -59,7 +129,8 @@ func positionForeground(bg image.Image, pos string) image.Rectangle { } } -func foregroundImage(rec image.Rectangle, c string) *image.RGBA { +// foregroundImage creates an Image with the given mask and the given color. +func foregroundImage(rec image.Rectangle, c string) draw.Image { fg := image.NewRGBA(image.Rectangle{image.ZP, rec.Size()}) if c == "" { return fg @@ -74,7 +145,10 @@ func foregroundImage(rec image.Rectangle, c string) *image.RGBA { return fg } -func drawForeground(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA { +// drawForeground draw the given images inside the destination foreground. +// if the foreground image has a height superior to its width, the images +// are vertically aligned, else they are horizontally aligned. +func drawForeground(fg draw.Image, images []image.Image, options *Options) draw.Image { n := len(images) if n == 0 { return fg @@ -103,7 +177,10 @@ func drawForeground(fg *image.RGBA, images []image.Image, options *Options) *ima } } -func foregroundHorizontal(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA { +// foregroundHorizontal splits the fg according to the number of images in +// equal parts horizontally aligned and draw each images in the given order in +// the center of each of theses parts. +func foregroundHorizontal(fg draw.Image, images []image.Image, options *Options) draw.Image { position := fg.Bounds().Min totalHeight := fg.Bounds().Dy() cellWidth := fg.Bounds().Dx() / len(images) @@ -120,7 +197,10 @@ func foregroundHorizontal(fg *image.RGBA, images []image.Image, options *Options return fg } -func foregroundVertical(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA { +// foregroundVertical splits the fg according to the number of images in +// equal parts vertically aligned and draw each images in the given order in +// the center of each of theses parts. +func foregroundVertical(fg draw.Image, images []image.Image, options *Options) draw.Image { position := fg.Bounds().Min cellHeight := fg.Bounds().Dy() / len(images) totalWidth := fg.Bounds().Dx() diff --git a/engine/config/config.go b/engine/config/config.go index 545bb1cd..349d695d 100644 --- a/engine/config/config.go +++ b/engine/config/config.go @@ -6,6 +6,7 @@ type Backends struct { } type Backend struct { + Weight int Mimetypes []string } diff --git a/engine/constants.go b/engine/constants.go index c0914f53..283a55ce 100644 --- a/engine/constants.go +++ b/engine/constants.go @@ -7,3 +7,10 @@ var ContentTypes = map[string]string{ "bmp": "image/bmp", "gif": "image/gif", } + +var MimeTypes = []string{ + "image/jpeg", + "image/png", + "image/bmp", + "image/gif", +} diff --git a/engine/engine.go b/engine/engine.go index 8413655b..058b4938 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "sort" "strings" "github.com/thoas/picfit/engine/backend" @@ -14,41 +15,46 @@ type Engine struct { Format string DefaultQuality int - backends []backend.Backend - mimetypes map[string]backend.Backend + backends []Backend +} + +type Backend struct { + backend.Backend + weight int + mimetypes []string } // New initializes an Engine func New(cfg config.Config) *Engine { - var ( - b []backend.Backend - mimetypes = map[string]backend.Backend{} - ) + var b []Backend if cfg.Backends == nil { - b = append(b, &backend.GoImage{}) + b = append(b, Backend{ + Backend: &backend.GoImage{}, + mimetypes: MimeTypes, + }) } else { if cfg.Backends.Lilliput != nil { - back := backend.NewLilliput(cfg) - - b = append(b, back) - - for _, mimetype := range cfg.Backends.Lilliput.Mimetypes { - mimetypes[mimetype] = back - } + b = append(b, Backend{ + Backend: backend.NewLilliput(cfg), + mimetypes: cfg.Backends.Lilliput.Mimetypes, + weight: cfg.Backends.Lilliput.Weight, + }) } if cfg.Backends.GoImage != nil { - back := &backend.GoImage{} - - b = append(b, back) - - for _, mimetype := range cfg.Backends.GoImage.Mimetypes { - mimetypes[mimetype] = back - } + b = append(b, Backend{ + Backend: &backend.GoImage{}, + mimetypes: cfg.Backends.GoImage.Mimetypes, + weight: cfg.Backends.GoImage.Weight, + }) } } + sort.Slice(b, func(i, j int) bool { + return b[i].weight < b[j].weight + }) + quality := config.DefaultQuality if cfg.Quality != 0 { quality = cfg.Quality @@ -59,7 +65,6 @@ func New(cfg config.Config) *Engine { Format: cfg.Format, DefaultQuality: quality, backends: b, - mimetypes: mimetypes, } } @@ -79,16 +84,22 @@ func (e Engine) Transform(output *image.ImageFile, operations []EngineOperation) source = output.Source ) + ct := output.ContentType() for i := range operations { - backends := e.backends + for j := range e.backends { + var processing bool + for k := range e.backends[j].mimetypes { + if ct == e.backends[j].mimetypes[k] { + processing = true + break + } + } - back, ok := e.mimetypes[output.ContentType()] - if ok { - backends = []backend.Backend{back} - } + if !processing { + continue + } - for j := range backends { - processed, err = operate(backends[j], output, operations[i].Operation, operations[i].Options) + processed, err = operate(e.backends[j], output, operations[i].Operation, operations[i].Options) if err == nil { output.Source = processed break diff --git a/parameters.go b/parameters.go index 70959c3d..164d89d9 100644 --- a/parameters.go +++ b/parameters.go @@ -8,6 +8,7 @@ import ( "github.com/disintegration/imaging" "github.com/pkg/errors" + "github.com/thoas/picfit/constants" "github.com/thoas/picfit/engine" "github.com/thoas/picfit/engine/backend" "github.com/thoas/picfit/failure" @@ -194,6 +195,20 @@ func (p Processor) newBackendOptionsFromParameters(operation engine.Operation, q return nil, fmt.Errorf("Parameter \"pos\" not found in query string") } + stick, _ := qs["stick"].(string) + if stick != "" { + var exists bool + for i := range constants.StickPositions { + if stick == constants.StickPositions[i] { + exists = true + break + } + } + if !exists { + return nil, fmt.Errorf("Parameter \"stick\" has wrong value. Available values are: %v", constants.StickPositions) + } + } + color, _ := qs["color"].(string) if deg, ok := qs["deg"].(string); ok { @@ -229,6 +244,7 @@ func (p Processor) newBackendOptionsFromParameters(operation engine.Operation, q Height: height, Upscale: upscale, Position: position, + Stick: stick, Quality: quality, Degree: degree, Color: color,