Refactor image metadata handling for user color embedding

- Updated useUserColor hook to support multiple image formats (PNG, WebP, JPEG, GIF) for color embedding and extraction.
- Introduced unified image metadata utilities in imageMetadata.ts for detecting formats and handling color operations.
- Added GIF metadata utilities in gifMetadata.ts for reading and writing user color.
- Implemented JPEG metadata utilities in jpegMetadata.ts for embedding and extracting user color.
- Created WebP metadata utilities in webpMetadata.ts for handling user color in WebP images.
- Enhanced error handling and logging for unsupported formats and operations.
This commit is contained in:
2026-02-06 15:27:15 +11:00
parent 516000a25f
commit bfbdf98468
5 changed files with 1141 additions and 14 deletions

View File

@@ -3,10 +3,18 @@ import { MatrixClient } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { useMediaAuthentication } from './useMediaAuthentication';
import { mxcUrlToHttp } from '../utils/matrix';
import { embedColorInPNG, extractColorFromPNG, fetchAndExtractColor, removeColorFromPNG } from '../utils/pngMetadata';
import {
embedColorInImage,
extractColorFromImage,
removeColorFromImage,
fetchAndExtractColor,
detectImageFormat,
getMimeType,
getExtension,
} from '../utils/imageMetadata';
/**
* Fetches the user's current avatar as raw PNG data
* Fetches the user's current avatar as raw image data
*/
async function fetchAvatarData(
mx: MatrixClient,
@@ -26,8 +34,8 @@ async function fetchAvatarData(
}
/**
* Hook to manage the user's chosen profile color, stored in avatar PNG metadata
* The color is embedded in the PNG tEXt chunk and syncs via the avatar
* Hook to manage the user's chosen profile color, stored in avatar image metadata
* The color is embedded in PNG tEXt chunk or WebP XMP chunk and syncs via the avatar
* @returns The current color, a setter function, and loading state
*/
export function useUserColor(): [
@@ -102,23 +110,32 @@ export function useUserColor(): [
console.log('Original avatar size:', avatarData.byteLength, 'bytes');
// Modify PNG metadata
// Detect image format
const format = detectImageFormat(avatarData);
console.log('Detected image format:', format);
if (format === 'unknown') {
console.error('Unsupported avatar image format - must be PNG, WebP, JPEG, or GIF');
throw new Error('Unsupported image format');
}
// Modify image metadata
let newAvatarData: Uint8Array | null;
if (newColor) {
newAvatarData = embedColorInPNG(avatarData, newColor);
newAvatarData = embedColorInImage(avatarData, newColor);
} else {
newAvatarData = removeColorFromPNG(avatarData);
newAvatarData = removeColorFromImage(avatarData);
}
if (!newAvatarData) {
console.error('Failed to modify avatar PNG metadata - not a valid PNG?');
throw new Error('Failed to modify PNG metadata');
console.error('Failed to modify avatar image metadata');
throw new Error('Failed to modify image metadata');
}
console.log('Modified avatar size:', newAvatarData.byteLength, 'bytes');
// Verify the color was embedded correctly before uploading
const verifyColor = extractColorFromPNG(newAvatarData);
const verifyColor = extractColorFromImage(newAvatarData);
console.log('Verification - embedded color:', verifyColor);
if (newColor && verifyColor !== newColor) {
@@ -126,11 +143,13 @@ export function useUserColor(): [
throw new Error('Color embedding verification failed');
}
// Upload modified avatar
const blob = new Blob([newAvatarData], { type: 'image/png' });
// Upload modified avatar with correct MIME type
const mimeType = getMimeType(format);
const extension = getExtension(format);
const blob = new Blob([newAvatarData], { type: mimeType });
const uploadResponse = await mx.uploadContent(blob, {
name: 'avatar.png',
type: 'image/png',
name: `avatar.${extension}`,
type: mimeType,
});
console.log('Uploaded new avatar:', uploadResponse.content_uri);