diff --git a/src/BuiltinExtensions/ImageBatchTool/Assets/image_batcher.js b/src/BuiltinExtensions/ImageBatchTool/Assets/image_batcher.js index 3ad171b51..6e70caea3 100644 --- a/src/BuiltinExtensions/ImageBatchTool/Assets/image_batcher.js +++ b/src/BuiltinExtensions/ImageBatchTool/Assets/image_batcher.js @@ -1,6 +1,13 @@ class ImageBatcherClass { + updateSideLengthModeVisibility() { + let isSideLength = getRequiredElementById('ext_image_batcher_res_mode').value == 'Scale Input To Side Length'; + let useSameSideLength = getRequiredElementById('ext_image_batcher_use_same_side_length').checked; + getRequiredElementById('ext_image_batcher_side_length_wrap').style.display = isSideLength ? 'flex' : 'none'; + getRequiredElementById('ext_image_batcher_output_side_length_wrap').style.display = isSideLength && !useSameSideLength ? 'block' : 'none'; + } + doGenerate() { resetBatchIfNeeded(); let batch_id = mainGenHandler.getBatchId(); @@ -12,7 +19,10 @@ class ImageBatcherClass { 'revision': getRequiredElementById('ext_image_batcher_use_as_revision').checked, 'controlnet': getRequiredElementById('ext_image_batcher_use_as_controlnet').checked, 'append_filename_to_prompt': getRequiredElementById('ext_image_batcher_append_filename_to_prompt').checked, - 'resMode': getRequiredElementById('ext_image_batcher_res_mode').value + 'resMode': getRequiredElementById('ext_image_batcher_res_mode').value, + 'use_same_side_length': getRequiredElementById('ext_image_batcher_use_same_side_length').checked, + 'input_side_length': parseInt(getRequiredElementById('ext_image_batcher_input_side_length').value) || 1024, + 'output_side_length': parseInt(getRequiredElementById('ext_image_batcher_output_side_length').value) || 1024 }; let timeLastGenHit = [Date.now()]; let images = {}; @@ -40,7 +50,24 @@ class ImageBatcherClass { + makeCheckboxInput(null, 'ext_image_batcher_use_as_controlnet', '', 'Use As ControlNet Input', 'Whether to use the image as input to ControlNet (only applies if a ControlNet model is enabled).', true, false, true, true) + makeCheckboxInput(null, 'ext_image_batcher_use_as_revision', '', 'Use As Image Prompt', 'Whether to use the image as an Image Prompting input.', false, false, true, true) + makeCheckboxInput(null, 'ext_image_batcher_append_filename_to_prompt', '', 'Append Filename to Prompt', 'Whether to append the filename to the prompt.', false, false, true, true) - + `Resolution: `; + + makeGenericPopover('ext_image_batcher_res_mode', 'Resolution', 'Dropdown', `Choose how the batcher sets generation resolution.`, '') + + makeDropdownInput(null, 'ext_image_batcher_res_mode', '', 'Resolution', '', ['From Parameter', 'From Image', 'Scale To Model', 'Scale To Model Or Above', 'Scale Input To Side Length'], 'From Parameter', false, true) + + ``; + document.getElementById('ext_image_batcher_res_mode').addEventListener('change', () => { + this.updateSideLengthModeVisibility(); + }); + document.getElementById('ext_image_batcher_use_same_side_length').addEventListener('change', () => { + this.updateSideLengthModeVisibility(); + }); + enableSlidersIn(this.mainDiv); + this.updateSideLengthModeVisibility(); toolSelector.addEventListener('change', () => { if (toolSelector.value == 'image_batcher') { showRevisionInputs(); diff --git a/src/BuiltinExtensions/ImageBatchTool/ImageBatchToolExtension.cs b/src/BuiltinExtensions/ImageBatchTool/ImageBatchToolExtension.cs index 6293e5ee4..2cbf11be4 100644 --- a/src/BuiltinExtensions/ImageBatchTool/ImageBatchToolExtension.cs +++ b/src/BuiltinExtensions/ImageBatchTool/ImageBatchToolExtension.cs @@ -10,6 +10,8 @@ using System; using System.IO; using System.Net.WebSockets; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Processing; using ISImage = SixLabors.ImageSharp.Image; namespace SwarmUI.Builtin_ImageBatchToolExtension; @@ -30,7 +32,7 @@ public override void OnInit() } /// API route to generate images with WebSocket updates. - public static async Task ImageBatchRun(WebSocket socket, Session session, JObject rawInput, string input_folder, string output_folder, bool init_image, bool revision, bool controlnet, string resMode, bool append_filename_to_prompt) + public static async Task ImageBatchRun(WebSocket socket, Session session, JObject rawInput, string input_folder, string output_folder, bool init_image, bool revision, bool controlnet, string resMode, bool append_filename_to_prompt, bool use_same_side_length = true, int input_side_length = 1024, int output_side_length = 1024) { // TODO: Strict path validation / user permission confirmation. if (input_folder.Length < 5 || output_folder.Length < 5) @@ -61,17 +63,23 @@ public static async Task ImageBatchRun(WebSocket socket, Session sessio await socket.SendAndReportError($"ImageBatchRun request from {session.User.UserID}, for folder '{input_folder}'", "Image batch needs to supply the images to at least one parameter.", API.WebsocketTimeout); return null; } + // In case someone tries to leverage the websocket API directly, not possible from UI + if (input_side_length <= 0 || output_side_length <= 0) + { + await socket.SendAndReportError($"ImageBatchRun request from {session.User.UserID}", "Side lengths must be positive values.", API.WebsocketTimeout); + return null; + } Directory.CreateDirectory(output_folder); - await API.RunWebsocketHandlerCallWS(GenBatchRun_Internal, session, (rawInput, input_folder, output_folder, init_image, revision, controlnet, imageFiles, resMode, append_filename_to_prompt), socket); + await API.RunWebsocketHandlerCallWS(GenBatchRun_Internal, session, (rawInput, input_folder, output_folder, init_image, revision, controlnet, imageFiles, resMode, append_filename_to_prompt, use_same_side_length, input_side_length, output_side_length), socket); Logs.Info("Image Batcher completed successfully"); await socket.SendJson(new JObject() { ["success"] = "complete" }, API.WebsocketTimeout); return null; } - public static async Task GenBatchRun_Internal(Session session, (JObject, string, string, bool, bool, bool, string[], string, bool) input, Action output, bool isWS) + public static async Task GenBatchRun_Internal(Session session, (JObject, string, string, bool, bool, bool, string[], string, bool, bool, int, int) input, Action output, bool isWS) { // TODO: This is a silly way of passing data, time for a struct? - (JObject rawInput, string input_folder, string output_folder, bool init_image, bool revision, bool controlnet, string[] imageFiles, string resMode, bool appendFilenameToPrompt) = input; + (JObject rawInput, string input_folder, string output_folder, bool init_image, bool revision, bool controlnet, string[] imageFiles, string resMode, bool appendFilenameToPrompt, bool useSameSideLength, int inputSideLength, int outputSideLength) = input; using Session.GenClaim claim = session.Claim(gens: imageFiles.Length); async Task sendStatus() { @@ -129,6 +137,17 @@ void removeDoneTasks() } Image image = new(File.ReadAllBytes(file), MediaType.GetByExtension(file.AfterLast('.'))); ISImage imgData = image.ToIS; + // Check EXIF to make sure we have the correct orientation + if (imgData.Metadata?.ExifProfile?.TryGetValue(ExifTag.Orientation, out IExifValue orientationValue) ?? false) + { + ushort orientation = orientationValue.Value; + if (orientation != 1) + { + using ISImage oriented = imgData.Clone(x => x.AutoOrient()); + image = new Image(ImageFile.ISImgToPngBytes(oriented), MediaType.ImagePng); + imgData = image.ToIS; + } + } T2IParamInput param = baseParams.Clone(); void setRes(int width, int height) { @@ -164,6 +183,20 @@ void setRes(int width, int height) setRes(width, height); } break; + case "Scale Input To Side Length": + (int scaledInputWidth, int scaledInputHeight) = Utilities.ResToModelFit(imgData.Width, imgData.Height, inputSideLength * inputSideLength, 16); + image = (Image)((ImageFile)image).Resize(scaledInputWidth, scaledInputHeight); + imgData = image.ToIS; + if (useSameSideLength) + { + setRes(scaledInputWidth, scaledInputHeight); + } + else + { + (int scaledOutputWidth, int scaledOutputHeight) = Utilities.ResToModelFit(imgData.Width, imgData.Height, outputSideLength * outputSideLength, 16); + setRes(scaledOutputWidth, scaledOutputHeight); + } + break; default: throw new SwarmUserErrorException("Invalid resolution mode"); }