Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] master from gohugoio:master #1455

Merged
merged 2 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
imports.*
dist/
public/
.DS_Store
4 changes: 4 additions & 0 deletions docs/content/en/functions/images/Text.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ x
y
: (`int`) The vertical offset, in pixels, relative to the top of the image. Default is `10`.

alignx
{{< new-in 0.141.0 >}}
: (`string`) The horizontal alignment of the text relative to the `x` position. One of `left`, `center`, or `right`. Default is `left`.

[global resource]: /getting-started/glossary/#global-resource
[page resource]: /getting-started/glossary/#page-resource
[remote resource]: /getting-started/glossary/#remote-resource
Expand Down
5 changes: 5 additions & 0 deletions htesting/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ func IsCI() bool {
return (os.Getenv("CI") != "" || os.Getenv("CI_LOCAL") != "") && os.Getenv("CIRCLE_BRANCH") == ""
}

// IsRealCI reports whether we're running in a CI server, but not in a local CI setup.
func IsRealCI() bool {
return IsCI() && os.Getenv("CI_LOCAL") == ""
}

// IsGitHubAction reports whether we're running in a GitHub Action.
func IsGitHubAction() bool {
return os.Getenv("GITHUB_ACTION") != ""
Expand Down
6 changes: 6 additions & 0 deletions hugolib/integrationtest_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ func TestOptWithOSFs() TestOpt {
}
}

func TestOptWithPrintAndKeepTempDir(b bool) TestOpt {
return func(c *IntegrationTestConfig) {
c.PrintAndKeepTempDir = b
}
}

// TestOptWithWorkingDir allows setting any config optiona as a function al option.
func TestOptWithConfig(fn func(c *IntegrationTestConfig)) TestOpt {
return func(c *IntegrationTestConfig) {
Expand Down
324 changes: 0 additions & 324 deletions resources/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,20 @@ package resources_test
import (
"context"
"fmt"
"image"
"image/gif"
"io/fs"
"math/rand"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"testing"
"time"

"github.com/bep/imagemeta"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/resources/images/webp"

"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/paths"

"github.com/spf13/afero"

"github.com/disintegration/gift"

"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images"
"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -533,320 +523,6 @@ func BenchmarkImageExif(b *testing.B) {
})
}

// usesFMA indicates whether "fused multiply and add" (FMA) instruction is
// used. The command "grep FMADD go/test/codegen/floats.go" can help keep
// the FMA-using architecture list updated.
var usesFMA = runtime.GOARCH == "s390x" ||
runtime.GOARCH == "ppc64" ||
runtime.GOARCH == "ppc64le" ||
runtime.GOARCH == "arm64" ||
runtime.GOARCH == "riscv64"

// goldenEqual compares two NRGBA images. It is used in golden tests only.
// A small tolerance is allowed on architectures using "fused multiply and add"
// (FMA) instruction to accommodate for floating-point rounding differences
// with control golden images that were generated on amd64 architecture.
// See https://golang.org/ref/spec#Floating_point_operators
// and https://github.com/gohugoio/hugo/issues/6387 for more information.
//
// Borrowed from https://github.com/disintegration/gift/blob/a999ff8d5226e5ab14b64a94fca07c4ac3f357cf/gift_test.go#L598-L625
// Copyright (c) 2014-2019 Grigory Dryapak
// Licensed under the MIT License.
func goldenEqual(img1, img2 *image.NRGBA) bool {
maxDiff := 0
if usesFMA {
maxDiff = 1
}
if !img1.Rect.Eq(img2.Rect) {
return false
}
if len(img1.Pix) != len(img2.Pix) {
return false
}
for i := 0; i < len(img1.Pix); i++ {
diff := int(img1.Pix[i]) - int(img2.Pix[i])
if diff < 0 {
diff = -diff
}
if diff > maxDiff {
return false
}
}
return true
}

// Issue #8729
func TestImageOperationsGoldenWebp(t *testing.T) {
if !htesting.IsCI() {
t.Skip("skip long running test in local mode")
}
if !webp.Supports() {
t.Skip("skip webp test")
}
c := qt.New(t)
c.Parallel()

devMode := false

testImages := []string{"fuzzy-cirlcle.png"}

spec, workDir := newTestResourceOsFs(c)
defer func() {
if !devMode {
os.Remove(workDir)
}
}()

if devMode {
fmt.Println(workDir)
}

for _, imageName := range testImages {
image := fetchImageForSpec(spec, c, imageName)
imageWebp, err := image.Resize("200x webp")
c.Assert(err, qt.IsNil)
c.Assert(imageWebp.Width(), qt.Equals, 200)
}

if devMode {
return
}

dir1 := filepath.Join(workDir, "resources/_gen/images/a")
dir2 := filepath.FromSlash("testdata/golden_webp")

assetGoldenDirs(c, dir1, dir2)
}

func TestImageOperationsGolden(t *testing.T) {
if !htesting.IsCI() {
t.Skip("skip long running test in local mode")
}
c := qt.New(t)
c.Parallel()

// Note, if you're enabling this on a MacOS M1 (ARM) you need to run the test with GOARCH=amd64.
// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestImageOperationsGolden$" ./resources -v
// The above will print out a folder.
// Replace testdata/golden with resources/_gen/images in that folder.
devMode := false

testImages := []string{"sunset.jpg", "gohugoio8.png", "gohugoio24.png"}

spec, workDir := newTestResourceOsFs(c)
defer func() {
if !devMode {
os.Remove(workDir)
}
}()

if devMode {
fmt.Println(workDir)
}

gopher := fetchImageForSpec(spec, c, "gopher-hero8.png")
var err error
gopher, err = gopher.Resize("30x")
c.Assert(err, qt.IsNil)

f := &images.Filters{}

sunset := fetchImageForSpec(spec, c, "sunset.jpg")

// 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.Assert(rel, qt.Not(qt.Equals), "")

}

