Skip to content

Commit

Permalink
resources: Support output image format in image operations
Browse files Browse the repository at this point in the history
The image format is defined as the image extension of the known formats,
excluding the dot.
All of 'img.Resize "600x jpeg"', 'img.Resize "600x jpg"',
and 'img.Resize "600x png"' are valid format definitions.
If the target format is defined in the operation definition string,
then the converted image will be stored in this format. Permalinks and
media type are updated correspondingly.
Unknown image extensions in the operation definition have not effect.

See #6298
  • Loading branch information
jansorg authored and bep committed Sep 21, 2019
1 parent 34dc06b commit e5856e6
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 15 deletions.
7 changes: 5 additions & 2 deletions media/mediaType.go
Expand Up @@ -140,8 +140,11 @@ var (
YAMLType = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter}

// Common image types
PNGType = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, Delimiter: defaultDelimiter}
JPGType = Type{MainType: "image", SubType: "jpg", Suffixes: []string{"jpg", "jpeg"}, Delimiter: defaultDelimiter}
PNGType = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, Delimiter: defaultDelimiter}
JPGType = Type{MainType: "image", SubType: "jpg", Suffixes: []string{"jpg", "jpeg"}, Delimiter: defaultDelimiter}
GIFType = Type{MainType: "image", SubType: "gif", Suffixes: []string{"gif"}, Delimiter: defaultDelimiter}
TIFFType = Type{MainType: "image", SubType: "tiff", Suffixes: []string{"tif", "tiff"}, Delimiter: defaultDelimiter}
BMPType = Type{MainType: "image", SubType: "bmp", Suffixes: []string{"bmp"}, Delimiter: defaultDelimiter}

OctetType = Type{MainType: "application", SubType: "octet-stream"}
)
Expand Down
24 changes: 13 additions & 11 deletions resources/image.go
Expand Up @@ -42,8 +42,6 @@ import (
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/images"

// Blind import for image.Decode

// Blind import for image.Decode
_ "golang.org/x/image/webp"
)
Expand Down Expand Up @@ -220,17 +218,13 @@ func (i *imageResource) Filter(filters ...interface{}) (resource.Image, error) {
}

conf.Key = internal.HashString(gfilters)
conf.TargetFormat = i.Format

return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
return i.Proc.Filter(src, gfilters...)
})
}

func (i *imageResource) isJPEG() bool {
name := strings.ToLower(i.getResourcePaths().relTargetDirFile.file)
return strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg")
}

// Serialize image processing. The imaging library spins up its own set of Go routines,
// so there is not much to gain from adding more load to the mix. That
// can even have negative effect in low resource scenarios.
Expand Down Expand Up @@ -260,7 +254,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
}

if i.Format == images.PNG {
if conf.TargetFormat == images.PNG {
// Apply the colour palette from the source
if paletted, ok := src.(*image.Paletted); ok {
tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
Expand All @@ -271,6 +265,8 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im

ci := i.clone(converted)
ci.setBasePath(conf)
ci.Format = conf.TargetFormat
ci.setMediaType(conf.TargetFormat.MediaType())

return ci, converted, nil
})
Expand All @@ -282,11 +278,14 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
return conf, err
}

iconf := i.Proc.Cfg
// default to the source format
if conf.TargetFormat == 0 {
conf.TargetFormat = i.Format
}

