fix(nextcloud-status): grosse Logos beim Hochladen verkleinern, Doppelklick auf Jetzt pruefen sperren
Ein 2346-px-Logo liess die Linux-App beim Neuzeichnen (drehende Lade-Symbole) kurz einfrieren; Logos werden jetzt im Browser auf max. 256 px verkleinert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { shrinkImage } from './shrink-image';
|
||||
|
||||
function file(type: string, size = 10): File {
|
||||
return new File([new Uint8Array(size)], `logo.${type.split('/')[1]}`, { type });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('shrinkImage', () => {
|
||||
it('laesst GIFs unveraendert (koennen animiert sein)', async () => {
|
||||
const gif = file('image/gif');
|
||||
expect(await shrinkImage(gif)).toBe(gif);
|
||||
});
|
||||
|
||||
it('gibt ohne createImageBitmap das Original zurueck', async () => {
|
||||
vi.stubGlobal('createImageBitmap', undefined);
|
||||
const png = file('image/png');
|
||||
expect(await shrinkImage(png)).toBe(png);
|
||||
});
|
||||
|
||||
it('laesst kleine Bilder unveraendert', async () => {
|
||||
const close = vi.fn();
|
||||
vi.stubGlobal(
|
||||
'createImageBitmap',
|
||||
vi.fn(async () => ({ width: 200, height: 100, close })),
|
||||
);
|
||||
const png = file('image/png');
|
||||
expect(await shrinkImage(png)).toBe(png);
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('verkleinert grosse Bilder auf hoechstens 256 px an der laengsten Kante', async () => {
|
||||
vi.stubGlobal(
|
||||
'createImageBitmap',
|
||||
vi.fn(async () => ({ width: 2346, height: 1173, close: vi.fn() })),
|
||||
);
|
||||
const drawImage = vi.fn();
|
||||
const canvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => ({ drawImage, imageSmoothingQuality: 'low' }),
|
||||
toBlob: (cb: (b: Blob) => void) => cb(new Blob([new Uint8Array(5)], { type: 'image/png' })),
|
||||
};
|
||||
const orig = document.createElement.bind(document);
|
||||
vi.spyOn(document, 'createElement').mockImplementation((tag: string) =>
|
||||
tag === 'canvas' ? (canvas as unknown as HTMLCanvasElement) : orig(tag),
|
||||
);
|
||||
const big = file('image/jpeg', 50_000);
|
||||
const out = await shrinkImage(big);
|
||||
expect(canvas.width).toBe(256);
|
||||
expect(canvas.height).toBe(128);
|
||||
expect(out.type).toBe('image/png');
|
||||
expect(out.name).toBe('logo.png');
|
||||
expect(out.size).toBe(5);
|
||||
});
|
||||
|
||||
it('behaelt das Original, wenn das Ergebnis nicht kleiner wird', async () => {
|
||||
vi.stubGlobal(
|
||||
'createImageBitmap',
|
||||
vi.fn(async () => ({ width: 1000, height: 1000, close: vi.fn() })),
|
||||
);
|
||||
const canvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => ({ drawImage: vi.fn() }),
|
||||
toBlob: (cb: (b: Blob) => void) => cb(new Blob([new Uint8Array(100)])),
|
||||
};
|
||||
const orig = document.createElement.bind(document);
|
||||
vi.spyOn(document, 'createElement').mockImplementation((tag: string) =>
|
||||
tag === 'canvas' ? (canvas as unknown as HTMLCanvasElement) : orig(tag),
|
||||
);
|
||||
const small = file('image/png', 50);
|
||||
expect(await shrinkImage(small)).toBe(small);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Verkleinert ein hochgeladenes Bild im Browser, bevor es zum Server geht
|
||||
* (quick-261002). Logos werden nur etwa 40 Pixel hoch angezeigt; ein
|
||||
* 2000-Pixel-Bild muss die Linux-Webview bei jedem Neuzeichnen (z. B. drehende
|
||||
* Lade-Symbole) herunterrechnen, was die App kurz einfrieren laesst.
|
||||
*
|
||||
* - Bilder, die schon klein genug sind, und GIFs (evtl. animiert) bleiben unveraendert.
|
||||
* - Ergebnis als PNG (Transparenz bleibt), Seitenverhaeltnis bleibt.
|
||||
* - Ohne Canvas/`createImageBitmap` (alte Umgebung, Tests) oder bei Fehlern: Original.
|
||||
*/
|
||||
export const LOGO_MAX_EDGE = 256;
|
||||
|
||||
export async function shrinkImage(file: File, maxEdge = LOGO_MAX_EDGE): Promise<File> {
|
||||
if (file.type === 'image/gif') return file;
|
||||
if (typeof createImageBitmap !== 'function' || typeof document === 'undefined') return file;
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const { width, height } = bitmap;
|
||||
if (width <= maxEdge && height <= maxEdge) {
|
||||
bitmap.close();
|
||||
return file;
|
||||
}
|
||||
const scale = maxEdge / Math.max(width, height);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(width * scale));
|
||||
canvas.height = Math.max(1, Math.round(height * scale));
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
bitmap.close();
|
||||
return file;
|
||||
}
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
bitmap.close();
|
||||
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
|
||||
if (!blob || blob.size >= file.size) return file;
|
||||
const name = file.name.replace(/\.[^.]+$/, '') + '.png';
|
||||
return new File([blob], name, { type: 'image/png' });
|
||||
} catch {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user