From 8dff1a6cdeaa7161912553b51657e1880ea917f2 Mon Sep 17 00:00:00 2001 From: kwaroran Date: Sun, 2 Jun 2024 23:38:55 +0900 Subject: [PATCH] feat: add export via ccv3 charx and json --- src/lang/en.ts | 1 + src/lib/Others/AlertComp.svelte | 18 +++++--- src/ts/alert.ts | 3 +- src/ts/characterCards.ts | 79 +++++++++++++++++++++++++++------ src/ts/pngChunk.ts | 2 +- src/ts/process/processzip.ts | 57 ++++++++++++++++++++++++ src/ts/storage/globalApi.ts | 17 ++++++- 7 files changed, 154 insertions(+), 23 deletions(-) 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