'object-fit: get resulting dimensions

When using the new CSS feature object-fit, how can I access the resulting dimensions that the browser has chosen by JavaScript?

So let's assume foo.jpg is 100x200 pixels. The browser page / viewport is 400px wide and 300px high. Then given this CSS code:

img.foo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 25% 0;
}

The browser would now show the image on the very top with correct aspect ration stretching to the very bottom on the second quarter from the left. This results in those image dimensions:

  • width: 150px
  • height: 300px
  • left: 62.5px
  • right: 212.5px

So what JavaScript call (jQuery allowed) would give me those numbers that I've calculated manually? (Note: the CSS information themselves are not known by the JavaScript as the user could overwrite them and even add stuff like min-width)

To play with the code I've created a fiddle: https://jsfiddle.net/sydeo244/



Solution 1:[1]

Thanks to @bfred I didn't have to make the initial method.

Here is an extended (and rewritten) version of his, that does calculate the object-position values as well.

function getRenderedSize(contains, cWidth, cHeight, width, height, pos){
  var oRatio = width / height,
      cRatio = cWidth / cHeight;
  return function() {
    if (contains ? (oRatio > cRatio) : (oRatio < cRatio)) {
      this.width = cWidth;
      this.height = cWidth / oRatio;
    } else {
      this.width = cHeight * oRatio;
      this.height = cHeight;
    }      
    this.left = (cWidth - this.width)*(pos/100);
    this.right = this.width + this.left;
    return this;
  }.call({});
}

function getImgSizeInfo(img) {
  var pos = window.getComputedStyle(img).getPropertyValue('object-position').split(' ');
  return getRenderedSize(true,
                         img.width,
                         img.height,
                         img.naturalWidth,
                         img.naturalHeight,
                         parseInt(pos[0]));
}

document.querySelector('#foo').addEventListener('load', function(e) {
  console.log(getImgSizeInfo(e.target));
});
#container {
  width: 400px;
  height: 300px;
  border: 1px solid blue;
}

#foo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 25% 0;
}
<div id="container">
  <img id="foo" src="http://dummyimage.com/100x200/000/fff.jpg"/>
</div>

Side note

It appears that object-position can have more than 2 values, and when, you need to adjust (or add) which parameter returns the left position value

Solution 2:[2]

There's an npm package called intrinsic-scale that will calculate that for you, but it doesn't support the equivalent of object-position: https://www.npmjs.com/package/intrinsic-scale

This is the whole code:

// adapted from: https://www.npmjs.com/package/intrinsic-scale
function getObjectFitSize(contains /* true = contain, false = cover */, containerWidth, containerHeight, width, height){
    var doRatio = width / height;
    var cRatio = containerWidth / containerHeight;
    var targetWidth = 0;
    var targetHeight = 0;
    var test = contains ? (doRatio > cRatio) : (doRatio < cRatio);

    if (test) {
        targetWidth = containerWidth;
        targetHeight = targetWidth / doRatio;
    } else {
        targetHeight = containerHeight;
        targetWidth = targetHeight * doRatio;
    }

    return {
        width: targetWidth,
        height: targetHeight,
        x: (containerWidth - targetWidth) / 2,
        y: (containerHeight - targetHeight) / 2
    };
}

And the usage would be:

getObjectFitSize(true, img.width, img.height, img.naturalWidth, img.naturalHeight);

Solution 3:[3]

Here is a more comprehensive algorithm, tested, in order to determine the way the image is displayed on the screen.

var imageComputedStyle = window.getComputedStyle(image);
var imageObjectFit = imageComputedStyle.getPropertyValue("object-fit");
coordinates = {};
var imagePositions = imageComputedStyle.getPropertyValue("object-position").split(" ");
var horizontalPercentage = parseInt(imagePositions[0]) / 100;
var verticalPercentage = parseInt(imagePositions[1]) / 100;
var naturalRatio = image.naturalWidth / image.naturalHeight;
var visibleRatio = image.width / image.height;
if (imageObjectFit === "none")
{
  coordinates.sourceWidth = image.width;
  coordinates.sourceHeight = image.height;
  coordinates.sourceX = (image.naturalWidth - image.width) * horizontalPercentage;
  coordinates.sourceY = (image.naturalHeight - image.height) * verticalPercentage;
  coordinates.destinationWidthPercentage = 1;
  coordinates.destinationHeightPercentage = 1;
  coordinates.destinationXPercentage = 0;
  coordinates.destinationYPercentage = 0;
}
else if (imageObjectFit === "contain" || imageObjectFit === "scale-down")
{
  // TODO: handle the "scale-down" appropriately, once its meaning will be clear
  coordinates.sourceWidth = image.naturalWidth;
  coordinates.sourceHeight = image.naturalHeight;
  coordinates.sourceX = 0;
  coordinates.sourceY = 0;
  if (naturalRatio > visibleRatio)
  {
    coordinates.destinationWidthPercentage = 1;
    coordinates.destinationHeightPercentage = (image.naturalHeight / image.height) / (image.naturalWidth / image.width);
    coordinates.destinationXPercentage = 0;
    coordinates.destinationYPercentage = (1 - coordinates.destinationHeightPercentage) * verticalPercentage;
  }
  else
  {
    coordinates.destinationWidthPercentage = (image.naturalWidth / image.width) / (image.naturalHeight / image.height);
    coordinates.destinationHeightPercentage = 1;
    coordinates.destinationXPercentage = (1 - coordinates.destinationWidthPercentage) * horizontalPercentage;
    coordinates.destinationYPercentage = 0;
  }
}
else if (imageObjectFit === "cover")
{
  if (naturalRatio > visibleRatio)
  {
    coordinates.sourceWidth = image.naturalHeight * visibleRatio;
    coordinates.sourceHeight = image.naturalHeight;
    coordinates.sourceX = (image.naturalWidth - coordinates.sourceWidth) * horizontalPercentage;
    coordinates.sourceY = 0;
  }
  else
  {
    coordinates.sourceWidth = image.naturalWidth;
    coordinates.sourceHeight = image.naturalWidth / visibleRatio;
    coordinates.sourceX = 0;
    coordinates.sourceY = (image.naturalHeight - coordinates.sourceHeight) * verticalPercentage;
  }
  coordinates.destinationWidthPercentage = 1;
  coordinates.destinationHeightPercentage = 1;
  coordinates.destinationXPercentage = 0;
  coordinates.destinationYPercentage = 0;
}
else
{
  if (imageObjectFit !== "fill")
  {
    console.error("unexpected 'object-fit' attribute with value '" + imageObjectFit + "' relative to");
  }
  coordinates.sourceWidth = image.naturalWidth;
  coordinates.sourceHeight = image.naturalHeight;
  coordinates.sourceX = 0;
  coordinates.sourceY = 0;
  coordinates.destinationWidthPercentage = 1;
  coordinates.destinationHeightPercentage = 1;
  coordinates.destinationXPercentage = 0;
  coordinates.destinationYPercentage = 0;
}

