feat: implement legacy imgproxy compatibility and URL mapping
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m32s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m32s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "image-service",
|
||||
"version": "1.8.16",
|
||||
"version": "1.8.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/image-processor",
|
||||
"version": "1.8.16",
|
||||
"version": "1.8.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -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<ProcessImageOptions> {
|
||||
const parts = optionsStr.split("/");
|
||||
const options: Partial<ProcessImageOptions> = {};
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user