Skip to content

Commit

Permalink
Allow images to be cropped without being resized
Browse files Browse the repository at this point in the history
Introduces the Crop method for image processing which implements gift.CropToSize. Also allows a smartCrop without resizing, and updates the documentation.

Fixes #9499
  • Loading branch information
johnsvenn authored and bep committed Feb 23, 2022
1 parent aebde49 commit 7732da9
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 8 deletions.
24 changes: 18 additions & 6 deletions docs/content/en/content-management/image-processing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion docs/layouts/shortcodes/imgproc.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
4 changes: 4 additions & 0 deletions resources/errorResource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
13 changes: 13 additions & 0 deletions resources/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions resources/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion resources/images/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
15 changes: 15 additions & 0 deletions resources/images/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions resources/resource/resourcetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions resources/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 7732da9

Please sign in to comment.