images: Rework the golden tests
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
|||
imports.*
|
||||
dist/
|
||||
public/
|
||||
.DS_Store
|
|
@ -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") != ""
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
@ -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)
|
||||
|
|
320
resources/images/images_golden_integration_test.go
Normal file
|
@ -0,0 +1,320 @@
|
|||
// Copyright 2024 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_test
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/gif"
|
||||
_ "image/jpeg"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/disintegration/gift"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var eq = qt.CmpEquals(
|
||||
cmp.Comparer(func(p1, p2 os.FileInfo) bool {
|
||||
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
|
||||
}),
|
||||
cmp.Comparer(func(d1, d2 fs.DirEntry) bool {
|
||||
p1, err1 := d1.Info()
|
||||
p2, err2 := d2.Info()
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
|
||||
}),
|
||||
)
|
||||
|
||||
var goldenOpts = struct {
|
||||
// Toggle this to write golden files to disk.
|
||||
// Note: Remember to set this to false before committing.
|
||||
writeGoldenFiles bool
|
||||
|
||||
// This will skip any assertions. Useful when adding new golden variants to a test.
|
||||
devMode bool
|
||||
}{
|
||||
writeGoldenFiles: false,
|
||||
devMode: false,
|
||||
}
|
||||
|
||||
// Note, if you're enabling writeGoldenFiles on a MacOS ARM 64 you need to run the test with GOARCH=amd64, e.g.
|
||||
// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestGolden" ./resources/images
|
||||
func TestGoldenFiltersMisc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if skipGolden {
|
||||
t.Skip("Skip golden test on this architecture")
|
||||
}
|
||||
|
||||
// Will be used to generate golden files.
|
||||
name := "filters_misc"
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/rotate270.jpg --
|
||||
sourcefilename: ../testdata/exif/orientation6.jpg
|
||||
-- assets/sunset.jpg --
|
||||
sourcefilename: ../testdata/sunset.jpg
|
||||
-- assets/gopher.png --
|
||||
sourcefilename: ../testdata/gopher-hero8.png
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
{{ $sunset := resources.Get "sunset.jpg" }}
|
||||
{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }}
|
||||
{{ $gopher := resources.Get "gopher.png" }}
|
||||
{{ $overlayFilter := images.Overlay $gopher 20 20 }}
|
||||
|
||||
{{ $textOpts := dict
|
||||
"color" "#fbfaf5"
|
||||
"linespacing" 8
|
||||
"size" 40
|
||||
"x" 25
|
||||
"y" 190
|
||||
}}
|
||||
|
||||
{{/* These are sorted. */}}
|
||||
{{ template "filters" (dict "name" "brightness-40.jpg" "img" $sunset "filters" (images.Brightness 40)) }}
|
||||
{{ template "filters" (dict "name" "contrast-50.jpg" "img" $sunset "filters" (images.Contrast 50)) }}
|
||||
{{ template "filters" (dict "name" "dither-default.jpg" "img" $sunset "filters" (images.Dither)) }}
|
||||
{{ template "filters" (dict "name" "gamma-1.667.jpg" "img" $sunset "filters" (images.Gamma 1.667)) }}
|
||||
{{ template "filters" (dict "name" "gaussianblur-5.jpg" "img" $sunset "filters" (images.GaussianBlur 5)) }}
|
||||
{{ template "filters" (dict "name" "grayscale.jpg" "img" $sunset "filters" (images.Grayscale)) }}
|
||||
{{ template "filters" (dict "name" "grayscale+colorize-180-50-20.jpg" "img" $sunset "filters" (slice images.Grayscale (images.Colorize 180 50 20))) }}
|
||||
{{ template "filters" (dict "name" "colorbalance-180-50-20.jpg" "img" $sunset "filters" (images.ColorBalance 180 50 20)) }}
|
||||
{{ template "filters" (dict "name" "hue--15.jpg" "img" $sunset "filters" (images.Hue -15)) }}
|
||||
{{ template "filters" (dict "name" "invert.jpg" "img" $sunset "filters" (images.Invert)) }}
|
||||
{{ template "filters" (dict "name" "opacity-0.65.jpg" "img" $sunset "filters" (images.Opacity 0.65)) }}
|
||||
{{ template "filters" (dict "name" "overlay-20-20.jpg" "img" $sunset "filters" ($overlayFilter)) }}
|
||||
{{ template "filters" (dict "name" "padding-20-40-#976941.jpg" "img" $sunset "filters" (images.Padding 20 40 "#976941" )) }}
|
||||
{{ template "filters" (dict "name" "pixelate-10.jpg" "img" $sunset "filters" (images.Pixelate 10)) }}
|
||||
{{ template "filters" (dict "name" "rotate270.jpg" "img" (resources.Get "rotate270.jpg") "filters" images.AutoOrient) }}
|
||||
{{ template "filters" (dict "name" "saturation-65.jpg" "img" $sunset "filters" (images.Saturation 65)) }}
|
||||
{{ template "filters" (dict "name" "sepia-80.jpg" "img" $sunsetGrayscale "filters" (images.Sepia 80)) }}
|
||||
{{ template "filters" (dict "name" "sigmoid-0.6--4.jpg" "img" $sunset "filters" (images.Sigmoid 0.6 -4 )) }}
|
||||
{{ template "filters" (dict "name" "text.jpg" "img" $sunset "filters" (images.Text "Hugo Rocks!" $textOpts )) }}
|
||||
{{ template "filters" (dict "name" "unsharpmask.jpg" "img" $sunset "filters" (images.UnsharpMask 10 0.4 0.03)) }}
|
||||
|
||||
|
||||
{{ define "filters"}}
|
||||
{{ if lt (len (path.Ext .name)) 4 }}
|
||||
{{ errorf "No extension in %q" .name }}
|
||||
{{ end }}
|
||||
{{ $img := .img.Filter .filters }}
|
||||
{{ $name := printf "images/%s" .name }}
|
||||
{{ with $img | resources.Copy $name }}
|
||||
{{ .Publish }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
runGolden(t, name, files)
|
||||
}
|
||||
|
||||
func TestGoldenProcessMisc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if skipGolden {
|
||||
t.Skip("Skip golden test on this architecture")
|
||||
}
|
||||
|
||||
// Will be used to generate golden files.
|
||||
name := "process_misc"
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/giphy.gif --
|
||||
sourcefilename: ../testdata/giphy.gif
|
||||
-- assets/sunset.jpg --
|
||||
sourcefilename: ../testdata/sunset.jpg
|
||||
-- assets/gopher.png --
|
||||
sourcefilename: ../testdata/gopher-hero8.png
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
{{ $sunset := resources.Get "sunset.jpg" }}
|
||||
{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }}
|
||||
{{ $gopher := resources.Get "gopher.png" }}
|
||||
{{ $giphy := resources.Get "giphy.gif" }}
|
||||
|
||||
|
||||
{{/* These are sorted. The end file name will be created from the spec + extension, so make sure these are unique. */}}
|
||||
{{ template "process" (dict "spec" "crop 500x200 smart" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "fill 500x200 smart" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "fit 500x200 smart" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 100x100 gif" "img" $giphy) }}
|
||||
{{ template "process" (dict "spec" "resize 100x100 r180" "img" $gopher) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 jpg #b31280" "img" $gopher) }}
|
||||
|
||||
{{ define "process"}}
|
||||
{{ $img := .img.Process .spec }}
|
||||
{{ $ext := path.Ext $img.RelPermalink }}
|
||||
{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }}
|
||||
{{ with $img | resources.Copy $name }}
|
||||
{{ .Publish }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
runGolden(t, name, files)
|
||||
}
|
||||
|
||||
func runGolden(t testing.TB, name, files string) *hugolib.IntegrationTestBuilder {
|
||||
t.Helper()
|
||||
|
||||
c := hugolib.Test(t, files, hugolib.TestOptWithOSFs()) // hugolib.TestOptWithPrintAndKeepTempDir(true))
|
||||
c.AssertFileContent("public/index.html", "Home.")
|
||||
|
||||
outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images")
|
||||
goldenBaseDir := filepath.Join("testdata", "images_golden")
|
||||
goldenDir := filepath.Join(goldenBaseDir, name)
|
||||
if goldenOpts.writeGoldenFiles {
|
||||
c.Assert(htesting.IsRealCI(), qt.IsFalse)
|
||||
c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
|
||||
c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
|
||||
c.Assert(hugio.CopyDir(hugofs.Os, outputDir, goldenDir, nil), qt.IsNil)
|
||||
return c
|
||||
}
|
||||
|
||||
if goldenOpts.devMode {
|
||||
c.Assert(htesting.IsRealCI(), qt.IsFalse)
|
||||
return c
|
||||
}
|
||||
|
||||
decodeAll := func(f *os.File) []image.Image {
|
||||
c.Helper()
|
||||
|
||||
var images []image.Image
|
||||
|
||||
if strings.HasSuffix(f.Name(), ".gif") {
|
||||
gif, err := gif.DecodeAll(f)
|
||||
c.Assert(err, qt.IsNil, qt.Commentf(f.Name()))
|
||||
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, qt.Commentf(f.Name()))
|
||||
images = append(images, img)
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
entries1, err := os.ReadDir(outputDir)
|
||||
c.Assert(err, qt.IsNil)
|
||||
entries2, err := os.ReadDir(goldenDir)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(len(entries1), qt.Equals, len(entries2))
|
||||
for i, e1 := range entries1 {
|
||||
c.Assert(filepath.Ext(e1.Name()), qt.Not(qt.Equals), "")
|
||||
func() {
|
||||
e2 := entries2[i]
|
||||
|
||||
f1, err := os.Open(filepath.Join(outputDir, e1.Name()))
|
||||
c.Assert(err, qt.IsNil)
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := os.Open(filepath.Join(goldenDir, e2.Name()))
|
||||
c.Assert(err, qt.IsNil)
|
||||
defer f2.Close()
|
||||
|
||||
imgs2 := decodeAll(f2)
|
||||
imgs1 := decodeAll(f1)
|
||||
c.Assert(len(imgs1), qt.Equals, len(imgs2))
|
||||
|
||||
if !usesFMA {
|
||||
c.Assert(e1, eq, e2)
|
||||
_, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
c.Assert(goldenEqual(nrgba1, nrgba2), qt.Equals, true, qt.Commentf(e1.Name()))
|
||||
}
|
||||
}()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Based on 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 runtime.GOARCH != "amd64" {
|
||||
// The golden files are created using the AMD64 architecture.
|
||||
// Be lenient on other platforms due to floaging point and dithering differences.
|
||||
maxDiff = 15
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// We don't have a CI test environment for these, and there are known dithering issues that makes these time consuming to maintain.
|
||||
var skipGolden = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "s390x"
|
||||
|
||||
// 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"
|
BIN
resources/images/testdata/images_golden/filters_misc/brightness-40.jpg
vendored
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/colorbalance-180-50-20.jpg
vendored
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/contrast-50.jpg
vendored
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/dither-default.jpg
vendored
Normal file
After Width: | Height: | Size: 206 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/gamma-1.667.jpg
vendored
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/gaussianblur-5.jpg
vendored
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/grayscale+colorize-180-50-20.jpg
vendored
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/grayscale.jpg
vendored
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/hue--15.jpg
vendored
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/invert.jpg
vendored
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/opacity-0.65.jpg
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/overlay-20-20.jpg
vendored
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/padding-20-40-#976941.jpg
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/pixelate-10.jpg
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/rotate270.jpg
vendored
Normal file
After Width: | Height: | Size: 688 B |
BIN
resources/images/testdata/images_golden/filters_misc/saturation-65.jpg
vendored
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/sepia-80.jpg
vendored
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/sigmoid-0.6--4.jpg
vendored
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/text.jpg
vendored
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
resources/images/testdata/images_golden/filters_misc/unsharpmask.jpg
vendored
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
resources/images/testdata/images_golden/process_misc/crop-500x200-smart.jpg
vendored
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
resources/images/testdata/images_golden/process_misc/fill-500x200-smart.jpg
vendored
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
resources/images/testdata/images_golden/process_misc/fit-500x200-smart.jpg
vendored
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
resources/images/testdata/images_golden/process_misc/resize-100x100-gif.gif
vendored
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
resources/images/testdata/images_golden/process_misc/resize-100x100-r180.png
vendored
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
resources/images/testdata/images_golden/process_misc/resize-300x300-jpg-b31280.jpg
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 304 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.1 KiB |