Skip to content

Commit 4b2bf38

Browse files
authored
fix: correctly handle fit mode in Bunny.net, Cloudflare and Kontent.ai (#68)
* fix: correctly handle fit mode in Bunny.net, Cloudflare and Kontent.ai * Add note on e2e test * chore: fix tests
1 parent 0b8b9a1 commit 4b2bf38

12 files changed

+103
-32
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ jobs:
1616
- name: Typecheck
1717
run: deno check mod.ts
1818
- name: Test
19-
run: deno test src/
19+
run: deno test -A src/

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ if supported, to ensure consistent behaviour across all CDNs:
3737
should be AVIF, WebP, then the original format.
3838
- Fit = cover. The image should fill the requested dimensions, cropping if
3939
necessary and without distortion. This is the equivalent of the CSS
40-
`object-fit: cover` setting.
40+
`object-fit: cover` setting. There is an e2e test for this.
4141
- No upscaling. The image should not be upscaled if it is smaller than the
4242
requested dimensions. Instead it should return the largest available size, but
4343
maintain the requested aspect ratio.

demo/src/examples.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
],
1010
"builder.io": [
1111
"Builder.io",
12-
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?format=webp&fit=cover"
12+
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f"
1313
],
1414
"cloudinary": [
1515
"Cloudinary",
1616
"https://res.cloudinary.com/demo/image/upload/c_lfill,w_800,h_550,f_auto/dog.webp"
1717
],
1818
"imgix": [
19-
"Imgix (Unsplash)",
19+
"Imgix",
2020
"https://images.unsplash.com/photo-1674255909399-9bcb2cab6489?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=996&q=80"
2121
],
2222
"wordpress": [

deno.lock

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/e2e.test.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
assertEquals,
3+
assertExists,
4+
} from "https://deno.land/[email protected]/testing/asserts.ts";
5+
import examples from "../demo/src/examples.json" assert { type: "json" };
6+
import { getPixels } from "https://deno.land/x/[email protected]/mod.ts";
7+
import { transformUrl } from "./transform.ts";
8+
import type { ImageCdn } from "./types.ts";
9+
10+
Deno.test("E2E tests", async (t) => {
11+
for (const [cdn, example] of Object.entries(examples)) {
12+
const [name, url] = example;
13+
await t.step(`${name} resizes an image`, async () => {
14+
const image = transformUrl({
15+
url,
16+
width: 100,
17+
cdn: cdn as ImageCdn,
18+
});
19+
20+
assertExists(image, `Failed to resize ${name} with ${cdn}`);
21+
const { width } = await getPixels(image);
22+
23+
assertEquals(width, 100);
24+
});
25+
26+
// Builder.io handles crop incorrectly at the moment
27+
if (cdn === "builder.io") {
28+
continue;
29+
}
30+
31+
await t.step(`${name} returns requested aspect ratio`, async () => {
32+
const image = transformUrl({
33+
url,
34+
width: 100,
35+
height: 50,
36+
cdn: cdn as ImageCdn,
37+
});
38+
39+
assertExists(image, `Failed to resize ${name} with ${cdn}`);
40+
41+
const { width, height } = await getPixels(image);
42+
43+
assertEquals(width, 100);
44+
assertEquals(height, 50);
45+
});
46+
}
47+
});

src/transformers/builder.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ Deno.test("builder.io", async (t) => {
1414
});
1515
assertEquals(
1616
result?.toString(),
17-
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=200&height=100",
17+
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200&height=100&fit=cover",
1818
);
1919
});
2020
await t.step("should not set height if not provided", () => {
2121
const result = transform({ url: img, width: 200 });
2222
assertEquals(
2323
result?.toString(),
24-
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=200",
24+
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200",
2525
);
2626
});
2727
await t.step("should delete height if not set", () => {
@@ -30,7 +30,7 @@ Deno.test("builder.io", async (t) => {
3030
const result = transform({ url, width: 200 });
3131
assertEquals(
3232
result?.toString(),
33-
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=200",
33+
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200",
3434
);
3535
});
3636

@@ -42,7 +42,7 @@ Deno.test("builder.io", async (t) => {
4242
});
4343
assertEquals(
4444
result?.toString(),
45-
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=201&height=100",
45+
"https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=201&height=100&fit=cover",
4646
);
4747
});
4848

src/transformers/builder.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ export const transform: UrlTransformer = (
2929
{ url: originalUrl, width, height, format },
3030
) => {
3131
const url = new URL(originalUrl);
32-
setParamIfUndefined(url, "fit", "cover");
3332
setParamIfDefined(url, "width", width, true, true);
3433
setParamIfDefined(url, "height", height, true, true);
3534
setParamIfDefined(url, "format", format);
35+
if (width && height) {
36+
setParamIfUndefined(url, "fit", "cover");
37+
}
3638
return url;
3739
};

src/transformers/bunny.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { UrlParser, UrlTransformer } from "../types.ts";
2-
import { getNumericParam, setParamIfDefined } from "../utils.ts";
2+
import {
3+
getNumericParam,
4+
setParamIfDefined,
5+
setParamIfUndefined,
6+
} from "../utils.ts";
37

