diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 95c84652faa28..1c68d75785442 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -16,6 +16,7 @@ type ImageProps = Omit< 'src' | 'srcSet' | 'ref' > & { src: string + quality?: string priority?: boolean lazy?: boolean unoptimized?: boolean @@ -23,6 +24,10 @@ type ImageProps = Omit< let imageData: any = process.env.__NEXT_IMAGE_OPTS const breakpoints = imageData.sizes || [640, 1024, 1600] +// Auto optimize defaults to on if not specified +if (imageData.autoOptimize === undefined) { + imageData.autoOptimize = true +} let cachedObserver: IntersectionObserver const IntersectionObserver = @@ -59,28 +64,39 @@ function getObserver(): IntersectionObserver | undefined { )) } -function computeSrc(src: string, unoptimized: boolean): string { +function computeSrc( + src: string, + unoptimized: boolean, + quality?: string +): string { if (unoptimized) { return src } - return callLoader(src) + return callLoader({ src, quality }) +} + +type CallLoaderProps = { + src: string + width?: number + quality?: string } -function callLoader(src: string, width?: number): string { +function callLoader(loaderProps: CallLoaderProps) { let loader = loaders[imageData.loader || 'default'] - return loader({ root: imageData.path, src, width }) + return loader({ root: imageData.path, ...loaderProps }) } type SrcSetData = { src: string widths: number[] + quality?: string } -function generateSrcSet({ src, widths }: SrcSetData): string { +function generateSrcSet({ src, widths, quality }: SrcSetData): string { // At each breakpoint, generate an image url using the loader, such as: // ' www.example.com/foo.jpg?w=480 480w, ' return widths - .map((width: number) => `${callLoader(src, width)} ${width}w`) + .map((width: number) => `${callLoader({ src, width, quality })} ${width}w`) .join(', ') } @@ -89,6 +105,7 @@ type PreloadData = { widths: number[] sizes?: string unoptimized?: boolean + quality?: string } function generatePreload({ @@ -96,6 +113,7 @@ function generatePreload({ widths, unoptimized = false, sizes, + quality, }: PreloadData): ReactElement { // This function generates an image preload that makes use of the "imagesrcset" and "imagesizes" // attributes for preloading responsive images. They're still experimental, but fully backward @@ -106,9 +124,9 @@ function generatePreload({ @@ -122,6 +140,7 @@ export default function Image({ priority = false, lazy = false, className, + quality, ...rest }: ImageProps) { const thisEl = useRef(null) @@ -159,11 +178,12 @@ export default function Image({ }, [thisEl, lazy]) // Generate attribute values - const imgSrc = computeSrc(src, unoptimized) + const imgSrc = computeSrc(src, unoptimized, quality) const imgSrcSet = !unoptimized ? generateSrcSet({ src, widths: breakpoints, + quality, }) : undefined @@ -220,23 +240,46 @@ export default function Image({ //BUILT IN LOADERS -type LoaderProps = { - root: string - src: string - width?: number -} +type LoaderProps = CallLoaderProps & { root: string } -function imgixLoader({ root, src, width }: LoaderProps): string { - return `${root}${src}${width ? '?w=' + width : ''}` +function imgixLoader({ root, src, width, quality }: LoaderProps): string { + const params = [] + let paramsString = '' + if (width) { + params.push('w=' + width) + } + if (quality) { + params.push('q=' + quality) + } + if (imageData.autoOptimize) { + params.push('auto=compress') + } + if (params.length) { + paramsString = '?' + params.join('&') + } + return `${root}${src}${paramsString}` } -function cloudinaryLoader({ root, src, width }: LoaderProps): string { - return `${root}${width ? 'w_' + width + '/' : ''}${src}` +function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string { + const params = [] + let paramsString = '' + if (!quality && imageData.autoOptimize) { + quality = 'auto' + } + if (width) { + params.push('w_' + width) + } + if (quality) { + params.push('q_' + quality) + } + if (params.length) { + paramsString = params.join(',') + '/' + } + return `${root}${paramsString}${src}` } -function defaultLoader({ root, src, width }: LoaderProps): string { - // TODO: change quality parameter to be configurable +function defaultLoader({ root, src, width, quality }: LoaderProps): string { return `${root}?url=${encodeURIComponent(src)}&${ width ? `w=${width}&` : '' - }q=100` + }q=${quality || '100'}` } diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js index 8f260dea043fd..45c3041575129 100644 --- a/test/integration/image-component/basic/next.config.js +++ b/test/integration/image-component/basic/next.config.js @@ -1,6 +1,7 @@ module.exports = { images: { sizes: [480, 1024, 1600], + autoOptimize: false, path: 'https://example.com/myaccount/', loader: 'imgix', }, diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js index 0615e65d175aa..b4f7f9da18d5b 100644 --- a/test/integration/image-component/basic/pages/client-side.js +++ b/test/integration/image-component/basic/pages/client-side.js @@ -6,7 +6,7 @@ const ClientSide = () => { return (

This is a client side page

- + { return (

Hello World

- + { expect(await browser.elementById('basic-image').getAttribute('src')).toBe( - 'https://example.com/myaccount/foo.jpg' + 'https://example.com/myaccount/foo.jpg?q=60' ) }) it('should correctly generate src even if preceding slash is included in prop', async () => { @@ -44,7 +44,7 @@ function runTests() { expect( await browser.elementById('basic-image').getAttribute('srcset') ).toBe( - 'https://example.com/myaccount/foo.jpg?w=480 480w, https://example.com/myaccount/foo.jpg?w=1024 1024w, https://example.com/myaccount/foo.jpg?w=1600 1600w' + 'https://example.com/myaccount/foo.jpg?w=480&q=60 480w, https://example.com/myaccount/foo.jpg?w=1024&q=60 1024w, https://example.com/myaccount/foo.jpg?w=1600&q=60 1600w' ) }) it('should add a srcset even with preceding slash in prop', async () => {