mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 03:32:23 +08:00
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
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:
parent
fb656d480e
commit
ac0fed6402
|
@ -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>
|
||||||
|
|
|
@ -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'>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user