mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 10:19:08 +00:00
207 lines
5.4 KiB
TypeScript
207 lines
5.4 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { blobToArrayBuffer } from 'blob-util';
|
|
|
|
import type {
|
|
AttachmentType,
|
|
LocalAttachmentV2Type,
|
|
} from '../types/Attachment.std.js';
|
|
import { type MIMEType, IMAGE_PNG } from '../types/MIME.std.js';
|
|
import type { MakeVideoScreenshotResultType } from '../types/VisualAttachment.dom.js';
|
|
import type { LoggerType } from '../types/Logging.std.js';
|
|
import { getLocalAttachmentUrl } from './getLocalAttachmentUrl.std.js';
|
|
import { toLogFormat } from '../types/errors.std.js';
|
|
import {
|
|
isImageTypeSupported,
|
|
isVideoTypeSupported,
|
|
} from './GoogleChrome.std.js';
|
|
import { strictAssert } from './assert.std.js';
|
|
|
|
const THUMBNAIL_SIZE = 150;
|
|
const THUMBNAIL_CONTENT_TYPE = IMAGE_PNG;
|
|
|
|
export async function captureDimensionsAndScreenshot(
|
|
attachment: AttachmentType,
|
|
options: { generateThumbnail: boolean },
|
|
params: {
|
|
writeNewAttachmentData: (
|
|
data: Uint8Array
|
|
) => Promise<LocalAttachmentV2Type>;
|
|
makeObjectUrl: (
|
|
data: Uint8Array | ArrayBuffer,
|
|
contentType: MIMEType
|
|
) => string;
|
|
revokeObjectUrl: (path: string) => void;
|
|
getImageDimensions: (params: {
|
|
objectUrl: string;
|
|
logger: LoggerType;
|
|
}) => Promise<{
|
|
width: number;
|
|
height: number;
|
|
}>;
|
|
makeImageThumbnail: (params: {
|
|
size: number;
|
|
objectUrl: string;
|
|
contentType: MIMEType;
|
|
logger: LoggerType;
|
|
}) => Promise<Blob>;
|
|
makeVideoScreenshot: (params: {
|
|
objectUrl: string;
|
|
contentType: MIMEType;
|
|
logger: LoggerType;
|
|
}) => Promise<MakeVideoScreenshotResultType>;
|
|
logger: LoggerType;
|
|
}
|
|
): Promise<AttachmentType> {
|
|
const { contentType } = attachment;
|
|
|
|
const {
|
|
writeNewAttachmentData,
|
|
makeObjectUrl,
|
|
revokeObjectUrl,
|
|
getImageDimensions: getImageDimensionsFromURL,
|
|
makeImageThumbnail,
|
|
makeVideoScreenshot,
|
|
logger,
|
|
} = params;
|
|
|
|
if (
|
|
!isImageTypeSupported(contentType) &&
|
|
!isVideoTypeSupported(contentType)
|
|
) {
|
|
return attachment;
|
|
}
|
|
|
|
// If the attachment hasn't been downloaded yet, we won't have a path
|
|
if (!attachment.path) {
|
|
return attachment;
|
|
}
|
|
|
|
const localUrl = getLocalAttachmentUrl(attachment);
|
|
|
|
if (isImageTypeSupported(contentType)) {
|
|
if (attachment.thumbnail?.path) {
|
|
// Already generated thumbnail / width / height
|
|
return attachment;
|
|
}
|
|
|
|
try {
|
|
const { width, height } = await getImageDimensionsFromURL({
|
|
objectUrl: localUrl,
|
|
logger,
|
|
});
|
|
let thumbnail: LocalAttachmentV2Type | undefined;
|
|
|
|
if (options.generateThumbnail) {
|
|
const thumbnailBuffer = await blobToArrayBuffer(
|
|
await makeImageThumbnail({
|
|
size: THUMBNAIL_SIZE,
|
|
objectUrl: localUrl,
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
logger,
|
|
})
|
|
);
|
|
|
|
thumbnail = await writeNewAttachmentData(
|
|
new Uint8Array(thumbnailBuffer)
|
|
);
|
|
}
|
|
|
|
return {
|
|
...attachment,
|
|
width,
|
|
height,
|
|
thumbnail: thumbnail
|
|
? {
|
|
...thumbnail,
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
width: THUMBNAIL_SIZE,
|
|
height: THUMBNAIL_SIZE,
|
|
}
|
|
: undefined,
|
|
};
|
|
} catch (error) {
|
|
logger.error(
|
|
'captureDimensionsAndScreenshot:',
|
|
'error processing image; skipping screenshot generation',
|
|
toLogFormat(error)
|
|
);
|
|
return attachment;
|
|
}
|
|
}
|
|
|
|
strictAssert(isVideoTypeSupported(contentType), 'enforced by early return');
|
|
|
|
if (attachment.screenshot?.path) {
|
|
// Already generated screenshot / width / height
|
|
return attachment;
|
|
}
|
|
|
|
let screenshotObjectUrl: string | undefined;
|
|
try {
|
|
const { blob, duration } = await makeVideoScreenshot({
|
|
objectUrl: localUrl,
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
logger,
|
|
});
|
|
const screenshotBuffer = await blobToArrayBuffer(blob);
|
|
screenshotObjectUrl = makeObjectUrl(
|
|
screenshotBuffer,
|
|
THUMBNAIL_CONTENT_TYPE
|
|
);
|
|
const { width, height } = await getImageDimensionsFromURL({
|
|
objectUrl: screenshotObjectUrl,
|
|
logger,
|
|
});
|
|
const screenshot = await writeNewAttachmentData(
|
|
new Uint8Array(screenshotBuffer)
|
|
);
|
|
|
|
let thumbnail: LocalAttachmentV2Type | undefined;
|
|
if (options.generateThumbnail) {
|
|
const thumbnailBuffer = await blobToArrayBuffer(
|
|
await makeImageThumbnail({
|
|
size: THUMBNAIL_SIZE,
|
|
objectUrl: screenshotObjectUrl,
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
logger,
|
|
})
|
|
);
|
|
|
|
thumbnail = await writeNewAttachmentData(new Uint8Array(thumbnailBuffer));
|
|
}
|
|
|
|
return {
|
|
...attachment,
|
|
duration,
|
|
screenshot: {
|
|
...screenshot,
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
width,
|
|
height,
|
|
},
|
|
thumbnail: thumbnail
|
|
? {
|
|
...thumbnail,
|
|
contentType: THUMBNAIL_CONTENT_TYPE,
|
|
width: THUMBNAIL_SIZE,
|
|
height: THUMBNAIL_SIZE,
|
|
}
|
|
: undefined,
|
|
width,
|
|
height,
|
|
};
|
|
} catch (error) {
|
|
logger.error(
|
|
'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation',
|
|
toLogFormat(error)
|
|
);
|
|
return attachment;
|
|
} finally {
|
|
if (screenshotObjectUrl !== undefined) {
|
|
revokeObjectUrl(screenshotObjectUrl);
|
|
}
|
|
}
|
|
}
|