Repository: fonttools/fonttools

Issue:
[glyf] bbox of transformed & anchored component calculated incorrectly

The attached ttx file of a TrueType font (Variable, but this seems irrelevant) has its “O” glyph composed of a pair of “C” glyphs, one rotated by 180 degrees lined up to the other using anchor points (i.e., flag ARGS_ARE_XY_VALUES unset). The XML description of “O” glyph reads:
```
<TTGlyph name="O" xMin="56" yMin="-12" xMax="612" yMax="672">
  <component glyphName="C" x="0" y="0" flags="0x204"/>
  <component glyphName="C" firstPt="0" secondPt="9" scale="-1.0" flags="0x4"/>
</TTGlyph>
```
When converted to ttf by ttx then back to ttx, the XML description becomes:
```
<TTGlyph name="O" xMin="-334" yMin="-1356" xMax="334" yMax="672">
  <component glyphName="C" x="0" y="0" flags="0x204"/>
  <component glyphName="C" firstPt="0" secondPt="9" scale="-1.0" flags="0x4"/>
</TTGlyph>
```
The glyph bounding box is calculated incorrectly.
It appears as if translation offset for the component is calculated before rotation. These should be done backwards.

[SourceSansVariable-Roman.anchor.ttx.zip](https://github.com/fonttools/fonttools/files/3009833/SourceSansVariable-Roman.anchor.ttx.zip)




Discussion:
this is the code that computes the coordinates for a glyph component:

https://github.com/fonttools/fonttools/blob/c6006a7f8c31cd8e7dddad58894e1ba6c87e1362/Lib/fontTools/ttLib/tables/_g_l_y_f.py#L843-L874

By reading the description of SCALED_COMPONENT_OFFSET and UNSCALED_COMPONENT_OFFSET flags in https://docs.microsoft.com/en-us/typography/opentype/spec/glyf, I am not sure whether these flags apply also to the case when a component  args are not XY values but point numbers.

fonttools defaults to UNSCALED_COMPONENT_OFFSET, i.e. the component is fist scaled/rotated and then moved.

If I build the font from the provided ttx I get a font that fonttools itself can't process:

```python
Traceback (most recent call last):
  ...
  File "fontTools/ttLib/ttFont.py", line 774, in draw
  File "fontTools/ttLib/tables/_g_l_y_f.py", line 1002, in draw
  File "fontTools/ttLib/tables/_g_l_y_f.py", line 1096, in getComponentInfo
AttributeError: 'GlyphComponent' object has no attribute 'x'
```

yep

https://github.com/fonttools/fonttools/blob/c6006a7f8c31cd8e7dddad58894e1ba6c87e1362/Lib/fontTools/ttLib/tables/_g_l_y_f.py#L1094-L1100
And that comment is ... 20 years old :)

![image](https://user-images.githubusercontent.com/4246121/55077006-0ecb5080-5097-11e9-9a2a-6f9ae8f6bdea.png)

The number of fonts out there that use this feature must be rather small.
ok so, there are two separate issues here:
1) getComponentInfo doesn't support components with point numbers (so a glyph containing those can't be drawn using draw/drawPoints api)
2) the bbox is computed incorrectly because the Glyph.getCoordinates method is computing the offset (firstPoint - secondPoint) before applying the transformation, and then moving the transformed component by the unscaled offset.

The fix for issue 2) would be to compute the offset from the points only after the transform has been applied. Correct?
> The fix for issue 2) would be to compute the offset from the points only after the transform has been applied. Correct?

Looking at FreeType's `ttgload.c`, that seems to be correct.
Although I'm not sure this line is correct:
https://github.com/fonttools/fonttools/blob/c6006a7f8c31cd8e7dddad58894e1ba6c87e1362/Lib/fontTools/ttLib/tables/_g_l_y_f.py#L850

I'm not sure I understand the corresponding FreeType code correctly, but perhaps firstPoint is an index into the point list of the _last_ added glyph, and not the point list as a whole. See the `start_point` argument to `TT_Process_Composite_Component()`.
I always understood firstPoint as being an index into the whole point list, i.e. the points of all the glyph components added so far

