diff --git a/public/script.js b/public/script.js index 11d9499e5..61f49a397 100644 --- a/public/script.js +++ b/public/script.js @@ -239,6 +239,7 @@ let exportPopper = Popper.createPopper(document.getElementById('export_button'), let dialogueResolve = null; let chat_metadata = {}; let streamingProcessor = null; +let crop_data = undefined; let fav_ch_checked = false; @@ -2821,60 +2822,67 @@ async function saveChat(chat_name, withMetadata) { async function read_avatar_load(input) { if (input.files && input.files[0]) { - const reader = new FileReader(); if (selected_button == "create") { create_save_avatar = input.files; } - reader.onload = async function (e) { - /* $('#dialogue_popup').addClass('large_dialogue_popup'); - $('#dialogue_popup').addClass('wide_dialogue_popup'); - - await callPopup(` -

Click image to start cropping. Click Ok to finalize

-
- -
- `, 'avatarToCrop'); */ - $("#avatar_load_preview").attr("src", e.target.result); + const e = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = resolve; + reader.onerror = reject; + reader.readAsDataURL(input.files[0]); + }) - if (menu_type != "create") { - $("#create_button").trigger('click'); + $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); - const formData = new FormData($("#form_create").get(0)); + const croppedImage = await callPopup(getCropPopup(e.target.result), 'avatarToCrop'); - $(".mes").each(async function () { - if ($(this).attr("is_system") == 'true') { - return; - } - if ($(this).attr("is_user") == 'true') { - return; - } - if ($(this).attr("ch_name") == formData.get('ch_name')) { - const previewSrc = $("#avatar_load_preview").attr("src"); - const avatar = $(this).find(".avatar img"); - avatar.attr('src', default_avatar); - await delay(1); - avatar.attr('src', previewSrc); - } - }); + $("#avatar_load_preview").attr("src", croppedImage || e.target.result); - await delay(durationSaveEdit); - await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { - method: 'GET', - headers: { - 'pragma': 'no-cache', - 'cache-control': 'no-cache', - } - }); - console.log('Avatar refreshed'); + if (menu_type == "create") { + return; + } + + $("#create_button").trigger('click'); + + const formData = new FormData($("#form_create").get(0)); + + $(".mes").each(async function () { + if ($(this).attr("is_system") == 'true') { + return; } - }; + if ($(this).attr("is_user") == 'true') { + return; + } + if ($(this).attr("ch_name") == formData.get('ch_name')) { + const previewSrc = $("#avatar_load_preview").attr("src"); + const avatar = $(this).find(".avatar img"); + avatar.attr('src', default_avatar); + await delay(1); + avatar.attr('src', previewSrc); + } + }); + + await delay(durationSaveEdit); + await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { + method: 'GET', + headers: { + 'pragma': 'no-cache', + 'cache-control': 'no-cache', + } + }); + console.log('Avatar refreshed'); - reader.readAsDataURL(input.files[0]); } } +function getCropPopup(src) { + return `

Set the crop position of the avatar image and click Ok to confirm.

+
+ +
`; +} + function getThumbnailUrl(type, file) { return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; } @@ -3736,10 +3744,6 @@ function callPopup(text, type, inputValue = '') { $("#dialogue_popup_input").val(inputValue); - if (popup_type == 'avatarToCrop') { - - } - if (popup_type == 'input') { $("#dialogue_popup_input").css("display", "block"); $("#dialogue_popup_ok").text("Save"); @@ -3753,6 +3757,17 @@ function callPopup(text, type, inputValue = '') { if (popup_type == 'input') { $("#dialogue_popup_input").focus(); } + if (popup_type == 'avatarToCrop') { + // unset existing data + crop_data = undefined; + + $('#avatarToCrop').cropper({ + aspectRatio: 2 / 3, + crop: function (event) { + crop_data = event.detail; + } + }); + } $("#shadow_popup").transition({ opacity: 1, duration: 200, @@ -4403,7 +4418,7 @@ $(document).ready(function () { $(document).on("click", "#user_avatar_block .avatar_upload", function () { $("#avatar_upload_file").click(); }); - $("#avatar_upload_file").on("change", function (e) { + $("#avatar_upload_file").on("change", async function (e) { const file = e.target.files[0]; if (!file) { @@ -4412,9 +4427,25 @@ $(document).ready(function () { const formData = new FormData($("#form_upload_avatar").get(0)); + const dataUrl = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = resolve; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + + $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); + await callPopup(getCropPopup(dataUrl.target.result), 'avatarToCrop'); + + let url = "/uploaduseravatar"; + + if (crop_data !== undefined) { + url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; + } + jQuery.ajax({ type: "POST", - url: "/uploaduseravatar", + url: url, data: formData, beforeSend: () => { }, cache: false, @@ -4424,6 +4455,7 @@ $(document).ready(function () { if (data.path) { appendUserAvatar(data.path); } + crop_data = undefined; }, error: (jqXHR, exception) => { }, }); @@ -4522,32 +4554,7 @@ $(document).ready(function () { // $("#shadow_popup").css("opacity:", 0.0); if (popup_type == 'avatarToCrop') { - - //ideally this is where we would grab the crop coordinates - //and send back to read_avatar_load() to be sent to server for actual file resizing/cropping. - //the code below does not work 'cropper not defined'. - - $("#avatarToCrop").cropper.getCroppedCanvas(); - - $("#avatarToCrop").cropper.getCroppedCanvas({ - width: 160, - height: 90, - minWidth: 400, - minHeight: 600, - maxWidth: 4096, - maxHeight: 4096, - fillColor: 'transparent', - imageSmoothingEnabled: false, - imageSmoothingQuality: 'high', - }); - - $("#avatarToCrop").cropper.getCroppedCanvas().toBlob((blob) => { - currentCroppedAvatar = new FormData(); - currentCroppedAvatar.append('croppedImage', blob/*, 'example.png' */); - }); - - return currentCroppedAvatar; - + dialogueResolve($("#avatarToCrop").data('cropper').getCroppedCanvas().toDataURL('image/jpeg')); }; if (popup_type == "del_bg") { @@ -4705,10 +4712,15 @@ $(document).ready(function () { if ($("#form_create").attr("actiontype") == "createcharacter") { if ($("#character_name_pole").val().length > 0) { //if the character name text area isn't empty (only posible when creating a new character) - //console.log('/createcharacter entered'); + let url = "/createcharacter"; + + if (crop_data != undefined) { + url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; + } + jQuery.ajax({ type: "POST", - url: "/createcharacter", + url: url, data: formData, beforeSend: function () { $("#create_button").attr("disabled", true); @@ -4760,6 +4772,7 @@ $(document).ready(function () { select_rm_info(`Character created

${DOMPurify.sanitize(save_name)}

`, oldSelectedChar); $("#rm_info_block").transition({ opacity: 1.0, duration: 2000 }); + crop_data = undefined; }, error: function (jqXHR, exception) { $("#create_button").removeAttr("disabled"); @@ -4769,11 +4782,15 @@ $(document).ready(function () { $("#result_info").html("Name not entered"); } } else { - //console.log('/editcharacter -- entered.'); - //console.log('Avatar Button Value:'+$("#add_avatar_button").val()); + let url = '/editcharacter'; + + if (crop_data != undefined) { + url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; + } + jQuery.ajax({ type: "POST", - url: "/editcharacter", + url: url, data: formData, beforeSend: function () { //$("#create_button").attr("disabled", true); @@ -4817,6 +4834,7 @@ $(document).ready(function () { $("#add_avatar_button").val("").clone(true) ); $("#create_button").attr("value", "Save"); + crop_data = undefined; }, error: function (jqXHR, exception) { $("#create_button").removeAttr("disabled"); @@ -5878,28 +5896,4 @@ $(document).ready(function () { $(masterElement).val(myValue).trigger('input'); restoreCaretPosition($(this).get(0), caretPosition); }); - - /* $("#dialogue_popup_text").on('click', '#avatarToCrop', function () { - - var $image = $('#avatarToCrop'); - - $image.cropper({ - aspectRatio: 3 / 4, - crop: function (event) { - console.log(event.detail.x); - console.log(event.detail.y); - console.log(event.detail.width); - console.log(event.detail.height); - console.log(event.detail.rotate); - console.log(event.detail.scaleX); - console.log(event.detail.scaleY); - } - }); - - // Get the Cropper.js instance after initialized - var cropper = $image.data('cropper'); - - - }); */ - }) diff --git a/public/style.css b/public/style.css index 7f3263ab1..6b2aa3eb1 100644 --- a/public/style.css +++ b/public/style.css @@ -2617,7 +2617,7 @@ h5 { } #avatarCropWrap { - margin: 10px; + margin: 10px auto; max-height: 90%; max-width: 90%; } diff --git a/server.js b/server.js index f076ac54f..d6410587d 100644 --- a/server.js +++ b/server.js @@ -164,6 +164,8 @@ function humanizedISO8601DateTime() { var is_colab = process.env.colaburl !== undefined; var charactersPath = 'public/characters/'; var chatsPath = 'public/chats/'; +const AVATAR_WIDTH = 400; +const AVATAR_HEIGHT = 600; const jsonParser = express.json({ limit: '100mb' }); const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' }); const baseRequestArgs = { headers: { "Content-Type": "application/json" } }; @@ -703,8 +705,9 @@ app.post("/createcharacter", urlencodedParser, function (request, response) { if (!request.file) { charaWrite(defaultAvatar, char, internalName, response, avatarName); } else { + const crop = tryParse(request.query.crop); const uploadPath = path.join("./uploads/", request.file.filename); - charaWrite(uploadPath, char, internalName, response, avatarName); + charaWrite(uploadPath, char, internalName, response, avatarName, crop); } }); @@ -799,9 +802,10 @@ app.post("/editcharacter", urlencodedParser, async function (request, response) const avatarPath = path.join(charactersPath, request.body.avatar_url); await charaWrite(avatarPath, char, target_img, response, 'Character saved'); } else { + const crop = tryParse(request.query.crop); const newAvatarPath = path.join("./uploads/", request.file.filename); invalidateThumbnail('avatar', request.body.avatar_url); - await charaWrite(newAvatarPath, char, target_img, response, 'Character saved'); + await charaWrite(newAvatarPath, char, target_img, response, 'Character saved', crop); } } catch { @@ -845,11 +849,17 @@ app.post("/deletecharacter", urlencodedParser, function (request, response) { }); }); -async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok') { +async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok', crop = undefined) { try { // Read the image, resize, and save it as a PNG into the buffer - const rawImg = await jimp.read(img_url); - const image = await rawImg.cover(400, 600).getBufferAsync(jimp.MIME_PNG); + let rawImg = await jimp.read(img_url); + + // Apply crop if defined + if (typeof crop == 'object') { + rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height); + } + + const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG); // Get the chunks const chunks = extract(image); @@ -1525,6 +1535,7 @@ app.post("/importcharacter", urlencodedParser, async function (request, response let filedata = request.file; let uploadPath = path.join('./uploads', filedata.filename); var format = request.body.file_type; + const defaultAvatarPath = './public/img/ai4.png'; //console.log(format); if (filedata) { if (format == 'json') { @@ -1539,16 +1550,37 @@ app.post("/importcharacter", urlencodedParser, async function (request, response jsonData.name = sanitize(jsonData.name); png_name = getPngName(jsonData.name); - let char = { "name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 }; + let char = { + "name": jsonData.name, + "description": jsonData.description ?? '', + "personality": jsonData.personality ?? '', + "first_mes": jsonData.first_mes ?? '', + "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), + "mes_example": jsonData.mes_example ?? '', + "scenario": jsonData.scenario ?? '', + "create_date": humanizedISO8601DateTime(), + "talkativeness": jsonData.talkativeness ?? 0.5 + }; char = JSON.stringify(char); - charaWrite('./public/img/ai4.png', char, png_name, response, { file_name: png_name }); + charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name }); } else if (jsonData.char_name !== undefined) {//json Pygmalion notepad jsonData.char_name = sanitize(jsonData.char_name); png_name = getPngName(jsonData.char_name); - let char = { "name": jsonData.char_name, "description": jsonData.char_persona ?? '', "personality": '', "first_mes": jsonData.char_greeting ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.example_dialogue ?? '', "scenario": jsonData.world_scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 }; + let char = { + "name": jsonData.char_name, + "description": jsonData.char_persona ?? '', + "personality": '', + "first_mes": jsonData.char_greeting ?? '', + "avatar": 'none', + "chat": jsonData.name + " - " + humanizedISO8601DateTime(), + "mes_example": jsonData.example_dialogue ?? '', + "scenario": jsonData.world_scenario ?? '', + "create_date": humanizedISO8601DateTime(), + "talkativeness": jsonData.talkativeness ?? 0.5 + }; char = JSON.stringify(char); - charaWrite('./public/img/ai4.png', char, png_name, response, { file_name: png_name }); + charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name }); } else { console.log('Incorrect character format .json'); response.send({ error: true }); @@ -1561,15 +1593,32 @@ app.post("/importcharacter", urlencodedParser, async function (request, response jsonData.name = sanitize(jsonData.name); if (format == 'webp') { - let convertedPath = path.join('./uploads', path.basename(uploadPath, ".webp") + ".png") - await webp.dwebp(uploadPath, convertedPath, "-o"); - uploadPath = convertedPath; + try { + let convertedPath = path.join('./uploads', path.basename(uploadPath, ".webp") + ".png") + await webp.dwebp(uploadPath, convertedPath, "-o"); + uploadPath = convertedPath; + } + catch { + console.error('WEBP image conversion failed. Using the default character image.'); + uploadPath = defaultAvatarPath; + } } png_name = getPngName(jsonData.name); if (jsonData.name !== undefined) { - let char = { "name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 }; + let char = { + "name": jsonData.name, + "description": jsonData.description ?? '', + "personality": jsonData.personality ?? '', + "first_mes": jsonData.first_mes ?? '', + "avatar": 'none', + "chat": jsonData.name + " - " + humanizedISO8601DateTime(), + "mes_example": jsonData.mes_example ?? '', + "scenario": jsonData.scenario ?? '', + "create_date": humanizedISO8601DateTime(), + "talkativeness": jsonData.talkativeness ?? 0.5 + }; char = JSON.stringify(char); await charaWrite(uploadPath, char, png_name, response, { file_name: png_name }); } @@ -1804,8 +1853,14 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { try { const pathToUpload = path.join('./uploads/' + request.file.filename); - const rawImg = await jimp.read(pathToUpload); - const image = await rawImg.cover(400, 400).getBufferAsync(jimp.MIME_PNG); + const crop = tryParse(request.query.crop); + let rawImg = await jimp.read(pathToUpload); + + if (typeof crop == 'object') { + rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height); + } + + const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG); const filename = `${Date.now()}.png`; const pathToNewFile = path.join(directories.avatars, filename);