diff --git a/package.json b/package.json index ffe1716f1b2a..3862a4347705 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "get-port": "5.1.1", "glob": "7.1.6", "gzip-size": "5.1.1", + "is-animated": "2.0.0", "isomorphic-unfetch": "3.0.0", "jest-circus": "26.0.1", "jest-cli": "24.9.0", diff --git a/packages/next/compiled/is-animated/index.js b/packages/next/compiled/is-animated/index.js new file mode 100644 index 000000000000..422791a171e8 --- /dev/null +++ b/packages/next/compiled/is-animated/index.js @@ -0,0 +1 @@ +module.exports=function(e,r){"use strict";var t={};function __webpack_require__(r){if(t[r]){return t[r].exports}var a=t[r]={i:r,l:false,exports:{}};e[r].call(a.exports,a,a.exports,__webpack_require__);a.l=true;return a.exports}__webpack_require__.ab=__dirname+"/";function startup(){return __webpack_require__(441)}return startup()}({401:function(e,r){r.isWebp=function(e){var r=[87,69,66,80];for(var t=0;t1}}}); \ No newline at end of file diff --git a/packages/next/compiled/is-animated/package.json b/packages/next/compiled/is-animated/package.json new file mode 100644 index 000000000000..b1ef8288c8dd --- /dev/null +++ b/packages/next/compiled/is-animated/package.json @@ -0,0 +1 @@ +{"name":"is-animated","main":"index.js","author":"Józef Sokołowski ","license":"MIT"} diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 7a5c64093b55..9e39339fe473 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -7,14 +7,18 @@ import { createHash } from 'crypto' import Server from './next-server' import { getContentType, getExtension } from './serve-static' import { fileExists } from '../../lib/file-exists' +// @ts-ignore no types for is-animated +import isAnimated from 'next/dist/compiled/is-animated' let sharp: typeof import('sharp') //const AVIF = 'image/avif' const WEBP = 'image/webp' const PNG = 'image/png' const JPEG = 'image/jpeg' +const GIF = 'image/gif' const MIME_TYPES = [/* AVIF, */ WEBP, PNG, JPEG] const CACHE_VERSION = 1 +const ANIMATABLE_TYPES = [WEBP, PNG, GIF] export async function imageOptimizer( server: Server, @@ -146,6 +150,18 @@ export async function imageOptimizer( const expireAt = maxAge * 1000 + now let contentType: string + if ( + upstreamType && + ANIMATABLE_TYPES.includes(upstreamType) && + isAnimated(upstreamBuffer) + ) { + if (upstreamType) { + res.setHeader('Content-Type', upstreamType) + } + res.end(upstreamBuffer) + return { finished: true } + } + if (mimeType) { contentType = mimeType } else if (upstreamType?.startsWith('image/') && getExtension(upstreamType)) { diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 6533e1f6b10a..ba1b5fbcbc7f 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -241,6 +241,14 @@ export async function ncc_ignore_loader(task, opts) { .target('compiled/ignore-loader') } // eslint-disable-next-line camelcase +externals['is-animated'] = 'next/dist/compiled/is-animated' +export async function ncc_is_animated(task, opts) { + await task + .source(opts.src || relative(__dirname, require.resolve('is-animated'))) + .ncc({ packageName: 'is-animated', externals }) + .target('compiled/is-animated') +} +// eslint-disable-next-line camelcase externals['is-docker'] = 'next/dist/compiled/is-docker' export async function ncc_is_docker(task, opts) { await task @@ -504,6 +512,7 @@ export async function ncc(task) { 'ncc_gzip_size', 'ncc_http_proxy', 'ncc_ignore_loader', + 'ncc_is_animated', 'ncc_is_docker', 'ncc_is_wsl', 'ncc_json5', diff --git a/test/integration/image-optimizer/public/animated.gif b/test/integration/image-optimizer/public/animated.gif new file mode 100644 index 000000000000..0eab3349ca94 Binary files /dev/null and b/test/integration/image-optimizer/public/animated.gif differ diff --git a/test/integration/image-optimizer/public/animated.png b/test/integration/image-optimizer/public/animated.png new file mode 100644 index 000000000000..1b35084a369e Binary files /dev/null and b/test/integration/image-optimizer/public/animated.png differ diff --git a/test/integration/image-optimizer/public/animated.webp b/test/integration/image-optimizer/public/animated.webp new file mode 100644 index 000000000000..7a1d3fe146ae Binary files /dev/null and b/test/integration/image-optimizer/public/animated.webp differ diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 73233695b4e1..e3dc346de640 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -1,6 +1,7 @@ /* eslint-env jest */ import fs from 'fs-extra' import { join } from 'path' +import isAnimated from 'next/dist/compiled/is-animated' import { killApp, findPort, @@ -40,6 +41,30 @@ function runTests({ w, isDev }) { expect(await res.text()).toMatch(/Image Optimizer Home/m) }) + it('should maintain animated gif', async () => { + const query = { w, q: 90, url: '/animated.gif' } + const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('image/gif') + expect(isAnimated(await res.buffer())).toBe(true) + }) + + it('should maintain animated png', async () => { + const query = { w, q: 90, url: '/animated.png' } + const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('image/png') + expect(isAnimated(await res.buffer())).toBe(true) + }) + + it('should maintain animated webp', async () => { + const query = { w, q: 90, url: '/animated.webp' } + const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('image/webp') + expect(isAnimated(await res.buffer())).toBe(true) + }) + it('should fail when url is missing', async () => { const query = { w, q: 100 } const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) diff --git a/yarn.lock b/yarn.lock index e9616c3cb7a1..f4ffa3a70eb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8878,6 +8878,11 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-animated@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-animated/-/is-animated-2.0.0.tgz#ffb1f180ff10782b8442dcb0955b5fc915e1a111" + integrity sha512-ZsfhGnSRUjto9owW5WpaDL7z/03H77Ny9JgP/BSyOQ/mAPAKNbdX30WNYbg2R5NhvT5VqKs2KL38znj5OG8tDg== + is-arguments@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"