diff --git a/electron/commands/batch-upscayl.ts b/electron/commands/batch-upscayl.ts index df099b5..2a1cebe 100644 --- a/electron/commands/batch-upscayl.ts +++ b/electron/commands/batch-upscayl.ts @@ -3,12 +3,14 @@ import { getMainWindow } from "../main-window"; import { childProcesses, customModelsFolderPath, + customWidth, noImageProcessing, saveOutputFolder, setCompression, setNoImageProcessing, setStopped, stopped, + useCustomWidth, } from "../utils/config-variables"; import logit from "../utils/logit"; import { spawnUpscayl } from "../utils/spawn-upscayl"; @@ -47,11 +49,11 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { let initialScale = getModelScale(model); - const desiredScale = payload.scale as string; + const desiredScale = useCustomWidth + ? customWidth || payload.scale + : payload.scale; - const outputFolderName = `upscayl_${model}_x${ - noImageProcessing ? initialScale : desiredScale - }`; + const outputFolderName = `upscayl_${model}_${noImageProcessing ? initialScale : desiredScale}${useCustomWidth ? "px_" : "x_"}`; outputFolderPath += slash + outputFolderName; if (!fs.existsSync(outputFolderPath)) { fs.mkdirSync(outputFolderPath, { recursive: true }); @@ -78,9 +80,9 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { model, gpuId, saveImageAs, - initialScale + initialScale, ), - logit + logit, ); childProcesses.push(upscayl); @@ -94,7 +96,7 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { data = data.toString(); mainWindow.webContents.send( COMMAND.FOLDER_UPSCAYL_PROGRESS, - data.toString() + data.toString(), ); if (data.includes("invalid") || data.includes("failed")) { logit("❌ INVALID GPU OR INVALID FILES IN FOLDER - FAILED"); @@ -110,14 +112,14 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { mainWindow.setProgressBar(-1); mainWindow.webContents.send( COMMAND.FOLDER_UPSCAYL_PROGRESS, - data.toString() + data.toString(), ); failed = true; upscayl.kill(); mainWindow && mainWindow.webContents.send( COMMAND.UPSCAYL_ERROR, - "Error upscaling image. Error: " + data + "Error upscaling image. Error: " + data, ); return; }; @@ -133,7 +135,7 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { mainWindow.setProgressBar(-1); mainWindow.webContents.send( COMMAND.FOLDER_UPSCAYL_DONE, - outputFolderPath + outputFolderPath, ); return; } @@ -148,19 +150,19 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { isAlpha ? `${outputFolderPath}${slash}${removeFileExtension(file)}.png` : `${outputFolderPath}${slash}${removeFileExtension( - file + file, )}.${saveImageAs}`, `${outputFolderPath}${slash}${removeFileExtension( - file + file, )}.${saveImageAs}`, desiredScale, saveImageAs, - isAlpha + isAlpha, ); }); mainWindow.webContents.send( COMMAND.FOLDER_UPSCAYL_DONE, - outputFolderPath + outputFolderPath, ); showNotification("Upscayled", "Image upscayled successfully!"); } catch (error) { @@ -170,7 +172,7 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => { mainWindow.webContents.send( COMMAND.UPSCAYL_ERROR, "Error processing (scaling and converting) the image. Please report this error on Upscayl GitHub Issues page.\n" + - error + error, ); showNotification("Upscayl Failure", "Failed to upscale image!"); } diff --git a/electron/commands/double-upscayl.ts b/electron/commands/double-upscayl.ts index 7976881..1f9630a 100644 --- a/electron/commands/double-upscayl.ts +++ b/electron/commands/double-upscayl.ts @@ -3,6 +3,7 @@ import { getMainWindow } from "../main-window"; import { childProcesses, customModelsFolderPath, + customWidth, noImageProcessing, outputFolderPath, saveOutputFolder, @@ -10,6 +11,7 @@ import { setNoImageProcessing, setStopped, stopped, + useCustomWidth, } from "../utils/config-variables"; import slash from "../utils/slash"; import { spawnUpscayl } from "../utils/spawn-upscayl"; @@ -55,7 +57,9 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { let initialScale = getModelScale(model); - const desiredScale = parseInt(payload.scale) * parseInt(payload.scale); + const desiredScale = useCustomWidth + ? customWidth || parseInt(payload.scale) * parseInt(payload.scale) + : parseInt(payload.scale) * parseInt(payload.scale); const outFile = outputDir + @@ -65,7 +69,7 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { (noImageProcessing ? parseInt(initialScale) * parseInt(initialScale) : desiredScale) + - "x_" + + (useCustomWidth ? "px_" : "x_") + model + "." + saveImageAs; @@ -80,9 +84,9 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { model, gpuId, saveImageAs, - initialScale + initialScale, ), - logit + logit, ); childProcesses.push(upscayl); @@ -119,7 +123,7 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { mainWindow && mainWindow.webContents.send( COMMAND.UPSCAYL_ERROR, - "Error upscaling image. Error: " + data + "Error upscaling image. Error: " + data, ); showNotification("Upscayl Failure", "Failed to upscale image!"); upscayl.kill(); @@ -140,12 +144,12 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { isAlpha ? (outFile + ".png").replace( /([^/\\]+)$/i, - encodeURIComponent((outFile + ".png").match(/[^/\\]+$/i)![0]) + encodeURIComponent((outFile + ".png").match(/[^/\\]+$/i)![0]), ) : outFile.replace( /([^/\\]+)$/i, - encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]) - ) + encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]), + ), ); return; } @@ -157,15 +161,15 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { outFile, desiredScale.toString(), saveImageAs, - isAlpha + isAlpha, ); mainWindow.setProgressBar(-1); mainWindow.webContents.send( COMMAND.DOUBLE_UPSCAYL_DONE, outFile.replace( /([^/\\]+)$/i, - encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]) - ) + encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]), + ), ); if (isAlpha && saveImageAs === "jpg") { unlinkSync(outFile + ".png"); @@ -177,7 +181,7 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { mainWindow.webContents.send( COMMAND.UPSCAYL_ERROR, "Error processing (scaling and converting) the image. Please report this error on Upscayl GitHub Issues page.\n" + - error + error, ); showNotification("Upscayl Failure", "Failed to upscale image!"); upscayl.kill(); @@ -199,9 +203,9 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { model, gpuId, saveImageAs, - initialScale + initialScale, ), - logit + logit, ); childProcesses.push(upscayl2); @@ -228,7 +232,7 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => { mainWindow && mainWindow.webContents.send( COMMAND.UPSCAYL_ERROR, - "Error upscaling image. Error: " + data + "Error upscaling image. Error: " + data, ); showNotification("Upscayl Failure", "Failed to upscale image!"); upscayl2.kill(); diff --git a/electron/commands/image-upscayl.ts b/electron/commands/image-upscayl.ts index b4560bc..1138295 100644 --- a/electron/commands/image-upscayl.ts +++ b/electron/commands/image-upscayl.ts @@ -4,6 +4,7 @@ import COMMAND from "../../common/commands"; import { compression, customModelsFolderPath, + customWidth, folderPath, noImageProcessing, outputFolderPath, @@ -13,6 +14,7 @@ import { setNoImageProcessing, setStopped, stopped, + useCustomWidth, } from "../utils/config-variables"; import convertAndScale from "../utils/convert-and-scale"; import { getSingleImageArguments } from "../utils/get-arguments"; @@ -62,7 +64,9 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { let initialScale = getModelScale(model); - const desiredScale = payload.scale; + const desiredScale = useCustomWidth + ? customWidth || payload.scale + : payload.scale; const outFile = outputDir + @@ -70,7 +74,7 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { fileName + "_upscayl_" + (noImageProcessing ? initialScale : desiredScale) + - "x_" + + (useCustomWidth ? "px_" : "x_") + model + "." + saveImageAs; @@ -83,8 +87,8 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { COMMAND.UPSCAYL_DONE, outFile.replace( /([^/\\]+)$/i, - encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]) - ) + encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]), + ), ); } else { logit( @@ -101,7 +105,7 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { desiredScale, outFile, compression, - }) + }), ); const upscayl = spawnUpscayl( getSingleImageArguments( @@ -112,9 +116,9 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { model, initialScale, gpuId, - saveImageAs + saveImageAs, ), - logit + logit, ); setChildProcesses(upscayl); @@ -157,8 +161,8 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { COMMAND.UPSCAYL_DONE, outFile.replace( /([^/\\]+)$/i, - encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]) - ) + encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]), + ), ); return; } @@ -172,7 +176,7 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { outFile, desiredScale, saveImageAs, - isAlpha + isAlpha, ); // Remove the png file (default) if the saveImageAs is not png // fs.access( @@ -190,20 +194,20 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => { COMMAND.UPSCAYL_DONE, outFile.replace( /([^/\\]+)$/i, - encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]) - ) + encodeURIComponent(outFile.match(/[^/\\]+$/i)![0]), + ), ); showNotification("Upscayl", "Image upscayled successfully!"); } catch (error) { logit( "❌ Error processing (scaling and converting) the image. Please report this error on GitHub.", - error + error, ); upscayl.kill(); mainWindow.webContents.send( COMMAND.UPSCAYL_ERROR, "Error processing (scaling and converting) the image. Please report this error on Upscayl GitHub Issues page.\n" + - error + error, ); showNotification("Upscayl Failure", "Failed to upscale image!"); } diff --git a/electron/utils/config-variables.ts b/electron/utils/config-variables.ts index a36fb47..00da749 100644 --- a/electron/utils/config-variables.ts +++ b/electron/utils/config-variables.ts @@ -15,6 +15,8 @@ export let childProcesses: { }[] = []; export let noImageProcessing: boolean = false; export let turnOffNotifications: boolean = false; +export let customWidth: string | null = null; +export let useCustomWidth: boolean = false; export function setImagePath(value: string | undefined): void { imagePath = value; @@ -62,7 +64,7 @@ export function setChildProcesses(value: { JSON.stringify({ binary: childProcesses[0].process.spawnfile, args: childProcesses[0].process.spawnargs, - }) + }), ); } @@ -76,6 +78,16 @@ export function setTurnOffNotifications(value: boolean): void { logit("🔕 Updating Turn Off Notifications: ", turnOffNotifications); } +export function setCustomWidth(value: string | null): void { + customWidth = value; + logit("📏 Updating Custom Width: ", customWidth); +} + +export function setUseCustomWidth(value: boolean): void { + useCustomWidth = value; + logit("📏 Updating Use Custom Width: ", useCustomWidth); +} + // LOCAL STORAGE export function fetchLocalStorage(): void { const mainWindow = getMainWindow(); @@ -101,7 +113,7 @@ export function fetchLocalStorage(): void { mainWindow.webContents .executeJavaScript( 'localStorage.getItem("lastCustomModelsFolderPath");', - true + true, ) .then((lastCustomModelsFolderPath: string | null) => { if (lastCustomModelsFolderPath && lastCustomModelsFolderPath.length > 0) { @@ -149,4 +161,22 @@ export function fetchLocalStorage(): void { setTurnOffNotifications(lastSaved === "true"); } }); + + // GET CUSTOM WIDTH (STRING) FROM LOCAL STORAGE + mainWindow.webContents + .executeJavaScript('localStorage.getItem("customWidth");', true) + .then((lastSaved: string | null) => { + if (lastSaved !== null) { + setCustomWidth(lastSaved); + } + }); + + // GET USE CUSTOM WIDTH (BOOLEAN) FROM LOCAL STORAGE + mainWindow.webContents + .executeJavaScript('localStorage.getItem("useCustomWidth");', true) + .then((lastSaved: string | null) => { + if (lastSaved !== null) { + setUseCustomWidth(lastSaved === "true"); + } + }); } diff --git a/electron/utils/convert-and-scale.ts b/electron/utils/convert-and-scale.ts index 998b966..1cb596b 100644 --- a/electron/utils/convert-and-scale.ts +++ b/electron/utils/convert-and-scale.ts @@ -1,7 +1,7 @@ import fs from "fs"; import sharp, { FormatEnum, Metadata } from "sharp"; import logit from "./logit"; -import { compression } from "./config-variables"; +import { compression, customWidth } from "./config-variables"; import { ImageFormat } from "./types"; const convertAndScale = async ( @@ -10,9 +10,9 @@ const convertAndScale = async ( processedImagePath: string, scale: string, saveImageAs: ImageFormat, - isAlpha: boolean + isAlpha: boolean, ) => { - if (!isAlpha && scale === "4" && compression === 0) { + if (!isAlpha && !customWidth && scale === "4" && compression === 0) { logit("Skipping compression for 4x scale and 0% compression"); return; } @@ -28,7 +28,7 @@ const convertAndScale = async ( logit("🖼️ Checking if original image exists: ", originalImagePath); if (err) { throw new Error( - "Could not grab the original image from the path provided! - " + err + "Could not grab the original image from the path provided! - " + err, ); } }); @@ -38,23 +38,6 @@ const convertAndScale = async ( } console.log("🚀 => originalImage:", originalImage); - // Resize the image to the scale - const newImage = sharp(upscaledImagePath, { - limitInputPixels: false, - }) - .resize( - originalImage.width && originalImage.width * parseInt(scale), - originalImage.height && originalImage.height * parseInt(scale), - { - fit: "outside", - } - ) - .withMetadata({ - density: originalImage.density, - orientation: originalImage.orientation, - }); - - console.log("🚀 => newImage:", newImage); // Convert compression percentage (0-100) to compressionLevel (0-9) const compressionLevel = Math.round((compression / 100) * 9); @@ -63,13 +46,33 @@ const convertAndScale = async ( JSON.stringify({ originalWidth: originalImage.width, originalHeight: originalImage.height, + customWidth, scale, saveImageAs, compressionPercentage: compression, compressionLevel, - }) + }), ); + // Resize the image to the scale + const newImage = sharp(upscaledImagePath, { + limitInputPixels: false, + }) + .resize( + customWidth + ? parseInt(customWidth) + : originalImage.width && originalImage.width * parseInt(scale), + customWidth + ? null + : originalImage.height && originalImage.height * parseInt(scale), + ) + .withMetadata({ + density: originalImage.density, + orientation: originalImage.orientation, + }); + + console.log("🚀 => newImage:", newImage); + const buffer = await newImage .withMetadata({ density: originalImage.density, diff --git a/renderer/atoms/userSettingsAtom.ts b/renderer/atoms/userSettingsAtom.ts index b123be7..3e2b73d 100644 --- a/renderer/atoms/userSettingsAtom.ts +++ b/renderer/atoms/userSettingsAtom.ts @@ -3,7 +3,7 @@ import { atomWithStorage } from "jotai/utils"; export const customModelsPathAtom = atomWithStorage( "customModelsPath", - null + null, ); export const scaleAtom = atomWithStorage<"2" | "3" | "4">("scale", "4"); export const batchModeAtom = atom(false); @@ -12,17 +12,17 @@ export const progressAtom = atom(""); export const rememberOutputFolderAtom = atomWithStorage( "rememberOutputFolder", - false + false, ); export const dontShowCloudModalAtom = atomWithStorage( "dontShowCloudModal", - false + false, ); export const noImageProcessingAtom = atomWithStorage( "noImageProcessing", - false + false, ); export const compressionAtom = atomWithStorage("compression", 0); @@ -31,12 +31,22 @@ export const overwriteAtom = atomWithStorage("overwrite", false); export const turnOffNotificationsAtom = atomWithStorage( "turnOffNotifications", - false + false, ); export const viewTypeAtom = atomWithStorage<"slider" | "lens">( "viewType", - "lens" + "lens", ); export const lensSizeAtom = atomWithStorage("lensSize", 100); + +export const customWidthAtom = atomWithStorage( + "customWidth", + null, +); + +export const useCustomWidthAtom = atomWithStorage( + "useCustomWidth", + false, +); diff --git a/renderer/components/settings-tab/CustomResolutionInput.tsx b/renderer/components/settings-tab/CustomResolutionInput.tsx new file mode 100644 index 0000000..1e123db --- /dev/null +++ b/renderer/components/settings-tab/CustomResolutionInput.tsx @@ -0,0 +1,51 @@ +import { customWidthAtom, useCustomWidthAtom } from "@/atoms/userSettingsAtom"; +import { useAtom } from "jotai"; +import React, { useState } from "react"; + +export function CustomResolutionInput() { + const [useCustomWidth, setUseCustomWidth] = useAtom(useCustomWidthAtom); + const [customWidth, setCustomWidth] = useAtom(customWidthAtom); + + return ( +
+
+

