un-hardcoded jpg, switch to webp in a few cases
This commit is contained in:
@@ -12,7 +12,7 @@ const mockResizedImage = {
|
|||||||
size: 100,
|
size: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
mime: 'image/jpeg',
|
mime: 'image/webp',
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('downloadAndResize', () => {
|
describe('downloadAndResize', () => {
|
||||||
@@ -63,7 +63,7 @@ describe('downloadAndResize', () => {
|
|||||||
expect(manipulateAsync).toHaveBeenCalledWith(
|
expect(manipulateAsync).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[{resize: {height: opts.height, width: opts.width}}],
|
[{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), {
|
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
|
||||||
idempotent: true,
|
idempotent: true,
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import React from 'react'
|
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(
|
export function Img(
|
||||||
props: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
|
props: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
|
||||||
) {
|
) {
|
||||||
const {src, ...others} = props
|
const {src, ...others} = props
|
||||||
return (
|
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: {
|
SaveFormat: {
|
||||||
JPEG: 'jpeg',
|
JPEG: 'jpeg',
|
||||||
|
WEBP: 'webp',
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
+46
-36
@@ -8,6 +8,7 @@ import {
|
|||||||
EncodingType,
|
EncodingType,
|
||||||
getInfoAsync,
|
getInfoAsync,
|
||||||
makeDirectoryAsync,
|
makeDirectoryAsync,
|
||||||
|
moveAsync,
|
||||||
StorageAccessFramework,
|
StorageAccessFramework,
|
||||||
writeAsStringAsync,
|
writeAsStringAsync,
|
||||||
} from 'expo-file-system/legacy'
|
} from 'expo-file-system/legacy'
|
||||||
@@ -37,7 +38,7 @@ export async function compressIfNeeded(
|
|||||||
})
|
})
|
||||||
const finalImageMovedPath = await moveToPermanentPath(
|
const finalImageMovedPath = await moveToPermanentPath(
|
||||||
resizedImage.path,
|
resizedImage.path,
|
||||||
'.jpg',
|
'.webp',
|
||||||
)
|
)
|
||||||
const finalImg = {
|
const finalImg = {
|
||||||
...resizedImage,
|
...resizedImage,
|
||||||
@@ -56,25 +57,19 @@ export interface DownloadAndResizeOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||||
let appendExt = 'jpeg'
|
|
||||||
try {
|
try {
|
||||||
const urip = new URL(opts.uri)
|
new URL(opts.uri)
|
||||||
const ext = urip.pathname.split('.').pop()
|
|
||||||
if (ext === 'png') {
|
|
||||||
appendExt = 'png'
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Invalid URI', opts.uri, e)
|
console.error('Invalid URI', opts.uri, e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = createPath(appendExt)
|
const path = await downloadImage(opts.uri, String(uuid.v4()), opts.timeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadImage(opts.uri, path, opts.timeout)
|
|
||||||
return await doResize(path, opts)
|
return await doResize(path, opts)
|
||||||
} finally {
|
} finally {
|
||||||
safeDeleteAsync(path)
|
void safeDeleteAsync(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,27 +79,17 @@ export async function shareImageModal({uri}: {uri: string}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// we're currently relying on the fact our CDN only serves jpegs
|
const imagePath = await downloadImage(uri, String(uuid.v4()), 15e3)
|
||||||
// -prf
|
|
||||||
const imageUri = await downloadImage(uri, createPath('jpg'), 15e3)
|
|
||||||
const imagePath = await moveToPermanentPath(imageUri, '.jpg')
|
|
||||||
safeDeleteAsync(imageUri)
|
|
||||||
await Sharing.shareAsync(imagePath, {
|
await Sharing.shareAsync(imagePath, {
|
||||||
mimeType: 'image/jpeg',
|
mimeType: mimeFromExt(imagePath),
|
||||||
UTI: 'image/jpeg',
|
UTI: mimeFromExt(imagePath),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALBUM_NAME = 'Bluesky'
|
const ALBUM_NAME = 'Bluesky'
|
||||||
|
|
||||||
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||||
// download the file to cache
|
const imagePath = await downloadImage(uri, String(uuid.v4()), 15e3)
|
||||||
// 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')
|
|
||||||
|
|
||||||
// save
|
// save
|
||||||
try {
|
try {
|
||||||
@@ -219,7 +204,7 @@ async function doResize(
|
|||||||
localUri,
|
localUri,
|
||||||
[{resize: newDimensions}],
|
[{resize: newDimensions}],
|
||||||
{
|
{
|
||||||
format: SaveFormat.JPEG,
|
format: SaveFormat.WEBP,
|
||||||
compress: qualityPercentage / 100,
|
compress: qualityPercentage / 100,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -237,7 +222,7 @@ async function doResize(
|
|||||||
minQualityPercentage = qualityPercentage
|
minQualityPercentage = qualityPercentage
|
||||||
newDataUri = {
|
newDataUri = {
|
||||||
path: normalizePath(resizeRes.uri),
|
path: normalizePath(resizeRes.uri),
|
||||||
mime: 'image/jpeg',
|
mime: 'image/webp',
|
||||||
size: fileInfo.size,
|
size: fileInfo.size,
|
||||||
width: resizeRes.width,
|
width: resizeRes.width,
|
||||||
height: resizeRes.height,
|
height: resizeRes.height,
|
||||||
@@ -402,18 +387,15 @@ export function getResizedDimensions(originalDims: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPath(ext: string) {
|
async function downloadImage(uri: string, destName: string, timeout: number) {
|
||||||
// cacheDirectory will never be null on native, so the null check here is not necessary except for typescript.
|
// Download to a temp path first, then rename with the correct extension
|
||||||
// we use a web-only function for downloadAndResize on web
|
// based on the response's mimeType.
|
||||||
return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}`
|
const tempPath = `${cacheDirectory ?? ''}/${destName}.bin`
|
||||||
}
|
const dlResumable = createDownloadResumable(uri, tempPath, {cache: true})
|
||||||
|
|
||||||
async function downloadImage(uri: string, path: string, timeout: number) {
|
|
||||||
const dlResumable = createDownloadResumable(uri, path, {cache: true})
|
|
||||||
let timedOut = false
|
let timedOut = false
|
||||||
const to1 = setTimeout(() => {
|
const to1 = setTimeout(() => {
|
||||||
timedOut = true
|
timedOut = true
|
||||||
dlResumable.cancelAsync()
|
void dlResumable.cancelAsync()
|
||||||
}, timeout)
|
}, timeout)
|
||||||
|
|
||||||
const dlRes = await dlResumable.downloadAsync()
|
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 {
|
return {
|
||||||
path: newDataUri,
|
path: newDataUri,
|
||||||
mime: 'image/jpeg',
|
mime: 'image/webp',
|
||||||
size: getDataUriSize(newDataUri),
|
size: getDataUriSize(newDataUri),
|
||||||
width: opts.width,
|
width: opts.width,
|
||||||
height: opts.height,
|
height: opts.height,
|
||||||
@@ -146,7 +146,7 @@ function createResizedImage(
|
|||||||
canvas.height = h
|
canvas.height = h
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0, w, h)
|
ctx.drawImage(img, 0, 0, w, h)
|
||||||
resolve(canvas.toDataURL('image/jpeg', quality))
|
resolve(canvas.toDataURL('image/webp', quality))
|
||||||
})
|
})
|
||||||
img.addEventListener('error', ev => {
|
img.addEventListener('error', ev => {
|
||||||
reject(ev.error)
|
reject(ev.error)
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export async function openCamera(): Promise<PickerImage> {
|
|||||||
export async function openCropper(opts: OpenCropperOptions) {
|
export async function openCropper(opts: OpenCropperOptions) {
|
||||||
const item = await ExpoImageCropTool.openCropperAsync({
|
const item = await ExpoImageCropTool.openCropperAsync({
|
||||||
...opts,
|
...opts,
|
||||||
format: 'jpeg',
|
// @ts-expect-error update @bsky.app/expo-image-crop-tool to pick up webp support
|
||||||
|
format: 'webp',
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export async function openCropper(opts: OpenCropperOptions) {
|
|||||||
doneButtonText: t`Done`,
|
doneButtonText: t`Done`,
|
||||||
cancelButtonText: t`Cancel`,
|
cancelButtonText: t`Cancel`,
|
||||||
...opts,
|
...opts,
|
||||||
format: 'jpeg',
|
// @ts-expect-error update @bsky.app/expo-image-crop-tool to pick up webp support
|
||||||
|
format: 'webp',
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const PlaceholderCanvas = React.forwardRef<PlaceholderCanvasRef, {}>(
|
|||||||
ref={viewshotRef}
|
ref={viewshotRef}
|
||||||
options={{
|
options={{
|
||||||
fileName: 'placeholderAvatar',
|
fileName: 'placeholderAvatar',
|
||||||
format: 'jpg',
|
format: 'webp',
|
||||||
quality: 0.8,
|
quality: 0.8,
|
||||||
height: 150 * SIZE_MULTIPLIER,
|
height: 150 * SIZE_MULTIPLIER,
|
||||||
width: 150 * SIZE_MULTIPLIER,
|
width: 150 * SIZE_MULTIPLIER,
|
||||||
|
|||||||
@@ -114,15 +114,16 @@ export function StepProfile() {
|
|||||||
!asset.mimeType?.startsWith('image/') ||
|
!asset.mimeType?.startsWith('image/') ||
|
||||||
(!asset.mimeType?.endsWith('jpeg') &&
|
(!asset.mimeType?.endsWith('jpeg') &&
|
||||||
!asset.mimeType?.endsWith('jpg') &&
|
!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 false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.map(image => ({
|
.map(image => ({
|
||||||
mime: 'image/jpeg',
|
mime: image.mimeType || 'image/jpeg',
|
||||||
height: image.height,
|
height: image.height,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
path: image.uri,
|
path: image.uri,
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export async function compressImage(img: ComposerImage): Promise<PickerImage> {
|
|||||||
[{resize: {width: w, height: h}}],
|
[{resize: {width: w, height: h}}],
|
||||||
{
|
{
|
||||||
compress: qualityPercentage / 100,
|
compress: qualityPercentage / 100,
|
||||||
format: SaveFormat.JPEG,
|
format: SaveFormat.WEBP,
|
||||||
base64: true,
|
base64: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -232,7 +232,7 @@ export async function compressImage(img: ComposerImage): Promise<PickerImage> {
|
|||||||
path: await moveIfNecessary(res.uri),
|
path: await moveIfNecessary(res.uri),
|
||||||
width: res.width,
|
width: res.width,
|
||||||
height: res.height,
|
height: res.height,
|
||||||
mime: 'image/jpeg',
|
mime: 'image/webp',
|
||||||
size,
|
size,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user