Files
designcraft/crates/render/tests/text_stroke_behind.rs
aiongg 270dd46b50 Text: stroke behind fill (#619)
A character attribute strokeBehindFill paints a text stroke under the fill
at double width, so its full weight lies outside the letters and they keep
their shape. Screen and PNG rendering, PDF export (stroke as outlines
first, then the filled text, so the text is not duplicated; clipped to the
outside of the glyphs when there is no fill), Create Outlines (outside-
aligned stroke), the Properties panel and Character Style Options.

IDML StrokeAlignment on CharacterStyleRange, CharacterStyle and
ParagraphStyle imports and exports as the attribute (OutsideAlignment =
on, CenterAlignment = off).
2026-10-12 21:06:42 +00:00

64 lines
3.2 KiB
Rust

//! Stroke behind fill on text: the fill covers the inner half of a double-width stroke.
use designcraft_color::{Color, swatch::Swatch};
use designcraft_compose::Cache;
use designcraft_doc::build::NewDocument;
use designcraft_doc::{Document, ParaFormat, SpreadRef};
use designcraft_geom::Rect;
use designcraft_render::{RenderOptions, Rendered, Renderer};
const WHITE: [u8; 4] = [255, 255, 255, 255];
const RED: [u8; 4] = [255, 0, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
const WEIGHT: f64 = 12.0;
/// A 200 pt "I" filled with `fill`, stroked red at [`WEIGHT`] (none when `stroke` is false).
fn render(fill: &str, stroke: bool, behind: bool) -> Rendered {
let mut d = Document::new(&NewDocument { width: 400.0, height: 400.0, facing_pages: false, ..Default::default() });
d.swatches.push(Swatch::color("Red", Color::rgb8(255, 0, 0)));
d.swatches.push(Swatch::color("Blue", Color::rgb8(0, 0, 255)));
let lid = d.default_layer();
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(40.0, 0.0, 400.0, 400.0), lid, "I", ParaFormat::default()).unwrap();
d.story_mut(sid).unwrap().format_chars(0..1, |f| {
f.over.size = Some(200.0);
f.over.fill = Some(fill.into());
f.over.stroke = Some(if stroke { "Red" } else { designcraft_color::swatch::NONE }.into());
f.over.stroke_weight = Some(WEIGHT);
f.over.stroke_behind_fill = Some(behind);
});
let mut r = Renderer::new();
r.threads = 0;
let cache = Cache::new();
let cs = cache.get(&d, sid, None);
assert!(!cs.frames[0].lines.is_empty(), "the glyph is set");
r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions { paper: false, background: Some(WHITE), ..Default::default() }).unwrap()
}
/// The left edge of the stem on a row through the middle of the unstroked glyph.
fn stem_edge() -> (u32, u32) {
let img = render("Blue", false, false);
let blue: Vec<(u32, u32)> = (0..img.height).flat_map(|y| (0..img.width).map(move |x| (x, y))).filter(|&(x, y)| img.pixel(x, y) == BLUE).collect();
assert!(!blue.is_empty(), "the glyph is drawn");
let y0 = blue.iter().map(|p| p.1).min().unwrap();
let y1 = blue.iter().map(|p| p.1).max().unwrap();
let y = (y0 + y1) / 2;
let x = (0..img.width).find(|&x| img.pixel(x, y) == BLUE).unwrap();
(x, y)
}
#[test]
fn stroke_behind_fill_shows_only_outside_the_letter() {
let (x, y) = stem_edge();
let inner = x + 3; // inside the outline, within the inner half of a centred stroke
let outer = x - 9; // outside the outline, beyond the outer half of a centred stroke
let over = render("Blue", true, false);
assert_eq!(over.pixel(inner, y), RED, "a centred stroke covers the fill's edge");
assert_eq!(over.pixel(outer, y), WHITE, "a centred stroke reaches half its weight outside");
let behind = render("Blue", true, true);
assert_eq!(behind.pixel(inner, y), BLUE, "the fill covers the stroke behind it");
assert_eq!(behind.pixel(outer, y), RED, "the full stroke weight lies outside the letter");
assert_eq!(behind.pixel(x - 15, y), WHITE, "and no further");
let hollow = render(designcraft_color::swatch::NONE, true, true);
assert_eq!(hollow.pixel(inner, y), WHITE, "with no fill the letter stays empty");
assert_eq!(hollow.pixel(outer, y), RED);
}