Skip to content

Commit 73f1679

Browse files
committed
Add blur_areas support
1 parent f416250 commit 73f1679

6 files changed

Lines changed: 220 additions & 0 deletions

File tree

.changeset/blur-areas.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@imgproxy/imgproxy-js-core": minor
3+
---
4+
5+
Add support for [blur_areas](https://docs.imgproxy.net/usage/processing#blur-areas) option (imgproxy Pro). When `sigma` is greater than `0`, imgproxy applies a Gaussian blur filter to the provided areas of the resulting image. The option accepts a `sigma` value and a list of `areas` with `left`, `top`, `width`, and `height` floats between `0` and `1`. The short form `ba` is also supported.

src/options/blurAreas.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { BlurAreas, BlurAreasOptionsPartial } from "../types/blurAreas";
2+
import { guardIsUndef, guardIsNotNum, guardIsNotArray } from "../utils";
3+
4+
const getOpt = (options: BlurAreasOptionsPartial): BlurAreas | undefined =>
5+
options.blur_areas ?? options.ba;
6+
7+
const test = (options: BlurAreasOptionsPartial): boolean =>
8+
Boolean(getOpt(options));
9+
10+
const build = (options: BlurAreasOptionsPartial): string => {
11+
const blurAreasOpts = getOpt(options);
12+
13+
guardIsUndef(blurAreasOpts, "blur_areas");
14+
guardIsNotNum(blurAreasOpts.sigma, "blur_areas.sigma", {
15+
addParam: { min: 0 },
16+
});
17+
guardIsNotArray(blurAreasOpts.areas, "blur_areas.areas");
18+
19+
const parts: Array<string | number> = [blurAreasOpts.sigma];
20+
21+
blurAreasOpts.areas.forEach((area, index) => {
22+
guardIsNotNum(area?.left, `blur_areas.areas[${index}].left`, {
23+
addParam: { min: 0, max: 1 },
24+
});
25+
guardIsNotNum(area.top, `blur_areas.areas[${index}].top`, {
26+
addParam: { min: 0, max: 1 },
27+
});
28+
guardIsNotNum(area.width, `blur_areas.areas[${index}].width`, {
29+
addParam: { min: 0, max: 1 },
30+
});
31+
guardIsNotNum(area.height, `blur_areas.areas[${index}].height`, {
32+
addParam: { min: 0, max: 1 },
33+
});
34+
parts.push(area.left, area.top, area.width, area.height);
35+
});
36+
37+
return `ba:${parts.join(":")}`;
38+
};
39+
40+
export { test, build };

src/options/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * as avifOptions from "./avifOptions";
55
export * as background from "./background";
66
export * as backgroundAlpha from "./backgroundAlpha";
77
export * as blur from "./blur";
8+
export * as blurAreas from "./blurAreas";
89
export * as blurDetections from "./blurDetections";
910
export * as brightness from "./brightness";
1011
export * as cacheBuster from "../optionsShared/cacheBuster";

src/types/blurAreas.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* *Blur area coordinates*. **PRO feature**
3+
*
4+
* @param {number} left - The left edge of the area, as a floating-point value between `0` and `1`, relative to the source image width.
5+
* @param {number} top - The top edge of the area, as a floating-point value between `0` and `1`, relative to the source image height.
6+
* @param {number} width - The width of the area, as a floating-point value between `0` and `1`, relative to the source image width.
7+
* @param {number} height - The height of the area, as a floating-point value between `0` and `1`, relative to the source image height.
8+
*
9+
* @note The coordinates should be defined with respect to the source image EXIF orientation,
10+
* as imgproxy doesn't rotate/flip them according to EXIF orientation.
11+
* However, imgproxy rotates/flips the areas according to the `rotate` and `flip` options.
12+
*/
13+
interface BlurArea {
14+
left: number;
15+
top: number;
16+
width: number;
17+
height: number;
18+
}
19+
20+
/**
21+
* *Blur areas*. **PRO feature**
22+
*
23+
* When `sigma` is greater than `0`, imgproxy will apply a Gaussian blur filter
24+
* to the defined areas of the resulting image.
25+
*
26+
* @param {number} sigma - Defines the size of the mask imgproxy will use.
27+
* @param {BlurArea[]} areas - The list of areas to be blurred.
28+
*
29+
* @example
30+
* {blur_areas: {sigma: 5, areas: [{left: 0.1, top: 0.1, width: 0.2, height: 0.2}]}}
31+
*
32+
* @see {@link https://docs.imgproxy.net/usage/processing#blur-areas | blur areas option imgproxy docs}
33+
*/
34+
interface BlurAreas {
35+
sigma: number;
36+
areas: BlurArea[];
37+
}
38+
39+
/**
40+
* *Blur areas option*. **PRO feature**
41+
*
42+
* To describe the Blur areas option, you can use the keyword `blur_areas` or `ba`.
43+
*
44+
* @see https://docs.imgproxy.net/usage/processing#blur-areas
45+
*/
46+
interface BlurAreasOptionsPartial {
47+
blur_areas?: BlurAreas;
48+
ba?: BlurAreas;
49+
}
50+
51+
export { BlurArea, BlurAreas, BlurAreasOptionsPartial };

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AutoRotateOptionsPartial } from "./autoRotate";
44
import type { AvifOptionsPartial } from "./avifOptions";
55
import type { BackgroundOptionsPartial } from "./background";
66
import type { BackgroundAlphaOptionsPartial } from "./backgroundAlpha";
7+
import type { BlurAreasOptionsPartial } from "./blurAreas";
78
import type { BlurDetectionsOptionsPartial } from "./blurDetections";
89
import type { BlurOptionsPartial } from "./blur";
910
import type { BrightnessOptionsPartial } from "./brightness";
@@ -85,6 +86,7 @@ export type Options = AdjustOptionsPartial &
8586
AvifOptionsPartial &
8687
BackgroundOptionsPartial &
8788
BackgroundAlphaOptionsPartial &
89+
BlurAreasOptionsPartial &
8890
BlurDetectionsOptionsPartial &
8991
BlurOptionsPartial &
9092
BrightnessOptionsPartial &
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, expect, it } from "vitest";
2+
import { test, build } from "../../src/options/blurAreas";
3+
4+
describe("blurAreas", () => {
5+
describe("test", () => {
6+
it("should return true if blur_areas option is defined", () => {
7+
expect(
8+
test({
9+
blur_areas: {
10+
sigma: 2,
11+
areas: [{ left: 0, top: 0, width: 0.5, height: 0.5 }],
12+
},
13+
})
14+
).toEqual(true);
15+
});
16+
17+
it("should return true if ba option is defined", () => {
18+
expect(
19+
test({
20+
ba: {
21+
sigma: 2,
22+
areas: [{ left: 0, top: 0, width: 0.5, height: 0.5 }],
23+
},
24+
})
25+
).toEqual(true);
26+
});
27+
28+
it("should return false if blur_areas option is undefined", () => {
29+
expect(test({})).toEqual(false);
30+
});
31+
});
32+
33+
describe("build", () => {
34+
it("should throw an error if blur_areas option is undefined", () => {
35+
expect(() => build({})).toThrow("blur_areas option is undefined");
36+
});
37+
38+
it("should throw an error if blur_areas.sigma option is undefined", () => {
39+
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
40+
expect(() => build({ blur_areas: { areas: [] } })).toThrow(
41+
"blur_areas.sigma is not a number"
42+
);
43+
});
44+
45+
it("should throw an error if blur_areas.sigma is not a number", () => {
46+
expect(() =>
47+
build({
48+
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
49+
blur_areas: { sigma: "2", areas: [] },
50+
})
51+
).toThrow("blur_areas.sigma is not a number");
52+
});
53+
54+
it("should throw an error if blur_areas.sigma is less than 0", () => {
55+
expect(() => build({ blur_areas: { sigma: -1, areas: [] } })).toThrow(
56+
"blur_areas.sigma value can't be less than 0"
57+
);
58+
});
59+
60+
it("should throw an error if blur_areas.areas is not an array", () => {
61+
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
62+
expect(() => build({ blur_areas: { sigma: 2 } })).toThrow(
63+
"blur_areas.areas is not an array"
64+
);
65+
});
66+
67+
it("should throw an error if blur_areas.areas is empty", () => {
68+
expect(() => build({ blur_areas: { sigma: 2, areas: [] } })).toThrow(
69+
"blur_areas.areas is empty"
70+
);
71+
});
72+
73+
it("should throw an error if area.left is not a number", () => {
74+
expect(() =>
75+
build({
76+
blur_areas: {
77+
sigma: 2,
78+
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
79+
areas: [{ top: 0, width: 0.1, height: 0.1 }],
80+
},
81+
})
82+
).toThrow("blur_areas.areas[0].left is not a number");
83+
});
84+
85+
it("should throw an error if area.left is out of range", () => {
86+
expect(() =>
87+
build({
88+
blur_areas: {
89+
sigma: 2,
90+
areas: [{ left: 1.5, top: 0, width: 0.1, height: 0.1 }],
91+
},
92+
})
93+
).toThrow("blur_areas.areas[0].left value can't be more than 1");
94+
});
95+
96+
it("should build a url with a single area", () => {
97+
expect(
98+
build({
99+
blur_areas: {
100+
sigma: 2,
101+
areas: [{ left: 0.1, top: 0.2, width: 0.3, height: 0.4 }],
102+
},
103+
})
104+
).toEqual("ba:2:0.1:0.2:0.3:0.4");
105+
});
106+
107+
it("should build a url with multiple areas using `ba` alias", () => {
108+
expect(
109+
build({
110+
ba: {
111+
sigma: 5,
112+
areas: [
113+
{ left: 0, top: 0, width: 0.5, height: 0.5 },
114+
{ left: 0.5, top: 0.5, width: 0.25, height: 0.25 },
115+
],
116+
},
117+
})
118+
).toEqual("ba:5:0:0:0.5:0.5:0.5:0.5:0.25:0.25");
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)