feat: support png, gif, webp (#7947)
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions

Co-authored-by: xuanson9699 <84961581+xuanson9699@users.noreply.github.com>
This commit is contained in:
Nam Vu 2024-11-06 08:05:05 +07:00 committed by GitHub
parent fb656d480e
commit ac0fed6402
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 12 deletions

View File

@ -8,18 +8,22 @@ import classNames from 'classnames'
import { ImagePlus } from '../icons/src/vender/line/images' import { ImagePlus } from '../icons/src/vender/line/images'
import { useDraggableUploader } from './hooks' import { useDraggableUploader } from './hooks'
import { checkIsAnimatedImage } from './utils'
import { ALLOW_FILE_EXTENSIONS } from '@/types/app' import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
type UploaderProps = { type UploaderProps = {
className?: string className?: string
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
onUpload?: (file?: File) => void
} }
const Uploader: FC<UploaderProps> = ({ const Uploader: FC<UploaderProps> = ({
className, className,
onImageCropped, onImageCropped,
onUpload,
}) => { }) => {
const [inputImage, setInputImage] = useState<{ file: File; url: string }>() const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
return () => { return () => {
if (inputImage) if (inputImage)
@ -34,12 +38,19 @@ const Uploader: FC<UploaderProps> = ({
if (!inputImage) if (!inputImage)
return return
onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
onUpload?.(undefined)
} }
const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => { const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
if (file) if (file) {
setInputImage({ file, url: URL.createObjectURL(file) }) setInputImage({ file, url: URL.createObjectURL(file) })
checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage)
onUpload?.(file)
})
}
} }
const { const {
@ -52,6 +63,26 @@ const Uploader: FC<UploaderProps> = ({
const inputRef = createRef<HTMLInputElement>() const inputRef = createRef<HTMLInputElement>()
const handleShowImage = () => {
if (isAnimatedImage) {
return (
<img src={inputImage?.url} alt='' />
)
}
return (
<Cropper
image={inputImage?.url}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
)
}
return ( return (
<div className={classNames(className, 'w-full px-3 py-1.5')}> <div className={classNames(className, 'w-full px-3 py-1.5')}>
<div <div
@ -79,15 +110,7 @@ const Uploader: FC<UploaderProps> = ({
</div> </div>
<div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</div> <div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</div>
</> </>
: <Cropper : handleShowImage()
image={inputImage.url}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
} }
</div> </div>
</div> </div>

View File

@ -74,6 +74,11 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
setImageCropInfo({ tempUrl, croppedAreaPixels, fileName }) setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
} }
const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
const handleUpload = async (file?: File) => {
setUploadImageInfo({ file })
}
const handleSelect = async () => { const handleSelect = async () => {
if (activeTab === 'emoji') { if (activeTab === 'emoji') {
if (emoji) { if (emoji) {
@ -85,9 +90,13 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
} }
} }
else { else {
if (!imageCropInfo) if (!imageCropInfo && !uploadImageInfo)
return return
setUploading(true) setUploading(true)
if (imageCropInfo.file) {
handleLocalFileUpload(imageCropInfo.file)
return
}
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName) const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
const file = new File([blob], imageCropInfo.fileName, { type: blob.type }) const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
handleLocalFileUpload(file) handleLocalFileUpload(file)
@ -121,7 +130,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
<Divider className='m-0' /> <Divider className='m-0' />
<EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} /> <EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} /> <Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
<Divider className='m-0' /> <Divider className='m-0' />
<div className='w-full flex items-center justify-center p-3 gap-2'> <div className='w-full flex items-center justify-center p-3 gap-2'>

View File

@ -115,3 +115,52 @@ export default async function getCroppedImg(
}, mimeType) }, mimeType)
}) })
} }
export function checkIsAnimatedImage(file) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = function (e) {
const arr = new Uint8Array(e.target.result)
// Check file extension
const fileName = file.name.toLowerCase()
if (fileName.endsWith('.gif')) {
// If file is a GIF, assume it's animated
resolve(true)
}
// Check for WebP signature (RIFF and WEBP)
else if (isWebP(arr)) {
resolve(checkWebPAnimation(arr)) // Check if it's animated
}
else {
resolve(false) // Not a GIF or WebP
}
}
fileReader.onerror = function (err) {
reject(err) // Reject the promise on error
}
// Read the file as an array buffer
fileReader.readAsArrayBuffer(file)
})
}
// Function to check for WebP signature
function isWebP(arr) {
return (
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
) // "WEBP"
}
// Function to check if the WebP is animated (contains ANIM chunk)
function checkWebPAnimation(arr) {
// Search for the ANIM chunk in WebP to determine if it's animated
for (let i = 12; i < arr.length - 4; i++) {
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
return true // Found animation
}
return false // No animation chunk found
}