diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..124ebfe
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+github: fang2hou
+patreon: fang2hou
+ko_fi: fang2hou
+custom: ["http://paypal.me/fang2h0u", "https://afdian.net/@fang2hou"]
\ No newline at end of file
diff --git a/.github/workflows/auto-assign.yaml b/.github/workflows/auto-assign.yaml
new file mode 100644
index 0000000..b7a0ef9
--- /dev/null
+++ b/.github/workflows/auto-assign.yaml
@@ -0,0 +1,20 @@
+name: 🤖 Auto Assign
+on:
+ issues:
+ types: [opened, reopened]
+ pull_request:
+ types: [opened, reopened]
+jobs:
+ run:
+ name: 👦🏻 Assign
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Assign
+ uses: pozil/auto-assign-issue@v1
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ assignees: ${{ vars.DEFAULT_ASSIGNEE }}
+ numOfAssignee: 1
diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml
new file mode 100644
index 0000000..caf9158
--- /dev/null
+++ b/.github/workflows/auto-release.yaml
@@ -0,0 +1,41 @@
+name: 🚀 Release
+on:
+ push:
+ tags:
+ - 'v*'
+jobs:
+ tag-check:
+ name: 🏷️ Tag Check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check tag
+ run: echo "${{ github.ref_name }}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[0-9]+)?)?$'
+ check:
+ name: 🧐 Pre-release check
+ runs-on: ubuntu-latest
+ needs: tag-check
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup toolchain
+ run: rustup component add clippy rustfmt
+ - name: Build
+ run: cargo build --verbose
+ - name: Test
+ run: cargo test --verbose
+ - name: Lint
+ run: cargo clippy --verbose
+ - name: Format check
+ run: cargo fmt -- --check
+ release:
+ name: 🆕 New release
+ runs-on: ubuntu-latest
+ needs: check
+ steps:
+ - name: Create release via GitHub CLI
+ env:
+ GITHUB_TOKEN: ${{ secrets.ACTIONS_PERSONAL_ACCESS_TOKEN }}
+ tag: ${{ github.ref_name }}
+ run: gh release create "$tag" --repo="$GITHUB_REPOSITORY" --title="${GITHUB_REPOSITORY#*/} v${tag#v}" --generate-notes
diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml
new file mode 100644
index 0000000..5104aa6
--- /dev/null
+++ b/.github/workflows/release-build.yaml
@@ -0,0 +1,51 @@
+name: 🏗️ Release Build
+on:
+ release:
+ types: [published]
+env:
+ CARGO_TERM_COLOR: always
+jobs:
+ check:
+ name: 🧐 Pre-build check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup toolchain
+ run: rustup component add clippy rustfmt
+ - name: Build
+ run: cargo build --verbose
+ - name: Test
+ run: cargo test --verbose
+ - name: Lint
+ run: cargo clippy --verbose
+ - name: Format check
+ run: cargo fmt -- --check
+ build:
+ name: 🏗️ Build ${{ matrix.target }}
+ runs-on: ubuntu-latest
+ needs: check
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x86_64-pc-windows-gnu
+ archive: zip
+ - target: x86_64-unknown-linux-musl
+ archive: tar.gz tar.xz
+ - target: x86_64-apple-darwin
+ archive: zip
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Compile
+ uses: rust-build/rust-build.action@v1.4.5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ RUSTTARGET: ${{ matrix.target }}
+ EXTRA_FILES: 'README.md LICENSE'
diff --git a/.github/workflows/release-notify.yaml b/.github/workflows/release-notify.yaml
new file mode 100644
index 0000000..c22f681
--- /dev/null
+++ b/.github/workflows/release-notify.yaml
@@ -0,0 +1,23 @@
+name: 📢 Release Notify
+on:
+ release:
+ types: [published]
+jobs:
+ notify:
+ name: 📢 Notify
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout to actions repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ repository: wind-addons/actions
+ ref: main
+ - name: Send message to Discord
+ env:
+ RELEASE_EVENT_JSON: ${{ toJson(github.event.release) }}
+ REPOSITORY_NAME: ${{ github.repository }}
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ALL_RELEASE }}
+ BAR_IMAGE: ${{ vars.DISCORD_MESSAGE_BAR_IMAGE }}
+ ROCKET_IMAGE: ${{ vars.DISCORD_MESSAGE_ROCKET_IMAGE }}
+ run: node discord/release-message.js
\ No newline at end of file
diff --git a/.github/workflows/rust-check.yaml b/.github/workflows/rust-check.yaml
new file mode 100644
index 0000000..1a2cbea
--- /dev/null
+++ b/.github/workflows/rust-check.yaml
@@ -0,0 +1,25 @@
+name: 🦀 Rust
+on:
+ push:
+ branches: ["main", "master"]
+ pull_request:
+ types: [opened, synchronize, reopened]
+jobs:
+ check:
+ name: 🧐 Check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup toolchain
+ run: rustup component add clippy rustfmt
+ - name: Build
+ run: cargo build --verbose
+ - name: Test
+ run: cargo test --verbose
+ - name: Lint
+ run: cargo clippy --verbose
+ - name: Format check
+ run: cargo fmt -- --check
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..98c3917
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+# IDEs
+.idea
+.vscode
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..816b0cc
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "flemoji"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "flemoji"
+path = "src/bin/flemoji.rs"
+
+[dependencies]
+image = "0.25.2"
+walkdir = "2.5.0"
+rayon = "1.10.0"
+clap = { version = "4.5.13", features = ["derive"] }
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..aa5fa9e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Wind Addons
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..79af46d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# ✨ flemoji
+
+A simple tool to customize Microsoft Fluent UI emojis with given size and type written in Rust.
+
+
+📖 Table of Contents
+
+- [🚚 Installation](#-installation)
+- [💡 Usage](#-usage)
+ - [Help](#help)
+ - [Example](#example)
+- [📜 License](#-license)
+
+
+
+## 🚚 Installation
+
+```bash
+cargo install --git https://github.com/wind-addons/flemoji
+```
+
+## 💡 Usage
+
+### Help
+
+```text
+flemoji - Customize Microsoft Fluent UI emoji images.
+
+Usage: flemoji.exe [OPTIONS] --width --height --from --to
+
+Options:
+ -W, --width Sets the width of the output images
+ -H, --height Sets the height of the output images
+ --from Sets the input directory (assets)
+ --to Sets the output directory
+ -T, --filetype Sets the output file type (png, jpg, etc.) [default: png]
+ -h, --help Print help
+ -V, --version Print version
+```
+
+### Example
+
+```bash
+flemoji --width 64 --height 64 --from ./assets --to ./output --type png
+```
+
+## 📜 License
+
+MIT
diff --git a/src/bin/flemoji.rs b/src/bin/flemoji.rs
new file mode 100644
index 0000000..ee5ebb4
--- /dev/null
+++ b/src/bin/flemoji.rs
@@ -0,0 +1,142 @@
+use clap::{Arg, Command};
+use image::ImageFormat;
+use rayon::prelude::*;
+use std::fs;
+use std::path::Path;
+use walkdir::WalkDir;
+
+#[derive(Debug)]
+struct CustomOptions {
+ input_dir: String,
+ output_dir: String,
+ width: u32,
+ height: u32,
+ file_type: ImageFormat,
+}
+
+fn parse_args() -> CustomOptions {
+ let matches = Command::new("flemoji")
+ .version(env!("CARGO_PKG_VERSION"))
+ .about("flemoji - Customize Microsoft Fluent UI emoji images.")
+ .author("fang2hou")
+ .arg(
+ Arg::new("width")
+ .short('W')
+ .long("width")
+ .value_name("WIDTH")
+ .help("Sets the width of the output images")
+ .required(true),
+ )
+ .arg(
+ Arg::new("height")
+ .short('H')
+ .long("height")
+ .value_name("HEIGHT")
+ .help("Sets the height of the output images")
+ .required(true),
+ )
+ .arg(
+ Arg::new("from")
+ .long("from")
+ .value_name("DIR")
+ .help("Sets the input directory (assets)")
+ .required(true),
+ )
+ .arg(
+ Arg::new("to")
+ .long("to")
+ .value_name("DIR")
+ .help("Sets the output directory")
+ .required(true),
+ )
+ .arg(
+ Arg::new("filetype")
+ .short('T')
+ .long("filetype")
+ .value_name("FILETYPE")
+ .help("Sets the output file type (png, jpg, etc.)")
+ .default_value("png"),
+ )
+ .get_matches();
+
+ CustomOptions {
+ input_dir: matches.get_one::("from").unwrap().clone(),
+ output_dir: matches.get_one::("to").unwrap().clone(),
+ width: matches.get_one::("width").unwrap().parse().unwrap(),
+ height: matches
+ .get_one::("height")
+ .unwrap()
+ .parse()
+ .unwrap(),
+ file_type: match matches.get_one::("filetype").unwrap().as_str() {
+ "png" => ImageFormat::Png,
+ "jpg" | "jpeg" => ImageFormat::Jpeg,
+ "gif" => ImageFormat::Gif,
+ "bmp" => ImageFormat::Bmp,
+ "ico" => ImageFormat::Ico,
+ "tga" => ImageFormat::Tga,
+ _ => panic!("Unsupported file type"),
+ },
+ }
+}
+
+fn resize_and_save_image(
+ input_path: &Path,
+ output_dir: &str,
+ width: u32,
+ height: u32,
+ format: ImageFormat,
+) -> Result<(), Box> {
+ let img = image::open(input_path)?;
+ let img = img.resize(width, height, image::imageops::Lanczos3);
+ let output_path = Path::new(output_dir).join(
+ input_path
+ .file_name()
+ .ok_or("Invalid file name")?
+ .to_str()
+ .ok_or("Invalid file name")?
+ .replace(
+ &format!(".{}", input_path.extension().unwrap().to_str().unwrap()),
+ &format!(".{}", format.extensions_str()[0]),
+ ),
+ );
+ img.save_with_format(&output_path, format)?;
+ Ok(())
+}
+
+fn main() -> Result<(), Box> {
+ let args = parse_args();
+
+ fs::create_dir_all(&args.output_dir)?;
+
+ WalkDir::new(&args.input_dir)
+ .into_iter()
+ .filter_map(Result::ok)
+ .filter(|e| e.path().is_file())
+ .filter(|e| e.path().extension().and_then(|e| e.to_str()) == Some("png"))
+ .filter(|e| {
+ e.path()
+ .parent()
+ .and_then(|p| p.to_str())
+ .map_or(false, |s| s.contains("3D"))
+ })
+ .par_bridge()
+ .for_each(|entry| {
+ match resize_and_save_image(
+ entry.path(),
+ &args.output_dir,
+ args.width,
+ args.height,
+ args.file_type,
+ ) {
+ Ok(_) => println!("Processed: {}", entry.path().display()),
+ Err(e) => eprintln!("Error processing {}: {}", entry.path().display(), e),
+ }
+ });
+
+ println!(
+ "Conversion complete. Resized files saved in {}",
+ args.output_dir
+ );
+ Ok(())
+}