Merge branch 'master' into fealib-duplicate-sub-warning

This commit is contained in:
Jens Kutilek 2019-12-03 14:54:03 +01:00
commit bf4fedaa74
16 changed files with 379 additions and 31 deletions

View File

@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
version = __version__ = "4.1.1.dev0"
version = __version__ = "4.2.1.dev0"
__all__ = ["version", "log", "configLogger"]

View File

@ -1,12 +1,13 @@
from fontTools.misc.py23 import *
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import AbstractPointPen
from fontTools.pens.recordingPen import RecordingPen
class _PassThruComponentsMixin(object):
def addComponent(self, glyphName, transformation):
self._outPen.addComponent(glyphName, transformation)
def addComponent(self, glyphName, transformation, **kwargs):
self._outPen.addComponent(glyphName, transformation, **kwargs)
class FilterPen(_PassThruComponentsMixin, AbstractPen):
@ -118,3 +119,41 @@ class ContourFilterPen(_PassThruComponentsMixin, RecordingPen):
Otherwise, the return value is drawn with the output pen.
"""
return # or return contour
class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
""" Baseclass for point pens that apply some transformation to the
coordinates they receive and pass them to another point pen.
You can override any of its methods. The default implementation does
nothing, but passes the commands unmodified to the other pen.
>>> from fontTools.pens.recordingPen import RecordingPointPen
>>> rec = RecordingPointPen()
>>> pen = FilterPointPen(rec)
>>> v = iter(rec.value)
>>> pen.beginPath(identifier="abc")
>>> next(v)
('beginPath', (), {'identifier': 'abc'})
>>> pen.addPoint((1, 2), "line", False)
>>> next(v)
('addPoint', ((1, 2), 'line', False, None), {})
>>> pen.addComponent("a", (2, 0, 0, 2, 10, -10), identifier="0001")
>>> next(v)
('addComponent', ('a', (2, 0, 0, 2, 10, -10)), {'identifier': '0001'})
>>> pen.endPath()
>>> next(v)
('endPath', (), {})
"""
def __init__(self, outPointPen):
self._outPen = outPointPen
def beginPath(self, **kwargs):
self._outPen.beginPath(**kwargs)
def endPath(self):
self._outPen.endPath()
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)

View File

@ -1,9 +1,15 @@
"""Pen recording operations that can be accessed or replayed."""
from fontTools.misc.py23 import *
from fontTools.pens.basePen import AbstractPen, DecomposingPen
from fontTools.pens.pointPen import AbstractPointPen
__all__ = ["replayRecording", "RecordingPen", "DecomposingRecordingPen"]
__all__ = [
"replayRecording",
"RecordingPen",
"DecomposingRecordingPen",
"RecordingPointPen",
]
def replayRecording(recording, pen):
@ -89,6 +95,51 @@ class DecomposingRecordingPen(DecomposingPen, RecordingPen):
skipMissingComponents = False
class RecordingPointPen(AbstractPointPen):
"""PointPen recording operations that can be accessed or replayed.
The recording can be accessed as pen.value; or replayed using
pointPen.replay(otherPointPen).
Usage example:
==============
from defcon import Font
from fontTools.pens.recordingPen import RecordingPointPen
glyph_name = 'a'
font_path = 'MyFont.ufo'
font = Font(font_path)
glyph = font[glyph_name]
pen = RecordingPointPen()
glyph.drawPoints(pen)
print(pen.value)
new_glyph = font.newGlyph('b')
pen.replay(new_glyph.getPointPen())
"""
def __init__(self):
self.value = []
def beginPath(self, **kwargs):
self.value.append(("beginPath", (), kwargs))
def endPath(self):
self.value.append(("endPath", (), {}))
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
def addComponent(self, baseGlyphName, transformation, **kwargs):
self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
def replay(self, pointPen):
for operator, args, kwargs in self.value:
getattr(pointPen, operator)(*args, **kwargs)
if __name__ == "__main__":
from fontTools.pens.basePen import _TestPen
pen = RecordingPen()

View File

@ -0,0 +1,112 @@
from fontTools.misc.fixedTools import otRound
from fontTools.misc.transform import Transform
from fontTools.pens.filterPen import FilterPen, FilterPointPen
__all__ = ["RoundingPen", "RoundingPointPen"]
class RoundingPen(FilterPen):
"""
Filter pen that rounds point coordinates and component XY offsets to integer.
>>> from fontTools.pens.recordingPen import RecordingPen
>>> recpen = RecordingPen()
>>> roundpen = RoundingPen(recpen)
>>> roundpen.moveTo((0.4, 0.6))
>>> roundpen.lineTo((1.6, 2.5))
>>> roundpen.qCurveTo((2.4, 4.6), (3.3, 5.7), (4.9, 6.1))
>>> roundpen.curveTo((6.4, 8.6), (7.3, 9.7), (8.9, 10.1))
>>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
>>> recpen.value == [
... ('moveTo', ((0, 1),)),
... ('lineTo', ((2, 3),)),
... ('qCurveTo', ((2, 5), (3, 6), (5, 6))),
... ('curveTo', ((6, 9), (7, 10), (9, 10))),
... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10))),
... ]
True
"""
def __init__(self, outPen, roundFunc=otRound):
super().__init__(outPen)
self.roundFunc = roundFunc
def moveTo(self, pt):
self._outPen.moveTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
def lineTo(self, pt):
self._outPen.lineTo((self.roundFunc(pt[0]), self.roundFunc(pt[1])))
def curveTo(self, *points):
self._outPen.curveTo(
*((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
)
def qCurveTo(self, *points):
self._outPen.qCurveTo(
*((self.roundFunc(x), self.roundFunc(y)) for x, y in points)
)
def addComponent(self, glyphName, transformation):
self._outPen.addComponent(
glyphName,
Transform(
*transformation[:4],
self.roundFunc(transformation[4]),
self.roundFunc(transformation[5]),
),
)
class RoundingPointPen(FilterPointPen):
"""
Filter point pen that rounds point coordinates and component XY offsets to integer.
>>> from fontTools.pens.recordingPen import RecordingPointPen
>>> recpen = RecordingPointPen()
>>> roundpen = RoundingPointPen(recpen)
>>> roundpen.beginPath()
>>> roundpen.addPoint((0.4, 0.6), 'line')
>>> roundpen.addPoint((1.6, 2.5), 'line')
>>> roundpen.addPoint((2.4, 4.6))
>>> roundpen.addPoint((3.3, 5.7))
>>> roundpen.addPoint((4.9, 6.1), 'qcurve')
>>> roundpen.endPath()
>>> roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5))
>>> recpen.value == [
... ('beginPath', (), {}),
... ('addPoint', ((0, 1), 'line', False, None), {}),
... ('addPoint', ((2, 3), 'line', False, None), {}),
... ('addPoint', ((2, 5), None, False, None), {}),
... ('addPoint', ((3, 6), None, False, None), {}),
... ('addPoint', ((5, 6), 'qcurve', False, None), {}),
... ('endPath', (), {}),
... ('addComponent', ('a', (1.5, 0, 0, 1.5, 11, -10)), {}),
... ]
True
"""
def __init__(self, outPen, roundFunc=otRound):
super().__init__(outPen)
self.roundFunc = roundFunc
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._outPen.addPoint(
(self.roundFunc(pt[0]), self.roundFunc(pt[1])),
segmentType=segmentType,
smooth=smooth,
name=name,
**kwargs,
)
def addComponent(self, baseGlyphName, transformation, **kwargs):
self._outPen.addComponent(
baseGlyphName,
Transform(
*transformation[:4],
self.roundFunc(transformation[4]),
self.roundFunc(transformation[5]),
),
**kwargs,
)

View File

@ -1,5 +1,5 @@
from fontTools.misc.py23 import *
from fontTools.pens.filterPen import FilterPen
from fontTools.pens.filterPen import FilterPen, FilterPointPen
__all__ = ["TransformPen"]
@ -55,6 +55,51 @@ class TransformPen(FilterPen):
self._outPen.addComponent(glyphName, transformation)
class TransformPointPen(FilterPointPen):
"""PointPen that transforms all coordinates using a Affine transformation,
and passes them to another PointPen.
>>> from fontTools.pens.recordingPen import RecordingPointPen
>>> rec = RecordingPointPen()
>>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5))
>>> v = iter(rec.value)
>>> pen.beginPath(identifier="contour-0")
>>> next(v)
('beginPath', (), {'identifier': 'contour-0'})
>>> pen.addPoint((100, 100), "line")
>>> next(v)
('addPoint', ((190, 205), 'line', False, None), {})
>>> pen.endPath()
>>> next(v)
('endPath', (), {})
>>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0")
>>> next(v)
('addComponent', ('a', <Transform [2 0 0 2 -30 15]>), {'identifier': 'component-0'})
"""
def __init__(self, outPointPen, transformation):
"""The 'outPointPen' argument is another point pen object.
It will receive the transformed coordinates.
The 'transformation' argument can either be a six-tuple, or a
fontTools.misc.transform.Transform object.
"""
super().__init__(outPointPen)
if not hasattr(transformation, "transformPoint"):
from fontTools.misc.transform import Transform
transformation = Transform(*transformation)
self._transformation = transformation
self._transformPoint = transformation.transformPoint
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._outPen.addPoint(
self._transformPoint(pt), segmentType, smooth, name, **kwargs
)
def addComponent(self, baseGlyphName, transformation, **kwargs):
transformation = self._transformation.transform(transformation)
self._outPen.addComponent(baseGlyphName, transformation, **kwargs)
if __name__ == "__main__":
from fontTools.pens.basePen import _TestPen
pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0))

View File

@ -1,6 +1,6 @@
from fontTools.misc.py23 import *
from array import array
from fontTools.misc.fixedTools import MAX_F2DOT14
from fontTools.misc.fixedTools import MAX_F2DOT14, otRound
from fontTools.pens.basePen import LoggingPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib.tables import ttProgram
@ -118,7 +118,7 @@ class TTGlyphPen(LoggingPen):
component = GlyphComponent()
component.glyphName = glyphName
component.x, component.y = transformation[4:]
component.x, component.y = (otRound(v) for v in transformation[4:])
transformation = transformation[:4]
if transformation != (1, 0, 0, 1):
if (self.handleOverflowingTransforms and
@ -138,6 +138,7 @@ class TTGlyphPen(LoggingPen):
glyph = Glyph()
glyph.coordinates = GlyphCoordinates(self.points)
glyph.coordinates.toInt()
glyph.endPtsOfContours = self.endPts
glyph.flags = array("B", self.types)
self.init()

View File

@ -339,7 +339,8 @@ class UFOReader(_UFOBaseIO):
# convert kerning and groups
kerning, groups, conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning(
self._upConvertedKerningData["originalKerning"],
deepcopy(self._upConvertedKerningData["originalGroups"])
deepcopy(self._upConvertedKerningData["originalGroups"]),
self.getGlyphSet()
)
# store
self._upConvertedKerningData["kerning"] = kerning
@ -637,7 +638,7 @@ class UFOReader(_UFOBaseIO):
``validateRead`` will validate the read data, by default it is set to the
class's validate value, can be overridden.
``validateWrte`` will validate the written data, by default it is set to the
``validateWrite`` will validate the written data, by default it is set to the
class's validate value, can be overridden.
"""
from fontTools.ufoLib.glifLib import GlyphSet

