Start with highest quality compression and find a suitable size (#33)
parent
8652b74a38
commit
838fc601c1
|
@ -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,
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
mediaType: 'photo',
|
const cameraRes = await openCamera({
|
||||||
cropping: true,
|
mediaType: 'photo',
|
||||||
...IMAGE_PARAMS,
|
cropping: true,
|
||||||
}).then(
|
...IMAGE_PARAMS,
|
||||||
item => {
|
})
|
||||||
onSelectPhotos([item.path, ...selectedPhotos])
|
const uri = await compressIfNeeded(cameraRes, 300000)
|
||||||
},
|
onSelectPhotos([uri, ...selectedPhotos])
|
||||||
_err => {
|
} catch (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 {
|
||||||
mediaType: 'photo',
|
const cropperRes = await openCropper({
|
||||||
path: uri,
|
mediaType: 'photo',
|
||||||
...IMAGE_PARAMS,
|
path: uri,
|
||||||
})
|
...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])
|
||||||
})
|
})
|
||||||
|
|
|
@ -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) => {
|
||||||
setNewUserAvatar(img)
|
try {
|
||||||
setUserAvatar(img.path)
|
setNewUserAvatar(img)
|
||||||
|
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) => {
|
||||||
setNewUserBanner(img)
|
try {
|
||||||
setUserBanner(img.path)
|
setNewUserBanner(img)
|
||||||
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue