Last active
July 24, 2024 14:39
-
-
Save olikami/236e3c57ca73d145984ec6c127416340 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)}`; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { imgLoader } from '../helpers/imgLoader'; | |
<Image | |
loader={imgLoader} | |
src=<img source> | |
width=<width> | |
height=<height> | |
layout='responsive' | |
alt=<alt text> | |
/> |
Awesome, might actually use your library instead of my current
hot-glued-but-surprisingly-stable solution. Cheers
…On Thu, 20 Jan 2022 at 22:00, Josh McFarlin ***@***.***> wrote:
***@***.**** commented on this gist.
------------------------------
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
—
Reply to this email directly, view it on GitHub
<https://gist.github.com/236e3c57ca73d145984ec6c127416340#gistcomment-4035696>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAXC2GYAL2NPK2XPPM3MYO3UXBZWXANCNFSM5MNZTYCA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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