Skip to content

Commit aebc3a1

Browse files
committed
Refactor; add file copy; add support for size CLI arg
1 parent c9db750 commit aebc3a1

File tree

7 files changed

+224
-27
lines changed

7 files changed

+224
-27
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ on:
1212
env:
1313
CARGO_TERM_COLOR: always
1414

15-
# We explicitly only allow the read permission for security reasons; no other permission is needed.
15+
# We explicitly allow only the read permission for security reasons; no other permission is needed.
1616
permissions:
1717
contents: read
1818

1919
# A workflow run is made up of one or more jobs, which run in parallel by default.
20-
# Each job runs in a runner environment specified by runs-on.
20+
# Each job runs in a runner environment specified by `runs-on`.
2121
jobs:
2222

2323
test:
@@ -29,11 +29,12 @@ jobs:
2929
- name: Check out repository code
3030
uses: actions/checkout@v4
3131

32-
# This GitHub Action installs a Rust toolchain using rustup. It is designed for one-line concise usage and good defaults.
32+
# This GitHub Action installs a Rust toolchain using "rustup".
33+
# It is designed for one-line concise usage and good defaults.
3334
- name: Install the Rust toolchain
3435
uses: dtolnay/rust-toolchain@stable
3536

36-
# A GitHub Action that implements smart caching for rust/cargo projects with sensible defaults
37+
# A GitHub Action that implements smart caching for rust/cargo projects with sensible defaults.
3738
- name: Rust Cache Action
3839
uses: Swatinem/rust-cache@v2
3940

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.0] - 2024-
11+
12+
### Added
13+
1014
- Optional argument for minimum file size for which a user would like to perform file size reduction.
11-
- It can come in three sizes: S, M, L, for 100 kB, 500 kB and 1 MB, respectively.
15+
- It comes in three sizes: S, M, L, for 100 kB, 500 kB and 1 MB, respectively.
16+
- Add some info messages: at startup, then for copying and for skipping files.
17+
- Add a closing message that warns users in case of an error.
18+
- GitHub action "ci.yml" ("release.yml" had already been there).
19+
20+
### Changed
21+
22+
- When source and destination folders are different, non-supported files will simply be copied to the destination.
23+
- Previously, they would be left out.
24+
- Updated `README.md` with Examples and some new notes.
1225

1326
## [0.1.0] - 2023-12-29
1427
This is the very first (initial) fully-functioning version of the library and the program.

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
## Description
88
Reduces size of images in a folder (and optionally sub-folders, recursively).
99

10-
Supports JPEG and PNG formats.
10+
Supports JPEG and PNG image formats, with the following file extensions (case-insensitive): `jpg`, `jpeg`, `png`.
1111

1212
This is useful for archiving of photos, for example, as they look the same on a display even with a reduced file size.
13-
This application reduces the file sizes of the images in bulk.
13+
This application reduces file sizes of images in bulk.
1414

1515
By default, keeps the original images and creates copies with reduced file size.
1616

1717
By default, copies the entire folder tree, with all sub-folders that exist in the source tree.
1818
The target folder tree will be created automatically, and the new reduced-size images will be copied properly to their respective paths.
19-
It is only required to provide the root target folder, and it will also be created if it doesn't exist.
19+
It is only required to provide the root target folder, and it will also be created if it doesn't exist.
20+
Non-supported files will simply be copied to the destination.
2021

21-
The destination folder can be the same as the source folder, in which case the original images will be **overwritten**, and not retained.
22+
The destination folder can be the same as the source folder, in which case the original images will be **overwritten**, and not retained.
23+
Other, non-supported files, will be retained.
2224

