2 | pkgname=artem
3 | pkgver=1.1.3
4 | pkgrel=1
5 | pkgdesc='Convert images from multiple formats (jpg, png, webp, etc…) to ASCII art, written in Rust'
6 | arch=('x86_64' 'aarch64')
7 | url='https://github.com/finefindus/artem'
8 | license=('MPL2')
9 | makedepends=('cargo')
10 | provides=('artem')
11 | conflicts=('artem')
12 | source=("$pkgname-$pkgver=.tar.gz::$url/archive/v$pkgver.tar.gz")
13 | sha256sums=('be963f8fbf328ebfd0e2ea66dfe9a1a30ebdb248d0ff7f5c2685fa660b5af9cc')
14 |
15 |
16 | prepare() {
17 | cd "$pkgname-$pkgver"
18 | cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
19 | }
20 |
21 | build() {
22 | cd "$pkgname-$pkgver"
23 | export RUSTUP_TOOLCHAIN=stable
24 | export CARGO_TARGET_DIR=target
25 | cargo build --release --frozen
26 |
27 | #create a completions and doc folder
28 | mkdir -p deployment/completions
29 | mkdir -p deployment/doc
30 | #copy completion files and man page
31 | cp -u target/release/build/artem-*/out/* deployment/completions/
32 | #move man page to doc folder
33 | mv deployment/completions/artem.1 deployment/doc/
34 | #copy binary file
35 | cp -u target/release/artem deployment
36 | }
37 |
38 | check() {
39 | cd "$pkgname-$pkgver"
40 | cargo test --frozen --test '*'
41 | }
42 |
43 | package() {
44 | cd "$pkgname-$pkgver"
45 | install -Dm 755 target/release/artem -t "$pkgdir/usr/bin"
46 | install -Dm 644 README.md -t "$pkgdir/usr/share/doc/$pkgname"
47 | install -Dm 644 CHANGELOG.md -t "$pkgdir/usr/share/doc/$pkgname"
48 | install -Dm 644 deployment/doc/artem.1 -t "$pkgdir/usr/share/man/man1/"
49 | install -Dm 644 deployment/completions/artem.bash -t "$pkgdir/usr/share/bash-completion/completions/"
50 | install -Dm 644 deployment/completions/artem.fish -t "$pkgdir/usr/share/fish/vendor_completions.d/"
51 | install -Dm 644 deployment/completions/_artem -t "$pkgdir/usr/share/zsh/site-functions"
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/assets/aur/update_PKGBUILD.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Get version"
4 | version="$(sed -n 's/^version = "\(.*\)"/\1/p' ../../Cargo.toml | head -n1)"
5 | echo "Version: $version"
6 |
7 | echo "Creating stable release PKGBUILD-stable"
8 | echo "Replacing version in PKGBUILD-stable"
9 | sed -i "s/pkgver=[1-9]\+[0-9]*\(\.[0-9]\+\)\{2\}/pkgver=$version/" PKGBUILD-stable
10 |
11 | echo "Replacing hash in PKGBUILD-stable"
12 |
13 | echo "Downloading release and creating hash"
14 | hash="$(curl -sL https://github.com/finefindus/artem/archive/v$version.tar.gz | sha256sum | cut -d ' ' -f 1)"
15 |
16 | sed -i "s/sha256sums=('.*')/sha256sums=('$hash')/" PKGBUILD-stable
17 |
18 |
19 | echo "Creating bin release PKGBUILD-bin"
20 | echo "Replacing version in PKGBUILD-bin"
21 | sed -i "s/pkgver=[1-9]\+[0-9]*\(\.[0-9]\+\)\{2\}/pkgver=$version/" PKGBUILD-bin
22 |
23 | echo "Replacing hash in PKGBUILD-bin"
24 |
25 | echo "Downloading release and creating hash"
26 | hashbin="$(curl -sL https://github.com/FineFindus/artem/releases/download/v$version/artem-v$version-x86_64-unknown-linux-gnu.tar.gz | sha256sum | cut -d ' ' -f 1)"
27 |
28 | sed -i "s/sha256sums=('.*')/sha256sums=('$hashbin')/" PKGBUILD-bin
29 |
--------------------------------------------------------------------------------
/assets/images/abraham_lincoln.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineFindus/artem/9245935348e8f80af30da3a40e4d10f131f285b1/assets/images/abraham_lincoln.jpg
--------------------------------------------------------------------------------
/assets/images/moth.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineFindus/artem/9245935348e8f80af30da3a40e4d10f131f285b1/assets/images/moth.jpg
--------------------------------------------------------------------------------
/assets/images/radio_tower.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineFindus/artem/9245935348e8f80af30da3a40e4d10f131f285b1/assets/images/radio_tower.jpg
--------------------------------------------------------------------------------
/assets/images/standard_test_img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineFindus/artem/9245935348e8f80af30da3a40e4d10f131f285b1/assets/images/standard_test_img.png
--------------------------------------------------------------------------------
/assets/standard_test_img/standard_test_img.txt:
--------------------------------------------------------------------------------
1 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
2 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
3 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
4 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
5 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
6 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
7 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
8 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
9 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
10 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
11 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
12 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
13 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
14 | ::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::
15 | ccccccccccOOOOOOOOOkkkkkkkkxdddddddddddddddd:;;;;;;;;,,,,,,,,,'''''''';;;;;;;;;;
16 | OOOOOOOOOOWWWWWWWWXOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
17 | OOOOOOOOOOWWWWWWWWXOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
18 | XXXXXXXXXX................''',,;;;::ccclloodddxxkkOOO00KKXXNNNNNNNNNNN..........
19 | XXXXXXXXXX .......'',,;;::cclllooddxxkkOOO00KKXXNNWWWWWWWWW..........
20 | ;;;;;;;;;; :XXXXXXXXXXXXXXXXX........''''',,,,,;;;;;;;;;;;;..........
21 | .......... cWWWWWWWWWWWWWWWWW ..........
22 | .......... cWWWWWWWWWWWWWWWWW ..........
23 | .......... cWWWWWWWWWWWWWWWWW ..........
24 | .......... cWWWWWWWWWWWWWWWWW ..........
25 | .......... cWWWWWWWWWWWWWWWWW ..........
26 | .......... cWWWWWWWWWWWWWWWWW ..........
--------------------------------------------------------------------------------
/assets/standard_test_img/standard_test_img_border.txt:
--------------------------------------------------------------------------------
1 | ╔══════════════════════════════════════════════════════════════════════════════╗
2 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
3 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
4 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
5 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
6 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
7 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
8 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
9 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
10 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
11 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
12 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
13 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
14 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
15 | ║:::::::::lOOOOOOOOkkkkkkkkxddddddddoooooooo................. ;:::::::::║
16 | ║cccccccccoOOOOOOOOkkkkkkkkkdddddddddddddddd;;;;;;;;;,,,,,,,,'''''''';;;;;;;;;;║
17 | ║OOOOOOOOOXWWWWWWWWOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO: ║
18 | ║OOOOOOOOOXWWWWWWWWOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO: ║
19 | ║XXXXXXXXXO...............''',,;;;::ccclloodddxxkkOO000KKXXNNNNNNNNNNo.........║
20 | ║XXXXXXXXXO .......'',,;;::cclllooddxxkkOO00KKXXXNNWWWWWWWWo.........║
21 | ║;;;;;;;;;, KXXXXXXXXXXXXXXXX........''''',,,,,;;;;;;;;;;;'.........║
22 | ║.......... XWWWWWWWWWWWWWWWW ..........║
23 | ║.......... XWWWWWWWWWWWWWWWW ..........║
24 | ║.......... XWWWWWWWWWWWWWWWW ..........║
25 | ║.......... XWWWWWWWWWWWWWWWW ..........║
26 | ║.......... XWWWWWWWWWWWWWWWW ..........║
27 | ║.......... XWWWWWWWWWWWWWWWW ..........║
28 | ╚══════════════════════════════════════════════════════════════════════════════╝
--------------------------------------------------------------------------------
/assets/standard_test_img/standard_test_img_border_outline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Artem Ascii Image
9 |
10 |
11 |
12 | ╔══════════════════════════════════════════════════════════════════════════════╗
13 | ║ x . : . ll . : x ║
14 | ║ x . : . ll . : x ║
15 | ║ x . : . ll . : x ║
16 | ║ x . : . ll . : x ║
17 | ║ x . : . ll . : x ║
18 | ║ x . : . ll . : x ║
19 | ║ x . : . ll . : x ║
20 | ║ x . : . ll . : x ║
21 | ║ x . : . ll . : x ║
22 | ║ x . : . ll . : x ║
23 | ║ x . : . ll . : x ║
24 | ║ x . : . ll . : x ║
25 | ║ x . : . ll . : x ║
26 | ║ x . : . ll . : x ║
27 | ║cccccccccO::::::::, c;;;;;;;::::::::dxccccccclcccccccdccccccccOccccccccc║
28 | ║ l 'd x ║
29 | ║ l.......;x....................... ............x ║
30 | ║,,,,,,,,,k::::::::::::::::::::::::::::;;;;;,'.. ..',;;;;;;;;;;;;k,,,,,,,,,║
31 | ║ x x ║
32 | ║cccccccccd .;kcccccccccccccccxoccccccccccccccccccccccccccccl.........║
33 | ║ : ;x l; . : ║
34 | ║ : ;x l; . : ║
35 | ║ : ;x l; . : ║
36 | ║ : ;x l; . : ║
37 | ║ : ;x l; . : ║
38 | ║ : ;x l; . : ║
39 | ╚══════════════════════════════════════════════════════════════════════════════╝
40 |
--------------------------------------------------------------------------------
/assets/standard_test_img/standard_test_img_border_outline.txt:
--------------------------------------------------------------------------------
1 | ╔══════════════════════════════════════════════════════════════════════════════╗
2 | ║ x . : . ll . : x ║
3 | ║ x . : . ll . : x ║
4 | ║ x . : . ll . : x ║
5 | ║ x . : . ll . : x ║
6 | ║ x . : . ll . : x ║
7 | ║ x . : . ll . : x ║
8 | ║ x . : . ll . : x ║
9 | ║ x . : . ll . : x ║
10 | ║ x . : . ll . : x ║
11 | ║ x . : . ll . : x ║
12 | ║ x . : . ll . : x ║
13 | ║ x . : . ll . : x ║
14 | ║ x . : . ll . : x ║
15 | ║ x . : . ll . : x ║
16 | ║cccccccccO::::::::, c;;;;;;;::::::::dxccccccclcccccccdccccccccOccccccccc║
17 | ║ l 'd x ║
18 | ║ l.......;x....................... ............x ║
19 | ║,,,,,,,,,k::::::::::::::::::::::::::::;;;;;,'.. ..',;;;;;;;;;;;;k,,,,,,,,,║
20 | ║ x x ║
21 | ║cccccccccd .;kcccccccccccccccxoccccccccccccccccccccccccccccl.........║
22 | ║ : ;x l; . : ║
23 | ║ : ;x l; . : ║
24 | ║ : ;x l; . : ║
25 | ║ : ;x l; . : ║
26 | ║ : ;x l; . : ║
27 | ║ : ;x l; . : ║
28 | ╚══════════════════════════════════════════════════════════════════════════════╝
--------------------------------------------------------------------------------
/assets/standard_test_img/standard_test_img_outline.txt:
--------------------------------------------------------------------------------
1 | ll . : . ;x . : ll
2 | ll . : . ;x . : ll
3 | ll . : . ;x . : ll
4 | ll . : . ;x . : ll
5 | ll . : . ;x . : ll
6 | ll . : . ;x . : ll
7 | ll . : . ;x . : ll
8 | ll . : . ;x . : ll
9 | ll . : . ;x . : ll
10 | ll . : . ;x . : ll
11 | ll . : . ;x . : ll
12 | ll . : . ;x . : ll
13 | ll . : . ;x . : ll
14 | ll . : . ;x . : ll
15 | cccccccccxx:::::::: l;;;;;;;::::::::lOccccccclccccccccdcccccccxxccccccccc
16 | :: k ll
17 | cl.......k........................ ...........ol
18 | ,,,,,,,,,dd::::::::::::::::::::::::::::;;;;;,'... ..',;;;;;;;;;;;;dd,,,,,,,,,
19 | ll ll
20 | cccccccccd: .Occccccccccccccccxxccccccccccccccccccccccccccccdc.........
21 | :; O ll . . ;:
22 | :; O ll . . ;:
23 | :; O ll . . ;:
24 | :; O ll . . ;:
25 | :; O ll . . ;:
26 | :; O ll . . ;:
--------------------------------------------------------------------------------
/assets/standard_test_img/standard_test_img_outline_hysteresis.txt:
--------------------------------------------------------------------------------
1 | ll O ;x O ll
2 | ll O ;x O ll
3 | ll O ;x O ll
4 | ll O ;x O ll
5 | ll O ;x O ll
6 | ll O ;x O ll
7 | ll O ;x O ll
8 | ll O ;x O ll
9 | ll O ;x O ll
10 | ll O ;x O ll
11 | ll O ;x O ll
12 | ll O ;x O ll
13 | ll O ;x O ll
14 | ll O ;x O ll
15 | cccccccccxxcccccccc 0cccccccccccccccoOcccccccccccccccc0cccccccxxccccccccc
16 | ll O ll
17 | .........oo.......O............................ ...............oo.........
18 | :::::::::dd::::::::::::::::::::::::::::::::::::' .;::::::::::::::dd:::::::::
19 | ll ll
20 | cccccccccxl .Occccccccccccccccxxccccccccccccccccccccccccccccxl
21 | ll O ll ll
22 | ll O ll ll
23 | ll O ll ll
24 | ll O ll ll
25 | ll O ll ll
26 | ll O ll ll
--------------------------------------------------------------------------------
/assets/update_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This script can be used to easily update when the test results should change
3 | # That can happen even when to image is just slightly different, for example a single char is different.
4 |
5 | echo "Building release"
6 | cargo build --release
7 |
8 | echo "Creating text files"
9 |
10 | echo "Creating output file without extra arguments"
11 | cargo run --release images/standard_test_img.png -o standard_test_img/standard_test_img.txt
12 |
13 | echo "Creating output file with border"
14 | cargo run --release images/standard_test_img.png --border -o standard_test_img/standard_test_img_border.txt
15 |
16 | echo "Creating output file with outline and border"
17 | cargo run --release images/standard_test_img.png --border --outline -o standard_test_img/standard_test_img_border_outline.txt
18 |
19 | echo "Creating output file with outline"
20 | cargo run --release images/standard_test_img.png --outline -o standard_test_img/standard_test_img_outline.txt
21 |
22 | echo "Creating output file with outline and hysteresis"
23 | cargo run --release images/standard_test_img.png --outline --hysteresis -o standard_test_img/standard_test_img_outline_hysteresis.txt
24 |
25 |
26 | echo "Creating html files"
27 |
28 | echo "Creating .html output file without extra arguments"
29 | cargo run --release images/standard_test_img.png -o standard_test_img/standard_test_img.html
30 |
31 | echo "Creating .html output file with background color"
32 | cargo run --release images/standard_test_img.png --background -o standard_test_img/standard_test_img_background.html
33 |
34 | echo "Creating .html output file with border"
35 | cargo run --release images/standard_test_img.png --border -o standard_test_img/standard_test_img_border.html
36 |
37 | echo "Creating .html output file with outline and border"
38 | cargo run --release images/standard_test_img.png --border --outline -o standard_test_img/standard_test_img_border_outline.html
39 |
40 | echo "Creating .html output file with outline"
41 | cargo run --release images/standard_test_img.png --outline -o standard_test_img/standard_test_img_outline.html
42 |
43 | echo "Creating .html output file with outline and hysteresis"
44 | cargo run --release images/standard_test_img.png --outline --hysteresis -o standard_test_img/standard_test_img_outline_hysteresis.html
45 |
--------------------------------------------------------------------------------
/benches/artem_bench.rs:
--------------------------------------------------------------------------------
1 | use criterion::criterion_main;
2 |
3 | //import benchmarks
4 | mod benchmarks;
5 |
6 | criterion_main!(
7 | //without any options set
8 | benchmarks::default::benches,
9 | //different size options
10 | benchmarks::size::benches,
11 | //using the outline algorithm
12 | benchmarks::outline::benches,
13 | //using the outline algorithm with hysteresis and double threshold
14 | benchmarks::hysteresis::benches,
15 | );
16 |
--------------------------------------------------------------------------------
/benches/benchmarks/default.rs:
--------------------------------------------------------------------------------
1 | use crate::benchmarks::util;
2 | use criterion::{criterion_group, Criterion};
3 |
4 | /// Benchmarks for the default options.
5 | ///
6 | /// The default options can be viewed at [OptionBuilder::default()], in short
7 | /// it will use 1 thread, a target size of 80 and a scale of 0.42 as well as the
8 | /// default density.
9 | fn default_options_benchmark(c: &mut Criterion) {
10 | let mut group = c.benchmark_group("default options");
11 |
12 | let options = artem::config::ConfigBuilder::new();
13 |
14 | //use lower sample size for faster benchmarking
15 | //it should still take long enough to see relevant changes in performance
16 | group.sample_size(10);
17 |
18 | //test on different resolutions
19 |
20 | group.bench_function("low resolution", |b| {
21 | b.iter_batched(
22 | util::load_low_res_image,
23 | |data| artem::convert(data, &options.build()),
24 | criterion::BatchSize::LargeInput,
25 | );
26 | });
27 |
28 | group.bench_function("normal resolution", |b| {
29 | b.iter_batched(
30 | util::load_normal_res_image,
31 | |data| artem::convert(data, &options.build()),
32 | criterion::BatchSize::LargeInput,
33 | );
34 | });
35 |
36 | group.bench_function("high resolution", |b| {
37 | b.iter_batched(
38 | util::load_high_res_image,
39 | |data| artem::convert(data, &options.build()),
40 | criterion::BatchSize::LargeInput,
41 | );
42 | });
43 |
44 | group.finish();
45 | }
46 |
47 | criterion_group!(benches, default_options_benchmark);
48 |
--------------------------------------------------------------------------------
/benches/benchmarks/hysteresis.rs:
--------------------------------------------------------------------------------
1 | use crate::benchmarks::util;
2 | use criterion::{criterion_group, Criterion};
3 |
4 | /// Benchmarks for outlining an image with hysteresis.
5 | fn hysteresis_benchmark(c: &mut Criterion) {
6 | let mut group = c.benchmark_group("hysteresis");
7 |
8 | //use lower sample size for faster benchmarking
9 | //it should still take long enough to see relevant changes in performance
10 | group.sample_size(10);
11 |
12 | let mut options = artem::config::ConfigBuilder::new();
13 | //need to have outline enabled
14 | options.outline(true);
15 | //enable hysteresis
16 | options.hysteresis(true);
17 |
18 | //test on different resolutions
19 |
20 | group.bench_function("low resolution", |b| {
21 | b.iter_batched(
22 | util::load_low_res_image,
23 | |data| artem::convert(data, &options.build()),
24 | criterion::BatchSize::LargeInput,
25 | );
26 | });
27 |
28 | group.bench_function("normal resolution", |b| {
29 | b.iter_batched(
30 | util::load_normal_res_image,
31 | |data| artem::convert(data, &options.build()),
32 | criterion::BatchSize::LargeInput,
33 | );
34 | });
35 |
36 | group.bench_function("high resolution", |b| {
37 | b.iter_batched(
38 | util::load_high_res_image,
39 | |data| artem::convert(data, &options.build()),
40 | criterion::BatchSize::LargeInput,
41 | );
42 | });
43 |
44 | group.finish();
45 | }
46 |
47 | criterion_group!(benches, hysteresis_benchmark);
48 |
--------------------------------------------------------------------------------
/benches/benchmarks/mod.rs:
--------------------------------------------------------------------------------
1 | ///Benchmark for the default configuration.
2 | pub mod default;
3 | //Benchmark for different size arguments
4 | pub mod size;
5 | //outline version without hysteresis
6 | pub mod outline;
7 | //outline version with hysteresis
8 | pub mod hysteresis;
9 | ///Utils for loading different images.
10 | mod util;
11 |
--------------------------------------------------------------------------------
/benches/benchmarks/outline.rs:
--------------------------------------------------------------------------------
1 | use crate::benchmarks::util;
2 | use criterion::{criterion_group, Criterion};
3 |
4 | /// Benchmarks for outlined output.
5 | ///
6 | /// Benchmarks the `outline` options.
7 | fn outline_benchmark(c: &mut Criterion) {
8 | let mut group = c.benchmark_group("outline");
9 |
10 | //use lower sample size for faster benchmarking
11 | //it should still take long enough to see relevant changes in performance
12 | group.sample_size(10);
13 |
14 | let mut options = artem::config::ConfigBuilder::new();
15 | //enable outline
16 | options.outline(true);
17 |
18 | //test on different resolutions
19 |
20 | group.bench_function("low resolution", |b| {
21 | b.iter_batched(
22 | util::load_low_res_image,
23 | |data| artem::convert(data, &options.build()),
24 | criterion::BatchSize::LargeInput,
25 | );
26 | });
27 |
28 | group.bench_function("normal resolution", |b| {
29 | b.iter_batched(
30 | util::load_normal_res_image,
31 | |data| artem::convert(data, &options.build()),
32 | criterion::BatchSize::LargeInput,
33 | );
34 | });
35 |
36 | group.bench_function("high resolution", |b| {
37 | b.iter_batched(
38 | util::load_high_res_image,
39 | |data| artem::convert(data, &options.build()),
40 | criterion::BatchSize::LargeInput,
41 | );
42 | });
43 |
44 | group.finish();
45 | }
46 |
47 | criterion_group!(benches, outline_benchmark);
48 |
--------------------------------------------------------------------------------
/benches/benchmarks/size.rs:
--------------------------------------------------------------------------------
1 | use std::num::NonZeroU32;
2 |
3 | use crate::benchmarks::util;
4 | use criterion::{criterion_group, Criterion};
5 |
6 | /// Benchmarks for the target size of 10.
7 | ///
8 | /// All other options will remain as default. This benchmark, together
9 | /// with similar size benchmarks ensures that there is no gigantic and unexpected
10 | /// performance differences for different target sizes.
11 | fn size_10_benchmark(c: &mut Criterion) {
12 | let mut group = c.benchmark_group("size 10");
13 |
14 | //use lower sample size for faster benchmarking
15 | //it should still take long enough to see relevant changes in performance
16 | group.sample_size(10);
17 |
18 | let mut options = artem::config::ConfigBuilder::new();
19 | //set target size for all benches
20 | options.target_size(NonZeroU32::new(10).unwrap());
21 |
22 | //test on different resolutions
23 |
24 | group.bench_function("low resolution", |b| {
25 | b.iter_batched(
26 | util::load_low_res_image,
27 | |data| artem::convert(data, &options.build()),
28 | criterion::BatchSize::LargeInput,
29 | );
30 | });
31 |
32 | group.bench_function("normal resolution", |b| {
33 | b.iter_batched(
34 | util::load_normal_res_image,
35 | |data| artem::convert(data, &options.build()),
36 | criterion::BatchSize::LargeInput,
37 | );
38 | });
39 |
40 | group.bench_function("high resolution", |b| {
41 | b.iter_batched(
42 | util::load_high_res_image,
43 | |data| artem::convert(data, &options.build()),
44 | criterion::BatchSize::LargeInput,
45 | );
46 | });
47 |
48 | group.finish();
49 | }
50 |
51 | /// Benchmarks for the target size of 100.
52 | ///
53 | /// All other options will remain as default. This benchmark, together
54 | /// with similar size benchmarks ensures that there is no gigantic and unexpected
55 | /// performance differences for different target sizes.
56 | fn size_100_benchmark(c: &mut Criterion) {
57 | let mut group = c.benchmark_group("size 100");
58 |
59 | //use lower sample size for faster benchmarking
60 | //it should still take long enough to see relevant changes in performance
61 | group.sample_size(10);
62 |
63 | let mut options = artem::config::ConfigBuilder::new();
64 | //set target size for all benches
65 | options.target_size(NonZeroU32::new(100).unwrap());
66 |
67 | //test on different resolutions
68 |
69 | group.bench_function("low resolution", |b| {
70 | b.iter_batched(
71 | util::load_low_res_image,
72 | |data| artem::convert(data, &options.build()),
73 | criterion::BatchSize::LargeInput,
74 | );
75 | });
76 |
77 | group.bench_function("normal resolution", |b| {
78 | b.iter_batched(
79 | util::load_normal_res_image,
80 | |data| artem::convert(data, &options.build()),
81 | criterion::BatchSize::LargeInput,
82 | );
83 | });
84 |
85 | group.bench_function("high resolution", |b| {
86 | b.iter_batched(
87 | util::load_high_res_image,
88 | |data| artem::convert(data, &options.build()),
89 | criterion::BatchSize::LargeInput,
90 | );
91 | });
92 |
93 | group.finish();
94 | }
95 |
96 | /// Benchmarks for the target size of 500.
97 | ///
98 | /// All other options will remain as default. This benchmark, together
99 | /// with similar size benchmarks ensures that there is no gigantic and unexpected
100 | /// performance differences for different target sizes.
101 | fn size_500_benchmark(c: &mut Criterion) {
102 | let mut group = c.benchmark_group("size 500");
103 |
104 | //use lower sample size for faster benchmarking
105 | //it should still take long enough to see relevant changes in performance
106 | group.sample_size(10);
107 |
108 | let mut options = artem::config::ConfigBuilder::new();
109 | //set target size for all benches
110 | options.target_size(NonZeroU32::new(500).unwrap());
111 |
112 | //test on different resolutions
113 |
114 | group.bench_function("low resolution", |b| {
115 | b.iter_batched(
116 | util::load_low_res_image,
117 | |data| artem::convert(data, &options.build()),
118 | criterion::BatchSize::LargeInput,
119 | );
120 | });
121 |
122 | group.bench_function("normal resolution", |b| {
123 | b.iter_batched(
124 | util::load_normal_res_image,
125 | |data| artem::convert(data, &options.build()),
126 | criterion::BatchSize::LargeInput,
127 | );
128 | });
129 |
130 | group.bench_function("high resolution", |b| {
131 | b.iter_batched(
132 | util::load_high_res_image,
133 | |data| artem::convert(data, &options.build()),
134 | criterion::BatchSize::LargeInput,
135 | );
136 | });
137 |
138 | group.finish();
139 | }
140 |
141 | criterion_group!(
142 | benches,
143 | size_10_benchmark,
144 | size_100_benchmark,
145 | size_500_benchmark
146 | );
147 |
--------------------------------------------------------------------------------
/benches/benchmarks/util.rs:
--------------------------------------------------------------------------------
1 | use image::DynamicImage;
2 |
3 | /// Loads a low resolution image.
4 | ///
5 | /// The image is from
6 | /// and is stored in the assets/images directory.
7 | /// It has a resolution of 800x601.
8 | ///
9 | /// # Examples
10 | /// ```
11 | /// use benchmarks::util;
12 | /// let image = load_low_res_image();
13 | /// assert_eq!((800, 601), image.dimensions());
14 | /// ```
15 | pub fn load_low_res_image() -> DynamicImage {
16 | let path = "assets/images/moth.jpg";
17 | load_image(path)
18 | }
19 |
20 | /// Loads a normal resolution image.
21 | ///
22 | /// The image is from
23 | /// and is stored in the assets/images directory.
24 | /// It has a resolution of 2850x3742.
25 | ///
26 | /// # Examples
27 | /// ```
28 | /// use benchmarks::util;
29 | /// let image = load_normal_res_image();
30 | /// assert_eq!((2850, 3742), image.dimensions());
31 | /// ```
32 | pub fn load_normal_res_image() -> DynamicImage {
33 | let path = "assets/images/abraham_lincoln.jpg";
34 | load_image(path)
35 | }
36 |
37 | /// Loads a high resolution image.
38 | ///
39 | /// The image is from
40 | /// and is stored in the assets/images directory.
41 | /// It has a resolution of 3591x5386.
42 | ///
43 | /// # Examples
44 | /// ```
45 | /// use benchmarks::util;
46 | /// let image = load_high_res_image();
47 | /// assert_eq!((3591, 5386), image.dimensions());
48 | /// ```
49 | pub fn load_high_res_image() -> DynamicImage {
50 | let path = "assets/images/radio_tower.jpg";
51 | load_image(path)
52 | }
53 |
54 | /// Load and returns the image from the given path.
55 | ///
56 | /// # Panic
57 | /// Panics when failing to open the image.
58 | ///
59 | /// # Examples
60 | /// ```
61 | /// let image = load_image("test.png");
62 | /// ```
63 | fn load_image(path: impl AsRef) -> DynamicImage {
64 | let image = image::open(&path);
65 |
66 | if image.is_ok() {
67 | image.unwrap()
68 | } else {
69 | panic!("Failed to load image: {}", path.as_ref().to_str().unwrap())
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | use clap_complete::{
2 | generate_to,
3 | shells::{Bash, Elvish, Fish, PowerShell, Zsh},
4 | Generator,
5 | };
6 | use std::ffi::OsString;
7 | use std::{env, path};
8 |
9 | use std::io::Error;
10 |
11 | include!("src/cli.rs");
12 | //from https://docs.rs/clap_complete/3.0.6/clap_complete/generator/fn.generate_to.html
13 | fn main() -> Result<(), Error> {
14 | println!("cargo:rerun-if-changed=src/cli.rs");
15 |
16 | let out_dir = match env::var_os("OUT_DIR") {
17 | None => return Ok(()),
18 | Some(dir) => dir,
19 | };
20 |
21 | let mut cmd = build_cli();
22 | //this is only generated when the git ref changes???
23 | generate_shell_completion(&mut cmd, &out_dir, Bash).unwrap();
24 | generate_shell_completion(&mut cmd, &out_dir, PowerShell).unwrap();
25 | generate_shell_completion(&mut cmd, &out_dir, Zsh).unwrap();
26 | generate_shell_completion(&mut cmd, &out_dir, Fish).unwrap();
27 | generate_shell_completion(&mut cmd, &out_dir, Elvish).unwrap();
28 |
29 | let man = clap_mangen::Man::new(cmd);
30 | let mut buffer: Vec = Default::default();
31 | man.render(&mut buffer)?;
32 |
33 | let man_page_path = path::PathBuf::from(out_dir).join("artem.1");
34 |
35 | std::fs::write(&man_page_path, buffer)?;
36 |
37 | println!("cargo:warning=man page is generated: {:?}", man_page_path);
38 |
39 | Ok(())
40 | }
41 |
42 | fn generate_shell_completion(
43 | cmd: &mut Command,
44 | out_dir: &OsString,
45 | shell: T,
46 | ) -> Result
47 | where
48 | T: Generator,
49 | {
50 | //generate shell completions
51 | let path = generate_to(
52 | shell, cmd, // We need to specify what generator to use
53 | "artem", // We need to specify the bin name manually
54 | out_dir, // We need to specify where to write to
55 | )?;
56 | println!("cargo:warning=completion file is generated: {:?}", &path);
57 | Ok(path)
58 | }
59 |
--------------------------------------------------------------------------------
/examples/abraham_lincoln.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineFindus/artem/9245935348e8f80af30da3a40e4d10f131f285b1/examples/abraham_lincoln.jpg
--------------------------------------------------------------------------------
/examples/abraham_lincoln_ascii.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineFindus/artem/9245935348e8f80af30da3a40e4d10f131f285b1/examples/abraham_lincoln_ascii.png
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::{builder::PossibleValue, value_parser, Arg, ArgAction, Command, ValueEnum, ValueHint};
4 |
5 | /// Get arguments from the command line.
6 | ///
7 | /// It uses clap to build and return a [`Command`] struct, which then can be used
8 | /// configuration.
9 | ///
10 | /// This is a non-public module and should only be used by the binary file.
11 | ///
12 | /// # Examples
13 | /// ```
14 | /// //get clap matches
15 | /// let matches = build_cli();
16 | /// //for example check if an arg is present
17 | /// matches.is_present("arg");
18 | /// ```
19 | pub fn build_cli() -> Command {
20 | Command::new(clap::crate_name!())
21 | .version(clap::crate_version!())
22 | .author(clap::crate_authors!("\n"))
23 | .about(clap::crate_description!())
24 | .arg(
25 | Arg::new("INPUT")
26 | .help(
27 | if cfg!(feature = "web_image")
28 | {
29 | //special help message with url help
30 | "Paths or URLs to the target image. If the input is an URL, the image is downloaded and then converted. The original image is NOT altered."
31 | } else {
32 | //normal help text with only paths
33 | "Paths to the target image. The original image is NOT altered."
34 | }
35 |
36 | )
37 | .required(true)
38 | .value_hint(ValueHint::FilePath)
39 | //because of web images accept strings, which allows for URLs and files
40 | .value_parser(value_parser!(String))
41 | .action(ArgAction::Append)
42 | .num_args(..)
43 | )
44 | .arg(
45 | Arg::new("characters")
46 | .short('c')
47 | .long("characters")
48 | .value_parser(value_parser!(String))
49 | .action(ArgAction::Append)
50 | .value_hint(ValueHint::Other)
51 | //use "\" to keep this readable but still as a single line string
52 | .help("Change the characters that are used to display the image.\
53 | The first character should have the highest 'darkness' and the last should have the least (recommended to be a space ' '). \
54 | A lower detail map is recommend for smaller images. Included characters can be used with the argument 0 | 1 | 2. If no characters are passed in, the default set will be used."),
55 | )
56 | .arg(
57 | Arg::new("size")
58 | .short('s')
59 | .long("size")
60 | .value_parser(value_parser!(u32))
61 | .default_value("80")
62 | .value_hint(ValueHint::Other)
63 | .conflicts_with_all(["height", "width"])
64 | .help("Change the size of the output image. \
65 | The minimum size is 20. Lower values will be \
66 | ignored and changed to 20. This argument is conflicting with --width and --height."),
67 | )
68 | .arg(
69 | Arg::new("height")
70 | .long("height")
71 | .conflicts_with("width")
72 | .action(ArgAction::SetTrue)
73 | .help("Use the terminal maximum terminal height to display the image. \
74 | This argument is conflicting with --size and --width."),
75 | )
76 | .arg(
77 | Arg::new("width")
78 | .short('w')
79 | .long("width")
80 | .action(ArgAction::SetTrue)
81 | .help("Use the terminal maximum terminal width to display the image. \
82 | This argument is conflicting with --size and --height."),
83 | )
84 | .arg(
85 | Arg::new("scale")
86 | .long("ratio")
87 | .value_parser(value_parser!(f32))
88 | .default_value("0.42")
89 | .value_hint(ValueHint::Other)
90 | .help("Change the ratio between height and width, since ASCII characters are a bit higher than long. \
91 | The value has to be between 0.1 and 1.0. It is not recommend to change this setting."),
92 | ).arg(
93 | Arg::new("flipX")
94 | .long("flipX")
95 | .action(ArgAction::SetTrue)
96 | .help("Flip the image along the X-Axis/horizontally."),
97 | ).arg(
98 | Arg::new("flipY")
99 | .long("flipY")
100 | .action(ArgAction::SetTrue)
101 | .help("Flip the image along the Y-Axis/vertically."),
102 | ).arg(
103 | Arg::new("centerX")
104 | .long("centerX")
105 | .action(ArgAction::SetTrue)
106 | .help("Center the image along the X-Axis/horizontally in the terminal."),
107 | ).arg(
108 | Arg::new("centerY")
109 | .long("centerY")
110 | .action(ArgAction::SetTrue)
111 | .help("Center the image along the Y-Axis/vertically in the terminal."),
112 | )
113 | .arg(
114 | Arg::new("output-file")
115 | .short('o')
116 | .long("output")
117 | .value_parser(value_parser!(PathBuf))
118 | .value_hint(ValueHint::FilePath)
119 | .help("Output file for non-colored ascii. If the output file is a plaintext file, no color will be used. The use color, either use a file with an \
120 | .ansi extension, or an .svg/.html file, to convert the output to the respective format. \
121 | .ansi files will consider environment variables when creating colored output, for example when COLORTERM is not set to truecolor,\
122 | the resulting file will fallback to 8-bit colors."),
123 | )
124 | .arg(
125 | Arg::new("invert-density")
126 | .long("invert")
127 | .action(ArgAction::SetTrue)
128 | .help("Inverts the characters used for the image, so light characters will as dark ones. Can be useful if the image has a dark background."),
129 | )
130 | .arg(
131 | Arg::new("background-color")
132 | .long("background")
133 | .conflicts_with("no-color")
134 | .action(ArgAction::SetTrue)
135 | .help("Sets the background of the ascii as the color. This will be ignored if the terminal does not support truecolor. \
136 | This argument is mutually exclusive with the no-color argument."),
137 | )
138 | .arg(
139 | Arg::new("border")
140 | .long("border")
141 | .action(ArgAction::SetTrue)
142 | .help("Adds a decorative border surrounding the ascii image. This will make the image overall a bit smaller, \
143 | since it respects the user given size."),
144 | )
145 | .arg(
146 | Arg::new("no-color")
147 | .long("no-color")
148 | .action(ArgAction::SetTrue)
149 | .help("Do not use color when printing the image to the terminal."),
150 | )
151 | .arg(
152 | Arg::new("outline")
153 | .long("outline")
154 | .action(ArgAction::SetTrue)
155 | .help("Only create an outline of the image. This uses filters, so it will take more resources/time to complete, especially on larger images. \
156 | It might not produce the desired output, it is advised to use this only on images with a clear distinction between foreground and background."),
157 | )
158 | .arg(
159 | Arg::new("hysteresis")
160 | .long("hysteresis")
161 | .alias("hys")
162 | .requires("outline")
163 | .action(ArgAction::SetTrue)
164 | .help("When creating the outline use the hysteresis method, which will remove imperfection, but might not be as good looking in ascii form.\
165 | This will require the --outline argument to be present as well."),
166 | )
167 | .arg(
168 | Arg::new("verbosity")
169 | .long("verbose")
170 | .value_parser(value_parser!(Verbosity))
171 | .default_value("warn")
172 | .help("Choose the verbosity of the logging level. Warnings and errors will always be shown by default. To completely disable them, \
173 | use the off argument."),
174 | )
175 | }
176 | /// Verbosity enum for different logging levels.
177 | ///
178 | /// This enum is used for accepting the `--verbose` argument with different logging levels.
179 | ///
180 | /// This is basically a copy of the `log::LevelFilter`, with the
181 | /// additional implemented `clap::ValueEnum`, which allows clap to parse values as this enum.
182 | #[derive(Clone, Copy, Debug, Default)]
183 | pub enum Verbosity {
184 | /// Corresponds to the `Off` log level.
185 | Off,
186 | /// Corresponds to the `Error` log level.
187 | Error,
188 | /// Corresponds to the `Warn` log level.
189 | #[default]
190 | Warn,
191 | /// Corresponds to the `Info` log level.
192 | Info,
193 | /// Corresponds to the `Debug` log level.
194 | Debug,
195 | /// Corresponds to the `Trace` log level.
196 | Trace,
197 | }
198 |
199 | impl ValueEnum for Verbosity {
200 | fn value_variants<'a>() -> &'a [Self] {
201 | &[
202 | Verbosity::Off,
203 | Verbosity::Error,
204 | Verbosity::Warn,
205 | Verbosity::Info,
206 | Verbosity::Debug,
207 | Verbosity::Trace,
208 | ]
209 | }
210 |
211 | fn to_possible_value<'a>(&self) -> Option {
212 | Some(match self {
213 | Verbosity::Off => PossibleValue::new("off").help("Do not show logs"),
214 | Verbosity::Error => PossibleValue::new("error").help("Only show errors"),
215 | Verbosity::Warn => PossibleValue::new("warn").help("Show errors and warnings"),
216 | Verbosity::Info => PossibleValue::new("info").help("Show info logs"),
217 | Verbosity::Debug => PossibleValue::new("debug").help("Show debug logs"),
218 | Verbosity::Trace => PossibleValue::new("trace").help("Show trace logs"),
219 | })
220 | }
221 | }
222 |
223 | impl std::fmt::Display for Verbosity {
224 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 | self.to_possible_value()
226 | .expect("no values are skipped")
227 | .get_name()
228 | .fmt(f)
229 | }
230 | }
231 |
232 | impl std::str::FromStr for Verbosity {
233 | type Err = String;
234 |
235 | fn from_str(s: &str) -> Result {
236 | for variant in Self::value_variants() {
237 | if variant.to_possible_value().unwrap().matches(s, false) {
238 | return Ok(*variant);
239 | }
240 | }
241 | Err(format!("invalid variant: {s}"))
242 | }
243 | }
244 |
245 | impl From for log::LevelFilter {
246 | fn from(value: Verbosity) -> Self {
247 | match value {
248 | Verbosity::Off => log::LevelFilter::Off,
249 | Verbosity::Error => log::LevelFilter::Error,
250 | Verbosity::Warn => log::LevelFilter::Warn,
251 | Verbosity::Info => log::LevelFilter::Info,
252 | Verbosity::Debug => log::LevelFilter::Debug,
253 | Verbosity::Trace => log::LevelFilter::Trace,
254 | }
255 | }
256 | }
257 |
258 | #[cfg(test)]
259 | mod test {
260 | use super::*;
261 |
262 | #[test]
263 | fn fail_missing_input() {
264 | let matches = build_cli().try_get_matches_from(["artem"]);
265 | assert!(matches.is_err());
266 | }
267 |
268 | #[test]
269 | fn success_input() {
270 | let matches = build_cli().try_get_matches_from(["artem", "../example/abraham_lincoln.jpg"]);
271 | assert!(matches.is_ok());
272 | }
273 |
274 | #[test]
275 | fn fail_conflicting_args_size_width() {
276 | //size and width conflict
277 | let matches = build_cli().try_get_matches_from([
278 | "artem",
279 | "../example/abraham_lincoln.jpg",
280 | "-s 20",
281 | "-w",
282 | ]);
283 | assert!(matches.is_err());
284 | }
285 |
286 | #[test]
287 | fn fail_conflicting_args_size_height() {
288 | //size and height conflict
289 | let matches = build_cli().try_get_matches_from([
290 | "artem",
291 | "../example/abraham_lincoln.jpg",
292 | "-s 20",
293 | "-h",
294 | ]);
295 | assert!(matches.is_err());
296 | }
297 |
298 | #[test]
299 | fn fail_conflicting_args_height_width() {
300 | //height and width conflict
301 | let matches = build_cli().try_get_matches_from([
302 | "artem",
303 | "../example/abraham_lincoln.jpg",
304 | "-h",
305 | "-w",
306 | ]);
307 | assert!(matches.is_err());
308 | }
309 |
310 | #[test]
311 | fn fail_conflicting_args_no_color_background() {
312 | //height and width conflict
313 | let matches = build_cli().try_get_matches_from([
314 | "artem",
315 | "../example/abraham_lincoln.jpg",
316 | "--no-color",
317 | "--backgrounds",
318 | ]);
319 | assert!(matches.is_err());
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! # artem
2 | //! `artem` is a program to convert images to ascii art.
3 | //! While it's primary usages is through the command line, it also provides a rust crate.
4 | //!
5 | //! # Usage
6 | //! To use it, load an image using the [image crate](https://crates.io/crates/image) and pass it to
7 | //! artem. Addiontially the [`crate::convert`] function takes an [`crate::config::Config`], which can be used to configure
8 | //! the resulting output. Whilst [`crate::config::Config`] implements [`Default`], it is
9 | //! recommended to do the configuration through [`crate::config::ConfigBuilder`] instead.
10 | //! ```
11 | //! # let path = "./assets/images/standard_test_img.png";
12 | //! let image = image::open(path).expect("Failed to open image");
13 | //! let ascii_art = artem::convert(image, &artem::config::ConfigBuilder::new().build());
14 | //! ```
15 |
16 | //condense all arguments into a single struct
17 | pub mod config;
18 |
19 | //functions for working with pixels
20 | mod pixel;
21 |
22 | //outlining filter
23 | mod filter;
24 | //functions for dealing with output targets/files
25 | mod target;
26 |
27 | use std::sync::LazyLock;
28 |
29 | use image::{DynamicImage, GenericImageView};
30 |
31 | pub use crate::config::ConfigBuilder;
32 | use crate::config::{Config, ResizingDimension, TargetType};
33 |
34 | /// Takes an image and returns it as an ascii art string.
35 | ///
36 | /// The result can be changed using the [`crate::config::Config`] argument
37 | /// # Examples
38 | /// ```no_run
39 | /// use artem::config::ConfigBuilder;
40 | ///
41 | /// let img = image::open("examples/abraham_lincoln.jpg").unwrap();
42 | /// let converted_image = artem::convert(img, &ConfigBuilder::new().build());
43 | /// ```
44 | pub fn convert(image: DynamicImage, config: &Config) -> String {
45 | log::debug!("Using inverted color: {}", config.invert);
46 | //get img dimensions
47 | let input_width = image.width();
48 | let input_height = image.height();
49 | log::debug!("Input Image Width: {input_width}");
50 | log::debug!("Input Image Height: {input_height}");
51 |
52 | //calculate the needed dimensions
53 | let (columns, rows, tile_width, tile_height) = ResizingDimension::calculate_dimensions(
54 | config.target_size,
55 | input_height,
56 | input_width,
57 | config.scale,
58 | config.border,
59 | config.dimension,
60 | );
61 | log::debug!("Columns: {columns}");
62 | log::debug!("Rows: {rows}");
63 | log::debug!("Tile Width: {tile_width}");
64 | log::debug!("Tile Height: {tile_height}");
65 |
66 | let mut input_img = image;
67 |
68 | if config.outline {
69 | //create an outline using an algorithm loosely based on the canny edge algorithm
70 | input_img = filter::edge_detection_filter(input_img, config.hysteresis);
71 | }
72 |
73 | if config.transform_x {
74 | log::info!("Flipping image horizontally");
75 | input_img = input_img.fliph();
76 | }
77 |
78 | if config.transform_y {
79 | log::info!("Flipping image vertically");
80 | input_img = input_img.flipv();
81 | }
82 |
83 | log::info!("Resizing image to fit new dimensions");
84 | //use the thumbnail method, since its way faster, it may result in artifacts, but the ascii art will be pixelate anyway
85 | let source_img = input_img.thumbnail_exact(columns * tile_width, rows * tile_height);
86 |
87 | log::debug!("Resized Image Width: {}", source_img.width());
88 | log::debug!("Resized Image Height: {}", source_img.height());
89 |
90 | //output string
91 | let mut output = String::with_capacity((tile_width * tile_height) as usize);
92 | log::trace!("Created output string");
93 |
94 | if config.target == TargetType::HtmlFile {
95 | log::trace!("Adding html top part");
96 | output.push_str(&target::html::html_top());
97 | }
98 |
99 | log::trace!("Calculating horizontal spacing");
100 | let horizontal_spacing = if config.center_x {
101 | spacing_horizontal(if config.border {
102 | //two columns are missing because the border takes up two lines
103 | columns + 2
104 | } else {
105 | columns
106 | })
107 | } else {
108 | String::with_capacity(0)
109 | };
110 |
111 | if config.center_y && config.target == TargetType::Shell {
112 | log::trace!("Adding vertical top spacing");
113 | output.push_str(&spacing_vertical(if config.border {
114 | //two rows are missing because the border takes up two lines
115 | rows + 2
116 | } else {
117 | rows
118 | }));
119 | }
120 |
121 | if config.border {
122 | //add spacing for centering
123 | if config.center_x {
124 | output.push_str(&horizontal_spacing);
125 | }
126 |
127 | //add top part of border before conversion
128 | log::trace!("Adding top part of border");
129 | output.push('╔');
130 | output.push_str(&"═".repeat(columns as usize));
131 | output.push_str("╗\n");
132 | }
133 |
134 | log::info!("Starting conversion to ascii");
135 | let width = source_img.width();
136 |
137 | //convert source img to a target string
138 | let target = source_img
139 | .pixels()
140 | .step_by(tile_width as usize)
141 | .filter(|(x, y, _)| y % tile_height == 0 && x % tile_width == 0)
142 | .map(|(x, y, _)| {
143 | //pre-allocate vector with the with space for all pixels in the tile
144 | let mut pixels = Vec::with_capacity((tile_height * tile_width) as usize);
145 |
146 | //get all pixel of the tile
147 | for p_x in 0..tile_width {
148 | for p_y in 0..tile_height {
149 | pixels.push(unsafe { source_img.unsafe_get_pixel(x + p_x, y + p_y) })
150 | }
151 | }
152 |
153 | //convert pixels to a char/string
154 | let mut ascii_char = pixel::correlating_char(&pixels, config);
155 |
156 | //add border at the start
157 | //this cannot be done in single if-else, since the image might only be a single pixel wide
158 | if x == 0 {
159 | //add outer border (left)
160 | if config.border {
161 | ascii_char.insert(0, '║');
162 | }
163 |
164 | //add spacing for centering the image
165 | if config.center_x {
166 | ascii_char.insert_str(0, &horizontal_spacing);
167 | }
168 | }
169 |
170 | //add a break at line end
171 | if x == width - tile_width {
172 | //add outer border (right)
173 | if config.border {
174 | ascii_char.push('║');
175 | }
176 |
177 | ascii_char.push('\n');
178 | }
179 |
180 | ascii_char
181 | })
182 | .collect::();
183 |
184 | output.push_str(&target);
185 |
186 | if config.border {
187 | //add spacing for centering
188 | if config.center_x {
189 | output.push_str(&horizontal_spacing);
190 | }
191 |
192 | //add bottom part of border after conversion
193 | log::trace!("Adding bottom border");
194 | output.push('╚');
195 | output.push_str(&"═".repeat(columns as usize));
196 | output.push('╝');
197 | }
198 |
199 | //compare it, ignoring the enum value such as true, true
200 | if config.target == TargetType::HtmlFile {
201 | log::trace!("Adding html bottom part");
202 | output.push_str(&target::html::html_bottom());
203 | }
204 |
205 | if config.center_y && config.target == TargetType::Shell {
206 | log::trace!("Adding vertical bottom spacing");
207 | output.push_str(&spacing_vertical(if config.border {
208 | //two rows are missing because the border takes up two lines
209 | rows + 2
210 | } else {
211 | rows
212 | }));
213 | }
214 |
215 | output
216 | }
217 |
218 | /// Return a spacer string, which can be used to center the ascii image in the middle of the terminal.
219 | ///
220 | /// When the terminal width is not existing, for example when the output is not a terminal, the returned string will be empty.
221 | fn spacing_horizontal(width: u32) -> String {
222 | let term_width = terminal_size::terminal_size()
223 | .map(|dimensions| dimensions.0 .0 as u32)
224 | .unwrap_or_default();
225 | " ".repeat(term_width.saturating_sub(width).saturating_div(2) as usize)
226 | }
227 |
228 | /// Return a spacer string, which can be used to center the ascii image in the middle of the terminal.
229 | ///
230 | /// When the terminal height is not existing, for example when the output is not a terminal, the returned string will be empty.
231 | fn spacing_vertical(height: u32) -> String {
232 | let term_height = terminal_size::terminal_size()
233 | .map(|dimensions| dimensions.1 .0 as u32)
234 | .unwrap_or_default();
235 | log::trace!("H: {term_height}, h: {height}");
236 | "\n".repeat(term_height.saturating_sub(height).saturating_div(2) as usize)
237 | }
238 |
239 | /// Returns if the terminal supports truecolor mode.
240 | ///
241 | /// It checks the `COLORTERM` environment variable,
242 | /// if it is either set to
243 | /// `truecolor` or `24bit` true is returned.
244 | ///
245 | /// In all other cases false will be returned.
246 | ///
247 | /// # Examples
248 | /// ```
249 | /// use artem::SUPPORTS_TRUECOLOR;
250 | /// # use std::env;
251 | ///
252 | /// # env::set_var("COLORTERM", "truecolor");
253 | /// //only true when run in a shell that supports true color
254 | /// let color_support = *SUPPORTS_TRUECOLOR;
255 | /// assert!(color_support);
256 | /// ```
257 | pub static SUPPORTS_TRUECOLOR: LazyLock = LazyLock::new(|| {
258 | std::env::var("COLORTERM")
259 | .is_ok_and(|value| value.contains("truecolor") || value.contains("24bit"))
260 | });
261 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | //! # artem
2 | //! `artem` is a program to convert images to ascii art.
3 | //! While it's primary usages is through the command line, it also provides a rust crate.
4 | //!
5 | //! # Usage
6 | //! To use it, load an image using the [image crate](https://crates.io/crates/image) and pass it to
7 | //! artem. Addiontially the [`crate::convert`] function takes an [`crate::config::Config`], which can be used to configure
8 | //! the resulting output. Whilst [`crate::config::Config`] implements [`Default`], it is
9 | //! recommended to do the configuration through [`crate::config::ConfigBuilder`] instead.
10 | //! ```
11 | //! # let path = "./assets/images/standard_test_img.png";
12 | //! let image = image::open(path).expect("Failed to open image");
13 | //! let ascii_art = artem::convert(image, &artem::config::ConfigBuilder::new().build());
14 | //! ```
15 |
16 | use std::{
17 | fs::File,
18 | io::Write,
19 | num::NonZeroU32,
20 | path::{Path, PathBuf},
21 | };
22 |
23 | use image::{DynamicImage, ImageDecoder, ImageError, ImageReader};
24 |
25 | use artem::config::{self, ConfigBuilder, TargetType};
26 |
27 | //import cli
28 | mod cli;
29 |
30 | fn main() {
31 | //get args from cli
32 | let matches = cli::build_cli().get_matches();
33 |
34 | //get log level from args
35 | //enable logging
36 | env_logger::builder()
37 | .format_target(false)
38 | .format_timestamp(None)
39 | .filter_level(
40 | (*matches
41 | .get_one::("verbosity")
42 | .unwrap_or(&cli::Verbosity::Warn))
43 | .into(),
44 | )
45 | .init();
46 | log::trace!("Started logger with trace");
47 |
48 | //log enabled features
49 | log::trace!("Feature web_image: {}", cfg!(feature = "web_image"));
50 |
51 | let mut config_builder = ConfigBuilder::new();
52 |
53 | //at least one input must exist, so its safe to unwrap
54 | let input = matches.get_many::("INPUT").unwrap();
55 |
56 | let mut img_paths = Vec::with_capacity(input.len());
57 |
58 | log::info!("Checking inputs");
59 | for value in input {
60 | #[cfg(feature = "web_image")]
61 | if value.starts_with("http") {
62 | log::debug!("Input {} is a URL", value);
63 | img_paths.push(value);
64 | continue;
65 | }
66 |
67 | let path = Path::new(value);
68 | //check if file exist and is a file (not a directory)
69 | if !path.exists() {
70 | fatal_error(&format!("File {value} does not exist"), Some(66));
71 | } else if !Path::new(path).is_file() {
72 | fatal_error(&format!("{value} is not a file"), Some(66));
73 | }
74 | log::debug!("Input {} is a file", value);
75 | img_paths.push(value);
76 | }
77 |
78 | //density char map
79 | let density = match matches
80 | .get_one::("characters")
81 | .map(|res| res.as_str())
82 | {
83 | Some("short") | Some("s") | Some("0") => r#"Ñ@#W$9876543210?!abc;:+=-,._ "#,
84 | Some("flat") | Some("f") | Some("1") => r#"MWNXK0Okxdolc:;,'... "#,
85 | Some("long") | Some("l") | Some("2") => {
86 | r#"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^`'. "#
87 | }
88 | Some(chars) if !chars.is_empty() => {
89 | log::debug!("Using user provided characters");
90 | chars
91 | }
92 | _ => {
93 | //density map from jp2a
94 | log::debug!("Using default characters");
95 | r#"MWNXK0Okxdolc:;,'... "#
96 | }
97 | };
98 | log::debug!("Characters used: '{density}'");
99 | config_builder.characters(density.to_string());
100 |
101 | //set the default resizing dimension to width
102 | config_builder.dimension(config::ResizingDimension::Width);
103 |
104 | let terminal_size = |height: bool| -> u32 {
105 | //read terminal size, error when STDOUT is not a tty
106 | terminal_size::terminal_size()
107 | .map(|size| if height { size.1 .0 } else { size.0 .0 } as u32)
108 | .unwrap_or_else(|| {
109 | fatal_error(
110 | "Failed to read terminal size, STDOUT is not a tty",
111 | Some(72),
112 | )
113 | })
114 | };
115 | let height = matches.get_flag("height");
116 | //get target size from args
117 | //only one arg should be present
118 | let target_size = if matches.get_flag("width") || height {
119 | if height {
120 | config_builder.dimension(config::ResizingDimension::Height);
121 | }
122 | terminal_size(height)
123 | } else {
124 | //use given input size
125 | log::trace!("Using user input size as target size");
126 | *matches.get_one::("size").unwrap_or_else(|| {
127 | fatal_error(
128 | "Failed to read terminal size, STDOUT is not a tty",
129 | Some(72),
130 | )
131 | })
132 | }
133 | .max(20); //min should be 20 to ensure a somewhat visible picture
134 |
135 | log::debug!("Target Size: {target_size}");
136 | config_builder.target_size(NonZeroU32::new(target_size).unwrap()); //safe to unwrap, since it is clamped before
137 |
138 | //best ratio between height and width is 0.43
139 | let Some(scale) = matches.get_one::("scale").map(|scale| {
140 | scale.clamp(
141 | 0.1f32, //a negative or 0 scale is not allowed
142 | 1f32, //even a scale above 0.43 is not looking good
143 | )
144 | }) else {
145 | fatal_error("Could not work with ratio input value", Some(65));
146 | };
147 | log::debug!("Scale: {scale}");
148 | config_builder.scale(scale);
149 |
150 | let invert = matches.get_flag("invert-density");
151 | log::debug!("Invert is set to: {invert}");
152 | config_builder.invert(invert);
153 |
154 | let background_color = matches.get_flag("background-color");
155 | log::debug!("BackgroundColor is set to: {background_color}");
156 | config_builder.background_color(background_color);
157 |
158 | //check if no colors should be used or the if a output file will be used
159 | //since text documents don`t support ansi ascii colors
160 | let color = if matches.get_flag("no-color") {
161 | //print the "normal" non-colored conversion
162 | log::info!("Using non-colored ascii");
163 | false
164 | } else {
165 | if matches.get_flag("outline") {
166 | log::warn!("Using outline, result will only be in grayscale");
167 | //still set colors to true, since grayscale has different gray tones
168 | }
169 |
170 | //print colored terminal conversion, this should already respect truecolor support/use ansi colors if not supported
171 | log::info!("Using colored ascii");
172 | if !*artem::SUPPORTS_TRUECOLOR {
173 | if background_color {
174 | log::warn!("Background flag will be ignored, since truecolor is not supported.")
175 | }
176 | log::warn!("Truecolor is not supported. Using ansi color.")
177 | } else {
178 | log::info!("Using truecolor ascii")
179 | }
180 | true
181 | };
182 | config_builder.color(color);
183 |
184 | //get flag for border around image
185 | let border = matches.get_flag("border");
186 | config_builder.border(border);
187 | log::info!("Using border: {border}");
188 |
189 | //get flags for flipping along x axis
190 | let transform_x = matches.get_flag("flipX");
191 | config_builder.transform_x(transform_x);
192 | log::debug!("Flipping X-Axis: {transform_x}");
193 |
194 | //get flags for flipping along y axis
195 | let transform_y = matches.get_flag("flipY");
196 | config_builder.transform_y(transform_y);
197 | log::debug!("Flipping Y-Axis: {transform_y}");
198 |
199 | //get flags for centering the image
200 | let center_x = matches.get_flag("centerX");
201 | config_builder.center_x(center_x);
202 | log::debug!("Centering X-Axis: {center_x}");
203 |
204 | let center_y = matches.get_flag("centerY");
205 | config_builder.center_y(center_y);
206 | log::debug!("Center Y-Axis: {center_y}");
207 |
208 | //get flag for creating an outline
209 | let outline = matches.get_flag("outline");
210 | config_builder.outline(outline);
211 | log::debug!("Outline: {outline}");
212 |
213 | //if outline is set, also check for hysteresis
214 | if outline {
215 | let hysteresis = matches.get_flag("hysteresis");
216 | config_builder.hysteresis(hysteresis);
217 | log::debug!("Hysteresis: {hysteresis}");
218 | if hysteresis {
219 | log::warn!("Using hysteresis might result in an worse looking ascii image than only using --outline")
220 | }
221 | }
222 |
223 | //get output file extension for specific output, default to plain text
224 | if let Some(output_file) = matches.get_one::("output-file") {
225 | log::debug!("Output-file: {}", output_file.to_str().unwrap());
226 |
227 | //check file extension
228 | let file_extension = output_file.extension().and_then(std::ffi::OsStr::to_str);
229 | log::debug!("FileExtension: {:?}", file_extension);
230 |
231 | config_builder.target(match file_extension {
232 | Some("html") | Some("htm") => {
233 | log::debug!("Target: Html-File");
234 | TargetType::HtmlFile
235 | }
236 | Some("ansi") | Some("ans") => {
237 | log::debug!("Target: Ansi-File");
238 |
239 | //by definition ansi file must have colors, only the background color is optional
240 | if matches.get_flag("no-color") {
241 | log::warn!("The --no-color argument conflicts with the target file type. Falling back to plain text file without colors.");
242 | TargetType::File
243 | } else {
244 | if !*artem::SUPPORTS_TRUECOLOR {
245 | log::warn!("truecolor is disabled, output file will not use truecolor chars")
246 | }
247 | TargetType::AnsiFile
248 | }
249 | }
250 | Some("svg") => {
251 | log::debug!("Target: SVG");
252 | TargetType::Svg
253 | }
254 | _ => {
255 | log::debug!("Target: File");
256 |
257 | if !matches.get_flag("no-color") {
258 | //warn user that output is not colored
259 | log::warn!("Filetype does not support using colors. For colored output file please use either .html or .ansi files");
260 | }
261 | TargetType::File
262 | }
263 | });
264 | } else {
265 | log::debug!("Target: Shell");
266 | config_builder.target(TargetType::Shell);
267 | }
268 |
269 | let config = config_builder.build();
270 | let mut output = img_paths
271 | .iter()
272 | .map(|path| load_image(path).unwrap_or_else(|err| fatal_error(&err.to_string(), Some(66))))
273 | .filter(|img| img.height() != 0 || img.width() != 0)
274 | .map(|img| artem::convert(img, &config))
275 | .collect::();
276 |
277 | //remove last linebreak, we cannot use `.trim_end()` here
278 | //as it may end up remove whitespace that is part of the image
279 | if output.ends_with('\n') {
280 | output.remove(output.len() - 1);
281 | }
282 |
283 | //create and write to output file
284 | if let Some(output_file) = matches.get_one::("output-file") {
285 | log::info!("Writing output to output file");
286 |
287 | let Ok(mut file) = File::create(output_file) else {
288 | fatal_error("Could not create output file", Some(73));
289 | };
290 |
291 | if config.target == TargetType::Svg {
292 | //convert terminal text to svg
293 | output = anstyle_svg::Term::new().render_svg(&output);
294 | }
295 |
296 | log::trace!("Created output file");
297 | let Ok(bytes_count) = file.write(output.as_bytes()) else {
298 | fatal_error("Could not write to output file", Some(74));
299 | };
300 | log::info!("Written ascii chars to output file");
301 | println!("Written {} bytes to {}", bytes_count, output_file.display())
302 | } else {
303 | //print the ascii img to the terminal
304 | log::info!("Printing output");
305 | println!("{}", output);
306 | }
307 | }
308 |
309 | /// Return the image from the specified path.
310 | ///
311 | /// Loads the image from the specified path.
312 | /// If the path is a url and the web_image feature is enabled,
313 | /// the image will be downloaded and opened from memory.
314 | ///
315 | /// # Examples
316 | /// ```
317 | /// let image = load_image("../examples/abraham_lincoln.jpg")
318 | /// ```
319 | fn load_image(path: &str) -> Result {
320 | #[cfg(feature = "web_image")]
321 | if path.starts_with("http") {
322 | log::info!("Started to download image from: {}", path);
323 | let now = std::time::Instant::now();
324 | let Ok(resp) = ureq::get(path).call() else {
325 | fatal_error(
326 | &format!("Failed to load image bytes from {}", path),
327 | Some(66),
328 | );
329 | };
330 |
331 | //get bytes of the images
332 | let mut bytes: Vec = Vec::new();
333 | resp.into_reader()
334 | .read_to_end(&mut bytes)
335 | .expect("Failed to read bytes");
336 | log::info!("Downloading took {:3} ms", now.elapsed().as_millis());
337 |
338 | log::debug!("Opening downloaded image from memory");
339 | return image::load_from_memory(&bytes);
340 | }
341 |
342 | log::info!("Opening image");
343 | let mut decoder = ImageReader::open(path)?.into_decoder()?;
344 | let orientation = decoder.orientation()?;
345 | let mut img = DynamicImage::from_decoder(decoder)?;
346 | img.apply_orientation(orientation);
347 |
348 | Ok(img)
349 | }
350 |
351 | /// Function for fatal errors.
352 | ///
353 | /// A fatal error is an error, from which the program can no recover, meaning the only option left is to print
354 | /// an error message letting the user know what went wrong. For example if a non-existing file was passed in,
355 | /// this program can not work correctly and should print an error message and exit.
356 | ///
357 | /// This function will print the passed in error message as well as a exit message, then it will exit the program with the exit code.
358 | /// If non is specified, it will use exit code 1 by default.
359 | /// A list of exit code can be found here:
360 | ///
361 | /// # Examples
362 | /// ```no_run
363 | /// use std::fs::File;
364 | ///
365 | /// let f = File::open("hello.txt");
366 | /// let f = match f {
367 | /// Ok(file) => file,
368 | /// Err(error) => fatal_error(&error.to_string(), Some(66)),
369 | /// };
370 | /// ```
371 | pub fn fatal_error(message: &str, code: Option) -> ! {
372 | //This function never returns, since it always exit the program
373 | log::error!("{}", message);
374 | log::error!("Artem exited with code: {}", code.unwrap_or(1));
375 | std::process::exit(code.unwrap_or(1));
376 | }
377 |
--------------------------------------------------------------------------------
/src/pixel.rs:
--------------------------------------------------------------------------------
1 | use image::Rgba;
2 |
3 | use crate::{
4 | config::{self, Config},
5 | target,
6 | };
7 |
8 | /// Convert a pixel block to a char (as a String) from the given density string.
9 | ///
10 | /// # Panics
11 | ///
12 | /// Panics if either the given pixel block or the density is empty.
13 | ///
14 | /// # Examples
15 | ///
16 | /// ```compile_fail, compile will fail, this is an internal example
17 | /// use image::Rgba;
18 | /// use artem::config::TargetType;
19 | ///
20 | /// //example pixels, use them from the directly if possible
21 | /// let pixels = vec![
22 | /// Rgba::::from([255, 255, 255, 255]),
23 | /// Rgba::::from([0, 0, 0, 255]),
24 | /// ];
25 | ///
26 | /// assert_eq!(".", correlating_char(&pixels, "#k. ", false, TargetType::default()));
27 | /// ```
28 | ///
29 | /// To use color, use the `color` argument, if only the background should be colored, use the `on_background_color` arg instead.
30 | ///
31 | /// The `invert` arg, inverts the mapping from pixel luminosity to density string.
32 | pub fn correlating_char(block: &[Rgba], config: &Config) -> String {
33 | assert!(!block.is_empty());
34 | assert!(!config.characters.is_empty());
35 |
36 | let (red, green, blue) = average_color(block);
37 |
38 | //calculate luminosity from avg. pixel color
39 | let luminosity = luminosity(red, green, blue);
40 |
41 | //use chars length to support unicode chars
42 | let length = config.characters.chars().count();
43 |
44 | //swap to range for white to black values
45 | //convert from rgb values (0 - 255) to the density string index (0 - string length)
46 | let density_index = map_range(
47 | (0f32, 255f32),
48 | if config.invert {
49 | (0f32, length as f32)
50 | } else {
51 | (length as f32, 0f32)
52 | },
53 | luminosity,
54 | )
55 | .floor()
56 | .clamp(0f32, length as f32 - 1.0);
57 |
58 | //get correct char from map
59 | assert!((density_index as usize) < length);
60 | let density_char = config
61 | .characters
62 | .chars()
63 | .nth(density_index as usize)
64 | .expect("Failed to get char");
65 |
66 | //return the correctly formatted/colored string depending on the target
67 | match config.target {
68 | //if no color, use default case
69 | config::TargetType::Shell | config::TargetType::AnsiFile | config::TargetType::Svg
70 | if config.color() =>
71 | {
72 | target::ansi::colored_char(red, green, blue, density_char, config.background_color())
73 | }
74 | config::TargetType::HtmlFile => {
75 | if config.color() {
76 | target::html::colored_char(
77 | red,
78 | green,
79 | blue,
80 | density_char,
81 | config.background_color(),
82 | )
83 | } else {
84 | density_char.to_string()
85 | }
86 | }
87 | //all other case, including a plain text file and shell without colors
88 | _ => density_char.to_string(),
89 | }
90 | }
91 |
92 | #[cfg(test)]
93 | mod test_pixel_density {
94 | use std::env;
95 |
96 | use crate::ConfigBuilder;
97 |
98 | use super::*;
99 |
100 | #[test]
101 | fn invert_returns_first_instead_of_last_char() {
102 | let pixels = vec![
103 | Rgba::::from([255, 255, 255, 255]),
104 | Rgba::::from([255, 255, 255, 255]),
105 | Rgba::::from([0, 0, 0, 255]),
106 | ];
107 | let config = ConfigBuilder::new()
108 | .characters("# ".to_owned())
109 | .invert(true)
110 | .color(false)
111 | .build();
112 | assert_eq!(" ", correlating_char(&pixels, &config));
113 | }
114 |
115 | #[test]
116 | fn medium_density_char() {
117 | let pixels = vec![
118 | Rgba::::from([255, 255, 255, 255]),
119 | Rgba::::from([0, 0, 0, 255]),
120 | ];
121 | let config = ConfigBuilder::new()
122 | .characters("#k. ".to_owned())
123 | .color(false)
124 | .build();
125 | assert_eq!("k", correlating_char(&pixels, &config));
126 | }
127 |
128 | #[test]
129 | fn dark_density_char() {
130 | let pixels = vec![
131 | Rgba::::from([255, 255, 255, 255]),
132 | Rgba::::from([255, 255, 255, 255]),
133 | Rgba::::from([0, 0, 0, 255]),
134 | ];
135 | let config = ConfigBuilder::new()
136 | .characters("#k. ".to_owned())
137 | .color(false)
138 | .build();
139 | assert_eq!("#", correlating_char(&pixels, &config));
140 | }
141 |
142 | #[test]
143 | #[ignore = "Requires truecolor support"]
144 | fn colored_char() {
145 | //set needed env vars
146 | env::set_var("COLORTERM", "truecolor");
147 | //force color, this is not printed to the terminal anyways
148 | env::set_var("CLICOLOR_FORCE", "1");
149 |
150 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
151 | let config = ConfigBuilder::new().characters("#k. ".to_owned()).build();
152 | assert_eq!(
153 | "\u{1b}[38;2;0;0;255m \u{1b}[0m", //blue color
154 | correlating_char(&pixels, &config)
155 | );
156 | }
157 |
158 | #[test]
159 | fn ansi_colored_char_shell() {
160 | //set no color support
161 | env::set_var("COLORTERM", "false");
162 | //force color, this is not printed to the terminal anyways
163 | env::set_var("CLICOLOR_FORCE", "1");
164 | //just some random color
165 | let pixels = vec![Rgba::::from([123, 42, 244, 255])];
166 | let config = ConfigBuilder::new().characters("#k. ".to_owned()).build();
167 | assert_eq!("\u{1b}[35m.\u{1b}[0m", correlating_char(&pixels, &config));
168 | }
169 |
170 | #[test]
171 | fn ansi_colored_char_ansi() {
172 | //set no color support
173 | env::set_var("COLORTERM", "false");
174 | //force color, this is not printed to the terminal anyways
175 | env::set_var("CLICOLOR_FORCE", "1");
176 | let pixels = vec![Rgba::::from([123, 42, 244, 255])];
177 | let config = ConfigBuilder::new()
178 | .characters("#k. ".to_owned())
179 | .target(config::TargetType::AnsiFile)
180 | .build();
181 | assert_eq!("\u{1b}[35m.\u{1b}[0m", correlating_char(&pixels, &config));
182 | }
183 |
184 | #[test]
185 | #[ignore = "Requires truecolor support"]
186 | fn colored_background_char_shell() {
187 | //set needed env vars
188 | env::set_var("COLORTERM", "truecolor");
189 | //force color, this is not printed to the terminal anyways
190 | env::set_var("CLICOLOR_FORCE", "1");
191 |
192 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
193 | let config = ConfigBuilder::new()
194 | .characters("#k. ".to_owned())
195 | .background_color(true)
196 | .build();
197 | assert_eq!(
198 | "\u{1b}[48;2;0;0;255m \u{1b}[0m",
199 | correlating_char(&pixels, &config)
200 | );
201 | }
202 |
203 | #[test]
204 | #[ignore = "Requires truecolor support"]
205 | fn colored_background_char_ansi() {
206 | //set needed env vars
207 | env::set_var("COLORTERM", "truecolor");
208 | //force color, this is not printed to the terminal anyways
209 | env::set_var("CLICOLOR_FORCE", "1");
210 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
211 | let config = ConfigBuilder::new()
212 | .characters("#k. ".to_owned())
213 | .target(config::TargetType::AnsiFile)
214 | .background_color(true)
215 | .build();
216 | assert_eq!(
217 | "\u{1b}[48;2;0;0;255m \u{1b}[0m",
218 | correlating_char(&pixels, &config)
219 | );
220 | }
221 |
222 | #[test]
223 | fn target_file_returns_non_colored_string() {
224 | //force color, this is not printed to the terminal anyways
225 | env::set_var("COLORTERM", "truecolor");
226 | env::set_var("CLICOLOR_FORCE", "1");
227 |
228 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
229 | let config = ConfigBuilder::new()
230 | .characters("#k. ".to_owned())
231 | .target(config::TargetType::File)
232 | .build();
233 | assert_eq!(" ", correlating_char(&pixels, &config));
234 | }
235 |
236 | #[test]
237 | fn white_has_no_tag() {
238 | //force color, this is not printed to the terminal anyways
239 | env::set_var("COLORTERM", "truecolor");
240 | env::set_var("CLICOLOR_FORCE", "1");
241 |
242 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
243 | let config = ConfigBuilder::new()
244 | .characters("#k. ".to_owned())
245 | .target(config::TargetType::HtmlFile)
246 | .build();
247 | assert_eq!(" ", correlating_char(&pixels, &config));
248 | }
249 |
250 | #[test]
251 | fn target_html_colored_string() {
252 | //force color, this is not printed to the terminal anyways
253 | env::set_var("COLORTERM", "truecolor");
254 | env::set_var("CLICOLOR_FORCE", "1");
255 |
256 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
257 | let config = ConfigBuilder::new()
258 | .characters("#k:.".to_owned())
259 | .target(config::TargetType::HtmlFile)
260 | .color(true)
261 | .build();
262 | assert_eq!(
263 | ".",
264 | correlating_char(&pixels, &config)
265 | );
266 | }
267 |
268 | #[test]
269 | fn target_html_background_string() {
270 | //force color, this is not printed to the terminal anyways
271 | env::set_var("COLORTERM", "truecolor");
272 | env::set_var("CLICOLOR_FORCE", "1");
273 |
274 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
275 | let config = ConfigBuilder::new()
276 | .characters("#k:. ".to_owned())
277 | .target(config::TargetType::HtmlFile)
278 | .background_color(true)
279 | .build();
280 | assert_eq!(
281 | " ",
282 | correlating_char(&pixels, &config)
283 | );
284 | }
285 |
286 | #[test]
287 | fn target_html_no_color() {
288 | //force color, this is not printed to the terminal anyways
289 | env::set_var("COLORTERM", "truecolor");
290 | env::set_var("CLICOLOR_FORCE", "1");
291 |
292 | let pixels = vec![Rgba::::from([0, 0, 255, 255])];
293 | let config = ConfigBuilder::new()
294 | .characters("#k. ".to_owned())
295 | .target(config::TargetType::HtmlFile)
296 | .color(false)
297 | .build();
298 | assert_eq!(" ", correlating_char(&pixels, &config));
299 | }
300 | }
301 |
302 | ///Remap a value from one range to another.
303 | ///
304 | /// If the value is outside of the specified range, it will still be
305 | /// converted as if it was in the range. This means it could be much larger or smaller than expected.
306 | /// This can be fixed by using the `clamp` function after the remapping.
307 | fn map_range(from_range: (f32, f32), to_range: (f32, f32), value: f32) -> f32 {
308 | to_range.0 + (value - from_range.0) * (to_range.1 - to_range.0) / (from_range.1 - from_range.0)
309 | }
310 |
311 | #[cfg(test)]
312 | mod test_map_range {
313 | use super::*;
314 |
315 | #[test]
316 | fn remap_values() {
317 | //remap 2 to 4
318 | assert_eq!(4f32, map_range((0f32, 10f32), (0f32, 20f32), 2f32));
319 | }
320 |
321 | #[test]
322 | fn remap_values_above_range() {
323 | //remap 21 to 42, since the value will be doubled
324 | assert_eq!(42f32, map_range((0f32, 10f32), (0f32, 20f32), 21f32));
325 | }
326 |
327 | #[test]
328 | fn remap_values_below_range() {
329 | //remap -1 to -2, since the value will be doubled
330 | assert_eq!(-2f32, map_range((0f32, 10f32), (0f32, 20f32), -1f32));
331 | }
332 | }
333 |
334 | /// Returns the average rbg color of multiple pixel.
335 | ///
336 | /// If the input block is empty, all pixels are seen and calculated as if there were black.
337 | ///
338 | /// # Examples
339 | ///
340 | /// ```compile_fail, compile will fail, this is an internal example
341 | /// let pixels: Vec> = Vec::new();
342 | /// assert_eq!((0, 0, 0, 0.0), get_pixel_color_luminosity(&pixels));
343 | /// ```
344 | ///
345 | /// The formula for calculating the rbg colors is based an a minutephysics video
346 | fn average_color(block: &[Rgba]) -> (u8, u8, u8) {
347 | let sum = block
348 | .iter()
349 | .map(|pixel| {
350 | (
351 | pixel.0[0] as f32 * pixel.0[0] as f32,
352 | pixel.0[1] as f32 * pixel.0[1] as f32,
353 | pixel.0[2] as f32 * pixel.0[2] as f32,
354 | )
355 | })
356 | .fold((0f32, 0f32, 0f32), |acc, value| {
357 | (acc.0 + value.0, acc.1 + value.1, acc.2 + value.2)
358 | });
359 | (
360 | (sum.0 / block.len() as f32).sqrt() as u8,
361 | (sum.1 / block.len() as f32).sqrt() as u8,
362 | (sum.2 / block.len() as f32).sqrt() as u8,
363 | )
364 | }
365 |
366 | #[cfg(test)]
367 | mod test_avg_color {
368 | use super::*;
369 |
370 | #[test]
371 | fn red_green() {
372 | let pixels = vec![
373 | Rgba::::from([255, 0, 0, 255]),
374 | Rgba::::from([0, 255, 0, 255]),
375 | ];
376 |
377 | assert_eq!((180, 180, 0), average_color(&pixels));
378 | }
379 |
380 | #[test]
381 | fn green_blue() {
382 | let pixels = vec![
383 | Rgba::::from([0, 255, 0, 255]),
384 | Rgba::::from([0, 0, 255, 255]),
385 | ];
386 |
387 | assert_eq!((0, 180, 180), average_color(&pixels));
388 | }
389 |
390 | #[test]
391 | fn empty_input() {
392 | let pixels: Vec> = Vec::new();
393 | let (r, g, b) = average_color(&pixels);
394 | assert_eq!(0, r);
395 | assert_eq!(0, g);
396 | assert_eq!(0, b);
397 | }
398 | }
399 |
400 | /// Returns the luminosity of the given rgb colors as an float.
401 | ///
402 | /// It converts the rgb values to floats, adds them with weightings and then returns them
403 | /// as a float value.
404 | ///
405 | /// # Examples
406 | ///
407 | /// ```compile_fail, compile will fail, this is an internal example
408 | /// use artem::pixel;
409 | ///
410 | /// let luminosity = luminosity(154, 85, 54);
411 | /// assert_eq!(97f32, luminosity);
412 | /// ```
413 | ///
414 | /// The formula/weighting for the colors comes from
415 | pub fn luminosity(red: u8, green: u8, blue: u8) -> f32 {
416 | (0.21 * red as f32) + (0.72 * green as f32) + (0.07 * blue as f32)
417 | }
418 |
419 | #[cfg(test)]
420 | mod tests {
421 | use super::*;
422 |
423 | #[test]
424 | fn luminosity_black_is_zero() {
425 | assert_eq!(0f32, luminosity(0, 0, 0))
426 | }
427 |
428 | #[test]
429 | fn luminosity_white_is_255() {
430 | assert_eq!(255.00002, luminosity(255, 255, 255))
431 | }
432 |
433 | #[test]
434 | fn luminosity_rust_color_is_255() {
435 | assert_eq!(97.32f32, luminosity(154, 85, 54))
436 | }
437 | }
438 |
--------------------------------------------------------------------------------
/src/target/ansi.rs:
--------------------------------------------------------------------------------
1 | use colored::{ColoredString, Colorize};
2 |
3 | /// Returns an colored string with the given colors.
4 | ///
5 | /// Checks if true_colors are supported, by checking the `COLORTERM` environnement variable,
6 | /// it then returns the given char as a colored string, either using true colors or ansi colors as a fallback.
7 | /// Background colors are only supported when true colors are enabled.
8 | /// # Examples
9 | /// ```compile_fail, compile will fail, this is an internal example
10 | /// println!("{}", get_colored_string(100, 100, 100, 'x', false));
11 | /// ```
12 | pub fn colored_char(red: u8, green: u8, blue: u8, char: char, background_color: bool) -> String {
13 | if *crate::SUPPORTS_TRUECOLOR {
14 | //return true color string
15 | if background_color {
16 | char.to_string().on_truecolor(red, green, blue).to_string()
17 | } else {
18 | char.to_string().truecolor(red, green, blue).to_string()
19 | }
20 | } else {
21 | //otherwise use basic (8 color) ansi color
22 | rgb_to_ansi(&char.to_string(), red, green, blue).to_string()
23 | }
24 | }
25 |
26 | #[cfg(test)]
27 | mod test_colored_string {
28 | use std::env;
29 |
30 | use super::*;
31 |
32 | #[test]
33 | #[ignore = "Requires truecolor support"]
34 | fn rust_color_no_background() {
35 | //ensure that colors will be used
36 | env::set_var("COLORTERM", "truecolor");
37 | env::set_var("CLICOLOR_FORCE", "1");
38 | assert_eq!(
39 | "x".truecolor(154, 85, 54).to_string(),
40 | colored_char(154, 85, 54, 'x', false)
41 | );
42 | }
43 |
44 | #[test]
45 | #[ignore = "Requires truecolor support"]
46 | fn rust_color_with_background() {
47 | //ensure that colors will be used
48 | env::set_var("COLORTERM", "truecolor");
49 | env::set_var("CLICOLOR_FORCE", "1");
50 | assert_eq!(
51 | "x".on_truecolor(154, 85, 54).to_string(),
52 | colored_char(154, 85, 54, 'x', true)
53 | );
54 | }
55 |
56 | #[test]
57 | fn rust_color_ansi_no_background() {
58 | //set true color support to false
59 | env::set_var("COLORTERM", "false");
60 | //ensure that colors will be used
61 | env::set_var("CLICOLOR_FORCE", "1");
62 | assert_eq!(
63 | "\u{1b}[33mx\u{1b}[0m",
64 | colored_char(154, 85, 54, 'x', false)
65 | );
66 | }
67 |
68 | #[test]
69 | fn rust_color_ansi_with_background() {
70 | //set true color support to false
71 | env::set_var("COLORTERM", "false");
72 | //ensure that colors will be used
73 | env::set_var("CLICOLOR_FORCE", "1");
74 | //ansi does not support background, so it is the same as without
75 | assert_eq!("\u{1b}[33mx\u{1b}[0m", colored_char(154, 85, 54, 'x', true));
76 | }
77 | }
78 |
79 | ///Converts the given input string to an ansi colored string
80 | ///
81 | /// It tries to match the ANSI-Color as closely as possible by calculating the distance between all
82 | /// 8 colors and the given input color from `r`, `b` and `b`, then returning the nearest.
83 | /// It will not be 100% accurate, since every terminal has slightly different
84 | /// ANSI-Colors. It used the VGA-Colors as ANSI-Color.
85 | ///
86 | /// # Examples
87 | /// ```compile_fail, compile will fail, this is an internal example
88 | /// //convert black to ansi black color
89 | /// assert_eq!("input".black(), rgb_to_ansi("input", 0, 0, 0));
90 | /// ```
91 | fn rgb_to_ansi(input: &str, r: u8, g: u8, b: u8) -> ColoredString {
92 | //get rgb values and convert them to i32, since later on the could negative when subtracting
93 | let r = r as i32;
94 | let g = g as i32;
95 | let b = b as i32;
96 |
97 | //vga colors as example ansi color
98 | //from https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
99 | let vga_colors = [
100 | [0, 0, 0], //black
101 | [170, 0, 0], //red
102 | [0, 170, 0], //green
103 | [170, 85, 0], //yellow
104 | [0, 0, 170], //blue
105 | [170, 0, 170], //magenta
106 | [0, 170, 170], //cyan
107 | [170, 170, 170], //white
108 | [128, 128, 128], //bright black/gray
109 | [255, 0, 0], //bright red
110 | [0, 255, 0], //bright green
111 | [255, 255, 0], //bright yellow
112 | [0, 0, 255], //bright blue
113 | [255, 0, 255], //bright magenta
114 | [0, 255, 255], //bright cyan
115 | [255, 255, 255], //bright white
116 | ];
117 |
118 | //find nearest color
119 | let mut smallest_distance = i32::MAX;
120 | let mut smallest_distance_index: u8 = 7;
121 | //maybe there is a better method for this
122 | for (index, vga_color) in vga_colors.iter().enumerate() {
123 | let distance =
124 | (r - vga_color[0]).pow(2) + (g - vga_color[1]).pow(2) + (b - vga_color[2]).pow(2);
125 |
126 | if distance < smallest_distance {
127 | smallest_distance = distance;
128 | smallest_distance_index = index as u8;
129 | }
130 | }
131 |
132 | //convert string to matching color
133 | match smallest_distance_index {
134 | 0 => input.black(),
135 | 1 => input.red(),
136 | 2 => input.green(),
137 | 3 => input.yellow(),
138 | 4 => input.blue(),
139 | 5 => input.magenta(),
140 | 6 => input.cyan(),
141 | 7 => input.white(),
142 | 8 => input.bright_black(),
143 | 9 => input.bright_red(),
144 | 10 => input.bright_green(),
145 | 11 => input.bright_yellow(),
146 | 12 => input.bright_blue(),
147 | 13 => input.bright_magenta(),
148 | 14 => input.bright_cyan(),
149 | 15 => input.bright_white(),
150 | _ => input.normal(),
151 | }
152 | }
153 |
154 | #[cfg(test)]
155 | mod test_convert_rgb_ansi {
156 | use super::*;
157 |
158 | #[test]
159 | fn convert_vga_normal_values() {
160 | //convert black to ansi black color
161 | assert_eq!("input".black(), rgb_to_ansi("input", 0, 0, 0));
162 | //convert red to ansi red color
163 | assert_eq!("input".red(), rgb_to_ansi("input", 170, 0, 0));
164 | //convert green to ansi green color
165 | assert_eq!("input".green(), rgb_to_ansi("input", 0, 170, 0));
166 | //convert yellow to ansi yellow color
167 | assert_eq!("input".yellow(), rgb_to_ansi("input", 170, 85, 0));
168 | //convert blue to ansi blue color
169 | assert_eq!("input".blue(), rgb_to_ansi("input", 0, 0, 170));
170 | //convert magenta to ansi magenta color
171 | assert_eq!("input".magenta(), rgb_to_ansi("input", 170, 0, 170));
172 | //convert cyan to ansi cyan color
173 | assert_eq!("input".cyan(), rgb_to_ansi("input", 0, 170, 170));
174 | //convert white to ansi white color
175 | assert_eq!("input".white(), rgb_to_ansi("input", 170, 170, 170));
176 | }
177 |
178 | #[test]
179 | fn convert_vga_bright_values() {
180 | //convert bright black to ansi bright black color
181 | assert_eq!("input".bright_black(), rgb_to_ansi("input", 128, 128, 128));
182 | //convert bright red to ansi bright red color
183 | assert_eq!("input".bright_red(), rgb_to_ansi("input", 255, 0, 0));
184 | //convert bright green to ansi bright green color
185 | assert_eq!("input".bright_green(), rgb_to_ansi("input", 0, 255, 0));
186 | //convert bright yellow to ansi bright yellow color
187 | assert_eq!("input".bright_yellow(), rgb_to_ansi("input", 255, 255, 0));
188 | //convert bright blue to ansi bright blue color
189 | assert_eq!("input".bright_blue(), rgb_to_ansi("input", 0, 0, 255));
190 | //convert bright magenta to ansi bright magenta color
191 | assert_eq!("input".bright_magenta(), rgb_to_ansi("input", 255, 0, 255));
192 | //convert bright cyan to ansi bright cyan color
193 | assert_eq!("input".bright_cyan(), rgb_to_ansi("input", 0, 255, 255));
194 | //convert bright white to ansi bright white color
195 | assert_eq!("input".bright_white(), rgb_to_ansi("input", 255, 255, 255));
196 | }
197 |
198 | #[test]
199 | fn rgb_blue() {
200 | //convert a blue rgb tone to ansi blue
201 | assert_eq!("input".blue(), rgb_to_ansi("input", 0, 0, 88));
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/target/html.rs:
--------------------------------------------------------------------------------
1 | ///Returns the top part of the output html file.
2 | ///
3 | /// This contains the html elements needed for a correct html file.
4 | /// The title will be set to `Artem Ascii Image`.
5 | /// It will also have the pre tag for correct spacing/line breaking
6 | ///
7 | /// # Examples
8 | /// ```compile_fail, compile will fail, this is an internal example
9 | /// use artem::target::html;
10 | ///
11 | /// let string = String::new();
12 | /// string.push_str(&html_top())
13 | /// ```
14 | pub fn html_top() -> String {
15 | r#"
16 |
17 |
18 |
19 |
20 |
21 |
22 | Artem Ascii Image
23 |
24 |
25 |
26 | "#
27 | .to_string()
28 | }
29 |
30 | #[cfg(test)]
31 | mod test_push_html_top {
32 | use super::*;
33 | #[test]
34 | fn push_top_html_returns_correct_string() {
35 | assert_eq!(
36 | r#"
37 |
38 |
39 |
40 |
41 |
42 |
43 | Artem Ascii Image
44 |
45 |
46 |
47 | "#,
48 | html_top()
49 | )
50 | }
51 | }
52 |
53 | ///Returns the bottom part of the output html file.
54 | ///
55 | /// The matching closing tags fro [`html_top`]. It will close
56 | /// the pres, body and html tag.
57 | ///
58 | /// # Examples
59 | /// ```compile_fail, compile will fail, this is an internal example
60 | /// use artem::target::html;
61 | ///
62 | /// let string = String::new();
63 | /// string.push_str(&html_top())
64 | /// string.push_str(&html_bottom())
65 | /// ```
66 | pub fn html_bottom() -> String {
67 | "\n
".to_string()
68 | }
69 |
70 | #[cfg(test)]
71 | mod test_push_html_bottom {
72 | use super::*;
73 |
74 | #[test]
75 | fn push_bottom_html_returns_correct_string() {
76 | assert_eq!("\n
", html_bottom())
77 | }
78 | }
79 |
80 | /// Returns an html string representation of the given char with optional background color support.
81 | ///
82 | /// Creates an element with style attribute, which sets the (background) color to the
83 | /// given rgb inputs.
84 | /// Technically the span can have more than a single char, but the complexity needed for a system to group
85 | /// characters with the same color would be unnecessary and out of scope.
86 | ///
87 | /// # Examples
88 | /// ```compile_fail, compile will fail, this is an internal example
89 | /// println!("{}", get_html(100, 100, 100, 'x', false));
90 | /// ```
91 | pub fn colored_char(red: u8, green: u8, blue: u8, char: char, background_color: bool) -> String {
92 | if background_color {
93 | format!(
94 | "{}",
95 | red, green, blue, char
96 | )
97 | } else if char.is_whitespace() {
98 | //white spaces don't have a visible foreground color,
99 | //it saves space when not having an entire useless span tag
100 | String::from(char)
101 | } else {
102 | format!(
103 | "{}",
104 | red, green, blue, char
105 | )
106 | }
107 | }
108 |
109 | #[cfg(test)]
110 | mod test_html_string {
111 | use super::*;
112 |
113 | #[test]
114 | fn whitespace_no_tag() {
115 | assert_eq!(" ", colored_char(0, 0, 0, ' ', false))
116 | }
117 |
118 | #[test]
119 | fn black_no_background() {
120 | assert_eq!(
121 | "x",
122 | colored_char(0, 0, 0, 'x', false)
123 | )
124 | }
125 |
126 | #[test]
127 | fn black_with_background() {
128 | assert_eq!(
129 | "x",
130 | colored_char(0, 0, 0, 'x', true)
131 | )
132 | }
133 |
134 | #[test]
135 | fn rust_color_no_background() {
136 | assert_eq!(
137 | "x",
138 | colored_char(154, 85, 54, 'x', false)
139 | )
140 | }
141 |
142 | #[test]
143 | fn rust_color_with_background() {
144 | assert_eq!(
145 | "x",
146 | colored_char(154, 85, 54, 'x', true)
147 | )
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/target/mod.rs:
--------------------------------------------------------------------------------
1 | //!This module contains utilities for dealing with different output targets.
2 | //!These include the shell/terminal, plain text files and text files, who support colored output.
3 | //!For example a valid `html` file need to have certain tags, which can be added with
4 | //!methods found in `files::html`
5 |
6 | /// Contains methods for dealing with html files.
7 | /// These can add starting and closing tags.
8 | pub mod html;
9 |
10 | /// Contains methods for converting characters to targets, who support
11 | /// Ansi formatted colors. This includes the shell/terminal as well as `.ans`/`.ansi`
12 | /// files.
13 | pub mod ansi;
14 |
--------------------------------------------------------------------------------
/tests/arguments/characters.rs:
--------------------------------------------------------------------------------
1 | pub mod characters {
2 | use assert_cmd::prelude::*;
3 | use predicates::prelude::*;
4 | use std::process::Command;
5 |
6 | use crate::common::load_correct_file;
7 |
8 | #[test]
9 | fn arg_is_none() {
10 | let mut cmd = Command::cargo_bin("artem").unwrap();
11 |
12 | cmd.arg("assets/images/standard_test_img.png").arg("-c");
13 | cmd.assert().failure().stderr(predicate::str::contains(
14 | "error: a value is required for '--characters ' but none was supplied",
15 | ));
16 | }
17 |
18 | #[test]
19 | fn arg_is_number() {
20 | let mut cmd = Command::cargo_bin("artem").unwrap();
21 | //should panic when trying to convert the arg
22 | cmd.arg("assets/images/standard_test_img.png").arg("-c 0.6");
23 | cmd.assert().success().stdout(predicate::str::starts_with(
24 | "..........0000000000000000000000000000000000.6666666666666666666666666..........",
25 | ));
26 | }
27 |
28 | #[test]
29 | fn arg_is_correct() {
30 | let mut cmd = Command::cargo_bin("artem").unwrap();
31 | cmd.arg("assets/images/standard_test_img.png")
32 | .args(["-c", "M0123-."]);
33 | //only check first line
34 | cmd.assert().success().stdout(predicate::str::starts_with(
35 | "333333333311111111111111111122222222222222223-----------------........3333333333",
36 | ));
37 | }
38 |
39 | #[test]
40 | fn arg_preset_0_short_s() {
41 | for arg in ["short", "s", "0"] {
42 | let mut cmd = Command::cargo_bin("artem").unwrap();
43 | cmd.arg("assets/images/standard_test_img.png")
44 | .args(["-c", arg]);
45 | //only check first line
46 | cmd.assert().success().stdout(predicate::str::starts_with(
47 | "aaaaaaaaaa6666666665555555542222222211111111b:::::::+=========,,,,,,,,aaaaaaaaaa",
48 | ));
49 | }
50 | }
51 |
52 | #[test]
53 | fn arg_preset_1_flat_f() {
54 | for arg in ["flat", "f", "1"] {
55 | let mut cmd = Command::cargo_bin("artem").unwrap();
56 | cmd.arg("assets/images/standard_test_img.png")
57 | .args(["-c", arg]);
58 | //only check first line
59 | cmd.assert()
60 | .success()
61 | .stdout(predicate::str::starts_with(load_correct_file()));
62 | }
63 | }
64 |
65 | #[test]
66 | fn arg_preset_2_long_l() {
67 | for arg in ["long", "l", "2"] {
68 | let mut cmd = Command::cargo_bin("artem").unwrap();
69 | cmd.arg("assets/images/standard_test_img.png")
70 | .args(["-c", arg]);
71 | //only check first line
72 | cmd.assert().success().stdout(predicate::str::starts_with(
73 | r"\\\\\\\\\\ZZZZZZZZOQQQQQQQQJzzzzzzzzuuuuuuuu)++++++++>>>>>>>>i::::::::\\\\\\\\\\",
74 | ));
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/arguments/color.rs:
--------------------------------------------------------------------------------
1 | pub mod invert {
2 | use assert_cmd::prelude::*;
3 | use predicates::prelude::*;
4 | use std::process::Command;
5 |
6 | #[test]
7 | fn arg_with_value() {
8 | let mut cmd = Command::cargo_bin("artem").unwrap();
9 | cmd.arg("assets/images/standard_test_img.png")
10 | .args(["--invert", "123"]);
11 | cmd.assert().failure().stderr(predicate::str::starts_with(
12 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
13 | ));
14 | }
15 |
16 | #[test]
17 | fn arg_is_correct() {
18 | let mut cmd = Command::cargo_bin("artem").unwrap();
19 | cmd.arg("assets/images/standard_test_img.png")
20 | .arg("--invert");
21 | //only check first line
22 | cmd.assert().success().stdout(predicate::str::starts_with(
23 | "dddddddddd'''''''',,,,,,,,,;::::::::ccccccccx00000000KKKKKKKKKNNNNNNNNdddddddddd",
24 | ));
25 | }
26 | }
27 |
28 | pub mod no_color {
29 | use assert_cmd::prelude::*;
30 | use predicates::prelude::*;
31 | use std::process::Command;
32 |
33 | use crate::common::load_correct_file;
34 |
35 | #[test]
36 | fn arg_with_value() {
37 | let mut cmd = Command::cargo_bin("artem").unwrap();
38 | cmd.arg("assets/images/standard_test_img.png")
39 | .args(["--no-color", "123"]);
40 | cmd.assert().failure().stderr(predicate::str::starts_with(
41 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
42 | ));
43 | }
44 |
45 | #[test]
46 | fn arg_conflict_background() {
47 | let mut cmd = Command::cargo_bin("artem").unwrap();
48 | cmd.arg("assets/images/standard_test_img.png")
49 | .args(["--no-color", "--background"]);
50 | cmd.assert().failure().stderr(predicate::str::starts_with(
51 | "error: the argument '--no-color' cannot be used with '--background'",
52 | ));
53 | }
54 |
55 | #[test]
56 | fn arg_is_correct() {
57 | let mut cmd = Command::cargo_bin("artem").unwrap();
58 | cmd.arg("assets/images/standard_test_img.png")
59 | .arg("--no-color");
60 | //only check first line
61 | cmd.assert()
62 | .success()
63 | .stdout(predicate::str::starts_with(load_correct_file()));
64 | }
65 | }
66 |
67 | pub mod background_color {
68 | use assert_cmd::prelude::*;
69 | use predicates::prelude::*;
70 | use std::process::Command;
71 |
72 | use crate::common::load_correct_file;
73 |
74 | #[test]
75 | fn arg_with_value() {
76 | let mut cmd = Command::cargo_bin("artem").unwrap();
77 | cmd.arg("assets/images/standard_test_img.png")
78 | .args(["--background", "123"]);
79 | cmd.assert().failure().stderr(predicate::str::starts_with(
80 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
81 | ));
82 | }
83 |
84 | #[test]
85 | fn arg_conflict_no_color() {
86 | let mut cmd = Command::cargo_bin("artem").unwrap();
87 | cmd.arg("assets/images/standard_test_img.png")
88 | .args(["--background", "--no-color"]);
89 | cmd.assert().failure().stderr(predicate::str::starts_with(
90 | "error: the argument '--background' cannot be used with '--no-color'",
91 | ));
92 | }
93 |
94 | #[test]
95 | fn arg_is_correct() {
96 | let mut cmd = Command::cargo_bin("artem").unwrap();
97 | cmd.arg("assets/images/standard_test_img.png")
98 | .arg("--background");
99 | //only check first line
100 | cmd.assert()
101 | .success()
102 | .stdout(predicate::str::starts_with(load_correct_file()));
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tests/arguments/input.rs:
--------------------------------------------------------------------------------
1 | ///! Test the input argument, including url and file inputs
2 |
3 | pub mod input {
4 | use assert_cmd::prelude::*; // Add methods on commands
5 | use predicates::prelude::*; // Used for writing assertions
6 | use std::process::Command;
7 |
8 | use crate::common::load_correct_file;
9 |
10 | #[test]
11 | fn input_does_not_exist() {
12 | let mut cmd = Command::cargo_bin("artem").unwrap();
13 |
14 | cmd.arg("test/non-existing/file");
15 | cmd.assert()
16 | .failure()
17 | .stderr(predicate::str::contains("does not exist"));
18 | }
19 |
20 | #[test]
21 | fn input_is_dir() {
22 | let mut cmd = Command::cargo_bin("artem").unwrap();
23 |
24 | cmd.arg("test/");
25 | cmd.assert()
26 | .failure()
27 | .stderr(predicate::str::contains("does not exist"));
28 | }
29 |
30 | #[test]
31 | fn correct_input() {
32 | let mut cmd = Command::cargo_bin("artem").unwrap();
33 |
34 | cmd.arg("assets/images/standard_test_img.png");
35 | //check only the first line, the rest is likely to be correct as well
36 | cmd.assert()
37 | .success()
38 | .stdout(predicate::str::starts_with(load_correct_file()));
39 | }
40 |
41 | #[test]
42 | #[cfg(not(feature = "web_image"))]
43 | fn url_disabled_input() {
44 | let mut cmd = Command::cargo_bin("artem").unwrap();
45 |
46 | cmd.arg(
47 | "https://raw.githubusercontent.com/FineFindus/artem/master/assets/images/standard_test_img.png",
48 | );
49 | //check only the first line, the rest is likely to be correct as well
50 | cmd.assert()
51 | .failure()
52 | .stderr(predicate::str::starts_with("[ERROR] File https://raw.githubusercontent.com/FineFindus/artem/master/assets/images/standard_test_img.png does not exist"));
53 | }
54 |
55 | #[test]
56 | #[cfg(not(feature = "web_image"))]
57 | fn help_shows_correct_info_no_url() {
58 | let mut cmd = Command::cargo_bin("artem").unwrap();
59 |
60 | cmd.arg("--help");
61 | cmd.assert().success().stdout(predicate::str::contains(
62 | //only test beginning, since different formatting would break the rest
63 | "Paths to the target image. The original image is NOT altered.",
64 | ));
65 | }
66 |
67 | #[test]
68 | fn multiple_input_is_false() {
69 | let mut cmd = Command::cargo_bin("artem").unwrap();
70 |
71 | cmd.args([
72 | "assets/images/standard_test_img.png",
73 | "examples/non_existing.jpg",
74 | ]);
75 | cmd.assert()
76 | .failure()
77 | .stderr(predicate::str::contains("does not exist"));
78 | }
79 |
80 | #[test]
81 | fn multiple_correct_input() {
82 | let mut cmd = Command::cargo_bin("artem").unwrap();
83 |
84 | cmd.args([
85 | "assets/images/standard_test_img.png",
86 | "assets/images/standard_test_img.png",
87 | ]);
88 |
89 | let mut ascii_img = String::new();
90 | //add img twice, since it was given twice as an input
91 | ascii_img.push_str(&load_correct_file());
92 | ascii_img.push('\n');
93 | ascii_img.push_str(&load_correct_file());
94 | //check only the first line, the rest is likely to be correct as well
95 | cmd.assert()
96 | .success()
97 | .stdout(predicate::str::starts_with(ascii_img));
98 | }
99 | }
100 |
101 | #[cfg(feature = "web_image")]
102 | pub mod url_input {
103 | use assert_cmd::prelude::*; // Add methods on commands
104 | use predicates::prelude::*; // Used for writing assertions
105 | use std::process::Command;
106 |
107 | use crate::common::load_correct_file;
108 |
109 | #[test]
110 | fn input_does_not_exist() {
111 | let mut cmd = Command::cargo_bin("artem").unwrap();
112 |
113 | cmd.arg("https://example.com/no.png");
114 | cmd.assert().failure().stderr(predicate::str::contains(
115 | "[ERROR] Failed to load image bytes from https://example.com/no.png",
116 | ));
117 | }
118 |
119 | #[test]
120 | fn correct_input() {
121 | let mut cmd = Command::cargo_bin("artem").unwrap();
122 |
123 | //use example abraham lincoln image from github repo
124 | cmd.arg(
125 | "https://raw.githubusercontent.com/FineFindus/artem/master/assets/images/standard_test_img.png",
126 | );
127 | //check only the first line, the rest is likely to be correct as well
128 | cmd.assert()
129 | .success()
130 | .stdout(predicate::str::starts_with(load_correct_file()));
131 | }
132 |
133 | #[test]
134 | fn multiple_input_is_false() {
135 | let mut cmd = Command::cargo_bin("artem").unwrap();
136 |
137 | cmd.args([
138 | "https://example.com/no-image.jpg",
139 | "https://example.com/no.png",
140 | ]);
141 | cmd.assert().failure().stderr(predicate::str::contains(
142 | "[ERROR] Failed to load image bytes from https://example.com/no-image.jpg",
143 | ));
144 | }
145 |
146 | #[test]
147 | fn multiple_correct_input() {
148 | let mut cmd = Command::cargo_bin("artem").unwrap();
149 |
150 | cmd.args([
151 | "https://raw.githubusercontent.com/FineFindus/artem/master/assets/images/standard_test_img.png",
152 | "https://raw.githubusercontent.com/FineFindus/artem/master/assets/images/standard_test_img.png",
153 | ]);
154 |
155 | let mut ascii_img = String::new();
156 | //add img twice, since it was given twice as an input
157 | ascii_img.push_str(&load_correct_file());
158 | ascii_img.push('\n');
159 | ascii_img.push_str(&load_correct_file());
160 | //check only the first line, the rest is likely to be correct as well
161 | cmd.assert()
162 | .success()
163 | .stdout(predicate::str::starts_with(ascii_img));
164 | }
165 |
166 | #[test]
167 | #[cfg(feature = "web_image")]
168 | fn help_shows_correct_info() {
169 | let mut cmd = Command::cargo_bin("artem").unwrap();
170 |
171 | cmd.arg("--help");
172 | cmd.assert().success().stdout(predicate::str::contains(
173 | //only test beginning, since different formatting would break the rest
174 | "Paths or URLs to the target image. If the input is an URL, the image is",
175 | ));
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/tests/arguments/mod.rs:
--------------------------------------------------------------------------------
1 | ///! Tests for the different arguments.
2 | ///! Some of the them are bundled into the same file, since they are similar.
3 | ///! For example all color arguments.
4 | pub mod characters;
5 | pub mod color;
6 | pub mod input;
7 | pub mod output;
8 | pub mod scale;
9 | pub mod size;
10 | pub mod transform;
11 |
--------------------------------------------------------------------------------
/tests/arguments/output.rs:
--------------------------------------------------------------------------------
1 | pub mod output_file {
2 | use assert_cmd::prelude::*;
3 | use predicates::prelude::*;
4 | use std::{fs, process::Command};
5 |
6 | #[test]
7 | fn arg_is_none() {
8 | let mut cmd = Command::cargo_bin("artem").unwrap();
9 | cmd.arg("assets/images/standard_test_img.png").arg("-o");
10 | cmd.assert().failure().stderr(predicate::str::starts_with(
11 | "error: a value is required for '--output ' but none was supplied",
12 | ));
13 | }
14 |
15 | #[test]
16 | //windows does not like this test, it can not create the file
17 | #[cfg(not(target_os = "windows"))]
18 | fn file_is_ansi() {
19 | let mut cmd = Command::cargo_bin("artem").unwrap();
20 | cmd.arg("assets/images/standard_test_img.png")
21 | .args(["-o", "/tmp/ascii.ans"]);
22 | //only check first line
23 | cmd.assert().success().stdout(predicate::str::starts_with(
24 | "Written 2105 bytes to /tmp/ascii.ans",
25 | ));
26 | //delete output file
27 | fs::remove_file("/tmp/ascii.ans").unwrap();
28 | }
29 |
30 | #[test]
31 | //windows does not like this test, it can not create the file
32 | #[cfg(not(target_os = "windows"))]
33 | fn file_is_html() {
34 | let mut cmd = Command::cargo_bin("artem").unwrap();
35 | cmd.arg("assets/images/standard_test_img.png")
36 | .args(["-o", "/tmp/ascii.html"]);
37 | //only check first line
38 | cmd.assert().success().stdout(predicate::str::starts_with(
39 | "Written 62626 bytes to /tmp/ascii.html",
40 | ));
41 | //delete output file
42 | fs::remove_file("/tmp/ascii.html").unwrap();
43 | }
44 |
45 | #[test]
46 | //windows does not like this test, it can not create the file
47 | #[cfg(not(target_os = "windows"))]
48 | fn file_plain_text() {
49 | let mut cmd = Command::cargo_bin("artem").unwrap();
50 | cmd.arg("assets/images/standard_test_img.png")
51 | .args(["-o", "/tmp/test.txt"]);
52 | //only check first line
53 | cmd.assert().success().stdout(predicate::str::starts_with(
54 | "Written 2105 bytes to /tmp/test.txt",
55 | ));
56 | //delete output file
57 | fs::remove_file("/tmp/test.txt").unwrap();
58 | }
59 | }
60 |
61 | pub mod verbosity {
62 | use assert_cmd::prelude::*;
63 | use predicates::prelude::*;
64 | use std::process::Command;
65 |
66 | #[test]
67 | fn arg_is_none() {
68 | let mut cmd = Command::cargo_bin("artem").unwrap();
69 | cmd.arg("assets/images/standard_test_img.png")
70 | .arg("--verbose");
71 | cmd.assert().failure().stderr(predicate::str::starts_with(
72 | "error: a value is required for '--verbose ' but none was supplied",
73 | ));
74 | }
75 |
76 | #[test]
77 | fn arg_info() {
78 | let mut cmd = Command::cargo_bin("artem").unwrap();
79 | cmd.arg("assets/images/standard_test_img.png")
80 | .args(["--verbose", "info"]);
81 | //only check first line
82 | cmd.assert()
83 | .success()
84 | .stderr(predicate::str::contains("INFO"));
85 | }
86 |
87 | #[test]
88 | fn arg_debug() {
89 | let mut cmd = Command::cargo_bin("artem").unwrap();
90 | cmd.arg("assets/images/standard_test_img.png")
91 | .args(["--verbose", "debug"]);
92 | //only check first line
93 | cmd.assert()
94 | .success()
95 | .stderr(predicate::str::contains("DEBUG"));
96 | }
97 |
98 | #[test]
99 | fn arg_error() {
100 | let mut cmd = Command::cargo_bin("artem").unwrap();
101 | cmd.arg("examples/abraham_lincoln.nonexisting") //this causes a fatal error
102 | .args(["--verbose", "error"]);
103 | //only check first line
104 | cmd.assert()
105 | .failure()
106 | .stderr(predicate::str::contains("ERROR"));
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tests/arguments/scale.rs:
--------------------------------------------------------------------------------
1 | pub mod scale {
2 | use assert_cmd::prelude::*;
3 | use predicates::prelude::*;
4 | use std::process::Command;
5 |
6 | #[test]
7 | fn arg_is_none() {
8 | let mut cmd = Command::cargo_bin("artem").unwrap();
9 |
10 | cmd.arg("assets/images/standard_test_img.png")
11 | .arg("--ratio");
12 | cmd.assert().failure().stderr(predicate::str::contains(
13 | "error: a value is required for '--ratio ' but none was supplied",
14 | ));
15 | }
16 |
17 | #[test]
18 | fn arg_is_nan() {
19 | let mut cmd = Command::cargo_bin("artem").unwrap();
20 | //should panic when trying to convert the arg
21 | cmd.arg("assets/images/standard_test_img.png")
22 | .args(["--ratio", "string"]);
23 | cmd.assert().failure().stderr(predicate::str::contains(
24 | "error: invalid value 'string' for '--ratio ': invalid float literal",
25 | ));
26 | }
27 |
28 | #[test]
29 | fn arg_is_negative() {
30 | let mut cmd = Command::cargo_bin("artem").unwrap();
31 | //should panic when trying to convert the arg
32 | cmd.arg("assets/images/standard_test_img.png")
33 | .args(["--ratio", "-6"]);
34 | cmd.assert().failure().stderr(predicate::str::starts_with(
35 | "error: unexpected argument '-6' found",
36 | ));
37 | }
38 |
39 | #[test]
40 | fn arg_is_larger_max() {
41 | let mut cmd = Command::cargo_bin("artem").unwrap();
42 | //should panic when trying to convert the arg
43 | cmd.arg("assets/images/standard_test_img.png")
44 | .args(["--ratio", &f64::MAX.to_string()]);
45 | cmd.assert().success().stdout(predicate::str::starts_with(
46 | "::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::",
47 | ));
48 | }
49 |
50 | #[test]
51 | fn arg_is_zero() {
52 | let mut cmd = Command::cargo_bin("artem").unwrap();
53 | //should panic when trying to convert the arg
54 | cmd.arg("assets/images/standard_test_img.png")
55 | .args(["--ratio", "0"]);
56 | cmd.assert().success().stdout(predicate::str::starts_with(
57 | "::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::",
58 | ));
59 | }
60 |
61 | #[test]
62 | fn arg_is_correct() {
63 | let mut cmd = Command::cargo_bin("artem").unwrap();
64 | cmd.arg("assets/images/standard_test_img.png")
65 | .args(["--ratio", "0.75"]);
66 | //only check first line
67 | cmd.assert().success().stdout(predicate::str::starts_with(
68 | "::::::::::OOOOOOOOkkkkkkkkkxddddddddoooooooo;................. ::::::::::",
69 | ));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/arguments/size.rs:
--------------------------------------------------------------------------------
1 | pub mod size {
2 | use assert_cmd::prelude::*;
3 | use predicates::prelude::*;
4 | use std::process::Command;
5 |
6 | #[test]
7 | fn arg_is_none() {
8 | let mut cmd = Command::cargo_bin("artem").unwrap();
9 |
10 | cmd.arg("assets/images/standard_test_img.png").arg("-s");
11 | cmd.assert().failure().stderr(predicate::str::contains(
12 | "error: a value is required for '--size ' but none was supplied",
13 | ));
14 | }
15 |
16 | #[test]
17 | fn arg_is_nan() {
18 | let mut cmd = Command::cargo_bin("artem").unwrap();
19 | //should panic when trying to convert the arg
20 | cmd.arg("assets/images/standard_test_img.png")
21 | .arg("-s string");
22 | cmd.assert().failure().stderr(predicate::str::contains(
23 | "error: invalid value ' string' for '--size ': invalid digit found in string",
24 | ));
25 | }
26 |
27 | #[test]
28 | fn arg_is_float() {
29 | let mut cmd = Command::cargo_bin("artem").unwrap();
30 | //should panic when trying to convert the arg
31 | cmd.arg("assets/images/standard_test_img.png").arg("-s 0.6");
32 | cmd.assert().failure().stderr(predicate::str::contains(
33 | "error: invalid value ' 0.6' for '--size ': invalid digit found in string",
34 | ));
35 | }
36 |
37 | #[test]
38 | fn arg_is_negative() {
39 | let mut cmd = Command::cargo_bin("artem").unwrap();
40 | //should panic when trying to convert the arg
41 | cmd.arg("assets/images/standard_test_img.png").arg("-s -6");
42 | cmd.assert().failure().stderr(predicate::str::contains(
43 | "error: invalid value ' -6' for '--size ': invalid digit found in string",
44 | ));
45 | }
46 |
47 | #[test]
48 | fn arg_is_larger_max() {
49 | let mut cmd = Command::cargo_bin("artem").unwrap();
50 | //should panic when trying to convert the arg
51 | cmd.arg("assets/images/standard_test_img.png")
52 | .arg(format!("-s {}", u32::MAX));
53 | cmd.assert().failure().stderr(predicate::str::contains(
54 | "error: invalid value ' 4294967295' for '--size ': invalid digit found in string",
55 | ));
56 | }
57 |
58 | #[test]
59 | fn arg_conflict_width() {
60 | let mut cmd = Command::cargo_bin("artem").unwrap();
61 | //should panic when trying using both args
62 | cmd.arg("assets/images/standard_test_img.png")
63 | .args(["-s", "75"])
64 | .arg("-w");
65 | cmd.assert().failure().stderr(predicate::str::contains(
66 | "error: the argument '--size ' cannot be used with '--width'",
67 | ));
68 | }
69 |
70 | #[test]
71 | fn arg_conflict_height() {
72 | let mut cmd = Command::cargo_bin("artem").unwrap();
73 | //should panic when trying using both args
74 | cmd.arg("assets/images/standard_test_img.png")
75 | .args(["-s", "75"])
76 | .arg("--height");
77 | cmd.assert().failure().stderr(predicate::str::contains(
78 | "error: the argument '--size ' cannot be used with '--height'",
79 | ));
80 | }
81 |
82 | #[test]
83 | fn arg_is_correct() {
84 | let mut cmd = Command::cargo_bin("artem").unwrap();
85 | cmd.arg("assets/images/standard_test_img.png")
86 | .args(["-s", "75"]);
87 | //only check first line
88 | cmd.assert().success().stdout(predicate::str::starts_with(
89 | ":::::::::dOOOOOOOkkkkkkkkxdddddddoooooooo:................ ':::::::::",
90 | ));
91 | }
92 | }
93 |
94 | pub mod width {
95 | use assert_cmd::prelude::*;
96 | use predicates::prelude::*;
97 | use std::process::Command;
98 |
99 | #[test]
100 | fn arg_with_value() {
101 | let mut cmd = Command::cargo_bin("artem").unwrap();
102 | cmd.arg("assets/images/standard_test_img.png")
103 | .args(["-w", "123"]);
104 | cmd.assert().failure().stderr(predicate::str::starts_with(
105 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
106 | ));
107 | }
108 |
109 | #[test]
110 | fn arg_conflict_size() {
111 | let mut cmd = Command::cargo_bin("artem").unwrap();
112 | cmd.arg("assets/images/standard_test_img.png")
113 | .arg("-w")
114 | .args(["-s", "75"]);
115 | //should panic when trying using both args
116 | cmd.assert().failure().stderr(predicate::str::contains(
117 | "error: the argument '--width' cannot be used with '--size '",
118 | ));
119 | }
120 |
121 | #[test]
122 | fn arg_conflict_height() {
123 | let mut cmd = Command::cargo_bin("artem").unwrap();
124 | //should panic when trying using both args
125 | cmd.arg("assets/images/standard_test_img.png")
126 | .arg("-w")
127 | .arg("--height");
128 | cmd.assert().failure().stderr(predicate::str::contains(
129 | "error: the argument '--width' cannot be used with '--height'",
130 | ));
131 | }
132 |
133 | #[test]
134 | #[should_panic]
135 | fn arg_is_correct() {
136 | let mut cmd = Command::cargo_bin("artem").unwrap();
137 | cmd.arg("assets/images/standard_test_img.png")
138 | .arg("--width");
139 | //should panic in the test case, since the terminal size is 0
140 | cmd.assert().success().stdout(predicate::str::starts_with(
141 | "WWWNNNNNNXXXXXXKXXXKK0000OO000OOOOOOOOOOOkkkkkkOkkkkkkxxxxxkkOOOkOO0000KKKKKKKXX",
142 | ));
143 | }
144 | }
145 |
146 | pub mod height {
147 | use assert_cmd::prelude::*;
148 | use predicates::prelude::*;
149 | use std::process::Command;
150 |
151 | #[test]
152 | fn arg_with_value() {
153 | let mut cmd = Command::cargo_bin("artem").unwrap();
154 | cmd.arg("assets/images/standard_test_img.png")
155 | .args(["--height", "123"]);
156 | cmd.assert().failure().stderr(predicate::str::starts_with(
157 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
158 | ));
159 | }
160 |
161 | #[test]
162 | fn arg_conflict_size() {
163 | let mut cmd = Command::cargo_bin("artem").unwrap();
164 | cmd.arg("assets/images/standard_test_img.png")
165 | .arg("--height")
166 | .args(["-s", "75"]);
167 | //should panic when trying using both args
168 | cmd.assert().failure().stderr(predicate::str::contains(
169 | "error: the argument '--height' cannot be used with '--size '",
170 | ));
171 | }
172 |
173 | #[test]
174 | fn arg_conflict_height() {
175 | let mut cmd = Command::cargo_bin("artem").unwrap();
176 | //should panic when trying using both args
177 | cmd.arg("assets/images/standard_test_img.png")
178 | .arg("--height")
179 | .arg("-w");
180 | cmd.assert().failure().stderr(predicate::str::contains(
181 | "error: the argument '--height' cannot be used with '--width'",
182 | ));
183 | }
184 |
185 | #[test]
186 | #[should_panic]
187 | fn arg_is_correct() {
188 | let mut cmd = Command::cargo_bin("artem").unwrap();
189 | cmd.arg("assets/images/standard_test_img.png")
190 | .arg("--height");
191 | //should panic in the test case, since the terminal size is 0
192 | cmd.assert().success().stdout(predicate::str::starts_with(
193 | "WWWNNNNNNXXXXXXKXXXKK0000OO000OOOOOOOOOOOkkkkkkOkkkkkkxxxxxkkOOOkOO0000KKKKKKKXX",
194 | ));
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/tests/arguments/transform.rs:
--------------------------------------------------------------------------------
1 | pub mod flip_x {
2 | use assert_cmd::prelude::*;
3 | use predicates::prelude::*;
4 | use std::process::Command;
5 |
6 | #[test]
7 | fn arg_with_value() {
8 | let mut cmd = Command::cargo_bin("artem").unwrap();
9 | cmd.arg("assets/images/standard_test_img.png")
10 | .args(["--flipX", "123"]);
11 | cmd.assert().failure().stderr(predicate::str::starts_with(
12 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
13 | ));
14 | }
15 |
16 | #[test]
17 | fn arg_is_correct() {
18 | let mut cmd = Command::cargo_bin("artem").unwrap();
19 | cmd.arg("assets/images/standard_test_img.png")
20 | .arg("--flipX");
21 | //only check first line
22 | cmd.assert().success().stdout(predicate::str::starts_with(
23 | ":::::::::: .................;ooooooooddddddddxkkkkkkkkkOOOOOOOO::::::::::",
24 | ));
25 | }
26 | }
27 |
28 | pub mod flip_y {
29 | use assert_cmd::prelude::*;
30 | use predicates::prelude::*;
31 | use std::process::Command;
32 |
33 | #[test]
34 | fn arg_with_value() {
35 | let mut cmd = Command::cargo_bin("artem").unwrap();
36 | cmd.arg("assets/images/standard_test_img.png")
37 | .args(["--flipY", "123"]);
38 | cmd.assert().failure().stderr(predicate::str::starts_with(
39 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
40 | ));
41 | }
42 |
43 | #[test]
44 | fn arg_is_correct() {
45 | let mut cmd = Command::cargo_bin("artem").unwrap();
46 | cmd.arg("assets/images/standard_test_img.png")
47 | .arg("--flipY");
48 | //only check first line
49 | cmd.assert().success().stdout(predicate::str::starts_with(
50 | ".......... cWWWWWWWWWWWWWWWWW ..........",
51 | ));
52 | }
53 | }
54 |
55 | pub mod flip_x_y {
56 | use assert_cmd::prelude::*;
57 | use predicates::prelude::*;
58 | use std::process::Command;
59 |
60 | #[test]
61 | fn arg_is_correct() {
62 | let mut cmd = Command::cargo_bin("artem").unwrap();
63 | cmd.arg("assets/images/standard_test_img.png")
64 | .args(["--flipY", "--flipX"]);
65 | //only check first line
66 | cmd.assert().success().stdout(predicate::str::starts_with(
67 | ".......... WWWWWWWWWWWWWWWWWc ..........",
68 | ));
69 | }
70 | }
71 |
72 | pub mod outline {
73 | use assert_cmd::prelude::*;
74 | use predicates::prelude::*;
75 | use std::process::Command;
76 |
77 | #[test]
78 | fn arg_with_value() {
79 | let mut cmd = Command::cargo_bin("artem").unwrap();
80 | cmd.arg("assets/images/standard_test_img.png")
81 | .args(["--outline", "123"]);
82 | cmd.assert().failure().stderr(predicate::str::starts_with(
83 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
84 | ));
85 | }
86 |
87 | #[test]
88 | fn arg_is_correct() {
89 | let mut cmd = Command::cargo_bin("artem").unwrap();
90 | cmd.arg("assets/images/standard_test_img.png")
91 | .arg("--outline");
92 | //only check first line
93 | cmd.assert().success().stdout(predicate::str::starts_with(
94 | " ll . : . ;x . : ll ",
95 | ));
96 | }
97 | }
98 |
99 | pub mod hysteresis {
100 | use assert_cmd::prelude::*;
101 | use predicates::prelude::*;
102 | use std::process::Command;
103 |
104 | #[test]
105 | fn outline_is_required() {
106 | let mut cmd = Command::cargo_bin("artem").unwrap();
107 | cmd.arg("assets/images/standard_test_img.png")
108 | .arg("--hysteresis");
109 | cmd.assert()
110 | .failure()
111 | .stderr(predicate::str::starts_with(
112 | "error: the following required arguments were not provided:",
113 | ))
114 | .stderr(predicate::str::contains("--outline"));
115 | }
116 |
117 | #[test]
118 | fn arg_with_value() {
119 | let mut cmd = Command::cargo_bin("artem").unwrap();
120 | cmd.arg("assets/images/standard_test_img.png")
121 | .args(["--outline", "--hysteresis", "123"]);
122 | cmd.assert().failure().stderr(predicate::str::starts_with(
123 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
124 | ));
125 | }
126 |
127 | #[test]
128 | fn arg_is_correct() {
129 | let mut cmd = Command::cargo_bin("artem").unwrap();
130 | cmd.arg("assets/images/standard_test_img.png")
131 | .args(["--outline", "--hys"]);
132 | //only check first line
133 | cmd.assert().success().stdout(predicate::str::starts_with(
134 | " ll O ;x O ll ",
135 | ));
136 | }
137 | }
138 |
139 | pub mod border {
140 | use assert_cmd::prelude::*;
141 | use predicates::prelude::*;
142 | use std::process::Command;
143 |
144 | #[test]
145 | fn arg_with_value() {
146 | let mut cmd = Command::cargo_bin("artem").unwrap();
147 | cmd.arg("assets/images/standard_test_img.png")
148 | .args(["--border", "123"]);
149 | cmd.assert().failure().stderr(predicate::str::starts_with(
150 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
151 | ));
152 | }
153 |
154 | #[test]
155 | fn arg_is_correct() {
156 | let mut cmd = Command::cargo_bin("artem").unwrap();
157 | cmd.arg("assets/images/standard_test_img.png")
158 | .arg("--border");
159 | //only check first line
160 | cmd.assert()
161 | .success().stdout(predicate::str::starts_with(
162 | "╔══════════════════════════════════════════════════════════════════════════════╗",
163 | ))
164 | .success().stdout(predicate::str::ends_with(
165 | "╚══════════════════════════════════════════════════════════════════════════════╝\n",
166 | ));
167 | }
168 | }
169 |
170 | pub mod center_x {
171 | use assert_cmd::prelude::*;
172 | use predicates::prelude::*;
173 | use std::process::Command;
174 |
175 | #[test]
176 | fn arg_with_value() {
177 | let mut cmd = Command::cargo_bin("artem").unwrap();
178 | cmd.arg("assets/images/standard_test_img.png")
179 | .args(["--centerX", "123"]);
180 | cmd.assert().failure().stderr(predicate::str::starts_with(
181 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
182 | ));
183 | }
184 |
185 | #[test]
186 | fn arg_is_correct() {
187 | let mut cmd = Command::cargo_bin("artem").unwrap();
188 | cmd.arg("assets/images/standard_test_img.png")
189 | .arg("--centerX");
190 |
191 | //this is more or less a placeholder test, since the terminal size can and will be different during tests
192 | cmd.assert()
193 | .success()
194 | .stdout(predicate::str::contains(" "));
195 | }
196 | }
197 |
198 | pub mod center_y {
199 | use assert_cmd::prelude::*;
200 | use predicates::prelude::*;
201 | use std::process::Command;
202 |
203 | #[test]
204 | fn arg_with_value() {
205 | let mut cmd = Command::cargo_bin("artem").unwrap();
206 | cmd.arg("assets/images/standard_test_img.png")
207 | .args(["--centerY", "123"]);
208 | cmd.assert().failure().stderr(predicate::str::starts_with(
209 | "[ERROR] File 123 does not exist\n[ERROR] Artem exited with code: 66\n",
210 | ));
211 | }
212 |
213 | #[test]
214 | fn arg_is_correct() {
215 | let mut cmd = Command::cargo_bin("artem").unwrap();
216 | cmd.arg("assets/images/standard_test_img.png")
217 | .arg("--centerY");
218 |
219 | //this is more or less a placeholder test, since the terminal size can and will be different during tests thus
220 | //affecting the size. ANd the command output will be trimmed, so any spacing will be lost
221 | cmd.assert()
222 | .success()
223 | .stdout(predicate::str::contains("\n"));
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/tests/arguments_test.rs:
--------------------------------------------------------------------------------
1 | ///Test all arguments
2 | mod arguments;
3 | mod common;
4 |
--------------------------------------------------------------------------------
/tests/common/mod.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | ///! Utilities and common function between tests.
3 | ///! It includes functions to help loading expected results to compare against.
4 |
5 | /// Load the correct files.
6 | ///
7 | /// Loads a string containing the correct and expected result of the command output.
8 | /// The returned String does not have color.
9 | pub fn load_correct_file() -> String {
10 | //ignore errors
11 | fs::read_to_string("assets/standard_test_img/standard_test_img.txt").unwrap()
12 | }
13 |
--------------------------------------------------------------------------------
/tests/integration_tests.rs:
--------------------------------------------------------------------------------
1 | use assert_cmd::prelude::*; // Add methods on commands
2 | use predicates::prelude::*;
3 | use std::fs::{self};
4 | // Used for writing assertions
5 | use pretty_assertions::assert_str_eq;
6 | use std::process::Command; // Run programs
7 |
8 | #[test]
9 | fn full_file_compare_no_args() {
10 | let mut cmd = Command::cargo_bin("artem").unwrap();
11 |
12 | cmd.arg("assets/images/standard_test_img.png");
13 |
14 | //load file contents to compare
15 | let desired_output =
16 | fs::read_to_string("assets/standard_test_img/standard_test_img.txt").unwrap(); //ignore errors
17 | cmd.assert()
18 | .success()
19 | .stdout(predicate::str::contains(desired_output));
20 | }
21 |
22 | #[test]
23 | #[cfg(feature = "web_image")]
24 | fn full_file_compare_url() {
25 | let mut cmd = Command::cargo_bin("artem").unwrap();
26 |
27 | cmd.arg(
28 | "https://raw.githubusercontent.com/FineFindus/artem/master/assets/images/standard_test_img.png",
29 | );
30 |
31 | //load file contents to compare
32 | let desired_output =
33 | fs::read_to_string("assets/standard_test_img/standard_test_img.txt").unwrap(); //ignore errors
34 | cmd.assert()
35 | .success()
36 | .stdout(predicate::str::contains(desired_output));
37 | }
38 |
39 | #[test]
40 | fn full_file_compare_border() {
41 | let mut cmd = Command::cargo_bin("artem").unwrap();
42 |
43 | cmd.arg("assets/images/standard_test_img.png")
44 | .arg("--border");
45 |
46 | //load file contents to compare
47 | let desired_output =
48 | fs::read_to_string("assets/standard_test_img/standard_test_img_border.txt").unwrap(); //ignore errors
49 | cmd.assert()
50 | .success()
51 | .stdout(predicate::str::contains(desired_output));
52 | }
53 |
54 | #[test]
55 | fn full_file_compare_outline() {
56 | let mut cmd = Command::cargo_bin("artem").unwrap();
57 |
58 | //this example image is not the best case for the outline, since its already grayscale, and the person is a lot darker than the background
59 | cmd.arg("assets/images/standard_test_img.png")
60 | .arg("--outline");
61 |
62 | //load file contents to compare
63 | let desired_output =
64 | fs::read_to_string("assets/standard_test_img/standard_test_img_outline.txt").unwrap(); //ignore errors
65 | cmd.assert()
66 | .success()
67 | .stdout(predicate::str::contains(desired_output));
68 | }
69 |
70 | #[test]
71 | fn full_file_compare_border_outline() {
72 | let mut cmd = Command::cargo_bin("artem").unwrap();
73 |
74 | //this example image is not the best case for the outline, since its already grayscale, and the person is a lot darker than the background
75 | cmd.arg("assets/images/standard_test_img.png")
76 | .arg("--outline")
77 | .arg("--border");
78 |
79 | //load file contents to compare
80 | let desired_output =
81 | fs::read_to_string("assets/standard_test_img/standard_test_img_border_outline.txt")
82 | .unwrap(); //ignore errors
83 | cmd.assert()
84 | .success()
85 | .stdout(predicate::str::contains(desired_output));
86 | }
87 |
88 | #[test]
89 | fn full_file_compare_outline_hysteresis() {
90 | let mut cmd = Command::cargo_bin("artem").unwrap();
91 |
92 | //this example image is not the best case for the outline, since its already grayscale, and the person is a lot darker than the background
93 | cmd.arg("assets/images/standard_test_img.png")
94 | .args(["--outline", "--hysteresis"]);
95 |
96 | //load file contents to compare
97 | let desired_output =
98 | fs::read_to_string("assets/standard_test_img/standard_test_img_outline_hysteresis.txt")
99 | .unwrap(); //ignore errors
100 | cmd.assert()
101 | .success()
102 | .stdout(predicate::str::contains(desired_output));
103 | }
104 |
105 | #[test]
106 | #[cfg(not(target_os = "windows"))]
107 | fn full_file_compare_html() {
108 | let mut cmd = Command::cargo_bin("artem").unwrap();
109 |
110 | cmd.arg("assets/images/standard_test_img.png")
111 | .args(["-o", "/tmp/ascii.html"]);
112 |
113 | //load file contents to compare
114 | let desired_output =
115 | fs::read_to_string("assets/standard_test_img/standard_test_img.html").unwrap(); //ignore errors
116 | cmd.assert().success().stdout(predicate::str::contains(
117 | "Written 62626 bytes to /tmp/ascii.html",
118 | ));
119 |
120 | let file_output = fs::read_to_string("/tmp/ascii.html").unwrap(); //ignore errors
121 |
122 | //delete output file
123 | fs::remove_file("/tmp/ascii.html").unwrap();
124 |
125 | assert_str_eq!(desired_output, file_output);
126 | }
127 |
128 | #[test]
129 | #[cfg(not(target_os = "windows"))]
130 | fn full_file_compare_html_border() {
131 | let mut cmd = Command::cargo_bin("artem").unwrap();
132 |
133 | cmd.arg("assets/images/standard_test_img.png")
134 | .args(["-o", "/tmp/ascii.html", "--border"]);
135 |
136 | //load file contents to compare
137 | let desired_output =
138 | fs::read_to_string("assets/standard_test_img/standard_test_img_border.html").unwrap(); //ignore errors
139 | cmd.assert().success().stdout(predicate::str::contains(
140 | "Written 61663 bytes to /tmp/ascii.html",
141 | ));
142 |
143 | let file_output = fs::read_to_string("/tmp/ascii.html").unwrap(); //ignore errors
144 |
145 | //delete output file
146 | fs::remove_file("/tmp/ascii.html").unwrap();
147 |
148 | assert_str_eq!(desired_output, file_output);
149 | }
150 |
151 | #[test]
152 | #[cfg(not(target_os = "windows"))]
153 | fn full_file_compare_html_outline() {
154 | let mut cmd = Command::cargo_bin("artem").unwrap();
155 |
156 | cmd.arg("assets/images/standard_test_img.png")
157 | .args(["-o", "/tmp/ascii.html", "--outline"]);
158 |
159 | //load file contents to compare
160 | let desired_output =
161 | fs::read_to_string("assets/standard_test_img/standard_test_img_outline.html").unwrap(); //ignore errors
162 | cmd.assert().success().stdout(predicate::str::contains(
163 | "Written 19786 bytes to /tmp/ascii.html",
164 | ));
165 |
166 | let file_output = fs::read_to_string("/tmp/ascii.html").unwrap(); //ignore errors
167 |
168 | //delete output file
169 | fs::remove_file("/tmp/ascii.html").unwrap();
170 |
171 | assert_str_eq!(desired_output, file_output);
172 | }
173 |
174 | #[test]
175 | #[cfg(not(target_os = "windows"))]
176 | fn full_file_compare_html_background_color() {
177 | let mut cmd = Command::cargo_bin("artem").unwrap();
178 |
179 | cmd.arg("assets/images/standard_test_img.png")
180 | .args(["-o", "/tmp/ascii.html", "--background"]);
181 |
182 | //load file contents to compare
183 | let desired_output =
184 | fs::read_to_string("assets/standard_test_img/standard_test_img_background.html").unwrap(); //ignore errors
185 | cmd.assert().success().stdout(predicate::str::contains(
186 | "Written 100194 bytes to /tmp/ascii.html",
187 | ));
188 |
189 | let file_output = fs::read_to_string("/tmp/ascii.html").unwrap(); //ignore errors
190 |
191 | //delete output file
192 | fs::remove_file("/tmp/ascii.html").unwrap();
193 |
194 | assert_str_eq!(desired_output, file_output);
195 | }
196 |
--------------------------------------------------------------------------------