session-desktop/ts/util/attachmentsUtil.ts

163 lines
4.9 KiB
TypeScript

import { StagedAttachmentType } from '../components/session/conversation/composition/CompositionBox';
import { SignalService } from '../protobuf';
import { Constants } from '../session';
import loadImage from 'blueimp-load-image';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { sendDataExtractionNotification } from '../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
import { AttachmentType, save } from '../types/Attachment';
export interface MaxScaleSize {
maxSize?: number;
maxHeight?: number;
maxWidth?: number;
maxSide?: number; // use this to make avatars cropped if too big and centered if too small.
}
/**
* Scale down an image to fit in the required dimension.
* Note: This method won't crop if needed,
* @param attachment The attachment to scale down
* @param maxMeasurements any of those will be used if set
*/
export async function autoScale<T extends { contentType: string; file: any }>(
attachment: T,
maxMeasurements?: MaxScaleSize
): Promise<T> {
const { contentType, file } = attachment;
if (contentType.split('/')[0] !== 'image' || contentType === 'image/tiff') {
// nothing to do
return Promise.resolve(attachment);
}
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = document.createElement('img');
img.onerror = reject;
// tslint:disable-next-line: cyclomatic-complexity
img.onload = () => {
URL.revokeObjectURL(url);
if (maxMeasurements?.maxSide && (maxMeasurements?.maxHeight || maxMeasurements?.maxWidth)) {
reject('Cannot have maxSide and another dimension set together');
}
const maxSize =
maxMeasurements?.maxSize || Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES;
const makeSquare = Boolean(maxMeasurements?.maxSide);
const maxHeight = maxMeasurements?.maxHeight || maxMeasurements?.maxSide || 4096;
const maxWidth = maxMeasurements?.maxWidth || maxMeasurements?.maxSide || 4096;
if (
img.naturalWidth <= maxWidth &&
img.naturalHeight <= maxHeight &&
file.size <= maxSize &&
!makeSquare
) {
resolve(attachment);
return;
}
if (
file.type === 'image/gif' &&
file.size <= Constants.CONVERSATION.MAX_ATTACHMENT_FILESIZE_BYTES
) {
resolve(attachment);
return;
}
if (file.type === 'image/gif') {
reject(new Error('GIF is too large'));
return;
}
const canvas = (loadImage as any).scale(img, {
canvas: true,
maxWidth: makeSquare ? maxMeasurements?.maxSide : maxWidth,
maxHeight: makeSquare ? maxMeasurements?.maxSide : maxHeight,
crop: makeSquare,
});
let quality = 0.95;
let i = 4;
let blob;
do {
i -= 1;
blob = window.dataURLToBlobSync(canvas.toDataURL('image/jpeg', quality));
quality = (quality * maxSize) / blob.size;
if (quality > 1) {
quality = 0.95;
}
} while (i > 0 && blob.size > maxSize);
resolve({
...attachment,
file: new File([blob], 'blob-file'),
});
};
img.src = url;
});
}
export async function getFile(attachment: StagedAttachmentType, maxMeasurements?: MaxScaleSize) {
if (!attachment) {
return null;
}
const attachmentFlags = attachment.isVoiceMessage
? SignalService.AttachmentPointer.Flags.VOICE_MESSAGE
: null;
const scaled = await autoScale(attachment, maxMeasurements);
const fileRead = await readFile(scaled);
return {
caption: attachment.caption,
...fileRead,
url: undefined,
flags: attachmentFlags || null,
};
}
export type AttachmentFileType = {
attachment: any;
data: ArrayBuffer;
size: number;
};
// export async function readFile(attachment: any): Promise<object> {
export async function readFile(attachment: any): Promise<AttachmentFileType> {
return new Promise((resolve, reject) => {
const FR = new FileReader();
FR.onload = e => {
const data = e?.target?.result as ArrayBuffer;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
FR.onerror = reject;
FR.onabort = reject;
FR.readAsArrayBuffer(attachment.file);
});
}
export const saveAttachmentToDisk = async ({
attachment,
messageTimestamp,
messageSender,
conversationId,
}: {
attachment: AttachmentType;
messageTimestamp: number;
messageSender: string;
conversationId: string;
}) => {
const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType, false);
save({
attachment: { ...attachment, url: decryptedUrl },
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp: messageTimestamp,
});
await sendDataExtractionNotification(conversationId, messageSender, messageTimestamp);
};