|
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; |
|
} |