Skip to content

Commit c9c3559

Browse files
committed
feat: Adapt the new createThumbnail function to get base 64 url from image and video media type.
1 parent 59ff8c1 commit c9c3559

File tree

3 files changed

+113
-103
lines changed

3 files changed

+113
-103
lines changed

Diff for: packages/ui/src/elements/Thumbnail/createThumbnail.ts

+84-40
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,94 @@
11
/**
22
* Create a thumbnail from a File object by drawing it onto an OffscreenCanvas
33
*/
4-
export const createThumbnail = (file: File): Promise<string> => {
4+
export const createThumbnail = (
5+
file: File,
6+
fileSrc?: string,
7+
mimeType?: string,
8+
): Promise<string> => {
59
return new Promise((resolve, reject) => {
6-
const img = new Image()
7-
img.src = URL.createObjectURL(file) // Use Object URL directly
8-
9-
img.onload = () => {
10-
const maxDimension = 280
11-
let drawHeight: number, drawWidth: number
12-
13-
// Calculate aspect ratio
14-
const aspectRatio = img.width / img.height
15-
16-
// Determine dimensions to fit within maxDimension while maintaining aspect ratio
17-
if (aspectRatio > 1) {
18-
// Image is wider than tall
19-
drawWidth = maxDimension
20-
drawHeight = maxDimension / aspectRatio
21-
} else {
22-
// Image is taller than wide, or square
23-
drawWidth = maxDimension * aspectRatio
24-
drawHeight = maxDimension
25-
}
10+
/**
11+
* Create DOM element
12+
* Draw media on offscreen canvas
13+
* Resolve fn promise with the base64 image url
14+
* @param media
15+
* @param maxDimension
16+
*/
17+
const _getBase64ImageUrl = async (
18+
media: HTMLImageElement | HTMLVideoElement,
19+
maxDimension = 280,
20+
): Promise<string> => {
21+
return new Promise((_resolve, _reject) => {
22+
let drawHeight: number, drawWidth: number
23+
24+
// Calculate aspect ratio
25+
const width = media.width || (media as HTMLVideoElement).videoWidth
26+
const height = media.height || (media as HTMLVideoElement).videoHeight
27+
const aspectRatio = width / height
28+
29+
// Determine dimensions to fit within maxDimension while maintaining aspect ratio
30+
if (aspectRatio > 1) {
31+
// Image is wider than tall
32+
drawWidth = maxDimension
33+
drawHeight = maxDimension / aspectRatio
34+
} else {
35+
// Image is taller than wide, or square
36+
drawWidth = maxDimension * aspectRatio
37+
drawHeight = maxDimension
38+
}
39+
40+
// Create an OffscreenCanvas
41+
const canvas = new OffscreenCanvas(drawWidth, drawHeight)
42+
const ctx = canvas.getContext('2d')
43+
44+
// Draw the image onto the OffscreenCanvas with calculated dimensions
45+
ctx.drawImage(media, 0, 0, drawWidth, drawHeight)
46+
47+
// Convert the OffscreenCanvas to a Blob and free up memory
48+
canvas
49+
.convertToBlob({ type: 'image/jpeg', quality: 0.25 })
50+
.then((blob) => {
51+
// Release the Object URL
52+
URL.revokeObjectURL(media.src)
53+
const reader = new FileReader()
2654

27-
const canvas = new OffscreenCanvas(drawWidth, drawHeight) // Create an OffscreenCanvas
28-
const ctx = canvas.getContext('2d')
29-
30-
// Draw the image onto the OffscreenCanvas with calculated dimensions
31-
ctx.drawImage(img, 0, 0, drawWidth, drawHeight)
32-
33-
// Convert the OffscreenCanvas to a Blob and free up memory
34-
canvas
35-
.convertToBlob({ type: 'image/jpeg', quality: 0.25 })
36-
.then((blob) => {
37-
URL.revokeObjectURL(img.src) // Release the Object URL
38-
const reader = new FileReader()
39-
reader.onload = () => resolve(reader.result as string) // Resolve as data URL
40-
reader.onerror = reject
41-
reader.readAsDataURL(blob)
42-
})
43-
.catch(reject)
55+
// Resolve as data URL
56+
reader.onload = () => {
57+
_resolve(reader.result as string)
58+
}
59+
reader.onerror = _reject
60+
reader.readAsDataURL(blob)
61+
})
62+
.catch(_reject)
63+
})
64+
}
65+
66+
const fileType = mimeType?.split('/')?.[0]
67+
const url = fileSrc || URL.createObjectURL(file)
68+
let media: HTMLImageElement | HTMLVideoElement
69+
70+
if (fileType === 'video') {
71+
media = document.createElement('video')
72+
media.src = url
73+
media.crossOrigin = 'anonymous'
74+
media.onloadeddata = () => {
75+
_getBase64ImageUrl(media)
76+
.then((url) => resolve(url))
77+
.catch(reject)
78+
}
79+
} else {
80+
media = new Image()
81+
media.src = url
82+
media.onload = () => {
83+
_getBase64ImageUrl(media)
84+
.then((url) => resolve(url))
85+
.catch(reject)
86+
}
4487
}
4588

46-
img.onerror = (error) => {
47-
URL.revokeObjectURL(img.src) // Release Object URL on error
89+
media.onerror = (error) => {
90+
// Release Object URL on error
91+
URL.revokeObjectURL(media.src)
4892
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
4993
reject(error)
5094
}

Diff for: packages/ui/src/elements/Thumbnail/index.tsx

+26-60
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import type { SanitizedCollectionConfig } from 'payload'
1010
import { File } from '../../graphics/File/index.js'
1111
import { useIntersect } from '../../hooks/useIntersect.js'
1212
import { ShimmerEffect } from '../ShimmerEffect/index.js'
13+
import { createThumbnail } from './createThumbnail.js'
1314

1415
export type ThumbnailProps = {
16+
alt?: string
1517
className?: string
1618
collectionSlug?: string
1719
doc?: Record<string, unknown>
@@ -21,80 +23,44 @@ export type ThumbnailProps = {
2123
uploadConfig?: SanitizedCollectionConfig['upload']
2224
}
2325

24-
export const Thumbnail: React.FC<ThumbnailProps> = (props) => {
25-
const { className = '', doc: { filename } = {}, fileSrc, imageCacheTag, size } = props
26-
const [fileExists, setFileExists] = React.useState(undefined)
27-
26+
export const Thumbnail = (props: ThumbnailProps) => {
27+
const { className = '', doc: { filename, mimeType } = {}, fileSrc, imageCacheTag, size } = props
2828
const classNames = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ')
29+
const fileType = (mimeType as string)?.split('/')?.[0]
30+
const [fileExists, setFileExists] = React.useState<boolean | undefined>(undefined)
31+
const [src, setSrc] = React.useState<null | string>(
32+
fileSrc ? `${fileSrc}${imageCacheTag ? `?${imageCacheTag}` : ''}` : null,
33+
)
34+
const [intersectionRef, entry] = useIntersect()
35+
const [hasPreloaded, setHasPreloaded] = React.useState(false)
2936

3037
React.useEffect(() => {
3138
if (!fileSrc) {
32-
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
3339
setFileExists(false)
3440
return
3541
}
3642

37-
const img = new Image()
38-
img.src = fileSrc
39-
img.onload = () => {
40-
setFileExists(true)
41-
}
42-
img.onerror = () => {
43-
setFileExists(false)
44-
}
45-
}, [fileSrc])
46-
47-
return (
48-
<div className={classNames}>
49-
{fileExists === undefined && <ShimmerEffect height="100%" />}
50-
{fileExists && (
51-
<img
52-
alt={filename as string}
53-
src={`${fileSrc}${imageCacheTag ? `?${imageCacheTag}` : ''}`}
54-
/>
55-
)}
56-
{fileExists === false && <File />}
57-
</div>
58-
)
59-
}
60-
61-
type ThumbnailComponentProps = {
62-
readonly alt?: string
63-
readonly className?: string
64-
readonly filename: string
65-
readonly fileSrc: string
66-
readonly imageCacheTag?: string
67-
readonly size?: 'expand' | 'large' | 'medium' | 'small'
68-
}
69-
export function ThumbnailComponent(props: ThumbnailComponentProps) {
70-
const { alt, className = '', filename, fileSrc, imageCacheTag, size } = props
71-
const [fileExists, setFileExists] = React.useState(undefined)
72-
73-
const classNames = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ')
74-
75-
React.useEffect(() => {
76-
if (!fileSrc) {
77-
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
78-
setFileExists(false)
43+
if (!entry?.isIntersecting || hasPreloaded) {
7944
return
8045
}
46+
setHasPreloaded(true)
8147

82-
const img = new Image()
83-
img.src = fileSrc
84-
img.onload = () => {
85-
setFileExists(true)
86-
}
87-
img.onerror = () => {
88-
setFileExists(false)
89-
}
90-
}, [fileSrc])
48+
createThumbnail(null, fileSrc, mimeType as string)
49+
.then((src) => {
50+
setSrc(src)
51+
setFileExists(true)
52+
})
53+
.catch(() => {
54+
setFileExists(false)
55+
})
56+
}, [fileSrc, fileType, imageCacheTag, entry, hasPreloaded])
57+
58+
const alt = props.alt || (filename as string)
9159

9260
return (
93-
<div className={classNames}>
61+
<div className={classNames} ref={intersectionRef}>
9462
{fileExists === undefined && <ShimmerEffect height="100%" />}
95-
{fileExists && (
96-
<img alt={alt || filename} src={`${fileSrc}${imageCacheTag ? `?${imageCacheTag}` : ''}`} />
97-
)}
63+
{fileExists && <img alt={alt} src={src} />}
9864
{fileExists === false && <File />}
9965
</div>
10066
)

Diff for: packages/ui/src/fields/Upload/RelationshipContent/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React from 'react'
55

66
import { Button } from '../../../elements/Button/index.js'
77
import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js'
8-
import { ThumbnailComponent } from '../../../elements/Thumbnail/index.js'
8+
import { Thumbnail } from '../../../elements/Thumbnail/index.js'
99
import './index.scss'
1010

1111
const baseClass = 'upload-relationship-details'
@@ -71,10 +71,10 @@ export function RelationshipContent(props: Props) {
7171
return (
7272
<div className={[baseClass, className].filter(Boolean).join(' ')}>
7373
<div className={`${baseClass}__imageAndDetails`}>
74-
<ThumbnailComponent
74+
<Thumbnail
7575
alt={alt}
7676
className={`${baseClass}__thumbnail`}
77-
filename={filename}
77+
doc={{ filename, mimeType }}
7878
fileSrc={src}
7979
size="small"
8080
/>

0 commit comments

Comments
 (0)