Blame Raw
· · · 98 lines (3.2 KB)
0 contributors
1import fs from 'node:fs';
2import { Buffer } from 'node:buffer';
3
4import encode from './png/encode.js';
5import extract from 'png-chunks-extract';
6import PNGtext from 'png-chunk-text';
7
8/**
9 * Writes Character metadata to a PNG image buffer.
10 * Writes only 'chara', 'ccv3' is not supported and removed not to create a mismatch.
11 * @param {Buffer} image PNG image buffer
12 * @param {string} data Character data to write
13 * @returns {Buffer} PNG image buffer with metadata
14 */
15export const write = (image, data) => {
16 const chunks = extract(new Uint8Array(image));
17 const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
18
19 // Remove existing tEXt chunks
20 for (const tEXtChunk of tEXtChunks) {
21 const data = PNGtext.decode(tEXtChunk.data);
22 if (data.keyword.toLowerCase() === 'chara' || data.keyword.toLowerCase() === 'ccv3') {
23 chunks.splice(chunks.indexOf(tEXtChunk), 1);
24 }
25 }
26
27 // Add new v2 chunk before the IEND chunk
28 const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
29 chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
30
31 // Try adding v3 chunk before the IEND chunk
32 try {
33 //change v2 format to v3
34 const v3Data = JSON.parse(data);
35 v3Data.spec = 'chara_card_v3';
36 v3Data.spec_version = '3.0';
37
38 const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64');
39 chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData));
40 } catch (error) {
41 // Ignore errors when adding v3 chunk
42 }
43
44 const newBuffer = Buffer.from(encode(chunks));
45 return newBuffer;
46};
47
48/**
49 * Reads Character metadata from a PNG image buffer.
50 * Supports both V2 (chara) and V3 (ccv3). V3 (ccv3) takes precedence.
51 * @param {Buffer} image PNG image buffer
52 * @returns {string} Character data
53 */
54export const read = (image) => {
55 const chunks = extract(new Uint8Array(image));
56
57 const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));
58
59 if (textChunks.length === 0) {
60 console.error('PNG metadata does not contain any text chunks.');
61 throw new Error('No PNG metadata.');
62 }
63
64 const ccv3Index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'ccv3');
65
66 if (ccv3Index > -1) {
67 return Buffer.from(textChunks[ccv3Index].text, 'base64').toString('utf8');
68 }
69
70 const charaIndex = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() === 'chara');
71
72 if (charaIndex > -1) {
73 return Buffer.from(textChunks[charaIndex].text, 'base64').toString('utf8');
74 }
75
76 console.error('PNG metadata does not contain any character data.');
77 throw new Error('No PNG metadata.');
78};
79
80/**
81 * Parses a card image and returns the character metadata.
82 * @param {string} cardUrl Path to the card image
83 * @param {string} format File format
84 * @returns {Promise<string>} Character data
85 */
86export const parse = async (cardUrl, format) => {
87 let fileFormat = format === undefined ? 'png' : format;
88
89 switch (fileFormat) {
90 case 'png': {
91 const buffer = fs.readFileSync(cardUrl);
92 return read(buffer);
93 }
94 }
95
96 throw new Error('Unsupported format');
97};
98