// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { AvatarColorType } from '../types/Colors'; import { AvatarColorMap } from '../types/Colors'; import type { AvatarDataType } from '../types/Avatar'; import { canvasToBytes } from './canvasToBytes'; import { getFittedFontSize } from './avatarTextSizeCalculator'; const CANVAS_SIZE = 1024; function getAvatarColor(color: AvatarColorType): { bg: string; fg: string } { return AvatarColorMap.get(color) || { bg: 'black', fg: 'white' }; } function setCanvasBackground( bg: string, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement ): void { context.fillStyle = bg; context.fillRect(0, 0, canvas.width, canvas.height); } async function drawImage( src: string, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement ): Promise { const image = new Image(); image.src = src; await image.decode(); // eslint-disable-next-line no-param-reassign canvas.width = image.width; // eslint-disable-next-line no-param-reassign canvas.height = image.height; context.drawImage(image, 0, 0); } async function getFont(text: string): Promise { const font = new window.FontFace( 'Inter', 'url("fonts/inter-v3.10/Inter-Regular.woff2")' ); await font.load(); const measurerCanvas = document.createElement('canvas'); measurerCanvas.width = CANVAS_SIZE; measurerCanvas.height = CANVAS_SIZE; const measurerContext = measurerCanvas.getContext('2d'); if (!measurerContext) { throw new Error('getFont: could not get canvas rendering context'); } const fontSize = getFittedFontSize(CANVAS_SIZE, text, candidateFontSize => { const candidateFont = `${candidateFontSize}px Inter`; measurerContext.font = candidateFont; const { actualBoundingBoxLeft, actualBoundingBoxRight, actualBoundingBoxAscent, actualBoundingBoxDescent, } = measurerContext.measureText(text); const width = Math.abs(actualBoundingBoxLeft) + Math.abs(actualBoundingBoxRight); const height = Math.abs(actualBoundingBoxAscent) + Math.abs(actualBoundingBoxDescent); return { height, width }; }); return `${fontSize}px Inter`; } export async function avatarDataToBytes( avatarData: AvatarDataType ): Promise { const canvas = document.createElement('canvas'); canvas.width = CANVAS_SIZE; canvas.height = CANVAS_SIZE; const context = canvas.getContext('2d'); if (!context) { throw new Error( 'avatarDataToBytes: could not get canvas rendering context' ); } const { color, icon, imagePath, text } = avatarData; if (imagePath) { await drawImage( window.Signal?.Migrations ? window.Signal.Migrations.getAbsoluteAvatarPath(imagePath) : imagePath, context, canvas ); } else if (color && text) { const { bg, fg } = getAvatarColor(color); const textToWrite = text.toLocaleUpperCase(); setCanvasBackground(bg, context, canvas); context.fillStyle = fg; const font = await getFont(textToWrite); context.font = font; context.textBaseline = 'middle'; context.textAlign = 'center'; context.fillText(textToWrite, CANVAS_SIZE / 2, CANVAS_SIZE / 2 + 30); } else if (color && icon) { const iconPath = `images/avatars/avatar_${icon}.svg`; await drawImage(iconPath, context, canvas); context.globalCompositeOperation = 'destination-over'; const { bg } = getAvatarColor(color); setCanvasBackground(bg, context, canvas); } return canvasToBytes(canvas); }