This commit is contained in:
seb
2026-07-31 20:37:43 +02:00
parent d5d30fb286
commit 9582370b0b
15 changed files with 174 additions and 70 deletions

20
src/utils/notify.js Normal file
View File

@@ -0,0 +1,20 @@
import { enqueueSnackbar } from 'notistack';
import { backendErrorFrom } from './backendError';
/** Show error toast and log to console for debugging. */
export function notifyError(err) {
const normalized = backendErrorFrom(err);
console.error('[error]', normalized.message, {
status: normalized.status,
path: normalized.path,
method: normalized.method,
err,
});
enqueueSnackbar(normalized.message, { variant: 'error' });
return normalized;
}
export function notifyErrorMessage(message) {
console.error('[error]', message);
enqueueSnackbar(message, { variant: 'error' });
}

View File

@@ -1,6 +1,9 @@
const MAX_EDGE = 1000;
const AVIF_MIME = 'image/avif';
const AVIF_QUALITY = 0.8;
const AVIF_QUALITY = 80;
let encodeAvif;
let resizeImage;
function scaledDimensions(width, height) {
const scale = Math.min(1, MAX_EDGE / width, MAX_EDGE / height);
@@ -10,8 +13,43 @@ function scaledDimensions(width, height) {
};
}
async function getAvifEncoder() {
if (!encodeAvif) {
const { default: encode } = await import('@jsquash/avif/encode');
encodeAvif = encode;
}
return encodeAvif;
}
async function getResizer() {
if (!resizeImage) {
const { default: resize } = await import('@jsquash/resize');
resizeImage = resize;
}
return resizeImage;
}
async function decodeToImageData(input) {
const bitmap = await createImageBitmap(input);
try {
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas nicht verfügbar.');
}
ctx.drawImage(bitmap, 0, 0);
return ctx.getImageData(0, 0, bitmap.width, bitmap.height);
} finally {
bitmap.close();
}
}
/**
* Resize to fit within 1000×1000 (aspect ratio kept, no upscale) and encode as AVIF.
* Resize to fit within 1000×1000 (aspect ratio kept, no upscale).
* Encodes as AVIF via @jsquash/avif (WASM).
* @param {Blob|File} input
* @returns {Promise<File>}
*/
@@ -23,39 +61,21 @@ export async function prepareArticlePhoto(input) {
throw new Error('Kein Bild.');
}
const bitmap = await createImageBitmap(input);
try {
const { width, height } = scaledDimensions(bitmap.width, bitmap.height);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const originalImageData = await decodeToImageData(input);
const { width, height } = scaledDimensions(
originalImageData.width,
originalImageData.height,
);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas nicht verfügbar.');
}
ctx.drawImage(bitmap, 0, 0, width, height);
const resize = await getResizer();
const imageData = await resize(originalImageData, { width, height });
const avifBlob = await new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => {
if (!blob || blob.type !== AVIF_MIME) {
reject(new Error('AVIF wird von diesem Browser nicht unterstützt.'));
return;
}
resolve(blob);
},
AVIF_MIME,
AVIF_QUALITY,
);
});
const encode = await getAvifEncoder();
const avifBuffer = await encode(imageData, { quality: AVIF_QUALITY });
const baseName = input instanceof File && input.name
? input.name.replace(/\.[^.]+$/, '')
: 'photo';
const baseName = input instanceof File && input.name
? input.name.replace(/\.[^.]+$/, '')
: 'photo';
return new File([avifBlob], `${baseName}.avif`, { type: AVIF_MIME });
} finally {
bitmap.close();
}
return new File([avifBuffer], `${baseName}.avif`, { type: AVIF_MIME });
}