diff --git a/apps/image-service/package.json b/apps/image-service/package.json index be60573..1a206bd 100644 --- a/apps/image-service/package.json +++ b/apps/image-service/package.json @@ -1,6 +1,6 @@ { "name": "image-service", - "version": "1.8.16", + "version": "1.8.19", "private": true, "type": "module", "scripts": { diff --git a/apps/image-service/src/index.ts b/apps/image-service/src/index.ts index 4c7e69a..0ed25ac 100644 --- a/apps/image-service/src/index.ts +++ b/apps/image-service/src/index.ts @@ -1,37 +1,45 @@ import Fastify from "fastify"; -import { processImageWithSmartCrop } from "@mintel/image-processor"; +import { + processImageWithSmartCrop, + parseImgproxyOptions, + mapUrl, +} from "@mintel/image-processor"; const fastify = Fastify({ logger: true, }); fastify.get("/unsafe/:options/:urlSafeB64", async (request, reply) => { - // Compatibility endpoint for old imgproxy calls (optional, but requested by some systems sometimes) - // For now, replacing logic in clients is preferred. So we just redirect or error. - return reply - .status(400) - .send({ error: "Legacy imgproxy API not supported. Use /process" }); -}); - -fastify.get("/process", async (request, reply) => { - const query = request.query as { - url?: string; - w?: string; - h?: string; - q?: string; - format?: string; + const { options, urlSafeB64 } = request.params as { + options: string; + urlSafeB64: string; }; - const { url } = query; - const width = parseInt(query.w || "800", 10); - const height = parseInt(query.h || "600", 10); - const quality = parseInt(query.q || "80", 10); - const format = (query.format || "webp") as "webp" | "jpeg" | "png" | "avif"; - - if (!url) { - return reply.status(400).send({ error: 'Parameter "url" is required' }); + // urlSafeB64 might be "plain/http://..." or a Base64 string + let url = ""; + if (urlSafeB64.startsWith("plain/")) { + url = urlSafeB64.substring(6); + } else { + try { + url = Buffer.from(urlSafeB64, "base64").toString("utf-8"); + } catch (e) { + return reply.status(400).send({ error: "Invalid Base64 URL" }); + } } + const parsedOptions = parseImgproxyOptions(options); + const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING); + + return handleProcessing(mappedUrl, parsedOptions, reply); +}); + +// Helper to avoid duplication +async function handleProcessing(url: string, options: any, reply: any) { + const width = options.width || 800; + const height = options.height || 600; + const quality = options.quality || 80; + const format = options.format || "webp"; + try { const response = await fetch(url); if (!response.ok) { @@ -60,6 +68,29 @@ fastify.get("/process", async (request, reply) => { .status(500) .send({ error: "Internal Server Error processing image" }); } +} + +fastify.get("/process", async (request, reply) => { + const query = request.query as { + url?: string; + w?: string; + h?: string; + q?: string; + format?: string; + }; + + const { url } = query; + const width = parseInt(query.w || "800", 10); + const height = parseInt(query.h || "600", 10); + const quality = parseInt(query.q || "80", 10); + const format = (query.format || "webp") as any; + + if (!url) { + return reply.status(400).send({ error: 'Parameter "url" is required' }); + } + + const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING); + return handleProcessing(mappedUrl, { width, height, quality, format }, reply); }); fastify.get("/health", async () => { diff --git a/packages/image-processor/package.json b/packages/image-processor/package.json index 25f4002..9654726 100644 --- a/packages/image-processor/package.json +++ b/packages/image-processor/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/image-processor", - "version": "1.8.16", + "version": "1.8.19", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/image-processor/src/processor.ts b/packages/image-processor/src/processor.ts index 34aff38..c6d8c71 100644 --- a/packages/image-processor/src/processor.ts +++ b/packages/image-processor/src/processor.ts @@ -8,6 +8,54 @@ export interface ProcessImageOptions { openRouterApiKey?: string; } +/** + * Maps a URL based on the IMGPROXY_URL_MAPPING environment variable. + * Format: "match1:replace1,match2:replace2" + */ +export function mapUrl(url: string, mappingString?: string): string { + if (!mappingString) return url; + + const mappings = mappingString.split(",").map((m) => m.split(":")); + let mappedUrl = url; + + for (const [match, replace] of mappings) { + if (match && replace && url.includes(match)) { + mappedUrl = url.replace(match, replace); + } + } + + return mappedUrl; +} + +/** + * Parses legacy imgproxy options string. + * Example: rs:fill:300:400/q:80 + */ +export function parseImgproxyOptions( + optionsStr: string, +): Partial { + const parts = optionsStr.split("/"); + const options: Partial = {}; + + for (const part of parts) { + if (part.startsWith("rs:")) { + const [, , w, h] = part.split(":"); + if (w) options.width = parseInt(w, 10); + if (h) options.height = parseInt(h, 10); + } else if (part.startsWith("q:")) { + const q = part.split(":")[1]; + if (q) options.quality = parseInt(q, 10); + } else if (part.startsWith("ext:")) { + const ext = part.split(":")[1] as any; + if (["webp", "jpeg", "png", "avif"].includes(ext)) { + options.format = ext; + } + } + } + + return options; +} + interface FaceDetection { x: number; y: number;