import type { PicletInstance } from '$lib/db/schema'; const METADATA_KEY = 'snaplings-piclet-v1'; interface PicletMetadata { version: 1; data: Omit; checksum?: string; } /** * Extract piclet metadata from a PNG image */ export async function extractPicletMetadata(file: File): Promise { try { const arrayBuffer = await file.arrayBuffer(); const bytes = new Uint8Array(arrayBuffer); // Check PNG signature if (!isPNG(bytes)) { return null; } // Find tEXt chunks const chunks = parsePNGChunks(bytes); const textChunk = chunks.find(chunk => chunk.type === 'tEXt' && chunk.keyword === METADATA_KEY ); if (!textChunk || !textChunk.text) { return null; } // Parse metadata const metadata: PicletMetadata = JSON.parse(textChunk.text); // Validate version if (metadata.version !== 1) { console.warn('Unsupported piclet metadata version:', metadata.version); return null; } // Create PicletInstance from metadata const piclet: PicletInstance = { ...metadata.data, caughtAt: new Date(), // Use current date for import isInRoster: false, rosterPosition: undefined }; return piclet; } catch (error) { console.error('Failed to extract piclet metadata:', error); return null; } } /** * Embed piclet metadata into a PNG image */ export async function embedPicletMetadata(imageBlob: Blob, piclet: PicletInstance): Promise { const arrayBuffer = await imageBlob.arrayBuffer(); const bytes = new Uint8Array(arrayBuffer); // Prepare metadata const metadata: PicletMetadata = { version: 1, data: { typeId: piclet.typeId, nickname: piclet.nickname, primaryType: piclet.primaryType, secondaryType: piclet.secondaryType, currentHp: piclet.maxHp, // Reset to full HP for sharing maxHp: piclet.maxHp, level: piclet.level, xp: piclet.xp, attack: piclet.attack, defense: piclet.defense, fieldAttack: piclet.fieldAttack, fieldDefense: piclet.fieldDefense, speed: piclet.speed, baseHp: piclet.baseHp, baseAttack: piclet.baseAttack, baseDefense: piclet.baseDefense, baseFieldAttack: piclet.baseFieldAttack, baseFieldDefense: piclet.baseFieldDefense, baseSpeed: piclet.baseSpeed, moves: piclet.moves, nature: piclet.nature, bst: piclet.bst, tier: piclet.tier, role: piclet.role, variance: piclet.variance, imageUrl: piclet.imageUrl, imageData: piclet.imageData, imageCaption: piclet.imageCaption, concept: piclet.concept, imagePrompt: piclet.imagePrompt } }; // Create tEXt chunk const textChunk = createTextChunk(METADATA_KEY, JSON.stringify(metadata)); // Insert chunk after IHDR const newBytes = insertChunkAfterIHDR(bytes, textChunk); return new Blob([newBytes], { type: 'image/png' }); } /** * Check if bytes represent a PNG file */ function isPNG(bytes: Uint8Array): boolean { const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10]; if (bytes.length < 8) return false; for (let i = 0; i < 8; i++) { if (bytes[i] !== pngSignature[i]) return false; } return true; } /** * Parse PNG chunks */ function parsePNGChunks(bytes: Uint8Array): any[] { const chunks = []; let pos = 8; // Skip PNG signature while (pos < bytes.length) { // Read chunk length const length = readUInt32BE(bytes, pos); pos += 4; // Read chunk type const type = String.fromCharCode(...bytes.slice(pos, pos + 4)); pos += 4; // Read chunk data const data = bytes.slice(pos, pos + length); pos += length; // Skip CRC pos += 4; // Parse tEXt chunks if (type === 'tEXt') { const nullIndex = data.indexOf(0); if (nullIndex !== -1) { const keyword = String.fromCharCode(...data.slice(0, nullIndex)); const text = String.fromCharCode(...data.slice(nullIndex + 1)); chunks.push({ type, keyword, text }); } } else { chunks.push({ type, data }); } if (type === 'IEND') break; } return chunks; } /** * Create a tEXt chunk */ function createTextChunk(keyword: string, text: string): Uint8Array { const keywordBytes = new TextEncoder().encode(keyword); const textBytes = new TextEncoder().encode(text); // Create chunk data: keyword + null + text const data = new Uint8Array(keywordBytes.length + 1 + textBytes.length); data.set(keywordBytes); data[keywordBytes.length] = 0; // null separator data.set(textBytes, keywordBytes.length + 1); // Create full chunk: length + type + data + crc const chunk = new Uint8Array(4 + 4 + data.length + 4); // Length writeUInt32BE(chunk, 0, data.length); // Type: 'tEXt' chunk[4] = 116; // t chunk[5] = 69; // E chunk[6] = 88; // X chunk[7] = 116; // t // Data chunk.set(data, 8); // CRC const crc = calculateCRC(chunk.slice(4, 8 + data.length)); writeUInt32BE(chunk, 8 + data.length, crc); return chunk; } /** * Insert chunk after IHDR */ function insertChunkAfterIHDR(bytes: Uint8Array, newChunk: Uint8Array): Uint8Array { // Find IHDR chunk end let ihdrEnd = 8; // PNG signature ihdrEnd += 4; // IHDR length ihdrEnd += 4; // IHDR type const ihdrLength = readUInt32BE(bytes, 8); ihdrEnd += ihdrLength; // IHDR data ihdrEnd += 4; // IHDR CRC // Create new array const result = new Uint8Array(bytes.length + newChunk.length); // Copy up to IHDR end result.set(bytes.slice(0, ihdrEnd)); // Insert new chunk result.set(newChunk, ihdrEnd); // Copy rest result.set(bytes.slice(ihdrEnd), ihdrEnd + newChunk.length); return result; } /** * Read 32-bit unsigned integer (big endian) */ function readUInt32BE(bytes: Uint8Array, offset: number): number { return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; } /** * Write 32-bit unsigned integer (big endian) */ function writeUInt32BE(bytes: Uint8Array, offset: number, value: number): void { bytes[offset] = (value >>> 24) & 0xff; bytes[offset + 1] = (value >>> 16) & 0xff; bytes[offset + 2] = (value >>> 8) & 0xff; bytes[offset + 3] = value & 0xff; } /** * Calculate CRC32 for PNG chunk */ function calculateCRC(bytes: Uint8Array): number { const crcTable = getCRCTable(); let crc = 0xffffffff; for (let i = 0; i < bytes.length; i++) { crc = crcTable[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8); } return crc ^ 0xffffffff; } /** * Get CRC table (cached) */ let crcTable: Uint32Array | null = null; function getCRCTable(): Uint32Array { if (crcTable) return crcTable; crcTable = new Uint32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) { c = (c & 1) ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } crcTable[i] = c; } return crcTable; }