Practice Rust and TAURI: Make an Image Viewer #4

mar-m. nakamura
7 min readMar 6, 2022

--

I will make an image viewer and studying. 😊
It is a continuation from #1 , #2 and #3.

Last time, I got a directory entries from PC.

This time, I will try to make a process to select the entry list with the up and down keys, and display the image file.

As usual, I’ve pasted the full modified source code at the end of the article.
It’s a cramped UI like a peephole because it doesn’t implement a list view yet.

An example of selecting an image file.
This time’s goal is like this.

Display image

Create a custom protocol to handle image requests.

Edit: tauri_imgv/src-tauri/src/main.rs

Add the following declaration.

use tauri::http::ResponseBuilder;
use tauri::Manager;

Concatenate .register_uri_scheme_protocol to tauri::Builder::default() in main() as shown below.

fn main() {
tauri::Builder::default()
:
.invoke_handler(tauri::generate_handler![
:
])
.register_uri_scheme_protocol("reqimg", move |app, request| {
let res_not_img = ResponseBuilder::new()
.status(404)
.body(Vec::new());
if request.method() != "GET" { return res_not_img; }
let uri = request.uri();
let start_pos = match uri.find("?n=") {
Some(_pos) => _pos + 3,
None => return res_not_img,
};
let end_pos = match uri.find("&") {
Some(_pos) => _pos,
None => return res_not_img,
};
let entry_num: usize = match &uri[start_pos..end_pos].parse() {
Ok(_i) => *_i,
Err(_) => return res_not_img,
};
let dir_entries: State<DirEntries> = app.state();
let v_dirs = &*dir_entries.0.lock().unwrap();
let target_file = match v_dirs.get(entry_num) {
Some(_dir) => &v_dirs[entry_num],
None => return res_not_img,
};
let extension = match target_file.extension() {
Some(_ex) => _ex.to_string_lossy().to_string(),
None => return res_not_img,
};
if !is_img_extension(&extension) {
return res_not_img;
}
println!("🚩Request: {} / {:?}", entry_num, target_file);
let local_img = if let Ok(data) = read(target_file) {
tauri::http::ResponseBuilder::new()
.mimetype(format!("image/{}", &extension).as_str())
.body(data)
} else {
res_not_img
};
local_img
})
:
}

Finally, add a function to determine the extension of the image.

fn is_img_extension(extension: &str) -> bool {
let ex: [&str; 6] = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
ex.iter().any(|e| *e == extension.to_lowercase())
}

