返回 AiToEarn
index.tsx
1 import type { ImageLoader, ImageLoaderProps, ImageProps, StaticImageData } from 'next/image'
2 import Image from 'next/image'
3 import { getOssThumbnailUrl, getOssUrl } from '@/utils/oss'
4
5 export type OssImageProps = ImageProps & {
6 thumbnailSize?: number
7 thumbnailWidth?: number
8 thumbnailHeight?: number
9 disableOssThumbnail?: boolean
10 }
11
12 const DEVICE_PIXEL_RATIO = 2
13 const THUMBNAIL_SHARPNESS_SCALE = 1.5
14 const THUMBNAIL_SHARPNESS_MAX_DIMENSION = 320
15 const DEFAULT_THUMBNAIL_MAX_DIMENSION = 2048
16 const MIN_THUMBNAIL_DIMENSION = 16
17 const SIZE_PIXEL_PATTERN = /(?:^|\s)([1-9]\d*)px(?:\s|$)/
18
19 type ResolveOssImageSrcOptions = Pick<ImageProps, 'height' | 'loader' | 'overrideSrc' | 'quality' | 'sizes' | 'width'> & Pick<OssImageProps, 'disableOssThumbnail' | 'thumbnailHeight' | 'thumbnailSize' | 'thumbnailWidth'>
20
21 function getThumbnailMaxDimension() {
22 const rawValue = process.env.NEXT_PUBLIC_OSS_IMAGE_THUMBNAIL_MAX_WIDTH
23 const value = rawValue ? Number(rawValue) : DEFAULT_THUMBNAIL_MAX_DIMENSION
24 return Number.isFinite(value) && value > 0 ? value : DEFAULT_THUMBNAIL_MAX_DIMENSION
25 }
26
27 function isThumbnailEnabled() {
28 return process.env.NEXT_PUBLIC_ENABLE_OSS_IMAGE_THUMBNAIL !== 'false'
29 }
30
31 function isStaticImageImport(src: ImageProps['src']) {
32 return typeof src !== 'string'
33 }
34
35 function isLocalOrRuntimeImageSrc(src: string) {
36 return src.startsWith('/')
37 || src.startsWith('blob:')
38 || src.startsWith('data:')
39 || src.startsWith('ossProxy')
40 || src.startsWith('/ossProxy/')
41 }
42
43 function isSvgImageSrc(src: string) {
44 const pathname = src.split(/[?#]/)[0]
45 return pathname.toLowerCase().endsWith('.svg')
46 }
47
48 function shouldUseOriginalSrc(src: string, options: ResolveOssImageSrcOptions) {
49 if (isLocalOrRuntimeImageSrc(src))
50 return true
51
52 if (isSvgImageSrc(src))
53 return true
54
55 if (!isThumbnailEnabled())
56 return true
57
58 if (options.disableOssThumbnail)
59 return true
60
61 if (options.loader)
62 return true
63
64 if (options.overrideSrc)
65 return true
66
67 return false
68 }
69
70 function getNumericDimension(value: ImageProps['width'] | ImageProps['height']) {
71 if (typeof value === 'number' && Number.isFinite(value) && value > 0)
72 return value
73
74 if (typeof value === 'string') {
75 const numericValue = Number.parseFloat(value)
76 return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : undefined
77 }
78
79 return undefined
80 }
81
82 function getNumericQuality(value: ImageProps['quality']) {
83 if (typeof value === 'number' && Number.isFinite(value) && value > 0)
84 return value
85
86 if (typeof value === 'string') {
87 const numericValue = Number.parseFloat(value)
88 return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : undefined
89 }
90
91 return undefined
92 }
93
94 function shouldSharpenThumbnail(width?: number, height?: number) {
95 const dimensions = [width, height].filter((dimension): dimension is number => (
96 typeof dimension === 'number' && Number.isFinite(dimension) && dimension > 0
97 ))
98
99 if (dimensions.length === 0)
100 return false
101
102 return Math.max(...dimensions) < THUMBNAIL_SHARPNESS_MAX_DIMENSION
103 }
104
105 function normalizeThumbnailDimension(value?: number, scale = 1) {
106 if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
107 return undefined
108
109 const dimension = Math.ceil(value)
110 const scaledDimension = Math.ceil(dimension * scale)
111
112 return Math.min(
113 Math.max(scaledDimension, MIN_THUMBNAIL_DIMENSION),
114 getThumbnailMaxDimension(),
115 )
116 }
117
118 function getLargestPixelSize(sizes?: string) {
119 if (!sizes)
120 return undefined
121
122 const pixelSizes = sizes
123 .split(',')
124 .map((size) => {
125 const normalizedSize = size.trim()
126 const sizeValue = normalizedSize.includes(')')
127 ? normalizedSize.slice(normalizedSize.lastIndexOf(')') + 1).trim()
128 : normalizedSize
129 return SIZE_PIXEL_PATTERN.exec(sizeValue)?.[1]
130 })
131 .filter((value): value is string => Boolean(value))
132 .map(value => Number(value))
133
134 if (pixelSizes.length === 0)
135 return undefined
136
137 return Math.max(...pixelSizes)
138 }
139
140 function getRequestedThumbnailDimensions(options: ResolveOssImageSrcOptions) {
141 const explicitWidth = options.thumbnailWidth ?? options.thumbnailSize
142 const explicitHeight = options.thumbnailHeight ?? options.thumbnailSize
143
144 if (explicitWidth || explicitHeight) {
145 const scale = shouldSharpenThumbnail(explicitWidth, explicitHeight) ? THUMBNAIL_SHARPNESS_SCALE : 1
146
147 return {
148 width: normalizeThumbnailDimension(explicitWidth, scale),
149 height: normalizeThumbnailDimension(explicitHeight, scale),
150 }
151 }
152
153 const width = getLargestPixelSize(options.sizes) ?? getNumericDimension(options.width)
154 const height = getNumericDimension(options.height)
155 const thumbnailWidth = width ? width * DEVICE_PIXEL_RATIO : undefined
156 const thumbnailHeight = height ? height * DEVICE_PIXEL_RATIO : undefined
157 const scale = shouldSharpenThumbnail(thumbnailWidth, thumbnailHeight) ? THUMBNAIL_SHARPNESS_SCALE : 1
158
159 return {
160 width: normalizeThumbnailDimension(thumbnailWidth, scale),
161 height: normalizeThumbnailDimension(thumbnailHeight, scale),
162 }
163 }
164
165 function resolveOssImageSrc(src: ImageProps['src'], options: ResolveOssImageSrcOptions) {
166 if (isStaticImageImport(src))
167 return src
168
169 if (isLocalOrRuntimeImageSrc(src))
170 return src
171
172 const normalizedSrc = getOssUrl(src)
173 if (shouldUseOriginalSrc(src, options))
174 return normalizedSrc
175
176 const { width, height } = getRequestedThumbnailDimensions(options)
177 if (!width && !height)
178 return normalizedSrc
179
180 return getOssThumbnailUrl(normalizedSrc, { width, height, quality: getNumericQuality(options.quality) })
181 }
182
183 export function OssImage({
184 src,
185 thumbnailSize,
186 thumbnailWidth,
187 thumbnailHeight,
188 disableOssThumbnail,
189 ...imageProps
190 }: OssImageProps) {
191 return (
192 <Image
193 {...imageProps}
194 src={resolveOssImageSrc(src, {
195 ...imageProps,
196 disableOssThumbnail,
197 thumbnailHeight,
198 thumbnailSize,
199 thumbnailWidth,
200 })}
201 />
202 )
203 }
204
205 export default OssImage
206 export { getImageProps } from 'next/image'
207 export type { ImageLoader, ImageLoaderProps, ImageProps, StaticImageData }
208
208 lines Plain Text