diff --git a/src/lang/en.ts b/src/lang/en.ts
index 631378ce..70f65f67 100644
--- a/src/lang/en.ts
+++ b/src/lang/en.ts
@@ -646,4 +646,5 @@ export const languageEnglish = {
index: "Index",
search: "Search",
goCharacterOnImport: "Go to Character on Realm Import",
+ format: "Format",
}
\ No newline at end of file
diff --git a/src/lib/Others/AlertComp.svelte b/src/lib/Others/AlertComp.svelte
index 58fa0b53..be3d1a5b 100644
--- a/src/lib/Others/AlertComp.svelte
+++ b/src/lib/Others/AlertComp.svelte
@@ -21,7 +21,7 @@
let btn
let input = ''
let cardExportType = 'realm'
- let cardExportPassword = ''
+ let cardExportType2 = ''
let cardLicense = ''
let generationInfoMenuIndex = 0
$: {
@@ -33,7 +33,7 @@
}
if($alertStore.type !== 'cardexport'){
cardExportType = 'realm'
- cardExportPassword = ''
+ cardExportType2 = ''
cardLicense = ''
}
}
@@ -383,8 +383,7 @@
type: 'none',
msg: JSON.stringify({
type: 'cancel',
- password: cardExportPassword,
- license: cardLicense
+ type2: cardExportType2
})
})
}}>
@@ -421,13 +420,20 @@
{/if}
+ {#if $alertStore.submsg === '' && cardExportType === ''}
+ {language.format}
+
+ PNG
+ JSON
+ CHARX
+
+ {/if}
diff --git a/src/ts/alert.ts b/src/ts/alert.ts
index b226e1ce..cca5724f 100644
--- a/src/ts/alert.ts
+++ b/src/ts/alert.ts
@@ -222,8 +222,7 @@ export async function alertCardExport(type:string = ''){
return JSON.parse(get(alertStore).msg) as {
type: string,
- password: string,
- license: string
+ type2: string,
}
}
diff --git a/src/ts/characterCards.ts b/src/ts/characterCards.ts
index c66b59ea..7ae2b880 100644
--- a/src/ts/characterCards.ts
+++ b/src/ts/characterCards.ts
@@ -5,13 +5,14 @@ import { checkNullish, decryptBuffer, encryptBuffer, isKnownUri, selectFileByDom
import { language } from "src/lang"
import { v4 as uuidv4, v4 } from 'uuid';
import { characterFormatUpdate } from "./characters"
-import { AppendableBuffer, checkCharOrder, downloadFile, loadAsset, LocalWriter, openURL, readImage, saveAsset, VirtualWriter } from "./storage/globalApi"
+import { AppendableBuffer, BlankWriter, checkCharOrder, downloadFile, loadAsset, LocalWriter, openURL, readImage, saveAsset, VirtualWriter } from "./storage/globalApi"
import { CurrentCharacter, SettingsMenuIndex, ShowRealmFrameStore, selectedCharID, settingsOpen } from "./stores"
import { convertImage, hasher } from "./parser"
import { CCardLib, type CharacterCardV3, type LorebookEntry } from '@risuai/ccardlib'
import { reencodeImage } from "./process/files/image"
import { PngChunk } from "./pngChunk"
import type { OnnxModelFiles } from "./process/transformers"
+import { CharXWriter } from "./process/processzip"
export const hubURL = "https://sv.risuai.xyz"
@@ -335,7 +336,7 @@ export async function exportChar(charaID:number):Promise {
const option = await alertCardExport()
if(option.type === ''){
- exportCharacterCard(char,'png', {spec: 'v3'})
+ exportCharacterCard(char, option.type2 === 'json' ? 'json' : (option.type2 === 'charx' ? 'charx' : 'png'), {spec: 'v3'})
}
else if(option.type === 'ccv2'){
exportCharacterCard(char,'png', {spec: 'v2'})
@@ -748,7 +749,7 @@ async function createBaseV2(char:character) {
}
-export async function exportCharacterCard(char:character, type:'png'|'json' = 'png', arg:{
+export async function exportCharacterCard(char:character, type:'png'|'json'|'charx' = 'png', arg:{
password?:string
writer?:LocalWriter|VirtualWriter,
spec?:'v2'|'v3'
@@ -759,10 +760,17 @@ export async function exportCharacterCard(char:character, type:'png'|'json' = 'p
char.image = ''
img = await reencodeImage(img)
const localWriter = arg.writer ?? (new LocalWriter())
- if(!arg.writer){
- await (localWriter as LocalWriter).init(`Image file`, ['png'])
+ if(!arg.writer && type !== 'json'){
+ const nameExt = {
+ 'png': ['Image File', 'png'],
+ 'json': ['JSON File', 'json'],
+ 'charx': ['CharX File', 'charx']
+ }
+ const ext = nameExt[type]
+ console.log(ext)
+ await (localWriter as LocalWriter).init(ext[0], [ext[1]])
}
- const writer = new PngChunk.streamWriter(img, localWriter)
+ const writer = type === 'charx' ? (new CharXWriter(localWriter)) : type === 'json' ? (new BlankWriter()) : (new PngChunk.streamWriter(img, localWriter))
await writer.init()
let assetIndex = 0
if(spec === 'v2'){
@@ -835,15 +843,55 @@ export async function exportCharacterCard(char:character, type:'png'|'json' = 'p
type: 'wait',
msg: `Loading... (Adding Assets ${i} / ${card.data.assets.length})`
})
- const key = card.data.assets[i].uri
- if(isKnownUri(key)){
+ let key = card.data.assets[i].uri
+ let rData:Uint8Array
+ if(key === 'ccdefault:' && type !== 'png'){
+ key = char.image
+ rData = img
+ }
+ else if(isKnownUri(key)){
continue
}
- const rData = await readImage(key)
- const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
+ else{
+ rData = await readImage(key)
+ }
assetIndex++
- card.data.assets[i].uri = `__asset:${assetIndex}`
- await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
+ if(type === 'png'){
+ const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
+ card.data.assets[i].uri = `__asset:${assetIndex}`
+ await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
+ }
+ else if(type === 'json'){
+ const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
+ card.data.assets[i].uri = `data:application/octet-stream;base64,${b64encoded}`
+ }
+ else{
+ let type = 'other'
+ switch(card.data.assets[i].type){
+ case 'emotion':
+ type = 'emotion'
+ break
+ case 'background':
+ type = 'background'
+ break
+ case 'user_icon':
+ type = 'user_icon'
+ break
+ case 'icon':
+ type = 'icon'
+ break
+ }
+ let path = ''
+ const name = `${assetIndex}`
+ if(card.data.assets[i].ext === 'unknown'){
+ path = `assets/${type}/${name}.png`
+ }
+ else{
+ path = `assets/${type}/${name}.${card.data.assets[i].ext}`
+ }
+ card.data.assets[i].uri = 'embeded://' + path
+ await writer.write(path, rData)
+ }
}
}
if(type === 'json'){
@@ -858,7 +906,12 @@ export async function exportCharacterCard(char:character, type:'png'|'json' = 'p
msg: 'Loading... (Writing)'
})
- await writer.write("ccv3", Buffer.from(JSON.stringify(card)).toString('base64'))
+ if(type === 'charx'){
+ await writer.write("card.json", Buffer.from(JSON.stringify(card, null, 4)))
+ }
+ else{
+ await writer.write("ccv3", Buffer.from(JSON.stringify(card)).toString('base64'))
+ }
}
await writer.end()
diff --git a/src/ts/pngChunk.ts b/src/ts/pngChunk.ts
index 8d6e8bc0..9f6a59e1 100644
--- a/src/ts/pngChunk.ts
+++ b/src/ts/pngChunk.ts
@@ -34,7 +34,7 @@ class StreamChunkWriter{
}
}
}
- async write(key:string, val:string){
+ async write(key:string, val:string|Uint8Array){
const keyData = new TextEncoder().encode(key)
const value = Buffer.from(val)
diff --git a/src/ts/process/processzip.ts b/src/ts/process/processzip.ts
index 8fefe851..eaaa8a0f 100644
--- a/src/ts/process/processzip.ts
+++ b/src/ts/process/processzip.ts
@@ -1,3 +1,5 @@
+import { AppendableBuffer, type LocalWriter, type VirtualWriter } from "../storage/globalApi";
+import * as fflate from "fflate";
export async function processZip(dataArray: Uint8Array): Promise {
const jszip = await import("jszip");
@@ -12,4 +14,59 @@ export async function processZip(dataArray: Uint8Array): Promise {
} else {
throw new Error("No image found in ZIP file");
}
+}
+
+export class CharXWriter{
+ zip:fflate.Zip
+ writeEnd:boolean = false
+ apb = new AppendableBuffer()
+ constructor(private writer:LocalWriter|WritableStreamDefaultWriter|VirtualWriter){
+ const handlerAsync = async (err:Error, dat:Uint8Array, final:boolean) => {
+ if(dat){
+ this.apb.append(dat)
+ }
+ if(final){
+ this.writeEnd = true
+ }
+ }
+
+
+ this.zip = new fflate.Zip()
+ this.zip.ondata = handlerAsync
+ }
+ async init(){
+ //do nothing, just to make compatible with other writer
+ }
+
+ async write(key:string,data:Uint8Array|string){
+ console.log('write',key)
+ let dat:Uint8Array
+ if(typeof data === 'string'){
+ dat = new TextEncoder().encode(data)
+ }
+ else{
+ dat = data
+ }
+ this.writeEnd = false
+ const file = new fflate.ZipDeflate(key, {
+ level: 0
+ });
+ await this.zip.add(file)
+ await file.push(dat, true)
+ await this.writer.write(this.apb.buffer)
+ this.apb.buffer = new Uint8Array(0)
+ if(this.writeEnd){
+ await this.writer.close()
+ }
+
+ }
+
+ async end(){
+ await this.zip.end()
+ await this.writer.write(this.apb.buffer)
+ this.apb.buffer = new Uint8Array(0)
+ if(this.writeEnd){
+ await this.writer.close()
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts
index 10b581ea..549ed060 100644
--- a/src/ts/storage/globalApi.ts
+++ b/src/ts/storage/globalApi.ts
@@ -1144,7 +1144,7 @@ export function getModelMaxContext(model:string):number|undefined{
return undefined
}
-class TauriWriter{
+export class TauriWriter{
path: string
firstWrite: boolean = true
constructor(path: string){
@@ -1560,4 +1560,19 @@ export function updateHeightMode(){
root.style.setProperty('--risu-height-size', '100%');
break
}
+}
+
+export class BlankWriter{
+ constructor(){
+ }
+ async init(){
+ //do nothing, just to make compatible with other writer
+
+ }
+ async write(key:string,data:Uint8Array|string){
+ //do nothing, just to make compatible with other writer
+ }
+ async end(){
+ //do nothing, just to make compatible with other writer
+ }
}
\ No newline at end of file