resource: Preserve color palette for PNG images
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 19 Feb 2018 15:34:49 +0000 (16:34 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 19 Feb 2018 19:15:58 +0000 (20:15 +0100)
This commit will force a reprocessing of PNG images with new names, so it is adviced to run a `hugo --gc` to remove stale files.

Fixes #4416

resource/image.go
resource/image_test.go
resource/resource.go
resource/testdata/gohugoio.png [new file with mode: 0644]

index 208a0e9fb0f43df4c71e76fc111f01b74c01e3e9..2529413cc9038695a2309392ea173994f440ce83 100644 (file)
@@ -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
index e981a208fb1d46f9ffa4669251833a083f37b3f0..1a937d56b6d55fbbdfdec611cd3fd4d1076de590 100644 (file)
@@ -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)
index da62db65cb834d61df2c930dd54d7f9dfaf5c641..66fda4c1bfafdafb87e41665d92f67884c177e3a 100644 (file)
@@ -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 (file)
index 0000000..0591db9
Binary files /dev/null and b/resource/testdata/gohugoio.png differ