DEV Community

Cover image for Crop it like it's hot: Cropping Images in Sanity v3
Cameron
Cameron

Posted on

Crop it like it's hot: Cropping Images in Sanity v3

If you're reading this, you probably couldn't find a solid guide on how to set up image cropping with Sanity either.

I think this is partly because Sanity provides a tonne of flexibility to manipulate your image using their CDN and image builder (more on this later). Which is both fantastic, and a touch overwhelming.

In this guide, you'll learn the easiest and clearest way that I discovered on my path to set up Sanity cropping in my frontend application. This guide is not a comprehensive overview into everything you can do with Sanity's image manipulation tools. For that, I'd recommend the Sanity documentation.

Prerequisites

  • Sanity project (v3) with your document queries prepared
  • Frontend project with the following dependencies:
  1. @sanity/image-url
  2. @sanity/asset-utils

Let's jump in.

Updating your Sanity schema

{
  name: 'myImage',
  title: 'My Image',
  type: 'image',
  options: {
    hotspot: true
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

After nesting this options object into your image schema, you should see in your Studio instance the 'crop' aciton in the top right of the image form.

Image description

Updating your frontend code

We know that when you query your Sanity document(s), images are returned not with their source, but with a reference. And, when you add the hotspot in your schema as per section 1, it will also return the cropping/hotspot properties specified in Sanity studio. These are properties you can leverage in your frontend code however you like. Sanity does not have a direct pattern for how these should be used (albeit with good reason). That being said, we'll dive into one pattern you can use to easily apply cropping to images. Let's get started.

The boilerplate

I'll assume you've already set up your query(s) to retrieve the image(s) returned from Sanity. For more info on this I would recommend the Sanity documentation.

To generate image urls from an image reference, we will use Sanity's helper methods to build the src url:

import imageUrlBuilder from '@sanity/image-url'

const builder = imageUrlBuilder({
  dataset: process.env.MY_DATASET,
  projectId: process.env.MY_PROJECT_ID,
})

export const urlFor = (imageRef: string) => builder.image(imageRef)
Enter fullscreen mode Exit fullscreen mode

Here, the urlFor function accepts the imageRef returned from the Sanity groq query (more on this later) and returns an "ImageBuilder" object - an inbuilt Sanity type that lets you build the CDN src something like this:

urlFor(imageRef).width(1600).height(900).url()
Enter fullscreen mode Exit fullscreen mode

Which generates a url like:
https://cdn.sanity.io/images/project_id/development/28e908-1680x1204.png?width=1600&height=900

So how the crop do we use the crop/hotspot properties returned from Sanity in our image builder? Maybe I missed something, but this is where the Sanity documentation (as ace as they are), left me high and dry.

Cropping

It's probably what you're here for, so here ya go:

import { getImageDimensions } from '@sanity/asset-utils'
import { urlFor } from './imageUrlFor'

export const getCroppedImageSrc = (
  image: SanityImageQueryResult, // Details on this type in the appendix
) => {

  const imageRef = image?.src?.asset?._ref
  const crop = image.src.crop

  // get the image's og dimensions
  const { width, height } = getImageDimensions(imageRef)

  if (Boolean(crop)) {
    // compute the cropped image's area
    const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)))

    const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)))

    // compute the cropped image's position
    const left = Math.floor(width * crop.left)
    const top = Math.floor(height * crop.top)

    // gather into a url
    return urlFor(imageRef)
            .rect(left, top, croppedWidth, croppedHeight)
            .url()
  }

  return urlFor(imageRef).url()
}
Enter fullscreen mode Exit fullscreen mode

For those interested, let's break it down.

The width, height properties returned from getImageDimensions do not return the cropped width and height, but the original image's width and height.

This is why we use the crop settings to compute the cropped width and height as set in Sanity Studio. Crop settings return the cropped area's (percentage) distance from each edge.

For example, {top: 0.2, bottom: 0.3, ...} means that the cropped area is 20% away from the top of the original area, and 30% away from the bottom of the original area; similar to absolute positioning in css - probably why Sanity chose this convention 🤔.

This is why:

1 - (crop.right + crop.left)

can be thought of as

1 - (percentage distance from horizontal edges)

can be thought as

percentage of image to retain/not crop

Multiplying by this value the original width gives us the cropped width. The corollary is true for the cropping in the y/height axis.

This gives us the size of the crop, but not the position. For that, we simply retrieve the cropped area's actual distance (not percentage distance) from the top and left of the original image's area.

    const left = Math.floor(width * crop.left)
    const top = Math.floor(height * crop.top)
Enter fullscreen mode Exit fullscreen mode

Rembember that width and height here are the image's original properties.

Now we use the 'rect' method that accepts our computed values for the size and position:

return urlFor(image)
  .rect(left, top, croppedWidth, croppedHeight)
  .url()
Enter fullscreen mode Exit fullscreen mode

Which generates a url like:
https://cdn.sanity.io/images/project_id/development/28e903-1680x704.png?rect=10,40,1074,704

For those curious, Math.floor is required because the Sanity CDN expects integer values.

Voila

To caveat, you still have a responsibility to correctly place and position your image within your HTML or JSX.

I hope you found this helpful! Please let me know if you are interested in image manipulation using the hotspot and the imageBuilder's focalpoint method.

Appendix

This is the shape of an image type returned from a Sanity query.

export type SanityImageQueryResult = {
  src: {
    asset: { _ref: string }
    crop: {
      _type: 'sanity.imageCrop'
      bottom: number
      left: number
      right: number
      top: number
    }
    hotspot: {
      _type: 'sanity.imageHotspot'
      height: number
      width: number
      x: number
      y: number
    }
  }
  alt: string
}

Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
andrilla profile image
Christian Francis

Isn't is easier to use .fit('crop') on the image builder function? That's what I've always done and it seems to work great.

I struggle hardcore with the hotspot though. I can never seem to get that to work. If you have any info on that, that'd be super helpful!

Collapse
 
tinymachine profile image
MJ

Super-helpful. Sanity is so powerful (even at the free level) but I find the documentation lacking. Thanks for writing this up!

Collapse
 
illiasdev profile image
Illia

Hello. Thank you for the article. This is really helped me solve my issue.

I wanted to ask you, have you crop a image's preview?
I mean lqip or blurHash?

If yes, could you show an example?