[feat] better image saving
This commit is contained in:
@@ -5,7 +5,7 @@ import { checkNullish, selectMultipleFile, sleep } from "./util"
|
|||||||
import { language } from "src/lang"
|
import { language } from "src/lang"
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { characterFormatUpdate } from "./characters"
|
import { characterFormatUpdate } from "./characters"
|
||||||
import { checkCharOrder, downloadFile, readImage, saveAsset } from "./storage/globalApi"
|
import { checkCharOrder, downloadFile, LocalWriter, readImage, saveAsset } from "./storage/globalApi"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import { selectedCharID } from "./stores"
|
import { selectedCharID } from "./stores"
|
||||||
import { convertImage } from "./parser"
|
import { convertImage } from "./parser"
|
||||||
@@ -61,19 +61,38 @@ async function importCharacterProcess(f:{
|
|||||||
await sleep(10)
|
await sleep(10)
|
||||||
const img = f.data
|
const img = f.data
|
||||||
|
|
||||||
const readed = PngChunk.read(img, ['chara'])?.['chara']
|
// const readed = PngChunk.read(img, ['chara'])?.['chara']
|
||||||
if(!readed){
|
let readedChara = ''
|
||||||
|
const readGenerator = PngChunk.readGenerator(img)
|
||||||
|
const assets:{[key:string]:string} = {}
|
||||||
|
for await(const chunk of readGenerator){
|
||||||
|
if(!chunk){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if(chunk.key === 'chara'){
|
||||||
|
readedChara = chunk.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if(chunk.key.startsWith('chara-ext-asset_')){
|
||||||
|
const assetIndex = (chunk.key.replace('chara-ext-asset_', ''))
|
||||||
|
alertWait('Loading... (Reading Asset ' + assetIndex + ')' )
|
||||||
|
const assetData = Buffer.from(chunk.value, 'base64')
|
||||||
|
const assetId = await saveAsset(assetData)
|
||||||
|
assets[assetIndex] = assetId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!readedChara){
|
||||||
alertError(language.errors.noData)
|
alertError(language.errors.noData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const charaData:CharacterCardV2 = JSON.parse(Buffer.from(readed, 'base64').toString('utf-8'))
|
const charaData:CharacterCardV2 = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
|
||||||
if(await importSpecv2(charaData, img)){
|
if(await importSpecv2(charaData, img, "normal", assets)){
|
||||||
let db = get(DataBase)
|
let db = get(DataBase)
|
||||||
return db.characters.length - 1
|
return db.characters.length - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const charaData:OldTavernChar = JSON.parse(Buffer.from(readed, 'base64').toString('utf-8'))
|
const charaData:OldTavernChar = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
|
||||||
const imgp = await saveAsset(await reencodeImage(img))
|
const imgp = await saveAsset(await reencodeImage(img))
|
||||||
let db = get(DataBase)
|
let db = get(DataBase)
|
||||||
db.characters.push(convertOldTavernAndJSON(charaData, imgp))
|
db.characters.push(convertOldTavernAndJSON(charaData, imgp))
|
||||||
@@ -203,13 +222,10 @@ export async function exportChar(charaID:number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|'normal'):Promise<boolean>{
|
async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'normal' = 'normal', assetDict:{[key:string]:string} = {}):Promise<boolean>{
|
||||||
if(!card ||card.spec !== 'chara_card_v2'){
|
if(!card ||card.spec !== 'chara_card_v2'){
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if(!mode){
|
|
||||||
mode = 'normal'
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = card.data
|
const data = card.data
|
||||||
const im = img ? await saveAsset(await reencodeImage(img)) : undefined
|
const im = img ? await saveAsset(await reencodeImage(img)) : undefined
|
||||||
@@ -232,6 +248,15 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|'
|
|||||||
msg: `Loading... (Getting Emotions ${i} / ${risuext.emotions.length})`
|
msg: `Loading... (Getting Emotions ${i} / ${risuext.emotions.length})`
|
||||||
})
|
})
|
||||||
await sleep(10)
|
await sleep(10)
|
||||||
|
if(risuext.emotions[i][1].startsWith('__asset:')){
|
||||||
|
const key = risuext.emotions[i][1].replace('__asset:', '')
|
||||||
|
const imgp = assetDict[key]
|
||||||
|
if(!imgp){
|
||||||
|
throw new Error('Error while importing, asset ' + key + ' not found')
|
||||||
|
}
|
||||||
|
emotions.push([risuext.emotions[i][0],imgp])
|
||||||
|
continue
|
||||||
|
}
|
||||||
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.emotions[i][1])) : Buffer.from(risuext.emotions[i][1], 'base64'))
|
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.emotions[i][1])) : Buffer.from(risuext.emotions[i][1], 'base64'))
|
||||||
emotions.push([risuext.emotions[i][0],imgp])
|
emotions.push([risuext.emotions[i][0],imgp])
|
||||||
}
|
}
|
||||||
@@ -246,6 +271,15 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|'
|
|||||||
let fileName = ''
|
let fileName = ''
|
||||||
if(risuext.additionalAssets[i].length >= 3)
|
if(risuext.additionalAssets[i].length >= 3)
|
||||||
fileName = risuext.additionalAssets[i][2]
|
fileName = risuext.additionalAssets[i][2]
|
||||||
|
if(risuext.additionalAssets[i][1].startsWith('__asset:')){
|
||||||
|
const key = risuext.additionalAssets[i][1].replace('__asset:', '')
|
||||||
|
const imgp = assetDict[key]
|
||||||
|
if(!imgp){
|
||||||
|
throw new Error('Error while importing, asset ' + key + ' not found')
|
||||||
|
}
|
||||||
|
extAssets.push([risuext.additionalAssets[i][0],imgp,fileName])
|
||||||
|
continue
|
||||||
|
}
|
||||||
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.additionalAssets[i][1])) :Buffer.from(risuext.additionalAssets[i][1], 'base64'), '', fileName)
|
const imgp = await saveAsset(mode === 'hub' ? (await getHubResources(risuext.additionalAssets[i][1])) :Buffer.from(risuext.additionalAssets[i][1], 'base64'), '', fileName)
|
||||||
extAssets.push([risuext.additionalAssets[i][0],imgp,fileName])
|
extAssets.push([risuext.additionalAssets[i][0],imgp,fileName])
|
||||||
}
|
}
|
||||||
@@ -469,16 +503,26 @@ export async function exportSpecV2(char:character, type:'png'|'json' = 'png') {
|
|||||||
let img = await readImage(char.image)
|
let img = await readImage(char.image)
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
char.image = ''
|
||||||
const card = await createBaseV2(char)
|
const card = await createBaseV2(char)
|
||||||
|
img = await reencodeImage(img)
|
||||||
|
const localWriter = new LocalWriter()
|
||||||
|
await localWriter.init(`Image file`, ['png'])
|
||||||
|
const writer = new PngChunk.streamWriter(img, localWriter)
|
||||||
|
await writer.init()
|
||||||
|
let assetIndex = 0
|
||||||
if(card.data.extensions.risuai.emotions && card.data.extensions.risuai.emotions.length > 0){
|
if(card.data.extensions.risuai.emotions && card.data.extensions.risuai.emotions.length > 0){
|
||||||
for(let i=0;i<card.data.extensions.risuai.emotions.length;i++){
|
for(let i=0;i<card.data.extensions.risuai.emotions.length;i++){
|
||||||
alertStore.set({
|
alertStore.set({
|
||||||
type: 'wait',
|
type: 'wait',
|
||||||
msg: `Loading... (Adding Emotions ${i} / ${card.data.extensions.risuai.emotions.length})`
|
msg: `Loading... (Adding Emotions ${i} / ${card.data.extensions.risuai.emotions.length})`
|
||||||
})
|
})
|
||||||
const rData = await readImage(card.data.extensions.risuai.emotions[i][1])
|
const key = card.data.extensions.risuai.emotions[i][1]
|
||||||
char.emotionImages[i][1] = Buffer.from(await convertImage(rData)).toString('base64')
|
const rData = await readImage(key)
|
||||||
|
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
|
||||||
|
assetIndex++
|
||||||
|
card.data.extensions.risuai.emotions[i][1] = `__asset:${assetIndex}`
|
||||||
|
await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,39 +533,31 @@ export async function exportSpecV2(char:character, type:'png'|'json' = 'png') {
|
|||||||
type: 'wait',
|
type: 'wait',
|
||||||
msg: `Loading... (Adding Additional Assets ${i} / ${card.data.extensions.risuai.additionalAssets.length})`
|
msg: `Loading... (Adding Additional Assets ${i} / ${card.data.extensions.risuai.additionalAssets.length})`
|
||||||
})
|
})
|
||||||
const rData = await readImage(card.data.extensions.risuai.additionalAssets[i][1])
|
const key = card.data.extensions.risuai.additionalAssets[i][1]
|
||||||
char.additionalAssets[i][1] = Buffer.from(await convertImage(rData)).toString('base64')
|
const rData = await readImage(key)
|
||||||
|
const b64encoded = Buffer.from(await convertImage(rData)).toString('base64')
|
||||||
|
assetIndex++
|
||||||
|
card.data.extensions.risuai.additionalAssets[i][1] = `__asset:${assetIndex}`
|
||||||
|
await writer.write("chara-ext-asset_" + assetIndex, b64encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if(type === 'json'){
|
if(type === 'json'){
|
||||||
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8'))
|
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8'))
|
||||||
alertNormal(language.successExport)
|
alertNormal(language.successExport)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alertStore.set({
|
|
||||||
type: 'wait',
|
|
||||||
msg: 'Loading... (Writing Exif)'
|
|
||||||
})
|
|
||||||
|
|
||||||
await sleep(10)
|
await sleep(10)
|
||||||
|
|
||||||
img = await reencodeImage(img)
|
|
||||||
|
|
||||||
alertStore.set({
|
alertStore.set({
|
||||||
type: 'wait',
|
type: 'wait',
|
||||||
msg: 'Loading... (Writing)'
|
msg: 'Loading... (Writing)'
|
||||||
})
|
})
|
||||||
|
await writer.write("chara", Buffer.from(JSON.stringify(card)).toString('base64'))
|
||||||
img = (await PngChunk.write(img, {
|
|
||||||
"chara":Buffer.from(JSON.stringify(card)).toString('base64')
|
await writer.end()
|
||||||
})) as Uint8Array
|
|
||||||
|
|
||||||
char.image = ''
|
|
||||||
await sleep(10)
|
await sleep(10)
|
||||||
await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.png`, img)
|
|
||||||
|
|
||||||
alertNormal(language.successExport)
|
alertNormal(language.successExport)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,79 @@ import { Buffer } from 'buffer';
|
|||||||
import crc32 from 'crc/crc32';
|
import crc32 from 'crc/crc32';
|
||||||
import type { LocalWriter } from './storage/globalApi';
|
import type { LocalWriter } from './storage/globalApi';
|
||||||
|
|
||||||
|
class StreamChunkWriter{
|
||||||
|
constructor(private data:Uint8Array, private writer:LocalWriter){
|
||||||
|
|
||||||
|
}
|
||||||
|
async pushData(data:Uint8Array){
|
||||||
|
await this.writer.write(data)
|
||||||
|
}
|
||||||
|
async init(){
|
||||||
|
let pos = 8
|
||||||
|
let newData:Uint8Array[] = []
|
||||||
|
|
||||||
|
|
||||||
|
const data = this.data
|
||||||
|
await this.pushData(data.slice(0,8))
|
||||||
|
|
||||||
|
while(pos < data.length){
|
||||||
|
const len = data[pos] * 0x1000000 + data[pos+1] * 0x10000 + data[pos+2] * 0x100 + data[pos+3]
|
||||||
|
const type = data.slice(pos+4,pos+8)
|
||||||
|
const typeString = new TextDecoder().decode(type)
|
||||||
|
if(typeString === 'IEND'){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if(typeString === 'tEXt'){
|
||||||
|
pos += 12 + len
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
await this.pushData(data.slice(pos,pos+12+len))
|
||||||
|
pos += 12 + len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async write(key:string, val:string){
|
||||||
|
|
||||||
|
const keyData = new TextEncoder().encode(key)
|
||||||
|
const value = Buffer.from(val)
|
||||||
|
const lenNum = value.byteLength + keyData.byteLength + 1
|
||||||
|
//idk, but uint32array is not working
|
||||||
|
const length = new Uint8Array([
|
||||||
|
lenNum / 0x1000000 % 0x100,
|
||||||
|
lenNum / 0x10000 % 0x100,
|
||||||
|
lenNum / 0x100 % 0x100,
|
||||||
|
lenNum % 0x100
|
||||||
|
])
|
||||||
|
const type = new TextEncoder().encode('tEXt')
|
||||||
|
await this.pushData(length)
|
||||||
|
await this.pushData(type)
|
||||||
|
await this.pushData(keyData)
|
||||||
|
await this.pushData(new Uint8Array([0]))
|
||||||
|
await this.pushData(value)
|
||||||
|
const crc = crc32(Buffer.concat([type,keyData,new Uint8Array([0]),value]))
|
||||||
|
await this.pushData(new Uint8Array([
|
||||||
|
crc / 0x1000000 % 0x100,
|
||||||
|
crc / 0x10000 % 0x100,
|
||||||
|
crc / 0x100 % 0x100,
|
||||||
|
crc % 0x100
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
async end() {
|
||||||
|
const length = new Uint8Array((new Uint32Array([0])).buffer)
|
||||||
|
const type = new TextEncoder().encode('IEND')
|
||||||
|
await this.pushData(length)
|
||||||
|
await this.pushData(type)
|
||||||
|
const crc = crc32(type)
|
||||||
|
await this.pushData(new Uint8Array([
|
||||||
|
crc / 0x1000000 % 0x100,
|
||||||
|
crc / 0x10000 % 0x100,
|
||||||
|
crc / 0x100 % 0x100,
|
||||||
|
crc % 0x100
|
||||||
|
]))
|
||||||
|
this.writer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const PngChunk = {
|
export const PngChunk = {
|
||||||
read: (data:Uint8Array, chunkName:string[], arg:{checkCrc?:boolean} = {}) => {
|
read: (data:Uint8Array, chunkName:string[], arg:{checkCrc?:boolean} = {}) => {
|
||||||
let pos = 8
|
let pos = 8
|
||||||
@@ -25,7 +98,7 @@ export const PngChunk = {
|
|||||||
const chunkData = data.slice(pos+8,pos+8+len)
|
const chunkData = data.slice(pos+8,pos+8+len)
|
||||||
let key=''
|
let key=''
|
||||||
let value=''
|
let value=''
|
||||||
for(let i=0;i<10;i++){
|
for(let i=0;i<70;i++){
|
||||||
if(chunkData[i] === 0){
|
if(chunkData[i] === 0){
|
||||||
key = new TextDecoder().decode(chunkData.slice(0,i))
|
key = new TextDecoder().decode(chunkData.slice(0,i))
|
||||||
value = new TextDecoder().decode(chunkData.slice(i))
|
value = new TextDecoder().decode(chunkData.slice(i))
|
||||||
@@ -40,6 +113,41 @@ export const PngChunk = {
|
|||||||
}
|
}
|
||||||
return chunks
|
return chunks
|
||||||
},
|
},
|
||||||
|
|
||||||
|
readGenerator: function*(data:Uint8Array, arg:{checkCrc?:boolean} = {}):Generator<{key:string,value:string},null>{
|
||||||
|
let pos = 8
|
||||||
|
while(pos < data.length){
|
||||||
|
const len = data[pos] * 0x1000000 + data[pos+1] * 0x10000 + data[pos+2] * 0x100 + data[pos+3]
|
||||||
|
const type = data.slice(pos+4,pos+8)
|
||||||
|
const typeString = new TextDecoder().decode(type)
|
||||||
|
console.log(typeString, len)
|
||||||
|
if(arg.checkCrc){
|
||||||
|
const crc = data[pos+8+len] * 0x1000000 + data[pos+9+len] * 0x10000 + data[pos+10+len] * 0x100 + data[pos+11+len]
|
||||||
|
const crcCheck = crc32(data.slice(pos+4,pos+8+len))
|
||||||
|
if(crc !== crcCheck){
|
||||||
|
throw new Error('crc check failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(typeString === 'IEND'){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if(typeString === 'tEXt'){
|
||||||
|
const chunkData = data.slice(pos+8,pos+8+len)
|
||||||
|
let key=''
|
||||||
|
let value=''
|
||||||
|
for(let i=0;i<70;i++){
|
||||||
|
if(chunkData[i] === 0){
|
||||||
|
key = new TextDecoder().decode(chunkData.slice(0,i))
|
||||||
|
value = new TextDecoder().decode(chunkData.slice(i))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield {key,value}
|
||||||
|
}
|
||||||
|
pos += 12 + len
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
trim: (data:Uint8Array) => {
|
trim: (data:Uint8Array) => {
|
||||||
let pos = 8
|
let pos = 8
|
||||||
@@ -141,4 +249,5 @@ export const PngChunk = {
|
|||||||
return Buffer.concat(newData)
|
return Buffer.concat(newData)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
streamWriter: StreamChunkWriter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1172,12 +1172,12 @@ class MobileWriter{
|
|||||||
|
|
||||||
export class LocalWriter{
|
export class LocalWriter{
|
||||||
writer: WritableStreamDefaultWriter|TauriWriter|MobileWriter
|
writer: WritableStreamDefaultWriter|TauriWriter|MobileWriter
|
||||||
async init() {
|
async init(name = 'Binary', ext = ['bin']) {
|
||||||
if(isTauri){
|
if(isTauri){
|
||||||
const filePath = await save({
|
const filePath = await save({
|
||||||
filters: [{
|
filters: [{
|
||||||
name: 'Binary',
|
name: name,
|
||||||
extensions: ['bin']
|
extensions: ext
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
if(!filePath){
|
if(!filePath){
|
||||||
|
|||||||
Reference in New Issue
Block a user