Allow images to be cropped without being resized

Introduces the Crop method for image processing which implements gift.CropToSize. Also allows a smartCrop without resizing, and updates the documentation.

Fixes #9499
This commit is contained in:
John Elliott 2022-02-22 16:50:23 +00:00 committed by Bjørn Erik Pedersen
parent aebde49b88
commit 7732da9f93
9 changed files with 75 additions and 8 deletions

View file

@ -39,7 +39,7 @@ The `image` resource can also be retrieved from a [global resource]({{< relref "
## Image Processing Methods
The `image` resource implements the `Resize`, `Fit`, `Fill`, and `Filter` methods, each returning a transformed image using the specified dimensions and processing options.
The `image` resource implements the `Resize`, `Fit`, `Fill`, `Crop`, and `Filter` methods, each returning a transformed image using the specified dimensions and processing options.
{{% note %}}
Metadata (EXIF, IPTC, XMP, etc.) is not preserved during image transformation. Use the [`Exif`](#exif) method with the _original_ image to extract EXIF metadata from JPEG or TIFF images.
@ -70,12 +70,20 @@ Scale down the image to fit the given dimensions while maintaining aspect ratio.
### Fill
Resize and crop the image to match the given dimensions. Both height and width are required.
Crop and resize the image to match the given dimensions. Both height and width are required.
```go
{{ $image := $resource.Fill "600x400" }}
```
### Crop
Crop the image to match the given dimensions without resizing. Both height and width are required.
```go
{{ $image := $resource.Crop "400x400" }}
```
### Filter
Apply one or more filters to your image. See [Image Filters](/functions/images/#image-filters) for a full list.
@ -203,7 +211,7 @@ Rotates an image by the given angle counter-clockwise. The rotation will be perf
### Anchor
Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner.
Only relevant for the `Crop` and `Fill` methods. This is useful for thumbnail generation where the main motive is located in, say, the left corner.
Valid values are `Smart`, `Center`, `TopLeft`, `Top`, `TopRight`, `Left`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`.
@ -249,6 +257,8 @@ _The photo of the sunset used in the examples below is Copyright [Bjørn Erik Pe
{{< imgproc sunset Fit "90x90" />}}
{{< imgproc sunset Crop "250x250 center" />}}
{{< imgproc sunset Resize "300x q10" />}}
This is the shortcode used in the examples above:
@ -286,7 +296,7 @@ quality = 75
# Valid values are "picture", "photo", "drawing", "icon", or "text".
hint = "photo"
# Anchor used when cropping pictures.
# Anchor used when cropping pictures with either .Fill or .Crop
# Default is "smart" which does Smart Cropping, using https://github.com/muesli/smartcrop
# Smart Cropping is content aware and tries to find the best crop for each image.
# Valid values are Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight
@ -323,12 +333,14 @@ disableLatLong = false
## Smart Cropping of Images
By default, Hugo will use [Smartcrop](https://github.com/muesli/smartcrop), a library created by [muesli](https://github.com/muesli), when cropping images with `.Fill`. You can set the anchor point manually, but in most cases the smart option will make a good choice. And we will work with the library author to improve this in the future.
By default, Hugo will use [Smartcrop](https://github.com/muesli/smartcrop), a library created by [muesli](https://github.com/muesli), when cropping images with `.Fill` or `.Crop`. You can set the anchor point manually, but in most cases the smart option will make a good choice. And we will work with the library author to improve this in the future.
An example using the sunset image from above:
Examples using the sunset image from above:
{{< imgproc sunset Fill "200x200 smart" />}}
{{< imgproc sunset Crop "200x200 smart" />}}
## Image Processing Performance Consideration
Processed images are stored below `<project-dir>/resources` (can be set with `resourceDir` config setting). This folder is deliberately placed in the project, as it is recommended to check these into source control as part of the project. These images are not "Hugo fast" to generate, but once generated they can be reused.

View file

@ -7,8 +7,10 @@
{{ .Scratch.Set "image" ($original.Resize $options) }}
{{ else if eq $command "Fill"}}
{{ .Scratch.Set "image" ($original.Fill $options) }}
{{ else if eq $command "Crop"}}
{{ .Scratch.Set "image" ($original.Crop $options) }}
{{ else }}
{{ errorf "Invalid image processing command: Must be one of Fit, Fill or Resize."}}
{{ errorf "Invalid image processing command: Must be one of Crop, Fit, Fill or Resize."}}
{{ end }}
{{ $image := .Scratch.Get "image" }}
<figure style="padding: 0.25rem; margin: 2rem 0; background-color: #cccc">

View file

@ -100,6 +100,10 @@ func (e *errorResource) Width() int {
panic(e.error)
}
func (e *errorResource) Crop(spec string) (resource.Image, error) {
panic(e.error)
}
func (e *errorResource) Fill(spec string) (resource.Image, error) {
panic(e.error)
}

View file

@ -181,6 +181,19 @@ func (i *imageResource) Resize(spec string) (resource.Image, error) {
})
}
// Crop the image to the specified dimensions without resizing using the given anchor point.
// Space delimited config: 200x300 TopLeft
func (i *imageResource) Crop(spec string) (resource.Image, 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)
})
}
// Fit scales down the image using the specified resample filter to fit the specified
// maximum width and height.
func (i *imageResource) Fit(spec string) (resource.Image, error) {

View file

@ -137,6 +137,22 @@ func TestImageTransformBasic(t *testing.T) {
filledAgain, err := image.Fill("200x100 bottomLeft")
c.Assert(err, qt.IsNil)
c.Assert(filled, eq, filledAgain)
cropped, err := image.Crop("300x300 topRight")
c.Assert(err, qt.IsNil)
c.Assert(cropped.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x300_crop_q68_linear_topright.jpg")
assertWidthHeight(cropped, 300, 300)
smartcropped, err := image.Crop("200x200 smart")
c.Assert(err, qt.IsNil)
c.Assert(smartcropped.RelPermalink(), qt.Equals, fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_crop_q68_linear_smart%d.jpg", 1))
assertWidthHeight(smartcropped, 200, 200)
// Check cache
croppedAgain, err := image.Crop("300x300 topRight")
c.Assert(err, qt.IsNil)
c.Assert(cropped, eq, croppedAgain)
}
func TestImageTransformFormat(t *testing.T) {

View file

@ -364,7 +364,7 @@ func (i ImageConfig) GetKey(format Format) string {
k += "_" + i.FilterStr
if strings.EqualFold(i.Action, "fill") {
if strings.EqualFold(i.Action, "fill") || strings.EqualFold(i.Action, "crop") {
k += "_" + anchor
}

View file

@ -207,6 +207,21 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi
switch conf.Action {
case "resize":
filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
case "crop":
if conf.AnchorStr == smartCropIdentifier {
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
if err != nil {
return nil, err
}
// First crop using the bounds returned by smartCrop.
filters = append(filters, gift.Crop(bounds))
// Then center crop the image to get an image the desired size without resizing.
filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor))
} else {
filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
}
case "fill":
if conf.AnchorStr == smartCropIdentifier {
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)

View file

@ -62,6 +62,7 @@ type Image interface {
type ImageOps interface {
Height() int
Width() int
Crop(spec string) (Image, error)
Fill(spec string) (Image, error)
Fit(spec string) (Image, error)
Resize(spec string) (Image, error)

View file

@ -176,6 +176,10 @@ func (r *resourceAdapter) Data() interface{} {
return r.target.Data()
}
func (r *resourceAdapter) Crop(spec string) (resource.Image, error) {
return r.getImageOps().Crop(spec)
}
func (r *resourceAdapter) Fill(spec string) (resource.Image, error) {
return r.getImageOps().Fill(spec)
}