├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── continuous_integration.yaml │ └── release_build.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── RELEASE-CHECKLIST.md ├── assets ├── aur │ ├── PKGBUILD-bin │ ├── PKGBUILD-stable │ └── update_PKGBUILD.sh ├── images │ ├── abraham_lincoln.jpg │ ├── moth.jpg │ ├── radio_tower.jpg │ └── standard_test_img.png ├── standard_test_img │ ├── standard_test_img.html │ ├── standard_test_img.txt │ ├── standard_test_img_background.html │ ├── standard_test_img_border.html │ ├── standard_test_img_border.txt │ ├── standard_test_img_border_outline.html │ ├── standard_test_img_border_outline.txt │ ├── standard_test_img_outline.html │ ├── standard_test_img_outline.txt │ ├── standard_test_img_outline_hysteresis.html │ └── standard_test_img_outline_hysteresis.txt └── update_tests.sh ├── benches ├── artem_bench.rs └── benchmarks │ ├── default.rs │ ├── hysteresis.rs │ ├── mod.rs │ ├── outline.rs │ ├── size.rs │ └── util.rs ├── build.rs ├── examples ├── abraham_lincoln.jpg └── abraham_lincoln_ascii.png ├── src ├── cli.rs ├── config.rs ├── filter │ └── mod.rs ├── lib.rs ├── main.rs ├── pixel.rs └── target │ ├── ansi.rs │ ├── html.rs │ └── mod.rs └── tests ├── arguments ├── characters.rs ├── color.rs ├── input.rs ├── mod.rs ├── output.rs ├── scale.rs ├── size.rs └── transform.rs ├── arguments_test.rs ├── common └── mod.rs └── integration_tests.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Use command with '...' 17 | 2. Scroll down to '....' 18 | 3. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Information** 27 | 28 | - OS: [e.g. Ubuntu 20.10] 29 | - Shell: [e.g. Bash, PowerShell] 30 | - Version: [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | 35 | **Output** 36 | 37 |
38 | Output 39 | 40 | 42 | 43 | ``` 44 | 45 | ``` 46 | 47 |
48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | **Information** 23 | 24 | - OS: [e.g. Ubuntu 20.10] 25 | - Shell: [e.g. Bash, PowerShell] 26 | - Version: [e.g. 22] 27 | -------------------------------------------------------------------------------- /.github/workflows/continuous_integration.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | pull_request: 5 | 6 | name: Continuous Integration 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check: 14 | name: Check 15 | #format does not need to run on all platforms 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 15 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v4 21 | - name: Install stable toolchain 22 | uses: dtolnay/rust-toolchain@stable 23 | - name: Run check 24 | run: cargo check 25 | 26 | clippy: 27 | name: Clippy 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 15 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v4 33 | - name: Install stable toolchain 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy 37 | - name: Run clippy 38 | run: cargo clippy -- -Dwarnigns 39 | 40 | test: 41 | name: Test Suite 42 | strategy: 43 | matrix: 44 | platform: [ubuntu-latest] # test only on ubuntu, testing on every platform would take too much time and fail too often due to a few flaky test 45 | rust: 46 | - 1.82.0 # MSRV 47 | - stable 48 | - beta 49 | - nightly 50 | #tests should pass on all platforms 51 | runs-on: ${{ matrix.platform }} 52 | timeout-minutes: 25 53 | steps: 54 | - name: Checkout sources 55 | uses: actions/checkout@v4 56 | 57 | - name: Install ${{ matrix.rust }} toolchain 58 | uses: dtolnay/rust-toolchain@stable 59 | with: 60 | toolchain: ${{ matrix.rust }} 61 | 62 | - name: Build artem 63 | continue-on-error: false 64 | run: cargo build 65 | 66 | - name: Run tests 67 | if: contains(${{ matrix.target }}, "x86_64") #arm build can not run tests 68 | continue-on-error: false 69 | run: cargo test --locked --verbose 70 | -------------------------------------------------------------------------------- /.github/workflows/release_build.yaml: -------------------------------------------------------------------------------- 1 | # Reference: 2 | # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ 3 | # https://github.com/sharkdp/bat/blob/master/.github/workflows/CICD.yml 4 | # https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml 5 | 6 | # This workflow creates all the needed binaries, uploads the binaries to github releases and updates the crate as well as the 7 | # homebrew tap. 8 | # The binaries include versions from linux x86_64 and arm, windows gnu and mscv and macos x86_64 and arm versions. 9 | # It also creates a .deb package using the cargo-deb crate. 10 | 11 | name: Build Release files 12 | on: 13 | #switch for debugging 14 | # [push] 15 | release: 16 | types: [published] 17 | 18 | jobs: 19 | release_assets_linux: 20 | name: Release Linux Assets 21 | #create and release different linux builds for arm and x86 22 | runs-on: ${{ matrix.platform }} 23 | strategy: 24 | matrix: 25 | platform: [ubuntu-latest] 26 | rust: 27 | - stable 28 | target: 29 | #compile for x86 and armv8 (64-Bit) 30 | - x86_64-unknown-linux-gnu 31 | - aarch64-unknown-linux-gnu 32 | #compile with musl 33 | - x86_64-unknown-linux-musl 34 | - aarch64-unknown-linux-musl 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v4 38 | 39 | - name: Install ${{ matrix.rust }} toolchain 40 | uses: dtolnay/rust-toolchain@master 41 | with: 42 | toolchain: ${{ matrix.rust }} 43 | 44 | - name: Install cross release 45 | run: cargo install cross 46 | 47 | - name: Build artem release 48 | continue-on-error: false 49 | run: cross build --locked --release --verbose --target=${{ matrix.target }} 50 | 51 | #from: https://github.com/sharkdp/bat/blob/master/.github/workflows/CICD.yml 52 | - name: Extract Version Number 53 | shell: bash 54 | run: echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV 55 | 56 | - name: Copy completion files and man page 57 | shell: bash 58 | run: | 59 | #create a completions and doc folder 60 | mkdir -p deployment/completions 61 | mkdir -p deployment/doc 62 | #copy completion files and man page 63 | cp -u target/*/release/build/artem-*/out/* deployment/completions/ 64 | # replace the input file completions, since it won't work after inputting the paths otherwise 65 | sed -i "s/*::/*:/" deployment/completions/_artem 66 | #move man page and CHANGELOG/README to doc folder 67 | mv deployment/completions/artem.1 deployment/doc/ 68 | mv CHANGELOG.md deployment/doc/ 69 | mv README.md deployment/ 70 | #copy binary file 71 | cp -u target/*/release/artem deployment 72 | 73 | - name: Compress release files 74 | shell: bash 75 | run: | 76 | #compress deployment directory 77 | cd deployment/ 78 | tar -czvf ../artem-v$PROJECT_VERSION-${{ matrix.target }}.tar.gz * 79 | 80 | - name: Upload Release Build 81 | uses: softprops/action-gh-release@v1 82 | with: 83 | files: artem-v${{ env.PROJECT_VERSION }}-${{ matrix.target }}.tar.gz 84 | draft: true 85 | prerelease: true 86 | 87 | release_assets_windows: 88 | #this is only to release the windows build, since it has a .exe extensions will be uploaded as a zip file 89 | name: Release Windows Assets 90 | runs-on: ${{ matrix.platform }} 91 | strategy: 92 | matrix: 93 | platform: [windows-latest] 94 | rust: 95 | - stable 96 | target: 97 | #windows gnu 98 | # - x86_64-pc-windows-gnu 99 | # mscv 100 | - x86_64-pc-windows-msvc 101 | steps: 102 | - name: Checkout code 103 | uses: actions/checkout@v4 104 | 105 | - name: Install ${{ matrix.rust }} toolchain 106 | uses: dtolnay/rust-toolchain@master 107 | with: 108 | toolchain: ${{ matrix.rust }} 109 | 110 | - name: Build artem release 111 | continue-on-error: false 112 | run: cargo build --locked --release --verbose --target=${{ matrix.target }} 113 | 114 | - name: Extract Version Number 115 | shell: bash 116 | run: echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $ 117 | 118 | - name: Copy completion files and man page 119 | shell: bash 120 | run: | 121 | # create a completions and doc folder 122 | mkdir -p deployment/completions 123 | mkdir -p deployment/doc 124 | # copy completion files and man page 125 | cp -u target/*/release/build/artem-*/out/* deployment/completions/ 126 | # replace the input file completions, since it won't work after inputting the paths otherwise 127 | sed -i "s/*::/*:/" deployment/completions/_artem 128 | #move man page and CHANGELOG/README to doc folder 129 | mv deployment/completions/artem.1 deployment/doc/ 130 | mv CHANGELOG.md deployment/doc/ 131 | mv README.md deployment/ 132 | #copy binary file 133 | cp -u target/${{ matrix.target }}/release/artem.exe deployment 134 | 135 | #create zip file from .exe release build 136 | - name: Create zip file 137 | shell: bash 138 | run: | 139 | 7z a artem-v${{ env.PROJECT_VERSION }}-${{ matrix.target }}.zip ./deployment/* 140 | 141 | #upload zip file 142 | - name: Upload Release Build for Windows 143 | uses: softprops/action-gh-release@v1 144 | with: 145 | files: artem-v${{ env.PROJECT_VERSION }}-${{ matrix.target }}.zip 146 | draft: true 147 | prerelease: true 148 | 149 | release_assets_macos: 150 | name: Release MacOS Assets 151 | runs-on: ${{ matrix.platform }} 152 | strategy: 153 | matrix: 154 | platform: [macos-latest] 155 | rust: 156 | - stable 157 | target: 158 | #compile for x86 mac os 159 | - x86_64-apple-darwin 160 | #compile arm version for M1 macs (BigSur+) 161 | - aarch64-apple-darwin 162 | steps: 163 | - name: Checkout code 164 | uses: actions/checkout@v4 165 | 166 | - name: Install ${{ matrix.rust }} toolchain 167 | uses: dtolnay/rust-toolchain@v1 168 | with: 169 | toolchain: ${{ matrix.rust }} 170 | target: ${{ matrix.target }} 171 | 172 | - name: Build artem release 173 | continue-on-error: false 174 | run: cargo build --locked --release --verbose --target=${{ matrix.target }} 175 | 176 | - name: Extract Version Number 177 | shell: bash 178 | run: echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV 179 | 180 | - name: Copy completion files and man page 181 | shell: bash 182 | run: | 183 | #create a completions and doc folder 184 | mkdir -p deployment/completions 185 | mkdir -p deployment/doc 186 | #copy completion files and man page 187 | cp target/*/release/build/artem-*/out/* deployment/completions/ 188 | # replace the input file completions, since it won't work after inputting the paths otherwise 189 | sed -i "" "s/*::/*:/" deployment/completions/_artem 190 | #move man page and CHANGELOG/README to doc folder 191 | mv deployment/completions/artem.1 deployment/doc/ 192 | mv CHANGELOG.md deployment/doc/ 193 | mv README.md deployment/ 194 | #copy binary file 195 | cp target/*/release/artem deployment 196 | 197 | - name: Compress release files 198 | shell: bash 199 | run: | 200 | #compress deployment directory 201 | cd deployment/ 202 | tar -czvf ../artem-v$PROJECT_VERSION-${{ matrix.target }}.tar.gz * 203 | 204 | - name: Upload Release Build 205 | uses: softprops/action-gh-release@v1 206 | with: 207 | files: artem-v${{ env.PROJECT_VERSION }}-${{ matrix.target }}.tar.gz 208 | draft: true 209 | prerelease: true 210 | 211 | release_deb: 212 | name: Create Debian 213 | #create a .deb package using cargo-deb 214 | runs-on: ${{ matrix.platform }} 215 | strategy: 216 | matrix: 217 | platform: [ubuntu-latest] 218 | rust: 219 | - stable 220 | steps: 221 | - name: Checkout code 222 | uses: actions/checkout@v4 223 | 224 | - name: Install ${{ matrix.rust }} toolchain 225 | uses: dtolnay/rust-toolchain@v1 226 | with: 227 | toolchain: ${{ matrix.rust }} 228 | 229 | #build release, so it contains the completion files 230 | - name: Build artem release 231 | continue-on-error: false 232 | run: cargo build --locked --release --verbose 233 | 234 | #copy to completion files from the out directory to the deployment/assets dir 235 | - name: Copy completion files and man page 236 | shell: bash 237 | run: | 238 | mkdir -p deployment/assets 239 | cp -u target/release/build/artem-*/out/* deployment/assets/ 240 | 241 | - name: Install cargo deb 242 | continue-on-error: false 243 | run: cargo install cargo-deb 244 | 245 | - name: Run cargo deb 246 | continue-on-error: false 247 | run: cargo deb 248 | 249 | - name: Upload Debian Package 250 | uses: softprops/action-gh-release@v1 251 | with: 252 | files: ./target/debian/*.deb 253 | draft: true 254 | prerelease: true 255 | 256 | cargo_publish: 257 | name: Publish to Cargo 258 | # update the cargo version, using cargo publish. 259 | # this can fail, so it should be locally checked with cargo publish --dry-run before 260 | runs-on: ${{ matrix.platform }} 261 | strategy: 262 | matrix: 263 | platform: [ubuntu-latest] 264 | steps: 265 | - uses: actions/checkout@v4 266 | - uses: dtolnay/rust-toolchain@v1 267 | with: 268 | toolchain: stable 269 | - name: Publish to creates.io 270 | continue-on-error: false 271 | run: cargo publish --token ${{ secrets.CARGO_API_KEY }} 272 | 273 | # update_homebrew_formula: 274 | # name: Update Homebrew Formula 275 | # # Update the released version in the homebrew tap (https://github.com/finefindus/homebrew-tap) 276 | # # using action from: https://github.com/dawidd6/action-homebrew-bump-formula 277 | # runs-on: ubuntu-latest # this could alternatively be run on macos 278 | # steps: 279 | # - uses: dawidd6/action-homebrew-bump-formula@v3 280 | # with: 281 | # # this token will expire after 90 days and should be manually renewed 282 | # # use this link to create a new one: https://github.com/settings/tokens/new?scopes=public_repo,workflow 283 | # token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 284 | # tap: finefindus/homebrew-tap 285 | # formula: artem 286 | # force: true 287 | 288 | publish_aur_package: 289 | name: Publish AUR package 290 | # publish a compiled from source and binary release to the aur 291 | needs: 292 | - release_assets_linux 293 | 294 | runs-on: ubuntu-latest 295 | 296 | strategy: 297 | fail-fast: true 298 | 299 | steps: 300 | - name: Checkout code 301 | uses: actions/checkout@v4 302 | 303 | - name: Extract Version Number 304 | shell: bash 305 | run: echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV 306 | 307 | - name: Generate PKGBUILD files 308 | shell: bash 309 | run: | 310 | #update PKGBUILD with new values 311 | cd assets/aur 312 | #update PKGBUILDs 313 | ./update_PKGBUILD.sh 314 | #copy files to the deployment dir 315 | cd ../../ 316 | mkdir pkgbuilds/ 317 | cp -u assets/aur/PKGBUILD-* pkgbuilds 318 | 319 | - name: Publish artem to the AUR 320 | uses: KSXGitHub/github-actions-deploy-aur@v2.7.0 321 | with: 322 | pkgname: artem 323 | pkgbuild: ./pkgbuilds/PKGBUILD-stable 324 | commit_username: ${{ secrets.AUR_USERNAME }} 325 | commit_email: ${{ secrets.AUR_EMAIL }} 326 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 327 | commit_message: Updating to v${{ env.PROJECT_VERSION }} 328 | force_push: "true" 329 | 330 | - name: Publish artem-bin to the AUR 331 | uses: KSXGitHub/github-actions-deploy-aur@v2.7.0 332 | with: 333 | pkgname: artem-bin 334 | pkgbuild: ./pkgbuilds/PKGBUILD-bin 335 | commit_username: ${{ secrets.AUR_USERNAME }} 336 | commit_email: ${{ secrets.AUR_EMAIL }} 337 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 338 | commit_message: Updating to v${{ env.PROJECT_VERSION }} 339 | force_push: "true" 340 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | /deployment -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.0.0] - 2024-03-27 9 | 10 | ### Added 11 | - Support SVG output 12 | 13 | ### Changed 14 | - **BREAKING**: Marked `TargetType` as non-exhaustive 15 | - **BREAKING**: Moved `color` and `background_color` from `TargetType` to `Config` 16 | - Updated dependencies 17 | - Exclude assets from cargo upload 18 | 19 | ### Removed 20 | - **BREAKING**: Removed lifetimes from `Config::characters` 21 | 22 | ## [2.0.6] - 2024-02-13 23 | 24 | ### Fixed 25 | - CI/CD correctly building releases 26 | 27 | ## [2.0.5] - 2024-02-12 28 | 29 | ### Removed 30 | - Drop aarch release builds 31 | 32 | ## [2.0.4] - 2024-02-12 33 | 34 | ### Fixed 35 | - Failing release CI 36 | 37 | ## [2.0.3] - 2024-02-12 38 | 39 | ### Changed 40 | - Increase MSRV to 1.74.0 41 | - Updated dependencies 42 | - Updated dependencies of CI 43 | 44 | ## [2.0.2] - 2023-08-24 45 | 46 | ### Changed 47 | - Updated dependencies 48 | 49 | ### Fixed 50 | - Failing release build of .deb 51 | - Fixed AUR install failing due to wrong release tag 52 | 53 | ## [2.0.1] - 2023-08-23 54 | 55 | ### Changed 56 | - Increased maximum size to u32::MAX (4_294_967_295) 57 | - Updated dependencies 58 | 59 | ## [2.0.0] - 2023-07-22 60 | 61 | ### Changed 62 | - Improved performance 63 | - Use more idiomatic code 64 | - Fully support Unicode char input 65 | - Switched to ureq for http requests 66 | - Renamed `Option` to `Config` 67 | - Use ref for conversion 68 | - Restructure code 69 | 70 | 71 | ### Removed 72 | - `-h` now shows help instead of using the terminal height (use `--height` instead) 73 | 74 | ## [1.2.1] - 2023-06-20 75 | 76 | ### Fixed 77 | - False version for release 78 | 79 | ## [1.2.0] - 2023-06-20 80 | 81 | ### Changed 82 | - Fixed typos 83 | - Removed hardcoded Courier font family 84 | 85 | 86 | ## [1.1.7] - 2023-03-25 87 | 88 | ### Changed 89 | - Fixed an issue with the macOs CI builds 90 | 91 | ## [1.1.6] - 2023-03-25 92 | 93 | ### Changed 94 | 95 | - HTML Output has now a line break before the closing tags 96 | - Reduced produced html file size, by not coloring invisible whitespace 97 | - Update dependencies 98 | 99 | ## [1.1.5] - 2022-06-01 100 | 101 | ### Changed 102 | 103 | - Fixed MacOS release not building due to false sed command 104 | - `brew` release should now be working again 105 | 106 | ## [1.1.4] - 2022-06-01 107 | 108 | ### Added 109 | 110 | - README now states that `brew` packages is currently outdated, due to brew providing an old rust version 111 | - Minimum rust version downgraded to `1.59.0`, so `brew` will be able to compile the release again 112 | 113 | ### Changed 114 | 115 | - Fixed error where description for `--width` and `--height` stated that both changed the height of the output 116 | - Fixed [#7](https://github.com/FineFindus/artem/issues/7), the zsh completion works now after the input path 117 | - Updated README with more usage info 118 | 119 | ## [1.1.3] - 2022-05-28 120 | 121 | ### Changed 122 | 123 | - Fixed windows release 124 | 125 | ## [1.1.2] - 2022-05-28 126 | 127 | ### Changed 128 | 129 | - Fixed issues with release not recognizing license 130 | 131 | ## [1.1.1] - 2022-05-28 132 | 133 | ### Changed 134 | 135 | - Fixed cross compilation, by switch from `https-native` to `https-bundled-probe`, since dynamically linked openssl is not supported by cross 136 | 137 | ## [1.1.0] - 2022-05-28 138 | 139 | ### Added 140 | 141 | - It is now possible to input multiple files 142 | - Improved handling of `.ansi`/`.ans` files ( includes more warnings) 143 | - Improved documentation of output files 144 | - Added `web_image` feature, which allows URL instead of path input. It is enabled by default 145 | - Authors and description will now be shown in the command help text and man page 146 | - New image has been added for testing purposes 147 | - The `--centerX` and `--centerY` can be used to center the image in the terminal 148 | - `artem` is now available as an aur package (artem) as well as as a aur binary package (artem-bin) 149 | 150 | ### Changed 151 | 152 | - Added `--release` flag to example build command (Thanks to @talwat) 153 | - Fixed failing doc test on rust nightly 154 | - Fixed an error that could occur when the image was only 1x1 155 | - Fixed an issue where the border was not applied correctly if the image was only a single pixel wide 156 | - Adapted tests to work with multiple file inputs 157 | - Increased minimum rust version in CI to 1.60.0 158 | - The clap command now uses information from Cargo.toml, including version, authors, description and name 159 | - Improved command help texts 160 | - Rewrote description of the command help 161 | - Rewrote description of the `.deb` package 162 | - Renamed `--character` input text to be **_characters_** instead of **_density_** 163 | - Argument tests have been refactored to their own files 164 | - Test now complete much faster (down to 5s from 104s) due to using a smaller image, since the large one was unnecessary used 165 | 166 | ## [1.0.3] - 2022-05-05 167 | 168 | ### Changed 169 | 170 | - Fixed error in windows releases building process 171 | - Fixed error with brew tap 172 | 173 | ## [1.0.2] - 2022-05-04 174 | 175 | ### Changed 176 | 177 | - Fixed error in windows releases 178 | 179 | ## [1.0.1] - 2022-05-04 180 | 181 | ### Changed 182 | 183 | - Fixed error in release workflow 184 | 185 | ## [1.0.0] - 2022-05-04 186 | 187 | ### Added 188 | 189 | - A [homebrew tap](https://github.com/FineFindus/homebrew-tap) is now available 190 | - Cargo publishing is now done in the release workflow 191 | - The release workflow now updates the homebrew tap 192 | - Shell completion files and the man page are now contained in the compressed released files 193 | - A new README sections explains how to install the completion files 194 | - The new `--outline` flag will only produce an only of the image 195 | - When using the `--hysteresis` flag along the `--outline` flag the ascii art will be even more outlined, with less imperfections 196 | - Added more test cases and examples to the README to cover the newly added functionality 197 | - Major refactoring of the code 198 | - Artem is now a library, which is used by the command-line interface 199 | - Due to a refactoring of the code, the output ascii image now resembles the input image more closely 200 | 201 | ### Changed 202 | 203 | - Overhauled the installation section in the README, it now contains much more detailed installations instructions 204 | - Switched from `f64` to `f32`, since the additional precision has literally **_no_** impact (it gets rounded/cut away), but yields worse performance 205 | - Refactored `average_color` to be iterator based, according to some microbenchmarks, this makes it a few nanoseconds faster 206 | - Refactored the `convert`, `blur`, `apply_sober` and `edge_tracking` functions to use `iterator`s instead of for loops. This removes a lot of nasty and hard to deal with bug, especially together with multi-threading 207 | - Removed multithreading, it was a constant source of bugs, since pixels can't be split for threads/output characters. It also rarely brought noticeable performance improvements 208 | - The new iterator-based implementation opens the possibility to use [rayon](https://crates.io/crates/rayon) in the future 209 | - Fixed a crash which could occur when piping to a file using the maximum terminal size 210 | - Fixed a bug, where the `--height` argument would not actually use the correct height and being a bit too high 211 | 212 | ## [0.6.1] - 2022-03-24 213 | 214 | ### Added 215 | 216 | - Linux Binaries will now also be compiled with `musl` 217 | - Completion scripts for the `.deb` will be copied in the CD process 218 | - With mscv compiled windows binaries are available as an alternative to the gnu compiled ones 219 | - MacOS binaries for (x86 and arm) have been added to the CD process 220 | 221 | ## [0.6.0] - 2022-03-24 222 | 223 | ### Added 224 | 225 | - When using an html output file, artem will now converted the result to html, this also works with .ans files respectively 226 | - More Documentation to better describe the code 227 | - The `--border` flag can be used to create a border around the ascii image 228 | - The `--flipX` flag can be used horizontally flip the image 229 | - The `--flipY` flag can be used vertically flip the image 230 | - Two more tests, which fully compare the results 231 | 232 | ### Changed 233 | 234 | - Major refactoring 235 | 236 | ## [0.5.1] - 2022-03-14 237 | 238 | ### Changed 239 | 240 | - Using a new workflow job for the windows build 241 | 242 | ## [0.5.0] - 2022-03-14 243 | 244 | ### Added 245 | 246 | - Release builds are now available for more targets (linux x64 and arm) and windows (using gnu-target) 247 | 248 | ### Changed 249 | 250 | - Using the `--width` argument now correctly resizes the image 251 | - Using the `--height` argument now uses the correct height of the terminal 252 | - Using multiple Threads now display the full image instead of leaving a few pixels out 253 | - Updated the example image in the README to reflect the changes 254 | 255 | ## [0.4.1] - 2022-03-01 256 | 257 | ### Added 258 | 259 | - Changed version to 0.4.1, since github actions would not use the right files otherwise 260 | - Fixed error with tar command in cd 261 | 262 | ## [0.4.0] - 2022-03-01 263 | 264 | ### Added 265 | 266 | - README now contains an installation section 267 | - Use the `--background` flag to let the ascii chars have a background color. Might be useful for images with dark backgrounds. 268 | - Use the `--invert` flag to change the brightness of the used characters. Can be useful for images with a dark background 269 | - README now lists some example formats that can be used 270 | - Tab completions now works in other shells as well (fish and zsh in deb package) 271 | - Removed linting problems found by clippy 272 | - CI tests now against the stable, beta and nightly rust version 273 | - CI now checks for clippy warnings 274 | - Changelog file to document changes to the project 275 | - A Feature template can be used to easily request features over Github 276 | 277 | ### Changed 278 | 279 | - Logging no longer logs the date, since it is not needed 280 | - Man Page String are now formatted correctly 281 | 282 | ## [0.3.0] - 2022-02-25 283 | 284 | ### Added 285 | 286 | - Logging with different verbosity levels to help debugging 287 | - `verbose` flag can be used to change the verbosity, defaults to `error` 288 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "artem" 3 | version = "3.0.0" 4 | authors = ["@FineFindus"] 5 | description = "Convert images from multiple formats (jpg, png, webp, etc…) to ASCII art" 6 | edition = "2021" 7 | rust-version = "1.82.0" 8 | readme = "README.md" 9 | license = "MPL-2.0" 10 | homepage = "https://github.com/FineFindus/artem" 11 | repository = "https://github.com/FineFindus/artem" 12 | keywords = ["text", "ascii", "ascii-art", "terminal"] 13 | categories = ["command-line-utilities", "multimedia::images"] 14 | #exclude test image files from cargo upload 15 | exclude = ["/assets", "/examples"] 16 | 17 | #build file 18 | build = "build.rs" 19 | 20 | [profile.release] 21 | opt-level = 3 22 | 23 | #bin definition 24 | [[bin]] 25 | name = "artem" 26 | path = "src/main.rs" 27 | 28 | [lib] 29 | name = "artem" 30 | path = "src/lib.rs" 31 | 32 | [[bench]] 33 | name = "artem_bench" 34 | harness = false 35 | 36 | [build-dependencies] 37 | clap = { version = "4.5", features = ["cargo", "derive"] } 38 | clap_complete = "4.5" 39 | clap_mangen = "0.2" 40 | log = "0.4" 41 | 42 | [dev-dependencies] 43 | assert_cmd = "2.0" 44 | predicates = "3.1" 45 | criterion = "0.5" 46 | pretty_assertions = "1.4.1" 47 | 48 | [dependencies] 49 | image = "0.25.4" 50 | colored = "2.1" 51 | clap = { version = "4.5", features = ["cargo"] } 52 | terminal_size = "0.4.0" 53 | log = "0.4" 54 | env_logger = "0.11" 55 | ureq = { version = "2.10", optional = true } 56 | anstyle-svg = "0.1" 57 | 58 | [features] 59 | default = ["web_image"] 60 | web_image = ["ureq"] 61 | 62 | 63 | [package.metadata.deb] 64 | section = "graphics" 65 | priority = "optional" 66 | assets = [ 67 | #file locations are partailly from https://github.com/BurntSushi/ripgrep/blob/master/Cargo.toml 68 | [ 69 | "target/release/artem", 70 | "usr/bin/", 71 | "755", 72 | ], 73 | [ 74 | "deployment/assets/artem.1", 75 | "usr/share/man/man1/artem.1", 76 | "644", 77 | ], 78 | [ 79 | "README.md", 80 | "usr/share/doc/artem/README", 81 | "644", 82 | ], 83 | [ 84 | "CHANGELOG.md", 85 | "usr/share/doc/artem/CHANGELOG", 86 | "644", 87 | ], 88 | [ 89 | "LICENSE", 90 | "usr/share/doc/artem/", 91 | "644", 92 | ], 93 | #the completion files and man page is generated and copied by the build script 94 | [ 95 | "deployment/assets/artem.bash", 96 | "usr/share/bash-completion/completions/artem", 97 | "644", 98 | ], 99 | [ 100 | "deployment/assets/artem.fish", 101 | "usr/share/fish/vendor_completions.d/artem.fish", 102 | "644", 103 | ], 104 | [ 105 | "deployment/assets/_artem", 106 | "usr/share/zsh/vendor-completions/", 107 | "644", 108 | ], 109 | ] 110 | extended-description = """\ 111 | artem is a rust command-line interface to convert images from multiple formats (jpg, png, webp, gif and many more) to ASCII art, inspired by jp2a. 112 | 113 | It suppots modern features, such as truecolor by default, although ANSI-Colors can be used as a fallback when truecolor is disabled. 114 | It also respects environment variables, like NO_COLOR, to completely disable colored output. 115 | 116 | For questions, bug reports or feedback, please visit https://github.com/FineFindus/artem. 117 | """ 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License 2 | Version 2.0 3 | 4 | 1. Definitions 5 | 1.1. “Contributor” 6 | means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 7 | 8 | 1.2. “Contributor Version” 9 | means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 10 | 11 | 1.3. “Contribution” 12 | means Covered Software of a particular Contributor. 13 | 14 | 1.4. “Covered Software” 15 | means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 16 | 17 | 1.5. “Incompatible With Secondary Licenses” 18 | means 19 | 20 | that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or 21 | 22 | that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 23 | 24 | 1.6. “Executable Form” 25 | means any form of the work other than Source Code Form. 26 | 27 | 1.7. “Larger Work” 28 | means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 29 | 30 | 1.8. “License” 31 | means this document. 32 | 33 | 1.9. “Licensable” 34 | means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 35 | 36 | 1.10. “Modifications” 37 | means any of the following: 38 | 39 | any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or 40 | 41 | any new file in Source Code Form that contains any Covered Software. 42 | 43 | 1.11. “Patent Claims” of a Contributor 44 | means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 45 | 46 | 1.12. “Secondary License” 47 | means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 48 | 49 | 1.13. “Source Code Form” 50 | means the form of the work preferred for making modifications. 51 | 52 | 1.14. “You” (or “Your”) 53 | means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 54 | 55 | 2. License Grants and Conditions 56 | 2.1. Grants 57 | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: 58 | 59 | under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and 60 | 61 | under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 62 | 63 | 2.2. Effective Date 64 | The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 65 | 66 | 2.3. Limitations on Grant Scope 67 | The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: 68 | 69 | for any code that a Contributor has removed from Covered Software; or 70 | 71 | for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or 72 | 73 | under Patent Claims infringed by Covered Software in the absence of its Contributions. 74 | 75 | This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 76 | 77 | 2.4. Subsequent Licenses 78 | No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 79 | 80 | 2.5. Representation 81 | Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 82 | 83 | 2.6. Fair Use 84 | This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 85 | 86 | 2.7. Conditions 87 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 88 | 89 | 3. Responsibilities 90 | 3.1. Distribution of Source Form 91 | All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 92 | 93 | 3.2. Distribution of Executable Form 94 | If You distribute Covered Software in Executable Form then: 95 | 96 | such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and 97 | 98 | You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 99 | 100 | 3.3. Distribution of a Larger Work 101 | You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 102 | 103 | 3.4. Notices 104 | You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 105 | 106 | 3.5. Application of Additional Terms 107 | You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 108 | 109 | 4. Inability to Comply Due to Statute or Regulation 110 | If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 111 | 112 | 5. Termination 113 | 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 114 | 115 | 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 116 | 117 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 118 | 119 | 6. Disclaimer of Warranty 120 | Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 121 | 122 | 7. Limitation of Liability 123 | Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 124 | 125 | 8. Litigation 126 | Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 127 | 128 | 9. Miscellaneous 129 | This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 130 | 131 | 10. Versions of the License 132 | 10.1. New Versions 133 | Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 134 | 135 | 10.2. Effect of New Versions 136 | You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 137 | 138 | 10.3. Modified Versions 139 | If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 140 | 141 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 142 | If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. 143 | 144 | Exhibit A - Source Code Form License Notice 145 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 146 | 147 | If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. 148 | 149 | You may add additional accurate notices of copyright ownership. 150 | 151 | Exhibit B - “Incompatible With Secondary Licenses” Notice 152 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![artem crate](https://img.shields.io/crates/v/artem.svg)](https://crates.io/crates/artem) 2 | ![Terminal](https://badgen.net/badge/icon/terminal?icon=terminal&label) 3 | [![Continuous Integration](https://github.com/FineFindus/artem/actions/workflows/continuous_integration.yaml/badge.svg)](https://github.com/FineFindus/artem/actions/workflows/continuous_integration.yaml) 4 | ![maintenance-status](https://img.shields.io/badge/maintenance-passively--maintained-yellowgreen.svg) 5 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) 6 | 7 | 8 | 9 | # Artem 10 | 11 | Artem is a small cli program, written in rust, to easily convert images 12 | to ascii art, named after the latin word for art. By default it tries to use truecolor, if the terminal does not support truecolor, it falls back to 16 Color ANSI. When the ascii image is written to a file, the image will not use colors. 13 | It supports `.jpeg`, `.png`, `.gif`, `.webp` and many more. 14 | 15 | If you want to use this project as a library, please refer to the [docs](https://docs.rs/artem/latest/artem/). 16 | 17 | ## Maintenance 18 | 19 | #### **Is this project still being maintained?** 20 | 21 | Yes. Although there is currently no active development of new features, issue and new feature request will still be worked on. If you have any issue or have an idea for a new feature, please [create an issue](https://github.com/FineFindus/artem/issues/new/choose). 22 | 23 | ## Examples 24 | 25 | ### Input 26 | 27 | _source:_ https://upload.wikimedia.org/wikipedia/commons/4/44/Abraham_Lincoln_head_on_shoulders_photo_portrait.jpg 28 | ![Abraham Lincoln](/examples/abraham_lincoln.jpg) 29 | 30 | ### Output 31 | 32 | ![Abraham Lincoln](/examples/abraham_lincoln_ascii.png) 33 | 34 | ## Usage 35 | 36 | For simply converting an image: 37 | 38 | ```bash 39 | artem path 40 | ``` 41 | 42 | The input can either be one or multiple file paths or URLs. 43 | 44 | **NOTE**: To use URLs, the `web_image` feature has to be enabled. It is enabled by default. 45 | 46 | For more options use: 47 | 48 | ```bash 49 | artem --help 50 | ``` 51 | 52 | To use custom ascii chars, use the `--characters` (or `-c` for short) argument.The characters should be ordered from darkest/densest to lightest. 53 | If the background should be invisible, add a space at the end. Alternatively this program has already 3 predefined character sets, 54 | accessibly by supplying the `--characters` argument to gether with the number (`0`, `1` or `2`) of the preset that should be used. 55 | By default preset `1` is used. 56 | 57 | ```bash 58 | artem PATH --characters "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<>|,.-#+!$%&/()=?*'_:; " 59 | ``` 60 | 61 | To change the size at which the converted image is displayed, use: 62 | 63 | ```bash 64 | #for auto sizing height 65 | artem PATH --height 66 | #for auto-sizing width 67 | artem PATH --width 68 | #for manual resizing use the --size flag 69 | artem PATH --size 100 70 | ``` 71 | 72 | It is also possible to center the image using: 73 | 74 | ```bash 75 | #center the image horizontally 76 | artem PATH --centerX 77 | #center the image vertically 78 | artem PATH --centerY 79 | ``` 80 | 81 | To save the the image to a file, use the `--output` flag. 82 | 83 | ```bash 84 | artem PATH --output ascii.txt 85 | #if the output file is an html file, the resulting ascii art will be saved as html ascii art, which supports colors 86 | artem PATH --output ascii.html 87 | # or alternatively, use an .asn file for colored ascii text 88 | artem PATH --output ascii.ans 89 | ``` 90 | 91 | Using the `--outline` flag, the given input image will be filtered, to only contain an outline, which will then be converted. Please be aware, that this will take some additional time, as well as that it might not perfectly work on every image. For the best result, please use an image with a clear distinction between the background and the foreground. 92 | 93 | ```bash 94 | artem PATH --outline 95 | ``` 96 | 97 | For an even better result, it might be worthwhile trying out the `--hysteresis`/`--hys` flag, potentially with characters better suited for outlines, for example. 98 | 99 | ```bash 100 | artem PATH --outline --hysteresis --characters "|/\_. " 101 | ``` 102 | 103 | ## Installation 104 | 105 | ### All platforms (recommended) 106 | 107 | The easiest way to install artem is using `cargo` with 108 | 109 | ```bash 110 | cargo install artem 111 | ``` 112 | 113 | It will automatically add `artem` to your PATH variable, so it can used like shown in the [usage section](#usage). 114 | 115 | If `cargo` is not installed, visit the [cargo book](https://doc.rust-lang.org/cargo/getting-started/installation.html) for installation instructions. 116 | 117 | ### Linux 118 | 119 | #### Debian-based Distributions (e.g. Ubuntu) 120 | 121 | For Debian-based Distributions, like Ubuntu, download the `.deb` file from the [release](https://github.com/FineFindus/artem/releases) page and install it with: 122 | 123 | ```bash 124 | sudo dpkg -i artem.deb 125 | ``` 126 | 127 | The `.deb` package also contains tab completions (for bash, zsh and fish) and a man page. 128 | 129 | #### Archlinux-based Distributions 130 | 131 | `artem` is available as an AUR package. You can install it with your favorite aur-helper, for example with `yay`: 132 | 133 | ```bash 134 | yay -S artem 135 | ``` 136 | 137 | This will build it from source. 138 | Alternatively, it is also available as a precompiled binary (`artem-bin`): 139 | 140 | ```bash 141 | yay -S artem-bin 142 | ``` 143 | 144 | #### Other Distributions 145 | 146 | On other distributions use the binary file provided in the [release tab](https://github.com/FineFindus/artem/releases). 147 | 148 | Alternatively, if `brew` is installed, you can also use `brew` to install it. See the [MacOS Homebrew section](#using-homebrew) for more information. 149 | 150 | ### MacOS 151 | 152 | #### Using Homebrew 153 | 154 | > **Warning** 155 | > It's no longer recommended to install `artem` via Homebrew, as the tap is no longer maintained due to difficulty working with Homebrew. If you wish to maintain the tap, please contact me. 156 | 157 | ```bash 158 | brew install finefindus/tap/artem 159 | ``` 160 | 161 | The homebrew version has the added benefit of also installing the man page and tab completions for bash, zsh and fish. 162 | 163 | #### Binary files 164 | 165 | Alternatively binary files (for x86_64 and Arm) are provided in the [release tab](https://github.com/FineFindus/artem/releases). This way of installing is NOT recommend over using [`cargo`](#all-platforms-recommended). 166 | 167 | ### Windows 168 | 169 | To install the windows version, without using `cargo`, download either the gnu- or the mscv compiled `.zip` files from [release tab](https://github.com/FineFindus/artem/releases) and extract the `.exe`. It should be noted that you will have to add the `.exe` manually to the PATH variable. 170 | 171 | ## Shell completions 172 | 173 | `artem` has shell completions and a man page available. When using the homebrew version, the `.deb` package, or the aur versions, they are installed automatically, whilst for using the binary files with shell completions, the completion files, which be can be found in the compressed release file, have to be copied to the correct locations. 174 | Assuming the compressed file has been uncompressed, use following commands to copy the files to their correct location for unix-like systems: 175 | 176 | ### Shell Completions and Man page 177 | 178 | For **bash**: 179 | 180 | ```bash 181 | #copy the bash completion file 182 | sudo cp completions/artem.bash /etc/bash_completion.d/ 183 | ``` 184 | 185 | For **zsh** add the file to a `$fpath` directory: 186 | 187 | ```zsh 188 | #copy the zsh completion file 189 | cp completions/_artem $fpath 190 | ``` 191 | 192 | For **fish** add the file to the fish completions directory: 193 | 194 | ```fish 195 | #copy the fish completion file 196 | cp completions/artem.fish $HOME/.config/fish/completions/ 197 | ``` 198 | 199 | For Windows add `. /path/to/_artem.ps1` (including the dot) to the PowerShell [profile](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.2). 200 | 201 | ### Man Page 202 | 203 | `artem` also provides a man page for the binary releases contained in the `doc` directory. To view it using the `-l` flag for `man` to view a local file. 204 | 205 | ```bash 206 | #view the local man page 207 | man -l doc/artem.1 208 | ``` 209 | 210 | ## Building from source 211 | 212 | Assuming you have rust/cargo installed, you can build the project with: 213 | 214 | ```bash 215 | cargo build --release 216 | ``` 217 | 218 | The `--release` flag disables debugging options, increasing performance. 219 | 220 | Visit the [rust homepage](https://www.rust-lang.org/learn/get-started) for installation instructions if rust is not installed. 221 | 222 | ### Features 223 | 224 | This disables the default features, whilst enabling all other specified features: 225 | 226 | ```bash 227 | cargo build --release --no-default-features --features FEATURES 228 | ``` 229 | 230 | For more information about the usage of features, please refer to the [cargo book](https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options). 231 | 232 | The following features are currently available: 233 | 234 | - `web_image` Accept Image URLs as input (enabled by default) 235 | 236 | ## Contributing 237 | 238 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Please be aware that it might take some time for someone to respond. 239 | 240 | ## Credits/Inspiration 241 | 242 | This projects was heavily inspired by [jp2a](https://github.com/cslarsen/jp2a) as well as 243 | the [coding train video on ascii art](https://www.youtube.com/watch?v=55iwMYv8tGI). 244 | 245 | Also a big thanks to [ripgrep](https://github.com/BurntSushi/ripgrep/) for indirectly helping with inspiration for the build setup. 246 | 247 | The following images are used for testing/examples: 248 | 249 | - [Abraham Lincoln](https://upload.wikimedia.org/wikipedia/commons/4/44/Abraham_Lincoln_head_on_shoulders_photo_portrait.jpg) 250 | - [Radio tower](https://unsplash.com/photos/hDXk9iOi9bM) 251 | - [Moth](https://altphotos.com/photo/deaths-head-hawkmoth-3464/) 252 | 253 | ## Todo 254 | 255 | - [x] Better average the RGB values of multiple pixel 256 | 257 | - [x] Use the current terminal size to auto fit the image 258 | 259 | - [x] Support ANSI terminal colors 260 | 261 | - [x] Convert output to colored html 262 | 263 | - [x] Use multithreading 264 | 265 | - [x] Add tests 266 | 267 | - [x] Add even more test 268 | 269 | - [x] Convert multiple files at once 270 | 271 | - [x] Automate copying of completion files from OUT_DIR to deployment/assets 272 | 273 | - [x] Change name 274 | 275 | - [x] Publish 276 | 277 | ### Potential Ideas 278 | 279 | - [x] Use edge detection and directional ascii 280 | 281 | - [ ] Implement better resizing 282 | 283 | ## License 284 | 285 | [Mozilla Public License 2.0.](LICENSE) 286 | -------------------------------------------------------------------------------- /RELEASE-CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | These steps should be performed when making a new release. Do not commit marked checks in this file. 4 | 5 | ## Pre-release Test Checklist 6 | 7 | ### Before Committing 8 | 9 | - [ ] Local and Remote branches are synced 10 | - [ ] All the tests are passing 11 | - [ ] Continuous Integration is passing 12 | - [ ] `cargo clippy` finds no errors 13 | - [ ] `cargo publish --dry-run` passes 14 | - [ ] Ensure that the aur packages build correctly 15 | - [ ] README has been updated 16 | - [ ] All changes were documented in the Changelog 17 | - [ ] Added the correct semantic version in the Changelog 18 | - [ ] Changed the changes from Unreleased to the new version in the Changelog 19 | - [ ] Updated the version number in Cargo.toml 20 | - [ ] Build version matches version in Cargo.toml 21 | - [ ] Example images still represents the project accurately 22 | - [ ] Example commands still represents the project accurately 23 | - [ ] Documentation has been updated to reflect the changes 24 | - [ ] Manpage contains the correct help 25 | - [ ] Tab-completions works in a all supported shells 26 | 27 | ### After Committing 28 | 29 | - [ ] Copied the changes to a new release 30 | - [ ] Build artifacts have been attached to the release through continuous delivery 31 | - [ ] Cargo deb builds the correct package 32 | 33 | ## Post-release Test Checklist 34 | 35 | - [ ] Installation instructions work using the released artefact 36 | -------------------------------------------------------------------------------- /assets/aur/PKGBUILD-bin: -------------------------------------------------------------------------------- 1 | # Maintainer: FineFindus 2 | pkgname=artem-bin 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 | url='https://github.com/finefindus/artem' 7 | license=('MPL2') 8 | arch=('x86_64' 'aarch64') 9 | provides=('artem') 10 | conflicts=('artem') 11 | source=("https://github.com/FineFindus/artem/releases/download/v$pkgver/artem-v$pkgver-$CARCH-unknown-linux-gnu.tar.gz") 12 | sha256sums=('03eb9f8c39ee27420435ff4d993ce5b7d9a6748947bde636eb7d2a338e82ff4c') 13 | 14 | package() { 15 | install -Dm 755 artem -t "$pkgdir/usr/bin" 16 | install -Dm 644 README.md -t "$pkgdir/usr/share/doc/$pkgname" 17 | install -Dm 644 doc/CHANGELOG.md -t "$pkgdir/usr/share/doc/$pkgname" 18 | install -Dm 644 doc/artem.1 -t "$pkgdir/usr/share/man/man1" 19 | install -Dm 644 completions/artem.bash -t "$pkgdir/usr/share/bash-completion/completions" 20 | install -Dm 644 completions/artem.fish -t "$pkgdir/usr/share/fish/vendor_completions.d" 21 | install -Dm 644 completions/_artem -t "$pkgdir/usr/share/zsh/site-functions" 22 | } 23 | -------------------------------------------------------------------------------- /assets/aur/PKGBUILD-stable: -------------------------------------------------------------------------------- 1 | # Maintainer: FineFindus 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 | --------------------------------------------------------------------------------