Remove JPG hardcoding from image processing systems (#9955)

This commit is contained in:
Samuel Newman
2026-03-02 20:23:07 +00:00
committed by GitHub
parent d247a65683
commit c2fd87bd8b
4 changed files with 81 additions and 52 deletions
+12 -1
View File
@@ -1,10 +1,21 @@
import React from 'react'
function detectMime(buf: Buffer): string {
if (buf[0] === 0xff && buf[1] === 0xd8) return 'image/jpeg'
if (buf[0] === 0x89 && buf[1] === 0x50) return 'image/png'
if (buf[0] === 0x52 && buf[1] === 0x49) return 'image/webp'
if (buf[0] === 0x47 && buf[1] === 0x49) return 'image/gif'
return 'image/jpeg'
}
export function Img(
props: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
) {
const {src, ...others} = props
return (
<img {...others} src={`data:image/jpeg;base64,${src.toString('base64')}`} />
<img
{...others}
src={`data:${detectMime(src)};base64,${src.toString('base64')}`}
/>
)
}
+2
View File
@@ -34,6 +34,7 @@ jest.mock('react-native-safe-area-context', () => {
jest.mock('expo-file-system/legacy', () => ({
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
deleteAsync: jest.fn(),
moveAsync: jest.fn().mockResolvedValue(undefined),
createDownloadResumable: jest.fn(),
}))
@@ -43,6 +44,7 @@ jest.mock('expo-image-manipulator', () => ({
}),
SaveFormat: {
JPEG: 'jpeg',
WEBP: 'webp',
},
}))
+40 -31
View File
@@ -8,6 +8,7 @@ import {
EncodingType,
getInfoAsync,
makeDirectoryAsync,
moveAsync,
StorageAccessFramework,
writeAsStringAsync,
} from 'expo-file-system/legacy'
@@ -56,25 +57,19 @@ export interface DownloadAndResizeOpts {
}
export async function downloadAndResize(opts: DownloadAndResizeOpts) {
let appendExt = 'jpeg'
try {
const urip = new URL(opts.uri)
const ext = urip.pathname.split('.').pop()
if (ext === 'png') {
appendExt = 'png'
}
new URL(opts.uri)
} catch (e: any) {
console.error('Invalid URI', opts.uri, e)
return
}
const path = createPath(appendExt)
const path = await downloadImage(opts.uri, String(uuid.v4()), opts.timeout)
try {
await downloadImage(opts.uri, path, opts.timeout)
return await doResize(path, opts)
} finally {
safeDeleteAsync(path)
void safeDeleteAsync(path)
}
}
@@ -84,11 +79,13 @@ export async function shareImageModal({uri}: {uri: string}) {
return
}
// we're currently relying on the fact our CDN only serves jpegs
// -prf
const imageUri = await downloadImage(uri, createPath('jpg'), 15e3)
const imagePath = await moveToPermanentPath(imageUri, '.jpg')
safeDeleteAsync(imageUri)
const downloadedPath = await downloadImage(uri, String(uuid.v4()), 15e3)
const {uri: jpegUri} = await manipulateAsync(downloadedPath, [], {
format: SaveFormat.JPEG,
compress: 1.0,
})
void safeDeleteAsync(downloadedPath)
const imagePath = await moveToPermanentPath(jpegUri, '.jpg')
await Sharing.shareAsync(imagePath, {
mimeType: 'image/jpeg',
UTI: 'image/jpeg',
@@ -98,13 +95,13 @@ export async function shareImageModal({uri}: {uri: string}) {
const ALBUM_NAME = 'Bluesky'
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// download the file to cache
// NOTE
// assuming JPEG
// we're currently relying on the fact our CDN only serves jpegs
// -prf
const imageUri = await downloadImage(uri, createPath('jpg'), 15e3)
const imagePath = await moveToPermanentPath(imageUri, '.jpg')
const downloadedPath = await downloadImage(uri, String(uuid.v4()), 15e3)
const {uri: jpegUri} = await manipulateAsync(downloadedPath, [], {
format: SaveFormat.JPEG,
compress: 1.0,
})
void safeDeleteAsync(downloadedPath)
const imagePath = await moveToPermanentPath(jpegUri, '.jpg')
// save
try {
@@ -402,18 +399,15 @@ export function getResizedDimensions(originalDims: {
}
}
function createPath(ext: string) {
// cacheDirectory will never be null on native, so the null check here is not necessary except for typescript.
// we use a web-only function for downloadAndResize on web
return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}`
}
async function downloadImage(uri: string, path: string, timeout: number) {
const dlResumable = createDownloadResumable(uri, path, {cache: true})
async function downloadImage(uri: string, destName: string, timeout: number) {
// Download to a temp path first, then rename with the correct extension
// based on the response's mimeType.
const tempPath = `${cacheDirectory ?? ''}/${destName}.bin`
const dlResumable = createDownloadResumable(uri, tempPath, {cache: true})
let timedOut = false
const to1 = setTimeout(() => {
timedOut = true
dlResumable.cancelAsync()
void dlResumable.cancelAsync()
}, timeout)
const dlRes = await dlResumable.downloadAsync()
@@ -427,5 +421,20 @@ async function downloadImage(uri: string, path: string, timeout: number) {
}
}
return normalizePath(dlRes.uri)
const ext = extFromMime(dlRes.mimeType)
const finalPath = `${cacheDirectory ?? ''}/${destName}.${ext}`
await moveAsync({from: dlRes.uri, to: finalPath})
return normalizePath(finalPath)
}
const MIME_TO_EXT: Record<string, string> = {
'image/jpeg': 'jpg',
'image/webp': 'webp',
'image/png': 'png',
'image/gif': 'gif',
}
function extFromMime(mimeType?: string | null): string {
return (mimeType && MIME_TO_EXT[mimeType]) || 'jpg'
}
+27 -20
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {View} from 'react-native'
import {Image as ExpoImage} from 'expo-image'
import {ImageManipulator, SaveFormat} from 'expo-image-manipulator'
import {
type ImagePickerOptions,
launchImageLibraryAsync,
@@ -107,27 +108,33 @@ export function StepProfile() {
}),
)
return (response.assets ?? [])
.slice(0, 1)
.filter(asset => {
if (
!asset.mimeType?.startsWith('image/') ||
(!asset.mimeType?.endsWith('jpeg') &&
!asset.mimeType?.endsWith('jpg') &&
!asset.mimeType?.endsWith('png'))
) {
setError(_(msg`Only .jpg and .png files are supported`))
return false
}
return true
const asset = (response.assets ?? [])[0]
if (!asset) return []
try {
const context = ImageManipulator.manipulate(asset.uri)
const rendered = await context.renderAsync()
const result = await rendered.saveAsync({
format: SaveFormat.JPEG,
compress: 1.0,
})
.map(image => ({
mime: 'image/jpeg',
height: image.height,
width: image.width,
path: image.uri,
size: getDataUriSize(image.uri),
}))
return [
{
mime: 'image/jpeg',
height: rendered.height,
width: rendered.width,
path: result.uri,
size: getDataUriSize(result.uri),
},
]
} catch {
setError(
_(
msg`This image could not be used. Try a different format like .jpg or .png.`,
),
)
return []
}
},
[_, setError, sheetWrapper],
)