(EDIT: but I might be mistaken 🤔)

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -1012,33 +1012,37 @@ def getCoordinates(self, glyfTable):
 					coordinates, endPts, flags = g.getCoordinates(glyfTable)
 				except RecursionError:
 					raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName)
+				coordinates = GlyphCoordinates(coordinates)
 				if hasattr(compo, "firstPt"):
-					# move according to two reference points
+					# component uses two reference points: we apply the transform _before_
+					# computing the offset between the points
+					if hasattr(compo, "transform"):
+						coordinates.transform(compo.transform)
 					x1,y1 = allCoords[compo.firstPt]
 					x2,y2 = coordinates[compo.secondPt]
 					move = x1-x2, y1-y2
-				else:
-					move = compo.x, compo.y
-
-				coordinates = GlyphCoordinates(coordinates)
-				if not hasattr(compo, "transform"):
 					coordinates.translate(move)
 				else:
-					apple_way = compo.flags & SCALED_COMPONENT_OFFSET
-					ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
-					assert not (apple_way and ms_way)
-					if not (apple_way or ms_way):
-						scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT  # see top of this file
-					else:
-						scale_component_offset = apple_way
-					if scale_component_offset:
-						# the Apple way: first move, then scale (ie. scale the component offset)
+					# component uses XY offsets
+					move = compo.x, compo.y
+					if not hasattr(compo, "transform"):
 						coordinates.translate(move)
-						coordinates.transform(compo.transform)
 					else:
-						# the MS way: first scale, then move
-						coordinates.transform(compo.transform)
-						coordinates.translate(move)
+						apple_way = compo.flags & SCALED_COMPONENT_OFFSET
+						ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
+						assert not (apple_way and ms_way)
+						if not (apple_way or ms_way):
+							scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT  # see top of this file
+						else:
+							scale_component_offset = apple_way
+						if scale_component_offset:
+							# the Apple way: first move, then scale (ie. scale the component offset)
+							coordinates.translate(move)
+							coordinates.transform(compo.transform)
+						else:
+							# the MS way: first scale, then move
+							coordinates.transform(compo.transform)
+							coordinates.translate(move)
 				offset = len(allCoords)
 				allEndPts.extend(e + offset for e in endPts)
 				allCoords.extend(coordinates)


Test patch:
diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py
--- a/Tests/ttLib/tables/_g_l_y_f_test.py
+++ b/Tests/ttLib/tables/_g_l_y_f_test.py
@@ -10,6 +10,8 @@
     GlyphCoordinates,
     GlyphComponent,
     ARGS_ARE_XY_VALUES,
+    SCALED_COMPONENT_OFFSET,
+    UNSCALED_COMPONENT_OFFSET,
     WE_HAVE_A_SCALE,
     WE_HAVE_A_TWO_BY_TWO,
     WE_HAVE_AN_X_AND_Y_SCALE,
@@ -362,6 +364,114 @@ def test_decompile_empty_table(self):
         self.assertEqual(font["glyf"]["space"].numberOfContours, 0)
 
 
