]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add images.Process filter
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 23 Sep 2023 09:45:17 +0000 (11:45 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 24 Sep 2023 09:54:29 +0000 (11:54 +0200)
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

docs/content/en/functions/images/index.md
resources/image.go
resources/images/filters.go
resources/images/image.go
resources/images/process.go [new file with mode: 0644]
resources/integration_test.go

index fcd0796e3fe78d5966b6f96c36bd647459613cac..2b106714ea7467cd74fc849a84d39fd63f907d94 100644 (file)
@@ -12,6 +12,28 @@ toc: true
 
 See [images.Filter](#filter) for how to apply these filters to an image.
 
+## Process
+
+{{< new-in "0.119.0" >}}
+
+{{% funcsig %}}
+images.Overlay SRC SPEC
+{{% /funcsig %}}
+
+A general purpose image processing function.
+
+This filter has all the same options as the [Process](/content-management/image-processing/#process) method, but using it as a filter may be more effective if you need to apply multiple filters to an image:
+
+```go-html-template
+{{ $filters := slice 
+  images.Grayscale
+  (images.GaussianBlur 8)
+  (images.Process "resize 200x jpg q30") 
+}}
+{{ $img = $img | images.Filter $filters }}
+```
+
+
 ## Overlay
 
 {{% funcsig %}}
@@ -36,6 +58,8 @@ The above will overlay `$logo` in the upper left corner of `$img` (at position `
 
 ## Opacity
 
+{{< new-in "0.119.0" >}}
+
 {{% funcsig %}}
 images.Opacity SRC OPACITY
 {{% /funcsig %}}
@@ -47,6 +71,15 @@ The OPACITY parameter must be in range (0, 1).
 {{ $img := $img.Filter (images.Opacity 0.5 )}}
 ```
 
+Note that target format must support transparency, e.g. PNG. If the source image is e.g. JPG, the most effective way would be to combine it with the [`Process`] filter:
+
+```go-html-template
+{{ $png := $jpg.Filter 
+  (images.Opacity 0.5)
+  (images.Process "png") 
+}}
+```
+
 ## Text
 
 Using the `Text` filter, you can add text to an image.
@@ -237,3 +270,5 @@ images.ImageConfig PATH
 favicon.ico: {{ .Width }} x {{ .Height }}
 {{ end }}
 ```
+
+[`Process`]: #process
\ No newline at end of file
index 5b030fde97d5b6ea334a81fcc5c8ed40d67d9d41..9a24fd21c206abfc0d227d251b030525cc541eb6 100644 (file)
@@ -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))
 }
index 63e90d2ada954c56c11a399d8f7c23f2a34d16fe..dca8ff0e8fc972b7610e6296cc13cd5877df7fc9 100644 (file)
@@ -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{
index 714d0e26d90a283a27effaadb6ac5914f1ad2568..1637d0cf8b09aa4ef11e81552383ee699caf72ed 100644 (file)
@@ -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 (file)
index 0000000..984ac3c
--- /dev/null
@@ -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
+}
index 2075079dcc2fa109c002bf03e4d322acded8b381..5570b15d669d6471564926ef81feaa957b6638e1 100644 (file)
@@ -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|",
+       )
 }