diff options
author | Bjørn Erik Pedersen <[email protected]> | 2023-09-23 11:45:17 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2023-09-24 11:54:29 +0200 |
commit | 6a246d1152dbd5bc0ac4ce8101c89838a3cba7ba (patch) | |
tree | e1fd76916c73ae949eebe403f548a6cb0086b1aa /resources | |
parent | ef0e7149d63c64269b852cf68a2af67b94b8eec3 (diff) | |
download | hugo-6a246d1152dbd5bc0ac4ce8101c89838a3cba7ba.tar.gz hugo-6a246d1152dbd5bc0ac4ce8101c89838a3cba7ba.zip |
Add images.Process filter
This allows for constructs like:
```
{{ $filters := slice (images.GaussianBlur 8) (images.Grayscale) (images.Process "jpg q30 resize 200x") }}
{{ $img = $img | images.Filter $filters }}
```
Note that the `action` option in `images.Process` is optional (`resize` in the example above), so you can use the above to just set the target format, e.g.:
```
{{ $filters := slice (images.GaussianBlur 8) (images.Grayscale) (images.Process "jpg") }}
{{ $img = $img | images.Filter $filters }}
```
Fixes #8439
Diffstat (limited to 'resources')
-rw-r--r-- | resources/image.go | 79 | ||||
-rw-r--r-- | resources/images/filters.go | 9 | ||||
-rw-r--r-- | resources/images/image.go | 19 | ||||
-rw-r--r-- | resources/images/process.go | 43 | ||||
-rw-r--r-- | resources/integration_test.go | 33 |
5 files changed, 168 insertions, 15 deletions
diff --git a/resources/image.go b/resources/image.go index 5b030fde9..9a24fd21c 100644 --- a/resources/image.go +++ b/resources/image.go @@ -206,15 +206,7 @@ var imageActions = []string{images.ActionResize, images.ActionCrop, images.Actio // This makes this method a more flexible version that covers all of Resize, Crop, Fit and Fill, // but it also supports e.g. format conversions without any resize action. func (i *imageResource) Process(spec string) (images.ImageResource, error) { - var action string - options := strings.Fields(spec) - for i, p := range options { - if hstrings.InSlicEqualFold(imageActions, p) { - action = p - options = append(options[:i], options[i+1:]...) - break - } - } + action, options := i.resolveActionOptions(spec) return i.processActionOptions(action, options) } @@ -245,7 +237,7 @@ func (i *imageResource) Fill(spec string) (images.ImageResource, error) { } func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) { - conf := images.GetDefaultImageConfig("filter", i.Proc.Cfg) + var conf images.ImageConfig var gfilters []gift.Filter @@ -253,14 +245,77 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) { gfilters = append(gfilters, images.ToFilters(f)...) } + var ( + targetFormat images.Format + configSet bool + ) + for _, f := range gfilters { + f = images.UnwrapFilter(f) + if specProvider, ok := f.(images.ImageProcessSpecProvider); ok { + action, options := i.resolveActionOptions(specProvider.ImageProcessSpec()) + var err error + conf, err = images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format) + if err != nil { + return nil, err + } + configSet = true + if conf.TargetFormat != 0 { + targetFormat = conf.TargetFormat + // We only support one target format, but prefer the last one, + // so we keep going. + } + } + } + + if !configSet { + conf = images.GetDefaultImageConfig("filter", i.Proc.Cfg) + } + + conf.Action = "filter" conf.Key = identity.HashString(gfilters) - conf.TargetFormat = i.Format + conf.TargetFormat = targetFormat + if conf.TargetFormat == 0 { + conf.TargetFormat = i.Format + } return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) { - return i.Proc.Filter(src, gfilters...) + filters := gfilters + for j, f := range gfilters { + f = images.UnwrapFilter(f) + if specProvider, ok := f.(images.ImageProcessSpecProvider); ok { + processSpec := specProvider.ImageProcessSpec() + action, options := i.resolveActionOptions(processSpec) + conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format) + if err != nil { + return nil, err + } + pFilters, err := i.Proc.FiltersFromConfig(src, conf) + if err != nil { + return nil, err + } + // Replace the filter with the new filters. + // This slice will be empty if this is just a format conversion. + filters = append(filters[:j], append(pFilters, filters[j+1:]...)...) + + } + } + return i.Proc.Filter(src, filters...) }) } +func (i *imageResource) resolveActionOptions(spec string) (string, []string) { + var action string + options := strings.Fields(spec) + for i, p := range options { + if hstrings.InSlicEqualFold(imageActions, p) { + action = p + options = append(options[:i], options[i+1:]...) + break + } + } + return action, options +} + func (i *imageResource) processActionSpec(action, spec string) (images.ImageResource, error) { return i.processActionOptions(action, strings.Fields(spec)) } diff --git a/resources/images/filters.go b/resources/images/filters.go index 63e90d2ad..dca8ff0e8 100644 --- a/resources/images/filters.go +++ b/resources/images/filters.go @@ -30,6 +30,15 @@ const filterAPIVersion = 0 type Filters struct{} +func (*Filters) Process(spec any) gift.Filter { + return filter{ + Options: newFilterOpts(spec), + Filter: processFilter{ + spec: cast.ToString(spec), + }, + } +} + // Overlay creates a filter that overlays src at position x y. func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter { return filter{ diff --git a/resources/images/image.go b/resources/images/image.go index 714d0e26d..1637d0cf8 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -201,7 +201,7 @@ func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) { return p.exifDecoder.Decode(r) } -func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) { +func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) { var filters []gift.Filter if conf.Rotate != 0 { @@ -246,6 +246,14 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi default: } + return filters, nil +} + +func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) { + filters, err := p.FiltersFromConfig(src, conf) + if err != nil { + return nil, err + } if len(filters) == 0 { return p.resolveSrc(src, conf.TargetFormat), nil @@ -396,6 +404,15 @@ func imageConfigFromImage(img image.Image) image.Config { return image.Config{Width: b.Max.X, Height: b.Max.Y} } +// UnwrapFilter unwraps the given filter if it is a filter wrapper. +func UnwrapFilter(in gift.Filter) gift.Filter { + if f, ok := in.(filter); ok { + return f.Filter + } + return in +} + +// ToFilters converts the given input to a slice of gift.Filter. func ToFilters(in any) []gift.Filter { switch v := in.(type) { case []gift.Filter: diff --git a/resources/images/process.go b/resources/images/process.go new file mode 100644 index 000000000..984ac3c8f --- /dev/null +++ b/resources/images/process.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "image" + "image/draw" + + "github.com/disintegration/gift" +) + +var _ ImageProcessSpecProvider = (*processFilter)(nil) + +type ImageProcessSpecProvider interface { + ImageProcessSpec() string +} + +type processFilter struct { + spec string +} + +func (f processFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) { + panic("not supported") +} + +func (f processFilter) Bounds(srcBounds image.Rectangle) image.Rectangle { + panic("not supported") +} + +func (f processFilter) ImageProcessSpec() string { + return f.spec +} diff --git a/resources/integration_test.go b/resources/integration_test.go index 2075079dc..5570b15d6 100644 --- a/resources/integration_test.go +++ b/resources/integration_test.go @@ -73,7 +73,6 @@ anigif: {{ $anigif.RelPermalink }}|{{ $anigif.Width }}|{{ $anigif.Height }}|{{ $ b.Build() assertImages() - } func TestSVGError(t *testing.T) { @@ -98,7 +97,6 @@ Width: {{ $svg.Width }} b.Assert(err, qt.IsNotNil) b.Assert(err.Error(), qt.Contains, `error calling Width: this method is only available for raster images. To determine if an image is SVG, you can do {{ if eq .MediaType.SubType "svg" }}{{ end }}`) - } // Issue 10255. @@ -137,5 +135,36 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA b.AssertFileCount("public/images", 1) b.Build() } +} + +func TestProcessFilter(t *testing.T) { + t.Parallel() + + files := ` +-- assets/images/pixel.png -- +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== +-- layouts/index.html -- +{{ $pixel := resources.Get "images/pixel.png" }} +{{ $filters := slice (images.GaussianBlur 6) (images.Pixelate 8) (images.Process "jpg") }} +{{ $image := $pixel.Filter $filters }} +jpg|RelPermalink: {{ $image.RelPermalink }}|MediaType: {{ $image.MediaType }}|Width: {{ $image.Width }}|Height: {{ $image.Height }}| +{{ $filters := slice (images.GaussianBlur 6) (images.Pixelate 8) (images.Process "jpg resize 20x30") }} +{{ $image := $pixel.Filter $filters }} +resize 1|RelPermalink: {{ $image.RelPermalink }}|MediaType: {{ $image.MediaType }}|Width: {{ $image.Width }}|Height: {{ $image.Height }}| +{{ $image := $pixel.Filter $filters }} +resize 2|RelPermalink: {{ $image.RelPermalink }}|MediaType: {{ $image.MediaType }}|Width: {{ $image.Width }}|Height: {{ $image.Height }}| + +` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }).Build() + b.AssertFileContent("public/index.html", + "jpg|RelPermalink: /images/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_filter_17010532266664966692.jpg|MediaType: image/jpeg|Width: 1|Height: 1|", + "resize 1|RelPermalink: /images/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_filter_6707036659822075562.jpg|MediaType: image/jpeg|Width: 20|Height: 30|", + "resize 2|RelPermalink: /images/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_filter_6707036659822075562.jpg|MediaType: image/jpeg|Width: 20|Height: 30|", + ) } |