|
|
|
import { promises as fs } from 'fs'
|
|
|
|
import { createHash } from 'node:crypto'
|
|
|
|
import path from 'path'
|
|
|
|
import { ImgHTMLAttributes } from 'react'
|
|
|
|
import sharp from 'sharp'
|
|
|
|
|
|
|
|
const outputDir = path.join(process.cwd(), '.next/static/images')
|
|
|
|
const serverPath = '/_next/static/images/'
|
|
|
|
|
|
|
|
interface ReqProps {
|
|
|
|
src: string,
|
|
|
|
alt: string,
|
|
|
|
}
|
|
|
|
|
|
|
|
type FixedSingleDimension = {width: number} | {height: number}
|
|
|
|
type FixedBothDimension = {width: number, height: number}
|
|
|
|
type MultiSize = {width: number[]} | {height: number[]}
|
|
|
|
type SizeProps = FixedSingleDimension | FixedBothDimension | MultiSize
|
|
|
|
type ImageAttrs = Omit<ImgHTMLAttributes<HTMLImageElement>, "width" | "height" | "src">
|
|
|
|
|
|
|
|
type ImageProps = ImageAttrs
|
|
|
|
& SizeProps
|
|
|
|
& ReqProps
|
|
|
|
|
|
|
|
interface ProcessedImage {
|
|
|
|
hostPath: string,
|
|
|
|
randPrefix: string,
|
|
|
|
origSrc: string,
|
|
|
|
aspectRatio: number,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function ImageServer({src,...imgAttrs}: ImageProps) {
|
|
|
|
const width = 'width' in imgAttrs ? imgAttrs.width : null
|
|
|
|
const height = 'height' in imgAttrs ? imgAttrs.height : null
|
|
|
|
|
|
|
|
// Handle where we have an array of widths
|
|
|
|
if (Array.isArray(width)) {
|
|
|
|
let _src : string = src
|
|
|
|
const srcSet = (await Promise.all(width.map(async width => {
|
|
|
|
const {outputPath, origSrc} = await processImage(src, {width})
|
|
|
|
_src = origSrc
|
|
|
|
return `${outputPath} ${width}w`
|
|
|
|
}))).join(',')
|
|
|
|
|
|
|
|
return (
|
|
|
|
<img
|
|
|
|
src={_src}
|
|
|
|
srcSet={srcSet}
|
|
|
|
{...imgAttrs}
|
|
|
|
width={undefined}
|
|
|
|
height={undefined}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}else if (Array.isArray(height)) {
|
|
|
|
let _src : string = src
|
|
|
|
const srcSet = (await Promise.all(height.map(async height => {
|
|
|
|
const {outputPath, origSrc} = await processImage(src, {height})
|
|
|
|
_src = origSrc
|
|
|
|
return `${outputPath} ${width}w`
|
|
|
|
}))).join(',')
|
|
|
|
|
|
|
|
return (
|
|
|
|
<img
|
|
|
|
src={_src}
|
|
|
|
srcSet={srcSet}
|
|
|
|
{...imgAttrs}
|
|
|
|
width={undefined}
|
|
|
|
height={undefined}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
const {outputPath, resolvedWidth} = await processImage(src, {width, height})
|
|
|
|
return <img src={outputPath} {...imgAttrs} height={undefined} width={resolvedWidth} />
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processImage(sourcePath: string, {width, height} : {width?: number, height?: number}) : Promise<{outputPath: string, resolvedWidth: number, origSrc: string}> {
|
|
|
|
await fs.mkdir(outputDir, {recursive: true})
|
|
|
|
|
|
|
|
const hostPath = path.join(process.cwd(), sourcePath)
|
|
|
|
const srcImg = sharp(hostPath)
|
|
|
|
const metadata = await srcImg.metadata()
|
|
|
|
const extension = path.extname(hostPath)
|
|
|
|
const filename = path.basename(hostPath, extension)
|
|
|
|
const hashPrefix = createHash('sha256').update(hostPath).digest('hex').substr(0, 7)
|
|
|
|
const origSrc = `${hashPrefix}-${filename}${extension}`
|
|
|
|
const aspectRatio = metadata.width / metadata.height
|
|
|
|
const resolvedWidth = width ? width : aspectRatio * height
|
|
|
|
const outputName = `${hashPrefix}-${filename}-${resolvedWidth}w${extension}`
|
|
|
|
|
|
|
|
const origCopyStat = await fs.stat(path.join(outputDir, origSrc)).catch(() => undefined)
|
|
|
|
if (!origCopyStat?.isFile()) {
|
|
|
|
console.log(`copying file ${hostPath}`)
|
|
|
|
await fs.copyFile(hostPath, path.join(outputDir, origSrc))
|
|
|
|
}
|
|
|
|
|
|
|
|
const outputStat = await fs.stat(path.join(outputDir, outputName)).catch(() => undefined)
|
|
|
|
if (!outputStat?.isFile()) {
|
|
|
|
console.log(`resizing file ${hostPath} for width ${resolvedWidth}`)
|
|
|
|
|
|
|
|
await srcImg
|
|
|
|
.resize({width: resolvedWidth})
|
|
|
|
.toFile(path.join(outputDir, outputName))
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
outputPath: serverPath + outputName,
|
|
|
|
resolvedWidth,
|
|
|
|
origSrc,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function ImageServerWrap(props: ImageProps) {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{/* @ts-expect-error Async Server Component */}
|
|
|
|
<ImageServer {...props} />
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|