├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ ├── rust.yml │ └── upload.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── bin ├── generate-preview ├── generate-variety └── snapshots-comment ├── build.rs ├── codecov.yml ├── src ├── assets │ └── fonts │ │ ├── IBMPlexMono-LICENSE.txt │ │ ├── IBMPlexMono-Medium.ttf │ │ └── IBMPlexMono-Regular.ttf ├── builder.rs ├── builder │ └── svg.rs ├── cli.rs ├── encryption.rs ├── main.rs └── page.rs └── tests ├── cli.rs └── data ├── some_value.svg └── too_large.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/data/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | groups: 13 | cli-dependencies: 14 | patterns: 15 | - "clap*" 16 | - "rpassword" 17 | dev-dependencies: 18 | patterns: 19 | - "assert_*" 20 | - "predicates" 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "monthly" 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | attestations: write 7 | 8 | on: 9 | push: 10 | tags: 11 | - v[0-9]+.* 12 | 13 | jobs: 14 | create-release: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | tag: ${{ steps.create-gh-release.outputs.computed-prefix }}${{ steps.create-gh-release.outputs.version }} 18 | version: ${{ steps.create-gh-release.outputs.version }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Changelog variable 22 | if: ${{ !(contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'prerelease') || contains(github.ref, 'rc')) }} 23 | run: | 24 | echo "changelog=CHANGELOG.md" >> $GITHUB_ENV 25 | - id: create-gh-release 26 | uses: taiki-e/create-gh-release-action@v1 27 | with: 28 | draft: true 29 | changelog: ${{ env.changelog }} 30 | # (required) GitHub token for creating GitHub Releases. 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | upload-assets: 34 | needs: [create-release] 35 | strategy: 36 | matrix: 37 | include: 38 | # Linux (ARM and x86-64) 39 | - target: aarch64-unknown-linux-gnu 40 | os: ubuntu-latest 41 | - target: x86_64-unknown-linux-gnu 42 | os: ubuntu-latest 43 | # macOS targets (Universal only) 44 | - target: universal-apple-darwin 45 | os: macos-latest 46 | # Windows (x86-64) 47 | - target: x86_64-pc-windows-msvc 48 | os: windows-latest 49 | runs-on: ${{ matrix.os }} 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Configure cache 53 | uses: Swatinem/rust-cache@v2 54 | - name: Include documentation files variable (macOS) 55 | if: ${{ matrix.target == 'universal-apple-darwin' }} 56 | run: | 57 | echo "include_docs=target/aarch64-apple-darwin/release/man,target/aarch64-apple-darwin/release/completion" >> $GITHUB_ENV 58 | - name: Install the Apple certificate, provisioning profile, and API key (macOS) 59 | if: ${{ matrix.target == 'universal-apple-darwin' }} 60 | id: keychain 61 | env: 62 | BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} 63 | P12_PASSWORD: ${{ secrets.P12_PASSWORD }} 64 | BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} 65 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 66 | AUTH_KEY_BASE64: ${{ secrets.AUTH_KEY_BASE64 }} 67 | run: | 68 | # create variables 69 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 70 | PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile 71 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 72 | AUTH_KEY_PATH=$RUNNER_TEMP/AuthKey.p8 73 | 74 | # import certificate and provisioning profile from secrets 75 | echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH 76 | echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH 77 | 78 | # create temporary keychain 79 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 80 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 81 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 82 | 83 | # import certificate to keychain 84 | security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 85 | security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 86 | security list-keychain -d user -s $KEYCHAIN_PATH 87 | 88 | # apply provisioning profile 89 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 90 | cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles 91 | 92 | # create auth key file for notarization 93 | echo -n "$AUTH_KEY_BASE64" | base64 --decode -o $AUTH_KEY_PATH 94 | 95 | # setup outputs 96 | echo "auth_key_path=$AUTH_KEY_PATH" >> $GITHUB_OUTPUT 97 | echo "keychain_path=$KEYCHAIN_PATH" >> $GITHUB_OUTPUT 98 | echo "pp_path=$PP_PATH" >> $GITHUB_OUTPUT 99 | echo "certificate_path=$CERTIFICATE_PATH" >> $GITHUB_OUTPUT 100 | - name: Include documentation files variable (windows) 101 | if: ${{ matrix.target == 'x86_64-pc-windows-msvc' }} 102 | run: | 103 | echo "include_docs=target/${{ matrix.target }}/release/man,target/${{ matrix.target }}/release/completion" >> $env:GITHUB_ENV 104 | - name: Include documentation files variable (linux) 105 | if: ${{ contains(matrix.target, 'linux') }} 106 | run: | 107 | echo "include_docs=target/${{ matrix.target }}/release/man,target/${{ matrix.target }}/release/completion" >> $GITHUB_ENV 108 | - id: upload-rust-binary-action 109 | uses: taiki-e/upload-rust-binary-action@v1.24.0 110 | with: 111 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 112 | # Note that glob pattern is not supported yet. 113 | bin: paper-age 114 | # (optional) Comma-separated list of algorithms to be used for checksum (sha256, sha512, sha1, or md5) 115 | checksum: sha256 116 | # (optional) Target triple, default is host triple. 117 | target: ${{ matrix.target }} 118 | # (optional) Comma-separated list of additional files to be included to archive. 119 | include: README.md,LICENSE.txt,src/assets/fonts/IBMPlexMono-LICENSE.txt,${{ env.include_docs }} 120 | # (required) GitHub token for uploading assets to GitHub Releases. 121 | token: ${{ secrets.GITHUB_TOKEN }} 122 | # Sign build products using codesign on macOS 123 | codesign: '7FP48PW9TN' 124 | codesign-prefix: 'fi.matiaskorhonen.' 125 | codesign-options: 'runtime' 126 | - name: Zip the binary for notarization (macOS) 127 | if: ${{ matrix.target == 'universal-apple-darwin' }} 128 | run: zip -r $RUNNER_TEMP/paper-age-signed.zip target/${{ matrix.target }}/release/paper-age 129 | - name: Upload the binary for notarization (macOS) 130 | if: ${{ matrix.target == 'universal-apple-darwin' }} 131 | env: 132 | KEY_ID: ${{ secrets.KEY_ID }} 133 | ISSUER: ${{ secrets.ISSUER }} 134 | run: | 135 | xcrun notarytool submit $RUNNER_TEMP/paper-age-signed.zip \ 136 | --key "${{ steps.keychain.outputs.auth_key_path }}" \ 137 | --key-id "$KEY_ID" \ 138 | --issuer "$ISSUER" \ 139 | --wait 140 | - uses: actions/attest-build-provenance@v2 141 | with: 142 | subject-path: "${{ steps.upload-rust-binary-action.outputs.archive }}.*" 143 | 144 | publish-release: 145 | needs: [create-release, upload-assets] 146 | runs-on: ubuntu-latest 147 | env: 148 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | steps: 150 | - uses: actions/checkout@v4 151 | - name: mark release as non-draft 152 | run: | 153 | gh release edit ${{ needs.create-release.outputs.tag }} --draft=false 154 | 155 | update-homebrew: 156 | needs: [create-release, upload-assets, publish-release] 157 | if: ${{ !(contains(needs.create-release.outputs.tag, 'alpha') || contains(needs.create-release.outputs.tag, 'beta') || contains(needs.create-release.outputs.tag, 'prerelease') || contains(needs.create-release.outputs.tag, 'rc')) }} 158 | runs-on: ubuntu-latest 159 | steps: 160 | - name: Update Homebrew Formula 161 | uses: actions/github-script@v7 162 | with: 163 | github-token: ${{ secrets.PAT }} 164 | script: | 165 | github.rest.repos.createDispatchEvent({ 166 | owner: "matiaskorhonen", 167 | repo: "homebrew-paper-age", 168 | event_type: "Update Homebrew Formula", 169 | client_payload: { 170 | version: ${{ toJSON(needs.create-release.outputs.version) }} 171 | } 172 | }) 173 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main", "next" ] 6 | pull_request: 7 | branches: [ "main", "next" ] 8 | 9 | permissions: 10 | checks: write 11 | contents: read 12 | pull-requests: write 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | 17 | jobs: 18 | clippy: 19 | if: github.event_name == 'pull_request' 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Rust 25 | run: | 26 | rustup toolchain install stable --component clippy --profile minimal --no-self-update 27 | rustup default stable 28 | - name: Configure cache 29 | uses: Swatinem/rust-cache@v2 30 | - uses: giraffate/clippy-action@v1 31 | with: 32 | reporter: github-pr-review 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | test: 36 | strategy: 37 | matrix: 38 | os: [ "ubuntu-latest", "macos-14", "windows-latest" ] 39 | 40 | runs-on: ${{ matrix.os }} 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Create local bin directory 45 | if: startsWith(matrix.os, 'ubuntu') 46 | run: mkdir -p "${HOME}/.local/bin" 47 | - name: Download markdown-test-report 48 | if: startsWith(matrix.os, 'ubuntu') 49 | uses: robinraju/release-downloader@v1.11 50 | with: 51 | repository: "ctron/markdown-test-report" 52 | tag: "v0.3.8" 53 | fileName: markdown-test-report-linux-amd64 54 | out-file-path: tmp 55 | - name: Install markdown-test-report 56 | if: startsWith(matrix.os, 'ubuntu') 57 | run: | 58 | mv tmp/markdown-test-report-linux-amd64 "${HOME}/.local/bin/markdown-test-report" 59 | chmod +x "${HOME}/.local/bin/markdown-test-report" 60 | - name: Download grcov 61 | if: startsWith(matrix.os, 'ubuntu') 62 | uses: robinraju/release-downloader@v1.11 63 | with: 64 | repository: "mozilla/grcov" 65 | latest: true 66 | fileName: grcov-x86_64-unknown-linux-gnu.tar.bz2 67 | out-file-path: tmp 68 | - name: Install grcov 69 | if: startsWith(matrix.os, 'ubuntu') 70 | run: | 71 | tar -xvf tmp/grcov-x86_64-unknown-linux-gnu.tar.bz2 72 | mv grcov "${HOME}/.local/bin" 73 | chmod +x "${HOME}/.local/bin/grcov" 74 | - name: Set up Rust 75 | run: | 76 | rustup toolchain install nightly --component llvm-tools --profile minimal --no-self-update 77 | rustup default nightly 78 | - name: Configure cache 79 | uses: Swatinem/rust-cache@v2 80 | - name: Run tests (JSON output) 81 | if: startsWith(matrix.os, 'ubuntu') 82 | shell: bash 83 | run: cargo test --verbose -- -Z unstable-options --report-time --format json | tee test-output.json 84 | env: 85 | CARGO_INCREMENTAL: 0 86 | RUSTFLAGS: -Cinstrument-coverage 87 | LLVM_PROFILE_FILE: cargo-test-%p-%m.profraw 88 | - name: Run tests (normal output) 89 | if: ${{ !startsWith(matrix.os, 'ubuntu') }} 90 | run: cargo test 91 | - name: Process code coverage with grcov 92 | if: startsWith(matrix.os, 'ubuntu') 93 | run: grcov . --binary-path ./target/debug/deps/ -s . -t cobertura --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/cobertura.xml 94 | - name: Upload coverage reports to Codecov 95 | if: startsWith(matrix.os, 'ubuntu') 96 | uses: codecov/codecov-action@v5 97 | with: 98 | files: target/cobertura.xml 99 | - name: Job summary (tests) 100 | if: ${{ always() }} 101 | shell: bash 102 | run: | 103 | [ -f test-output.json ] && \ 104 | command -v markdown-test-report &> /dev/null && \ 105 | markdown-test-report --no-front-matter --output - >> $GITHUB_STEP_SUMMARY || true 106 | continue-on-error: true 107 | 108 | build-binary: 109 | runs-on: ubuntu-latest 110 | 111 | steps: 112 | - uses: actions/checkout@v4 113 | - name: Set up Rust 114 | run: | 115 | rustup toolchain install stable --profile minimal --no-self-update 116 | rustup default stable 117 | - name: Configure cache 118 | uses: Swatinem/rust-cache@v2 119 | - name: Compile binary 120 | run: cargo build --release 121 | - name: Store binary artifact 122 | uses: actions/upload-artifact@v4 123 | with: 124 | name: paper-age-binary 125 | path: target/release/paper-age 126 | 127 | visual-snapshots: 128 | runs-on: ubuntu-latest 129 | 130 | needs: [build-binary] 131 | 132 | env: 133 | PAPERAGE_PASSPHRASE: supersecret 134 | 135 | steps: 136 | - uses: actions/checkout@v4 137 | - name: Setup Ruby 138 | uses: ruby/setup-ruby@v1 139 | with: 140 | ruby-version: "3.2" 141 | - name: Install latest PaperAge release 142 | uses: robinraju/release-downloader@v1.11 143 | with: 144 | repository: "matiaskorhonen/paper-age" 145 | latest: true 146 | extract: true 147 | fileName: paper-age-x86_64-unknown-linux-gnu.tar.gz 148 | out-file-path: tmp 149 | - name: Move the release binary to bin 150 | run: mv tmp/paper-age bin/paper-age-release 151 | - name: Install pdfium-cli 152 | uses: robinraju/release-downloader@v1.11 153 | with: 154 | repository: "klippa-app/pdfium-cli" 155 | tag: "v0.1.1" 156 | fileName: pdfium-linux-x64 157 | out-file-path: bin 158 | - name: Make PDFium executable 159 | run: chmod u+x bin/pdfium-linux-x64 160 | - name: Download PaperAge binary for this branch 161 | uses: actions/download-artifact@v4 162 | with: 163 | name: paper-age-binary 164 | - name: Make the binary executable 165 | run: chmod u+x paper-age 166 | - name: Generate PDFs 167 | run: | 168 | mkdir -p visual-snapshots 169 | echo "Hello World" | ./paper-age --title="A4 secret" --page-size=a4 --output=visual-snapshots/a4-current.pdf 170 | echo "Hello World" | ./paper-age --title="Letter secret" --page-size=letter --output=visual-snapshots/letter-current.pdf 171 | echo "Hello World" | ./bin/paper-age-release --title="A4 secret" --page-size=a4 --output=visual-snapshots/a4-release.pdf 172 | echo "Hello World" | ./bin/paper-age-release --title="Letter secret" --page-size=letter --output=visual-snapshots/letter-release.pdf 173 | - name: Convert the PDFs to PNGs 174 | run: | 175 | for f in visual-snapshots/*.pdf; do 176 | echo "Converting $f to PNG" 177 | ./bin/pdfium-linux-x64 render "$f" "${f%.*}.png" --combine-pages --dpi 300 --file-type png 178 | done 179 | - name: Generate image diffs 180 | run: | 181 | for f in visual-snapshots/*-current.png; do 182 | echo "Comparing ${f/-current.png/-release.png} to $f" 183 | npx --package=odiff-bin --yes -- odiff --parsable-stdout --threshold=0.5 "${f/-current.png/-release.png}" "$f" "${f/-current.png/-diff.png}" || true 184 | done 185 | - name: Save PR number 186 | run: | 187 | mkdir -p ./pr 188 | echo ${{ github.event.number }} > ./visual-snapshots/PR.txt 189 | - name: Save snapshots 190 | uses: actions/upload-artifact@v4 191 | with: 192 | name: visual-snapshots 193 | path: visual-snapshots 194 | -------------------------------------------------------------------------------- /.github/workflows/upload.yml: -------------------------------------------------------------------------------- 1 | name: Upload 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Rust] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | checks: write 11 | contents: read 12 | pull-requests: write 13 | 14 | jobs: 15 | upload: 16 | runs-on: ubuntu-latest 17 | 18 | if: > 19 | github.event.workflow_run.conclusion == 'success' 20 | 21 | env: 22 | PAPERAGE_PASSPHRASE: supersecret 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: 'Download artifact' 27 | uses: actions/github-script@v7 28 | with: 29 | script: | 30 | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | run_id: ${{github.event.workflow_run.id }}, 34 | name: "visual-snapshots" 35 | }); 36 | var matchArtifact = artifacts.data.artifacts[0]; 37 | var download = await github.rest.actions.downloadArtifact({ 38 | owner: context.repo.owner, 39 | repo: context.repo.repo, 40 | artifact_id: matchArtifact.id, 41 | archive_format: 'zip', 42 | }); 43 | var fs = require('fs'); 44 | fs.writeFileSync('${{github.workspace}}/visual-snapshots.zip', Buffer.from(download.data)); 45 | - name: Create directory 46 | run: mkdir -p visual-snapshots 47 | - name: Unzip artifact 48 | run: unzip visual-snapshots.zip -d ./visual-snapshots 49 | - name: Display structure of downloaded files 50 | run: ls -R visual-snapshots/ 51 | - name: Set variables 52 | run: | 53 | PR_NUMBER=$(cat ./visual-snapshots/PR.txt) 54 | echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV 55 | - name: Upload to B2 56 | uses: shallwefootball/s3-upload-action@master 57 | id: B2 58 | with: 59 | aws_key_id: ${{ secrets.AWS_KEY_ID }} 60 | aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY}} 61 | aws_bucket: ${{ vars.AWS_BUCKET }} 62 | endpoint: ${{ vars.AWS_ENDPOINT }} 63 | source_dir: "visual-snapshots" 64 | destination_dir: "30-days/${{ github.repository }}/${{ github.run_id }}" 65 | - name: Generate snapshots comment 66 | run: ./bin/snapshots-comment '${{steps.B2.outputs.object_locations}}' 67 | - uses: marocchino/sticky-pull-request-comment@v2 68 | if: ${{ env.PR_NUMBER != '' }} 69 | with: 70 | number: ${{ env.PR_NUMBER }} 71 | header: "Visual Snapshots" 72 | path: "./visual-snapshots.tmp" 73 | - name: Job summary (visuals) 74 | run: cat visual-snapshots.tmp > $GITHUB_STEP_SUMMARY 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*.jpeg 3 | /*.jpg 4 | /*.pdf 5 | /*.png 6 | /*.svg 7 | /*.dat 8 | /test.* 9 | test-output.json 10 | /visual-snapshots 11 | *.profraw 12 | -------------------------------------------------------------------------------- /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 | 9 | ## [Unreleased] - ReleaseDate 10 | 11 | ## [1.3.4] - 2024-12-25 12 | 13 | ### Changed 14 | 15 | - Update the age crate to v0.11.1 16 | - Generated PDFs now have a white background layer 17 | 18 | ## [1.3.3] - 2024-08-14 19 | 20 | ### Changed 21 | 22 | - Adds codesigning and notarization on macOS 23 | - Minor dependency updates 24 | 25 | ## [1.3.2] - 2024-07-01 26 | 27 | ### Changed 28 | 29 | - Minor dependency updates 30 | 31 | ## [1.3.1] - 2024-06-07 32 | 33 | ### Changed 34 | 35 | - Added [artifact attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds) 36 | - Updates the time crate to fix build issues with Rust nightly 37 | 38 | ## [1.3.0] - 2024-05-01 39 | 40 | ### Changed 41 | 42 | - Updated age to v0.10.0 43 | - Use rpassword directly for passphrase prompt 44 | - Removes indirect dependency on atty (GHSA-g98v-hv3f-hcfr) 45 | - Other minor dependency updates 46 | 47 | ## [1.2.1] - 2024-01-27 48 | 49 | ### Changed 50 | 51 | - Minor dependency updates 52 | 53 | ## [1.2.0] - 2023-10-21 54 | 55 | ### Added 56 | 57 | - The passphrase/notes field below the QR code is now configurable 58 | - Use font subsetting to reduce the file size of the output PDFs 59 | 60 | ### Changed 61 | 62 | - Update rustix to v0.38.20 (GHSA-c827-hfw6-qwvm) 63 | - Other minor dependency updates 64 | 65 | ## [1.1.4] - 2023-10-12 66 | 67 | ### Changed 68 | 69 | - Update printpdf to v0.6.0 70 | - Other minor dependency updates 71 | 72 | ## [1.1.3] - 2023-07-10 73 | 74 | ### Changed 75 | 76 | - Update the age crate to v0.9.2 77 | - Other minor dependency updates 78 | 79 | ## [1.1.2] - 2023-04-25 80 | 81 | ### Changed 82 | 83 | - Minor dependency updates 84 | 85 | ## [1.1.1] - 2023-03-08 86 | 87 | ### Changed 88 | 89 | - Documentation fixes and improvements 90 | - Minor dependency updates 91 | 92 | ## [1.1.0] - 2023-02-14 93 | 94 | ### Added 95 | 96 | - Support for the letter paper size 97 | - Shell completion scripts for Bash, Fish, and Zsh 98 | 99 | ### Fixed 100 | 101 | - The man page is now included in the release archives 102 | 103 | ## [1.0.1] - 2023-02-07 104 | 105 | ### Added 106 | 107 | - PaperAge can now be installed via a [Homebrew Tap](https://github.com/matiaskorhonen/paper-age#homebrew) 108 | 109 | ### Fixed 110 | 111 | - Documentation fixes 112 | 113 | ## [1.0.0] - 2023-02-07 114 | 115 | ### Added 116 | 117 | - Enables writing the PDF to standard out 118 | 119 | ### Changed 120 | 121 | - Default to reading from standard input 122 | 123 | ## [0.1.0] - 2023-02-07 124 | 125 | ### Added 126 | 127 | - First version 128 | - The passphrase can also be read from the `PAPERAGE_PASSPHRASE` environment variable 129 | - Better documentation 130 | - Better logging, including command line flags for verbosity 131 | - Development: added a job summary to the Github Action test job 132 | 133 | ### Changed 134 | 135 | - Renamed the project from ‘Paper Rage’ to ‘PaperAge’ 136 | - Better test coverage 137 | - Development: output test summaries and code coverage information in GitHub actions 138 | 139 | ### Fixed 140 | 141 | - Fixed prerelease versioning 142 | - Removed unused dependency (`is-terminal`) 143 | - Prevent accidental overwrites of the output file 144 | 145 | 146 | [Unreleased]: https://github.com/matiaskorhonen/paper-age/compare/v1.3.4...HEAD 147 | [1.3.4]: https://github.com/matiaskorhonen/paper-age/compare/v1.3.3...v1.3.4 148 | [1.3.3]: https://github.com/matiaskorhonen/paper-age/compare/v1.3.2...v1.3.3 149 | [1.3.2]: https://github.com/matiaskorhonen/paper-age/compare/v1.3.1...v1.3.2 150 | [1.3.1]: https://github.com/matiaskorhonen/paper-age/compare/v1.3.0...v1.3.1 151 | [1.3.0]: https://github.com/matiaskorhonen/paper-age/compare/v1.2.1...v1.3.0 152 | [1.2.1]: https://github.com/matiaskorhonen/paper-age/compare/v1.2.0...v1.2.1 153 | [1.2.0]: https://github.com/matiaskorhonen/paper-age/compare/v1.1.4...v1.2.0 154 | [1.1.4]: https://github.com/matiaskorhonen/paper-age/compare/v1.1.3...v1.1.4 155 | [1.1.3]: https://github.com/matiaskorhonen/paper-age/compare/v1.1.2...v1.1.3 156 | [1.1.2]: https://github.com/matiaskorhonen/paper-age/compare/v1.1.1...v1.1.2 157 | [1.1.1]: https://github.com/matiaskorhonen/paper-age/compare/v1.1.0...v1.1.1 158 | [1.1.0]: https://github.com/matiaskorhonen/paper-age/compare/v1.0.1...v1.1.0 159 | [1.0.1]: https://github.com/matiaskorhonen/paper-age/compare/v1.0.0...v1.0.1 160 | [1.0.0]: https://github.com/matiaskorhonen/paper-age/compare/v0.1.0...v1.0.0 161 | [0.1.0]: https://github.com/matiaskorhonen/paper-age/compare/v0.1.0-prerelease4...v0.1.0 162 | [0.1.0-prerelease4]: https://github.com/matiaskorhonen/paper-age/releases/tag/b0534db779720e912750d0107b3b03b6551abcdd...v0.1.0-prerelease4 163 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aead" 13 | version = "0.5.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 16 | dependencies = [ 17 | "crypto-common", 18 | "generic-array", 19 | ] 20 | 21 | [[package]] 22 | name = "age" 23 | version = "0.11.1" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" 26 | dependencies = [ 27 | "age-core", 28 | "base64", 29 | "bech32", 30 | "chacha20poly1305", 31 | "cookie-factory", 32 | "hmac", 33 | "i18n-embed", 34 | "i18n-embed-fl", 35 | "lazy_static", 36 | "nom", 37 | "pin-project", 38 | "rand", 39 | "rust-embed", 40 | "scrypt", 41 | "sha2", 42 | "subtle", 43 | "x25519-dalek", 44 | "zeroize", 45 | ] 46 | 47 | [[package]] 48 | name = "age-core" 49 | version = "0.11.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" 52 | dependencies = [ 53 | "base64", 54 | "chacha20poly1305", 55 | "cookie-factory", 56 | "hkdf", 57 | "io_tee", 58 | "nom", 59 | "rand", 60 | "secrecy", 61 | "sha2", 62 | ] 63 | 64 | [[package]] 65 | name = "aho-corasick" 66 | version = "1.1.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 69 | dependencies = [ 70 | "memchr", 71 | ] 72 | 73 | [[package]] 74 | name = "aliasable" 75 | version = "0.1.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 78 | 79 | [[package]] 80 | name = "alloc-no-stdlib" 81 | version = "2.0.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 84 | 85 | [[package]] 86 | name = "alloc-stdlib" 87 | version = "0.2.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 90 | dependencies = [ 91 | "alloc-no-stdlib", 92 | ] 93 | 94 | [[package]] 95 | name = "allsorts" 96 | version = "0.14.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "256467e8518f46b4ddbebd7cb8df2add04a720e3a6bdd0800c3896a5fc97ae3a" 99 | dependencies = [ 100 | "bitflags 1.3.2", 101 | "bitreader", 102 | "brotli-decompressor", 103 | "byteorder", 104 | "encoding_rs", 105 | "flate2", 106 | "glyph-names", 107 | "itertools", 108 | "lazy_static", 109 | "libc", 110 | "log", 111 | "num-traits", 112 | "ouroboros", 113 | "rustc-hash", 114 | "tinyvec", 115 | "ucd-trie", 116 | "unicode-canonical-combining-class", 117 | "unicode-general-category", 118 | "unicode-joining-type", 119 | ] 120 | 121 | [[package]] 122 | name = "anstream" 123 | version = "0.6.11" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 126 | dependencies = [ 127 | "anstyle", 128 | "anstyle-parse", 129 | "anstyle-query", 130 | "anstyle-wincon", 131 | "colorchoice", 132 | "utf8parse", 133 | ] 134 | 135 | [[package]] 136 | name = "anstyle" 137 | version = "1.0.6" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 140 | 141 | [[package]] 142 | name = "anstyle-parse" 143 | version = "0.2.2" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 146 | dependencies = [ 147 | "utf8parse", 148 | ] 149 | 150 | [[package]] 151 | name = "anstyle-query" 152 | version = "1.0.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 155 | dependencies = [ 156 | "windows-sys", 157 | ] 158 | 159 | [[package]] 160 | name = "anstyle-wincon" 161 | version = "3.0.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 164 | dependencies = [ 165 | "anstyle", 166 | "windows-sys", 167 | ] 168 | 169 | [[package]] 170 | name = "arc-swap" 171 | version = "1.6.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" 174 | 175 | [[package]] 176 | name = "arrayref" 177 | version = "0.3.7" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" 180 | 181 | [[package]] 182 | name = "arrayvec" 183 | version = "0.7.4" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 186 | 187 | [[package]] 188 | name = "assert_cmd" 189 | version = "2.0.16" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 192 | dependencies = [ 193 | "anstyle", 194 | "bstr", 195 | "doc-comment", 196 | "libc", 197 | "predicates", 198 | "predicates-core", 199 | "predicates-tree", 200 | "wait-timeout", 201 | ] 202 | 203 | [[package]] 204 | name = "assert_fs" 205 | version = "1.1.2" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" 208 | dependencies = [ 209 | "anstyle", 210 | "doc-comment", 211 | "globwalk", 212 | "predicates", 213 | "predicates-core", 214 | "predicates-tree", 215 | "tempfile", 216 | ] 217 | 218 | [[package]] 219 | name = "autocfg" 220 | version = "1.1.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 223 | 224 | [[package]] 225 | name = "base64" 226 | version = "0.21.7" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 229 | 230 | [[package]] 231 | name = "basic-toml" 232 | version = "0.1.7" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" 235 | dependencies = [ 236 | "serde", 237 | ] 238 | 239 | [[package]] 240 | name = "bech32" 241 | version = "0.9.1" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" 244 | 245 | [[package]] 246 | name = "bitflags" 247 | version = "1.3.2" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 250 | 251 | [[package]] 252 | name = "bitflags" 253 | version = "2.4.1" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 256 | 257 | [[package]] 258 | name = "bitreader" 259 | version = "0.3.8" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff" 262 | dependencies = [ 263 | "cfg-if", 264 | ] 265 | 266 | [[package]] 267 | name = "block-buffer" 268 | version = "0.10.4" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 271 | dependencies = [ 272 | "generic-array", 273 | ] 274 | 275 | [[package]] 276 | name = "brotli-decompressor" 277 | version = "2.5.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" 280 | dependencies = [ 281 | "alloc-no-stdlib", 282 | "alloc-stdlib", 283 | ] 284 | 285 | [[package]] 286 | name = "bstr" 287 | version = "1.7.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" 290 | dependencies = [ 291 | "memchr", 292 | "regex-automata", 293 | "serde", 294 | ] 295 | 296 | [[package]] 297 | name = "bumpalo" 298 | version = "3.14.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 301 | 302 | [[package]] 303 | name = "bytemuck" 304 | version = "1.14.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" 307 | 308 | [[package]] 309 | name = "byteorder" 310 | version = "1.5.0" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 313 | 314 | [[package]] 315 | name = "cfg-if" 316 | version = "1.0.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 319 | 320 | [[package]] 321 | name = "chacha20" 322 | version = "0.9.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" 325 | dependencies = [ 326 | "cfg-if", 327 | "cipher", 328 | "cpufeatures", 329 | ] 330 | 331 | [[package]] 332 | name = "chacha20poly1305" 333 | version = "0.10.1" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" 336 | dependencies = [ 337 | "aead", 338 | "chacha20", 339 | "cipher", 340 | "poly1305", 341 | "zeroize", 342 | ] 343 | 344 | [[package]] 345 | name = "cipher" 346 | version = "0.4.4" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 349 | dependencies = [ 350 | "crypto-common", 351 | "inout", 352 | "zeroize", 353 | ] 354 | 355 | [[package]] 356 | name = "clap" 357 | version = "4.5.13" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" 360 | dependencies = [ 361 | "clap_builder", 362 | "clap_derive", 363 | ] 364 | 365 | [[package]] 366 | name = "clap-verbosity-flag" 367 | version = "3.0.2" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" 370 | dependencies = [ 371 | "clap", 372 | "log", 373 | ] 374 | 375 | [[package]] 376 | name = "clap_builder" 377 | version = "4.5.13" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" 380 | dependencies = [ 381 | "anstream", 382 | "anstyle", 383 | "clap_lex", 384 | "strsim", 385 | ] 386 | 387 | [[package]] 388 | name = "clap_complete" 389 | version = "4.5.13" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "aa3c596da3cf0983427b0df0dba359df9182c13bd5b519b585a482b0c351f4e8" 392 | dependencies = [ 393 | "clap", 394 | ] 395 | 396 | [[package]] 397 | name = "clap_derive" 398 | version = "4.5.13" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 401 | dependencies = [ 402 | "heck 0.5.0", 403 | "proc-macro2", 404 | "quote", 405 | "syn 2.0.38", 406 | ] 407 | 408 | [[package]] 409 | name = "clap_lex" 410 | version = "0.7.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 413 | 414 | [[package]] 415 | name = "clap_mangen" 416 | version = "0.2.24" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" 419 | dependencies = [ 420 | "clap", 421 | "roff", 422 | ] 423 | 424 | [[package]] 425 | name = "color_quant" 426 | version = "1.1.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 429 | 430 | [[package]] 431 | name = "colorchoice" 432 | version = "1.0.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 435 | 436 | [[package]] 437 | name = "cookie-factory" 438 | version = "0.3.2" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" 441 | 442 | [[package]] 443 | name = "cpufeatures" 444 | version = "0.2.10" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" 447 | dependencies = [ 448 | "libc", 449 | ] 450 | 451 | [[package]] 452 | name = "crc32fast" 453 | version = "1.3.2" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 456 | dependencies = [ 457 | "cfg-if", 458 | ] 459 | 460 | [[package]] 461 | name = "crossbeam-utils" 462 | version = "0.8.20" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 465 | 466 | [[package]] 467 | name = "crypto-common" 468 | version = "0.1.6" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 471 | dependencies = [ 472 | "generic-array", 473 | "typenum", 474 | ] 475 | 476 | [[package]] 477 | name = "curve25519-dalek" 478 | version = "4.1.3" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 481 | dependencies = [ 482 | "cfg-if", 483 | "cpufeatures", 484 | "curve25519-dalek-derive", 485 | "fiat-crypto", 486 | "rustc_version", 487 | "subtle", 488 | "zeroize", 489 | ] 490 | 491 | [[package]] 492 | name = "curve25519-dalek-derive" 493 | version = "0.1.1" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 496 | dependencies = [ 497 | "proc-macro2", 498 | "quote", 499 | "syn 2.0.38", 500 | ] 501 | 502 | [[package]] 503 | name = "dashmap" 504 | version = "6.1.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 507 | dependencies = [ 508 | "cfg-if", 509 | "crossbeam-utils", 510 | "hashbrown", 511 | "lock_api", 512 | "once_cell", 513 | "parking_lot_core", 514 | ] 515 | 516 | [[package]] 517 | name = "data-url" 518 | version = "0.2.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" 521 | 522 | [[package]] 523 | name = "deranged" 524 | version = "0.3.9" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" 527 | dependencies = [ 528 | "powerfmt", 529 | ] 530 | 531 | [[package]] 532 | name = "difflib" 533 | version = "0.4.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 536 | 537 | [[package]] 538 | name = "digest" 539 | version = "0.10.7" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 542 | dependencies = [ 543 | "block-buffer", 544 | "crypto-common", 545 | "subtle", 546 | ] 547 | 548 | [[package]] 549 | name = "displaydoc" 550 | version = "0.2.4" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" 553 | dependencies = [ 554 | "proc-macro2", 555 | "quote", 556 | "syn 2.0.38", 557 | ] 558 | 559 | [[package]] 560 | name = "doc-comment" 561 | version = "0.3.3" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 564 | 565 | [[package]] 566 | name = "either" 567 | version = "1.9.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 570 | 571 | [[package]] 572 | name = "encoding_rs" 573 | version = "0.8.33" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 576 | dependencies = [ 577 | "cfg-if", 578 | ] 579 | 580 | [[package]] 581 | name = "env_filter" 582 | version = "0.1.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 585 | dependencies = [ 586 | "log", 587 | "regex", 588 | ] 589 | 590 | [[package]] 591 | name = "env_logger" 592 | version = "0.11.6" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 595 | dependencies = [ 596 | "anstream", 597 | "anstyle", 598 | "env_filter", 599 | "humantime", 600 | "log", 601 | ] 602 | 603 | [[package]] 604 | name = "errno" 605 | version = "0.3.5" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 608 | dependencies = [ 609 | "libc", 610 | "windows-sys", 611 | ] 612 | 613 | [[package]] 614 | name = "exitcode" 615 | version = "1.1.2" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" 618 | 619 | [[package]] 620 | name = "fastrand" 621 | version = "2.0.1" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 624 | 625 | [[package]] 626 | name = "fdeflate" 627 | version = "0.3.4" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 630 | dependencies = [ 631 | "simd-adler32", 632 | ] 633 | 634 | [[package]] 635 | name = "fiat-crypto" 636 | version = "0.2.6" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" 639 | 640 | [[package]] 641 | name = "find-crate" 642 | version = "0.6.3" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" 645 | dependencies = [ 646 | "toml", 647 | ] 648 | 649 | [[package]] 650 | name = "flate2" 651 | version = "1.0.28" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 654 | dependencies = [ 655 | "crc32fast", 656 | "miniz_oxide", 657 | ] 658 | 659 | [[package]] 660 | name = "float-cmp" 661 | version = "0.9.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 664 | 665 | [[package]] 666 | name = "float-cmp" 667 | version = "0.10.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 670 | dependencies = [ 671 | "num-traits", 672 | ] 673 | 674 | [[package]] 675 | name = "fluent" 676 | version = "0.16.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7" 679 | dependencies = [ 680 | "fluent-bundle", 681 | "unic-langid", 682 | ] 683 | 684 | [[package]] 685 | name = "fluent-bundle" 686 | version = "0.15.2" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" 689 | dependencies = [ 690 | "fluent-langneg", 691 | "fluent-syntax", 692 | "intl-memoizer", 693 | "intl_pluralrules", 694 | "rustc-hash", 695 | "self_cell 0.10.3", 696 | "smallvec", 697 | "unic-langid", 698 | ] 699 | 700 | [[package]] 701 | name = "fluent-langneg" 702 | version = "0.13.0" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" 705 | dependencies = [ 706 | "unic-langid", 707 | ] 708 | 709 | [[package]] 710 | name = "fluent-syntax" 711 | version = "0.11.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" 714 | dependencies = [ 715 | "thiserror", 716 | ] 717 | 718 | [[package]] 719 | name = "fnv" 720 | version = "1.0.7" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 723 | 724 | [[package]] 725 | name = "fontconfig-parser" 726 | version = "0.5.6" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" 729 | dependencies = [ 730 | "roxmltree 0.19.0", 731 | ] 732 | 733 | [[package]] 734 | name = "fontdb" 735 | version = "0.14.1" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "af8d8cbea8f21307d7e84bca254772981296f058a1d36b461bf4d83a7499fc9e" 738 | dependencies = [ 739 | "fontconfig-parser", 740 | "log", 741 | "memmap2", 742 | "slotmap", 743 | "tinyvec", 744 | "ttf-parser 0.19.2", 745 | ] 746 | 747 | [[package]] 748 | name = "generic-array" 749 | version = "0.14.7" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 752 | dependencies = [ 753 | "typenum", 754 | "version_check", 755 | ] 756 | 757 | [[package]] 758 | name = "getrandom" 759 | version = "0.2.10" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 762 | dependencies = [ 763 | "cfg-if", 764 | "libc", 765 | "wasi", 766 | ] 767 | 768 | [[package]] 769 | name = "gif" 770 | version = "0.12.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" 773 | dependencies = [ 774 | "color_quant", 775 | "weezl", 776 | ] 777 | 778 | [[package]] 779 | name = "globset" 780 | version = "0.4.13" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" 783 | dependencies = [ 784 | "aho-corasick", 785 | "bstr", 786 | "fnv", 787 | "log", 788 | "regex", 789 | ] 790 | 791 | [[package]] 792 | name = "globwalk" 793 | version = "0.9.1" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" 796 | dependencies = [ 797 | "bitflags 2.4.1", 798 | "ignore", 799 | "walkdir", 800 | ] 801 | 802 | [[package]] 803 | name = "glyph-names" 804 | version = "0.2.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "c3531d702d6c1a3ba92a5fb55a404c7b8c476c8e7ca249951077afcbe4bc807f" 807 | 808 | [[package]] 809 | name = "hashbrown" 810 | version = "0.14.2" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" 813 | 814 | [[package]] 815 | name = "heck" 816 | version = "0.4.1" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 819 | 820 | [[package]] 821 | name = "heck" 822 | version = "0.5.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 825 | 826 | [[package]] 827 | name = "hkdf" 828 | version = "0.12.3" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" 831 | dependencies = [ 832 | "hmac", 833 | ] 834 | 835 | [[package]] 836 | name = "hmac" 837 | version = "0.12.1" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 840 | dependencies = [ 841 | "digest", 842 | ] 843 | 844 | [[package]] 845 | name = "humantime" 846 | version = "2.1.0" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 849 | 850 | [[package]] 851 | name = "i18n-config" 852 | version = "0.4.7" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" 855 | dependencies = [ 856 | "basic-toml", 857 | "log", 858 | "serde", 859 | "serde_derive", 860 | "thiserror", 861 | "unic-langid", 862 | ] 863 | 864 | [[package]] 865 | name = "i18n-embed" 866 | version = "0.15.2" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "a7839d8c7bb8da7bd58c1112d3a1aeb7f178ff3df4ae87783e758ca3bfb750b7" 869 | dependencies = [ 870 | "arc-swap", 871 | "fluent", 872 | "fluent-langneg", 873 | "fluent-syntax", 874 | "i18n-embed-impl", 875 | "intl-memoizer", 876 | "lazy_static", 877 | "log", 878 | "parking_lot", 879 | "rust-embed", 880 | "thiserror", 881 | "unic-langid", 882 | "walkdir", 883 | ] 884 | 885 | [[package]] 886 | name = "i18n-embed-fl" 887 | version = "0.9.2" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "f6e9571c3cba9eba538eaa5ee40031b26debe76f0c7e17bafc97ea57a76cd82e" 890 | dependencies = [ 891 | "dashmap", 892 | "find-crate", 893 | "fluent", 894 | "fluent-syntax", 895 | "i18n-config", 896 | "i18n-embed", 897 | "lazy_static", 898 | "proc-macro-error2", 899 | "proc-macro2", 900 | "quote", 901 | "strsim", 902 | "syn 2.0.38", 903 | "unic-langid", 904 | ] 905 | 906 | [[package]] 907 | name = "i18n-embed-impl" 908 | version = "0.8.4" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" 911 | dependencies = [ 912 | "find-crate", 913 | "i18n-config", 914 | "proc-macro2", 915 | "quote", 916 | "syn 2.0.38", 917 | ] 918 | 919 | [[package]] 920 | name = "ignore" 921 | version = "0.4.20" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" 924 | dependencies = [ 925 | "globset", 926 | "lazy_static", 927 | "log", 928 | "memchr", 929 | "regex", 930 | "same-file", 931 | "thread_local", 932 | "walkdir", 933 | "winapi-util", 934 | ] 935 | 936 | [[package]] 937 | name = "image" 938 | version = "0.24.7" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" 941 | dependencies = [ 942 | "bytemuck", 943 | "byteorder", 944 | "color_quant", 945 | "gif", 946 | "jpeg-decoder", 947 | "num-rational", 948 | "num-traits", 949 | "png", 950 | ] 951 | 952 | [[package]] 953 | name = "image" 954 | version = "0.25.1" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" 957 | dependencies = [ 958 | "bytemuck", 959 | "byteorder", 960 | "num-traits", 961 | ] 962 | 963 | [[package]] 964 | name = "imagesize" 965 | version = "0.12.0" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" 968 | 969 | [[package]] 970 | name = "inout" 971 | version = "0.1.3" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 974 | dependencies = [ 975 | "generic-array", 976 | ] 977 | 978 | [[package]] 979 | name = "intl-memoizer" 980 | version = "0.5.1" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" 983 | dependencies = [ 984 | "type-map", 985 | "unic-langid", 986 | ] 987 | 988 | [[package]] 989 | name = "intl_pluralrules" 990 | version = "7.0.2" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" 993 | dependencies = [ 994 | "unic-langid", 995 | ] 996 | 997 | [[package]] 998 | name = "io_tee" 999 | version = "0.1.1" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" 1002 | 1003 | [[package]] 1004 | name = "itertools" 1005 | version = "0.10.5" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 1008 | dependencies = [ 1009 | "either", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "itoa" 1014 | version = "1.0.9" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 1017 | 1018 | [[package]] 1019 | name = "jpeg-decoder" 1020 | version = "0.3.1" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 1023 | 1024 | [[package]] 1025 | name = "js-sys" 1026 | version = "0.3.64" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 1029 | dependencies = [ 1030 | "wasm-bindgen", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "kurbo" 1035 | version = "0.9.5" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" 1038 | dependencies = [ 1039 | "arrayvec", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "lazy_static" 1044 | version = "1.4.0" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 1047 | 1048 | [[package]] 1049 | name = "libc" 1050 | version = "0.2.149" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" 1053 | 1054 | [[package]] 1055 | name = "linked-hash-map" 1056 | version = "0.5.6" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 1059 | 1060 | [[package]] 1061 | name = "linux-raw-sys" 1062 | version = "0.4.10" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 1065 | 1066 | [[package]] 1067 | name = "lock_api" 1068 | version = "0.4.11" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 1071 | dependencies = [ 1072 | "autocfg", 1073 | "scopeguard", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "log" 1078 | version = "0.4.22" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 1081 | 1082 | [[package]] 1083 | name = "lopdf" 1084 | version = "0.31.0" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" 1087 | dependencies = [ 1088 | "encoding_rs", 1089 | "flate2", 1090 | "itoa", 1091 | "linked-hash-map", 1092 | "log", 1093 | "md5", 1094 | "pom", 1095 | "time", 1096 | "weezl", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "md5" 1101 | version = "0.7.0" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 1104 | 1105 | [[package]] 1106 | name = "memchr" 1107 | version = "2.6.4" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 1110 | 1111 | [[package]] 1112 | name = "memmap2" 1113 | version = "0.6.2" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" 1116 | dependencies = [ 1117 | "libc", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "minimal-lexical" 1122 | version = "0.2.1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1125 | 1126 | [[package]] 1127 | name = "miniz_oxide" 1128 | version = "0.7.1" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 1131 | dependencies = [ 1132 | "adler", 1133 | "simd-adler32", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "nom" 1138 | version = "7.1.3" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1141 | dependencies = [ 1142 | "memchr", 1143 | "minimal-lexical", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "normalize-line-endings" 1148 | version = "0.3.0" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 1151 | 1152 | [[package]] 1153 | name = "num-conv" 1154 | version = "0.1.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1157 | 1158 | [[package]] 1159 | name = "num-integer" 1160 | version = "0.1.45" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 1163 | dependencies = [ 1164 | "autocfg", 1165 | "num-traits", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "num-rational" 1170 | version = "0.4.1" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" 1173 | dependencies = [ 1174 | "autocfg", 1175 | "num-integer", 1176 | "num-traits", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "num-traits" 1181 | version = "0.2.17" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 1184 | dependencies = [ 1185 | "autocfg", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "once_cell" 1190 | version = "1.18.0" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 1193 | 1194 | [[package]] 1195 | name = "opaque-debug" 1196 | version = "0.3.0" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 1199 | 1200 | [[package]] 1201 | name = "ouroboros" 1202 | version = "0.17.2" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" 1205 | dependencies = [ 1206 | "aliasable", 1207 | "ouroboros_macro", 1208 | "static_assertions", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "ouroboros_macro" 1213 | version = "0.17.2" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" 1216 | dependencies = [ 1217 | "heck 0.4.1", 1218 | "proc-macro-error", 1219 | "proc-macro2", 1220 | "quote", 1221 | "syn 2.0.38", 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "owned_ttf_parser" 1226 | version = "0.19.0" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" 1229 | dependencies = [ 1230 | "ttf-parser 0.19.2", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "paper-age" 1235 | version = "1.3.4" 1236 | dependencies = [ 1237 | "age", 1238 | "assert_cmd", 1239 | "assert_fs", 1240 | "clap", 1241 | "clap-verbosity-flag", 1242 | "clap_complete", 1243 | "clap_mangen", 1244 | "env_logger", 1245 | "exitcode", 1246 | "log", 1247 | "path-absolutize", 1248 | "predicates", 1249 | "printpdf", 1250 | "qrcode", 1251 | "rpassword", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "parking_lot" 1256 | version = "0.12.1" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1259 | dependencies = [ 1260 | "lock_api", 1261 | "parking_lot_core", 1262 | ] 1263 | 1264 | [[package]] 1265 | name = "parking_lot_core" 1266 | version = "0.9.9" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 1269 | dependencies = [ 1270 | "cfg-if", 1271 | "libc", 1272 | "redox_syscall 0.4.1", 1273 | "smallvec", 1274 | "windows-targets", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "path-absolutize" 1279 | version = "3.1.1" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" 1282 | dependencies = [ 1283 | "path-dedot", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "path-dedot" 1288 | version = "3.1.1" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" 1291 | dependencies = [ 1292 | "once_cell", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "pbkdf2" 1297 | version = "0.12.2" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 1300 | dependencies = [ 1301 | "digest", 1302 | "hmac", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "pdf-writer" 1307 | version = "0.9.2" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "644b654f2de28457bf1e25a4905a76a563d1128a33ce60cf042f721f6818feaf" 1310 | dependencies = [ 1311 | "bitflags 1.3.2", 1312 | "itoa", 1313 | "memchr", 1314 | "ryu", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "pico-args" 1319 | version = "0.5.0" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 1322 | 1323 | [[package]] 1324 | name = "pin-project" 1325 | version = "1.1.3" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 1328 | dependencies = [ 1329 | "pin-project-internal", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "pin-project-internal" 1334 | version = "1.1.3" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 1337 | dependencies = [ 1338 | "proc-macro2", 1339 | "quote", 1340 | "syn 2.0.38", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "png" 1345 | version = "0.17.13" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" 1348 | dependencies = [ 1349 | "bitflags 1.3.2", 1350 | "crc32fast", 1351 | "fdeflate", 1352 | "flate2", 1353 | "miniz_oxide", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "poly1305" 1358 | version = "0.8.0" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" 1361 | dependencies = [ 1362 | "cpufeatures", 1363 | "opaque-debug", 1364 | "universal-hash", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "pom" 1369 | version = "3.3.0" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "5c2d73a5fe10d458e77534589512104e5aa8ac480aa9ac30b74563274235cce4" 1372 | dependencies = [ 1373 | "bstr", 1374 | ] 1375 | 1376 | [[package]] 1377 | name = "powerfmt" 1378 | version = "0.2.0" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1381 | 1382 | [[package]] 1383 | name = "ppv-lite86" 1384 | version = "0.2.17" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1387 | 1388 | [[package]] 1389 | name = "predicates" 1390 | version = "3.1.3" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 1393 | dependencies = [ 1394 | "anstyle", 1395 | "difflib", 1396 | "float-cmp 0.10.0", 1397 | "normalize-line-endings", 1398 | "predicates-core", 1399 | "regex", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "predicates-core" 1404 | version = "1.0.6" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 1407 | 1408 | [[package]] 1409 | name = "predicates-tree" 1410 | version = "1.0.9" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 1413 | dependencies = [ 1414 | "predicates-core", 1415 | "termtree", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "printpdf" 1420 | version = "0.7.0" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d" 1423 | dependencies = [ 1424 | "allsorts", 1425 | "js-sys", 1426 | "lopdf", 1427 | "owned_ttf_parser", 1428 | "pdf-writer", 1429 | "svg2pdf", 1430 | "time", 1431 | "usvg", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "proc-macro-error" 1436 | version = "1.0.4" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1439 | dependencies = [ 1440 | "proc-macro-error-attr", 1441 | "proc-macro2", 1442 | "quote", 1443 | "syn 1.0.109", 1444 | "version_check", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "proc-macro-error-attr" 1449 | version = "1.0.4" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1452 | dependencies = [ 1453 | "proc-macro2", 1454 | "quote", 1455 | "version_check", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "proc-macro-error-attr2" 1460 | version = "2.0.0" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 1463 | dependencies = [ 1464 | "proc-macro2", 1465 | "quote", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "proc-macro-error2" 1470 | version = "2.0.1" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 1473 | dependencies = [ 1474 | "proc-macro-error-attr2", 1475 | "proc-macro2", 1476 | "quote", 1477 | "syn 2.0.38", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "proc-macro2" 1482 | version = "1.0.69" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 1485 | dependencies = [ 1486 | "unicode-ident", 1487 | ] 1488 | 1489 | [[package]] 1490 | name = "qrcode" 1491 | version = "0.14.1" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" 1494 | dependencies = [ 1495 | "image 0.25.1", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "quote" 1500 | version = "1.0.33" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 1503 | dependencies = [ 1504 | "proc-macro2", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "rand" 1509 | version = "0.8.5" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1512 | dependencies = [ 1513 | "libc", 1514 | "rand_chacha", 1515 | "rand_core", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "rand_chacha" 1520 | version = "0.3.1" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1523 | dependencies = [ 1524 | "ppv-lite86", 1525 | "rand_core", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "rand_core" 1530 | version = "0.6.4" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1533 | dependencies = [ 1534 | "getrandom", 1535 | ] 1536 | 1537 | [[package]] 1538 | name = "rctree" 1539 | version = "0.5.0" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" 1542 | 1543 | [[package]] 1544 | name = "redox_syscall" 1545 | version = "0.3.5" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 1548 | dependencies = [ 1549 | "bitflags 1.3.2", 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "redox_syscall" 1554 | version = "0.4.1" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1557 | dependencies = [ 1558 | "bitflags 1.3.2", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "regex" 1563 | version = "1.10.2" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 1566 | dependencies = [ 1567 | "aho-corasick", 1568 | "memchr", 1569 | "regex-automata", 1570 | "regex-syntax", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "regex-automata" 1575 | version = "0.4.3" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 1578 | dependencies = [ 1579 | "aho-corasick", 1580 | "memchr", 1581 | "regex-syntax", 1582 | ] 1583 | 1584 | [[package]] 1585 | name = "regex-syntax" 1586 | version = "0.8.2" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1589 | 1590 | [[package]] 1591 | name = "roff" 1592 | version = "0.2.1" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" 1595 | 1596 | [[package]] 1597 | name = "roxmltree" 1598 | version = "0.18.1" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" 1601 | dependencies = [ 1602 | "xmlparser", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "roxmltree" 1607 | version = "0.19.0" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" 1610 | 1611 | [[package]] 1612 | name = "rpassword" 1613 | version = "7.3.1" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" 1616 | dependencies = [ 1617 | "libc", 1618 | "rtoolbox", 1619 | "windows-sys", 1620 | ] 1621 | 1622 | [[package]] 1623 | name = "rtoolbox" 1624 | version = "0.0.2" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" 1627 | dependencies = [ 1628 | "libc", 1629 | "windows-sys", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "rust-embed" 1634 | version = "8.2.0" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f" 1637 | dependencies = [ 1638 | "rust-embed-impl", 1639 | "rust-embed-utils", 1640 | "walkdir", 1641 | ] 1642 | 1643 | [[package]] 1644 | name = "rust-embed-impl" 1645 | version = "8.2.0" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" 1648 | dependencies = [ 1649 | "proc-macro2", 1650 | "quote", 1651 | "rust-embed-utils", 1652 | "syn 2.0.38", 1653 | "walkdir", 1654 | ] 1655 | 1656 | [[package]] 1657 | name = "rust-embed-utils" 1658 | version = "8.2.0" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" 1661 | dependencies = [ 1662 | "sha2", 1663 | "walkdir", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "rustc-hash" 1668 | version = "1.1.0" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1671 | 1672 | [[package]] 1673 | name = "rustc_version" 1674 | version = "0.4.0" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1677 | dependencies = [ 1678 | "semver", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "rustix" 1683 | version = "0.38.20" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" 1686 | dependencies = [ 1687 | "bitflags 2.4.1", 1688 | "errno", 1689 | "libc", 1690 | "linux-raw-sys", 1691 | "windows-sys", 1692 | ] 1693 | 1694 | [[package]] 1695 | name = "rustybuzz" 1696 | version = "0.7.0" 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" 1698 | checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a" 1699 | dependencies = [ 1700 | "bitflags 1.3.2", 1701 | "bytemuck", 1702 | "smallvec", 1703 | "ttf-parser 0.18.1", 1704 | "unicode-bidi-mirroring", 1705 | "unicode-ccc", 1706 | "unicode-general-category", 1707 | "unicode-script", 1708 | ] 1709 | 1710 | [[package]] 1711 | name = "ryu" 1712 | version = "1.0.15" 1713 | source = "registry+https://github.com/rust-lang/crates.io-index" 1714 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 1715 | 1716 | [[package]] 1717 | name = "salsa20" 1718 | version = "0.10.2" 1719 | source = "registry+https://github.com/rust-lang/crates.io-index" 1720 | checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" 1721 | dependencies = [ 1722 | "cipher", 1723 | ] 1724 | 1725 | [[package]] 1726 | name = "same-file" 1727 | version = "1.0.6" 1728 | source = "registry+https://github.com/rust-lang/crates.io-index" 1729 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1730 | dependencies = [ 1731 | "winapi-util", 1732 | ] 1733 | 1734 | [[package]] 1735 | name = "scopeguard" 1736 | version = "1.2.0" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1739 | 1740 | [[package]] 1741 | name = "scrypt" 1742 | version = "0.11.0" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" 1745 | dependencies = [ 1746 | "pbkdf2", 1747 | "salsa20", 1748 | "sha2", 1749 | ] 1750 | 1751 | [[package]] 1752 | name = "secrecy" 1753 | version = "0.10.3" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" 1756 | dependencies = [ 1757 | "zeroize", 1758 | ] 1759 | 1760 | [[package]] 1761 | name = "self_cell" 1762 | version = "0.10.3" 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" 1764 | checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" 1765 | dependencies = [ 1766 | "self_cell 1.0.2", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "self_cell" 1771 | version = "1.0.2" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" 1774 | 1775 | [[package]] 1776 | name = "semver" 1777 | version = "1.0.22" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 1780 | 1781 | [[package]] 1782 | name = "serde" 1783 | version = "1.0.193" 1784 | source = "registry+https://github.com/rust-lang/crates.io-index" 1785 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 1786 | dependencies = [ 1787 | "serde_derive", 1788 | ] 1789 | 1790 | [[package]] 1791 | name = "serde_derive" 1792 | version = "1.0.193" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 1795 | dependencies = [ 1796 | "proc-macro2", 1797 | "quote", 1798 | "syn 2.0.38", 1799 | ] 1800 | 1801 | [[package]] 1802 | name = "sha2" 1803 | version = "0.10.8" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1806 | dependencies = [ 1807 | "cfg-if", 1808 | "cpufeatures", 1809 | "digest", 1810 | ] 1811 | 1812 | [[package]] 1813 | name = "simd-adler32" 1814 | version = "0.3.7" 1815 | source = "registry+https://github.com/rust-lang/crates.io-index" 1816 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1817 | 1818 | [[package]] 1819 | name = "simplecss" 1820 | version = "0.2.1" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" 1823 | dependencies = [ 1824 | "log", 1825 | ] 1826 | 1827 | [[package]] 1828 | name = "siphasher" 1829 | version = "0.3.11" 1830 | source = "registry+https://github.com/rust-lang/crates.io-index" 1831 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1832 | 1833 | [[package]] 1834 | name = "slotmap" 1835 | version = "1.0.7" 1836 | source = "registry+https://github.com/rust-lang/crates.io-index" 1837 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 1838 | dependencies = [ 1839 | "version_check", 1840 | ] 1841 | 1842 | [[package]] 1843 | name = "smallvec" 1844 | version = "1.11.1" 1845 | source = "registry+https://github.com/rust-lang/crates.io-index" 1846 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 1847 | 1848 | [[package]] 1849 | name = "static_assertions" 1850 | version = "1.1.0" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1853 | 1854 | [[package]] 1855 | name = "strict-num" 1856 | version = "0.1.1" 1857 | source = "registry+https://github.com/rust-lang/crates.io-index" 1858 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 1859 | dependencies = [ 1860 | "float-cmp 0.9.0", 1861 | ] 1862 | 1863 | [[package]] 1864 | name = "strsim" 1865 | version = "0.11.0" 1866 | source = "registry+https://github.com/rust-lang/crates.io-index" 1867 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 1868 | 1869 | [[package]] 1870 | name = "subtle" 1871 | version = "2.5.0" 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" 1873 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 1874 | 1875 | [[package]] 1876 | name = "svg2pdf" 1877 | version = "0.8.0" 1878 | source = "registry+https://github.com/rust-lang/crates.io-index" 1879 | checksum = "a4283ae77d25dbee75aa1bce6b5bc8e7d802960135d6de306eaec0a5241d80c8" 1880 | dependencies = [ 1881 | "image 0.24.7", 1882 | "miniz_oxide", 1883 | "pdf-writer", 1884 | "usvg", 1885 | ] 1886 | 1887 | [[package]] 1888 | name = "svgtypes" 1889 | version = "0.11.0" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "ed4b0611e7f3277f68c0fa18e385d9e2d26923691379690039548f867cef02a7" 1892 | dependencies = [ 1893 | "kurbo", 1894 | "siphasher", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "syn" 1899 | version = "1.0.109" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1902 | dependencies = [ 1903 | "proc-macro2", 1904 | "unicode-ident", 1905 | ] 1906 | 1907 | [[package]] 1908 | name = "syn" 1909 | version = "2.0.38" 1910 | source = "registry+https://github.com/rust-lang/crates.io-index" 1911 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" 1912 | dependencies = [ 1913 | "proc-macro2", 1914 | "quote", 1915 | "unicode-ident", 1916 | ] 1917 | 1918 | [[package]] 1919 | name = "tempfile" 1920 | version = "3.8.0" 1921 | source = "registry+https://github.com/rust-lang/crates.io-index" 1922 | checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" 1923 | dependencies = [ 1924 | "cfg-if", 1925 | "fastrand", 1926 | "redox_syscall 0.3.5", 1927 | "rustix", 1928 | "windows-sys", 1929 | ] 1930 | 1931 | [[package]] 1932 | name = "termtree" 1933 | version = "0.4.1" 1934 | source = "registry+https://github.com/rust-lang/crates.io-index" 1935 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1936 | 1937 | [[package]] 1938 | name = "thiserror" 1939 | version = "1.0.50" 1940 | source = "registry+https://github.com/rust-lang/crates.io-index" 1941 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 1942 | dependencies = [ 1943 | "thiserror-impl", 1944 | ] 1945 | 1946 | [[package]] 1947 | name = "thiserror-impl" 1948 | version = "1.0.50" 1949 | source = "registry+https://github.com/rust-lang/crates.io-index" 1950 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 1951 | dependencies = [ 1952 | "proc-macro2", 1953 | "quote", 1954 | "syn 2.0.38", 1955 | ] 1956 | 1957 | [[package]] 1958 | name = "thread_local" 1959 | version = "1.1.7" 1960 | source = "registry+https://github.com/rust-lang/crates.io-index" 1961 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 1962 | dependencies = [ 1963 | "cfg-if", 1964 | "once_cell", 1965 | ] 1966 | 1967 | [[package]] 1968 | name = "time" 1969 | version = "0.3.36" 1970 | source = "registry+https://github.com/rust-lang/crates.io-index" 1971 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1972 | dependencies = [ 1973 | "deranged", 1974 | "itoa", 1975 | "num-conv", 1976 | "powerfmt", 1977 | "serde", 1978 | "time-core", 1979 | "time-macros", 1980 | ] 1981 | 1982 | [[package]] 1983 | name = "time-core" 1984 | version = "0.1.2" 1985 | source = "registry+https://github.com/rust-lang/crates.io-index" 1986 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1987 | 1988 | [[package]] 1989 | name = "time-macros" 1990 | version = "0.2.18" 1991 | source = "registry+https://github.com/rust-lang/crates.io-index" 1992 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1993 | dependencies = [ 1994 | "num-conv", 1995 | "time-core", 1996 | ] 1997 | 1998 | [[package]] 1999 | name = "tiny-skia-path" 2000 | version = "0.10.0" 2001 | source = "registry+https://github.com/rust-lang/crates.io-index" 2002 | checksum = "2f60aa35c89ac2687ace1a2556eaaea68e8c0d47408a2e3e7f5c98a489e7281c" 2003 | dependencies = [ 2004 | "arrayref", 2005 | "bytemuck", 2006 | "strict-num", 2007 | ] 2008 | 2009 | [[package]] 2010 | name = "tinystr" 2011 | version = "0.7.4" 2012 | source = "registry+https://github.com/rust-lang/crates.io-index" 2013 | checksum = "d5d0e245e80bdc9b4e5356fc45a72184abbc3861992603f515270e9340f5a219" 2014 | dependencies = [ 2015 | "displaydoc", 2016 | ] 2017 | 2018 | [[package]] 2019 | name = "tinyvec" 2020 | version = "1.6.0" 2021 | source = "registry+https://github.com/rust-lang/crates.io-index" 2022 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 2023 | dependencies = [ 2024 | "tinyvec_macros", 2025 | ] 2026 | 2027 | [[package]] 2028 | name = "tinyvec_macros" 2029 | version = "0.1.1" 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" 2031 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2032 | 2033 | [[package]] 2034 | name = "toml" 2035 | version = "0.5.11" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 2038 | dependencies = [ 2039 | "serde", 2040 | ] 2041 | 2042 | [[package]] 2043 | name = "ttf-parser" 2044 | version = "0.18.1" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" 2047 | 2048 | [[package]] 2049 | name = "ttf-parser" 2050 | version = "0.19.2" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" 2053 | 2054 | [[package]] 2055 | name = "type-map" 2056 | version = "0.4.0" 2057 | source = "registry+https://github.com/rust-lang/crates.io-index" 2058 | checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" 2059 | dependencies = [ 2060 | "rustc-hash", 2061 | ] 2062 | 2063 | [[package]] 2064 | name = "typenum" 2065 | version = "1.17.0" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2068 | 2069 | [[package]] 2070 | name = "ucd-trie" 2071 | version = "0.1.6" 2072 | source = "registry+https://github.com/rust-lang/crates.io-index" 2073 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 2074 | 2075 | [[package]] 2076 | name = "unic-langid" 2077 | version = "0.9.1" 2078 | source = "registry+https://github.com/rust-lang/crates.io-index" 2079 | checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" 2080 | dependencies = [ 2081 | "unic-langid-impl", 2082 | ] 2083 | 2084 | [[package]] 2085 | name = "unic-langid-impl" 2086 | version = "0.9.1" 2087 | source = "registry+https://github.com/rust-lang/crates.io-index" 2088 | checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" 2089 | dependencies = [ 2090 | "serde", 2091 | "tinystr", 2092 | ] 2093 | 2094 | [[package]] 2095 | name = "unicode-bidi" 2096 | version = "0.3.13" 2097 | source = "registry+https://github.com/rust-lang/crates.io-index" 2098 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 2099 | 2100 | [[package]] 2101 | name = "unicode-bidi-mirroring" 2102 | version = "0.1.0" 2103 | source = "registry+https://github.com/rust-lang/crates.io-index" 2104 | checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" 2105 | 2106 | [[package]] 2107 | name = "unicode-canonical-combining-class" 2108 | version = "0.5.0" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "6925586af9268182c711e47c0853ed84131049efaca41776d0ca97f983865c32" 2111 | 2112 | [[package]] 2113 | name = "unicode-ccc" 2114 | version = "0.1.2" 2115 | source = "registry+https://github.com/rust-lang/crates.io-index" 2116 | checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" 2117 | 2118 | [[package]] 2119 | name = "unicode-general-category" 2120 | version = "0.6.0" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" 2123 | 2124 | [[package]] 2125 | name = "unicode-ident" 2126 | version = "1.0.12" 2127 | source = "registry+https://github.com/rust-lang/crates.io-index" 2128 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 2129 | 2130 | [[package]] 2131 | name = "unicode-joining-type" 2132 | version = "0.7.0" 2133 | source = "registry+https://github.com/rust-lang/crates.io-index" 2134 | checksum = "22f8cb47ccb8bc750808755af3071da4a10dcd147b68fc874b7ae4b12543f6f5" 2135 | 2136 | [[package]] 2137 | name = "unicode-script" 2138 | version = "0.5.5" 2139 | source = "registry+https://github.com/rust-lang/crates.io-index" 2140 | checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" 2141 | 2142 | [[package]] 2143 | name = "unicode-vo" 2144 | version = "0.1.0" 2145 | source = "registry+https://github.com/rust-lang/crates.io-index" 2146 | checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" 2147 | 2148 | [[package]] 2149 | name = "universal-hash" 2150 | version = "0.5.1" 2151 | source = "registry+https://github.com/rust-lang/crates.io-index" 2152 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 2153 | dependencies = [ 2154 | "crypto-common", 2155 | "subtle", 2156 | ] 2157 | 2158 | [[package]] 2159 | name = "usvg" 2160 | version = "0.35.0" 2161 | source = "registry+https://github.com/rust-lang/crates.io-index" 2162 | checksum = "14d09ddfb0d93bf84824c09336d32e42f80961a9d1680832eb24fdf249ce11e6" 2163 | dependencies = [ 2164 | "base64", 2165 | "log", 2166 | "pico-args", 2167 | "usvg-parser", 2168 | "usvg-text-layout", 2169 | "usvg-tree", 2170 | "xmlwriter", 2171 | ] 2172 | 2173 | [[package]] 2174 | name = "usvg-parser" 2175 | version = "0.35.0" 2176 | source = "registry+https://github.com/rust-lang/crates.io-index" 2177 | checksum = "d19bf93d230813599927d88557014e0908ecc3531666d47c634c6838bc8db408" 2178 | dependencies = [ 2179 | "data-url", 2180 | "flate2", 2181 | "imagesize", 2182 | "kurbo", 2183 | "log", 2184 | "roxmltree 0.18.1", 2185 | "simplecss", 2186 | "siphasher", 2187 | "svgtypes", 2188 | "usvg-tree", 2189 | ] 2190 | 2191 | [[package]] 2192 | name = "usvg-text-layout" 2193 | version = "0.35.0" 2194 | source = "registry+https://github.com/rust-lang/crates.io-index" 2195 | checksum = "035044604e89652c0a2959b8b356946997a52649ba6cade45928c2842376feb4" 2196 | dependencies = [ 2197 | "fontdb", 2198 | "kurbo", 2199 | "log", 2200 | "rustybuzz", 2201 | "unicode-bidi", 2202 | "unicode-script", 2203 | "unicode-vo", 2204 | "usvg-tree", 2205 | ] 2206 | 2207 | [[package]] 2208 | name = "usvg-tree" 2209 | version = "0.35.0" 2210 | source = "registry+https://github.com/rust-lang/crates.io-index" 2211 | checksum = "7939a7e4ed21cadb5d311d6339730681c3e24c3e81d60065be80e485d3fc8b92" 2212 | dependencies = [ 2213 | "rctree", 2214 | "strict-num", 2215 | "svgtypes", 2216 | "tiny-skia-path", 2217 | ] 2218 | 2219 | [[package]] 2220 | name = "utf8parse" 2221 | version = "0.2.1" 2222 | source = "registry+https://github.com/rust-lang/crates.io-index" 2223 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 2224 | 2225 | [[package]] 2226 | name = "version_check" 2227 | version = "0.9.4" 2228 | source = "registry+https://github.com/rust-lang/crates.io-index" 2229 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 2230 | 2231 | [[package]] 2232 | name = "wait-timeout" 2233 | version = "0.2.0" 2234 | source = "registry+https://github.com/rust-lang/crates.io-index" 2235 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 2236 | dependencies = [ 2237 | "libc", 2238 | ] 2239 | 2240 | [[package]] 2241 | name = "walkdir" 2242 | version = "2.4.0" 2243 | source = "registry+https://github.com/rust-lang/crates.io-index" 2244 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 2245 | dependencies = [ 2246 | "same-file", 2247 | "winapi-util", 2248 | ] 2249 | 2250 | [[package]] 2251 | name = "wasi" 2252 | version = "0.11.0+wasi-snapshot-preview1" 2253 | source = "registry+https://github.com/rust-lang/crates.io-index" 2254 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2255 | 2256 | [[package]] 2257 | name = "wasm-bindgen" 2258 | version = "0.2.87" 2259 | source = "registry+https://github.com/rust-lang/crates.io-index" 2260 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 2261 | dependencies = [ 2262 | "cfg-if", 2263 | "wasm-bindgen-macro", 2264 | ] 2265 | 2266 | [[package]] 2267 | name = "wasm-bindgen-backend" 2268 | version = "0.2.87" 2269 | source = "registry+https://github.com/rust-lang/crates.io-index" 2270 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 2271 | dependencies = [ 2272 | "bumpalo", 2273 | "log", 2274 | "once_cell", 2275 | "proc-macro2", 2276 | "quote", 2277 | "syn 2.0.38", 2278 | "wasm-bindgen-shared", 2279 | ] 2280 | 2281 | [[package]] 2282 | name = "wasm-bindgen-macro" 2283 | version = "0.2.87" 2284 | source = "registry+https://github.com/rust-lang/crates.io-index" 2285 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 2286 | dependencies = [ 2287 | "quote", 2288 | "wasm-bindgen-macro-support", 2289 | ] 2290 | 2291 | [[package]] 2292 | name = "wasm-bindgen-macro-support" 2293 | version = "0.2.87" 2294 | source = "registry+https://github.com/rust-lang/crates.io-index" 2295 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 2296 | dependencies = [ 2297 | "proc-macro2", 2298 | "quote", 2299 | "syn 2.0.38", 2300 | "wasm-bindgen-backend", 2301 | "wasm-bindgen-shared", 2302 | ] 2303 | 2304 | [[package]] 2305 | name = "wasm-bindgen-shared" 2306 | version = "0.2.87" 2307 | source = "registry+https://github.com/rust-lang/crates.io-index" 2308 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 2309 | 2310 | [[package]] 2311 | name = "weezl" 2312 | version = "0.1.7" 2313 | source = "registry+https://github.com/rust-lang/crates.io-index" 2314 | checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" 2315 | 2316 | [[package]] 2317 | name = "winapi" 2318 | version = "0.3.9" 2319 | source = "registry+https://github.com/rust-lang/crates.io-index" 2320 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2321 | dependencies = [ 2322 | "winapi-i686-pc-windows-gnu", 2323 | "winapi-x86_64-pc-windows-gnu", 2324 | ] 2325 | 2326 | [[package]] 2327 | name = "winapi-i686-pc-windows-gnu" 2328 | version = "0.4.0" 2329 | source = "registry+https://github.com/rust-lang/crates.io-index" 2330 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2331 | 2332 | [[package]] 2333 | name = "winapi-util" 2334 | version = "0.1.6" 2335 | source = "registry+https://github.com/rust-lang/crates.io-index" 2336 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 2337 | dependencies = [ 2338 | "winapi", 2339 | ] 2340 | 2341 | [[package]] 2342 | name = "winapi-x86_64-pc-windows-gnu" 2343 | version = "0.4.0" 2344 | source = "registry+https://github.com/rust-lang/crates.io-index" 2345 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2346 | 2347 | [[package]] 2348 | name = "windows-sys" 2349 | version = "0.48.0" 2350 | source = "registry+https://github.com/rust-lang/crates.io-index" 2351 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2352 | dependencies = [ 2353 | "windows-targets", 2354 | ] 2355 | 2356 | [[package]] 2357 | name = "windows-targets" 2358 | version = "0.48.5" 2359 | source = "registry+https://github.com/rust-lang/crates.io-index" 2360 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2361 | dependencies = [ 2362 | "windows_aarch64_gnullvm", 2363 | "windows_aarch64_msvc", 2364 | "windows_i686_gnu", 2365 | "windows_i686_msvc", 2366 | "windows_x86_64_gnu", 2367 | "windows_x86_64_gnullvm", 2368 | "windows_x86_64_msvc", 2369 | ] 2370 | 2371 | [[package]] 2372 | name = "windows_aarch64_gnullvm" 2373 | version = "0.48.5" 2374 | source = "registry+https://github.com/rust-lang/crates.io-index" 2375 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2376 | 2377 | [[package]] 2378 | name = "windows_aarch64_msvc" 2379 | version = "0.48.5" 2380 | source = "registry+https://github.com/rust-lang/crates.io-index" 2381 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2382 | 2383 | [[package]] 2384 | name = "windows_i686_gnu" 2385 | version = "0.48.5" 2386 | source = "registry+https://github.com/rust-lang/crates.io-index" 2387 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2388 | 2389 | [[package]] 2390 | name = "windows_i686_msvc" 2391 | version = "0.48.5" 2392 | source = "registry+https://github.com/rust-lang/crates.io-index" 2393 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2394 | 2395 | [[package]] 2396 | name = "windows_x86_64_gnu" 2397 | version = "0.48.5" 2398 | source = "registry+https://github.com/rust-lang/crates.io-index" 2399 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2400 | 2401 | [[package]] 2402 | name = "windows_x86_64_gnullvm" 2403 | version = "0.48.5" 2404 | source = "registry+https://github.com/rust-lang/crates.io-index" 2405 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2406 | 2407 | [[package]] 2408 | name = "windows_x86_64_msvc" 2409 | version = "0.48.5" 2410 | source = "registry+https://github.com/rust-lang/crates.io-index" 2411 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2412 | 2413 | [[package]] 2414 | name = "x25519-dalek" 2415 | version = "2.0.1" 2416 | source = "registry+https://github.com/rust-lang/crates.io-index" 2417 | checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" 2418 | dependencies = [ 2419 | "curve25519-dalek", 2420 | "rand_core", 2421 | "serde", 2422 | "zeroize", 2423 | ] 2424 | 2425 | [[package]] 2426 | name = "xmlparser" 2427 | version = "0.13.6" 2428 | source = "registry+https://github.com/rust-lang/crates.io-index" 2429 | checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" 2430 | 2431 | [[package]] 2432 | name = "xmlwriter" 2433 | version = "0.1.0" 2434 | source = "registry+https://github.com/rust-lang/crates.io-index" 2435 | checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" 2436 | 2437 | [[package]] 2438 | name = "zeroize" 2439 | version = "1.6.0" 2440 | source = "registry+https://github.com/rust-lang/crates.io-index" 2441 | checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" 2442 | dependencies = [ 2443 | "zeroize_derive", 2444 | ] 2445 | 2446 | [[package]] 2447 | name = "zeroize_derive" 2448 | version = "1.4.2" 2449 | source = "registry+https://github.com/rust-lang/crates.io-index" 2450 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 2451 | dependencies = [ 2452 | "proc-macro2", 2453 | "quote", 2454 | "syn 2.0.38", 2455 | ] 2456 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paper-age" 3 | description = "Easy and secure paper backups of secrets" 4 | version = "1.3.4" 5 | edition = "2021" 6 | rust-version = "1.74" 7 | repository = "https://github.com/matiaskorhonen/paper-age" 8 | authors = ["Matias Korhonen "] 9 | license = "MIT" 10 | categories = ["command-line-utilities", "cryptography"] 11 | 12 | [package.metadata.release] 13 | pre-release-commit-message = "Release {{crate_name}} version {{version}}" 14 | tag-message = "Release {{crate_name}} version {{version}}" 15 | sign-tag = true 16 | pre-release-replacements = [ 17 | {file="README.md", search="download/v[0-9\\.-]+/paper-age", replace="download/{{tag_name}}/paper-age", min=3} , 18 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, 19 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 20 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, 21 | {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, 22 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/matiaskorhonen/paper-age/compare/{{tag_name}}...HEAD", exactly=1}, 23 | ] 24 | 25 | [dependencies] 26 | age = { version = "0.11.1", features = ["armor"] } 27 | clap = { version = "4.5", features = ["derive"] } 28 | clap-verbosity-flag = "3.0" 29 | exitcode = "1.1.2" 30 | printpdf = { version = "0.7.0", features = ["svg", "font_subsetting"] } 31 | qrcode = "0.14.1" 32 | rpassword = "7" 33 | log = "0.4" 34 | env_logger = "0.11" 35 | 36 | [dev-dependencies] 37 | assert_cmd = "2.0" 38 | assert_fs = "1.1" 39 | predicates = "3.1" 40 | 41 | [build-dependencies] 42 | clap = { version = "4.5", features = ["derive"] } 43 | clap_complete = "4.5" 44 | clap-verbosity-flag = "3.0" 45 | clap_mangen = { version = "0.2" } 46 | path-absolutize = "3.1" 47 | printpdf = { version = "0.7.0", features = ["svg"] } 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matias Korhonen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # PaperAge 8 | 9 | Easy and secure paper backups of (smallish) secrets using the Age format ([age-encryption.org/v1](https://age-encryption.org/v1)). 10 | 11 | [![Rust build](https://github.com/matiaskorhonen/paper-age/actions/workflows/rust.yml/badge.svg)](https://github.com/matiaskorhonen/paper-age/actions/workflows/rust.yml) [![codecov](https://codecov.io/gh/matiaskorhonen/paper-age/graph/badge.svg?token=KM9VSJ6CCT)](https://codecov.io/gh/matiaskorhonen/paper-age) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/matiaskorhonen/paper-age)](https://github.com/matiaskorhonen/paper-age/releases/latest) [![Crates.io](https://img.shields.io/crates/v/paper-age)](https://crates.io/crates/paper-age) 12 | 13 | ## Features 14 | 15 | * Accepts input either from a file or stdin 16 | * Encrypts that input with a passphrase 17 | * Outputs a PDF with a QR code of the encrypted ciphertext 18 | * Support for both A4 and letter paper sizes 19 | * The error correction level of the QR code is optimised (less data → more error correction) 20 | * The passphrase **isn't** rendered on the PDF so that it can be printed on an untrusted printer (for example at work or the library) 21 | * You don't need PaperAge to recover from the backup: use any QR code scanner and [any implementation of Age](https://github.com/FiloSottile/awesome-age#implementations). 22 | 23 | ## Limitations 24 | 25 | * The maximum input size is about 1.9 KiB as QR codes cannot encode arbitrarily large payloads 26 | * Only passphrase-based encryption is supported at the moment 27 | 28 | ## Threat models and use cases 29 | 30 | * The main use case is keeping secrets, such as TFA recovery codes, in a safe place 31 | * Adding the passphrase by hand allows the use of public printers, for example in libraries, offices, copy shops, and so forth 32 | * For extra protection, memorize the passphrase or store it separately from the printout 33 | * Needing to scan and decrypt protects against unsophisticated adversaries even if the passphrase is right there (the average burglar isn't going to care about your Mastodon account) 34 | * If you need protection from nation-states or other advanced threats, look elsewhere 35 | 36 | ## Example 37 | 38 | This is what the output PDF looks like (left A4, right letter). The QR code is easily readable with an iPhone (or any modern smartphone). 39 | 40 | ![Preview of the A4 and letter PDFs](https://github.com/user-attachments/assets/53c4ef67-298a-4522-a2e3-600693430963) 41 | 42 | If you want to try decoding it yourself, the passphrase is `snakeoil`. 43 | 44 | ## Installation 45 | 46 | Release builds are available for macOS (Apple Silicon and Intel), Linux (ARM and x86-64), and Windows (x86-64). 47 | 48 | While the Windows build *should* work on both Windows 10 and 11, only Windows 11 is “officially” supported. 49 | 50 | ### Homebrew 51 | 52 | Add the PaperAge Tap to install the latest version with Homebrew: 53 | 54 | ```sh 55 | brew tap matiaskorhonen/paper-age 56 | brew install paper-age 57 | ``` 58 | 59 | ### Binary 60 | 61 | Download the latest release from the [Releases](https://github.com/matiaskorhonen/paper-age/releases) page, extract the files, and install the `paper-age` binary somewhere in `PATH` (for example `/usr/local/bin`). 62 | 63 | ```sh 64 | # Download the latest release (pick your OS) 65 | # macOS (Intel or Apple Silicon): 66 | curl -Lo paper-age.tar.gz https://github.com/matiaskorhonen/paper-age/releases/download/v1.3.4/paper-age-universal-apple-darwin.tar.gz 67 | # Linux (x86-64): 68 | curl -Lo paper-age.tar.gz https://github.com/matiaskorhonen/paper-age/releases/download/v1.3.4/paper-age-x86_64-unknown-linux-gnu.tar.gz 69 | # Linux (ARM): 70 | curl -Lo paper-age.tar.gz https://github.com/matiaskorhonen/paper-age/releases/download/v1.3.4/paper-age-aarch64-unknown-linux-gnu.tar.gz 71 | 72 | # Verify the artifact attestation using the GitHub CLI tool (optional) 73 | gh attestation verify paper-age.tar.gz --repo matiaskorhonen/paper-age 74 | 75 | # Extract the files 76 | tar -xf paper-age.tar.gz 77 | 78 | # Install the binary in /usr/local/bin 79 | sudo install paper-age /usr/local/bin/ 80 | # Or: sudo mv paper-age /usr/local/bin/ 81 | ``` 82 | 83 | ### Cargo 84 | 85 | If you already have Rust installed, PaperAge can be installed with Cargo: 86 | 87 | ```sh 88 | cargo install paper-age 89 | ``` 90 | 91 | ### Artifact attestations 92 | 93 | Starting with v1.3.1, PaperAge releases have [artifact attestations](https://github.com/matiaskorhonen/paper-age/attestations). Attestations are generated using [GitHub's tooling](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). 94 | 95 | ## Usage 96 | 97 | ``` 98 | paper-age [OPTIONS] [INPUT] 99 | ``` 100 | 101 | ### **Arguments** 102 | 103 | * `` — The path to the file to read. Defaults to standard input. Max. ~1.9KB. 104 | 105 | ### **Options** 106 | 107 | * `-t`, `--title ` — Page title (max. 64 characters) 108 | 109 | Default value: `PaperAge` 110 | * `-n`, `--notes-label <NOTES_LABEL>` — Notes label below the QR code (max. 32 characters) 111 | 112 | Default value: `Passphrase:` 113 | * `--skip-notes-line` — Skip the notes placeholder line (e.g. Passphrase: ________) 114 | * `-o`, `--output <OUTPUT>` — Output file name. Use - for STDOUT. 115 | 116 | Default value: `out.pdf` 117 | * `-s`, `--page-size <PAGE_SIZE>` — Paper size [default: `a4`] [possible values: `a4`, `letter`] 118 | * `-f`, `--force` — Overwrite the output file if it already exists 119 | * `-g`, `--grid` — Draw a grid pattern for debugging layout issues 120 | * `--fonts-license` — Print out the license for the embedded fonts 121 | * `-v`, `--verbose...` — More output per occurrence 122 | * `-q`, `--quiet...` — Less output per occurrence 123 | * `-h`, `--help` — Print help 124 | * `-V`, `--version` — Print version 125 | 126 | ## Notes/passphrase field 127 | 128 | The notes field below the QR code can be customised with the `--notes-label <TEXT>` and `--skip-notes-line` arguments. There's no enforced limit for the label length but eventually the text will overflow the page bounds. 129 | 130 | ### Examples 131 | 132 | * Print a placeholder for a hint instead of the passphrase: 133 | 134 | ```sh 135 | paper-age --notes-label="Hint:" 136 | ``` 137 | 138 | * Print a timestamp instead of the notes field: 139 | 140 | ```sh 141 | paper-age --notes-label="Created at: $(date -Iseconds)" --skip-notes-line 142 | ``` 143 | 144 | ## Compression 145 | 146 | PaperAge is entirely agnostic about the input file type. If you need to squeeze in more data, you can apply compression to the input file before passing it on to PaperAge, for example: 147 | 148 | ```sh 149 | gzip --best --stdout in.txt | paper-age --output=compressed.pdf --title="in.txt.gz" 150 | ``` 151 | 152 | Compression ratios vary wildly depending on the input data, so whether or not this is worth it is up to you. 153 | 154 | ## Scanning the QR code 155 | 156 | On iOS, it's best to use the [Code Scanner](https://support.apple.com/en-gb/guide/iphone/iphe8bda8762/ios) from Control Center instead of the Camera app. The Code Scanner lets you copy the QR code contents to the clipboard instead of just searching for it. 157 | 158 | On Android, the built-in camera app should let you copy the QR code contents to the clipboard. The [Google Lens](https://play.google.com/store/apps/details?id=com.google.ar.lens&hl=en) app seems to work fine too. 159 | 160 | ## Development 161 | 162 | Run the latest from git locally, assuming you have already [installed Rust](https://www.rust-lang.org/learn/get-started): 163 | 164 | 1. Pull this repo 165 | 2. Run the tests: `cargo test` 166 | 3. Get help: `cargo run -- -h` 167 | 4. Encrypt from stdin: `echo "Hello World" | cargo run -- --title="secrets from stdin" --out="stdin.pdf"` 168 | 5. Run with maximum verbosity: `echo "Hello World" | cargo run -- -vvvv` 169 | 170 | ### Releases 171 | 172 | Releases are compiled and released on GitHub when new versions are tagged in git. 173 | 174 | Use [cargo release](https://github.com/crate-ci/cargo-release) to tag and publish a new version, for example: 175 | 176 | ```sh 177 | cargo release 1.2.3 178 | ``` 179 | 180 | ⚠️ Append `--execute` to the command to actually execute the release. 181 | 182 | ## License & Credits 183 | 184 | PaperAge is released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details. 185 | 186 | Includes the SIL Open Font Licensed [IBM Plex Mono](https://www.ibm.com/plex/) font. See [IBMPlexMono-LICENSE.txt](src/assets/fonts/IBMPlexMono-LICENSE.txt). 187 | 188 | Uses the Rust implementation of Age from [github.com/str4d/rage](https://github.com/str4d/rage) and the [printpdf](https://github.com/fschutt/printpdf) library. 189 | 190 | Thanks to [Ariel Salminen](https://arie.ls) for the PaperAge icon. 191 | -------------------------------------------------------------------------------- /bin/generate-preview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script can be used to generate a preview image for the README 4 | 5 | set -euo pipefail 6 | 7 | function generate_preview { 8 | export PAPERAGE_PASSPHRASE="snakeoil" 9 | 10 | echo "Generating PDFs" 11 | echo "Hello World" | cargo run "$@" -- -vvv -f --page-size a4 -o a4.pdf 12 | echo "Hello World" | cargo run "$@" -- -vvv -f --page-size letter -o letter.pdf 13 | 14 | echo "Generating preview image" 15 | magick montage \ 16 | -background "rgba(255,255,255,0)" \ 17 | -density 300 \ 18 | -geometry 1024x+16+16 \ 19 | -gravity North \ 20 | -border 1 \ 21 | -bordercolor "#CCCCCC" \ 22 | -tile 2x a4.pdf letter.pdf miff:- | magick - -trim preview.png 23 | 24 | unset PAPERAGE_PASSPHRASE 25 | } 26 | 27 | generate_preview "$@" 28 | -------------------------------------------------------------------------------- /bin/generate-variety: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script can be used to generate a variety of PDFs for testing while 4 | # developing PaperAge. 5 | 6 | set -euo pipefail 7 | 8 | function generate_variety { 9 | export PAPERAGE_PASSPHRASE="snakeoil" 10 | 11 | # echo "Small amount of data" 12 | openssl rand -hex 6 | cargo run "$@" -- -vvv -f --page-size a4 -o a4-small.pdf 13 | openssl rand -hex 6 | cargo run "$@" -- -vvv -f --page-size letter -o letter-small.pdf 14 | 15 | # echo "Medium amount of data" 16 | openssl rand -hex 256 | cargo run "$@" -- -vvv -f --page-size a4 -o a4-medium.pdf 17 | openssl rand -hex 256 | cargo run "$@" -- -vvv -f --page-size letter -o letter-medium.pdf 18 | 19 | # echo "Large amount of data" 20 | openssl rand -hex 900 | cargo run "$@" -- -vvv -f --page-size a4 -o a4-large.pdf 21 | openssl rand -hex 900 | cargo run "$@" -- -vvv -f --page-size letter -o letter-large.pdf 22 | 23 | unset PAPERAGE_PASSPHRASE 24 | } 25 | 26 | generate_variety "$@" 27 | -------------------------------------------------------------------------------- /bin/snapshots-comment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | 5 | HELP_MESSAGE = <<~EOS 6 | Usage: ./bin/format-snapshots JSON_ARRAY_OF_SNAPSHOTS 7 | EOS 8 | 9 | if ARGV.length != 1 10 | puts HELP_MESSAGE 11 | exit 1 12 | end 13 | 14 | urls = JSON.parse(ARGV[0]) 15 | 16 | snapshots = urls.reduce({}) do |hash, url| 17 | file_type = url.end_with?(".pdf") ? :pdf : :png 18 | page_size = url.include?("letter") ? :letter : :a4 19 | version = case url 20 | when /-release\./ 21 | :release 22 | when /-current\./ 23 | :current 24 | when /-diff\./ 25 | :diff 26 | end 27 | 28 | hash[page_size] ||= {} 29 | hash[page_size][:"#{version}_#{file_type}"] = url 30 | hash 31 | end 32 | 33 | table = <<~TABLE 34 | ## PaperAge visual snapshots 35 | 36 | Compare the output of the latest release version of PaperAge with the results from the current commit. 37 | 38 | | | Latest release | Current commit | Diff | 39 | |---|---|---|---| 40 | | A4 | [![A4 release](#{snapshots.dig(:a4, :release_png)})](#{snapshots.dig(:a4, :release_pdf)}) | [![A4 current](#{snapshots.dig(:a4, :current_png)})](#{snapshots.dig(:a4, :current_pdf)}) | ![A4 diff](#{snapshots.dig(:a4, :diff_png)}) | 41 | | Letter | [![Letter release](#{snapshots.dig(:letter, :release_png)})](#{snapshots.dig(:letter, :release_pdf)}) | [![Letter current](#{snapshots.dig(:letter, :current_png)})](#{snapshots.dig(:letter, :current_pdf)}) | ![Letter diff](#{snapshots.dig(:letter, :diff_png)}) | 42 | 43 | Encryption passphrase: `#{ENV['PAPERAGE_PASSPHRASE']}` 44 | 45 | *Note: Snapshots are deleted after 30 days.* 46 | 47 | TABLE 48 | 49 | File.open('visual-snapshots.tmp', 'w') { _1.write(table) } 50 | 51 | puts "Wrote visual-snapshots.tmp" 52 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use clap::CommandFactory; 2 | use clap_complete::{generate_to, shells::Shell}; 3 | use path_absolutize::*; 4 | 5 | #[path = "src/cli.rs"] 6 | mod cli; 7 | 8 | #[path = "src/page.rs"] 9 | pub mod page; 10 | 11 | fn main() -> std::io::Result<()> { 12 | let out_dir = 13 | std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or(std::io::ErrorKind::NotFound)?); 14 | let mut cmd = cli::Args::command(); 15 | 16 | let man = clap_mangen::Man::new(cmd.clone()); 17 | let mut buffer: Vec<u8> = Default::default(); 18 | man.render(&mut buffer)?; 19 | 20 | // Create a man directory at the same level as the binary, even though build 21 | // scripts shouldn't 22 | let man_dir = &out_dir.join("../../../man"); 23 | let absolute_man_dir = man_dir.absolutize()?; 24 | std::fs::create_dir_all(&absolute_man_dir)?; 25 | let man_path = absolute_man_dir.join("paper-age.1"); 26 | std::fs::write(man_path, buffer)?; 27 | 28 | // Create a completion directory the same level as the binary 29 | let completion_dir = out_dir.join("../../../completion"); 30 | let absolute_completion_dir = completion_dir.absolutize()?; 31 | std::fs::create_dir_all(absolute_completion_dir.clone())?; 32 | for shell in [Shell::Bash, Shell::Fish, Shell::Zsh] { 33 | generate_to( 34 | shell, 35 | &mut cmd, 36 | "paper-age", 37 | absolute_completion_dir.as_ref(), 38 | )?; 39 | } 40 | 41 | // Re-run if the cli or page files change 42 | println!("cargo:rerun-if-changed=src/cli.rs"); 43 | println!("cargo:rerun-if-changed=src/page.rs"); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | target: auto 7 | threshold: 0.5% 8 | patch: 9 | default: 10 | informational: true 11 | -------------------------------------------------------------------------------- /src/assets/fonts/IBMPlexMono-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /src/assets/fonts/IBMPlexMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiaskorhonen/paper-age/7fb7c114f22e902da8f4398b230dbae073a1b1ab/src/assets/fonts/IBMPlexMono-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/IBMPlexMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiaskorhonen/paper-age/7fb7c114f22e902da8f4398b230dbae073a1b1ab/src/assets/fonts/IBMPlexMono-Regular.ttf -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | //! PaperAge 2 | use std::io::{BufReader, Cursor}; 3 | 4 | use printpdf::{ 5 | Color, IndirectFontRef, Line, LineDashPattern, Mm, PdfDocument, PdfDocumentReference, 6 | PdfLayerIndex, PdfLayerReference, PdfPageIndex, Point, Pt, Rect, Rgb, Svg, SvgTransform, 7 | }; 8 | 9 | use crate::page::*; 10 | 11 | pub mod svg; 12 | 13 | /// PaperAge version 14 | pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); 15 | 16 | /// Font width / height = 3 / 5 17 | const FONT_RATIO: f32 = 3.0 / 5.0; 18 | 19 | /// Container for all the data required to insert elements into the PDF 20 | pub struct Document { 21 | /// A reference to the printpdf PDF document 22 | pub doc: PdfDocumentReference, 23 | 24 | /// Index of the first page in the PDF document 25 | pub page: PdfPageIndex, 26 | 27 | /// Index of the initial layer in the PDF document 28 | pub layer: PdfLayerIndex, 29 | 30 | /// Reference to the medium weight font 31 | pub title_font: IndirectFontRef, 32 | 33 | /// Reference to the regular weight font 34 | pub code_font: IndirectFontRef, 35 | 36 | /// Page size 37 | pub page_size: PageSize, 38 | } 39 | 40 | impl Document { 41 | /// Initialize the PDF with default dimensions and the required fonts. Also 42 | /// sets the title and the producer in the PDF metadata. 43 | pub fn new(title: String, page_size: PageSize) -> Result<Document, Box<dyn std::error::Error>> { 44 | debug!("Initializing PDF"); 45 | 46 | let dimensions = page_size.dimensions(); 47 | 48 | let (mut doc, page, background_ref) = 49 | PdfDocument::new(title, dimensions.width, dimensions.height, "Background"); 50 | 51 | let layer = doc.get_page(page).add_layer("Foreground").layer; 52 | 53 | let fill_color = Color::Rgb(Rgb::new(1.0, 1.0, 1.0, None)); 54 | 55 | let background_layer = doc.get_page(page).get_layer(background_ref); 56 | background_layer.set_fill_color(fill_color); 57 | 58 | let bg = Rect::new(Mm(0.0), Mm(0.0), dimensions.width, dimensions.height); 59 | background_layer.add_rect(bg); 60 | 61 | let producer = format!("Paper Rage v{}", VERSION.unwrap_or("0.0.0")); 62 | doc = doc.with_producer(producer); 63 | 64 | let code_data = include_bytes!("assets/fonts/IBMPlexMono-Regular.ttf"); 65 | let code_font = doc.add_external_font(BufReader::new(Cursor::new(code_data)))?; 66 | 67 | let title_data = include_bytes!("assets/fonts/IBMPlexMono-Medium.ttf"); 68 | let title_font = doc.add_external_font(BufReader::new(Cursor::new(title_data)))?; 69 | 70 | Ok(Document { 71 | doc, 72 | page, 73 | layer, 74 | title_font, 75 | code_font, 76 | page_size, 77 | }) 78 | } 79 | 80 | /// Get the default layer from the PDF 81 | fn get_current_layer(&self) -> PdfLayerReference { 82 | self.doc.get_page(self.page).get_layer(self.layer) 83 | } 84 | 85 | /// Insert the given title at the top of the PDF 86 | pub fn insert_title_text(&self, title: String) { 87 | debug!("Inserting title: {}", title.as_str()); 88 | 89 | let current_layer = self.get_current_layer(); 90 | 91 | let font_size = 14.0; 92 | 93 | // Align the title with the QR code if the title is narrower than the QR code 94 | let margin = { 95 | if title.len() <= 37 { 96 | self.page_size.qrcode_left_edge() 97 | } else { 98 | self.page_size.dimensions().margin 99 | } 100 | }; 101 | 102 | current_layer.use_text( 103 | title, 104 | font_size, 105 | margin, 106 | self.page_size.dimensions().height 107 | - self.page_size.dimensions().margin 108 | - Mm::from(Pt(font_size)), 109 | &self.title_font, 110 | ); 111 | } 112 | 113 | /// Insert the given PEM ciphertext in the bottom half of the page 114 | pub fn insert_pem_text(&self, pem: String) { 115 | debug!("Inserting PEM encoded ciphertext"); 116 | 117 | let current_layer = self.get_current_layer(); 118 | 119 | let mut font_size = 13.0; 120 | let mut line_height = 15.0; 121 | 122 | // Rudimentary text scaling to get the Ascii Armor text to fit 123 | if pem.lines().count() > 42 { 124 | font_size = 6.5; 125 | line_height = 7.0; 126 | } else if pem.lines().count() > 39 { 127 | font_size = 7.0; 128 | line_height = 8.0; 129 | } else if pem.lines().count() > 27 { 130 | font_size = 8.0; 131 | line_height = 9.0; 132 | } else if pem.lines().count() > 22 { 133 | font_size = 10.0; 134 | line_height = 12.0; 135 | } 136 | 137 | current_layer.begin_text_section(); 138 | 139 | current_layer.set_text_cursor( 140 | self.page_size.dimensions().margin, 141 | (self.page_size.dimensions().height / 2.0) 142 | - Mm::from(Pt(font_size)) 143 | - self.page_size.dimensions().margin, 144 | ); 145 | current_layer.set_line_height(line_height); 146 | current_layer.set_font(&self.code_font, font_size); 147 | 148 | for line in pem.lines() { 149 | current_layer.write_text(line, &self.code_font); 150 | current_layer.add_line_break(); 151 | } 152 | 153 | current_layer.end_text_section(); 154 | } 155 | 156 | /// Insert the QR code of the PEM encoded ciphertext in the top half of the page 157 | pub fn insert_qr_code(&self, text: String) -> Result<(), Box<dyn std::error::Error>> { 158 | debug!("Inserting QR code"); 159 | 160 | let image = svg::qrcode(text)?; 161 | let qrcode = Svg::parse(image.as_str())?; 162 | 163 | let current_layer = self.get_current_layer(); 164 | 165 | let desired_qr_size = self.page_size.qrcode_size(); 166 | let initial_qr_size = Mm::from(qrcode.height.into_pt(300.0)); 167 | let qr_scale = desired_qr_size / initial_qr_size; 168 | 169 | let scale = qr_scale; 170 | let dpi = 300.0; 171 | let code_width = qrcode.width.into_pt(dpi) * scale; 172 | let code_height = qrcode.height.into_pt(dpi) * scale; 173 | 174 | let translate_x = (self.page_size.dimensions().width.into_pt() - code_width) / 2.0; 175 | let translate_y = self.page_size.dimensions().height.into_pt() 176 | - code_height 177 | - (self.page_size.dimensions().margin.into_pt() * 2.0); 178 | 179 | qrcode.add_to_layer( 180 | ¤t_layer, 181 | SvgTransform { 182 | translate_x: Some(translate_x), 183 | translate_y: Some(translate_y), 184 | rotate: None, 185 | scale_x: Some(scale), 186 | scale_y: Some(scale), 187 | dpi: Some(dpi), 188 | }, 189 | ); 190 | 191 | Ok(()) 192 | } 193 | 194 | /// Draw a grid debugging layout issues 195 | pub fn draw_grid(&self) { 196 | debug!("Drawing grid"); 197 | 198 | let grid_size = Mm(5.0); 199 | let thickness = 0.0; 200 | 201 | let mut x = Mm(0.0); 202 | let mut y = self.page_size.dimensions().height; 203 | while x < self.page_size.dimensions().width { 204 | x += grid_size; 205 | 206 | self.draw_line( 207 | vec![ 208 | Point::new(x, self.page_size.dimensions().height), 209 | Point::new(x, Mm(0.0)), 210 | ], 211 | thickness, 212 | LineDashPattern::default(), 213 | ); 214 | 215 | while y > Mm(0.0) { 216 | y -= grid_size; 217 | 218 | self.draw_line( 219 | vec![ 220 | Point::new(self.page_size.dimensions().width, y), 221 | Point::new(Mm(0.0), y), 222 | ], 223 | thickness, 224 | LineDashPattern::default(), 225 | ); 226 | } 227 | } 228 | } 229 | 230 | /// Draw a line on the page 231 | pub fn draw_line(&self, points: Vec<Point>, thickness: f32, dash_pattern: LineDashPattern) { 232 | trace!("Drawing line"); 233 | 234 | let current_layer = self.get_current_layer(); 235 | 236 | current_layer.set_line_dash_pattern(dash_pattern); 237 | 238 | let outline_color = Color::Rgb(Rgb::new(0.75, 0.75, 0.75, None)); 239 | current_layer.set_outline_color(outline_color); 240 | 241 | current_layer.set_outline_thickness(thickness); 242 | 243 | let divider = Line { 244 | points: points.iter().map(|p| (*p, false)).collect(), 245 | is_closed: false, 246 | }; 247 | 248 | current_layer.add_line(divider); 249 | } 250 | 251 | /// Insert the notes field label and placeholder in the PDF 252 | pub fn insert_notes_field(&self, label: String, skip_line: bool) { 253 | debug!("Inserting notes/passphrase placeholder"); 254 | const MAX_LABEL_LEN: usize = 32; 255 | 256 | let current_layer = self.get_current_layer(); 257 | 258 | let baseline = 259 | self.page_size.dimensions().height / 2.0 + self.page_size.dimensions().margin; 260 | 261 | let label_len = label.len(); 262 | 263 | let font_size = 13.0; 264 | 265 | current_layer.use_text( 266 | label, 267 | font_size, 268 | self.page_size.qrcode_left_edge(), 269 | baseline, 270 | &self.title_font, 271 | ); 272 | 273 | // If the placeholder line would be ridiculously short, don't draw it 274 | if label_len <= MAX_LABEL_LEN && !skip_line { 275 | self.draw_line( 276 | vec![ 277 | Point::new( 278 | self.page_size.qrcode_left_edge() 279 | + Mm::from(Pt(FONT_RATIO * font_size * label_len as f32)), 280 | baseline - Mm(1.0), 281 | ), 282 | Point::new( 283 | self.page_size.qrcode_left_edge() + self.page_size.qrcode_size(), 284 | baseline - Mm(1.0), 285 | ), 286 | ], 287 | 1.0, 288 | LineDashPattern::default(), 289 | ) 290 | } 291 | } 292 | 293 | /// Add the footer at the bottom of the page 294 | pub fn insert_footer(&self) { 295 | debug!("Inserting footer"); 296 | 297 | let current_layer = self.get_current_layer(); 298 | 299 | current_layer.use_text( 300 | "Scan QR code and decrypt using Age <https://age-encryption.org>", 301 | 13.0, 302 | self.page_size.dimensions().margin, 303 | self.page_size.dimensions().margin, 304 | &self.title_font, 305 | ); 306 | } 307 | } 308 | 309 | #[test] 310 | fn test_paper_dimensions_default() { 311 | let default = PageDimensions::default(); 312 | assert_eq!(default.width, Mm(210.0)); 313 | assert_eq!(default.height, Mm(297.0)); 314 | } 315 | 316 | #[test] 317 | fn test_new_document() { 318 | let title = String::from("Hello World!"); 319 | let result = Document::new(title, PageSize::A4); 320 | assert!(result.is_ok()); 321 | 322 | let doc = result.unwrap(); 323 | assert_eq!(doc.page_size.dimensions(), crate::page::A4_PAGE); 324 | } 325 | 326 | #[test] 327 | fn test_new_letter_document() { 328 | let title = String::from("Hello Letter!"); 329 | let result = Document::new(title, PageSize::Letter); 330 | assert!(result.is_ok()); 331 | 332 | let doc = result.unwrap(); 333 | assert_eq!(doc.page_size.dimensions(), crate::page::LETTER_PAGE); 334 | } 335 | 336 | #[test] 337 | fn test_qrcode() { 338 | let result = Document::new(String::from("QR code"), PageSize::A4); 339 | let document = result.unwrap(); 340 | let result = document.insert_qr_code(String::from("payload")); 341 | assert!(result.is_ok()); 342 | } 343 | 344 | #[test] 345 | fn test_qrcode_too_large() { 346 | let document = Document::new(String::from("QR code"), PageSize::A4).unwrap(); 347 | let result = document.insert_qr_code(String::from(include_str!("../tests/data/too_large.txt"))); 348 | 349 | assert!(result.is_err()); 350 | assert!(result.unwrap_err().is::<qrcode::types::QrError>()); 351 | } 352 | -------------------------------------------------------------------------------- /src/builder/svg.rs: -------------------------------------------------------------------------------- 1 | //! Generate SVG formnat QR codes 2 | use qrcode::{render::svg, types::QrError, EcLevel, QrCode}; 3 | 4 | /// Generate a QR code svg for the given string. The error correction level of 5 | /// the QR code is optimised (less data → more error correction) 6 | pub fn qrcode(text: String) -> Result<String, QrError> { 7 | // QR Code Error Correction Capability (approx.) 8 | // H: 30% 9 | // Q: 25% 10 | // M: 15% 11 | // L: 7% 12 | let levels = [EcLevel::H, EcLevel::Q, EcLevel::M, EcLevel::L]; 13 | 14 | // Find the best level of EC level possible for the data 15 | let mut result: Result<QrCode, QrError> = Result::Err(QrError::DataTooLong); 16 | for ec_level in levels.iter() { 17 | debug!("Trying EC level {:?}", *ec_level); 18 | result = QrCode::with_error_correction_level(text.clone(), *ec_level); 19 | 20 | if result.is_ok() { 21 | break; 22 | } 23 | } 24 | let code = result?; 25 | 26 | info!("QR code EC level: {:?}", code.error_correction_level()); 27 | info!("QR code version: {:?}", code.version()); 28 | 29 | let image = code 30 | .render() 31 | .min_dimensions(256, 256) 32 | .dark_color(svg::Color("#000000")) 33 | .light_color(svg::Color("#ffffff")) 34 | .quiet_zone(false) 35 | .build(); 36 | 37 | Ok(image) 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | 44 | #[test] 45 | fn test_svg_qrcode() { 46 | let svg = qrcode(String::from("Some value")).unwrap(); 47 | assert_eq!( 48 | svg, 49 | String::from(include_str!("../../tests/data/some_value.svg")).trim() 50 | ); 51 | } 52 | 53 | #[test] 54 | fn test_input_too_large() { 55 | let result = qrcode(String::from(include_str!("../../tests/data/too_large.txt"))); 56 | assert!(result.is_err()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Command line arguments 2 | use std::path::PathBuf; 3 | 4 | use clap::Parser; 5 | use clap_verbosity_flag::Verbosity; 6 | 7 | use crate::page::PageSize; 8 | 9 | /// Command line arguments 10 | #[derive(Parser, Debug)] 11 | #[command(author, version, about, long_about = None)] 12 | pub struct Args { 13 | /// Page title (max. 64 characters) 14 | #[arg(short, long, default_value = "PaperAge")] 15 | pub title: String, 16 | 17 | /// Notes label below the QR code (max. 32 characters) 18 | #[arg(short, long, default_value = "Passphrase:")] 19 | pub notes_label: String, 20 | 21 | /// Skip the notes placeholder line (e.g. Passphrase: ________) 22 | #[arg(long, default_value_t = false)] 23 | pub skip_notes_line: bool, 24 | 25 | /// Output file name. Use - for STDOUT. 26 | #[arg(short, long, default_value = "out.pdf")] 27 | pub output: PathBuf, 28 | 29 | /// Paper size 30 | #[arg(short = 's', long, default_value_t = PageSize::A4)] 31 | pub page_size: PageSize, 32 | 33 | /// Overwrite the output file if it already exists 34 | #[arg(short, long, default_value_t = false)] 35 | pub force: bool, 36 | 37 | /// Draw a grid pattern for debugging layout issues 38 | #[arg(short, long, default_value_t = false)] 39 | pub grid: bool, 40 | 41 | /// Print out the license for the embedded fonts 42 | #[arg(long, default_value_t = false, exclusive = true)] 43 | pub fonts_license: bool, 44 | 45 | /// Verbose output for debugging 46 | #[clap(flatten)] 47 | pub verbose: Verbosity, 48 | 49 | /// The path to the file to read. Defaults to standard input. Max. ~1.9KB. 50 | pub input: Option<PathBuf>, 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | 57 | #[test] 58 | fn verify_args() { 59 | use clap::CommandFactory; 60 | super::Args::command().debug_assert() 61 | } 62 | 63 | #[test] 64 | fn test_args() { 65 | let args = Args::parse_from([ 66 | "paper-age", 67 | "-f", 68 | "-g", 69 | "--title", 70 | "Hello", 71 | "--notes-label", 72 | "Notes:", 73 | "--skip-notes-line", 74 | "--output", 75 | "test.pdf", 76 | "input.txt", 77 | ]); 78 | assert!(args.force); 79 | assert!(args.grid); 80 | assert_eq!(args.title, "Hello"); 81 | assert_eq!(args.notes_label, "Notes:"); 82 | assert!(args.skip_notes_line); 83 | assert_eq!(args.output.to_str().unwrap(), "test.pdf"); 84 | assert_eq!(args.input.unwrap().to_str().unwrap(), "input.txt"); 85 | } 86 | 87 | #[test] 88 | fn test_defaults() { 89 | let args = Args::parse_from(["paper-age"]); 90 | assert_eq!(args.title, "PaperAge"); 91 | assert_eq!(args.notes_label, "Passphrase:"); 92 | assert!(!args.skip_notes_line); 93 | assert_eq!(args.output.to_str().unwrap(), "out.pdf"); 94 | assert_eq!(args.input, None); 95 | assert!(!args.force); 96 | } 97 | 98 | #[test] 99 | fn test_fonts_license() { 100 | let args = Args::parse_from(["paper-age", "--fonts-license"]); 101 | assert!(args.fonts_license); 102 | } 103 | 104 | #[test] 105 | fn test_fonts_license_conflict() -> Result<(), Box<dyn std::error::Error>> { 106 | let result = Args::try_parse_from(["paper-age", "--fonts-license", "--grid"]); 107 | 108 | assert!(result.is_err()); 109 | 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/encryption.rs: -------------------------------------------------------------------------------- 1 | //! Age based encryption 2 | use std::io::Write; 3 | 4 | use age::armor::ArmoredWriter; 5 | use age::armor::Format::AsciiArmor; 6 | use age::secrecy::SecretString; 7 | 8 | /// Encrypt the data from the reader and PEM encode the ciphertext 9 | pub fn encrypt_plaintext( 10 | reader: &mut dyn std::io::BufRead, 11 | passphrase: SecretString, 12 | ) -> Result<(usize, String), Box<dyn std::error::Error>> { 13 | debug!("Encrypting plaintext"); 14 | 15 | let mut plaintext: Vec<u8> = vec![]; 16 | reader.read_to_end(&mut plaintext)?; 17 | 18 | let encryptor = age::Encryptor::with_user_passphrase(passphrase); 19 | 20 | let mut encrypted = vec![]; 21 | 22 | let armored_writer = ArmoredWriter::wrap_output(&mut encrypted, AsciiArmor)?; 23 | 24 | let mut writer = encryptor.wrap_output(armored_writer)?; 25 | 26 | writer.write_all(&plaintext)?; 27 | 28 | let output = writer.finish().and_then(|armor| armor.finish())?; 29 | 30 | let utf8 = std::string::String::from_utf8(output.to_owned())?; 31 | 32 | Ok((plaintext.len(), utf8)) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_armored_output() { 41 | let mut input = b"some secrets" as &[u8]; 42 | let passphrase = SecretString::from("snakeoil".to_owned()); 43 | let result = encrypt_plaintext(&mut input, passphrase); 44 | 45 | assert!(result.is_ok()); 46 | 47 | let (plaintext_size, armored) = result.unwrap(); 48 | assert_eq!(plaintext_size, 12); 49 | 50 | let first_line: String = armored.lines().take(1).collect(); 51 | assert_eq!(first_line, "-----BEGIN AGE ENCRYPTED FILE-----"); 52 | 53 | let last_line: &str = armored.lines().last().unwrap(); 54 | assert_eq!(last_line, "-----END AGE ENCRYPTED FILE-----") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![doc( 3 | html_logo_url = "https://user-images.githubusercontent.com/43314/216838549-bc5cafc8-0211-44e2-9bcc-651c74bfc853.svg" 4 | )] 5 | #![doc(html_favicon_url = "https://shots.matiaskorhonen.fi/paper-age-favicon.ico")] 6 | 7 | use std::{ 8 | env, 9 | fs::File, 10 | io::{self, stdin, BufReader, BufWriter, Read, Write}, 11 | path::PathBuf, 12 | }; 13 | 14 | use age::secrecy::{ExposeSecret, SecretString}; 15 | use clap::Parser; 16 | use printpdf::LineDashPattern; 17 | use qrcode::types::QrError; 18 | use rpassword::prompt_password; 19 | 20 | pub mod builder; 21 | pub mod cli; 22 | pub mod encryption; 23 | pub mod page; 24 | 25 | #[macro_use] 26 | extern crate log; 27 | 28 | /// Maximum length of the document title 29 | const TITLE_MAX_LEN: usize = 64; 30 | 31 | fn main() -> Result<(), Box<dyn std::error::Error>> { 32 | let args = cli::Args::parse(); 33 | 34 | env_logger::Builder::new() 35 | .filter_level(args.verbose.log_level_filter()) 36 | .init(); 37 | 38 | if args.fonts_license { 39 | let license = include_bytes!("assets/fonts/IBMPlexMono-LICENSE.txt"); 40 | io::stdout().write_all(license)?; 41 | return Ok(()); 42 | } 43 | 44 | if args.title.len() > TITLE_MAX_LEN { 45 | error!( 46 | "The title cannot be longer than {} characters", 47 | TITLE_MAX_LEN 48 | ); 49 | std::process::exit(exitcode::DATAERR); 50 | } 51 | 52 | let output = args.output; 53 | if output.exists() { 54 | if args.force { 55 | warn!("Overwriting existing output file: {}", output.display()); 56 | } else { 57 | error!("Output file already exists: {}", output.display()); 58 | std::process::exit(exitcode::CANTCREAT); 59 | } 60 | } 61 | 62 | let path = match args.input { 63 | Some(p) => p, 64 | None => PathBuf::from("-"), 65 | }; 66 | let mut reader: BufReader<Box<dyn Read>> = { 67 | if path == PathBuf::from("-") { 68 | BufReader::new(Box::new(stdin().lock())) 69 | } else if path.is_file() { 70 | let size = path.metadata()?.len(); 71 | if size >= 2048 { 72 | warn!("File too large ({size:?} bytes). The maximum file size is about 1.9 KiB."); 73 | } 74 | BufReader::new(Box::new(File::open(&path).unwrap())) 75 | } else { 76 | error!("File not found: {}", path.display()); 77 | std::process::exit(exitcode::NOINPUT); 78 | } 79 | }; 80 | 81 | let passphrase = get_passphrase()?; 82 | 83 | // Encrypt the plaintext to a ciphertext using the passphrase... 84 | let (plaintext_len, encrypted) = encryption::encrypt_plaintext(&mut reader, passphrase)?; 85 | 86 | info!("Plaintext length: {plaintext_len:?} bytes"); 87 | info!("Encrypted length: {:?} bytes", encrypted.len()); 88 | 89 | let pdf = builder::Document::new(args.title.clone(), args.page_size)?; 90 | 91 | if args.grid { 92 | pdf.draw_grid(); 93 | } 94 | 95 | pdf.insert_title_text(args.title); 96 | 97 | match pdf.insert_qr_code(encrypted.clone()) { 98 | Ok(()) => (), 99 | Err(error) => { 100 | if error.is::<QrError>() { 101 | error!("Too much data after encryption, please try a smaller file"); 102 | std::process::exit(exitcode::DATAERR); 103 | } else { 104 | error!("The QR code generation failed for an unknown reason"); 105 | std::process::exit(exitcode::SOFTWARE); 106 | } 107 | } 108 | } 109 | 110 | pdf.insert_notes_field(args.notes_label, args.skip_notes_line); 111 | 112 | pdf.draw_line( 113 | vec![ 114 | pdf.page_size.dimensions().center_left(), 115 | pdf.page_size.dimensions().center_right(), 116 | ], 117 | 1.0, 118 | LineDashPattern { 119 | dash_1: Some(5), 120 | ..LineDashPattern::default() 121 | }, 122 | ); 123 | 124 | pdf.insert_pem_text(encrypted); 125 | 126 | pdf.insert_footer(); 127 | 128 | if output == PathBuf::from("-") { 129 | debug!("Writing to STDOUT"); 130 | let bytes = pdf.doc.save_to_bytes()?; 131 | io::stdout().write_all(&bytes)?; 132 | } else { 133 | debug!("Writing to file: {}", output.to_string_lossy()); 134 | let file = File::create(output)?; 135 | pdf.doc.save(&mut BufWriter::new(file))?; 136 | } 137 | 138 | Ok(()) 139 | } 140 | 141 | /// Read a secret from the user 142 | pub fn read_secret(prompt: &str) -> Result<SecretString, io::Error> { 143 | let passphrase = prompt_password(format!("{}: ", prompt)).map(SecretString::from)?; 144 | 145 | if passphrase.expose_secret().is_empty() { 146 | return Err(io::Error::new( 147 | io::ErrorKind::InvalidInput, 148 | "Passphrase can't be empty", 149 | )); 150 | } 151 | 152 | Ok(passphrase) 153 | } 154 | 155 | /// Get the passphrase from an interactive prompt or from the PAPERAGE_PASSPHRASE 156 | /// environment variable 157 | fn get_passphrase() -> Result<SecretString, io::Error> { 158 | let env_passphrase = env::var("PAPERAGE_PASSPHRASE"); 159 | 160 | if let Ok(value) = env_passphrase { 161 | return Ok(SecretString::from(value)); 162 | } 163 | 164 | match read_secret("Passphrase") { 165 | Ok(secret) => Ok(secret), 166 | Err(e) => Err(io::Error::new(io::ErrorKind::Other, format!("{e}"))), 167 | } 168 | } 169 | 170 | #[cfg(test)] 171 | mod tests { 172 | use super::*; 173 | use age::secrecy::ExposeSecret; 174 | 175 | #[test] 176 | fn test_get_passphrase_from_env() -> Result<(), Box<dyn std::error::Error>> { 177 | env::set_var("PAPERAGE_PASSPHRASE", "secret"); 178 | 179 | let result = get_passphrase(); 180 | assert!(result.is_ok()); 181 | 182 | let passphrase = result?; 183 | passphrase.expose_secret(); 184 | 185 | assert_eq!(passphrase.expose_secret(), "secret"); 186 | 187 | Ok(()) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/page.rs: -------------------------------------------------------------------------------- 1 | //! Page size and dimensions 2 | use std::fmt; 3 | 4 | use printpdf::{Mm, Point}; 5 | 6 | /// PDF dimensions 7 | #[derive(Clone, Copy, Debug)] 8 | pub struct PageDimensions { 9 | pub width: Mm, 10 | pub height: Mm, 11 | pub margin: Mm, 12 | } 13 | 14 | impl PartialEq for PageDimensions { 15 | fn eq(&self, other: &PageDimensions) -> bool { 16 | self.width == other.width && self.height == other.height && self.margin == other.margin 17 | } 18 | } 19 | 20 | impl PageDimensions { 21 | /// Center point of the page 22 | pub fn center(&self) -> Point { 23 | Point::new(self.width / 2.0, self.height / 2.0) 24 | } 25 | 26 | /// Vertical center left of the page (with margin) 27 | pub fn center_left(&self) -> Point { 28 | Point::new(self.margin, self.height / 2.0) 29 | } 30 | 31 | /// Vertical center right of the page (with margin) 32 | pub fn center_right(&self) -> Point { 33 | Point::new(self.width - self.margin, self.height / 2.0) 34 | } 35 | 36 | /// Top left of the page (with margin) 37 | pub fn top_left(&self) -> Point { 38 | Point::new(self.margin, self.height - self.margin) 39 | } 40 | 41 | /// Top right of the page (with margin) 42 | pub fn top_right(&self) -> Point { 43 | Point::new(self.width - self.margin, self.height - self.margin) 44 | } 45 | 46 | /// Bottom left of the page (with margin) 47 | pub fn bottom_left(&self) -> Point { 48 | Point::new(self.margin, self.margin) 49 | } 50 | 51 | /// Bottom right of the page (with margin) 52 | pub fn bottom_right(&self) -> Point { 53 | Point::new(self.width - self.margin, self.margin) 54 | } 55 | } 56 | 57 | /// A4 dimensions with a 10mm margin 58 | pub const A4_PAGE: PageDimensions = PageDimensions { 59 | width: Mm(210.0), 60 | height: Mm(297.0), 61 | margin: Mm(10.0), 62 | }; 63 | 64 | /// Letter dimensions with a 10mm margin 65 | pub const LETTER_PAGE: PageDimensions = PageDimensions { 66 | width: Mm(215.9), 67 | height: Mm(279.4), 68 | margin: Mm(10.0), 69 | }; 70 | 71 | /// Default to an A4 page 72 | impl Default for PageDimensions { 73 | fn default() -> Self { 74 | A4_PAGE 75 | } 76 | } 77 | 78 | #[derive(clap::ValueEnum, Clone, Debug, PartialEq, Eq)] 79 | pub enum PageSize { 80 | A4, 81 | Letter, 82 | } 83 | 84 | impl PageSize { 85 | /// Page dimensions 86 | pub fn dimensions(&self) -> PageDimensions { 87 | match self { 88 | PageSize::A4 => A4_PAGE, 89 | PageSize::Letter => LETTER_PAGE, 90 | } 91 | } 92 | 93 | /// QR code size for the page size 94 | pub fn qrcode_size(&self) -> Mm { 95 | match self { 96 | PageSize::A4 => Mm(110.0), 97 | PageSize::Letter => Mm(102.0), 98 | } 99 | } 100 | 101 | /// The left edge of the QR code on the page 102 | pub fn qrcode_left_edge(&self) -> Mm { 103 | (self.dimensions().width - self.qrcode_size()) / 2.0 104 | } 105 | } 106 | 107 | impl fmt::Display for PageSize { 108 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 109 | write!(f, "{}", format!("{self:?}").to_lowercase()) 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | 117 | const TEST_DIMENSIONS: PageDimensions = PageDimensions { 118 | width: Mm(100.0), 119 | height: Mm(200.0), 120 | margin: Mm(10.0), 121 | }; 122 | 123 | #[test] 124 | fn page_dimensions_center() { 125 | assert_eq!(TEST_DIMENSIONS.center(), Point::new(Mm(50.0), Mm(100.0))); 126 | } 127 | 128 | #[test] 129 | fn page_dimensions_center_left() { 130 | assert_eq!( 131 | TEST_DIMENSIONS.center_left(), 132 | Point::new(Mm(10.0), Mm(100.0)) 133 | ); 134 | } 135 | 136 | #[test] 137 | fn page_dimensions_center_right() { 138 | assert_eq!( 139 | TEST_DIMENSIONS.center_right(), 140 | Point::new(Mm(90.0), Mm(100.0)) 141 | ); 142 | } 143 | 144 | #[test] 145 | fn page_dimensions_top_left() { 146 | assert_eq!(TEST_DIMENSIONS.top_left(), Point::new(Mm(10.0), Mm(190.0))); 147 | } 148 | 149 | #[test] 150 | fn page_dimensions_top_right() { 151 | assert_eq!(TEST_DIMENSIONS.top_right(), Point::new(Mm(90.0), Mm(190.0))); 152 | } 153 | 154 | #[test] 155 | fn page_dimensions_bottom_left() { 156 | assert_eq!( 157 | TEST_DIMENSIONS.bottom_left(), 158 | Point::new(Mm(10.0), Mm(10.0)) 159 | ); 160 | } 161 | 162 | #[test] 163 | fn page_dimensions_bottom_right() { 164 | assert_eq!( 165 | TEST_DIMENSIONS.bottom_right(), 166 | Point::new(Mm(90.0), Mm(10.0)) 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use assert_fs::prelude::*; 3 | use predicates::prelude::*; 4 | 5 | #[test] 6 | fn test_happy_path() -> Result<(), Box<dyn std::error::Error>> { 7 | let temp = assert_fs::TempDir::new().unwrap(); 8 | let input = temp.child("sample.txt"); 9 | input.write_str("Hello")?; 10 | let output = temp.child("output.pdf"); 11 | let mut cmd = Command::cargo_bin("paper-age")?; 12 | 13 | cmd.arg("--output") 14 | .arg(output.path()) 15 | .arg("--grid") 16 | .arg(input.path()) 17 | .env("PAPERAGE_PASSPHRASE", "secret"); 18 | cmd.assert().success(); 19 | 20 | output.assert(predicate::path::is_file()); 21 | 22 | Ok(()) 23 | } 24 | 25 | #[test] 26 | fn test_letter_support() -> Result<(), Box<dyn std::error::Error>> { 27 | let temp = assert_fs::TempDir::new().unwrap(); 28 | let input = temp.child("sample.txt"); 29 | input.write_str("Hello")?; 30 | let output = temp.child("letter.pdf"); 31 | let mut cmd = Command::cargo_bin("paper-age")?; 32 | 33 | cmd.arg("--output") 34 | .arg(output.path()) 35 | .arg("--page-size") 36 | .arg("letter") 37 | .arg(input.path()) 38 | .env("PAPERAGE_PASSPHRASE", "secret"); 39 | cmd.assert().success(); 40 | 41 | output.assert(predicate::path::is_file()); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[test] 47 | fn test_stdout() -> Result<(), Box<dyn std::error::Error>> { 48 | let temp = assert_fs::TempDir::new().unwrap(); 49 | let input = temp.child("sample.txt"); 50 | input.write_str("STDOUT")?; 51 | 52 | let mut cmd = Command::cargo_bin("paper-age")?; 53 | 54 | let output_size = 50 * 1024; // 50 KiB 55 | let len_predicate_fn = predicate::function(|x: &[u8]| x.len() > output_size); 56 | 57 | cmd.arg("--output") 58 | .arg("-") 59 | .arg(input.path()) 60 | .env("PAPERAGE_PASSPHRASE", "secret"); 61 | cmd.assert().stdout(len_predicate_fn).success(); 62 | 63 | Ok(()) 64 | } 65 | 66 | #[test] 67 | fn test_file_doesnt_exist() -> Result<(), Box<dyn std::error::Error>> { 68 | let temp = assert_fs::TempDir::new().unwrap(); 69 | let output = temp.child("output.pdf"); 70 | let mut cmd = Command::cargo_bin("paper-age")?; 71 | 72 | cmd.arg("--output") 73 | .arg(output.path()) 74 | .arg("test/file/doesnt/exist"); 75 | cmd.assert().failure().stderr(predicate::str::contains( 76 | "File not found: test/file/doesnt/exist", 77 | )); 78 | 79 | output.assert(predicate::path::missing()); 80 | 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | fn test_too_much_data() -> Result<(), Box<dyn std::error::Error>> { 86 | let temp = assert_fs::TempDir::new().unwrap(); 87 | let input = temp.child("sample.txt"); 88 | input.write_str("x".repeat(2048).as_str())?; 89 | let output = temp.child("output.pdf"); 90 | let mut cmd = Command::cargo_bin("paper-age")?; 91 | 92 | cmd.arg("--output") 93 | .arg(output.path()) 94 | .arg(input.path()) 95 | .env("PAPERAGE_PASSPHRASE", "secret"); 96 | cmd.assert() 97 | .failure() 98 | .stderr(predicate::str::contains("Too much data after encryption")); 99 | 100 | output.assert(predicate::path::missing()); 101 | 102 | Ok(()) 103 | } 104 | 105 | #[test] 106 | fn test_fonts_license() -> Result<(), Box<dyn std::error::Error>> { 107 | let mut cmd = Command::cargo_bin("paper-age")?; 108 | 109 | cmd.arg("--fonts-license"); 110 | cmd.assert() 111 | .success() 112 | .stdout(predicate::str::contains("SIL OPEN FONT LICENSE")); 113 | 114 | Ok(()) 115 | } 116 | 117 | #[test] 118 | fn test_title_too_long() -> Result<(), Box<dyn std::error::Error>> { 119 | let input = assert_fs::NamedTempFile::new("sample.txt")?; 120 | let mut cmd = Command::cargo_bin("paper-age")?; 121 | 122 | cmd.arg("--title").arg("x".repeat(80)).arg(input.path()); 123 | cmd.assert() 124 | .failure() 125 | .stderr(predicate::str::contains("The title cannot be longer than")); 126 | 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /tests/data/some_value.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="200" height="200" viewBox="0 0 200 200" shape-rendering="crispEdges"><rect x="0" y="0" width="200" height="200" fill="#ffffff"/><path fill="#000000" d="M0 0h8v8H0V0M8 0h8v8H8V0M16 0h8v8H16V0M24 0h8v8H24V0M32 0h8v8H32V0M40 0h8v8H40V0M48 0h8v8H48V0M64 0h8v8H64V0M96 0h8v8H96V0M104 0h8v8H104V0M120 0h8v8H120V0M128 0h8v8H128V0M144 0h8v8H144V0M152 0h8v8H152V0M160 0h8v8H160V0M168 0h8v8H168V0M176 0h8v8H176V0M184 0h8v8H184V0M192 0h8v8H192V0M0 8h8v8H0V8M48 8h8v8H48V8M80 8h8v8H80V8M88 8h8v8H88V8M96 8h8v8H96V8M104 8h8v8H104V8M120 8h8v8H120V8M144 8h8v8H144V8M192 8h8v8H192V8M0 16h8v8H0V16M16 16h8v8H16V16M24 16h8v8H24V16M32 16h8v8H32V16M48 16h8v8H48V16M64 16h8v8H64V16M72 16h8v8H72V16M80 16h8v8H80V16M112 16h8v8H112V16M144 16h8v8H144V16M160 16h8v8H160V16M168 16h8v8H168V16M176 16h8v8H176V16M192 16h8v8H192V16M0 24h8v8H0V24M16 24h8v8H16V24M24 24h8v8H24V24M32 24h8v8H32V24M48 24h8v8H48V24M72 24h8v8H72V24M88 24h8v8H88V24M96 24h8v8H96V24M144 24h8v8H144V24M160 24h8v8H160V24M168 24h8v8H168V24M176 24h8v8H176V24M192 24h8v8H192V24M0 32h8v8H0V32M16 32h8v8H16V32M24 32h8v8H24V32M32 32h8v8H32V32M48 32h8v8H48V32M64 32h8v8H64V32M72 32h8v8H72V32M80 32h8v8H80V32M88 32h8v8H88V32M104 32h8v8H104V32M112 32h8v8H112V32M120 32h8v8H120V32M128 32h8v8H128V32M144 32h8v8H144V32M160 32h8v8H160V32M168 32h8v8H168V32M176 32h8v8H176V32M192 32h8v8H192V32M0 40h8v8H0V40M48 40h8v8H48V40M80 40h8v8H80V40M112 40h8v8H112V40M144 40h8v8H144V40M192 40h8v8H192V40M0 48h8v8H0V48M8 48h8v8H8V48M16 48h8v8H16V48M24 48h8v8H24V48M32 48h8v8H32V48M40 48h8v8H40V48M48 48h8v8H48V48M64 48h8v8H64V48M80 48h8v8H80V48M96 48h8v8H96V48M112 48h8v8H112V48M128 48h8v8H128V48M144 48h8v8H144V48M152 48h8v8H152V48M160 48h8v8H160V48M168 48h8v8H168V48M176 48h8v8H176V48M184 48h8v8H184V48M192 48h8v8H192V48M64 56h8v8H64V56M72 56h8v8H72V56M112 56h8v8H112V56M120 56h8v8H120V56M128 56h8v8H128V56M40 64h8v8H40V64M48 64h8v8H48V64M80 64h8v8H80V64M96 64h8v8H96V64M104 64h8v8H104V64M112 64h8v8H112V64M144 64h8v8H144V64M160 64h8v8H160V64M176 64h8v8H176V64M192 64h8v8H192V64M0 72h8v8H0V72M24 72h8v8H24V72M40 72h8v8H40V72M56 72h8v8H56V72M96 72h8v8H96V72M104 72h8v8H104V72M152 72h8v8H152V72M160 72h8v8H160V72M168 72h8v8H168V72M8 80h8v8H8V80M16 80h8v8H16V80M40 80h8v8H40V80M48 80h8v8H48V80M56 80h8v8H56V80M72 80h8v8H72V80M80 80h8v8H80V80M88 80h8v8H88V80M136 80h8v8H136V80M144 80h8v8H144V80M152 80h8v8H152V80M184 80h8v8H184V80M192 80h8v8H192V80M16 88h8v8H16V88M32 88h8v8H32V88M40 88h8v8H40V88M56 88h8v8H56V88M72 88h8v8H72V88M96 88h8v8H96V88M112 88h8v8H112V88M136 88h8v8H136V88M144 88h8v8H144V88M152 88h8v8H152V88M168 88h8v8H168V88M184 88h8v8H184V88M0 96h8v8H0V96M8 96h8v8H8V96M24 96h8v8H24V96M32 96h8v8H32V96M48 96h8v8H48V96M56 96h8v8H56V96M64 96h8v8H64V96M72 96h8v8H72V96M80 96h8v8H80V96M88 96h8v8H88V96M96 96h8v8H96V96M112 96h8v8H112V96M120 96h8v8H120V96M136 96h8v8H136V96M144 96h8v8H144V96M168 96h8v8H168V96M0 104h8v8H0V104M16 104h8v8H16V104M32 104h8v8H32V104M72 104h8v8H72V104M104 104h8v8H104V104M144 104h8v8H144V104M152 104h8v8H152V104M168 104h8v8H168V104M0 112h8v8H0V112M16 112h8v8H16V112M32 112h8v8H32V112M48 112h8v8H48V112M56 112h8v8H56V112M64 112h8v8H64V112M72 112h8v8H72V112M88 112h8v8H88V112M112 112h8v8H112V112M120 112h8v8H120V112M128 112h8v8H128V112M136 112h8v8H136V112M184 112h8v8H184V112M192 112h8v8H192V112M0 120h8v8H0V120M24 120h8v8H24V120M56 120h8v8H56V120M64 120h8v8H64V120M80 120h8v8H80V120M88 120h8v8H88V120M96 120h8v8H96V120M104 120h8v8H104V120M112 120h8v8H112V120M128 120h8v8H128V120M152 120h8v8H152V120M176 120h8v8H176V120M184 120h8v8H184V120M0 128h8v8H0V128M24 128h8v8H24V128M40 128h8v8H40V128M48 128h8v8H48V128M56 128h8v8H56V128M64 128h8v8H64V128M80 128h8v8H80V128M96 128h8v8H96V128M120 128h8v8H120V128M128 128h8v8H128V128M136 128h8v8H136V128M144 128h8v8H144V128M152 128h8v8H152V128M160 128h8v8H160V128M168 128h8v8H168V128M176 128h8v8H176V128M192 128h8v8H192V128M64 136h8v8H64V136M72 136h8v8H72V136M88 136h8v8H88V136M96 136h8v8H96V136M104 136h8v8H104V136M112 136h8v8H112V136M128 136h8v8H128V136M160 136h8v8H160V136M184 136h8v8H184V136M192 136h8v8H192V136M0 144h8v8H0V144M8 144h8v8H8V144M16 144h8v8H16V144M24 144h8v8H24V144M32 144h8v8H32V144M40 144h8v8H40V144M48 144h8v8H48V144M80 144h8v8H80V144M96 144h8v8H96V144M120 144h8v8H120V144M128 144h8v8H128V144M144 144h8v8H144V144M160 144h8v8H160V144M168 144h8v8H168V144M176 144h8v8H176V144M192 144h8v8H192V144M0 152h8v8H0V152M48 152h8v8H48V152M64 152h8v8H64V152M72 152h8v8H72V152M88 152h8v8H88V152M96 152h8v8H96V152M104 152h8v8H104V152M112 152h8v8H112V152M120 152h8v8H120V152M128 152h8v8H128V152M160 152h8v8H160V152M168 152h8v8H168V152M0 160h8v8H0V160M16 160h8v8H16V160M24 160h8v8H24V160M32 160h8v8H32V160M48 160h8v8H48V160M72 160h8v8H72V160M80 160h8v8H80V160M112 160h8v8H112V160M128 160h8v8H128V160M136 160h8v8H136V160M144 160h8v8H144V160M152 160h8v8H152V160M160 160h8v8H160V160M176 160h8v8H176V160M0 168h8v8H0V168M16 168h8v8H16V168M24 168h8v8H24V168M32 168h8v8H32V168M48 168h8v8H48V168M80 168h8v8H80V168M136 168h8v8H136V168M168 168h8v8H168V168M176 168h8v8H176V168M192 168h8v8H192V168M0 176h8v8H0V176M16 176h8v8H16V176M24 176h8v8H24V176M32 176h8v8H32V176M48 176h8v8H48V176M72 176h8v8H72V176M88 176h8v8H88V176M96 176h8v8H96V176M112 176h8v8H112V176M128 176h8v8H128V176M144 176h8v8H144V176M152 176h8v8H152V176M168 176h8v8H168V176M176 176h8v8H176V176M192 176h8v8H192V176M0 184h8v8H0V184M48 184h8v8H48V184M72 184h8v8H72V184M80 184h8v8H80V184M88 184h8v8H88V184M120 184h8v8H120V184M144 184h8v8H144V184M152 184h8v8H152V184M168 184h8v8H168V184M192 184h8v8H192V184M0 192h8v8H0V192M8 192h8v8H8V192M16 192h8v8H16V192M24 192h8v8H24V192M32 192h8v8H32V192M40 192h8v8H40V192M48 192h8v8H48V192M72 192h8v8H72V192M104 192h8v8H104V192M112 192h8v8H112V192M120 192h8v8H120V192M128 192h8v8H128V192M144 192h8v8H144V192M152 192h8v8H152V192M160 192h8v8H160V192M168 192h8v8H168V192M192 192h8v8H192V192"/></svg> 2 | --------------------------------------------------------------------------------