diff --git a/tests/integration/border.test.ts b/tests/integration/border.test.ts index fbb3adb1..c56ad0e0 100644 --- a/tests/integration/border.test.ts +++ b/tests/integration/border.test.ts @@ -573,6 +573,72 @@ describe("Border", () => { expect(result.processedSize).toBeGreaterThan(0); }); + it("handles HEIF input (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { + name: "settings", + content: JSON.stringify({ borderWidth: 10, borderColor: "#00FF00" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/border", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.processedSize).toBeGreaterThan(0); + }); + + it("handles SVG input", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { + name: "settings", + content: JSON.stringify({ borderWidth: 10, borderColor: "#FF0000" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/border", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.processedSize).toBeGreaterThan(0); + }); + + it("handles animated GIF input", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { + name: "settings", + content: JSON.stringify({ borderWidth: 5, borderColor: "#000000" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/border", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.processedSize).toBeGreaterThan(0); + }); + it("rejects negative border width", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, diff --git a/tests/integration/color-adjustments.test.ts b/tests/integration/color-adjustments.test.ts index a12f796e..d9a51313 100644 --- a/tests/integration/color-adjustments.test.ts +++ b/tests/integration/color-adjustments.test.ts @@ -537,3 +537,42 @@ describe("No-op effect", () => { expect(result.downloadUrl).toBeDefined(); }); }); + +// ── HEIF input ────────────────────────────────────────────────── +describe("HEIF input", () => { + it("processes HEIF input (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const res = await postTool( + "adjust-colors", + { brightness: 20 }, + HEIF, + "photo.heif", + "image/heif", + ); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }, 60_000); +}); + +// ── Animated GIF input ────────────────────────────────────────── +describe("Animated GIF input", () => { + it("processes animated GIF", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const res = await postTool("adjust-colors", { saturation: 30 }, GIF, "anim.gif", "image/gif"); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +}); + +// ── SVG input ─────────────────────────────────────────────────── +describe("SVG input", () => { + it("processes SVG input", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const res = await postTool("adjust-colors", { contrast: 20 }, SVG, "icon.svg", "image/svg+xml"); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +}); diff --git a/tests/integration/color-palette.test.ts b/tests/integration/color-palette.test.ts index 5dcdfe88..28821095 100644 --- a/tests/integration/color-palette.test.ts +++ b/tests/integration/color-palette.test.ts @@ -400,6 +400,46 @@ describe("HEIF input", () => { }); }); +// ── Animated GIF input ────────────────────────────────────────── +describe("Animated GIF input", () => { + it("extracts palette from animated GIF", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body: payload, contentType } = makeFilePayload(GIF, "anim.gif", "image/gif"); + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/color-palette", + payload, + headers: { + "content-type": contentType, + authorization: `Bearer ${adminToken}`, + }, + }); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.colors.length).toBeGreaterThan(0); + }); +}); + +// ── SVG input ─────────────────────────────────────────────────── +describe("SVG input", () => { + it("extracts palette from SVG image", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body: payload, contentType } = makeFilePayload(SVG, "icon.svg", "image/svg+xml"); + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/color-palette", + payload, + headers: { + "content-type": contentType, + authorization: `Bearer ${adminToken}`, + }, + }); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.colors.length).toBeGreaterThan(0); + }); +}); + // ── Filename preserved in response ────────────────────────────── describe("Filename tracking", () => { it("returns the original filename in the response", async () => { diff --git a/tests/integration/favicon.test.ts b/tests/integration/favicon.test.ts index 1ee2e8b6..799638f2 100644 --- a/tests/integration/favicon.test.ts +++ b/tests/integration/favicon.test.ts @@ -627,6 +627,59 @@ describe("favicon", () => { expect(manifest.name).toBe("my.app.logo"); }); + // ── Animated GIF input ──────────────────────────────────────────── + + it("generates favicons from animated GIF input", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/favicon", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const zip = new AdmZip(Buffer.from(res.rawPayload)); + const entries = zip.getEntries().map((e) => e.entryName); + expect(entries).toContain("favicon-16x16.png"); + expect(entries).toContain("favicon.ico"); + }); + + // ── Batch: 5+ images ──────────────────────────────────────────── + + it("generates favicons for 5 images in subfolders", async () => { + const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg")); + const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp")); + const TINY = readFileSync(join(FIXTURES, "test-1x1.png")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.png", contentType: "image/png", content: PNG }, + { name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { name: "file", filename: "c.webp", contentType: "image/webp", content: WEBP }, + { name: "file", filename: "d.png", contentType: "image/png", content: TINY }, + { name: "file", filename: "e.png", contentType: "image/png", content: PNG }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/favicon", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const zip = new AdmZip(Buffer.from(res.rawPayload)); + const entries = zip.getEntries().map((e) => e.entryName); + expect(entries.some((e) => e.startsWith("a/"))).toBe(true); + expect(entries.some((e) => e.startsWith("b/"))).toBe(true); + expect(entries.some((e) => e.startsWith("c/"))).toBe(true); + expect(entries.some((e) => e.startsWith("d/"))).toBe(true); + expect(entries.some((e) => e.startsWith("e/"))).toBe(true); + }); + // ── HEIF format input ──────────────────────────────────────────── it("generates favicons from HEIF input", async () => { diff --git a/tests/integration/image-to-base64.test.ts b/tests/integration/image-to-base64.test.ts index dcab7752..ed597149 100644 --- a/tests/integration/image-to-base64.test.ts +++ b/tests/integration/image-to-base64.test.ts @@ -788,6 +788,29 @@ describe("image-to-base64", () => { expect(res.statusCode).toBe(400); }); + // ── Animated GIF input ────────────────────────────────────────── + + it("converts animated GIF to base64", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { name: "settings", content: JSON.stringify({}) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/image-to-base64", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.results).toHaveLength(1); + expect(json.results[0].base64.length).toBeGreaterThan(0); + expect(json.results[0].dataUri).toMatch(/^data:image\/.+;base64,/); + }); + // ── maxHeight=0 means no resize ────────────────────────────────── it("maxHeight=0 means no height resize (pass-through)", async () => { diff --git a/tests/integration/image-to-pdf.test.ts b/tests/integration/image-to-pdf.test.ts index 91b42c0c..f4817e19 100644 --- a/tests/integration/image-to-pdf.test.ts +++ b/tests/integration/image-to-pdf.test.ts @@ -739,6 +739,72 @@ describe("image-to-pdf", () => { expect(json.pages).toBe(1); }); + // ── HEIF input ──────────────────────────────────────────────────── + + it("converts HEIF image to PDF", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { name: "settings", content: JSON.stringify({}) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/image-to-pdf", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.pages).toBe(1); + expect(json.processedSize).toBeGreaterThan(0); + }, 60_000); + + // ── Animated GIF input ────────────────────────────────────────── + + it("converts animated GIF to PDF", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { name: "settings", content: JSON.stringify({}) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/image-to-pdf", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.pages).toBe(1); + expect(json.processedSize).toBeGreaterThan(0); + }); + + // ── SVG input ─────────────────────────────────────────────────── + + it("converts SVG to PDF", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { name: "settings", content: JSON.stringify({}) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/image-to-pdf", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.pages).toBe(1); + expect(json.processedSize).toBeGreaterThan(0); + }); + // ── Rejects invalid orientation ────────────────────────────────── it("rejects invalid orientation value", async () => { diff --git a/tests/integration/replace-color.test.ts b/tests/integration/replace-color.test.ts index 03b4233f..836fc3f3 100644 --- a/tests/integration/replace-color.test.ts +++ b/tests/integration/replace-color.test.ts @@ -420,3 +420,51 @@ describe("Target color validation", () => { expect(res.statusCode).toBe(400); }); }); + +// ── HEIF input ───────────────────────────────────────────────── +describe("HEIF input", () => { + it("processes HEIF image (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const res = await postTool( + { sourceColor: "#808080", targetColor: "#FF0000", tolerance: 50 }, + HEIF, + "photo.heif", + "image/heif", + ); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }, 60_000); +}); + +// ── Animated GIF input ───────────────────────────────────────── +describe("Animated GIF input", () => { + it("processes animated GIF", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const res = await postTool( + { sourceColor: "#808080", targetColor: "#00FF00", tolerance: 60 }, + GIF, + "anim.gif", + "image/gif", + ); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +}); + +// ── SVG input ────────────────────────────────────────────────── +describe("SVG input", () => { + it("processes SVG image", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const res = await postTool( + { sourceColor: "#000000", targetColor: "#FF0000", tolerance: 30 }, + SVG, + "icon.svg", + "image/svg+xml", + ); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +}); diff --git a/tests/integration/sharpening.test.ts b/tests/integration/sharpening.test.ts index 00b3cf9b..7282762b 100644 --- a/tests/integration/sharpening.test.ts +++ b/tests/integration/sharpening.test.ts @@ -452,3 +452,46 @@ describe("HEIC with different methods", () => { expect(res.statusCode).toBe(200); }); }); + +// ── HEIF input ────────────────────────────────────────────────── +describe("HEIF input", () => { + it("processes HEIF image (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const res = await postTool( + { method: "adaptive", sigma: 2.0 }, + HEIF, + "photo.heif", + "image/heif", + ); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }, 60_000); +}); + +// ── Animated GIF input ────────────────────────────────────────── +describe("Animated GIF input", () => { + it("processes animated GIF", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const res = await postTool( + { method: "unsharp-mask", amount: 150 }, + GIF, + "anim.gif", + "image/gif", + ); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +}); + +// ── SVG input ─────────────────────────────────────────────────── +describe("SVG input", () => { + it("processes SVG image", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const res = await postTool({ method: "adaptive" }, SVG, "icon.svg", "image/svg+xml"); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + }); +}); diff --git a/tests/integration/text-overlay.test.ts b/tests/integration/text-overlay.test.ts index d5583e36..551a4cc6 100644 --- a/tests/integration/text-overlay.test.ts +++ b/tests/integration/text-overlay.test.ts @@ -530,6 +530,69 @@ describe("text-overlay", () => { expect(res.statusCode).toBe(400); }); + // ── HEIF input ─────────────────────────────────────────────────── + + it("handles HEIF input (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { name: "settings", content: JSON.stringify({ text: "HEIF overlay" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/text-overlay", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + }, 60_000); + + // ── Animated GIF input ────────────────────────────────────────── + + it("handles animated GIF input", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { name: "settings", content: JSON.stringify({ text: "GIF overlay" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/text-overlay", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + }); + + // ── SVG input ─────────────────────────────────────────────────── + + it("handles SVG input", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { name: "settings", content: JSON.stringify({ text: "SVG overlay" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/text-overlay", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + }); + // ── Preserves image dimensions ─────────────────────────────────── it("preserves image dimensions after text overlay", async () => { diff --git a/tests/integration/watermark-image.test.ts b/tests/integration/watermark-image.test.ts index 988bb248..5a8c779d 100644 --- a/tests/integration/watermark-image.test.ts +++ b/tests/integration/watermark-image.test.ts @@ -483,6 +483,51 @@ describe("watermark-image", () => { expect(res.statusCode).toBe(401); }); + // ── HEIF input ──────────────────────────────────────────────────── + + it("processes HEIF main image (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { name: "watermark", filename: "wm.png", contentType: "image/png", content: SMALL_PNG }, + { name: "settings", content: JSON.stringify({ scale: 10 }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/watermark-image", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + expect(json.processedSize).toBeGreaterThan(0); + }, 60_000); + + // ── Animated GIF input ────────────────────────────────────────── + + it("processes animated GIF main image", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { name: "watermark", filename: "wm.png", contentType: "image/png", content: SMALL_PNG }, + { name: "settings", content: JSON.stringify({ scale: 20 }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/watermark-image", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.processedSize).toBeGreaterThan(0); + }); + // ── WebP watermark image ───────────────────────────────────────── it("processes WebP watermark image", async () => { diff --git a/tests/integration/watermark-text.test.ts b/tests/integration/watermark-text.test.ts index a3c193cb..11afc56c 100644 --- a/tests/integration/watermark-text.test.ts +++ b/tests/integration/watermark-text.test.ts @@ -518,6 +518,69 @@ describe("watermark-text", () => { expect(res.statusCode).toBe(400); }); + // ── HEIF input ─────────────────────────────────────────────────── + + it("handles HEIF input (motorcycle.heif)", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { name: "settings", content: JSON.stringify({ text: "HEIF Test" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/watermark-text", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + }, 60_000); + + // ── Animated GIF input ────────────────────────────────────────── + + it("handles animated GIF input", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { name: "settings", content: JSON.stringify({ text: "GIF Test" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/watermark-text", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + }); + + // ── SVG input ─────────────────────────────────────────────────── + + it("handles SVG input", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { name: "settings", content: JSON.stringify({ text: "SVG Test" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/watermark-text", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toBeDefined(); + }); + // ── Preserves image dimensions ─────────────────────────────────── it("preserves image dimensions after watermark", async () => {