// 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; 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; makeVideoScreenshot: (params: { objectUrl: string; contentType: MIMEType; logger: LoggerType; }) => Promise; logger: LoggerType; } ): Promise { 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); } } }