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.