if conf.Quality <= 0 && i.isJPEG() {
if conf.Quality <= 0 && conf.TargetFormat.RequiresDefaultQuality() {
// We need a quality setting for all JPEGs
conf.Quality = iconf.Quality
conf.Quality = i.Proc.Cfg.Quality
}

return conf, nil
Expand Down Expand Up @@ -339,6 +338,9 @@ func (i *imageResource) getImageMetaCacheTargetPath() string {

func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile {
p1, p2 := helpers.FileAndExt(i.getResourcePaths().relTargetDirFile.file)
if conf.TargetFormat != i.Format {
p2 = conf.TargetFormat.DefaultExtension()
}

h, _ := i.hash()
idStr := fmt.Sprintf("_hu%s_%d", h, i.size())
Expand Down
40 changes: 40 additions & 0 deletions resources/image_test.go
Expand Up @@ -133,6 +133,46 @@ func TestImageTransformBasic(t *testing.T) {
c.Assert(filled, eq, filledAgain)
}

func TestImageTransformFormat(t *testing.T) {
c := qt.New(t)

image := fetchSunset(c)

fileCache := image.(specProvider).getSpec().FileCaches.ImageCache().Fs

assertExtWidthHeight := func(img resource.Image, ext string, w, h int) {
c.Helper()
c.Assert(img, qt.Not(qt.IsNil))
c.Assert(helpers.Ext(img.RelPermalink()), qt.Equals, ext)
c.Assert(img.Width(), qt.Equals, w)
c.Assert(img.Height(), qt.Equals, h)
}

c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
c.Assert(image.ResourceType(), qt.Equals, "image")
assertExtWidthHeight(image, ".jpg", 900, 562)

imagePng, err := image.Resize("450x png")
c.Assert(err, qt.IsNil)
c.Assert(imagePng.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_450x0_resize_linear.png")
c.Assert(imagePng.ResourceType(), qt.Equals, "image")
assertExtWidthHeight(imagePng, ".png", 450, 281)
c.Assert(imagePng.Name(), qt.Equals, "sunset.jpg")
c.Assert(imagePng.MediaType().String(), qt.Equals, "image/png")

assertFileCache(c, fileCache, path.Base(imagePng.RelPermalink()), 450, 281)

imageGif, err := image.Resize("225x gif")
c.Assert(err, qt.IsNil)
c.Assert(imageGif.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_225x0_resize_linear.gif")
c.Assert(imageGif.ResourceType(), qt.Equals, "image")
assertExtWidthHeight(imageGif, ".gif", 225, 141)
c.Assert(imageGif.Name(), qt.Equals, "sunset.jpg")
c.Assert(imageGif.MediaType().String(), qt.Equals, "image/gif")

assertFileCache(c, fileCache, path.Base(imageGif.RelPermalink()), 225, 141)
}

// https://github.com/gohugoio/hugo/issues/4261
func TestImageTransformLongFilename(t *testing.T) {
c := qt.New(t)
Expand Down
6 changes: 5 additions & 1 deletion resources/images/config.go
Expand Up @@ -187,7 +187,8 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
} else {
return c, errors.New("invalid image dimensions")
}

} else if f, ok := ImageFormatFromExt("." + part); ok {
c.TargetFormat = f
}
}

Expand All @@ -212,6 +213,9 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er

// ImageConfig holds configuration to create a new image from an existing one, resize etc.
type ImageConfig struct {
// This defines the output format of the output image. It defaults to the source format
TargetFormat Format

Action string

// If set, this will be used as the key in filenames etc.
Expand Down
32 changes: 31 additions & 1 deletion resources/images/image.go
Expand Up @@ -23,6 +23,7 @@ import (
"io"
"sync"

"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images/exif"

"github.com/disintegration/gift"
Expand Down Expand Up @@ -59,7 +60,7 @@ type Image struct {
}

func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
switch i.Format {
switch conf.TargetFormat {
case JPEG:

var rgba *image.RGBA
Expand Down Expand Up @@ -250,6 +251,35 @@ const (
BMP
)

// RequiresDefaultQuality returns if the default quality needs to be applied to images of this format
func (f Format) RequiresDefaultQuality() bool {
return f == JPEG
}

// DefaultExtension returns the default file extension of this format, starting with a dot.
// For example: .jpg for JPEG
func (f Format) DefaultExtension() string {
return f.MediaType().FullSuffix()
}

// MediaType returns the media type of this image, e.g. image/jpeg for JPEG
func (f Format) MediaType() media.Type {
switch f {
case JPEG:
return media.JPGType
case PNG:
return media.PNGType
case GIF:
return media.GIFType
case TIFF:
return media.TIFFType
case BMP:
return media.BMPType
default:
panic(fmt.Sprintf("%d is not a valid image format", f))
}
}

type imageConfig struct {
config image.Config
configInit sync.Once
Expand Down
4 changes: 4 additions & 0 deletions resources/resource.go
Expand Up @@ -220,6 +220,10 @@ func (l *genericResource) MediaType() media.Type {
return l.mediaType
}

func (l *genericResource) setMediaType(mediaType media.Type) {
l.mediaType = mediaType
}

func (l *genericResource) Name() string {
return l.name
}
Expand Down
2 changes: 2 additions & 0 deletions resources/resource_metadata.go
Expand Up @@ -18,6 +18,7 @@ import (
"strconv"

"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/resource"

"github.com/pkg/errors"
Expand All @@ -42,6 +43,7 @@ type metaAssignerProvider interface {
type metaAssigner interface {
setTitle(title string)
setName(name string)
setMediaType(mediaType media.Type)
updateParams(params map[string]interface{})
}

Expand Down

0 comments on commit e5856e6

Please sign in to comment.