Skip to content

Commit

Permalink
Merge pull request Expensify#45448 from nkdengineer/fix/reduce-size-i…
Browse files Browse the repository at this point in the history
…mage-44084
  • Loading branch information
luacmartins authored Aug 5, 2024
2 parents bdeb6aa + b8b8eea commit 4c0c160
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 30 deletions.
71 changes: 71 additions & 0 deletions patches/expo-image-manipulator+11.8.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
diff --git a/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js b/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js
index 5b77ad6..a3ecdb0 100644
--- a/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js
+++ b/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js
@@ -1,5 +1,13 @@
import { crop, extent, flip, resize, rotate } from './actions/index.web';
import { getContext } from './utils/getContext.web';
+
+const SAFARI_MOBILE_CANVAS_LIMIT = 4096;
+
+const isMobileIOS = () => {
+ const userAgent = navigator.userAgent;
+ return /iP(ad|od|hone)/i.test(userAgent) && /(WebKit|CriOS|FxiOS|OPiOS|mercury)/i.test(userAgent);
+};
+
function getResults(canvas, options) {
let uri;
if (options) {
@@ -21,16 +29,49 @@ function getResults(canvas, options) {
base64: uri.replace(/^data:image\/\w+;base64,/, ''),
};
}
+
+function getAdjustedCanvasSize(originalWidth, originalHeight) {
+ if(!isMobileIOS()) return { width: originalWidth, height: originalHeight };
+
+ const aspectRatio = originalWidth / originalHeight;
+ let newWidth;
+ let newHeight;
+
+ if (originalWidth <= SAFARI_MOBILE_CANVAS_LIMIT && originalHeight <= SAFARI_MOBILE_CANVAS_LIMIT) {
+ return { width: originalWidth, height: originalHeight };
+ }
+
+ if (aspectRatio > 1) {
+ newWidth = SAFARI_MOBILE_CANVAS_LIMIT;
+ newHeight = Math.round(newWidth / aspectRatio);
+ } else {
+ newHeight = SAFARI_MOBILE_CANVAS_LIMIT;
+ newWidth = Math.round(newHeight * aspectRatio);
+ }
+
+ if (newWidth > SAFARI_MOBILE_CANVAS_LIMIT) {
+ newWidth = SAFARI_MOBILE_CANVAS_LIMIT;
+ newHeight = Math.round(newWidth / aspectRatio);
+ } else if (newHeight > SAFARI_MOBILE_CANVAS_LIMIT) {
+ newHeight = SAFARI_MOBILE_CANVAS_LIMIT;
+ newWidth = Math.round(newHeight * aspectRatio);
+ }
+
+ return { width: newWidth, height: newHeight };
+}
+
function loadImageAsync(uri) {
return new Promise((resolve, reject) => {
const imageSource = new Image();
imageSource.crossOrigin = 'anonymous';
const canvas = document.createElement('canvas');
imageSource.onload = () => {
- canvas.width = imageSource.naturalWidth;
- canvas.height = imageSource.naturalHeight;
+ const adjudstedCanvasSize = getAdjustedCanvasSize(imageSource.naturalWidth, imageSource.naturalHeight);
+
+ canvas.width = adjudstedCanvasSize.width;
+ canvas.height = adjudstedCanvasSize.height;
const context = getContext(canvas);
- context.drawImage(imageSource, 0, 0, imageSource.naturalWidth, imageSource.naturalHeight);
+ context.drawImage(imageSource, 0, 0, adjudstedCanvasSize.width, adjudstedCanvasSize.height);
resolve(canvas);
};
imageSource.onerror = () => reject(canvas);
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const CONST = {
BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
SCREEN_TRANSITION_END_TIMEOUT: 1000,
ARROW_HIDE_DELAY: 3000,
MAX_IMAGE_CANVAS_AREA: 16777216,

API_ATTACHMENT_VALIDATIONS: {
// 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion
Expand Down Expand Up @@ -143,6 +144,8 @@ const CONST = {

LOGO_MAX_SCALE: 1.5,

MAX_IMAGE_DIMENSION: 2400,

BREADCRUMB_TYPE: {
ROOT: 'root',
STRONG: 'strong',
Expand Down
20 changes: 19 additions & 1 deletion src/libs/fileDownload/FileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import DateUtils from '@libs/DateUtils';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import getImageManipulator from './getImageManipulator';
import getImageResolution from './getImageResolution';
import type {ReadFileAsync, SplitExtensionFromFileName} from './types';

Expand Down Expand Up @@ -244,7 +245,7 @@ function base64ToFile(base64: string, filename: string): File {
return file;
}

function validateImageForCorruption(file: FileObject): Promise<void> {
function validateImageForCorruption(file: FileObject): Promise<{width: number; height: number} | void> {
if (!Str.isImage(file.name ?? '') || !file.uri) {
return Promise.resolve();
}
Expand Down Expand Up @@ -285,6 +286,21 @@ function isHighResolutionImage(resolution: {width: number; height: number} | nul
return resolution !== null && (resolution.width > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD || resolution.height > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD);
}

const getImageDimensionsAfterResize = (file: FileObject) =>
ImageSize.getSize(file.uri ?? '').then(({width, height}) => {
const scaleFactor = CONST.MAX_IMAGE_DIMENSION / (width < height ? height : width);
const newWidth = Math.max(1, width * scaleFactor);
const newHeight = Math.max(1, height * scaleFactor);

return {width: newWidth, height: newHeight};
});

const resizeImageIfNeeded = (file: FileObject) => {
if (!file || !Str.isImage(file.name ?? '') || (file?.size ?? 0) <= CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
return Promise.resolve(file);
}
return getImageDimensionsAfterResize(file).then(({width, height}) => getImageManipulator({fileUri: file.uri ?? '', width, height, fileName: file.name ?? '', type: file.type}));
};
export {
showGeneralErrorAlert,
showSuccessAlert,
Expand All @@ -302,4 +318,6 @@ export {
isImage,
getFileResolution,
isHighResolutionImage,
getImageDimensionsAfterResize,
resizeImageIfNeeded,
};
13 changes: 13 additions & 0 deletions src/libs/fileDownload/getImageManipulator/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {manipulateAsync} from 'expo-image-manipulator';
import type {FileObject} from '@components/AttachmentModal';
import type ImageManipulatorConfig from './type';

export default function getImageManipulator({fileUri, width, height, type, fileName}: ImageManipulatorConfig): Promise<FileObject> {
return manipulateAsync(fileUri ?? '', [{resize: {width, height}}]).then((result) => ({
uri: result.uri,
width: result.width,
height: result.height,
type,
name: fileName,
}));
}
14 changes: 14 additions & 0 deletions src/libs/fileDownload/getImageManipulator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {manipulateAsync} from 'expo-image-manipulator';
import type ImageManipulatorConfig from './type';

export default function getImageManipulator({fileUri, width, height, fileName}: ImageManipulatorConfig): Promise<File> {
return manipulateAsync(fileUri ?? '', [{resize: {width, height}}]).then((result) =>
fetch(result.uri)
.then((res) => res.blob())
.then((blob) => {
const resizedFile = new File([blob], `${fileName}.jpeg`, {type: 'image/jpeg'});
resizedFile.uri = URL.createObjectURL(resizedFile);
return resizedFile;
}),
);
}
9 changes: 9 additions & 0 deletions src/libs/fileDownload/getImageManipulator/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type ImageManipulatorConfig = {
fileUri: string;
fileName: string;
width: number;
height: number;
type?: string;
};

export default ImageManipulatorConfig;
30 changes: 16 additions & 14 deletions src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function IOURequestStepScan({
return false;
}

if ((file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
if (!Str.isImage(file.name ?? '') && (file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
Alert.alert(translate('attachmentPicker.attachmentTooLarge'), translate('attachmentPicker.sizeExceeded'));
return false;
}
Expand Down Expand Up @@ -387,28 +387,30 @@ function IOURequestStepScan({
/**
* Sets the Receipt objects and navigates the user to the next page
*/
const setReceiptAndNavigate = (file: FileObject, isPdfValidated?: boolean) => {
if (!validateReceipt(file)) {
const setReceiptAndNavigate = (originalFile: FileObject, isPdfValidated?: boolean) => {
if (!validateReceipt(originalFile)) {
return;
}

// If we have a pdf file and if it is not validated then set the pdf file for validation and return
if (Str.isPDF(file.name ?? '') && !isPdfValidated) {
setPdfFile(file);
if (Str.isPDF(originalFile.name ?? '') && !isPdfValidated) {
setPdfFile(originalFile);
return;
}

// Store the receipt on the transaction object in Onyx
// On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file.
// So, let us also save the file type in receipt for later use during blob fetch
IOU.setMoneyRequestReceipt(transactionID, file?.uri ?? '', file.name ?? '', action !== CONST.IOU.ACTION.EDIT, file.type);
FileUtils.resizeImageIfNeeded(originalFile).then((file) => {
// Store the receipt on the transaction object in Onyx
// On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file.
// So, let us also save the file type in receipt for later use during blob fetch
IOU.setMoneyRequestReceipt(transactionID, file?.uri ?? '', file.name ?? '', action !== CONST.IOU.ACTION.EDIT, file.type);

if (action === CONST.IOU.ACTION.EDIT) {
updateScanAndNavigate(file, file?.uri ?? '');
return;
}
if (action === CONST.IOU.ACTION.EDIT) {
updateScanAndNavigate(file, file?.uri ?? '');
return;
}

navigateToConfirmationStep(file, file.uri ?? '');
navigateToConfirmationStep(file, file.uri ?? '');
});
};

const capturePhoto = useCallback(() => {
Expand Down
33 changes: 18 additions & 15 deletions src/pages/iou/request/step/IOURequestStepScan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ function IOURequestStepScan({
const {isSmallScreenWidth} = useResponsiveLayout();
const {translate} = useLocalize();
const {isDraggingOver} = useContext(DragAndDropContext);
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? -1}`);
const [cameraPermissionState, setCameraPermissionState] = useState<PermissionState | undefined>('prompt');
const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false);
const [isTorchAvailable, setIsTorchAvailable] = useState(false);
Expand All @@ -78,6 +77,7 @@ function IOURequestStepScan({
const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false);

const getScreenshotTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? -1}`);

const [videoConstraints, setVideoConstraints] = useState<MediaTrackConstraints>();
const tabIndex = 1;
Expand Down Expand Up @@ -207,7 +207,7 @@ function IOURequestStepScan({
return false;
}

if ((file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
if (!Str.isImage(file.name ?? '') && (file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooLarge', 'attachmentPicker.sizeExceeded');
return false;
}
Expand Down Expand Up @@ -419,27 +419,30 @@ function IOURequestStepScan({
/**
* Sets the Receipt objects and navigates the user to the next page
*/
const setReceiptAndNavigate = (file: FileObject, isPdfValidated?: boolean) => {
validateReceipt(file).then((isFileValid) => {
const setReceiptAndNavigate = (originalFile: FileObject, isPdfValidated?: boolean) => {
validateReceipt(originalFile).then((isFileValid) => {
if (!isFileValid) {
return;
}

// If we have a pdf file and if it is not validated then set the pdf file for validation and return
if (Str.isPDF(file.name ?? '') && !isPdfValidated) {
setPdfFile(file);
if (Str.isPDF(originalFile.name ?? '') && !isPdfValidated) {
setPdfFile(originalFile);
return;
}
// Store the receipt on the transaction object in Onyx
const source = URL.createObjectURL(file as Blob);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
IOU.setMoneyRequestReceipt(transactionID, source, file.name || '', action !== CONST.IOU.ACTION.EDIT);

if (action === CONST.IOU.ACTION.EDIT) {
updateScanAndNavigate(file, source);
return;
}
navigateToConfirmationStep(file, source);
FileUtils.resizeImageIfNeeded(originalFile).then((file) => {
// Store the receipt on the transaction object in Onyx
const source = URL.createObjectURL(file as Blob);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
IOU.setMoneyRequestReceipt(transactionID, source, file.name || '', action !== CONST.IOU.ACTION.EDIT);

if (action === CONST.IOU.ACTION.EDIT) {
updateScanAndNavigate(file, source);
return;
}
navigateToConfirmationStep(file, source);
});
});
};

Expand Down

0 comments on commit 4c0c160

Please sign in to comment.