Remove JPG hardcoding from image processing systems (#9955)
This commit is contained in:
@@ -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')}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user