resources: Support output image format in image operations
authorJ. Ansorg <github@joachim-ansorg.de>
Sat, 21 Sep 2019 14:50:27 +0000 (16:50 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 21 Sep 2019 14:50:27 +0000 (16:50 +0200)
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

media/mediaType.go
resources/image.go
resources/image_test.go
resources/images/config.go
resources/images/image.go
resources/resource.go
resources/resource_metadata.go

index 434672c43252c42a1456ebb40f9fa312e0b5449e..baf7b7e2740a8424203393bcbb84fdc4bc7cabce 100644 (file)
@@ -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"}
 )
index 26b9b8710b04cb4df0dd97f845ebcdbb5d7666dd..bb9c987a5c530a29c456dec612e1333e085463b1 100644 (file)
@@ -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"
 )
@@ -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.
@@ -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)
@@ -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
        })
@@ -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
@@ -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())
index 59d6b7c9a2c029af9ebca57cee4a29c462dfbd4e..5fa5021c9568d511fce23d4e4ac692f0e57464a3 100644 (file)
@@ -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)
index a290922abb22486008869082bdf6a5339cad49ab..6bc701bfe28cfa140f0deee0a4bdb7fc38123b8e 100644 (file)
@@ -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
                }
        }
 
@@ -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.
index e72d968370b1a4a14bf3946baff691fe8bfa2945..bd7500c28389dd105955190e752f5df0e2398153 100644 (file)
@@ -23,6 +23,7 @@ import (
        "io"
        "sync"
 
+       "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/resources/images/exif"
 
        "github.com/disintegration/gift"
@@ -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
@@ -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
index 7f52a7135581c2948df733441e7e202926b2dccf..acf8e37c0d80452e9bd12329a93c56d4fb78341d 100644 (file)
@@ -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
 }
index 79e61e1a07aca4ef12509dccedaa4dbb91fa918e..ce17df022fab6f5095a0067244938039503e22f1 100644 (file)
@@ -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"
@@ -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{})
 }