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 }