|
1 |
| -const path = require('path') |
2 | 1 | const { builder } = require('@netlify/functions')
|
3 | 2 | const sharp = require('sharp')
|
4 | 3 | const fetch = require('node-fetch')
|
| 4 | +const imageType = require('image-type') |
| 5 | +const isSvg = require('is-svg') |
5 | 6 |
|
6 |
| -// Function used to mimic next/image and sharp |
| 7 | +function getImageType(buffer) { |
| 8 | + const type = imageType(buffer) |
| 9 | + if (type) { |
| 10 | + return type |
| 11 | + } |
| 12 | + if (isSvg(buffer)) { |
| 13 | + return { ext: 'svg', mime: 'image/svg' } |
| 14 | + } |
| 15 | + return null |
| 16 | +} |
| 17 | + |
| 18 | +const IGNORED_FORMATS = new Set(['svg', 'gif']) |
| 19 | +const OUTPUT_FORMATS = new Set(['png', 'jpg', 'webp', 'avif']) |
| 20 | + |
| 21 | +// Function used to mimic next/image |
7 | 22 | const handler = async (event) => {
|
8 | 23 | const [, , url, w = 500, q = 75] = event.path.split('/')
|
9 |
| - const parsedUrl = decodeURIComponent(url) |
| 24 | + // Work-around a bug in redirect handling. Remove when fixed. |
| 25 | + const parsedUrl = decodeURIComponent(url).replace('+', '%20') |
10 | 26 | const width = parseInt(w)
|
11 |
| - const quality = parseInt(q) |
| 27 | + |
| 28 | + if (!width) { |
| 29 | + return { |
| 30 | + statusCode: 400, |
| 31 | + body: 'Invalid image parameters', |
| 32 | + } |
| 33 | + } |
| 34 | + |
| 35 | + const quality = parseInt(q) || 60 |
12 | 36 |
|
13 | 37 | const imageUrl = parsedUrl.startsWith('/')
|
14 | 38 | ? `${process.env.DEPLOY_URL || `http://${event.headers.host}`}${parsedUrl}`
|
15 | 39 | : parsedUrl
|
| 40 | + |
16 | 41 | const imageData = await fetch(imageUrl)
|
| 42 | + |
| 43 | + if (!imageData.ok) { |
| 44 | + console.error(`Failed to download image ${imageUrl}. Status ${imageData.status} ${imageData.statusText}`) |
| 45 | + return { |
| 46 | + statusCode: imageData.status, |
| 47 | + body: imageData.statusText, |
| 48 | + } |
| 49 | + } |
| 50 | + |
17 | 51 | const bufferData = await imageData.buffer()
|
18 |
| - const ext = path.extname(imageUrl) |
19 |
| - const mimeType = ext === 'jpg' ? `image/jpeg` : `image/${ext}` |
20 |
| - |
21 |
| - let image |
22 |
| - let imageBuffer |
23 |
| - |
24 |
| - if (mimeType === 'image/gif') { |
25 |
| - image = await sharp(bufferData, { animated: true }) |
26 |
| - // gif resizing in sharp seems unstable (https://github.com/lovell/sharp/issues/2275) |
27 |
| - imageBuffer = await image.toBuffer() |
28 |
| - } else { |
29 |
| - image = await sharp(bufferData) |
30 |
| - if (mimeType === 'image/webp') { |
31 |
| - image = image.webp({ quality }) |
32 |
| - } else if (mimeType === 'image/jpeg') { |
33 |
| - image = image.jpeg({ quality }) |
34 |
| - } else if (mimeType === 'image/png') { |
35 |
| - image = image.png({ quality }) |
36 |
| - } else if (mimeType === 'image/avif') { |
37 |
| - image = image.avif({ quality }) |
38 |
| - } else if (mimeType === 'image/tiff') { |
39 |
| - image = image.tiff({ quality }) |
40 |
| - } else if (mimeType === 'image/heif') { |
41 |
| - image = image.heif({ quality }) |
| 52 | + |
| 53 | + const type = getImageType(bufferData) |
| 54 | + |
| 55 | + if (!type) { |
| 56 | + return { statusCode: 400, body: 'Source does not appear to be an image' } |
| 57 | + } |
| 58 | + |
| 59 | + let { ext } = type |
| 60 | + |
| 61 | + // For unsupported formats (gif, svg) we redirect to the original |
| 62 | + if (IGNORED_FORMATS.has(ext)) { |
| 63 | + return { |
| 64 | + statusCode: 302, |
| 65 | + headers: { |
| 66 | + Location: imageUrl, |
| 67 | + }, |
42 | 68 | }
|
43 |
| - imageBuffer = await image.resize(width).toBuffer() |
44 | 69 | }
|
45 | 70 |
|
| 71 | + if (process.env.FORCE_WEBP_OUTPUT) { |
| 72 | + ext = 'webp' |
| 73 | + } |
| 74 | + |
| 75 | + if (!OUTPUT_FORMATS.has(ext)) { |
| 76 | + ext = 'jpg' |
| 77 | + } |
| 78 | + |
| 79 | + // The format methods are just to set options: they don't |
| 80 | + // make it return that format. |
| 81 | + const { info, data: imageBuffer } = await sharp(bufferData) |
| 82 | + .jpeg({ quality, force: ext === 'jpg' }) |
| 83 | + .webp({ quality, force: ext === 'webp' }) |
| 84 | + .png({ quality, force: ext === 'png' }) |
| 85 | + .avif({ quality, force: ext === 'avif' }) |
| 86 | + .resize(width) |
| 87 | + .toBuffer({ resolveWithObject: true }) |
| 88 | + |
46 | 89 | return {
|
47 | 90 | statusCode: 200,
|
48 | 91 | headers: {
|
49 |
| - 'Content-Type': mimeType, |
| 92 | + 'Content-Type': `image/${info.format}`, |
50 | 93 | },
|
51 | 94 | body: imageBuffer.toString('base64'),
|
52 | 95 | isBase64Encoded: true,
|
|
0 commit comments