Skip to content

Commit

Permalink
Do not transcode images if they meet the size thresholds
Browse files Browse the repository at this point in the history
Co-authored-by: Josh Perez <60019601+josh-signal@users.noreply.github.com>
  • Loading branch information
automated-signal and josh-signal committed Aug 23, 2021
1 parent 347bb4a commit b0dc901
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 10 deletions.
8 changes: 8 additions & 0 deletions .storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

const webpack = require('webpack');

module.exports = ({ config }) => {
config.entry.unshift(
'!!style-loader!css-loader!sanitize.css',
Expand Down Expand Up @@ -29,5 +31,11 @@ module.exports = ({ config }) => {
net: 'net',
};

config.plugins.unshift(
new webpack.IgnorePlugin({
resourceRegExp: /sharp$/,
})
);

return config;
};
27 changes: 27 additions & 0 deletions patches/@types+sharp+0.28.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
diff --git a/node_modules/@types/sharp/index.d.ts b/node_modules/@types/sharp/index.d.ts
index 3210332..4808af0 100755
--- a/node_modules/@types/sharp/index.d.ts
+++ b/node_modules/@types/sharp/index.d.ts
@@ -23,7 +23,21 @@ import { Duplex } from "stream";
* @returns A sharp instance that can be used to chain operations
*/
declare function sharp(options?: sharp.SharpOptions): sharp.Sharp;
-declare function sharp(input?: string | Buffer, options?: sharp.SharpOptions): sharp.Sharp;
+declare function sharp(
+ input?:
+ | Buffer
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int8Array
+ | Uint16Array
+ | Int16Array
+ | Uint32Array
+ | Int32Array
+ | Float32Array
+ | Float64Array
+ | string,
+ options?: sharp.SharpOptions
+): sharp.Sharp;

declare namespace sharp {
/** Object containing nested boolean values representing the available input and output formats/methods. */
1 change: 0 additions & 1 deletion preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ try {
window.emojiData = require('emoji-datasource');
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
window.loadImage = require('blueimp-load-image');
window.getGuid = require('uuid/v4');

const activeWindowService = new ActiveWindowService();
Expand Down
6 changes: 5 additions & 1 deletion ts/types/Attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ export async function autoOrientJPEG(
attachment.data,
attachment.contentType
);
const xcodedDataBlob = await scaleImageToLevel(dataBlob, isIncoming);
const { blob: xcodedDataBlob } = await scaleImageToLevel(
dataBlob,
attachment.contentType,
isIncoming
);
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);

// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
Expand Down
14 changes: 13 additions & 1 deletion ts/util/handleImageAttachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,19 @@ export async function autoScale({
return { contentType, file, fileName };
}

const blob = await scaleImageToLevel(file, true);
const { blob, contentType: newContentType } = await scaleImageToLevel(
file,
contentType,
true
);

if (newContentType !== IMAGE_JPEG) {
return {
contentType,
file: blob,
fileName,
};
}

const { name } = path.parse(fileName);

Expand Down
43 changes: 36 additions & 7 deletions ts/util/scaleImageToLevel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import sharp from 'sharp';
import loadImage from 'blueimp-load-image';

import { IMAGE_JPEG } from '../types/MIME';
import { MIMEType, IMAGE_JPEG } from '../types/MIME';
import { canvasToBlob } from './canvasToBlob';
import { getValue } from '../RemoteConfig';

Expand All @@ -21,6 +22,7 @@ const DEFAULT_LEVEL_DATA = {
maxDimensions: 1600,
quality: 0.7,
size: MiB,
thresholdSize: 0.2 * MiB,
};

const MEDIA_QUALITY_LEVEL_DATA = new Map([
Expand All @@ -31,6 +33,7 @@ const MEDIA_QUALITY_LEVEL_DATA = new Map([
maxDimensions: 2048,
quality: 0.75,
size: MiB * 1.5,
thresholdSize: 0.3 * MiB,
},
],
[
Expand All @@ -39,6 +42,7 @@ const MEDIA_QUALITY_LEVEL_DATA = new Map([
maxDimensions: 4096,
quality: 0.75,
size: MiB * 3,
thresholdSize: 0.4 * MiB,
},
],
]);
Expand Down Expand Up @@ -82,7 +86,7 @@ function getMediaQualityLevel(): MediaQualityLevels {
return countryValues.get('*') || DEFAULT_LEVEL;
}

async function getCanvasBlob(
async function getCanvasBlobAsJPEG(
image: HTMLCanvasElement,
dimensions: number,
quality: number
Expand All @@ -98,10 +102,20 @@ async function getCanvasBlob(
return canvasToBlob(canvas, IMAGE_JPEG, quality);
}

async function stripImageFileEXIFData(file: File | Blob): Promise<Blob> {
const arrayBuffer = await file.arrayBuffer();
const xArrayBuffer = await sharp(new Uint8Array(arrayBuffer)).toBuffer();
return new Blob([xArrayBuffer]);
}

export async function scaleImageToLevel(
fileOrBlobOrURL: File | Blob,
contentType: MIMEType,
sendAsHighQuality?: boolean
): Promise<Blob> {
): Promise<{
blob: Blob;
contentType: MIMEType;
}> {
let image: HTMLCanvasElement;
try {
const data = await loadImage(fileOrBlobOrURL, {
Expand All @@ -121,9 +135,17 @@ export async function scaleImageToLevel(
const level = sendAsHighQuality
? MediaQualityLevels.Three
: getMediaQualityLevel();
const { maxDimensions, quality, size } =
const { maxDimensions, quality, size, thresholdSize } =
MEDIA_QUALITY_LEVEL_DATA.get(level) || DEFAULT_LEVEL_DATA;

if (fileOrBlobOrURL.size <= thresholdSize) {
const blob = await stripImageFileEXIFData(fileOrBlobOrURL);
return {
blob,
contentType,
};
}

for (let i = 0; i < SCALABLE_DIMENSIONS.length; i += 1) {
const scalableDimensions = SCALABLE_DIMENSIONS[i];
if (maxDimensions < scalableDimensions) {
Expand All @@ -132,11 +154,18 @@ export async function scaleImageToLevel(

// We need these operations to be in serial
// eslint-disable-next-line no-await-in-loop
const blob = await getCanvasBlob(image, scalableDimensions, quality);
const blob = await getCanvasBlobAsJPEG(image, scalableDimensions, quality);
if (blob.size <= size) {
return blob;
return {
blob,
contentType: IMAGE_JPEG,
};
}
}

return getCanvasBlob(image, MIN_DIMENSIONS, quality);
const blob = await getCanvasBlobAsJPEG(image, MIN_DIMENSIONS, quality);
return {
blob,
contentType: IMAGE_JPEG,
};
}

0 comments on commit b0dc901

Please sign in to comment.