mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-29 09:02:06 -05:00
resources: Support output image format in image operations
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
This commit is contained in:
parent
34dc06b032
commit
e5856e61d8
7 changed files with 100 additions and 15 deletions
|
@ -142,6 +142,9 @@ var (
|
||||||
// Common image types
|
// Common image types
|
||||||
PNGType = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, 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}
|
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"}
|
OctetType = Type{MainType: "application", SubType: "octet-stream"}
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,8 +42,6 @@ import (
|
||||||
"github.com/gohugoio/hugo/helpers"
|
"github.com/gohugoio/hugo/helpers"
|
||||||
"github.com/gohugoio/hugo/resources/images"
|
"github.com/gohugoio/hugo/resources/images"
|
||||||
|
|
||||||
// Blind import for image.Decode
|
|
||||||
|
|
||||||
// Blind import for image.Decode
|
// Blind import for image.Decode
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
@ -220,17 +218,13 @@ func (i *imageResource) Filter(filters ...interface{}) (resource.Image, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.Key = internal.HashString(gfilters)
|
conf.Key = internal.HashString(gfilters)
|
||||||
|
conf.TargetFormat = i.Format
|
||||||
|
|
||||||
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
||||||
return i.Proc.Filter(src, gfilters...)
|
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,
|
// 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
|
// so there is not much to gain from adding more load to the mix. That
|
||||||
// can even have negative effect in low resource scenarios.
|
// 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}
|
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
|
// Apply the colour palette from the source
|
||||||
if paletted, ok := src.(*image.Paletted); ok {
|
if paletted, ok := src.(*image.Paletted); ok {
|
||||||
tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
|
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 := i.clone(converted)
|
||||||
ci.setBasePath(conf)
|
ci.setBasePath(conf)
|
||||||
|
ci.Format = conf.TargetFormat
|
||||||
|
ci.setMediaType(conf.TargetFormat.MediaType())
|
||||||
|
|
||||||
return ci, converted, nil
|
return ci, converted, nil
|
||||||
})
|
})
|
||||||
|
@ -282,11 +278,14 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
|
||||||
return conf, err
|
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
|
// We need a quality setting for all JPEGs
|
||||||
conf.Quality = iconf.Quality
|
conf.Quality = i.Proc.Cfg.Quality
|
||||||
}
|
}
|
||||||
|
|
||||||
return conf, nil
|
return conf, nil
|
||||||
|
@ -339,6 +338,9 @@ func (i *imageResource) getImageMetaCacheTargetPath() string {
|
||||||
|
|
||||||
func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile {
|
func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile {
|
||||||
p1, p2 := helpers.FileAndExt(i.getResourcePaths().relTargetDirFile.file)
|
p1, p2 := helpers.FileAndExt(i.getResourcePaths().relTargetDirFile.file)
|
||||||
|
if conf.TargetFormat != i.Format {
|
||||||
|
p2 = conf.TargetFormat.DefaultExtension()
|
||||||
|
}
|
||||||
|
|
||||||
h, _ := i.hash()
|
h, _ := i.hash()
|
||||||
idStr := fmt.Sprintf("_hu%s_%d", h, i.size())
|
idStr := fmt.Sprintf("_hu%s_%d", h, i.size())
|
||||||
|
|
|
@ -133,6 +133,46 @@ func TestImageTransformBasic(t *testing.T) {
|
||||||
c.Assert(filled, eq, filledAgain)
|
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
|
// https://github.com/gohugoio/hugo/issues/4261
|
||||||
func TestImageTransformLongFilename(t *testing.T) {
|
func TestImageTransformLongFilename(t *testing.T) {
|
||||||
c := qt.New(t)
|
c := qt.New(t)
|
||||||
|
|
|
@ -187,7 +187,8 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
|
||||||
} else {
|
} else {
|
||||||
return c, errors.New("invalid image dimensions")
|
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.
|
// ImageConfig holds configuration to create a new image from an existing one, resize etc.
|
||||||
type ImageConfig struct {
|
type ImageConfig struct {
|
||||||
|
// This defines the output format of the output image. It defaults to the source format
|
||||||
|
TargetFormat Format
|
||||||
|
|
||||||
Action string
|
Action string
|
||||||
|
|
||||||
// If set, this will be used as the key in filenames etc.
|
// If set, this will be used as the key in filenames etc.
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/media"
|
||||||
"github.com/gohugoio/hugo/resources/images/exif"
|
"github.com/gohugoio/hugo/resources/images/exif"
|
||||||
|
|
||||||
"github.com/disintegration/gift"
|
"github.com/disintegration/gift"
|
||||||
|
@ -59,7 +60,7 @@ type Image struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
|
func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
|
||||||
switch i.Format {
|
switch conf.TargetFormat {
|
||||||
case JPEG:
|
case JPEG:
|
||||||
|
|
||||||
var rgba *image.RGBA
|
var rgba *image.RGBA
|
||||||
|
@ -250,6 +251,35 @@ const (
|
||||||
BMP
|
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 {
|
type imageConfig struct {
|
||||||
config image.Config
|
config image.Config
|
||||||
configInit sync.Once
|
configInit sync.Once
|
||||||
|
|
|
@ -220,6 +220,10 @@ func (l *genericResource) MediaType() media.Type {
|
||||||
return l.mediaType
|
return l.mediaType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *genericResource) setMediaType(mediaType media.Type) {
|
||||||
|
l.mediaType = mediaType
|
||||||
|
}
|
||||||
|
|
||||||
func (l *genericResource) Name() string {
|
func (l *genericResource) Name() string {
|
||||||
return l.name
|
return l.name
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/hugofs/glob"
|
"github.com/gohugoio/hugo/hugofs/glob"
|
||||||
|
"github.com/gohugoio/hugo/media"
|
||||||
"github.com/gohugoio/hugo/resources/resource"
|
"github.com/gohugoio/hugo/resources/resource"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -42,6 +43,7 @@ type metaAssignerProvider interface {
|
||||||
type metaAssigner interface {
|
type metaAssigner interface {
|
||||||
setTitle(title string)
|
setTitle(title string)
|
||||||
setName(name string)
|
setName(name string)
|
||||||
|
setMediaType(mediaType media.Type)
|
||||||
updateParams(params map[string]interface{})
|
updateParams(params map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue