piclets / src /lib /services /picletMetadata.ts
Fraser's picture
better gen
a46ce65
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;
}
/**
* Extract piclet metadata from a PNG image
*/
export async function extractPicletMetadata(file: File): Promise<PicletInstance | null> {
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<Blob> {
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;
}