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

@@ -0,0 +1,215 @@
/**
* Unified image metadata utilities for embedding/extracting user color
* Supports PNG, WebP, JPEG, and GIF formats
*/
/* eslint-disable no-plusplus */
/* eslint-disable no-console */
import { embedColorInPNG, extractColorFromPNG, removeColorFromPNG } from './pngMetadata';
import { embedColorInWebP, extractColorFromWebP, removeColorFromWebP } from './webpMetadata';
import { embedColorInJPEG, extractColorFromJPEG, removeColorFromJPEG } from './jpegMetadata';
import { embedColorInGIF, extractColorFromGIF, removeColorFromGIF } from './gifMetadata';
/** 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;
}
}
/**
* 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;
}
}
/**
* 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 PNG and WebP)
* @param url - HTTP URL to fetch the image from
* @returns The color string if found, or undefined
*/
export async function fetchAndExtractColor(url: string): Promise<string | undefined> {
try {
const response = await fetch(url);
if (!response.ok) return undefined;
const data = await response.arrayBuffer();
return extractColorFromImage(data);
} catch {
return undefined;
}
}