- Updated `useUserColor` hook to support fetching and embedding user color from avatar metadata with authentication. - Introduced `useUserBanner` hook for managing user banners stored in avatar metadata, including fetching and embedding functionality. - Added `fetchAndExtractMetadata` utility to retrieve both color and banner from images. - Implemented `CustomStatusDialog` component for users to set and clear custom status messages. - Modified `SettingsTab` to include custom status functionality and improved user experience. - Enhanced image metadata utilities to support banner extraction and embedding in PNG format. - Updated sidebar settings to handle user profile changes more gracefully.
325 lines
9.1 KiB
TypeScript
325 lines
9.1 KiB
TypeScript
/**
|
|
* Unified image metadata utilities for embedding/extracting user color and banner
|
|
* Supports PNG, WebP, JPEG, and GIF formats
|
|
*/
|
|
|
|
/* eslint-disable no-plusplus */
|
|
/* eslint-disable no-console */
|
|
|
|
import {
|
|
embedColorInPNG,
|
|
extractColorFromPNG,
|
|
removeColorFromPNG,
|
|
embedMetadataInPNG,
|
|
extractMetadataFromPNG,
|
|
extractBannerFromPNG,
|
|
} from './pngMetadata';
|
|
import { embedColorInWebP, extractColorFromWebP, removeColorFromWebP } from './webpMetadata';
|
|
import { embedColorInJPEG, extractColorFromJPEG, removeColorFromJPEG } from './jpegMetadata';
|
|
import { embedColorInGIF, extractColorFromGIF, removeColorFromGIF } from './gifMetadata';
|
|
|
|
export type ImageMetadata = {
|
|
color?: string;
|
|
banner?: string;
|
|
};
|
|
|
|
/** PNG signature bytes */
|
|
const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
|
|
/** RIFF/WebP signature bytes */
|
|
const RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
|
|
const WEBP_OFFSET_SIGNATURE = [0x57, 0x45, 0x42, 0x50]; // "WEBP" at offset 8
|
|
|
|
/** JPEG signature */
|
|
const JPEG_SIGNATURE = [0xFF, 0xD8];
|
|
|
|
/** GIF signatures */
|
|
const GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; // "GIF87a"
|
|
const GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; // "GIF89a"
|
|
|
|
export type ImageFormat = 'png' | 'webp' | 'jpeg' | 'gif' | 'unknown';
|
|
|
|
/**
|
|
* Detect image format from raw data
|
|
*/
|
|
export function detectImageFormat(data: ArrayBuffer | Uint8Array): ImageFormat {
|
|
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
|
|
if (bytes.length < 12) return 'unknown';
|
|
|
|
// Check PNG signature
|
|
let isPNG = true;
|
|
for (let i = 0; i < 8; i++) {
|
|
if (bytes[i] !== PNG_SIGNATURE[i]) {
|
|
isPNG = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isPNG) return 'png';
|
|
|
|
// Check WebP signature (RIFF + WEBP)
|
|
let isRIFF = true;
|
|
for (let i = 0; i < 4; i++) {
|
|
if (bytes[i] !== RIFF_SIGNATURE[i]) {
|
|
isRIFF = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isRIFF) {
|
|
let isWebP = true;
|
|
for (let i = 0; i < 4; i++) {
|
|
if (bytes[8 + i] !== WEBP_OFFSET_SIGNATURE[i]) {
|
|
isWebP = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isWebP) return 'webp';
|
|
}
|
|
|
|
// Check JPEG signature (FFD8)
|
|
if (bytes[0] === JPEG_SIGNATURE[0] && bytes[1] === JPEG_SIGNATURE[1]) {
|
|
return 'jpeg';
|
|
}
|
|
|
|
// Check GIF signatures
|
|
let isGIF87a = true;
|
|
for (let i = 0; i < 6; i++) {
|
|
if (bytes[i] !== GIF87A_SIGNATURE[i]) {
|
|
isGIF87a = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isGIF87a) return 'gif';
|
|
|
|
let isGIF89a = true;
|
|
for (let i = 0; i < 6; i++) {
|
|
if (bytes[i] !== GIF89A_SIGNATURE[i]) {
|
|
isGIF89a = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isGIF89a) return 'gif';
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Extract paarrot color from image data (PNG, WebP, JPEG, or GIF)
|
|
* @param imageData - Raw image data as ArrayBuffer or Uint8Array
|
|
* @returns The color string if found, or undefined
|
|
*/
|
|
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
|
|
const format = detectImageFormat(imageData);
|
|
|
|
switch (format) {
|
|
case 'png':
|
|
return extractColorFromPNG(imageData);
|
|
case 'webp':
|
|
return extractColorFromWebP(imageData);
|
|
case 'jpeg':
|
|
return extractColorFromJPEG(imageData);
|
|
case 'gif':
|
|
return extractColorFromGIF(imageData);
|
|
default:
|
|
console.warn('[extractColorFromImage] Unknown image format');
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract paarrot banner URL from image data (any supported format)
|
|
* @param imageData - Raw image data as ArrayBuffer or Uint8Array
|
|
* @returns The banner MXC URL if found, or undefined
|
|
*/
|
|
export function extractBannerFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
|
|
const format = detectImageFormat(imageData);
|
|
|
|
switch (format) {
|
|
case 'png':
|
|
return extractBannerFromPNG(imageData);
|
|
// For now, only PNG supports banner. Add other formats later
|
|
default:
|
|
console.warn('[extractBannerFromImage] Format not yet supported for banner');
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract all paarrot metadata from image data (any supported format)
|
|
* @param imageData - Raw image data as ArrayBuffer or Uint8Array
|
|
* @returns Object with color and banner if found
|
|
*/
|
|
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
|
|
const format = detectImageFormat(imageData);
|
|
|
|
switch (format) {
|
|
case 'png':
|
|
return extractMetadataFromPNG(imageData);
|
|
// For other formats, extract color only for now
|
|
case 'webp':
|
|
case 'jpeg':
|
|
case 'gif': {
|
|
const color = extractColorFromImage(imageData);
|
|
return color ? { color } : {};
|
|
}
|
|
default:
|
|
console.warn('[extractMetadataFromImage] Unknown image format');
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Embed paarrot color into image data (PNG, WebP, JPEG, or GIF)
|
|
* @param imageData - Raw image data as ArrayBuffer or Uint8Array
|
|
* @param color - The color string to embed (e.g., '#ff5500')
|
|
* @returns New image data with embedded color, or null if not a supported format
|
|
*/
|
|
export function embedColorInImage(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
|
|
const format = detectImageFormat(imageData);
|
|
|
|
switch (format) {
|
|
case 'png':
|
|
return embedColorInPNG(imageData, color);
|
|
case 'webp':
|
|
return embedColorInWebP(imageData, color);
|
|
case 'jpeg':
|
|
return embedColorInJPEG(imageData, color);
|
|
case 'gif':
|
|
return embedColorInGIF(imageData, color);
|
|
default:
|
|
console.warn('[embedColorInImage] Unknown image format, cannot embed color');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Embed paarrot metadata (color and/or banner) into image data
|
|
* Preserves existing metadata that is not being updated
|
|
* @param imageData - Raw image data as ArrayBuffer or Uint8Array
|
|
* @param metadata - Object with color and/or banner to embed
|
|
* @returns New image data with embedded metadata, or null if not a supported format
|
|
*/
|
|
export function embedMetadataInImage(
|
|
imageData: ArrayBuffer | Uint8Array,
|
|
metadata: ImageMetadata
|
|
): Uint8Array | null {
|
|
const format = detectImageFormat(imageData);
|
|
|
|
switch (format) {
|
|
case 'png':
|
|
return embedMetadataInPNG(imageData, metadata);
|
|
// For other formats, only embed color for now
|
|
case 'webp':
|
|
case 'jpeg':
|
|
case 'gif':
|
|
if (metadata.color) {
|
|
return embedColorInImage(imageData, metadata.color);
|
|
}
|
|
return null;
|
|
default:
|
|
console.warn('[embedMetadataInImage] Unknown image format, cannot embed metadata');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove paarrot color from image data (PNG, WebP, JPEG, or GIF)
|
|
* @param imageData - Raw image data as ArrayBuffer or Uint8Array
|
|
* @returns New image data without the color, or null if not a supported format
|
|
*/
|
|
export function removeColorFromImage(imageData: ArrayBuffer | Uint8Array): Uint8Array | null {
|
|
const format = detectImageFormat(imageData);
|
|
|
|
switch (format) {
|
|
case 'png':
|
|
return removeColorFromPNG(imageData);
|
|
case 'webp':
|
|
return removeColorFromWebP(imageData);
|
|
case 'jpeg':
|
|
return removeColorFromJPEG(imageData);
|
|
case 'gif':
|
|
return removeColorFromGIF(imageData);
|
|
default:
|
|
console.warn('[removeColorFromImage] Unknown image format');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the MIME type for an image format
|
|
*/
|
|
export function getMimeType(format: ImageFormat): string {
|
|
switch (format) {
|
|
case 'png':
|
|
return 'image/png';
|
|
case 'webp':
|
|
return 'image/webp';
|
|
case 'jpeg':
|
|
return 'image/jpeg';
|
|
case 'gif':
|
|
return 'image/gif';
|
|
default:
|
|
return 'application/octet-stream';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the file extension for an image format
|
|
*/
|
|
export function getExtension(format: ImageFormat): string {
|
|
switch (format) {
|
|
case 'png':
|
|
return 'png';
|
|
case 'webp':
|
|
return 'webp';
|
|
case 'jpeg':
|
|
return 'jpg';
|
|
case 'gif':
|
|
return 'gif';
|
|
default:
|
|
return 'bin';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch and extract color from an image URL (supports all formats)
|
|
* @param url - HTTP URL to fetch the image from
|
|
* @param accessToken - Optional access token for authenticated media
|
|
* @returns The color string if found, or undefined
|
|
*/
|
|
export async function fetchAndExtractColor(url: string, accessToken?: string | null): Promise<string | undefined> {
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
|
});
|
|
if (!response.ok) return undefined;
|
|
|
|
const data = await response.arrayBuffer();
|
|
return extractColorFromImage(data);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch and extract all metadata from an image URL
|
|
* @param url - HTTP URL to fetch the image from
|
|
* @param accessToken - Optional access token for authenticated media
|
|
* @returns Object with color and banner if found
|
|
*/
|
|
export async function fetchAndExtractMetadata(url: string, accessToken?: string | null): Promise<ImageMetadata> {
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
|
});
|
|
if (!response.ok) return {};
|
|
|
|
const data = await response.arrayBuffer();
|
|
return extractMetadataFromImage(data);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|