Last active
April 18, 2019 08:30
-
-
Save Miha-x64/4397d092515fe743ee2d4f6ffa8e66dd to your computer and use it in GitHub Desktop.
Rust script for extracting a transparent colour from the given pre-blent opaque colours
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::env; | |
use std::io; | |
use std::io::Write; | |
use std::str::FromStr; | |
#[macro_use] | |
extern crate lazy_static; | |
fn main() { | |
match obtain_colour_args() { | |
Some((bg, fg, formats)) => { | |
let unblent = unblend(&bg, &fg); | |
match single_ref(&formats) { | |
Some(format) => println!("{}", (format.format)(&unblent)), | |
None => { | |
for format in formats.iter() { | |
println!("{}: {}", format.name, (format.format)(&unblent)); | |
} | |
} | |
} | |
}, | |
None => println!( | |
"Usage: unblend [output format[, output format[...]]] background foreground\n\ | |
\twhere 'background' and 'foreground' are #RRGGBB colours\n\ | |
output formats:\n\ | |
\t--android: #AARRGGBB\n\ | |
\t--java: 0xAA_RRGGBB\n\ | |
\t--html: rgba(red/255, green/255, blue/255, alpha/1.0)" | |
) | |
} | |
} | |
#[derive(PartialEq, Eq)] | |
struct OpaqueColour { | |
red: u8, | |
green: u8, | |
blue: u8, | |
} | |
impl FromStr for OpaqueColour { | |
type Err = (); | |
fn from_str(hex: &str) -> Result<Self, Self::Err> { | |
let hex = hex.trim(); | |
let hex = if hex.starts_with("#") { &hex[1..] } else { &hex }; | |
if hex.len() == 6 { | |
let r = from_hex(hex[0..2].as_bytes()).ok_or(())?; | |
let g = from_hex(hex[2..4].as_bytes()).ok_or(())?; | |
let b = from_hex(hex[4..6].as_bytes()).ok_or(())?; | |
Ok(OpaqueColour { red: r, green: g, blue: b }) | |
} else { | |
Err(()) | |
} | |
} | |
} | |
struct TransparentColour { | |
alpha: u8, | |
red: u8, | |
green: u8, | |
blue: u8, | |
} | |
struct ColourFormat { | |
name: &'static str, | |
slug: &'static str, | |
format: fn(source: &TransparentColour) -> String, | |
} | |
const ALL_FORMATS: &[&ColourFormat] = &[ | |
&ColourFormat { | |
name: "Android ColorRes", | |
slug: "android", | |
format: |source| { | |
format!("#{:02X}{:02X}{:02X}{:02X}", source.alpha, source.red, source.green, source.blue) | |
}, | |
}, | |
&ColourFormat { | |
name: "Java HEX literal", | |
slug: "java", | |
format: |source| { | |
format!("0x{:02X}_{:02X}{:02X}{:02X}", source.alpha, source.red, source.green, source.blue) | |
}, | |
}, | |
&ColourFormat { | |
name: "HTML/CSS", | |
slug: "html", | |
format: |source| { | |
format!("rgba({}, {}, {}, {:.3})", | |
source.red, source.green, source.blue, source.alpha as f32 / 255.0) | |
}, | |
}, | |
]; | |
fn obtain_colour_args() -> Option<(OpaqueColour, OpaqueColour, Vec<&'static ColourFormat>)> { | |
let mut args: Vec<String> = env::args().collect(); | |
let mut formats: Vec<&ColourFormat> = Vec::new(); | |
args.remove(0); // pop first useless arg | |
loop { | |
if has_option(&args) { | |
let slug = &args.remove(0)[2..]; | |
let format = ALL_FORMATS.iter() | |
.find(|format| { format.slug == slug })?; | |
formats.push(format) | |
} else { | |
break; | |
} | |
} | |
if formats.is_empty() { | |
formats = ALL_FORMATS.into(); | |
} | |
match args.len() { | |
0 => { // interactive | |
let mut buf = String::new(); | |
let bg = read_colour_interactive( | |
"Background colour (#RRGGBB):", &mut buf)?; | |
let fg = read_colour_interactive( | |
"Foreground colour:", &mut buf)?; | |
Some((bg, fg, formats)) | |
}, | |
2 => { // CLI arguments | |
let bg: OpaqueColour = args.remove(0).parse().ok()?; | |
let fg: OpaqueColour = args.remove(0).parse().ok()?; | |
Some((bg, fg, formats)) | |
}, | |
_ => None | |
} | |
} | |
fn has_option(vec: &Vec<String>) -> bool { | |
if let Some(arg) = vec.first() { | |
if arg.starts_with("--") { | |
true | |
} else { | |
false | |
} | |
} else { | |
false | |
} | |
} | |
fn read_colour_interactive(prompt: &str, io_buf: &mut String) -> Option<OpaqueColour> { | |
println!("{}", prompt); | |
io::stdout().flush().unwrap(); | |
io::stdin().read_line(io_buf).unwrap(); | |
let colour_result = io_buf.parse(); | |
io_buf.clear(); | |
colour_result.ok() | |
} | |
fn unblend(bg: &OpaqueColour, fg: &OpaqueColour) -> TransparentColour { | |
if bg == fg { | |
return TransparentColour { alpha: 0, /*nevermind:*/ red: 0, green: 0, blue: 0 }; | |
} | |
for i in 1 .. 256 { | |
let alpha_guess = i as u8; | |
let r = colour_component(bg.red, fg.red, alpha_guess); | |
if r > 255 { continue; } | |
let g = colour_component(bg.green, fg.green, alpha_guess); | |
if g > 255 { continue; } | |
let b = colour_component(bg.blue, fg.blue, alpha_guess); | |
if b > 255 { continue; } | |
return TransparentColour { alpha: alpha_guess, red: r as u8, green: g as u8, blue: b as u8 }; | |
} | |
unreachable!(); | |
} | |
fn colour_component(bg: u8, fg: u8, alpha: u8) -> u16 { | |
// fg = (1-α)*bg + α*x | |
// fg = bg - α*bg + α*x | |
// α*x = -bg + α*bg + fg | |
// x = (-bg + α*bg + fg) / α | |
let bg = bg as i32; | |
let fg = fg as i32; | |
let alpha = alpha as i32; | |
((-bg + alpha*bg/255 + fg)*255/alpha) as u16 | |
} | |
fn from_hex(hex: &[u8]) -> Option<u8> { | |
lazy_static! { | |
static ref VALUES: [u8; 'f' as usize + 1] = { | |
let mut v = [0xFF; 'f' as usize + 1]; | |
(&mut v['0' as usize ..= '9' as usize]).write(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap(); | |
(&mut v['A' as usize ..= 'F' as usize]).write(&[0xA, 0xB, 0xC, 0xD, 0xE, 0xF]).unwrap(); | |
(&mut v['a' as usize ..= 'f' as usize]).write(&[0xA, 0xB, 0xC, 0xD, 0xE, 0xF]).unwrap(); | |
v | |
}; | |
} | |
if hex.len() == 2 { | |
let hi_char = hex[0] as usize; | |
let lo_char = hex[1] as usize; | |
if hi_char < VALUES.len() && lo_char < VALUES.len() { | |
let hi = VALUES[hi_char]; | |
let lo = VALUES[lo_char]; | |
if hi == 0xFF || lo == 0xFF { // 0xFF stands for 'invalid hex' | |
None | |
} else { | |
Some((hi << 4) | lo) | |
} | |
} else { | |
None | |
} | |
} else { | |
None | |
} | |
} | |
fn single_ref<'a, T : ?Sized>(vec: &'a Vec<&T>) -> Option<&'a T> { | |
vec.first().map(|x: &&T| { *x }) | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
fn parse() { | |
assert_eq!(from_hex(b""), None); | |
assert_eq!(from_hex(b"0"), None); | |
assert_eq!(from_hex(b"00"), Some(0)); | |
assert_eq!(from_hex(b"01"), Some(1)); | |
assert_eq!(from_hex(b"10"), Some(0x10)); | |
assert_eq!(from_hex(b"1f"), Some(0x1f)); | |
assert_eq!(from_hex(b"bE"), Some(0xBE)); | |
assert_eq!(from_hex(b"Da"), Some(0xDA)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "unblend" | |
version = "0.0.1" | |
authors = ["miha-x64"] | |
[dependencies] | |
lazy_static = "1.3.0" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment