Bug in responsiveImage width/height after ImgIx "trim" transformation applied

I am not sure if this is a bug or not but seems that DatoCMS sets wrong width/height on responsiveImage object, than the actual image width/height after ImgIx transforms.

I am using this GraphQL query with ImIx transforms:

photo {
  id
  responsiveImage(imgixParams: { 
    auto: format, 
    w: 400, 
    h: 300, 
    fit: clip, 
    trim: color 
  }) {
    srcSet
    webpSrcSet
    sizes
    src
    width
    height
    aspectRatio
    alt
    title
    base64
  }
}

This is the image I am getting from ImgIx and is what I want → https://www.datocms-assets.com/53444/1679491853-krypton-6xstg.png?auto=format&dpr=2&fit=clip&h=300&trim=color&w=400

When I pass this responsiveImage to the <Image /> component of react-datocms the image is rendered disorted. I figured out that responsiveImage.width is okay 400 but responsiveImage.height is 127 - but it should really be 102.

<Image data={photo.responsiveImage} />

I did some debugging on my end, and if I remove this ImgIx param trim: color then image is rendered okay. Seems like Dato is not applying widh height properly if image is trimmed. I am using this parameter to cut the transparent/color margin on the photo.

Best regards,
Primoz

Hi @primoz.rome,

Hmm, interesting. Thanks for the detailed report! This may indeed be an edge case we hadn’t considered.

I’ll report it to the devs and let you know what we find out.

Preliminarily, it does seem like the CDA isn’t correctly taking into account some of the Imgix transformations that can affect size: like trim, yes, but also pad or rot and maybe others. The devs will look into it and I’ll provide updates as I can.

In the meantime, if this is an urgent need, you can either provide the correct dimensions manually, like:

<Image data={{...photo.responsiveImage, height: 102}} />

Or since you’re using the clientside <Image/> component, you can kinda hack around it by letting JS load the image in the background and getting its real dimensions:

Example code:

import "./App.css";
import { Image as DatoImage, type ResponsiveImageType } from "react-datocms";
import { useEffect, useState } from "react";

const App = () => {
  const mockCdaResponse: ResponsiveImageType = {
    src: "https://www.datocms-assets.com/160930/1747248478-test.png?trim=color",
    width: 200, // Assuming you specified an explicit `w:` parameter
    height: 100, // Bug: This comes back as a function of the original aspect ratio, EVEN if you set an explicit `h` parameter
  };

  // We have to handle this asynchronously because image loading/rendering is inherently async
  const [realDimensions, setRealDimensions] = useState<{
    width: number;
    height: number;
  }>();

  // So we use an useEffect to essentially "preload" the image in JS and use that hidden render to calculate the dimensions
  useEffect(() => {
    if (!mockCdaResponse.src) {
      return;
    }

    (async () => {
      try {
        // getImageDimensions() is defined at the bottom. Helper func that loads an image and calculates its size
        const dimensions = await getImageDimensions(mockCdaResponse.src);
        if (dimensions?.width && dimensions?.height) {
          setRealDimensions(dimensions);
        }
      } catch (error) {
        console.error("Error", error);
        setRealDimensions(undefined);
      }
    })();
  }, [mockCdaResponse.src]);

  return (
    <>
      <h1>DatoCMS Trim() Image Test</h1>

      <h2>Trimmed (default CDA response)</h2>
      <DatoImage data={mockCdaResponse} />

      <h2>Trimmed (using calculated dimensions)</h2>
      <DatoImage data={{ ...mockCdaResponse, ...realDimensions }} />
    </>
  );
};

export default App;

/**
 * Asynchronously retrieves the intrinsic dimensions of an image given its URL.
 *
 * This function creates a new HTMLImageElement and sets its `src` attribute to the provided URL.
 * It waits for the image to load (or fail to load), then returns the image's natural width and height.
 *
 * The loading process uses the browser's native image-fetching mechanism, which benefits from
 * built-in caching. The image does not need to be added to the DOM and is never displayed.
 *
 * @param url - The URL of the image to measure. Must be a non-empty, valid URL string.
 * @returns A Promise that resolves to an object containing the `width` and `height` of the image in pixels.
 * @throws If the URL is invalid, empty, or the image fails to load (e.g. due to network error or CORS).
 *
 * @example
 * ```ts
 * const { width, height } = await getImageDimensions('https://example.com/photo.jpg');
 * console.log(`Image is ${width}x${height}`);
 * ```
 */
const getImageDimensions = async (
  url?: string | null,
): Promise<{ width: number; height: number }> => {
  // Validate input URL
  if (!url) {
    throw new Error('Image URL must be a non-empty string.');
  }

  try {
    new URL(url); // Throws if not a valid URL
  } catch {
    throw new Error(`Invalid image URL: "${url}"`);
  }

  const img = new Image();

  // Wait for the image to load or fail
  await new Promise<void>((resolve, reject) => {
    img.onload = () => resolve();
    img.onerror = () => reject(new Error("Failed to load image."));
    img.src = url;
  });

  return { width: img.width, height: img.height };
};

It’s hacky, of course, but might be better than distorted images in a pinch…?

Sorry about that. I’ll let you know once the underlying issue is resolved (if possible).

Hey @primoz.rome,

Heard back from the devs, and sorry, I actually have some bad news :frowning:

It was as you suspected: Unfortunately the CDA never really ā€œknowsā€ the real dimensions of the processed image — it’s not looking at the Imgix output, but just estimating it based on input operators. For simpler operations (like crop) we can replicate the math and correctly guess the final size, but for something like a color-based trim operation, our server cannot know how Imgix handles it (that’s up to their algorithm), and our guess will be wrong.

Sadly, this is not something we can easily change. It would require changing too much of the API, and slow it down a lot, since then we’d have to fetch every Imgix asset in a query and look at its dimensions before returning the GraphQL response… that would take way too long =/

For your use case, do you think specifying it manually and/or using a frontend/middleware calculation like I showed would be enough? If there’s a particular use case you have in mind that makes this difficult, let us know and we can think through some options?

We’ll at least clarify this situation in the documentation. Sorry about that!


Also… one super-easy fix is to just download the trimmed version (https://www.datocms-assets.com/53444/1679491853-krypton-6xstg.png?auto=format&dpr=2&fit=clip&h=300&trim=color&w=400) and re-upload it to DatoCMS as a separate image, like krypton-trimmed.png :slight_smile:

Ah okay, pitty. Well we will just make sure to upload trimmed photo’s where we need them. Thank you in any case for looking into possibility to fix this. Kind regards, Primoz

1 Like

Sorry about the inconvenience here!