Skip to content

Commit

Permalink
Merge pull request #96 from thoas/superpose-images
Browse files Browse the repository at this point in the history
backend method: flat
  • Loading branch information
thoas authored Dec 11, 2018
2 parents 30ea8f9 + 7534f56 commit d2591bc
Show file tree
Hide file tree
Showing 37 changed files with 2,431 additions and 716 deletions.
12 changes: 6 additions & 6 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,7 @@
[[constraint]]
name = "github.com/rwcarlsen/goexif"
revision = "8d986c03457a2057c7b0fb0a48113f7dd48f9619"

[[constraint]]
name = "github.com/lucasb-eyer/go-colorful"
revision = "c7842319cf3ac2eff253e8b3ebe15fcc56b6414a"
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,18 @@ Rotate rotates the image to the desired degree and returns the transformed image
You have to pass the ``rotate`` value to the ``op`` parameter
to use this operation.

Flat
----

Flat draws a given image on the image resulted by the previous operation.
Flat can be used only with the [multiple operation system].

- **path** - the foreground image path
- **color** - the foreground color in Hex (without ``#``), default is transparent
- **pos** - the destination rectange

In order to undersand the Flat operation, please read the following `docs <https://github.com/thoas/picfit/blob/superpose-images/docs/flat.md>`_.

Methods
=======

Expand Down
6 changes: 3 additions & 3 deletions application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,13 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool)
qs := c.MustGet("parameters").(map[string]interface{})

var err error
s := storage.SourceFromContext(c)

u, exists := c.Get("url")
if exists {
file, err = image.FromURL(u.(*url.URL), cfg.Options.DefaultUserAgent)
} else {
// URL provided we use http protocol to retrieve it
s := storage.SourceFromContext(c)

filepath = qs["path"].(string)
if !s.Exists(filepath) {
return nil, errs.ErrFileNotExists
Expand All @@ -275,7 +275,7 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool)
}

e := engine.FromContext(c)
parameters, err := NewParameters(e, file, qs)
parameters, err := NewParameters(e, s, file, qs)
if err != nil {
return nil, err
}
Expand Down
36 changes: 29 additions & 7 deletions application/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import (
"strings"

"github.com/disintegration/imaging"
"github.com/ulule/gostorages"

"github.com/thoas/picfit/engine"
"github.com/thoas/picfit/engine/backend"
"github.com/thoas/picfit/errs"
"github.com/thoas/picfit/image"
)

Expand All @@ -31,7 +34,8 @@ type Parameters struct {
Operations []engine.EngineOperation
}

func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) {
// NewParameters returns Parameters for engine.
func NewParameters(e *engine.Engine, s gostorages.Storage, input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) {
format, ok := qs["fmt"].(string)
filepath := input.Filepath

Expand Down Expand Up @@ -101,7 +105,7 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter
return nil, err
}
} else {
engineOperation, err = newEngineOperationFromQuery(e, ops[i])
engineOperation, err = newEngineOperationFromQuery(e, s, ops[i])
if err != nil {
return nil, err
}
Expand All @@ -120,12 +124,17 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter
}, nil
}

