diff --git a/.changeset/two-days-look.md b/.changeset/two-days-look.md new file mode 100644 index 00000000..ce9dd2b2 --- /dev/null +++ b/.changeset/two-days-look.md @@ -0,0 +1,5 @@ +--- +"@imgproxy/imgproxy-js-core": minor +--- + +Add support for objw mode for [gravity option](https://docs.imgproxy.net/usage/processing#gravity) diff --git a/src/options/gravity.ts b/src/options/gravity.ts index 68b13a50..b1095aa3 100644 --- a/src/options/gravity.ts +++ b/src/options/gravity.ts @@ -3,6 +3,7 @@ import type { Gravity, FPGravity, ObjGravity, + ObjwGravity, BaseGravity, } from "../types/gravity"; import { @@ -33,6 +34,7 @@ const currentAllTypes = { sm: true, fp: true, obj: true, + objw: true, }; const getOpt = (options: GravityOptionsPartial): Gravity | undefined => @@ -64,6 +66,9 @@ const build = ( if (gravityOpts.class_names && type !== "obj") throw new Error("gravity.class_names can be used only with type obj"); // @ts-expect-error: Let's ignore an error. + if (gravityOpts.class_weights && type !== "objw") + throw new Error("gravity.class_weights can be used only with type objw"); + // @ts-expect-error: Let's ignore an error. if ((gravityOpts.x || gravityOpts.y) && type !== "fp") throw new Error("gravity.x and gravity.y can be used only with type fp"); @@ -90,6 +95,28 @@ const build = ( const class_names = gravityObj.class_names; return withHead(`${type}:${class_names.join(":")}`, headless); + } + + if (type === "objw") { + const gravityObjw = gravityOpts as ObjwGravity; + + guardIsUndef(gravityObjw.class_weights, "gravity.class_weights"); + guardIsNotArray(gravityObjw.class_weights, "gravity.class_weights"); + + const weightPairs = gravityObjw.class_weights.map(item => { + if ( + typeof item !== "object" || + !item.class || + typeof item.weight !== "number" + ) { + throw new Error( + "Each item in gravity.class_weights must have 'class' and 'weight' properties" + ); + } + return `${item.class}:${item.weight}`; + }); + + return withHead(`${type}:${weightPairs.join(":")}`, headless); } else { const gravityBase = gravityOpts as BaseGravity; const x_offset = diff --git a/src/types/gravity.ts b/src/types/gravity.ts index e979def5..e6f5e4cc 100644 --- a/src/types/gravity.ts +++ b/src/types/gravity.ts @@ -106,6 +106,26 @@ interface ObjGravity { class_names: string[]; } +/** + * **PRO feature.** + * + * Object-weighted gravity. imgproxy detects objects of provided classes on the image, calculates the resulting image center using their positions, and adds weights to these positions. + * + * If class weights are omited, imgproxy will use all the detected objects with equal weights. + * + * @param {string} type - Must be `objw`. + * @param {Array<{class: string, weight: number}>} class_weights - Array of objects with class names and their weights. + * + * @example + * {gravity: {type: "objw", class_weights: [{class: "face", weight: 1}, {class: "person", weight: 0.5}]}} + * + * @see https://docs.imgproxy.net/generating_the_url?id=gravity + */ +interface ObjwGravity { + type: "objw"; + class_weights: Array<{ class: string; weight: number }>; +} + /** * *Gravity option* * @@ -138,6 +158,12 @@ interface ObjGravity { * If class names are omited, imgproxy will use all the detected objects. * @param {string} type - Must be `obj`. * @param {string[]} class_names - Array of class names. + * + * *Object-weighted gravity*. **PRO feature.** + * imgproxy detects objects of provided classes on the image, calculates the resulting image center using their positions, and adds weights to these positions. + * If class weights are omited, imgproxy will use all the detected objects with equal weights. + * @param {string} type - Must be `objw`. + * @param {Array<{class: string, weight: number}>} class_weights - Array of objects with class names and their weights. * * *FP gravity*. * The gravity focus point. @@ -164,13 +190,21 @@ interface ObjGravity { * * @example Object-oriented gravity * {gravity: {type: "obj", class_names: ["face", "person"]}} + * + * @example Object-weighted gravity + * {gravity: {type: "objw", class_weights: [{class: "face", weight: 1}, {class: "person", weight: 0.5}]}} * * @example FP gravity * {gravity: {type: "fp", x: 0.5, y: 0.5}} * * @see https://docs.imgproxy.net/generating_the_url?id=gravity */ -type Gravity = BaseGravity | SmartGravity | ObjGravity | FPGravity; +type Gravity = + | BaseGravity + | SmartGravity + | ObjGravity + | ObjwGravity + | FPGravity; /** * *Gravity option* @@ -184,4 +218,11 @@ interface GravityOptionsPartial { g?: Gravity; } -export { BaseGravity, FPGravity, ObjGravity, Gravity, GravityOptionsPartial }; +export { + BaseGravity, + FPGravity, + ObjGravity, + ObjwGravity, + Gravity, + GravityOptionsPartial, +}; diff --git a/tests/optionsBasic/gravity.test.ts b/tests/optionsBasic/gravity.test.ts index dd48cb87..5521f5de 100644 --- a/tests/optionsBasic/gravity.test.ts +++ b/tests/optionsBasic/gravity.test.ts @@ -240,5 +240,70 @@ describe("gravity", () => { ).toEqual("g:fp:0:0"); }); }); + + describe("ObjwGravity", () => { + it("should throw an error if gravity includes property class_weights but type is not 'objw'", () => { + expect(() => + build({ + gravity: { + type: "no", + // @ts-expect-error: Let's ignore an error (check for users with vanilla js). + class_weights: [{ class: "face", weight: 1 }], + }, + }) + ).toThrow(`gravity.class_weights can be used only with type objw`); + }); + + it("should throw an error if class_weights is undefined", () => { + expect(() => + build({ + // @ts-expect-error: Let's ignore an error (check for users with vanilla js). + gravity: { + type: "objw", + }, + }) + ).toThrow(`gravity.class_weights is undefined`); + }); + + it("should throw an error if class_weights is not an array", () => { + expect(() => + build({ + gravity: { + type: "objw", + // @ts-expect-error: Let's ignore an error (check for users with vanilla js). + class_weights: "face:1", + }, + }) + ).toThrow(`gravity.class_weights is not an array`); + }); + + it("should throw an error if a class_weights item is missing class or weight", () => { + expect(() => + build({ + gravity: { + type: "objw", + // @ts-expect-error: Let's ignore an error (check for users with vanilla js). + class_weights: [{ class: "face" }], + }, + }) + ).toThrow( + `Each item in gravity.class_weights must have 'class' and 'weight' properties` + ); + }); + + it("should return g:objw:face:1:person:0.5 if gravity is {type: 'objw', class_weights: [{class: 'face', weight: 1}, {class: 'person', weight: 0.5}]} ", () => { + expect( + build({ + gravity: { + type: "objw", + class_weights: [ + { class: "face", weight: 1 }, + { class: "person", weight: 0.5 }, + ], + }, + }) + ).toEqual("g:objw:face:1:person:0.5"); + }); + }); }); });