aboutsummaryrefslogtreecommitdiffhomepage
path: root/resources
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2023-09-23 11:45:17 +0200
committerBjørn Erik Pedersen <[email protected]>2023-09-24 11:54:29 +0200
commit6a246d1152dbd5bc0ac4ce8101c89838a3cba7ba (patch)
treee1fd76916c73ae949eebe403f548a6cb0086b1aa /resources
parentef0e7149d63c64269b852cf68a2af67b94b8eec3 (diff)
downloadhugo-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.go79
-rw-r--r--resources/images/filters.go9
-rw-r--r--resources/images/image.go19
-rw-r--r--resources/images/process.go43
-rw-r--r--resources/integration_test.go33
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|",
+ )
}