mod common;
use common::*;
#[test]
fn glyph_run_draws_pixels() {
let provider = default_provider();
let families = vec!["Noto Sans".to_string()];
let font_size = 32.0_f32;
let req = ShapeRequest {
text: "A",
families: &families,
weight: 400,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
let run = RustybuzzEngine::new()
.shape(&req, &provider)
.expect("shaping must succeed");
let page_w = 80.0_f64;
let page_h = 40.0_f64;
let baseline_y = 34.0_f64;
let origin_x = 4.0_f64;
let ink_color = Color::srgb(0, 0, 200, 255);
let glyphs: Vec<SceneGlyph> = run
.glyphs
.iter()
.map(|g| SceneGlyph {
glyph_id: g.glyph_id,
dx: g.x,
dy: g.y,
text: String::new(),
})
.collect();
let mut scene = Scene::new(page_w, page_h);
scene.commands.push(SceneCommand::DrawGlyphRun {
x: origin_x,
y: baseline_y,
font_id: run.font_id.clone(),
font_size,
color: ink_color,
stroke_color: None,
stroke_width: None,
link: None,
selectable: true,
glyphs,
});
let img = render_image(&scene, &provider, &no_assets()).expect("render must succeed");
let any_ink = (0..img.height).any(|py| {
(0..img.width).any(|px| {
let (r, g, b, a) = pixel(&img.rgba, img.width, px, py);
a > 0 && b > r && b > g
})
});
assert!(
any_ink,
"DrawGlyphRun must rasterize at least one ink pixel for 'A' at 32px"
);
}
#[test]
fn glyph_run_deterministic_png() {
let provider = default_provider();
let families = vec!["Noto Sans".to_string()];
let font_size = 24.0_f32;
let req = ShapeRequest {
text: "Zenith",
families: &families,
weight: 400,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
let run = RustybuzzEngine::new()
.shape(&req, &provider)
.expect("shaping must succeed");
let glyphs: Vec<SceneGlyph> = run
.glyphs
.iter()
.map(|g| SceneGlyph {
glyph_id: g.glyph_id,
dx: g.x,
dy: g.y,
text: String::new(),
})
.collect();
let mut scene = Scene::new(200.0, 40.0);
scene.commands.push(SceneCommand::DrawGlyphRun {
x: 4.0,
y: 30.0,
font_id: run.font_id.clone(),
font_size,
color: Color::srgb(10, 10, 10, 255),
stroke_color: None,
stroke_width: None,
link: None,
selectable: true,
glyphs,
});
let png1 = render_png(&scene, &provider, &no_assets()).expect("first render");
let png2 = render_png(&scene, &provider, &no_assets()).expect("second render");
assert_eq!(
png1, png2,
"glyph run PNG must be byte-identical across two renders"
);
}
#[test]
fn glyph_run_missing_font_id_succeeds_silently() {
let provider = default_provider();
let mut scene = Scene::new(40.0, 40.0);
scene.commands.push(SceneCommand::DrawGlyphRun {
x: 0.0,
y: 20.0,
font_id: "nonexistent-font-000-normal".to_string(),
font_size: 16.0,
color: Color::srgb(255, 0, 0, 255),
stroke_color: None,
stroke_width: None,
link: None,
selectable: true,
glyphs: vec![SceneGlyph {
glyph_id: 36,
dx: 0.0,
dy: 0.0,
text: String::new(),
}],
});
let img = render_image(&scene, &provider, &no_assets())
.expect("render must succeed even with unknown font");
let any_opaque = (0..img.height).any(|py| {
(0..img.width).any(|px| {
let (_, _, _, a) = pixel(&img.rgba, img.width, px, py);
a > 0
})
});
assert!(
!any_opaque,
"no pixels should be drawn when the font id is unknown"
);
}
#[test]
fn glyph_run_clipped_to_subpage_clip() {
let provider = default_provider();
let families = vec!["Noto Sans".to_string()];
let font_size = 32.0_f32;
let req = ShapeRequest {
text: "A",
families: &families,
weight: 400,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
let run = RustybuzzEngine::new()
.shape(&req, &provider)
.expect("shaping must succeed");
let glyphs: Vec<SceneGlyph> = run
.glyphs
.iter()
.map(|g| SceneGlyph {
glyph_id: g.glyph_id,
dx: g.x,
dy: g.y,
text: String::new(),
})
.collect();
let mut scene = Scene::new(80.0, 40.0);
scene.commands.push(SceneCommand::PushClip {
x: 0.0,
y: 0.0,
w: 4.0,
h: 4.0,
});
scene.commands.push(SceneCommand::DrawGlyphRun {
x: 20.0,
y: 34.0,
font_id: run.font_id.clone(),
font_size,
color: Color::srgb(0, 0, 200, 255),
stroke_color: None,
stroke_width: None,
link: None,
selectable: true,
glyphs,
});
scene.commands.push(SceneCommand::PopClip);
let backend = TinySkiaBackend;
let img = backend
.rasterize(&scene, &provider, &no_assets())
.expect("rasterize must succeed");
let any_opaque = (0..img.height).any(|py| {
(0..img.width).any(|px| {
let (_, _, _, a) = pixel(&img.rgba, img.width, px, py);
a > 0
})
});
assert!(
!any_opaque,
"glyph ink must be fully clipped by the sub-page mask; found opaque pixels"
);
}
#[test]
fn glyph_run_with_stroke_renders_without_panic() {
let provider = default_provider();
let families = vec!["Noto Sans".to_string()];
let font_size = 32.0_f32;
let req = ShapeRequest {
text: "A",
families: &families,
weight: 400,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
let run = RustybuzzEngine::new()
.shape(&req, &provider)
.expect("shaping must succeed");
let glyphs: Vec<SceneGlyph> = run
.glyphs
.iter()
.map(|g| SceneGlyph {
glyph_id: g.glyph_id,
dx: g.x,
dy: g.y,
text: String::new(),
})
.collect();
let mut scene_with = Scene::new(80.0, 50.0);
scene_with.commands.push(SceneCommand::DrawGlyphRun {
x: 4.0,
y: 38.0,
font_id: run.font_id.clone(),
font_size,
color: Color::srgb(0, 0, 200, 255),
stroke_color: Some(Color::srgb(200, 0, 0, 255)),
stroke_width: Some(3.0),
link: None,
selectable: true,
glyphs: glyphs.clone(),
});
let mut scene_without = Scene::new(80.0, 50.0);
scene_without.commands.push(SceneCommand::DrawGlyphRun {
x: 4.0,
y: 38.0,
font_id: run.font_id.clone(),
font_size,
color: Color::srgb(0, 0, 200, 255),
stroke_color: None,
stroke_width: None,
link: None,
selectable: true,
glyphs,
});
let backend = TinySkiaBackend;
let img_with = backend
.rasterize(&scene_with, &provider, &no_assets())
.expect("stroke render must succeed without panic");
let img_without = backend
.rasterize(&scene_without, &provider, &no_assets())
.expect("no-stroke render must succeed");
assert_ne!(
img_with.rgba, img_without.rgba,
"a 3px red stroke must alter at least one pixel vs. fill-only"
);
}
#[test]
fn glyph_run_without_stroke_is_byte_identical() {
let provider = default_provider();
let families = vec!["Noto Sans".to_string()];
let font_size = 24.0_f32;
let req = ShapeRequest {
text: "Z",
families: &families,
weight: 400,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
let run = RustybuzzEngine::new()
.shape(&req, &provider)
.expect("shaping must succeed");
let glyphs: Vec<SceneGlyph> = run
.glyphs
.iter()
.map(|g| SceneGlyph {
glyph_id: g.glyph_id,
dx: g.x,
dy: g.y,
text: String::new(),
})
.collect();
let make = |sc: Option<Color>, sw: Option<f64>| {
let mut scene = Scene::new(60.0, 40.0);
scene.commands.push(SceneCommand::DrawGlyphRun {
x: 4.0,
y: 30.0,
font_id: run.font_id.clone(),
font_size,
color: Color::srgb(10, 10, 10, 255),
stroke_color: sc,
stroke_width: sw,
link: None,
selectable: true,
glyphs: glyphs.clone(),
});
scene
};
let img_a = backend_render(&make(None, None), &provider);
let img_b = backend_render(&make(None, None), &provider);
assert_eq!(img_a, img_b, "two no-stroke renders must be byte-identical");
let img_zero = backend_render(
&make(Some(Color::srgb(255, 0, 0, 255)), Some(0.0)),
&provider,
);
assert_eq!(
img_a, img_zero,
"stroke_width=0 must produce byte-identical output to no stroke"
);
}