2325
If there is enough disk space, it is advised to specify a different destination folder than the source folder,
2426
so that the original images can be retained and the newly-created reduced-size images can be inspected for quality.
@@ -29,11 +31,25 @@ If satisfied with the result, original images can be deleted afterwards easily t
2931
## Options
3032
- Look into subdirectories recursively (process the entire tree); recommended: `-r`, `--recursive`
3133
- Reduce both image dimensions by half: `--resize`
32-
- JPEG quality, on a scale from 1 (worst) to 100 (best); the default is 75; ignored in case of PNGs: `--quality <QUALITY>`
34+
- JPEG quality, on a scale from 1 (worst) to 100 (best); the default is 75; ignored in case of PNGs: `-q`, `--quality <QUALITY>`
35+
- A minimum file size for which a user would like to perform file size reduction: `-s {s,m,l,S,M,L}`, `--size {s,m,l,S,M,L}`
36+
- S = 100 kB, M = 500 kB, L = 1 MB
37+
- Files that are smaller than the designated size will simply be copied to the destination folder.
38+
- If this option is left out, then all files are considered for size reduction; i.e., minimal size is 0.
39+
40+
### Examples
41+
See below for how to prepare the application for running.
42+
The file paths in the examples are for Windows.
43+
- `reduce_image_size D:\img_src D:\img_dst`
44+
- `reduce_image_size D:\img_src D:\img_dst -r`
45+
- `reduce_image_size D:\img_src D:\img_dst -r -s m`
46+
- `reduce_image_size D:\img_src D:\img_dst --recursive --size L`
47+
- `reduce_image_size D:\img_src D:\img_dst -r --resize -q 60 -s l`
48+
- `reduce_image_size D:\img_src D:\img_dst --recursive --resize --quality 60 --size L`
3349

3450
## Notes
3551
- Developed in Rust 1.74.1.
36-
- Tested on x86-64 CPUs with Windows 10 and Windows 11.
52+
- Tested on x86-64 CPUs on Windows 10 and Windows 11.
3753
- Also tested on WSL - Ubuntu 22.04.2 LTS (GNU/Linux 5.15.133.1-microsoft-standard-WSL2 x86_64) on Windows 11.
3854
- Other OSes haven't been tested, but should work.
3955

src/cli.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
use std::path::PathBuf;
44

5-
use clap::Parser;
5+
use clap::builder::PossibleValue;
6+
use clap::{Parser, ValueEnum};
67

78
use crate::constants::QUALITY;
89

@@ -31,4 +32,71 @@ pub struct Args {
3132
#[arg(short, long, default_value_t = QUALITY,
3233
value_parser = clap::value_parser!(i32).range(1..=100))]
3334
pub quality: i32,
35+
36+
/// A minimum file size for which to perform file size reduction;
37+
/// DEFAULT = 0, S = 100 kB, M = 500 kB, L = 1 MB
38+
#[arg(short, long, default_value_t = SizeCLI::DEFAULT)]
39+
pub size: SizeCLI,
40+
}
41+
42+
/// A minimum file size for which to perform file size reduction;
43+
/// DEFAULT = 0, S = 100 kB, M = 500 kB, L = 1 MB
44+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
45+
pub enum SizeCLI {
46+
DEFAULT,
47+
S,
48+
M,
49+
L,
50+
}
51+
52+
impl SizeCLI {
53+
/// Report all `possible_values`.
54+
pub fn possible_values() -> impl Iterator<Item = PossibleValue> {
55+
Self::value_variants()
56+
.iter()
57+
.filter_map(ValueEnum::to_possible_value)
58+
}
59+
}
60+
61+
impl Default for SizeCLI {
62+
fn default() -> Self {
63+
Self::DEFAULT
64+
}
65+
}
66+
67+
impl std::fmt::Display for SizeCLI {
68+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69+
self.to_possible_value()
70+
.expect("No values are skipped.")
71+
.get_name()
72+
.fmt(f)
73+
}
74+
}
75+
76+
impl std::str::FromStr for SizeCLI {
77+
type Err = String;
78+
79+
fn from_str(s: &str) -> Result<Self, Self::Err> {
80+
for variant in Self::value_variants() {
81+
if variant.to_possible_value().unwrap().matches(s, false) {
82+
return Ok(*variant);
83+
}
84+
}
85+
Err(format!("Invalid variant: {s}"))
86+
}
87+
}
88+
89+
impl ValueEnum for SizeCLI {
90+
fn value_variants<'a>() -> &'a [Self] {
91+
&[Self::DEFAULT, Self::S, Self::M, Self::L]
92+
}
93+
94+
fn to_possible_value(&self) -> Option<PossibleValue> {
95+
Some(match self {
96+
Self::DEFAULT => PossibleValue::new("DEFAULT"),
97+
Self::S => PossibleValue::new("S").alias("s"),
98+
Self::M => PossibleValue::new("M").alias("m"),
99+
Self::L => PossibleValue::new("L").alias("l"),
100+
})
101+
}
34102
}

