mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
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:
parent
aebde49b88
commit
7732da9f93
9 changed files with 75 additions and 8 deletions
|
@ -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.
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue