Skip to content

Commit

Permalink
fix: check is animated image uploader (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
xuanson9699 authored Sep 25, 2024
1 parent 2e3a566 commit 3c1c3b0
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 10 deletions.
13 changes: 9 additions & 4 deletions web/app/components/base/app-icon-picker/Uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import classNames from 'classnames'

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

type UploaderProps = {
Expand All @@ -20,6 +20,7 @@ const Uploader: FC<UploaderProps> = ({
onImageCropped,
}) => {
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
useEffect(() => {
return () => {
if (inputImage)
Expand All @@ -40,8 +41,11 @@ const Uploader: FC<UploaderProps> = ({
const file = e.target.files?.[0]
if (file) {
setInputImage({ file, url: URL.createObjectURL(file) })
if (isAnimatedImage(file))
onImageCropped?.(URL.createObjectURL(file), { x: 0, y: 0, width: 0, height: 0 }, file.name, file)
checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage)
onImageCropped?.(URL.createObjectURL(file), { x: 0, y: 0, width: 0, height: 0 }, file.name, file)
})
}
}

Expand All @@ -56,11 +60,12 @@ const Uploader: FC<UploaderProps> = ({
const inputRef = createRef<HTMLInputElement>()

const handleShowImage = () => {
if (isAnimatedImage(inputImage?.file || { name: '' })) {
if (isAnimatedImage) {
return (
<img src={inputImage?.url} alt='' />
)
}

return (
<Cropper
image={inputImage?.url}
Expand Down
6 changes: 3 additions & 3 deletions web/app/components/base/app-icon-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useLocalFileUploader } from '../image-uploader/hooks'
import EmojiPickerInner from '../emoji-picker/Inner'
import Uploader from './Uploader'
import s from './style.module.css'
import getCroppedImg, { isAnimatedImage } from './utils'
import getCroppedImg from './utils'
import type { AppIconType, ImageFile } from '@/types/app'
import cn from '@/utils/classnames'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
Expand Down Expand Up @@ -88,11 +88,11 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
if (!imageCropInfo)
return
setUploading(true)
if (isAnimatedImage({ name: imageCropInfo.fileName }) && imageCropInfo.file) {
if (imageCropInfo.file) {
handleLocalFileUpload(imageCropInfo.file)
return
}
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels)
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
handleLocalFileUpload(file)
}
Expand Down
70 changes: 67 additions & 3 deletions web/app/components/base/app-icon-picker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ export function getRadianAngle(degreeValue: number) {
return (degreeValue * Math.PI) / 180
}

export function getMimeType(fileName: string): string {
const extension = fileName.split('.').pop()?.toLowerCase()
switch (extension) {
case 'png':
return 'image/png'
case 'jpg':
case 'jpeg':
return 'image/jpeg'
case 'gif':
return 'image/gif'
case 'webp':
return 'image/webp'
default:
return 'image/jpeg'
}
}

/**
* Returns the new bounding area of a rotated rectangle.
*/
Expand All @@ -31,12 +48,14 @@ export function rotateSize(width: number, height: number, rotation: number) {
export default async function getCroppedImg(
imageSrc: string,
pixelCrop: { x: number; y: number; width: number; height: number },
fileName: string,
rotation = 0,
flip = { horizontal: false, vertical: false },
): Promise<Blob> {
const image = await createImage(imageSrc)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const mimeType = getMimeType(fileName)

if (!ctx)
throw new Error('Could not create a canvas context')
Expand Down Expand Up @@ -93,10 +112,55 @@ export default async function getCroppedImg(
resolve(file)
else
reject(new Error('Could not create a blob'))
}, 'image/png')
}, 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)
})
}

export function isAnimatedImage(file: { name: string }) {
return (file?.name?.endsWith('.webp') || file?.name?.endsWith('.gif'))
// 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 3c1c3b0

Please sign in to comment.