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 }