From 47b5a3b52a4075c134046fa3f94c99288f00ebb1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 19 Oct 2026 06:12:55 -0700 Subject: [PATCH] Tests: non-Sharp type in a drop shadow, a Rasterize or a pattern takes the output's pixels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A drop shadow draws its object moved by the shadow's offset, Effect › Rasterize draws it on the pixels of its image, and a pattern draws its tile on the pixels of the tile's image and, in pattern editing mode, draws copies of the tile moved by the tile's size. Four tests draw type through each of these: a shadow whose offset (3.4, 45.3 pt) is not a whole number of pixels, a Rasterize at 36 ppi whose image lies on the canvas's pixel grid, a pattern swatch that shows copies of the tile 80.25 pixels away, and the copies pattern editing mode draws around the tile. In each, None, Crisp and Strong type give every pixel the share edge_coverage defines from the pixel Sharp type gets there. The new each_method_takes_the_share helper holds that check. --- crates/render/src/tests_typeedges.rs | 112 ++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/crates/render/src/tests_typeedges.rs b/crates/render/src/tests_typeedges.rs index 4a4e9c1cb5..edf9fbb803 100644 --- a/crates/render/src/tests_typeedges.rs +++ b/crates/render/src/tests_typeedges.rs @@ -1,9 +1,11 @@ //! Character panel › Anti-aliasing Method: type drawn as pixels with None, Sharp, Crisp or Strong //! edges. +use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::pattern::{PatternDef, PatternEdit}; use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun, TypeAntiAlias}; -use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeKind, TextKind}; +use vectorcraft_doc::{Appearance, AppearanceItem, Effect, FillLayer, Node, NodeKind, TextKind}; use vectorcraft_geom::Point; use vectorcraft_geom::shapes::rectangle; @@ -271,6 +273,114 @@ fn type_in_a_placed_document_takes_the_pages_pixels() { } } +/// The opacity of each pixel of `d` drawn at `view` on a transparent canvas `w` × `h` pixels. +fn alphas_at(d: &Document, (w, h): (u32, u32), view: Affine) -> Vec { + Renderer::new().render(d, w, h, view, &RenderOptions::default()).pixels.chunks(4).map(|p| p[3]).collect() +} + +/// `page(m, None)` with the effect `id` (parameters `params`) on the type object, and the object's +/// id. +fn page_with_effect(m: TypeAntiAlias, id: &str, params: Value) -> (Document, NodeId) { + let mut d = page(m, None); + let n = d.layers[0].children().and_then(|c| c.first()).map(|n| n.id).unwrap(); + d.node_mut(n).unwrap().appearance.effects.push(Effect { id: id.into(), params, visible: true }); + (d, n) +} + +/// "Ham" in 30 pt type drawn with `m`, its baseline at (5.3, 30.4). +fn ham(d: &mut Document, m: TypeAntiAlias) -> Node { + let mut t = TextObject::point(Point::new(5.3, 30.4), "Ham", CharStyle { size: 30.0, ..Default::default() }); + t.anti_alias = m; + Node::new(d.alloc_id(), NodeKind::Text(Box::new(t))) +} + +/// `draw` gives the pixels of type drawn with a method: Sharp type has smooth edges there, and +/// each other method changes them and gives every pixel the share of it `edge_coverage` defines. +fn each_method_takes_the_share(what: &str, draw: impl Fn(TypeAntiAlias) -> Vec) { + let sharp = draw(TypeAntiAlias::Sharp); + assert!(partial(&sharp) > 200, "{what}: Sharp draws smooth edges: {} partial pixels", partial(&sharp)); + for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] { + let drawn = draw(m); + assert_ne!(drawn, sharp, "{what}: {m:?} changes the edges"); + let wrong = off_the_share(m, &sharp, &drawn); + assert!(wrong.is_empty(), "{what}, {m:?}: (pixel, Sharp, drawn) {wrong:?}"); + } +} + +/// A drop shadow whose offset is not a whole number of pixels draws the type on the canvas's +/// pixels: each method gives every pixel, the shadow's below the type included, the share of it +/// `edge_coverage` defines. +#[test] +fn type_in_a_drop_shadow_takes_the_canvas_pixels() { + let shadow = json!({"mode": "normal", "opacity": 100, "x": 3.4, "y": 45.3, "blur": 0, "color": "#000000"}); + let draw = |m| alphas_at(&page_with_effect(m, "stylize.dropShadow", shadow.clone()).0, (W, 130), Affine::IDENTITY); + // Below the type's descender, the shadow alone. + let below = |a: &[u8]| a[(W * 75) as usize..].iter().filter(|v| **v == 255).count(); + assert!(below(&draw(TypeAntiAlias::Sharp)) > 500, "the shadow is drawn below the type"); + each_method_takes_the_share("drop shadow", draw); +} + +/// Effect › Rasterize draws the type on the pixels of its image: at 36 ppi each pixel of the image +/// is 2 × 2 pixels of a canvas whose grid it lies on, and each method gives every pixel the share +/// of it `edge_coverage` defines. +#[test] +fn type_in_a_rasterize_takes_the_images_pixels() { + let draw = |m| { + let (d, id) = page_with_effect(m, "rasterize", json!({"useDocumentResolution": false, "resolution": 36})); + let image = drawn_bounds(&d, d.node(id).unwrap()).unwrap(); + // The image's corner on a corner of a canvas pixel. + let view = Affine::translate((image.x0.ceil() - image.x0, image.y0.ceil() - image.y0)); + alphas_at(&d, (W, H), view) + }; + each_method_takes_the_share("rasterize", draw); +} + +/// A pattern tile holding type draws it on the pixels of the tile's image, also in the copies of +/// the tile 80.25 pixels away: in the swatch each method gives every pixel the share of it +/// `edge_coverage` defines. +#[test] +fn type_in_a_pattern_tile_takes_the_tiles_pixels() { + let draw = |m| { + let mut d = Document::new(f64::from(W), f64::from(H)); + let text = ham(&mut d, m); + let mut def = PatternDef::new("Type", vec![Arc::new(text)]); + // The swatch draws 2 pixels a point and shows the copies above and below the tile, 80.25 + // pixels away. + def.tile = Rect::new(0.0, 0.0, 120.5, 40.125); + d.patterns.push(def); + let swatch = render_pattern_swatch(&d, "Type", 241).unwrap(); + swatch.pixels.chunks(4).map(|p| p[3]).collect::>() + }; + // The rows of the copy above the tile. + let above = |a: &[u8]| a[..241 * 60].iter().filter(|v| **v == 255).count(); + assert!(above(&draw(TypeAntiAlias::Sharp)) > 200, "the swatch shows the copy above the tile"); + each_method_takes_the_share("pattern tile", draw); +} + +/// Pattern editing mode draws the copies of the tile, 120.25 pixels apart across and 40.125 pixels +/// apart down, on the canvas's pixels: each method gives every pixel the share of it +/// `edge_coverage` defines. +#[test] +fn type_in_the_copies_of_an_edited_tile_takes_the_canvas_pixels() { + let draw = |m| { + let mut d = Document::new(400.0, 200.0); + let text = ham(&mut d, m); + let mut def = PatternDef::new("Type", vec![Arc::new(text.clone())]); + def.tile = Rect::new(0.0, 0.0, 120.25, 40.125); + // The eight copies around the tile, at full opacity. + (def.copies, def.dim_copies, def.show_tile_edge) = (3, 100.0, false); + d.patterns.push(def); + let layer = d.add_layer(Some("Pattern Editing Mode")); + d.insert(Some(layer), 0, text).unwrap(); + d.pattern_edit = Some(PatternEdit { pattern: "Type".into(), layer, original: None }); + alphas_at(&d, (380, 130), Affine::translate((125.0, 45.0))) + }; + // The rows of the copies above the tile. + let above = |a: &[u8]| a[..380 * 40].iter().filter(|v| **v == 255).count(); + assert!(above(&draw(TypeAntiAlias::Sharp)) > 200, "the copies above the tile are drawn"); + each_method_takes_the_share("pattern editing", draw); +} + /// Type with a brush stroke of its own is drawn as Sharp draws it. #[test] fn type_with_a_brush_stroke_is_drawn_as_sharp() {