mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
parent
689f647baf
commit
4b286b9d27
15 changed files with 356 additions and 45 deletions
|
@ -2,7 +2,6 @@
|
||||||
title: "Image Processing"
|
title: "Image Processing"
|
||||||
description: "Image Page resources can be resized and cropped."
|
description: "Image Page resources can be resized and cropped."
|
||||||
date: 2018-01-24T13:10:00-05:00
|
date: 2018-01-24T13:10:00-05:00
|
||||||
lastmod: 2018-01-26T15:59:07-05:00
|
|
||||||
linktitle: "Image Processing"
|
linktitle: "Image Processing"
|
||||||
categories: ["content management"]
|
categories: ["content management"]
|
||||||
keywords: [bundle,content,resources,images]
|
keywords: [bundle,content,resources,images]
|
||||||
|
@ -72,31 +71,42 @@ Image operations in Hugo currently **do not preserve EXIF data** as this is not
|
||||||
|
|
||||||
In addition to the dimensions (e.g. `600x400`), Hugo supports a set of additional image options.
|
In addition to the dimensions (e.g. `600x400`), Hugo supports a set of additional image options.
|
||||||
|
|
||||||
|
### Background Color
|
||||||
|
|
||||||
JPEG Quality
|
The background color to fill into the transparency layer. This is mostly useful when converting to a format that does not support transparency, e.g. `JPEG`.
|
||||||
: Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75.
|
|
||||||
|
You can set the background color to use with a 3 or 6 digit hex code starting with `#`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ $image.Resize "600x jpg #b31280" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
For color codes, see https://www.google.com/search?q=color+picker
|
||||||
|
|
||||||
|
### JPEG Quality
|
||||||
|
Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ $image.Resize "600x q50" }}
|
{{ $image.Resize "600x q50" }}
|
||||||
```
|
```
|
||||||
|
|
||||||
Rotate
|
### Rotate
|
||||||
: Rotates an image by the given angle counter-clockwise. The rotation will be performed first to get the dimensions correct. The main use of this is to be able to manually correct for [EXIF orientation](https://github.com/golang/go/issues/4341) of JPEG images.
|
Rotates an image by the given angle counter-clockwise. The rotation will be performed first to get the dimensions correct. The main use of this is to be able to manually correct for [EXIF orientation](https://github.com/golang/go/issues/4341) of JPEG images.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ $image.Resize "600x r90" }}
|
{{ $image.Resize "600x r90" }}
|
||||||
```
|
```
|
||||||
|
|
||||||
Anchor
|
### Anchor
|
||||||
: Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner.
|
Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner.
|
||||||
Valid are `Center`, `TopLeft`, `Top`, `TopRight`, `Left`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`.
|
Valid are `Center`, `TopLeft`, `Top`, `TopRight`, `Left`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ $image.Fill "300x200 BottomLeft" }}
|
{{ $image.Fill "300x200 BottomLeft" }}
|
||||||
```
|
```
|
||||||
|
|
||||||
Resample Filter
|
### Resample Filter
|
||||||
: Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling.
|
Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling.
|
||||||
|
|
||||||
Examples are: `Box`, `NearestNeighbor`, `Linear`, `Gaussian`.
|
Examples are: `Box`, `NearestNeighbor`, `Linear`, `Gaussian`.
|
||||||
|
|
||||||
|
@ -106,6 +116,16 @@ See https://github.com/disintegration/imaging for more. If you want to trade qua
|
||||||
{{ $image.Resize "600x400 Gaussian" }}
|
{{ $image.Resize "600x400 Gaussian" }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Target Format
|
||||||
|
|
||||||
|
By default the images is encoded in the source format, but you can set the target format as an option.
|
||||||
|
|
||||||
|
Valid values are `jpg`, `png`, `tif`, `bmp`, and `gif`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ $image.Resize "600x jpg" }}
|
||||||
|
```
|
||||||
|
|
||||||
## Image Processing Examples
|
## Image Processing Examples
|
||||||
|
|
||||||
_The photo of the sunset used in the examples below is Copyright [Bjørn Erik Pedersen](https://commons.wikimedia.org/wiki/User:Bep) (Creative Commons Attribution-Share Alike 4.0 International license)_
|
_The photo of the sunset used in the examples below is Copyright [Bjørn Erik Pedersen](https://commons.wikimedia.org/wiki/User:Bep) (Creative Commons Attribution-Share Alike 4.0 International license)_
|
||||||
|
@ -160,6 +180,13 @@ quality = 75
|
||||||
# Valid values are Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight
|
# Valid values are Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight
|
||||||
anchor = "smart"
|
anchor = "smart"
|
||||||
|
|
||||||
|
# Default background color.
|
||||||
|
# Hugo will preserve transparency for target formats that supports it,
|
||||||
|
# but will fall back to this color for JPEG.
|
||||||
|
# Expects a standard HEX color string with 3 or 6 digits.
|
||||||
|
# See https://www.google.com/search?q=color+picker
|
||||||
|
bgColor = "#ffffff"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
All of the above settings can also be set per image procecssing.
|
All of the above settings can also be set per image procecssing.
|
||||||
|
|
|
@ -205,10 +205,11 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex
|
||||||
|
|
||||||
// Check the file cache
|
// Check the file cache
|
||||||
b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_resize_q75_box.jpg")
|
b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_resize_q75_box.jpg")
|
||||||
b.AssertFileContent("resources/_gen/images/bundle/sunset_17701188623491591036.json",
|
|
||||||
|
b.AssertFileContent("resources/_gen/images/bundle/sunset_7645215769587362592.json",
|
||||||
"DateTimeDigitized|time.Time", "PENTAX")
|
"DateTimeDigitized|time.Time", "PENTAX")
|
||||||
b.AssertImage(123, 234, "resources/_gen/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_123x234_resize_q75_box.jpg")
|
b.AssertImage(123, 234, "resources/_gen/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_123x234_resize_q75_box.jpg")
|
||||||
b.AssertFileContent("resources/_gen/images/sunset_17701188623491591036.json",
|
b.AssertFileContent("resources/_gen/images/sunset_7645215769587362592.json",
|
||||||
"DateTimeDigitized|time.Time", "PENTAX")
|
"DateTimeDigitized|time.Time", "PENTAX")
|
||||||
|
|
||||||
// TODO(bep) add this as a default assertion after Build()?
|
// TODO(bep) add this as a default assertion after Build()?
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
@ -254,10 +255,32 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
|
||||||
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
|
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasAlpha := !images.IsOpaque(converted)
|
||||||
|
shouldFill := conf.BgColor != nil && hasAlpha
|
||||||
|
shouldFill = shouldFill || (!conf.TargetFormat.SupportsTransparency() && hasAlpha)
|
||||||
|
var bgColor color.Color
|
||||||
|
|
||||||
|
if shouldFill {
|
||||||
|
bgColor = conf.BgColor
|
||||||
|
if bgColor == nil {
|
||||||
|
bgColor = i.Proc.Cfg.BgColor
|
||||||
|
}
|
||||||
|
tmp := image.NewRGBA(converted.Bounds())
|
||||||
|
draw.Draw(tmp, tmp.Bounds(), image.NewUniform(bgColor), image.Point{}, draw.Src)
|
||||||
|
draw.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min, draw.Over)
|
||||||
|
converted = tmp
|
||||||
|
}
|
||||||
|
|
||||||
if conf.TargetFormat == images.PNG {
|
if conf.TargetFormat == images.PNG {
|
||||||
// Apply the colour palette from the source
|
// Apply the colour palette from the source
|
||||||
if paletted, ok := src.(*image.Paletted); ok {
|
if paletted, ok := src.(*image.Paletted); ok {
|
||||||
tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
|
palette := paletted.Palette
|
||||||
|
if bgColor != nil && len(palette) < 256 {
|
||||||
|
palette = images.AddColorToPalette(bgColor, palette)
|
||||||
|
} else if bgColor != nil {
|
||||||
|
images.ReplaceColorInPalette(bgColor, palette)
|
||||||
|
}
|
||||||
|
tmp := image.NewPaletted(converted.Bounds(), palette)
|
||||||
draw.FloydSteinberg.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min)
|
draw.FloydSteinberg.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min)
|
||||||
converted = tmp
|
converted = tmp
|
||||||
}
|
}
|
||||||
|
@ -273,7 +296,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConfig, error) {
|
func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConfig, error) {
|
||||||
conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg)
|
conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg.Cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conf, err
|
return conf, err
|
||||||
}
|
}
|
||||||
|
@ -285,7 +308,14 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
|
||||||
|
|
||||||
if conf.Quality <= 0 && conf.TargetFormat.RequiresDefaultQuality() {
|
if conf.Quality <= 0 && conf.TargetFormat.RequiresDefaultQuality() {
|
||||||
// We need a quality setting for all JPEGs
|
// We need a quality setting for all JPEGs
|
||||||
conf.Quality = i.Proc.Cfg.Quality
|
conf.Quality = i.Proc.Cfg.Cfg.Quality
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.BgColor == nil && conf.TargetFormat != i.Format {
|
||||||
|
if i.Format.SupportsTransparency() && !conf.TargetFormat.SupportsTransparency() {
|
||||||
|
conf.BgColor = i.Proc.Cfg.BgColor
|
||||||
|
conf.BgColorStr = i.Proc.Cfg.Cfg.BgColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return conf, nil
|
return conf, nil
|
||||||
|
@ -325,7 +355,7 @@ func (i *imageResource) setBasePath(conf images.ImageConfig) {
|
||||||
func (i *imageResource) getImageMetaCacheTargetPath() string {
|
func (i *imageResource) getImageMetaCacheTargetPath() string {
|
||||||
const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache
|
const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache
|
||||||
|
|
||||||
cfg := i.getSpec().imaging.Cfg
|
cfg := i.getSpec().imaging.Cfg.Cfg
|
||||||
df := i.getResourcePaths().relTargetDirFile
|
df := i.getResourcePaths().relTargetDirFile
|
||||||
if fi := i.getFileInfo(); fi != nil {
|
if fi := i.getFileInfo(); fi != nil {
|
||||||
df.dir = filepath.Dir(fi.Meta().Path())
|
df.dir = filepath.Dir(fi.Meta().Path())
|
||||||
|
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -540,6 +539,18 @@ func TestImageOperationsGolden(t *testing.T) {
|
||||||
fmt.Println(workDir)
|
fmt.Println(workDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test PNGs with alpha channel.
|
||||||
|
for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
|
||||||
|
orig := fetchImageForSpec(spec, c, img)
|
||||||
|
for _, resizeSpec := range []string{"200x #e3e615", "200x jpg #e3e615"} {
|
||||||
|
resized, err := orig.Resize(resizeSpec)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
rel := resized.RelPermalink()
|
||||||
|
c.Log("resize", rel)
|
||||||
|
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, img := range testImages {
|
for _, img := range testImages {
|
||||||
|
|
||||||
orig := fetchImageForSpec(spec, c, img)
|
orig := fetchImageForSpec(spec, c, img)
|
||||||
|
@ -618,9 +629,6 @@ func TestImageOperationsGolden(t *testing.T) {
|
||||||
c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
|
c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
|
||||||
|
|
||||||
for i, fi1 := range dirinfos1 {
|
for i, fi1 := range dirinfos1 {
|
||||||
if regexp.MustCompile("gauss").MatchString(fi1.Name()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fi2 := dirinfos2[i]
|
fi2 := dirinfos2[i]
|
||||||
c.Assert(fi1.Name(), qt.Equals, fi2.Name())
|
c.Assert(fi1.Name(), qt.Equals, fi2.Name())
|
||||||
|
|
||||||
|
|
85
resources/images/color.go
Normal file
85
resources/images/color.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"image/color"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddColorToPalette adds c as the first color in p if not already there.
|
||||||
|
// Note that it does no additional checks, so callers must make sure
|
||||||
|
// that the palette is valid for the relevant format.
|
||||||
|
func AddColorToPalette(c color.Color, p color.Palette) color.Palette {
|
||||||
|
var found bool
|
||||||
|
for _, cc := range p {
|
||||||
|
if c == cc {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
p = append(color.Palette{c}, p...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceColorInPalette will replace the color in palette p closest to c in Euclidean
|
||||||
|
// R,G,B,A space with c.
|
||||||
|
func ReplaceColorInPalette(c color.Color, p color.Palette) {
|
||||||
|
p[p.Index(c)] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexStringToColor(s string) (color.Color, error) {
|
||||||
|
s = strings.TrimPrefix(s, "#")
|
||||||
|
|
||||||
|
if len(s) != 3 && len(s) != 6 {
|
||||||
|
return nil, errors.Errorf("invalid color code: %q", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
|
||||||
|
if len(s) == 3 {
|
||||||
|
var v string
|
||||||
|
for _, r := range s {
|
||||||
|
v += string(r) + string(r)
|
||||||
|
}
|
||||||
|
s = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard colors.
|
||||||
|
if s == "ffffff" {
|
||||||
|
return color.White, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == "000000" {
|
||||||
|
return color.Black, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Alfa to white.
|
||||||
|
s += "ff"
|
||||||
|
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return color.RGBA{b[0], b[1], b[2], b[3]}, nil
|
||||||
|
|
||||||
|
}
|
90
resources/images/color_test.go
Normal file
90
resources/images/color_test.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHexStringToColor(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
arg string
|
||||||
|
expect interface{}
|
||||||
|
}{
|
||||||
|
{"f", false},
|
||||||
|
{"#f", false},
|
||||||
|
{"#fffffff", false},
|
||||||
|
{"fffffff", false},
|
||||||
|
{"#fff", color.White},
|
||||||
|
{"fff", color.White},
|
||||||
|
{"FFF", color.White},
|
||||||
|
{"FfF", color.White},
|
||||||
|
{"#ffffff", color.White},
|
||||||
|
{"ffffff", color.White},
|
||||||
|
{"#000", color.Black},
|
||||||
|
{"#4287f5", color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}},
|
||||||
|
{"777", color.RGBA{R: 0x77, G: 0x77, B: 0x77, A: 0xff}},
|
||||||
|
} {
|
||||||
|
|
||||||
|
test := test
|
||||||
|
c.Run(test.arg, func(c *qt.C) {
|
||||||
|
c.Parallel()
|
||||||
|
|
||||||
|
result, err := hexStringToColor(test.arg)
|
||||||
|
|
||||||
|
if b, ok := test.expect.(bool); ok && !b {
|
||||||
|
c.Assert(err, qt.Not(qt.IsNil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Assert(result, qt.DeepEquals, test.expect)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddColorToPalette(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
palette := color.Palette{color.White, color.Black}
|
||||||
|
|
||||||
|
c.Assert(AddColorToPalette(color.White, palette), qt.HasLen, 2)
|
||||||
|
|
||||||
|
blue1, _ := hexStringToColor("34c3eb")
|
||||||
|
blue2, _ := hexStringToColor("34c3eb")
|
||||||
|
white, _ := hexStringToColor("fff")
|
||||||
|
|
||||||
|
c.Assert(AddColorToPalette(white, palette), qt.HasLen, 2)
|
||||||
|
c.Assert(AddColorToPalette(blue1, palette), qt.HasLen, 3)
|
||||||
|
c.Assert(AddColorToPalette(blue2, palette), qt.HasLen, 3)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceColorInPalette(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
palette := color.Palette{color.White, color.Black}
|
||||||
|
offWhite, _ := hexStringToColor("fcfcfc")
|
||||||
|
|
||||||
|
ReplaceColorInPalette(offWhite, palette)
|
||||||
|
|
||||||
|
c.Assert(palette, qt.HasLen, 2)
|
||||||
|
c.Assert(palette[0], qt.Equals, offWhite)
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ package images
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ import (
|
||||||
const (
|
const (
|
||||||
defaultJPEGQuality = 75
|
defaultJPEGQuality = 75
|
||||||
defaultResampleFilter = "box"
|
defaultResampleFilter = "box"
|
||||||
|
defaultBgColor = "ffffff"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -87,16 +89,28 @@ func ImageFormatFromExt(ext string) (Format, bool) {
|
||||||
return f, found
|
return f, found
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeConfig(m map[string]interface{}) (Imaging, error) {
|
func DecodeConfig(m map[string]interface{}) (ImagingConfig, error) {
|
||||||
var i Imaging
|
var i Imaging
|
||||||
|
var ic ImagingConfig
|
||||||
if err := mapstructure.WeakDecode(m, &i); err != nil {
|
if err := mapstructure.WeakDecode(m, &i); err != nil {
|
||||||
return i, err
|
return ic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Quality == 0 {
|
if i.Quality == 0 {
|
||||||
i.Quality = defaultJPEGQuality
|
i.Quality = defaultJPEGQuality
|
||||||
} else if i.Quality < 0 || i.Quality > 100 {
|
} else if i.Quality < 0 || i.Quality > 100 {
|
||||||
return i, errors.New("JPEG quality must be a number between 1 and 100")
|
return ic, errors.New("JPEG quality must be a number between 1 and 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.BgColor != "" {
|
||||||
|
i.BgColor = strings.TrimPrefix(i.BgColor, "#")
|
||||||
|
} else {
|
||||||
|
i.BgColor = defaultBgColor
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
ic.BgColor, err = hexStringToColor(i.BgColor)
|
||||||
|
if err != nil {
|
||||||
|
return ic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
|
if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
|
||||||
|
@ -104,7 +118,7 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
|
||||||
} else {
|
} else {
|
||||||
i.Anchor = strings.ToLower(i.Anchor)
|
i.Anchor = strings.ToLower(i.Anchor)
|
||||||
if _, found := anchorPositions[i.Anchor]; !found {
|
if _, found := anchorPositions[i.Anchor]; !found {
|
||||||
return i, errors.New("invalid anchor value in imaging config")
|
return ic, errors.New("invalid anchor value in imaging config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +128,7 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
|
||||||
filter := strings.ToLower(i.ResampleFilter)
|
filter := strings.ToLower(i.ResampleFilter)
|
||||||
_, found := imageFilters[filter]
|
_, found := imageFilters[filter]
|
||||||
if !found {
|
if !found {
|
||||||
return i, fmt.Errorf("%q is not a valid resample filter", filter)
|
return ic, fmt.Errorf("%q is not a valid resample filter", filter)
|
||||||
}
|
}
|
||||||
i.ResampleFilter = filter
|
i.ResampleFilter = filter
|
||||||
}
|
}
|
||||||
|
@ -124,7 +138,9 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
|
||||||
i.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
|
i.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
|
||||||
}
|
}
|
||||||
|
|
||||||
return i, nil
|
ic.Cfg = i
|
||||||
|
|
||||||
|
return ic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
|
func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
|
||||||
|
@ -151,6 +167,12 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
|
||||||
} else if filter, ok := imageFilters[part]; ok {
|
} else if filter, ok := imageFilters[part]; ok {
|
||||||
c.Filter = filter
|
c.Filter = filter
|
||||||
c.FilterStr = part
|
c.FilterStr = part
|
||||||
|
} else if part[0] == '#' {
|
||||||
|
c.BgColorStr = part[1:]
|
||||||
|
c.BgColor, err = hexStringToColor(c.BgColorStr)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
} else if part[0] == 'q' {
|
} else if part[0] == 'q' {
|
||||||
c.Quality, err = strconv.Atoi(part[1:])
|
c.Quality, err = strconv.Atoi(part[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -230,6 +252,14 @@ type ImageConfig struct {
|
||||||
// The rotation will be performed first.
|
// The rotation will be performed first.
|
||||||
Rotate int
|
Rotate int
|
||||||
|
|
||||||
|
// Used to fill any transparency.
|
||||||
|
// When set in site config, it's used when converting to a format that does
|
||||||
|
// not support transparency.
|
||||||
|
// When set per image operation, it's used even for formats that does support
|
||||||
|
// transparency.
|
||||||
|
BgColor color.Color
|
||||||
|
BgColorStr string
|
||||||
|
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
|
|
||||||
|
@ -255,6 +285,10 @@ func (i ImageConfig) GetKey(format Format) string {
|
||||||
if i.Rotate != 0 {
|
if i.Rotate != 0 {
|
||||||
k += "_r" + strconv.Itoa(i.Rotate)
|
k += "_r" + strconv.Itoa(i.Rotate)
|
||||||
}
|
}
|
||||||
|
if i.BgColorStr != "" {
|
||||||
|
k += "_bg" + i.BgColorStr
|
||||||
|
}
|
||||||
|
|
||||||
anchor := i.AnchorStr
|
anchor := i.AnchorStr
|
||||||
if anchor == smartCropIdentifier {
|
if anchor == smartCropIdentifier {
|
||||||
anchor = anchor + strconv.Itoa(smartCropVersionNumber)
|
anchor = anchor + strconv.Itoa(smartCropVersionNumber)
|
||||||
|
@ -277,6 +311,13 @@ func (i ImageConfig) GetKey(format Format) string {
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImagingConfig struct {
|
||||||
|
BgColor color.Color
|
||||||
|
|
||||||
|
// Config as provided by the user.
|
||||||
|
Cfg Imaging
|
||||||
|
}
|
||||||
|
|
||||||
// Imaging contains default image processing configuration. This will be fetched
|
// Imaging contains default image processing configuration. This will be fetched
|
||||||
// from site (or language) config.
|
// from site (or language) config.
|
||||||
type Imaging struct {
|
type Imaging struct {
|
||||||
|
@ -289,6 +330,9 @@ type Imaging struct {
|
||||||
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
|
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
|
||||||
Anchor string
|
Anchor string
|
||||||
|
|
||||||
|
// Default color used in fill operations (e.g. "fff" for white).
|
||||||
|
BgColor string
|
||||||
|
|
||||||
Exif ExifConfig
|
Exif ExifConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,17 +29,19 @@ func TestDecodeConfig(t *testing.T) {
|
||||||
"anchor": "topLeft",
|
"anchor": "topLeft",
|
||||||
}
|
}
|
||||||
|
|
||||||
imaging, err := DecodeConfig(m)
|
imagingConfig, err := DecodeConfig(m)
|
||||||
|
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
imaging := imagingConfig.Cfg
|
||||||
c.Assert(imaging.Quality, qt.Equals, 42)
|
c.Assert(imaging.Quality, qt.Equals, 42)
|
||||||
c.Assert(imaging.ResampleFilter, qt.Equals, "nearestneighbor")
|
c.Assert(imaging.ResampleFilter, qt.Equals, "nearestneighbor")
|
||||||
c.Assert(imaging.Anchor, qt.Equals, "topleft")
|
c.Assert(imaging.Anchor, qt.Equals, "topleft")
|
||||||
|
|
||||||
m = map[string]interface{}{}
|
m = map[string]interface{}{}
|
||||||
|
|
||||||
imaging, err = DecodeConfig(m)
|
imagingConfig, err = DecodeConfig(m)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
imaging = imagingConfig.Cfg
|
||||||
c.Assert(imaging.Quality, qt.Equals, defaultJPEGQuality)
|
c.Assert(imaging.Quality, qt.Equals, defaultJPEGQuality)
|
||||||
c.Assert(imaging.ResampleFilter, qt.Equals, "box")
|
c.Assert(imaging.ResampleFilter, qt.Equals, "box")
|
||||||
c.Assert(imaging.Anchor, qt.Equals, "smart")
|
c.Assert(imaging.Anchor, qt.Equals, "smart")
|
||||||
|
@ -59,18 +61,20 @@ func TestDecodeConfig(t *testing.T) {
|
||||||
})
|
})
|
||||||
c.Assert(err, qt.Not(qt.IsNil))
|
c.Assert(err, qt.Not(qt.IsNil))
|
||||||
|
|
||||||
imaging, err = DecodeConfig(map[string]interface{}{
|
imagingConfig, err = DecodeConfig(map[string]interface{}{
|
||||||
"anchor": "Smart",
|
"anchor": "Smart",
|
||||||
})
|
})
|
||||||
|
imaging = imagingConfig.Cfg
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(imaging.Anchor, qt.Equals, "smart")
|
c.Assert(imaging.Anchor, qt.Equals, "smart")
|
||||||
|
|
||||||
imaging, err = DecodeConfig(map[string]interface{}{
|
imagingConfig, err = DecodeConfig(map[string]interface{}{
|
||||||
"exif": map[string]interface{}{
|
"exif": map[string]interface{}{
|
||||||
"disableLatLong": true,
|
"disableLatLong": true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
imaging = imagingConfig.Cfg
|
||||||
c.Assert(imaging.Exif.DisableLatLong, qt.Equals, true)
|
c.Assert(imaging.Exif.DisableLatLong, qt.Equals, true)
|
||||||
c.Assert(imaging.Exif.ExcludeFields, qt.Equals, "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance")
|
c.Assert(imaging.Exif.ExcludeFields, qt.Equals, "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance")
|
||||||
|
|
||||||
|
@ -81,11 +85,12 @@ func TestDecodeImageConfig(t *testing.T) {
|
||||||
in string
|
in string
|
||||||
expect interface{}
|
expect interface{}
|
||||||
}{
|
}{
|
||||||
{"300x400", newImageConfig(300, 400, 0, 0, "", "")},
|
{"300x400", newImageConfig(300, 400, 0, 0, "", "", "")},
|
||||||
{"100x200 bottomRight", newImageConfig(100, 200, 0, 0, "", "BottomRight")},
|
{"300x400 #fff", newImageConfig(300, 400, 0, 0, "", "", "fff")},
|
||||||
{"10x20 topleft Lanczos", newImageConfig(10, 20, 0, 0, "Lanczos", "topleft")},
|
{"100x200 bottomRight", newImageConfig(100, 200, 0, 0, "", "BottomRight", "")},
|
||||||
{"linear left 10x r180", newImageConfig(10, 0, 0, 180, "linear", "left")},
|
{"10x20 topleft Lanczos", newImageConfig(10, 20, 0, 0, "Lanczos", "topleft", "")},
|
||||||
{"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right")},
|
{"linear left 10x r180", newImageConfig(10, 0, 0, 180, "linear", "left", "")},
|
||||||
|
{"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right", "")},
|
||||||
|
|
||||||
{"", false},
|
{"", false},
|
||||||
{"foo", false},
|
{"foo", false},
|
||||||
|
@ -107,13 +112,15 @@ func TestDecodeImageConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newImageConfig(width, height, quality, rotate int, filter, anchor string) ImageConfig {
|
func newImageConfig(width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
|
||||||
var c ImageConfig
|
var c ImageConfig
|
||||||
c.Action = "resize"
|
c.Action = "resize"
|
||||||
c.Width = width
|
c.Width = width
|
||||||
c.Height = height
|
c.Height = height
|
||||||
c.Quality = quality
|
c.Quality = quality
|
||||||
c.Rotate = rotate
|
c.Rotate = rotate
|
||||||
|
c.BgColorStr = bgColor
|
||||||
|
c.BgColor, _ = hexStringToColor(bgColor)
|
||||||
|
|
||||||
if filter != "" {
|
if filter != "" {
|
||||||
filter = strings.ToLower(filter)
|
filter = strings.ToLower(filter)
|
||||||
|
|
|
@ -51,11 +51,8 @@ func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image {
|
||||||
|
|
||||||
type Image struct {
|
type Image struct {
|
||||||
Format Format
|
Format Format
|
||||||
|
Proc *ImageProcessor
|
||||||
Proc *ImageProcessor
|
Spec Spec
|
||||||
|
|
||||||
Spec Spec
|
|
||||||
|
|
||||||
*imageConfig
|
*imageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,8 +155,8 @@ func (i *Image) initConfig() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewImageProcessor(cfg Imaging) (*ImageProcessor, error) {
|
func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) {
|
||||||
e := cfg.Exif
|
e := cfg.Cfg.Exif
|
||||||
exifDecoder, err := exif.NewDecoder(
|
exifDecoder, err := exif.NewDecoder(
|
||||||
exif.WithDateDisabled(e.DisableDate),
|
exif.WithDateDisabled(e.DisableDate),
|
||||||
exif.WithLatLongDisabled(e.DisableLatLong),
|
exif.WithLatLongDisabled(e.DisableLatLong),
|
||||||
|
@ -179,7 +176,7 @@ func NewImageProcessor(cfg Imaging) (*ImageProcessor, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageProcessor struct {
|
type ImageProcessor struct {
|
||||||
Cfg Imaging
|
Cfg ImagingConfig
|
||||||
exifDecoder *exif.Decoder
|
exifDecoder *exif.Decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +215,12 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi
|
||||||
return nil, errors.Errorf("unsupported action: %q", conf.Action)
|
return nil, errors.Errorf("unsupported action: %q", conf.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.Filter(src, filters...)
|
img, err := p.Filter(src, filters...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
|
func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
|
||||||
|
@ -231,7 +233,7 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.
|
||||||
func (p *ImageProcessor) GetDefaultImageConfig(action string) ImageConfig {
|
func (p *ImageProcessor) GetDefaultImageConfig(action string) ImageConfig {
|
||||||
return ImageConfig{
|
return ImageConfig{
|
||||||
Action: action,
|
Action: action,
|
||||||
Quality: p.Cfg.Quality,
|
Quality: p.Cfg.Cfg.Quality,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,6 +258,11 @@ func (f Format) RequiresDefaultQuality() bool {
|
||||||
return f == JPEG
|
return f == JPEG
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportsTransparency reports whether it supports transparency in any form.
|
||||||
|
func (f Format) SupportsTransparency() bool {
|
||||||
|
return f != JPEG
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultExtension returns the default file extension of this format, starting with a dot.
|
// DefaultExtension returns the default file extension of this format, starting with a dot.
|
||||||
// For example: .jpg for JPEG
|
// For example: .jpg for JPEG
|
||||||
func (f Format) DefaultExtension() string {
|
func (f Format) DefaultExtension() string {
|
||||||
|
@ -307,3 +314,15 @@ func ToFilters(in interface{}) []gift.Filter {
|
||||||
panic(fmt.Sprintf("%T is not an image filter", in))
|
panic(fmt.Sprintf("%T is not an image filter", in))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsOpaque returns false if the image has alpha channel and there is at least 1
|
||||||
|
// pixel that is not (fully) opaque.
|
||||||
|
func IsOpaque(img image.Image) bool {
|
||||||
|
if oim, ok := img.(interface {
|
||||||
|
Opaque() bool
|
||||||
|
}); ok {
|
||||||
|
return oim.Opaque()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
resources/testdata/gopher-hero8.png
vendored
Normal file
BIN
resources/testdata/gopher-hero8.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
resources/testdata/gradient-circle.png
vendored
Normal file
BIN
resources/testdata/gradient-circle.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Loading…
Reference in a new issue