diff --git a/.changeset/blur-areas.md b/.changeset/blur-areas.md new file mode 100644 index 0000000..2db8bf0 --- /dev/null +++ b/.changeset/blur-areas.md @@ -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. diff --git a/src/options/blurAreas.ts b/src/options/blurAreas.ts new file mode 100644 index 0000000..73c33c0 --- /dev/null +++ b/src/options/blurAreas.ts @@ -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 = [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 }; diff --git a/src/options/index.ts b/src/options/index.ts index b441c27..8516776 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -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"; diff --git a/src/types/blurAreas.ts b/src/types/blurAreas.ts new file mode 100644 index 0000000..c40b38d --- /dev/null +++ b/src/types/blurAreas.ts @@ -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 }; diff --git a/src/types/index.ts b/src/types/index.ts index 304967f..c7b8cec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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"; @@ -85,6 +86,7 @@ export type Options = AdjustOptionsPartial & AvifOptionsPartial & BackgroundOptionsPartial & BackgroundAlphaOptionsPartial & + BlurAreasOptionsPartial & BlurDetectionsOptionsPartial & BlurOptionsPartial & BrightnessOptionsPartial & diff --git a/tests/optionsBasic/blurAreas.test.ts b/tests/optionsBasic/blurAreas.test.ts new file mode 100644 index 0000000..c1a2a1c --- /dev/null +++ b/tests/optionsBasic/blurAreas.test.ts @@ -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"); + }); + }); +});