Using the Electron framework to weed out images

Ready, Steady, Go

An article on setting up the Electron Framework [3] was published in Linux Magazine a few months ago, so I mention the preparation only briefly and then head on directly to processing the photo folders.

The following commands install the Electron framework on Ubuntu:

sudo apt-get install npm nodejs-legacy

The additional nodejs-legacy package only installs some symlinks that many older node modules need during build and execution. In a fresh directory, then run

npm init
npm install electron --save-dev

to create a new project that not only installs the Electron framework locally, but also adds its dependencies to its dependency list, helping adopters to modify and rebuild the code to their heart's content. The npm init command prompts the user to enter some project parameters, such as the application name, its version, or the author name (Figure 2). The --save-dev option of the npm install statement appends the name of the installed package to the devDependencies list in package.json. For comparison, --save would list the package as a run-time dependency.

Figure 2: npm init creates the JavaScript file package.js as the basic Node.js configuration for the iNuke application.

If you also add the following to the scripts section inside package.json,

"start": "electron ."

the application can be launched later by using npm start. Electron then initially loads the main.js start script in Listing  1  [4] (specified in the configuration file under main) and passes it to the Node.js interpreter for execution.

Courage to be Different

As is well known, the asynchronous functional approach used by Node.js means a very different programming style compared with "normal" languages like Python or Perl. Instead of sequentially processing calls, Node.js code often adds a callback to a function call. The function later resumes execution by calling it at the end.

The GUI code builds a state machine, between whose states the code jumps back and forth, as controlled by events. At the same time, the event loop always needs to watch out for new events such as mouse clicks, to which it must respond promptly. This would not work if the code were just blocked for a while because it was reading a large file from disk.

The code in Listing 1 does not execute anything at first but waits until the node environment reports the ready event. If this occurs, it starts the callback from line 25 and first submits a web page to the renderer process for display with createWindow(). This happens from line 8 and with an object of the BrowserWindow class, whose loadURL() method is given the path to the index.html file in Listing 2.

Listing 1

main.js

01 const {app,globalShortcut,BrowserWindow} =
02   require('electron');
03 const path = require('path');
04 const url = require('url');
05
06 let win;
07
08 function createWindow(){
09   win = new BrowserWindow({
10     width:800, height:600});
11
12   win.loadURL(url.format({
13     pathname:
14       path.join(__dirname, 'index.html'),
15     protocol: 'file:', slashes: true
16   }));
17
18   win.webContents.openDevTools();
19
20   win.on('closed', () => {
21     win = null;
22   });
23 }
24
25 app.on('ready', () => {
26     createWindow();
27     globalShortcut.register('l', () => {
28       win.webContents.send('nextImage');
29     });
30     globalShortcut.register('h', () => {
31       win.webContents.send('prevImage');
32     });
33     globalShortcut.register('d', () => {
34       win.webContents.send('deleteImage');
35     });
36     win.webContents.send('prevImage');
37 });
38
39 app.on('will-quit', () => {
40   ['h','l','d'].forEach(function(key){
41     globalShortcut.unregister(key);
42   });
43 });
44
45 app.on('window-all-closed', () => {
46     app.quit();
47 });

Listing 2

index.html

01 <html>
02   <head> </head>
03
04   <body>
05   <h1>iNuke My Photos</h1>
06
07   <script>
08     require('./renderer.js');
09   </script>
10
11   <img id="image"></img>
12
13   </body>
14 </html>

At the same time, Listing 1 uses the global variable win to store a reference to the browser window. It can reset the variable during later callbacks, as being performed by the handler of the closed event, which gets triggered by the windowing system, and handles freeing up memory before shutting down the program.

During the debug phase of a new Electron application, it is extremely useful to open Chromium's debug window in the browser's main window using openDevTools() (line 18) and either read the warnings at the command line or analyze the HTML of the dynamically refreshed web page (Figure 3).

Figure 3: When the debug window is open, the developer can analyze the displayed HTML or track messages on the console.

Short and Sweet

Intercepting keyboard input is also a task of the main process in main.js. The register calls in lines 27, 30, and 33 ensure that the user can move to the next image with L and to the previous image with H (just as you move left or right in Vim) and delete the displayed image with D.

Among other things, these keystroke commands affect the displayed web page, which is why the main process main.js sends them as events to the renderer process in Listing 3 via IPC and win.webContents.send(). The renderer process starts out at the very beginning of the main process in Listing 1. It loads the index.html file (Listing 2) into the browser in lines 12 to 16, which in turn executes the renderer's JavaScript (Listing 3) in line 8 of Listing 2 via require(./renderer.js).

Listing 3

renderer.js

01 loadImage = require('blueimp-load-image');
02 fs = require( 'fs' );
03 ipc = require('electron').ipcRenderer;
04
05 images     = [];
06 images_idx = -1;
07
08 function displayImage(file) {
09   loaded = loadImage(file, function(img) {
10     scaled_img = loadImage.scale(
11       img, {maxWidth: 600});
12     scaled_img.id = "image";
13     node = window.document.getElementById(
14       'image');
15     node.replaceWith(scaled_img);
16   } );
17 }
18
19 function scroll(direction){
20   images_idx += direction;
21   if(images_idx > images.length-1){
22     images_idx = images.length-1;
23   }else if(images_idx<0) {
24     images_idx = 0;
25   }
26   displayImage( images[ images_idx ] );
27 }
28
29 function deleteImage() {
30   fs.unlink(images[ images_idx ]);
31   images.splice(images_idx, 1);
32   if(images.length == 0) {
33     console.log("That's it. Good-bye!");
34     require('electron').remote.app.quit();
35   }
36   scroll(-1);
37 }
38
39 dir = "images"; // change to process.cwd()
40 fs.readdir(dir, function(err, files) {
41   if( err ) {
42      console.error("readdir:", err);
43      require('electron').remote.app.quit();
44   }
45   files.forEach(function(file, index) {
46     images.push( dir + "/" + file );
47   });
48   scroll(0);
49 } );
50
51 ipc.on('nextImage', () => { scroll(1); });
52 ipc.on('prevImage', () => { scroll(-1); });
53 ipc.on('deleteImage', deleteImage);

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Electron

    GitHub's Electron project brings the benefits of web programming to the realm of desktop applications.

  • PhotoFilmStrip

    Easy-to-use PhotoFilmStrip produces high-quality videos and offers plenty of useful features.

  • Picasa 3.0

    In the age of the digital camera, users are overwhelmed by a flood of images. Picasa not only helps you manage photos but also will enhance the image quality with just a couple of mouse clicks.

  • Picasa 2.7

    In the age of the digital camera, users are overwhelmed by a flood of images. Picasa not only helps you manage photos but also will enhance the image quality with just a couple of mouse clicks.

  • Straight to the Point

    With the Fyne framework, Go offers an easy-to-use graphical interface for all popular platforms. As a sample application, Mike uses an algorithm to draw arrows onto images.

comments powered by Disqus

Direct Download

Read full article as PDF:

Price $2.95

News