diff --git a/.changeset/full-plums-attend.md b/.changeset/full-plums-attend.md new file mode 100644 index 0000000..fbbd4b4 --- /dev/null +++ b/.changeset/full-plums-attend.md @@ -0,0 +1,5 @@ +--- +"unpic": minor +--- + +feat(ipx): improve ipx provider typing and test coverage diff --git a/src/providers/ipx.test.ts b/src/providers/ipx.test.ts index d1aa51e..f3ab7d2 100644 --- a/src/providers/ipx.test.ts +++ b/src/providers/ipx.test.ts @@ -32,6 +32,21 @@ Deno.test("ipx extract", async (t) => { format: "webp", }); }); + + await t.step("should extract crop with URL-encoded underscores", () => { + const url = `${baseURL}/crop_100%5F50%5F300%5F200,f_auto/images/test.jpg`; + const result = extract(url); + assertEquals(result?.src, "/images/test.jpg"); + assertEquals(result?.operations.crop, "100_50_300_200"); + }); + + await t.step("should extract fit and position", () => { + const url = + `${baseURL}/s_800x600,fit_cover,position_top,f_auto/images/test.jpg`; + const result = extract(url); + assertEquals(result?.operations.fit, "cover"); + assertEquals(result?.operations.position, "top"); + }); }); Deno.test("ipx generate", async (t) => { @@ -139,3 +154,36 @@ Deno.test("ipx transform", async (t) => { ); }); }); + +Deno.test("ipx generate with extended operations", async (t) => { + await t.step("should generate URL with crop", () => { + const result = generate( + img, + { width: 800, height: 600, crop: "100_50_300_200" }, + { baseURL }, + ); + assertEquals(result.includes("crop_100%5F50%5F300%5F200"), true); + assertEquals(result.includes("s_800x600"), true); + }); + + await t.step("should generate URL with fit and position", () => { + const result = generate( + img, + { width: 800, height: 600, fit: "cover", position: "top" }, + { baseURL }, + ); + assertEquals(result.includes("fit_cover"), true); + assertEquals(result.includes("position_top"), true); + }); + + await t.step("should generate URL with effects", () => { + const result = generate( + img, + { width: 300, rotate: 90, blur: 5, grayscale: true }, + { baseURL }, + ); + assertEquals(result.includes("rotate_90"), true); + assertEquals(result.includes("blur_5"), true); + assertEquals(result.includes("grayscale_true"), true); + }); +}); diff --git a/src/providers/ipx.ts b/src/providers/ipx.ts index 978259a..04cf2e4 100644 --- a/src/providers/ipx.ts +++ b/src/providers/ipx.ts @@ -38,6 +38,78 @@ export interface IPXOperations extends Operations { * Output format of the image. */ f?: ImageFormat | "auto"; + + /** + * Resize fit mode. Only applies when both width and height are specified. + */ + fit?: "contain" | "cover" | "fill" | "inside" | "outside"; + + /** + * Position/gravity for resize. Only applies when both width and height are specified. + * @example "top" + */ + position?: string; + + /** + * Alias for position. + */ + pos?: string; + + /** + * Extract/crop a region of the image. + * Format: "left_top_width_height" + * @example "100_50_300_200" + */ + extract?: string; + + /** + * Alias for extract. + */ + crop?: string; + + /** + * Rotation angle in degrees. + * @example 90 + */ + rotate?: number; + + /** + * Flip image vertically. + */ + flip?: boolean; + + /** + * Flip image horizontally. + */ + flop?: boolean; + + /** + * Blur sigma value. + * @example 5 + */ + blur?: number; + + /** + * Sharpen sigma value. + * @example 30 + */ + sharpen?: number; + + /** + * Convert to grayscale. + */ + grayscale?: boolean; + + /** + * Background color (hex without #). + * @example "ff0000" + */ + background?: string; + + /** + * Alias for background. + */ + b?: string; } export interface IPXOptions {