├── src ├── lib.rs ├── resources │ ├── browser-config.xml │ └── favicon.html ├── cli.rs └── main.rs ├── .github ├── dependabot.yml └── workflows │ ├── ci-version.yml │ └── ci.yml ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── rustfmt.toml └── .gitignore /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Favicon Generator 3 | 4 | It helps you generate favicons with different formats and sizes. 5 | */ 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /src/resources/browser-config.xml: -------------------------------------------------------------------------------- 1 | {% for size in mstile_size %}{% endfor %}{{background_color | lower}} -------------------------------------------------------------------------------- /src/resources/favicon.html: -------------------------------------------------------------------------------- 1 | {% for size in apple_touch_icon_sizes %} 2 | {% endfor %} 3 | {% for size in icon_sizes %} 4 | {% endfor %} 5 | 6 | 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "favicon-generator" 3 | version = "0.4.3" 4 | authors = ["Magic Len "] 5 | edition = "2021" 6 | rust-version = "1.74" 7 | repository = "https://github.com/magiclen/favicon-generator" 8 | homepage = "https://magiclen.org/favicon-generator" 9 | keywords = ["favicon", "generating", "web-app", "icon"] 10 | categories = ["command-line-utilities"] 11 | description = "It helps you generate favicons with different formats and sizes." 12 | license = "MIT" 13 | include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE"] 14 | 15 | [profile.release] 16 | lto = true 17 | codegen-units = 1 18 | panic = "abort" 19 | strip = true 20 | 21 | [dependencies] 22 | clap = { version = "4", features = ["derive"] } 23 | concat-with = "0.2" 24 | terminal_size = "0.3" 25 | 26 | anyhow = "1" 27 | 28 | image-convert = "0.17" 29 | scanner-rust = "2" 30 | serde_json = "1" 31 | tera = "1" 32 | html-escape = "0.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 magiclen.org (Ron Li) 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXECUTABLE_NAME := favicon-generator 2 | 3 | all: ./target/x86_64-unknown-linux-musl/release/$(EXECUTABLE_NAME) 4 | 5 | ./target/x86_64-unknown-linux-musl/release/$(EXECUTABLE_NAME): $(shell find . -type f -iname '*.rs' -o -name 'Cargo.toml' | grep -v ./target | sed 's/ /\\ /g') 6 | PWD=$$(pwd) 7 | cd $$MAGICK_PATH && bash build.sh 8 | cd $$PWD 9 | IMAGE_MAGICK_INCLUDE_DIRS="$$MAGICK_PATH/linux/include/ImageMagick-7" IMAGE_MAGICK_LIB_DIRS="$$MUSL_PATH/x86_64-linux-musl/lib:$$MUSL_PATH/lib/gcc/x86_64-linux-musl/11.4.0:$$MAGICK_PATH/linux/lib" IMAGE_MAGICK_LIBS=z:bz2:lzma:zstd:jpeg:png:tiff:openjp2:jbig:sharpyuv:webpmux:webpdemux:webp:de265:x265:aom:stdc++:heif:iconv:gcc:xml2:freetype:fontconfig:gomp:MagickWand-7.Q16HDRI:MagickCore-7.Q16HDRI IMAGE_MAGICK_STATIC=1 cargo build --release --target x86_64-unknown-linux-musl 10 | 11 | install: 12 | $(MAKE) 13 | sudo cp ./target/x86_64-unknown-linux-musl/release/$(EXECUTABLE_NAME) /usr/local/bin/$(EXECUTABLE_NAME) 14 | sudo chown root: /usr/local/bin/$(EXECUTABLE_NAME) 15 | sudo chmod 0755 /usr/local/bin/$(EXECUTABLE_NAME) 16 | 17 | test: 18 | cargo test --verbose 19 | 20 | clean: 21 | cargo clean 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Favicon Generator 2 | ==================== 3 | 4 | [![CI](https://github.com/magiclen/favicon-generator/actions/workflows/ci.yml/badge.svg)](https://github.com/magiclen/favicon-generator/actions/workflows/ci.yml) 5 | 6 | It helps you generate favicons with different formats and sizes. 7 | 8 | ## Help 9 | 10 | ``` 11 | EXAMPLES: 12 | favicon-generator /path/to/image /path/to/folder # Uses /path/to/image to generate favicons into /path/to/folder 13 | 14 | Usage: favicon-generator [OPTIONS] 15 | 16 | Arguments: 17 | Assign an image for generating favicons. It should be a path of a file 18 | Assign a destination of your generated files. It should be a path of a directory 19 | 20 | Options: 21 | -y, --overwrite Overwrite exiting files without asking 22 | --path-prefix Specify the path prefix of your favicon files [default: /] 23 | --no-sharpen Disable the automatic sharpening 24 | --app-name Assign a name for your web app [default: App] 25 | --app-short-name Assign a short name for your web app 26 | -h, --help Print help 27 | -V, --version Print version 28 | ``` 29 | 30 | ## License 31 | 32 | [MIT](LICENSE) -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # array_width = 60 2 | # attr_fn_like_width = 70 3 | binop_separator = "Front" 4 | blank_lines_lower_bound = 0 5 | blank_lines_upper_bound = 1 6 | brace_style = "PreferSameLine" 7 | # chain_width = 60 8 | color = "Auto" 9 | # comment_width = 100 10 | condense_wildcard_suffixes = true 11 | control_brace_style = "AlwaysSameLine" 12 | empty_item_single_line = true 13 | enum_discrim_align_threshold = 80 14 | error_on_line_overflow = false 15 | error_on_unformatted = false 16 | # fn_call_width = 60 17 | fn_params_layout = "Tall" 18 | fn_single_line = false 19 | force_explicit_abi = true 20 | force_multiline_blocks = false 21 | format_code_in_doc_comments = true 22 | doc_comment_code_block_width = 80 23 | format_generated_files = true 24 | format_macro_matchers = true 25 | format_macro_bodies = true 26 | skip_macro_invocations = [] 27 | format_strings = true 28 | hard_tabs = false 29 | hex_literal_case = "Upper" 30 | imports_indent = "Block" 31 | imports_layout = "Mixed" 32 | indent_style = "Block" 33 | inline_attribute_width = 0 34 | match_arm_blocks = true 35 | match_arm_leading_pipes = "Never" 36 | match_block_trailing_comma = true 37 | max_width = 100 38 | merge_derives = true 39 | imports_granularity = "Crate" 40 | newline_style = "Unix" 41 | normalize_comments = false 42 | normalize_doc_attributes = true 43 | overflow_delimited_expr = true 44 | remove_nested_parens = true 45 | reorder_impl_items = true 46 | reorder_imports = true 47 | group_imports = "StdExternalCrate" 48 | reorder_modules = true 49 | short_array_element_width_threshold = 10 50 | # single_line_if_else_max_width = 50 51 | space_after_colon = true 52 | space_before_colon = false 53 | spaces_around_ranges = false 54 | struct_field_align_threshold = 80 55 | struct_lit_single_line = false 56 | # struct_lit_width = 18 57 | # struct_variant_width = 35 58 | tab_spaces = 4 59 | trailing_comma = "Vertical" 60 | trailing_semicolon = true 61 | type_punctuation_density = "Wide" 62 | use_field_init_shorthand = true 63 | use_small_heuristics = "Max" 64 | use_try_shorthand = true 65 | where_single_line = false 66 | wrap_comments = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/intellij+all 2 | 3 | ### Intellij+all ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/modules.xml 32 | # .idea/*.iml 33 | # .idea/modules 34 | 35 | # CMake 36 | cmake-build-*/ 37 | 38 | # Mongo Explorer plugin 39 | .idea/**/mongoSettings.xml 40 | 41 | # File-based project format 42 | *.iws 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # Crashlytics plugin (for Android Studio and IntelliJ) 57 | com_crashlytics_export_strings.xml 58 | crashlytics.properties 59 | crashlytics-build.properties 60 | fabric.properties 61 | 62 | # Editor-based Rest Client 63 | .idea/httpRequests 64 | 65 | ### Intellij+all Patch ### 66 | # Ignores the whole .idea folder and all .iml files 67 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 68 | 69 | .idea/ 70 | 71 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 72 | 73 | *.iml 74 | modules.xml 75 | .idea/misc.xml 76 | *.ipr 77 | 78 | 79 | # End of https://www.gitignore.io/api/intellij+all 80 | 81 | 82 | ### Rust ### 83 | # Generated by Cargo 84 | # will have compiled files and executables 85 | /target/ 86 | 87 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 88 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 89 | Cargo.lock 90 | 91 | # These are backup files generated by rustfmt 92 | **/*.rs.bk 93 | 94 | 95 | # End of https://www.gitignore.io/api/rust 96 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{CommandFactory, FromArgMatches, Parser}; 4 | use concat_with::concat_line; 5 | use terminal_size::terminal_size; 6 | 7 | const APP_NAME: &str = "Favicon Generator"; 8 | const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); 9 | const CARGO_PKG_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); 10 | 11 | const AFTER_HELP: &str = "Enjoy it! https://magiclen.org"; 12 | 13 | const APP_ABOUT: &str = concat!( 14 | "It helps you generate favicons with different formats and sizes.\n\nEXAMPLES:\n", 15 | concat_line!(prefix "favicon-generator ", 16 | "/path/to/image /path/to/folder # Uses /path/to/image to generate favicons into /path/to/folder", 17 | ) 18 | ); 19 | 20 | #[derive(Debug, Parser)] 21 | #[command(name = APP_NAME)] 22 | #[command(term_width = terminal_size().map(|(width, _)| width.0 as usize).unwrap_or(0))] 23 | #[command(version = CARGO_PKG_VERSION)] 24 | #[command(author = CARGO_PKG_AUTHORS)] 25 | #[command(after_help = AFTER_HELP)] 26 | pub struct CLIArgs { 27 | #[arg(value_hint = clap::ValueHint::FilePath)] 28 | #[arg(help = "Assign an image for generating favicons. It should be a path of a file")] 29 | pub input_path: PathBuf, 30 | 31 | #[arg(value_hint = clap::ValueHint::DirPath)] 32 | #[arg( 33 | help = "Assign a destination of your generated files. It should be a path of a directory" 34 | )] 35 | pub output_path: PathBuf, 36 | 37 | #[arg(short = 'y', long)] 38 | #[arg(help = "Overwrite exiting files without asking")] 39 | pub overwrite: bool, 40 | 41 | #[arg(long)] 42 | #[arg(default_value = "/")] 43 | #[arg(help = "Specify the path prefix of your favicon files")] 44 | pub path_prefix: String, 45 | 46 | #[arg(long)] 47 | #[arg(help = "Disable the automatic sharpening")] 48 | pub no_sharpen: bool, 49 | 50 | #[arg(long)] 51 | #[arg(default_value = "App")] 52 | #[arg(help = "Assign a name for your web app")] 53 | pub app_name: String, 54 | 55 | #[arg(long)] 56 | #[arg(help = "Assign a short name for your web app")] 57 | pub app_short_name: Option, 58 | } 59 | 60 | pub fn get_args() -> CLIArgs { 61 | let args = CLIArgs::command(); 62 | 63 | let about = format!("{APP_NAME} {CARGO_PKG_VERSION}\n{CARGO_PKG_AUTHORS}\n{APP_ABOUT}"); 64 | 65 | let args = args.about(about); 66 | 67 | let matches = args.get_matches(); 68 | 69 | match CLIArgs::from_arg_matches(&matches) { 70 | Ok(args) => args, 71 | Err(err) => { 72 | err.exit(); 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/ci-version.yml: -------------------------------------------------------------------------------- 1 | name: CI-version 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | # - macos-latest # jpeg will break things 19 | toolchain: 20 | - stable 21 | - nightly 22 | features: 23 | - 24 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - name: Install libwebp (Linux) 28 | run: | 29 | sudo apt update 30 | sudo apt install libwebp-dev 31 | if: runner.os == 'Linux' 32 | - name: Install libwebp (macOS) 33 | run: | 34 | brew update 35 | brew list webp || brew install webp 36 | if: runner.os == 'macOS' 37 | - name: Install ImageMagick 38 | run: | 39 | wget https://imagemagick.org/archive/ImageMagick.tar.gz 40 | tar xf ImageMagick.tar.gz 41 | mkdir /tmp/ImageMagick-lib 42 | cd ImageMagick-* 43 | ./configure --enable-hdri 44 | make -j$(nproc) 45 | sudo make install 46 | - run: sudo ldconfig 47 | if: runner.os == 'Linux' 48 | - uses: actions/checkout@v4 49 | - uses: actions-rust-lang/setup-rust-toolchain@v1 50 | with: 51 | toolchain: ${{ matrix.toolchain }} 52 | - run: cargo test --release ${{ matrix.features }} 53 | - run: cargo doc --release ${{ matrix.features }} 54 | 55 | MSRV: 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | os: 60 | - ubuntu-latest 61 | # - macos-latest # jpeg will break things 62 | toolchain: 63 | - "1.74" 64 | features: 65 | - 66 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 67 | runs-on: ${{ matrix.os }} 68 | steps: 69 | - name: Install libwebp (Linux) 70 | run: | 71 | sudo apt update 72 | sudo apt install libwebp-dev 73 | if: runner.os == 'Linux' 74 | - name: Install libwebp (macOS) 75 | run: | 76 | brew update 77 | brew list webp || brew install webp 78 | if: runner.os == 'macOS' 79 | - name: Install ImageMagick 80 | run: | 81 | wget https://imagemagick.org/archive/ImageMagick.tar.gz 82 | tar xf ImageMagick.tar.gz 83 | mkdir /tmp/ImageMagick-lib 84 | cd ImageMagick-* 85 | ./configure --enable-hdri 86 | make -j$(nproc) 87 | sudo make install 88 | - run: sudo ldconfig 89 | if: runner.os == 'Linux' 90 | - uses: actions/checkout@v4 91 | - uses: actions-rust-lang/setup-rust-toolchain@v1 92 | with: 93 | toolchain: ${{ matrix.toolchain }} 94 | - run: cargo test --release --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | rustfmt: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions-rust-lang/setup-rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rustfmt 17 | - uses: actions-rust-lang/rustfmt@v1 18 | 19 | clippy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Install ImageMagick 23 | run: | 24 | sudo apt update 25 | sudo apt install libwebp-dev 26 | wget https://imagemagick.org/archive/ImageMagick.tar.gz 27 | tar xf ImageMagick.tar.gz 28 | mkdir /tmp/ImageMagick-lib 29 | cd ImageMagick-* 30 | ./configure --enable-hdri 31 | make -j$(nproc) 32 | sudo make install 33 | - uses: actions/checkout@v4 34 | - uses: actions-rust-lang/setup-rust-toolchain@v1 35 | with: 36 | components: clippy 37 | - run: cargo clippy --all-targets --all-features -- -D warnings 38 | 39 | tests: 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | os: 44 | - ubuntu-latest 45 | # - macos-latest # jpeg will break things 46 | toolchain: 47 | - stable 48 | - nightly 49 | features: 50 | - 51 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 52 | runs-on: ${{ matrix.os }} 53 | steps: 54 | - name: Install libwebp (Linux) 55 | run: | 56 | sudo apt update 57 | sudo apt install libwebp-dev 58 | if: runner.os == 'Linux' 59 | - name: Install libwebp (macOS) 60 | run: | 61 | brew update 62 | brew list webp || brew install webp 63 | if: runner.os == 'macOS' 64 | - name: Install ImageMagick 65 | run: | 66 | wget https://imagemagick.org/archive/ImageMagick.tar.gz 67 | tar xf ImageMagick.tar.gz 68 | mkdir /tmp/ImageMagick-lib 69 | cd ImageMagick-* 70 | ./configure --enable-hdri 71 | make -j$(nproc) 72 | sudo make install 73 | - run: sudo ldconfig 74 | if: runner.os == 'Linux' 75 | - uses: actions/checkout@v4 76 | - uses: actions-rust-lang/setup-rust-toolchain@v1 77 | with: 78 | toolchain: ${{ matrix.toolchain }} 79 | - run: cargo test ${{ matrix.features }} 80 | - run: cargo doc ${{ matrix.features }} 81 | 82 | MSRV: 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | os: 87 | - ubuntu-latest 88 | # - macos-latest # jpeg will break things 89 | toolchain: 90 | - "1.74" 91 | features: 92 | - 93 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 94 | runs-on: ${{ matrix.os }} 95 | steps: 96 | - name: Install libwebp (Linux) 97 | run: | 98 | sudo apt update 99 | sudo apt install libwebp-dev 100 | if: runner.os == 'Linux' 101 | - name: Install libwebp (macOS) 102 | run: | 103 | brew update 104 | brew list webp || brew install webp 105 | if: runner.os == 'macOS' 106 | - name: Install ImageMagick 107 | run: | 108 | wget https://imagemagick.org/archive/ImageMagick.tar.gz 109 | tar xf ImageMagick.tar.gz 110 | mkdir /tmp/ImageMagick-lib 111 | cd ImageMagick-* 112 | ./configure --enable-hdri 113 | make -j$(nproc) 114 | sudo make install 115 | - run: sudo ldconfig 116 | if: runner.os == 'Linux' 117 | - uses: actions/checkout@v4 118 | - uses: actions-rust-lang/setup-rust-toolchain@v1 119 | with: 120 | toolchain: ${{ matrix.toolchain }} 121 | - run: cargo test --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | 3 | use std::{fmt::Write as FmtWrite, fs, io, io::Write}; 4 | 5 | use anyhow::{anyhow, Context}; 6 | use cli::*; 7 | use scanner_rust::{generic_array::typenum::U8, Scanner}; 8 | use serde_json::json; 9 | 10 | const FILE_WEB_APP_MANIFEST: &str = "manifest.json"; 11 | const FILE_FAVICON: &str = "favicon.ico"; 12 | 13 | const ICO_SIZE: &[u16] = &[48, 32, 16]; 14 | const PNG_SIZES_FOR_ICON: &[u16] = &[196, 160, 95, 64, 32, 16]; 15 | const PNG_SIZES_FOR_APPLE_TOUCH_ICON: &[u16] = &[180, 152, 144, 120, 114, 76, 72, 60, 57]; 16 | 17 | fn main() -> anyhow::Result<()> { 18 | let args = get_args(); 19 | 20 | let web_app_manifest = args.output_path.join(FILE_WEB_APP_MANIFEST); 21 | let ico = args.output_path.join(FILE_FAVICON); 22 | 23 | let png_sizes = { 24 | let mut v = PNG_SIZES_FOR_ICON.to_vec(); 25 | 26 | v.extend_from_slice(PNG_SIZES_FOR_APPLE_TOUCH_ICON); 27 | 28 | v.sort(); 29 | 30 | v 31 | }; 32 | 33 | let png_vec = { 34 | let mut v = Vec::with_capacity(png_sizes.len()); 35 | 36 | for size in png_sizes.iter() { 37 | v.push(args.output_path.join(format!("favicon-{size}.png"))); 38 | } 39 | 40 | v 41 | }; 42 | 43 | match args.output_path.metadata() { 44 | Ok(metadata) => { 45 | if !metadata.is_dir() { 46 | return Err(anyhow!("{:?} is not a directory.", args.output_path)); 47 | } 48 | 49 | let need_overwrite = { 50 | let mut path_vec = Vec::with_capacity(2 + png_vec.len()); 51 | 52 | path_vec.push(&ico); 53 | path_vec.push(&web_app_manifest); 54 | 55 | for png in png_vec.iter() { 56 | path_vec.push(png); 57 | } 58 | 59 | let mut need_overwrite = false; 60 | 61 | for path in path_vec { 62 | match path.metadata() { 63 | Ok(metadata) => { 64 | if metadata.is_dir() { 65 | return Err(anyhow!("{path:?} is a directory.")); 66 | } 67 | 68 | need_overwrite = true; 69 | }, 70 | Err(error) if error.kind() == io::ErrorKind::NotFound => { 71 | // do nothing 72 | }, 73 | Err(error) => { 74 | return Err(error).with_context(|| anyhow!("{path:?}")); 75 | }, 76 | } 77 | } 78 | 79 | need_overwrite 80 | }; 81 | 82 | if need_overwrite && !args.overwrite { 83 | let mut sc: Scanner<_, U8> = Scanner::new2(io::stdin()); 84 | 85 | loop { 86 | print!("Overwrite files? [Y/N] "); 87 | io::stdout().flush().with_context(|| "stdout")?; 88 | 89 | match sc.next_line().with_context(|| "stdin")? { 90 | Some(token) => match token.to_ascii_uppercase().as_str() { 91 | "Y" => { 92 | break; 93 | }, 94 | "N" => { 95 | return Ok(()); 96 | }, 97 | _ => { 98 | continue; 99 | }, 100 | }, 101 | None => { 102 | return Ok(()); 103 | }, 104 | } 105 | } 106 | } 107 | }, 108 | Err(error) if error.kind() == io::ErrorKind::NotFound => { 109 | fs::create_dir_all(args.output_path.as_path()) 110 | .with_context(|| anyhow!("{:?}", args.output_path))?; 111 | }, 112 | Err(error) => { 113 | return Err(error).with_context(|| anyhow!("{:?}", args.output_path)); 114 | }, 115 | } 116 | 117 | let input = image_convert::ImageResource::Data( 118 | fs::read(args.input_path.as_path()).with_context(|| anyhow!("{:?}", args.input_path))?, 119 | ); 120 | 121 | let mut tera = tera::Tera::default(); 122 | 123 | tera.add_raw_template("browser-config", include_str!("resources/browser-config.xml")).unwrap(); 124 | 125 | { 126 | // web_app_manifest 127 | let mut icons = Vec::with_capacity(PNG_SIZES_FOR_ICON.len()); 128 | 129 | for size in PNG_SIZES_FOR_ICON { 130 | let src = format!("{path_prefix}favicon-{size}.png", path_prefix = args.path_prefix); 131 | let sizes = format!("{size}x{size}"); 132 | 133 | icons.push(json!({ 134 | "src": src, 135 | "sizes": sizes, 136 | "type": "image/png", 137 | })); 138 | } 139 | 140 | let mut content = json!( 141 | { 142 | "name": args.app_name, 143 | "icons": icons, 144 | } 145 | ); 146 | 147 | if let Some(app_short_name) = args.app_short_name { 148 | content.as_object_mut().unwrap().insert("short_name".into(), app_short_name.into()); 149 | } 150 | 151 | let content = serde_json::to_string(&content).unwrap(); 152 | 153 | fs::write(web_app_manifest.as_path(), content) 154 | .with_context(|| anyhow!("{web_app_manifest:?}"))?; 155 | } 156 | 157 | let (input, vector) = { 158 | let mut pgm_config = image_convert::PGMConfig::new(); 159 | 160 | pgm_config.background_color = Some(image_convert::ColorName::White); 161 | pgm_config.crop = Some(image_convert::Crop::Center(1f64, 1f64)); 162 | 163 | let (mw, vector) = image_convert::fetch_magic_wand(&input, &pgm_config) 164 | .with_context(|| anyhow!("fetch_magic_wand {:?}", args.input_path))?; 165 | 166 | let mw_input = image_convert::ImageResource::MagickWand(mw); 167 | 168 | (mw_input, vector) 169 | }; 170 | 171 | let sharpen = if vector { false } else { !args.no_sharpen }; 172 | 173 | { 174 | // ico 175 | let mut ico_config = image_convert::ICOConfig::new(); 176 | 177 | if !sharpen { 178 | ico_config.sharpen = 0f64; 179 | } 180 | 181 | for size in ICO_SIZE.iter().copied() { 182 | ico_config.size.push((size, size)); 183 | } 184 | 185 | let mut output = image_convert::ImageResource::from_path(ico.as_path()); 186 | 187 | image_convert::to_ico(&mut output, &input, &ico_config) 188 | .with_context(|| anyhow!("to_ico {ico:?}"))?; 189 | } 190 | 191 | { 192 | // png_vec 193 | for (i, png) in png_vec.iter().enumerate() { 194 | let size = png_sizes[i]; 195 | 196 | let mut png_config = image_convert::PNGConfig::new(); 197 | png_config.shrink_only = false; 198 | png_config.width = size; 199 | png_config.height = size; 200 | 201 | if !sharpen { 202 | png_config.sharpen = 0f64; 203 | } 204 | 205 | let mut output = image_convert::ImageResource::from_path(png.as_path()); 206 | 207 | image_convert::to_png(&mut output, &input, &png_config) 208 | .with_context(|| anyhow!("to_ico {png:?}"))?; 209 | } 210 | } 211 | 212 | let ico_sizes_concat = { 213 | let mut s = String::new(); 214 | 215 | for size in ICO_SIZE { 216 | s.write_fmt(format_args!("{size}x{size} ")).unwrap(); 217 | } 218 | 219 | s.truncate(s.len() - 1); 220 | 221 | s 222 | }; 223 | 224 | tera.add_raw_template("html-head", include_str!("resources/favicon.html")).unwrap(); 225 | 226 | let mut context = tera::Context::new(); 227 | 228 | context.insert( 229 | "path_prefix", 230 | html_escape::encode_double_quoted_attribute(args.path_prefix.as_str()).as_ref(), 231 | ); 232 | context.insert("web_app_manifest", FILE_WEB_APP_MANIFEST); 233 | context.insert("apple_touch_icon_sizes", PNG_SIZES_FOR_APPLE_TOUCH_ICON); 234 | context.insert("icon_sizes", PNG_SIZES_FOR_ICON); 235 | context.insert("ico_sizes_concat", &ico_sizes_concat); 236 | 237 | let content = tera.render("html-head", &context).unwrap(); 238 | 239 | println!("{content}"); 240 | 241 | Ok(()) 242 | } 243 | --------------------------------------------------------------------------------