where image is the HTML <img> element and coordinates contains the following attributes, given that we consider sourceFrame being the rectangle defined by the image if it were totally printed, i.e. its natural dimensions, and printFrame being the actual displayed region, i.e. printFrame.width = image.width and printFrame.height = image.height:

  • sourceX: the horizontal position of the left-top point where the sourceFrame should be cut,
  • sourceY: the vertical position of the left-top point where the sourceFrame should be cut,
  • sourceWidth: how much horizontal space of the sourceFrame should be cut,
  • sourceHeight: how much vertical space of the sourceFrame should be cut,
  • destinationXPercentage: the percentage of the horizontal position of the left-top point on the printFrame where the image will be printed, relative to the printFrame width,
  • destinationYPercentage: the percentage of the vertical position of the left-top point on the printFrame where the image will be printed, relative to the printFrame height,
  • destinationWidthPercentage: the percentage of the printFrame width on which the image will be printed, relative to the printFrame width,
  • destinationHeightPercentage: the percentage of the printFrame height on which the image will be printed, relative to the printFrame height.

Sorry, the scale-down case is not handled, since its definition is not that clear.

Solution 4:[4]

Here is an updated piece of TypeScript code that handles all values including object-fit: scale-down and object-position both with relative, absolute, and keyword values:

type Rect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

const dom2rect = (rect: DOMRect): Rect => {
  const { x, y, width, height } = rect;
  return { x, y, width, height };
};

const intersectRects = (a: Rect, b: Rect): Rect | null => {
  const x = Math.max(a.x, b.x);
  const y = Math.max(a.y, b.y);
  const width = Math.min(a.x + a.width, b.x + b.width) - x;
  const height = Math.min(a.y + a.height, b.y + b.height) - y;

  if (width <= 0 || height <= 0) return null;

  return { x, y, width, height };
};

type ObjectRects = {
  container: Rect; // client-space size of container element
  content: Rect; // natural size of content
  positioned: Rect; // scaled rect of content relative to container element (may overlap out of container)
  visible: Rect | null; // intersection of container & positioned rect
};

const parsePos = (str: string, ref: number): number => {
  switch (str) {
    case "left":
    case "top":
      return 0;

    case "center":
      return ref / 2;

    case "right":
    case "bottom":
      return ref;

    default:
      const num = parseFloat(str);
      if (str.endsWith("%")) return (num / 100) * ref;
      else if (str.endsWith("px")) return num;
      else
        throw new Error(`unexpected unit object-position unit/value: '${str}'`);
  }
};

const getObjectRects = (
  image: HTMLImageElement | HTMLVideoElement
): ObjectRects => {
  const style = window.getComputedStyle(image);
  const objectFit = style.getPropertyValue("object-fit");

  const naturalWidth =
    image instanceof HTMLImageElement ? image.naturalWidth : image.videoWidth;
  const naturalHeight =
    image instanceof HTMLImageElement ? image.naturalHeight : image.videoHeight;

  const content = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
  const container = dom2rect(image.getBoundingClientRect());

  let scaleX = 1;
  let scaleY = 1;

  switch (objectFit) {
    case "none":
      break;

    case "fill":
      scaleX = container.width / naturalWidth;
      scaleY = container.height / naturalHeight;
      break;

    case "contain":
    case "scale-down": {
      let scale = Math.min(
        container.width / naturalWidth,
        container.height / naturalHeight
      );

      if (objectFit === "scale-down") scale = Math.min(1, scale);

      scaleX = scale;
      scaleY = scale;
      break;
    }

    case "cover": {
      const scale = Math.max(
        container.width / naturalWidth,
        container.height / naturalHeight
      );
      scaleX = scale;
      scaleY = scale;
      break;
    }

    default:
      throw new Error(`unexpected 'object-fit' value ${objectFit}`);
  }

  const positioned = {
    x: 0,
    y: 0,
    width: naturalWidth * scaleX,
    height: naturalHeight * scaleY,
  };

  const objectPos = style.getPropertyValue("object-position").split(" ");
  positioned.x = parsePos(objectPos[0], container.width - positioned.width);
  positioned.y = parsePos(objectPos[1], container.height - positioned.height);

  const containerInner = { x: 0, y: 0, width: container.width, height: container.height };

  return {
    container,
    content,
    positioned,
    visible: intersectRects(containerInner, positioned),
  };
};

You can adjust the return value to only output what you need.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 fregante
Solution 2
Solution 3 Édouard Mercier
Solution 4