func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOperation, error) {
func newEngineOperationFromQuery(e *engine.Engine, s gostorages.Storage, op string) (*engine.EngineOperation, error) {
params := make(map[string]interface{})
var imagePaths []string
for _, p := range strings.Split(op, " ") {
l := strings.Split(p, ":")
if len(l) > 1 {
params[l[0]] = l[1]
if l[0] == "path" {
imagePaths = append(imagePaths, l[1])
} else {
params[l[0]] = l[1]
}
}
}

Expand All @@ -140,6 +149,18 @@ func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOpe
return nil, err
}

for i := range imagePaths {
if !s.Exists(imagePaths[i]) {
return nil, errs.ErrFileNotExists
}

file, err := image.FromStorage(s, imagePaths[i])
if err != nil {
return nil, err
}
opts.Images = append(opts.Images, *file)
}

return &engine.EngineOperation{
Options: opts,
Operation: operation,
Expand All @@ -149,7 +170,7 @@ func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOpe
func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operation, qs map[string]interface{}) (*backend.Options, error) {
var (
err error
quality int
quality = e.DefaultQuality
upscale = defaultUpscale
height = defaultHeight
width = defaultWidth
Expand All @@ -166,15 +187,15 @@ func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operatio
if quality > 100 {
return nil, fmt.Errorf("Quality should be <= 100")
}
} else {
quality = e.DefaultQuality
}

position, ok := qs["pos"].(string)
if !ok && operation == engine.Flip {
return nil, fmt.Errorf("Parameter \"pos\" not found in query string")
}

color, _ := qs["color"].(string)

if deg, ok := qs["deg"].(string); ok {
degree, err = strconv.Atoi(deg)
if err != nil {
Expand Down Expand Up @@ -210,5 +231,6 @@ func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operatio
Position: position,
Quality: quality,
Degree: degree,
Color: color,
}, nil
}
2 changes: 1 addition & 1 deletion application/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func TestEngineOperationFromQuery(t *testing.T) {
op := "op:resize w:123 h:321 upscale:true pos:top q:99"
operation, err := newEngineOperationFromQuery(&engine.Engine{}, op)
operation, err := newEngineOperationFromQuery(&engine.Engine{}, nil, op)
assert.Nil(t, err)

assert.Equal(t, operation.Operation.String(), "resize")
Expand Down
48 changes: 48 additions & 0 deletions docs/flat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Flat

Flat is a method implemented to the engine goimage in order to draw
images on a background image.

This method can be used only with the multiple operation parameter `op`,
in the URL.

## Parameters:

* `path`: the foreground image, can be multiple.
* `pos`: the foreground destination as a rectangle
* `color`: the foreground color in Hex (without `#`), default is transparent.


## Usage

The background is defined by the image transformed by the previous
operation. In order to draw an image on the background a position must
be given in the sub-parameter `pos` and the image path in the
sub-parameter `path`.

example:
```
/display?path=path/to/background.png&op=resize&w=100&h=100
&op=op:flat+path:path/to/foreground.png+pos:60.10.80.30
```


The value of `pos` must be the coordinates of the rectangle defined
according to the [go image package](https://blog.golang.org/go-image-package):
an axis-aligned rectangle on the integer grid, defined by its top-left and
bottom-right Point.

![Rectangle position](https://github.com/thoas/picfit/blob/superpose-images/docs/picfit-dst-position.png)

The foreground image is resized in order to fit in the given rectangle
and centered inside.

If several images are given in the same flat operation with the
subparameters path. The rectangle is cut in equal parts, **horizontally** if
the rectangle width `Dx` is superior to its height `Dy` and
**vertically** if it is not the case. Each images are then resized in
order to fit in each parts and centered inside. The order follow the
given order of `path` parameters in the URL.

![Flat multiple images](https://github.com/thoas/picfit/blob/superpose-images/docs/picfit-flat.png)

Binary file added docs/picfit-dst-position.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/picfit-flat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions engine/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ type Options struct {
Width int
Height int
Position string
Color string
Degree int
Images []image.ImageFile
}

// Engine is an interface to define an image engine
Expand All @@ -28,4 +30,5 @@ type Backend interface {
Flip(img *image.ImageFile, options *Options) ([]byte, error)
Rotate(img *image.ImageFile, options *Options) ([]byte, error)
Fit(img *image.ImageFile, options *Options) ([]byte, error)
Flat(background *image.ImageFile, options *Options) ([]byte, error)
}
138 changes: 138 additions & 0 deletions engine/backend/goimage_flat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package backend

import (
"fmt"
"image"
"image/draw"
"strconv"
"strings"

"github.com/disintegration/imaging"
colorful "github.com/lucasb-eyer/go-colorful"

imagefile "github.com/thoas/picfit/image"
)

func (e *GoImage) Flat(backgroundFile *imagefile.ImageFile, options *Options) ([]byte, error) {
if options.Format == imaging.GIF {
return e.TransformGIF(backgroundFile, options, imaging.Resize)
}

background, err := e.Source(backgroundFile)
if err != nil {
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 := image.NewRGBA(image.Rectangle{image.Point{}, background.Bounds().Size()})
draw.Draw(bg, background.Bounds(), background, image.Point{}, draw.Src)

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)
}

func positionForeground(bg image.Image, pos string) image.Rectangle {
ratios := []int{100, 100, 100, 100}
val := strings.Split(pos, ".")
for i := range val {
if i+1 > len(ratios) {
break
}
ratios[i], _ = strconv.Atoi(val[i])
}
b := bg.Bounds()
return image.Rectangle{
image.Point{b.Dx() * ratios[0], b.Dy() * ratios[1]}.Div(100),
image.Point{b.Dx() * ratios[2], b.Dy() * ratios[3]}.Div(100),
}
}

func foregroundImage(rec image.Rectangle, c string) *image.RGBA {
fg := image.NewRGBA(image.Rectangle{image.ZP, rec.Size()})
if c == "" {
return fg
}

col, err := colorful.Hex(fmt.Sprintf("#%s", c))
if err != nil {
return fg
}

draw.Draw(fg, fg.Bounds(), &image.Uniform{col}, fg.Bounds().Min, draw.Src)
return fg
}

func drawForeground(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA {
n := len(images)
if n == 0 {
return fg
}

// resize images for foreground
b := fg.Bounds()
opts := &Options{Upscale: true}

if b.Dx() > b.Dy() {
opts.Width = b.Dx() / n
opts.Height = b.Dy()
} else {
opts.Width = b.Dx()
opts.Height = b.Dy() / n
}

for i := range images {
images[i] = scale(images[i], opts, imaging.Fit)
}

if b.Dx() > b.Dy() {
return foregroundHorizontal(fg, images, options)
} else {
return foregroundVertical(fg, images, options)
}
}

func foregroundHorizontal(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA {
position := fg.Bounds().Min
totalHeight := fg.Bounds().Dy()
cellWidth := fg.Bounds().Dx() / len(images)
for i := range images {
bounds := images[i].Bounds()
position.Y = (totalHeight - bounds.Dy()) / 2
position.X = fg.Bounds().Min.X + i*cellWidth + (cellWidth-bounds.Dx())/2
r := image.Rectangle{
position,
position.Add(fg.Bounds().Size()),
}
draw.Draw(fg, r, images[i], bounds.Min, draw.Over)
}
return fg
}

func foregroundVertical(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA {
position := fg.Bounds().Min
cellHeight := fg.Bounds().Dy() / len(images)
totalWidth := fg.Bounds().Dx()
for i := range images {
bounds := images[i].Bounds()
position.Y = fg.Bounds().Min.Y + i*cellHeight + (cellHeight-bounds.Dy())/2
position.X = fg.Bounds().Min.X + (totalWidth-bounds.Dx())/2
r := image.Rectangle{
position,
position.Add(image.Point{bounds.Dx(), bounds.Dy()}),
}
draw.Draw(fg, r, images[i], bounds.Min, draw.Over)
}
return fg
}
Loading

0 comments on commit d2591bc

Please sign in to comment.