1 import { getImage } from '../api/images';
2 import { REGEX_IMAGE_EXTENSION } from '../constants';
3 import { createUrl } from '../fetch/helpers';
4 import { toBase64 } from './file';
7 * Use to encode Image URI when loading images
9 export const encodeImageUri = (url: string) => {
10 // Only replace spaces for the moment
11 return url.trim().replaceAll(' ', '%20');
15 * Forge a url to load an image through the Proton proxy
17 export const forgeImageURL = ({
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();
35 * Convert url to Image
37 export const toImage = (url: string, crossOrigin = true): Promise<HTMLImageElement> => {
38 return new Promise((resolve, reject) => {
40 return reject(new Error('url required'));
42 const image = new Image();
43 image.onload = () => {
46 image.onerror = reject;
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
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.
61 image.crossOrigin = 'anonymous';
63 image.referrerPolicy = 'no-referrer';
68 interface ResizeImageProps {
70 * Base64 representation of image to be resized.
74 * Maximum amount of pixels for the width of the resized image.
78 * Maximum amount of pixels for the height of the resized image.
82 * Mime type of the resulting resized image.
84 finalMimeType?: string;
86 * A Number between 0 and 1 indicating image quality if the requested type is image/jpeg or image/webp.
88 encoderOptions?: number;
90 * If both maxHeight and maxWidth are specified, pick the smaller resize factor.
94 * Does the image needs to be loaded with the crossOrigin attribute
96 crossOrigin?: boolean;
98 * Is transparency allowed?
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
108 export const resizeImage = async ({
112 finalMimeType = 'image/jpeg',
116 transparencyAllowed = true,
117 }: ResizeImageProps) => {
118 const image = await toImage(original, crossOrigin);
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) {
130 const invert = maxWidth && maxHeight && bigResize;
132 if (widthRatio >= heightRatio === !invert) {
133 height /= widthRatio;
136 width /= heightRatio;
140 canvas.width = width;
141 canvas.height = height;
143 if (transparencyAllowed) {
144 ctx?.drawImage(image, 0, 0, width, height);
146 ctx.fillStyle = '#FFFFFF';
147 ctx?.fillRect(0, 0, canvas.width, canvas.height);
148 ctx?.drawImage(image, 0, 0, width, height);
151 return canvas.toDataURL(finalMimeType, encoderOptions);
155 * Extract the mime and base64 str from a base64 image.
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.
166 const toUint8Array = (base64str: string) => {
167 const bstr = atob(base64str);
169 const u8arr = new Uint8Array(n);
171 u8arr[n] = bstr.charCodeAt(n);
177 * Convert a data URL to a Blob Object
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
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
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({
201 finalMimeType: mimeType,
204 const { size } = new Blob([resized]);
206 if (size <= maxSize) {
210 return process(resized, Math.round(maxWidth * 0.9), Math.round(maxHeight * 0.9));
213 const { height, width } = await toImage(base64str);
214 return process(base64str, width, height);
218 * Returns true if the URL is an inline embedded image.
220 export const isInlineEmbedded = (src = '') => src.startsWith('data:');
223 * Returns true if the URL is an embedded image.
225 export const isEmbedded = (src = '') => src.startsWith('cid:');
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
238 export const formatImage = (value = '') => {
241 REGEX_IMAGE_EXTENSION.test(value) ||
242 value.startsWith('data:') ||
243 value.startsWith('http://') ||
244 value.startsWith('https://')
249 return `data:image/png;base64,${value}`;