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(()) +}