48
export const parse: UrlParser<{ fit?: string }> = (url) => {
59
const parsedUrl = new URL(url);
@@ -25,6 +29,8 @@ export const transform: UrlTransformer = (
2529
) => {
2630
const url = new URL(originalUrl);
2731
setParamIfDefined(url, "width", width, true, true);
28-
setParamIfDefined(url, "height", height, true, true);
32+
if (width && height) {
33+
setParamIfUndefined(url, "aspect_ratio", `${width}:${height}`);
34+
}
2935
return url;
3036
};

src/transformers/cloudflare.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ParsedUrl } from "../types.ts";
33
import { CloudflareParams, parse, transform } from "./cloudflare.ts";
44

55
const img =
6-
"https://assets.brevity.io/cdn-cgi/image/background=red,width=128,height=128,f=auto/uploads/generic/avatar-sample.jpeg"
6+
"https://assets.brevity.io/cdn-cgi/image/background=red,width=128,height=128,f=auto/uploads/generic/avatar-sample.jpeg";
77

88
Deno.test("cloudflare parser", () => {
99
const parsed = parse(img);
@@ -33,7 +33,7 @@ Deno.test("cloudflare transformer", async (t) => {
3333
});
3434
assertEquals(
3535
result?.toString(),
36-
"https://assets.brevity.io/cdn-cgi/image/background=red,width=100,height=200,f=auto/uploads/generic/avatar-sample.jpeg"
36+
"https://assets.brevity.io/cdn-cgi/image/background=red,width=100,height=200,f=auto,fit=cover/uploads/generic/avatar-sample.jpeg",
3737
);
3838
});
3939
});

src/transformers/cloudflare.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const formatUrl = (
1515
{
1616
host,
1717
transformations = {},
18-
path
18+
path,
1919
}: CloudflareParams,
2020
): string => {
2121
const transformString = Object.entries(transformations).map(
@@ -27,7 +27,7 @@ const formatUrl = (
2727
"cdn-cgi",
2828
"image",
2929
transformString,
30-
path
30+
path,
3131
].join("/");
3232
return `https://${pathSegments}`;
3333
};
@@ -87,6 +87,9 @@ export const generate: UrlGenerator<CloudflareParams> = (
8787
if (format) {
8888
props.transformations.f = format;
8989
}
90+
91+
props.transformations.fit ||= "cover";
92+
9093
return new URL(formatUrl(props));
9194
};
9295

src/transformers/kontentai.test.ts

+18-15
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
33
import { transform } from "./kontentai.ts";
44

55
const img =
6-
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg"
6+
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg";
77

88
Deno.test("kontent.ai", async (t) => {
99
await t.step("should format a URL", () => {
1010
const result = transform({
1111
url: img,
1212
width: 200,
1313
height: 100,
14-
format: 'webp'
14+
format: "webp",
1515
});
1616
assertEquals(
1717
result?.toString(),
@@ -22,7 +22,7 @@ Deno.test("kontent.ai", async (t) => {
2222
const result = transform({ url: img, width: 200 });
2323
assertEquals(
2424
result?.toString(),
25-
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg?w=200&fit=crop",
25+
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg?w=200",
2626
);
2727
});
2828

@@ -38,24 +38,27 @@ Deno.test("kontent.ai", async (t) => {
3838
);
3939
});
4040

41-
await t.step("should add fit=scale when height or width (or both) provided and no other fit setting", () => {
42-
const result = transform({
43-
url: img,
44-
width: 200,
45-
height: 100,
46-
});
47-
assertEquals(
48-
result?.toString(),
49-
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg?w=200&h=100&fit=crop",
50-
);
51-
});
41+
await t.step(
42+
"should add fit=scale when height or width (or both) provided and no other fit setting",
43+
() => {
44+
const result = transform({
45+
url: img,
46+
width: 200,
47+
height: 100,
48+
});
49+
assertEquals(
50+
result?.toString(),
51+
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg?w=200&h=100&fit=crop",
52+
);
53+
},
54+
);
5255
await t.step("should not set fit=scale if another value exists", () => {
5356
const url = new URL(img);
5457
url.searchParams.set("fit", "scale");
5558
const result = transform({
5659
url: url,
5760
width: 200,
58-
height: 100
61+
height: 100,
5962
});
6063
assertEquals(
6164
result?.toString(),

src/transformers/kontentai.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { UrlParser, UrlTransformer } from "../types.ts";
22
import {
33
getNumericParam,
44
setParamIfDefined,
5-
setParamIfUndefined
5+
setParamIfUndefined,
66
} from "../utils.ts";
77

88
export const parse: UrlParser<{ fit?: string }> = (url) => {
@@ -32,7 +32,7 @@ export const transform: UrlTransformer = (
3232
setParamIfDefined(url, "w", width, true, true);
3333
setParamIfDefined(url, "h", height, true, true);
3434
setParamIfDefined(url, "fm", format, true);
35-
if (width || height) {
35+
if (width && height) {
3636
setParamIfUndefined(url, "fit", "crop");
3737
}
3838
return url;

0 commit comments

Comments
 (0)