diff --git a/.changeset/imagekit-ar-encoding.md b/.changeset/imagekit-ar-encoding.md new file mode 100644 index 0000000..738b49a --- /dev/null +++ b/.changeset/imagekit-ar-encoding.md @@ -0,0 +1,5 @@ +--- +"unpic": patch +--- + +fix(imagekit): stop double-encoding hyphens in aspect ratio (`ar`) values diff --git a/src/providers/imagekit.test.ts b/src/providers/imagekit.test.ts index 5d801b7..1798c0d 100644 --- a/src/providers/imagekit.test.ts +++ b/src/providers/imagekit.test.ts @@ -29,6 +29,33 @@ Deno.test("imagekit extract", async (t) => { height: 300, }); }); + + await t.step("should extract a dashed aspect ratio from a query URL", () => { + const url = `${img}?tr=ar-9-16,w-640`; + const result = extract(url); + assertEquals(result?.src, img); + assertEquals(result?.operations, { + ar: "9-16", + width: 640, + }); + }); + + await t.step( + "should extract a dashed aspect ratio from a path-based URL", + () => { + const url = + `https://ik.imagekit.io/ikmedia/tr:ar-4-3,w-400/docs_images/examples/example_food_3.jpg`; + const result = extract(url); + assertEquals( + result?.src, + "https://ik.imagekit.io/ikmedia/docs_images/examples/example_food_3.jpg", + ); + assertEquals(result?.operations, { + ar: "4-3", + width: 400, + }); + }, + ); }); Deno.test("imagekit transform", async (t) => { @@ -93,6 +120,19 @@ Deno.test("imagekit transform", async (t) => { ); }, ); + + await t.step( + "should preserve a dashed aspect ratio when transforming an existing URL", + () => { + const result = transform(`${img}?tr=ar-9-16,w-640`, { + quality: 80, + }); + assertEquals( + new URL(result).searchParams.get("tr"), + "ar-9-16,w-640,q-80,c-maintain_ratio,fo-auto", + ); + }, + ); }); Deno.test("imagekit generate", async (t) => { @@ -159,4 +199,21 @@ Deno.test("imagekit generate", async (t) => { ); }, ); + + await t.step( + "should not double-encode hyphens in aspect ratio values", + () => { + const result = generate(`${img}?updatedAt=1778956861259`, { + ar: "9-16", + width: 640, + }); + const url = new URL(result); + assertEquals( + url.searchParams.get("tr"), + "ar-9-16,w-640,c-maintain_ratio,fo-auto", + ); + assertEquals(url.searchParams.get("updatedAt"), "1778956861259"); + assertEquals(result.includes("%252D"), false); + }, + ); }); diff --git a/src/providers/imagekit.ts b/src/providers/imagekit.ts index baa8fcc..9dfadbc 100644 --- a/src/providers/imagekit.ts +++ b/src/providers/imagekit.ts @@ -26,7 +26,7 @@ export interface ImageKitOperations extends Operations { /** * Aspect ratio of the output image. - * Example: `ar=16:9` + * Example: `ar-4-3` or `ar-9-16` */ ar?: string; @@ -152,13 +152,32 @@ const { operationsGenerator, operationsParser } = createOperationsHandlers< paramSeparator: ",", }); +/** + * ImageKit values such as aspect ratio `9-16` contain the same `-` used as + * the key/value separator. Encode extra hyphens so the shared parser keeps + * the full value instead of truncating at the second dash. + */ +function encodeHyphensInTrValues(trPart: string): string { + return trPart.split(",").map((pair) => { + const separatorIndex = pair.indexOf("-"); + if (separatorIndex === -1) { + return pair; + } + const key = pair.slice(0, separatorIndex); + const value = pair.slice(separatorIndex + 1).replaceAll("-", "%2D"); + return `${key}-${value}`; + }).join(","); +} + export const generate: URLGenerator<"imagekit"> = ( src, operations, ) => { const modifiers = operationsGenerator(operations); const url = toUrl(src); - url.searchParams.set("tr", modifiers); + // Decode formatter output so URLSearchParams does not double-encode + // hyphens in values such as aspect ratio `ar-9-16` (#195). + url.searchParams.set("tr", decodeURIComponent(modifiers)); return toCanonicalUrlString(url); }; @@ -190,7 +209,7 @@ export const extract: URLExtractor<"imagekit"> = (url) => { parsedUrl.pathname = path; - const operations = operationsParser(trPart); + const operations = operationsParser(encodeHyphensInTrValues(trPart)); return { src: toCanonicalUrlString(parsedUrl),