diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts deleted file mode 100644 index a5acad25f6..0000000000 --- a/__tests__/lib/images.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import {deleteAsync} from 'expo-file-system' -import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' -import RNFetchBlob from 'rn-fetch-blob' - -import { - downloadAndResize, - DownloadAndResizeOpts, - getResizedDimensions, -} from '../../src/lib/media/manip' - -const mockResizedImage = { - path: 'file://resized-image.jpg', - size: 100, - width: 100, - height: 100, - mime: 'image/jpeg', -} - -describe('downloadAndResize', () => { - const errorSpy = jest.spyOn(global.console, 'error') - - beforeEach(() => { - const mockedCreateResizedImage = manipulateAsync as jest.Mock - mockedCreateResizedImage.mockResolvedValue({ - uri: 'file://resized-image.jpg', - ...mockResizedImage, - }) - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it('should return resized image for valid URI and options', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image.jpg'), - info: jest.fn().mockReturnValue({status: 200}), - flush: jest.fn(), - }) - - const opts: DownloadAndResizeOpts = { - uri: 'https://example.com/image.jpg', - width: 100, - height: 100, - maxSize: 500000, - mode: 'cover', - timeout: 10000, - } - - const result = await downloadAndResize(opts) - expect(result).toEqual(mockResizedImage) - expect(RNFetchBlob.config).toHaveBeenCalledWith({ - fileCache: true, - appendExt: 'jpeg', - }) - expect(RNFetchBlob.fetch).toHaveBeenCalledWith( - 'GET', - 'https://example.com/image.jpg', - ) - - // First time it gets called is to get dimensions - expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {}) - expect(manipulateAsync).toHaveBeenCalledWith( - expect.any(String), - [{resize: {height: opts.height, width: opts.width}}], - {format: SaveFormat.JPEG, compress: 1.0}, - ) - expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), { - idempotent: true, - }) - }) - - it('should return undefined for invalid URI', async () => { - const opts: DownloadAndResizeOpts = { - uri: 'invalid-uri', - width: 100, - height: 100, - maxSize: 500000, - mode: 'cover', - timeout: 10000, - } - - const result = await downloadAndResize(opts) - expect(errorSpy).toHaveBeenCalled() - expect(result).toBeUndefined() - }) - - it('should return undefined for non-200 response', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image'), - info: jest.fn().mockReturnValue({status: 400}), - flush: jest.fn(), - }) - - const opts: DownloadAndResizeOpts = { - uri: 'https://example.com/image', - width: 100, - height: 100, - maxSize: 500000, - mode: 'cover', - timeout: 10000, - } - - const result = await downloadAndResize(opts) - expect(errorSpy).not.toHaveBeenCalled() - expect(result).toBeUndefined() - }) - - it('should not downsize whenever dimensions are below the max dimensions', () => { - const initialDimensionsOne = { - width: 1200, - height: 1000, - } - const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne) - - const initialDimensionsTwo = { - width: 1000, - height: 1200, - } - const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo) - - expect(resizedDimensionsOne).toEqual(initialDimensionsOne) - expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo) - }) - - it('should resize dimensions and maintain aspect ratio if they are above the max dimensons', () => { - const initialDimensionsOne = { - width: 3000, - height: 1500, - } - const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne) - - const initialDimensionsTwo = { - width: 2000, - height: 4000, - } - const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo) - - expect(resizedDimensionsOne).toEqual({ - width: 2000, - height: 1000, - }) - expect(resizedDimensionsTwo).toEqual({ - width: 1000, - height: 2000, - }) - }) -}) diff --git a/jest/jestSetup.js b/jest/jestSetup.js index 0ed7201180..3705562abf 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -44,15 +44,6 @@ jest.mock('expo-file-system', () => ({ deleteAsync: jest.fn(), })) -jest.mock('expo-image-manipulator', () => ({ - manipulateAsync: jest.fn().mockResolvedValue({ - uri: 'file://resized-image', - }), - SaveFormat: { - JPEG: 'jpeg', - }, -})) - jest.mock('expo-camera', () => ({ Camera: { useCameraPermissions: jest.fn(() => [true]), diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index e75f13755f..da597eb8d7 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -11,7 +11,7 @@ import { StorageAccessFramework, writeAsStringAsync, } from 'expo-file-system' -import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {ImageManipulator, SaveFormat} from 'expo-image-manipulator' import * as MediaLibrary from 'expo-media-library' import * as Sharing from 'expo-sharing' import {Buffer} from 'buffer' @@ -172,7 +172,10 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise { // 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 ( + await ImageManipulator.manipulate(localUri).renderAsync() + ).saveAsync() + const newDimensions = getResizedDimensions({ width: imageRes.width, height: imageRes.height, @@ -181,14 +184,14 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise { for (let i = 0; i < 9; i++) { // nearest 10th const quality = Math.round((1 - 0.1 * i) * 10) / 10 - const resizeRes = await manipulateAsync( - localUri, - [{resize: newDimensions}], - { - format: SaveFormat.JPEG, - compress: quality, - }, - ) + const resizeRes = await ( + await ImageManipulator.manipulate(localUri) + .resize(newDimensions) + .renderAsync() + ).saveAsync({ + format: SaveFormat.JPEG, + compress: quality, + }) const fileInfo = await getInfoAsync(resizeRes.uri) if (!fileInfo.exists) { diff --git a/src/state/gallery.ts b/src/state/gallery.ts index f4c8b712ef..316e8ba8e4 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -7,7 +7,7 @@ import { import { Action, ActionCrop, - manipulateAsync, + ImageManipulator, SaveFormat, } from 'expo-image-manipulator' import {nanoid} from 'nanoid/non-secure' @@ -179,7 +179,24 @@ export async function manipulateImage( } const source = img.source - const result = await manipulateAsync(source.path, actions, { + const context = ImageManipulator.manipulate(source.path) + for (let action of actions) { + if ('resize' in action) { + context.resize(action.resize) + } else if ('rotate' in action) { + context.rotate(action.rotate) + } else if ('flip' in action) { + context.flip(action.flip) + } else if ('crop' in action) { + context.crop(action.crop) + } else if ('extent' in action) { + context.extent(action.extent) + } + } + + const result = await ( + await context.renderAsync() + ).saveAsync({ format: SaveFormat.PNG, }) @@ -216,15 +233,15 @@ export async function compressImage(img: ComposerImage): Promise { // Float precision const factor = i / 10 - const res = await manipulateAsync( - source.path, - [{resize: {width: w, height: h}}], - { - compress: factor, - format: SaveFormat.JPEG, - base64: true, - }, - ) + const res = await ( + await ImageManipulator.manipulate(source.path) + .resize({width: w, height: h}) + .renderAsync() + ).saveAsync({ + compress: factor, + format: SaveFormat.JPEG, + base64: true, + }) const base64 = res.base64 diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx index 41ca306573..ea17f71650 100644 --- a/src/view/com/modals/CropImage.web.tsx +++ b/src/view/com/modals/CropImage.web.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {Image as RNImage} from 'react-native-image-crop-picker' -import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {ImageManipulator, SaveFormat} from 'expo-image-manipulator' import {LinearGradient} from 'expo-linear-gradient' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -45,25 +45,21 @@ export function Component({ const onPressDone = async () => { const img = imageRef.current! - const result = await manipulateAsync( - uri, - isEmpty - ? [] - : [ - { - crop: { - originX: (crop.x * img.naturalWidth) / 100, - originY: (crop.y * img.naturalHeight) / 100, - width: (crop.width * img.naturalWidth) / 100, - height: (crop.height * img.naturalHeight) / 100, - }, - }, - ], - { - base64: true, - format: SaveFormat.JPEG, - }, - ) + const context = ImageManipulator.manipulate(uri) + + if (!isEmpty) { + context.crop({ + originX: (crop.x * img.naturalWidth) / 100, + originY: (crop.y * img.naturalHeight) / 100, + width: (crop.width * img.naturalWidth) / 100, + height: (crop.height * img.naturalHeight) / 100, + }) + } + const image = await context.renderAsync() + const result = await image.saveAsync({ + base64: true, + format: SaveFormat.JPEG, + }) onSelect({ path: result.uri,