Skip to content

Instantly share code, notes, and snippets.

@olikami
Last active July 24, 2024 14:39
Show Gist options
  • Save olikami/236e3c57ca73d145984ec6c127416340 to your computer and use it in GitHub Desktop.
Save olikami/236e3c57ca73d145984ec6c127416340 to your computer and use it in GitHub Desktop.
// underscore should be slashes in filename
import axios from 'axios';
import FileType from 'file-type';
import fs from 'fs';
import Cache from 'hybrid-disk-cache';
import sharp from 'sharp';
import { decode } from 'universal-base64';
const selfUrl = new URL(process.env.BASE_URL);
const whiteListedDomains = ['<domains.where.your.images.life', selfUrl.host];
const cache = new Cache({
path: 'tmp/img',
ttl: 24 * 60 * 60,
tbd: 365 * 24 * 60 * 60,
});
const generateKey = (width, quality, src, webp) =>
`${width}_${quality}_${src}_${webp}`;
async function getImg(imgUrl, width, quality, req) {
let buffer;
let contentType;
let resultImg;
let imageResponse = {};
if (imgUrl.origin == selfUrl.origin) {
const filepath = imgUrl.pathname.replace('/_next/', '.next/');
// Fake the image response from file
const contentType = await FileType.fromFile(filepath);
imageResponse.headers = {
'content-type': contentType ? contentType : 'image/svg+xml',
};
buffer = fs.readFileSync(filepath);
} else {
imageResponse = await axios({
url: imgUrl.toString(),
responseType: 'arraybuffer',
});
buffer = Buffer.from(imageResponse.data, 'binary');
}
if (imageResponse.headers['content-type'] == 'image/svg+xml') {
contentType = imageResponse.headers['content-type'];
resultImg = buffer;
} else {
let image = sharp(buffer);
image
.resize({ width: parseInt(width) })
.jpeg({
quality: parseInt(quality),
progressive: true,
force: false,
})
.png({
progressive: true,
compressionLevel: 9,
force: false,
});
if (req.headers.accept.includes('image/webp')) {
image.webp({
quality: parseInt(quality),
});
}
resultImg = await image.toBuffer();
contentType = (await FileType.fromBuffer(resultImg)).mime;
}
return { resultImg: resultImg, contentType: contentType };
}
export default async function handler(req, res) {
const { width, quality, src } = req.query;
const webpSupport = req.headers.accept.includes('image/webp');
let cache_value;
let resultImg;
let contentType;
const cacheKey = generateKey(width, quality, src, webpSupport);
if (cache.has(cacheKey) !== 'miss') {
if (cache.has(cacheKey) == 'stale') {
setTimeout(async () => {
console.log(`Recalculating stale img: ${cacheKey}`);
const myUrl = new URL(decode(src), selfUrl);
const res = await getImg(myUrl, width, quality, req);
resultImg = res['resultImg'];
contentType = res['contentType'];
cache.set(cacheKey, resultImg);
}, 1000);
}
cache_value = cache.get(cacheKey);
}
if (cache_value) {
console.log(
`Cache ${cache.has(
cacheKey
)}: w:${width}, q:${quality}, src:${decode(src)}`
);
try {
contentType = (await FileType.fromBuffer(cache_value)).mime;
} catch {
contentType = 'image/svg+xml';
}
resultImg = cache_value;
} else {
console.log(
`Cache ${cache.has(
cacheKey
)}: w:${width}, q:${quality}, src:${decode(src)}`
);
const myUrl = new URL(decode(src), selfUrl);
if (!whiteListedDomains.includes(myUrl.host)) {
res.statusCode = 403;
res.send('Domain not allowed');
} else if (parseInt(width) > 4000) {
res.statusCode = 406;
res.send('Requested Image too large');
} else {
// Get Image
const res = await getImg(myUrl, width, quality, req);
resultImg = res['resultImg'];
contentType = res['contentType'];
}
}
res.setHeader('Content-Type', contentType);
res.setHeader(
'Cache-Control',
`private, max-age=${cache.ttl}, max-stale=${cache.tbd}`
);
res.statusCode = 200;
res.send(resultImg);
cache.set(cacheKey, resultImg);
}
// underscore should be slashes in filename
import { encode } from 'universal-base64';
export const imgLoader = ({ src, width, quality }) => {
return `/api/image/${width}/${quality || 80}/${encode(src)}`;
};
import { imgLoader } from '../helpers/imgLoader';
<Image
loader={imgLoader}
src=<img source>
width=<width>
height=<height>
layout='responsive'
alt=<alt text>
/>
@Josh-McFarlin
Copy link

Hi, I wanted to let you know I turned this gist into an npm package that enables implementing this functionality with a single resource route. I would love it if you could check over the implementation and possibly contribute any improvements since you wrote this gist. Thanks!

https://github.com/Josh-McFarlin/remix-image

@olikami
Copy link
Author

olikami commented Jan 20, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment