Skip to content
Permalink
Browse files

resources/images: Allow to set background fill colour

Closes #6298
  • Loading branch information...
bep committed Oct 20, 2019
1 parent 689f647 commit 4b286b9d2722909d0682e50eeecdfe16c1f47fd8
@@ -2,7 +2,6 @@
title: "Image Processing"
description: "Image Page resources can be resized and cropped."
date: 2018-01-24T13:10:00-05:00
lastmod: 2018-01-26T15:59:07-05:00
linktitle: "Image Processing"
categories: ["content management"]
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.

### Background Color

JPEG Quality
: Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75.
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`.

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
{{ $image.Resize "600x q50" }}
```

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.
### 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.

```go
{{ $image.Resize "600x r90" }}
```

Anchor
: Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner.
### Anchor
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`.

```go
{{ $image.Fill "300x200 BottomLeft" }}
```

Resample Filter
: Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling.
### Resample Filter
Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling.

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" }}
```

### 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

_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
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.
@@ -205,10 +205,11 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex

// Check the file cache
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")
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")

// TODO(bep) add this as a default assertion after Build()?
@@ -17,6 +17,7 @@ import (
"encoding/json"
"fmt"
"image"
"image/color"
"image/draw"
_ "image/gif"
_ "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}
}

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 {
// Apply the colour palette from the source
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)
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) {
conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg)
conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg.Cfg)
if err != nil {
return conf, err
}
@@ -285,7 +308,14 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf

if conf.Quality <= 0 && conf.TargetFormat.RequiresDefaultQuality() {
// 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
@@ -325,7 +355,7 @@ func (i *imageResource) setBasePath(conf images.ImageConfig) {
func (i *imageResource) getImageMetaCacheTargetPath() string {
const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache

cfg := i.getSpec().imaging.Cfg
cfg := i.getSpec().imaging.Cfg.Cfg
df := i.getResourcePaths().relTargetDirFile
if fi := i.getFileInfo(); fi != nil {
df.dir = filepath.Dir(fi.Meta().Path())
@@ -22,7 +22,6 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"sync"
@@ -540,6 +539,18 @@ func TestImageOperationsGolden(t *testing.T) {
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 {

orig := fetchImageForSpec(spec, c, img)
@@ -618,9 +629,6 @@ func TestImageOperationsGolden(t *testing.T) {
c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))

for i, fi1 := range dirinfos1 {
if regexp.MustCompile("gauss").MatchString(fi1.Name()) {
continue
}
fi2 := dirinfos2[i]
c.Assert(fi1.Name(), qt.Equals, fi2.Name())

@@ -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

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

0 comments on commit 4b286b9

Please sign in to comment.
You can’t perform that action at this time.