File size: 3,037 Bytes
abcf568
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useState } from 'react'

const PREVIEW_MAX_EDGE = 2400

interface PlotPreviewImageState {
  previewSrc?: string
  isPreviewReady: boolean
}

export function usePlotPreviewImage(source?: string): PlotPreviewImageState {
  const [previewSrc, setPreviewSrc] = useState<string | undefined>(source)
  const [isPreviewReady, setIsPreviewReady] = useState(false)

  useEffect(() => {
    if (!source) {
      setPreviewSrc(undefined)
      setIsPreviewReady(false)
      return undefined
    }

    if (looksLikeSvg(source)) {
      setPreviewSrc(source)
      setIsPreviewReady(true)
      return undefined
    }

    let cancelled = false
    let objectUrl: string | undefined

    setPreviewSrc(source)
    setIsPreviewReady(false)

    void createPreviewBitmap(source, PREVIEW_MAX_EDGE).then((nextPreviewSrc) => {
      if (cancelled) {
        if (nextPreviewSrc?.startsWith('blob:')) {
          URL.revokeObjectURL(nextPreviewSrc)
        }
        return
      }

      objectUrl = nextPreviewSrc?.startsWith('blob:') ? nextPreviewSrc : undefined
      setPreviewSrc(nextPreviewSrc ?? source)
      setIsPreviewReady(true)
    }).catch(() => {
      if (!cancelled) {
        setPreviewSrc(source)
        setIsPreviewReady(true)
      }
    })

    return () => {
      cancelled = true
      if (objectUrl) {
        URL.revokeObjectURL(objectUrl)
      }
    }
  }, [source])

  return {
    previewSrc,
    isPreviewReady,
  }
}

async function createPreviewBitmap(source: string, maxEdge: number) {
  const image = await loadImage(source)
  const width = Math.max(1, image.naturalWidth || image.width || 1)
  const height = Math.max(1, image.naturalHeight || image.height || 1)
  const longestEdge = Math.max(width, height)

  if (longestEdge <= maxEdge) {
    return source
  }

  const scale = maxEdge / longestEdge
  const targetWidth = Math.max(1, Math.round(width * scale))
  const targetHeight = Math.max(1, Math.round(height * scale))
  const canvas = document.createElement('canvas')
  canvas.width = targetWidth
  canvas.height = targetHeight

  const context = canvas.getContext('2d', { alpha: false })
  if (!context) {
    return source
  }

  context.imageSmoothingEnabled = true
  context.imageSmoothingQuality = 'high'
  context.drawImage(image, 0, 0, targetWidth, targetHeight)

  const blob = await new Promise<Blob | null>((resolve) => {
    canvas.toBlob(resolve, 'image/webp', 0.88)
  })
  if (!blob) {
    return source
  }

  return URL.createObjectURL(blob)
}

async function loadImage(source: string) {
  return await new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image()
    image.decoding = 'async'
    image.onload = () => resolve(image)
    image.onerror = () => reject(new Error('Failed to load preview source'))
    if (!source.startsWith('data:')) {
      image.crossOrigin = 'anonymous'
    }
    image.src = source
  })
}

function looksLikeSvg(source: string) {
  return source.startsWith('data:image/svg+xml') || /\.svg(?:[?#]|$)/i.test(source)
}