Welcome folks today in this blog post we will be building a image resizer desktop app in node.js using electron framework & toastify library. All the full source code of the application will be shown below.
Get Started
In order to get started you need to install the below dependencies and packages which are required for this desktop App
- Electron: It is an open source framework used in node.js for developing desktop apps with the use of HTML5 CSS3 and Javascript
2) Toastify: This is a alert library where we can show colorful toast alert messages to the user
Installation
Now to install these modules we need to first of all start a new electron.js project as shown below in the commands
npm init -y
First of all we have initialized the package.json file for our node.js project. Now it’s time to install the dependencies as shown below
npm i electron --save-dev
This will install the electron framework for developing desktop apps. And -dev flag is assigned because it’s a dev dependency because it’s only need in development not in production.
npm i resize-img
This is the module which will actually resize the images inside node.js and electron.
npm i toastify-js
This is the library for showing colorful alert toast messages to the user whether success and error.
Starting the Electron App With Hot Reload
Now guys we will start the electron.js app with hot reload functionality so that whenever we make any kind of changes we don’t need to restart the server. You need to execute the below command as shown below
npx electronmon .
Directory Structure of Electronjs Project
Now we will be showing the directory structure of electronjs project as shown below
As you can see in the above pic we have two sections in electron.js app we have the renderer & main.js (Server-side) Code we need to communicate between them so that we can safely build out the app. For this we use inter process communication which is commonly called as IPC. We have bi-directional communication implemented in electron.js using events.
Creating a Basic Window in Electron
Now inside the main.js file we will be creating a basic window first of all of fixed width and height which will be displayed on the screen.
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const path = require('path'); const os = require('os'); const fs = require('fs'); const resizeImg = require('resize-img'); const { app, BrowserWindow, Menu, ipcMain, shell } = require('electron'); process.env.NODE_ENV = "production" const isDev = process.env.NODE_ENV !== 'production'; const isMac = process.platform === 'darwin'; let mainWindow; app.on('ready', () => { createMainWindow(); // Remove variable from memory mainWindow.on('closed', () => (mainWindow = null)); }); |
As you can see in the above code we are importing the different methods from electron library. And then we have a simple electron app which have an event listener of ready. This means whenever the app loads and is ready we will calling this simple function of createMainWindow() which will actually renders out a window on the screen.
Now we need to define this createWindow() function where we will define how to create a new window in electron.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function createMainWindow() { mainWindow = new BrowserWindow({ width: isDev ? 1000 : 500, height: 600, resizable: isDev, webPreferences: { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), }, }); // Show devtools automatically if in development if (isDev) { mainWindow.webContents.openDevTools(); } // mainWindow.loadURL(`file://${__dirname}/renderer/index.html`); mainWindow.loadFile(path.join(__dirname, './renderer/index.html')); } |
As you can see in the above code we are initializing a new window having fixed width and height and also passing some options to it. And also we are including a special file called preload.js which contains the utility based methods. And also after that we are loading a index.html
Now you need to copy paste some code inside the index.html
file inside the renderer
folder
renderer/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<!DOCTYPE html> <html lang="en"> <head> <script src="js/renderer.js" defer></script> <title>ImageResizer</title> </head> <body> <label> <span>Select an image to resize</span> <input id="img" type="file" /> </label> </div> <!-- Form --> <form id="img-form" class="hidden"> <div> <label>Width</label> <input type="number" name="width" id="width" placeholder="Width" /> </div> <div> <label>Height</label> <input type="number" name="height" id="height" placeholder="Height" /> </div> <!-- Button --> <div> <button type="submit"> Resize </button> </form> <p><strong>File: </strong><span id="filename"></span></p> <p><strong>Output: </strong><span id="output-path"></span></p> </div> </body> </html> |
As you can see we have a simple form where we have input fields for choosing the input file and also input number text fields where the width and height will be auto populated after selecting it. And then we have the simple button to submit the form.
As you can see the interface which is shown of the application uptil now. As you can see we are also including the renderer.js file which is the actual javascript code required for this purpose at the frontend.
Adding the Menu inside Electron
Now we will look on how to add Menu items inside electron.js. For this you need to add the below code to your main.js
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// Menu template const menu = [ ...(isMac ? [ { label: app.name, submenu: [ { label: 'About', click: createAboutWindow, }, ], }, ] : []), { role: 'fileMenu', }, ...(!isMac ? [ { label: 'Help', submenu: [ { label: 'About', click: createAboutWindow, }, ], }, ] : []), ...(isDev ? [ { label: 'Developer', submenu: [ { role: 'reload' }, { role: 'forcereload' }, { type: 'separator' }, { role: 'toggledevtools' }, ], }, ] : []), ]; |
As you can see we have the menu template in the form of array inside this menu we have multiple menu items. Depending upon which OS is there we are showing different menu items. As Electron.js is cross platform it can build for different OS at same time.
Now we will be adding the menu defined in the earlier step to the electron app. For this you need to go to the app event of ready where you copy paste this line
1 2 3 4 5 6 7 8 9 |
app.on('ready', () => { createMainWindow(); const mainMenu = Menu.buildFromTemplate(menu); Menu.setApplicationMenu(mainMenu); // Remove variable from memory mainWindow.on('closed', () => (mainWindow = null)); }); |
As you can see we using building the menu using the template defined in the earlier step. And lastly we are using setApplicationMenu() to set the application menu
1 2 3 4 5 6 7 8 9 |
// Quit when all windows are closed. app.on('window-all-closed', () => { if (!isMac) app.quit(); }); // Open a window if none are open (macOS) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); }); |
So guys in the above line of code we are quitting the app when all the windows are closed. That is the first event and in the second event we are activating the window when we open the windows
Creating the About Window of App
Now we will see how to create Multiple windows in electron. In that case when the user click the about menu item it should open a new about window as shown below
1 2 3 4 5 6 7 8 9 10 11 |
let aboutWindow function createAboutWindow() { aboutWindow = new BrowserWindow({ width: 300, height: 300, title: 'About Electron', }); aboutWindow.loadFile(path.join(__dirname, './renderer/about.html')); } |
As you can see we are using the BrowserWindow again to create another window and here again we are passing the width and height of the window and the title of the window. And here also we are fetching the contents to be shown inside the window. For this we have created another about.html inside the same directory where index.html is stored
So now you also need to copy paste the code inside about.html
renderer/about.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="en"> <head> <title>About ImageShrink</title> </head> <body> <div> <h2>FileResizer App</h2> <p>Version 1.0.0</p> <p>MIT License</p> </div> </body> </html> |
So Now if you click about menu item you will see the below about window
Now you need to make a preload.js
file which will contain all the methods that we will expose to the renderer side because we need these methods at the client to use the OS and other modules.
preload.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const os = require('os'); const path = require('path'); const { contextBridge, ipcRenderer } = require('electron'); const Toastify = require('toastify-js'); contextBridge.exposeInMainWorld('os', { homedir: () => os.homedir(), }); contextBridge.exposeInMainWorld('path', { join: (...args) => path.join(...args), }); contextBridge.exposeInMainWorld('ipcRenderer', { send: (channel, data) => ipcRenderer.send(channel, data), on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)), }); contextBridge.exposeInMainWorld('Toastify', { toast: (options) => Toastify(options).showToast(), }); |
Here as you can see at the top we are importing contextBridge and ipcRenderer which allows us to expose methods and also communicate data between the renderer and main process. That’s the point of having this file. In this file we have different methods first of all for showing toast alert messages we are receiving alert options in the function. We are also exposing ipcRenderer process. Also we are exposing the os and path modules inside the client side.
Now we need to write the javascript code required for this application at the renderer side. So inside the renderer folder make a js folder and inside it make a renderer.js file and copy paste the below code
renderer/js/renderer.js
1 2 3 4 5 6 |
const form = document.querySelector('#img-form'); const img = document.querySelector('#img'); const outputPath = document.querySelector('#output-path'); const filename = document.querySelector('#filename'); const heightInput = document.querySelector('#height'); const widthInput = document.querySelector('#width'); |
As you can see we are first of all getting all the reference of all the DOM elements inside the html. And then we will accessing these elements in javascript
Adding Event Listeners to Form & Input Field
So now we will be attaching the form submit and onChange event handler to the form and the input fields as shown below
1 2 3 4 |
// File select listener img.addEventListener('change', loadImage); // Form submit listener form.addEventListener('submit', resizeImage); |
As you can see we have attached the event handlers. So now when the input field value is changed then this loadImage() function will be executed. So now we need to write the loadImage function as shown below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function loadImage(e) { const file = e.target.files[0]; // Check if file is an image if (!isFileImage(file)) { alertError('Please select an image'); return; } // Add current height and width to form using the URL API const image = new Image(); image.src = URL.createObjectURL(file); image.onload = function () { widthInput.value = this.width; heightInput.value = this.height; }; // Show form, image name and output path form.style.display = 'block'; filename.innerHTML = img.files[0].name; outputPath.innerText = path.join(os.homedir(), 'imageresizer'); } |
As you can see in the above code we are fetching the image which the user has selected. And then we have some validation inside the if condition to check if the selected file is image or not. As you can see if any error takes place we are calling alertError() method which will show toast alert message containing the error. And after that we are initializing a new Image by using the Image() constructor. And then we are using the URL.createObjectURL() method to convert image file to url. And we are getting the width and height of the original image and populating those values inside the input field of width and height. And we will also show the filename and the outputPath as well.
Now we will be making the method to show error inside toast alert message which is red in color as shown below
1 2 3 4 5 6 7 8 9 10 11 12 |
function alertError(message) { Toastify.toast({ text: message, duration: 5000, close: false, style: { background: 'red', color: 'white', textAlign: 'center', }, }); } |
1 2 3 4 5 |
// Make sure file is an image function isFileImage(file) { const acceptedImageTypes = ['image/gif', 'image/jpeg', 'image/png']; return file && acceptedImageTypes.includes(file['type']); } |
And now inside this above javascript function we are checking whether the given file is image or not. Here we have defined the array of accepted file types which are gif,jpeg and png. So we are checking if the file mimetype matches to them or not. And we are retuning the mimetype from this function.
And now guys we will writing the function when we click the submit button of the form. This function will get executed as shown below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Resize image function resizeImage(e) { e.preventDefault(); if (!img.files[0]) { alertError('Please upload an image'); return; } if (widthInput.value === '' || heightInput.value === '') { alertError('Please enter a width and height'); return; } // Electron adds a bunch of extra properties to the file object including the path const imgPath = img.files[0].path; const width = widthInput.value; const height = heightInput.value; ipcRenderer.send('image:resize', { imgPath, height, width, }); } |
As you can see inside this function we are getting the width and height of the image that the user has entered. If either of the fields are empty then we will be showing error to the user again we are calling alertError() method passing custom error. And then if all fields are good then we will be storing the image path, width and height and then we are calling the method defined inside the main process. Here we are using the ipcRenderer.send() method to send the event which is called image:resize and in the second argument we are passing the information in an object which is path of image, width and height.
Now we will be responding to this event which is passed from the renderer to main process. Now inside the main.js file we will define the resize:image event which is received in main process as shown below
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
ipcMain.on('image:resize', (e, options) => { // console.log(options); options.dest = path.join(os.homedir(), 'imageresizer'); resizeImage(options); }); // Resize and save image async function resizeImage({ imgPath, height, width, dest }) { try { // console.log(imgPath, height, width, dest); // Resize image const newPath = await resizeImg(fs.readFileSync(imgPath), { width: +width, height: +height, }); // Get filename const filename = path.basename(imgPath); // Create destination folder if it doesn't exist if (!fs.existsSync(dest)) { fs.mkdirSync(dest); } // Write the file to the destination folder fs.writeFileSync(path.join(dest, filename), newPath); // Send success to renderer mainWindow.webContents.send('image:done'); // Open the folder in the file explorer shell.openPath(dest); } catch (err) { console.log(err); } } |
Here as you can see in the above code we are using ipcMain.on() method to receive the sent event by renderer. In this we are calling resizeImage() function which is async function
Now in this async resizeImage() function guys we are resizing the image file using the resize-img module. First of all we are getting the img file path and then we are getting the width and height and then resizing the image to that width and height. And then we are writing the output file to output path. And also we are sending the event called image:done which is sent from main to renderer process. So now we need to handle this event inside renderer.js file And also after that as you can see we are using the shell method to open the outputPath or the outputImage automatically.
renderer/js/renderer.js
1 2 3 4 |
// When done, show message ipcRenderer.on('image:done', () => alertSuccess(`Image resized to ${heightInput.value} x ${widthInput.value}`) ); |
Now inside this above code we are receiving image:done event we are showing the alert success toast message which will be in green color. Here we are calling the alertSuccess() method. Now we need to define this method as shown below
1 2 3 4 5 6 7 8 9 10 11 12 |
function alertSuccess(message) { Toastify.toast({ text: message, duration: 5000, close: false, style: { background: 'green', color: 'white', textAlign: 'center', }, }); } |
Here as you can see we are using the Toastify module to show toast message using the toast() method. Inside this we have the style object we have the background color which is green and text color is white and text align is center. Basically this alert message will appear for only 5 seconds then it will disappear after that automatically.
Full Source Code
Wrapping it up we will now see the full source code of the application as shown below
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
const path = require('path'); const os = require('os'); const fs = require('fs'); const resizeImg = require('resize-img'); const { app, BrowserWindow, Menu, ipcMain, shell } = require('electron'); process.env.NODE_ENV = "production" const isDev = process.env.NODE_ENV !== 'production'; const isMac = process.platform === 'darwin'; let mainWindow; let aboutWindow; // Main Window function createMainWindow() { mainWindow = new BrowserWindow({ width: isDev ? 1000 : 500, height: 600, resizable: isDev, webPreferences: { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), }, }); // Show devtools automatically if in development if (isDev) { mainWindow.webContents.openDevTools(); } // mainWindow.loadURL(`file://${__dirname}/renderer/index.html`); mainWindow.loadFile(path.join(__dirname, './renderer/index.html')); } // About Window function createAboutWindow() { aboutWindow = new BrowserWindow({ width: 300, height: 300, title: 'About Electron', }); aboutWindow.loadFile(path.join(__dirname, './renderer/about.html')); } // When the app is ready, create the window app.on('ready', () => { createMainWindow(); const mainMenu = Menu.buildFromTemplate(menu); Menu.setApplicationMenu(mainMenu); // Remove variable from memory mainWindow.on('closed', () => (mainWindow = null)); }); // Menu template const menu = [ ...(isMac ? [ { label: app.name, submenu: [ { label: 'About', click: createAboutWindow, }, ], }, ] : []), { role: 'fileMenu', }, ...(!isMac ? [ { label: 'Help', submenu: [ { label: 'About', click: createAboutWindow, }, ], }, ] : []), ...(isDev ? [ { label: 'Developer', submenu: [ { role: 'reload' }, { role: 'forcereload' }, { type: 'separator' }, { role: 'toggledevtools' }, ], }, ] : []), ]; // Respond to the resize image event ipcMain.on('image:resize', (e, options) => { // console.log(options); options.dest = path.join(os.homedir(), 'imageresizer'); resizeImage(options); }); // Resize and save image async function resizeImage({ imgPath, height, width, dest }) { try { // console.log(imgPath, height, width, dest); // Resize image const newPath = await resizeImg(fs.readFileSync(imgPath), { width: +width, height: +height, }); // Get filename const filename = path.basename(imgPath); // Create destination folder if it doesn't exist if (!fs.existsSync(dest)) { fs.mkdirSync(dest); } // Write the file to the destination folder fs.writeFileSync(path.join(dest, filename), newPath); // Send success to renderer mainWindow.webContents.send('image:done'); // Open the folder in the file explorer shell.openPath(dest); } catch (err) { console.log(err); } } // Quit when all windows are closed. app.on('window-all-closed', () => { if (!isMac) app.quit(); }); // Open a window if none are open (macOS) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); }); |
renderer/js/renderer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
const form = document.querySelector('#img-form'); const img = document.querySelector('#img'); const outputPath = document.querySelector('#output-path'); const filename = document.querySelector('#filename'); const heightInput = document.querySelector('#height'); const widthInput = document.querySelector('#width'); // Load image and show form function loadImage(e) { const file = e.target.files[0]; // Check if file is an image if (!isFileImage(file)) { alertError('Please select an image'); return; } // Add current height and width to form using the URL API const image = new Image(); image.src = URL.createObjectURL(file); image.onload = function () { widthInput.value = this.width; heightInput.value = this.height; }; // Show form, image name and output path form.style.display = 'block'; filename.innerHTML = img.files[0].name; outputPath.innerText = path.join(os.homedir(), 'imageresizer'); } // Make sure file is an image function isFileImage(file) { const acceptedImageTypes = ['image/gif', 'image/jpeg', 'image/png']; return file && acceptedImageTypes.includes(file['type']); } // Resize image function resizeImage(e) { e.preventDefault(); if (!img.files[0]) { alertError('Please upload an image'); return; } if (widthInput.value === '' || heightInput.value === '') { alertError('Please enter a width and height'); return; } // Electron adds a bunch of extra properties to the file object including the path const imgPath = img.files[0].path; const width = widthInput.value; const height = heightInput.value; ipcRenderer.send('image:resize', { imgPath, height, width, }); } // When done, show message ipcRenderer.on('image:done', () => alertSuccess(`Image resized to ${heightInput.value} x ${widthInput.value}`) ); function alertSuccess(message) { Toastify.toast({ text: message, duration: 5000, close: false, style: { background: 'green', color: 'white', textAlign: 'center', }, }); } function alertError(message) { Toastify.toast({ text: message, duration: 5000, close: false, style: { background: 'red', color: 'white', textAlign: 'center', }, }); } // File select listener img.addEventListener('change', loadImage); // Form submit listener form.addEventListener('submit', resizeImage); |
renderer/about.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="en"> <head> <title>About ImageShrink</title> </head> <body> <div> <h2>FileResizer App</h2> <p>Version 1.0.0</p> <p>MIT License</p> </div> </body> </html> |
renderer/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<!DOCTYPE html> <html lang="en"> <head> <script src="js/renderer.js" defer></script> <title>ImageResizer</title> </head> <body> <label> <span>Select an image to resize</span> <input id="img" type="file" /> </label> </div> <!-- Form --> <form id="img-form" class="hidden"> <div> <label>Width</label> <input type="number" name="width" id="width" placeholder="Width" /> </div> <div> <label>Height</label> <input type="number" name="height" id="height" placeholder="Height" /> </div> <!-- Button --> <div> <button type="submit"> Resize </button> </form> <p><strong>File: </strong><span id="filename"></span></p> <p><strong>Output: </strong><span id="output-path"></span></p> </div> </body> </html> |