diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index f0e618a661246..076b44e5feb82 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -19,6 +19,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {cleanFileName, resizeImageIfNeeded, showCameraPermissionsAlert, verifyFileFormat} from '@libs/fileDownload/FileUtils'; +import Log from '@libs/Log'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {FileObject, ImagePickerResponse as FileResponse} from '@src/types/utils/Attachment'; @@ -219,7 +220,9 @@ function AttachmentPicker({ checkAllProcessed(); }) .catch((error: Error) => { - showGeneralAlert(error.message ?? 'An unknown error occurred'); + Log.warn('Failed to convert HEIC image, falling back to original', {error: error.message}); + const fallbackAsset = processAssetWithFallbacks(asset); + processedAssets.push(fallbackAsset); checkAllProcessed(); }); } else { diff --git a/src/hooks/useFilesValidation.tsx b/src/hooks/useFilesValidation.tsx index fc3506f970088..b55e59acc470e 100644 --- a/src/hooks/useFilesValidation.tsx +++ b/src/hooks/useFilesValidation.tsx @@ -146,12 +146,13 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer }); }; - const convertHeicImageToJpegPromise = (file: FileObject): Promise => { - return new Promise((resolve, reject) => { + const convertHeicToProcessableImagePromise = (file: FileObject): Promise => { + return new Promise((resolve) => { convertHeicImage(file, { onSuccess: (convertedFile) => resolve(convertedFile), - onError: (nonConvertedFile) => { - reject(nonConvertedFile); + onError: (_error, originalFile) => { + Log.warn('HEIC conversion failed, falling back to original file', {fileName: file.name}); + resolve(originalFile); }, }); }); @@ -218,7 +219,7 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer if (otherFiles.some((file) => hasHeicOrHeifExtension(file))) { setIsLoaderVisible(true); - return Promise.all(otherFiles.map((file) => convertHeicImageToJpegPromise(file))).then((convertedImages) => { + return Promise.all(otherFiles.map((file) => convertHeicToProcessableImagePromise(file))).then((convertedImages) => { for (const [index, convertedFile] of convertedImages.entries()) { updateFileOrderMapping(otherFiles.at(index), convertedFile); } diff --git a/src/libs/cropOrRotateImage/index.native.ts b/src/libs/cropOrRotateImage/index.native.ts index f418d4d3358b7..41091c25ba048 100644 --- a/src/libs/cropOrRotateImage/index.native.ts +++ b/src/libs/cropOrRotateImage/index.native.ts @@ -1,26 +1,85 @@ -import {manipulateAsync} from 'expo-image-manipulator'; +import {ImageManipulator} from 'expo-image-manipulator'; +import {Platform} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; +import ImageSize from 'react-native-image-size'; +import Log from '@libs/Log'; import getSaveFormat from './getSaveFormat'; import type {CropOrRotateImage} from './types'; /** - * Crops and rotates the image on ios/android + * Crops and rotates the image on ios/android. + * On iOS, falls back to the original unprocessed image if manipulation fails + * (e.g. CGContext allocation failure on 48MP photos). */ const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) => - new Promise((resolve) => { + new Promise((resolve, reject) => { const format = getSaveFormat(options.type); - // We need to remove the base64 value from the result, as it is causing crashes on Release builds. - // More info: https://github.com/Expensify/App/issues/37963#issuecomment-1989260033 - manipulateAsync(uri, actions, {compress: options.compress, format}).then(({base64, ...result}) => { - RNFetchBlob.fs.stat(result.uri.replace('file://', '')).then(({size}) => { - resolve({ - ...result, - size, - type: options.type || 'image/jpeg', - name: options.name || 'fileName.jpg', - }); + const context = ImageManipulator.manipulate(uri); + for (const action of actions) { + if ('crop' in action) { + context.crop(action.crop); + } else if ('rotate' in action) { + context.rotate(action.rotate); + } + } + context + .renderAsync() + .then((imageRef) => imageRef.saveAsync({compress: options.compress, format})) + // We need to remove the base64 value from the result, as it is causing crashes on Release builds. + // More info: https://github.com/Expensify/App/issues/37963#issuecomment-1989260033 + .then(({base64, ...result}) => { + RNFetchBlob.fs + .stat(result.uri.replace('file://', '')) + .then(({size}) => { + resolve({ + ...result, + size, + type: options.type || 'image/jpeg', + name: options.name || 'fileName.jpg', + }); + }) + .catch(reject); + }) + .catch((error) => { + if (Platform.OS !== 'ios') { + reject(error); + return; + } + + Log.warn('Error cropping/rotating image, falling back to original', {error: error instanceof Error ? error.message : String(error)}); + const filePath = uri.replace('file://', ''); + ImageSize.getSize(uri) + .then(({width, height}) => { + RNFetchBlob.fs + .stat(filePath) + .then(({size}) => { + resolve({ + uri, + width: width ?? 0, + height: height ?? 0, + size, + type: options.type || 'image/jpeg', + name: options.name || 'fileName.jpg', + }); + }) + .catch(reject); + }) + .catch(() => { + RNFetchBlob.fs + .stat(filePath) + .then(({size}) => { + resolve({ + uri, + width: 0, + height: 0, + size, + type: options.type || 'image/jpeg', + name: options.name || 'fileName.jpg', + }); + }) + .catch(reject); + }); }); - }); }); export default cropOrRotateImage; diff --git a/src/libs/cropOrRotateImage/index.ts b/src/libs/cropOrRotateImage/index.ts index 1b669999a1eef..4a7a6c76b55ad 100644 --- a/src/libs/cropOrRotateImage/index.ts +++ b/src/libs/cropOrRotateImage/index.ts @@ -1,19 +1,32 @@ -import {manipulateAsync} from 'expo-image-manipulator'; +import {ImageManipulator} from 'expo-image-manipulator'; import getSaveFormat from './getSaveFormat'; import type {CropOrRotateImage} from './types'; const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) => - new Promise((resolve) => { + new Promise((resolve, reject) => { const format = getSaveFormat(options.type); - manipulateAsync(uri, actions, {compress: options.compress, format}).then((result) => { - fetch(result.uri) - .then((res) => res.blob()) - .then((blob) => { - const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}); - file.uri = URL.createObjectURL(file); - resolve(file); - }); - }); + const context = ImageManipulator.manipulate(uri); + for (const action of actions) { + if ('crop' in action) { + context.crop(action.crop); + } else if ('rotate' in action) { + context.rotate(action.rotate); + } + } + context + .renderAsync() + .then((imageRef) => imageRef.saveAsync({compress: options.compress, format})) + .then((result) => + fetch(result.uri) + .then((res) => res.blob()) + .then((blob) => { + const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}); + file.uri = URL.createObjectURL(file); + resolve(file); + }) + .catch(reject), + ) + .catch(reject); }); export default cropOrRotateImage; diff --git a/src/libs/fileDownload/heicConverter/index.native.ts b/src/libs/fileDownload/heicConverter/index.native.ts index eaa6776c5f071..34a9ad3001d4f 100644 --- a/src/libs/fileDownload/heicConverter/index.native.ts +++ b/src/libs/fileDownload/heicConverter/index.native.ts @@ -1,5 +1,6 @@ import {ImageManipulator, SaveFormat} from 'expo-image-manipulator'; import {verifyFileFormat} from '@libs/fileDownload/FileUtils'; +import Log from '@libs/Log'; import CONST from '@src/CONST'; import type {FileObject} from '@src/types/utils/Attachment'; import type {HeicConverterFunction} from './types'; @@ -41,7 +42,7 @@ const convertImageWithManipulator = ( onSuccess(convertedFile); }) .catch((err) => { - console.error('Error converting HEIC/HEIF to JPEG:', err); + Log.warn('Error converting HEIC/HEIF to JPEG', {error: err instanceof Error ? err.message : String(err)}); onError(err, file); }) .finally(() => { @@ -103,7 +104,7 @@ const convertHeicImage: HeicConverterFunction = (file, {onSuccess = () => {}, on onSuccess(file); }) .catch((err) => { - console.error('Error processing the file:', err); + Log.warn('Error processing the file', {error: err instanceof Error ? err.message : String(err)}); onError(err, file); }) .finally(() => {