CUSTOM OUTPUT WIDTH

+

+ REQUIRES RESTART +
+ Use a custom width for the output images. The height will be adjusted + automatically. Enabling this will override the scale setting. +

+
+
+ { + if (!e.currentTarget.checked) { + localStorage.removeItem("customWidth"); + } + setUseCustomWidth(!useCustomWidth); + }} + /> + { + if (e.currentTarget.value === "") { + setUseCustomWidth(false); + setCustomWidth(null); + localStorage.removeItem("customWidth"); + return; + } + setCustomWidth(parseInt(e.currentTarget.value)); + }} + step="1" + className="input input-primary mt-2 w-full" + /> +
+
+ ); +} diff --git a/renderer/components/settings-tab/ImageScaleSelect.tsx b/renderer/components/settings-tab/ImageScaleSelect.tsx index 332d89f..9c73944 100644 --- a/renderer/components/settings-tab/ImageScaleSelect.tsx +++ b/renderer/components/settings-tab/ImageScaleSelect.tsx @@ -1,3 +1,5 @@ +import { useCustomWidthAtom } from "@/atoms/userSettingsAtom"; +import { useAtom } from "jotai"; import React from "react"; type ImageScaleSelectProps = { @@ -6,16 +8,21 @@ type ImageScaleSelectProps = { }; export function ImageScaleSelect({ scale, setScale }: ImageScaleSelectProps) { + const [useCustomWidth, setUseCustomWidth] = useAtom(useCustomWidthAtom); + return ( -
+
-

IMAGE SCALE

+

+ IMAGE SCALE {useCustomWidth && "DISABLED"} +

{/*

EXPERIMENTAL

*/}
model.value === currentlySavedModel.value + (model) => model.value === currentlySavedModel.value, ) === undefined ) { localStorage.setItem("model", JSON.stringify(modelOptions[0])); @@ -113,7 +114,7 @@ function SettingsTab({ setModel(currentlySavedModel.value); logit( "⚙️ Getting model from localStorage: ", - JSON.stringify(currentlySavedModel) + JSON.stringify(currentlySavedModel), ); } @@ -131,14 +132,14 @@ function SettingsTab({ localStorage.setItem("rememberOutputFolder", "false"); } else { const currentlySavedRememberOutputFolder = localStorage.getItem( - "rememberOutputFolder" + "rememberOutputFolder", ); logit( "⚙️ Getting rememberOutputFolder from localStorage: ", - currentlySavedRememberOutputFolder + currentlySavedRememberOutputFolder, ); setRememberOutputFolder( - currentlySavedRememberOutputFolder === "true" ? true : false + currentlySavedRememberOutputFolder === "true" ? true : false, ); } }, []); @@ -167,24 +168,26 @@ function SettingsTab({ }; const upscaylVersion = navigator?.userAgent?.match( - /Upscayl\/([\d\.]+\d+)/ + /Upscayl\/([\d\.]+\d+)/, )[1]; return ( -
+

Having issues?

+ target="_blank" + > HELP ME! {featureFlags.APP_STORE_BUILD && ( + target="_blank" + > EMAIL DEVELOPER )} @@ -217,6 +220,8 @@ function SettingsTab({ {/* IMAGE SCALE */} + + diff --git a/renderer/components/upscayl-tab/config/LeftPaneImageSteps.tsx b/renderer/components/upscayl-tab/config/LeftPaneImageSteps.tsx index d05aaea..6a0e8f2 100644 --- a/renderer/components/upscayl-tab/config/LeftPaneImageSteps.tsx +++ b/renderer/components/upscayl-tab/config/LeftPaneImageSteps.tsx @@ -289,42 +289,14 @@ function LeftPaneImageSteps({

Step 4

{dimensions.width && dimensions.height && (

- Upscayl from
+ Upscayl from{" "} {dimensions.width}x{dimensions.height} {" "} - to -

- { - if (parseInt(e.target.value) > 32768) { - setTargetWidth(32768); - } else if (e.target.value === "") { - setTargetWidth(1); - } - setTargetWidth(parseInt(e.target.value)); - }} - />{" "} - x{" "} - { - if (parseInt(e.target.value) > 32768) { - setTargetHeight(32768); - } else if (e.target.value === "") { - setTargetHeight(1); - } else setTargetHeight(parseInt(e.target.value)); - }} - /> -
- {/* + to{" "} + {getUpscaleResolution().width}x{getUpscaleResolution().height} - */} +

)}