Unable to get ipx to process my images with custom URL and storage location #44
Replies: 1 comment
-
Persistence is key, I guess. I now have a working solution for my use case of storing images using KV: Should anyone want to copy this: Beware, it is an incomplete implementation. My validation does not check for all possible inputs and only the main ones I will use. Furthermore, this does image processing from the URL via query parameters, instead of integrating them in the URL like the IPX examples show; so, import { createIPX, IPXOptions, IPXStorage, IPXStorageMeta, IPXStorageOptions } from 'ipx';
import { handleError } from "~~/server/lib/errorHandler";
import { ImageNotFoundError, InvalidQueryParametersError } from "~~/server/lib/errors";
class KVStorageAdapter implements IPXStorage {
name = 'kvStorage';
async getData(key: string, opts?: IPXStorageOptions): Promise<ArrayBuffer | undefined> {
const storage = useStorage('data');
const relativeKey = key.replace(/^\/img\//, '');
// Retrieve the raw file from KV storage
const fileBuffer = await storage.getItemRaw(relativeKey);
if (!fileBuffer) {
throw new ImageNotFoundError(`The requested image file could not be found in KV storage: ${relativeKey}`);
}
// Convert the retrieved file to an ArrayBuffer
const buffer = Buffer.isBuffer(fileBuffer) ? fileBuffer : Buffer.from(fileBuffer);
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
async getMeta(key: string, opts?: IPXStorageOptions): Promise<IPXStorageMeta | undefined> {
// Optionally return metadata; currently only returning `mtime`
return {
mtime: Date.now(), // Last modified time (can be customized)
};
}
}
// Define IPX options
const ipxOptions: IPXOptions = {
storage: new KVStorageAdapter(),/*ipxFSStorage({
dir: join(process.cwd(), '.data/kv/'), // Directory where images are stored
}),*/
};
// Create IPX instance
const ipx = createIPX(ipxOptions);
// Export the IPX H3 handler
// export default createIPXH3Handler(ipx);
export default defineEventHandler(async (event) => {
try {
const requestedPath = event.context.params?.path || '';
// Extract modifiers from query parameters
const modifiers = processAndValidateModifiers(getQuery(event));
// Use IPX's process function with the extracted modifiers
const result = await ipx(requestedPath, modifiers).process();
console.log('Result: ' + result)
//if (!result.meta) throw new ImageNotFoundError();
// Set appropriate headers
event.node.res.setHeader('Content-Type', result.meta?.type || 'application/octet-stream');
if (process.env.NODE_ENV === 'development') {
event.node.res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
event.node.res.setHeader('Pragma', 'no-cache');
event.node.res.setHeader('Expires', '0');
} else {
event.node.res.setHeader('Cache-Control', `max-age=${ipxOptions.maxAge || 3600}`);
}
// Send processed image data
event.node.res.end(Buffer.from(result.data));
} catch (e) {
return handleError('/routes/img/[...path].get.ts:defineEventHandler', e);
}
});
function processAndValidateModifiers(query) {
const modifiers: Partial<Record<
'w' | 'width' | 'h' | 'height' | 's' | 'resize' | 'kernel' | 'fit' | 'pos' | 'position' | 'trim' | 'extend' | 'b' | 'background' | 'extract' | 'f' | 'format' | 'q' | 'quality' | 'rotate' | 'enlarge' | 'flip' | 'flop' | 'sharpen' | 'median' | 'blur' | 'gamma' | 'negate' | 'normalize' | 'threshold' | 'tint' | 'grayscale' | 'animated', string>> = {
w: (query as any)['w'] ?? '',
width: (query as any)['width'] ?? '',
h: (query as any)['h'] ?? '',
height: (query as any)['height'] ?? '',
s: (query as any)['s'] ?? '', // width and height separated by 'x'
resize: (query as any)['resize'] ?? '',
kernel: (query as any)['kernel'] ?? '', // default is lanczos3
fit: (query as any)['fit'] ?? '', // sets fit option for resize
pos: (query as any)['pos'] ?? '', // position of the crop
position: (query as any)['position'] ?? '',
trim: (query as any)['trim'] ?? '', // Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel.
extend: (query as any)['extend'] ?? '', // Extend / pad / extrude one or more edges of the image with either the provided background colour or
// pixels derived from the image. {left, top, width, height}
b: (query as any)['b'] ?? '',
background: (query as any)['background'] ?? '',
extract: (query as any)['extract'] ?? '', // {left, top, width, height}
f: (query as any)['f'] ?? '', // output format. {jpg, jpeg, png, webp, avif, gif, heif, tiff}
format: (query as any)['format'] ?? '',
q: (query as any)['q'] ?? '', // quality (0-100)
quality: (query as any)['quality'] ?? '',
rotate: (query as any)['rotate'] ?? '',
enlarge: (query as any)['enlarge'] ?? '',
flip: query.hasOwnProperty('flip') ? 'flip' : '',
flop: query.hasOwnProperty('flop') ? 'flop' : '',
sharpen: (query as any)['sharpen'] ?? '',
median: (query as any)['median'] ?? '',
blur: (query as any)['blur'] ?? '',
gamma: (query as any)['gamma'] ?? '',
negate: query.hasOwnProperty('negate') ? 'negate' : '',
normalize: query.hasOwnProperty('normalize') ? 'normalize' : '',
threshold: (query as any)['threshold'] ?? '',
tint: (query as any)['tint'] ?? '',
grayscale: query.hasOwnProperty('grayscale') ? 'grayscale' : '',
animated: query.hasOwnProperty('animated') ? 'animated' : '',
};
const modifiersWithValues = Object.fromEntries(
Object.entries(modifiers).filter(([key, value]) => value !== '' && value !== undefined)
);
const errors = validateModifiers(modifiersWithValues);
if (errors.length > 0) throw new InvalidQueryParametersError(errors);
return modifiersWithValues;
}
function validateModifiers(modifiers: Partial<Record<string, string>>): string[] {
const errors: string[] = [];
const isValidNumber = (value: string, min: number = 0, max?: number): boolean => {
const num = Number(value);
return !isNaN(num) && num > min && (max === undefined || num <= max);
};
const isValidHexColor = (value: string): boolean => /^[0-9a-fA-F]{6}$/.test(value);
const isValidFormat = (value: string, regex: RegExp): boolean => regex.test(value);
const validationRules: Record<string, { validate: (value: string) => boolean, error: string }> = {
w: {validate: (value) => isValidNumber(value, 0), error: 'w/width needs to be a number > 0'},
width: {validate: (value) => isValidNumber(value, 0), error: 'width needs to be a number > 0'},
h: {validate: (value) => isValidNumber(value, 0), error: 'h/height needs to be a number > 0'},
height: {validate: (value) => isValidNumber(value, 0), error: 'height needs to be a number > 0'},
resize: {validate: (value) => isValidFormat(value, /^\d+x\d+$/), error: "resize needs to be in the format '0x0' where '0' are numbers > 0"},
kernel: {
validate: (value) => ['nearest', 'cubic', 'mitchell', 'lanczos2', 'lanczos3'].includes(value),
error: 'kernel needs to be one of nearest, cubic, mitchell, lanczos2, lanczos3'
},
fit: {
validate: (value) => ['cover', 'contain', 'fill', 'inside', 'outside'].includes(value),
error: 'fit needs to be one of cover, contain, fill, inside, outside'
},
position: {
validate: (value) => ['centre', 'top', 'right top', 'right', 'right bottom', 'bottom', 'left bottom', 'left', 'left top'].includes(value),
error: 'position needs to be one of centre, top, right top, right, right bottom, bottom, left bottom, left, left top'
},
trim: {validate: (value) => isValidNumber(value, 0), error: 'trim needs to be a number > 0'},
extend: {
validate: (value) => isValidFormat(value, /^\d+_\d+_\d+_\d+$/),
error: "extend needs to be in the format '0_0_0_0' where '0' are numbers >= 0"
},
b: {validate: (value) => isValidHexColor(value), error: 'b/background needs to be a valid 6-digit HEX color'},
background: {validate: (value) => isValidHexColor(value), error: 'background needs to be a valid 6-digit HEX color'},
extract: {
validate: (value) => isValidFormat(value, /^\d+_\d+_\d+_\d+$/),
error: "extract needs to be in the format '0_0_0_0' where '0' are numbers >= 0"
},
f: {
validate: (value) => ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'heif', 'tiff'].includes(value),
error: 'f/format needs to be one of jpg, jpeg, png, webp, avif, gif, heif, tiff'
},
format: {
validate: (value) => ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'heif', 'tiff'].includes(value),
error: 'format needs to be one of jpg, jpeg, png, webp, avif, gif, heif, tiff'
},
q: {validate: (value) => isValidNumber(value, -1, 100), error: 'q/quality needs to be >= 0 and <= 100'},
quality: {validate: (value) => isValidNumber(value, -1, 100), error: 'quality needs to be >= 0 and <= 100'},
rotate: {validate: (value) => !isNaN(Number(value)), error: 'rotate needs to be a number'},
threshold: {validate: (value) => isValidNumber(value, 0), error: 'threshold needs to be a number >= 0'},
};
for (const [key, value] of Object.entries(modifiers)) {
if (value && validationRules[key] && !validationRules[key].validate(value)) {
errors.push(validationRules[key].error);
}
}
return errors;
} |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
While I am using Nuxt 3, I also needed a way to server images from a different directory and control access to this directory.
Using KV storage, I have a test file in
.data/kv/users/b36111fc-b9ae-4e3f-b7ec-24b75926a3e9/pixelArtNeutral-1733659729705.png
.I created a file in
server/routes/img/[...path].ts
(the KVStorageAdapter is ChatGPT-generated):With this, I can successfully get my test image with
localhost:3000/img/users/b36111fc-b9ae-4e3f-b7ec-24b75926a3e9/pixelArtNeutral-1733659729705.png
. However, I cannot get processing to work.Again, most of my attempts the last few hours have been aided by ChatGPT:
These two always fail at
result.meta
which is undefined. I have no idea where ChatGPT took this code from, but it seems to come back to this solution no matter how I prompt it; I've tried 4o with browser, o1, even other LLMs locally.Any help would be much appreciated.
Beta Was this translation helpful? Give feedback.
All reactions