hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

image_test.go (24094B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package resources
   15 
   16 import (
   17 	"fmt"
   18 	"image"
   19 	"image/gif"
   20 	"io/ioutil"
   21 	"math/big"
   22 	"math/rand"
   23 	"os"
   24 	"path"
   25 	"path/filepath"
   26 	"runtime"
   27 	"strconv"
   28 	"strings"
   29 	"sync"
   30 	"testing"
   31 	"time"
   32 
   33 	"github.com/gohugoio/hugo/resources/images/webp"
   34 
   35 	"github.com/gohugoio/hugo/common/paths"
   36 
   37 	"github.com/spf13/afero"
   38 
   39 	"github.com/disintegration/gift"
   40 
   41 	"github.com/gohugoio/hugo/helpers"
   42 
   43 	"github.com/gohugoio/hugo/media"
   44 	"github.com/gohugoio/hugo/resources/images"
   45 	"github.com/google/go-cmp/cmp"
   46 
   47 	"github.com/gohugoio/hugo/htesting/hqt"
   48 
   49 	qt "github.com/frankban/quicktest"
   50 )
   51 
   52 var eq = qt.CmpEquals(
   53 	cmp.Comparer(func(p1, p2 *resourceAdapter) bool {
   54 		return p1.resourceAdapterInner == p2.resourceAdapterInner
   55 	}),
   56 	cmp.Comparer(func(p1, p2 os.FileInfo) bool {
   57 		return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
   58 	}),
   59 	cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }),
   60 	cmp.Comparer(func(m1, m2 media.Type) bool {
   61 		return m1.Type() == m2.Type()
   62 	}),
   63 	cmp.Comparer(
   64 		func(v1, v2 *big.Rat) bool {
   65 			return v1.RatString() == v2.RatString()
   66 		},
   67 	),
   68 	cmp.Comparer(func(v1, v2 time.Time) bool {
   69 		return v1.Unix() == v2.Unix()
   70 	}),
   71 )
   72 
   73 func TestImageTransformBasic(t *testing.T) {
   74 	c := qt.New(t)
   75 
   76 	image := fetchSunset(c)
   77 
   78 	fileCache := image.(specProvider).getSpec().FileCaches.ImageCache().Fs
   79 
   80 	assertWidthHeight := func(img images.ImageResource, w, h int) {
   81 		c.Helper()
   82 		c.Assert(img, qt.Not(qt.IsNil))
   83 		c.Assert(img.Width(), qt.Equals, w)
   84 		c.Assert(img.Height(), qt.Equals, h)
   85 	}
   86 
   87 	c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
   88 	c.Assert(image.ResourceType(), qt.Equals, "image")
   89 	assertWidthHeight(image, 900, 562)
   90 
   91 	resized, err := image.Resize("300x200")
   92 	c.Assert(err, qt.IsNil)
   93 	c.Assert(image != resized, qt.Equals, true)
   94 	c.Assert(image, qt.Not(eq), resized)
   95 	assertWidthHeight(resized, 300, 200)
   96 	assertWidthHeight(image, 900, 562)
   97 
   98 	resized0x, err := image.Resize("x200")
   99 	c.Assert(err, qt.IsNil)
  100 	assertWidthHeight(resized0x, 320, 200)
  101 	assertFileCache(c, fileCache, path.Base(resized0x.RelPermalink()), 320, 200)
  102 
  103 	resizedx0, err := image.Resize("200x")
  104 	c.Assert(err, qt.IsNil)
  105 	assertWidthHeight(resizedx0, 200, 125)
  106 	assertFileCache(c, fileCache, path.Base(resizedx0.RelPermalink()), 200, 125)
  107 
  108 	resizedAndRotated, err := image.Resize("x200 r90")
  109 	c.Assert(err, qt.IsNil)
  110 	assertWidthHeight(resizedAndRotated, 125, 200)
  111 
  112 	assertWidthHeight(resized, 300, 200)
  113 	c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_resize_q68_linear.jpg")
  114 
  115 	fitted, err := resized.Fit("50x50")
  116 	c.Assert(err, qt.IsNil)
  117 	c.Assert(fitted.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_625708021e2bb281c9f1002f88e4753f.jpg")
  118 	assertWidthHeight(fitted, 50, 33)
  119 
  120 	// Check the MD5 key threshold
  121 	fittedAgain, _ := fitted.Fit("10x20")
  122 	fittedAgain, err = fittedAgain.Fit("10x20")
  123 	c.Assert(err, qt.IsNil)
  124 	c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f65ba24dc2b7fba0f56d7f104519157.jpg")
  125 	assertWidthHeight(fittedAgain, 10, 7)
  126 
  127 	filled, err := image.Fill("200x100 bottomLeft")
  128 	c.Assert(err, qt.IsNil)
  129 	c.Assert(filled.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_bottomleft.jpg")
  130 	assertWidthHeight(filled, 200, 100)
  131 
  132 	smart, err := image.Fill("200x100 smart")
  133 	c.Assert(err, qt.IsNil)
  134 	c.Assert(smart.RelPermalink(), qt.Equals, fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_smart%d.jpg", 1))
  135 	assertWidthHeight(smart, 200, 100)
  136 
  137 	// Check cache
  138 	filledAgain, err := image.Fill("200x100 bottomLeft")
  139 	c.Assert(err, qt.IsNil)
  140 	c.Assert(filled, eq, filledAgain)
  141 
  142 	cropped, err := image.Crop("300x300 topRight")
  143 	c.Assert(err, qt.IsNil)
  144 	c.Assert(cropped.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x300_crop_q68_linear_topright.jpg")
  145 	assertWidthHeight(cropped, 300, 300)
  146 
  147 	smartcropped, err := image.Crop("200x200 smart")
  148 	c.Assert(err, qt.IsNil)
  149 	c.Assert(smartcropped.RelPermalink(), qt.Equals, fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_crop_q68_linear_smart%d.jpg", 1))
  150 	assertWidthHeight(smartcropped, 200, 200)
  151 
  152 	// Check cache
  153 	croppedAgain, err := image.Crop("300x300 topRight")
  154 	c.Assert(err, qt.IsNil)
  155 	c.Assert(cropped, eq, croppedAgain)
  156 
  157 }
  158 
  159 func TestImageTransformFormat(t *testing.T) {
  160 	c := qt.New(t)
  161 
  162 	image := fetchSunset(c)
  163 
  164 	fileCache := image.(specProvider).getSpec().FileCaches.ImageCache().Fs
  165 
  166 	assertExtWidthHeight := func(img images.ImageResource, ext string, w, h int) {
  167 		c.Helper()
  168 		c.Assert(img, qt.Not(qt.IsNil))
  169 		c.Assert(paths.Ext(img.RelPermalink()), qt.Equals, ext)
  170 		c.Assert(img.Width(), qt.Equals, w)
  171 		c.Assert(img.Height(), qt.Equals, h)
  172 	}
  173 
  174 	c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
  175 	c.Assert(image.ResourceType(), qt.Equals, "image")
  176 	assertExtWidthHeight(image, ".jpg", 900, 562)
  177 
  178 	imagePng, err := image.Resize("450x png")
  179 	c.Assert(err, qt.IsNil)
  180 	c.Assert(imagePng.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_450x0_resize_linear.png")
  181 	c.Assert(imagePng.ResourceType(), qt.Equals, "image")
  182 	assertExtWidthHeight(imagePng, ".png", 450, 281)
  183 	c.Assert(imagePng.Name(), qt.Equals, "sunset.jpg")
  184 	c.Assert(imagePng.MediaType().String(), qt.Equals, "image/png")
  185 
  186 	assertFileCache(c, fileCache, path.Base(imagePng.RelPermalink()), 450, 281)
  187 
  188 	imageGif, err := image.Resize("225x gif")
  189 	c.Assert(err, qt.IsNil)
  190 	c.Assert(imageGif.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_225x0_resize_linear.gif")
  191 	c.Assert(imageGif.ResourceType(), qt.Equals, "image")
  192 	assertExtWidthHeight(imageGif, ".gif", 225, 141)
  193 	c.Assert(imageGif.Name(), qt.Equals, "sunset.jpg")
  194 	c.Assert(imageGif.MediaType().String(), qt.Equals, "image/gif")
  195 
  196 	assertFileCache(c, fileCache, path.Base(imageGif.RelPermalink()), 225, 141)
  197 }
  198 
  199 // https://github.com/gohugoio/hugo/issues/5730
  200 func TestImagePermalinkPublishOrder(t *testing.T) {
  201 	for _, checkOriginalFirst := range []bool{true, false} {
  202 		name := "OriginalFirst"
  203 		if !checkOriginalFirst {
  204 			name = "ResizedFirst"
  205 		}
  206 
  207 		t.Run(name, func(t *testing.T) {
  208 			c := qt.New(t)
  209 			spec, workDir := newTestResourceOsFs(c)
  210 			defer func() {
  211 				os.Remove(workDir)
  212 			}()
  213 
  214 			check1 := func(img images.ImageResource) {
  215 				resizedLink := "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x50_resize_q75_box.jpg"
  216 				c.Assert(img.RelPermalink(), qt.Equals, resizedLink)
  217 				assertImageFile(c, spec.PublishFs, resizedLink, 100, 50)
  218 			}
  219 
  220 			check2 := func(img images.ImageResource) {
  221 				c.Assert(img.RelPermalink(), qt.Equals, "/a/sunset.jpg")
  222 				assertImageFile(c, spec.PublishFs, "a/sunset.jpg", 900, 562)
  223 			}
  224 
  225 			orignal := fetchImageForSpec(spec, c, "sunset.jpg")
  226 			c.Assert(orignal, qt.Not(qt.IsNil))
  227 
  228 			if checkOriginalFirst {
  229 				check2(orignal)
  230 			}
  231 
  232 			resized, err := orignal.Resize("100x50")
  233 			c.Assert(err, qt.IsNil)
  234 
  235 			check1(resized.(images.ImageResource))
  236 
  237 			if !checkOriginalFirst {
  238 				check2(orignal)
  239 			}
  240 		})
  241 	}
  242 }
  243 
  244 func TestImageBugs(t *testing.T) {
  245 	c := qt.New(t)
  246 
  247 	// Issue #4261
  248 	c.Run("Transform long filename", func(c *qt.C) {
  249 		image := fetchImage(c, "1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg")
  250 		c.Assert(image, qt.Not(qt.IsNil))
  251 
  252 		resized, err := image.Resize("200x")
  253 		c.Assert(err, qt.IsNil)
  254 		c.Assert(resized, qt.Not(qt.IsNil))
  255 		c.Assert(resized.Width(), qt.Equals, 200)
  256 		c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_65b757a6e14debeae720fe8831f0a9bc.jpg")
  257 		resized, err = resized.Resize("100x")
  258 		c.Assert(err, qt.IsNil)
  259 		c.Assert(resized, qt.Not(qt.IsNil))
  260 		c.Assert(resized.Width(), qt.Equals, 100)
  261 		c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg")
  262 
  263 	})
  264 
  265 	// Issue #6137
  266 	c.Run("Transform upper case extension", func(c *qt.C) {
  267 		image := fetchImage(c, "sunrise.JPG")
  268 
  269 		resized, err := image.Resize("200x")
  270 		c.Assert(err, qt.IsNil)
  271 		c.Assert(resized, qt.Not(qt.IsNil))
  272 		c.Assert(resized.Width(), qt.Equals, 200)
  273 
  274 	})
  275 
  276 	// Issue #7955
  277 	c.Run("Fill with smartcrop", func(c *qt.C) {
  278 		sunset := fetchImage(c, "sunset.jpg")
  279 
  280 		for _, test := range []struct {
  281 			originalDimensions string
  282 			targetWH           int
  283 		}{
  284 			{"408x403", 400},
  285 			{"425x403", 400},
  286 			{"459x429", 400},
  287 			{"476x442", 400},
  288 			{"544x403", 400},
  289 			{"476x468", 400},
  290 			{"578x585", 550},
  291 			{"578x598", 550},
  292 		} {
  293 			c.Run(test.originalDimensions, func(c *qt.C) {
  294 				image, err := sunset.Resize(test.originalDimensions)
  295 				c.Assert(err, qt.IsNil)
  296 				resized, err := image.Fill(fmt.Sprintf("%dx%d smart", test.targetWH, test.targetWH))
  297 				c.Assert(err, qt.IsNil)
  298 				c.Assert(resized, qt.Not(qt.IsNil))
  299 				c.Assert(resized.Width(), qt.Equals, test.targetWH)
  300 				c.Assert(resized.Height(), qt.Equals, test.targetWH)
  301 			})
  302 
  303 		}
  304 
  305 	})
  306 }
  307 
  308 func TestImageTransformConcurrent(t *testing.T) {
  309 	var wg sync.WaitGroup
  310 
  311 	c := qt.New(t)
  312 
  313 	spec, workDir := newTestResourceOsFs(c)
  314 	defer func() {
  315 		os.Remove(workDir)
  316 	}()
  317 
  318 	image := fetchImageForSpec(spec, c, "sunset.jpg")
  319 
  320 	for i := 0; i < 4; i++ {
  321 		wg.Add(1)
  322 		go func(id int) {
  323 			defer wg.Done()
  324 			for j := 0; j < 5; j++ {
  325 				img := image
  326 				for k := 0; k < 2; k++ {
  327 					r1, err := img.Resize(fmt.Sprintf("%dx", id-k))
  328 					if err != nil {
  329 						t.Error(err)
  330 					}
  331 
  332 					if r1.Width() != id-k {
  333 						t.Errorf("Width: %d:%d", r1.Width(), j)
  334 					}
  335 
  336 					r2, err := r1.Resize(fmt.Sprintf("%dx", id-k-1))
  337 					if err != nil {
  338 						t.Error(err)
  339 					}
  340 
  341 					img = r2
  342 				}
  343 			}
  344 		}(i + 20)
  345 	}
  346 
  347 	wg.Wait()
  348 }
  349 
  350 func TestImageWithMetadata(t *testing.T) {
  351 	c := qt.New(t)
  352 
  353 	image := fetchSunset(c)
  354 
  355 	meta := []map[string]any{
  356 		{
  357 			"title": "My Sunset",
  358 			"name":  "Sunset #:counter",
  359 			"src":   "*.jpg",
  360 		},
  361 	}
  362 
  363 	c.Assert(AssignMetadata(meta, image), qt.IsNil)
  364 	c.Assert(image.Name(), qt.Equals, "Sunset #1")
  365 
  366 	resized, err := image.Resize("200x")
  367 	c.Assert(err, qt.IsNil)
  368 	c.Assert(resized.Name(), qt.Equals, "Sunset #1")
  369 }
  370 
  371 func TestImageResize8BitPNG(t *testing.T) {
  372 	c := qt.New(t)
  373 
  374 	image := fetchImage(c, "gohugoio.png")
  375 
  376 	c.Assert(image.MediaType().Type(), qt.Equals, "image/png")
  377 	c.Assert(image.RelPermalink(), qt.Equals, "/a/gohugoio.png")
  378 	c.Assert(image.ResourceType(), qt.Equals, "image")
  379 	c.Assert(image.Exif(), qt.IsNil)
  380 
  381 	resized, err := image.Resize("800x")
  382 	c.Assert(err, qt.IsNil)
  383 	c.Assert(resized.MediaType().Type(), qt.Equals, "image/png")
  384 	c.Assert(resized.RelPermalink(), qt.Equals, "/a/gohugoio_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_800x0_resize_linear_3.png")
  385 	c.Assert(resized.Width(), qt.Equals, 800)
  386 }
  387 
  388 func TestImageResizeInSubPath(t *testing.T) {
  389 	c := qt.New(t)
  390 
  391 	image := fetchImage(c, "sub/gohugoio2.png")
  392 
  393 	c.Assert(image.MediaType(), eq, media.PNGType)
  394 	c.Assert(image.RelPermalink(), qt.Equals, "/a/sub/gohugoio2.png")
  395 	c.Assert(image.ResourceType(), qt.Equals, "image")
  396 	c.Assert(image.Exif(), qt.IsNil)
  397 
  398 	resized, err := image.Resize("101x101")
  399 	c.Assert(err, qt.IsNil)
  400 	c.Assert(resized.MediaType().Type(), qt.Equals, "image/png")
  401 	c.Assert(resized.RelPermalink(), qt.Equals, "/a/sub/gohugoio2_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_101x101_resize_linear_3.png")
  402 	c.Assert(resized.Width(), qt.Equals, 101)
  403 	c.Assert(resized.Exif(), qt.IsNil)
  404 
  405 	publishedImageFilename := filepath.Clean(resized.RelPermalink())
  406 
  407 	spec := image.(specProvider).getSpec()
  408 
  409 	assertImageFile(c, spec.BaseFs.PublishFs, publishedImageFilename, 101, 101)
  410 	c.Assert(spec.BaseFs.PublishFs.Remove(publishedImageFilename), qt.IsNil)
  411 
  412 	// Clear mem cache to simulate reading from the file cache.
  413 	spec.imageCache.clear()
  414 
  415 	resizedAgain, err := image.Resize("101x101")
  416 	c.Assert(err, qt.IsNil)
  417 	c.Assert(resizedAgain.RelPermalink(), qt.Equals, "/a/sub/gohugoio2_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_101x101_resize_linear_3.png")
  418 	c.Assert(resizedAgain.Width(), qt.Equals, 101)
  419 	assertImageFile(c, image.(specProvider).getSpec().BaseFs.PublishFs, publishedImageFilename, 101, 101)
  420 }
  421 
  422 func TestSVGImage(t *testing.T) {
  423 	c := qt.New(t)
  424 	spec := newTestResourceSpec(specDescriptor{c: c})
  425 	svg := fetchResourceForSpec(spec, c, "circle.svg")
  426 	c.Assert(svg, qt.Not(qt.IsNil))
  427 }
  428 
  429 func TestSVGImageContent(t *testing.T) {
  430 	c := qt.New(t)
  431 	spec := newTestResourceSpec(specDescriptor{c: c})
  432 	svg := fetchResourceForSpec(spec, c, "circle.svg")
  433 	c.Assert(svg, qt.Not(qt.IsNil))
  434 
  435 	content, err := svg.Content()
  436 	c.Assert(err, qt.IsNil)
  437 	c.Assert(content, hqt.IsSameType, "")
  438 	c.Assert(content.(string), qt.Contains, `<svg height="100" width="100">`)
  439 }
  440 
  441 func TestImageExif(t *testing.T) {
  442 	c := qt.New(t)
  443 	fs := afero.NewMemMapFs()
  444 	spec := newTestResourceSpec(specDescriptor{fs: fs, c: c})
  445 	image := fetchResourceForSpec(spec, c, "sunset.jpg").(images.ImageResource)
  446 
  447 	getAndCheckExif := func(c *qt.C, image images.ImageResource) {
  448 		x := image.Exif()
  449 		c.Assert(x, qt.Not(qt.IsNil))
  450 
  451 		c.Assert(x.Date.Format("2006-01-02"), qt.Equals, "2017-10-27")
  452 
  453 		// Malaga: https://goo.gl/taazZy
  454 		c.Assert(x.Lat, qt.Equals, float64(36.59744166666667))
  455 		c.Assert(x.Long, qt.Equals, float64(-4.50846))
  456 
  457 		v, found := x.Tags["LensModel"]
  458 		c.Assert(found, qt.Equals, true)
  459 		lensModel, ok := v.(string)
  460 		c.Assert(ok, qt.Equals, true)
  461 		c.Assert(lensModel, qt.Equals, "smc PENTAX-DA* 16-50mm F2.8 ED AL [IF] SDM")
  462 		resized, _ := image.Resize("300x200")
  463 		x2 := resized.Exif()
  464 		c.Assert(x2, eq, x)
  465 	}
  466 
  467 	getAndCheckExif(c, image)
  468 	image = fetchResourceForSpec(spec, c, "sunset.jpg").(images.ImageResource)
  469 	// This will read from file cache.
  470 	getAndCheckExif(c, image)
  471 }
  472 
  473 func BenchmarkImageExif(b *testing.B) {
  474 	getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []images.ImageResource {
  475 		spec := newTestResourceSpec(specDescriptor{fs: fs, c: c})
  476 		imgs := make([]images.ImageResource, b.N)
  477 		for i := 0; i < b.N; i++ {
  478 			imgs[i] = fetchResourceForSpec(spec, c, "sunset.jpg", strconv.Itoa(i)).(images.ImageResource)
  479 		}
  480 		return imgs
  481 	}
  482 
  483 	getAndCheckExif := func(c *qt.C, image images.ImageResource) {
  484 		x := image.Exif()
  485 		c.Assert(x, qt.Not(qt.IsNil))
  486 		c.Assert(x.Long, qt.Equals, float64(-4.50846))
  487 	}
  488 
  489 	b.Run("Cold cache", func(b *testing.B) {
  490 		b.StopTimer()
  491 		c := qt.New(b)
  492 		images := getImages(c, b, afero.NewMemMapFs())
  493 
  494 		b.StartTimer()
  495 		for i := 0; i < b.N; i++ {
  496 			getAndCheckExif(c, images[i])
  497 		}
  498 	})
  499 
  500 	b.Run("Cold cache, 10", func(b *testing.B) {
  501 		b.StopTimer()
  502 		c := qt.New(b)
  503 		images := getImages(c, b, afero.NewMemMapFs())
  504 
  505 		b.StartTimer()
  506 		for i := 0; i < b.N; i++ {
  507 			for j := 0; j < 10; j++ {
  508 				getAndCheckExif(c, images[i])
  509 			}
  510 		}
  511 	})
  512 
  513 	b.Run("Warm cache", func(b *testing.B) {
  514 		b.StopTimer()
  515 		c := qt.New(b)
  516 		fs := afero.NewMemMapFs()
  517 		images := getImages(c, b, fs)
  518 		for i := 0; i < b.N; i++ {
  519 			getAndCheckExif(c, images[i])
  520 		}
  521 
  522 		images = getImages(c, b, fs)
  523 
  524 		b.StartTimer()
  525 		for i := 0; i < b.N; i++ {
  526 			getAndCheckExif(c, images[i])
  527 		}
  528 	})
  529 }
  530 
  531 // usesFMA indicates whether "fused multiply and add" (FMA) instruction is
  532 // used.  The command "grep FMADD go/test/codegen/floats.go" can help keep
  533 // the FMA-using architecture list updated.
  534 var usesFMA = runtime.GOARCH == "s390x" ||
  535 	runtime.GOARCH == "ppc64" ||
  536 	runtime.GOARCH == "ppc64le" ||
  537 	runtime.GOARCH == "arm64"
  538 
  539 // goldenEqual compares two NRGBA images.  It is used in golden tests only.
  540 // A small tolerance is allowed on architectures using "fused multiply and add"
  541 // (FMA) instruction to accommodate for floating-point rounding differences
  542 // with control golden images that were generated on amd64 architecture.
  543 // See https://golang.org/ref/spec#Floating_point_operators
  544 // and https://github.com/gohugoio/hugo/issues/6387 for more information.
  545 //
  546 // Borrowed from https://github.com/disintegration/gift/blob/a999ff8d5226e5ab14b64a94fca07c4ac3f357cf/gift_test.go#L598-L625
  547 // Copyright (c) 2014-2019 Grigory Dryapak
  548 // Licensed under the MIT License.
  549 func goldenEqual(img1, img2 *image.NRGBA) bool {
  550 	maxDiff := 0
  551 	if usesFMA {
  552 		maxDiff = 1
  553 	}
  554 	if !img1.Rect.Eq(img2.Rect) {
  555 		return false
  556 	}
  557 	if len(img1.Pix) != len(img2.Pix) {
  558 		return false
  559 	}
  560 	for i := 0; i < len(img1.Pix); i++ {
  561 		diff := int(img1.Pix[i]) - int(img2.Pix[i])
  562 		if diff < 0 {
  563 			diff = -diff
  564 		}
  565 		if diff > maxDiff {
  566 			return false
  567 		}
  568 	}
  569 	return true
  570 }
  571 
  572 // Issue #8729
  573 func TestImageOperationsGoldenWebp(t *testing.T) {
  574 	if !webp.Supports() {
  575 		t.Skip("skip webp test")
  576 	}
  577 	c := qt.New(t)
  578 	c.Parallel()
  579 
  580 	devMode := false
  581 
  582 	testImages := []string{"fuzzy-cirlcle.png"}
  583 
  584 	spec, workDir := newTestResourceOsFs(c)
  585 	defer func() {
  586 		if !devMode {
  587 			os.Remove(workDir)
  588 		}
  589 	}()
  590 
  591 	if devMode {
  592 		fmt.Println(workDir)
  593 	}
  594 
  595 	for _, imageName := range testImages {
  596 		image := fetchImageForSpec(spec, c, imageName)
  597 		imageWebp, err := image.Resize("200x webp")
  598 		c.Assert(err, qt.IsNil)
  599 		c.Assert(imageWebp.Width(), qt.Equals, 200)
  600 	}
  601 
  602 	if devMode {
  603 		return
  604 	}
  605 
  606 	dir1 := filepath.Join(workDir, "resources/_gen/images")
  607 	dir2 := filepath.FromSlash("testdata/golden_webp")
  608 
  609 	assetGoldenDirs(c, dir1, dir2)
  610 
  611 }
  612 
  613 func TestImageOperationsGolden(t *testing.T) {
  614 	c := qt.New(t)
  615 	c.Parallel()
  616 
  617 	// Note, if you're enabling this on a MacOS M1 (ARM) you need to run the test with GOARCH=amd64.
  618 	// GOARCH=amd64 go test -timeout 30s -run "^TestImageOperationsGolden$" ./resources -v
  619 	devMode := false
  620 
  621 	testImages := []string{"sunset.jpg", "gohugoio8.png", "gohugoio24.png"}
  622 
  623 	spec, workDir := newTestResourceOsFs(c)
  624 	defer func() {
  625 		if !devMode {
  626 			os.Remove(workDir)
  627 		}
  628 	}()
  629 
  630 	if devMode {
  631 		fmt.Println(workDir)
  632 	}
  633 
  634 	gopher := fetchImageForSpec(spec, c, "gopher-hero8.png")
  635 	var err error
  636 	gopher, err = gopher.Resize("30x")
  637 	c.Assert(err, qt.IsNil)
  638 
  639 	// Test PNGs with alpha channel.
  640 	for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
  641 		orig := fetchImageForSpec(spec, c, img)
  642 		for _, resizeSpec := range []string{"200x #e3e615", "200x jpg #e3e615"} {
  643 			resized, err := orig.Resize(resizeSpec)
  644 			c.Assert(err, qt.IsNil)
  645 			rel := resized.RelPermalink()
  646 
  647 			c.Assert(rel, qt.Not(qt.Equals), "")
  648 		}
  649 	}
  650 
  651 	// A simple Gif file (no animation).
  652 	orig := fetchImageForSpec(spec, c, "gohugoio-card.gif")
  653 	for _, resizeSpec := range []string{"100x", "220x"} {
  654 		resized, err := orig.Resize(resizeSpec)
  655 		c.Assert(err, qt.IsNil)
  656 		rel := resized.RelPermalink()
  657 		c.Assert(rel, qt.Not(qt.Equals), "")
  658 	}
  659 
  660 	// Animated GIF
  661 	orig = fetchImageForSpec(spec, c, "giphy.gif")
  662 	for _, resizeSpec := range []string{"200x", "512x"} {
  663 		resized, err := orig.Resize(resizeSpec)
  664 		c.Assert(err, qt.IsNil)
  665 		rel := resized.RelPermalink()
  666 		c.Assert(rel, qt.Not(qt.Equals), "")
  667 	}
  668 
  669 	for _, img := range testImages {
  670 
  671 		orig := fetchImageForSpec(spec, c, img)
  672 		for _, resizeSpec := range []string{"200x100", "600x", "200x r90 q50 Box"} {
  673 			resized, err := orig.Resize(resizeSpec)
  674 			c.Assert(err, qt.IsNil)
  675 			rel := resized.RelPermalink()
  676 			c.Assert(rel, qt.Not(qt.Equals), "")
  677 		}
  678 
  679 		for _, fillSpec := range []string{"300x200 Gaussian Smart", "100x100 Center", "300x100 TopLeft NearestNeighbor", "400x200 BottomLeft"} {
  680 			resized, err := orig.Fill(fillSpec)
  681 			c.Assert(err, qt.IsNil)
  682 			rel := resized.RelPermalink()
  683 			c.Assert(rel, qt.Not(qt.Equals), "")
  684 		}
  685 
  686 		for _, fitSpec := range []string{"300x200 Linear"} {
  687 			resized, err := orig.Fit(fitSpec)
  688 			c.Assert(err, qt.IsNil)
  689 			rel := resized.RelPermalink()
  690 			c.Assert(rel, qt.Not(qt.Equals), "")
  691 		}
  692 
  693 		f := &images.Filters{}
  694 
  695 		filters := []gift.Filter{
  696 			f.Grayscale(),
  697 			f.GaussianBlur(6),
  698 			f.Saturation(50),
  699 			f.Sepia(100),
  700 			f.Brightness(30),
  701 			f.ColorBalance(10, -10, -10),
  702 			f.Colorize(240, 50, 100),
  703 			f.Gamma(1.5),
  704 			f.UnsharpMask(1, 1, 0),
  705 			f.Sigmoid(0.5, 7),
  706 			f.Pixelate(5),
  707 			f.Invert(),
  708 			f.Hue(22),
  709 			f.Contrast(32.5),
  710 			f.Overlay(gopher.(images.ImageSource), 20, 30),
  711 			f.Text("No options"),
  712 			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."),
  713 			f.Text("Hugo rocks!", map[string]any{"x": 3, "y": 3, "size": 20, "color": "#fc03b1"}),
  714 		}
  715 
  716 		resized, err := orig.Fill("400x200 center")
  717 		c.Assert(err, qt.IsNil)
  718 
  719 		for _, filter := range filters {
  720 			resized, err := resized.Filter(filter)
  721 			c.Assert(err, qt.IsNil)
  722 			rel := resized.RelPermalink()
  723 			c.Assert(rel, qt.Not(qt.Equals), "")
  724 		}
  725 
  726 		resized, err = resized.Filter(filters[0:4])
  727 		c.Assert(err, qt.IsNil)
  728 		rel := resized.RelPermalink()
  729 		c.Assert(rel, qt.Not(qt.Equals), "")
  730 	}
  731 
  732 	if devMode {
  733 		return
  734 	}
  735 
  736 	dir1 := filepath.Join(workDir, "resources/_gen/images")
  737 	dir2 := filepath.FromSlash("testdata/golden")
  738 
  739 	assetGoldenDirs(c, dir1, dir2)
  740 
  741 }
  742 
  743 func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
  744 
  745 	// The two dirs above should now be the same.
  746 	dirinfos1, err := ioutil.ReadDir(dir1)
  747 	c.Assert(err, qt.IsNil)
  748 	dirinfos2, err := ioutil.ReadDir(dir2)
  749 	c.Assert(err, qt.IsNil)
  750 	c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
  751 
  752 	for i, fi1 := range dirinfos1 {
  753 		fi2 := dirinfos2[i]
  754 		c.Assert(fi1.Name(), qt.Equals, fi2.Name())
  755 
  756 		f1, err := os.Open(filepath.Join(dir1, fi1.Name()))
  757 		c.Assert(err, qt.IsNil)
  758 		f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
  759 		c.Assert(err, qt.IsNil)
  760 
  761 		decodeAll := func(f *os.File) []image.Image {
  762 			var images []image.Image
  763 
  764 			if strings.HasSuffix(f.Name(), ".gif") {
  765 				gif, err := gif.DecodeAll(f)
  766 				c.Assert(err, qt.IsNil)
  767 				images = make([]image.Image, len(gif.Image))
  768 				for i, img := range gif.Image {
  769 					images[i] = img
  770 				}
  771 			} else {
  772 				img, _, err := image.Decode(f)
  773 				c.Assert(err, qt.IsNil)
  774 				images = append(images, img)
  775 			}
  776 			return images
  777 		}
  778 
  779 		imgs1 := decodeAll(f1)
  780 		imgs2 := decodeAll(f2)
  781 		c.Assert(len(imgs1), qt.Equals, len(imgs2))
  782 
  783 	LOOP:
  784 		for i, img1 := range imgs1 {
  785 			img2 := imgs2[i]
  786 			nrgba1 := image.NewNRGBA(img1.Bounds())
  787 			gift.New().Draw(nrgba1, img1)
  788 			nrgba2 := image.NewNRGBA(img2.Bounds())
  789 			gift.New().Draw(nrgba2, img2)
  790 
  791 			if !goldenEqual(nrgba1, nrgba2) {
  792 				switch fi1.Name() {
  793 				case "gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_73c19c5f80881858a85aa23cd0ca400d.png",
  794 					"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_ae631e5252bb5d7b92bc766ad1a89069.png",
  795 					"gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_d1bbfa2629bffb90118cacce3fcfb924.png",
  796 					"giphy_hu3eafc418e52414ace6236bf1d31f82e1_52213_200x0_resize_box.gif":
  797 					c.Log("expectedly differs from golden due to dithering:", fi1.Name())
  798 				default:
  799 					c.Errorf("resulting image differs from golden: %s", fi1.Name())
  800 					break LOOP
  801 				}
  802 			}
  803 		}
  804 
  805 		if !usesFMA {
  806 			c.Assert(fi1, eq, fi2)
  807 
  808 			_, err = f1.Seek(0, 0)
  809 			c.Assert(err, qt.IsNil)
  810 			_, err = f2.Seek(0, 0)
  811 			c.Assert(err, qt.IsNil)
  812 
  813 			hash1, err := helpers.MD5FromReader(f1)
  814 			c.Assert(err, qt.IsNil)
  815 			hash2, err := helpers.MD5FromReader(f2)
  816 			c.Assert(err, qt.IsNil)
  817 
  818 			c.Assert(hash1, qt.Equals, hash2)
  819 		}
  820 
  821 		f1.Close()
  822 		f2.Close()
  823 	}
  824 }
  825 
  826 func BenchmarkResizeParallel(b *testing.B) {
  827 	c := qt.New(b)
  828 	img := fetchSunset(c)
  829 
  830 	b.RunParallel(func(pb *testing.PB) {
  831 		for pb.Next() {
  832 			w := rand.Intn(10) + 10
  833 			resized, err := img.Resize(strconv.Itoa(w) + "x")
  834 			if err != nil {
  835 				b.Fatal(err)
  836 			}
  837 			_, err = resized.Resize(strconv.Itoa(w-1) + "x")
  838 			if err != nil {
  839 				b.Fatal(err)
  840 			}
  841 		}
  842 	})
  843 }