Building a Snipping Tool with Electron, React and Node.js

Modern web browsers allow us to access connected media devices like microphones, cameras, and screens through MediaDevices interface. Since Electron uses chromium and Nodejs, MediaDevices devices API is supported out of the box. Electron’s desktopCapturer module provides access to media sources that can be used to capture audio and video from the renderer process.

In this tutorial, I’ll show how to build a snipping tool using  desktopCapturer module. We’ll use React for building our application’s user interface and Nodejs as a backend server for uploading snipped images.

Structure

A few months ago, I wrote a Hacker News app with React and Electron. It was my first Electron app. I’m going to follow the same directory structure. Here’s what it’s gonna look like.

Getting Started

Let’s start off by creating app.js and index.html file inside app/src/main directory. app.js will run the main process and display index.html inside BrowserWindow. There’s a lot of boilerplate code in this file. I’m only focusing on the part where we create our main BrowserWindow. You can view the complete code in the linked repository.

There’s no rocket science here. We’re creating a new browser window, removing its menu and frame and loading our index.html. I would like to point one important line of code here. I’m concatenating  + '?main' at the end of our formatted URL. I was looking for a way to pass additional information to BrowserWindow and then retrieve it from renderer process and this seems to be the only way to I found. I’m sure there must have been a better way to do it. if you know one, please share in comments. We’ll render two main components in our BrowserWindow, a Snipper and a Cropper. I didn’t want to create different HTML files for rendering different components. By passing additional info, we can choose which component to render.

Add this code to index.html  file.

We’ve configured Webpack to start a dev server and enable hot reloading for development builds. We’ll conditionally render script and styles tags DOM based on the environment. You can view Webpack’s dev and prod config files in the linked repository.

Render

Let’s create root App.js component inside src/main/components directory. It doesn’t do much. It renders a child  Snipper component and mounts.

Snipping Component

Our Snipper component will look like this.

We’re rendering a logo, title and button control for capturing a full screenshot and creating an image cropper. Add this code to your Snipper.js component file.

Remember we concatenated a string when loading BrowserWindow. We can access it now and set view state with it. Add this method to the component.

We’re dividing Snipper components into two parts. Whenever we’ll load our app from the main process with ?main string concatenated to the URL, we’ll see a Fullscreen and Crop Image buttons.

Cropper Component

Our Cropper components will look like this.

We’re using react-rnd module. It lets you create a component with drag and resize support and keep track of coordinates and offsets with its onResize and OnDragStop prop callbacks.

So far, we’ve only defined how our Snipper and Cropper components will look how. Now, let’s implement their functionality.

Displaying Cropper

We’ve registered initCropper onClick handler method on Crop Image button. Let’s add it to our component.

On click, we’re creating a new BrowserWindow with same file URL but a different info string. This time we’re concatenating ?snip to the loadURL file path. In our click handler, we’ll keep a reference to main  BrowserWindow and hide it instead of destroying it. We’ll create a new transparent window with full height and width of client screen. To get current instance of BrowserWindow, add this method your component.

When our new BrowserWindow loads, it will render Cropper components instead of button controls. We’re also registering event listening with ipcRenderer.once. ipcRenderer is an instance of EventEmitter class and lets us communicate between different windows. We’re registering two events, snip and canceled. We’ll call them from cropper window and listen to them in the main window.

We’re passing two callback prop methods to Cropper component. snip method will be called when a user clicks capture button after cropping a screen region and destroySnipView will be called when a user cancels a crop. Add these methods to Cropper component.

On snip and cancel, we’ll call snip and canceled events that we registered in our main window before creating the second one. On snip, we’ll pass the current state of Cropper component. It has x and y coordinates and height and width of the cropped region. After calling events and passing the data, we’ll destroy cropper window.

Capturing Screen

We’re listening to snip event in our main window.

Once we receive data, we’ll call captureScreen method. Let’s define it.

Inside this method, we’ll retrieve our main window and hide it before capturing the screenshot so that it won’t get in the way. We’re also using jimp module to crop our images. After that, we’re calling getScreenShot method and passing it a callback. It will return us a base64data string after capturing a screenshot. Add this method to your component.

At the end of getScreenShot method, we’re filtering available screens by iterating over media sources returned by desktopCapturer.getSources. After selecting the main screen, we’re calling navigator.webkitGetUserMedia and passing it two methods to handle captured stream and handle errors. In handleStream method, we’re creating a hidden video tag and drawing it on a canvas, then passing base64 data string to callback method after converting canvas to data URL by calling canvas.toDataURL method.

We have a coordinates param on captureScreen method. When an snip event from cropper window calls captureScreen method, coordinates will be passed to this method as the state. When a user will click Fullscreen button, it will call  captureScreen with null coordinates. On null, we’ll capture full main screen. After capturing the image, we’ll update image and set save_controls to true to display Save disk, Upload URL and Discard buttons.

Here’s what it looks like when your click Full screen or select a crop. Now we need to add event handlers to save an image to disk, upload and discard. Add these methods to your component.

On discard, we’ll simply remove it from the state, disable save_controls and resize the window. On save to disk, we’ll write the base64 data string to file using fs.writeFile method and then open the file in default file explorer using shell.showItemInFolder method. On Upload, we’ll use axios post method to pass base64 data string to our Nodejs backend server.  Let’s create a simple express server to handle a post request, saving file and returning image URL.

Upload Server

Under server directory, create a server.js  file and add this code.

We’ll serve uploaded files through /upload route as static content. Inside post callback, we’re creating uploads directory if it doesn’t exist. We’re creating a file with base64 data string and returning the filename. After receiving success response from the web server, we’ll concatenate filename with our static content route and open it in default web browser will shell.openExternal method.

Snipper Demo

Here’s a functioning demo of our snipping tool.

I’ve created a GitHub repository. I’ve tested the code on Windows 10 machine. if you get any errors, please open an issue in repository or mention in comments. Setup instructions are available in repository readme file.

 

Share this post

12 thoughts on “Building a Snipping Tool with Electron, React and Node.js”

  1. I failed to try build this on windows 10 and ubuntu1804,because of ‘fsevents@1.2.3’ needs os darwin.

          1. Thank you, this version worked. I run success, but failed to run dist. My network is terrible

  2. Cool tutorial. First time using electron. Really like all the things you can do with this application. One thing I am trying to figure out is hiding the cursor when the screen is being captured. I started by playing around with hover and focus on the buttons but windows change and there is a small timeout before screen capture so its tricky. So when screen shooting MS VSCode the cursor kinda falls through to that program. I have enough time to drag the cursor to my other monitor before the pic fires and of course this isnt a big deal at all…but was wondering if you had any ideas. Again awesome writeup Im learning a lot studying this code.

    1. Hi Benjamin. Sorry for the late reply. Currently, Electron’s screen capturer API doesn’t support cursor hiding when capturing screen. However, you can hide cursor at the corners of the screen. You can use Robot.js to programmatically move your cursor before taking a screenshot instead of doing it manually.

  3. hi! just wanna ask if you already updated to higher version of electron. I really like your project.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.