]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add $image.Process
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 22 Sep 2023 13:15:16 +0000 (15:15 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 24 Sep 2023 09:54:29 +0000 (11:54 +0200)
Which supports all the existing actions: resize, crop, fit, fill.

But it also allows plain format conversions:

```
{{ $img = $img.Process "webp" }}
```

Which will be a simple re-encoding of the source image.

Fixes #11483

13 files changed:
common/hstrings/strings.go
docs/content/en/content-management/image-processing/index.md
helpers/general.go
hugolib/site_output_test.go
resources/errorResource.go
resources/image.go
resources/image_test.go
resources/images/config.go
resources/images/config_test.go
resources/images/image.go
resources/images/image_resource.go
resources/page/permalinks.go
resources/transform.go

index 2fd791f43ba9aa1f782acf735bd0687a36ca745b..88df97607cf232cfd6e8db7108daec1337881f9c 100644 (file)
@@ -99,3 +99,26 @@ var reCache = regexpCache{re: make(map[string]*regexp.Regexp)}
 func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
        return reCache.getOrCompileRegexp(pattern)
 }
+
+// InSlice checks if a string is an element of a slice of strings
+// and returns a boolean value.
+func InSlice(arr []string, el string) bool {
+       for _, v := range arr {
+               if v == el {
+                       return true
+               }
+       }
+       return false
+}
+
+// InSlicEqualFold checks if a string is an element of a slice of strings
+// and returns a boolean value.
+// It uses strings.EqualFold to compare.
+func InSlicEqualFold(arr []string, el string) bool {
+       for _, v := range arr {
+               if strings.EqualFold(v, el) {
+                       return true
+               }
+       }
+       return false
+}
index 63c9d4d2fa2a892269f541e2c6899ed97d0f8178..124cadc68eb014fa56ab6c2aae4460e34421fdae 100644 (file)
@@ -101,12 +101,41 @@ Example 4: Skips rendering if there's problem accessing a remote resource.
 
 ## Image processing methods
 
-The `image` resource implements the  [`Resize`], [`Fit`], [`Fill`], [`Crop`], [`Filter`], [`Colors`] and [`Exif`] methods.
+The `image` resource implements the  [`Process`],  [`Resize`], [`Fit`], [`Fill`], [`Crop`], [`Filter`], [`Colors`] and [`Exif`] methods.
 
 {{% note %}}
 Metadata (EXIF, IPTC, XMP, etc.) is not preserved during image transformation. Use the [`Exif`] method with the _original_ image to extract EXIF metadata from JPEG or TIFF images.
 {{% /note %}}
 
+### Process
+
+{{< new-in "0.119.0" >}}
+
+Process processes the image with the given specification. The specification can contain an optional action, one of `resize`, `crop`, `fit` or `fill`. This means that you can use this method instead of [`Resize`], [`Fit`], [`Fill`], or [`Crop`]. 
+
+See [Options](#image-processing-options) for available options.
+
+You can also use this method apply image processing that does not need any scaling, e.g. format conversions:
+
+```go-html-template
+{{/* Convert the image from JPG to PNG. */}}
+{{ $png := $jpg.Process "png" }}
+```
+
+Some more examples:
+
+```go-html-template
+{{/* Rotate the image 90 degrees counter-clockwise. */}}
+{{ $image := $image.Process "r90" }}
+
+{{/* Scaling actions. */}}
+{{ $image := $image.Process "resize 600x" }}
+{{ $image := $image.Process "crop 600x400" }}
+{{ $image := $image.Process "fit 600x400" }}
+{{ $image := $image.Process "fill 600x400" }}
+```
+
+
 ### Resize
 
 Resize an image to the specified width and/or height.
@@ -477,6 +506,7 @@ hugo --gc
 [github.com/disintegration/imaging]: <https://github.com/disintegration/imaging#image-resizing>
 [Smartcrop]: <https://github.com/muesli/smartcrop#smartcrop>
 [Exif]: <https://en.wikipedia.org/wiki/Exif>
+[`Process`]: #process
 [`Colors`]: #colors
 [`Crop`]: #crop
 [`Exif`]: #exif
index f8f273397dddbdb1ced94e434d6b2ac26f7a0800..e484b92f09602d05a836e434de7da20407d47b3f 100644 (file)
@@ -53,18 +53,6 @@ func TCPListen() (net.Listener, *net.TCPAddr, error) {
        }
        l.Close()
        return nil, nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr)
