Improve handling of animated images, add support for AVIF animations (#30932)
* Only set MSC4230 is_animated flag if we are able to tell if the media is animated Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Set blob type correctly to not need to weave the mimetype around Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use ImageDecoder to determine whether media is animated or not, adding support for AVIF and other formats Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -32,42 +32,55 @@ describe("Image", () => {
|
||||
|
||||
describe("blobIsAnimated", () => {
|
||||
it("Animated GIF", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "animated-logo.gif"))]);
|
||||
expect(await blobIsAnimated("image/gif", img)).toBeTruthy();
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "animated-logo.gif"))], {
|
||||
type: "image/gif",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Static GIF", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "static-logo.gif"))]);
|
||||
expect(await blobIsAnimated("image/gif", img)).toBeFalsy();
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "static-logo.gif"))], {
|
||||
type: "image/gif",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Animated WEBP", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "animated-logo.webp"))]);
|
||||
expect(await blobIsAnimated("image/webp", img)).toBeTruthy();
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "animated-logo.webp"))], {
|
||||
type: "image/webp",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Static WEBP", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "static-logo.webp"))]);
|
||||
expect(await blobIsAnimated("image/webp", img)).toBeFalsy();
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "static-logo.webp"))], {
|
||||
type: "image/webp",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Static WEBP in extended file format", async () => {
|
||||
const img = new Blob([
|
||||
fs.readFileSync(path.resolve(__dirname, "images", "static-logo-extended-file-format.webp")),
|
||||
]);
|
||||
expect(await blobIsAnimated("image/webp", img)).toBeFalsy();
|
||||
const img = new Blob(
|
||||
[fs.readFileSync(path.resolve(__dirname, "images", "static-logo-extended-file-format.webp"))],
|
||||
{ type: "image/webp" },
|
||||
);
|
||||
expect(await blobIsAnimated(img)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Animated PNG", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "animated-logo.apng"))]);
|
||||
expect(await blobIsAnimated("image/png", img)).toBeTruthy();
|
||||
expect(await blobIsAnimated("image/apng", img)).toBeTruthy();
|
||||
const pngBlob = img.slice(0, img.size, "image/png");
|
||||
const apngBlob = img.slice(0, img.size, "image/apng");
|
||||
expect(await blobIsAnimated(pngBlob)).toBeTruthy();
|
||||
expect(await blobIsAnimated(apngBlob)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Static PNG", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(__dirname, "images", "static-logo.png"))]);
|
||||
expect(await blobIsAnimated("image/png", img)).toBeFalsy();
|
||||
expect(await blobIsAnimated("image/apng", img)).toBeFalsy();
|
||||
const pngBlob = img.slice(0, img.size, "image/png");
|
||||
const apngBlob = img.slice(0, img.size, "image/apng");
|
||||
expect(await blobIsAnimated(pngBlob)).toBeFalsy();
|
||||
expect(await blobIsAnimated(apngBlob)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper.ts";
|
||||
import { stubClient } from "../../test-utils";
|
||||
|
||||
describe("MediaEventHelper", () => {
|
||||
it("should set the mime type on the blob based on the event metadata", async () => {
|
||||
stubClient();
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.image",
|
||||
body: "image.png",
|
||||
info: {
|
||||
mimetype: "image/png",
|
||||
size: 1234,
|
||||
w: 100,
|
||||
h: 100,
|
||||
thumbnail_info: {
|
||||
mimetype: "image/png",
|
||||
},
|
||||
thumbnail_url: "mxc://matrix.org/thumbnail",
|
||||
},
|
||||
url: "mxc://matrix.org/abcdef",
|
||||
},
|
||||
});
|
||||
const helper = new MediaEventHelper(event);
|
||||
|
||||
const blob = await helper.thumbnailBlob.value;
|
||||
expect(blob?.type).toBe(event.getContent().info.thumbnail_info?.mimetype);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user