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/exclusive-gravity-union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@imgproxy/imgproxy-js-core": minor
---

Make the `Gravity` union exclusive: fields belonging to one gravity variant are now typed as forbidden (`?: never`) on the others, so mixing fields from different variants (such as `{ type: "sm", x_offset: 0 }` or `{ type: "no", x: 0.5 }`) is a compile-time error for TypeScript users instead of only a runtime one. Runtime behavior is unchanged. TypeScript code that previously compiled while passing such invalid combinations will now fail to compile — the same combinations have always thrown at runtime.
7 changes: 7 additions & 0 deletions .changeset/falsy-values-skipping-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@imgproxy/imgproxy-js-core": patch
---

Fix optional arguments being checked for truthiness instead of presence in `resize`, `size`, `watermark`, `watermark_size`, `trim`, `unsharp_masking`, `png_options`, `jpeg_options`, `colorize`, `monochrome` and `gravity`. Falsy but invalid values (such as `null` or an empty `color`) skipped validation and were silently rendered as omitted arguments, so `{ watermark_size: { width: null, height: 100 } }` produced `wms::100` instead of raising. Such values now raise an error, and explicitly provided `0` values are rendered instead of being dropped. `colorize.color` and `monochrome.color` are now validated as hex colors, matching `trim.color` and `gradient.color`.

Also fix `jpeg_options` rejecting `progressive: false` when `optimize_scans` was set to `false`. The cross-check now applies only when `optimize_scans` is actually enabled.
4 changes: 3 additions & 1 deletion src/options/colorize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Colorize, ColorizeOptionsPartial } from "../types/colorize";
import { guardIsNotNum, guardIsUndef } from "../utils";
import { guardIsNotNum, guardIsNotStr, guardIsUndef } from "../utils";

const getOpt = (options: ColorizeOptionsPartial): Colorize | undefined =>
options.colorize ?? options.col;
Expand All @@ -22,6 +22,8 @@ const build = (options: ColorizeOptionsPartial): string => {
},
});

if (color !== undefined) guardIsNotStr(color, "colorize.color", true);

let result = `col:${opacity}`;

