Uniformize size of accepted attachment to 10MB

Some image files can be scaled automatically, so this size is not the
same for them, they will just be scaled down
pull/1381/head
Audric Ackermann 5 years ago
parent 97ff60f3bb
commit f2074f502a
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -653,64 +653,69 @@
const data = await readFile({ file: avatar }); const data = await readFile({ file: avatar });
// Ensure that this file is either small enough or is resized to meet our // Ensure that this file is either small enough or is resized to meet our
// requirements for attachments // requirements for attachments
const withBlob = await window.Signal.Util.AttachmentUtil.autoScale( try {
{ const withBlob = await window.Signal.Util.AttachmentUtil.autoScale(
contentType: avatar.type, {
file: new Blob([data.data], { contentType: avatar.type,
type: avatar.contentType, file: new Blob([data.data], {
}), type: avatar.contentType,
maxMeasurements: { }),
maxSize: 1000 * 1024, maxMeasurements: {
maxHeight: 512, maxSize: 1000 * 1024, // 1Mb for our profile picture
maxWidth: 512, },
}, }
} );
); const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile(
const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile( withBlob.file
withBlob.file );
);
// For simplicity we use the same attachment pointer that would send to
// For simplicity we use the same attachment pointer that would send to // others, which means we need to wait for the database response.
// others, which means we need to wait for the database response. // To avoid the wait, we create a temporary url for the local image
// To avoid the wait, we create a temporary url for the local image // and use it until we the the response from the server
// and use it until we the the response from the server const tempUrl = window.URL.createObjectURL(avatar);
const tempUrl = window.URL.createObjectURL(avatar); conversation.setLokiProfile({ displayName: newName });
conversation.setLokiProfile({ displayName: newName }); conversation.set('avatar', tempUrl);
conversation.set('avatar', tempUrl);
// Encrypt with a new key every time
// Encrypt with a new key every time profileKey = libsignal.crypto.getRandomBytes(32);
profileKey = libsignal.crypto.getRandomBytes(32); const encryptedData = await textsecure.crypto.encryptProfile(
const encryptedData = await textsecure.crypto.encryptProfile( dataResized,
dataResized, profileKey
profileKey );
);
const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatar(
const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatar( {
{ ...dataResized,
...dataResized, data: encryptedData,
data: encryptedData, size: encryptedData.byteLength,
size: encryptedData.byteLength, }
} );
);
({ url } = avatarPointer);
({ url } = avatarPointer);
storage.put('profileKey', profileKey);
storage.put('profileKey', profileKey);
conversation.set('avatarPointer', url);
conversation.set('avatarPointer', url);
const upgraded = await Signal.Migrations.processNewAttachment({
const upgraded = await Signal.Migrations.processNewAttachment({ isRaw: true,
isRaw: true, data: data.data,
data: data.data, url,
url, });
}); newAvatarPath = upgraded.path;
newAvatarPath = upgraded.path; // Replace our temporary image with the attachment pointer from the server:
// Replace our temporary image with the attachment pointer from the server: conversation.set('avatar', null);
conversation.set('avatar', null); conversation.setLokiProfile({
conversation.setLokiProfile({ displayName: newName,
displayName: newName, avatar: newAvatarPath,
avatar: newAvatarPath, });
}); } catch (error) {
window.log.error(
'showEditProfileDialog Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
}
} else { } else {
// do not update the avatar if it did not change // do not update the avatar if it did not change
conversation.setLokiProfile({ conversation.setLokiProfile({

@ -805,11 +805,16 @@ export class SessionCompositionBox extends React.Component<Props, State> {
} }
} }
// this function is called right before sending a message, to gather really files bejind attachments. // this function is called right before sending a message, to gather really the files behind attachments.
private async getFiles() { private async getFiles() {
const { stagedAttachments } = this.props; const { stagedAttachments } = this.props;
// scale them down
const files = await Promise.all( const files = await Promise.all(
stagedAttachments.map(attachment => AttachmentUtil.getFile(attachment)) stagedAttachments.map(attachment =>
AttachmentUtil.getFile(attachment, {
maxSize: Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES,
})
)
); );
this.props.clearAttachments(); this.props.clearAttachments();
return files; return files;

@ -1058,6 +1058,7 @@ export class SessionConversation extends React.Component<Props, State> {
} }
const url = await window.autoOrientImage(file); const url = await window.autoOrientImage(file);
this.addAttachments([ this.addAttachments([
{ {
file, file,
@ -1070,42 +1071,22 @@ export class SessionConversation extends React.Component<Props, State> {
]); ]);
}; };
let blob = null;
try { try {
const blob = await AttachmentUtil.autoScale({ blob = await AttachmentUtil.autoScale({
contentType, contentType,
file, file,
}); });
let limitKb = 10000;
const blobType = if (
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0]; blob.file.size >= Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES
) {
switch (blobType) { ToastUtils.pushFileSizeErrorAsByte(
case 'image': Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES
limitKb = 6000; );
break; return;
case 'gif':
limitKb = 10000;
break;
case 'audio':
limitKb = 10000;
break;
case 'video':
limitKb = 10000;
break;
default:
limitKb = 10000;
} }
// if ((blob.file.size / 1024).toFixed(4) >= limitKb) {
// const units = ['kB', 'MB', 'GB'];
// let u = -1;
// let limit = limitKb * 1000;
// do {
// limit /= 1000;
// u += 1;
// } while (limit >= 1000 && u < units.length - 1);
// // this.showFileSizeError(limit, units[u]);
// return;
// }
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'Error ensuring that image is properly sized:', 'Error ensuring that image is properly sized:',
@ -1118,6 +1099,10 @@ export class SessionConversation extends React.Component<Props, State> {
try { try {
if (GoogleChrome.isImageTypeSupported(contentType)) { if (GoogleChrome.isImageTypeSupported(contentType)) {
// this does not add the preview to the message outgoing
// this is just for us, for the list of attachments we are sending
// the files are scaled down under getFiles()
await renderImagePreview(); await renderImagePreview();
} else if (GoogleChrome.isVideoTypeSupported(contentType)) { } else if (GoogleChrome.isVideoTypeSupported(contentType)) {
await renderVideoPreview(); await renderVideoPreview();

@ -11,6 +11,7 @@ import {
SessionButtonType, SessionButtonType,
} from '../SessionButton'; } from '../SessionButton';
import { Constants } from '../../../session'; import { Constants } from '../../../session';
import { ToastUtils } from '../../../session/utils';
interface Props { interface Props {
onExitVoiceNoteView: any; onExitVoiceNoteView: any;
@ -422,8 +423,10 @@ export class SessionRecording extends React.Component<Props, State> {
} }
// Is the audio file > attachment filesize limit // Is the audio file > attachment filesize limit
if (audioBlob.size > Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE) { if (audioBlob.size > Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES) {
// TODO VINCE: warn the user that it's too big ToastUtils.pushFileSizeErrorAsByte(
Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES
);
return; return;
} }

@ -62,12 +62,15 @@ export const getPreview = async (
// Ensure that this file is either small enough or is resized to meet our // Ensure that this file is either small enough or is resized to meet our
// requirements for attachments // requirements for attachments
const withBlob = await AttachmentUtil.autoScale({ const withBlob = await AttachmentUtil.autoScale(
contentType: fullSizeImage.contentType, {
file: new Blob([fullSizeImage.data], { contentType: fullSizeImage.contentType,
type: fullSizeImage.contentType, file: new Blob([fullSizeImage.data], {
}), type: fullSizeImage.contentType,
}); }),
},
{ maxSize: 100 * 1000 } // this is a preview image. No need for it to be crazy big. 100k is big enough
);
const data = await arrayBufferFromFile(withBlob.file); const data = await arrayBufferFromFile(withBlob.file);
objectUrl = URL.createObjectURL(withBlob.file); objectUrl = URL.createObjectURL(withBlob.file);

@ -28,8 +28,8 @@ export const CONVERSATION = {
// Maximum voice message duraton of 5 minutes // Maximum voice message duraton of 5 minutes
// which equates to 1.97 MB // which equates to 1.97 MB
MAX_VOICE_MESSAGE_DURATION: 300, MAX_VOICE_MESSAGE_DURATION: 300,
// Max attachment size: 10 MB // Max attachment size: 6 MB
MAX_ATTACHMENT_FILESIZE: 10000000, MAX_ATTACHMENT_FILESIZE_BYTES: 6 * 1000 * 1000,
}; };
export const UI = { export const UI = {

@ -80,6 +80,17 @@ export function pushFileSizeError(limit: number, units: string) {
); );
} }
export function pushFileSizeErrorAsByte(bytesCount: number) {
const units = ['kB', 'MB', 'GB'];
let u = -1;
let limit = bytesCount;
do {
limit /= 1000;
u += 1;
} while (limit >= 1000 && u < units.length - 1);
pushFileSizeError(limit, units[u]);
}
export function pushMultipleNonImageError() { export function pushMultipleNonImageError() {
pushToastError( pushToastError(
'cannotMixImageAndNonImageAttachments', 'cannotMixImageAndNonImageAttachments',

@ -1,16 +1,18 @@
import { StagedAttachmentType } from '../components/session/conversation/SessionCompositionBox'; import { StagedAttachmentType } from '../components/session/conversation/SessionCompositionBox';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { Constants } from '../session';
export interface MaxScaleSize { export interface MaxScaleSize {
maxSize: number; maxSize?: number;
maxHeight: number; maxHeight?: number;
maxWidth: number; maxWidth?: number;
} }
export async function autoScale< export async function autoScale<T extends { contentType: string; file: any }>(
T extends { contentType: string; file: any; maxMeasurements?: MaxScaleSize } attachment: T,
>(attachment: T): Promise<T> { maxMeasurements?: MaxScaleSize
const { contentType, file, maxMeasurements } = attachment; ): Promise<T> {
const { contentType, file } = attachment;
if (contentType.split('/')[0] !== 'image' || contentType === 'image/tiff') { if (contentType.split('/')[0] !== 'image' || contentType === 'image/tiff') {
// nothing to do // nothing to do
return Promise.resolve(attachment); return Promise.resolve(attachment);
@ -23,7 +25,9 @@ export async function autoScale<
img.onload = () => { img.onload = () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
const maxSize = maxMeasurements?.maxSize || 6000 * 1024; const maxSize =
maxMeasurements?.maxSize ||
Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES;
const maxHeight = maxMeasurements?.maxHeight || 4096; const maxHeight = maxMeasurements?.maxHeight || 4096;
const maxWidth = maxMeasurements?.maxWidth || 4096; const maxWidth = maxMeasurements?.maxWidth || 4096;
@ -36,7 +40,7 @@ export async function autoScale<
return; return;
} }
const gifMaxSize = 25000 * 1024; const gifMaxSize = Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES;
if (file.type === 'image/gif' && file.size <= gifMaxSize) { if (file.type === 'image/gif' && file.size <= gifMaxSize) {
resolve(attachment); resolve(attachment);
return; return;
@ -62,11 +66,15 @@ export async function autoScale<
canvas.toDataURL('image/jpeg', quality) canvas.toDataURL('image/jpeg', quality)
); );
quality = (quality * maxSize) / blob.size; quality = (quality * maxSize) / blob.size;
// Should we disallow the algo drop the quality too low?
// if (quality < 0.5) {
// quality = 0.5;
// }
// NOTE: During testing with a large image, we observed the // NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]? // `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
if (quality < 0.5) { if (quality > 1) {
quality = 0.5; quality = 1;
} }
} while (i > 0 && blob.size > maxSize); } while (i > 0 && blob.size > maxSize);
@ -79,7 +87,10 @@ export async function autoScale<
}); });
} }
export async function getFile(attachment: StagedAttachmentType) { export async function getFile(
attachment: StagedAttachmentType,
maxMeasurements?: MaxScaleSize
) {
if (!attachment) { if (!attachment) {
return Promise.resolve(); return Promise.resolve();
} }
@ -88,7 +99,7 @@ export async function getFile(attachment: StagedAttachmentType) {
? SignalService.AttachmentPointer.Flags.VOICE_MESSAGE ? SignalService.AttachmentPointer.Flags.VOICE_MESSAGE
: null; : null;
const scaled = await autoScale(attachment); const scaled = await autoScale(attachment, maxMeasurements);
const fileRead = await readFile(scaled); const fileRead = await readFile(scaled);
return { return {
...fileRead, ...fileRead,

Loading…
Cancel
Save