-
-}
-
-// InStringArray checks if a string is an element of a slice of strings
-// and returns a boolean value.
-func InStringArray(arr []string, el string) bool {
-       for _, v := range arr {
-               if v == el {
-                       return true
-               }
-       }
-       return false
 }
 
 // FirstUpper returns a string with the first character as upper case.
index ce415a8242f4f642c5597beb60a87cab8caf8ba2..c2a14c3ebc69d5fba0f98272799842da3eb4649f 100644 (file)
@@ -20,12 +20,12 @@ import (
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/common/hstrings"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/resources/kinds"
 
        "github.com/spf13/afero"
 
-       "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/output"
 )
 
@@ -152,7 +152,7 @@ Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.P
 
        // There is currently always a JSON output to make it simpler ...
        altFormats := lenOut - 1
-       hasHTML := helpers.InStringArray(outputs, "html")
+       hasHTML := hstrings.InSlice(outputs, "html")
        b.AssertFileContent("public/index.json",
                "List JSON",
                fmt.Sprintf("Alt formats: %d", altFormats),
@@ -205,7 +205,7 @@ Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.P
        b.Assert(json.RelPermalink(), qt.Equals, "/blog/index.json")
        b.Assert(json.Permalink(), qt.Equals, "http://example.com/blog/index.json")
 
-       if helpers.InStringArray(outputs, "cal") {
+       if hstrings.InSlice(outputs, "cal") {
                cal := of.Get("calendar")
                b.Assert(cal, qt.Not(qt.IsNil))
                b.Assert(cal.RelPermalink(), qt.Equals, "/blog/index.ics")
index c8c32dfc3135d6f144fb21e11afa26d7f39ce342..d94207b79e8519d568f00a2f0042db0c1bca33e7 100644 (file)
@@ -100,6 +100,10 @@ func (e *errorResource) Width() int {
        panic(e.ResourceError)
 }
 
+func (e *errorResource) Process(spec string) (images.ImageResource, error) {
+       panic(e.ResourceError)
+}
+
 func (e *errorResource) Crop(spec string) (images.ImageResource, error) {
        panic(e.ResourceError)
 }
index ad2f9de322739574fd87bc733ae85ba6bf413e3e..5b030fde97d5b6ea334a81fcc5c8ed40d67d9d41 100644 (file)
@@ -31,6 +31,7 @@ import (
 
        color_extractor "github.com/marekm4/color-extractor"
 
+       "github.com/gohugoio/hugo/common/hstrings"
        "github.com/gohugoio/hugo/common/paths"
        "github.com/gohugoio/hugo/identity"
 
@@ -198,75 +199,49 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource,
        }, nil
 }
 
+var imageActions = []string{images.ActionResize, images.ActionCrop, images.ActionFit, images.ActionFill}
+
+// Process processes the image with the given spec.
+// The spec can contain an optional action, one of "resize", "crop", "fit" or "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.
+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
+               }
+       }
+       return i.processActionOptions(action, options)
+}
+
 // Resize resizes the image to the specified width and height using the specified resampling
 // filter and returns the transformed image. If one of width or height is 0, the image aspect
 // ratio is preserved.
 func (i *imageResource) Resize(spec string) (images.ImageResource, error) {
-       conf, err := i.decodeImageConfig("resize", spec)
-       if err != nil {
-               return nil, err
-       }
-
-       return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
-               return i.Proc.ApplyFiltersFromConfig(src, conf)
-       })
+       return i.processActionSpec(images.ActionResize, spec)
 }
 
 // Crop the image to the specified dimensions without resizing using the given anchor point.
 // Space delimited config, e.g. `200x300 TopLeft`.
 func (i *imageResource) Crop(spec string) (images.ImageResource, error) {
-       conf, err := i.decodeImageConfig("crop", spec)
-       if err != nil {
-               return nil, err
-       }
-
-       return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
-               return i.Proc.ApplyFiltersFromConfig(src, conf)
-       })
+       return i.processActionSpec(images.ActionCrop, spec)
 }
 
 // Fit scales down the image using the specified resample filter to fit the specified
 // maximum width and height.
 func (i *imageResource) Fit(spec string) (images.ImageResource, error) {
-       conf, err := i.decodeImageConfig("fit", spec)
-       if err != nil {
-               return nil, err
-       }
-
-       return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
-               return i.Proc.ApplyFiltersFromConfig(src, conf)
-       })
+       return i.processActionSpec(images.ActionFit, spec)
 }
 
 // Fill scales the image to the smallest possible size that will cover the specified dimensions,
 // crops the resized image to the specified dimensions using the given anchor point.
 // Space delimited config, e.g. `200x300 TopLeft`.
 func (i *imageResource) Fill(spec string) (images.ImageResource, error) {
-       conf, err := i.decodeImageConfig("fill", spec)
-       if err != nil {
-               return nil, err
-       }
-
-       img, err := i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
-               return i.Proc.ApplyFiltersFromConfig(src, conf)
-       })
-
-       if err != nil {
-               return nil, err
-       }
-
-       if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
-               // See https://github.com/gohugoio/hugo/issues/7955
-               // Smartcrop fails silently in some rare cases.
-               // Fall back to a center fill.
-               conf.Anchor = gift.CenterAnchor
-               conf.AnchorStr = "center"
-               return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
-                       return i.Proc.ApplyFiltersFromConfig(src, conf)
-               })
-       }
-
-       return img, err
+       return i.processActionSpec(images.ActionFill, spec)
 }
 
 func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
