Skip to content

Commit c4c9be2

Browse files
committed
feat: handles object-fit of img and video element
1 parent 6020386 commit c4c9be2

File tree

6 files changed

+156
-14
lines changed

6 files changed

+156
-14
lines changed

src/css/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {paintOrder} from './property-descriptors/paint-order';
8080
import {webkitTextStrokeColor} from './property-descriptors/webkit-text-stroke-color';
8181
import {webkitTextStrokeWidth} from './property-descriptors/webkit-text-stroke-width';
8282
import {Context} from '../core/context';
83+
import {objectFit} from './property-descriptors/object-fit';
8384

8485
export class CSSParsedDeclaration {
8586
animationDuration: ReturnType<typeof duration.parse>;
@@ -130,6 +131,7 @@ export class CSSParsedDeclaration {
130131
overflowX: OVERFLOW;
131132
overflowY: OVERFLOW;
132133
overflowWrap: ReturnType<typeof overflowWrap.parse>;
134+
objectFit: ReturnType<typeof objectFit.parse>;
133135
paddingTop: LengthPercentage;
134136
paddingRight: LengthPercentage;
135137
paddingBottom: LengthPercentage;
@@ -199,6 +201,7 @@ export class CSSParsedDeclaration {
199201
this.overflowX = overflowTuple[0];
200202
this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0];
201203
this.overflowWrap = parse(context, overflowWrap, declaration.overflowWrap);
204+
this.objectFit = parse(context, objectFit, declaration.objectFit);
202205
this.paddingTop = parse(context, paddingTop, declaration.paddingTop);
203206
this.paddingRight = parse(context, paddingRight, declaration.paddingRight);
204207
this.paddingBottom = parse(context, paddingBottom, declaration.paddingBottom);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {Context} from '../../core/context';
2+
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
3+
export const enum OBJECT_FIT {
4+
CONTAIN = 'contain',
5+
COVER = 'cover',
6+
FILL = 'fill'
7+
}
8+
9+
export const objectFit: IPropertyIdentValueDescriptor<OBJECT_FIT> = {
10+
name: 'object-fit',
11+
initialValue: 'fill',
12+
prefix: false,
13+
type: PropertyDescriptorParsingType.IDENT_VALUE,
14+
parse: function (_context: Context, token: string) {
15+
switch (token) {
16+
case 'contain':
17+
return OBJECT_FIT.CONTAIN;
18+
case 'cover':
19+
return OBJECT_FIT.COVER;
20+
default:
21+
return OBJECT_FIT.FILL;
22+
}
23+
}
24+
};
25+
26+
export const getObjectFitSize = (
27+
contains: boolean /* true = contain, false = cover */,
28+
containerWidth: number,
29+
containerHeight: number,
30+
width: number,
31+
height: number
32+
): {
33+
width: number;
34+
height: number;
35+
x: number;
36+
y: number;
37+
} => {
38+
const doRatio = width / height;
39+
const cRatio = containerWidth / containerHeight;
40+
let targetWidth = 0;
41+
let targetHeight = 0;
42+
const test = contains ? doRatio > cRatio : doRatio < cRatio;
43+
44+
if (test) {
45+
targetWidth = containerWidth;
46+
targetHeight = targetWidth / doRatio;
47+
} else {
48+
targetHeight = containerHeight;
49+
targetWidth = targetHeight * doRatio;
50+
}
51+
52+
return {
53+
width: targetWidth,
54+
height: targetHeight,
55+
x: (containerWidth - targetWidth) / 2,
56+
y: (containerHeight - targetHeight) / 2
57+
};
58+
};

src/dom/document-cloner.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {CSSParsedCounterDeclaration, CSSParsedPseudoDeclaration} from '../css/in
2424
import {getQuote} from '../css/property-descriptors/quotes';
2525
import {Context} from '../core/context';
2626
import {DebuggerType, isDebugging} from '../core/debugger';
27+
import {getObjectFitSize, OBJECT_FIT, objectFit} from '../css/property-descriptors/object-fit';
2728

2829
export interface CloneOptions {
2930
ignoreElements?: (element: Element) => boolean;
@@ -256,9 +257,38 @@ export class DocumentCloner {
256257

257258
try {
258259
if (ctx) {
259-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
260-
if (!this.options.allowTaint) {
261-
ctx.getImageData(0, 0, canvas.width, canvas.height);
260+
const elObjectFit = objectFit.parse(
261+
this.context,
262+
video.ownerDocument.defaultView?.getComputedStyle(video).objectFit ?? ''
263+
);
264+
if (elObjectFit == OBJECT_FIT.CONTAIN || elObjectFit == OBJECT_FIT.COVER) {
265+
const dimension = getObjectFitSize(
266+
elObjectFit == OBJECT_FIT.CONTAIN,
267+
video.offsetWidth,
268+
video.offsetHeight,
269+
video.videoWidth,
270+
video.videoHeight
271+
);
272+
273+
ctx.drawImage(
274+
video,
275+
0,
276+
0,
277+
video.videoWidth,
278+
video.videoHeight,
279+
dimension.x,
280+
dimension.y,
281+
dimension.width,
282+
dimension.height
283+
);
284+
if (!this.options.allowTaint) {
285+
ctx.getImageData(0, 0, canvas.width, canvas.height);
286+
}
287+
} else {
288+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
289+
if (!this.options.allowTaint) {
290+
ctx.getImageData(0, 0, canvas.width, canvas.height);
291+
}
262292
}
263293
}
264294
return canvas;

src/render/canvas/canvas-renderer.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
4444
import {Renderer} from '../renderer';
4545
import {Context} from '../../core/context';
4646
import {DIRECTION} from '../../css/property-descriptors/direction';
47+
import {getObjectFitSize} from '../../css/property-descriptors/object-fit';
4748

4849
export type RenderConfigurations = RenderOptions & {
4950
backgroundColor: Color | null;
@@ -270,23 +271,53 @@ export class CanvasRenderer extends Renderer {
270271
curves: BoundCurves,
271272
image: HTMLImageElement | HTMLCanvasElement
272273
): void {
274+
const objectFit = container.styles.objectFit;
273275
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
274276
const box = contentBox(container);
275277
const path = calculatePaddingBoxPath(curves);
276278
this.path(path);
277279
this.ctx.save();
278280
this.ctx.clip();
279-
this.ctx.drawImage(
280-
image,
281-
0,
282-
0,
283-
container.intrinsicWidth,
284-
container.intrinsicHeight,
285-
box.left,
286-
box.top,
287-
box.width,
288-
box.height
289-
);
281+
282+
let newWidth;
283+
let newHeight;
284+
const newX = box.left;
285+
const newY = box.top;
286+
287+
if (objectFit === 'contain' || objectFit === 'cover') {
288+
const {width, height, x, y} = getObjectFitSize(
289+
objectFit === 'contain',
290+
box.width,
291+
box.height,
292+
image.width,
293+
image.height
294+
);
295+
newWidth = width;
296+
newHeight = height;
297+
this.ctx.drawImage(
298+
image,
299+
0,
300+
0,
301+
container.intrinsicWidth,
302+
container.intrinsicHeight,
303+
newX + x,
304+
newY + y,
305+
newWidth,
306+
newHeight
307+
);
308+
} else {
309+
this.ctx.drawImage(
310+
image,
311+
0,
312+
0,
313+
container.intrinsicWidth,
314+
container.intrinsicHeight,
315+
box.left,
316+
box.top,
317+
box.width,
318+
box.height
319+
);
320+
}
290321
this.ctx.restore();
291322
}
292323
}

tests/reftests/images/images.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727

2828
<img srcset="../../assets/image.jpg, ../../assets/image2.jpg 2x" src="../../assets/image.jpg" style="width: 75px">
2929

30+
<img src="../../assets/image.jpg" style="width:50px;height:400px;object-fit:contain" />
31+
<img src="../../assets/image.jpg" style="width:50px;height:400px;object-fit:cover" />
32+
<img src="../../assets/image.jpg" style="width:400px;height:50px;object-fit:contain" />
33+
<img src="../../assets/image.jpg" style="width:400px;height:50px;object-fit:cover" />
3034
<script>
3135
const base64 = "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";
3236
const bytes = atob(base64);

tests/reftests/images/video.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ <h2>Same origin</h2>
1111
<source src="../../assets/cc0-video.mp4" type="video/mp4">
1212
Sorry, your browser doesn't support embedded videos.
1313
</video>
14+
<video controls style="width: 50px; height: 400px; object-fit: contain;">
15+
<source src="../../assets/cc0-video.mp4" type="video/mp4">
16+
Sorry, your browser doesn't support embedded videos.
17+
</video>
18+
<video controls style="width: 50px; height: 400px; object-fit: cover;">
19+
<source src="../../assets/cc0-video.mp4" type="video/mp4">
20+
Sorry, your browser doesn't support embedded videos.
21+
</video>
22+
<video controls style="width: 400px; height: 50px; object-fit: contain;">
23+
<source src="../../assets/cc0-video.mp4" type="video/mp4">
24+
Sorry, your browser doesn't support embedded videos.
25+
</video>
26+
<video controls style="width: 400px; height: 50px; object-fit: cover;">
27+
<source src="../../assets/cc0-video.mp4" type="video/mp4">
28+
Sorry, your browser doesn't support embedded videos.
29+
</video>
1430
<h2>Cross-origin (doesn't taint)</h2>
1531
<video controls width="250">
1632
<source src="http://localhost:8081/cors/tests/assets/cc0-video.mp4" type="video/mp4">

0 commit comments

Comments
 (0)