diff --git a/package-lock.json b/package-lock.json index 4a435ec2c..1f3a54419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,12 +23,13 @@ "multer": "^1.4.5-lts.1", "node-rest-client": "^3.1.1", "open": "^8.4.0", + "piexifjs": "^1.0.6", "png-chunk-text": "^1.0.0", "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", "rimraf": "^3.0.2", "sanitize-filename": "^1.6.3", - "webp-converter": "^2.3.3", + "webp-converter": "2.3.2", "ws": "^8.13.0" }, "bin": { @@ -1581,6 +1582,11 @@ "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==" }, + "node_modules/piexifjs": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz", + "integrity": "sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==" + }, "node_modules/pixelmatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", @@ -2028,14 +2034,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2050,12 +2048,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webp-converter": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/webp-converter/-/webp-converter-2.3.3.tgz", - "integrity": "sha512-2p4XvPCIQ/CbUztEFA9vdkILVrRTdMtMxFpQTxlnPc3qx14MV5wnpVvK7m6pG70QdeL+Ser0+Tp843ONwh8VbQ==", - "dependencies": { - "uuid": "^8.3.2" - } + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/webp-converter/-/webp-converter-2.3.2.tgz", + "integrity": "sha512-9kQ9Q/MPzUV2mye8Tv7vA6vDIPk77rI4AWWm2vSaCyGAEsxqyVZYeVU2MSJY5fLkf6u7G5K343vLxKubOxz16Q==" }, "node_modules/whatwg-fetch": { "version": "3.6.2", @@ -3305,6 +3300,11 @@ "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==" }, + "piexifjs": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz", + "integrity": "sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==" + }, "pixelmatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", @@ -3665,11 +3665,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3681,12 +3676,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webp-converter": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/webp-converter/-/webp-converter-2.3.3.tgz", - "integrity": "sha512-2p4XvPCIQ/CbUztEFA9vdkILVrRTdMtMxFpQTxlnPc3qx14MV5wnpVvK7m6pG70QdeL+Ser0+Tp843ONwh8VbQ==", - "requires": { - "uuid": "^8.3.2" - } + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/webp-converter/-/webp-converter-2.3.2.tgz", + "integrity": "sha512-9kQ9Q/MPzUV2mye8Tv7vA6vDIPk77rI4AWWm2vSaCyGAEsxqyVZYeVU2MSJY5fLkf6u7G5K343vLxKubOxz16Q==" }, "whatwg-fetch": { "version": "3.6.2", diff --git a/package.json b/package.json index 48668408f..9124f0644 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,13 @@ "multer": "^1.4.5-lts.1", "node-rest-client": "^3.1.1", "open": "^8.4.0", + "piexifjs": "^1.0.6", "png-chunk-text": "^1.0.0", "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", "rimraf": "^3.0.2", "sanitize-filename": "^1.6.3", - "webp-converter": "^2.3.3", + "webp-converter": "2.3.2", "ws": "^8.13.0" }, "name": "TavernAI", diff --git a/public/index.html b/public/index.html index 90ef8816f..e897b3728 100644 --- a/public/index.html +++ b/public/index.html @@ -1172,6 +1172,11 @@ +
+
PNG
+
JSON
+
WEBP
+
diff --git a/public/script.js b/public/script.js index 48da24d84..ce391084e 100644 --- a/public/script.js +++ b/public/script.js @@ -182,6 +182,9 @@ let is_mes_reload_avatar = false; let optionsPopper = Popper.createPopper(document.getElementById('send_form'), document.getElementById('options'), { placement: 'top-start' }); +let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), { + placement: 'left', +}); let dialogueResolve = null; let chat_metadata = {}; @@ -4482,12 +4485,41 @@ $(document).ready(function () { }, }); }); - $("#export_button").click(function () { - var link = document.createElement("a"); - link.href = "characters/" + characters[this_chid].avatar; - link.download = characters[this_chid].avatar; - document.body.appendChild(link); - link.click(); + $("#export_button").click(function (e) { + $('#export_format_popup').toggle(); + exportPopper.update(); + }); + $(document).on('click', '.export_format', async function () { + const format = $(this).data('format'); + + if (!format) { + return; + } + + const body = { format, avatar_url: characters[this_chid].avatar }; + + const response = await fetch('/exportcharacter', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': token, + }, + body: JSON.stringify(body), + }); + + if (response.ok) { + const filename = characters[this_chid].avatar.replace('.png', `.${format}`); + const blob = await response.blob(); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.setAttribute("download", filename); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + + + $('#export_format_popup').hide(); }); //**************************CHAT IMPORT EXPORT*************************// $("#chat_import_button").click(function () { @@ -4589,6 +4621,10 @@ $(document).ready(function () { $("html").on('touchstart mousedown', function (e) { var clickTarget = $(e.target); + if ($('#export_format_popup').is(':visible') && clickTarget.closest('#export_button').length == 0 && clickTarget.closest('#export_format_popup').length == 0) { + $('#export_format_popup').hide(); + } + const forbiddenTargets = ['#character_cross', '#avatar-and-name-block', '#shadow_popup', '#world_popup']; for (const id of forbiddenTargets) { if (clickTarget.closest(id).length > 0) { @@ -4607,7 +4643,6 @@ $(document).ready(function () { } } - } }); diff --git a/public/scripts/extensions/dice/style.css b/public/scripts/extensions/dice/style.css index 3d7d069e6..268e363b8 100644 --- a/public/scripts/extensions/dice/style.css +++ b/public/scripts/extensions/dice/style.css @@ -18,32 +18,3 @@ #roll_dice:hover { opacity: 1; } - -.list-group { - display: flex; - flex-direction: column; - padding-left: 0; - margin-top: 0; - margin-bottom: 3px; - overflow: hidden; - background-color: black; - border: 1px solid #666; - border-radius: 15px; - box-shadow: 0 0 5px black; - text-shadow: 0 0 3px black; -} - -.list-group-item:hover { - background-color: rgba(255, 255, 255, 0.3); -} - -.list-group-item { - color: rgba(229, 224, 216, 1); - position: relative; - display: block; - padding: 0.75rem 1.25rem; - margin-bottom: -1px; - box-sizing: border-box; - user-select: none; - cursor: pointer; -} \ No newline at end of file diff --git a/public/style.css b/public/style.css index 784ab3e57..0ef7f72b1 100644 --- a/public/style.css +++ b/public/style.css @@ -2576,6 +2576,38 @@ a { text-decoration: none; } +#export_format_popup { + display: none; +} + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-top: 0; + margin-bottom: 3px; + overflow: hidden; + background-color: black; + border: 1px solid #666; + border-radius: 15px; + box-shadow: 0 0 5px black; + text-shadow: 0 0 3px black; +} + +.list-group-item:hover { + background-color: rgba(255, 255, 255, 0.3); +} + +.list-group-item { + color: rgba(229, 224, 216, 1); + position: relative; + display: block; + padding: 0.75rem 1.25rem; + margin-bottom: -1px; + box-sizing: border-box; + user-select: none; + cursor: pointer; +} /* ############################################################# */ /* Right nav panel and nav-toggle */ diff --git a/server.js b/server.js index 4905ddb28..958ff684c 100644 --- a/server.js +++ b/server.js @@ -26,6 +26,7 @@ const ipaddr = require('ipaddr.js'); const json5 = require('json5'); const ExifReader = require('exifreader'); +const exif = require('piexifjs'); const webp = require('webp-converter'); const config = require(path.join(process.cwd(), './config.conf')); @@ -1341,6 +1342,66 @@ app.post("/importcharacter", urlencodedParser, async function (request, response } }); +app.post("/exportcharacter", jsonParser, async function (request, response) { + if (!request.body.format || !request.body.avatar_url) { + return response.sendStatus(400); + } + + let filename = path.join(directories.characters, sanitize(request.body.avatar_url)); + + if (!fs.existsSync(filename)) { + return response.sendStatus(404); + } + + switch (request.body.format) { + case 'png': + return response.sendFile(filename, { root: __dirname }); + case 'json': { + try { + let json = await charaRead(filename); + let jsonObject = json5.parse(json); + return response.type('json').send(jsonObject) + } + catch { + return response.sendStatus(400); + } + } + case 'webp': { + try { + let json = await charaRead(filename); + let inputWebpPath = `./uploads/${Date.now()}_input.webp`; + let outputWebpPath = `./uploads/${Date.now()}_output.webp`; + let metadataPath = `./uploads/${Date.now()}_metadata.exif`; + let metadata = + { + "Exif": { + [exif.ExifIFD.UserComment]: json, + }, + }; + const exifString = exif.dump(metadata); + fs.writeFileSync(metadataPath, exifString, 'binary'); + + await webp.cwebp(filename, inputWebpPath, '-q 95'); + await webp.webpmux_add(inputWebpPath, outputWebpPath, metadataPath, 'exif'); + + response.sendFile(outputWebpPath, { root: __dirname }); + + fs.rmSync(inputWebpPath); + fs.rmSync(metadataPath); + + return; + } + catch (err) { + console.log(err); + return response.sendStatus(400); + } + } + } + + return response.sendStatus(400); +}); + + app.post("/importchat", urlencodedParser, function (request, response) { //console.log(humanizedISO8601DateTime()+':/importchat begun'); if (!request.body) return response.sendStatus(400);