genicons 0.2.0

A quick utility to generate favicons
Documentation
use std::{fs};
use std::fs::File;
use std::io::Write;
use std::process::ExitCode;
use arboard::{Clipboard};
use clap::Parser;
use crossterm::style::Stylize;
use ico::{IconDir, IconDirEntry, IconImage, ResourceType};
use photon_rs::native::{open_image, save_image};
use photon_rs::transform;
use photon_rs::transform::SamplingFilter;
use rayon::prelude::*;
use genicons::arguments::Arguments;
use genicons::manifest::{WebIcon, WebManifest};

const SIZE_NAME_PAIRS: [(&str, u32, u32); 5] = [
    ("android-chrome-192x192.png", 192, 192),
    ("android-chrome-512x512.png", 512, 512),
    ("apple-touch-icon.png", 180, 180),
    ("favicon-16.png", 16, 16),
    ("favicon-32.png", 32, 32)
];

fn main() -> ExitCode {
    let args = Arguments::parse();

    let master_image = match open_image(&args.master_image) {
        Ok(i) => i,
        Err(e) => {
            eprintln!("{}", format!("Failed to open image {:?}. {}", args.master_image, e).red());

            return ExitCode::FAILURE
        }
    };

    let hosted_path = args.path.trim_end_matches("/");

    match fs::exists(&args.output_dir) {
        Ok(false) | Err(_) => {
            match fs::create_dir(&args.output_dir) {
                Err(e) => {
                    eprintln!("{}", format!("Failed to create output directory. {}", e).red());
                    return ExitCode::FAILURE;
                }
                _ => {}
            };
        }
        Ok(true) => {}
    }

    if !args.silent {
        println!("{}", "Generating icon files".green());
    }

    SIZE_NAME_PAIRS.into_par_iter().for_each(|(name, width, height)| {
        let resized = transform::resize(&master_image, width, height, SamplingFilter::CatmullRom);

        if let Err(e) = save_image(resized, &args.output_dir.join(name)) {
            eprintln!("Failed to save image {:?}. {}", name, e)
        }
    });

    if !args.silent {
        println!("{}", "Generating multi-size favicon.ico".green());
    }

    let mut out_ico = IconDir::new(ResourceType::Icon);

    for (width, height) in [(16, 16), (32, 32), (48, 48)] {
        let resized = transform::resize(&master_image, width, height, SamplingFilter::CatmullRom);

        let ico = IconImage::from_rgba_data(width, height, resized.get_raw_pixels());

        let entry = match IconDirEntry::encode(&ico) {
            Ok(i) => i,
            Err(e) => {
                eprintln!("{}", format!("Failed to create ico entry for size {}x{}. {}", width, height, e));
                continue
            }
        };

        out_ico.add_entry(entry)
    }

    let out_file = match File::create(&args.output_dir.join("favicon.ico")) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("{}", format!("Failed to create favicon.ico. {}", e));
            return ExitCode::FAILURE
        }
    };

    if let Err(e) = out_ico.write(out_file) {
        eprintln!("{}", format!("Failed to write favicon.ico. {}", e));
        return ExitCode::FAILURE
    }

    if !args.silent {
        println!("{}", "Generating manifest".green());
    }

    let icons = vec![
        WebIcon{
            src: format!("{}/android-chrome-192x192.png", &hosted_path),
            sizes: "192x192",
            typ: "image/png",
        },
        WebIcon{
            src: format!("{}/android-chrome-512x512.png", &hosted_path),
            sizes: "512x512",
            typ: "image/png",
        }
    ];
    let man = WebManifest{
        short_name: args.short_name.clone(),
        start_url: &args.start_url,
        display: "standalone",
        theme_color: &args.app_color,
        name: &args.app_name,
        icons,
    };

    let js = match serde_json::to_string(&man) {
        Ok(js) => js,
        Err(e) => {
            eprintln!("{}", format!("Failed to create manifest.json. {}", e).red());
            return ExitCode::FAILURE
        }
    };

    let mut man_file = match File::create(&args.output_dir.join("manifest.json")) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("{}", format!("Failed to create manifest.json. {}", e));
            return ExitCode::FAILURE
        }
    };

    if let Err(e) =man_file.write_all(js.as_bytes()) {
        eprintln!("{}", format!("Failed to write manifest.json. {}", e));
        return ExitCode::FAILURE
    }

    let html = format!(r#"<link rel="apple-touch-icon" sizes="180x180" href="{}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{}/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{}/favicon-16.png">
<link rel="manifest" href="{}/manifest.json">"#, &hosted_path, &hosted_path, &hosted_path, &hosted_path);

    if !loop {
        if args.no_html_copy {
            break false;
        }

        let mut clip = match Clipboard::new() {
            Ok(c) => c,
            Err(e) => {
                println!("{e}");
                eprintln!("{}", "Failed to setup clipboard".red());
                break false;
            }
        };

        if let Err(e) = clip.set_text(&html) {
            eprintln!("{}", "Failed to write to clipboard".red());
            println!("{}", e);
            break false;
        }

        break true
    } {
        if !args.silent {
            println!("{}", "Clipboard disabled or failed. Copy this in the head of your html:".yellow());
            println!("{}", html)
        }
    } else {
        if !args.silent {
            println!("{}", "HTML Head copied.".green())
        }
    }

    if !args.silent {
        println!("{}", "Page meta generated!".green());
    }

    ExitCode::SUCCESS
}