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 {useCallback, useEffect, useRef} from 'react'
|
||||||
import {Keyboard} from 'react-native'
|
import {Keyboard} from 'react-native'
|
||||||
|
import {File} from 'expo-file-system'
|
||||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {msg, plural} from '@lingui/macro'
|
import {msg, plural} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
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 {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||||
import * as toast from '#/components/Toast'
|
import * as toast from '#/components/Toast'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
import {isAnimatedGif} from './videos/isAnimatedGif'
|
||||||
|
|
||||||
export type SelectMediaButtonProps = {
|
export type SelectMediaButtonProps = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -128,7 +130,7 @@ const extensionToMimeType: Record<
|
|||||||
* `mimeType`. If `mimeType` is not available, we try to infer it through
|
* `mimeType`. If `mimeType` is not available, we try to infer it through
|
||||||
* various means.
|
* various means.
|
||||||
*/
|
*/
|
||||||
function classifyImagePickerAsset(asset: ImagePickerAsset):
|
async function classifyImagePickerAsset(asset: ImagePickerAsset): Promise<
|
||||||
| {
|
| {
|
||||||
success: true
|
success: true
|
||||||
type: AssetType
|
type: AssetType
|
||||||
@@ -138,7 +140,8 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
|
|||||||
success: false
|
success: false
|
||||||
type: undefined
|
type: undefined
|
||||||
mimeType: undefined
|
mimeType: undefined
|
||||||
} {
|
}
|
||||||
|
> {
|
||||||
/*
|
/*
|
||||||
* Try to use the `mimeType` reported by `expo-image-picker` first.
|
* Try to use the `mimeType` reported by `expo-image-picker` first.
|
||||||
*/
|
*/
|
||||||
@@ -178,7 +181,22 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
|
|||||||
*/
|
*/
|
||||||
let type: AssetType | undefined
|
let type: AssetType | undefined
|
||||||
if (mimeType === 'image/gif') {
|
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/')) {
|
} else if (mimeType?.startsWith('video/')) {
|
||||||
type = 'video'
|
type = 'video'
|
||||||
} else if (mimeType?.startsWith('image/')) {
|
} else if (mimeType?.startsWith('image/')) {
|
||||||
@@ -236,7 +254,7 @@ async function processImagePickerAssets(
|
|||||||
let supportedAssets: ValidatedImagePickerAsset[] = []
|
let supportedAssets: ValidatedImagePickerAsset[] = []
|
||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
const {success, type, mimeType} = classifyImagePickerAsset(asset)
|
const {success, type, mimeType} = await classifyImagePickerAsset(asset)
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errors.add(SelectedAssetError.Unsupported)
|
errors.add(SelectedAssetError.Unsupported)
|
||||||
@@ -469,7 +487,7 @@ export function SelectMediaButton({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoOpen && !hasAutoOpened.current && !disabled) {
|
if (autoOpen && !hasAutoOpened.current && !disabled) {
|
||||||
hasAutoOpened.current = true
|
hasAutoOpened.current = true
|
||||||
onPressSelectMedia()
|
void onPressSelectMedia()
|
||||||
}
|
}
|
||||||
}, [autoOpen, disabled, 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