diff --git a/crates/pdf/src/draw.rs b/crates/pdf/src/draw.rs index 99f4e390..1ff48409 100644 --- a/crates/pdf/src/draw.rs +++ b/crates/pdf/src/draw.rs @@ -116,7 +116,7 @@ pub(crate) fn rule(r: figcraft_render::FillRule) -> FillRule { } /// The renderer's stroke fill rule: even-odd ring paths for aligned strokes, else non-zero. -fn stroke_rule(n: &Node, closed: bool) -> FillRule { +pub(crate) fn stroke_rule(n: &Node, closed: bool) -> FillRule { if n.stroke.sides.is_some() || (n.stroke.dashes.is_empty() && closed && n.stroke.align != StrokeAlign::Center) { FillRule::EvenOdd } else { diff --git a/crates/pdf/src/text.rs b/crates/pdf/src/text.rs index c425404d..cb2ec7a2 100644 --- a/crates/pdf/src/text.rs +++ b/crates/pdf/src/text.rs @@ -5,7 +5,7 @@ use std::collections::BTreeSet; use std::ops::Range; use std::sync::Arc; -use figcraft_doc::{Node, NodeKind, StrokeAlign, StrokeProps}; +use figcraft_doc::{Node, NodeKind}; use figcraft_geom::{Affine, BezPath}; use figcraft_text::fontdb::FontBytes; use figcraft_text::{Face, Glyph, TextLayout}; @@ -16,7 +16,7 @@ use krilla::text::{Font, GlyphId, KrillaGlyph}; use figcraft_color::{GradientKind, Paint, PaintKind}; -use crate::draw::{Exporter, above, affine_ok, blend, to_xf, vector_paint}; +use crate::draw::{Exporter, above, affine_ok, blend, stroke_rule, to_xf, vector_paint}; use crate::raster::Raster; /// Glyphs of one face and size on one line, written as one text run. @@ -84,11 +84,11 @@ pub(crate) fn draw_text(ex: &mut Exporter, s: &mut Surface, n: &Node, xf: Affine } } ex.place_inner(s, inner); - let props = StrokeProps { align: StrokeAlign::Center, ..n.stroke.clone() }; + // Glyph outlines are closed: inside and outside strokes clip to them, as on the canvas (#615). for outlines in &stroked { - let area = figcraft_pathops::stroke_center(outlines, props.weight, &props); + let area = figcraft_render::draw::stroke_area(n, outlines, true); for p in &n.strokes { - ex.fill(s, &area, FillRule::NonZero, p, w, h, gxf); + ex.fill(s, &area, stroke_rule(n, true), p, w, h, gxf); } } } diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index 021af92d..4e99afb0 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -621,11 +621,11 @@ fn draw_content(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize, f(pix); } if n.has_visible_stroke() { - let props = figcraft_doc::StrokeProps { align: StrokeAlign::Center, ..n.stroke.clone() }; + // Glyph outlines are closed: inside and outside strokes clip to them (#615). for path in keys.iter().filter_map(|k| by_style.get(k)) { - let s = figcraft_pathops::stroke_center(path, props.weight, &props); + let s = stroke_area(n, path, true); if let Some(ts) = ts_path(&s) { - fill_with(pix, &ts, &n.strokes, w, h, gxf, FillRule::Winding, ctx, None); + fill_with(pix, &ts, &n.strokes, w, h, gxf, stroke_rule(n, true), ctx, None); } } } diff --git a/crates/render/src/paint_behaviour_tests.rs b/crates/render/src/paint_behaviour_tests.rs index f4bad2ca..30c583d0 100644 --- a/crates/render/src/paint_behaviour_tests.rs +++ b/crates/render/src/paint_behaviour_tests.rs @@ -630,3 +630,35 @@ fn changing_the_colour_profile_converts_or_keeps_values() { let seen = draw(&kept); assert!(seen[0] > before[0] && seen[2] < before[2], "kept values look more saturated: {seen:?} vs {before:?}"); } + +// ---- strokes on text ---- + +#[test] +fn text_strokes_follow_their_alignment() { + // #615: as on other shapes (and in SVG export), an inside stroke stays in the glyphs, a centre + // one reaches half its weight past them and an outside one all of it. + figcraft_text::scan_system_fonts(); + let left_edge = |align: StrokeAlign| -> Option { + let (mut d, p) = page(); + let mut td = figcraft_doc::TextData::new("I"); + td.style.font_size = 120.0; + let t = d.create(p, NodeKind::Text(td), "T", 60.0, 30.0, 1.0, 1.0).unwrap(); + let n = d.get_mut(t).unwrap(); + n.fills.clear(); + n.strokes = vec![Paint::solid(hex("#000000"))]; + n.stroke.weight = 12.0; + n.stroke.align = align; + let (w, h) = figcraft_text::auto_size(n)?; + (n.width, n.height) = (w, h); + let pix = render(&d); + (0..200).find(|x| (0..200).any(|y| px(&pix, *x, y)[0] < 128)) + }; + let (Some(inside), Some(centre), Some(outside)) = + (left_edge(StrokeAlign::Inside), left_edge(StrokeAlign::Center), left_edge(StrokeAlign::Outside)) + else { + eprintln!("no font to draw text with; skipping"); + return; + }; + assert!((5..=7).contains(&(inside - centre)), "centre reaches half the weight past inside: {inside} → {centre}"); + assert!((5..=7).contains(&(centre - outside)), "outside reaches the rest: {centre} → {outside}"); +}