From eba5fb3c3dfedae70b80e72e4981b05a4327be50 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 23 Jul 2024 15:51:32 +0200 Subject: [PATCH 1/7] fix conversions wip --- package-lock.json | 7 ++++ package.json | 1 + src/CONST.ts | 1 + .../AttachmentPicker/index.native.tsx | 33 ++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index df337fdd663d..2c6ed800e5a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -173,6 +173,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", @@ -17238,6 +17239,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 29030d148853..8439481adb8c 100644 --- a/package.json +++ b/package.json @@ -228,6 +228,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 a1193fd8bf32..b9869f641e21 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -74,6 +74,7 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + HEIC_SIGNATURE: '000000186674797068656963', 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..c56f0c9949c1 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,9 +1,13 @@ +/* eslint-disable @lwc/lwc/no-async-await */ +import {decode} from 'base-64'; 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 RNFS from 'react-native-fs'; import {launchImageLibrary} from 'react-native-image-picker'; import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; import ImageSize from 'react-native-image-size'; @@ -46,6 +50,7 @@ const imagePickerOptions = { saveToPhotos: false, selectionLimit: 1, includeExtra: false, + assetRepresentationMode: 'current', }; /** @@ -140,7 +145,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s const showImagePicker = useCallback( (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => { + imagePickerFunc(getImagePickerOptions(type), async (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -158,6 +163,32 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s return reject(new Error(`Error during attachment selection: ${response.errorMessage}`)); } + const targetAsset = response.assets?.[0]; + + console.log('TARGET ASSET ', targetAsset); + const fileContent = await RNFS.read(targetAsset?.uri, 12, 0, 'base64'); + const hexSignature = Array.from(decode(fileContent)) + .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0')) + .slice(0, 32) + .join('') + .toUpperCase(); + + const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE); + + // 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) { + const manipResult = await manipulateAsync(targetAsset?.uri, [], {format: SaveFormat.JPEG}); + + const convertedAsset = { + uri: manipResult.uri, + type: 'image/jpeg', + width: manipResult.width, + height: manipResult.height, + }; + + return resolve([convertedAsset]); + } + return resolve(response.assets); }); }), From 7bf0fe98952c9ee74c417b54bffbed61bee0624c Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 23 Jul 2024 16:59:06 +0200 Subject: [PATCH 2/7] cleanups, fix for video, type changes --- .../AttachmentPicker/index.native.tsx | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index c56f0c9949c1..2314abc87ab5 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -9,7 +9,7 @@ import RNDocumentPicker from 'react-native-document-picker'; import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker'; import RNFS from 'react-native-fs'; 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'; @@ -45,7 +45,7 @@ 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, @@ -165,28 +165,33 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s const targetAsset = response.assets?.[0]; - console.log('TARGET ASSET ', targetAsset); - const fileContent = await RNFS.read(targetAsset?.uri, 12, 0, 'base64'); - const hexSignature = Array.from(decode(fileContent)) - .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0')) - .slice(0, 32) - .join('') - .toUpperCase(); + if (!targetAsset?.uri) { + return resolve(); + } - const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE); + if (targetAsset?.type?.startsWith('image')) { + const fileContent = await RNFS.read(targetAsset.uri, 12, 0, 'base64'); + const hexSignature = Array.from(decode(fileContent)) + .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0')) + .slice(0, 32) + .join('') + .toUpperCase(); - // 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) { - const manipResult = await manipulateAsync(targetAsset?.uri, [], {format: SaveFormat.JPEG}); + const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE); - const convertedAsset = { - uri: manipResult.uri, - type: 'image/jpeg', - width: manipResult.width, - height: manipResult.height, - }; + // 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) { + const manipResult = await manipulateAsync(targetAsset.uri, [], {format: SaveFormat.JPEG}); - return resolve([convertedAsset]); + const convertedAsset = { + uri: manipResult.uri, + type: 'image/jpeg', + width: manipResult.width, + height: manipResult.height, + }; + + return resolve([convertedAsset]); + } } return resolve(response.assets); From ccfcbaa6969fb9f4f48ac7149f5d99348dbdf768 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 26 Jul 2024 16:37:55 +0200 Subject: [PATCH 3/7] generalize function, migrate from async await to then --- .../AttachmentPicker/index.native.tsx | 56 +++++++++---------- src/libs/fileDownload/FileUtils.ts | 21 +++++++ 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 2314abc87ab5..e60e1b5497bb 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,5 +1,3 @@ -/* eslint-disable @lwc/lwc/no-async-await */ -import {decode} from 'base-64'; import {Str} from 'expensify-common'; import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'; import React, {useCallback, useMemo, useRef, useState} from 'react'; @@ -7,7 +5,6 @@ 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 RNFS from 'react-native-fs'; import {launchImageLibrary} from 'react-native-image-picker'; import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker'; import ImageSize from 'react-native-image-size'; @@ -145,7 +142,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s const showImagePicker = useCallback( (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type), async (response: ImagePickerResponse) => { + imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -164,42 +161,41 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s } const targetAsset = response.assets?.[0]; + const targetAssetUri = targetAsset?.uri; - if (!targetAsset?.uri) { + if (!targetAssetUri) { return resolve(); } if (targetAsset?.type?.startsWith('image')) { - const fileContent = await RNFS.read(targetAsset.uri, 12, 0, 'base64'); - const hexSignature = Array.from(decode(fileContent)) - .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0')) - .slice(0, 32) - .join('') - .toUpperCase(); - - const isHEIC = hexSignature.startsWith(CONST.HEIC_SIGNATURE); - - // 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) { - const manipResult = await manipulateAsync(targetAsset.uri, [], {format: SaveFormat.JPEG}); - - const convertedAsset = { - uri: manipResult.uri, - type: 'image/jpeg', - width: manipResult.width, - height: manipResult.height, - }; - - return resolve([convertedAsset]); - } + FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignature: CONST.HEIC_SIGNATURE}) + .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); } - - 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 7422d8bd8d8b..0e0476519b1a 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -1,5 +1,7 @@ +import {decode} from 'base-64'; import {Str} from 'expensify-common'; import {Alert, Linking, Platform} from 'react-native'; +import RNFS from 'react-native-fs'; import ImageSize from 'react-native-image-size'; import type {FileObject} from '@components/AttachmentModal'; import DateUtils from '@libs/DateUtils'; @@ -255,6 +257,24 @@ function validateImageForCorruption(file: FileObject): Promise { }); } +function verifyFileFormat({fileUri, formatSignature}: {fileUri: string; formatSignature: string}) { + return RNFS.read(fileUri, 12, 0, 'base64') + .then((fileContent) => { + const hexSignature = Array.from(decode(fileContent)) + .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0')) + .slice(0, 32) + .join('') + .toUpperCase(); + + const isHEIC = hexSignature.startsWith(formatSignature); + return isHEIC; + }) + .catch((error: Error) => { + Log.hmmm('Failed to verify the file format: ', error); + return null; + }); +} + function isLocalFile(receiptUri?: string | number): boolean { if (!receiptUri) { return false; @@ -302,4 +322,5 @@ export { isImage, getFileResolution, isHighResolutionImage, + verifyFileFormat, }; From 88cda03e1ba33b52d45fa7978d9ed196957ac9cf Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 30 Jul 2024 10:35:08 +0200 Subject: [PATCH 4/7] rename variable --- src/libs/fileDownload/FileUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 0e0476519b1a..345115b9509f 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -266,8 +266,8 @@ function verifyFileFormat({fileUri, formatSignature}: {fileUri: string; formatSi .join('') .toUpperCase(); - const isHEIC = hexSignature.startsWith(formatSignature); - return isHEIC; + const isOfProvidedFormat = hexSignature.startsWith(formatSignature); + return isOfProvidedFormat; }) .catch((error: Error) => { Log.hmmm('Failed to verify the file format: ', error); From ab63c3e430820240cdbd67f5f8689ca1a7f022b0 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 2 Aug 2024 16:47:35 +0200 Subject: [PATCH 5/7] add multi image format, move implementation to fetch to make it compatible with other platforms --- src/CONST.ts | 8 ++++- .../AttachmentPicker/index.native.tsx | 2 +- src/libs/fileDownload/FileUtils.ts | 29 +++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index b9869f641e21..0ae637c2052a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,8 +73,14 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; +// 667479706D69663100000000 const CONST = { - HEIC_SIGNATURE: '000000186674797068656963', + 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 e60e1b5497bb..0a14d18a2324 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -168,7 +168,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s } if (targetAsset?.type?.startsWith('image')) { - FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignature: CONST.HEIC_SIGNATURE}) + 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) { diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 345115b9509f..d3ed4a0a3b96 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -1,7 +1,5 @@ -import {decode} from 'base-64'; import {Str} from 'expensify-common'; import {Alert, Linking, Platform} from 'react-native'; -import RNFS from 'react-native-fs'; import ImageSize from 'react-native-image-size'; import type {FileObject} from '@components/AttachmentModal'; import DateUtils from '@libs/DateUtils'; @@ -257,21 +255,20 @@ function validateImageForCorruption(file: FileObject): Promise { }); } -function verifyFileFormat({fileUri, formatSignature}: {fileUri: string; formatSignature: string}) { - return RNFS.read(fileUri, 12, 0, 'base64') - .then((fileContent) => { - const hexSignature = Array.from(decode(fileContent)) - .map((char) => char.charCodeAt(0).toString(16).padStart(2, '0')) - .slice(0, 32) - .join('') - .toUpperCase(); - - const isOfProvidedFormat = hexSignature.startsWith(formatSignature); - return isOfProvidedFormat; +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; }) - .catch((error: Error) => { - Log.hmmm('Failed to verify the file format: ', error); - return null; + .then((hexSignature) => { + return formatSignatures.some((signature) => hexSignature.startsWith(signature)); }); } From 591320e67c69249db998a095d4f343f15a55a58b Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 2 Aug 2024 16:57:19 +0200 Subject: [PATCH 6/7] cleanup --- src/CONST.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0ae637c2052a..8c7f9c2e4faf 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,7 +73,6 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; -// 667479706D69663100000000 const CONST = { HEIC_SIGNATURES: [ '6674797068656963', // 'ftypheic' - Indicates standard HEIC file From b8cef07402d3a40c63f36815e0a8416347c1af3b Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 6 Aug 2024 10:20:46 +0200 Subject: [PATCH 7/7] add description for verifyFileFormat --- src/libs/fileDownload/FileUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 9fe1a32c2e61..c8520f3c66cd 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -256,6 +256,7 @@ 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())