View File

@ -6,16 +6,16 @@ Conversion functions.
# adapted from the UFO spec
def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups):
def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups, glyphSet=()):
# gather known kerning groups based on the prefixes
firstReferencedGroups, secondReferencedGroups = findKnownKerningGroups(groups)
# Make lists of groups referenced in kerning pairs.
for first, seconds in list(kerning.items()):
if first in groups:
if first in groups and first not in glyphSet:
if not first.startswith("public.kern1."):
firstReferencedGroups.add(first)
for second in list(seconds.keys()):
if second in groups:
if second in groups and second not in glyphSet:
if not second.startswith("public.kern2."):
secondReferencedGroups.add(second)
# Create new names for these groups.
@ -154,7 +154,7 @@ def test():
... "DGroup" : ["D"],
... }
>>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning(
... testKerning, testGroups)
... testKerning, testGroups, [])
>>> expected = {
... "A" : {
... "A": 1,
@ -220,7 +220,7 @@ def test():
... "@MMK_R_XGroup" : ["X"],
... }
>>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning(
... testKerning, testGroups)
... testKerning, testGroups, [])
>>> expected = {
... "A" : {
... "A": 1,
@ -293,7 +293,7 @@ def test():
... "DGroup" : ["D"],
... }
>>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning(
... testKerning, testGroups)
... testKerning, testGroups, [])
>>> expected = {
... "A" : {
... "A": 1,

View File

@ -883,7 +883,7 @@ def groupsValidator(value):
return False, "A group has an empty name."
if groupName.startswith("public."):
if not groupName.startswith("public.kern1.") and not groupName.startswith("public.kern2."):
# unknown pubic.* name. silently skip.
# unknown public.* name. silently skip.
continue
else:
if len("public.kernN.") == len(groupName):

View File

@ -795,7 +795,7 @@ def set_default_weight_width_slant(font, location):
if "wght" in location:
weight_class = otRound(max(1, min(location["wght"], 1000)))
if font["OS/2"].usWeightClass != weight_class:
log.info("Setting OS/2.usWidthClass = %s", weight_class)
log.info("Setting OS/2.usWeightClass = %s", weight_class)
font["OS/2"].usWeightClass = weight_class
if "wdth" in location:

View File

@ -1,3 +1,21 @@
4.2.0 (released 2019-11-28)
---------------------------
- [pens] Added the following pens:
* ``roundingPen.RoundingPen``: filter pen that rounds coordinates and components'
offsets to integer;
* ``roundingPen.RoundingPointPen``: like the above, but using PointPen protocol.
* ``filterPen.FilterPointPen``: base class for filter point pens;
* ``transformPen.TransformPointPen``: filter point pen to apply affine transform;
* ``recordingPen.RecordingPointPen``: records and replays point-pen commands.
- [ttGlyphPen] Always round float coordinates and component offsets to integers
(#1763).
- [ufoLib] When converting kerning groups from UFO2 to UFO3, avoid confusing
groups with the same name as one of the glyphs (#1761, #1762,
unified-font-object/ufo-spec#98).
4.1.0 (released 2019-11-18)
---------------------------

View File

@ -1,19 +1,29 @@
from fontTools.misc.py23 import *
from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen
from fontTools.pens.recordingPen import (
RecordingPen,
DecomposingRecordingPen,
RecordingPointPen,
)
import pytest
class _TestGlyph(object):
def draw(self, pen):
pen.moveTo((0.0, 0.0))
pen.lineTo((0.0, 100.0))
pen.curveTo((50.0, 75.0), (60.0, 50.0), (50.0, 0.0))
pen.closePath()
def drawPoints(self, pen):
pen.beginPath(identifier="abc")
pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
pen.addPoint((0.0, 100.0), "line", False, None, identifier="0001")
pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
pen.endPath()
class RecordingPenTest(object):
def test_addComponent(self):
pen = RecordingPen()
pen.addComponent("a", (2, 0, 0, 3, -10, 5))
@ -21,18 +31,42 @@ class RecordingPenTest(object):
class DecomposingRecordingPenTest(object):
def test_addComponent_decomposed(self):
pen = DecomposingRecordingPen({"a": _TestGlyph()})
pen.addComponent("a", (2, 0, 0, 3, -10, 5))
assert pen.value == [
('moveTo', ((-10.0, 5.0),)),
('lineTo', ((-10.0, 305.0),)),
('curveTo', ((90.0, 230.0), (110.0, 155.0), (90.0, 5.0),)),
('closePath', ())]
("moveTo", ((-10.0, 5.0),)),
("lineTo", ((-10.0, 305.0),)),
("curveTo", ((90.0, 230.0), (110.0, 155.0), (90.0, 5.0))),
("closePath", ()),
]
def test_addComponent_missing_raises(self):
pen = DecomposingRecordingPen(dict())
with pytest.raises(KeyError) as excinfo:
pen.addComponent("a", (1, 0, 0, 1, 0, 0))
assert excinfo.value.args[0] == "a"
class RecordingPointPenTest:
def test_record_and_replay(self):
pen = RecordingPointPen()
glyph = _TestGlyph()
glyph.drawPoints(pen)
pen.addComponent("a", (2, 0, 0, 2, -10, 5))
assert pen.value == [
("beginPath", (), {"identifier": "abc"}),
("addPoint", ((0.0, 0.0), "line", False, "start"), {"identifier": "0000"}),
("addPoint", ((0.0, 100.0), "line", False, None), {"identifier": "0001"}),
("addPoint", ((50.0, 75.0), None, False, None), {"identifier": "0002"}),
("addPoint", ((60.0, 50.0), None, False, None), {"identifier": "0003"}),
("addPoint", ((50.0, 0.0), "curve", True, "last"), {"identifier": "0004"}),
("endPath", (), {}),
("addComponent", ("a", (2, 0, 0, 2, -10, 5)), {}),
]
pen2 = RecordingPointPen()
pen.replay(pen2)
assert pen2.value == pen.value

View File

@ -239,6 +239,31 @@ class TTGlyphPenTest(TestCase):
with self.assertRaises(struct.error):
compositeGlyph.compile({'a': baseGlyph})
def assertGlyphBoundsEqual(self, glyph, bounds):
self.assertEqual((glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax), bounds)
def test_round_float_coordinates_and_component_offsets(self):
glyphSet = {}
pen = TTGlyphPen(glyphSet)
pen.moveTo((0, 0))
pen.lineTo((0, 1))
pen.lineTo((367.6, 0))
pen.closePath()
simpleGlyph = pen.glyph()
simpleGlyph.recalcBounds(glyphSet)
self.assertGlyphBoundsEqual(simpleGlyph, (0, 0, 368, 1))
componentName = 'a'
glyphSet[componentName] = simpleGlyph
pen.addComponent(componentName, (1, 0, 0, 1, -86.4, 0))
compositeGlyph = pen.glyph()
compositeGlyph.recalcBounds(glyphSet)
self.assertGlyphBoundsEqual(compositeGlyph, (-86, 0, 282, 1))
class _TestGlyph(object):
def __init__(self, glyph):

View File

@ -137,7 +137,9 @@ class KerningUpConversionTestCase(unittest.TestCase):
("A", "public.kern2.CGroup"): 3,
("A", "public.kern2.DGroup"): 4,
("A", "A"): 1,
("A", "B"): 2
("A", "B"): 2,
("X", "A"): 13,
("X", "public.kern2.CGroup"): 14
}
expectedGroups = {
@ -148,7 +150,8 @@ class KerningUpConversionTestCase(unittest.TestCase):
"public.kern1.CGroup": ["C", "Ccedilla"],
"public.kern2.CGroup": ["C", "Ccedilla"],
"public.kern2.DGroup": ["D"],
"Not A Kerning Group" : ["A"]
"Not A Kerning Group" : ["A"],
"X": ["X", "X.sc"]
}
def setUp(self):
@ -163,6 +166,20 @@ class KerningUpConversionTestCase(unittest.TestCase):
self.clearUFO()
if not os.path.exists(self.ufoPath):
os.mkdir(self.ufoPath)
# glyphs
glyphsPath = os.path.join(self.ufoPath, "glyphs")
if not os.path.exists(glyphsPath):
os.mkdir(glyphsPath)
glyphFile = "X_.glif"
glyphsContents = dict(X=glyphFile)
path = os.path.join(glyphsPath, "contents.plist")
with open(path, "wb") as f:
plistlib.dump(glyphsContents, f)
path = os.path.join(glyphsPath, glyphFile)
with open(path, "w") as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
# metainfo.plist
metaInfo = dict(creator="test", formatVersion=formatVersion)
path = os.path.join(self.ufoPath, "metainfo.plist")
@ -187,6 +204,10 @@ class KerningUpConversionTestCase(unittest.TestCase):
"B" : 10,
"CGroup" : 11,
"DGroup" : 12
},
"X": {
"A" : 13,
"CGroup" : 14
}
}
path = os.path.join(self.ufoPath, "kerning.plist")
@ -197,7 +218,8 @@ class KerningUpConversionTestCase(unittest.TestCase):
"BGroup" : ["B"],
"CGroup" : ["C", "Ccedilla"],
"DGroup" : ["D"],
"Not A Kerning Group" : ["A"]
"Not A Kerning Group" : ["A"],
"X" : ["X", "X.sc"] # a group with a name that is also a glyph name
}
path = os.path.join(self.ufoPath, "groups.plist")
with open(path, "wb") as f:

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.1.1.dev0
current_version = 4.2.1.dev0
commit = True
tag = False
tag_name = {new_version}

View File

@ -345,7 +345,7 @@ def find_data_files(manpath="share/man"):
setup(
name="fonttools",
version="4.1.1.dev0",
version="4.2.1.dev0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",