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. 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: The Apache License Version 2.0 applies to:

View File

@ -39,7 +39,7 @@ use syntect::parsing::SyntaxSet;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; 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::image::ImageStore;
use crate::layout::RootNode; use crate::layout::RootNode;
use crate::library::{self, DecoLine, TextNode}; use crate::library::{self, DecoLine, TextNode};
@ -278,8 +278,8 @@ impl RawNode {
let foreground = THEME let foreground = THEME
.settings .settings
.foreground .foreground
.map(RgbaColor::from) .map(Color::from)
.unwrap_or(RgbaColor::BLACK) .unwrap_or(Color::BLACK)
.into(); .into();
match syntax { match syntax {

View File

@ -7,8 +7,10 @@ use std::sync::Arc;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba};
use pdf_writer::types::{ 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 pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr};
use ttf_parser::{name_id, GlyphId, Tag}; 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) 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. /// An exporter for a whole PDF document.
struct PdfExporter<'a> { struct PdfExporter<'a> {
fonts: &'a FontStore, fonts: &'a FontStore,
@ -316,6 +321,8 @@ impl<'a> PdfExporter<'a> {
pages.count(page_refs.len() as i32).kids(page_refs); pages.count(page_refs.len() as i32).kids(page_refs);
let mut resources = pages.resources(); let mut resources = pages.resources();
resources.color_spaces().insert(SRGB).start::<ColorSpace>().srgb();
let mut fonts = resources.fonts(); let mut fonts = resources.fonts();
for (font_ref, f) in self.face_map.pdf_indices(&self.face_refs) { for (font_ref, f) in self.face_map.pdf_indices(&self.face_refs) {
let name = format_eco!("F{}", f); let name = format_eco!("F{}", f);
@ -390,6 +397,8 @@ impl<'a> PageExporter<'a> {
// Make the coordinate system start at the top-left. // Make the coordinate system start at the top-left.
self.bottom = frame.size.y.to_f32(); self.bottom = frame.size.y.to_f32();
self.content.transform([1.0, 0.0, 0.0, -1.0, 0.0, self.bottom]); 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); self.write_frame(frame);
Page { Page {
size: frame.size, size: frame.size,
@ -624,23 +633,32 @@ impl<'a> PageExporter<'a> {
fn set_fill(&mut self, fill: Paint) { fn set_fill(&mut self, fill: Paint) {
if self.state.fill != Some(fill) { if self.state.fill != Some(fill) {
let Paint::Solid(Color::Rgba(c)) = fill; let f = |c| c as f32 / 255.0;
self.content.set_fill_rgb( let Paint::Solid(color) = fill;
c.r as f32 / 255.0, match color {
c.g as f32 / 255.0, Color::Rgba(c) => {
c.b as f32 / 255.0, 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) { fn set_stroke(&mut self, stroke: Stroke) {
if self.state.stroke != Some(stroke) { if self.state.stroke != Some(stroke) {
let Paint::Solid(Color::Rgba(c)) = stroke.paint; let f = |c| c as f32 / 255.0;
self.content.set_stroke_rgb( let Paint::Solid(color) = stroke.paint;
c.r as f32 / 255.0, match color {
c.g as f32 / 255.0, Color::Rgba(c) => {
c.b as f32 / 255.0, 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()); 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::font::{Face, FaceId};
use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; 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::image::{Image, RasterImage, Svg};
use crate::Context; use crate::Context;
@ -279,7 +279,8 @@ fn render_outline_glyph(
let bottom = top + mh; let bottom = top + mh;
// Premultiply the text color. // 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(); 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. // 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> { impl From<Paint> for sk::Paint<'static> {
fn from(paint: Paint) -> Self { fn from(paint: Paint) -> Self {
let mut sk_paint = sk::Paint::default(); 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.set_color_rgba8(c.r, c.g, c.b, c.a);
sk_paint.anti_alias = true; sk_paint.anti_alias = true;
sk_paint sk_paint

View File

@ -26,12 +26,44 @@ where
pub enum Color { pub enum Color {
/// An 8-bit RGBA color. /// An 8-bit RGBA color.
Rgba(RgbaColor), 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 { impl Debug for Color {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Rgba(c) => Debug::fmt(c, f), Self::Rgba(c) => Debug::fmt(c, f),
Self::Cmyk(c) => Debug::fmt(c, f),
} }
} }
} }
@ -59,19 +91,13 @@ pub struct RgbaColor {
} }
impl 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. /// 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 } Self { r, g, b, a }
} }
/// Construct a new, opaque gray color. /// 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) Self::new(luma, luma, luma, 255)
} }
} }
@ -155,6 +181,72 @@ impl Display for RgbaError {
impl std::error::Error 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -142,6 +142,7 @@ pub fn new() -> Scope {
std.def_func("mod", modulo); std.def_func("mod", modulo);
std.def_func("range", range); std.def_func("range", range);
std.def_func("rgb", rgb); std.def_func("rgb", rgb);
std.def_func("cmyk", cmyk);
std.def_func("lower", lower); std.def_func("lower", lower);
std.def_func("upper", upper); std.def_func("upper", upper);
std.def_func("roman", roman); std.def_func("roman", roman);
@ -150,12 +151,24 @@ pub fn new() -> Scope {
std.def_func("sorted", sorted); std.def_func("sorted", sorted);
// Predefined colors. // Predefined colors.
// TODO: More colors. std.def_const("black", Color::BLACK);
std.def_const("white", RgbaColor::WHITE); std.def_const("gray", Color::GRAY);
std.def_const("black", RgbaColor::BLACK); std.def_const("silver", Color::SILVER);
std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); std.def_const("white", Color::WHITE);
std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); std.def_const("navy", Color::NAVY);
std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); 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. // Other constants.
std.def_const("ltr", Dir::LTR); 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 thickness = styles.get(Self::THICKNESS);
let stroke = styles let stroke = styles
.get(Self::STROKE) .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 }); .map(|paint| Stroke { paint, thickness });
if fill.is_some() || stroke.is_some() { if fill.is_some() || stroke.is_some() {

View File

@ -21,7 +21,7 @@ impl TableNode {
/// The secondary cell fill color. /// The secondary cell fill color.
pub const SECONDARY: Option<Paint> = None; pub const SECONDARY: Option<Paint> = None;
/// How the stroke the cells. /// 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. /// The stroke's thickness.
pub const THICKNESS: Length = Length::pt(1.0); pub const THICKNESS: Length = Length::pt(1.0);
/// How much to pad the cells's content. /// How much to pad the cells's content.

View File

@ -51,7 +51,7 @@ impl TextNode {
/// Whether a monospace font should be preferred. /// Whether a monospace font should be preferred.
pub const MONOSPACE: bool = false; pub const MONOSPACE: bool = false;
/// The glyph fill color. /// The glyph fill color.
pub const FILL: Paint = RgbaColor::BLACK.into(); pub const FILL: Paint = Color::BLACK.into();
/// Decorative lines. /// Decorative lines.
#[fold(|a, b| a.into_iter().chain(b).collect())] #[fold(|a, b| a.into_iter().chain(b).collect())]
pub const LINES: Vec<Decoration> = vec![]; 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"), Err(_) => bail!(string.span, "invalid hex string"),
} }
} else { } else {
let r = args.expect("red component")?; struct Component(u8);
let g = args.expect("green component")?;
let b = args.expect("blue component")?; castable! {
let a = args.eat()?.unwrap_or(Spanned::new(1.0, Span::detached())); Component,
let f = |Spanned { v, span }: Spanned<f64>| { Expected: "integer or relative",
if (0.0 ..= 1.0).contains(&v) { Value::Int(v) => match v {
Ok((v * 255.0).round() as u8) 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 { } else {
bail!(span, "value must be between 0.0 and 1.0"); Err("must be between 0% and 100%")?
} },
}; }
RgbaColor::new(f(r)?, f(g)?, f(b)?, f(a)?)
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. /// The absolute value of a numeric value.
pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult<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 shaded = {
let v = 0 let v = 0%
let next() = { v += 0.1; rgb(v, v, v) } let next() = { v += 10%; rgb(v, v, v) }
w => rect(width: w, height: 10pt, fill: next()) w => rect(width: w, height: 10pt, fill: next())
} }

View File

@ -3,20 +3,21 @@
--- ---
// Compare both ways. // Compare both ways.
#test(rgb(0.0, 0.3, 0.7), rgb("004db3")) #test(rgb(0%, 30%, 70%), rgb("004db3"))
// Alpha channel. // 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 for values that are out of range.
// Error: 11-14 value must be between 0.0 and 1.0 // Error: 11-14 must be between 0 and 255
#test(rgb(-30, 15.5, 0.5)) #test(rgb(-30, 15, 50))
---
// 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: 6-11 invalid hex string // Error: 6-11 invalid hex string
@ -31,5 +32,5 @@
#rgb(0, 1) #rgb(0, 1)
--- ---
// Error: 21-26 expected float, found boolean // Error: 21-26 expected integer or relative, found boolean
#rgb(0.1, 0.2, 0.3, false) #rgb(10%, 20%, 30%, false)

View File

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