Skip to content

Commit

Permalink
Part 5a: Gradients on text with relative: auto or `relative: "pare…
Browse files Browse the repository at this point in the history
…nt"` (#2364)
  • Loading branch information
Dherse committed Oct 12, 2023
1 parent d3b62bd commit a596663
Show file tree
Hide file tree
Showing 15 changed files with 389 additions and 84 deletions.
2 changes: 1 addition & 1 deletion crates/typst-library/src/text/deco.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ pub(super) fn decorate(

let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
let stroke = stroke.clone().unwrap_or(FixedStroke {
paint: text.fill.clone(),
paint: text.fill.as_decoration(),
thickness: metrics.thickness.at(text.size),
..FixedStroke::default()
});
Expand Down
14 changes: 10 additions & 4 deletions crates/typst-library/src/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,16 @@ pub struct TextElem {
#[parse({
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
if let Some(paint) = &paint {
// TODO: Implement gradients on text.
if matches!(paint.v, Paint::Gradient(_)) {
bail!(error!(paint.span, "text fill must be a solid color")
.with_hint("gradients on text will be supported soon"));
if let Paint::Gradient(gradient) = &paint.v {
if gradient.relative() == Smart::Custom(Relative::Self_) {
bail!(
error!(
paint.span,
"gradients on text must be relative to the parent"
)
.with_hint("make sure to set `relative: auto` on your text fill")
);
}
}
}
paint.map(|paint| paint.v)
Expand Down
25 changes: 15 additions & 10 deletions crates/typst/src/export/pdf/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,30 +274,35 @@ impl ColorEncode for ColorSpace {
/// Encodes a paint into either a fill or stroke color.
pub(super) trait PaintEncode {
/// Set the paint as the fill color.
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms);
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms);

/// Set the paint as the stroke color.
fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms);
fn set_as_stroke(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms);
}

impl PaintEncode for Paint {
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
match self {
Self::Solid(c) => c.set_as_fill(ctx, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, transforms),
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms),
}
}

fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_stroke(
&self,
ctx: &mut PageContext,
on_text: bool,
transforms: Transforms,
) {
match self {
Self::Solid(c) => c.set_as_stroke(ctx, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms),
Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms),
}
}
}

