Add parsing of image filter strings

This commit is contained in:
Moritz Ruth 2023-12-30 20:10:45 +01:00
parent 85214d6d35
commit 3fe1912389
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
7 changed files with 182 additions and 79 deletions

1
Cargo.lock generated
View file

@ -643,6 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
dependencies = [
"bytemuck",
"serde",
]
[[package]]

View file

@ -8,7 +8,7 @@ color-eyre = "0.6.2"
humantime-serde = "1.1.1"
loupedeck_serial = { path = "../loupedeck_serial" }
piet = "0.6.2"
rgb = "0.8.37"
rgb = { version = "0.8.37", features = ["serde"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_regex = "1.1.0"
serde_with = "3.4.0"

View file

@ -3,7 +3,7 @@ icon = "@ph/microphone-light"
mode.audio_volume.mode = "input"
mode.audio_volume.regex = "Microphone"
mode.audio_volume.label.muted = "Muted"
mode.audio_volume.icon.inactive = "@ph/microphone-sllash-light[brighten=50,huerotate=red,opacity=90]"
mode.audio_volume.icon.inactive = "@ph/microphone-sllash-light[alpha=90|brighten=50|huerotate=red]"
mode.audio_volume.circle_indicator.color = "#ffffff"
mode.audio_volume.circle_indicator.width = 2
mode.audio_volume.circle_indicator.radius = 40

View file

@ -0,0 +1,36 @@
use std::str::FromStr;
use piet::kurbo::{Rect, Vec2};
pub fn parse_positive_int_vec2_from_str(s: &str, separator: char) -> Result<Vec2, ()> {
let values = s.split_once(separator);
if let Some((x, y)) = values {
if let Some(x) = usize::from_str(x).ok() {
if let Some(y) = usize::from_str(y).ok() {
return Ok(Vec2::new(x as f64, y as f64));
}
}
}
Err(())
}
pub fn parse_positive_rect_from_str(s: &str) -> Result<Rect, ()> {
let (pairs, is_corner_points_mode) = if let Some(pairs) = s.split_once('+') {
(pairs, false)
} else if let Some(pairs) = s.split_once('-') {
(pairs, true)
} else {
return Err(());
};
let first_vec = parse_positive_int_vec2_from_str(pairs.0, 'x')?;
let second_vec = parse_positive_int_vec2_from_str(pairs.1, 'x')?;
Ok(if is_corner_points_mode {
Rect::from_points(first_vec.to_point(), second_vec.to_point())
} else {
Rect::from_origin_size(first_vec.to_point(), second_vec.to_size())
})
}

View file

@ -1,40 +1,22 @@
use crate::model::geometry::parse_positive_rect_from_str;
use crate::model::rgb::parse_rgb8_from_hex_str;
use piet::kurbo::Rect;
use rgb::RGB8;
use serde_with::DeserializeFromStr;
use std::fmt::{Display, Formatter, Write};
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Clone)]
pub enum ImageTransform {
Invert,
Brighten(f32),
Crop(Rect),
HorizontalFlip,
VerticalFlip,
Rotate { quarter_clockwise_turns: u8 },
}
impl Display for ImageTransform {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImageTransform::Invert => f.write_str("invert"),
ImageTransform::Brighten(value) => f.write_fmt(format_args!("brighten={}", value)),
ImageTransform::Crop(_) => f.write_str("crop=<...>"),
ImageTransform::HorizontalFlip => f.write_str("horizontal_flip"),
ImageTransform::VerticalFlip => f.write_str("vertical_flip"),
ImageTransform::Rotate { quarter_clockwise_turns } => f.write_fmt(format_args!("rotate={}", quarter_clockwise_turns)),
}?;
Ok(())
}
}
#[derive(DeserializeFromStr)]
pub struct ImageFilter {
crop_original: Option<Rect>, // applied before scale and rotate
scale: f32,
clockwise_quarter_rotations: u8,
crop: Option<Rect>, // applied after scale and rotate
color: Option<RGB8>,
alpha: f32,
blur: f32,
transforms: Vec<ImageTransform>,
grayscale: bool,
invert: bool,
}
#[derive(Debug, Error)]
@ -43,17 +25,17 @@ pub enum ImageFilterFromStringError {
UnknownFilter { name: String },
#[error("Filter {name} can only be used once.")]
SingletonSpecifiedMoreThanOnce { name: String },
FilterUsedMoreThanOnce { name: String },
#[error("All singleton filters must be placed before any transforms, but {singleton} was placed after {transform}.")]
SingletonSpecifiedAfterTransform { singleton: String, transform: ImageTransform },
#[error("{quarter_clockwise_turns} 90° turns can be simplified.")]
PointlessRotation { quarter_clockwise_turns: u8 },
#[error("Only the following values are for rotate: -90, 90, 180, 270")]
RotationNotAllowed,
#[error("Filter {filter_name} requires a value ({filter_name}=<value>).")]
FilterValueMissing { filter_name: String },
#[error("Filter {filter_name} does not require or allow a value.")]
FilterValueIgnored { filter_name: String },
#[error("The following value supplied to {filter_name} could not be parsed: {raw_value}")]
FilterValueNotParsable { filter_name: String, raw_value: String },
@ -64,14 +46,51 @@ pub enum ImageFilterFromStringError {
impl FromStr for ImageFilter {
type Err = ImageFilterFromStringError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn from_str<'a>(s: &str) -> Result<Self, Self::Err> {
fn parse_rect_filter_value(filter_name: &str, raw_value: String) -> Result<Rect, ImageFilterFromStringError> {
parse_positive_rect_from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})
}
fn parse_f32_filter_value(
filter_name: &str,
raw_value: String,
range_string: &str,
check_range: Box<dyn Fn(&f32) -> bool>,
) -> Result<f32, ImageFilterFromStringError> {
let value = f32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?;
if !check_range(&value) {
return Err(ImageFilterFromStringError::FilterValueNotInRange {
filter_name: filter_name.to_string(),
range: range_string.to_string(),
value: value.to_string(),
});
}
Ok(value)
}
let filters: Vec<&str> = s.split('|').map(|f| f.trim()).collect();
let scale: Option<f32> = None;
let alpha: Option<f32> = None;
let blur: Option<f32> = None;
let mut result = ImageFilter {
crop_original: None,
scale: 1.0,
clockwise_quarter_rotations: 0,
crop: None,
color: None,
alpha: 1.0,
blur: 0.0,
grayscale: false,
invert: false,
};
let mut transforms: Vec<ImageTransform> = Vec::new();
let mut previous_filter_names: Vec<String> = Vec::new();
for filter in filters {
let split_filter = filter.split_once('=');
@ -81,6 +100,23 @@ impl FromStr for ImageFilter {
(filter.to_owned(), None)
};
if previous_filter_names.contains(&filter_name) {
return Err(ImageFilterFromStringError::FilterUsedMoreThanOnce { name: filter_name });
} else {
previous_filter_names.push(filter_name.clone());
}
let value_is_present = optional_raw_value.is_some();
let use_bool_value = || -> Result<bool, ImageFilterFromStringError> {
if value_is_present {
Err(ImageFilterFromStringError::FilterValueIgnored {
filter_name: filter_name.clone(),
})
} else {
Ok(true)
}
};
let use_raw_value = || -> Result<String, ImageFilterFromStringError> {
optional_raw_value.ok_or_else(|| ImageFilterFromStringError::FilterValueMissing {
filter_name: filter_name.clone(),
@ -88,51 +124,42 @@ impl FromStr for ImageFilter {
};
match filter_name.as_str() {
"scale" | "alpha" | "blur" => {
if let Some(transform) = transforms.first() {
return Err(ImageFilterFromStringError::SingletonSpecifiedAfterTransform {
singleton: filter_name.clone(),
transform: transform.clone(),
});
}
#[allow(clippy::type_complexity)]
let (mut variable, check_range, range_string): (Option<f32>, Box<dyn Fn(&f32) -> bool>, &'static str) = match filter_name.as_str() {
"scale" => (scale, Box::new(|v| (0.0..=10.0_f32).contains(v)), "0..=10"),
"alpha" => (alpha, Box::new(|v| (0.0..1.0_f32).contains(v)), "0..<1"),
"blur" => (blur, Box::new(|v| (0.0..=10.0_f32).contains(v)), "0..=10"),
_ => unreachable!("filter_name is narrowed down in the outer match statement"),
};
if variable.is_some() {
return Err(ImageFilterFromStringError::SingletonSpecifiedMoreThanOnce { name: filter_name.clone() });
}
"crop_original" => result.crop_original = Some(parse_rect_filter_value(&filter_name, use_raw_value()?)?),
"scale" => result.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?,
"rotate" => {
let raw_value = use_raw_value()?;
let value = f32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.clone(),
let value = i32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?;
if !check_range(&value) {
return Err(ImageFilterFromStringError::FilterValueNotInRange {
filter_name: filter_name.clone(),
range: range_string.to_owned(),
value: value.to_string(),
});
}
let _ = variable.insert(value);
result.clockwise_quarter_rotations = match value {
0 => 0,
90 => 1,
180 => 2,
270 | -90 => 3,
_ => return Err(ImageFilterFromStringError::RotationNotAllowed),
};
}
"crop" => result.crop = Some(parse_rect_filter_value(&filter_name, use_raw_value()?)?),
"color" => {
let raw_value = use_raw_value()?;
result.color = Some(
parse_rgb8_from_hex_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?,
)
}
"alpha" => result.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?,
"blur" => result.blur = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=10", Box::new(|v| (0.0..=10.0).contains(v)))?,
"grayscale" => result.grayscale = use_bool_value()?,
"invert" => result.invert = use_bool_value()?,
_ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }),
}
};
}
Ok(ImageFilter {
scale: scale.unwrap_or(1.0),
alpha: alpha.unwrap_or(1.0),
blur: blur.unwrap_or(0.0),
transforms,
})
Ok(result)
}
}

