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)
}
|