+class GlyphTest:
+
+    def test_getCoordinates(self):
+        glyphSet = {}
+        pen = TTGlyphPen(glyphSet)
+        pen.moveTo((0, 0))
+        pen.lineTo((100, 0))
+        pen.lineTo((100, 100))
+        pen.lineTo((0, 100))
+        pen.closePath()
+        # simple contour glyph
+        glyphSet["a"] = a = pen.glyph()
+
+        assert a.getCoordinates(glyphSet) == (
+            GlyphCoordinates([(0, 0), (100, 0), (100, 100), (0, 100)]),
+            [3],
+            array.array("B", [1, 1, 1, 1]),
+        )
+
+        # composite glyph with only XY offset
+        pen = TTGlyphPen(glyphSet)
+        pen.addComponent("a", (1, 0, 0, 1, 10, 20))
+        glyphSet["b"] = b = pen.glyph()
+
+        assert b.getCoordinates(glyphSet) == (
+            GlyphCoordinates([(10, 20), (110, 20), (110, 120), (10, 120)]),
+            [3],
+            array.array("B", [1, 1, 1, 1]),
+        )
+
+        # composite glyph with a scale (and referencing another composite glyph)
+        pen = TTGlyphPen(glyphSet)
+        pen.addComponent("b", (0.5, 0, 0, 0.5, 0, 0))
+        glyphSet["c"] = c = pen.glyph()
+
+        assert c.getCoordinates(glyphSet) == (
+            GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]),
+            [3],
+            array.array("B", [1, 1, 1, 1]),
+        )
+
+        # composite glyph with unscaled offset (MS-style)
+        pen = TTGlyphPen(glyphSet)
+        pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20))
+        glyphSet["d"] = d = pen.glyph()
+        d.components[0].flags |= UNSCALED_COMPONENT_OFFSET
+
+        assert d.getCoordinates(glyphSet) == (
+            GlyphCoordinates([(10, 20), (60, 20), (60, 70), (10, 70)]),
+            [3],
+            array.array("B", [1, 1, 1, 1]),
+        )
+
+        # composite glyph with a scaled offset (Apple-style)
+        pen = TTGlyphPen(glyphSet)
+        pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20))
+        glyphSet["e"] = e = pen.glyph()
+        e.components[0].flags |= SCALED_COMPONENT_OFFSET
+
+        assert e.getCoordinates(glyphSet) == (
+            GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]),
+            [3],
+            array.array("B", [1, 1, 1, 1]),
+        )
+
+        # composite glyph where the 2nd and 3rd components use anchor points
+        pen = TTGlyphPen(glyphSet)
+        pen.addComponent("a", (1, 0, 0, 1, 0, 0))
+        glyphSet["f"] = f = pen.glyph()
+
+        comp1 = GlyphComponent()
+        comp1.glyphName = "a"
+        # aling the new component's pt 0 to pt 2 of contour points added so far
+        comp1.firstPt = 2
+        comp1.secondPt = 0
+        comp1.flags = 0
+        f.components.append(comp1)
+
+        comp2 = GlyphComponent()
+        comp2.glyphName = "a"
+        # aling the new component's pt 0 to pt 6 of contour points added so far
+        comp2.firstPt = 6
+        comp2.secondPt = 0
+        comp2.transform = [[0.707107, 0.707107], [-0.707107, 0.707107]]  # rotate 45 deg
+        comp2.flags = WE_HAVE_A_TWO_BY_TWO
+        f.components.append(comp2)
+
+        coords, end_pts, flags = f.getCoordinates(glyphSet)
+        assert end_pts == [3, 7, 11]
+        assert flags == array.array("B", [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
+        assert list(sum(coords, ())) == pytest.approx(
+            [
+                0, 0,
+                100, 0,
+                100, 100,
+                0, 100,
+                100, 100,
+                200, 100,
+                200, 200,
+                100, 200,
+                200, 200,
+                270.7107, 270.7107,
+                200.0, 341.4214,
+                129.2893, 270.7107,
+            ]
+        )
+
+
 class GlyphComponentTest:
 
     def test_toXML_no_transform(self):
@@ -479,6 +589,29 @@ def test_fromXML_transform_2x2_scale(self):
         ):
             assert value == pytest.approx(expected)
 
+    def test_toXML_reference_points(self):
+        comp = GlyphComponent()
+        comp.glyphName = "a"
+        comp.flags = 0
+        comp.firstPt = 1
+        comp.secondPt = 2
+
+        assert getXML(comp.toXML) == [
+            '<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>'
+        ]
+
+    def test_fromXML_reference_points(self):
+        comp = GlyphComponent()
+        for name, attrs, content in parseXML(
+            ['<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>']
+        ):
+            comp.fromXML(name, attrs, content, ttFont=None)
+
+        assert comp.glyphName == "a"
+        assert comp.flags == 0
+        assert (comp.firstPt, comp.secondPt) == (1, 2)
+        assert not hasattr(comp, "transform")
+
 
 if __name__ == "__main__":
     import sys