New color stuff

- CMYK function
- More default colors
- Interpret RGB values as sRGB
This commit is contained in:
Martin Haug 2022-02-08 21:12:09 +01:00
parent 62cf2a19d7
commit fe70db1f4c
14 changed files with 246 additions and 61 deletions

28
NOTICE
View File

@ -104,6 +104,34 @@ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================================================
================================================================================
The MIT License applies to:
* The default color set defined in `src/geom/color.rs` which is adapted from
the colors.css project
(https://clrs.cc/)
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
================================================================================
The Apache License Version 2.0 applies to:

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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

View File

@ -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::*;

View File

@ -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);

View File

@ -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() {

View File

@ -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.

View File

@ -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![];

View File

@ -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> {

BIN
tests/ref/utility/color.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

View File

@ -8,8 +8,8 @@
)
#let shaded = {
let v = 0
let next() = { v += 0.1; rgb(v, v, v) }
let v = 0%
let next() = { v += 10%; rgb(v, v, v) }
w => rect(width: w, height: 10pt, fill: next())
}

View File

@ -3,20 +3,21 @@
---
// Compare both ways.
#test(rgb(0.0, 0.3, 0.7), rgb("004db3"))
#test(rgb(0%, 30%, 70%), rgb("004db3"))
// Alpha channel.
#test(rgb(1.0, 0.0, 0.0, 0.5), rgb("ff000080"))
#test(rgb(255, 0, 0, 50%), rgb("ff000080"))
---
// Test CMYK color conversion.
// Ref: true
#rect(fill: cmyk(69%, 11%, 69%, 41%))
#rect(fill: cmyk(50%, 64%, 16%, 17%))
---
// Error for values that are out of range.
// Error: 11-14 value must be between 0.0 and 1.0
#test(rgb(-30, 15.5, 0.5))
---
// Error for values that are out of range.
// Error: 26-30 value must be between 0.0 and 1.0
#test(rgb(0.1, 0.2, 0.3, -0.1))
// Error: 11-14 must be between 0 and 255
#test(rgb(-30, 15, 50))
---
// Error: 6-11 invalid hex string
@ -31,5 +32,5 @@
#rgb(0, 1)
---
// Error: 21-26 expected float, found boolean
#rgb(0.1, 0.2, 0.3, false)
// Error: 21-26 expected integer or relative, found boolean
#rgb(10%, 20%, 30%, false)

View File

@ -11,7 +11,7 @@ use walkdir::WalkDir;
use typst::diag::Error;
use typst::eval::{Smart, StyleMap, Value};
use typst::frame::{Element, Frame};
use typst::geom::Length;
use typst::geom::{Length, RgbaColor};
use typst::library::{PageNode, TextNode};
use typst::loading::FsLoader;
use typst::parse::Scanner;
@ -77,6 +77,8 @@ fn main() {
// Hook up an assert function into the global scope.
let mut std = typst::library::new();
std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
std.def_func("test", move |_, args| {
let lhs = args.expect::<Value>("left-hand side")?;
let rhs = args.expect::<Value>("right-hand side")?;