mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-15 01:55:05 +00:00
resource: Preserve color palette for PNG images
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
This commit is contained in:
parent
faa3159e5e
commit
799c654b0d
4 changed files with 79 additions and 20 deletions
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
// Importing image codecs for image.DecodeConfig
|
// Importing image codecs for image.DecodeConfig
|
||||||
"image"
|
"image"
|
||||||
|
"image/draw"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
@ -65,15 +66,27 @@ const (
|
||||||
defaultResampleFilter = "box"
|
defaultResampleFilter = "box"
|
||||||
)
|
)
|
||||||
|
|
||||||
var imageFormats = map[string]imaging.Format{
|
var (
|
||||||
".jpg": imaging.JPEG,
|
imageFormats = map[string]imaging.Format{
|
||||||
".jpeg": imaging.JPEG,
|
".jpg": imaging.JPEG,
|
||||||
".png": imaging.PNG,
|
".jpeg": imaging.JPEG,
|
||||||
".tif": imaging.TIFF,
|
".png": imaging.PNG,
|
||||||
".tiff": imaging.TIFF,
|
".tif": imaging.TIFF,
|
||||||
".bmp": imaging.BMP,
|
".tiff": imaging.TIFF,
|
||||||
".gif": imaging.GIF,
|
".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{
|
var anchorPositions = map[string]imaging.Anchor{
|
||||||
strings.ToLower("Center"): imaging.Center,
|
strings.ToLower("Center"): imaging.Center,
|
||||||
|
@ -117,6 +130,8 @@ type Image struct {
|
||||||
|
|
||||||
imaging *Imaging
|
imaging *Imaging
|
||||||
|
|
||||||
|
format imaging.Format
|
||||||
|
|
||||||
hash string
|
hash string
|
||||||
|
|
||||||
*genericResource
|
*genericResource
|
||||||
|
@ -137,6 +152,7 @@ func (i *Image) WithNewBase(base string) Resource {
|
||||||
return &Image{
|
return &Image{
|
||||||
imaging: i.imaging,
|
imaging: i.imaging,
|
||||||
hash: i.hash,
|
hash: i.hash,
|
||||||
|
format: i.format,
|
||||||
genericResource: i.genericResource.WithNewBase(base).(*genericResource)}
|
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}
|
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()
|
b := converted.Bounds()
|
||||||
ci.config = image.Config{Width: b.Max.X, Height: b.Max.Y}
|
ci.config = image.Config{Width: b.Max.X, Height: b.Max.Y}
|
||||||
ci.configLoaded = true
|
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)
|
k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
|
||||||
if i.Action != "" {
|
if i.Action != "" {
|
||||||
k += "_" + i.Action
|
k += "_" + i.Action
|
||||||
|
@ -277,6 +302,14 @@ func (i imageConfig) key() string {
|
||||||
k += "_" + anchor
|
k += "_" + anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := imageFormatsVersions[format]; ok {
|
||||||
|
k += "_" + strconv.Itoa(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mainImageVersionNumber > 0 {
|
||||||
|
k += "_" + strconv.Itoa(mainImageVersionNumber)
|
||||||
|
}
|
||||||
|
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +443,8 @@ func (i *Image) decodeSource() (image.Image, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
return imaging.Decode(file)
|
img, _, err := image.Decode(file)
|
||||||
|
return img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Image) copyToDestination(src string) error {
|
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 {
|
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)
|
target := filepath.Join(i.absPublishDir, filename)
|
||||||
|
|
||||||
|
@ -509,7 +537,7 @@ func (i *Image) encodeToDestinations(img image.Image, conf imageConfig, resource
|
||||||
w = file1
|
w = file1
|
||||||
}
|
}
|
||||||
|
|
||||||
switch imgFormat {
|
switch i.format {
|
||||||
case imaging.JPEG:
|
case imaging.JPEG:
|
||||||
|
|
||||||
var rgba *image.RGBA
|
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})
|
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
|
||||||
}
|
}
|
||||||
default:
|
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{
|
return &Image{
|
||||||
imaging: i.imaging,
|
imaging: i.imaging,
|
||||||
hash: i.hash,
|
hash: i.hash,
|
||||||
|
format: i.format,
|
||||||
genericResource: &g}
|
genericResource: &g}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,7 +584,7 @@ func (i *Image) filenameFromConfig(conf imageConfig) string {
|
||||||
// Do not change for no good reason.
|
// Do not change for no good reason.
|
||||||
const md5Threshold = 100
|
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
|
// 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
|
// can easily be too long to read, and maybe even too long
|
||||||
|
|
|
@ -19,6 +19,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func BenchmarkResizeParallel(b *testing.B) {
|
||||||
assert := require.New(b)
|
assert := require.New(b)
|
||||||
img := fetchSunset(assert)
|
img := fetchSunset(assert)
|
||||||
|
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
|
@ -297,8 +299,16 @@ func (r *Spec) newResource(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(helpers.Ext(absSourceFilename))
|
||||||
|
|
||||||
|
imgFormat, ok := imageFormats[ext]
|
||||||
|
if !ok {
|
||||||
|
return nil, imaging.ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
|
||||||
return &Image{
|
return &Image{
|
||||||
hash: hash,
|
hash: hash,
|
||||||
|
format: imgFormat,
|
||||||
imaging: r.imaging,
|
imaging: r.imaging,
|
||||||
genericResource: gr}, nil
|
genericResource: gr}, nil
|
||||||
}
|
}
|
||||||
|
|
BIN
resource/testdata/gohugoio.png
vendored
Normal file
BIN
resource/testdata/gohugoio.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
Loading…
Add table
Reference in a new issue