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