summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2022-02-08 21:12:09 +0100
committerMartin Haug <mhaug@live.de>2022-02-08 21:12:09 +0100
commitfe70db1f4ce078f7b41c163a1c0ead31fd04850a (patch)
tree0f85390a89a05dc763cbc2eb6c611c48877c08cd /src
parent62cf2a19d7207118f115c05c8f7c453d6db77a0a (diff)
New color stuff
- CMYK function - More default colors - Interpret RGB values as sRGB
Diffstat (limited to 'src')
-rw-r--r--src/eval/mod.rs6
-rw-r--r--src/export/pdf.rs44
-rw-r--r--src/export/render.rs8
-rw-r--r--src/geom/paint.rs108
-rw-r--r--src/library/mod.rs25
-rw-r--r--src/library/shape.rs2
-rw-r--r--src/library/table.rs2
-rw-r--r--src/library/text.rs2
-rw-r--r--src/library/utility.rs51
9 files changed, 201 insertions, 47 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index ae680d95..5129a41f 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -39,7 +39,7 @@ use syntect::parsing::SyntaxSet;
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult};
-use crate::geom::{Angle, Fractional, Length, Paint, Relative, RgbaColor};
+use crate::geom::{Angle, Color, Fractional, Length, Paint, Relative};
use crate::image::ImageStore;
use crate::layout::RootNode;
use crate::library::{self, DecoLine, TextNode};
@@ -278,8 +278,8 @@ impl RawNode {
let foreground = THEME
.settings
.foreground
- .map(RgbaColor::from)
- .unwrap_or(RgbaColor::BLACK)
+ .map(Color::from)
+ .unwrap_or(Color::BLACK)
.into();
match syntax {
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 19134f99..18bb5385 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -7,8 +7,10 @@ use std::sync::Arc;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
use pdf_writer::types::{
- ActionType, AnnotationType, CidFontType, FontFlags, SystemInfo, UnicodeCmap,
+ ActionType, AnnotationType, CidFontType, ColorSpaceOperand, FontFlags, SystemInfo,
+ UnicodeCmap,
};
+use pdf_writer::writers::ColorSpace;
use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr};
use ttf_parser::{name_id, GlyphId, Tag};
@@ -30,6 +32,9 @@ pub fn pdf(ctx: &Context, frames: &[Arc<Frame>]) -> Vec<u8> {
PdfExporter::new(ctx).export(frames)
}
+/// Identifies the sRGB color space definition.
+pub const SRGB: Name<'static> = Name(b"sRGB");
+
/// An exporter for a whole PDF document.
struct PdfExporter<'a> {
fonts: &'a FontStore,
@@ -316,6 +321,8 @@ impl<'a> PdfExporter<'a> {
pages.count(page_refs.len() as i32).kids(page_refs);
let mut resources = pages.resources();
+ resources.color_spaces().insert(SRGB).start::<ColorSpace>().srgb();
+
let mut fonts = resources.fonts();
for (font_ref, f) in self.face_map.pdf_indices(&self.face_refs) {
let name = format_eco!("F{}", f);
@@ -390,6 +397,8 @@ impl<'a> PageExporter<'a> {
// Make the coordinate system start at the top-left.
self.bottom = frame.size.y.to_f32();
self.content.transform([1.0, 0.0, 0.0, -1.0, 0.0, self.bottom]);
+ self.content.set_fill_color_space(ColorSpaceOperand::Named(SRGB));
+ self.content.set_stroke_color_space(ColorSpaceOperand::Named(SRGB));
self.write_frame(frame);
Page {
size: frame.size,
@@ -624,23 +633,32 @@ impl<'a> PageExporter<'a> {
fn set_fill(&mut self, fill: Paint) {
if self.state.fill != Some(fill) {
- let Paint::Solid(Color::Rgba(c)) = fill;
- self.content.set_fill_rgb(
- c.r as f32 / 255.0,
- c.g as f32 / 255.0,
- c.b as f32 / 255.0,
- );
+ let f = |c| c as f32 / 255.0;
+ let Paint::Solid(color) = fill;
+ match color {
+ Color::Rgba(c) => {
+ self.content.set_fill_color([f(c.r), f(c.g), f(c.b)]);
+ }
+ Color::Cmyk(c) => {
+ self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
+ }
+ }
}
}
fn set_stroke(&mut self, stroke: Stroke) {
if self.state.stroke != Some(stroke) {
- let Paint::Solid(Color::Rgba(c)) = stroke.paint;
- self.content.set_stroke_rgb(
- c.r as f32 / 255.0,
- c.g as f32 / 255.0,
- c.b as f32 / 255.0,
- );
+ let f = |c| c as f32 / 255.0;
+ let Paint::Solid(color) = stroke.paint;
+ match color {
+ Color::Rgba(c) => {
+ self.content.set_stroke_color([f(c.r), f(c.g), f(c.b)]);
+ }
+ Color::Cmyk(c) => {
+ self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
+ }
+ }
+
self.content.set_line_width(stroke.thickness.to_f32());
}
}
diff --git a/src/export/render.rs b/src/export/render.rs
index c41bcbf2..8b7aa46d 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -10,7 +10,7 @@ use usvg::FitTo;
use crate::font::{Face, FaceId};
use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
-use crate::geom::{self, Color, Length, Paint, PathElement, Size, Transform};
+use crate::geom::{self, Length, Paint, PathElement, Size, Transform};
use crate::image::{Image, RasterImage, Svg};
use crate::Context;
@@ -279,7 +279,8 @@ fn render_outline_glyph(
let bottom = top + mh;
// Premultiply the text color.
- let Paint::Solid(Color::Rgba(c)) = text.fill;
+ let Paint::Solid(color) = text.fill;
+ let c = color.to_rgba();
let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get();
// Blend the glyph bitmap with the existing pixels on the canvas.
@@ -453,7 +454,8 @@ impl From<Transform> for sk::Transform {
impl From<Paint> for sk::Paint<'static> {
fn from(paint: Paint) -> Self {
let mut sk_paint = sk::Paint::default();
- let Paint::Solid(Color::Rgba(c)) = paint;
+ let Paint::Solid(color) = paint;
+ let c = color.to_rgba();
sk_paint.set_color_rgba8(c.r, c.g, c.b, c.a);
sk_paint.anti_alias = true;
sk_paint
diff --git a/src/geom/paint.rs b/src/geom/paint.rs
index f8638656..7342024c 100644
--- a/src/geom/paint.rs
+++ b/src/geom/paint.rs
@@ -26,12 +26,44 @@ where
pub enum Color {
/// 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::Rgba(rgba) => rgba,
+ Self::Cmyk(cmyk) => cmyk.to_rgba(),
+ }
+ }
}
impl Debug for Color {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Rgba(c) => Debug::fmt(c, f),
+ Self::Cmyk(c) => Debug::fmt(c, f),
}
}
}
@@ -59,19 +91,13 @@ pub struct RgbaColor {
}
impl RgbaColor {
- /// Black color.
- pub const BLACK: Self = Self { r: 0, g: 0, b: 0, a: 255 };
-
- /// White color.
- pub const WHITE: Self = Self { r: 255, g: 255, b: 255, a: 255 };
-
/// Construct a new RGBA color.
- pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
+ pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
/// Construct a new, opaque gray color.
- pub fn gray(luma: u8) -> Self {
+ pub const fn gray(luma: u8) -> Self {
Self::new(luma, luma, luma, 255)
}
}
@@ -155,6 +181,72 @@ impl Display for RgbaError {
impl std::error::Error for RgbaError {}
+/// 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 }
+ }
+
+ /// Construct a new, opaque gray color as a fraction of true black.
+ pub fn gray(luma: u8) -> Self {
+ Self::new(
+ (luma as f64 * 0.75) as u8,
+ (luma as f64 * 0.68) as u8,
+ (luma as f64 * 0.67) as u8,
+ (luma as f64 * 0.90) as u8,
+ )
+ }
+
+ /// Convert this color to RGBA.
+ pub fn to_rgba(self) -> RgbaColor {
+ let k = self.k as f32 / 255.0;
+ let f = |c| {
+ let c = c as f32 / 255.0;
+ (255.0 * (1.0 - c) * (1.0 - k)).round() as u8
+ };
+
+ RgbaColor {
+ r: f(self.c),
+ g: f(self.m),
+ b: f(self.y),
+ a: 255,
+ }
+ }
+}
+
+impl Debug for CmykColor {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ let g = |c| 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)
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 55017645..33b327e9 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -142,6 +142,7 @@ pub fn new() -> Scope {
std.def_func("mod", modulo);
std.def_func("range", range);
std.def_func("rgb", rgb);
+ std.def_func("cmyk", cmyk);
std.def_func("lower", lower);
std.def_func("upper", upper);
std.def_func("roman", roman);
@@ -150,12 +151,24 @@ pub fn new() -> Scope {
std.def_func("sorted", sorted);
// Predefined colors.
- // TODO: More colors.
- std.def_const("white", RgbaColor::WHITE);
- std.def_const("black", RgbaColor::BLACK);
- std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF));
- std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
- std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
+ std.def_const("black", Color::BLACK);
+ std.def_const("gray", Color::GRAY);
+ std.def_const("silver", Color::SILVER);
+ std.def_const("white", Color::WHITE);
+ std.def_const("navy", Color::NAVY);
+ std.def_const("blue", Color::BLUE);
+ std.def_const("aqua", Color::AQUA);
+ std.def_const("teal", Color::TEAL);
+ std.def_const("eastern", Color::EASTERN);
+ std.def_const("purple", Color::PURPLE);
+ std.def_const("fuchsia", Color::FUCHSIA);
+ std.def_const("maroon", Color::MAROON);
+ std.def_const("red", Color::RED);
+ std.def_const("orange", Color::ORANGE);
+ std.def_const("yellow", Color::YELLOW);
+ std.def_const("olive", Color::OLIVE);
+ std.def_const("green", Color::GREEN);
+ std.def_const("lime", Color::LIME);
// Other constants.
std.def_const("ltr", Dir::LTR);
diff --git a/src/library/shape.rs b/src/library/shape.rs
index 5d31d570..0dd75c08 100644
--- a/src/library/shape.rs
+++ b/src/library/shape.rs
@@ -125,7 +125,7 @@ impl<S: ShapeKind> Layout for ShapeNode<S> {
let thickness = styles.get(Self::THICKNESS);
let stroke = styles
.get(Self::STROKE)
- .unwrap_or(fill.is_none().then(|| RgbaColor::BLACK.into()))
+ .unwrap_or(fill.is_none().then(|| Color::BLACK.into()))
.map(|paint| Stroke { paint, thickness });
if fill.is_some() || stroke.is_some() {
diff --git a/src/library/table.rs b/src/library/table.rs
index d7aa61db..b0f0fbf5 100644
--- a/src/library/table.rs
+++ b/src/library/table.rs
@@ -21,7 +21,7 @@ impl TableNode {
/// The secondary cell fill color.
pub const SECONDARY: Option<Paint> = None;
/// How the stroke the cells.
- pub const STROKE: Option<Paint> = Some(RgbaColor::BLACK.into());
+ pub const STROKE: Option<Paint> = Some(Color::BLACK.into());
/// The stroke's thickness.
pub const THICKNESS: Length = Length::pt(1.0);
/// How much to pad the cells's content.
diff --git a/src/library/text.rs b/src/library/text.rs
index 00c20a9e..019f8c35 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -51,7 +51,7 @@ impl TextNode {
/// Whether a monospace font should be preferred.
pub const MONOSPACE: bool = false;
/// The glyph fill color.
- pub const FILL: Paint = RgbaColor::BLACK.into();
+ pub const FILL: Paint = Color::BLACK.into();
/// Decorative lines.
#[fold(|a, b| a.into_iter().chain(b).collect())]
pub const LINES: Vec<Decoration> = vec![];
diff --git a/src/library/utility.rs b/src/library/utility.rs
index 0f533be4..5b9831c5 100644
--- a/src/library/utility.rs
+++ b/src/library/utility.rs
@@ -94,21 +94,50 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
Err(_) => bail!(string.span, "invalid hex string"),
}
} else {
- let r = args.expect("red component")?;
- let g = args.expect("green component")?;
- let b = args.expect("blue component")?;
- let a = args.eat()?.unwrap_or(Spanned::new(1.0, Span::detached()));
- let f = |Spanned { v, span }: Spanned<f64>| {
- if (0.0 ..= 1.0).contains(&v) {
- Ok((v * 255.0).round() as u8)
+ struct Component(u8);
+
+ castable! {
+ Component,
+ Expected: "integer or relative",
+ Value::Int(v) => match v {
+ 0 ..= 255 => Self(v as u8),
+ _ => Err("must be between 0 and 255")?,
+ },
+ Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
} else {
- bail!(span, "value must be between 0.0 and 1.0");
- }
- };
- RgbaColor::new(f(r)?, f(g)?, f(b)?, f(a)?)
+ Err("must be between 0% and 100%")?
+ },
+ }
+
+ let Component(r) = args.expect("red component")?;
+ let Component(g) = args.expect("green component")?;
+ let Component(b) = args.expect("blue component")?;
+ let Component(a) = args.eat()?.unwrap_or(Component(255));
+ RgbaColor::new(r, g, b, a)
},
))
}
+/// Create an CMYK color.
+pub fn cmyk(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ struct Component(u8);
+
+ castable! {
+ Component,
+ Expected: "relative",
+ Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ Err("must be between 0% and 100%")?
+ },
+ }
+
+ let Component(c) = args.expect("cyan component")?;
+ let Component(m) = args.expect("magenta component")?;
+ let Component(y) = args.expect("yellow component")?;
+ let Component(k) = args.expect("key component")?;
+ Ok(Value::Color(CmykColor::new(c, m, y, k).into()))
+}
/// The absolute value of a numeric value.
pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {