Send and respect MSC4230 is_animated flag (#28513)

* PoC implementation for is_animated m.image flag

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update MSC reference

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-11-22 13:58:37 +00:00 committed by GitHub
parent 7d20bd4d06
commit 3d6664109b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 26 additions and 18 deletions

View file

@ -22,6 +22,13 @@ declare module "matrix-js-sdk/src/types" {
[BLURHASH_FIELD]?: string; [BLURHASH_FIELD]?: string;
} }
export interface ImageInfo {
/**
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4230
*/
"org.matrix.msc4230.is_animated"?: boolean;
}
export interface StateEvents { export interface StateEvents {
// Jitsi-backed video room state events // Jitsi-backed video room state events
[JitsiCallMemberEventType]: JitsiCallMemberContent; [JitsiCallMemberEventType]: JitsiCallMemberContent;

View file

@ -56,6 +56,7 @@ import { createThumbnail } from "./utils/image-media";
import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer"; import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer";
import { doMaybeLocalRoomAction } from "./utils/local-room"; import { doMaybeLocalRoomAction } from "./utils/local-room";
import { SdkContextClass } from "./contexts/SDKContext"; import { SdkContextClass } from "./contexts/SDKContext";
import { blobIsAnimated } from "./utils/Image.ts";
// scraped out of a macOS hidpi (5660ppm) screenshot png // scraped out of a macOS hidpi (5660ppm) screenshot png
// 5669 px (x-axis) , 5669 px (y-axis) , per metre // 5669 px (x-axis) , 5669 px (y-axis) , per metre
@ -150,15 +151,20 @@ async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imag
thumbnailType = "image/jpeg"; thumbnailType = "image/jpeg";
} }
// We don't await this immediately so it can happen in the background
const isAnimatedPromise = blobIsAnimated(imageFile.type, imageFile);
const imageElement = await loadImageElement(imageFile); const imageElement = await loadImageElement(imageFile);
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType); const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
const imageInfo = result.info; const imageInfo = result.info;
imageInfo["org.matrix.msc4230.is_animated"] = await isAnimatedPromise;
// For lesser supported image types, always include the thumbnail even if it is larger // For lesser supported image types, always include the thumbnail even if it is larger
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) { if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) {
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from. // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size; const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size!;
if ( if (
// image is small enough already // image is small enough already
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||

View file

@ -275,7 +275,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
const content = this.props.mxEvent.getContent<ImageContent>(); const content = this.props.mxEvent.getContent<ImageContent>();
let isAnimated = mayBeAnimated(content.info?.mimetype); let isAnimated = content.info?.["org.matrix.msc4230.is_animated"] ?? mayBeAnimated(content.info?.mimetype);
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail. // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
@ -298,8 +298,15 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
try { try {
const blob = await this.props.mediaEventHelper!.sourceBlob.value; // If we didn't receive the MSC4230 is_animated flag
if (!(await blobIsAnimated(content.info?.mimetype, blob))) { // then we need to check if the image is animated by downloading it.
if (
content.info?.["org.matrix.msc4230.is_animated"] === false ||
!(await blobIsAnimated(
content.info?.mimetype,
await this.props.mediaEventHelper!.sourceBlob.value,
))
) {
isAnimated = false; isAnimated = false;
} }

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { EncryptedFile } from "matrix-js-sdk/src/types"; import { ImageInfo } from "matrix-js-sdk/src/types";
import { BlurhashEncoder } from "../BlurhashEncoder"; import { BlurhashEncoder } from "../BlurhashEncoder";
@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
interface IThumbnail { interface IThumbnail {
info: { info: ImageInfo;
thumbnail_info?: {
w: number;
h: number;
mimetype: string;
size: number;
};
w: number;
h: number;
[BLURHASH_FIELD]?: string;
thumbnail_url?: string;
thumbnail_file?: EncryptedFile;
};
thumbnail: Blob; thumbnail: Blob;
} }