diff --git a/src/exec/context.rs b/src/exec/context.rs index 311619cc6..761977fcc 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -178,6 +178,7 @@ impl<'a> ExecContext<'a> { font_size: self.state.font.font_size(), top_edge: self.state.font.top_edge, bottom_edge: self.state.font.bottom_edge, + color: self.state.font.color, } } diff --git a/src/exec/state.rs b/src/exec/state.rs index 6775f394b..f66694fd7 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -2,8 +2,9 @@ use std::rc::Rc; use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight}; +use crate::color::{Color, RgbaColor}; use crate::geom::*; -use crate::layout::VerticalFontMetric; +use crate::layout::{Fill, VerticalFontMetric}; use crate::paper::{Paper, PaperClass, PAPER_A4}; /// The evaluation state. @@ -115,6 +116,8 @@ pub struct FontState { /// Whether the emphasis toggle is active or inactive. This determines /// whether the next `_` makes italic or non-italic. pub emph: bool, + /// The glyph fill color / texture. + pub color: Fill, } impl FontState { @@ -149,6 +152,7 @@ impl Default for FontState { scale: Linear::ONE, strong: false, emph: false, + color: Fill::Color(Color::Rgba(RgbaColor::BLACK)), } } } diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 43c58403e..d8391e2dd 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -133,6 +133,22 @@ impl<'a> PdfExporter<'a> { // do that, we need to remember the active face. let mut face = FaceId::MAX; let mut size = Length::ZERO; + let mut fill: Option = None; + let mut change_color = |content: &mut Content, new_fill: Fill| { + if fill != Some(new_fill) { + match new_fill { + Fill::Color(Color::Rgba(c)) => { + content.fill_rgb( + c.r as f32 / 255.0, + c.g as f32 / 255.0, + c.b as f32 / 255.0, + ); + } + Fill::Image(_) => todo!(), + } + fill = Some(new_fill); + } + }; for (pos, element) in &page.elements { let x = pos.x.to_pt() as f32; @@ -152,17 +168,7 @@ impl<'a> PdfExporter<'a> { Element::Geometry(geometry) => { content.save_state(); - - match geometry.fill { - Fill::Color(Color::Rgba(c)) => { - content.fill_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); - } - Fill::Image(_) => todo!(), - } + change_color(&mut content, geometry.fill); match &geometry.shape { Shape::Rect(r) => { @@ -179,9 +185,12 @@ impl<'a> PdfExporter<'a> { } Element::Text(shaped) => { + change_color(&mut content, shaped.color); + let mut text = content.text(); - // Check if we need to issue a font switching action. + // Then, also check if we need to + // issue a font switching action. if shaped.face != face || shaped.font_size != size { face = shaped.face; size = shaped.font_size; diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index 1b769db7d..df27e2871 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -11,7 +11,7 @@ use ttf_parser::{Face, GlyphId}; use crate::env::FontLoader; use crate::geom::{Dir, Length, Point, Size}; -use crate::layout::{Element, Frame}; +use crate::layout::{Element, Fill, Frame}; /// A shaped run of text. #[derive(Clone, PartialEq)] @@ -27,17 +27,20 @@ pub struct Shaped { pub offsets: Vec, /// The font size. pub font_size: Length, + /// The glyph fill color / texture. + pub color: Fill, } impl Shaped { /// Create a new shape run with empty `text`, `glyphs` and `offsets`. - pub fn new(face: FaceId, font_size: Length) -> Self { + pub fn new(face: FaceId, font_size: Length, color: Fill) -> Self { Self { text: String::new(), face, glyphs: vec![], offsets: vec![], font_size, + color: color, } } @@ -100,10 +103,11 @@ pub fn shape( font_size: Length, top_edge: VerticalFontMetric, bottom_edge: VerticalFontMetric, + color: Fill, loader: &mut FontLoader, ) -> Frame { let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO)); - let mut shaped = Shaped::new(FaceId::MAX, font_size); + let mut shaped = Shaped::new(FaceId::MAX, font_size, color); let mut width = Length::ZERO; let mut top = Length::ZERO; let mut bottom = Length::ZERO; @@ -133,7 +137,7 @@ pub fn shape( if shaped.face != id { place(&mut frame, shaped, width, top, bottom); - shaped = Shaped::new(id, font_size); + shaped = Shaped::new(id, font_size, color); width = Length::ZERO; top = convert(f64::from(lookup_metric(face, top_edge))); bottom = convert(f64::from(lookup_metric(face, bottom_edge))); diff --git a/src/layout/text.rs b/src/layout/text.rs index 7faefa0d5..7f8f97cc6 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -24,6 +24,8 @@ pub struct TextNode { pub top_edge: VerticalFontMetric, /// The bottom end of the text bounding box. pub bottom_edge: VerticalFontMetric, + /// The glyph fill. + pub color: Fill, } impl Layout for TextNode { @@ -37,6 +39,7 @@ impl Layout for TextNode { self.font_size, self.top_edge, self.bottom_edge, + self.color, &mut ctx.env.fonts, ), self.aligns, diff --git a/src/library/font.rs b/src/library/font.rs index 00fd0e81c..0993f7f0f 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -1,3 +1,4 @@ +use crate::layout::Fill; use fontdock::{FontStretch, FontStyle, FontWeight}; use super::*; @@ -15,6 +16,7 @@ use super::*; /// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0. /// - Top edge of the font: `top-edge`, of type `vertical-font-metric`. /// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`. +/// - Fill color the glyphs: `color`, of type `color`. /// - Serif family definition: `serif`, of type `font-familiy-list`. /// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`. /// - Monospace family definition: `monospace`, of type `font-familiy-list`. @@ -62,6 +64,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let stretch = args.get(ctx, "stretch"); let top_edge = args.get(ctx, "top-edge"); let bottom_edge = args.get(ctx, "bottom-edge"); + let color = args.get(ctx, "color"); let serif = args.get(ctx, "serif"); let sans_serif = args.get(ctx, "sans-serif"); let monospace = args.get(ctx, "monospace"); @@ -105,6 +108,10 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { ctx.state.font.bottom_edge = bottom_edge; } + if let Some(color) = color { + ctx.state.font.color = Fill::Color(color); + } + for (variant, arg) in &[ (FontFamily::Serif, &serif), (FontFamily::SansSerif, &sans_serif), diff --git a/tests/ref/library/font.png b/tests/ref/library/font.png index 199a15f2c..1cfbc4997 100644 Binary files a/tests/ref/library/font.png and b/tests/ref/library/font.png differ diff --git a/tests/typ/library/font.typ b/tests/typ/library/font.typ index ecea303f7..3e818f197 100644 --- a/tests/typ/library/font.typ +++ b/tests/typ/library/font.typ @@ -31,6 +31,9 @@ Emoji: 🐪, 🌋, 🏞 ∫ 𝛼 + 3𝛽 d𝑡 ] +// Colors. +#font(color: #239DAD)[This is #font(color: #FA644B)[way more] colorful.] + --- // Test top and bottom edge. diff --git a/tests/typeset.rs b/tests/typeset.rs index 7e7efc677..653470339 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -426,7 +426,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) { .transform(&Transform::from_row(scale, 0.0, 0.0, -scale, x, y).unwrap()) .unwrap(); - let mut paint = Paint::default(); + let mut paint = paint_from_fill(shaped.color); paint.anti_alias = true; canvas.fill_path(&placed, &paint, FillRule::default()); @@ -438,13 +438,7 @@ fn draw_geometry(_: &Env, canvas: &mut Canvas, pos: Point, element: &Geometry) { let x = pos.x.to_pt() as f32; let y = pos.y.to_pt() as f32; - let mut paint = Paint::default(); - match &element.fill { - Fill::Color(c) => match c { - typst::color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a), - }, - Fill::Image(_) => todo!(), - }; + let paint = paint_from_fill(element.fill); match &element.shape { Shape::Rect(s) => { @@ -454,6 +448,18 @@ fn draw_geometry(_: &Env, canvas: &mut Canvas, pos: Point, element: &Geometry) { }; } +fn paint_from_fill(fill: Fill) -> Paint<'static> { + let mut paint = Paint::default(); + match fill { + Fill::Color(c) => match c { + typst::color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a), + }, + Fill::Image(_) => todo!(), + } + + paint +} + fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) { let img = &env.resources.loaded::(element.res);