src/constants.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,16 @@
22
33
/// JPEG quality default value
44
pub const QUALITY: i32 = 75;
5+
6+
/// A minimum file size for which to perform file size reduction.
7+
/// - DEFAULT = 0
8+
/// - S = 100 kB
9+
/// - M = 500 kB
10+
/// - L = 1 MB
11+
#[derive(Debug)]
12+
pub enum Size {
13+
DEFAULT = 0,
14+
S = 102400,
15+
M = 512000,
16+
L = 1048576,
17+
}

src/logic.rs

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,63 @@ fn print_success(src_path: &Path, dst_path: &Path, different_paths: bool, lock:
171171
}
172172
}
173173

174-
/// Prints an error message to `stdout`.
174+
/// Sets the flag `has_error`. Prints an error message to `stdout`.
175175
///
176176
/// Wraps around the received error message,
177177
/// and notifies the end user that the image file will be skipped.
178178
#[inline]
179-
fn print_error(src_path: &Path, err: Box<dyn Error>, lock: &mut StdoutLock) {
179+
fn set_and_print_error(
180+
src_path: &Path,
181+
err: Box<dyn Error>,
182+
lock: &mut StdoutLock,
183+
has_error: &mut bool,
184+
) {
185+
*has_error = true;
186+
180187
writeln!(
181188
lock,
182189
"\t[ERROR] Trying to reduce size of \"{}\" failed with the following error: {}.\n\
183190
\tSkipping that file.\n",
184191
src_path.display(),
185192
err
186193
)
187-
.expect("Failed to write to stdout.")
194+
.expect("Failed to write to stdout.");
195+
}
196+
197+
/// Copies a file in case of different source and destination paths.
198+
///
199+
/// Skips a file in case of same source and destination path.
200+
///
201+
/// Prints an info message in either case.
202+
fn copy_or_skip(
203+
src_path: &Path,
204+
dst_path: &Path,
205+
different_paths: bool,
206+
lock: &mut StdoutLock,
207+
err: Option<Box<dyn Error>>,
208+
has_error: &mut bool,
209+
) {
210+
if let Some(error) = err {
211+
writeln!(lock, "{}", error).expect("Failed to write to stdout.");
212+
};
213+
214+
if different_paths {
215+
match fs::copy(src_path, dst_path) {
216+
Ok(_) => writeln!(
217+
lock,
218+
"Copied \"{}\" to \"{}\".",
219+
src_path.display(),
220+
dst_path.display()
221+
)
222+
.expect("Failed to write to stdout."),
223+
Err(e) => set_and_print_error(src_path, Box::from(e), lock, has_error),
224+
};
225+
} else {
226+
writeln!(lock, "Skipped \"{}\".", src_path.display()).expect("Failed to write to stdout.");
227+
}
188228
}
189229

230+
// TODO: Get file size and compare. Then copy_or_skip(), but without `err`. It can be Optional. Pass `None`.
190231
/// The main business logic.
191232
/// Loops over files and calls appropriate functions for processing images.
192233
/// Processing consists of optional resizing first, and of optimizing images
@@ -198,15 +239,20 @@ fn print_error(src_path: &Path, err: Box<dyn Error>, lock: &mut StdoutLock) {
198239
/// * `recursive` - Whether to look into entire directory sub-tree.
199240
/// * `resize` - Whether to resize image dimensions.
200241
/// * `quality` - JPEG image quality. Ignored in case of PNGs.
242+
///
243+
/// Returns `bool` stating whether there was any error in trying to reduce size of a file or to copy it.
244+
/// This `bool` can be `true` only in case where source and destination directories are different,
245+
/// because in case where they are same and a file cannot have its size reduced, it will be left intact
246+
/// in its source directory.
201247
pub fn process_images(
202248
src_dir: PathBuf,
203249
dst_dir: PathBuf,
204250
recursive: bool,
205251
resize: bool,
206252
quality: i32,
207-
) {
208-
println!("JPEG quality = {quality}\n");
209-
stdout().flush().expect("Failed to flush stdout.");
253+
_size: i32,
254+
) -> bool {
255+
let mut has_error = false;
210256

211257
let different_paths = src_dir != dst_dir;
212258

@@ -219,6 +265,10 @@ pub fn process_images(
219265
if let Some(extension) = src_path.extension() {
220266
let mut dst_path = PathBuf::from(src_path);
221267

268+
// TODO: Extract this into a function? See at the end. The `continue`s can be problematic.
269+
// TODO: Return a Boolean flag? Or Option<PathBuf>, for dst_path?
270+
// TODO: But, we shouldn't skip a file! We should copy it. Alright, but what if `mkdir` fails?
271+
// TODO: Well, we can then abort the program. Currently, we are reporting the failure, which could be not only okay, but perhaps a better option.
222272
if different_paths {
223273
dst_path = dst_dir.as_path().join(
224274
diff_paths(
@@ -236,13 +286,18 @@ pub fn process_images(
236286
"\n\tFailed to create the subdirectory {:?} with the following error: {}",
237287
parent, err
238288
);
239-
print_error(src_path, Box::from(err), &mut lock);
289+
set_and_print_error(
290+
src_path,
291+
Box::from(err),
292+
&mut lock,
293+
&mut has_error,
294+
);
240295
continue;
241296
}
242297
};
243298
} else {
244299
let err_msg = format!("Destination path {:?} doesn't have a parent.", dst_path);
245-
print_error(src_path, Box::from(err_msg), &mut lock);
300+
set_and_print_error(src_path, Box::from(err_msg), &mut lock, &mut has_error);
246301
continue;
247302
};
248303
}
@@ -251,15 +306,25 @@ pub fn process_images(
251306
"jpg" | "jpeg" => {
252307
match process_jpeg(src_path, &dst_path, resize, quality, &mut lock) {
253308
Ok(_) => print_success(src_path, &dst_path, different_paths, &mut lock),
254-
Err(err) => print_error(src_path, err, &mut lock),
309+
Err(err) => set_and_print_error(src_path, err, &mut lock, &mut has_error), // TODO: Or copy_or_skip()? Pass `err` in and print it first, like in Python.
255310
}
256311
}
257312
"png" => match process_png(src_path, &dst_path, resize, &mut lock) {
258313
Ok(_) => print_success(src_path, &dst_path, different_paths, &mut lock),
259-
Err(err) => print_error(src_path, err, &mut lock),
314+
Err(err) => set_and_print_error(src_path, err, &mut lock, &mut has_error), // TODO: Or copy_or_skip()? Pass `err` in and print it first, like in Python.
260315
},
261-
_ => (),
316+
// _ => (), // TODO: copy_or_skip(), but without `err`? Pass `None`.
317+
_ => copy_or_skip(
318+
src_path,
319+
&dst_path,
320+
different_paths,
321+
&mut lock,
322+
None,
323+
&mut has_error,
324+
),
262325
}
263326
}
264327
}
328+
329+
has_error
265330
}

0 commit comments

Comments
 (0)