26

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/

Chris
  • 3,265
  • 5
  • 37
  • 50
  • if you use `$('img.foo').outerWidth()` you'll obtain the total calculated width of the element. Not clear what you are asking – Marcos Pérez Gude May 16 '16 at 14:50
  • No, `outerWidth()` doesn't work in this case as it will result in the width of the "full" image - so it would return `400` due to the `width:100%` and not `150` as desired. – Chris May 16 '16 at 15:06
  • There is no special property that will give you that value, you need to make use of the generated size of the `img` element and then use the properties `naturalWidth`/`naturalHeight` to get its aspect ratio and then calculate its rendered size (pretty much same you did manually). – Asons May 16 '16 at 15:51

4 Answers4

19

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

fregante
  • 29,050
  • 14
  • 119
  • 159
Asons
  • 84,923
  • 12
  • 110
  • 165
  • Manual "compression" makes the code less readable; let uglifyjs do the dirty job just for the browser. In reality, uglifyjs compresses the intial code more than the manually-compressed one. (146 bytes vs 179 bytes) – fregante May 17 '16 at 14:24
  • @bfred.it Agree with you on that ... "uncompressed" it a little :) – Asons May 17 '16 at 15:05
  • Thanks, I hoped there's an API that was hidden from me. But the anwers it's clear that it doesn't exist (yet?). As your answer is fulfilling the task it's a correct one and thus I accepted it. – Chris May 22 '16 at 12:04
13

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);
fregante
  • 29,050
  • 14
  • 119
  • 159
  • Excellent answer. FYI, you can do a similar call for canvas rather than img (much rarer case but what I needed) by doing: getObjectFitSize(true, c.scrollWidth, c.scrollHeight, c.width, c.height); where "c" is the canvas. Canvases are weird because the width and height are actually the "natural" dimensions and scrollWidth and scrollHeight are just a trick to get the actual height and width (because height and width are overridden to mean the "natural" height and width.) – DoomGoober Mar 29 '20 at 05:24
4

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.

Édouard Mercier
  • 485
  • 5
  • 12
  • Hi, hope you don't mind, I adapted your code slightly to support video elements too, Stackoverflow won't let me put the detail in a comment, so I put it in this gist instead: https://gist.github.com/ciaranj/7177fb342102e571db2784dc831f868b – ciaranj Jan 24 '19 at 13:04
  • Thank you for taking the time for this gist and for having adapter to videos as well. You did well. – Édouard Mercier Jan 24 '19 at 20:39
1

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.

s-ol
  • 1,674
  • 17
  • 28