Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blur-areas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imgproxy/imgproxy-js-core": minor
---

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.
40 changes: 40 additions & 0 deletions src/options/blurAreas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { BlurAreas, BlurAreasOptionsPartial } from "../types/blurAreas";
import { guardIsUndef, guardIsNotNum, guardIsNotArray } from "../utils";

const getOpt = (options: BlurAreasOptionsPartial): BlurAreas | undefined =>
options.blur_areas ?? options.ba;

const test = (options: BlurAreasOptionsPartial): boolean =>
Boolean(getOpt(options));

const build = (options: BlurAreasOptionsPartial): string => {
const blurAreasOpts = getOpt(options);

guardIsUndef(blurAreasOpts, "blur_areas");
guardIsNotNum(blurAreasOpts.sigma, "blur_areas.sigma", {
addParam: { min: 0 },
});
guardIsNotArray(blurAreasOpts.areas, "blur_areas.areas");

const parts: Array<string | number> = [blurAreasOpts.sigma];

blurAreasOpts.areas.forEach((area, index) => {
guardIsNotNum(area?.left, `blur_areas.areas[${index}].left`, {
addParam: { min: 0, max: 1 },
});
guardIsNotNum(area.top, `blur_areas.areas[${index}].top`, {
addParam: { min: 0, max: 1 },
});
guardIsNotNum(area.width, `blur_areas.areas[${index}].width`, {
addParam: { min: 0, max: 1 },
});
guardIsNotNum(area.height, `blur_areas.areas[${index}].height`, {
addParam: { min: 0, max: 1 },
});
parts.push(area.left, area.top, area.width, area.height);
});

return `ba:${parts.join(":")}`;
};

export { test, build };
1 change: 1 addition & 0 deletions src/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * as avifOptions from "./avifOptions";
export * as background from "./background";
export * as backgroundAlpha from "./backgroundAlpha";
export * as blur from "./blur";
export * as blurAreas from "./blurAreas";
export * as blurDetections from "./blurDetections";
export * as brightness from "./brightness";
export * as cacheBuster from "../optionsShared/cacheBuster";
Expand Down
51 changes: 51 additions & 0 deletions src/types/blurAreas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* *Blur area coordinates*. **PRO feature**
*
* @param {number} left - The left edge of the area, as a floating-point value between `0` and `1`, relative to the source image width.
* @param {number} top - The top edge of the area, as a floating-point value between `0` and `1`, relative to the source image height.
* @param {number} width - The width of the area, as a floating-point value between `0` and `1`, relative to the source image width.
* @param {number} height - The height of the area, as a floating-point value between `0` and `1`, relative to the source image height.
*
* @note The coordinates should be defined with respect to the source image EXIF orientation,
* as imgproxy doesn't rotate/flip them according to EXIF orientation.
* However, imgproxy rotates/flips the areas according to the `rotate` and `flip` options.
*/
interface BlurArea {
left: number;
top: number;
width: number;
height: number;
}

/**
* *Blur areas*. **PRO feature**
*
* When `sigma` is greater than `0`, imgproxy will apply a Gaussian blur filter
* to the defined areas of the resulting image.
*
* @param {number} sigma - Defines the size of the mask imgproxy will use.
* @param {BlurArea[]} areas - The list of areas to be blurred.
*
* @example
* {blur_areas: {sigma: 5, areas: [{left: 0.1, top: 0.1, width: 0.2, height: 0.2}]}}
*
* @see {@link https://docs.imgproxy.net/usage/processing#blur-areas | blur areas option imgproxy docs}
*/
interface BlurAreas {
sigma: number;
areas: BlurArea[];
}

/**
* *Blur areas option*. **PRO feature**
*
* To describe the Blur areas option, you can use the keyword `blur_areas` or `ba`.
*
* @see https://docs.imgproxy.net/usage/processing#blur-areas
*/
interface BlurAreasOptionsPartial {
blur_areas?: BlurAreas;
ba?: BlurAreas;
}

