|
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