File size: 1,848 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { Jimp } from 'jimp'
import path from 'path'
import fs from 'fs/promises'
import crypto from 'crypto'
import { isAddonEnabled } from '../adminService'
import { ADDON_IDS } from '../../addons'

const THUMB_MAX = 800
const THUMB_QUALITY = 80

export async function ensureLocalThumbnail(
  uploadsRoot: string,
  originalRelPath: string,
): Promise<{ thumbnailRelPath: string; width: number; height: number } | null> {
  if (!isAddonEnabled(ADDON_IDS.JOURNEY)) return null

  const originalAbs = path.join(uploadsRoot, originalRelPath)
  try { await fs.access(originalAbs) } catch { return null }

  // Deterministic name so concurrent requests don't race on the same photo.
  const hash = crypto.createHash('sha1').update(originalRelPath).digest('hex').slice(0, 16)
  const thumbRel = `journey/thumbs/${hash}.jpg`
  const thumbAbs = path.join(uploadsRoot, thumbRel)

  try {
    const [srcStat, dstStat] = await Promise.all([
      fs.stat(originalAbs),
      fs.stat(thumbAbs).catch(() => null),
    ])
    if (dstStat && dstStat.mtimeMs >= srcStat.mtimeMs) {
      const img = await Jimp.read(thumbAbs)
      return { thumbnailRelPath: thumbRel, width: img.bitmap.width, height: img.bitmap.height }
    }

    await fs.mkdir(path.dirname(thumbAbs), { recursive: true })

    // Jimp auto-applies EXIF orientation on read, matching sharp's .rotate() behavior.
    const img = await Jimp.read(originalAbs)
    const { width: w, height: h } = img.bitmap
    if (w > THUMB_MAX || h > THUMB_MAX) {
      img.scaleToFit({ w: THUMB_MAX, h: THUMB_MAX })
    }
    await img.write(thumbAbs as `${string}.jpg`, { quality: THUMB_QUALITY })

    return { thumbnailRelPath: thumbRel, width: img.bitmap.width, height: img.bitmap.height }
  } catch {
    // Unsupported format, corrupt file, etc. — fall back to original in caller.
    return null
  }
}