v4: a still is scaled from its first frame alone, a second image in its file made the copy flash — an avatar came out as a WebP of two frames looping at 40 ms each, the picture then a blank. A still's file may hold more than one image, a phone's JPEG carrying a gain map or a depth map after the photo, and ffmpeg reads each as a frame: only the JPEG and PNG copies were held to one, the WebP keeping them all as it does a GIF's. scalableImage (lib/upload.ts) now says whether the image is an animation, which a GIF alone is, and scaleImage (lib/ffmpeg.ts) writes one frame of anything else, in every format; the image proxy scales through the same call. The copies already made stay, a copy being current while it has its upload's date: uploads/scaled is to be deleted once on an instance that ran the previous version, everything in it being made again on first request.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 52s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 26s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-10 19:40:14 +00:00
parent d2b7d21824
commit cbb770bff8
5 changed files with 68 additions and 25 deletions

View File

@@ -38,6 +38,24 @@ test("avatars: upload, serve, and 404 when there is none", async () => {
);
});
test("avatars: a still holding a second image is scaled to its first alone", async () => {
if (!await hasFfmpeg()) return;
const user = await createUser();
// As a phone's photo carries a gain map after it, which ffmpeg reads as a
// second frame.
const photo = await makeImage(600, 400, "red");
const extra = await makeImage(150, 100, "white");
const both = new Uint8Array(photo.length + extra.length);
both.set(photo);
both.set(extra, photo.length);
await put("/api/me/avatar", { auth: user.auth, form: formWithFile(both) });
const small = await get(`/api/users/${user.id}/avatar?w=128`);
assertEquals(small.headers.get("content-type"), "image/webp");
assertEquals(webpWidth(small.bytes), 128);
assertFalse(new TextDecoder().decode(small.bytes).includes("ANMF"));
});
test("avatars: `?w=` answers a scaled copy, made again from a new avatar", async () => {
if (!await hasFfmpeg()) return;
const user = await createUser();