resources/images: Create padding image filter

Closes #11599
This commit is contained in:
Joe Mooring 2023-10-27 20:19:39 -07:00 committed by Bjørn Erik Pedersen
parent db14238ba3
commit 3ed28e4bfe
4 changed files with 146 additions and 4 deletions

View file

@ -104,7 +104,6 @@ The following example will add the text `Hugo rocks!` to the image with the spec
You can load a custom font if needed. Load the font as a Hugo `Resource` and set it as an option: You can load a custom font if needed. Load the font as a Hugo `Resource` and set it as an option:
```go-html-template ```go-html-template
{{ $font := resources.GetRemote "https://github.com/google/fonts/raw/main/apache/roboto/static/Roboto-Black.ttf" }} {{ $font := resources.GetRemote "https://github.com/google/fonts/raw/main/apache/roboto/static/Roboto-Black.ttf" }}
{{ $img := resources.Get "/images/background.png" }} {{ $img := resources.Get "/images/background.png" }}
{{ $img = $img.Filter (images.Text "Hugo rocks!" (dict {{ $img = $img.Filter (images.Text "Hugo rocks!" (dict
@ -112,6 +111,33 @@ You can load a custom font if needed. Load the font as a Hugo `Resource` and set
))}} ))}}
``` ```
## Padding
Padding creates a filter that resizes the image canvas without resizing the image. The last argument is the canvas color, expressed as an RGB or RGBA [hexadecimal color]. The default value is `ffffffff` (opaque white). The preceding arguments are the padding values, in pixels, using the CSS [shorthand property] syntax. Negative padding values will crop the image.
[hexadecimal color]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
[shorthand property]: https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties#edges_of_a_box
{{% funcsig %}}
images.Padding V1 [V2] [V3] [V4] [COLOR]
{{% /funcsig %}}
This example resizes the image to 300px wide, converts it to the WebP format, adds 20px vertical padding and 50px horizontal padding, then sets the canvas color to dark green with 33% opacity.
```go-html-template
{{ $img := resources.Get "images/a.jpg" }}
{{ $filters := slice
(images.Process "resize 300x webp")
(images.Padding 20 50 "#0705")
}}
{{ $img = $img.Filter $filters }}
```
To add a 2px gray border to an image:
```go-html-template
{{ $img = $img.Filter (images.Padding 2 "#777") }}
```
## Brightness ## Brightness

View file

@ -56,13 +56,13 @@ func ColorToHexString(c color.Color) string {
func hexStringToColor(s string) (color.Color, error) { func hexStringToColor(s string) (color.Color, error) {
s = strings.TrimPrefix(s, "#") s = strings.TrimPrefix(s, "#")
if len(s) != 3 && len(s) != 6 { if len(s) != 3 && len(s) != 4 && len(s) != 6 && len(s) != 8 {
return nil, fmt.Errorf("invalid color code: %q", s) return nil, fmt.Errorf("invalid color code: %q", s)
} }
s = strings.ToLower(s) s = strings.ToLower(s)
if len(s) == 3 { if len(s) == 3 || len(s) == 4 {
var v string var v string
for _, r := range s { for _, r := range s {
v += string(r) + string(r) v += string(r) + string(r)
@ -80,7 +80,9 @@ func hexStringToColor(s string) (color.Color, error) {
} }
// Set Alfa to white. // Set Alfa to white.
s += "ff" if len(s) == 6 {
s += "ff"
}
b, err := hex.DecodeString(s) b, err := hex.DecodeString(s)
if err != nil { if err != nil {

View file

@ -16,6 +16,7 @@ package images
import ( import (
"fmt" "fmt"
"image/color"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
@ -30,6 +31,7 @@ const filterAPIVersion = 0
type Filters struct{} type Filters struct{}
// Process creates a filter that processes an image using the given specification.
func (*Filters) Process(spec any) gift.Filter { func (*Filters) Process(spec any) gift.Filter {
return filter{ return filter{
Options: newFilterOpts(spec), Options: newFilterOpts(spec),
@ -110,6 +112,68 @@ func (*Filters) Text(text string, options ...any) gift.Filter {
} }
} }
// Padding creates a filter that resizes the image canvas without resizing the
// image. The last argument is the canvas color, expressed as an RGB or RGBA
// hexadecimal color. The default value is `ffffffff` (opaque white). The
// preceding arguments are the padding values, in pixels, using the CSS
// shorthand property syntax. Negative padding values will crop the image. The
// signature is images.Padding V1 [V2] [V3] [V4] [COLOR].
func (*Filters) Padding(args ...any) gift.Filter {
if len(args) < 1 || len(args) > 5 {
panic("the padding filter requires between 1 and 5 arguments")
}
var top, right, bottom, left int
var ccolor color.Color = color.White // canvas color
var err error
_args := args // preserve original args for most stable hash
if vcs, ok := (args[len(args)-1]).(string); ok {
ccolor, err = hexStringToColor(vcs)
if err != nil {
panic("invalid canvas color: specify RGB or RGBA using hex notation")
}
args = args[:len(args)-1]
if len(args) == 0 {
panic("not enough arguments: provide one or more padding values using the CSS shorthand property syntax")
}
}
var vals []int
for _, v := range args {
vi := cast.ToInt(v)
if vi > 5000 {
panic("padding values must not exceed 5000 pixels")
}
vals = append(vals, vi)
}
switch len(args) {
case 1:
top, right, bottom, left = vals[0], vals[0], vals[0], vals[0]
case 2:
top, right, bottom, left = vals[0], vals[1], vals[0], vals[1]
case 3:
top, right, bottom, left = vals[0], vals[1], vals[2], vals[1]
case 4:
top, right, bottom, left = vals[0], vals[1], vals[2], vals[3]
default:
panic(fmt.Sprintf("too many padding values: received %d, expected maximum of 4", len(args)))
}
return filter{
Options: newFilterOpts(_args...),
Filter: paddingFilter{
top: top,
right: right,
bottom: bottom,
left: left,
ccolor: ccolor,
},
}
}
// Brightness creates a filter that changes the brightness of an image. // Brightness creates a filter that changes the brightness of an image.
// The percentage parameter must be in range (-100, 100). // The percentage parameter must be in range (-100, 100).
func (*Filters) Brightness(percentage any) gift.Filter { func (*Filters) Brightness(percentage any) gift.Filter {

View file

@ -0,0 +1,50 @@
// Copyright 2023 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"
"image/color"
"image/draw"
"github.com/disintegration/gift"
)
var _ gift.Filter = (*paddingFilter)(nil)
type paddingFilter struct {
top, right, bottom, left int
ccolor color.Color // canvas color
}
func (f paddingFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
w := src.Bounds().Dx() + f.left + f.right
h := src.Bounds().Dy() + f.top + f.bottom
if w < 1 {
panic("final image width will be less than 1 pixel: check padding values")
}
if h < 1 {
panic("final image height will be less than 1 pixel: check padding values")
}
i := image.NewRGBA(image.Rect(0, 0, w, h))
draw.Draw(i, i.Bounds(), image.NewUniform(f.ccolor), image.Point{}, draw.Src)
gift.New().Draw(dst, i)
gift.New().DrawAt(dst, src, image.Pt(f.left, f.top), gift.OverOperator)
}
func (f paddingFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
return image.Rect(0, 0, srcBounds.Dx()+f.left+f.right, srcBounds.Dy()+f.top+f.bottom)
}