diff --git a/README.md b/README.md
index b7ebe8f5..f1f1426e 100644
--- a/README.md
+++ b/README.md
@@ -224,8 +224,8 @@ Satori uses the same Flexbox [layout engine](https://yogalayout.com) as React Na
objectFit |
-contain, cover, none, default to none |
- |
+Supported |
+Example |
diff --git a/src/builder/rect.ts b/src/builder/rect.ts
index 62dc963f..49907031 100644
--- a/src/builder/rect.ts
+++ b/src/builder/rect.ts
@@ -243,12 +243,64 @@ export default async function rect(
}
}
const alignment = `x${xAlign}Y${yAlign}`
- const preserveAspectRatio =
- style.objectFit === 'contain'
- ? alignment
- : style.objectFit === 'cover'
- ? `${alignment} slice`
- : 'none'
+
+ // Calculate objectFit behavior
+ let preserveAspectRatio: string
+ let imageWidth = width - offsetLeft - offsetRight
+ let imageHeight = height - offsetTop - offsetBottom
+ let imageX = left + offsetLeft
+ let imageY = top + offsetTop
+
+ if (style.objectFit === 'contain') {
+ preserveAspectRatio = alignment
+ } else if (style.objectFit === 'cover') {
+ preserveAspectRatio = `${alignment} slice`
+ } else if (style.objectFit === 'fill') {
+ preserveAspectRatio = 'none'
+ } else if (style.objectFit === 'scale-down') {
+ // Get natural dimensions
+ const naturalWidth = style.__naturalWidth as number
+ const naturalHeight = style.__naturalHeight as number
+
+ if (naturalWidth && naturalHeight) {
+ // Calculate if we need to scale down
+ const containerWidth = width - offsetLeft - offsetRight
+ const containerHeight = height - offsetTop - offsetBottom
+ const scaleX = containerWidth / naturalWidth
+ const scaleY = containerHeight / naturalHeight
+ const minScale = Math.min(scaleX, scaleY)
+
+ if (minScale >= 1) {
+ // Image is smaller than or equal to container
+ // Use natural size (don't scale up)
+ imageWidth = naturalWidth
+ imageHeight = naturalHeight
+ preserveAspectRatio = 'none'
+
+ // Center according to objectPosition
+ const extraWidth = containerWidth - naturalWidth
+ const extraHeight = containerHeight - naturalHeight
+
+ // Apply objectPosition alignment
+ if (xAlign === 'Min') imageX += 0
+ else if (xAlign === 'Max') imageX += extraWidth
+ else imageX += extraWidth / 2 // Mid
+
+ if (yAlign === 'Min') imageY += 0
+ else if (yAlign === 'Max') imageY += extraHeight
+ else imageY += extraHeight / 2 // Mid
+ } else {
+ // Image is larger than container, scale down like 'contain'
+ preserveAspectRatio = alignment
+ }
+ } else {
+ // Fall back to 'contain' behavior if natural dimensions are unavailable
+ preserveAspectRatio = alignment
+ }
+ } else {
+ // Default/none: fill (stretch)
+ preserveAspectRatio = 'none'
+ }
if (style.transform) {
imageBorderRadius = getBorderRadiusClipPath(
@@ -266,10 +318,10 @@ export default async function rect(
}
shape += buildXMLString('image', {
- x: left + offsetLeft,
- y: top + offsetTop,
- width: width - offsetLeft - offsetRight,
- height: height - offsetTop - offsetBottom,
+ x: imageX,
+ y: imageY,
+ width: imageWidth,
+ height: imageHeight,
href: src,
preserveAspectRatio,
transform: matrix ? matrix : undefined,
diff --git a/src/handler/compute.ts b/src/handler/compute.ts
index 6e46f9b2..80ebf594 100644
--- a/src/handler/compute.ts
+++ b/src/handler/compute.ts
@@ -111,6 +111,8 @@ export default async function compute(
? (contentBoxHeight as number) + extraVertical
: contentBoxHeight
style.__src = resolvedSrc
+ style.__naturalWidth = imageWidth
+ style.__naturalHeight = imageHeight
}
if (type === 'svg') {
diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-fill-should-stretch-image-to-fill-container-aspect-ratio-not-preserved-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-fill-should-stretch-image-to-fill-container-aspect-ratio-not-preserved-1-snap.png
new file mode 100644
index 00000000..0200e7d0
Binary files /dev/null and b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-fill-should-stretch-image-to-fill-container-aspect-ratio-not-preserved-1-snap.png differ
diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-fill-should-stretch-with-fill-on-non-square-container-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-fill-should-stretch-with-fill-on-non-square-container-1-snap.png
new file mode 100644
index 00000000..fb6ed3d0
Binary files /dev/null and b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-fill-should-stretch-with-fill-on-non-square-container-1-snap.png differ
diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-not-scale-up-when-image-is-smaller-than-container-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-not-scale-up-when-image-is-smaller-than-container-1-snap.png
new file mode 100644
index 00000000..6d4d670a
Binary files /dev/null and b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-not-scale-up-when-image-is-smaller-than-container-1-snap.png differ
diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-respect-object-position-bottom-right-with-scale-down-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-respect-object-position-bottom-right-with-scale-down-1-snap.png
new file mode 100644
index 00000000..dbc2ce46
Binary files /dev/null and b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-respect-object-position-bottom-right-with-scale-down-1-snap.png differ
diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-respect-object-position-with-scale-down-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-respect-object-position-with-scale-down-1-snap.png
new file mode 100644
index 00000000..749d1bbd
Binary files /dev/null and b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-respect-object-position-with-scale-down-1-snap.png differ
diff --git a/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-scale-down-when-image-is-larger-than-container-1-snap.png b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-scale-down-when-image-is-larger-than-container-1-snap.png
new file mode 100644
index 00000000..6b4fee18
Binary files /dev/null and b/test/__image_snapshots__/image-test-tsx-test-image-test-tsx-object-fit-and-object-position-object-fit-scale-down-should-scale-down-when-image-is-larger-than-container-1-snap.png differ
diff --git a/test/image.test.tsx b/test/image.test.tsx
index 5cc69e47..0d8479f1 100644
--- a/test/image.test.tsx
+++ b/test/image.test.tsx
@@ -979,4 +979,122 @@ describe('objectFit and objectPosition', () => {
)
expect(toImage(svg, 100)).toMatchImageSnapshot()
})
+
+ describe('objectFit: fill', () => {
+ it('should stretch image to fill container (aspect ratio not preserved)', async () => {
+ const svg = await satori(
+
+

+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should stretch with fill on non-square container', async () => {
+ const svg = await satori(
+
+

+
,
+ { width: 150, height: 100, fonts }
+ )
+ expect(toImage(svg, 150)).toMatchImageSnapshot()
+ })
+ })
+
+ describe('objectFit: scale-down', () => {
+ it('should not scale up when image is smaller than container', async () => {
+ // PNG_SAMPLE is 20x15, container is 100x100
+ // Should show image at 20x15, centered
+ const svg = await satori(
+
+

+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should scale down when image is larger than container', async () => {
+ // PNG_SAMPLE is 20x15, container is 10x10
+ // Should scale down like 'contain'
+ const svg = await satori(
+
+

+
,
+ { width: 10, height: 10, fonts }
+ )
+ expect(toImage(svg, 10)).toMatchImageSnapshot()
+ })
+
+ it('should respect objectPosition with scale-down', async () => {
+ const svg = await satori(
+
+

+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+
+ it('should respect objectPosition bottom right with scale-down', async () => {
+ const svg = await satori(
+
+

+
,
+ { width: 100, height: 100, fonts }
+ )
+ expect(toImage(svg, 100)).toMatchImageSnapshot()
+ })
+ })
})