cloudinary-community · GitHub

The goal of this document is to highlight current issues with the existing 2-stage cropping that occurs on the CldImage component and a proposed solution to resolve it.

Here's a video that also covers some of the below, I'd recommend reviewing both: https://res.cloudinary.com/colbycloud/video/upload/f_auto,q_auto/2024-02-16_12-27-12_r7vote.mov

Update Feb 22, 2024

Implemented and merged into beta via #431

Current Solution

The CldImage currently utilizes a 2-stage cropping and resizing process in order to provide an intuitive experience when working with the dynamic cropping and resizing methods that Cloudinary provides.

CldImage renders a Cloudinary URL, where in a basic example, we may serve an image "as is":

<CldImage
  width="960"
  height="600"
  src="<Public ID>"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_limit,w_960/f_auto/q_auto/v1/images/turtle

Because the CldImage component wraps the Next.js Image component, we can also opt into responsive sizing, so the scale of our image changes based on the size that is being generated by Next.js:

<CldImage
  width="960"
  height="600"
  src="<Public ID>"
  sizes="100w"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_limit,w_960/c_limit,w_828/f_auto/q_auto/v1/images/turtle

The important point to see here is the width of 828 which is automatically generated through the Next.js Image component. This is done by using a Loader function then generated using Cloudinary URLs.

Another important point here is that the URL also contains 2 widths:

c_limit,w_960/c_limit,w_828

The goal here is to provide a "base canvas" for all images generated with responsive sizing for a consistent and intuitive experience.

To illustrate why this is important, we can start to add some transformations, such a simply a crop mode of "thumb":

<CldImage
  width="600"
  height="600"
  crop="thumb"
  src="<Public ID>"
  sizes="100w"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/c_limit,w_828/f_auto/q_auto/v1/images/turtle
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/c_limit,w_640/f_auto/q_auto/v1/images/turtle

These are two different image sizes that produce the same result.

However, the crop mode of "thumb" may produce different results if the crop is based on different sizes. We can see this in a modified example of the above:

> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/f_auto/q_auto/v1/images/turtle
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_600,h_600,g_auto/f_auto/q_auto/v1/images/turtle

The "thumb" cropping is being based on two different sizes, 1200x1200 and 600x600, producing different results, which in context of the CldImage component, would produce different results based on different sizes contained within the srcset:

srcset="https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_600,h_600,g_auto/f_auto/q_auto/v1/images/turtle 600w, https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/f_auto/q_auto/v1/images/turtle 1200w"

Thus, the 2-stage sizing resolves this issue being able to first crop/resize the image on the "base layer" then provide the responsive sizing on a secondary stage.

The Problem

While this solves our problem of consistency, we create a big issue in our responsive sizing, in that the 1st stage results in a smaller sized image (usually) that may be smaller than the original.

The reason why this is important, is the 2nd stage that does the responsive sizing can no longer resize the image above the original size without distortion, as in the order of operations, the 1st stage sizes it down before other transformations are applied (such as the 2nd stage).

In the example of:

https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/c_limit,w_640/f_auto/q_auto/v1/images/turtle

The image is first resized down to 1200, then further resized down to 640.

In the example of:

https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_600,h_600,g_auto/c_limit,w_1200/f_auto/q_auto/v1/images/turtle

The image is first resized down to 600, but because we have already resized the image down to 600, we can no longer resized beyond that to 1200.

In practice, we technically can avoid using the crop mode of limit, which would allow upscaling to 1200 in this example, but the 1200 will no longer be based on the original image size, but it would be based on 600, resulting in a now distorted image.

Proposed Solution

To avoid this, the first step is to get rid of the 2-stage cropping and resizing all together.

The default experience of CldImage would be that any transformations and effects would be based on the original image size and the responsive sizing would still apply after all transformations are made.

In one of the original examples of basic responsive sizing:

<CldImage
  width="960"
  height="600"
  src="<Public ID>"
  sizes="100w"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_limit,w_828/f_auto/q_auto/v1/images/turtle

This is a simple example that really doesn't impact anything. Another such example could be using a crop mode of "fill", that performs a cropping transformation:

