Start with highest quality compression and find a suitable size (#33)

zio/stable
Paul Frazee 2022-12-26 12:01:40 -06:00 committed by GitHub
parent 8652b74a38
commit 838fc601c1
7 changed files with 108 additions and 48 deletions

View File

@ -1,4 +1,4 @@
import {downloadAndResize, DownloadAndResizeOpts} from '../../src/lib/download' import {downloadAndResize, DownloadAndResizeOpts} from '../../src/lib/images'
import ImageResizer from '@bam.tech/react-native-image-resizer' import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob' import RNFetchBlob from 'rn-fetch-blob'
@ -16,6 +16,7 @@ describe('downloadAndResize', () => {
const mockResizedImage = { const mockResizedImage = {
path: jest.fn().mockReturnValue('file://resized-image.jpg'), path: jest.fn().mockReturnValue('file://resized-image.jpg'),
size: 100,
} }
beforeEach(() => { beforeEach(() => {
@ -37,6 +38,7 @@ describe('downloadAndResize', () => {
uri: 'https://example.com/image.jpg', uri: 'https://example.com/image.jpg',
width: 100, width: 100,
height: 100, height: 100,
maxSize: 500000,
mode: 'cover', mode: 'cover',
timeout: 10000, timeout: 10000,
} }
@ -56,7 +58,7 @@ describe('downloadAndResize', () => {
100, 100,
100, 100,
'JPEG', 'JPEG',
0.7, 1,
undefined, undefined,
undefined, undefined,
undefined, undefined,
@ -69,6 +71,7 @@ describe('downloadAndResize', () => {
uri: 'invalid-uri', uri: 'invalid-uri',
width: 100, width: 100,
height: 100, height: 100,
maxSize: 500000,
mode: 'cover', mode: 'cover',
timeout: 10000, timeout: 10000,
} }
@ -83,6 +86,7 @@ describe('downloadAndResize', () => {
uri: 'https://example.com/image.bmp', uri: 'https://example.com/image.bmp',
width: 100, width: 100,
height: 100, height: 100,
maxSize: 500000,
mode: 'cover', mode: 'cover',
timeout: 10000, timeout: 10000,
} }

View File

@ -1,11 +1,13 @@
import RNFetchBlob from 'rn-fetch-blob' import RNFetchBlob from 'rn-fetch-blob'
import ImageResizer from '@bam.tech/react-native-image-resizer' import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Image as PickedImage} from 'react-native-image-crop-picker'
export interface DownloadAndResizeOpts { export interface DownloadAndResizeOpts {
uri: string uri: string
width: number width: number
height: number height: number
mode: 'contain' | 'cover' | 'stretch' mode: 'contain' | 'cover' | 'stretch'
maxSize: number
timeout: number timeout: number
} }
@ -41,21 +43,55 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
localUri = `file://${localUri}` localUri = `file://${localUri}`
} }
const resizeRes = await ImageResizer.createResizedImage( return await resize(localUri, opts)
localUri,
opts.width,
opts.height,
'JPEG',
0.7,
undefined,
undefined,
undefined,
{mode: opts.mode},
)
return resizeRes
} finally { } finally {
if (downloadRes) { if (downloadRes) {
downloadRes.flush() downloadRes.flush()
} }
} }
} }
export interface ResizeOpts {
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxSize: number
}
export async function resize(localUri: string, opts: ResizeOpts) {
for (let i = 0; i < 9; i++) {
const quality = 1.0 - i / 10
const resizeRes = await ImageResizer.createResizedImage(
localUri,
opts.width,
opts.height,
'JPEG',
quality,
undefined,
undefined,
undefined,
{mode: opts.mode},
)
console.log(quality, resizeRes)
if (resizeRes.size < opts.maxSize) {
return resizeRes
}
}
throw new Error(
`This image is too big! We couldn't compress it down to ${opts.maxSize} bytes`,
)
}
export async function compressIfNeeded(img: PickedImage, maxSize: number) {
const origUri = `file://${img.path}`
if (img.size < maxSize) {
return origUri
}
const resizeRez = await resize(origUri, {
width: img.width,
height: img.height,
mode: 'stretch',
maxSize,
})
return resizeRez.uri
}

View File

@ -14,7 +14,7 @@ import {AtUri} from '../../third-party/uri'
import {RootStoreModel} from '../models/root-store' import {RootStoreModel} from '../models/root-store'
import {extractEntities} from '../../lib/strings' import {extractEntities} from '../../lib/strings'
import {isNetworkError} from '../../lib/errors' import {isNetworkError} from '../../lib/errors'
import {downloadAndResize} from '../../lib/download' import {downloadAndResize} from '../../lib/images'
import {getLikelyType, LikelyType, getLinkMeta} from '../../lib/link-meta' import {getLikelyType, LikelyType, getLinkMeta} from '../../lib/link-meta'
const TIMEOUT = 10e3 // 10s const TIMEOUT = 10e3 // 10s
@ -85,6 +85,7 @@ export async function post(
width: 250, width: 250,
height: 250, height: 250,
mode: 'contain', mode: 'contain',
maxSize: 100000,
timeout: 15e3, timeout: 15e3,
}).catch(() => undefined) }).catch(() => undefined)
if (thumbLocal) { if (thumbLocal) {

View File

@ -7,13 +7,14 @@ import {
openCamera, openCamera,
openCropper, openCropper,
} from 'react-native-image-crop-picker' } from 'react-native-image-crop-picker'
import {compressIfNeeded} from '../../../lib/images'
const IMAGE_PARAMS = { const IMAGE_PARAMS = {
width: 500, width: 500,
height: 500, height: 500,
freeStyleCropEnabled: true, freeStyleCropEnabled: true,
forceJpg: true, // ios only forceJpg: true, // ios only
compressImageQuality: 0.7, compressImageQuality: 1.0,
} }
export const PhotoCarouselPicker = ({ export const PhotoCarouselPicker = ({
@ -25,29 +26,35 @@ export const PhotoCarouselPicker = ({
onSelectPhotos: (v: string[]) => void onSelectPhotos: (v: string[]) => void
localPhotos: any localPhotos: any
}) => { }) => {
const handleOpenCamera = useCallback(() => { const handleOpenCamera = useCallback(async () => {
openCamera({ try {
const cameraRes = await openCamera({
mediaType: 'photo', mediaType: 'photo',
cropping: true, cropping: true,
...IMAGE_PARAMS, ...IMAGE_PARAMS,
}).then( })
item => { const uri = await compressIfNeeded(cameraRes, 300000)
onSelectPhotos([item.path, ...selectedPhotos]) onSelectPhotos([uri, ...selectedPhotos])
}, } catch (err) {
_err => {
// ignore // ignore
}, console.log('Error using camera', err)
) }
}, [selectedPhotos, onSelectPhotos]) }, [selectedPhotos, onSelectPhotos])
const handleSelectPhoto = useCallback( const handleSelectPhoto = useCallback(
async (uri: string) => { async (uri: string) => {
const img = await openCropper({ try {
const cropperRes = await openCropper({
mediaType: 'photo', mediaType: 'photo',
path: uri, path: uri,
...IMAGE_PARAMS, ...IMAGE_PARAMS,
}) })
onSelectPhotos([img.path, ...selectedPhotos]) const finalUri = await compressIfNeeded(cropperRes, 300000)
onSelectPhotos([finalUri, ...selectedPhotos])
} catch (err) {
// ignore
console.log('Error selecting photo', err)
}
}, },
[selectedPhotos, onSelectPhotos], [selectedPhotos, onSelectPhotos],
) )
@ -60,13 +67,14 @@ export const PhotoCarouselPicker = ({
}).then(async items => { }).then(async items => {
const result = [] const result = []
for await (const image of items) { for (const image of items) {
const img = await openCropper({ const cropperRes = await openCropper({
mediaType: 'photo', mediaType: 'photo',
path: image.path, path: image.path,
...IMAGE_PARAMS, ...IMAGE_PARAMS,
}) })
result.push(img.path) const finalUri = await compressIfNeeded(cropperRes, 300000)
result.push(finalUri)
} }
onSelectPhotos([...result, ...selectedPhotos]) onSelectPhotos([...result, ...selectedPhotos])
}) })

View File

@ -20,6 +20,7 @@ import {
MAX_DESCRIPTION, MAX_DESCRIPTION,
} from '../../../lib/strings' } from '../../../lib/strings'
import {isNetworkError} from '../../../lib/errors' import {isNetworkError} from '../../../lib/errors'
import {compressIfNeeded} from '../../../lib/images'
import {UserBanner} from '../util/UserBanner' import {UserBanner} from '../util/UserBanner'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
@ -52,13 +53,23 @@ export function Component({
const onPressCancel = () => { const onPressCancel = () => {
store.shell.closeModal() store.shell.closeModal()
} }
const onSelectNewAvatar = (img: PickedImage) => { const onSelectNewAvatar = async (img: PickedImage) => {
try {
setNewUserAvatar(img) setNewUserAvatar(img)
setUserAvatar(img.path) const uri = await compressIfNeeded(img, 300000)
setUserAvatar(uri)
} catch (e: any) {
setError(e.message || e.toString())
} }
const onSelectNewBanner = (img: PickedImage) => { }
const onSelectNewBanner = async (img: PickedImage) => {
try {
setNewUserBanner(img) setNewUserBanner(img)
setUserBanner(img.path) const uri = await compressIfNeeded(img, 500000)
setUserBanner(uri)
} catch (e: any) {
setError(e.message || e.toString())
}
} }
const onPressSave = async () => { const onPressSave = async () => {
setProcessing(true) setProcessing(true)

View File

@ -39,7 +39,7 @@ export function UserAvatar({
height: 400, height: 400,
cropperCircleOverlay: true, cropperCircleOverlay: true,
forceJpg: true, // ios only forceJpg: true, // ios only
compressImageQuality: 0.7, compressImageQuality: 1,
}).then(onSelectNewAvatar) }).then(onSelectNewAvatar)
}, },
}, },
@ -56,7 +56,7 @@ export function UserAvatar({
height: 400, height: 400,
cropperCircleOverlay: true, cropperCircleOverlay: true,
forceJpg: true, // ios only forceJpg: true, // ios only
compressImageQuality: 0.7, compressImageQuality: 1,
}).then(onSelectNewAvatar) }).then(onSelectNewAvatar)
}) })
}, },

View File

@ -35,7 +35,7 @@ export function UserBanner({
compressImageMaxHeight: 500, compressImageMaxHeight: 500,
height: 500, height: 500,
forceJpg: true, // ios only forceJpg: true, // ios only
compressImageQuality: 0.4, compressImageQuality: 1,
includeExif: true, includeExif: true,
}).then(onSelectNewBanner) }).then(onSelectNewBanner)
}, },
@ -54,7 +54,7 @@ export function UserBanner({
compressImageMaxHeight: 500, compressImageMaxHeight: 500,
height: 500, height: 500,
forceJpg: true, // ios only forceJpg: true, // ios only
compressImageQuality: 0.4, compressImageQuality: 1,
includeExif: true, includeExif: true,
}).then(onSelectNewBanner) }).then(onSelectNewBanner)
}) })