Only treat animated gifs as videos, leave static gifs as images (#9814)
* only treat *animated* gifs as videos * fix export
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {useCallback, useEffect, useRef} from 'react'
|
||||
import {Keyboard} from 'react-native'
|
||||
import {File} from 'expo-file-system'
|
||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -18,6 +19,7 @@ import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||
import * as toast from '#/components/Toast'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {isAnimatedGif} from './videos/isAnimatedGif'
|
||||
|
||||
export type SelectMediaButtonProps = {
|
||||
disabled?: boolean
|
||||
@@ -128,7 +130,7 @@ const extensionToMimeType: Record<
|
||||
* `mimeType`. If `mimeType` is not available, we try to infer it through
|
||||
* various means.
|
||||
*/
|
||||
function classifyImagePickerAsset(asset: ImagePickerAsset):
|
||||
async function classifyImagePickerAsset(asset: ImagePickerAsset): Promise<
|
||||
| {
|
||||
success: true
|
||||
type: AssetType
|
||||
@@ -138,7 +140,8 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
|
||||
success: false
|
||||
type: undefined
|
||||
mimeType: undefined
|
||||
} {
|
||||
}
|
||||
> {
|
||||
/*
|
||||
* Try to use the `mimeType` reported by `expo-image-picker` first.
|
||||
*/
|
||||
@@ -178,7 +181,22 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
|
||||
*/
|
||||
let type: AssetType | undefined
|
||||
if (mimeType === 'image/gif') {
|
||||
type = 'gif'
|
||||
let bytes: ArrayBuffer | undefined
|
||||
if (IS_WEB) {
|
||||
bytes = await asset.file?.arrayBuffer()
|
||||
} else {
|
||||
const file = new File(asset.uri)
|
||||
if (file.exists) {
|
||||
bytes = await file.arrayBuffer()
|
||||
}
|
||||
}
|
||||
if (bytes) {
|
||||
const {isAnimated} = isAnimatedGif(bytes)
|
||||
type = isAnimated ? 'gif' : 'image'
|
||||
} else {
|
||||
// If we can't read the file, assume it's animated
|
||||
type = 'gif'
|
||||
}
|
||||
} else if (mimeType?.startsWith('video/')) {
|
||||
type = 'video'
|
||||
} else if (mimeType?.startsWith('image/')) {
|
||||
@@ -236,7 +254,7 @@ async function processImagePickerAssets(
|
||||
let supportedAssets: ValidatedImagePickerAsset[] = []
|
||||
|
||||
for (const asset of assets) {
|
||||
const {success, type, mimeType} = classifyImagePickerAsset(asset)
|
||||
const {success, type, mimeType} = await classifyImagePickerAsset(asset)
|
||||
|
||||
if (!success) {
|
||||
errors.add(SelectedAssetError.Unsupported)
|
||||
@@ -469,7 +487,7 @@ export function SelectMediaButton({
|
||||
useEffect(() => {
|
||||
if (autoOpen && !hasAutoOpened.current && !disabled) {
|
||||
hasAutoOpened.current = true
|
||||
onPressSelectMedia()
|
||||
void onPressSelectMedia()
|
||||
}
|
||||
}, [autoOpen, disabled, onPressSelectMedia])
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Checks if a GIF is animated. Cooked up by Claude, validated with some examples.
|
||||
* @param bytes - The GIF bytes, as a Uint8Array.
|
||||
* @returns An object with properties isGif, isAnimated, and frames.
|
||||
*/
|
||||
export function isAnimatedGif(buffer: ArrayBuffer): {
|
||||
isGif: boolean
|
||||
isAnimated: boolean
|
||||
frames: number
|
||||
} {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
// Verify GIF signature
|
||||
const sig = String.fromCharCode(...bytes.slice(0, 6))
|
||||
if (!sig.startsWith('GIF'))
|
||||
return {isGif: false, isAnimated: false, frames: 0}
|
||||
|
||||
let i = 13 // Skip header + logical screen descriptor
|
||||
|
||||
// Skip global color table if present
|
||||
if (bytes[10] & 0x80) {
|
||||
const gctSize = 3 * (1 << ((bytes[10] & 0x07) + 1))
|
||||
i += gctSize
|
||||
}
|
||||
|
||||
let frames = 0
|
||||
|
||||
while (i < bytes.length) {
|
||||
const block = bytes[i++]
|
||||
|
||||
if (block === 0x2c) {
|
||||
// Image descriptor
|
||||
frames++
|
||||
|
||||
// Skip image descriptor fields
|
||||
i += 8
|
||||
// Skip local color table if present
|
||||
if (bytes[i] & 0x80) {
|
||||
const lctSize = 3 * (1 << ((bytes[i] & 0x07) + 1))
|
||||
i += lctSize + 1
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
// Skip image data blocks
|
||||
i++ // LZW minimum code size
|
||||
while (bytes[i]) i += bytes[i] + 1
|
||||
i++
|
||||
} else if (block === 0x21) {
|
||||
// Extension
|
||||
i++ // Extension type
|
||||
while (bytes[i]) i += bytes[i] + 1
|
||||
i++
|
||||
} else if (block === 0x3b) {
|
||||
// Trailer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {isGif: true, isAnimated: frames > 1, frames}
|
||||
}
|
||||
Reference in New Issue
Block a user