diff options
-rw-r--r-- | __tests__/lib/images.test.ts (renamed from __tests__/lib/download.test.ts) | 8 | ||||
-rw-r--r-- | src/lib/images.ts (renamed from src/lib/download.ts) | 46 | ||||
-rw-r--r-- | src/state/lib/api.ts | 3 | ||||
-rw-r--r-- | src/view/com/composer/PhotoCarouselPicker.tsx | 54 | ||||
-rw-r--r-- | src/view/com/modals/EditProfile.tsx | 23 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 4 |
7 files changed, 101 insertions, 41 deletions
diff --git a/__tests__/lib/download.test.ts b/__tests__/lib/images.test.ts index d90e8c895..461bd04cc 100644 --- a/__tests__/lib/download.test.ts +++ b/__tests__/lib/images.test.ts @@ -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 RNFetchBlob from 'rn-fetch-blob' @@ -16,6 +16,7 @@ describe('downloadAndResize', () => { const mockResizedImage = { path: jest.fn().mockReturnValue('file://resized-image.jpg'), + size: 100, } beforeEach(() => { @@ -37,6 +38,7 @@ describe('downloadAndResize', () => { uri: 'https://example.com/image.jpg', width: 100, height: 100, + maxSize: 500000, mode: 'cover', timeout: 10000, } @@ -56,7 +58,7 @@ describe('downloadAndResize', () => { 100, 100, 'JPEG', - 0.7, + 1, undefined, undefined, undefined, @@ -69,6 +71,7 @@ describe('downloadAndResize', () => { uri: 'invalid-uri', width: 100, height: 100, + maxSize: 500000, mode: 'cover', timeout: 10000, } @@ -83,6 +86,7 @@ describe('downloadAndResize', () => { uri: 'https://example.com/image.bmp', width: 100, height: 100, + maxSize: 500000, mode: 'cover', timeout: 10000, } diff --git a/src/lib/download.ts b/src/lib/images.ts index c53d809b1..caeb96d27 100644 --- a/src/lib/download.ts +++ b/src/lib/images.ts @@ -1,11 +1,13 @@ import RNFetchBlob from 'rn-fetch-blob' import ImageResizer from '@bam.tech/react-native-image-resizer' +import {Image as PickedImage} from 'react-native-image-crop-picker' export interface DownloadAndResizeOpts { uri: string width: number height: number mode: 'contain' | 'cover' | 'stretch' + maxSize: number timeout: number } @@ -41,21 +43,55 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) { localUri = `file://${localUri}` } + return await resize(localUri, opts) + } finally { + if (downloadRes) { + 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', - 0.7, + quality, undefined, undefined, undefined, {mode: opts.mode}, ) - return resizeRes - } finally { - if (downloadRes) { - downloadRes.flush() + 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 } diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index f0f33d134..31df20468 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -14,7 +14,7 @@ import {AtUri} from '../../third-party/uri' import {RootStoreModel} from '../models/root-store' import {extractEntities} from '../../lib/strings' import {isNetworkError} from '../../lib/errors' -import {downloadAndResize} from '../../lib/download' +import {downloadAndResize} from '../../lib/images' import {getLikelyType, LikelyType, getLinkMeta} from '../../lib/link-meta' const TIMEOUT = 10e3 // 10s @@ -85,6 +85,7 @@ export async function post( width: 250, height: 250, mode: 'contain', + maxSize: 100000, timeout: 15e3, }).catch(() => undefined) if (thumbLocal) { diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx index 86d23313d..2afe2e5a5 100644 --- a/src/view/com/composer/PhotoCarouselPicker.tsx +++ b/src/view/com/composer/PhotoCarouselPicker.tsx @@ -7,13 +7,14 @@ import { openCamera, openCropper, } from 'react-native-image-crop-picker' +import {compressIfNeeded} from '../../../lib/images' const IMAGE_PARAMS = { width: 500, height: 500, freeStyleCropEnabled: true, forceJpg: true, // ios only - compressImageQuality: 0.7, + compressImageQuality: 1.0, } export const PhotoCarouselPicker = ({ @@ -25,29 +26,35 @@ export const PhotoCarouselPicker = ({ onSelectPhotos: (v: string[]) => void localPhotos: any }) => { - const handleOpenCamera = useCallback(() => { - openCamera({ - mediaType: 'photo', - cropping: true, - ...IMAGE_PARAMS, - }).then( - item => { - onSelectPhotos([item.path, ...selectedPhotos]) - }, - _err => { - // ignore - }, - ) + const handleOpenCamera = useCallback(async () => { + try { + const cameraRes = await openCamera({ + mediaType: 'photo', + cropping: true, + ...IMAGE_PARAMS, + }) + const uri = await compressIfNeeded(cameraRes, 300000) + onSelectPhotos([uri, ...selectedPhotos]) + } catch (err) { + // ignore + console.log('Error using camera', err) + } }, [selectedPhotos, onSelectPhotos]) const handleSelectPhoto = useCallback( async (uri: string) => { - const img = await openCropper({ - mediaType: 'photo', - path: uri, - ...IMAGE_PARAMS, - }) - onSelectPhotos([img.path, ...selectedPhotos]) + try { + const cropperRes = await openCropper({ + mediaType: 'photo', + path: uri, + ...IMAGE_PARAMS, + }) + const finalUri = await compressIfNeeded(cropperRes, 300000) + onSelectPhotos([finalUri, ...selectedPhotos]) + } catch (err) { + // ignore + console.log('Error selecting photo', err) + } }, [selectedPhotos, onSelectPhotos], ) @@ -60,13 +67,14 @@ export const PhotoCarouselPicker = ({ }).then(async items => { const result = [] - for await (const image of items) { - const img = await openCropper({ + for (const image of items) { + const cropperRes = await openCropper({ mediaType: 'photo', path: image.path, ...IMAGE_PARAMS, }) - result.push(img.path) + const finalUri = await compressIfNeeded(cropperRes, 300000) + result.push(finalUri) } onSelectPhotos([...result, ...selectedPhotos]) }) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 1ebd4e767..bd97ced53 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -20,6 +20,7 @@ import { MAX_DESCRIPTION, } from '../../../lib/strings' import {isNetworkError} from '../../../lib/errors' +import {compressIfNeeded} from '../../../lib/images' import {UserBanner} from '../util/UserBanner' import {UserAvatar} from '../util/UserAvatar' @@ -52,13 +53,23 @@ export function Component({ const onPressCancel = () => { store.shell.closeModal() } - const onSelectNewAvatar = (img: PickedImage) => { - setNewUserAvatar(img) - setUserAvatar(img.path) + const onSelectNewAvatar = async (img: PickedImage) => { + try { + setNewUserAvatar(img) + const uri = await compressIfNeeded(img, 300000) + setUserAvatar(uri) + } catch (e: any) { + setError(e.message || e.toString()) + } } - const onSelectNewBanner = (img: PickedImage) => { - setNewUserBanner(img) - setUserBanner(img.path) + const onSelectNewBanner = async (img: PickedImage) => { + try { + setNewUserBanner(img) + const uri = await compressIfNeeded(img, 500000) + setUserBanner(uri) + } catch (e: any) { + setError(e.message || e.toString()) + } } const onPressSave = async () => { setProcessing(true) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 07d657007..67f039551 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -39,7 +39,7 @@ export function UserAvatar({ height: 400, cropperCircleOverlay: true, forceJpg: true, // ios only - compressImageQuality: 0.7, + compressImageQuality: 1, }).then(onSelectNewAvatar) }, }, @@ -56,7 +56,7 @@ export function UserAvatar({ height: 400, cropperCircleOverlay: true, forceJpg: true, // ios only - compressImageQuality: 0.7, + compressImageQuality: 1, }).then(onSelectNewAvatar) }) }, diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 697e900b1..5137e19ca 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -35,7 +35,7 @@ export function UserBanner({ compressImageMaxHeight: 500, height: 500, forceJpg: true, // ios only - compressImageQuality: 0.4, + compressImageQuality: 1, includeExif: true, }).then(onSelectNewBanner) }, @@ -54,7 +54,7 @@ export function UserBanner({ compressImageMaxHeight: 500, height: 500, forceJpg: true, // ios only - compressImageQuality: 0.4, + compressImageQuality: 1, includeExif: true, }).then(onSelectNewBanner) }) |