if (color) {
Expand Down
11 changes: 5 additions & 6 deletions src/options/gravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,16 @@ const build = (
const type = gravityOpts.type;

guardIsValidVal(currentAllTypes, type, "gravity.type");
// @ts-expect-error: Let's ignore an error.
if (gravityOpts.x_offset || gravityOpts.y_offset)
if (gravityOpts.x_offset !== undefined || gravityOpts.y_offset !== undefined)
guardIsValidVal(currentBaseTypes, type, "gravity.type");
// @ts-expect-error: Let's ignore an error.
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")
if (
(gravityOpts.x !== undefined || gravityOpts.y !== undefined) &&
type !== "fp"
)
throw new Error("gravity.x and gravity.y can be used only with type fp");

if (type === "sm") {
Expand Down
16 changes: 9 additions & 7 deletions src/options/jpegOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,23 @@ const build = (options: JPEGOptionsPartial): string => {
quant_table,
} = jpegOptions;

if (progressive) guardIsNotBool(progressive, "jpeg_options.progressive");
if (no_subsample) guardIsNotBool(no_subsample, "jpeg_options.no_subsample");
if (trellis_quant)
if (progressive !== undefined)
guardIsNotBool(progressive, "jpeg_options.progressive");
if (no_subsample !== undefined)
guardIsNotBool(no_subsample, "jpeg_options.no_subsample");
if (trellis_quant !== undefined)
guardIsNotBool(trellis_quant, "jpeg_options.trellis_quant");
if (overshoot_deringing)
if (overshoot_deringing !== undefined)
guardIsNotBool(overshoot_deringing, "jpeg_options.overshoot_deringing");

if (optimize_scans) {
if (optimize_scans !== undefined) {
guardIsNotBool(optimize_scans, "jpeg_options.optimize_scans");
if (progressive === false)
if (optimize_scans && progressive === false)
throw new Error(
"jpeg_options.progressive must be true if jpeg_options.optimize_scans is true"
);
}
if (quant_table)
if (quant_table !== undefined)
guardIsNotNum(quant_table, "jpeg_options.quant_table", {
addParam: { min: 0, max: 8 },
});
Expand Down
5 changes: 3 additions & 2 deletions src/options/monochrome.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Monochrome, MonochromeOptionsPartial } from "../types/monochrome";
import { guardIsNotNum, guardIsUndef } from "../utils";
import { guardIsNotNum, guardIsNotStr, guardIsUndef } from "../utils";

const getOpt = (options: MonochromeOptionsPartial): Monochrome | undefined =>
options.monochrome ?? options.mc;
Expand All @@ -22,7 +22,8 @@ const build = (options: MonochromeOptionsPartial): string => {
},
});

if (color) {
if (color !== undefined) {
guardIsNotStr(color, "monochrome.color", true);
return `mc:${intensity}:${color}`;
}

Expand Down
9 changes: 5 additions & 4 deletions src/options/pngOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ const build = (options: PNGOptionsPartial): string => {
guardIsUndef(pngOptions, "png_options");
const { interlaced, quantize, quantization_colors } = pngOptions;

if (interlaced) guardIsNotBool(interlaced, "png_options.interlaced");
if (quantize) guardIsNotBool(quantize, "png_options.quantize");
if (quantization_colors)
if (interlaced !== undefined)
guardIsNotBool(interlaced, "png_options.interlaced");
if (quantize !== undefined) guardIsNotBool(quantize, "png_options.quantize");
if (quantization_colors !== undefined)
guardIsNotNum(quantization_colors, "png_options.quantization_colors", {
addParam: { min: 2, max: 256 },
});

const inter = interlaced === undefined ? "" : interlaced;
const quant = quantize === undefined ? "" : quantize;
const qc = quantization_colors || "";
const qc = quantization_colors ?? "";

return `pngo:${inter}:${quant}:${qc}`.replace(/:+$/, "");
};
Expand Down
16 changes: 9 additions & 7 deletions src/options/resize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ const build = (options: ResizeOptionsPartial): string => {
guardIsUndef(resizeOpts, "resize");
const { resizing_type, width, height, enlarge } = resizeOpts;

if (resizing_type)
if (resizing_type !== undefined)
guardIsValidVal(correctTypes, resizing_type, "resize.resizing_type");
if (width) guardIsNotNum(width, "resize.width", { addParam: { min: 0 } });
if (height) guardIsNotNum(height, "resize.height", { addParam: { min: 0 } });

const rt = resizing_type || "";
const w = width || "";
const h = height || "";
if (width !== undefined)
guardIsNotNum(width, "resize.width", { addParam: { min: 0 } });
if (height !== undefined)
guardIsNotNum(height, "resize.height", { addParam: { min: 0 } });

const rt = resizing_type ?? "";
const w = width ?? "";
const h = height ?? "";
const el = enlarge === undefined ? "" : normalizeBoolean(enlarge);
const ex = extendOpt.test(resizeOpts)
? extendOpt.build(resizeOpts, { headless: true })
Expand Down
10 changes: 6 additions & 4 deletions src/options/size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ const build = (options: SizeOptionsPartial): string => {
guardIsUndef(sizeOpts, "size");
const { width, height, enlarge } = sizeOpts;

if (width) guardIsNotNum(width, "size.width", { addParam: { min: 0 } });
if (height) guardIsNotNum(height, "size.height", { addParam: { min: 0 } });
if (width !== undefined)
guardIsNotNum(width, "size.width", { addParam: { min: 0 } });
if (height !== undefined)
guardIsNotNum(height, "size.height", { addParam: { min: 0 } });

const w = width || "";
const h = height || "";
const w = width ?? "";
const h = height ?? "";
const el = enlarge === undefined ? "" : normalizeBoolean(enlarge);
const ex = extendOpt.test(sizeOpts)
? extendOpt.build(sizeOpts, { headless: true })
Expand Down
4 changes: 2 additions & 2 deletions src/options/trim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ const build = (options: TrimOptionsPartial): string => {

guardIsUndef(threshold, "trim.threshold");
guardIsNotNum(threshold, "trim.threshold");
if (color) guardIsNotStr(color, "trim.color", true);
if (color !== undefined) guardIsNotStr(color, "trim.color", true);

const eh = equal_hor === undefined ? "" : normalizeBoolean(equal_hor);
const ev = equal_ver === undefined ? "" : normalizeBoolean(equal_ver);
const cl = color || "";
const cl = color ?? "";

return `t:${threshold}:${cl}:${eh}:${ev}`.replace(/:+$/, "");
};
Expand Down
9 changes: 5 additions & 4 deletions src/options/unsharpMasking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const build = (options: UnsharpMaskingOptionsPartial): string => {

guardIsUndef(unsharpMaskingOpts, "unsharp_masking");
const { mode, weight, divider } = unsharpMaskingOpts;
if (mode) guardIsValidVal(correctMode, mode, "unsharp_masking.mode");
if (mode !== undefined)
guardIsValidVal(correctMode, mode, "unsharp_masking.mode");
if (weight !== undefined)
guardIsNotNum(weight, "unsharp_masking.weight", {
addParam: { min: 0, minEqual: true },
Expand All @@ -32,9 +33,9 @@ const build = (options: UnsharpMaskingOptionsPartial): string => {
addParam: { min: 0, minEqual: true },
});

const modeStr = mode || "";
const weightStr = weight || "";
const dividerStr = divider || "";
const modeStr = mode ?? "";
const weightStr = weight ?? "";
const dividerStr = divider ?? "";

return `ush:${modeStr}:${weightStr}:${dividerStr}`.replace(/:+$/, "");
};
Expand Down
16 changes: 8 additions & 8 deletions src/options/watermark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ const build = (options: WatermarkOptionsPartial): string => {
guardIsNotNum(opacity, "watermark.opacity", { addParam: { min: 0, max: 1 } });

// watermarkOpts.position
if (position) {
if (position !== undefined) {
guardIsNotStr(position, "watermark.position");
guardIsValidVal(currentPositions, position, "watermark.position");
}

if (x_offset) guardIsNotNum(x_offset, "watermark.x_offset");
if (y_offset) guardIsNotNum(y_offset, "watermark.y_offset");
if (scale) guardIsNotNum(scale, "watermark.scale");
if (x_offset !== undefined) guardIsNotNum(x_offset, "watermark.x_offset");
if (y_offset !== undefined) guardIsNotNum(y_offset, "watermark.y_offset");
if (scale !== undefined) guardIsNotNum(scale, "watermark.scale");

const pos = position || "";
const xOffset = x_offset || "";
const yOffset = y_offset || "";
const sc = scale || "";
const pos = position ?? "";
const xOffset = x_offset ?? "";
const yOffset = y_offset ?? "";
const sc = scale ?? "";

return `wm:${opacity}:${pos}:${xOffset}:${yOffset}:${sc}`.replace(/:+$/, "");
};
Expand Down
8 changes: 4 additions & 4 deletions src/options/watermarkSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ const build = (options: WatermarkSizeOptionsPartial): string => {

guardIsUndef(watermarkSizeOpts, "watermark_size");
const { width, height } = watermarkSizeOpts;
if (width)
if (width !== undefined)
guardIsNotNum(width, "watermark_size.width", { addParam: { min: 0 } });
if (height)
if (height !== undefined)
guardIsNotNum(height, "watermark_size.height", { addParam: { min: 0 } });

const w = width || "";
const h = height || "";
const w = width ?? "";
const h = height ?? "";

return `wms:${w}:${h}`.replace(/:+$/, "");
};
Expand Down
26 changes: 21 additions & 5 deletions src/types/gravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,28 @@ interface ObjwGravity {
*
* @see https://docs.imgproxy.net/generating_the_url?id=gravity
*/
type GravityFieldKeys =
| keyof BaseGravity
| keyof SmartGravity
| keyof ObjGravity
| keyof ObjwGravity
| keyof FPGravity;

/**
* Marks every gravity field that doesn't belong to the variant as
* forbidden (`?: never`), so mixing fields from different variants
* (e.g. `{type: "sm", x_offset: 0}`) is a compile-time error.
*/
type OnlyGravity<T> = T & {
[K in Exclude<GravityFieldKeys, keyof T>]?: never;
};

type Gravity =
| BaseGravity
| SmartGravity
| ObjGravity
| ObjwGravity
| FPGravity;
| OnlyGravity<BaseGravity>
| OnlyGravity<SmartGravity>
| OnlyGravity<ObjGravity>
| OnlyGravity<ObjwGravity>
| OnlyGravity<FPGravity>;

/**
* *Gravity option*
Expand Down
8 changes: 8 additions & 0 deletions tests/optionsBasic/colorize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,12 @@ describe("colorize", () => {
});
});
});

describe("build (falsy values)", () => {
it("should throw an error if color is an empty string", () => {
expect(() => build({ col: { opacity: 0.5, color: "" } })).toThrow(
"colorize.color must be 3, 6 or 8 characters long (with alpha)"
);
});
});
});
10 changes: 10 additions & 0 deletions tests/optionsBasic/gravity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ describe("gravity", () => {
type: "no",
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
x: 0.5,
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
y: 0.5,
},
})
Expand Down Expand Up @@ -306,4 +307,13 @@ describe("gravity", () => {
});
});
});

describe("build (falsy values)", () => {
it("should throw an error if x_offset is 0 and type is not a base type", () => {
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
expect(() => build({ g: { type: "sm", x_offset: 0 } })).toThrow(
"gravity.type is invalid"
);
});
});
});
15 changes: 15 additions & 0 deletions tests/optionsBasic/jpegOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,19 @@ describe("jpegOptions", () => {
).toEqual("jpgo::::::7");
});
});

describe("build (falsy values)", () => {
it("should throw an error if quant_table is null", () => {
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
expect(() => build({ jpgo: { quant_table: null } })).toThrow(
"jpeg_options.quant_table is not a number"
);
});

it("should not require progressive when optimize_scans is false", () => {
expect(
build({ jpgo: { progressive: false, optimize_scans: false } })
).toEqual("jpgo:false::::false");
});
});
});
8 changes: 8 additions & 0 deletions tests/optionsBasic/monochrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ describe("monochrome", () => {
});
});
});

describe("build (falsy values)", () => {
it("should throw an error if color is not hexadecimal", () => {
expect(() => build({ mc: { intensity: 0.5, color: "zzz" } })).toThrow(
"monochrome.color must be hexadecimal"
);
});
});
});
15 changes: 15 additions & 0 deletions tests/optionsBasic/pngOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,19 @@ describe("pngOptions", () => {
).toEqual("pngo:true");
});
});

describe("build (falsy values)", () => {
it("should throw an error if quantization_colors is 0", () => {
expect(() =>
build({ pngo: { quantize: true, quantization_colors: 0 } })
).toThrow("png_options.quantization_colors value can't be less than 2");
});

it("should throw an error if interlaced is null", () => {
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
expect(() => build({ pngo: { interlaced: null } })).toThrow(
"png_options.interlaced is not a boolean"
);
});
});
});
22 changes: 22 additions & 0 deletions tests/optionsBasic/resize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,26 @@ describe("resize", () => {
).toEqual("rs:auto::150:f");
});
});

describe("build (falsy values)", () => {
it("should throw an error if width is null", () => {
expect(() =>
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
build({ rs: { resizing_type: "fit", width: null, height: 100 } })
).toThrow("resize.width is not a number");
});

it("should throw an error if resizing_type is an empty string", () => {
// @ts-expect-error: Let's ignore an error (check for users with vanilla js).
expect(() => build({ rs: { resizing_type: "", height: 100 } })).toThrow(
"resize.resizing_type is invalid"
);
});

it("should keep an explicit 0 width", () => {
expect(
build({ rs: { resizing_type: "fit", width: 0, height: 100 } })
).toEqual("rs:fit:0:100");
});
});
});
Loading
Loading