@@ -286,6 +261,39 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
        })
 }
 
+func (i *imageResource) processActionSpec(action, spec string) (images.ImageResource, error) {
+       return i.processActionOptions(action, strings.Fields(spec))
+}
+
+func (i *imageResource) processActionOptions(action string, options []string) (images.ImageResource, error) {
+       conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
+       if err != nil {
+               return nil, err
+       }
+
+       img, err := i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
+               return i.Proc.ApplyFiltersFromConfig(src, conf)
+       })
+       if err != nil {
+               return nil, err
+       }
+
+       if action == images.ActionFill {
+               if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
+                       // See https://github.com/gohugoio/hugo/issues/7955
+                       // Smartcrop fails silently in some rare cases.
+                       // Fall back to a center fill.
+                       conf.Anchor = gift.CenterAnchor
+                       conf.AnchorStr = "center"
+                       return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
+                               return i.Proc.ApplyFiltersFromConfig(src, conf)
+                       })
+               }
+       }
+
+       return img, nil
+}
+
 // Serialize image processing. The imaging library spins up its own set of Go routines,
 // so there is not much to gain from adding more load to the mix. That
 // can even have negative effect in low resource scenarios.
@@ -362,7 +370,8 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
 }
 
 func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConfig, error) {
-       conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg, i.Format)
+       options := strings.Fields(spec)
+       conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
        if err != nil {
                return conf, err
        }
