Merge branch 'IDTEAM-1.26.0' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / helpers / image.ts
blob7bd83ac5e064dd3a75b7be9b4fa8831dd3986ea8
1 import { getImage } from '../api/images';
2 import { REGEX_IMAGE_EXTENSION } from '../constants';
3 import { createUrl } from '../fetch/helpers';
4 import { toBase64 } from './file';
6 /**
7  * Use to encode Image URI when loading images
8  */
9 export const encodeImageUri = (url: string) => {
10     // Only replace spaces for the moment
11     return url.trim().replaceAll(' ', '%20');
14 /**
15  * Forge a url to load an image through the Proton proxy
16  */
17 export const forgeImageURL = ({
18     apiUrl,
19     url,
20     uid,
21     origin,
22 }: {
23     apiUrl: string;
24     url: string;
25     uid: string;
26     origin: string;
27 }) => {
28     const config = getImage(url, 0, uid);
29     const prefixedUrl = `${apiUrl}/${config.url}`; // api/ is required to set the AUTH cookie
30     const urlToLoad = createUrl(prefixedUrl, config.params, origin);
31     return urlToLoad.toString();
34 /**
35  * Convert url to Image
36  */
37 export const toImage = (url: string, crossOrigin = true): Promise<HTMLImageElement> => {
38     return new Promise((resolve, reject) => {
39         if (!url) {
40             return reject(new Error('url required'));
41         }
42         const image = new Image();
43         image.onload = () => {
44             resolve(image);
45         };
46         image.onerror = reject;
48         /**
49          * allow external images to be used in a canvas as if they were loaded
50          * from the current origin without sending any user credentials.
51          * (otherwise canvas.toDataURL in resizeImage will throw complaining that the canvas is tainted)
52          * An error will be thrown if the requested resource hasn't specified an appropriate CORS policy
53          * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
54          *
55          * However, on contact side, we are now using the proxy to load images.
56          * If the user choose explicitly not to use it or load the image using its default URL because loading through proxy failed,
57          * we consider that he really wants to load the image.
58          * Removing the crossOrigin attribute will allow us to load the image on more cases.
59          */
60         if (crossOrigin) {
61             image.crossOrigin = 'anonymous';
62         }
63         image.referrerPolicy = 'no-referrer';
64         image.src = url;
65     });
68 interface ResizeImageProps {
69     /**
70      * Base64 representation of image to be resized.
71      */
72     original: string;
73     /**
74      * Maximum amount of pixels for the width of the resized image.
75      */
76     maxWidth?: number;
77     /**
78      * Maximum amount of pixels for the height of the resized image.
79      */
80     maxHeight?: number;
81     /**
82      * Mime type of the resulting resized image.
83      */
84     finalMimeType?: string;
85     /**
86      * A Number between 0 and 1 indicating image quality if the requested type is image/jpeg or image/webp.
87      */
88     encoderOptions?: number;
89     /**
90      * If both maxHeight and maxWidth are specified, pick the smaller resize factor.
91      */
92     bigResize?: boolean;
93     /**
94      * Does the image needs to be loaded with the crossOrigin attribute
95      */
96     crossOrigin?: boolean;
97     /**
98      * Is transparency allowed?
99      */
100     transparencyAllowed?: boolean;
104  * Resizes a picture to a maximum height/width (preserving height/width ratio). When both dimensions are specified,
105  * two resizes are possible: we pick the one with the bigger resize factor (so that both max dimensions are respected in the resized image)
106  * @dev If maxWidth or maxHeight are equal to zero, the corresponding dimension is ignored
107  */
108 export const resizeImage = async ({
109     original,
110     maxWidth = 0,
111     maxHeight = 0,
112     finalMimeType = 'image/jpeg',
113     encoderOptions = 1,
114     bigResize = false,
115     crossOrigin = true,
116     transparencyAllowed = true,
117 }: ResizeImageProps) => {
118     const image = await toImage(original, crossOrigin);
119     // Resize the image
120     let { width, height } = image;
122     const canvas = document.createElement('canvas');
123     const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
124     const [widthRatio, heightRatio] = [maxWidth && width / maxWidth, maxHeight && height / maxHeight].map(Number);
126     if (widthRatio <= 1 && heightRatio <= 1) {
127         return image.src;
128     }
130     const invert = maxWidth && maxHeight && bigResize;
132     if (widthRatio >= heightRatio === !invert) {
133         height /= widthRatio;
134         width = maxWidth;
135     } else {
136         width /= heightRatio;
137         height = maxHeight;
138     }
140     canvas.width = width;
141     canvas.height = height;
143     if (transparencyAllowed) {
144         ctx?.drawImage(image, 0, 0, width, height);
145     } else {
146         ctx.fillStyle = '#FFFFFF';
147         ctx?.fillRect(0, 0, canvas.width, canvas.height);
148         ctx?.drawImage(image, 0, 0, width, height);
149     }
151     return canvas.toDataURL(finalMimeType, encoderOptions);
155  * Extract the mime and base64 str from a base64 image.
156  */
157 export const extractBase64Image = (str = '') => {
158     const [mimeInfo = '', base64 = ''] = (str || '').split(',');
159     const [, mime = ''] = mimeInfo.match(/:(.*?);/) || [];
160     return { mime, base64 };
164  * Convert a base 64 str to an uint8 array.
165  */
166 const toUint8Array = (base64str: string) => {
167     const bstr = atob(base64str);
168     let n = bstr.length;
169     const u8arr = new Uint8Array(n);
170     while (n--) {
171         u8arr[n] = bstr.charCodeAt(n);
172     }
173     return u8arr;
177  * Convert a data URL to a Blob Object
178  */
179 export const toFile = (base64str: string, filename = 'file') => {
180     const { base64, mime } = extractBase64Image(base64str);
181     return new File([toUint8Array(base64)], filename, { type: mime });
185  * Convert a data URL to a Blob Object
186  */
187 export const toBlob = (base64str: string) => {
188     const { base64, mime } = extractBase64Image(base64str);
189     return new Blob([toUint8Array(base64)], { type: mime });
193  * Down size image to reach the max size limit
194  */
195 export const downSize = async (base64str: string, maxSize: number, mimeType = 'image/jpeg', encoderOptions = 1) => {
196     const process = async (source: string, maxWidth: number, maxHeight: number): Promise<string> => {
197         const resized = await resizeImage({
198             original: source,
199             maxWidth,
200             maxHeight,
201             finalMimeType: mimeType,
202             encoderOptions,
203         });
204         const { size } = new Blob([resized]);
206         if (size <= maxSize) {
207             return resized;
208         }
210         return process(resized, Math.round(maxWidth * 0.9), Math.round(maxHeight * 0.9));
211     };
213     const { height, width } = await toImage(base64str);
214     return process(base64str, width, height);
218  * Returns true if the URL is an inline embedded image.
219  */
220 export const isInlineEmbedded = (src = '') => src.startsWith('data:');
223  * Returns true if the URL is an embedded image.
224  */
225 export const isEmbedded = (src = '') => src.startsWith('cid:');
228  * Resize image file
229  */
230 export const resize = async (fileImage: File, maxSize: number) => {
231     const base64str = await toBase64(fileImage);
232     return downSize(base64str, maxSize, fileImage.type);
236  * Prepare image source to be display
237  */
238 export const formatImage = (value = '') => {
239     if (
240         !value ||
241         REGEX_IMAGE_EXTENSION.test(value) ||
242         value.startsWith('data:') ||
243         value.startsWith('http://') ||
244         value.startsWith('https://')
245     ) {
246         return value;
247     }
249     return `data:image/png;base64,${value}`;