Spaces:
Paused
Paused
| 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, | |
| }; | |