un-hardcoded jpg, switch to webp in a few cases
This commit is contained in:
@@ -12,7 +12,7 @@ const mockResizedImage = {
|
||||
size: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
mime: 'image/jpeg',
|
||||
mime: 'image/webp',
|
||||
}
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
@@ -63,7 +63,7 @@ describe('downloadAndResize', () => {
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[{resize: {height: opts.height, width: opts.width}}],
|
||||
{format: SaveFormat.JPEG, compress: 1.0},
|
||||
{format: SaveFormat.WEBP, compress: 1.0},
|
||||
)
|
||||
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
|
||||
idempotent: true,
|
||||
|
||||
@@ -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')}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ jest.mock('expo-image-manipulator', () => ({
|
||||
}),
|
||||
SaveFormat: {
|
||||
JPEG: 'jpeg',
|
||||
WEBP: 'webp',
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
+46
-36
@@ -8,6 +8,7 @@ import {
|
||||
EncodingType,
|
||||
getInfoAsync,
|
||||
makeDirectoryAsync,
|
||||
moveAsync,
|
||||
StorageAccessFramework,
|
||||
writeAsStringAsync,
|
||||
} from 'expo-file-system/legacy'
|
||||
@@ -37,7 +38,7 @@ export async function compressIfNeeded(
|
||||
})
|
||||
const finalImageMovedPath = await moveToPermanentPath(
|
||||
resizedImage.path,
|
||||
'.jpg',
|
||||
'.webp',
|
||||
)
|
||||
const finalImg = {
|
||||
...resizedImage,
|
||||
@@ -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,27 +79,17 @@ 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 imagePath = await downloadImage(uri, String(uuid.v4()), 15e3)
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/jpeg',
|
||||
UTI: 'image/jpeg',
|
||||
mimeType: mimeFromExt(imagePath),
|
||||
UTI: mimeFromExt(imagePath),
|
||||
})
|
||||
}
|
||||
|
||||
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 imagePath = await downloadImage(uri, String(uuid.v4()), 15e3)
|
||||
|
||||
// save
|
||||
try {
|
||||
@@ -219,7 +204,7 @@ async function doResize(
|
||||
localUri,
|
||||
[{resize: newDimensions}],
|
||||
{
|
||||
format: SaveFormat.JPEG,
|
||||
format: SaveFormat.WEBP,
|
||||
compress: qualityPercentage / 100,
|
||||
},
|
||||
)
|
||||
@@ -237,7 +222,7 @@ async function doResize(
|
||||
minQualityPercentage = qualityPercentage
|
||||
newDataUri = {
|
||||
path: normalizePath(resizeRes.uri),
|
||||
mime: 'image/jpeg',
|
||||
mime: 'image/webp',
|
||||
size: fileInfo.size,
|
||||
width: resizeRes.width,
|
||||
height: resizeRes.height,
|
||||
@@ -402,18 +387,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 +409,33 @@ 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'
|
||||
}
|
||||
|
||||
const EXT_TO_MIME: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
webp: 'image/webp',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
}
|
||||
|
||||
function mimeFromExt(path: string): string {
|
||||
const ext = path.split('.').pop()
|
||||
return (ext && EXT_TO_MIME[ext]) || 'image/jpeg'
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ async function doResize(
|
||||
}
|
||||
return {
|
||||
path: newDataUri,
|
||||
mime: 'image/jpeg',
|
||||
mime: 'image/webp',
|
||||
size: getDataUriSize(newDataUri),
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
@@ -146,7 +146,7 @@ function createResizedImage(
|
||||
canvas.height = h
|
||||
|
||||
ctx.drawImage(img, 0, 0, w, h)
|
||||
resolve(canvas.toDataURL('image/jpeg', quality))
|
||||
resolve(canvas.toDataURL('image/webp', quality))
|
||||
})
|
||||
img.addEventListener('error', ev => {
|
||||
reject(ev.error)
|
||||
|
||||
@@ -64,7 +64,8 @@ export async function openCamera(): Promise<PickerImage> {
|
||||
export async function openCropper(opts: OpenCropperOptions) {
|
||||
const item = await ExpoImageCropTool.openCropperAsync({
|
||||
...opts,
|
||||
format: 'jpeg',
|
||||
// @ts-expect-error update @bsky.app/expo-image-crop-tool to pick up webp support
|
||||
format: 'webp',
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -37,7 +37,8 @@ export async function openCropper(opts: OpenCropperOptions) {
|
||||
doneButtonText: t`Done`,
|
||||
cancelButtonText: t`Cancel`,
|
||||
...opts,
|
||||
format: 'jpeg',
|
||||
// @ts-expect-error update @bsky.app/expo-image-crop-tool to pick up webp support
|
||||
format: 'webp',
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -52,7 +52,7 @@ export const PlaceholderCanvas = React.forwardRef<PlaceholderCanvasRef, {}>(
|
||||
ref={viewshotRef}
|
||||
options={{
|
||||
fileName: 'placeholderAvatar',
|
||||
format: 'jpg',
|
||||
format: 'webp',
|
||||
quality: 0.8,
|
||||
height: 150 * SIZE_MULTIPLIER,
|
||||
width: 150 * SIZE_MULTIPLIER,
|
||||
|
||||
@@ -114,15 +114,16 @@ export function StepProfile() {
|
||||
!asset.mimeType?.startsWith('image/') ||
|
||||
(!asset.mimeType?.endsWith('jpeg') &&
|
||||
!asset.mimeType?.endsWith('jpg') &&
|
||||
!asset.mimeType?.endsWith('png'))
|
||||
!asset.mimeType?.endsWith('png') &&
|
||||
!asset.mimeType?.endsWith('webp'))
|
||||
) {
|
||||
setError(_(msg`Only .jpg and .png files are supported`))
|
||||
setError(_(msg`Only .jpg, .png, and .webp files are supported`))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map(image => ({
|
||||
mime: 'image/jpeg',
|
||||
mime: image.mimeType || 'image/jpeg',
|
||||
height: image.height,
|
||||
width: image.width,
|
||||
path: image.uri,
|
||||
|
||||
@@ -219,7 +219,7 @@ export async function compressImage(img: ComposerImage): Promise<PickerImage> {
|
||||
[{resize: {width: w, height: h}}],
|
||||
{
|
||||
compress: qualityPercentage / 100,
|
||||
format: SaveFormat.JPEG,
|
||||
format: SaveFormat.WEBP,
|
||||
base64: true,
|
||||
},
|
||||
)
|
||||
@@ -232,7 +232,7 @@ export async function compressImage(img: ComposerImage): Promise<PickerImage> {
|
||||
path: await moveIfNecessary(res.uri),
|
||||
width: res.width,
|
||||
height: res.height,
|
||||
mime: 'image/jpeg',
|
||||
mime: 'image/webp',
|
||||
size,
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user