├── 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 | [](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 |
--------------------------------------------------------------------------------