export { BlurArea, BlurAreas, BlurAreasOptionsPartial };
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AutoRotateOptionsPartial } from "./autoRotate";
import type { AvifOptionsPartial } from "./avifOptions";
import type { BackgroundOptionsPartial } from "./background";
import type { BackgroundAlphaOptionsPartial } from "./backgroundAlpha";
import type { BlurAreasOptionsPartial } from "./blurAreas";
import type { BlurDetectionsOptionsPartial } from "./blurDetections";
import type { BlurOptionsPartial } from "./blur";
import type { BrightnessOptionsPartial } from "./brightness";
Expand Down Expand Up @@ -85,6 +86,7 @@ export type Options = AdjustOptionsPartial &
AvifOptionsPartial &
BackgroundOptionsPartial &
BackgroundAlphaOptionsPartial &
BlurAreasOptionsPartial &
BlurDetectionsOptionsPartial &
BlurOptionsPartial &
BrightnessOptionsPartial &
Expand Down
121 changes: 121 additions & 0 deletions tests/optionsBasic/blurAreas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import { test, build } from "../../src/options/blurAreas";

describe("blurAreas", () => {
describe("test", () => {
it("should return true if blur_areas option is defined", () => {
expect(
test({
blur_areas: {
sigma: 2,
areas: [{ left: 0, top: 0, width: 0.5, height: 0.5 }],
},
})
).toEqual(true);
});

it("should return true if ba option is defined", () => {
expect(
test({
ba: {
sigma: 2,
areas: [{ left: 0, top: 0, width: 0.5, height: 0.5 }],
},
})
).toEqual(true);
});

it("should return false if blur_areas option is undefined", () => {
expect(test({})).toEqual(false);
});
});

describe("build", () => {
it("should throw an error if blur_areas option is undefined", () => {
expect(() => build({})).toThrow("blur_areas option is undefined");
});

it("should throw an error if blur_areas.sigma option is undefined", () => {
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
expect(() => build({ blur_areas: { areas: [] } })).toThrow(
"blur_areas.sigma is not a number"
);
});

it("should throw an error if blur_areas.sigma is not a number", () => {
expect(() =>
build({
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
blur_areas: { sigma: "2", areas: [] },
})
).toThrow("blur_areas.sigma is not a number");
});

it("should throw an error if blur_areas.sigma is less than 0", () => {
expect(() => build({ blur_areas: { sigma: -1, areas: [] } })).toThrow(
"blur_areas.sigma value can't be less than 0"
);
});

it("should throw an error if blur_areas.areas is not an array", () => {
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
expect(() => build({ blur_areas: { sigma: 2 } })).toThrow(
"blur_areas.areas is not an array"
);
});

it("should throw an error if blur_areas.areas is empty", () => {
expect(() => build({ blur_areas: { sigma: 2, areas: [] } })).toThrow(
"blur_areas.areas is empty"
);
});

it("should throw an error if area.left is not a number", () => {
expect(() =>
build({
blur_areas: {
sigma: 2,
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
areas: [{ top: 0, width: 0.1, height: 0.1 }],
},
})
).toThrow("blur_areas.areas[0].left is not a number");
});

it("should throw an error if area.left is out of range", () => {
expect(() =>
build({
blur_areas: {
sigma: 2,
areas: [{ left: 1.5, top: 0, width: 0.1, height: 0.1 }],
},
})
).toThrow("blur_areas.areas[0].left value can't be more than 1");
});

it("should build a url with a single area", () => {
expect(
build({
blur_areas: {
sigma: 2,
areas: [{ left: 0.1, top: 0.2, width: 0.3, height: 0.4 }],
},
})
).toEqual("ba:2:0.1:0.2:0.3:0.4");
});

it("should build a url with multiple areas using `ba` alias", () => {
expect(
build({
ba: {
sigma: 5,
areas: [
{ left: 0, top: 0, width: 0.5, height: 0.5 },
{ left: 0.5, top: 0.5, width: 0.25, height: 0.25 },
],
},
})
).toEqual("ba:5:0:0:0.5:0.5:0.5:0.5:0.25:0.25");
});
});
});
Loading