feat(user-avatar): users can now edit there image (image is taken from OIDC picture)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Client-side image resize utility.
|
||||
*
|
||||
* Accepts a File/Blob, resizes it to fit within maxSize × maxSize pixels
|
||||
* (never upscales), and returns a data URI (WebP preferred, JPEG fallback).
|
||||
*
|
||||
* Used by the profile page before uploading an avatar image so that
|
||||
* data URIs stay well within the 512 KiB backend limit.
|
||||
*/
|
||||
|
||||
/** Accepted MIME types for avatar uploads. */
|
||||
const ACCEPTED_TYPES = new Set(['image/png', 'image/webp', 'image/jpeg']);
|
||||
|
||||
/**
|
||||
* Load an image File/Blob as a data URL via FileReader.
|
||||
* @param {File | Blob} file
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function _readAsDataUrl(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(/** @type {string} */ (reader.result));
|
||||
reader.onerror = () => reject(new Error('FileReader failed'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a data URL into an HTMLImageElement (waits for `onload`).
|
||||
* @param {string} src
|
||||
* @returns {Promise<HTMLImageElement>}
|
||||
*/
|
||||
function _loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error('Image failed to load'));
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a canvas to a data URI, preferring WebP at quality 0.85.
|
||||
* Falls back to JPEG if the browser does not support WebP encoding.
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @returns {string}
|
||||
*/
|
||||
function _canvasToDataUri(canvas) {
|
||||
const webp = canvas.toDataURL('image/webp', 0.85);
|
||||
// toDataURL returns a PNG if the MIME type is not supported — detect by prefix
|
||||
if (webp.startsWith('data:image/webp')) return webp;
|
||||
return canvas.toDataURL('image/jpeg', 0.85);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize an image File to fit within maxSize × maxSize, then return
|
||||
* a data URI (WebP at quality 0.85, or JPEG as fallback).
|
||||
*
|
||||
* - Images already within maxSize × maxSize are not upscaled.
|
||||
* - Only `image/png`, `image/webp`, and `image/jpeg` are accepted;
|
||||
* all other MIME types throw an Error.
|
||||
*
|
||||
* @param {File} file Image file to resize
|
||||
* @param {number} [maxSize=512] Maximum width and height in pixels
|
||||
* @returns {Promise<string>} data URI of the (possibly resized) image
|
||||
*/
|
||||
export async function resizeImageToDataUrl(file, maxSize = 104) {
|
||||
if (!ACCEPTED_TYPES.has(file.type)) {
|
||||
throw new Error(`Unsupported image type: ${file.type}. Accepted: PNG, WebP, JPEG.`);
|
||||
}
|
||||
|
||||
const dataUrl = await _readAsDataUrl(file);
|
||||
const img = await _loadImage(dataUrl);
|
||||
|
||||
const { naturalWidth: w, naturalHeight: h } = img;
|
||||
|
||||
// Compute output dimensions — scale down proportionally if needed, never upscale
|
||||
let outW = w;
|
||||
let outH = h;
|
||||
if (w > maxSize || h > maxSize) {
|
||||
const ratio = Math.min(maxSize / w, maxSize / h);
|
||||
outW = Math.round(w * ratio);
|
||||
outH = Math.round(h * ratio);
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = outW;
|
||||
canvas.height = outH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not get 2D canvas context');
|
||||
ctx.drawImage(img, 0, 0, outW, outH);
|
||||
|
||||
return _canvasToDataUri(canvas);
|
||||
}
|
||||
Reference in New Issue
Block a user