diff --git a/src/lib/media/image-manipulator.ts b/src/lib/media/image-manipulator.ts new file mode 100644 index 0000000000..41fde37c81 --- /dev/null +++ b/src/lib/media/image-manipulator.ts @@ -0,0 +1,27 @@ +import { + ImageManipulator, + type ImageManipulatorContext, + type ImageResult, + type SaveOptions, +} from 'expo-image-manipulator' + +export async function renderImage( + source: string, + manipulate?: (context: ImageManipulatorContext) => void, + saveOptions?: SaveOptions, +): Promise { + const context = ImageManipulator.manipulate(source) + + try { + manipulate?.(context) + const image = await context.renderAsync() + + try { + return await image.saveAsync(saveOptions) + } finally { + image.release() + } + } finally { + context.release() + } +} diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 904e3c48ca..49136b7e96 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -12,12 +12,13 @@ import { StorageAccessFramework, writeAsStringAsync, } from 'expo-file-system/legacy' -import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {SaveFormat} from 'expo-image-manipulator' import * as MediaLibrary from 'expo-media-library/legacy' import * as Sharing from 'expo-sharing' import {logger} from '#/logger' import {IS_ANDROID, IS_IOS} from '#/env' +import {renderImage} from './image-manipulator' import {type PickerImage} from './picker.shared' import {type Dimensions} from './types' import {convertCdnPreset, getResizedDimensions} from './util' @@ -78,7 +79,7 @@ export async function shareImageModal({uri}: {uri: string}) { } const downloadedPath = await downloadImage(uri, String(uuid.v4()), 15e3) - const {uri: jpegUri} = await manipulateAsync(downloadedPath, [], { + const {uri: jpegUri} = await renderImage(downloadedPath, undefined, { format: SaveFormat.JPEG, compress: 1.0, }) @@ -199,7 +200,7 @@ async function doResize( // Now instead, we have to supply the final dimensions to the manipulation function instead. // Performing an "empty" manipulation lets us get the dimensions of the original image. React Native's Image.getSize() // does not work for local files... - const imageRes = await manipulateAsync(localUri, [], {}) + const imageRes = await renderImage(localUri) const newDimensions = getResizedDimensions( { width: imageRes.width, @@ -217,9 +218,9 @@ async function doResize( const qualityPercentage = Math.round( (maxQualityPercentage + minQualityPercentage) / 2, ) - const resizeRes = await manipulateAsync( + const resizeRes = await renderImage( localUri, - [{resize: newDimensions}], + context => context.resize(newDimensions), { format: SaveFormat.JPEG, compress: qualityPercentage / 100, diff --git a/src/platform/misc.web-check.d.ts b/src/platform/misc.web-check.d.ts index d3eaf3e971..dfa41bf6a4 100644 --- a/src/platform/misc.web-check.d.ts +++ b/src/platform/misc.web-check.d.ts @@ -113,17 +113,8 @@ declare module 'expo-file-system' { declare module 'expo-image-manipulator' { import {type ImageManipulator as ImageManipulatorModule} from 'expo-image-manipulator/build/ImageManipulator.types' export const ImageManipulator: ImageManipulatorModule + export {useImageManipulator} from 'expo-image-manipulator/build/ImageManipulator' export { - manipulateAsync, - useImageManipulator, - } from 'expo-image-manipulator/build/ImageManipulator' - export { - type Action, - type ActionCrop, - type ActionExtent, - type ActionFlip, - type ActionResize, - type ActionRotate, FlipType, type ImageResult, SaveFormat, diff --git a/src/state/gallery.ts b/src/state/gallery.ts index fbb5edd4c2..15c3a9e5b6 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -5,14 +5,10 @@ import { makeDirectoryAsync, moveAsync, } from 'expo-file-system/legacy' -import { - type Action, - type ActionCrop, - manipulateAsync, - SaveFormat, -} from 'expo-image-manipulator' +import {type ImageManipulatorContext, SaveFormat} from 'expo-image-manipulator' import {nanoid} from 'nanoid/non-secure' +import {renderImage} from '#/lib/media/image-manipulator' import {getImageDim} from '#/lib/media/manip' import {openCropper} from '#/lib/media/picker' import {type PickerImage} from '#/lib/media/picker.shared' @@ -22,7 +18,7 @@ import {logger} from '#/logger' import {IS_NATIVE, IS_WEB} from '#/env' export type ImageTransformation = { - crop?: ActionCrop['crop'] + crop?: Parameters[0] } export type ImageMeta = { @@ -160,11 +156,8 @@ export async function manipulateImage( img: ComposerImage, trans: ImageTransformation, ): Promise { - const rawActions: (Action | undefined)[] = [trans.crop && {crop: trans.crop}] - - const actions = rawActions.filter((a): a is Action => a !== undefined) - - if (actions.length === 0) { + const crop = trans.crop + if (!crop) { if (img.transformed === undefined) { return img } @@ -173,7 +166,7 @@ export async function manipulateImage( } const source = img.source - const result = await manipulateAsync(source.path, actions, { + const result = await renderImage(source.path, context => context.crop(crop), { format: SaveFormat.PNG, }) @@ -244,9 +237,9 @@ export async function compressImage( continue } - const res = await manipulateAsync( + const res = await renderImage( source.path, - [{resize: {width: w, height: h}}], + context => context.resize({width: w, height: h}), { compress: qualityPercentage / 100, format: SaveFormat.JPEG, @@ -362,8 +355,8 @@ function blobToDataUri(blob: Blob): Promise { * media to a post. They live alongside our own `bsky-composer` dir under the OS * cache directory. expo-image-picker copies every originally selected photo and * video here, and expo-image-manipulator leaves intermediate full-resolution - * outputs here (compressImage makes several manipulateAsync passes, only the - * last of which gets moved into `bsky-composer`). Nothing else cleans these up, + * outputs here (compressImage makes several rendering passes, only the last of + * which gets moved into `bsky-composer`). Nothing else cleans these up, * so on iOS - where the OS exposes no "clear cache" - they accumulate * indefinitely, one full-resolution copy per attached item. */