Crop it like it's hot: Cropping Images in Sanity v3
Tags: sanity, react, webdev, cms
Published: 2023-06-02 15:31:00

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](https://www.sanity.io/docs/image-urls). 


### 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
  },
  ...
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2itdclfq38czuu2va8wg.png)


## 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](https://www.sanity.io/docs/how-queries-work).

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)
```

Here, the _urlFor_ function accepts the imageRef returned from the Sanity groq query (more on this later) and returns an "[ImageBuilder](https://www.sanity.io/docs/image-urls)" object - an inbuilt Sanity type that lets you build the CDN src something like this:

```
urlFor(imageRef).width(1600).height(900).url()
```

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()
}
```

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)
```
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()
```


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
}

```