With some performance improvements upstream, there's improvements in memory usage. Note that the benchmark below is full site builds with image processing:
```
goos: darwin
goarch: arm64
pkg: github.com/gohugoio/hugo/resources/images
cpu: Apple M1 Pro
│ benchcmp.bench │ feat-forkgift-14397.bench │
│ sec/op │ sec/op vs base │
ImageResize-10 74.60m ± 4% 73.75m ± 4% ~ (p=0.093 n=6)
│ benchcmp.bench │ feat-forkgift-14397.bench │
│ B/op │ B/op vs base │
ImageResize-10 32.38Mi ± 1% 29.31Mi ± 1% -9.49% (p=0.002 n=6)
│ benchcmp.bench │ feat-forkgift-14397.bench │
│ allocs/op │ allocs/op vs base │
ImageResize-10 12.53k ± 1% 12.05k ± 1% -3.82% (p=0.002 n=6)
```
Closes #14397
github.com/bits-and-blooms/bitset v1.24.4
github.com/cespare/xxhash/v2 v2.3.0
github.com/clbanning/mxj/v2 v2.7.0
- github.com/disintegration/gift v1.2.1
github.com/dustin/go-humanize v1.0.1
github.com/evanw/esbuild v0.27.2
github.com/fatih/color v1.18.0
github.com/gobuffalo/flect v1.0.3
github.com/gobwas/glob v0.2.3
github.com/goccy/go-yaml v1.19.2
+ github.com/gohugoio/gift v0.1.0
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6
github.com/gohugoio/go-radix v1.2.0
github.com/gohugoio/hashstructure v0.6.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
-github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/gohugoio/gift v0.1.0 h1:KDx+OBrSn/sXmnMdMLYocOLn35MrBpnVhgQghHxd7g8=
+github.com/gohugoio/gift v0.1.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/gohugoio/hugo/resources/images/meta"
"github.com/gohugoio/hugo/resources/internal"
"image"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
var _ gift.Filter = (*autoOrientFilter)(nil)
AutoOrient(orientation int) gift.Filter
}
-func (f autoOrientFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+func (f autoOrientFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
panic("not supported")
}
"github.com/gohugoio/hugo/media"
"github.com/mitchellh/mapstructure"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
const (
"image"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/makeworld-the-better-one/dither/v2"
)
"vertical5x3": dither.Vertical5x3,
}
-func (f ditherFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
- gift.New().Draw(dst, f.ditherer.Dither(src))
+func (f ditherFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
+ return gift.New().Draw(dst, f.ditherer.Dither(src))
}
func (f ditherFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
"github.com/makeworld-the-better-one/dither/v2"
"github.com/mitchellh/mapstructure"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/spf13/cast"
)
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images/meta"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/gohugoio/hugo/common/himage"
"github.com/gohugoio/hugo/common/hugio"
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Orientation: 6|eq 6: true|")
}
+
+func BenchmarkImageResize(b *testing.B) {
+ files := `
+-- content/p1/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p1/index.md --
+-- content/p2/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p2/index.md --
+-- content/p3/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p3/index.md --
+-- content/p4/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p4/index.md --
+-- content/p5/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p5/index.md --
+-- content/p6/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p6/index.md --
+-- content/p7/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p7/index.md --
+-- content/p8/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p8/index.md --
+-- content/p9/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p9/index.md --
+-- content/p10/sunrise.jpg --
+sourcefilename: ../../resources/testdata/sunrise.jpg
+-- content/p10/index.md --
+-- layouts/home.html --
+Home.
+-- layouts/page.html --
+Page.
+{{ $image := .Resources.Get "sunrise.jpg" }}
+{{ ($image.Process "resize 200x200").Publish }}
+
+`
+
+ cfg := hugolib.IntegrationTestConfig{
+ T: b,
+ TxtarString: files,
+ }
+
+ for b.Loop() {
+ b.StopTimer()
+ builder := hugolib.NewIntegrationTestBuilder(cfg).Init()
+ b.StartTimer()
+ builder.Build()
+ }
+}
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/himage"
"github.com/gohugoio/hugo/common/hugio"
"image/color"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
// maskFilter applies a mask image to a base image.
}
// Draw applies the mask to the base image.
-func (f maskFilter) Draw(dst draw.Image, baseImage image.Image, options *gift.Options) {
+func (f maskFilter) Draw(dst draw.Image, baseImage image.Image, options *gift.Options) error {
maskImage, err := f.mask.DecodeImage()
if err != nil {
- panic(fmt.Sprintf("failed to decode image: %s", err))
+ return fmt.Errorf("failed to decode image: %s", err)
}
// Ensure the mask is the same size as the base image
draw.DrawMask(outputImage, baseBounds, baseImage, image.Point{}, alphaMask, image.Point{}, draw.Over)
// Copy the result to the destination
- gift.New().Draw(dst, outputImage)
+ if err := gift.New().Draw(dst, outputImage); err != nil {
+ return fmt.Errorf("failed to draw masked image: %s", err)
+ }
+
+ return nil
}
// Bounds returns the bounds of the resulting image.
"image/color"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
var _ gift.Filter = (*opacityFilter)(nil)
opacity float32
}
-func (f opacityFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+func (f opacityFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
// 0 is fully transparent and 255 is opaque.
alpha := uint8(f.opacity * 255)
mask := image.NewUniform(color.Alpha{alpha})
draw.DrawMask(dst, dst.Bounds(), src, image.Point{}, mask, image.Point{}, draw.Over)
+ return nil
}
func (f opacityFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
"image"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
var _ gift.Filter = (*overlayFilter)(nil)
x, y int
}
-func (f overlayFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+func (f overlayFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
overlaySrc, err := f.src.DecodeImage()
if err != nil {
- panic(fmt.Sprintf("failed to decode image: %s", err))
+ return fmt.Errorf("failed to decode image: %s", err)
}
-
- gift.New().Draw(dst, src)
- gift.New().DrawAt(dst, overlaySrc, image.Pt(f.x, f.y), gift.OverOperator)
+ if err := gift.New().Draw(dst, src); err != nil {
+ return err
+ }
+ return gift.New().DrawAt(dst, overlaySrc, image.Pt(f.x, f.y), gift.OverOperator)
}
func (f overlayFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
"image/color"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
var _ gift.Filter = (*paddingFilter)(nil)
ccolor color.Color // canvas color
}
-func (f paddingFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+func (f paddingFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
w := src.Bounds().Dx() + f.left + f.right
h := src.Bounds().Dy() + f.top + f.bottom
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)
+ if err := gift.New().Draw(dst, i); err != nil {
+ return err
+ }
+ return gift.New().DrawAt(dst, src, image.Pt(f.left, f.top), gift.OverOperator)
}
func (f paddingFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
"image"
"image/draw"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
)
var _ ImageProcessSpecProvider = (*processFilter)(nil)
spec string
}
-func (f processFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+func (f processFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
panic("not supported")
}
"image"
"math"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/muesli/smartcrop"
)
"io"
"strings"
- "github.com/disintegration/gift"
+ "github.com/gohugoio/gift"
"github.com/gohugoio/hugo/common/hugio"
"golang.org/x/image/font"
fontSource hugio.ReadSeekCloserProvider
}
-func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) error {
// Load and parse font
ttf := goregular.TTF
if f.fontSource != nil {
d.DrawString(line)
y = y + fontHeight + f.linespacing
}
+ return nil
}
func (f textFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {