mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-21 22:47:41 +01:00
99 lines
3.2 KiB
JavaScript
99 lines
3.2 KiB
JavaScript
import fs from 'node:fs';
|
|
import { Buffer } from 'node:buffer';
|
|
|
|
import encode from 'png-chunks-encode';
|
|
import extract from 'png-chunks-extract';
|
|
import PNGtext from '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
|
|
*/
|
|
export const write = (image, data) => {
|
|
const chunks = extract(new Uint8Array(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) {
|
|
// Ignore errors when adding v3 chunk
|
|
}
|
|
|
|
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
|
|
*/
|
|
export const read = (image) => {
|
|
const chunks = extract(new Uint8Array(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
|
|
*/
|
|
export 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');
|
|
};
|
|
|