From 5f2275eb22b0a6cce8733f21d1230f8cee4bfde9 Mon Sep 17 00:00:00 2001 From: kwaroran Date: Thu, 4 Jan 2024 03:18:11 +0900 Subject: [PATCH] [feat] better image saving --- src/ts/characterCards.ts | 98 +++++++++++++++++++++---------- src/ts/pngChunk.ts | 111 +++++++++++++++++++++++++++++++++++- src/ts/storage/globalApi.ts | 6 +- 3 files changed, 180 insertions(+), 35 deletions(-) diff --git a/src/ts/characterCards.ts b/src/ts/characterCards.ts index c3fe513a..47a11d99 100644 --- a/src/ts/characterCards.ts +++ b/src/ts/characterCards.ts @@ -5,7 +5,7 @@ import { checkNullish, selectMultipleFile, sleep } from "./util" import { language } from "src/lang" import { v4 as uuidv4 } from 'uuid'; 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 { selectedCharID } from "./stores" import { convertImage } from "./parser" @@ -61,19 +61,38 @@ async function importCharacterProcess(f:{ await sleep(10) const img = f.data - const readed = PngChunk.read(img, ['chara'])?.['chara'] - if(!readed){ + // const readed = PngChunk.read(img, ['chara'])?.['chara'] + 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) return } { - const charaData:CharacterCardV2 = JSON.parse(Buffer.from(readed, 'base64').toString('utf-8')) - if(await importSpecv2(charaData, img)){ + const charaData:CharacterCardV2 = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8')) + if(await importSpecv2(charaData, img, "normal", assets)){ let db = get(DataBase) 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)) let db = get(DataBase) 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{ +async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode:'hub'|'normal' = 'normal', assetDict:{[key:string]:string} = {}):Promise{ if(!card ||card.spec !== 'chara_card_v2'){ return false } - if(!mode){ - mode = 'normal' - } const data = card.data 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})` }) 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')) emotions.push([risuext.emotions[i][0],imgp]) } @@ -246,6 +271,15 @@ async function importSpecv2(card:CharacterCardV2, img?:Uint8Array, mode?:'hub'|' let fileName = '' if(risuext.additionalAssets[i].length >= 3) 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) 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) try{ + char.image = '' 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){ for(let i=0;i:"/\\|?*\.\,]/g, "")}_export.json`, Buffer.from(JSON.stringify(card, null, 4), 'utf-8')) alertNormal(language.successExport) return } - alertStore.set({ - type: 'wait', - msg: 'Loading... (Writing Exif)' - }) - await sleep(10) - - img = await reencodeImage(img) - alertStore.set({ type: 'wait', msg: 'Loading... (Writing)' }) - - img = (await PngChunk.write(img, { - "chara":Buffer.from(JSON.stringify(card)).toString('base64') - })) as Uint8Array + await writer.write("chara", Buffer.from(JSON.stringify(card)).toString('base64')) + + await writer.end() - char.image = '' await sleep(10) - await downloadFile(`${char.name.replace(/[<>:"/\\|?*\.\,]/g, "")}_export.png`, img) alertNormal(language.successExport) diff --git a/src/ts/pngChunk.ts b/src/ts/pngChunk.ts index b0a7c9a5..80b77126 100644 --- a/src/ts/pngChunk.ts +++ b/src/ts/pngChunk.ts @@ -2,6 +2,79 @@ import { Buffer } from 'buffer'; import crc32 from 'crc/crc32'; 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 = { read: (data:Uint8Array, chunkName:string[], arg:{checkCrc?:boolean} = {}) => { let pos = 8 @@ -25,7 +98,7 @@ export const PngChunk = { const chunkData = data.slice(pos+8,pos+8+len) let key='' let value='' - for(let i=0;i<10;i++){ + 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)) @@ -40,6 +113,41 @@ export const PngChunk = { } 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) => { let pos = 8 @@ -141,4 +249,5 @@ export const PngChunk = { return Buffer.concat(newData) } }, + streamWriter: StreamChunkWriter } diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts index a5f6b6fb..889a1994 100644 --- a/src/ts/storage/globalApi.ts +++ b/src/ts/storage/globalApi.ts @@ -1172,12 +1172,12 @@ class MobileWriter{ export class LocalWriter{ writer: WritableStreamDefaultWriter|TauriWriter|MobileWriter - async init() { + async init(name = 'Binary', ext = ['bin']) { if(isTauri){ const filePath = await save({ filters: [{ - name: 'Binary', - extensions: ['bin'] + name: name, + extensions: ext }] }); if(!filePath){