Part 5a: Gradients on text with relative: auto or relative: "parent" (#2364)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-10-12 18:03:52 +02:00 committed by GitHub
parent d3b62bd02e
commit a59666369b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 389 additions and 84 deletions

View File

@ -400,7 +400,7 @@ pub(super) fn decorate(
let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift; let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
let stroke = stroke.clone().unwrap_or(FixedStroke { let stroke = stroke.clone().unwrap_or(FixedStroke {
paint: text.fill.clone(), paint: text.fill.as_decoration(),
thickness: metrics.thickness.at(text.size), thickness: metrics.thickness.at(text.size),
..FixedStroke::default() ..FixedStroke::default()
}); });

View File

@ -182,10 +182,16 @@ pub struct TextElem {
#[parse({ #[parse({
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?; let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
if let Some(paint) = &paint { if let Some(paint) = &paint {
// TODO: Implement gradients on text. if let Paint::Gradient(gradient) = &paint.v {
if matches!(paint.v, Paint::Gradient(_)) { if gradient.relative() == Smart::Custom(Relative::Self_) {
bail!(error!(paint.span, "text fill must be a solid color") bail!(
.with_hint("gradients on text will be supported soon")); 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) paint.map(|paint| paint.v)

View File

@ -274,30 +274,35 @@ impl ColorEncode for ColorSpace {
/// Encodes a paint into either a fill or stroke color. /// Encodes a paint into either a fill or stroke color.
pub(super) trait PaintEncode { pub(super) trait PaintEncode {
/// Set the paint as the fill color. /// 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. /// 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 { 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 { match self {
Self::Solid(c) => c.set_as_fill(ctx, transforms), Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, 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 { match self {
Self::Solid(c) => c.set_as_stroke(ctx, transforms), Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, transforms), Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms),
} }
} }
} }
impl PaintEncode for Color { 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 { match self {
Color::Luma(_) => { Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
@ -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 { match self {
Color::Luma(_) => { Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);

View File

@ -27,12 +27,14 @@ pub struct PdfGradient {
pub aspect_ratio: Ratio, pub aspect_ratio: Ratio,
/// The gradient. /// The gradient.
pub gradient: Gradient, pub gradient: Gradient,
/// Whether the gradient is applied to text.
pub on_text: bool,
} }
/// Writes the actual gradients (shading patterns) to the PDF. /// Writes the actual gradients (shading patterns) to the PDF.
/// This is performed once after writing all pages. /// This is performed once after writing all pages.
pub fn write_gradients(ctx: &mut PdfContext) { 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<_>>() ctx.gradient_map.items().cloned().collect::<Vec<_>>()
{ {
let shading = ctx.alloc.bump(); let shading = ctx.alloc.bump();
@ -89,7 +91,7 @@ pub fn write_gradients(ctx: &mut PdfContext) {
shading_pattern shading_pattern
} }
Gradient::Conic(conic) => { 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 stream_shading_id = ctx.alloc.bump();
let mut stream_shading = let mut stream_shading =
@ -254,20 +256,25 @@ fn single_gradient(
} }
impl PaintEncode for 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(); 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()); let name = Name(id.as_bytes());
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name); 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(); 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()); let name = Name(id.as_bytes());
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
@ -279,6 +286,7 @@ impl PaintEncode for Gradient {
fn register_gradient( fn register_gradient(
ctx: &mut PageContext, ctx: &mut PageContext,
gradient: &Gradient, gradient: &Gradient,
on_text: bool,
mut transforms: Transforms, mut transforms: Transforms,
) -> EcoString { ) -> EcoString {
// Edge cases for strokes. // Edge cases for strokes.
@ -290,17 +298,21 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0); 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::Self_ => transforms.size,
Relative::Parent => transforms.container_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 { let (offset_x, offset_y) = match gradient {
Gradient::Conic(conic) => ( Gradient::Conic(conic) => (
-size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0, -size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0,
-size.y * (1.0 - conic.center.y.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::First => (Abs::zero(), Abs::zero()),
Quadrant::Second => (size.x, Abs::zero()), Quadrant::Second => (size.x, Abs::zero()),
Quadrant::Third => (size.x, size.y), Quadrant::Third => (size.x, size.y),
@ -310,10 +322,10 @@ fn register_gradient(
let rotation = match gradient { let rotation = match gradient {
Gradient::Conic(_) => Angle::zero(), 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::Self_ => transforms.transform,
Relative::Parent => transforms.container_transform, Relative::Parent => transforms.container_transform,
}; };
@ -339,6 +351,7 @@ fn register_gradient(
size.aspect_ratio(), size.aspect_ratio(),
))), ))),
gradient: gradient.clone(), gradient: gradient.clone(),
on_text,
}; };
let index = ctx.parent.gradient_map.insert(pdf_gradient); let index = ctx.parent.gradient_map.insert(pdf_gradient);
@ -371,9 +384,16 @@ fn write_patch(
c0: [u16; 3], c0: [u16; 3],
c1: [u16; 3], c1: [u16; 3],
angle: Angle, angle: Angle,
on_text: bool,
) { ) {
let theta = -TAU * t + angle.to_rad() as f32 + PI; let mut theta = -TAU * t + angle.to_rad() as f32 + PI;
let theta1 = -TAU * t1 + 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) = let (cp1, cp2) =
control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1); control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1);
@ -434,10 +454,17 @@ fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point,
} }
#[comemo::memoize] #[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 // Generated vertices for the Coons patches
let mut vertices = Vec::new(); 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: // We want to generate a vertex based on some conditions, either:
// - At the boundary of a stop // - At the boundary of a stop
// - At the boundary of a quadrant // - At the boundary of a quadrant
@ -507,10 +534,19 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_prime, t_prime,
conic.space.convert(c), conic.space.convert(c),
c0, 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( write_patch(
&mut vertices, &mut vertices,
@ -518,7 +554,8 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_next as f32, t_next as f32,
c1, c1,
conic.space.convert(c_next), conic.space.convert(c_next),
conic.angle, angle,
on_text,
); );
t_x = t_next; t_x = t_next;
@ -533,7 +570,8 @@ fn compute_vertex_stream(conic: &ConicGradient) -> Arc<Vec<u8>> {
t_next as f32, t_next as f32,
conic.space.convert(c), conic.space.convert(c),
conic.space.convert(c_next), conic.space.convert(c_next),
conic.angle, angle,
on_text,
); );
t_x = t_next; t_x = t_next;

View File

@ -354,11 +354,11 @@ impl PageContext<'_, '_> {
self.state.size = size; 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) if self.state.fill.as_ref() != Some(fill)
|| matches!(self.state.fill, Some(Paint::Gradient(_))) || 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()); self.state.fill = Some(fill.clone());
} }
} }
@ -390,7 +390,7 @@ impl PageContext<'_, '_> {
miter_limit, miter_limit,
} = stroke; } = stroke;
paint.set_as_stroke(self, transforms); paint.set_as_stroke(self, false, transforms);
self.content.set_line_width(thickness.to_f32()); self.content.set_line_width(thickness.to_f32());
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
@ -455,13 +455,21 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
let translation = Transform::translate(pos.x, pos.y); let translation = Transform::translate(pos.x, pos.y);
ctx.save_state(); ctx.save_state();
ctx.transform(translation.pre_concat(group.transform));
if group.frame.kind().is_hard() { 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.size(group.frame.size());
} }
ctx.transform(translation.pre_concat(group.transform));
if let Some(clip_path) = &group.clip_path { if let Some(clip_path) = &group.clip_path {
write_path(ctx, 0.0, 0.0, clip_path); write_path(ctx, 0.0, 0.0, clip_path);
ctx.content.clip_nonzero(); ctx.content.clip_nonzero();
@ -485,7 +493,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
glyph_set.entry(g.id).or_insert_with(|| segment.into()); 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_font(&text.font, text.size);
ctx.set_opacities(None, Some(&text.fill)); ctx.set_opacities(None, Some(&text.fill));
ctx.content.begin_text(); ctx.content.begin_text();
@ -550,7 +558,7 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
} }
if let Some(fill) = &shape.fill { 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 { if let Some(stroke) = stroke {

View File

@ -149,7 +149,7 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
for (pos, item) in frame.items() { for (pos, item) in frame.items() {
match item { match item {
FrameItem::Group(group) => { FrameItem::Group(group) => {
render_group(canvas, state.pre_translate(*pos), group); render_group(canvas, state, *pos, group);
} }
FrameItem::Text(text) => { FrameItem::Text(text) => {
render_text(canvas, state.pre_translate(*pos), text); render_text(canvas, state.pre_translate(*pos), text);
@ -172,11 +172,18 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
} }
/// Render a group frame with optional transform and clipping into the canvas. /// Render a group frame with optional transform and clipping into the canvas.
fn render_group(canvas: &mut sk::Pixmap, state: State, group: &GroupItem) { fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &GroupItem) {
let state = match group.frame.kind() { let state = match group.frame.kind() {
FrameKind::Soft => state.pre_concat(group.transform.into()), FrameKind::Soft => state.pre_translate(pos).pre_concat(group.transform.into()),
FrameKind::Hard => state FrameKind::Hard => state
.pre_translate(pos)
.pre_concat(group.transform.into()) .pre_concat(group.transform.into())
.pre_concat_container(
state
.transform
.post_concat(state.container_transform.invert().unwrap()),
)
.pre_concat_container(Transform::translate(pos.x, pos.y).into())
.pre_concat_container(group.transform.into()) .pre_concat_container(group.transform.into())
.with_size(group.frame.size()), .with_size(group.frame.size()),
}; };
@ -375,15 +382,23 @@ fn render_outline_glyph(
builder.0.finish()? builder.0.finish()?
}; };
// TODO: Implement gradients on text. let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let mut pixmap = None; let mut pixmap = None;
let paint = to_sk_paint(&text.fill, state, Size::zero(), None, &mut pixmap, None); let paint = to_sk_paint(
&text.fill,
state.pre_concat(sk::Transform::from_scale(scale, -scale)),
Size::zero(),
true,
None,
&mut pixmap,
None,
);
let rule = sk::FillRule::default(); let rule = sk::FillRule::default();
// Flip vertically because font design coordinate // Flip vertically because font design coordinate
// system is Y-up. // system is Y-up.
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let ts = ts.pre_scale(scale, -scale); let ts = ts.pre_scale(scale, -scale);
canvas.fill_path(&path, &paint, rule, ts, state.mask); canvas.fill_path(&path, &paint, rule, ts, state.mask);
return Some(()); return Some(());
@ -410,31 +425,47 @@ fn render_outline_glyph(
// doesn't exist, yet. // doesn't exist, yet.
let bitmap = let bitmap =
rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?; rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?;
match &text.fill {
Paint::Gradient(gradient) => {
let sampler = GradientSampler::new(gradient, &state, Size::zero(), true);
write_bitmap(canvas, &bitmap, &state, sampler)?;
}
Paint::Solid(color) => {
write_bitmap(canvas, &bitmap, &state, *color)?;
}
}
Some(())
}
fn write_bitmap<S: PaintSampler>(
canvas: &mut sk::Pixmap,
bitmap: &Bitmap,
state: &State,
sampler: S,
) -> Option<()> {
// If we have a clip mask we first render to a pixmap that we then blend // If we have a clip mask we first render to a pixmap that we then blend
// with our canvas // with our canvas
if state.mask.is_some() { if state.mask.is_some() {
let mw = bitmap.width; let mw = bitmap.width;
let mh = bitmap.height; let mh = bitmap.height;
let color = text.fill.unwrap_solid();
let color = sk::ColorU8::from(color);
// Pad the pixmap with 1 pixel in each dimension so that we do // Pad the pixmap with 1 pixel in each dimension so that we do
// not get any problem with floating point errors along their border // not get any problem with floating point errors along their border
let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?; let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?;
for x in 0..mw { for x in 0..mw {
for y in 0..mh { for y in 0..mh {
let alpha = bitmap.coverage[(y * mw + x) as usize]; let alpha = bitmap.coverage[(y * mw + x) as usize];
let color = sk::ColorU8::from_rgba( let color: sk::ColorU8 = sampler.sample((x, y)).into();
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] =
sk::ColorU8::from_rgba(
color.red(), color.red(),
color.green(), color.green(),
color.blue(), color.blue(),
alpha, alpha,
) )
.premultiply(); .premultiply();
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = color;
} }
} }
@ -461,10 +492,6 @@ fn render_outline_glyph(
let top = bitmap.top; let top = bitmap.top;
let bottom = top + mh; let bottom = top + mh;
// Premultiply the text color.
let Paint::Solid(color) = text.fill else { todo!() };
let color = bytemuck::cast(sk::ColorU8::from(color).premultiply());
// Blend the glyph bitmap with the existing pixels on the canvas. // Blend the glyph bitmap with the existing pixels on the canvas.
let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut()); let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
for x in left.clamp(0, cw)..right.clamp(0, cw) { for x in left.clamp(0, cw)..right.clamp(0, cw) {
@ -475,6 +502,8 @@ fn render_outline_glyph(
continue; continue;
} }
let color: sk::ColorU8 = sampler.sample((x as _, y as _)).into();
let color = bytemuck::cast(color.premultiply());
let pi = (y * cw + x) as usize; let pi = (y * cw + x) as usize;
if cov == 255 { if cov == 255 {
pixels[pi] = color; pixels[pi] = color;
@ -510,8 +539,15 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
if let Some(fill) = &shape.fill { if let Some(fill) = &shape.fill {
let mut pixmap = None; let mut pixmap = None;
let mut paint: sk::Paint = let mut paint: sk::Paint = to_sk_paint(
to_sk_paint(fill, state, shape.geometry.bbox_size(), None, &mut pixmap, None); fill,
state,
shape.geometry.bbox_size(),
false,
None,
&mut pixmap,
None,
);
if matches!(shape.geometry, Geometry::Rect(_)) { if matches!(shape.geometry, Geometry::Rect(_)) {
paint.anti_alias = false; paint.anti_alias = false;
@ -578,6 +614,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
paint, paint,
state, state,
offset_bbox, offset_bbox,
false,
fill_transform, fill_transform,
&mut pixmap, &mut pixmap,
gradient_map, gradient_map,
@ -731,6 +768,71 @@ impl From<sk::Transform> for Transform {
} }
} }
/// Trait for sampling of a paint, used as a generic
/// abstraction over solid colors and gradients.
trait PaintSampler: Copy {
/// Sample the color at the `pos` in the pixmap.
fn sample(self, pos: (u32, u32)) -> Color;
}
impl PaintSampler for Color {
fn sample(self, _: (u32, u32)) -> Color {
self
}
}
/// State used when sampling colors for text.
///
/// It caches the inverse transform to the parent, so that we can
/// reuse it instead of recomputing it for each pixel.
#[derive(Clone, Copy)]
struct GradientSampler<'a> {
gradient: &'a Gradient,
container_size: Size,
transform_to_parent: sk::Transform,
}
impl<'a> GradientSampler<'a> {
fn new(
gradient: &'a Gradient,
state: &State,
item_size: Size,
on_text: bool,
) -> Self {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
Relative::Self_ => item_size,
Relative::Parent => state.size,
};
let fill_transform = match relative {
Relative::Self_ => sk::Transform::identity(),
Relative::Parent => state.container_transform.invert().unwrap(),
};
Self {
gradient,
container_size,
transform_to_parent: fill_transform,
}
}
}
impl PaintSampler for GradientSampler<'_> {
/// Samples a single point in a glyph.
fn sample(self, (x, y): (u32, u32)) -> Color {
// Compute the point in the gradient's coordinate space.
let mut point = sk::Point { x: x as f32, y: y as f32 };
self.transform_to_parent.map_point(&mut point);
// Sample the gradient
self.gradient.sample_at(
(point.x, point.y),
(self.container_size.x.to_f32(), self.container_size.y.to_f32()),
)
}
}
/// Transforms a [`Paint`] into a [`sk::Paint`]. /// Transforms a [`Paint`] into a [`sk::Paint`].
/// Applying the necessary transform, if the paint is a gradient. /// Applying the necessary transform, if the paint is a gradient.
/// ///
@ -740,6 +842,7 @@ fn to_sk_paint<'a>(
paint: &Paint, paint: &Paint,
state: State, state: State,
item_size: Size, item_size: Size,
on_text: bool,
fill_transform: Option<sk::Transform>, fill_transform: Option<sk::Transform>,
pixmap: &'a mut Option<Arc<sk::Pixmap>>, pixmap: &'a mut Option<Arc<sk::Pixmap>>,
gradient_map: Option<(Point, Axes<Ratio>)>, gradient_map: Option<(Point, Axes<Ratio>)>,
@ -782,7 +885,7 @@ fn to_sk_paint<'a>(
sk_paint.anti_alias = true; sk_paint.anti_alias = true;
} }
Paint::Gradient(gradient) => { Paint::Gradient(gradient) => {
let relative = gradient.unwrap_relative(false); let relative = gradient.unwrap_relative(on_text);
let container_size = match relative { let container_size = match relative {
Relative::Self_ => item_size, Relative::Self_ => item_size,
Relative::Parent => state.size, Relative::Parent => state.size,

View File

@ -51,7 +51,7 @@ pub fn svg_merged(frames: &[Frame], padding: Abs) -> String {
let [x, mut y] = [padding; 2]; let [x, mut y] = [padding; 2];
for frame in frames { for frame in frames {
let ts = Transform::translate(x, y); let ts = Transform::translate(x, y);
let state = State::new(frame.size(), ts); let state = State::new(frame.size(), Transform::identity());
renderer.render_frame(state, ts, frame); renderer.render_frame(state, ts, frame);
y += frame.height() + padding; y += frame.height() + padding;
} }
@ -262,9 +262,9 @@ impl SVGRenderer {
fn render_group(&mut self, state: State, group: &GroupItem) { fn render_group(&mut self, state: State, group: &GroupItem) {
let state = match group.frame.kind() { let state = match group.frame.kind() {
FrameKind::Soft => state.pre_concat(group.transform), FrameKind::Soft => state.pre_concat(group.transform),
FrameKind::Hard => { FrameKind::Hard => state
state.with_transform(group.transform).with_size(group.frame.size()) .with_transform(Transform::identity())
} .with_size(group.frame.size()),
}; };
self.xml.start_element("g"); self.xml.start_element("g");
@ -283,8 +283,7 @@ impl SVGRenderer {
/// Render a text item. The text is rendered as a group of glyphs. We will /// Render a text item. The text is rendered as a group of glyphs. We will
/// try to render the text as SVG first, then bitmap, then outline. If none /// try to render the text as SVG first, then bitmap, then outline. If none
/// of them works, we will skip the text. /// of them works, we will skip the text.
// TODO: implement gradient on text. fn render_text(&mut self, state: State, text: &TextItem) {
fn render_text(&mut self, _state: State, text: &TextItem) {
let scale: f64 = text.size.to_pt() / text.font.units_per_em(); let scale: f64 = text.size.to_pt() / text.font.units_per_em();
let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt(); let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt();
@ -302,7 +301,23 @@ impl SVGRenderer {
self.render_svg_glyph(text, id, offset, inv_scale) self.render_svg_glyph(text, id, offset, inv_scale)
.or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale)) .or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale))
.or_else(|| self.render_outline_glyph(text, id, offset, inv_scale)); .or_else(|| {
self.render_outline_glyph(
state
.pre_concat(Transform::scale(
Ratio::new(scale),
Ratio::new(-scale),
))
.pre_translate(Point::new(
Abs::pt(offset / scale),
Abs::zero(),
)),
text,
id,
offset,
inv_scale,
)
});
x += glyph.x_advance.at(text.size).to_pt(); x += glyph.x_advance.at(text.size).to_pt();
} }
@ -388,25 +403,45 @@ impl SVGRenderer {
/// Render a glyph defined by an outline. /// Render a glyph defined by an outline.
fn render_outline_glyph( fn render_outline_glyph(
&mut self, &mut self,
state: State,
text: &TextItem, text: &TextItem,
id: GlyphId, glyph_id: GlyphId,
x_offset: f64, x_offset: f64,
inv_scale: f64, inv_scale: f64,
) -> Option<()> { ) -> Option<()> {
let path = convert_outline_glyph_to_path(&text.font, id)?; let path = convert_outline_glyph_to_path(&text.font, glyph_id)?;
let hash = hash128(&(&text.font, id)); let hash = hash128(&(&text.font, glyph_id));
let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path)); let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path));
self.xml.start_element("use"); self.xml.start_element("use");
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
self.xml self.xml
.write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale)); .write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale));
self.write_fill(&text.fill, Size::zero(), Transform::identity()); self.write_fill(
&text.fill,
state.size,
self.text_paint_transform(state, &text.fill),
);
self.xml.end_element(); self.xml.end_element();
Some(()) Some(())
} }
fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform {
let Paint::Gradient(gradient) = paint else {
return Transform::identity();
};
match gradient.unwrap_relative(true) {
Relative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
Relative::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
.post_concat(state.transform.invert().unwrap()),
}
}
/// Render a shape element. /// Render a shape element.
fn render_shape(&mut self, state: State, shape: &Shape) { fn render_shape(&mut self, state: State, shape: &Shape) {
self.xml.start_element("path"); self.xml.start_element("path");

View File

@ -19,9 +19,6 @@ use crate::syntax::{Span, Spanned};
/// the [`gradient.radial` function]($gradient.radial), and conic gradients /// the [`gradient.radial` function]($gradient.radial), and conic gradients
/// through the [`gradient.conic` function]($gradient.conic). /// through the [`gradient.conic` function]($gradient.conic).
/// ///
/// See the [tracking issue](https://github.com/typst/typst/issues/2282) for
/// more details on the progress of gradient implementation.
///
/// ```example /// ```example
/// #stack( /// #stack(
/// dir: ltr, /// dir: ltr,
@ -31,6 +28,25 @@ use crate::syntax::{Span, Spanned};
/// ) /// )
/// ``` /// ```
/// ///
/// # Gradients on text
/// Gradients are supported on text but only when setting the relativeness to
/// either `{auto}` (the default value) or `{"parent"}`. It was decided that
/// glyph-by-glyph gradients would not be supported out-of-the-box but can be
/// emulated using [show rules]($styling/#show-rules).
///
/// You can use gradients on text as follows:
///
/// ```example
/// #set page(margin: 1pt)
/// #set text(fill: gradient.linear(red, blue))
/// #let rainbow(content) = {
/// set text(fill: gradient.linear(..color.map.rainbow))
/// box(content)
/// }
///
/// This is a gradient on text, but with a #rainbow[twist]!
/// ```
///
/// # Stops /// # Stops
/// A gradient is composed of a series of stops. Each of these stops has a color /// A gradient is composed of a series of stops. Each of these stops has a color
/// and an offset. The offset is a [ratio]($ratio) between `{0%}` and `{100%}` or /// and an offset. The offset is a [ratio]($ratio) between `{0%}` and `{100%}` or
@ -56,7 +72,8 @@ use crate::syntax::{Span, Spanned};
/// of a container. This container can either be the shape they are painted on, /// of a container. This container can either be the shape they are painted on,
/// or to the closest container ancestor. This is controlled by the `relative` /// or to the closest container ancestor. This is controlled by the `relative`
/// argument of a gradient constructor. By default, gradients are relative to /// argument of a gradient constructor. By default, gradients are relative to
/// the shape they are painted on. /// the shape they are painted on, unless the gradient is applied on text, in
/// which case they are relative to the closest ancestor container.
/// ///
/// Typst determines the ancestor container as follows: /// Typst determines the ancestor container as follows:
/// - For shapes that are placed at the root/top level of the document, the /// - For shapes that are placed at the root/top level of the document, the
@ -707,6 +724,22 @@ impl Gradient {
} }
impl Gradient { impl Gradient {
/// Clones this gradient, but with a different relative placement.
pub fn with_relative(mut self, relative: Relative) -> Self {
match &mut self {
Self::Linear(linear) => {
Arc::make_mut(linear).relative = Smart::Custom(relative);
}
Self::Radial(radial) => {
Arc::make_mut(radial).relative = Smart::Custom(relative);
}
Self::Conic(conic) => {
Arc::make_mut(conic).relative = Smart::Custom(relative);
}
}
self
}
/// Returns a reference to the stops of this gradient. /// Returns a reference to the stops of this gradient.
pub fn stops_ref(&self) -> &[(Color, Ratio)] { pub fn stops_ref(&self) -> &[(Color, Ratio)] {
match self { match self {

View File

@ -10,14 +10,26 @@ pub enum Paint {
} }
impl Paint { impl Paint {
/// Temporary method to unwrap a solid color used for text rendering. /// Unwraps a solid color used for text rendering.
pub fn unwrap_solid(&self) -> Color { pub fn unwrap_solid(&self) -> Color {
// TODO: Implement gradients on text.
match self { match self {
Self::Solid(color) => *color, Self::Solid(color) => *color,
Self::Gradient(_) => panic!("expected solid color"), Self::Gradient(_) => panic!("expected solid color"),
} }
} }
/// Turns this paint into a paint for a text decoration.
///
/// If this paint is a gradient, it will be converted to a gradient with
/// relative set to [`Relative::Parent`].
pub fn as_decoration(&self) -> Self {
match self {
Self::Solid(color) => Self::Solid(*color),
Self::Gradient(gradient) => {
Self::Gradient(gradient.clone().with_relative(Relative::Parent))
}
}
}
} }
impl<T: Into<Color>> From<T> for Paint { impl<T: Into<Color>> From<T> for Paint {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,9 @@
// Tests gradients on text decorations.
---
#set text(fill: gradient.linear(red, blue))
Hello #underline[World]! \
Hello #overline[World]! \
Hello #strike[World]! \

View File

@ -0,0 +1,14 @@
// Test text gradients with radial and conic gradients.
---
#set page(width: 200pt, height: auto, margin: 10pt)
#set par(justify: true)
#set text(fill: gradient.radial(red, blue))
#lorem(30)
---
#set page(width: 200pt, height: auto, margin: 10pt)
#set par(justify: true)
#set text(fill: gradient.conic(red, blue, angle: 45deg))
#lorem(30)

View File

@ -1,7 +1,49 @@
// Test that gradient fills on text don't work (for now). // Test that gradient fills on text.
// Ref: false // The solid bar gradients are used to make sure that all transforms are
// correct: if you can see the text through the bar, then the gradient is
// misaligned to its reference container.
// Ref: true
--- ---
// Hint: 17-43 gradients on text will be supported soon // Ref: false
// Error: 17-43 text fill must be a solid color // Make sure they don't work when `relative: "self"`.
// Hint: 17-61 make sure to set `relative: auto` on your text fill
// Error: 17-61 gradients on text must be relative to the parent
#set text(fill: gradient.linear(red, blue, relative: "self"))
---
// Test that gradient fills on text work for globally defined gradients.
#set page(width: 200pt, height: auto, margin: 10pt, background: {
rect(width: 100%, height: 30pt, fill: gradient.linear(red, blue))
})
#set par(justify: true)
#set text(fill: gradient.linear(red, blue)) #set text(fill: gradient.linear(red, blue))
#lorem(30)
---
// Sanity check that the direction works on text.
#set page(width: 200pt, height: auto, margin: 10pt, background: {
rect(height: 100%, width: 30pt, fill: gradient.linear(dir: btt, red, blue))
})
#set par(justify: true)
#set text(fill: gradient.linear(dir: btt, red, blue))
#lorem(30)
---
// Test that gradient fills on text work for locally defined gradients.
#set page(width: auto, height: auto, margin: 10pt)
#show box: set text(fill: gradient.linear(..color.map.rainbow))
Hello, #box[World]!
---
// Test that gradients fills on text work with transforms.
#set page(width: auto, height: auto, margin: 10pt)
#show box: set text(fill: gradient.linear(..color.map.rainbow))
#rotate(45deg, box[World])