/** * 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 { 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 { 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 {}; } }