Add stroke for text (#2970)
This commit is contained in:
parent
111a69f6aa
commit
81ff34d80d
@ -205,7 +205,7 @@ pub(super) trait PaintEncode {
|
||||
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 {
|
||||
@ -217,11 +217,16 @@ impl PaintEncode for Paint {
|
||||
}
|
||||
}
|
||||
|
||||
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::Pattern(pattern) => pattern.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),
|
||||
Self::Pattern(pattern) => pattern.set_as_stroke(ctx, on_text, transforms),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -267,7 +272,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);
|
||||
|
@ -225,10 +225,15 @@ impl PaintEncode for Gradient {
|
||||
.insert(PageResource::new(ResourceKind::Gradient, id), index);
|
||||
}
|
||||
|
||||
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 index = register_gradient(ctx, self, false, transforms);
|
||||
let index = register_gradient(ctx, self, on_text, transforms);
|
||||
let id = eco_format!("Gr{index}");
|
||||
let name = Name(id.as_bytes());
|
||||
|
||||
|
@ -504,7 +504,12 @@ impl PageContext<'_, '_> {
|
||||
self.state.fill_space = None;
|
||||
}
|
||||
|
||||
fn set_stroke(&mut self, stroke: &FixedStroke, transforms: Transforms) {
|
||||
fn set_stroke(
|
||||
&mut self,
|
||||
stroke: &FixedStroke,
|
||||
on_text: bool,
|
||||
transforms: Transforms,
|
||||
) {
|
||||
if self.state.stroke.as_ref() != Some(stroke)
|
||||
|| matches!(
|
||||
self.state.stroke.as_ref().map(|s| &s.paint),
|
||||
@ -520,7 +525,7 @@ impl PageContext<'_, '_> {
|
||||
miter_limit,
|
||||
} = stroke;
|
||||
|
||||
paint.set_as_stroke(self, transforms);
|
||||
paint.set_as_stroke(self, on_text, transforms);
|
||||
|
||||
self.content.set_line_width(thickness.to_f32());
|
||||
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
|
||||
@ -620,13 +625,18 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
||||
let segment = &text.text[g.range()];
|
||||
glyph_set.entry(g.id).or_insert_with(|| segment.into());
|
||||
}
|
||||
|
||||
ctx.set_fill(&text.fill, true, ctx.state.transforms(Size::zero(), pos));
|
||||
let fill_transform = ctx.state.transforms(Size::zero(), pos);
|
||||
ctx.set_fill(&text.fill, true, fill_transform);
|
||||
if let Some(stroke) = &text.stroke {
|
||||
ctx.set_stroke(stroke, true, fill_transform);
|
||||
ctx.content
|
||||
.set_text_rendering_mode(pdf_writer::types::TextRenderingMode::FillStroke);
|
||||
}
|
||||
ctx.set_font(&text.font, text.size);
|
||||
ctx.set_opacities(None, Some(&text.fill));
|
||||
ctx.set_opacities(text.stroke.as_ref(), Some(&text.fill));
|
||||
ctx.content.begin_text();
|
||||
|
||||
// Positiosn the text.
|
||||
// Position the text.
|
||||
ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
|
||||
|
||||
let mut positioned = ctx.content.show_positioned();
|
||||
@ -690,7 +700,11 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
|
||||
}
|
||||
|
||||
if let Some(stroke) = stroke {
|
||||
ctx.set_stroke(stroke, ctx.state.transforms(shape.geometry.bbox_size(), pos));
|
||||
ctx.set_stroke(
|
||||
stroke,
|
||||
false,
|
||||
ctx.state.transforms(shape.geometry.bbox_size(), pos),
|
||||
);
|
||||
}
|
||||
|
||||
ctx.set_opacities(stroke, shape.fill.as_ref());
|
||||
|
@ -140,10 +140,15 @@ impl PaintEncode for Pattern {
|
||||
.insert(PageResource::new(ResourceKind::Pattern, id), index);
|
||||
}
|
||||
|
||||
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 index = register_pattern(ctx, self, false, transforms);
|
||||
let index = register_pattern(ctx, self, on_text, transforms);
|
||||
let id = eco_format!("P{index}");
|
||||
let name = Name(id.as_bytes());
|
||||
|
||||
|
@ -15,8 +15,8 @@ use typst::layout::{
|
||||
};
|
||||
use typst::text::{Font, TextItem};
|
||||
use typst::visualize::{
|
||||
Color, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, LineJoin, Paint,
|
||||
Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
|
||||
Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap,
|
||||
LineJoin, Paint, Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
|
||||
};
|
||||
use usvg::{NodeExt, TreeParsing};
|
||||
|
||||
@ -377,7 +377,12 @@ fn render_outline_glyph(
|
||||
// Render a glyph directly as a path. This only happens when the fast glyph
|
||||
// rasterization can't be used due to very large text size or weird
|
||||
// scale/skewing transforms.
|
||||
if ppem > 100.0 || ts.kx != 0.0 || ts.ky != 0.0 || ts.sx != ts.sy {
|
||||
if ppem > 100.0
|
||||
|| ts.kx != 0.0
|
||||
|| ts.ky != 0.0
|
||||
|| ts.sx != ts.sy
|
||||
|| text.stroke.is_some()
|
||||
{
|
||||
let path = {
|
||||
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
|
||||
text.font.ttf().outline_glyph(id, &mut builder)?;
|
||||
@ -387,22 +392,56 @@ fn render_outline_glyph(
|
||||
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
|
||||
|
||||
let 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();
|
||||
|
||||
// Flip vertically because font design coordinate
|
||||
// system is Y-up.
|
||||
let ts = ts.pre_scale(scale, -scale);
|
||||
let state_ts = state.pre_concat(sk::Transform::from_scale(scale, -scale));
|
||||
let paint = to_sk_paint(
|
||||
&text.fill,
|
||||
state_ts,
|
||||
Size::zero(),
|
||||
true,
|
||||
None,
|
||||
&mut pixmap,
|
||||
None,
|
||||
);
|
||||
canvas.fill_path(&path, &paint, rule, ts, state.mask);
|
||||
|
||||
if let Some(FixedStroke {
|
||||
paint,
|
||||
thickness,
|
||||
line_cap,
|
||||
line_join,
|
||||
dash_pattern,
|
||||
miter_limit,
|
||||
}) = &text.stroke
|
||||
{
|
||||
if thickness.to_f32() > 0.0 {
|
||||
let dash = dash_pattern.as_ref().and_then(to_sk_dash_pattern);
|
||||
|
||||
let paint = to_sk_paint(
|
||||
paint,
|
||||
state_ts,
|
||||
Size::zero(),
|
||||
true,
|
||||
None,
|
||||
&mut pixmap,
|
||||
None,
|
||||
);
|
||||
let stroke = sk::Stroke {
|
||||
width: thickness.to_f32() / scale, // When we scale the path, we need to scale the stroke width, too.
|
||||
line_cap: to_sk_line_cap(*line_cap),
|
||||
line_join: to_sk_line_join(*line_join),
|
||||
dash,
|
||||
miter_limit: miter_limit.get() as f32,
|
||||
};
|
||||
|
||||
canvas.stroke_path(&path, &paint, &stroke, ts, state.mask);
|
||||
}
|
||||
}
|
||||
return Some(());
|
||||
}
|
||||
|
||||
@ -581,17 +620,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
|
||||
|
||||
// Don't draw zero-pt stroke.
|
||||
if width > 0.0 {
|
||||
let dash = dash_pattern.as_ref().and_then(|pattern| {
|
||||
// tiny-skia only allows dash patterns with an even number of elements,
|
||||
// while pdf allows any number.
|
||||
let pattern_len = pattern.array.len();
|
||||
let len =
|
||||
if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len };
|
||||
let dash_array =
|
||||
pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
|
||||
|
||||
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
||||
});
|
||||
let dash = dash_pattern.as_ref().and_then(to_sk_dash_pattern);
|
||||
|
||||
let bbox = shape.geometry.bbox_size();
|
||||
let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..)))
|
||||
@ -1045,6 +1074,15 @@ fn to_sk_transform(transform: &Transform) -> sk::Transform {
|
||||
)
|
||||
}
|
||||
|
||||
fn to_sk_dash_pattern(pattern: &DashPattern<Abs, Abs>) -> Option<sk::StrokeDash> {
|
||||
// tiny-skia only allows dash patterns with an even number of elements,
|
||||
// while pdf allows any number.
|
||||
let pattern_len = pattern.array.len();
|
||||
let len = if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len };
|
||||
let dash_array = pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
|
||||
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
||||
}
|
||||
|
||||
/// Allows to build tiny-skia paths from glyph outlines.
|
||||
struct WrappedPathBuilder(sk::PathBuilder);
|
||||
|
||||
|
@ -452,6 +452,13 @@ impl SVGRenderer {
|
||||
Size::new(Abs::pt(width), Abs::pt(height)),
|
||||
self.text_paint_transform(state, &text.fill),
|
||||
);
|
||||
if let Some(stroke) = &text.stroke {
|
||||
self.write_stroke(
|
||||
stroke,
|
||||
Size::new(Abs::pt(width), Abs::pt(height)),
|
||||
self.text_paint_transform(state, &stroke.paint),
|
||||
);
|
||||
}
|
||||
self.xml.end_element();
|
||||
|
||||
Some(())
|
||||
|
@ -230,6 +230,7 @@ impl<'a> ShapedText<'a> {
|
||||
let lang = TextElem::lang_in(self.styles);
|
||||
let decos = TextElem::deco_in(self.styles);
|
||||
let fill = TextElem::fill_in(self.styles);
|
||||
let stroke = TextElem::stroke_in(self.styles);
|
||||
|
||||
for ((font, y_offset), group) in
|
||||
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
|
||||
@ -302,6 +303,7 @@ impl<'a> ShapedText<'a> {
|
||||
size: self.size,
|
||||
lang,
|
||||
fill: fill.clone(),
|
||||
stroke: stroke.clone().map(|s| s.unwrap_or_default()),
|
||||
text: self.text[range.start - self.base..range.end - self.base].into(),
|
||||
glyphs,
|
||||
};
|
||||
|
@ -308,6 +308,7 @@ impl GlyphFragment {
|
||||
fill: self.fill,
|
||||
lang: self.lang,
|
||||
text: self.c.into(),
|
||||
stroke: None,
|
||||
glyphs: vec![Glyph {
|
||||
id: self.id.0,
|
||||
x_advance: Em::from_length(self.width, self.font_size),
|
||||
|
@ -6,7 +6,7 @@ use ecow::EcoString;
|
||||
use crate::layout::{Abs, Em};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::{Font, Lang};
|
||||
use crate::visualize::Paint;
|
||||
use crate::visualize::{FixedStroke, Paint};
|
||||
|
||||
/// A run of shaped text.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
@ -17,6 +17,8 @@ pub struct TextItem {
|
||||
pub size: Abs,
|
||||
/// Glyph color.
|
||||
pub fill: Paint,
|
||||
/// Glyph stroke.
|
||||
pub stroke: Option<FixedStroke>,
|
||||
/// The natural language of the text.
|
||||
pub lang: Lang,
|
||||
/// The item's plain text.
|
||||
|
@ -43,7 +43,7 @@ use crate::foundations::{
|
||||
use crate::layout::{Abs, Axis, Dir, Length, Rel};
|
||||
use crate::model::ParElem;
|
||||
use crate::syntax::Spanned;
|
||||
use crate::visualize::{Color, Paint, RelativeTo};
|
||||
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
|
||||
|
||||
/// Text styling.
|
||||
///
|
||||
@ -240,6 +240,15 @@ pub struct TextElem {
|
||||
#[ghost]
|
||||
pub fill: Paint,
|
||||
|
||||
/// How to stroke the text.
|
||||
///
|
||||
/// ```example
|
||||
/// #text(stroke: 0.5pt + red)[Stroked]
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[ghost]
|
||||
pub stroke: Option<Stroke>,
|
||||
|
||||
/// The amount of space that should be added between characters.
|
||||
///
|
||||
/// ```example
|
||||
|
BIN
tests/ref/text/stroke.png
Normal file
BIN
tests/ref/text/stroke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 KiB |
21
tests/typ/text/stroke.typ
Normal file
21
tests/typ/text/stroke.typ
Normal file
@ -0,0 +1,21 @@
|
||||
#set text(size: 20pt)
|
||||
#set page(width: auto)
|
||||
测试字体 #lorem(5)
|
||||
|
||||
#text(stroke: 0.3pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 0.5pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 0.7pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 1pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 2pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 5pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 7pt + red)[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: (paint: blue, thickness: 1pt, dash: "dashed"))[测试字体#lorem(5)]
|
||||
|
||||
#text(stroke: 1pt + gradient.linear(..color.map.rainbow))[测试字体#lorem(5)] // gradient doesn't work now
|
Loading…
Reference in New Issue
Block a user