Skip to content

Commit

Permalink
Make sure animated assets aren't de-animated by optimizer (#17974)
Browse files Browse the repository at this point in the history
This makes sure the image optimizer doesn't de-animate images by transforming them with sharp since sharp doesn't currently handle outputting animated images

x-ref: #17749
  • Loading branch information
ijjk committed Oct 17, 2020
1 parent c9eb3dc commit bbdebd4
Show file tree
Hide file tree
Showing 10 changed files with 58 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/next/compiled/is-animated/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/next/compiled/is-animated/package.json
@@ -0,0 +1 @@
{"name":"is-animated","main":"index.js","author":"Józef Sokołowski <j.k.sokolowski@gmail.com>","license":"MIT"}
16 changes: 16 additions & 0 deletions packages/next/next-server/server/image-optimizer.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down
9 changes: 9 additions & 0 deletions packages/next/taskfile.js
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
25 changes: 25 additions & 0 deletions 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,
Expand Down Expand Up @@ -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, {})
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -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"
Expand Down

0 comments on commit bbdebd4

Please sign in to comment.