signal-desktop/ts/util/scaleImageToLevel.ts
Scott Nonnenberg 99b2bc304e
Use streams to download attachments directly to disk
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
2023-10-30 09:24:28 -07:00

182 lines
4.2 KiB
TypeScript

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { LoadImageResult } from 'blueimp-load-image';
import loadImage from 'blueimp-load-image';
import type { MIMEType } from '../types/MIME';
import { IMAGE_JPEG } from '../types/MIME';
import { canvasToBlob } from './canvasToBlob';
import { getValue } from '../RemoteConfig';
import { parseNumber } from './libphonenumberUtil';
enum MediaQualityLevels {
One = 1,
Two = 2,
Three = 3,
}
const DEFAULT_LEVEL = MediaQualityLevels.One;
const MiB = 1024 * 1024;
const DEFAULT_LEVEL_DATA = {
maxDimensions: 1600,
quality: 0.7,
size: MiB,
thresholdSize: 0.2 * MiB,
};
const MEDIA_QUALITY_LEVEL_DATA = new Map([
[MediaQualityLevels.One, DEFAULT_LEVEL_DATA],
[
MediaQualityLevels.Two,
{
maxDimensions: 2048,
quality: 0.75,
size: MiB * 1.5,
thresholdSize: 0.3 * MiB,
},
],
[
MediaQualityLevels.Three,
{
maxDimensions: 4096,
quality: 0.75,
size: MiB * 3,
thresholdSize: 0.4 * MiB,
},
],
]);
const SCALABLE_DIMENSIONS = [3072, 2048, 1600, 1024, 768];
const MIN_DIMENSIONS = 512;
function parseCountryValues(values: string): Map<string, MediaQualityLevels> {
const map = new Map<string, MediaQualityLevels>();
values.split(',').forEach(value => {
const [countryCode, level] = value.split(':');
map.set(
countryCode,
Number(level) === 2 ? MediaQualityLevels.Two : MediaQualityLevels.One
);
});
return map;
}
function getMediaQualityLevel(): MediaQualityLevels {
const values = getValue('desktop.mediaQuality.levels');
if (!values) {
return DEFAULT_LEVEL;
}
const e164 = window.textsecure.storage.user.getNumber();
if (!e164) {
return DEFAULT_LEVEL;
}
const parsedPhoneNumber = parseNumber(e164);
if (!parsedPhoneNumber.isValidNumber) {
return DEFAULT_LEVEL;
}
const countryValues = parseCountryValues(values);
const level = parsedPhoneNumber.countryCode
? countryValues.get(parsedPhoneNumber.countryCode)
: undefined;
if (level) {
return level;
}
return countryValues.get('*') || DEFAULT_LEVEL;
}
async function getCanvasBlobAsJPEG(
image: HTMLCanvasElement,
dimensions: number,
quality: number
): Promise<Blob> {
const canvas = loadImage.scale(image, {
canvas: true,
maxHeight: dimensions,
maxWidth: dimensions,
});
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error('image not a canvas');
}
return canvasToBlob(canvas, IMAGE_JPEG, quality);
}
export async function scaleImageToLevel(
fileOrBlobOrURL: File | Blob | string,
contentType: MIMEType,
size: number,
sendAsHighQuality?: boolean
): Promise<{
blob: Blob;
contentType: MIMEType;
}> {
let data: LoadImageResult;
try {
data = await loadImage(fileOrBlobOrURL, {
canvas: true,
orientation: true,
meta: true, // Check if we need to strip EXIF data
});
if (!(data.image instanceof HTMLCanvasElement)) {
throw new Error('image not a canvas');
}
} catch (cause) {
const error = new Error('scaleImageToLevel: Failed to process image', {
cause,
});
throw error;
}
const level = sendAsHighQuality
? MediaQualityLevels.Three
: getMediaQualityLevel();
const {
maxDimensions,
quality,
size: targetSize,
thresholdSize,
} = MEDIA_QUALITY_LEVEL_DATA.get(level) || DEFAULT_LEVEL_DATA;
if (size <= thresholdSize) {
// Always encode through canvas as a temporary fix for a library bug
const blob: Blob = await canvasToBlob(data.image, contentType);
return {
blob,
contentType,
};
}
for (let i = 0; i < SCALABLE_DIMENSIONS.length; i += 1) {
const scalableDimensions = SCALABLE_DIMENSIONS[i];
if (maxDimensions < scalableDimensions) {
continue;
}
// We need these operations to be in serial
// eslint-disable-next-line no-await-in-loop
const blob = await getCanvasBlobAsJPEG(
data.image,
scalableDimensions,
quality
);
if (blob.size <= targetSize) {
return {
blob,
contentType: IMAGE_JPEG,
};
}
}
const blob = await getCanvasBlobAsJPEG(data.image, MIN_DIMENSIONS, quality);
return {
blob,
contentType: IMAGE_JPEG,
};
}