View file

@ -1,2 +1,4 @@
mod deckster_file;
mod image_filter;
mod image_filter;
mod geometry;
mod rgb;

37
deckster/src/model/rgb.rs Normal file
View file

@ -0,0 +1,37 @@
use rgb::{RGB8, RGBA8};
pub fn parse_rgb8_from_hex_str(s: &str) -> Result<RGB8, ()> {
let first_index = if s.starts_with('#') { 1 } else { 0 };
if s.len() - first_index == 6 {
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ())?;
let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ())?;
let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ())?;
return Ok(RGB8::new(r, g, b));
}
Err(())
}
pub fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, ()> {
let first_index = if s.starts_with('#') { 1 } else { 0 };
if s.len() - first_index == 8 {
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ())?;
let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ())?;
let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ())?;
let a = u8::from_str_radix(&s[(first_index + 6)..(first_index + 8)], 16).map_err(|_| ())?;
return Ok(RGBA8::new(r, g, b, a));
}
Err(())
}
pub fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result<RGBA8, ()> {
// optionally +1 for the '#'
match s.len() {
6 | 7 => Ok(parse_rgb8_from_hex_str(s)?.alpha(fallback_alpha)),
8 | 9 => Ok(parse_rgba8_from_hex_str(s)?),
_ => Err(()),
}
}