index 751ef3f5d18c45230958afabb8fa4c15967fa430..96cc07b3b209dc31e3045772e96157ec9b0919f1 100644 (file)
@@ -84,10 +84,7 @@ func TestImageTransformBasic(t *testing.T) {
        fileCache := spec.FileCaches.ImageCache().Fs
 
        assertWidthHeight := func(img images.ImageResource, w, h int) {
-               c.Helper()
-               c.Assert(img, qt.Not(qt.IsNil))
-               c.Assert(img.Width(), qt.Equals, w)
-               c.Assert(img.Height(), qt.Equals, h)
+               assertWidthHeight(c, img, w, h)
        }
 
        colors, err := image.Colors()
@@ -164,6 +161,45 @@ func TestImageTransformBasic(t *testing.T) {
        c.Assert(cropped, qt.Equals, croppedAgain)
 }
 
+func TestImageProcess(t *testing.T) {
+       c := qt.New(t)
+       _, img := fetchSunset(c)
+       resized, err := img.Process("resiZe 300x200")
+       c.Assert(err, qt.IsNil)
+       assertWidthHeight(c, resized, 300, 200)
+       rotated, err := resized.Process("R90")
+       c.Assert(err, qt.IsNil)
+       assertWidthHeight(c, rotated, 200, 300)
+       converted, err := img.Process("png")
+       c.Assert(err, qt.IsNil)
+       c.Assert(converted.MediaType().Type, qt.Equals, "image/png")
+
+       checkProcessVsMethod := func(action, spec string) {
+               var expect images.ImageResource
+               var err error
+               switch action {
+               case images.ActionCrop:
+                       expect, err = img.Crop(spec)
+               case images.ActionFill:
+                       expect, err = img.Fill(spec)
+               case images.ActionFit:
+                       expect, err = img.Fit(spec)
+               case images.ActionResize:
+                       expect, err = img.Resize(spec)
+               }
+               c.Assert(err, qt.IsNil)
+               got, err := img.Process(spec + " " + action)
+               c.Assert(err, qt.IsNil)
+               assertWidthHeight(c, got, expect.Width(), expect.Height())
+               c.Assert(got.MediaType(), qt.Equals, expect.MediaType())
+       }
+
+       checkProcessVsMethod(images.ActionCrop, "300x200 topleFt")
+       checkProcessVsMethod(images.ActionFill, "300x200 topleft")
+       checkProcessVsMethod(images.ActionFit, "300x200 png")
+       checkProcessVsMethod(images.ActionResize, "300x R90")
+}
+
 func TestImageTransformFormat(t *testing.T) {
        c := qt.New(t)
 
@@ -852,3 +888,10 @@ func BenchmarkResizeParallel(b *testing.B) {
                }
        })
 }
