hangwin--mcp-chrome
202 行
5.1 KiB
TypeScript
202 行
5.1 KiB
TypeScript
/**
|
|
* GIF Encoder Module for Offscreen Document
|
|
*
|
|
* Handles GIF encoding using the gifenc library in the offscreen document context.
|
|
* This module provides frame-by-frame GIF encoding with palette quantization.
|
|
*/
|
|
|
|
import { GIFEncoder, quantize, applyPalette } from 'gifenc';
|
|
import { MessageTarget, OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
interface GifEncoderState {
|
|
encoder: ReturnType<typeof GIFEncoder> | null;
|
|
width: number;
|
|
height: number;
|
|
frameCount: number;
|
|
isInitialized: boolean;
|
|
}
|
|
|
|
interface GifAddFrameMessage {
|
|
target: MessageTarget;
|
|
type: typeof OFFSCREEN_MESSAGE_TYPES.GIF_ADD_FRAME;
|
|
imageData: number[];
|
|
width: number;
|
|
height: number;
|
|
delay: number;
|
|
maxColors?: number;
|
|
}
|
|
|
|
interface GifFinishMessage {
|
|
target: MessageTarget;
|
|
type: typeof OFFSCREEN_MESSAGE_TYPES.GIF_FINISH;
|
|
}
|
|
|
|
interface GifResetMessage {
|
|
target: MessageTarget;
|
|
type: typeof OFFSCREEN_MESSAGE_TYPES.GIF_RESET;
|
|
}
|
|
|
|
type GifMessage = GifAddFrameMessage | GifFinishMessage | GifResetMessage;
|
|
|
|
interface GifMessageResponse {
|
|
success: boolean;
|
|
error?: string;
|
|
frameCount?: number;
|
|
gifData?: number[];
|
|
byteLength?: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// State
|
|
// ============================================================================
|
|
|
|
const state: GifEncoderState = {
|
|
encoder: null,
|
|
width: 0,
|
|
height: 0,
|
|
frameCount: 0,
|
|
isInitialized: false,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Handlers
|
|
// ============================================================================
|
|
|
|
function initializeEncoder(width: number, height: number): void {
|
|
state.encoder = GIFEncoder();
|
|
state.width = width;
|
|
state.height = height;
|
|
state.frameCount = 0;
|
|
state.isInitialized = true;
|
|
}
|
|
|
|
function addFrame(
|
|
imageData: Uint8ClampedArray,
|
|
width: number,
|
|
height: number,
|
|
delay: number,
|
|
maxColors: number = 256,
|
|
): void {
|
|
// Initialize encoder on first frame
|
|
if (!state.isInitialized || state.width !== width || state.height !== height) {
|
|
initializeEncoder(width, height);
|
|
}
|
|
|
|
if (!state.encoder) {
|
|
throw new Error('GIF encoder not initialized');
|
|
}
|
|
|
|
// Quantize colors to create palette
|
|
const palette = quantize(imageData, maxColors, { format: 'rgb444' });
|
|
|
|
// Map pixels to palette indices
|
|
const indexedPixels = applyPalette(imageData, palette, 'rgb444');
|
|
|
|
// Write frame to encoder
|
|
state.encoder.writeFrame(indexedPixels, width, height, {
|
|
palette,
|
|
delay,
|
|
dispose: 2, // Restore to background color
|
|
});
|
|
|
|
state.frameCount++;
|
|
}
|
|
|
|
function finishEncoding(): Uint8Array {
|
|
if (!state.encoder) {
|
|
throw new Error('GIF encoder not initialized');
|
|
}
|
|
|
|
state.encoder.finish();
|
|
const bytes = state.encoder.bytes();
|
|
|
|
// Reset state after finishing
|
|
resetEncoder();
|
|
|
|
return bytes;
|
|
}
|
|
|
|
function resetEncoder(): void {
|
|
if (state.encoder) {
|
|
state.encoder.reset();
|
|
}
|
|
state.encoder = null;
|
|
state.width = 0;
|
|
state.height = 0;
|
|
state.frameCount = 0;
|
|
state.isInitialized = false;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Message Handler
|
|
// ============================================================================
|
|
|
|
function isGifMessage(message: unknown): message is GifMessage {
|
|
if (!message || typeof message !== 'object') return false;
|
|
const msg = message as Record<string, unknown>;
|
|
if (msg.target !== MessageTarget.Offscreen) return false;
|
|
|
|
const gifTypes = [
|
|
OFFSCREEN_MESSAGE_TYPES.GIF_ADD_FRAME,
|
|
OFFSCREEN_MESSAGE_TYPES.GIF_FINISH,
|
|
OFFSCREEN_MESSAGE_TYPES.GIF_RESET,
|
|
];
|
|
|
|
return gifTypes.includes(msg.type as string);
|
|
}
|
|
|
|
export function handleGifMessage(
|
|
message: unknown,
|
|
sendResponse: (response: GifMessageResponse) => void,
|
|
): boolean {
|
|
if (!isGifMessage(message)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
switch (message.type) {
|
|
case OFFSCREEN_MESSAGE_TYPES.GIF_ADD_FRAME: {
|
|
const { imageData, width, height, delay, maxColors } = message;
|
|
const clampedData = new Uint8ClampedArray(imageData);
|
|
addFrame(clampedData, width, height, delay, maxColors);
|
|
sendResponse({
|
|
success: true,
|
|
frameCount: state.frameCount,
|
|
});
|
|
break;
|
|
}
|
|
|
|
case OFFSCREEN_MESSAGE_TYPES.GIF_FINISH: {
|
|
const gifBytes = finishEncoding();
|
|
sendResponse({
|
|
success: true,
|
|
gifData: Array.from(gifBytes),
|
|
byteLength: gifBytes.byteLength,
|
|
});
|
|
break;
|
|
}
|
|
|
|
case OFFSCREEN_MESSAGE_TYPES.GIF_RESET: {
|
|
resetEncoder();
|
|
sendResponse({ success: true });
|
|
break;
|
|
}
|
|
|
|
default:
|
|
sendResponse({ success: false, error: `Unknown GIF message type` });
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
console.error('GIF encoder error:', errorMessage);
|
|
sendResponse({ success: false, error: errorMessage });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
console.log('GIF encoder module loaded');
|