Spaces:
Running
Running
const fs = require('fs'); | |
const encode = require('png-chunks-encode'); | |
const extract = require('png-chunks-extract'); | |
const PNGtext = require('png-chunk-text'); | |
/** | |
* Writes Character metadata to a PNG image buffer. | |
* Writes only 'chara', 'ccv3' is not supported and removed not to create a mismatch. | |
* @param {Buffer} image PNG image buffer | |
* @param {string} data Character data to write | |
* @returns {Buffer} PNG image buffer with metadata | |
*/ | |
const write = (image, data) => { | |
const chunks = extract(image); | |
const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt'); | |
// Remove existing tEXt chunks | |
for (const tEXtChunk of tEXtChunks) { | |
const data = PNGtext.decode(tEXtChunk.data); | |
if (data.keyword.toLowerCase() === 'chara' || data.keyword.toLowerCase() === 'ccv3') { | |
chunks.splice(chunks.indexOf(tEXtChunk), 1); | |
} | |
} | |
// Add new v2 chunk before the IEND chunk | |
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64'); | |
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData)); | |
// Try adding v3 chunk before the IEND chunk | |
try { | |
//change v2 format to v3 | |
const v3Data = JSON.parse(data); | |
v3Data.spec = 'chara_card_v3'; | |
v3Data.spec_version = '3.0'; | |
const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64'); | |
chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData)); | |
} catch (error) { } | |
const newBuffer = Buffer.from(encode(chunks)); | |
return newBuffer; | |
}; | |
/** | |
* Reads Character metadata from a PNG image buffer. | |
* Supports both V2 (chara) and V3 (ccv3). V3 (ccv3) takes precedence. | |
* @param {Buffer} image PNG image buffer | |
* @returns {string} Character data | |
*/ | |
const read = (image) => { | |
const chunks = extract(image); | |
const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data)); | |
if (textChunks.length === 0) { | |
console.error('PNG metadata does not contain any text chunks.'); | |
throw new Error('No PNG metadata.'); | |
} | |
const ccv3Index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'ccv3'); | |
if (ccv3Index > -1) { | |
return Buffer.from(textChunks[ccv3Index].text, 'base64').toString('utf8'); | |
} | |
const charaIndex = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'chara'); | |
if (charaIndex > -1) { | |
return Buffer.from(textChunks[charaIndex].text, 'base64').toString('utf8'); | |
} | |
console.error('PNG metadata does not contain any character data.'); | |
throw new Error('No PNG metadata.'); | |
}; | |
/** | |
* Parses a card image and returns the character metadata. | |
* @param {string} cardUrl Path to the card image | |
* @param {string} format File format | |
* @returns {string} Character data | |
*/ | |
const parse = (cardUrl, format) => { | |
let fileFormat = format === undefined ? 'png' : format; | |
switch (fileFormat) { | |
case 'png': { | |
const buffer = fs.readFileSync(cardUrl); | |
return read(buffer); | |
} | |
} | |
throw new Error('Unsupported format'); | |
}; | |
module.exports = { | |
parse, | |
write, | |
read, | |
}; | |