+
+func assertWidthHeight(c *qt.C, img images.ImageResource, w, h int) {
+       c.Helper()
+       c.Assert(img, qt.Not(qt.IsNil))
+       c.Assert(img.Width(), qt.Equals, w)
+       c.Assert(img.Height(), qt.Equals, h)
+}
index a3ca0c3597ab021e46e9da3dd26dbfb9d159f671..186f8fa6bce889a29f938ce78a4ed185a54834ce 100644 (file)
@@ -14,6 +14,7 @@
 package images
 
 import (
+       "errors"
        "fmt"
        "image/color"
        "strconv"
@@ -24,13 +25,18 @@ import (
        "github.com/gohugoio/hugo/media"
        "github.com/mitchellh/mapstructure"
 
-       "errors"
-
        "github.com/bep/gowebp/libwebp/webpoptions"
 
        "github.com/disintegration/gift"
 )
 
+const (
+       ActionResize = "resize"
+       ActionCrop   = "crop"
+       ActionFit    = "fit"
+       ActionFill   = "fill"
+)
+
 var (
        imageFormats = map[string]Format{
                ".jpg":  JPEG,
@@ -90,7 +96,6 @@ var hints = map[string]webpoptions.EncodingPreset{
 }
 
 var imageFilters = map[string]gift.Resampling{
-
        strings.ToLower("NearestNeighbor"):   gift.NearestNeighborResampling,
        strings.ToLower("Box"):               gift.BoxResampling,
        strings.ToLower("Linear"):            gift.LinearResampling,
@@ -194,23 +199,23 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
                return nil, fmt.Errorf("failed to decode media types: %w", err)
        }
        return ns, nil
-
 }
 
-func DecodeImageConfig(action, config string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
+func DecodeImageConfig(action string, options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
        var (
                c   ImageConfig = GetDefaultImageConfig(action, defaults)
                err error
        )
 
+       action = strings.ToLower(action)
+
        c.Action = action
 
-       if config == "" {
-               return c, errors.New("image config cannot be empty")
+       if options == nil {
+               return c, errors.New("image options cannot be empty")
        }
 
-       parts := strings.Fields(config)
-       for _, part := range parts {
+       for _, part := range options {
                part = strings.ToLower(part)
 
                if part == smartCropIdentifier {
@@ -272,19 +277,21 @@ func DecodeImageConfig(action, config string, defaults *config.ConfigNamespace[I
        }
 
        switch c.Action {
-       case "crop", "fill", "fit":
+       case ActionCrop, ActionFill, ActionFit:
                if c.Width == 0 || c.Height == 0 {
                        return c, errors.New("must provide Width and Height")
                }
-       case "resize":
+       case ActionResize:
                if c.Width == 0 && c.Height == 0 {
                        return c, errors.New("must provide Width or Height")
                }
        default:
-               return c, fmt.Errorf("BUG: unknown action %q encountered while decoding image configuration", c.Action)
+               if c.Width != 0 || c.Height != 0 {
+                       return c, errors.New("width or height are not supported for this action")
+               }
        }
 
-       if c.FilterStr == "" {
+       if action != "" && c.FilterStr == "" {
                c.FilterStr = defaults.Config.Imaging.ResampleFilter
                c.Filter = defaults.Config.ResampleFilter
        }
@@ -293,7 +300,7 @@ func DecodeImageConfig(action, config string, defaults *config.ConfigNamespace[I
                c.Hint = webpoptions.EncodingPresetPhoto
        }
 
-       if c.AnchorStr == "" {
+       if action != "" && c.AnchorStr == "" {
                c.AnchorStr = defaults.Config.Imaging.Anchor
                c.Anchor = defaults.Config.Anchor
        }
@@ -391,7 +398,7 @@ func (i ImageConfig) GetKey(format Format) string {
 
        k += "_" + i.FilterStr
 
-       if strings.EqualFold(i.Action, "fill") || strings.EqualFold(i.Action, "crop") {
+       if i.Action == ActionFill || i.Action == ActionCrop {
                k += "_" + anchor
        }
 
@@ -437,7 +444,6 @@ func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
        i.ResampleFilter = filter
 
        return nil
-
 }
 
 // ImagingConfig contains default image processing configuration. This will be fetched
@@ -487,7 +493,6 @@ func (cfg *ImagingConfig) init() error {
 }
 
 type ExifConfig struct {
-
        // Regexp matching the Exif fields you want from the (massive) set of Exif info
        // available. As we cache this info to disk, this is for performance and
        // disk space reasons more than anything.
index 2e0d6635d9292784156521ead467cbdb822ee272..86f70c1bfd7af57ea3c451b0c006a4387964fd67 100644 (file)
@@ -106,7 +106,7 @@ func TestDecodeImageConfig(t *testing.T) {
                if err != nil {
                        t.Fatal(err)
                }
-               result, err := DecodeImageConfig(this.action, this.in, cfg, PNG)
+               result, err := DecodeImageConfig(this.action, strings.Fields(this.in), cfg, PNG)
                if b, ok := this.expect.(bool); ok && !b {
                        if err == nil {
                                t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
index 672e8578f2176e071b445fe880d5bee6d876f915..714d0e26d90a283a27effaadb6ac5914f1ad2568 100644 (file)
@@ -14,6 +14,7 @@
 package images
 
 import (
+       "errors"
        "fmt"
        "image"
        "image/color"
@@ -35,8 +36,6 @@ import (
        "golang.org/x/image/bmp"
        "golang.org/x/image/tiff"
 
-       "errors"
-
        "github.com/gohugoio/hugo/common/hugio"
 )
 
@@ -245,7 +244,11 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi
        case "fit":
                filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter))
        default:
-               return nil, fmt.Errorf("unsupported action: %q", conf.Action)
+
+       }
+
+       if len(filters) == 0 {
+               return p.resolveSrc(src, conf.TargetFormat), nil
        }
 
        img, err := p.doFilter(src, conf.TargetFormat, filters...)
@@ -260,8 +263,17 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.
        return p.doFilter(src, 0, filters...)
 }
 
-func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters ...gift.Filter) (image.Image, error) {
+func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image.Image {
+       if giph, ok := src.(Giphy); ok {
+               g := giph.GIF()
+               if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) {
+                       src = g.Image[0]
+               }
+       }
+       return src
+}
 
+func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters ...gift.Filter) (image.Image, error) {
        filter := gift.New(filters...)
 
        if giph, ok := src.(Giphy); ok {
index dcd2b47416bcaffe4f1cc168ff0d3bedd0cd7c24..be40418b1fd6713f92ff2d7ad33b614617c0439c 100644 (file)
@@ -33,6 +33,9 @@ type ImageResourceOps interface {
        // Width returns the width of the Image.
        Width() int
 
+       // Process applies the given image processing options to the image.
+       Process(spec string) (ImageResource, error)
+
        // Crop an image to match the given dimensions without resizing.
        // You must provide both width and height.
        // Use the anchor option to change the crop box anchor point.
index ee9608dac328e4fc6eb81b86b747f9d771eebc88..4577f5240508eeb94bbd526c82f0964f020bb5c5 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "errors"
        "fmt"
        "os"
        "path"
@@ -23,8 +24,7 @@ import (
        "strings"
        "time"
 
-       "errors"
-
+       "github.com/gohugoio/hugo/common/hstrings"
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/resources/kinds"
@@ -396,7 +396,6 @@ func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
                }
                return s[n1:n2]
        }
-
 }
 
 var permalinksKindsSupport = []string{kinds.KindPage, kinds.KindSection, kinds.KindTaxonomy, kinds.KindTerm}
@@ -425,7 +424,7 @@ func DecodePermalinksConfig(m map[string]any) (map[string]map[string]string, err
                        // [permalinks.key]
                        //   xyz = ???
 
-                       if helpers.InStringArray(permalinksKindsSupport, k) {
+                       if hstrings.InSlice(permalinksKindsSupport, k) {
                                // TODO: warn if we overwrite an already set value
                                for k2, v2 := range v {
                                        switch v2 := v2.(type) {
index eb7d8fb0b3c7fb696a7e04a03f365cf01c7b62a0..0c38345ad1bc2514c8c7f543e69566cfe354614f 100644 (file)
@@ -195,6 +195,10 @@ func (r resourceAdapter) cloneTo(targetPath string) resource.Resource {
        return &r
 }
 
+func (r *resourceAdapter) Process(spec string) (images.ImageResource, error) {
+       return r.getImageOps().Process(spec)
+}
+
 func (r *resourceAdapter) Crop(spec string) (images.ImageResource, error) {
        return r.getImageOps().Crop(spec)
 }
@@ -287,7 +291,6 @@ func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransfo
 }
 
 func (r resourceAdapter) TransformWithContext(ctx context.Context, t ...ResourceTransformation) (ResourceTransformer, error) {
-
        r.resourceTransformations = &resourceTransformations{
                transformations: append(r.transformations, t...),
        }
@@ -459,7 +462,6 @@ func (r *resourceAdapter) transform(publish, setContent bool) error {
                                        errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS with transpiler set to 'libsass'."
                                } else if tr.Key().Name == "tocss-dart" {
                                        errMsg = ". You need dart-sass-embedded in your system $PATH."
-
                                } else if tr.Key().Name == "babel" {
                                        errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/"
                                }