From 8a0e4440703f41ff3a158276cdee84193f61edf2 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 21 Feb 2020 13:49:49 +0000 Subject: [PATCH 1/3] Add move() method to Contour, Component, Anchor and Point classes To be used for sidebearings setters --- src/ufoLib2/objects/anchor.py | 5 +++++ src/ufoLib2/objects/component.py | 4 ++++ src/ufoLib2/objects/contour.py | 4 ++++ src/ufoLib2/objects/glyph.py | 8 ++++++++ src/ufoLib2/objects/point.py | 5 +++++ 5 files changed, 26 insertions(+) diff --git a/src/ufoLib2/objects/anchor.py b/src/ufoLib2/objects/anchor.py index 95223027..075624b0 100644 --- a/src/ufoLib2/objects/anchor.py +++ b/src/ufoLib2/objects/anchor.py @@ -12,3 +12,8 @@ class Anchor(AttrDictMixin): name = attr.ib(default=None, type=Optional[str]) color = attr.ib(default=None, type=Optional[str]) identifier = attr.ib(default=None, type=Optional[str]) + + def move(self, delta): + x, y = delta + self.x += x + self.y += y diff --git a/src/ufoLib2/objects/component.py b/src/ufoLib2/objects/component.py index 6eac9513..c7dd47fe 100644 --- a/src/ufoLib2/objects/component.py +++ b/src/ufoLib2/objects/component.py @@ -16,6 +16,10 @@ class Component: ) identifier = attr.ib(default=None, type=Optional[str]) + def move(self, delta): + x, y = delta + self.transformation = self.transformation.translate(x, y) + # ----------- # Pen methods # ----------- diff --git a/src/ufoLib2/objects/contour.py b/src/ufoLib2/objects/contour.py index 81a875ad..3e9b0543 100644 --- a/src/ufoLib2/objects/contour.py +++ b/src/ufoLib2/objects/contour.py @@ -45,6 +45,10 @@ def open(self): return True return self.points[0].type == "move" + def move(self, delta): + for point in self.points: + point.move(delta) + # ----------- # Pen methods # ----------- diff --git a/src/ufoLib2/objects/glyph.py b/src/ufoLib2/objects/glyph.py index ad4bca82..d7e1ca20 100644 --- a/src/ufoLib2/objects/glyph.py +++ b/src/ufoLib2/objects/glyph.py @@ -186,6 +186,14 @@ def copyDataFromGlyph(self, glyph): pointPen = self.getPointPen() glyph.drawPoints(pointPen) + def move(self, delta): + for contour in self.contours: + contour.move(delta) + for component in self.components: + component.move(delta) + for anchor in self.anchors: + anchor.move(delta) + # ----------- # Pen methods # ----------- diff --git a/src/ufoLib2/objects/point.py b/src/ufoLib2/objects/point.py index a8d08c84..aa235ecd 100644 --- a/src/ufoLib2/objects/point.py +++ b/src/ufoLib2/objects/point.py @@ -16,3 +16,8 @@ class Point: def segmentType(self): # alias for backward compatibility with defcon API return self.type + + def move(self, delta): + x, y = delta + self.x += x + self.y += y From d467deefd970c754e809dc349f1dc0060c08d6b4 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 20 Feb 2020 18:56:08 +0000 Subject: [PATCH 2/3] Glyph: add getBounds and getControlBounds Fixes #22 --- src/ufoLib2/objects/glyph.py | 22 +++++++++++++++++ tests/objects/test_glyph.py | 47 +++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/ufoLib2/objects/glyph.py b/src/ufoLib2/objects/glyph.py index d7e1ca20..7dd8f792 100644 --- a/src/ufoLib2/objects/glyph.py +++ b/src/ufoLib2/objects/glyph.py @@ -1,9 +1,11 @@ +from collections import namedtuple from copy import deepcopy from typing import Any, Dict, List, Optional, Union import attr from fontTools.misc.transform import Transform from fontTools.pens.pointPen import PointToSegmentPen, SegmentToPointPen +from fontTools.pens.boundsPen import BoundsPen, ControlBoundsPen from ufoLib2.objects.anchor import Anchor from ufoLib2.objects.contour import Contour @@ -239,3 +241,23 @@ def verticalOrigin(self, value): self.lib["public.verticalOrigin"] = value elif "public.verticalOrigin" in self.lib: del self.lib["public.verticalOrigin"] + + # bounds and side-bearings + + BoundingBox = namedtuple("BoundingBox", "xMin yMin xMax yMax") + + def getBounds(self, layer=None): + if layer is None and self.components: + raise TypeError("layer is required to compute bounds of components") + + pen = BoundsPen(layer) + self.draw(pen) + return pen.bounds if pen.bounds is None else self.BoundingBox(*pen.bounds) + + def getControlBounds(self, layer=None): + if layer is None and self.components: + raise TypeError("layer is required to compute bounds of components") + + pen = ControlBoundsPen(layer) + self.draw(pen) + return pen.bounds if pen.bounds is None else self.BoundingBox(*pen.bounds) diff --git a/tests/objects/test_glyph.py b/tests/objects/test_glyph.py index f6433131..c2e66ffe 100644 --- a/tests/objects/test_glyph.py +++ b/tests/objects/test_glyph.py @@ -1,4 +1,13 @@ -from ufoLib2.objects import Anchor, Contour, Component, Glyph, Guideline, Image, Point +from ufoLib2.objects import ( + Anchor, + Contour, + Component, + Glyph, + Guideline, + Image, + Layer, + Point, +) import pytest @@ -101,3 +110,39 @@ def test_glyph_repr(): g = Glyph("a") assert repr(g) == f"" + + +def test_glyph_get_bounds(): + a = Glyph("a") + pen = a.getPen() + pen.moveTo((0, 0)) + pen.curveTo((10, 10), (10, 20), (0, 20)) + pen.closePath() + + b = Glyph("b", components=[Component("a", (1, 0, 0, 1, -50, 100))]) + + layer = Layer(glyphs=[a, b]) + + assert a.getBounds(layer) == Glyph.BoundingBox(xMin=0, yMin=0, xMax=7.5, yMax=20) + + assert a.getControlBounds(layer) == Glyph.BoundingBox( + xMin=0, yMin=0, xMax=10, yMax=20 + ) + + with pytest.raises( + TypeError, match="layer is required to compute bounds of components" + ): + b.getBounds() + with pytest.raises( + TypeError, match="layer is required to compute bounds of components" + ): + b.getControlBounds() + + assert b.getBounds(layer) == (-50, 100, -42.5, 120) # namedtuple is a tuple + assert b.getControlBounds(layer) == (-50, 100, -40, 120) + + +def test_glyph_get_bounds_empty(): + g = Glyph() + assert g.getBounds() is None + assert g.getControlBounds() is None From 54f21d6af94cd15537eb56872541c22f86c4f937 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 20 Feb 2020 19:27:23 +0000 Subject: [PATCH 3/3] Glyph: add getter and setter methods for side-bearings --- src/ufoLib2/objects/glyph.py | 73 +++++++++++++ tests/objects/test_glyph.py | 195 +++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) diff --git a/src/ufoLib2/objects/glyph.py b/src/ufoLib2/objects/glyph.py index 7dd8f792..f132af06 100644 --- a/src/ufoLib2/objects/glyph.py +++ b/src/ufoLib2/objects/glyph.py @@ -261,3 +261,76 @@ def getControlBounds(self, layer=None): pen = ControlBoundsPen(layer) self.draw(pen) return pen.bounds if pen.bounds is None else self.BoundingBox(*pen.bounds) + + def getLeftMargin(self, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + return bounds.xMin + + def setLeftMargin(self, value, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + diff = value - bounds.xMin + if diff: + self.width += diff + self.move((diff, 0)) + + def getRightMargin(self, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + return self.width - bounds.xMax + + def setRightMargin(self, value, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + self.width = bounds.xMax + value + + def getBottomMargin(self, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + if self.verticalOrigin is None: + return bounds.yMin + else: + return bounds.yMin - (self.verticalOrigin - self.height) + + def setBottomMargin(self, value, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + # blindly copied from defcon Glyph._set_bottomMargin; not sure it's correct + if self.verticalOrigin is None: + oldValue = bounds.yMin + self.verticalOrigin = self.height + else: + oldValue = bounds.yMin - (self.verticalOrigin - self.height) + diff = value - oldValue + if diff: + self.height += diff + + def getTopMargin(self, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return None + if self.verticalOrigin is None: + return self.height - bounds.yMax + else: + return self.verticalOrigin - bounds.yMax + + def setTopMargin(self, value, layer=None): + bounds = self.getBounds(layer) + if bounds is None: + return + if self.verticalOrigin is None: + oldValue = self.height - bounds.yMax + else: + oldValue = self.verticalOrigin - bounds.yMax + diff = value - oldValue + if oldValue != value: + # Is this still correct when verticalOrigin was not previously set? + self.verticalOrigin = bounds.yMax + value + self.height += diff diff --git a/tests/objects/test_glyph.py b/tests/objects/test_glyph.py index c2e66ffe..45aa96fc 100644 --- a/tests/objects/test_glyph.py +++ b/tests/objects/test_glyph.py @@ -146,3 +146,198 @@ def test_glyph_get_bounds_empty(): g = Glyph() assert g.getBounds() is None assert g.getControlBounds() is None + + +@pytest.fixture +def layer(): + a = Glyph("a") + pen = a.getPen() + pen.moveTo((8, 0)) + pen.lineTo((18, 0)) + pen.lineTo((18, 20)) + pen.lineTo((8, 20)) + pen.closePath() + a.width = 30 + a.appendAnchor({"x": 10, "y": 30, "name": "top"}) + + b = Glyph("b", width=a.width, components=[Component("a", (1, 0, 0, 1, 2, -5))]) + + layer = Layer(glyphs=[a, b]) + return layer + + +def test_glyph_get_margins(layer): + a = layer["a"] + + # for simple contour glyphs without components, layer is optional/unused + assert a.getLeftMargin() == 8 + assert a.getLeftMargin(layer) == 8 + assert a.getRightMargin() == 12 + assert a.getRightMargin(layer) == 12 + assert a.getBottomMargin() == 0 + assert a.getBottomMargin(layer) == 0 + assert a.getTopMargin() == -20 + assert a.getTopMargin(layer) == -20 + + a.verticalOrigin = 20 + assert a.getBottomMargin() == -20 + assert a.getBottomMargin(layer) == -20 + assert a.getTopMargin() == 0 + assert a.getTopMargin(layer) == 0 + + b = layer["b"] + # for composite glyphs, layer is required + for m in ("Left", "Right", "Top", "Bottom"): + with pytest.raises(TypeError, match="layer is required to compute bounds"): + getattr(b, f"get{m}Margin")() + + assert b.getLeftMargin(layer) == 10 + assert b.getRightMargin(layer) == 10 + assert b.getBottomMargin(layer) == -5 + assert b.getTopMargin(layer) == -15 + + b.verticalOrigin = 15 + assert b.getBottomMargin(layer) == -20 + assert b.getTopMargin(layer) == 0 + + c = Glyph() # empty glyph + assert c.getLeftMargin() is None + assert c.getRightMargin() is None + assert c.getBottomMargin() is None + assert c.getTopMargin() is None + + +def test_simple_glyph_set_left_margins(layer): + a = layer["a"] + b = layer["b"] # same width, has component 'a' shifted +2 horizontally + + assert a.getLeftMargin() == 8 + assert b.getLeftMargin(layer) == 10 + assert a.width == 30 + assert b.width == 30 + assert a.anchors[0].x, a.anchors[0].y == (10, 20) + + a.setLeftMargin(8) # no change + assert a.getLeftMargin() == 8 + assert a.width == 30 + + a.setLeftMargin(10) # +2 + assert a.getLeftMargin() == 10 + assert a.width == 32 + # anchors were shifted + assert a.anchors[0].x, a.anchors[0].y == (12, 20) + # composite glyph "b" also shifts, but keeps original width + assert b.getLeftMargin(layer) == 12 + assert b.width == 30 + + a.setLeftMargin(-2) # -12 + assert a.getLeftMargin(-2) + assert a.width == 20 + + +def test_composite_glyph_set_left_margins(layer): + b = layer["b"] + + assert b.getLeftMargin(layer) == 10 + assert b.width == 30 + + b.setLeftMargin(12, layer) # +2 + assert b.getLeftMargin(layer) == 12 + assert b.width == 32 + + +def test_simple_glyph_set_right_margins(layer): + a = layer["a"] + b = layer["b"] # same width, has component 'a' shifted +2 horizontally + + assert a.getRightMargin() == 12 + assert b.getRightMargin(layer) == 10 + assert a.width == 30 + assert b.width == 30 + assert a.anchors[0].x, a.anchors[0].y == (10, 20) + + a.setRightMargin(12) # no change + assert a.getRightMargin() == 12 + assert a.width == 30 + + a.setRightMargin(10) # -2 + assert a.getRightMargin() == 10 + # only width changes, anchors stay same + assert a.width == 28 + assert a.anchors[0].x, a.anchors[0].y == (10, 20) + # composite glyph "b" does _not_ change when "a" RSB changes + assert b.getRightMargin(layer) == 10 + assert b.width == 30 + + a.setRightMargin(-2) # -12 + assert a.getRightMargin() == -2 + assert a.width == 16 + + +def test_composite_glyph_set_right_margins(layer): + b = layer["b"] + + assert b.getRightMargin(layer) == 10 + assert b.width == 30 + + b.setRightMargin(12, layer) # +2 + assert b.getRightMargin(layer) == 12 + assert b.width == 32 + + +def test_simple_glyph_set_bottom_margins(layer): + a = layer["a"] + b = layer["b"] # same height/origin, has component 'a' shifted -5 vertically + a.verticalOrigin = b.verticalOrigin = a.height = b.height = 30 + + assert a.getBottomMargin() == 0 + assert b.getBottomMargin(layer) == -5 + + a.setBottomMargin(-10) + assert a.getBottomMargin(layer) == -10 + assert a.height == 20 + assert a.verticalOrigin == 30 + # composite glyph "b" does not change + assert b.getBottomMargin(layer) == -5 + assert b.height == b.verticalOrigin == 30 + + +def test_composite_glyph_set_bottom_margins(layer): + b = layer["b"] + b.verticalOrigin = b.height = 30 + + assert b.getBottomMargin(layer) == -5 + assert b.height == 30 + + b.setBottomMargin(0, layer) # +5 + assert b.getBottomMargin(layer) == 0 + assert b.height == 35 + + +def test_simple_glyph_set_top_margins(layer): + a = layer["a"] + b = layer["b"] # same height/origin, has component 'a' shifted -5 vertically + a.verticalOrigin = b.verticalOrigin = a.height = b.height = 30 + + assert a.getTopMargin() == 10 + assert b.getTopMargin(layer) == 15 + + a.setTopMargin(-10) + assert a.getTopMargin() == -10 + assert a.height == 10 + assert a.verticalOrigin == 10 + # composite glyph "b" does not change + assert b.getTopMargin(layer) == 15 + assert b.height == b.verticalOrigin == 30 + + +def test_composite_glyph_set_top_margins(layer): + b = layer["b"] + b.verticalOrigin = b.height = 30 + + assert b.getTopMargin(layer) == 15 + assert b.height == 30 + + b.setTopMargin(10, layer) # -5 + assert b.getTopMargin(layer) == 10 + assert b.height == 25