Develop image preview desktop application using Electron, Vite, ViewerJS and React

Background

I am currently working on an open source project – pear-rec. Pear-rec is a cross-platform screenshot, screen recording, audio and video software. The screenshot function has been discussed in the previous article. If you haven’t seen it, you can read this article – I will teach you step by step how to use electron to implement screenshot software. Now that I have developed it, I want a picture preview function. Just do it. Come on, let me introduce my development process.

Before doing it, let’s take a look at the final display effect:

Tools

  • nodejs
  • pnpm
  • electron
  • vite
  • react
  • antd
  • viewerjs

Implementation

Principle logic

In the last article, we have already built a vite + electron project, so I will not talk about how to build the project here (students who don’t know how to do it can read “Teaching you step by step, using electron to implement screenshot software”), in fact It is not difficult to understand how to implement picture preview. First, select the picture to be previewed in the main form, and then another window responsible for previewing the picture will open, and then operate the picture through the web page and realize functions such as zooming in and out.

Main form

We first prepare the main page Home, which is very simple, just put a button and click the button to open the pop-up box to select the picture.

As shown in the picture:

Because we are using electron, I chose the dialog method of electron to implement the image selection component. Of course, you can also use the input tag + change event to implement it.

// ipcRenderer
invokeViGetImgs: () => ipcRenderer.invoke("vi:get-imgs", "Select an image"),
// ipcMain
ipcMain.handle("vi:get-img", async (event, title) => {<!-- -->
let res = await dialog.showOpenDialog({<!-- -->
title: title,
buttonLabel: "Click here to open the file",
properties: ["openFile"],
filters: [
{<!-- --> name: "picture", extensions: ["jpg", "jpeg", "png", "webp", "svg"] },
],
});
res.filePaths.map((filePath, index) => {<!-- -->
setHistoryImg(filePath);
});
const img = getHistoryImg();
return img;
});

In this way we get the address of an image.

Preview picture form

    1. routing

@/router.tsx

const ViewImage = lazy(() => import("@/pages/viewImage"));

<Route path="/viewImage" element={<!-- --><ViewImage />}></Route>
  • 2.Install the plug-in
pnpm i viewerjs -S
pnpm i antd -S
    1. page

@/pages/viewImage

import {<!-- --> useEffect, useState, useRef } from "react";
import {<!-- --> useSearchParams } from "react-router-dom";
import {<!-- --> Button, Upload } from "antd";
import Viewer from "viewerjs";
import {<!-- -->
FileImageOutlined,
ZoomInOutlined,
ZoomOutOutlined,
RotateLeftOutlined,
RotateRightOutlined,
SyncOutlined,
DownloadOutlined,
PrinterOutlined,
LeftOutlined,
RightOutlined,
SwapOutlined,
ExpandOutlined,
CompressOutlined,
InboxOutlined,
PushpinOutlined,
EditOutlined,
CloseOutlined,
} from "@ant-design/icons";
import type {<!-- --> UploadProps } from "antd/es/upload/interface";
import defaultImg from "/imgs/th.webp";
import "viewerjs/dist/viewer.css";
import styles from "./index.module.scss";

const {<!-- --> Dragger } = Upload;
const ViewImage = () => {<!-- -->
const [search, setSearch] = useSearchParams();
const [imgs, setImgs] = useState([]);
const [isFull, setIsFull] = useState(false);

useEffect(() => {<!-- -->
initImgs();
}, []);

useEffect(() => {<!-- -->
imgs.length & amp; & amp; initViewer();
}, [imgs]);

function initViewer() {<!-- -->
const imgList = document.getElementById("viewImgs") as any;
const viewer = new Viewer(imgList, {<!-- -->
// 0: Do not display
// 1: display
// 2: width>768px
// 3: width>992px
// 4: width>1200px
inline: true,
className: "viewImgs",
toolbar: {<!-- -->
alwaysOnTopWin: handleToggleAlwaysOnTopWin,
zoomIn: 1,
zoomOut: 1,
oneToOne: 1,
reset: 1,
prev: 1,
// play: {<!-- -->
// show: 4,
// size: "large",
// },
next: 1,
rotateLeft: 1,
rotateRight: 1,
flipHorizontal: 1,
flipVertical: 1,
download: () => {<!-- -->
handleDownload(viewer);
},
print: () => {<!-- -->
window.print();
},
},
}) as any;
}

const props: UploadProps = {<!-- -->
accept: "image/png,image/jpeg,.webp",
name: "file",
multiple: false,
showUploadList: false,
beforeUpload: (file) => {<!-- -->
const imgUrl = window.URL.createObjectURL(file);
setSearch({<!-- --> url: imgUrl });
setImgs([imgUrl]);
return false;
},
};

function handleDownload(viewer) {<!-- -->
if (window.electronAPI) {<!-- -->
window.electronAPI.sendViDownloadImg({<!-- -->
url: viewer.image.src,
name: viewer.image.alt,
});
} else {<!-- -->
const a = document.createElement("a");

a.href = viewer.image.src;
a.download = viewer.image.alt;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}

function handleFullScreen() {<!-- -->
const element = document.querySelector("#root");
if (element.requestFullscreen) {<!-- -->
element.requestFullscreen();
setIsFull(true);
}
}

function handleExitFullscreen() {<!-- -->
if (document.exitFullscreen) {<!-- -->
document.exitFullscreen();
setIsFull(false);
}
}

async function handleOpenImage() {<!-- -->
const imgs = await window.electronAPI?.invokeViGetImgs();
// setImgs(imgs);
}

async function handleToggleAlwaysOnTopWin() {<!-- -->
const isAlwaysOnTop = await window.electronAPI?.invokeViSetIsAlwaysOnTop();
const icon = document.querySelector(".viewer-always-on-top-win");
isAlwaysOnTop
? icon.classList.add("current")
: icon.classList.remove("current");
}

async function initImgs() {<!-- -->
const imgUrl = search.get("url");
if (imgUrl) {<!-- -->
setImgs([imgUrl]);
} else {<!-- -->
const img = (await window.electronAPI?.invokeViSetImg()) || defaultImg;
setImgs([img]);
}
}

return (
<div className={<!-- -->styles.viewImgs} id="viewImgs">
{<!-- -->imgs.length ? (
imgs.map((img, key) => {<!-- -->
return <img className="viewImg" src={<!-- -->img} key={<!-- -->key} />;
})
) : (
<Dragger {<!-- -->...props} className="viewImageUpload">
<p className="ant-upload-drag-icon">
<InboxOutlined rev={<!-- -->undefined} />
\t\t\t\t\t</p>
<p className="ant-upload-text">Click or drag the image</p>
<p className="ant-upload-hint">
Supports .jpg, .jpeg, .jfif, .pjpeg, .pjp, .png, .apng, .webp, .avif, .bmp, .gif, .webp
\t\t\t\t\t</p>
</Dragger>
)}
</div>
);
};

export default ViewImage;

    1. style
.viewImgs {<!-- -->
width: 100vw;
height: 100vh;
overflow: hidden;
:global {<!-- -->
.viewImg {<!-- -->
display: none;
}
}
}
    1. logical explanation
      The image preview function on this page is implemented through the viewerjs plug-in. This plug-in has rich image preview functions, so we don’t need to reinvent the wheel manually. You can check its website for specific functions and api. Website: https://fengyuanchen.github.io/viewerjs/

Summary

The article is basically finished here. Let’s briefly review the content of the article.

  • Q: Is there source code?

Of course, the address is as follows: github.com/027xiguapi/…. If you are interested, you can discuss it together. We also welcome everyone to fork and star