mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-13 04:03:42 +08:00
106 lines
3.2 KiB
JavaScript
106 lines
3.2 KiB
JavaScript
import sharp from 'sharp';
|
|
|
|
const MAX_INPUT_BYTES = 20 * 1024 * 1024;
|
|
const DEFAULT_MAX_INPUT_PIXELS = 25_000_000;
|
|
|
|
const maxInputPixels = readPositiveInteger(
|
|
process.env.IMAGE_TRANSFORM_MAX_PIXELS,
|
|
DEFAULT_MAX_INPUT_PIXELS
|
|
);
|
|
const concurrency = Math.min(
|
|
readPositiveInteger(process.env.IMAGE_TRANSFORM_CONCURRENCY, 1),
|
|
8
|
|
);
|
|
|
|
// Limit native memory and parallel decoding so concurrent large requests do not
|
|
// unexpectedly exhaust a small Docker container.
|
|
sharp.cache({ memory: 32, files: 0, items: 32 });
|
|
sharp.concurrency(concurrency);
|
|
|
|
export const dockerImageProcessor = {
|
|
async transform(stream, options) {
|
|
const input = await readStreamWithLimit(stream, MAX_INPUT_BYTES);
|
|
|
|
// SVG is inherently scalable. Keep the source document unchanged,
|
|
// matching Cloudflare Image Transformations behavior.
|
|
if (options.sourceType === 'image/svg+xml') {
|
|
return new Response(input, {
|
|
headers: {
|
|
'Content-Type': options.sourceType,
|
|
'Content-Length': input.byteLength.toString(),
|
|
},
|
|
});
|
|
}
|
|
|
|
const animated = options.sourceType === 'image/gif' || options.sourceType === 'image/webp';
|
|
|
|
let pipeline = sharp(input, {
|
|
animated,
|
|
failOn: 'error',
|
|
limitInputPixels: maxInputPixels,
|
|
sequentialRead: true,
|
|
}).autoOrient().resize({
|
|
width: options.width,
|
|
height: options.height,
|
|
fit: resolveSharpFit(options.fit),
|
|
withoutEnlargement: !options.fit,
|
|
});
|
|
|
|
pipeline = applyOutputFormat(pipeline, options.outputFormat);
|
|
const output = await pipeline.toBuffer();
|
|
|
|
return new Response(output, {
|
|
headers: {
|
|
'Content-Type': options.outputFormat,
|
|
'Content-Length': output.byteLength.toString(),
|
|
},
|
|
});
|
|
},
|
|
};
|
|
|
|
function resolveSharpFit(fit) {
|
|
if (fit === 'cover') return 'cover';
|
|
if (fit === 'squeeze') return 'fill';
|
|
return 'inside';
|
|
}
|
|
|
|
function applyOutputFormat(pipeline, outputFormat) {
|
|
switch (outputFormat) {
|
|
case 'image/jpeg':
|
|
return pipeline.jpeg();
|
|
case 'image/png':
|
|
return pipeline.png();
|
|
case 'image/webp':
|
|
return pipeline.webp();
|
|
case 'image/avif':
|
|
return pipeline.avif();
|
|
case 'image/gif':
|
|
return pipeline.gif();
|
|
default:
|
|
throw new Error(`Unsupported Docker image output format: ${outputFormat}`);
|
|
}
|
|
}
|
|
|
|
async function readStreamWithLimit(stream, maxBytes) {
|
|
const chunks = [];
|
|
let totalBytes = 0;
|
|
|
|
for await (const chunk of stream) {
|
|
const buffer = Buffer.from(chunk);
|
|
totalBytes += buffer.byteLength;
|
|
if (totalBytes > maxBytes) {
|
|
const error = new Error('Image resizing supports source files up to 20 MB');
|
|
error.statusCode = 413;
|
|
throw error;
|
|
}
|
|
chunks.push(buffer);
|
|
}
|
|
|
|
return Buffer.concat(chunks, totalBytes);
|
|
}
|
|
|
|
function readPositiveInteger(value, fallback) {
|
|
const parsed = Number.parseInt(value, 10);
|
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|