Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby-plugin-image): Add resolver helper and improve custom hook #29342

Merged
merged 14 commits into from
Feb 10, 2021
104 changes: 98 additions & 6 deletions packages/gatsby-plugin-image/src/components/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
generateImageData,
Layout,
EVERY_BREAKPOINT,
IImage,
ImageFormat,
} from "../image-utils"
const imageCache = new Set<string>()

Expand Down Expand Up @@ -88,24 +90,114 @@ export async function applyPolyfill(
;(window as any).objectFitPolyfill(ref.current)
}

export function useGatsbyImage({
export interface IUrlBuilderArgs<OptionsType> {
width: number
height: number
baseUrl: string
format: ImageFormat
options: OptionsType
}
export interface IUseGatsbyImageArgs<OptionsType = {}> {
baseUrl: string
/**
* For constrained and fixed images, the size of the image element
*/
width?: number
height?: number
/**
* If available, pass the source image width and height
*/
sourceWidth?: number
sourceHeight?: number
/**
* If only one dimension is passed, then this will be used to calculate the other.
*/
aspectRatio?: number
layout?: Layout
/**
* Returns a URL based on the passed arguments. Should be a pure function
*/
urlBuilder: (args: IUrlBuilderArgs<OptionsType>) => string

/**
* Should be a data URI
*/
placeholderURL?: string
backgroundColor?: string
/**
* Used in error messages etc
*/
pluginName?: string

/**
* If you do not support auto-format, pass an array of image types here
*/
formats?: Array<ImageFormat>

breakpoints?: Array<number>

/**
* Passed to the urlBuilder function
*/
options?: OptionsType
}

/**
* Use this hook to generate gatsby-plugin-image data in the browser.
*/
export function useGatsbyImage<OptionsType>({
baseUrl,
urlBuilder,
sourceWidth,
sourceHeight,
pluginName = `useGatsbyImage`,
formats = [`auto`],
breakpoints = EVERY_BREAKPOINT,
...args
}: IGatsbyImageHelperArgs): IGatsbyImageData {
return generateImageData({ pluginName, breakpoints, ...args })
options,
...props
}: IUseGatsbyImageArgs<OptionsType>): IGatsbyImageData {
const generateImageSource = (
baseUrl: string,
width: number,
height?: number,
format?: ImageFormat
): IImage => {
return {
width,
height,
format,
src: urlBuilder({ baseUrl, width, height, options, format }),
}
}

const sourceMetadata: IGatsbyImageHelperArgs["sourceMetadata"] = {
width: sourceWidth,
height: sourceHeight,
format: `auto`,
}

const args: IGatsbyImageHelperArgs = {
...props,
pluginName,
generateImageSource,
filename: baseUrl,
formats,
breakpoints,
sourceMetadata,
}
return generateImageData(args)
}

export function getMainProps(
isLoading: boolean,
isLoaded: boolean,
images: any,
images: IGatsbyImageData["images"],
loading?: "eager" | "lazy",
toggleLoaded?: (loaded: boolean) => void,
cacheKey?: string,
ref?: RefObject<HTMLImageElement>,
style: CSSProperties = {}
): MainImageProps {
): Partial<MainImageProps> {
const onLoad: ReactEventHandler<HTMLImageElement> = function (e) {
if (isLoaded) {
return
Expand Down
71 changes: 64 additions & 7 deletions packages/gatsby-plugin-image/src/image-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-unused-expressions */
import { stripIndent } from "common-tags"
import camelCase from "camelcase"
import { IGatsbyImageData } from "."

const DEFAULT_PIXEL_DENSITIES = [0.25, 0.5, 1, 2]
Expand All @@ -19,7 +20,8 @@ export const EVERY_BREAKPOINT = [
4096,
]
const DEFAULT_FLUID_WIDTH = 800
const DEFAULT_FIXED_WIDTH = 400
const DEFAULT_FIXED_WIDTH = 800
const DEFAULT_ASPECT_RATIO = 4 / 3

export type Fit = "cover" | "fill" | "inside" | "outside" | "contain"

Expand Down Expand Up @@ -107,6 +109,8 @@ export interface IGatsbyImageHelperArgs {
fit?: Fit
options?: Record<string, unknown>
breakpoints?: Array<number>
backgroundColor?: string
aspectRatio?: number
}

const warn = (message: string): void => console.warn(message)
Expand Down Expand Up @@ -150,20 +154,68 @@ export function formatFromFilename(filename: string): ImageFormat | undefined {
return undefined
}

export function setDefaultDimensions(
args: IGatsbyImageHelperArgs
): IGatsbyImageHelperArgs {
let {
layout = `constrained`,
width,
height,
sourceMetadata,
breakpoints,
aspectRatio,
formats = [`auto`, `webp`],
} = args
formats = formats.map(format => format.toLowerCase() as ImageFormat)
layout = camelCase(layout) as Layout

if (width && height) {
return args
}
if (sourceMetadata.width && sourceMetadata.height && !aspectRatio) {
aspectRatio = sourceMetadata.width / sourceMetadata.height
}

if (layout === `fullWidth`) {
width = width || sourceMetadata.width || breakpoints[breakpoints.length - 1]
height = height || Math.round(width / (aspectRatio || DEFAULT_ASPECT_RATIO))
} else {
if (!width) {
if (height && aspectRatio) {
width = height * aspectRatio
} else if (sourceMetadata.width) {
width = sourceMetadata.width
} else if (height) {
width = Math.round(height / DEFAULT_ASPECT_RATIO)
} else {
width = DEFAULT_FIXED_WIDTH
}
}

if (aspectRatio && !height) {
height = Math.round(width / aspectRatio)
}
}
return { ...args, width, height, aspectRatio, layout, formats }
}

export function generateImageData(
args: IGatsbyImageHelperArgs
): IGatsbyImageData {
args = setDefaultDimensions(args)

let {
pluginName,
sourceMetadata,
generateImageSource,
layout = `constrained`,
layout,
fit,
options,
width,
height,
filename,
reporter = { warn },
backgroundColor,
} = args

if (!pluginName) {
Expand All @@ -175,18 +227,19 @@ export function generateImageData(
if (typeof generateImageSource !== `function`) {
throw new Error(`generateImageSource must be a function`)
}

if (!sourceMetadata || (!sourceMetadata.width && !sourceMetadata.height)) {
// No metadata means we let the CDN handle max size etc, aspect ratio etc
sourceMetadata = {
width,
height,
format: formatFromFilename(filename),
format: sourceMetadata?.format || formatFromFilename(filename) || `auto`,
}
} else if (!sourceMetadata.format) {
sourceMetadata.format = formatFromFilename(filename)
}
//
const formats = new Set<ImageFormat>(args.formats || [`auto`, `webp`])

const formats = new Set<ImageFormat>(args.formats)

if (formats.size === 0 || formats.has(`auto`) || formats.has(``)) {
formats.delete(`auto`)
Expand Down Expand Up @@ -262,7 +315,11 @@ export function generateImageData(
}
})

const imageProps: Partial<IGatsbyImageData> = { images: result, layout }
const imageProps: Partial<IGatsbyImageData> = {
images: result,
layout,
backgroundColor,
}
switch (layout) {
case `fixed`:
imageProps.width = imageSizes.presentationWidth
Expand Down Expand Up @@ -317,7 +374,7 @@ export function calculateImageSizes(args: IImageSizeArgs): IImageSizes {
return responsiveImageSizes({ breakpoints, ...args })
} else {
reporter.warn(
`No valid layout was provided for the image at ${filename}. Valid image layouts are fixed, fullWidth, and constrained.`
`No valid layout was provided for the image at ${filename}. Valid image layouts are fixed, fullWidth, and constrained. Found ${layout}`
)
return {
sizes: [imgDimensions.width],
Expand Down
2 changes: 2 additions & 0 deletions packages/gatsby-plugin-image/src/index.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export {
useGatsbyImage,
useArtDirection,
IArtDirectedImage,
IUseGatsbyImageArgs,
IUrlBuilderArgs,
} from "./components/hooks"
export {
generateImageData,
Expand Down
2 changes: 2 additions & 0 deletions packages/gatsby-plugin-image/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {
useGatsbyImage,
useArtDirection,
IArtDirectedImage,
IUseGatsbyImageArgs,
IUrlBuilderArgs,
} from "./components/hooks"
export {
generateImageData,
Expand Down
Loading