Repository: gohugoio/hugo (language: go)

Issue:
Create padding image filter
Return an image resource that you can use with `images.Text` and `images.Overlay`.

```
images.Create WIDTH HEIGHT [BGCOLOR] [FORMAT]
```

- Width and height must be in the range [1,10000] (or something smaller, but put a limit on it).
- Background color is RGBA, with length of 3, 4, 6, or 8 (e.g., `#123`, `#123f`, `#112233`, `#112233ff`. If alpha not specified, opaque. Default is white.
- Format is one of `jpeg`, `webp`, etc. Default is `png`.

The way to handle this today is with a `1x1.png` in the assets directory that you grab as a resource and then resize. 

Reference: <https://discourse.gohugo.io/t/should-it-be-possible-to-pad-an-an-image-using-0-119-image-processing/46684/4>

---

EDIT: Rethinking this, the specific case referenced above, image padding, would probably be handled better with an image filter.

```
images.Pad D1 [D2] [D3] [D4] [BGCOLOR]
```

Where the dimensions use the same syntax as CSS padding (e.g., if you provide one number if affects all sides), and background color is as previously described.




Discussion / hints:
That would be useful, but we cannot re-invent the wheel here, so I suggest:

```
{{ $image := images.Create "200x200 jpg #000" }}
```

Which is the same "string API" we use in Process etc.
>The way to handle this today is with a 1x1.png in the assets directory that you grab as a resource and then resize.

That said, I say that the common way today is to use a pretty background image (with some gradient or something) for your overlay images, which is trivial to make in most photo editors.
Agreed. The _primary_ use case behind this suggestions is padding an image, which today is `get+resize+get+overlay`.  

Instead of doing `create+get+overlay`, maybe just a filter so you can do `get+pad`.

Fix patch:
diff --git a/docs/content/en/functions/images/index.md b/docs/content/en/functions/images/index.md
index a71c2b61c1b..f886caa72ed 100644
--- a/docs/content/en/functions/images/index.md
+++ b/docs/content/en/functions/images/index.md
@@ -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:
 
 ```go-html-template
-
 {{ $font := resources.GetRemote "https://github.com/google/fonts/raw/main/apache/roboto/static/Roboto-Black.ttf" }}
 {{ $img := resources.Get "/images/background.png" }}
 {{ $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
 
diff --git a/resources/images/color.go b/resources/images/color.go
index 0eedecb890f..fe891fe8832 100644
--- a/resources/images/color.go
+++ b/resources/images/color.go
@@ -56,13 +56,13 @@ func ColorToHexString(c color.Color) string {
 func hexStringToColor(s string) (color.Color, error) {
 	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)
 	}
 
 	s = strings.ToLower(s)
 
-	if len(s) == 3 {
+	if len(s) == 3 || len(s) == 4 {
 		var v string
 		for _, r := range s {
 			v += string(r) + string(r)
@@ -80,7 +80,9 @@ func hexStringToColor(s string) (color.Color, error) {
 	}
 
 	// Set Alfa to white.
-	s += "ff"
+	if len(s) == 6 {
+		s += "ff"
+	}
 
 	b, err := hex.DecodeString(s)
 	if err != nil {
diff --git a/resources/images/filters.go b/resources/images/filters.go
index dca8ff0e8fc..2a1f41a0343 100644
--- a/resources/images/filters.go
+++ b/resources/images/filters.go
@@ -16,6 +16,7 @@ package images
 
 import (
 	"fmt"
+	"image/color"
 
 	"github.com/gohugoio/hugo/common/hugio"
 	"github.com/gohugoio/hugo/common/maps"
@@ -30,6 +31,7 @@ const filterAPIVersion = 0
 
 type Filters struct{}
 
+// Process creates a filter that processes an image using the given specification.
 func (*Filters) Process(spec any) gift.Filter {
 	return filter{
 		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.
 // The percentage parameter must be in range (-100, 100).
 func (*Filters) Brightness(percentage any) gift.Filter {
diff --git a/resources/images/padding.go b/resources/images/padding.go
new file mode 100644
index 00000000000..153d0bd82be
--- /dev/null
+++ b/resources/images/padding.go
@@ -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)
+}


Test patch: