summary refs log tree commit diff
path: root/components/Image/index.tsx
blob: 1fd139ed8b284754887fb3165789146fe12850d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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} />
    </>
  )
}