Skip to content

Commit

Permalink
feat: support png, gif, webp (langgenius#7947)
Browse files Browse the repository at this point in the history
Co-authored-by: xuanson9699 <[email protected]>
  • Loading branch information
2 people authored and JunXu01 committed Nov 9, 2024
1 parent 92b5ca1 commit 9333c6f
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 12 deletions.
43 changes: 33 additions & 10 deletions web/app/components/base/app-icon-picker/Uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ import classNames from 'classnames'

import { ImagePlus } from '../icons/src/vender/line/images'
import { useDraggableUploader } from './hooks'
import { checkIsAnimatedImage } from './utils'
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'

type UploaderProps = {
className?: string
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
onUpload?: (file?: File) => void
}

const Uploader: FC<UploaderProps> = ({
className,
onImageCropped,
onUpload,
}) => {
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
useEffect(() => {
return () => {
if (inputImage)
Expand All @@ -34,12 +38,19 @@ const Uploader: FC<UploaderProps> = ({
if (!inputImage)
return
onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
onUpload?.(undefined)
}

const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file)
if (file) {
setInputImage({ file, url: URL.createObjectURL(file) })
checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage)
onUpload?.(file)
})
}
}

const {
Expand All @@ -52,6 +63,26 @@ const Uploader: FC<UploaderProps> = ({

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 (
<div className={classNames(className, 'w-full px-3 py-1.5')}>
<div
Expand Down Expand Up @@ -79,15 +110,7 @@ const Uploader: FC<UploaderProps> = ({
</div>
<div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</div>
</>
: <Cropper
image={inputImage.url}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
: handleShowImage()
}
</div>
</div>
Expand Down
13 changes: 11 additions & 2 deletions web/app/components/base/app-icon-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
}

const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
const handleUpload = async (file?: File) => {
setUploadImageInfo({ file })
}

const handleSelect = async () => {
if (activeTab === 'emoji') {
if (emoji) {
Expand All @@ -85,9 +90,13 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
}
}
else {
if (!imageCropInfo)
if (!imageCropInfo && !uploadImageInfo)
return
setUploading(true)
if (imageCropInfo.file) {
handleLocalFileUpload(imageCropInfo.file)
return
}
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
handleLocalFileUpload(file)
Expand Down Expand Up @@ -121,7 +130,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
<Divider className='m-0' />

<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' />
<div className='w-full flex items-center justify-center p-3 gap-2'>
Expand Down
49 changes: 49 additions & 0 deletions web/app/components/base/app-icon-picker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,52 @@ export default async function getCroppedImg(
}, 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
}

0 comments on commit 9333c6f

Please sign in to comment.