From f34b9b2f5cbef461041baf89d04ed85b1ec54106 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 31 Mar 2026 21:22:35 -0700 Subject: [PATCH] [Text] fixup bounds and normalized form --- masque/shapes/text.py | 14 ++++++++++---- masque/test/test_shape_advanced.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index dec4c33..93f2024 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -108,6 +108,7 @@ class Text(PositionableImpl, RotatableImpl, Shape): and self.string == other.string and self.height == other.height and self.font_path == other.font_path + and self.mirrored == other.mirrored and self.rotation == other.rotation and self.repetition == other.repetition and annotations_eq(self.annotations, other.annotations) @@ -127,6 +128,8 @@ class Text(PositionableImpl, RotatableImpl, Shape): return self.font_path < other.font_path if not numpy.array_equal(self.offset, other.offset): return tuple(self.offset) < tuple(other.offset) + if self.mirrored != other.mirrored: + return self.mirrored < other.mirrored if self.rotation != other.rotation: return self.rotation < other.rotation if self.repetition != other.repetition: @@ -174,22 +177,25 @@ class Text(PositionableImpl, RotatableImpl, Shape): (self.offset, self.height / norm_value, rotation, bool(self.mirrored)), lambda: Text( string=self.string, - height=self.height * norm_value, + height=norm_value, font_path=self.font_path, rotation=rotation, ).mirror2d(across_x=self.mirrored), ) - def get_bounds_single(self) -> NDArray[numpy.float64]: + def get_bounds_single(self) -> NDArray[numpy.float64] | None: # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so # just convert to polygons instead polys = self.to_polygons() + if not polys: + return None + pbounds = numpy.full((len(polys), 2, 2), nan) for pp, poly in enumerate(polys): pbounds[pp] = poly.get_bounds_nonempty() bounds = numpy.vstack(( - numpy.min(pbounds[: 0, :], axis=0), - numpy.max(pbounds[: 1, :], axis=0), + numpy.min(pbounds[:, 0, :], axis=0), + numpy.max(pbounds[:, 1, :], axis=0), )) return bounds diff --git a/masque/test/test_shape_advanced.py b/masque/test/test_shape_advanced.py index 4e38e55..046df1a 100644 --- a/masque/test/test_shape_advanced.py +++ b/masque/test/test_shape_advanced.py @@ -27,6 +27,33 @@ def test_text_to_polygons() -> None: assert len(set(char_x_means)) >= 2 +def test_text_bounds_and_normalized_form() -> None: + pytest.importorskip("freetype") + font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf" + if not Path(font_path).exists(): + pytest.skip("Font file not found") + + text = Text("Hi", height=10, font_path=font_path) + _intrinsic, extrinsic, ctor = text.normalized_form(5) + normalized = ctor() + + assert extrinsic[1] == 2 + assert normalized.height == 5 + + bounds = text.get_bounds_single() + assert bounds is not None + assert numpy.isfinite(bounds).all() + assert numpy.all(bounds[1] > bounds[0]) + + +def test_text_mirroring_affects_comparison() -> None: + text = Text("A", height=10, font_path="dummy.ttf") + mirrored = Text("A", height=10, font_path="dummy.ttf", mirrored=True) + + assert text != mirrored + assert (text < mirrored) != (mirrored < text) + + # 2. Manhattanization tests def test_manhattanize() -> None: pytest.importorskip("float_raster")