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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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