diff --git a/package-lock.json b/package-lock.json index ec5aa82d94c7..d614e3e61495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -174,6 +174,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/base-64": "^1.0.2", "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", @@ -17344,6 +17345,12 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/base-64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.2.tgz", + "integrity": "sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "dev": true, diff --git a/package.json b/package.json index 6de190a3ee28..1d509860dc81 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/base-64": "^1.0.2", "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", diff --git a/src/CONST.ts b/src/CONST.ts index 73c9bf605b47..55c481b0dc4d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -74,6 +74,12 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + HEIC_SIGNATURES: [ + '6674797068656963', // 'ftypheic' - Indicates standard HEIC file + '6674797068656978', // 'ftypheix' - Indicates a variation of HEIC + '6674797068657631', // 'ftyphevc' - Typically for HEVC encoded media (common in HEIF) + '667479706d696631', // 'ftypmif1' - Multi-Image Format part of HEIF, broader usage + ], RECENT_WAYPOINTS_NUMBER: 20, DEFAULT_DB_NAME: 'OnyxDB', DEFAULT_TABLE_NAME: 'keyvaluepairs', diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 7d4fbd97f4f7..0a14d18a2324 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,11 +1,12 @@ import {Str} from 'expensify-common'; +import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Alert, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker'; import {launchImageLibrary} from 'react-native-image-picker'; -import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; +import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker'; import ImageSize from 'react-native-image-size'; import type {FileObject, ImagePickerResponse as FileResponse} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -41,11 +42,12 @@ type Item = { * See https://github.com/react-native-image-picker/react-native-image-picker/#options * for ImagePicker configuration options */ -const imagePickerOptions = { +const imagePickerOptions: Partial = { includeBase64: false, saveToPhotos: false, selectionLimit: 1, includeExtra: false, + assetRepresentationMode: 'current', }; /** @@ -158,12 +160,42 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s return reject(new Error(`Error during attachment selection: ${response.errorMessage}`)); } - return resolve(response.assets); + const targetAsset = response.assets?.[0]; + const targetAssetUri = targetAsset?.uri; + + if (!targetAssetUri) { + return resolve(); + } + + if (targetAsset?.type?.startsWith('image')) { + FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignatures: CONST.HEIC_SIGNATURES}) + .then((isHEIC) => { + // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature + if (isHEIC && targetAssetUri) { + manipulateAsync(targetAssetUri, [], {format: SaveFormat.JPEG}) + .then((manipResult) => { + const convertedAsset = { + uri: manipResult.uri, + type: 'image/jpeg', + width: manipResult.width, + height: manipResult.height, + }; + + return resolve([convertedAsset]); + }) + .catch((err) => reject(err)); + } else { + return resolve(response.assets); + } + }) + .catch((err) => reject(err)); + } else { + return resolve(response.assets); + } }); }), [showGeneralAlert, type], ); - /** * Launch the DocumentPicker. Results are in the same format as ImagePicker * diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index acf2e3bb32c5..c8520f3c66cd 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -256,6 +256,24 @@ function validateImageForCorruption(file: FileObject): Promise<{width: number; h }); } +/** Verify file format based on the magic bytes of the file - some formats might be identified by multiple signatures */ +function verifyFileFormat({fileUri, formatSignatures}: {fileUri: string; formatSignatures: readonly string[]}) { + return fetch(fileUri) + .then((file) => file.arrayBuffer()) + .then((arrayBuffer) => { + const uintArray = new Uint8Array(arrayBuffer, 4, 12); + + const hexString = Array.from(uintArray) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + return hexString; + }) + .then((hexSignature) => { + return formatSignatures.some((signature) => hexSignature.startsWith(signature)); + }); +} + function isLocalFile(receiptUri?: string | number): boolean { if (!receiptUri) { return false; @@ -318,6 +336,7 @@ export { isImage, getFileResolution, isHighResolutionImage, + verifyFileFormat, getImageDimensionsAfterResize, resizeImageIfNeeded, };