mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
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
This commit is contained in:
parent
ef0e7149d6
commit
6a246d1152
6 changed files with 204 additions and 16 deletions
|
@ -12,6 +12,28 @@ toc: true
|
||||||
|
|
||||||
See [images.Filter](#filter) for how to apply these filters to an image.
|
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
|
## Overlay
|
||||||
|
|
||||||
{{% funcsig %}}
|
{{% funcsig %}}
|
||||||
|
@ -36,6 +58,8 @@ The above will overlay `$logo` in the upper left corner of `$img` (at position `
|
||||||
|
|
||||||
## Opacity
|
## Opacity
|
||||||
|
|
||||||
|
{{< new-in "0.119.0" >}}
|
||||||
|
|
||||||
{{% funcsig %}}
|
{{% funcsig %}}
|
||||||
images.Opacity SRC OPACITY
|
images.Opacity SRC OPACITY
|
||||||
{{% /funcsig %}}
|
{{% /funcsig %}}
|
||||||
|
@ -47,6 +71,15 @@ The OPACITY parameter must be in range (0, 1).
|
||||||
{{ $img := $img.Filter (images.Opacity 0.5 )}}
|
{{ $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
|
## Text
|
||||||
|
|
||||||
Using the `Text` filter, you can add text to an image.
|
Using the `Text` filter, you can add text to an image.
|
||||||
|
@ -237,3 +270,5 @@ images.ImageConfig PATH
|
||||||
favicon.ico: {{ .Width }} x {{ .Height }}
|
favicon.ico: {{ .Width }} x {{ .Height }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[`Process`]: #process
|
|
@ -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,
|
// 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.
|
// but it also supports e.g. format conversions without any resize action.
|
||||||
func (i *imageResource) Process(spec string) (images.ImageResource, error) {
|
func (i *imageResource) Process(spec string) (images.ImageResource, error) {
|
||||||
var action string
|
action, options := i.resolveActionOptions(spec)
|
||||||
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 i.processActionOptions(action, options)
|
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) {
|
func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
|
||||||
conf := images.GetDefaultImageConfig("filter", i.Proc.Cfg)
|
var conf images.ImageConfig
|
||||||
|
|
||||||
var gfilters []gift.Filter
|
var gfilters []gift.Filter
|
||||||
|
|
||||||
|
@ -253,14 +245,77 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
|
||||||
gfilters = append(gfilters, images.ToFilters(f)...)
|
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.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.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) {
|
func (i *imageResource) processActionSpec(action, spec string) (images.ImageResource, error) {
|
||||||
return i.processActionOptions(action, strings.Fields(spec))
|
return i.processActionOptions(action, strings.Fields(spec))
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,15 @@ const filterAPIVersion = 0
|
||||||
|
|
||||||
type Filters struct{}
|
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.
|
// Overlay creates a filter that overlays src at position x y.
|
||||||
func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter {
|
func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter {
|
||||||
return filter{
|
return filter{
|
||||||
|
|
|
@ -201,7 +201,7 @@ func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) {
|
||||||
return p.exifDecoder.Decode(r)
|
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
|
var filters []gift.Filter
|
||||||
|
|
||||||
if conf.Rotate != 0 {
|
if conf.Rotate != 0 {
|
||||||
|
@ -246,6 +246,14 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi
|
||||||
default:
|
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 {
|
if len(filters) == 0 {
|
||||||
return p.resolveSrc(src, conf.TargetFormat), nil
|
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}
|
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 {
|
func ToFilters(in any) []gift.Filter {
|
||||||
switch v := in.(type) {
|
switch v := in.(type) {
|
||||||
case []gift.Filter:
|
case []gift.Filter:
|
||||||
|
|
43
resources/images/process.go
Normal file
43
resources/images/process.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -73,7 +73,6 @@ anigif: {{ $anigif.RelPermalink }}|{{ $anigif.Width }}|{{ $anigif.Height }}|{{ $
|
||||||
b.Build()
|
b.Build()
|
||||||
|
|
||||||
assertImages()
|
assertImages()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSVGError(t *testing.T) {
|
func TestSVGError(t *testing.T) {
|
||||||
|
@ -98,7 +97,6 @@ Width: {{ $svg.Width }}
|
||||||
|
|
||||||
b.Assert(err, qt.IsNotNil)
|
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 }}`)
|
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.
|
// Issue 10255.
|
||||||
|
@ -137,5 +135,36 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||||
b.AssertFileCount("public/images", 1)
|
b.AssertFileCount("public/images", 1)
|
||||||
b.Build()
|
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|",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue