Skip to content

Commit

Permalink
Make image quality for loaders configurable with attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
atcastle committed Oct 20, 2020
1 parent ae40797 commit 9c1c3b1
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 25 deletions.
85 changes: 64 additions & 21 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ type ImageProps = Omit<
'src' | 'srcSet' | 'ref'
> & {
src: string
quality?: string
priority?: boolean
lazy?: boolean
unoptimized?: boolean
}

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 =
Expand Down Expand Up @@ -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(', ')
}

Expand All @@ -89,13 +105,15 @@ type PreloadData = {
widths: number[]
sizes?: string
unoptimized?: boolean
quality?: string
}

function generatePreload({
src,
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
Expand All @@ -106,9 +124,9 @@ function generatePreload({
<link
rel="preload"
as="image"
href={computeSrc(src, unoptimized)}
href={computeSrc(src, unoptimized, quality)}
// @ts-ignore: imagesrcset and imagesizes not yet in the link element type
imagesrcset={generateSrcSet({ src, widths })}
imagesrcset={generateSrcSet({ src, widths, quality })}
imagesizes={sizes}
/>
</Head>
Expand All @@ -122,6 +140,7 @@ export default function Image({
priority = false,
lazy = false,
className,
quality,
...rest
}: ImageProps) {
const thisEl = useRef<HTMLImageElement>(null)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'}`
}
1 change: 1 addition & 0 deletions test/integration/image-component/basic/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
images: {
sizes: [480, 1024, 1600],
autoOptimize: false,
path: 'https://example.com/myaccount/',
loader: 'imgix',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const ClientSide = () => {
return (
<div>
<p id="stubtext">This is a client side page</p>
<Image id="basic-image" src="foo.jpg"></Image>
<Image id="basic-image" src="foo.jpg" quality="60"></Image>
<Image id="attribute-test" data-demo="demo-value" src="bar.jpg" />
<Image
id="secondary-image"
Expand Down
2 changes: 1 addition & 1 deletion test/integration/image-component/basic/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Page = () => {
return (
<div>
<p>Hello World</p>
<Image id="basic-image" src="foo.jpg"></Image>
<Image id="basic-image" src="foo.jpg" quality="60"></Image>
<Image id="attribute-test" data-demo="demo-value" src="bar.jpg" />
<Image
id="secondary-image"
Expand Down
4 changes: 2 additions & 2 deletions test/integration/image-component/basic/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function runTests() {
})
it('should modify src with the loader', async () => {
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 () => {
Expand All @@ -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 () => {
Expand Down

0 comments on commit 9c1c3b1

Please sign in to comment.