.register_uri_scheme_protocol("reqimg", … is registers its own uri scheme protocol, “reqimg”. This allows you to request an image from html or javascript with the URI following “https://reqimg./" .

📌 There are articles I’ve looked into in the past about custom protocols.

This time I took a number from “n” in the uri parameter and referenced it as the key number in the directory entry array.

The advantage of passing by key number is that validation is easy.
The disadvantage is to keep the entry array in both Rust and js.

The result of the reference is returned by ResponseBuilder.
The extension is identified by is_img_extension() and returns image data or 404.

Next, edit javascript.

Edit: tauri_imgv/dist/main.js

Add an image event listener to the beginning.

const Img = new Image();
Img.addEventListener("error", () => {
// Error : Suppress broken link icon.
document.getElementById('img_preview').src = "";
}, false);
Img.addEventListener("load", () => {
document.getElementById('img_preview').src = Img.src;
}, false);

Next, edit three places in the class UserOperation {}.
Add variable #activeEntryNum .

  class UserOperation {
#reqRust;
#drives = [];
#activeDriveNum = 0; // Key of #drives
#dirEntries = [];
+ #activeEntryNum = 0; // Key of #dirEntries
constructor() {
:

Add new method selectEntry() .

  /**
* Select entry item
* @param {number} inc Increment (or decrement) entry
*/
selectEntry(inc) {
let maxNum = this.#dirEntries.length - 1;
let n = (inc == 0) ? 0 : this.#activeEntryNum + parseInt(inc);
n = (n < 0) ? 0 : n;
n = (n > maxNum) ? maxNum : n;
if (n != this.#activeEntryNum || n == 0) {
// TODO: Make it a list view later
let isDir = (n < this.#reqRust.subDirCount);
let color = isDir ? "#FFAA00" : "#FFFFFF";
let filename = this.#dirEntries[n];
let item = n + " : " + filename;
document.querySelector('list').innerHTML =
'<font color="' + color + '">' + item + "</font>";
// Images are requested by number n,
// cc is just for cache control.
Img.src = 'https://reqimg./?n=' + n + "&cc=" + filename;
}
this.#activeEntryNum = n;
}

Modify the two lines of the method selectDrive() .

    /**
* Select drive
* @param {number} inc increment (or decrement) current drive number
*/
async selectDrive(inc) {
let n = this.#activeDriveNum += parseInt(inc);
let maxNum = this.#drives.length - 1;
n = (n < 0) ? maxNum : n;
n = (n > maxNum) ? 0 : n;
this.#activeDriveNum = await this.#reqRust.changeDrive(n);
console.log("Drv: " + this.#activeDriveNum);
- // Check the switching of the entry list
- document.querySelector('list').innerHTML = await this.#reqRust.scanDir();
+ this.#dirEntries = await this.#reqRust.scanDir();
+ this.selectEntry(0);
}

That’s all for modifying the class UserOperation {}.

Finally, add the up / down key judgment to the function main().

  const main = (payload) => {
:
switch (e.key) {
:

+ case 'ArrowUp':
+ op.selectEntry(-1);
+ break;
+ case 'ArrowDown':
+ op.selectEntry(1);
+ break;
:

I can now select the directory entry with the up and down keys.
It displays the directory name and file name by selectEntry(), but it is a cramped UI like a peephole because it does not implement the list view yet.

The color is changed with the value (subDirCount) prepared last time as the boundary so that the directory and the file can be easily distinguished.

Each time you select a directory entry, a request like “https://reqimg./?n=1&file_name" is made. And it is processed by “reqimg” of uri scheme protocol registered on Rust side.

The request is just a number with ?n=1, and &file_name is given for cache control.

At the moment, we have not implemented the process of switching to a subdirectory, so place an appropriate image file in the root of some drive in advance and test it.

Let’s run it.

An example of selecting an image file.
An example of selecting an image file. (The backslash is a yen sign because it is a Japanese font.)

Use the left and right keys to switch drives and the up and down keys to select an entry.
If you select an image file, the image will be displayed in the upper right area.

Switch directories

This time, let’s extend it a little more and implement directory switching.

Press Enter to switch to the directory in focus.
Also, pressing the backspace key will always return to the parent directory.

Edit: tauri_imgv/src-tauri/src/main.rs

Add a constant for the error.

const CHANGE_DIR_ERROR: i8 = -1;
const CHANGE_DIR_WARNING: i8 = -2;

Add the new command change_dir.

#[tauri::command]
fn change_dir(
chg_num: usize,
dir_entries: State<DirEntries>,
active_path: State<ActivePath>,
) -> Result<(), i8> {
let v_dirs = &*dir_entries.0.lock().unwrap();
let target_dir = if let Some(_dir) = v_dirs.get(chg_num) {
&v_dirs[chg_num]
} else {
// The number is outside the range of the v_dirs
return Err(CHANGE_DIR_ERROR);
};
if !target_dir.is_dir() {
// Not a directory
return Err(CHANGE_DIR_WARNING);
}
*active_path.0.lock().unwrap() = target_dir.to_path_buf();
Ok(())
}

Then add it to main().

fn main() {
tauri::Builder::default()
:

.invoke_handler(tauri::generate_handler![
change_drive,
scan_dir,
+ change_dir,
count_sub_dir,
])
:
}

The command change_dir receives the entry number and updates active_path. If there is a problem, an error value is returned.

Next, Edit: tauri_imgv/dist/main.js

Add a constant for the error.

const InvokeErr = -1;

Add the changeDir() function to the class RequestToRust.

  class RequestToRust {
:
+ /**
+ * Request directory change
+ * @param {number} entryNum Request directory number
+ * @returns {number} Changed directory number or InvokeErr
+ */
+ async changeDir(entryNum) {
+ const newEntryNum =
+ await this.#invoke('change_dir', {
+ chgNum: entryNum
+ }).then(() => {
+ return entryNum;
+ }).catch((e) => {
+ console.log("ChangeDir Error: " + e);
+ return InvokeErr;
+ });
+ return newEntryNum;
+ }
:
}

Add the enterDir() and goBackParentDir() functions to the class UserOperation.

  class UserOperation {
:
+ /**
+ * Enter the focused directory
+ * @returns void
+ */
+ async enterDir() {
+ let res = await this.#reqRust.changeDir(this.#activeEntryNum);
+ if (res == InvokeErr) {
+ return;
+ }
+ this.#activeEntryNum = 0;
+ this.#dirEntries = await this.#reqRust.scanDir();
+ this.selectEntry(0);
+ }
+ /**
+ * Return to the parent directory in one shot
+ */
+ goBackParentDir() {
+ this.selectEntry(0);
+ this.#activeEntryNum = 0;
+ this.enterDir();
+ }
:
}

Finally, add the enter key and backspace key verdicts to the main function.
e.preventDefault(); was also added. After pressing the key, disable normal operation.

  const main = (payload) => {
:
switch (e.key) {
:
+ case 'Enter':
+ op.enterDir();
+ break;
+ case 'Backspace':
+ op.goBackParentDir();
+ break;
default:
- break; // (!)Remove
+ return;
}
+ e.preventDefault();
});
}

Now changeDir() will issue the Rust command change_dir and if all goes well, the directory entry list will be updated.

Let’s run it.

Press the Enter key while selecting a directory to switch to that directory.
Select the parent directory name on the top line and press enter to return to the parent directory. Or, press the backspace key to always return to the parent directory.

Let’s test it in a directory with lots of image files.
If you look at the terminal, you can see that the cache is working.

I was able to browse the subdirectories. I am also checking the cache.

I managed to reach my goal this time as well.😊
I’m self-taught, so I’m not sure if this approach is optimal, but this time I implemented it this way.😙

Next time, I will challenge the GUI “list view”.

This time it was a little long. Thank you for reading to the end.🥰

The following is the full source code of this time. (main.js, main.rs)

--

--

mar-m. nakamura

CatShanty2 Author. I want to develop a modern “3” ! But … My brain is still 8-bit / 32KB.😅