| import type { PicletInstance } from '$lib/db/schema'; |
|
|
| const METADATA_KEY = 'snaplings-piclet-v1'; |
|
|
| interface PicletMetadata { |
| version: 1; |
| data: Omit<PicletInstance, 'id' | 'rosterPosition' | 'isInRoster' | 'caughtAt'>; |
| checksum?: string; |
| } |
|
|
| |
| |
| |
| export async function extractPicletMetadata(file: File): Promise<PicletInstance | null> { |
| try { |
| const arrayBuffer = await file.arrayBuffer(); |
| const bytes = new Uint8Array(arrayBuffer); |
| |
| |
| if (!isPNG(bytes)) { |
| return null; |
| } |
| |
| |
| const chunks = parsePNGChunks(bytes); |
| const textChunk = chunks.find(chunk => |
| chunk.type === 'tEXt' && |
| chunk.keyword === METADATA_KEY |
| ); |
| |
| if (!textChunk || !textChunk.text) { |
| return null; |
| } |
| |
| |
| const metadata: PicletMetadata = JSON.parse(textChunk.text); |
| |
| |
| if (metadata.version !== 1) { |
| console.warn('Unsupported piclet metadata version:', metadata.version); |
| return null; |
| } |
| |
| |
| const piclet: PicletInstance = { |
| ...metadata.data, |
| caughtAt: new Date(), |
| isInRoster: false, |
| rosterPosition: undefined |
| }; |
| |
| return piclet; |
| } catch (error) { |
| console.error('Failed to extract piclet metadata:', error); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| export async function embedPicletMetadata(imageBlob: Blob, piclet: PicletInstance): Promise<Blob> { |
| const arrayBuffer = await imageBlob.arrayBuffer(); |
| const bytes = new Uint8Array(arrayBuffer); |
| |
| |
| const metadata: PicletMetadata = { |
| version: 1, |
| data: { |
| typeId: piclet.typeId, |
| nickname: piclet.nickname, |
| primaryType: piclet.primaryType, |
| secondaryType: piclet.secondaryType, |
| currentHp: piclet.maxHp, |
| 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 |
| } |
| }; |
| |
| |
| const textChunk = createTextChunk(METADATA_KEY, JSON.stringify(metadata)); |
| |
| |
| const newBytes = insertChunkAfterIHDR(bytes, textChunk); |
| |
| return new Blob([newBytes], { type: 'image/png' }); |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| function parsePNGChunks(bytes: Uint8Array): any[] { |
| const chunks = []; |
| let pos = 8; |
| |
| while (pos < bytes.length) { |
| |
| const length = readUInt32BE(bytes, pos); |
| pos += 4; |
| |
| |
| const type = String.fromCharCode(...bytes.slice(pos, pos + 4)); |
| pos += 4; |
| |
| |
| const data = bytes.slice(pos, pos + length); |
| pos += length; |
| |
| |
| pos += 4; |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| function createTextChunk(keyword: string, text: string): Uint8Array { |
| const keywordBytes = new TextEncoder().encode(keyword); |
| const textBytes = new TextEncoder().encode(text); |
| |
| |
| const data = new Uint8Array(keywordBytes.length + 1 + textBytes.length); |
| data.set(keywordBytes); |
| data[keywordBytes.length] = 0; |
| data.set(textBytes, keywordBytes.length + 1); |
| |
| |
| const chunk = new Uint8Array(4 + 4 + data.length + 4); |
| |
| |
| writeUInt32BE(chunk, 0, data.length); |
| |
| |
| chunk[4] = 116; |
| chunk[5] = 69; |
| chunk[6] = 88; |
| chunk[7] = 116; |
| |
| |
| chunk.set(data, 8); |
| |
| |
| const crc = calculateCRC(chunk.slice(4, 8 + data.length)); |
| writeUInt32BE(chunk, 8 + data.length, crc); |
| |
| return chunk; |
| } |
|
|
| |
| |
| |
| function insertChunkAfterIHDR(bytes: Uint8Array, newChunk: Uint8Array): Uint8Array { |
| |
| let ihdrEnd = 8; |
| ihdrEnd += 4; |
| ihdrEnd += 4; |
| const ihdrLength = readUInt32BE(bytes, 8); |
| ihdrEnd += ihdrLength; |
| ihdrEnd += 4; |
| |
| |
| const result = new Uint8Array(bytes.length + newChunk.length); |
| |
| |
| result.set(bytes.slice(0, ihdrEnd)); |
| |
| |
| result.set(newChunk, ihdrEnd); |
| |
| |
| result.set(bytes.slice(ihdrEnd), ihdrEnd + newChunk.length); |
| |
| return result; |
| } |
|
|
| |
| |
| |
| function readUInt32BE(bytes: Uint8Array, offset: number): number { |
| return (bytes[offset] << 24) | |
| (bytes[offset + 1] << 16) | |
| (bytes[offset + 2] << 8) | |
| bytes[offset + 3]; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |