Writing & Exporting
Save character cards to PNG images, JSON files, or CHARX containers. Choose the format based on how the card will be used.
Prerequisites
Before starting, you should:
Overview
We'll cover:
- Writing to PNG (most common)
- Writing to JSON
- Writing to CHARX with assets
- Writing standalone lorebooks
Step 1: Write to PNG
PNG is the most portable format - the image displays normally, but applications can extract the embedded card.
import { writeCardToPng } from '@motioneffector/cards'
import { readFileSync, writeFileSync } from 'fs'
// You need an existing PNG image
const imageBytes = readFileSync('base-image.png')
// Embed the card into the image
const cardPng = writeCardToPng(card, imageBytes)
writeFileSync('character.png', cardPng)
The original image is preserved - only metadata chunks are added/replaced.
PNG Write Options
const cardPng = writeCardToPng(card, imageBytes, {
includeV2Chunk: true, // Also write 'chara' chunk for V2 compatibility (default: true)
serializeDecorators: true, // Convert decorator arrays back to @@syntax (default: true)
})
Setting includeV2Chunk: false reduces file size slightly but breaks compatibility with V2-only applications like older SillyTavern versions.
Step 2: Write to JSON
JSON is useful for editing cards in text editors, storing in databases, or debugging.
import { writeCardToJson } from '@motioneffector/cards'
import { writeFileSync } from 'fs'
const jsonString = writeCardToJson(card)
writeFileSync('character.json', jsonString)
The output is pretty-printed with 2-space indentation.
Step 3: Write to CHARX with Assets
CHARX bundles the card with asset files (icons, backgrounds, emotions) in a ZIP container.
import { writeCardToCharx } from '@motioneffector/cards'
import { readFileSync, writeFileSync } from 'fs'
// Load your asset files
const iconBytes = readFileSync('icon.png')
const happyBytes = readFileSync('emotions/happy.png')
const sadBytes = readFileSync('emotions/sad.png')
// Bundle everything
const charxBytes = writeCardToCharx(card, {
assets: [
{ type: 'icon', name: 'main', data: iconBytes, ext: 'png' },
{ type: 'emotion', name: 'happy', data: happyBytes, ext: 'png' },
{ type: 'emotion', name: 'sad', data: sadBytes, ext: 'png' },
]
})
writeFileSync('character.charx', charxBytes)
The card's assets array is automatically updated with embeded:// URIs pointing to the bundled files.
Step 4: Write Standalone Lorebooks
Lorebooks can be exported separately from cards.
import { writeLorebookToPng, writeLorebookToJson } from '@motioneffector/cards'
// To PNG (NovelAI-compatible)
const lorebookPng = writeLorebookToPng(lorebook, baseImageBytes)
writeFileSync('lorebook.png', lorebookPng)
// To JSON
const lorebookJson = writeLorebookToJson(lorebook)
writeFileSync('lorebook.json', lorebookJson)
Complete Example
import {
readCard,
writeCardToPng,
writeCardToJson,
writeCardToCharx
} from '@motioneffector/cards'
import { readFileSync, writeFileSync } from 'fs'
// Load and modify a card
const originalBytes = readFileSync('character.png')
const card = readCard(originalBytes)
card.data.character_version = '2.0.0'
card.data.tags.push('updated')
// Export to all formats
const asPng = writeCardToPng(card, originalBytes)
writeFileSync('character-v2.png', asPng)
const asJson = writeCardToJson(card)
writeFileSync('character-v2.json', asJson)
const asCharx = writeCardToCharx(card)
writeFileSync('character-v2.charx', asCharx)
console.log('Exported to PNG, JSON, and CHARX')
Variations
Creating a New Card from Scratch
import type { CharacterCard } from '@motioneffector/cards'
import { writeCardToPng } from '@motioneffector/cards'
import { readFileSync, writeFileSync } from 'fs'
const card: CharacterCard = {
spec: 'chara_card_v3',
spec_version: '3.0',
data: {
name: 'New Character',
description: 'A character description.',
personality: 'Personality traits.',
scenario: 'The setting.',
first_mes: 'Hello!',
mes_example: '',
creator_notes: '',
system_prompt: '',
post_history_instructions: '',
alternate_greetings: [],
tags: ['new'],
creator: 'your-name',
character_version: '1.0.0',
extensions: {},
group_only_greetings: [],
}
}
const baseImage = readFileSync('blank.png')
const cardPng = writeCardToPng(card, baseImage)
writeFileSync('new-character.png', cardPng)
Stripping V2 Compatibility for Smaller Files
const cardPng = writeCardToPng(card, imageBytes, {
includeV2Chunk: false // Only write ccv3 chunk
})
This reduces file size but older applications won't be able to read the card.
Preserving Raw Decorator Content
If you want to keep @@decorator lines in content without serializing:
// Read without parsing decorators
const card = readCard(bytes, { parseDecorators: false })
// Modify...
// Write - decorators stay as raw text in content
const output = writeCardToPng(card, imageBytes, {
serializeDecorators: false
})
Troubleshooting
Output file is the same size as input
Symptom: The PNG file size barely changed after adding card data.
Cause: Normal - card JSON is base64-encoded and added as metadata. For small cards, this adds only a few KB.
Solution: No action needed. Large lorebooks can add significant size.
Card data not appearing in other applications
Symptom: Other apps don't see the card after you wrote it.
Cause: The app might only support V2 format, and you disabled the V2 chunk.
Solution: Ensure includeV2Chunk: true (the default) when writing.
CHARX file won't open
Symptom: Applications reject the CHARX file.
Cause: The app might expect specific asset paths or card.json structure.
Solution: Verify the app's CHARX requirements. Try extracting the ZIP manually to inspect contents.
See Also
- File Formats - Format details and structure
- Format Compatibility - Ensuring V2 app compatibility
- Bundling Assets in CHARX - Detailed CHARX guide
- Writing API - Full function reference