Skip to content

Commit

Permalink
✨ Update definition of colorschemes to simpler format
Browse files Browse the repository at this point in the history
  • Loading branch information
TaylorBeeston committed Aug 6, 2024
1 parent 5d78eb4 commit e3a1ffb
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 27 deletions.
74 changes: 53 additions & 21 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ struct SerializedAppConfig {
spatial_averaging_radius: String,
}

fn load_config(config_path: Option<&str>) -> Result<SerializedAppConfig, AppError> {
#[derive(Debug)]
pub struct ConfigInfo {
config: SerializedAppConfig,
config_dir: PathBuf,
}

fn load_config(config_path: Option<&str>) -> Result<ConfigInfo, AppError> {
let mut builder = ConfigBuilder::default();

builder = builder
Expand All @@ -86,32 +92,62 @@ fn load_config(config_path: Option<&str>) -> Result<SerializedAppConfig, AppErro
.set_default("dither_amount", "0.1")?
.set_default("spatial_averaging_radius", "10")?;

let default_config_path = dirs::home_dir()
let default_config_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from(""))
.join(".config/colorizer/config.toml");
.join(".config/image-colorizer");
let default_config_path = default_config_dir.join("config.toml");

let (config_path, config_dir) = if let Some(path) = config_path {
(
PathBuf::from(path),
PathBuf::from(path).parent().unwrap().to_path_buf(),
)
} else if default_config_path.exists() {
(default_config_path, default_config_dir)
} else {
(PathBuf::new(), default_config_dir)
};

if default_config_path.exists() {
if config_path.exists() {
builder = ConfigBuilder::<DefaultState>::add_source(
builder,
File::from(default_config_path).required(false),
File::from(config_path).required(false),
);
}

if let Some(path) = config_path {
builder = builder.add_source(File::with_name(path).required(true));
}

let config = builder.build()?;

config.try_deserialize().map_err(AppError::from)
Ok(ConfigInfo {
config: config.try_deserialize()?,
config_dir,
})
}

fn parse_colorscheme(content: &str) -> Vec<String> {
content
.lines()
.filter_map(|line| {
let trimmed = line.split("//").next().unwrap_or("").trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
})
.collect()
}

fn load_colorscheme(name: &str, config_dir: &Path) -> Result<Vec<String>, AppError> {
let colorscheme_path = config_dir.join(format!("{}.toml", name));
let colorscheme_path = config_dir.join(format!("{}.txt", name));
if colorscheme_path.exists() {
let colorscheme_str = fs::read_to_string(colorscheme_path)?;
let colorscheme: Vec<String> = toml::from_str(&colorscheme_str)?;
Ok(colorscheme)
let colorscheme = parse_colorscheme(&colorscheme_str);

if colorscheme.is_empty() {
Err(AppError::Other(format!("Colorscheme '{}' is empty", name)))
} else {
Ok(colorscheme)
}
} else if name == "kanagawa" {
Ok(KANAGAWA.iter().map(|&s| s.to_string()).collect())
} else {
Expand Down Expand Up @@ -149,7 +185,7 @@ pub fn init() -> Result<Arc<AppConfig>, AppError> {
.version(VERSION)
.author("Taylor Beeston")
.about("Applies color schemes to images")
.after_help("Config should be a TOML that contains a colorscheme and a Blend Factor.\n\nBlend Factor is a [0.0-1.0] float. Higher values will make the image adhere more strictly to the colorscheme. Lower values will make artifacting less visible. Colorscheme is a string that should be the name of a TOML file (minus the extension) in the same directory as the config file. For example if 'kanagawa' is used as the name of the colorscheme string, there should be a 'kanagawa.toml' file in the same directory as the config file.")
.after_help("Config should be a TOML that contains a colorscheme and a Blend Factor.\n\nBlend Factor is a [0.0-1.0] float. Higher values will make the image adhere more strictly to the colorscheme. Lower values will make artifacting less visible. Colorscheme is a string that should be the name of a colorscheme txt file (minus the extension) in the same directory as the config file. For example if 'kanagawa' is used as the name of the colorscheme string, there should be a 'kanagawa.txt' file in the same directory as the config file.\n\nColorscheme files are simple files with one hex code per line and may optionally have comments using double slashes, e.g.\n\n// Grayscale\n#fff\n#000")
.arg(
Arg::with_name("Blend Factor")
.short('b')
Expand Down Expand Up @@ -184,7 +220,7 @@ pub fn init() -> Result<Arc<AppConfig>, AppError> {
Arg::with_name("config")
.short('c')
.long("config")
.value_name("/path/to/image.png")
.value_name("/path/to/config.toml")
.help("Sets a custom config file")
.takes_value(true),
)
Expand All @@ -205,20 +241,16 @@ pub fn init() -> Result<Arc<AppConfig>, AppError> {
)
.get_matches();

let config = load_config(matches.value_of("config"))?;
let ConfigInfo { config, config_dir } = load_config(matches.value_of("config"))?;

let input_paths: Vec<&str> = matches.values_of("Image Paths").unwrap().collect();
let output_dir = matches.value_of("output").map(PathBuf::from);

let input_output_pairs =
generate_input_output_pairs(&input_paths, output_dir, &config.colorscheme)?;

let config_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from(""))
.join(".config/colorizer");

let colors = load_colorscheme(&config.colorscheme, &config_dir)?;
let colors: Vec<Srgb<f32>> = colors.iter().map(|hex| hex_to_rgb(hex)).collect();
let colors: Vec<Srgb<f32>> = colors.iter().map(|hex| hex_to_rgb(hex).unwrap()).collect();

let blend_factor = matches
.value_of("Blend Factor")
Expand Down
38 changes: 32 additions & 6 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,38 @@ use image::RgbImage;
use indicatif::ProgressBar;
use palette::{IntoColor, Lab, Srgb};

pub fn hex_to_rgb(hex: &str) -> Srgb<f32> {
let hex = hex.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap() as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).unwrap() as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).unwrap() as f32 / 255.0;
Srgb::new(r, g, b)
pub fn hex_to_rgb(input: &str) -> Result<Srgb<f32>, String> {
let cleaned = input.trim_start_matches('#');

match cleaned.len() {
3 => {
// Three-character hex code
let r = u8::from_str_radix(&cleaned[0..1].repeat(2), 16).map_err(|e| e.to_string())?
as f32
/ 255.0;
let g = u8::from_str_radix(&cleaned[1..2].repeat(2), 16).map_err(|e| e.to_string())?
as f32
/ 255.0;
let b = u8::from_str_radix(&cleaned[2..3].repeat(2), 16).map_err(|e| e.to_string())?
as f32
/ 255.0;
Ok(Srgb::new(r, g, b))
}
6 => {
// Six-character hex code
let r =
u8::from_str_radix(&cleaned[0..2], 16).map_err(|e| e.to_string())? as f32 / 255.0;
let g =
u8::from_str_radix(&cleaned[2..4], 16).map_err(|e| e.to_string())? as f32 / 255.0;
let b =
u8::from_str_radix(&cleaned[4..6], 16).map_err(|e| e.to_string())? as f32 / 255.0;
Ok(Srgb::new(r, g, b))
}
_ => Err(format!(
"Invalid input: '{}'. Expected a 3 or 6-digit hex code.",
input
)),
}
}

pub fn interpolate_color(color1: &Lab, color2: &Lab, t: f32) -> Lab {
Expand Down

0 comments on commit e3a1ffb

Please sign in to comment.