'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 thesourceFrame
should be cut,sourceY
: the vertical position of the left-top point where thesourceFrame
should be cut,sourceWidth
: how much horizontal space of thesourceFrame
should be cut,sourceHeight
: how much vertical space of thesourceFrame
should be cut,destinationXPercentage
: the percentage of the horizontal position of the left-top point on theprintFrame
where the image will be printed, relative to theprintFrame
width,destinationYPercentage
: the percentage of the vertical position of the left-top point on theprintFrame
where the image will be printed, relative to theprintFrame
height,destinationWidthPercentage
: the percentage of theprintFrame
width on which the image will be printed, relative to theprintFrame
width,destinationHeightPercentage
: the percentage of theprintFrame
height on which the image will be printed, relative to theprintFrame
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 |