mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Add $image.Process
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
This commit is contained in:
parent
c32094ace1
commit
ef0e7149d6
13 changed files with 216 additions and 98 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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/"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue