diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..d4b616b6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "image" + - dependency-name: "qrcode" + - dependency-name: "rqrr" diff --git a/.github/workflows/CD.yaml b/.github/workflows/CD.yaml new file mode 100644 index 00000000..b5f75f59 --- /dev/null +++ b/.github/workflows/CD.yaml @@ -0,0 +1,91 @@ +name: CD + +on: + push: + branches: + - develop + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +jobs: + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + target: + - x86_64-unknown-linux-musl + - x86_64-apple-darwin + - x86_64-pc-windows-msvc + include: + - target: x86_64-unknown-linux-musl + os: ubuntu-20.04 + use-cross: true + - target: x86_64-apple-darwin + os: macos-11 + - target: x86_64-pc-windows-msvc + os: windows-2022 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.61.0 # MSRV + target: ${{ matrix.target }} + override: true + profile: minimal + - name: Cache build artifacts + uses: Swatinem/rust-cache@v1.3.0 + with: + key: ${{ matrix.target }} + - name: Build a package + uses: actions-rs/cargo@v1.0.1 + with: + command: build + args: --release --target ${{ matrix.target }} + use-cross: ${{ matrix.use-cross }} + - name: Get version + id: get_version + uses: battila7/get-version-action@v2.2.1 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + - name: Install Asciidoctor + run: | + gem install asciidoctor + asciidoctor -V + - name: Build man pages + run: | + mkdir -p build + asciidoctor -b manpage -a "revnumber=${{ steps.get_version.outputs.version-without-v }}" -D build doc/man/man1/*.adoc + - name: Create a package + shell: bash + run: | + if [ "${{ matrix.os }}" != "windows-2022" ] ; then + bin="target/${{ matrix.target }}/release/qrtool" + else + bin="target/${{ matrix.target }}/release/qrtool.exe" + fi + package="qrtool-${{ steps.get_version.outputs.version }}-${{ matrix.target }}" + + mkdir -p "${package}"/{doc,man} + cp README.md COPYRIGHT LICENSE-APACHE LICENSE-MIT "${bin}" "${package}" + cp {AUTHORS,BUILD,CHANGELOG,CONTRIBUTING}.adoc "${package}"/doc + cp build/* "${package}"/man + + if [ "${{ matrix.os }}" != "windows-2022" ] ; then + tar czvf "${package}.tar.gz" "${package}" + else + 7z a -bb "${package}.zip" "${package}" + fi + - name: Release + uses: softprops/action-gh-release@v0.1.14 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + files: 'qrtool-*' + name: "Release version ${{ steps.get_version.outputs.version-without-v }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 00000000..03392fec --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,101 @@ +name: CI + +on: + push: + branches: + - '**' + tags-ignore: + - '**' + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - x86_64-unknown-linux-musl + - x86_64-apple-darwin + - x86_64-pc-windows-msvc + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-20.04 + - target: x86_64-unknown-linux-musl + os: ubuntu-20.04 + use-cross: true + - target: x86_64-apple-darwin + os: macos-11 + - target: x86_64-pc-windows-msvc + os: windows-2022 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.61.0 # MSRV + target: ${{ matrix.target }} + override: true + profile: minimal + - name: Cache build artifacts + uses: Swatinem/rust-cache@v1.3.0 + with: + key: ${{ matrix.target }} + - name: Build a package + uses: actions-rs/cargo@v1.0.1 + with: + command: build + args: --target ${{ matrix.target }} + use-cross: ${{ matrix.use-cross }} + - name: Run tests + uses: actions-rs/cargo@v1.0.1 + with: + command: test + args: --target ${{ matrix.target }} + use-cross: ${{ matrix.use-cross }} + + rustfmt: + name: Rustfmt + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: stable + override: true + profile: minimal + components: rustfmt + - name: Cache build artifacts + uses: Swatinem/rust-cache@v1.3.0 + - name: Check code formatted + uses: actions-rs/cargo@v1.0.1 + with: + command: fmt + args: -- --check + + clippy: + name: Clippy + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: stable + override: true + profile: minimal + components: clippy + - name: Cache build artifacts + uses: Swatinem/rust-cache@v1.3.0 + - name: Check no lint warnings + uses: actions-rs/cargo@v1.0.1 + with: + command: clippy + args: -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bca6f9b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/AUTHORS.adoc b/AUTHORS.adoc new file mode 100644 index 00000000..5c3e13ed --- /dev/null +++ b/AUTHORS.adoc @@ -0,0 +1,5 @@ += List of Authors + +== Original author + +* https://github.com/sorairolake[Shun Sakai] diff --git a/BUILD.adoc b/BUILD.adoc new file mode 100644 index 00000000..2b1a39ec --- /dev/null +++ b/BUILD.adoc @@ -0,0 +1,24 @@ += Build + +== Prerequisites + +.To build *qrtool*, you will need the following dependencies +* https://doc.rust-lang.org/stable/cargo/[Cargo] (v1.61.0 or later) + +.To build man pages, you will need the following additional dependencies +* https://asciidoctor.org/[Asciidoctor] + +== Building from source + +.To clone the repository +[source, shell] +---- +git clone https://github.com/sorairolake/qrtool.git +cd qrtool +---- + +.To build a package +[source, shell] +---- +just build +---- diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc new file mode 100644 index 00000000..80e2c217 --- /dev/null +++ b/CHANGELOG.adoc @@ -0,0 +1,19 @@ += Changelog +:toc: macro +:project-url: https://github.com/sorairolake/qrtool +:compare-url: {project-url}/compare +:issue-url: {project-url}/issues +:pull-request-url: {project-url}/pull + +All notable changes to this project will be documented in this file. + +The format is based on https://keepachangelog.com/[Keep a Changelog], and this +project adheres to https://semver.org/[Semantic Versioning]. + +toc::[] + +== {project-url}/releases/tag/v0.1.0[0.1.0] - 2022-08-18 + +=== Added + +* Initial release diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 00000000..ce1749b9 --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,38 @@ += Contribution Guide +:git-flow-url: https://nvie.com/posts/a-successful-git-branching-model/ +:commit-messages-guide-url: https://github.com/RomuloOliveira/commit-messages-guide + +== Branching model + +The branching model of this project is based on the {git-flow-url}[git-flow]. + +== Style guides + +=== Commit message + +Please see the {commit-messages-guide-url}[Commit messages guide]. + +== Development + +=== Useful development tools + +The https://github.com/casey/just[just] command runner can be used. +Run `just --list` for more details. + +.Run tests +[source, shell] +---- +just test +---- + +.Run the formatter +[source, shell] +---- +just fmt +---- + +.Run the linter +[source, shell] +---- +just lint +---- diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 00000000..67a48d68 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,6 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://github.com/sorairolake/qrtool + +Files: * +Copyright: 2022 Shun Sakai +License: Apache-2.0 or MIT diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..e803deb7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1067 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "assert_cmd" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" +dependencies = [ + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + +[[package]] +name = "clap" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_complete" +version = "3.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4179da71abd56c26b54dd0c248cc081c1f43b0a1a7e8448e28e57a29baa993d" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "data-url" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" +dependencies = [ + "matches", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide 0.5.3", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "g2gen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc100b16c63808c5c388cd23ff94c5a35cf28ea459f336323f7948a39480555" +dependencies = [ + "g2poly", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "g2p" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf09bc632629cbe5420b330e45bcc8f80403e74ba1027d213258914fd5c62755" +dependencies = [ + "g2gen", + "g2poly", +] + +[[package]] +name = "g2poly" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e837767888fca507f07e89c90e0b350da7bbb89170f67a4655dc9bdc4cca457b" + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder 0.1.22", + "num-iter", + "num-rational", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "jpeg-decoder" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" + +[[package]] +name = "kurbo" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" +dependencies = [ + "arrayvec 0.7.2", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea2d928b485416e8908cff2d97d621db22b27f7b3b6729e438bcf42c671ba91" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate 0.8.6", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "png" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +dependencies = [ + "bitflags", + "crc32fast", + "deflate 1.0.0", + "miniz_oxide 0.5.3", +] + +[[package]] +name = "predicates" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" + +[[package]] +name = "predicates-tree" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.99", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.43", + "quote 1.0.21", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + +[[package]] +name = "qrtool" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "clap", + "clap_complete", + "image", + "predicates", + "qrcode", + "resvg", + "rqrr", + "sysexits", + "tiny-skia", + "usvg", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2 1.0.43", +] + +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rctree" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae028b272a6e99d9f8260ceefa3caa09300a8d6c8d2b2001316474bc52122e9" + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "resvg" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34489194784b86c03c3d688258e2ba73f3c82700ba4673ee2ecad5ae540b9438" +dependencies = [ + "gif", + "jpeg-decoder 0.2.6", + "log", + "pico-args", + "png 0.17.5", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b221de559e4a29df3b957eec92bc0de6bc8eaf6ca9cfed43e5e1d67ff65a34" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rqrr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa79947f53b20adb909a323d828d0fd744fa9d854792df07913b083bcd4d63b" +dependencies = [ + "g2p", + "image", + "lru", +] + +[[package]] +name = "safe_arch" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "svgtypes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc802f68b144cdf4d8ff21301f9a7863e837c627fde46537e29c05e8a18c85c1" +dependencies = [ + "siphasher", +] + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2 1.0.43", + "quote 1.0.21", + "unicode-ident", +] + +[[package]] +name = "sysexits" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c24dea646d0a2a4209a2d960275fd4416be9ab32c77927f51279a621e2b629" + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder 0.1.22", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "tiny-skia" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d049bfef0eaa2521e75d9ffb5ce86ad54480932ae19b85f78bec6f52c4d30d78" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "bytemuck", + "cfg-if", + "png 0.17.5", + "safe_arch", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "usvg" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a82565b5c96dcbb58c9bdbb6aa3642abd395a6a6b480658532c6f74c3c4b7a" +dependencies = [ + "base64", + "data-url", + "flate2", + "float-cmp", + "kurbo", + "log", + "pico-args", + "rctree", + "roxmltree", + "simplecss", + "siphasher", + "svgtypes", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xmlparser" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..167a1f4a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "qrtool" +version = "0.1.0" +authors = ["Shun Sakai "] +edition = "2021" +rust-version = "1.61.0" +description = "An utility for encoding or decoding QR code" +readme = "README.md" +repository = "https://github.com/sorairolake/qrtool" +license = "Apache-2.0 OR MIT" +keywords = ["qrcode"] +categories = ["command-line-utilities"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.62" +clap = { version = "3.2.17", features = ["derive"] } +clap_complete = "3.2.4" +image = { version = "0.23.14", default-features = false, features = ["png"] } +qrcode = "0.12.0" +resvg = { version = "0.23.0", default-features = false } +rqrr = "0.4.0" +sysexits = "0.3.0" +tiny-skia = "0.6.6" +usvg = { version = "0.23.0", default-features = false } + +[dev-dependencies] +assert_cmd = "2.0.4" +predicates = "2.1.1" + +[profile.release] +lto = true +strip = true diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..19241890 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Shun Sakai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..c244e13f --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# qrtool + +[![CI][ci-badge]][ci-url] +[![Version][version-badge]][version-url] +![License][license-badge] + +**qrtool** is a command-line utility for encoding or decoding QR code. + +## Installation + +### From source + +```sh +cargo install qrtool +``` + +### From binaries + +The [release page][release-page-url] contains pre-built binaries for Linux, +macOS and Windows. + +### How to build + +Please see [BUILD.adoc](BUILD.adoc). + +## Usage + +Please see the following: + +- [`qrtool(1)`](doc/man/man1/qrtool.1.adoc) +- [`qrtool-encode(1)`](doc/man/man1/qrtool-encode.1.adoc) +- [`qrtool-decode(1)`](doc/man/man1/qrtool-decode.1.adoc) + +## Changelog + +Please see [CHANGELOG.adoc](CHANGELOG.adoc). + +## Contributing + +Please see [CONTRIBUTING.adoc](CONTRIBUTING.adoc). + +## License + +Copyright (C) 2022 Shun Sakai (see [AUTHORS.adoc](AUTHORS.adoc)) + +This program is distributed under the terms of either the _Apache License 2.0_ +or the _MIT License_. + +See [COPYRIGHT](COPYRIGHT), [LICENSE-APACHE](LICENSE-APACHE) and +[LICENSE-MIT](LICENSE-MIT) for more details. + +[ci-badge]: https://github.com/sorairolake/qrtool/workflows/CI/badge.svg +[ci-url]: https://github.com/sorairolake/qrtool/actions?query=workflow%3ACI +[version-badge]: https://img.shields.io/crates/v/qrtool +[version-url]: https://crates.io/crates/qrtool +[license-badge]: https://img.shields.io/crates/l/qrtool +[release-page-url]: https://github.com/sorairolake/qrtool/releases diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..f6cc1ad4 --- /dev/null +++ b/build.rs @@ -0,0 +1,49 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +// Lint levels of rustc. +#![warn(rust_2018_idioms)] +#![deny(missing_debug_implementations)] +#![forbid(unsafe_code)] +// Lint levels of Clippy. +#![warn(clippy::cargo, clippy::nursery, clippy::pedantic)] + +use std::env; +use std::io; +use std::path::Path; +use std::process::{Command, ExitStatus}; + +fn generate_man_page(out_dir: impl AsRef) -> io::Result { + let man_dir = env::current_dir()?.join("doc/man/man1"); + Command::new("asciidoctor") + .args(["-b", "manpage"]) + .args(["-a", concat!("revnumber=", env!("CARGO_PKG_VERSION"))]) + .args(["-D".as_ref(), out_dir.as_ref()]) + .args([ + man_dir.join("qrtool.1.adoc"), + man_dir.join("qrtool-encode.1.adoc"), + man_dir.join("qrtool-decode.1.adoc"), + ]) + .status() +} + +fn main() { + println!( + "cargo:rerun-if-changed={}", + env::current_dir().unwrap().join("doc/man").display() + ); + + match generate_man_page(env::var_os("OUT_DIR").unwrap()) { + Ok(exit_status) => { + if !exit_status.success() { + println!("cargo:warning=Asciidoctor failed ({exit_status})"); + } + } + Err(err) => { + println!("cargo:warning=Failed to execute Asciidoctor ({err})"); + } + } +} diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..f5fcb567 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.61.0" diff --git a/doc/man/include/section-copyright.adoc b/doc/man/include/section-copyright.adoc new file mode 100644 index 00000000..1b7e6303 --- /dev/null +++ b/doc/man/include/section-copyright.adoc @@ -0,0 +1,15 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +== COPYRIGHT + +Copyright \(C) 2022 Shun Sakai + +This program is distributed under the terms of either the Apache License 2.0 or +the MIT License. + +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. diff --git a/doc/man/include/section-exit-status.adoc b/doc/man/include/section-exit-status.adoc new file mode 100644 index 00000000..b2ffb73b --- /dev/null +++ b/doc/man/include/section-exit-status.adoc @@ -0,0 +1,31 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +== EXIT STATUS + +*0*:: + Successful program execution. + +*1*:: + An error occurred. + +*65*:: + The input data was incorrect in some way. + +*66*:: + An input file did not exist or was not readable. + +*69*:: + A service is unavailable. + +*71*:: + An operating system error has been detected. + +*74*:: + An error occurred while doing I/O on some file. + +*77*:: + You did not have sufficient permission to perform the operation. diff --git a/doc/man/include/section-reporting-bugs.adoc b/doc/man/include/section-reporting-bugs.adoc new file mode 100644 index 00000000..898e649c --- /dev/null +++ b/doc/man/include/section-reporting-bugs.adoc @@ -0,0 +1,10 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +== REPORTING BUGS + +Bugs can be reported on GitHub at:{blank}:: + https://github.com/sorairolake/qrtool/issues diff --git a/doc/man/man1/qrtool-decode.1.adoc b/doc/man/man1/qrtool-decode.1.adoc new file mode 100644 index 00000000..c9207aa9 --- /dev/null +++ b/doc/man/man1/qrtool-decode.1.adoc @@ -0,0 +1,64 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + += qrtool-decode(1) +// Specify in UTC. +:docdate: 2022-08-17 +:doctype: manpage +ifdef::revnumber[:mansource: qrtool {revnumber}] +:manmanual: General Commands Manual +:includedir: ../include + +== NAME + +qrtool-decode - detect and decode a QR code + +== SYNOPSIS + +*qrtool decode* [_OPTION_]... _IMAGE_ + +== DESCRIPTION + +This command detects and decodes a QR code. + +== POSITIONAL ARGUMENTS + +_IMAGE_:: + Input image file. + +== OPTIONS + +*-t*, *--type* _FORMAT_:: + The format of the input. + + The possible values are:{blank}::: + + *png*:::: + + Portable Network Graphics. + + *svg*:::: + + Scalable Vector Graphics. + This also includes gzipped it. + +*-h*, *--help*:: + Print help information. + +include::{includedir}/section-exit-status.adoc[] + +== EXAMPLES + +Detect and decode a QR code from the given image:{blank}:: + $ *qrtool decode input.png* + +include::{includedir}/section-reporting-bugs.adoc[] + +include::{includedir}/section-copyright.adoc[] + +== SEE ALSO + +*qrtool*(1), *qrtool-encode*(1) diff --git a/doc/man/man1/qrtool-encode.1.adoc b/doc/man/man1/qrtool-encode.1.adoc new file mode 100644 index 00000000..799b51c0 --- /dev/null +++ b/doc/man/man1/qrtool-encode.1.adoc @@ -0,0 +1,149 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + += qrtool-encode(1) +// Specify in UTC. +:docdate: 2022-08-17 +:doctype: manpage +ifdef::revnumber[:mansource: qrtool {revnumber}] +:manmanual: General Commands Manual +:includedir: ../include + +== NAME + +qrtool-encode - encode input data in a QR code + +== SYNOPSIS + +*qrtool encode* [_OPTION_]... [_STRING_] + +== DESCRIPTION + +This command encodes input data in a QR code. + +== POSITIONAL ARGUMENTS + +_STRING_:: + Input data. + If it is not specified, data will be read from stdin. + This conflicts with *--read-from*. + +== OPTIONS + +*-o*, *--output* _FILE_:: + Output the result to a file. + +*-r*, *--read-from* _FILE_:: + Read input data from a file. + This conflicts with _STRING_. + +*-l*, *--error-correction-level* _LEVEL_:: + Error correction level. + + The possible values are:{blank}::: + + *l*:::: + + Level L. + 7% of codewords can be restored. + + *m*:::: + + Level M. + 15% of codewords can be restored. + This is the default. + + *q*:::: + + Level Q. + 25% of codewords can be restored. + + *h*:::: + + Level H. + 30% of codewords can be restored. + +*-v*, *--symbol-version* _NUMBER_:: + The version of the symbol. + For normal QR code, it should be between *1* and *40*. + For Micro QR code, it should be between *1* and *4*. + +*-m*, *--margin* _NUMBER_:: + The width of margin. + Default is 4. + +*-t*, *--type* _FORMAT_:: + The format of the output. + + The possible values are:{blank}::: + + *png*:::: + + Portable Network Graphics. + This is the default. + + *svg*:::: + + Scalable Vector Graphics. + + *unicode*:::: + + UTF-8 string. + +*-M*, *--mode* _MODE_:: + The mode of the output. + + The possible values are:{blank}::: + + *numeric*:::: + + Numbers from 0 to 9. + + *alphanumeric*:::: + + Uppercase letters from A to Z, numbers from 0 to 9 and few symbols. + + *byte*:::: + + Arbitrary binary data. + This is the default. + + *kanji*:::: + + Shift JIS text. + +*--variant* _TYPE_:: + The type of QR code. + This requires *--symbol-version*. + + The possible values are:{blank}::: + + *normal*:::: + + Normal QR code. + This is the default. + + *micro*:::: + + Micro QR code. + +*-h*, *--help*:: + Print help information. + +include::{includedir}/section-exit-status.adoc[] + +== EXAMPLES + +Encode the given string in a QR code:{blank}:: + $ *qrtool encode "QR code"* + +include::{includedir}/section-reporting-bugs.adoc[] + +include::{includedir}/section-copyright.adoc[] + +== SEE ALSO + +*qrtool*(1), *qrtool-decode*(1) diff --git a/doc/man/man1/qrtool.1.adoc b/doc/man/man1/qrtool.1.adoc new file mode 100644 index 00000000..c858e8cd --- /dev/null +++ b/doc/man/man1/qrtool.1.adoc @@ -0,0 +1,59 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + += qrtool(1) +// Specify in UTC. +:docdate: 2022-08-17 +:doctype: manpage +ifdef::revnumber[:mansource: qrtool {revnumber}] +:manmanual: General Commands Manual +:includedir: ../include + +== NAME + +qrtool - an utility for encoding or decoding QR code + +== SYNOPSIS + +*{manname}* [_OPTION_]... _COMMAND_ + +== DESCRIPTION + +*{manname}* is a command-line utility for encoding or decoding QR code. + +== COMMANDS + +*qrtool-encode*(1):: + Encode input data in a QR code. + +*qrtool-decode*(1):: + Detect and decode a QR code. + +== OPTIONS + +*-h*, *--help*:: + Print help information. + +*-V*, *--version*:: + Print version information. + +*--generate-completion* _SHELL_:: + Generate shell completion. + The completion is output to stdout. + + The possible values are:{blank}::: + + * *bash* + * *elvish* + * *fish* + * *powershell* + * *zsh* + +include::{includedir}/section-exit-status.adoc[] + +include::{includedir}/section-reporting-bugs.adoc[] + +include::{includedir}/section-copyright.adoc[] diff --git a/justfile b/justfile new file mode 100644 index 00000000..f93c10c9 --- /dev/null +++ b/justfile @@ -0,0 +1,43 @@ +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# Copyright (C) 2022 Shun Sakai +# + +alias all := default +alias lint := clippy + +# Run default recipe +default: build + +# Build a package +@build: + cargo build + +# Remove generated artifacts +@clean: + cargo clean + +# Check a package +@check: + cargo check + +# Run tests +@test: + cargo test + +# Run the formatter +@fmt: + cargo fmt + +# Run the linter +@clippy: + cargo clippy -- -D warnings + +# Run the linter for GitHub Actions workflow files +@lint-github-actions: + actionlint + +# Run the formatter for the README +@fmt-readme: + npx prettier -w README.md diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..3a26366d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..300e4f4a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,263 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +use std::io; +use std::path::PathBuf; + +use clap::{ + value_parser, AppSettings, Args, CommandFactory, Parser, Subcommand, ValueEnum, ValueHint, +}; +use clap_complete::{Generator, Shell}; +use image::{error::ImageFormatHint, ImageError, ImageFormat}; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Parser)] +#[clap( + version, + about, + propagate_version(true), + arg_required_else_help(true), + args_conflicts_with_subcommands(true) +)] +#[clap(setting(AppSettings::DeriveDisplayOrder))] +pub struct Opt { + /// Generate shell completion. + /// + /// The completion is output to stdout. + #[clap(long, value_enum, value_name("SHELL"))] + pub generate_completion: Option, + + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Encode input data in a QR code. + Encode(Encode), + + /// Detect and decode a QR code. + Decode(Decode), +} + +#[derive(Args, Debug)] +#[clap(setting(AppSettings::DeriveDisplayOrder))] +pub struct Encode { + /// Output the result to a file. + #[clap(short, long, value_name("FILE"))] + pub output: Option, + + /// Read input data from a file. + #[clap( + short, + long, + value_name("FILE"), + value_hint(ValueHint::FilePath), + conflicts_with("input") + )] + pub read_from: Option, + + /// Error correction level. + #[clap(short('l'), long, value_enum, default_value_t, value_name("LEVEL"))] + pub error_correction_level: Ecc, + + /// The version of the symbol. + /// + /// For normal QR code, it should be between 1 and 40. + /// For Micro QR code, it should be between 1 and 4. + #[clap( + value_parser(value_parser!(i16).range(1..=40)), + short('v'), + long, + value_name("NUMBER") + )] + pub symbol_version: Option, + + /// The width of margin. + #[clap(short, long, default_value("4"), value_name("NUMBER"))] + pub margin: u32, + + /// The format of the output. + #[clap( + short('t'), + long("type"), + value_enum, + default_value_t, + value_name("FORMAT") + )] + pub output_format: OutputFormat, + + /// The mode of the output. + #[clap(short('M'), long, value_enum, default_value_t, value_name("MODE"))] + pub mode: Mode, + + /// The type of QR code. + #[clap( + long, + value_enum, + default_value_t, + requires("symbol-version"), + value_name("TYPE") + )] + pub variant: Variant, + + /// Input data. + /// + /// If it is not specified, data will be read from stdin. + #[clap(value_name("STRING"))] + pub input: Option, +} + +#[derive(Args, Debug)] +#[clap(setting(AppSettings::DeriveDisplayOrder))] +pub struct Decode { + /// The format of the input. + #[clap(short('t'), long("type"), value_enum, value_name("FORMAT"))] + pub input_format: Option, + + /// Input image file. + #[clap(value_name("IMAGE"), value_hint(ValueHint::FilePath))] + pub input: PathBuf, +} + +impl Opt { + /// Generate shell completion and print it. + pub fn print_completion(gen: impl Generator) { + clap_complete::generate( + gen, + &mut Self::command(), + Self::command().get_name(), + &mut io::stdout(), + ); + } +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum Ecc { + /// Level L. + /// + /// 7% of codewords can be restored. + L, + + /// Level M. + /// + /// 15% of codewords can be restored. + M, + + /// Level Q. + /// + /// 25% of codewords can be restored. + Q, + + /// Level H. + /// + /// 30% of codewords can be restored. + H, +} + +impl Default for Ecc { + fn default() -> Self { + Self::M + } +} + +impl From for qrcode::EcLevel { + fn from(level: Ecc) -> Self { + match level { + Ecc::L => Self::L, + Ecc::M => Self::M, + Ecc::Q => Self::Q, + Ecc::H => Self::H, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)] +pub enum OutputFormat { + /// Portable Network Graphics. + Png, + + /// Scalable Vector Graphics. + Svg, + + /// UTF-8 string. + Unicode, +} + +impl Default for OutputFormat { + fn default() -> Self { + Self::Png + } +} + +impl TryFrom for ImageFormat { + type Error = ImageError; + + fn try_from(format: OutputFormat) -> Result { + match format { + OutputFormat::Png => Ok(Self::Png), + _ => Err(ImageError::Unsupported(ImageFormatHint::Unknown.into())), + } + } +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum Mode { + /// Numbers from 0 to 9. + Numeric, + + /// Uppercase letters from A to Z, numbers from 0 to 9 and few symbols. + Alphanumeric, + + /// Arbitrary binary data. + Byte, + + /// Shift JIS text. + Kanji, +} + +impl Default for Mode { + fn default() -> Self { + Self::Byte + } +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum Variant { + /// Normal QR code. + Normal, + + /// Micro QR code. + Micro, +} + +impl Default for Variant { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum InputFormat { + /// Portable Network Graphics. + Png, + + /// Scalable Vector Graphics. + /// + /// This also includes gzipped it. + Svg, +} + +impl TryFrom for ImageFormat { + type Error = ImageError; + + fn try_from(format: InputFormat) -> Result { + match format { + InputFormat::Png => Ok(Self::Png), + InputFormat::Svg => Err(ImageError::Unsupported(ImageFormatHint::Unknown.into())), + } + } +} diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 00000000..6d31553c --- /dev/null +++ b/src/core.rs @@ -0,0 +1,139 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +use std::fs; +use std::io::{self, Read, Write}; +use std::str; + +use anyhow::Context; +use clap::Parser; +use image::{io::Reader, ImageError, ImageFormat}; +use qrcode::{bits::Bits, QrCode}; +use rqrr::PreparedImage; + +use crate::cli::{Command, InputFormat, Opt, OutputFormat}; +use crate::{decode, encode}; + +/// Runs the program and returns the result. +#[allow(clippy::too_many_lines)] +pub fn run() -> anyhow::Result<()> { + let opt = Opt::parse(); + + if let Some(shell) = opt.generate_completion { + Opt::print_completion(shell); + return Ok(()); + } + + if let Some(command) = opt.command { + match command { + Command::Encode(arg) => { + let input = if let Some(string) = arg.input { + string.into_bytes() + } else if let Some(path) = arg.read_from { + fs::read(&path) + .with_context(|| format!("Could not read data from {}", path.display()))? + } else { + let mut buf = Vec::new(); + io::stdin() + .read_to_end(&mut buf) + .context("Could not read data from stdin")?; + buf + }; + + let level = arg.error_correction_level.into(); + let code = if let Some(version) = arg.symbol_version { + let v = encode::set_version(version, &arg.variant) + .context("Could not set the version")?; + let mut bits = Bits::new(v); + encode::push_data_for_selected_mode(&mut bits, input, &arg.mode) + .and_then(|_| bits.push_terminator(level)) + .and_then(|_| QrCode::with_bits(bits, level)) + } else { + QrCode::with_error_correction_level(&input, level) + } + .context("Could not construct a QR code")?; + + match arg.output_format { + format @ (OutputFormat::Svg | OutputFormat::Unicode) => { + let string = if format == OutputFormat::Svg { + encode::to_svg(&code, arg.margin) + } else { + encode::to_unicode(&code, arg.margin) + }; + + if let Some(file) = arg.output { + fs::write(&file, string).with_context(|| { + format!("Could not write the image to {}", file.display()) + })?; + } else { + println!("{string}"); + } + } + format => { + let image = encode::to_image(&code, arg.margin); + + let format = ImageFormat::try_from(format) + .expect("The image format is not supported"); + if let Some(file) = arg.output { + image.save_with_format(&file, format).with_context(|| { + format!("Could not write the image to {}", file.display()) + })?; + } else { + image + .write_to(&mut io::stdout(), format) + .context("Could not write the image to stdout")?; + } + } + } + } + Command::Decode(arg) => { + let input_format = if decode::is_svg(&arg.input) { + Some(InputFormat::Svg) + } else { + arg.input_format + }; + let image = match input_format { + Some(InputFormat::Svg) => decode::from_svg(&arg.input), + Some(format) => decode::load_image_file( + &arg.input, + format + .try_into() + .expect("The image format is not supported"), + ) + .map_err(anyhow::Error::from), + _ => Reader::open(&arg.input) + .and_then(Reader::with_guessed_format) + .map_err(ImageError::from) + .and_then(Reader::decode) + .map_err(anyhow::Error::from), + } + .with_context(|| { + format!("Could not read the image from {}", arg.input.display()) + })?; + let image = image.into_luma8(); + + let mut image = PreparedImage::prepare(image); + let grids = image.detect_grids(); + let contents = + decode::grids_as_bytes(grids).context("Could not decode the grid")?; + + for content in contents { + if let Ok(string) = str::from_utf8(&content.1) { + println!("{string}"); + } else { + io::stdout() + .write_all(&content.1) + .context("Could not write data to stdout")?; + } + } + } + } + } else { + unreachable!(); + } + + Ok(()) +} diff --git a/src/decode.rs b/src/decode.rs new file mode 100644 index 00000000..12b95b7a --- /dev/null +++ b/src/decode.rs @@ -0,0 +1,99 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io::{BufReader, Cursor}; +use std::path::Path; + +use anyhow::Context; +use image::{io::Reader, DynamicImage, ImageError, ImageFormat, ImageResult}; +use rqrr::{BitGrid, DeQRError, Grid, MetaData}; +use tiny_skia::{Pixmap, Transform}; +use usvg::{FitTo, Tree}; + +/// Returns `true` if `path` is SVG. +pub fn is_svg(path: impl AsRef) -> bool { + matches!( + path.as_ref().extension().and_then(OsStr::to_str), + Some("svg" | "svgz") + ) +} + +fn svg_to_png(path: &Path) -> anyhow::Result> { + let opt = usvg::Options { + resources_dir: path + .canonicalize() + .ok() + .and_then(|p| p.parent().map(Path::to_path_buf)), + ..Default::default() + }; + + let image = fs::read(path)?; + let tree = Tree::from_data(&image, &opt.to_ref())?; + + let pixmap_size = tree.svg_node().size.to_screen_size(); + let mut pixmap = Pixmap::new(pixmap_size.width(), pixmap_size.height()) + .context("Could not allocate a new pixmap")?; + resvg::render( + &tree, + FitTo::Original, + Transform::default(), + pixmap.as_mut(), + ) + .context("SVG has an invalid size")?; + pixmap.encode_png().map_err(anyhow::Error::from) +} + +fn from_png(data: &[u8]) -> ImageResult { + Reader::with_format(Cursor::new(data), ImageFormat::Png).decode() +} + +/// Reads the image from SVG. +pub fn from_svg(path: impl AsRef) -> anyhow::Result { + let data = svg_to_png(path.as_ref())?; + from_png(&data).map_err(anyhow::Error::from) +} + +/// Reads an image file. +pub fn load_image_file(path: impl AsRef, format: ImageFormat) -> ImageResult { + let reader = BufReader::new(File::open(path.as_ref()).map_err(ImageError::IoError)?); + image::load(reader, format) +} + +type DecodedBytes = (MetaData, Vec); + +fn grid_as_bytes(grid: &Grid) -> Result { + let mut writer = Vec::new(); + grid.decode_to(&mut writer).map(|meta| (meta, writer)) +} + +/// Decodes the grids as bytes. +pub fn grids_as_bytes( + grids: impl AsRef<[Grid]>, +) -> Result, DeQRError> { + grids + .as_ref() + .iter() + .map(|grid| grid_as_bytes(grid)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_extension_as_svg() { + assert!(is_svg("image.svg")); + assert!(is_svg("image.svgz")); + } + + #[test] + fn invalid_extension_as_svg() { + assert!(!is_svg("image.png")); + } +} diff --git a/src/encode.rs b/src/encode.rs new file mode 100644 index 00000000..90720f31 --- /dev/null +++ b/src/encode.rs @@ -0,0 +1,96 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +use image::{DynamicImage, Luma}; +use qrcode::{ + bits::Bits, + render::{svg, unicode, Renderer}, + types::QrError, + QrCode, QrResult, Version, +}; + +use crate::cli::{Mode, Variant}; + +/// Sets the version. +pub const fn set_version(version: i16, variant: &Variant) -> QrResult { + match variant { + Variant::Normal => { + if let 1..=40 = version { + Ok(Version::Normal(version)) + } else { + Err(QrError::InvalidVersion) + } + } + Variant::Micro => { + if let 1..=4 = version { + Ok(Version::Micro(version)) + } else { + Err(QrError::InvalidVersion) + } + } + } +} + +/// Encodes data for the selected mode to the bits. +pub fn push_data_for_selected_mode( + bits: &mut Bits, + data: impl AsRef<[u8]>, + mode: &Mode, +) -> QrResult<()> { + let data = data.as_ref(); + match mode { + Mode::Numeric => bits.push_numeric_data(data), + Mode::Alphanumeric => bits.push_alphanumeric_data(data), + Mode::Byte => bits.push_byte_data(data), + Mode::Kanji => bits.push_kanji_data(data), + } +} + +/// Renders the QR code into an image. +pub fn to_svg(code: &QrCode, margin: u32) -> String { + Renderer::>::new(&code.to_colors(), code.width(), margin).build() +} + +/// Renders the QR code into an image. +pub fn to_unicode(code: &QrCode, margin: u32) -> String { + Renderer::::new(&code.to_colors(), code.width(), margin).build() +} + +/// Renders the QR code into an image. +pub fn to_image(code: &QrCode, margin: u32) -> DynamicImage { + let image = Renderer::>::new(&code.to_colors(), code.width(), margin).build(); + DynamicImage::ImageLuma8(image) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_qr_code_version() { + // Valid normal QR code version. + assert_eq!( + set_version(1, &Variant::Normal).unwrap(), + Version::Normal(1) + ); + assert_eq!( + set_version(40, &Variant::Normal).unwrap(), + Version::Normal(40) + ); + + // Valid Micro QR code version. + assert_eq!(set_version(1, &Variant::Micro).unwrap(), Version::Micro(1)); + assert_eq!(set_version(4, &Variant::Micro).unwrap(), Version::Micro(4)); + + // Invalid normal QR code version. + assert!(set_version(0, &Variant::Normal).is_err()); + assert!(set_version(41, &Variant::Normal).is_err()); + + // Invalid Micro QR code version. + assert!(set_version(0, &Variant::Micro).is_err()); + assert!(set_version(5, &Variant::Micro).is_err()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..c6b95306 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,57 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +// Lint levels of rustc. +#![deny(missing_debug_implementations)] +#![warn(rust_2018_idioms)] +// Lint levels of Clippy. +#![warn(clippy::cargo, clippy::nursery, clippy::pedantic)] +#![allow(clippy::multiple_crate_versions)] + +mod cli; +mod core; +mod decode; +mod encode; + +use std::io; +use std::process::ExitCode; + +use image::ImageError; +use qrcode::types::QrError; +use rqrr::DeQRError; + +fn main() -> ExitCode { + match core::run() { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("Error: {:?}", err); + #[allow(clippy::option_if_let_else)] + if let Some(e) = err.downcast_ref::() { + match e.kind() { + io::ErrorKind::NotFound => sysexits::ExitCode::NoInput.into(), + io::ErrorKind::PermissionDenied => sysexits::ExitCode::NoPerm.into(), + _ => ExitCode::FAILURE, + } + } else if err.is::() { + sysexits::ExitCode::DataErr.into() + } else if let Some(e) = err.downcast_ref::() { + match e { + DeQRError::IoError => sysexits::ExitCode::IoErr.into(), + _ => sysexits::ExitCode::DataErr.into(), + } + } else if let Some(e) = err.downcast_ref::() { + match e { + ImageError::Limits(_) => sysexits::ExitCode::OsErr.into(), + ImageError::Unsupported(_) => sysexits::ExitCode::Unavailable.into(), + ImageError::IoError(_) => sysexits::ExitCode::IoErr.into(), + _ => sysexits::ExitCode::DataErr.into(), + } + } else { + ExitCode::FAILURE + } + } + } +} diff --git a/tests/data/8.png b/tests/data/8.png new file mode 100644 index 00000000..acdc50bd Binary files /dev/null and b/tests/data/8.png differ diff --git a/tests/data/basic.png b/tests/data/basic.png new file mode 100644 index 00000000..e7be5310 Binary files /dev/null and b/tests/data/basic.png differ diff --git a/tests/data/basic.svg b/tests/data/basic.svg new file mode 100644 index 00000000..1deb7d5f --- /dev/null +++ b/tests/data/basic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/basic.svgz b/tests/data/basic.svgz new file mode 100644 index 00000000..2a90fbd2 Binary files /dev/null and b/tests/data/basic.svgz differ diff --git a/tests/data/data.txt b/tests/data/data.txt new file mode 100644 index 00000000..1001b232 --- /dev/null +++ b/tests/data/data.txt @@ -0,0 +1 @@ +QR code \ No newline at end of file diff --git a/tests/data/high.png b/tests/data/high.png new file mode 100644 index 00000000..0271b634 Binary files /dev/null and b/tests/data/high.png differ diff --git a/tests/data/low.png b/tests/data/low.png new file mode 100644 index 00000000..40dbd1f8 Binary files /dev/null and b/tests/data/low.png differ diff --git a/tests/data/micro.png b/tests/data/micro.png new file mode 100644 index 00000000..7f72d07b Binary files /dev/null and b/tests/data/micro.png differ diff --git a/tests/data/quartile.png b/tests/data/quartile.png new file mode 100644 index 00000000..76c9449f Binary files /dev/null and b/tests/data/quartile.png differ diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 00000000..13c68c29 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,191 @@ +// +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Copyright (C) 2022 Shun Sakai +// + +// Lint levels of rustc. +#![deny(missing_debug_implementations)] +#![warn(rust_2018_idioms)] +// Lint levels of Clippy. +#![warn(clippy::cargo, clippy::nursery, clippy::pedantic)] +#![allow(clippy::multiple_crate_versions)] + +use assert_cmd::Command; +use predicates::prelude::predicate; + +fn command() -> Command { + let mut command = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + command.current_dir("tests"); + command +} + +#[test] +fn generate_completion_conflicts_with_subcommands() { + command() + .arg("--generate-completion") + .arg("bash") + .arg("encode") + .assert() + .failure() + .code(2); + command() + .arg("--generate-completion") + .arg("bash") + .arg("decode") + .assert() + .failure() + .code(2); +} + +#[test] +fn basic_encode() { + let output = command().arg("encode").arg("QR code").output().unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/basic.png").unwrap() + ); + assert!(output.status.success()); +} + +#[test] +fn encode_data_from_file() { + let output = command() + .arg("encode") + .arg("-r") + .arg("data/data.txt") + .output() + .unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/basic.png").unwrap() + ); + assert!(output.status.success()); +} + +#[test] +fn encode_with_error_correction_level() { + let output = command() + .arg("encode") + .arg("-l") + .arg("l") + .arg("QR code") + .output() + .unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/low.png").unwrap() + ); + assert!(output.status.success()); + + let output = command() + .arg("encode") + .arg("-l") + .arg("q") + .arg("QR code") + .output() + .unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/quartile.png").unwrap() + ); + assert!(output.status.success()); + + let output = command() + .arg("encode") + .arg("-l") + .arg("h") + .arg("QR code") + .output() + .unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/high.png").unwrap() + ); + assert!(output.status.success()); +} + +#[test] +fn encode_with_margin() { + let output = command() + .arg("encode") + .arg("-m") + .arg("8") + .arg("QR code") + .output() + .unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/8.png").unwrap() + ); + assert!(output.status.success()); +} + +#[test] +fn encode_as_micro_qr_code() { + let output = command() + .arg("encode") + .arg("-v") + .arg("3") + .arg("--variant") + .arg("micro") + .arg("QR code") + .output() + .unwrap(); + assert_eq!( + image::load_from_memory(&output.stdout).unwrap(), + image::open("tests/data/micro.png").unwrap() + ); + assert!(output.status.success()); +} + +#[test] +fn validate_the_options_dependencies_for_encode_command() { + command() + .arg("encode") + .arg("-r") + .arg("data/data.txt") + .arg("QR code") + .assert() + .failure() + .code(2); + + command() + .arg("encode") + .arg("--variant") + .arg("micro") + .arg("QR code") + .assert() + .failure() + .code(2); +} + +#[test] +fn basic_decode() { + command() + .arg("decode") + .arg("data/basic.png") + .assert() + .success() + .stdout(predicate::eq("QR code\n")); +} + +#[test] +fn decode_from_svg() { + command() + .arg("decode") + .arg("data/basic.svg") + .assert() + .success() + .stdout(predicate::eq("QR code\n")); +} + +#[test] +fn decode_from_svgz() { + command() + .arg("decode") + .arg("data/basic.svgz") + .assert() + .success() + .stdout(predicate::eq("QR code\n")); +}