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.
|
||||
|
||||
## 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
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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:
|
||||
|
|
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()
|
||||
|
||||
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|",
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue