diff --git a/resource/image.go b/resource/image.go index 208a0e9fb0f..2529413cc90 100644 --- a/resource/image.go +++ b/resource/image.go @@ -30,6 +30,7 @@ import ( // Importing image codecs for image.DecodeConfig "image" + "image/draw" _ "image/gif" "image/jpeg" _ "image/png" @@ -65,15 +66,27 @@ const ( defaultResampleFilter = "box" ) -var imageFormats = map[string]imaging.Format{ - ".jpg": imaging.JPEG, - ".jpeg": imaging.JPEG, - ".png": imaging.PNG, - ".tif": imaging.TIFF, - ".tiff": imaging.TIFF, - ".bmp": imaging.BMP, - ".gif": imaging.GIF, -} +var ( + imageFormats = map[string]imaging.Format{ + ".jpg": imaging.JPEG, + ".jpeg": imaging.JPEG, + ".png": imaging.PNG, + ".tif": imaging.TIFF, + ".tiff": imaging.TIFF, + ".bmp": imaging.BMP, + ".gif": imaging.GIF, + } + + // Add or increment if changes to an image format's processing requires + // re-generation. + imageFormatsVersions = map[imaging.Format]int{ + imaging.PNG: 1, // 1: Add proper palette handling + } + + // Increment to mark all processed images as stale. Only use when absolutely needed. + // See the finer grained smartCropVersionNumber and imageFormatsVersions. + mainImageVersionNumber = 0 +) var anchorPositions = map[string]imaging.Anchor{ strings.ToLower("Center"): imaging.Center, @@ -117,6 +130,8 @@ type Image struct { imaging *Imaging + format imaging.Format + hash string *genericResource @@ -137,6 +152,7 @@ func (i *Image) WithNewBase(base string) Resource { return &Image{ imaging: i.imaging, hash: i.hash, + format: i.format, genericResource: i.genericResource.WithNewBase(base).(*genericResource)} } @@ -246,6 +262,15 @@ func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, c return ci, &os.PathError{Op: errOp, Path: errPath, Err: err} } + if i.format == imaging.PNG { + // Apply the colour palette from the source + if paletted, ok := src.(*image.Paletted); ok { + tmp := image.NewPaletted(converted.Bounds(), paletted.Palette) + draw.Src.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min) + converted = tmp + } + } + b := converted.Bounds() ci.config = image.Config{Width: b.Max.X, Height: b.Max.Y} ci.configLoaded = true @@ -255,7 +280,7 @@ func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, c } -func (i imageConfig) key() string { +func (i imageConfig) key(format imaging.Format) string { k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height) if i.Action != "" { k += "_" + i.Action @@ -277,6 +302,14 @@ func (i imageConfig) key() string { k += "_" + anchor } + if v, ok := imageFormatsVersions[format]; ok { + k += "_" + strconv.Itoa(v) + } + + if mainImageVersionNumber > 0 { + k += "_" + strconv.Itoa(mainImageVersionNumber) + } + return k } @@ -410,7 +443,8 @@ func (i *Image) decodeSource() (image.Image, error) { return nil, err } defer file.Close() - return imaging.Decode(file) + img, _, err := image.Decode(file) + return img, err } func (i *Image) copyToDestination(src string) error { @@ -464,12 +498,6 @@ func (i *Image) copyToDestination(src string) error { } func (i *Image) encodeToDestinations(img image.Image, conf imageConfig, resourceCacheFilename, filename string) error { - ext := strings.ToLower(helpers.Ext(filename)) - - imgFormat, ok := imageFormats[ext] - if !ok { - return imaging.ErrUnsupportedFormat - } target := filepath.Join(i.absPublishDir, filename) @@ -509,7 +537,7 @@ func (i *Image) encodeToDestinations(img image.Image, conf imageConfig, resource w = file1 } - switch imgFormat { + switch i.format { case imaging.JPEG: var rgba *image.RGBA @@ -530,7 +558,7 @@ func (i *Image) encodeToDestinations(img image.Image, conf imageConfig, resource return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) } default: - return imaging.Encode(w, img, imgFormat) + return imaging.Encode(w, img, i.format) } } @@ -541,6 +569,7 @@ func (i *Image) clone() *Image { return &Image{ imaging: i.imaging, hash: i.hash, + format: i.format, genericResource: &g} } @@ -555,7 +584,7 @@ func (i *Image) filenameFromConfig(conf imageConfig) string { // Do not change for no good reason. const md5Threshold = 100 - key := conf.key() + key := conf.key(i.format) // It is useful to have the key in clear text, but when nesting transforms, it // can easily be too long to read, and maybe even too long diff --git a/resource/image_test.go b/resource/image_test.go index e981a208fb1..1a937d56b6d 100644 --- a/resource/image_test.go +++ b/resource/image_test.go @@ -19,6 +19,8 @@ import ( "strconv" "testing" + "github.com/disintegration/imaging" + "sync" "github.com/stretchr/testify/require" @@ -258,6 +260,24 @@ func TestImageWithMetadata(t *testing.T) { } +func TestImageResize8BitPNG(t *testing.T) { + + assert := require.New(t) + + image := fetchImage(assert, "gohugoio.png") + + assert.Equal(imaging.PNG, image.format) + assert.Equal("/a/gohugoio.png", image.RelPermalink()) + assert.Equal("image", image.ResourceType()) + + resized, err := image.Resize("800x") + assert.NoError(err) + assert.Equal(imaging.PNG, resized.format) + assert.Equal("/a/gohugoio_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_800x0_resize_linear_1.png", resized.RelPermalink()) + assert.Equal(800, resized.Width()) + +} + func BenchmarkResizeParallel(b *testing.B) { assert := require.New(b) img := fetchSunset(assert) diff --git a/resource/resource.go b/resource/resource.go index da62db65cb8..66fda4c1bfa 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -23,6 +23,8 @@ import ( "strings" "sync" + "github.com/disintegration/imaging" + "github.com/spf13/cast" "github.com/gobwas/glob" @@ -297,8 +299,16 @@ func (r *Spec) newResource( return nil, err } + ext := strings.ToLower(helpers.Ext(absSourceFilename)) + + imgFormat, ok := imageFormats[ext] + if !ok { + return nil, imaging.ErrUnsupportedFormat + } + return &Image{ hash: hash, + format: imgFormat, imaging: r.imaging, genericResource: gr}, nil } diff --git a/resource/testdata/gohugoio.png b/resource/testdata/gohugoio.png new file mode 100644 index 00000000000..0591db9592b Binary files /dev/null and b/resource/testdata/gohugoio.png differ