<CldImage
  width="600"
  height="600"
  crop="fill"
  src="<Public ID>"
  sizes="100w"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_fill,w_828,h_828,g_auto/f_auto/q_auto/v1/images/turtle

This works because no matter the image size, the image will fill the container.

Issue: Overlays & Relative Sized Transformations

The most obvious issue with removing the 1st stage cropping and resizing is that potentially all relatively positioned elements, such as text and image overlays, will now appear broken.

In an example with some basic text:

<CldImage
  width="600"
  height="600"
  src="<Public ID>"
  sizes="100w"
  overlays={[{
    text: {
      color: 'white',
      fontFamily: 'Source Sans Pro',
      fontSize: 160,
      fontWeight: 'bold',
      text: 'Turtle'
    }
  }]}
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_limit,w_960/l_text:Source%20Sans%20Pro_160_bold:Turtle,co_white/fl_layer_apply,fl_no_overflow/c_limit,w_828/f_auto/q_auto/v1/images/turtle

We can see the text is nice and large on the image, however when we remove the 1st stage:

> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/l_text:Source%20Sans%20Pro_160_bold:Turtle,co_white/fl_layer_apply,fl_no_overflow/c_limit,w_828/f_auto/q_auto/v1/images/turtle

The text is immediately smaller.

The solution here is to either resize the overlays or other relative sizing based on the new size:

<CldImage
  width="600"
  height="600"
  src="<Public ID>"
  sizes="100w"
  overlays={[{
    text: {
      color: 'white',
      fontFamily: 'Source Sans Pro',
      fontSize: 320, // doubled the size
      fontWeight: 'bold',
      text: 'Turtle'
    }
  }]}
  alt="Description of my image"
/>

Or define the object-based crop mode to set an initial base canvas similar to the original size, which would be the width and height defined on the CldImage top level props.

<CldImage
  width="600"
  height="600"
  src="<Public ID>"
  sizes="100w"
  crop={{
    width: 600,
    height: 600,
    crop: 'fill'
  }}
  overlays={[{
    text: {
      color: 'white',
      fontFamily: 'Source Sans Pro',
      fontSize: 160,
      fontWeight: 'bold',
      text: 'Turtle'
    }
  }]}
  alt="Description of my image"
/>

Tip: using relative sizing is a good way to produce transformations that will scale no matter the initial size, such as adding a flag of "relative" and using values such as w_1.0

Issue: Dynamic Crop Modes

These first examples used crop modes that gracefully scale no matter the size, but what about a crop mode like "thumb"?

In our earlier example, we were trying to avoid differing sizes by using the 2-stage approach, where now, in the example:

<CldImage
  width="600"
  height="600"
  crop="thumb"
  src="<Public ID>"
  sizes="100w"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/f_auto/q_auto/v1/images/turtle
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_600,h_600,g_auto/f_auto/q_auto/v1/images/turtle

We end up with different results.

To solve this, we can create an advanced way of utilizing the crop transformation by accepting an object that allows the user to specify a base width and height for the crop to be made on:

<CldImage
  width="600"
  height="600"
  crop={{
    width: 1200,
    height: 1200,
    crop: 'thumb',
    source: true
  }}
  src="<Public ID>"
  sizes="100w"
  alt="Description of my image"
/>
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/c_limit,w_1200,h_1200/f_auto/q_auto/v1/images/turtle
> https://res.cloudinary.com/colbycloud-next-cloudinary/image/upload/c_thumb,w_1200,h_1200,g_auto/c_limit,w_600,h_600/f_auto/q_auto/v1/images/turtle

This re-introduces the 2-stage cropping and resizing, but the differentiator is that it's user-defined and user opt-in.

It also allows the 1st stage, importantly, to specify the width and the height for the transformation to be based on.

While this isn't as easy as simply specifying crop="thumb" and it "just works", it's more reliable and doesn't sacrifice the experience and resolution of other transformations for this particular crop mode and other similar ones.

To help alleviate this, we can provide a warning when the crop mode of thumb and other similar ones are used in conjunction with responsive sizing, as they can produce inconsistent results for visitors.

Read the original on github.com ↗