typst/src/geom/paint.rs
2022-11-03 13:35:39 +01:00

428 lines
12 KiB
Rust

use std::str::FromStr;
use syntect::highlighting::Color as SynColor;
use super::*;
/// How a fill or stroke should be painted.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Paint {
/// A solid color.
Solid(Color),
}
impl<T> From<T> for Paint
where
T: Into<Color>,
{
fn from(t: T) -> Self {
Self::Solid(t.into())
}
}
impl Debug for Paint {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Solid(color) => color.fmt(f),
}
}
}
/// A color in a dynamic format.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Color {
/// An 8-bit luma color.
Luma(LumaColor),
/// An 8-bit RGBA color.
Rgba(RgbaColor),
/// An 8-bit CMYK color.
Cmyk(CmykColor),
}
impl Color {
pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF));
pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF));
pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF));
pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF));
pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF));
pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF));
pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF));
pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF));
pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF));
pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF));
pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF));
pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF));
pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF));
pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF));
pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF));
pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF));
pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF));
pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF));
/// Convert this color to RGBA.
pub fn to_rgba(self) -> RgbaColor {
match self {
Self::Luma(luma) => luma.to_rgba(),
Self::Rgba(rgba) => rgba,
Self::Cmyk(cmyk) => cmyk.to_rgba(),
}
}
/// Lighten this color by the given factor.
pub fn lighten(self, factor: Ratio) -> Self {
match self {
Self::Luma(luma) => Self::Luma(luma.lighten(factor)),
Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)),
Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)),
}
}
/// Darken this color by the given factor.
pub fn darken(self, factor: Ratio) -> Self {
match self {
Self::Luma(luma) => Self::Luma(luma.darken(factor)),
Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)),
Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)),
}
}
/// Negate this color.
pub fn negate(self) -> Self {
match self {
Self::Luma(luma) => Self::Luma(luma.negate()),
Self::Rgba(rgba) => Self::Rgba(rgba.negate()),
Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()),
}
}
}
impl Debug for Color {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Luma(c) => Debug::fmt(c, f),
Self::Rgba(c) => Debug::fmt(c, f),
Self::Cmyk(c) => Debug::fmt(c, f),
}
}
}
/// An 8-bit grayscale color.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct LumaColor(pub u8);
impl LumaColor {
/// Construct a new luma color.
pub const fn new(luma: u8) -> Self {
Self(luma)
}
/// Convert to an opque RGBA color.
pub const fn to_rgba(self) -> RgbaColor {
RgbaColor::new(self.0, self.0, self.0, u8::MAX)
}
/// Convert to CMYK as a fraction of true black.
pub fn to_cmyk(self) -> CmykColor {
CmykColor::new(
round_u8(self.0 as f64 * 0.75),
round_u8(self.0 as f64 * 0.68),
round_u8(self.0 as f64 * 0.67),
round_u8(self.0 as f64 * 0.90),
)
}
/// Lighten this color by a factor.
pub fn lighten(self, factor: Ratio) -> Self {
let inc = round_u8((u8::MAX - self.0) as f64 * factor.get());
Self(self.0.saturating_add(inc))
}
/// Darken this color by a factor.
pub fn darken(self, factor: Ratio) -> Self {
let dec = round_u8(self.0 as f64 * factor.get());
Self(self.0.saturating_sub(dec))
}
/// Negate this color.
pub fn negate(self) -> Self {
Self(u8::MAX - self.0)
}
}
impl Debug for LumaColor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "luma({})", self.0)
}
}
impl From<LumaColor> for Color {
fn from(luma: LumaColor) -> Self {
Self::Luma(luma)
}
}
/// An 8-bit RGBA color.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct RgbaColor {
/// Red channel.
pub r: u8,
/// Green channel.
pub g: u8,
/// Blue channel.
pub b: u8,
/// Alpha channel.
pub a: u8,
}
impl RgbaColor {
/// Construct a new RGBA color.
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
/// Lighten this color by a factor.
///
/// The alpha channel is not affected.
pub fn lighten(self, factor: Ratio) -> Self {
let lighten =
|c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get()));
Self {
r: lighten(self.r),
g: lighten(self.g),
b: lighten(self.b),
a: self.a,
}
}
/// Darken this color by a factor.
///
/// The alpha channel is not affected.
pub fn darken(self, factor: Ratio) -> Self {
let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get()));
Self {
r: darken(self.r),
g: darken(self.g),
b: darken(self.b),
a: self.a,
}
}
/// Negate this color.
///
/// The alpha channel is not affected.
pub fn negate(self) -> Self {
Self {
r: u8::MAX - self.r,
g: u8::MAX - self.g,
b: u8::MAX - self.b,
a: self.a,
}
}
}
impl FromStr for RgbaColor {
type Err = &'static str;
/// Constructs a new color from hex strings like the following:
/// - `#aef` (shorthand, with leading hashtag),
/// - `7a03c2` (without alpha),
/// - `abcdefff` (with alpha).
///
/// The hashtag is optional and both lower and upper case are fine.
fn from_str(hex_str: &str) -> Result<Self, Self::Err> {
let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str);
if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) {
return Err("string contains non-hexadecimal letters");
}
let len = hex_str.len();
let long = len == 6 || len == 8;
let short = len == 3 || len == 4;
let alpha = len == 4 || len == 8;
if !long && !short {
return Err("string has wrong length");
}
let mut values: [u8; 4] = [u8::MAX; 4];
for elem in if alpha { 0 .. 4 } else { 0 .. 3 } {
let item_len = if long { 2 } else { 1 };
let pos = elem * item_len;
let item = &hex_str[pos .. (pos + item_len)];
values[elem] = u8::from_str_radix(item, 16).unwrap();
if short {
// Duplicate number for shorthand notation, i.e. `a` -> `aa`
values[elem] += values[elem] * 16;
}
}
Ok(Self::new(values[0], values[1], values[2], values[3]))
}
}
impl From<SynColor> for RgbaColor {
fn from(SynColor { r, g, b, a }: SynColor) -> Self {
Self { r, g, b, a }
}
}
impl From<RgbaColor> for SynColor {
fn from(RgbaColor { r, g, b, a }: RgbaColor) -> Self {
Self { r, g, b, a }
}
}
impl Debug for RgbaColor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?;
} else {
write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?;
if self.a != 255 {
write!(f, "{:02x}", self.a)?;
}
write!(f, "\")")?;
}
Ok(())
}
}
impl<T> From<T> for Color
where
T: Into<RgbaColor>,
{
fn from(rgba: T) -> Self {
Self::Rgba(rgba.into())
}
}
/// An 8-bit CMYK color.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct CmykColor {
/// The cyan component.
pub c: u8,
/// The magenta component.
pub m: u8,
/// The yellow component.
pub y: u8,
/// The key (black) component.
pub k: u8,
}
impl CmykColor {
/// Construct a new CMYK color.
pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self {
Self { c, m, y, k }
}
/// Convert this color to RGBA.
pub fn to_rgba(self) -> RgbaColor {
let k = self.k as f64 / 255.0;
let f = |c| {
let c = c as f64 / 255.0;
round_u8(255.0 * (1.0 - c) * (1.0 - k))
};
RgbaColor {
r: f(self.c),
g: f(self.m),
b: f(self.y),
a: 255,
}
}
/// Lighten this color by a factor.
pub fn lighten(self, factor: Ratio) -> Self {
let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get()));
Self {
c: lighten(self.c),
m: lighten(self.m),
y: lighten(self.y),
k: lighten(self.k),
}
}
/// Darken this color by a factor.
pub fn darken(self, factor: Ratio) -> Self {
let darken =
|c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get()));
Self {
c: darken(self.c),
m: darken(self.m),
y: darken(self.y),
k: darken(self.k),
}
}
/// Negate this color.
///
/// Does not affect the key component.
pub fn negate(self) -> Self {
Self {
c: u8::MAX - self.c,
m: u8::MAX - self.m,
y: u8::MAX - self.y,
k: self.k,
}
}
}
impl Debug for CmykColor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let g = |c| 100.0 * (c as f64 / 255.0);
write!(
f,
"cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)",
g(self.c),
g(self.m),
g(self.y),
g(self.k),
)
}
}
impl From<CmykColor> for Color {
fn from(cmyk: CmykColor) -> Self {
Self::Cmyk(cmyk)
}
}
/// Convert to the closest u8.
fn round_u8(value: f64) -> u8 {
value.round() as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_color_strings() {
#[track_caller]
fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) {
assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a)));
}
test("f61243ff", 0xf6, 0x12, 0x43, 0xff);
test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff);
test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad);
test("233", 0x22, 0x33, 0x33, 0xff);
test("111b", 0x11, 0x11, 0x11, 0xbb);
}
#[test]
fn test_parse_invalid_colors() {
#[track_caller]
fn test(hex: &str, message: &str) {
assert_eq!(RgbaColor::from_str(hex), Err(message));
}
test("a5", "string has wrong length");
test("12345", "string has wrong length");
test("f075ff011", "string has wrong length");
test("hmmm", "string contains non-hexadecimal letters");
test("14B2AH", "string contains non-hexadecimal letters");
}
}