From 5289646abb229c9948dafda41d93a8bf85d56144 Mon Sep 17 00:00:00 2001
From: Ashelyn Rose <git@tempest.dev>
Date: Sat, 2 Dec 2023 17:58:07 -0700
Subject: dedup generated images

---
 components/Image/index.tsx | 115 +++++++++++++++++++++++++--------------------
 1 file changed, 64 insertions(+), 51 deletions(-)

(limited to 'components/Image/index.tsx')

diff --git a/components/Image/index.tsx b/components/Image/index.tsx
index e7a393c..1fd139e 100644
--- a/components/Image/index.tsx
+++ b/components/Image/index.tsx
@@ -1,4 +1,5 @@
 import { promises as fs } from 'fs'
+import { createHash } from 'node:crypto'
 import path from 'path'
 import { ImgHTMLAttributes } from 'react'
 import sharp from 'sharp'
@@ -24,77 +25,89 @@ type ImageProps = ImageAttrs
 interface ProcessedImage {
   hostPath: string,
   randPrefix: string,
-  widths: {[width: number] : string}
+  origSrc: string,
+  aspectRatio: number,
 }
 
+
 async function ImageServer({src,...imgAttrs}: ImageProps) {
-  const srcImg = sharp(src)
-  const extension = path.extname(src)
-  const filename = path.basename(src, extension)
-  const randPrefix = (Math.random() + 1).toString(36).substring(7)
   const width = 'width' in imgAttrs ? imgAttrs.width : null
   const height = 'height' in imgAttrs ? imgAttrs.height : null
 
-  console.log(`processing ${src} for width ${width} and height ${height}`)
-
-  // Make sure we can write to the dir
-  await fs.mkdir(outputDir, {recursive: true})
-
-  // Handle where we have an array of dimensions
-  if (Array.isArray(width) || Array.isArray(height)) {
-    const dimension = Array.isArray(width) ? 'width' : 'height'
-    const sizes = Array.isArray(width) ? width : height as number[]
-    const suffix = dimension.charAt(0)
-
-    // Copy in original
-    fs.copyFile(src, path.join(outputDir, `${randPrefix}-${filename}${extension}`))
-
-    // Create resized copies
-    const srcSetLines = await Promise.all(sizes
-      .map(async size => {
-        const outputName = `${randPrefix}-${filename}-${size}${suffix}${extension}`
-        const outputPath = path.join(outputDir, outputName)
-        return srcImg
-          .resize({[dimension]: size})
-          .toFile(outputPath)
-          .then(result => `${serverPath}${outputName} ${result.width}w`)
-      }))
+  // 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={srcSetLines.join(',')}
+        src={_src}
+        srcSet={srcSet}
         {...imgAttrs}
         width={undefined}
         height={undefined}
       />
     )
-  } else {
-    const dimensions = {
-      width: typeof width === 'number' ? width : undefined,
-      height: typeof height === 'number' ? height : undefined
-    }
-
-    const dimensionString = (() => {
-      if (dimensions.height && dimensions.height)
-        return `${dimensions.width}x${dimensions.height}`
-      if (dimensions.width)
-        return `${dimensions.width}w`
-      return `${dimensions.height}h`
-    })()
-
-    const outputName = `${randPrefix}-${filename}-${dimensionString}${extension}`
-    const outputPath = path.join(outputDir, outputName)
-    const outputSrc =   serverPath + outputName
-    await srcImg.resize(dimensions).toFile(outputPath)
+  }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={outputSrc}
+        src={_src}
+        srcSet={srcSet}
         {...imgAttrs}
-        {...dimensions}
+        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,
   }
 }
 
-- 
cgit 1.4.1

