Optimize compressImage method to also use binary search (#7490)

* use binary search for compressImage

* use binary search for doResize (mobile version)

* use binary search for doResize (web version)

* use safeDeleteAsync in compressImage

---------

Co-authored-by: nguyen <nguyensvo123@gmail.com>
This commit is contained in:
Kae
2025-04-27 04:10:32 +10:00
committed by GitHub
parent ea718190ca
commit 1eb678cabd
3 changed files with 56 additions and 21 deletions
+20 -7
View File
@@ -178,15 +178,20 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
height: imageRes.height, height: imageRes.height,
}) })
for (let i = 0; i < 9; i++) { let minQualityPercentage = 0
// nearest 10th let maxQualityPercentage = 101 // exclusive
const quality = Math.round((1 - 0.1 * i) * 10) / 10 let newDataUri
while (maxQualityPercentage - minQualityPercentage > 1) {
const qualityPercentage = Math.round(
(maxQualityPercentage + minQualityPercentage) / 2,
)
const resizeRes = await manipulateAsync( const resizeRes = await manipulateAsync(
localUri, localUri,
[{resize: newDimensions}], [{resize: newDimensions}],
{ {
format: SaveFormat.JPEG, format: SaveFormat.JPEG,
compress: quality, compress: qualityPercentage / 100,
}, },
) )
@@ -198,8 +203,8 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
} }
if (fileInfo.size < opts.maxSize) { if (fileInfo.size < opts.maxSize) {
safeDeleteAsync(imageRes.uri) minQualityPercentage = qualityPercentage
return { newDataUri = {
path: normalizePath(resizeRes.uri), path: normalizePath(resizeRes.uri),
mime: 'image/jpeg', mime: 'image/jpeg',
size: fileInfo.size, size: fileInfo.size,
@@ -207,9 +212,17 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
height: resizeRes.height, height: resizeRes.height,
} }
} else { } else {
safeDeleteAsync(resizeRes.uri) maxQualityPercentage = qualityPercentage
} }
safeDeleteAsync(resizeRes.uri)
} }
if (newDataUri) {
safeDeleteAsync(imageRes.uri)
return newDataUri
}
throw new Error( throw new Error(
`This image is too big! We couldn't compress it down to ${opts.maxSize} bytes`, `This image is too big! We couldn't compress it down to ${opts.maxSize} bytes`,
) )
+16 -5
View File
@@ -72,17 +72,28 @@ interface DoResizeOpts {
async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> { async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> {
let newDataUri let newDataUri
for (let i = 0; i <= 10; i++) { let minQualityPercentage = 0
newDataUri = await createResizedImage(dataUri, { let maxQualityPercentage = 101 //exclusive
while (maxQualityPercentage - minQualityPercentage > 1) {
const qualityPercentage = Math.round(
(maxQualityPercentage + minQualityPercentage) / 2,
)
const tempDataUri = await createResizedImage(dataUri, {
width: opts.width, width: opts.width,
height: opts.height, height: opts.height,
quality: 1 - i * 0.1, quality: qualityPercentage / 100,
mode: opts.mode, mode: opts.mode,
}) })
if (getDataUriSize(newDataUri) < opts.maxSize) {
break if (getDataUriSize(tempDataUri) < opts.maxSize) {
minQualityPercentage = qualityPercentage
newDataUri = tempDataUri
} else {
maxQualityPercentage = qualityPercentage
} }
} }
if (!newDataUri) { if (!newDataUri) {
throw new Error('Failed to compress image') throw new Error('Failed to compress image')
} }
+20 -9
View File
@@ -13,7 +13,7 @@ import {
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {POST_IMG_MAX} from '#/lib/constants' import {POST_IMG_MAX} from '#/lib/constants'
import {getImageDim} from '#/lib/media/manip' import {getImageDim, safeDeleteAsync} from '#/lib/media/manip'
import {openCropper} from '#/lib/media/picker' import {openCropper} from '#/lib/media/picker'
import {getDataUriSize} from '#/lib/media/util' import {getDataUriSize} from '#/lib/media/util'
import {isIOS, isNative} from '#/platform/detection' import {isIOS, isNative} from '#/platform/detection'
@@ -212,15 +212,20 @@ export async function compressImage(img: ComposerImage): Promise<ImageMeta> {
const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX) const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX)
const cacheDir = isNative && getImageCacheDirectory() const cacheDir = isNative && getImageCacheDirectory()
for (let i = 10; i > 0; i--) { let minQualityPercentage = 0
// Float precision let maxQualityPercentage = 101 // exclusive
const factor = i / 10 let newDataUri
while (maxQualityPercentage - minQualityPercentage > 1) {
const qualityPercentage = Math.round(
(maxQualityPercentage + minQualityPercentage) / 2,
)
const res = await manipulateAsync( const res = await manipulateAsync(
source.path, source.path,
[{resize: {width: w, height: h}}], [{resize: {width: w, height: h}}],
{ {
compress: factor, compress: qualityPercentage / 100,
format: SaveFormat.JPEG, format: SaveFormat.JPEG,
base64: true, base64: true,
}, },
@@ -229,17 +234,23 @@ export async function compressImage(img: ComposerImage): Promise<ImageMeta> {
const base64 = res.base64 const base64 = res.base64
if (base64 !== undefined && getDataUriSize(base64) <= POST_IMG_MAX.size) { if (base64 !== undefined && getDataUriSize(base64) <= POST_IMG_MAX.size) {
return { minQualityPercentage = qualityPercentage
newDataUri = {
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/jpeg',
} }
} else {
maxQualityPercentage = qualityPercentage
if (cacheDir) {
await safeDeleteAsync(res.uri)
}
} }
}
if (cacheDir) { if (newDataUri) {
await deleteAsync(res.uri) return newDataUri
}
} }
throw new Error(`Unable to compress image`) throw new Error(`Unable to compress image`)