impl PaintEncode for Color {
fn set_as_fill(&self, ctx: &mut PageContext, _: Transforms) {
fn set_as_fill(&self, ctx: &mut PageContext, _: bool, _: Transforms) {
match self {
Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
Expand Down Expand Up @@ -350,7 +355,7 @@ impl PaintEncode for Color {
}
}

fn set_as_stroke(&self, ctx: &mut PageContext, _: Transforms) {
fn set_as_stroke(&self, ctx: &mut PageContext, _: bool, _: Transforms) {
match self {
Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
Expand Down
72 changes: 55 additions & 17 deletions crates/typst/src/export/pdf/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ pub struct PdfGradient {
pub aspect_ratio: Ratio,
/// The gradient.
pub gradient: Gradient,
/// Whether the gradient is applied to text.
pub on_text: bool,
}

/// Writes the actual gradients (shading patterns) to the PDF.
/// This is performed once after writing all pages.
pub fn write_gradients(ctx: &mut PdfContext) {
for PdfGradient { transform, aspect_ratio, gradient } in
for PdfGradient { transform, aspect_ratio, gradient, on_text } in
ctx.gradient_map.items().cloned().collect::<Vec<_>>()
{
let shading = ctx.alloc.bump();
Expand Down Expand Up @@ -89,7 +91,7 @@ pub fn write_gradients(ctx: &mut PdfContext) {
shading_pattern
}
Gradient::Conic(conic) => {
let vertices = compute_vertex_stream(conic);
let vertices = compute_vertex_stream(conic, aspect_ratio, on_text);

let stream_shading_id = ctx.alloc.bump();
let mut stream_shading =
Expand Down Expand Up @@ -254,20 +256,25 @@ fn single_gradient(
}

impl PaintEncode for Gradient {
fn set_as_fill(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) {
ctx.reset_fill_color_space();

let id = register_gradient(ctx, self, transforms);
let id = register_gradient(ctx, self, on_text, transforms);
let name = Name(id.as_bytes());

ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name);
}

fn set_as_stroke(&self, ctx: &mut PageContext, transforms: Transforms) {
fn set_as_stroke(
&self,
ctx: &mut PageContext,
on_text: bool,
transforms: Transforms,
) {
ctx.reset_stroke_color_space();

let id = register_gradient(ctx, self, transforms);
let id = register_gradient(ctx, self, on_text, transforms);
let name = Name(id.as_bytes());

ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
Expand All @@ -279,6 +286,7 @@ impl PaintEncode for Gradient {
fn register_gradient(
ctx: &mut PageContext,
gradient: &Gradient,
on_text: bool,
mut transforms: Transforms,
) -> EcoString {
// Edge cases for strokes.
Expand All @@ -290,17 +298,21 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0);
}

let size = match gradient.unwrap_relative(false) {
let size = match gradient.unwrap_relative(on_text) {
Relative::Self_ => transforms.size,
Relative::Parent => transforms.container_size,
};

// Correction for y-axis flipping on text.
let angle = gradient.angle().unwrap_or_else(Angle::zero);
let angle = if on_text { Angle::rad(TAU as f64) - angle } else { angle };

let (offset_x, offset_y) = match gradient {
Gradient::Conic(conic) => (
-size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0,
-size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0,
),
gradient => match gradient.angle().unwrap_or_else(Angle::zero).quadrant() {
_ => match angle.quadrant() {
Quadrant::First => (Abs::zero(), Abs::zero()),
Quadrant::Second => (size.x, Abs::zero()),
Quadrant::Third => (size.x, size.y),
Expand All @@ -310,10 +322,10 @@ fn register_gradient(

let rotation = match gradient {
Gradient::Conic(_) => Angle::zero(),
gradient => gradient.angle().unwrap_or_default(),
_ => angle,
};

let transform = match gradient.unwrap_relative(false) {
let transform = match gradient.unwrap_relative(on_text) {
Relative::Self_ => transforms.transform,
Relative::Parent => transforms.container_transform,
};
Expand All @@ -339,6 +351,7 @@ fn register_gradient(
size.aspect_ratio(),
))),
gradient: gradient.clone(),
on_text,
};

let index = ctx.parent.gradient_map.insert(pdf_gradient);
Expand Down Expand Up @@ -371,9 +384,16 @@ fn write_patch(
c0: [u16; 3],
c1: [u16; 3],
angle: Angle,
on_text: bool,
) {
let theta = -TAU * t + angle.to_rad() as f32 + PI;
let theta1 = -TAU * t1 + angle.to_rad() as f32 + PI;
let mut theta = -TAU * t + angle.to_rad() as f32 + PI;
let mut theta1 = -TAU * t1 + angle.to_rad() as f32 + PI;

// Correction for y-axis flipping on text.
if on_text {
theta = (TAU - theta).rem_euclid(TAU);
theta1 = (TAU - theta1).rem_euclid(TAU);
}

let (cp1, cp2) =
control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1);
Expand Down Expand Up @@ -434,10 +454,17 @@ fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point,
}

#[comemo::memoize]
fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
fn compute_vertex_stream(
conic: &ConicGradient,
aspect_ratio: Ratio,
on_text: bool,
) -> Arc<Vec<u8>> {
// Generated vertices for the Coons patches
let mut vertices = Vec::new();

// Correct the gradient's angle
let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio);

// We want to generate a vertex based on some conditions, either:
// - At the boundary of a stop
// - At the boundary of a quadrant
Expand Down Expand Up @@ -507,18 +534,28 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_prime,
conic.space.convert(c),
c0,
conic.angle,
angle,
on_text,
);

write_patch(&mut vertices, t_prime, t_prime, c0, c1, conic.angle);
write_patch(
&mut vertices,
t_prime,
t_prime,
c0,
c1,
angle,
on_text,
);

write_patch(
&mut vertices,
t_prime,
t_next as f32,
c1,
conic.space.convert(c_next),
conic.angle,
angle,
on_text,
);

t_x = t_next;
Expand All @@ -533,7 +570,8 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_next as f32,
conic.space.convert(c),
conic.space.convert(c_next),
conic.angle,
angle,
on_text,
);

t_x = t_next;
Expand Down
22 changes: 15 additions & 7 deletions crates/typst/src/export/pdf/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,11 +354,11 @@ impl PageContext<'_, '_> {
self.state.size = size;
}

fn set_fill(&mut self, fill: &Paint, transforms: Transforms) {
fn set_fill(&mut self, fill: &Paint, on_text: bool, transforms: Transforms) {
if self.state.fill.as_ref() != Some(fill)
|| matches!(self.state.fill, Some(Paint::Gradient(_)))
{
fill.set_as_fill(self, transforms);
fill.set_as_fill(self, on_text, transforms);
self.state.fill = Some(fill.clone());
}
}
Expand Down Expand Up @@ -390,7 +390,7 @@ impl PageContext<'_, '_> {
miter_limit,
} = stroke;

paint.set_as_stroke(self, transforms);
paint.set_as_stroke(self, false, transforms);

self.content.set_line_width(thickness.to_f32());
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
Expand Down Expand Up @@ -455,13 +455,21 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
let translation = Transform::translate(pos.x, pos.y);

ctx.save_state();
ctx.transform(translation.pre_concat(group.transform));

if group.frame.kind().is_hard() {
ctx.group_transform(translation.pre_concat(group.transform));
ctx.group_transform(
translation
.pre_concat(
ctx.state
.transform
.post_concat(ctx.state.container_transform.invert().unwrap()),
)
.pre_concat(group.transform),
);
ctx.size(group.frame.size());
}

ctx.transform(translation.pre_concat(group.transform));
if let Some(clip_path) = &group.clip_path {
write_path(ctx, 0.0, 0.0, clip_path);
ctx.content.clip_nonzero();
Expand All @@ -485,7 +493,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
glyph_set.entry(g.id).or_insert_with(|| segment.into());
}

ctx.set_fill(&text.fill, ctx.state.transforms(Size::zero(), pos));
ctx.set_fill(&text.fill, true, ctx.state.transforms(Size::zero(), pos));
ctx.set_font(&text.font, text.size);
ctx.set_opacities(None, Some(&text.fill));
ctx.content.begin_text();
Expand Down Expand Up @@ -550,7 +558,7 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
}

if let Some(fill) = &shape.fill {
ctx.set_fill(fill, ctx.state.transforms(shape.geometry.bbox_size(), pos));
ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos));
}

if let Some(stroke) = stroke {
Expand Down

0 comments on commit a596663

Please sign in to comment.