// Check the Opacity filter.
opacity30, err := orig.Filter(f.Opacity(30))
c.Assert(err, qt.IsNil)
overlay, err := sunset.Filter(f.Overlay(opacity30.(images.ImageSource), 20, 20))
c.Assert(err, qt.IsNil)
rel := overlay.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")

}

// A simple Gif file (no animation).
orig := fetchImageForSpec(spec, c, "gohugoio-card.gif")
for _, width := range []int{100, 220} {
resized, err := orig.Resize(fmt.Sprintf("%dx", width))
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
c.Assert(resized.Width(), qt.Equals, width)
}

// Animated GIF
orig = fetchImageForSpec(spec, c, "giphy.gif")
for _, resizeSpec := range []string{"200x", "512x", "100x jpg"} {
resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
}

for _, img := range testImages {

orig := fetchImageForSpec(spec, c, img)
for _, resizeSpec := range []string{"200x100", "600x", "200x r90 q50 Box"} {
resized, err := orig.Resize(resizeSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
}

for _, fillSpec := range []string{"300x200 Gaussian Smart", "100x100 Center", "300x100 TopLeft NearestNeighbor", "400x200 BottomLeft"} {
resized, err := orig.Fill(fillSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
}

for _, fitSpec := range []string{"300x200 Linear"} {
resized, err := orig.Fit(fitSpec)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
}

filters := []gift.Filter{
f.Grayscale(),
f.GaussianBlur(6),
f.Saturation(50),
f.Sepia(100),
f.Brightness(30),
f.ColorBalance(10, -10, -10),
f.Colorize(240, 50, 100),
f.Gamma(1.5),
f.UnsharpMask(1, 1, 0),
f.Sigmoid(0.5, 7),
f.Pixelate(5),
f.Invert(),
f.Hue(22),
f.Contrast(32.5),
f.Overlay(gopher.(images.ImageSource), 20, 30),
f.Text("No options"),
f.Text("This long text is to test line breaks. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."),
f.Text("Hugo rocks!", map[string]any{"x": 3, "y": 3, "size": 20, "color": "#fc03b1"}),
}

resized, err := orig.Fill("400x200 center")
c.Assert(err, qt.IsNil)

for _, filter := range filters {
resized, err := resized.Filter(filter)
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
}

resized, err = resized.Filter(filters[0:4])
c.Assert(err, qt.IsNil)
rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "")
}

if devMode {
return
}

dir1 := filepath.Join(workDir, "resources/_gen/images/a/")
dir2 := filepath.FromSlash("testdata/golden")

assetGoldenDirs(c, dir1, dir2)
}

func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
// The two dirs above should now be the same.
dirinfos1, err := os.ReadDir(dir1)
c.Assert(err, qt.IsNil)
dirinfos2, err := os.ReadDir(dir2)
c.Assert(err, qt.IsNil)
c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))

for i, fi1 := range dirinfos1 {
fi2 := dirinfos2[i]
c.Assert(fi1.Name(), qt.Equals, fi2.Name(), qt.Commentf("i=%d", i))

f1, err := os.Open(filepath.Join(dir1, fi1.Name()))
c.Assert(err, qt.IsNil)
f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
c.Assert(err, qt.IsNil)

decodeAll := func(f *os.File) []image.Image {
var images []image.Image

if strings.HasSuffix(f.Name(), ".gif") {
gif, err := gif.DecodeAll(f)
c.Assert(err, qt.IsNil)
images = make([]image.Image, len(gif.Image))
for i, img := range gif.Image {
images[i] = img
}
} else {
img, _, err := image.Decode(f)
c.Assert(err, qt.IsNil)
images = append(images, img)
}
return images
}

imgs1 := decodeAll(f1)
imgs2 := decodeAll(f2)
c.Assert(len(imgs1), qt.Equals, len(imgs2))

LOOP:
for i, img1 := range imgs1 {
img2 := imgs2[i]
nrgba1 := image.NewNRGBA(img1.Bounds())
gift.New().Draw(nrgba1, img1)
nrgba2 := image.NewNRGBA(img2.Bounds())
gift.New().Draw(nrgba2, img2)

if !goldenEqual(nrgba1, nrgba2) {
switch fi1.Name() {
case "giphy_hu13007323561585908901.gif",
"gohugoio8_hu12690451569630232821.png",
"gohugoio8_hu1619987041333606118.png",
"gohugoio8_hu18164141965527013334.png":
c.Log("expectedly differs from golden due to dithering:", fi1.Name())
default:
c.Errorf("resulting image differs from golden: %s", fi1.Name())
break LOOP
}
}
}

if !usesFMA {
c.Assert(fi1, eq, fi2)

_, err = f1.Seek(0, 0)
c.Assert(err, qt.IsNil)
_, err = f2.Seek(0, 0)
c.Assert(err, qt.IsNil)

hash1, _, err := hashing.XXHashFromReader(f1)
c.Assert(err, qt.IsNil)
hash2, _, err := hashing.XXHashFromReader(f2)
c.Assert(err, qt.IsNil)

c.Assert(hash1, qt.Equals, hash2)
}

f1.Close()
f2.Close()
}
}

func BenchmarkResizeParallel(b *testing.B) {
c := qt.New(b)
_, img := fetchSunset(c)
Expand Down
Loading
Loading