Speeds up font compilation by around 200%

Cython is used to compile some hot paths into native Python extensions.
These hot paths were identified through running ufocompile with the hotshot
profiler and then converting file by file to Cython, starting with the "hottest"
paths and continuing until returns were deminishing. This means that only a few
Python files were converted to Cython.

Closes #23
Closes #20 (really this time)
This commit is contained in:
Rasmus Andersson 2017-09-03 23:03:17 -04:00
parent 31ae014e0c
commit 8234b62ab7
108 changed files with 26933 additions and 110 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.pyc
*.pyo
*.so
*.ttx
*.o
*.d

View file

@ -30,6 +30,7 @@ build/dist-unhinted/Interface-%.ttf: build/tmp/InterfaceTTF/Interface-%.ttf
# OTF
build/dist-unhinted/Interface-%.otf: build/tmp/InterfaceOTF/Interface-%.otf
@mkdir -p build/dist-unhinted
cp -a "$<" "$@"
build/dist:

34
init.sh
View file

@ -142,6 +142,35 @@ else
ln -vfs ../../../misc/ttf2woff/ttf2woff "$VENV_DIR/bin"
fi
has_newer() {
DIR=$1
REF_FILE=$2
for f in $(find "$DIR" -type f -name '*.pyx' -newer "$REF_FILE" -print -quit); do
return 0
done
return 1
}
check_cython_dep() {
DIR=$1
REF_FILE=$DIR/$2
set -e
if [ ! -f "$REF_FILE" ] || has_newer "$DIR" "$REF_FILE"; then
pushd "$DIR" >/dev/null
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
python setup.py build_ext --inplace
popd >/dev/null
fi
}
# native booleanOperations module
check_cython_dep misc/pylib/booleanOperations flatten.so
check_cython_dep misc/pylib/copy copy.so
check_cython_dep misc/pylib/fontbuild mix.so
check_cython_dep misc/pylib/robofab glifLib.so
# ————————————————————————————————————————————————————————————————————————————————————————————————
# $BUILD_TMP_DIR
# create and mount spare disk image needed on macOS to support case-sensitive filenames
@ -179,10 +208,9 @@ else
if $NEED_GENERATE; then
break
fi
for srcfile in $(find src/Interface-${style}.ufo -type f -newer "$GEN_MAKE_FILE"); do
if has_newer "src/Interface-${style}.ufo" "$GEN_MAKE_FILE"; then
NEED_GENERATE=true
break
done
fi
done
fi

View file

@ -0,0 +1,2 @@
*.c
build

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Frederik Berlaen
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,11 @@
from __future__ import print_function, division, absolute_import
from .booleanOperationManager import BooleanOperationManager
from .exceptions import BooleanOperationsError
from .version import __version__
# export BooleanOperationManager static methods
union = BooleanOperationManager.union
difference = BooleanOperationManager.difference
intersection = BooleanOperationManager.intersection
xor = BooleanOperationManager.xor
getIntersections = BooleanOperationManager.getIntersections

View file

@ -0,0 +1,257 @@
from __future__ import print_function, division, absolute_import
import weakref
from copy import deepcopy
try:
from robofab.pens.pointPen import AbstractPointPen
from robofab.pens.adapterPens import PointToSegmentPen, SegmentToPointPen
from robofab.pens.boundsPen import BoundsPen
except:
from ufoLib.pointPen import (
AbstractPointPen, PointToSegmentPen, SegmentToPointPen)
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.areaPen import AreaPen
from .booleanOperationManager import BooleanOperationManager
manager = BooleanOperationManager()
class BooleanGlyphDataPointPen(AbstractPointPen):
def __init__(self, glyph):
self._glyph = glyph
self._points = []
self.copyContourData = True
def _flushContour(self):
points = self._points
if len(points) == 1 and points[0][0] == "move":
# it's an anchor
segmentType, pt, smooth, name = points[0]
self._glyph.anchors.append((pt, name))
elif self.copyContourData:
# ignore double points on start and end
firstPoint = points[0]
if firstPoint[0] == "move":
# remove trailing off curves in an open path
while points[-1][0] is None:
points.pop()
lastPoint = points[-1]
if firstPoint[0] is not None and lastPoint[0] is not None:
if firstPoint[1] == lastPoint[1]:
if firstPoint[0] in ("line", "move"):
del points[0]
else:
raise AssertionError("Unhandled point type sequence")
elif firstPoint[0] == "move":
# auto close the path
_, pt, smooth, name = firstPoint
points[0] = "line", pt, smooth, name
contour = self._glyph.contourClass()
contour._points = points
self._glyph.contours.append(contour)
def beginPath(self):
self._points = []
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._points.append((segmentType, pt, smooth, name))
def endPath(self):
self._flushContour()
def addComponent(self, baseGlyphName, transformation):
self._glyph.components.append((baseGlyphName, transformation))
class BooleanContour(object):
"""
Contour like object.
"""
def __init__(self):
self._points = []
self._clockwise = None
self._bounds = None
def __len__(self):
return len(self._points)
# shallow contour API
def draw(self, pen):
pointPen = PointToSegmentPen(pen)
self.drawPoints(pointPen)
def drawPoints(self, pointPen):
pointPen.beginPath()
for segmentType, pt, smooth, name in self._points:
pointPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name)
pointPen.endPath()
def _get_clockwise(self):
if self._clockwise is None:
pen = AreaPen()
pen.endPath = pen.closePath
self.draw(pen)
self._clockwise = pen.value < 0
return self._clockwise
clockwise = property(_get_clockwise)
def _get_bounds(self):
if self._bounds is None:
pen = BoundsPen(None)
self.draw(pen)
self._bounds = pen.bounds
return self._bounds
bounds = property(_get_bounds)
class BooleanGlyph(object):
"""
Glyph like object handling boolean operations.
union:
result = BooleanGlyph(glyph).union(BooleanGlyph(glyph2))
result = BooleanGlyph(glyph) | BooleanGlyph(glyph2)
difference:
result = BooleanGlyph(glyph).difference(BooleanGlyph(glyph2))
result = BooleanGlyph(glyph) % BooleanGlyph(glyph2)
intersection:
result = BooleanGlyph(glyph).intersection(BooleanGlyph(glyph2))
result = BooleanGlyph(glyph) & BooleanGlyph(glyph2)
xor:
result = BooleanGlyph(glyph).xor(BooleanGlyph(glyph2))
result = BooleanGlyph(glyph) ^ BooleanGlyph(glyph2)
"""
contourClass = BooleanContour
def __init__(self, glyph=None, copyContourData=True):
self.contours = []
self.components = []
self.anchors = []
self.name = None
self.unicodes = None
self.width = None
self.lib = {}
self.note = None
if glyph:
pen = self.getPointPen()
pen.copyContourData = copyContourData
glyph.drawPoints(pen)
self.name = glyph.name
self.unicodes = glyph.unicodes
self.width = glyph.width
self.lib = deepcopy(glyph.lib)
self.note = glyph.note
if not isinstance(glyph, self.__class__):
self.getSourceGlyph = weakref.ref(glyph)
def __repr__(self):
return "<BooleanGlyph %s>" % self.name
def __len__(self):
return len(self.contours)
def __getitem__(self, index):
return self.contours[index]
def getSourceGlyph(self):
return None
def getParent(self):
source = self.getSourceGlyph()
if source:
return source.getParent()
return None
# shalllow glyph API
def draw(self, pen):
pointPen = PointToSegmentPen(pen)
self.drawPoints(pointPen)
def drawPoints(self, pointPen):
for contour in self.contours:
contour.drawPoints(pointPen)
for baseName, transformation in self.components:
pointPen.addComponent(baseName, transformation)
for pt, name in self.anchors:
pointPen.beginPath()
pointPen.addPoint(pt=pt, segmentType="move", smooth=False, name=name)
pointPen.endPath()
def getPen(self):
return SegmentToPointPen(self.getPointPen())
def getPointPen(self):
return BooleanGlyphDataPointPen(self)
# boolean operations
def _booleanMath(self, operation, other):
if not isinstance(other, self.__class__):
other = self.__class__(other)
destination = self.__class__(self, copyContourData=False)
func = getattr(manager, operation)
if operation == "union":
contours = self.contours
if other is not None:
contours += other.contours
func(contours, destination.getPointPen())
else:
subjectContours = self.contours
clipContours = other.contours
func(subjectContours, clipContours, destination.getPointPen())
return destination
def __or__(self, other):
return self.union(other)
__ror__ = __ior__ = __or__
def __mod__(self, other):
return self.difference(other)
__rmod__ = __imod__ = __mod__
def __and__(self, other):
return self.intersection(other)
__rand__ = __iand__ = __and__
def __xor__(self, other):
return self.xor(other)
__rxor__ = __ixor__ = __xor__
def union(self, other):
return self._booleanMath("union", other)
def difference(self, other):
return self._booleanMath("difference", other)
def intersection(self, other):
return self._booleanMath("intersection", other)
def xor(self, other):
return self._booleanMath("xor", other)
def removeOverlap(self):
return self._booleanMath("union", None)

View file

@ -0,0 +1,137 @@
from __future__ import print_function, division, absolute_import
from .flatten import InputContour, OutputContour
from .exceptions import (
InvalidSubjectContourError, InvalidClippingContourError, ExecutionError)
import pyclipper
"""
General Suggestions:
- Contours should only be sent here if they actually overlap.
This can be checked easily using contour bounds.
- Only perform operations on closed contours.
- contours must have an on curve point
- some kind of a log
"""
_operationMap = {
"union": pyclipper.CT_UNION,
"intersection": pyclipper.CT_INTERSECTION,
"difference": pyclipper.CT_DIFFERENCE,
"xor": pyclipper.CT_XOR,
}
_fillTypeMap = {
"evenOdd": pyclipper.PFT_EVENODD,
"nonZero": pyclipper.PFT_NONZERO,
# we keep the misspelling for compatibility with earlier versions
"noneZero": pyclipper.PFT_NONZERO,
}
def clipExecute(subjectContours, clipContours, operation, subjectFillType="nonZero",
clipFillType="nonZero"):
pc = pyclipper.Pyclipper()
for i, subjectContour in enumerate(subjectContours):
# ignore paths with no area
if pyclipper.Area(subjectContour):
try:
pc.AddPath(subjectContour, pyclipper.PT_SUBJECT)
except pyclipper.ClipperException:
raise InvalidSubjectContourError("contour %d is invalid for clipping" % i)
for j, clipContour in enumerate(clipContours):
# ignore paths with no area
if pyclipper.Area(clipContour):
try:
pc.AddPath(clipContour, pyclipper.PT_CLIP)
except pyclipper.ClipperException:
raise InvalidClippingContourError("contour %d is invalid for clipping" % j)
try:
solution = pc.Execute(_operationMap[operation],
_fillTypeMap[subjectFillType],
_fillTypeMap[clipFillType])
except pyclipper.ClipperException as exc:
raise ExecutionError(exc)
return [[tuple(p) for p in path] for path in solution]
def _performOperation(operation, subjectContours, clipContours, outPen):
# prep the contours
subjectInputContours = [InputContour(contour) for contour in subjectContours if contour and len(contour) > 1]
clipInputContours = [InputContour(contour) for contour in clipContours if contour and len(contour) > 1]
inputContours = subjectInputContours + clipInputContours
resultContours = clipExecute([subjectInputContour.originalFlat for subjectInputContour in subjectInputContours],
[clipInputContour.originalFlat for clipInputContour in clipInputContours],
operation, subjectFillType="nonZero", clipFillType="nonZero")
# convert to output contours
outputContours = [OutputContour(contour) for contour in resultContours]
# re-curve entire contour
for inputContour in inputContours:
for outputContour in outputContours:
if outputContour.final:
continue
if outputContour.reCurveFromEntireInputContour(inputContour):
# the input is expired if a match was made,
# so stop passing it to the outputs
break
# re-curve segments
for inputContour in inputContours:
# skip contours that were comppletely used in the previous step
if inputContour.used:
continue
# XXX this could be expensive if an input becomes completely used
# it doesn't stop from being passed to the output
for outputContour in outputContours:
outputContour.reCurveFromInputContourSegments(inputContour)
# curve fit
for outputContour in outputContours:
outputContour.reCurveSubSegments(inputContours)
# output the results
for outputContour in outputContours:
outputContour.drawPoints(outPen)
return outputContours
class BooleanOperationManager(object):
@staticmethod
def union(contours, outPen):
return _performOperation("union", contours, [], outPen)
@staticmethod
def difference(subjectContours, clipContours, outPen):
return _performOperation("difference", subjectContours, clipContours, outPen)
@staticmethod
def intersection(subjectContours, clipContours, outPen):
return _performOperation("intersection", subjectContours, clipContours, outPen)
@staticmethod
def xor(subjectContours, clipContours, outPen):
return _performOperation("xor", subjectContours, clipContours, outPen)
@staticmethod
def getIntersections(contours):
from .flatten import _scalePoints, inverseClipperScale
# prep the contours
inputContours = [InputContour(contour) for contour in contours if contour and len(contour) > 1]
inputFlatPoints = set()
for contour in inputContours:
inputFlatPoints.update(contour.originalFlat)
resultContours = clipExecute(
[inputContour.originalFlat for inputContour in inputContours], [],
"union", subjectFillType="nonZero", clipFillType="nonZero")
resultFlatPoints = set()
for contour in resultContours:
resultFlatPoints.update(contour)
intersections = resultFlatPoints - inputFlatPoints
return _scalePoints(intersections, inverseClipperScale)

View file

@ -0,0 +1,21 @@
from __future__ import print_function, division, absolute_import
class BooleanOperationsError(Exception):
"""Base BooleanOperations exception"""
class InvalidContourError(BooleanOperationsError):
"""Rased when any input contour is invalid"""
class InvalidSubjectContourError(InvalidContourError):
"""Rased when a 'subject' contour is not valid"""
class InvalidClippingContourError(InvalidContourError):
"""Rased when a 'clipping' contour is not valid"""
class ExecutionError(BooleanOperationsError):
"""Raised when clipping execution fails"""

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
pyclipper==1.0.5
fonttools==3.1.2
ufoLib==2.0.0

View file

@ -0,0 +1,15 @@
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [
Extension("booleanGlyph", ["booleanGlyph.pyx"]),
Extension("booleanOperationManager", ["booleanOperationManager.pyx"]),
Extension("flatten", ["flatten.pyx"]),
]
setup(
name = 'booleanOperations',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)

View file

@ -0,0 +1,4 @@
try:
__version__ = __import__('pkg_resources').require('booleanOperations')[0].version
except Exception:
__version__ = 'unknown'

2
misc/pylib/copy/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.c
build

254
misc/pylib/copy/LICENSE.txt Normal file
View file

@ -0,0 +1,254 @@
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see http://www.opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights
Reserved" are retained in Python alone or in any derivative version prepared by
Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the Internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the Internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View file

@ -0,0 +1,2 @@
from __future__ import absolute_import
from .copy import copy, deepcopy, Error

433
misc/pylib/copy/copy.pyx Normal file
View file

@ -0,0 +1,433 @@
"""Generic (shallow and deep) copying operations.
Interface summary:
import copy
x = copy.copy(y) # make a shallow copy of y
x = copy.deepcopy(y) # make a deep copy of y
For module specific errors, copy.Error is raised.
The difference between shallow and deep copying is only relevant for
compound objects (objects that contain other objects, like lists or
class instances).
- A shallow copy constructs a new compound object and then (to the
extent possible) inserts *the same objects* into it that the
original contains.
- A deep copy constructs a new compound object and then, recursively,
inserts *copies* into it of the objects found in the original.
Two problems often exist with deep copy operations that don't exist
with shallow copy operations:
a) recursive objects (compound objects that, directly or indirectly,
contain a reference to themselves) may cause a recursive loop
b) because deep copy copies *everything* it may copy too much, e.g.
administrative data structures that should be shared even between
copies
Python's deep copy operation avoids these problems by:
a) keeping a table of objects already copied during the current
copying pass
b) letting user-defined classes override the copying operation or the
set of components copied
This version does not copy types like module, class, function, method,
nor stack trace, stack frame, nor file, socket, window, nor array, nor
any similar types.
Classes can use the same interfaces to control copying that they use
to control pickling: they can define methods called __getinitargs__(),
__getstate__() and __setstate__(). See the documentation for module
"pickle" for information on these methods.
"""
import types
import weakref
from copy_reg import dispatch_table
class Error(Exception):
pass
error = Error # backward compatibility
try:
from org.python.core import PyStringMap
except ImportError:
PyStringMap = None
__all__ = ["Error", "copy", "deepcopy"]
def copy(x):
"""Shallow copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
cls = type(x)
copier = _copy_dispatch.get(cls)
if copier:
return copier(x)
copier = getattr(cls, "__copy__", None)
if copier:
return copier(x)
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(2)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
return _reconstruct(x, rv, 0)
_copy_dispatch = d = {}
def _copy_immutable(x):
return x
for t in (type(None), int, long, float, bool, str, tuple,
frozenset, type, xrange, types.ClassType,
types.BuiltinFunctionType, type(Ellipsis),
types.FunctionType, weakref.ref):
d[t] = _copy_immutable
for name in ("ComplexType", "UnicodeType", "CodeType"):
t = getattr(types, name, None)
if t is not None:
d[t] = _copy_immutable
def _copy_with_constructor(x):
return type(x)(x)
for t in (list, dict, set):
d[t] = _copy_with_constructor
def _copy_with_copy_method(x):
return x.copy()
if PyStringMap is not None:
d[PyStringMap] = _copy_with_copy_method
def _copy_inst(x):
if hasattr(x, '__copy__'):
return x.__copy__()
if hasattr(x, '__getinitargs__'):
args = x.__getinitargs__()
y = x.__class__(*args)
else:
y = _EmptyClass()
y.__class__ = x.__class__
if hasattr(x, '__getstate__'):
state = x.__getstate__()
else:
state = x.__dict__
if hasattr(y, '__setstate__'):
y.__setstate__(state)
else:
y.__dict__.update(state)
return y
d[types.InstanceType] = _copy_inst
del d
def deepcopy(x, memo=None, _nil=[]):
"""Deep copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
if memo is None:
memo = {}
d = id(x)
y = memo.get(d, _nil)
if y is not _nil:
return y
cls = type(x)
copier = _deepcopy_dispatch.get(cls)
if copier:
y = copier(x, memo)
else:
try:
issc = issubclass(cls, type)
except TypeError: # cls is not a class (old Boost; see SF #502085)
issc = 0
if issc:
y = _deepcopy_atomic(x, memo)
else:
copier = getattr(x, "__deepcopy__", None)
if copier:
y = copier(memo)
else:
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(2)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error(
"un(deep)copyable object of type %s" % cls)
y = _reconstruct(x, rv, 1, memo)
memo[d] = y
_keep_alive(x, memo) # Make sure x lives at least as long as d
return y
_deepcopy_dispatch = d = {}
def _deepcopy_atomic(x, memo):
return x
d[type(None)] = _deepcopy_atomic
d[type(Ellipsis)] = _deepcopy_atomic
d[int] = _deepcopy_atomic
d[long] = _deepcopy_atomic
d[float] = _deepcopy_atomic
d[bool] = _deepcopy_atomic
try:
d[complex] = _deepcopy_atomic
except NameError:
pass
d[str] = _deepcopy_atomic
try:
d[unicode] = _deepcopy_atomic
except NameError:
pass
try:
d[types.CodeType] = _deepcopy_atomic
except AttributeError:
pass
d[type] = _deepcopy_atomic
d[xrange] = _deepcopy_atomic
d[types.ClassType] = _deepcopy_atomic
d[types.BuiltinFunctionType] = _deepcopy_atomic
d[types.FunctionType] = _deepcopy_atomic
d[weakref.ref] = _deepcopy_atomic
def _deepcopy_list(x, memo):
y = []
memo[id(x)] = y
for a in x:
y.append(deepcopy(a, memo))
return y
d[list] = _deepcopy_list
def _deepcopy_tuple(x, memo):
y = []
for a in x:
y.append(deepcopy(a, memo))
d = id(x)
try:
return memo[d]
except KeyError:
pass
for i in range(len(x)):
if x[i] is not y[i]:
y = tuple(y)
break
else:
y = x
memo[d] = y
return y
d[tuple] = _deepcopy_tuple
def _deepcopy_dict(x, memo):
y = {}
memo[id(x)] = y
for key, value in x.iteritems():
y[deepcopy(key, memo)] = deepcopy(value, memo)
return y
d[dict] = _deepcopy_dict
if PyStringMap is not None:
d[PyStringMap] = _deepcopy_dict
def _deepcopy_method(x, memo): # Copy instance methods
return type(x)(x.im_func, deepcopy(x.im_self, memo), x.im_class)
_deepcopy_dispatch[types.MethodType] = _deepcopy_method
def _keep_alive(x, memo):
"""Keeps a reference to the object x in the memo.
Because we remember objects by their id, we have
to assure that possibly temporary objects are kept
alive by referencing them.
We store a reference at the id of the memo, which should
normally not be used unless someone tries to deepcopy
the memo itself...
"""
try:
memo[id(memo)].append(x)
except KeyError:
# aha, this is the first one :-)
memo[id(memo)]=[x]
def _deepcopy_inst(x, memo):
if hasattr(x, '__deepcopy__'):
return x.__deepcopy__(memo)
if hasattr(x, '__getinitargs__'):
args = x.__getinitargs__()
args = deepcopy(args, memo)
y = x.__class__(*args)
else:
y = _EmptyClass()
y.__class__ = x.__class__
memo[id(x)] = y
if hasattr(x, '__getstate__'):
state = x.__getstate__()
else:
state = x.__dict__
state = deepcopy(state, memo)
if hasattr(y, '__setstate__'):
y.__setstate__(state)
else:
y.__dict__.update(state)
return y
d[types.InstanceType] = _deepcopy_inst
def _reconstruct(x, info, deep, memo=None):
if isinstance(info, str):
return x
assert isinstance(info, tuple)
if memo is None:
memo = {}
n = len(info)
assert n in (2, 3, 4, 5)
callable, args = info[:2]
if n > 2:
state = info[2]
else:
state = None
if n > 3:
listiter = info[3]
else:
listiter = None
if n > 4:
dictiter = info[4]
else:
dictiter = None
if deep:
args = deepcopy(args, memo)
y = callable(*args)
memo[id(x)] = y
if state is not None:
if deep:
state = deepcopy(state, memo)
if hasattr(y, '__setstate__'):
y.__setstate__(state)
else:
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
else:
slotstate = None
if state is not None:
y.__dict__.update(state)
if slotstate is not None:
for key, value in slotstate.iteritems():
setattr(y, key, value)
if listiter is not None:
for item in listiter:
if deep:
item = deepcopy(item, memo)
y.append(item)
if dictiter is not None:
for key, value in dictiter:
if deep:
key = deepcopy(key, memo)
value = deepcopy(value, memo)
y[key] = value
return y
del d
del types
# Helper for instance creation without calling __init__
class _EmptyClass:
pass
def _test():
l = [None, 1, 2L, 3.14, 'xyzzy', (1, 2L), [3.14, 'abc'],
{'abc': 'ABC'}, (), [], {}]
l1 = copy(l)
print l1==l
l1 = map(copy, l)
print l1==l
l1 = deepcopy(l)
print l1==l
class C:
def __init__(self, arg=None):
self.a = 1
self.arg = arg
if __name__ == '__main__':
import sys
file = sys.argv[0]
else:
file = __file__
self.fp = open(file)
self.fp.close()
def __getstate__(self):
return {'a': self.a, 'arg': self.arg}
def __setstate__(self, state):
for key, value in state.iteritems():
setattr(self, key, value)
def __deepcopy__(self, memo=None):
new = self.__class__(deepcopy(self.arg, memo))
new.a = self.a
return new
c = C('argument sketch')
l.append(c)
l2 = copy(l)
print l == l2
print l
print l2
l2 = deepcopy(l)
print l == l2
print l
print l2
l.append({l[1]: l, 'xyz': l[2]})
l3 = copy(l)
import repr
print map(repr.repr, l)
print map(repr.repr, l1)
print map(repr.repr, l2)
print map(repr.repr, l3)
l3 = deepcopy(l)
import repr
print map(repr.repr, l)
print map(repr.repr, l1)
print map(repr.repr, l2)
print map(repr.repr, l3)
class odict(dict):
def __init__(self, d = {}):
self.a = 99
dict.__init__(self, d)
def __setitem__(self, k, i):
dict.__setitem__(self, k, i)
self.a
o = odict({"A" : "B"})
x = deepcopy(o)
print(o, x)
if __name__ == '__main__':
_test()

13
misc/pylib/copy/setup.py Normal file
View file

@ -0,0 +1,13 @@
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [
Extension("copy", ["copy.pyx"]),
]
setup(
name = 'copy',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)

1
misc/pylib/fontbuild/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.c

View file

@ -18,6 +18,7 @@ import os
import sys
from booleanOperations import BooleanOperationManager
from cu2qu.ufo import fonts_to_quadratic
from fontTools.misc.transform import Transform
from robofab.world import OpenFont
@ -201,25 +202,25 @@ class FontProject:
saveOTF(font, ttfName, self.glyphOrder, truetype=True)
def transformGlyphMembers(g, m):
g.width = int(g.width * m.a)
g.Transform(m)
for a in g.anchors:
p = Point(a.p)
p.Transform(m)
a.p = p
for c in g.components:
# Assumes that components have also been individually transformed
p = Point(0,0)
d = Point(c.deltas[0])
d.Transform(m)
p.Transform(m)
d1 = d - p
c.deltas[0].x = d1.x
c.deltas[0].y = d1.y
s = Point(c.scale)
s.Transform(m)
#c.scale = s
# def transformGlyphMembers(g, m):
# g.width = int(g.width * m.a)
# g.Transform(m)
# for a in g.anchors:
# p = Point(a.p)
# p.Transform(m)
# a.p = p
# for c in g.components:
# # Assumes that components have also been individually transformed
# p = Point(0,0)
# d = Point(c.deltas[0])
# d.Transform(m)
# p.Transform(m)
# d1 = d - p
# c.deltas[0].x = d1.x
# c.deltas[0].y = d1.y
# s = Point(c.scale)
# s.Transform(m)
# #c.scale = s
def swapContours(f,gName1,gName2):

View file

@ -108,13 +108,18 @@ def findCorner(pp, nn):
# print "parallel lines", np.arctan2(prev[1],prev[0]), np.arctan2(next[1],next[0])
# print prev, next
assert 0, "parallel lines"
if glyph.name is None:
# Never happens, but here to fix a bug in Python 2.7 with -OO
print ''
# if glyph.name is None:
# # Never happens, but here to fix a bug in Python 2.7 with -OO
# print ''
return lineIntersect(pStart, pEnd, nStart, nEnd)
def lineIntersect((x1,y1),(x2,y2),(x3,y3),(x4,y4)):
def lineIntersect(p1, p2, p3, p4):
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
x4, y4 = p4
x12 = x1 - x2
x34 = x3 - x4
y12 = y1 - y2

View file

@ -59,8 +59,8 @@ def italicize(glyph, angle=12, stemWidth=180, xoffset=-50):
ga, subsegments = segmentGlyph(glyph,25)
va, e = glyphToMesh(ga)
n = len(va)
grad = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
grad = mapEdges(lambda a, pn: normalize(pn[0]-a), va, e)
cornerWeights = mapEdges(lambda a, pn: normalize(pn[0]-a).dot(normalize(a-pn[1])), grad, e)[:,0].reshape((-1,1))
smooth = np.ones((n,1)) * CURVE_CORRECTION_WEIGHT
controlPoints = findControlPointsInMesh(glyph, va, subsegments)
@ -182,7 +182,7 @@ def findControlPointsInMesh(glyph, va, subsegments):
def recompose(v, grad, e, smooth=1, P=None, distance=None):
n = len(v)
if distance == None:
distance = mapEdges(lambda a,(p,n): norm(p - a), v, e)
distance = mapEdges(lambda a, pn: norm(pn[0] - a), v, e)
if (P == None):
P = mP(v,e)
P += np.identity(n) * smooth
@ -233,7 +233,7 @@ def getNormal(a,b,c):
def edgeNormals(v,e):
"Assumes a mesh where each vertex has exactly least two edges"
return mapEdges(lambda a,(p,n) : getNormal(a,p,n),v,e)
return mapEdges(lambda a, pn : getNormal(a,pn[0],pn[1]),v,e)
def rangePrevNext(count):
@ -268,10 +268,10 @@ def copyGradDetails(a,b,e,scale=15):
def copyMeshDetails(va,vb,e,scale=5,smooth=.01):
gradA = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
gradB = mapEdges(lambda a,(p,n): normalize(p-a), vb, e)
gradA = mapEdges(lambda a, pn: normalize(pn[0]-a), va, e)
gradB = mapEdges(lambda a, pn: normalize(pn[0]-a), vb, e)
grad = copyGradDetails(gradA, gradB, e, scale)
grad = mapEdges(lambda a,(p,n): normalize(a), grad, e)
grad = mapEdges(lambda a, pn: normalize(a), grad, e)
return recompose(vb, grad, e, smooth=smooth)
@ -282,7 +282,7 @@ def condenseGlyph(glyph, scale=.8, stemWidth=185):
normals = edgeNormals(va,e)
cn = va.dot(np.array([[scale, 0],[0,1]]))
grad = mapEdges(lambda a,(p,n): normalize(p-a), cn, e)
grad = mapEdges(lambda a, pn: normalize(pn[0]-a), cn, e)
# ograd = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
cn[:,0] -= normals[:,0] * stemWidth * .5 * (1 - scale)

View file

@ -269,7 +269,7 @@ class Mix:
def getFGlyph(self, master, gname):
if isinstance(master.font, Mix):
return font.mixGlyphs(gname)
return master.font.mixGlyphs(gname)
return master.ffont.getGlyph(gname)
def getGlyphMasters(self,gname):

View file

@ -0,0 +1,19 @@
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [
Extension("decomposeGlyph", ["decomposeGlyph.pyx"]),
Extension("alignpoints", ["alignpoints.pyx"]),
Extension("Build", ["Build.pyx"]),
Extension("convertCurves", ["convertCurves.pyx"]),
Extension("mitreGlyph", ["mitreGlyph.pyx"]),
Extension("mix", ["mix.pyx"]),
Extension("italics", ["italics.pyx"]),
]
setup(
name = 'copy',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)

2
misc/pylib/robofab/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.c
build

View file

@ -0,0 +1,22 @@
RoboFab License Agreement
Copyright (c) 2003-2013, The RoboFab Developers:
Erik van Blokland
Tal Leming
Just van Rossum
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the The RoboFab Developers nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Up to date info on RoboFab:
http://robofab.com/
This is the BSD license:
http://www.opensource.org/licenses/BSD-3-Clause

82
misc/pylib/robofab/__init__.py Executable file
View file

@ -0,0 +1,82 @@
"""
ROBOFAB
RoboFab is a Python library with objects
that deal with data usually associated
with fonts and type design.
DEVELOPERS
RoboFab is developed and maintained by
Tal Leming
Erik van Blokland
Just van Rossum
(in no particular order)
MORE INFO
The RoboFab homepage, documentation etc.
http://robofab.com
SVN REPOSITORY
http://svn.robofab.com
TRAC
http://code.robofab.com
RoboFab License Agreement
Copyright (c) 2003-2013, The RoboFab Developers:
Erik van Blokland
Tal Leming
Just van Rossum
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the The RoboFab Developers nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Up to date info on RoboFab:
http://robofab.com/
This is the BSD license:
http://www.opensource.org/licenses/BSD-3-Clause
HISTORY
RoboFab starts somewhere during the
TypoTechnica in Heidelberg, 2003.
DEPENDENCIES
RoboFab expects fontTools to be installed.
http://sourceforge.net/projects/fonttools/
Some of the RoboFab modules require data files
that are included in the source directory.
RoboFab likes to be able to calculate paths
to these data files all by itself, so keep them
together with the source files.
QUOTES
Yuri Yarmola:
"If data is somehow available to other programs
via some standard data-exchange interface which
can be accessed by some library in Python, you
can make a Python script that uses that library
to apply data to a font opened in FontLab."
W.A. Dwiggins:
"You will understand that I am not trying to
short-circuit any of your shop operations in
sending drawings of this kind. The closer I can
get to the machine the better the result.
Subtleties of curves are important, as you know,
and if I can make drawings that can be used in
the large size I have got one step closer to the
machine that cuts the punches." [1932]
"""
from .exceptions import RoboFabError, RoboFabWarning
numberVersion = (1, 2, "release", 1)
version = "1.2.1"

View file

@ -0,0 +1,11 @@
"""
Directory for contributed packages.
Packages stored here can be imported from
robofab.contrib.<packagename>
"""

View file

@ -0,0 +1,3 @@
class RoboFabError(Exception): pass
class RoboFabWarning(Warning): pass

625
misc/pylib/robofab/gString.py Executable file
View file

@ -0,0 +1,625 @@
"""A bunch of stuff useful for glyph name comparisons and such.
1. A group of sorted glyph name lists that can be called directly:
2. Some tools to work with glyph names to do things like build control strings."""
import string
######################################################
# THE LISTS
######################################################
uppercase_plain = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AE', 'OE', 'Oslash', 'Thorn', 'Eth',]
uppercase_accents = ['Aacute', 'Abreve', 'Acaron', 'Acircumflex', 'Adblgrave', 'Adieresis', 'Agrave', 'Amacron', 'Aogonek', 'Aring', 'Aringacute', 'Atilde', 'Bdotaccent', 'Cacute', 'Ccaron', 'Ccircumflex', 'Cdotaccent', 'Dcaron', 'Dcedilla', 'Ddotaccent', 'Eacute', 'Ebreve', 'Ecaron', 'Ecircumflex', 'Edblgrave', 'Edieresis', 'Edotaccent', 'Egrave', 'Emacron', 'Eogonek', 'Etilde', 'Fdotaccent', 'Gacute', 'Gbreve', 'Gcaron', 'Gcedilla', 'Gcircumflex', 'Gcommaaccent', 'Gdotaccent', 'Gmacron', 'Hcedilla', 'Hcircumflex', 'Hdieresis', 'Hdotaccent', 'Iacute', 'Ibreve', 'Icaron', 'Icircumflex', 'Idblgrave', 'Idieresis', 'Idieresisacute', 'Idieresisacute', 'Idotaccent', 'Igrave', 'Imacron', 'Iogonek', 'Itilde', 'Jcircumflex', 'Kacute', 'Kcaron', 'Kcedilla', 'Kcommaaccent', 'Lacute', 'Lcaron', 'Lcedilla', 'Lcommaaccent', 'Ldotaccent', 'Macute', 'Mdotaccent', 'Nacute', 'Ncaron', 'Ncedilla', 'Ncommaaccent', 'Ndotaccent', 'Ntilde', 'Oacute', 'Obreve', 'Ocaron', 'Ocircumflex', 'Odblgrave', 'Odieresis', 'Ograve', 'Ohorn', 'Ohungarumlaut', 'Omacron', 'Oogonek', 'Otilde', 'Pacute', 'Pdotaccent', 'Racute', 'Rcaron', 'Rcedilla', 'Rcommaaccent', 'Rdblgrave', 'Rdotaccent', 'Sacute', 'Scaron', 'Scedilla', 'Scircumflex', 'Scommaaccent', 'Sdotaccent', 'Tcaron', 'Tcedilla', 'Tcommaaccent', 'Tdotaccent', 'Uacute', 'Ubreve', 'Ucaron', 'Ucircumflex', 'Udblgrave', 'Udieresis', 'Udieresisacute', 'Udieresisacute', 'Udieresisgrave', 'Udieresisgrave', 'Ugrave', 'Uhorn', 'Uhungarumlaut', 'Umacron', 'Uogonek', 'Uring', 'Utilde', 'Vtilde', 'Wacute', 'Wcircumflex', 'Wdieresis', 'Wdotaccent', 'Wgrave', 'Xdieresis', 'Xdotaccent', 'Yacute', 'Ycircumflex', 'Ydieresis', 'Ydotaccent', 'Ygrave', 'Ytilde', 'Zacute', 'Zcaron', 'Zcircumflex', 'Zdotaccent', 'AEacute', 'Ccedilla', 'Oslashacute', 'Ldot']
uppercase_special_accents = ['Dcroat', 'Lslash', 'Hbar', 'Tbar', 'LL', 'Eng']
uppercase_ligatures = ['IJ']
uppercase = uppercase_plain+uppercase_accents+uppercase_special_accents+uppercase_ligatures
lowercase_plain = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'dotlessi', 'dotlessj', 'ae', 'oe', 'oslash', 'thorn', 'eth', 'germandbls', 'longs',]
lowercase_accents = ['aacute', 'abreve', 'acaron', 'acircumflex', 'adblgrave', 'adieresis', 'agrave', 'amacron', 'aogonek', 'aring', 'aringacute', 'atilde', 'bdotaccent', 'cacute', 'ccaron', 'ccircumflex', 'cdotaccent', 'dcaron', 'dcedilla', 'ddotaccent', 'dmacron', 'eacute', 'ebreve', 'ecaron', 'ecircumflex', 'edblgrave', 'edieresis', 'edotaccent', 'egrave', 'emacron', 'eogonek', 'etilde', 'fdotaccent', 'gacute', 'gbreve', 'gcaron', 'gcedilla', 'gcircumflex', 'gcommaaccent', 'gdotaccent', 'gmacron', 'hcedilla', 'hcircumflex', 'hdieresis', 'hdotaccent', 'iacute', 'ibreve', 'icaron', 'icircumflex', 'idblgrave', 'idieresis', 'idieresisacute', 'idieresisacute', 'igrave', 'imacron', 'iogonek', 'itilde', 'jcaron', 'jcircumflex', 'kacute', 'kcaron', 'kcedilla', 'kcommaaccent', 'lacute', 'lcaron', 'lcedilla', 'lcommaaccent', 'ldotaccent', 'macute', 'mdotaccent', 'nacute', 'ncaron', 'ncedilla', 'ncommaaccent', 'ndotaccent', 'ntilde', 'oacute', 'obreve', 'ocaron', 'ocircumflex', 'odblgrave', 'odieresis', 'ograve', 'ohorn', 'ohungarumlaut', 'omacron', 'oogonek', 'otilde', 'pacute', 'pdotaccent', 'racute', 'rcaron', 'rcedilla', 'rcommaaccent', 'rdblgrave', 'rdotaccent', 'sacute', 'scaron', 'scedilla', 'scircumflex', 'scommaaccent', 'sdotaccent', 'tcaron', 'tcedilla', 'tcommaaccent', 'tdieresis', 'tdotaccent', 'uacute', 'ubreve', 'ucaron', 'ucircumflex', 'udblgrave', 'udieresis', 'udieresisacute', 'udieresisacute', 'udieresisgrave', 'udieresisgrave', 'ugrave', 'uhorn', 'uhungarumlaut', 'umacron', 'uogonek', 'uring', 'utilde', 'vtilde', 'wacute', 'wcircumflex', 'wdieresis', 'wdotaccent', 'wgrave', 'wring', 'xdieresis', 'xdotaccent', 'yacute', 'ycircumflex', 'ydieresis', 'ydotaccent', 'ygrave', 'yring', 'ytilde', 'zacute', 'zcaron', 'zcircumflex', 'zdotaccent', 'aeacute', 'ccedilla', 'oslashacute', 'ldot', ]
lowercase_special_accents = ['dcroat', 'lslash', 'hbar', 'tbar', 'kgreenlandic', 'longs', 'll', 'eng']
lowercase_ligatures = ['fi', 'fl', 'ff', 'ffi', 'ffl', 'ij']
lowercase = lowercase_plain+lowercase_accents+lowercase_special_accents+lowercase_ligatures
smallcaps_plain = ['A.sc', 'B.sc', 'C.sc', 'D.sc', 'E.sc', 'F.sc', 'G.sc', 'H.sc', 'I.sc', 'J.sc', 'K.sc', 'L.sc', 'M.sc', 'N.sc', 'O.sc', 'P.sc', 'Q.sc', 'R.sc', 'S.sc', 'T.sc', 'U.sc', 'V.sc', 'W.sc', 'X.sc', 'Y.sc', 'Z.sc', 'AE.sc', 'OE.sc', 'Oslash.sc', 'Thorn.sc', 'Eth.sc', ]
smallcaps_accents = ['Aacute.sc', 'Acircumflex.sc', 'Adieresis.sc', 'Agrave.sc', 'Aring.sc', 'Atilde.sc', 'Ccedilla.sc', 'Eacute.sc', 'Ecircumflex.sc', 'Edieresis.sc', 'Egrave.sc', 'Iacute.sc', 'Icircumflex.sc', 'Idieresis.sc', 'Igrave.sc', 'Ntilde.sc', 'Oacute.sc', 'Ocircumflex.sc', 'Odieresis.sc', 'Ograve.sc', 'Otilde.sc', 'Scaron.sc', 'Uacute.sc', 'Ucircumflex.sc', 'Udieresis.sc', 'Ugrave.sc', 'Yacute.sc', 'Ydieresis.sc', 'Zcaron.sc', 'Ccedilla.sc', 'Lslash.sc', ]
smallcaps_special_accents = ['Dcroat.sc', 'Lslash.sc', 'Hbar.sc', 'Tbar.sc', 'LL.sc', 'Eng.sc']
smallcaps_ligatures = ['IJ.sc']
smallcaps = smallcaps_plain + smallcaps_accents + smallcaps_special_accents + smallcaps_ligatures
all_accents = uppercase_accents + uppercase_special_accents + lowercase_accents +lowercase_special_accents + smallcaps_accents + smallcaps_special_accents
digits = ['one', 'onefitted', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'zero']
digits_oldstyle = ['eight.oldstyle', 'five.oldstyle', 'four.oldstyle', 'nine.oldstyle', 'one.oldstyle', 'seven.oldstyle', 'six.oldstyle', 'three.oldstyle', 'two.oldstyle', 'zero.oldstyle']
digits_superior = ['eight.superior', 'five.superior', 'four.superior', 'nine.superior', 'one.superior', 'seven.superior', 'six.superior', 'three.superior', 'two.superior', 'zero.superior']
digits_inferior = ['eight.inferior', 'five.inferior', 'four.inferior', 'nine.inferior', 'one.inferior', 'seven.inferior', 'three.inferior', 'two.inferior', 'zero.inferior']
fractions = ['oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onequarter', 'threequarters', 'onethird', 'twothirds', 'onehalf']
currency = ['dollar', 'cent', 'currency', 'Euro', 'sterling', 'yen', 'florin', 'franc', 'lira']
currency_oldstyle = ['cent.oldstyle', 'dollar.oldstyle']
currency_superior = ['cent.superior', 'dollar.superior']
currency_inferior = ['cent.inferior', 'dollar.inferior']
inferior = ['eight.inferior', 'five.inferior', 'four.inferior', 'nine.inferior', 'one.inferior', 'seven.inferior', 'three.inferior', 'two.inferior', 'zero.inferior', 'cent.inferior', 'dollar.inferior', 'comma.inferior', 'hyphen.inferior', 'parenleft.inferior', 'parenright.inferior', 'period.inferior']
superior = ['eight.superior', 'five.superior', 'four.superior', 'nine.superior', 'one.superior', 'seven.superior', 'six.superior', 'three.superior', 'two.superior', 'zero.superior', 'cent.superior', 'dollar.superior', 'Rsmallinverted.superior', 'a.superior', 'b.superior', 'comma.superior', 'd.superior', 'equal.superior', 'e.superior', 'glottalstopreversed.superior', 'hhook.superior', 'h.superior', 'hyphen.superior', 'i.superior', 'j.superior', 'l.superior', 'm.superior', 'n.superior', 'o.superior', 'parenleft.superior', 'parenright.superior', 'period.superior', 'plus.superior', 'r.superior', 'rturned.superior', 's.superior', 't.superior', 'w.superior', 'x.superior', 'y.superior']
accents = ['acute', 'acutecomb', 'breve', 'caron', 'cedilla', 'circumflex', 'commaaccent', 'dblgrave', 'dieresis', 'dieresisacute', 'dieresisacute', 'dieresisgrave', 'dieresisgrave', 'dotaccent', 'grave', 'dblgrave', 'gravecomb', 'hungarumlaut', 'macron', 'ogonek', 'ring', 'ringacute', 'tilde', 'tildecomb', 'horn', 'Acute.sc', 'Breve.sc', 'Caron.sc', 'Cedilla.sc', 'Circumflex.sc', 'Dieresis.sc', 'Dotaccent.sc', 'Grave.sc', 'Hungarumlaut.sc', 'Macron.sc', 'Ogonek.sc', 'Ring.sc', 'Tilde.sc']
dashes = ['hyphen', 'endash', 'emdash', 'threequartersemdash', 'underscore', 'underscoredbl', 'figuredash']
legal = ['trademark', 'trademarksans', 'trademarkserif', 'copyright', 'copyrightsans', 'copyrightserif', 'registered', 'registersans', 'registerserif']
ligatures = ['fi', 'fl', 'ff', 'ffi', 'ffl', 'ij', 'IJ']
punctuation = ['period', 'periodcentered', 'comma', 'colon', 'semicolon', 'ellipsis', 'exclam', 'exclamdown', 'exclamdbl', 'question', 'questiondown']
numerical = ['percent', 'perthousand', 'infinity', 'numbersign', 'degree', 'colonmonetary', 'dotmath']
slashes = ['slash', 'backslash', 'bar', 'brokenbar', 'fraction']
special = ['ampersand', 'paragraph', 'section', 'bullet', 'dagger', 'daggerdbl', 'asterisk', 'at', 'asciicircum', 'asciitilde']
dependencies = {
'A': ['Aacute', 'Abreve', 'Acaron', 'Acircumflex', 'Adblgrave', 'Adieresis', 'Agrave', 'Amacron', 'Aogonek', 'Aring', 'Aringacute', 'Atilde'],
'B': ['Bdotaccent'],
'C': ['Cacute', 'Ccaron', 'Ccircumflex', 'Cdotaccent', 'Ccedilla'],
'D': ['Dcaron', 'Dcedilla', 'Ddotaccent'],
'E': ['Eacute', 'Ebreve', 'Ecaron', 'Ecircumflex', 'Edblgrave', 'Edieresis', 'Edotaccent', 'Egrave', 'Emacron', 'Eogonek', 'Etilde'],
'F': ['Fdotaccent'],
'G': ['Gacute', 'Gbreve', 'Gcaron', 'Gcedilla', 'Gcircumflex', 'Gcommaaccent', 'Gdotaccent', 'Gmacron'],
'H': ['Hcedilla', 'Hcircumflex', 'Hdieresis', 'Hdotaccent'],
'I': ['Iacute', 'Ibreve', 'Icaron', 'Icircumflex', 'Idblgrave', 'Idieresis', 'Idieresisacute', 'Idieresisacute', 'Idotaccent', 'Igrave', 'Imacron', 'Iogonek', 'Itilde'],
'J': ['Jcircumflex'],
'K': ['Kacute', 'Kcaron', 'Kcedilla', 'Kcommaaccent'],
'L': ['Lacute', 'Lcaron', 'Lcedilla', 'Lcommaaccent', 'Ldotaccent', 'Ldot'],
'M': ['Macute', 'Mdotaccent'],
'N': ['Nacute', 'Ncaron', 'Ncedilla', 'Ncommaaccent', 'Ndotaccent', 'Ntilde'],
'O': ['Oacute', 'Obreve', 'Ocaron', 'Ocircumflex', 'Odblgrave', 'Odieresis', 'Ograve', 'Ohorn', 'Ohungarumlaut', 'Omacron', 'Oogonek', 'Otilde'],
'P': ['Pacute', 'Pdotaccent'],
'R': ['Racute', 'Rcaron', 'Rcedilla', 'Rcommaaccent', 'Rdblgrave', 'Rdotaccent'],
'S': ['Sacute', 'Scaron', 'Scedilla', 'Scircumflex', 'Scommaaccent', 'Sdotaccent'],
'T': ['Tcaron', 'Tcedilla', 'Tcommaaccent', 'Tdotaccent'],
'U': ['Uacute', 'Ubreve', 'Ucaron', 'Ucircumflex', 'Udblgrave', 'Udieresis', 'Udieresisacute', 'Udieresisacute', 'Udieresisgrave', 'Udieresisgrave', 'Ugrave', 'Uhorn', 'Uhungarumlaut', 'Umacron', 'Uogonek', 'Uring', 'Utilde'],
'V': ['Vtilde'],
'W': ['Wacute', 'Wcircumflex', 'Wdieresis', 'Wdotaccent', 'Wgrave'],
'X': ['Xdieresis', 'Xdotaccent'],
'Y': ['Yacute', 'Ycircumflex', 'Ydieresis', 'Ydotaccent', 'Ygrave', 'Ytilde'],
'Z': ['Zacute', 'Zcaron', 'Zcircumflex', 'Zdotaccent'],
'AE': ['AEacute'],
'Oslash': ['Oslashacute'],
'a': ['aacute', 'abreve', 'acaron', 'acircumflex', 'adblgrave', 'adieresis', 'agrave', 'amacron', 'aogonek', 'aring', 'aringacute', 'atilde'],
'b': ['bdotaccent'],
'c': ['cacute', 'ccaron', 'ccircumflex', 'cdotaccent', 'ccedilla'],
'd': ['dcaron', 'dcedilla', 'ddotaccent', 'dmacron'],
'e': ['eacute', 'ebreve', 'ecaron', 'ecircumflex', 'edblgrave', 'edieresis', 'edotaccent', 'egrave', 'emacron', 'eogonek', 'etilde'],
'f': ['fdotaccent'],
'g': ['gacute', 'gbreve', 'gcaron', 'gcedilla', 'gcircumflex', 'gcommaaccent', 'gdotaccent', 'gmacron'],
'h': ['hcedilla', 'hcircumflex', 'hdieresis', 'hdotaccent'],
'i': ['iacute', 'ibreve', 'icaron', 'icircumflex', 'idblgrave', 'idieresis', 'idieresisacute', 'idieresisacute', 'igrave', 'imacron', 'iogonek', 'itilde'],
'j': ['jcaron', 'jcircumflex'],
'k': ['kacute', 'kcaron', 'kcedilla', 'kcommaaccent'],
'l': ['lacute', 'lcaron', 'lcedilla', 'lcommaaccent', 'ldotaccent', 'ldot'],
'm': ['macute', 'mdotaccent'],
'n': ['nacute', 'ncaron', 'ncedilla', 'ncommaaccent', 'ndotaccent', 'ntilde'],
'o': ['oacute', 'obreve', 'ocaron', 'ocircumflex', 'odblgrave', 'odieresis', 'ograve', 'ohorn', 'ohungarumlaut', 'omacron', 'oogonek', 'otilde'],
'p': ['pacute', 'pdotaccent'],
'r': ['racute', 'rcaron', 'rcedilla', 'rcommaaccent', 'rdblgrave', 'rdotaccent'],
's': ['sacute', 'scaron', 'scedilla', 'scircumflex', 'scommaaccent', 'sdotaccent'],
't': ['tcaron', 'tcedilla', 'tcommaaccent', 'tdieresis', 'tdotaccent'],
'u': ['uacute', 'ubreve', 'ucaron', 'ucircumflex', 'udblgrave', 'udieresis', 'udieresisacute', 'udieresisacute', 'udieresisgrave', 'udieresisgrave', 'ugrave', 'uhorn', 'uhungarumlaut', 'umacron', 'uogonek', 'uring', 'utilde'],
'v': ['vtilde'],
'w': ['wacute', 'wcircumflex', 'wdieresis', 'wdotaccent', 'wgrave', 'wring'],
'x': ['xdieresis', 'xdotaccent'],
'y': ['yacute', 'ycircumflex', 'ydieresis', 'ydotaccent', 'ygrave', 'yring', 'ytilde'],
'z': ['zacute', 'zcaron', 'zcircumflex', 'zdotaccent'],
'ae': ['aeacute'],
'oslash': ['oslashacute'],
}
######################################################
# MISC TOOLS
######################################################
def breakSuffix(glyphname):
"""
Breaks the glyphname into a two item list
0: glyphname
1: suffix
if a suffix is not found it returns None
"""
if glyphname.find('.') != -1:
split = glyphname.split('.')
return split
else:
return None
def findAccentBase(accentglyph):
"""Return the base glyph of an accented glyph
for example: Ugrave.sc returns U.sc"""
base = splitAccent(accentglyph)[0]
return base
def splitAccent(accentglyph):
"""
Split an accented glyph into a two items
0: base glyph
1: accent list
for example: Yacute.scalt45 returns: (Y.scalt45, [acute])
and: aacutetilde.alt45 returns (a.alt45, [acute, tilde])
"""
base = None
suffix = ''
accentList=[]
broken = breakSuffix(accentglyph)
if broken is not None:
suffix = broken[1]
base = broken[0]
else:
base=accentglyph
ogbase=base
temp_special = lowercase_special_accents + uppercase_special_accents
if base in lowercase_plain + uppercase_plain + smallcaps_plain:
pass
elif base not in temp_special:
for accent in accents:
if base.find(accent) != -1:
base = base.replace(accent, '')
accentList.append(accent)
counter={}
for accent in accentList:
counter[ogbase.find(accent)] = accent
counterList = counter.keys()
counterList.sort()
finalAccents = []
for i in counterList:
finalAccents.append(counter[i])
accentList = finalAccents
if len(suffix) != 0:
base = '.'.join([base, suffix])
return base, accentList
######################################################
# UPPER, LOWER, SMALL
######################################################
casedict = {
'germandbls' : 'S/S',
'dotlessi' : 'I',
'dotlessj' : 'J',
'ae' : 'AE',
'aeacute' : 'AEacute',
'oe' : 'OE',
'll' : 'LL'
}
casedictflip = {}
smallcapscasedict = {
'germandbls' : 'S.sc/S.sc',
'question' : 'question.sc',
'questiondown' : 'questiondown.sc',
'exclam' : 'exclam.sc',
'exclamdown' : 'exclamdown.sc',
'ampersand' : 'ampersand.sc'
}
class _InternalCaseFunctions:
"""internal functions for doing gymnastics with the casedicts"""
def expandsmallcapscasedict(self):
for i in casedict.values():
if i not in smallcapscasedict.keys():
if len(i) > 1:
if i[:1].upper() == i[:1]:
smallcapscasedict[i] = i[:1] + i[1:] + '.sc'
for i in uppercase:
if i + '.sc' in smallcaps:
if i not in smallcapscasedict.keys():
smallcapscasedict[i] = i + '.sc'
def flipcasedict(self):
for i in casedict.keys():
if i.find('dotless') != -1:
i = i.replace('dotless', '')
casedictflip[casedict[i]] = i
def expandcasedict(self):
for i in lowercase_ligatures:
casedict[i] = i.upper()
for i in lowercase:
if i not in casedict.keys():
if string.capitalize(i) in uppercase:
casedict[i] = string.capitalize(i)
def upper(glyphstring):
"""Convert all possible characters to uppercase in a glyph string."""
_InternalCaseFunctions().expandcasedict()
uc = []
for i in glyphstring.split('/'):
if i.find('.sc') != -1:
if i[-3] != '.sc':
x = i.replace('.sc', '.')
else:
x = i.replace('.sc', '')
i = x
suffix = ''
bS = breakSuffix(i)
if bS is not None:
suffix = bS[1]
i = bS[0]
if i in casedict.keys():
i = casedict[i]
if len(suffix) != 0:
i = '.'.join([i, suffix])
uc.append(i)
return '/'.join(uc)
def lower(glyphstring):
"""Convert all possible characters to lowercase in a glyph string."""
_InternalCaseFunctions().expandcasedict()
_InternalCaseFunctions().flipcasedict()
lc = []
for i in glyphstring.split('/'):
if i.find('.sc') != -1:
if i[-3] != '.sc':
x = i.replace('.sc', '.')
else:
x = i.replace('.sc', '')
i = x
suffix = ''
bS = breakSuffix(i)
if breakSuffix(i) is not None:
suffix = bS[1]
i = bS[0]
if i in casedictflip.keys():
i = casedictflip[i]
if len(suffix) != 0:
i = '.'.join([i, suffix])
lc.append(i)
return '/'.join(lc)
def small(glyphstring):
"""Convert all possible characters to smallcaps in a glyph string."""
_InternalCaseFunctions().expandcasedict()
_InternalCaseFunctions().expandsmallcapscasedict()
sc = []
for i in glyphstring.split('/'):
suffix = ''
bS = breakSuffix(i)
if bS is not None:
suffix = bS[1]
if suffix == 'sc':
suffix = ''
i = bS[0]
if i in lowercase:
if i not in smallcapscasedict.keys():
i = casedict[i]
if i in smallcapscasedict.keys():
i = smallcapscasedict[i]
if i != 'S.sc/S.sc':
if len(suffix) != 0:
if i[-3:] == '.sc':
i = ''.join([i, suffix])
else:
i = '.'.join([i, suffix])
sc.append(i)
return '/'.join(sc)
######################################################
# CONTROL STRING TOOLS
######################################################
controldict = {
'UC' : ['/H/H', '/H/O/H/O', '/O/O'],
'LC' : ['/n/n', '/n/o/n/o', '/o/o'],
'SC' : ['/H.sc/H.sc', '/H.sc/O.sc/H.sc/O.sc', '/O.sc/O.sc'],
'DIGITS' : ['/one/one', '/one/zero/one/zero', '/zero/zero'],
}
def controls(glyphname):
"""Send this a glyph name and get a control string
with all glyphs separated by slashes."""
controlslist = []
for value in controldict.values():
for v in value:
for i in v.split('/'):
if len(i) > 0:
if i not in controlslist:
controlslist.append(i)
cs = ''
if glyphname in controlslist:
for key in controldict.keys():
for v in controldict[key]:
if glyphname in v.split('/'):
con = controldict[key]
striptriple = []
hold1 = ''
hold2 = ''
for i in ''.join(con).split('/'):
if len(i) != 0:
if i == hold1 and i == hold2:
pass
else:
striptriple.append(i)
hold1 = hold2
hold2 = i
constr = '/' + '/'.join(striptriple)
# this is a bit of a hack since FL seems to have trouble
# when it encounters the same string more than once.
# so, let's stick the glyph at the end to differentiate it.
# for example: HHOHOOH and HHOHOOO
cs = constr + '/' + glyphname
else:
suffix = ''
bS = breakSuffix(glyphname)
if bS is not None:
suffix = bS[1]
glyphname = bS[0]
if suffix[:2] == 'sc':
controls = controldict['SC']
elif glyphname in uppercase:
controls = controldict['UC']
elif glyphname in lowercase:
controls = controldict['LC']
elif glyphname in digits:
controls = controldict['DIGITS']
else:
controls = controldict['UC']
if len(suffix) != 0:
glyphname = '.'.join([glyphname, suffix])
cs = controls[0] + '/' + glyphname + controls[1] + '/' + glyphname + controls[2]
return cs
def sortControlList(list):
"""Roughly sort a list of control strings."""
controls = []
for v in controldict.values():
for w in v:
for x in w.split('/'):
if len(x) is not None:
if x not in controls:
controls.append(x)
temp_digits = digits + digits_oldstyle + fractions
temp_currency = currency + currency_oldstyle
ss_uppercase = []
ss_lowercase = []
ss_smallcaps = []
ss_digits = []
ss_currency = []
ss_other = []
for i in list:
glyphs = i.split('/')
c = glyphs[2]
for glyph in glyphs:
if len(glyph) is not None:
if glyph not in controls:
c = glyph
if c in uppercase:
ss_uppercase.append(i)
elif c in lowercase:
ss_lowercase.append(i)
elif c in smallcaps:
ss_smallcaps.append(i)
elif c in temp_digits:
ss_digits.append(i)
elif c in temp_currency:
ss_currency.append(i)
else:
ss_other.append(i)
ss_uppercase.sort()
ss_lowercase.sort()
ss_smallcaps.sort()
ss_digits.sort()
ss_currency.sort()
ss_other.sort()
return ss_uppercase + ss_lowercase + ss_smallcaps + ss_digits + ss_currency + ss_other
# under contruction!
kerncontroldict = {
'UC/UC' : ['/H/H', '/H/O/H/O/O'],
'UC/LC' : ['', '/n/n/o/n/e/r/s'],
'UC/SORTS' : ['/H/H', '/H/O/H/O/O'],
'UC/DIGITS' : ['/H/H', '/H/O/H/O/O'],
'LC/LC' : ['/n/n', '/n/o/n/o/o'],
'LC/SORTS' : ['/n/n', '/n/o/n/o/o'],
'LC/DIGITS' : ['', '/n/n/o/n/e/r/s'],
'SC/SC' : ['/H.sc/H.sc', '/H.sc/O.sc/H.sc/O.sc/O.sc'],
'UC/SC' : ['', '/H.sc/H.sc/O.sc/H.sc/O.sc/O.sc'],
'SC/SORTS' : ['/H.sc/H.sc', '/H.sc/O.sc/H.sc/O.sc/O.sc'],
'SC/DIGITS' : ['', '/H.sc/H.sc/O.sc/H.sc/O.sc/O.sc'],
'DIGITS/DIGITS' : ['/H/H', '/H/O/H/O/O'],
'DIGITS/SORTS' : ['/H/H', '/H/O/H/O/O'],
'SORTS/SORTS' : ['/H/H', '/H/O/H/O/O'],
}
def kernControls(leftglyphname, rightglyphname):
"""build a control string based on the left glyph and right glyph"""
sorts = currency + accents + dashes + legal + numerical + slashes + special
l = leftglyphname
r = rightglyphname
lSuffix = ''
rSuffix = ''
bSL = breakSuffix(l)
if bSL is not None:
lSuffix = bSL[1]
l = bSL[0]
bSR = breakSuffix(r)
if bSR is not None:
rSuffix = bSR[1]
r = bSR[0]
if lSuffix[:2] == 'sc' or rSuffix[:2] == 'sc':
if l in uppercase or r in uppercase:
controls = kerncontroldict['UC/SC']
elif l in digits or r in digits:
controls = kerncontroldict['SC/DIGITS']
elif l in sorts or r in sorts:
controls = kerncontroldict['SC/SORTS']
else:
controls = kerncontroldict['SC/SC']
elif l in uppercase or r in uppercase:
if l in lowercase or r in lowercase:
controls = kerncontroldict['UC/LC']
elif l in digits or r in digits:
controls = kerncontroldict['UC/DIGITS']
elif l in sorts or r in sorts:
controls = kerncontroldict['UC/SORTS']
else:
controls = kerncontroldict['UC/UC']
elif l in lowercase or r in lowercase:
if l in uppercase or r in uppercase:
controls = kerncontroldict['UC/LC']
elif l in digits or r in digits:
controls = kerncontroldict['LC/DIGITS']
elif l in sorts or r in sorts:
controls = kerncontroldict['LC/SORTS']
else:
controls = kerncontroldict['LC/LC']
elif l in digits or r in digits:
if l in uppercase or r in uppercase:
controls = kerncontroldict['UC/DIGITS']
elif l in lowercase or r in lowercase:
controls = kerncontroldict['LC/DIGITS']
elif l in sorts or r in sorts:
controls = kerncontroldict['DIGITS/SORTS']
else:
controls = kerncontroldict['DIGITS/DIGITS']
elif l in sorts and r in sorts:
controls = kerncontroldict['SORTS/SORTS']
else:
controls = kerncontroldict['UC/UC']
if len(lSuffix) != 0:
l = '.'.join([l, lSuffix])
if len(rSuffix) != 0:
r = '.'.join([r, rSuffix])
cs = controls[0] + '/' + l + '/' + r + controls[1]
return cs
######################################################
class _testing:
def __init__(self):
print
print '##### testing!'
# self.listtest()
# self.accentbasetest()
# self.controlstest()
self.upperlowersmalltest()
# self.stringsorttest()
def listtest(self):
testlist = [
uppercase,
uppercase_accents,
lowercase,
lowercase_accents,
smallcaps,
smallcaps_accents,
digits,
digits_oldstyle,
digits_superior,
digits_inferior,
fractions,
currency,
currency_oldstyle,
currency_superior,
currency_inferior,
inferior,
superior,
accents,
dashes,
legal,
ligatures,
punctuation,
numerical,
slashes,
special
]
for i in testlist:
print i
def accentbasetest(self):
print findAccentBase('Adieresis')
print findAccentBase('Adieresis.sc')
print findAccentBase('Thorn.sc')
print findAccentBase('notaralglyphname')
def controlstest(self):
print kernControls('A', 'a.swash')
print kernControls('A.sc', '1')
print kernControls('bracket.sc', 'germandbls')
print kernControls('2', 'X')
print kernControls('Y', 'X')
print kernControls('Y.alt', 'X')
print kernControls('Y.scalt', 'X')
#print controls('x')
#print controls('germandbls')
#print controls('L')
#print controls('L.sc')
#print controls('Z.sc')
#print controls('seven')
#print controls('question')
#print controls('unknown')
def upperlowersmalltest(self):
u = upper('/H/i/Z.sc/ampersand.sc/dotlessi/germandbls/four.superior/LL')
l = lower('/H/I/Z.sc/ampersand.sc/dotlessi/germandbls/four.superior/LL')
s = small('/H/i/Z.sc/ampersand.alt/dotlessi/germandbls/four.superior/LL')
print u
print l
print s
print lower(u)
print upper(l)
print upper(s)
print lower(s)
def stringsorttest(self):
sample = "/H/H/Euro/H/O/H/O/Euro/O/O /H/H/R/H/O/H/O/R/O/O /H/H/question/H/O/H/O/question/O/O /H/H/sterling/H/O/H/O/sterling/O/O /n/n/r/n/o/n/o/r/o/o"
list = string.split(sample, ' ')
x = sortControlList(list)
print x
if __name__ == '__main__':
_testing()

718
misc/pylib/robofab/glifLib.pyx Executable file
View file

@ -0,0 +1,718 @@
# -*- coding: utf-8 -*-
"""glifLib.py -- Generic module for reading and writing the .glif format.
More info about the .glif format (GLyphInterchangeFormat) can be found here:
http://robofab.com/ufo/glif.html
The main class in this module is GlyphSet. It manages a set of .glif files
in a folder. It offers two ways to read glyph data, and one way to write
glyph data. See the class doc string for details.
"""
__all__ = ["GlyphSet", "GlifLibError",
"readGlyphFromString", "writeGlyphToString",
"glyphNameToFileName"]
import os
from robofab.xmlTreeBuilder import buildTree, stripCharacterData
from robofab.pens.pointPen import AbstractPointPen
from cStringIO import StringIO
class GlifLibError(Exception): pass
if os.name == "mac":
WRITE_MODE = "wb" # use unix line endings, even with Classic MacPython
READ_MODE = "rb"
else:
WRITE_MODE = "w"
READ_MODE = "r"
class Glyph:
"""Minimal glyph object. It has no glyph attributes until either
the draw() or the drawPoint() method has been called.
"""
def __init__(self, glyphName, glyphSet):
self.glyphName = glyphName
self.glyphSet = glyphSet
def draw(self, pen):
"""Draw this glyph onto a *FontTools* Pen."""
from robofab.pens.adapterPens import PointToSegmentPen
pointPen = PointToSegmentPen(pen)
self.drawPoints(pointPen)
def drawPoints(self, pointPen):
"""Draw this glyph onto a PointPen."""
self.glyphSet.readGlyph(self.glyphName, self, pointPen)
def glyphNameToFileName(glyphName, glyphSet):
"""Default algorithm for making a file name out of a glyph name.
This one has limited support for case insensitive file systems:
it assumes glyph names are not case sensitive apart from the first
character:
'a' -> 'a.glif'
'A' -> 'A_.glif'
'A.alt' -> 'A_.alt.glif'
'A.Alt' -> 'A_.Alt.glif'
'T_H' -> 'T__H_.glif'
'T_h' -> 'T__h.glif'
't_h' -> 't_h.glif'
'F_F_I' -> 'F__F__I_.glif'
'f_f_i' -> 'f_f_i.glif'
"""
if glyphName.startswith("."):
# some OSes consider filenames such as .notdef "hidden"
glyphName = "_" + glyphName[1:]
parts = glyphName.split(".")
if parts[0].find("_")!=-1:
# it is a compound name, check the separate parts
bits = []
for p in parts[0].split("_"):
if p != p.lower():
bits.append(p+"_")
continue
bits.append(p)
parts[0] = "_".join(bits)
else:
# it is a single name
if parts[0] != parts[0].lower():
parts[0] += "_"
for i in range(1, len(parts)):
# resolve additional, period separated parts, like alt / Alt
if parts[i] != parts[i].lower():
parts[i] += "_"
return ".".join(parts) + ".glif"
class GlyphSet:
"""GlyphSet manages a set of .glif files inside one directory.
GlyphSet's constructor takes a path to an existing directory as it's
first argument. Reading glyph data can either be done through the
readGlyph() method, or by using GlyphSet's dictionary interface, where
the keys are glyph names and the values are (very) simple glyph objects.
To write a glyph to the glyph set, you use the writeGlyph() method.
The simple glyph objects returned through the dict interface do not
support writing, they are just means as a convenient way to get at
the glyph data.
"""
glyphClass = Glyph
def __init__(self, dirName, glyphNameToFileNameFunc=None):
"""'dirName' should be a path to an existing directory.
The optional 'glyphNameToFileNameFunc' argument must be a callback
function that takes two arguments: a glyph name and the GlyphSet
instance. It should return a file name (including the .glif
extension). The glyphNameToFileName function is called whenever
a file name is created for a given glyph name.
"""
self.dirName = dirName
if glyphNameToFileNameFunc is None:
glyphNameToFileNameFunc = glyphNameToFileName
self.glyphNameToFileName = glyphNameToFileNameFunc
self.contents = self._findContents()
self._reverseContents = None
def rebuildContents(self):
"""Rebuild the contents dict by checking what glyphs are available
on disk.
"""
self.contents = self._findContents(forceRebuild=True)
self._reverseContents = None
def getReverseContents(self):
"""Return a reversed dict of self.contents, mapping file names to
glyph names. This is primarily an aid for custom glyph name to file
name schemes that want to make sure they don't generate duplicate
file names. The file names are converted to lowercase so we can
reliably check for duplicates that only differ in case, which is
important for case-insensitive file systems.
"""
if self._reverseContents is None:
d = {}
for k, v in self.contents.iteritems():
d[v.lower()] = k
self._reverseContents = d
return self._reverseContents
def writeContents(self):
"""Write the contents.plist file out to disk. Call this method when
you're done writing glyphs.
"""
from plistlib import writePlistToString
contentsPath = os.path.join(self.dirName, "contents.plist")
# We need to force Unix line endings, even in OS9 MacPython in FL,
# so we do the writing to file ourselves.
plist = writePlistToString(self.contents)
f = open(contentsPath, WRITE_MODE)
f.write(plist)
f.close()
# reading/writing API
def readGlyph(self, glyphName, glyphObject=None, pointPen=None):
"""Read a .glif file for 'glyphName' from the glyph set. The
'glyphObject' argument can be any kind of object (even None);
the readGlyph() method will attempt to set the following
attributes on it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional, in two ways:
1) An attribute *won't* be set if the .glif file doesn't
contain data for it. 'glyphObject' will have to deal
with default values itself.
2) If setting the attribute fails with an AttributeError
(for example if the 'glyphObject' attribute is read-
only), readGlyph() will not propagate that exception,
but ignore that attribute.
To retrieve outline information, you need to pass an object
conforming to the PointPen protocol as the 'pointPen' argument.
This argument may be None if you don't need the outline data.
readGlyph() will raise KeyError if the glyph is not present in
the glyph set.
"""
tree = self._getXMLTree(glyphName)
_readGlyphFromTree(tree, glyphObject, pointPen)
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None):
"""Write a .glif file for 'glyphName' to the glyph set. The
'glyphObject' argument can be any kind of object (even None);
the writeGlyph() method will attempt to get the following
attributes from it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional: if 'glyphObject' doesn't
have the attribute, it will simply be skipped.
To write outline data to the .glif file, writeGlyph() needs
a function (any callable object actually) that will take one
argument: an object that conforms to the PointPen protocol.
The function will be called by writeGlyph(); it has to call the
proper PointPen methods to transfer the outline to the .glif file.
"""
data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc)
fileName = self.contents.get(glyphName)
if fileName is None:
fileName = self.glyphNameToFileName(glyphName, self)
self.contents[glyphName] = fileName
if self._reverseContents is not None:
self._reverseContents[fileName.lower()] = glyphName
path = os.path.join(self.dirName, fileName)
if os.path.exists(path):
f = open(path, READ_MODE)
oldData = f.read()
f.close()
if data == oldData:
return
f = open(path, WRITE_MODE)
f.write(data)
f.close()
def deleteGlyph(self, glyphName):
"""Permanently delete the glyph from the glyph set on disk. Will
raise KeyError if the glyph is not present in the glyph set.
"""
fileName = self.contents[glyphName]
os.remove(os.path.join(self.dirName, fileName))
if self._reverseContents is not None:
del self._reverseContents[self.contents[glyphName].lower()]
del self.contents[glyphName]
# dict-like support
def keys(self):
return self.contents.keys()
def has_key(self, glyphName):
return glyphName in self.contents
__contains__ = has_key
def __len__(self):
return len(self.contents)
def __getitem__(self, glyphName):
if glyphName not in self.contents:
raise KeyError, glyphName
return self.glyphClass(glyphName, self)
# quickly fetching unicode values
def getUnicodes(self):
"""Return a dictionary that maps all glyph names to lists containing
the unicode value[s] for that glyph, if any. This parses the .glif
files partially, so is a lot faster than parsing all files completely.
"""
# XXX: This method is quite wasteful if we've already parsed many .glif
# files completely. We could collect unicodes values in readGlyph,
# and only do _fetchUnicodes() for those we haven't seen yet.
unicodes = {}
for glyphName, fileName in self.contents.iteritems():
path = os.path.join(self.dirName, fileName)
unicodes[glyphName] = _fetchUnicodes(path)
return unicodes
# internal methods
def _findContents(self, forceRebuild=False):
contentsPath = os.path.join(self.dirName, "contents.plist")
if forceRebuild or not os.path.exists(contentsPath):
fileNames = os.listdir(self.dirName)
fileNames = [n for n in fileNames if n.endswith(".glif")]
contents = {}
for n in fileNames:
glyphPath = os.path.join(self.dirName, n)
contents[_fetchGlyphName(glyphPath)] = n
else:
from plistlib import readPlist
contents = readPlist(contentsPath)
return contents
def _getXMLTree(self, glyphName):
fileName = self.contents[glyphName]
path = os.path.join(self.dirName, fileName)
if not os.path.exists(path):
raise KeyError, glyphName
return _glifTreeFromFile(path)
def readGlyphFromString(aString, glyphObject=None, pointPen=None):
"""Read .glif data from a string into a glyph object.
The 'glyphObject' argument can be any kind of object (even None);
the readGlyphFromString() method will attempt to set the following
attributes on it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional, in two ways:
1) An attribute *won't* be set if the .glif file doesn't
contain data for it. 'glyphObject' will have to deal
with default values itself.
2) If setting the attribute fails with an AttributeError
(for example if the 'glyphObject' attribute is read-
only), readGlyphFromString() will not propagate that
exception, but ignore that attribute.
To retrieve outline information, you need to pass an object
conforming to the PointPen protocol as the 'pointPen' argument.
This argument may be None if you don't need the outline data.
"""
tree = _glifTreeFromFile(StringIO(aString))
_readGlyphFromTree(tree, glyphObject, pointPen)
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, writer=None):
"""Return .glif data for a glyph as a UTF-8 encoded string.
The 'glyphObject' argument can be any kind of object (even None);
the writeGlyphToString() method will attempt to get the following
attributes from it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional: if 'glyphObject' doesn't
have the attribute, it will simply be skipped.
To write outline data to the .glif file, writeGlyphToString() needs
a function (any callable object actually) that will take one
argument: an object that conforms to the PointPen protocol.
The function will be called by writeGlyphToString(); it has to call the
proper PointPen methods to transfer the outline to the .glif file.
"""
if writer is None:
try:
from xmlWriter import XMLWriter
except ImportError:
# try the other location
from fontTools.misc.xmlWriter import XMLWriter
aFile = StringIO()
writer = XMLWriter(aFile, encoding="UTF-8")
else:
aFile = None
writer.begintag("glyph", [("name", glyphName), ("format", "1")])
writer.newline()
width = getattr(glyphObject, "width", None)
if width is not None:
if not isinstance(width, (int, float)):
raise GlifLibError, "width attribute must be int or float"
writer.simpletag("advance", width=repr(width))
writer.newline()
unicodes = getattr(glyphObject, "unicodes", None)
if unicodes:
if isinstance(unicodes, int):
unicodes = [unicodes]
for code in unicodes:
if not isinstance(code, int):
raise GlifLibError, "unicode values must be int"
hexCode = hex(code)[2:].upper()
if len(hexCode) < 4:
hexCode = "0" * (4 - len(hexCode)) + hexCode
writer.simpletag("unicode", hex=hexCode)
writer.newline()
note = getattr(glyphObject, "note", None)
if note is not None:
if not isinstance(note, (str, unicode)):
raise GlifLibError, "note attribute must be str or unicode"
note = note.encode('utf-8')
writer.begintag("note")
writer.newline()
for line in note.splitlines():
writer.write(line.strip())
writer.newline()
writer.endtag("note")
writer.newline()
if drawPointsFunc is not None:
writer.begintag("outline")
writer.newline()
pen = GLIFPointPen(writer)
drawPointsFunc(pen)
writer.endtag("outline")
writer.newline()
lib = getattr(glyphObject, "lib", None)
if lib:
from robofab.plistlib import PlistWriter
if not isinstance(lib, dict):
lib = dict(lib)
writer.begintag("lib")
writer.newline()
plistWriter = PlistWriter(writer.file, indentLevel=writer.indentlevel,
indent=writer.indentwhite, writeHeader=False)
plistWriter.writeValue(lib)
writer.endtag("lib")
writer.newline()
writer.endtag("glyph")
writer.newline()
if aFile is not None:
return aFile.getvalue()
else:
return None
# misc helper functions
def _stripGlyphXMLTree(nodes):
for element, attrs, children in nodes:
# "lib" is formatted as a plist, so we need unstripped
# character data so we can support strings with leading or
# trailing whitespace. Do strip everything else.
recursive = (element != "lib")
stripCharacterData(children, recursive=recursive)
def _glifTreeFromFile(aFile):
try:
tree = buildTree(aFile, stripData=False)
stripCharacterData(tree[2], recursive=False)
assert tree[0] == "glyph"
_stripGlyphXMLTree(tree[2])
return tree
except:
print "Problem with glif file", aFile
raise
return None
def _relaxedSetattr(object, attr, value):
try:
setattr(object, attr, value)
except AttributeError:
pass
def _number(s):
"""Given a numeric string, return an integer or a float, whichever
the string indicates. _number("1") will return the integer 1,
_number("1.0") will return the float 1.0.
"""
try:
n = int(s)
except ValueError:
n = float(s)
return n
def _readGlyphFromTree(tree, glyphObject=None, pointPen=None):
unicodes = []
assert tree[0] == "glyph"
formatVersion = int(tree[1].get("format", "0"))
if formatVersion not in (0, 1):
raise GlifLibError, "unsupported glif format version: %s" % formatVersion
glyphName = tree[1].get("name")
if glyphName and glyphObject is not None:
_relaxedSetattr(glyphObject, "name", glyphName)
for element, attrs, children in tree[2]:
if element == "outline":
if pointPen is not None:
if formatVersion == 0:
buildOutline_Format0(pointPen, children)
else:
buildOutline_Format1(pointPen, children)
elif glyphObject is None:
continue
elif element == "advance":
width = _number(attrs["width"])
_relaxedSetattr(glyphObject, "width", width)
elif element == "unicode":
unicodes.append(int(attrs["hex"], 16))
elif element == "note":
rawNote = "\n".join(children)
lines = rawNote.split("\n")
lines = [line.strip() for line in lines]
note = "\n".join(lines)
_relaxedSetattr(glyphObject, "note", note)
elif element == "lib":
from plistFromTree import readPlistFromTree
assert len(children) == 1
lib = readPlistFromTree(children[0])
_relaxedSetattr(glyphObject, "lib", lib)
if unicodes:
_relaxedSetattr(glyphObject, "unicodes", unicodes)
class _DoneParsing(Exception): pass
def _startElementHandler(tagName, attrs):
if tagName != "glyph":
# the top level element of any .glif file must be <glyph>
raise _DoneParsing(None)
glyphName = attrs["name"]
raise _DoneParsing(glyphName)
def _fetchGlyphName(glyphPath):
# Given a path to an existing .glif file, get the glyph name
# from the XML data.
from xml.parsers.expat import ParserCreate
p = ParserCreate()
p.StartElementHandler = _startElementHandler
p.returns_unicode = True
f = open(glyphPath)
try:
p.ParseFile(f)
except _DoneParsing, why:
glyphName = why.args[0]
if glyphName is None:
raise ValueError, (".glif file doen't have a <glyph> top-level "
"element: %r" % glyphPath)
else:
assert 0, "it's not expected that parsing the file ends normally"
return glyphName
def _fetchUnicodes(glyphPath):
# Given a path to an existing .glif file, get a list of all
# unicode values from the XML data.
# NOTE: this assumes .glif files written by glifLib, since
# we simply stop parsing as soon as we see anything else than
# <glyph>, <advance> or <unicode>. glifLib always writes those
# elements in that order, before anything else.
from xml.parsers.expat import ParserCreate
unicodes = []
def _startElementHandler(tagName, attrs, _unicodes=unicodes):
if tagName == "unicode":
_unicodes.append(int(attrs["hex"], 16))
elif tagName not in ("glyph", "advance"):
raise _DoneParsing()
p = ParserCreate()
p.StartElementHandler = _startElementHandler
p.returns_unicode = True
f = open(glyphPath)
try:
p.ParseFile(f)
except _DoneParsing:
pass
return unicodes
def buildOutline_Format0(pen, xmlNodes):
# This reads the "old" .glif format, retroactively named "format 0",
# later formats have a "format" attribute in the <glyph> element.
for element, attrs, children in xmlNodes:
if element == "contour":
pen.beginPath()
currentSegmentType = None
for subElement, attrs, dummy in children:
if subElement != "point":
continue
x = _number(attrs["x"])
y = _number(attrs["y"])
pointType = attrs.get("type", "onCurve")
if pointType == "bcp":
currentSegmentType = "curve"
elif pointType == "offCurve":
currentSegmentType = "qcurve"
elif currentSegmentType is None and pointType == "onCurve":
currentSegmentType = "line"
if pointType == "onCurve":
segmentType = currentSegmentType
currentSegmentType = None
else:
segmentType = None
smooth = attrs.get("smooth") == "yes"
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth)
pen.endPath()
elif element == "component":
baseGlyphName = attrs["base"]
transformation = []
for attr, default in _transformationInfo:
value = attrs.get(attr)
if value is None:
value = default
else:
value = _number(value)
transformation.append(value)
pen.addComponent(baseGlyphName, tuple(transformation))
elif element == "anchor":
name, x, y = attrs["name"], _number(attrs["x"]), _number(attrs["y"])
pen.beginPath()
pen.addPoint((x, y), segmentType="move", name=name)
pen.endPath()
def buildOutline_Format1(pen, xmlNodes):
for element, attrs, children in xmlNodes:
if element == "contour":
pen.beginPath()
for subElement, attrs, dummy in children:
if subElement != "point":
continue
x = _number(attrs["x"])
y = _number(attrs["y"])
segmentType = attrs.get("type", "offcurve")
if segmentType == "offcurve":
segmentType = None
smooth = attrs.get("smooth") == "yes"
name = attrs.get("name")
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
pen.endPath()
elif element == "component":
baseGlyphName = attrs["base"]
transformation = []
for attr, default in _transformationInfo:
value = attrs.get(attr)
if value is None:
value = default
else:
value = _number(value)
transformation.append(value)
pen.addComponent(baseGlyphName, tuple(transformation))
_transformationInfo = [
# field name, default value
("xScale", 1),
("xyScale", 0),
("yxScale", 0),
("yScale", 1),
("xOffset", 0),
("yOffset", 0),
]
class GLIFPointPen(AbstractPointPen):
"""Helper class using the PointPen protocol to write the <outline>
part of .glif files.
"""
def __init__(self, xmlWriter):
self.writer = xmlWriter
def beginPath(self):
self.writer.begintag("contour")
self.writer.newline()
def endPath(self):
self.writer.endtag("contour")
self.writer.newline()
def addPoint(self, pt, segmentType=None, smooth=None, name=None, **kwargs):
attrs = []
if pt is not None:
for coord in pt:
if not isinstance(coord, (int, float)):
raise GlifLibError, "coordinates must be int or float"
attrs.append(("x", repr(pt[0])))
attrs.append(("y", repr(pt[1])))
if segmentType is not None:
attrs.append(("type", segmentType))
if smooth:
attrs.append(("smooth", "yes"))
if name is not None:
attrs.append(("name", name))
self.writer.simpletag("point", attrs)
self.writer.newline()
def addComponent(self, glyphName, transformation):
attrs = [("base", glyphName)]
for (attr, default), value in zip(_transformationInfo, transformation):
if not isinstance(value, (int, float)):
raise GlifLibError, "transformation values must be int or float"
if value != default:
attrs.append((attr, repr(value)))
self.writer.simpletag("component", attrs)
self.writer.newline()
if __name__ == "__main__":
from pprint import pprint
from robofab.pens.pointPen import PrintingPointPen
class TestGlyph: pass
gs = GlyphSet(".")
def drawPoints(pen):
pen.beginPath()
pen.addPoint((100, 200), name="foo")
pen.addPoint((200, 250), segmentType="curve", smooth=True)
pen.endPath()
pen.addComponent("a", (1, 0, 0, 1, 20, 30))
glyph = TestGlyph()
glyph.width = 120
glyph.unicodes = [1, 2, 3, 43215, 66666]
glyph.lib = {"a": "b", "c": [1, 2, 3, True]}
glyph.note = " hallo! "
if 0:
gs.writeGlyph("a", glyph, drawPoints)
g2 = TestGlyph()
gs.readGlyph("a", g2, PrintingPointPen())
pprint(g2.__dict__)
else:
s = writeGlyphToString("a", glyph, drawPoints)
print s
g2 = TestGlyph()
readGlyphFromString(s, g2, PrintingPointPen())
pprint(g2.__dict__)

747
misc/pylib/robofab/glifLib2.py Executable file
View file

@ -0,0 +1,747 @@
# -*- coding: utf-8 -*-
"""glifLib.py -- Generic module for reading and writing the .glif format.
More info about the .glif format (GLyphInterchangeFormat) can be found here:
http://unifiedfontobject.org
The main class in this module is GlyphSet. It manages a set of .glif files
in a folder. It offers two ways to read glyph data, and one way to write
glyph data. See the class doc string for details.
"""
__all__ = ["GlyphSet", "GlifLibError",
"readGlyphFromString", "writeGlyphToString",
"glyphNameToFileName"]
import os
from robofab.xmlTreeBuilder import buildTree, stripCharacterData
from robofab.pens.pointPen import AbstractPointPen
from cStringIO import StringIO
class GlifLibError(Exception): pass
if os.name == "mac":
WRITE_MODE = "wb" # use unix line endings, even with Classic MacPython
READ_MODE = "rb"
else:
WRITE_MODE = "w"
READ_MODE = "r"
class Glyph:
"""Minimal glyph object. It has no glyph attributes until either
the draw() or the drawPoint() method has been called.
"""
def __init__(self, glyphName, glyphSet):
self.glyphName = glyphName
self.glyphSet = glyphSet
def draw(self, pen):
"""Draw this glyph onto a *FontTools* Pen."""
from robofab.pens.adapterPens import PointToSegmentPen
pointPen = PointToSegmentPen(pen)
self.drawPoints(pointPen)
def drawPoints(self, pointPen):
"""Draw this glyph onto a PointPen."""
self.glyphSet.readGlyph(self.glyphName, self, pointPen)
def glyphNameToFileName(glyphName, glyphSet):
"""Default algorithm for making a file name out of a glyph name.
This one has limited support for case insensitive file systems:
it assumes glyph names are not case sensitive apart from the first
character:
'a' -> 'a.glif'
'A' -> 'A_.glif'
'A.alt' -> 'A_.alt.glif'
'A.Alt' -> 'A_.Alt.glif'
'T_H' -> 'T__H_.glif'
'T_h' -> 'T__h.glif'
't_h' -> 't_h.glif'
'F_F_I' -> 'F__F__I_.glif'
'f_f_i' -> 'f_f_i.glif'
"""
if glyphName.startswith("."):
# some OSes consider filenames such as .notdef "hidden"
glyphName = "_" + glyphName[1:]
parts = glyphName.split(".")
if parts[0].find("_")!=-1:
# it is a compound name, check the separate parts
bits = []
for p in parts[0].split("_"):
if p != p.lower():
bits.append(p+"_")
continue
bits.append(p)
parts[0] = "_".join(bits)
else:
# it is a single name
if parts[0] != parts[0].lower():
parts[0] += "_"
for i in range(1, len(parts)):
# resolve additional, period separated parts, like alt / Alt
if parts[i] != parts[i].lower():
parts[i] += "_"
return ".".join(parts) + ".glif"
class GlyphSet:
"""GlyphSet manages a set of .glif files inside one directory.
GlyphSet's constructor takes a path to an existing directory as it's
first argument. Reading glyph data can either be done through the
readGlyph() method, or by using GlyphSet's dictionary interface, where
the keys are glyph names and the values are (very) simple glyph objects.
To write a glyph to the glyph set, you use the writeGlyph() method.
The simple glyph objects returned through the dict interface do not
support writing, they are just a convenient way to get at the glyph data.
"""
glyphClass = Glyph
def __init__(self, dirName, glyphNameToFileNameFunc=None):
"""'dirName' should be a path to an existing directory.
The optional 'glyphNameToFileNameFunc' argument must be a callback
function that takes two arguments: a glyph name and the GlyphSet
instance. It should return a file name (including the .glif
extension). The glyphNameToFileName function is called whenever
a file name is created for a given glyph name.
"""
self.dirName = dirName
if glyphNameToFileNameFunc is None:
glyphNameToFileNameFunc = glyphNameToFileName
self.glyphNameToFileName = glyphNameToFileNameFunc
self.contents = self._findContents()
self._reverseContents = None
self._glifCache = {}
def rebuildContents(self):
"""Rebuild the contents dict by checking what glyphs are available
on disk.
"""
self.contents = self._findContents(forceRebuild=True)
self._reverseContents = None
def getReverseContents(self):
"""Return a reversed dict of self.contents, mapping file names to
glyph names. This is primarily an aid for custom glyph name to file
name schemes that want to make sure they don't generate duplicate
file names. The file names are converted to lowercase so we can
reliably check for duplicates that only differ in case, which is
important for case-insensitive file systems.
"""
if self._reverseContents is None:
d = {}
for k, v in self.contents.iteritems():
d[v.lower()] = k
self._reverseContents = d
return self._reverseContents
def writeContents(self):
"""Write the contents.plist file out to disk. Call this method when
you're done writing glyphs.
"""
from plistlib import writePlistToString
contentsPath = os.path.join(self.dirName, "contents.plist")
# We need to force Unix line endings, even in OS9 MacPython in FL,
# so we do the writing to file ourselves.
plist = writePlistToString(self.contents)
f = open(contentsPath, WRITE_MODE)
f.write(plist)
f.close()
# read caching
def getGLIF(self, glyphName):
"""Get the raw GLIF text for a given glyph name. This only works
for GLIF files that are already on disk.
This method is useful in situations when the raw XML needs to be
read from a glyph set for a particular glyph before fully parsing
it into an object structure via the readGlyph method.
Internally, this method will load a GLIF the first time it is
called and then cache it. The next time this method is called
the GLIF will be pulled from the cache if the file's modification
time has not changed since the GLIF was cached. For memory
efficiency, the cached GLIF will be purged by various other methods
such as readGlyph.
"""
needRead = False
fileName = self.contents.get(glyphName)
path = None
if fileName is not None:
path = os.path.join(self.dirName, fileName)
if glyphName not in self._glifCache:
needRead = True
elif fileName is not None and os.path.getmtime(path) != self._glifCache[glyphName][1]:
needRead = True
if needRead:
fileName = self.contents[glyphName]
if not os.path.exists(path):
raise KeyError, glyphName
f = open(path, "rb")
text = f.read()
f.close()
self._glifCache[glyphName] = (text, os.path.getmtime(path))
return self._glifCache[glyphName][0]
def _purgeCachedGLIF(self, glyphName):
if glyphName in self._glifCache:
del self._glifCache[glyphName]
# reading/writing API
def readGlyph(self, glyphName, glyphObject=None, pointPen=None):
"""Read a .glif file for 'glyphName' from the glyph set. The
'glyphObject' argument can be any kind of object (even None);
the readGlyph() method will attempt to set the following
attributes on it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional, in two ways:
1) An attribute *won't* be set if the .glif file doesn't
contain data for it. 'glyphObject' will have to deal
with default values itself.
2) If setting the attribute fails with an AttributeError
(for example if the 'glyphObject' attribute is read-
only), readGlyph() will not propagate that exception,
but ignore that attribute.
To retrieve outline information, you need to pass an object
conforming to the PointPen protocol as the 'pointPen' argument.
This argument may be None if you don't need the outline data.
readGlyph() will raise KeyError if the glyph is not present in
the glyph set.
"""
text = self.getGLIF(glyphName)
self._purgeCachedGLIF(glyphName)
tree = _glifTreeFromFile(StringIO(text))
_readGlyphFromTree(tree, glyphObject, pointPen)
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None):
"""Write a .glif file for 'glyphName' to the glyph set. The
'glyphObject' argument can be any kind of object (even None);
the writeGlyph() method will attempt to get the following
attributes from it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional: if 'glyphObject' doesn't
have the attribute, it will simply be skipped.
To write outline data to the .glif file, writeGlyph() needs
a function (any callable object actually) that will take one
argument: an object that conforms to the PointPen protocol.
The function will be called by writeGlyph(); it has to call the
proper PointPen methods to transfer the outline to the .glif file.
"""
self._purgeCachedGLIF(glyphName)
data = writeGlyphToString(glyphName, glyphObject, drawPointsFunc)
fileName = self.contents.get(glyphName)
if fileName is None:
fileName = self.glyphNameToFileName(glyphName, self)
self.contents[glyphName] = fileName
if self._reverseContents is not None:
self._reverseContents[fileName.lower()] = glyphName
path = os.path.join(self.dirName, fileName)
if os.path.exists(path):
f = open(path, READ_MODE)
oldData = f.read()
f.close()
if data == oldData:
return
f = open(path, WRITE_MODE)
f.write(data)
f.close()
def deleteGlyph(self, glyphName):
"""Permanently delete the glyph from the glyph set on disk. Will
raise KeyError if the glyph is not present in the glyph set.
"""
self._purgeCachedGLIF(glyphName)
fileName = self.contents[glyphName]
os.remove(os.path.join(self.dirName, fileName))
if self._reverseContents is not None:
del self._reverseContents[self.contents[glyphName].lower()]
del self.contents[glyphName]
# dict-like support
def keys(self):
return self.contents.keys()
def has_key(self, glyphName):
return glyphName in self.contents
__contains__ = has_key
def __len__(self):
return len(self.contents)
def __getitem__(self, glyphName):
if glyphName not in self.contents:
raise KeyError, glyphName
return self.glyphClass(glyphName, self)
# quickly fetching unicode values
def getUnicodes(self):
"""Return a dictionary that maps all glyph names to lists containing
the unicode value[s] for that glyph, if any. This parses the .glif
files partially, so is a lot faster than parsing all files completely.
"""
unicodes = {}
for glyphName in self.contents.keys():
text = self.getGLIF(glyphName)
unicodes[glyphName] = _fetchUnicodes(text)
return unicodes
# internal methods
def _findContents(self, forceRebuild=False):
contentsPath = os.path.join(self.dirName, "contents.plist")
if forceRebuild or not os.path.exists(contentsPath):
fileNames = os.listdir(self.dirName)
fileNames = [n for n in fileNames if n.endswith(".glif")]
contents = {}
for n in fileNames:
glyphPath = os.path.join(self.dirName, n)
contents[_fetchGlyphName(glyphPath)] = n
else:
from plistlib import readPlist
contents = readPlist(contentsPath)
return contents
def readGlyphFromString(aString, glyphObject=None, pointPen=None):
"""Read .glif data from a string into a glyph object.
The 'glyphObject' argument can be any kind of object (even None);
the readGlyphFromString() method will attempt to set the following
attributes on it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional, in two ways:
1) An attribute *won't* be set if the .glif file doesn't
contain data for it. 'glyphObject' will have to deal
with default values itself.
2) If setting the attribute fails with an AttributeError
(for example if the 'glyphObject' attribute is read-
only), readGlyphFromString() will not propagate that
exception, but ignore that attribute.
To retrieve outline information, you need to pass an object
conforming to the PointPen protocol as the 'pointPen' argument.
This argument may be None if you don't need the outline data.
"""
tree = _glifTreeFromFile(StringIO(aString))
_readGlyphFromTree(tree, glyphObject, pointPen)
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, writer=None):
"""Return .glif data for a glyph as a UTF-8 encoded string.
The 'glyphObject' argument can be any kind of object (even None);
the writeGlyphToString() method will attempt to get the following
attributes from it:
"width" the advance with of the glyph
"unicodes" a list of unicode values for this glyph
"note" a string
"lib" a dictionary containing custom data
All attributes are optional: if 'glyphObject' doesn't
have the attribute, it will simply be skipped.
To write outline data to the .glif file, writeGlyphToString() needs
a function (any callable object actually) that will take one
argument: an object that conforms to the PointPen protocol.
The function will be called by writeGlyphToString(); it has to call the
proper PointPen methods to transfer the outline to the .glif file.
"""
if writer is None:
try:
from xmlWriter import XMLWriter
except ImportError:
# try the other location
from fontTools.misc.xmlWriter import XMLWriter
aFile = StringIO()
writer = XMLWriter(aFile, encoding="UTF-8")
else:
aFile = None
writer.begintag("glyph", [("name", glyphName), ("format", "1")])
writer.newline()
width = getattr(glyphObject, "width", None)
if width is not None:
if not isinstance(width, (int, float)):
raise GlifLibError, "width attribute must be int or float"
writer.simpletag("advance", width=repr(width))
writer.newline()
unicodes = getattr(glyphObject, "unicodes", None)
if unicodes:
if isinstance(unicodes, int):
unicodes = [unicodes]
for code in unicodes:
if not isinstance(code, int):
raise GlifLibError, "unicode values must be int"
hexCode = hex(code)[2:].upper()
if len(hexCode) < 4:
hexCode = "0" * (4 - len(hexCode)) + hexCode
writer.simpletag("unicode", hex=hexCode)
writer.newline()
note = getattr(glyphObject, "note", None)
if note is not None:
if not isinstance(note, (str, unicode)):
raise GlifLibError, "note attribute must be str or unicode"
note = note.encode('utf-8')
writer.begintag("note")
writer.newline()
for line in note.splitlines():
writer.write(line.strip())
writer.newline()
writer.endtag("note")
writer.newline()
if drawPointsFunc is not None:
writer.begintag("outline")
writer.newline()
pen = GLIFPointPen(writer)
drawPointsFunc(pen)
writer.endtag("outline")
writer.newline()
lib = getattr(glyphObject, "lib", None)
if lib:
from robofab.plistlib import PlistWriter
if not isinstance(lib, dict):
lib = dict(lib)
writer.begintag("lib")
writer.newline()
plistWriter = PlistWriter(writer.file, indentLevel=writer.indentlevel,
indent=writer.indentwhite, writeHeader=False)
plistWriter.writeValue(lib)
writer.endtag("lib")
writer.newline()
writer.endtag("glyph")
writer.newline()
if aFile is not None:
return aFile.getvalue()
else:
return None
# misc helper functions
def _stripGlyphXMLTree(nodes):
for element, attrs, children in nodes:
# "lib" is formatted as a plist, so we need unstripped
# character data so we can support strings with leading or
# trailing whitespace. Do strip everything else.
recursive = (element != "lib")
stripCharacterData(children, recursive=recursive)
def _glifTreeFromFile(aFile):
tree = buildTree(aFile, stripData=False)
stripCharacterData(tree[2], recursive=False)
assert tree[0] == "glyph"
_stripGlyphXMLTree(tree[2])
return tree
def _relaxedSetattr(object, attr, value):
try:
setattr(object, attr, value)
except AttributeError:
pass
def _number(s):
"""Given a numeric string, return an integer or a float, whichever
the string indicates. _number("1") will return the integer 1,
_number("1.0") will return the float 1.0.
"""
try:
n = int(s)
except ValueError:
n = float(s)
return n
def _readGlyphFromTree(tree, glyphObject=None, pointPen=None):
unicodes = []
assert tree[0] == "glyph"
formatVersion = int(tree[1].get("format", "0"))
if formatVersion not in (0, 1):
raise GlifLibError, "unsupported glif format version: %s" % formatVersion
glyphName = tree[1].get("name")
if glyphName and glyphObject is not None:
_relaxedSetattr(glyphObject, "name", glyphName)
for element, attrs, children in tree[2]:
if element == "outline":
if pointPen is not None:
if formatVersion == 0:
buildOutline_Format0(pointPen, children)
else:
buildOutline_Format1(pointPen, children)
elif glyphObject is None:
continue
elif element == "advance":
width = _number(attrs["width"])
_relaxedSetattr(glyphObject, "width", width)
elif element == "unicode":
unicodes.append(int(attrs["hex"], 16))
elif element == "note":
rawNote = "\n".join(children)
lines = rawNote.split("\n")
lines = [line.strip() for line in lines]
note = "\n".join(lines)
_relaxedSetattr(glyphObject, "note", note)
elif element == "lib":
from plistFromTree import readPlistFromTree
assert len(children) == 1
lib = readPlistFromTree(children[0])
_relaxedSetattr(glyphObject, "lib", lib)
if unicodes:
_relaxedSetattr(glyphObject, "unicodes", unicodes)
class _DoneParsing(Exception): pass
def _startElementHandler(tagName, attrs):
if tagName != "glyph":
# the top level element of any .glif file must be <glyph>
raise _DoneParsing(None)
glyphName = attrs["name"]
raise _DoneParsing(glyphName)
def _fetchGlyphName(glyphPath):
# Given a path to an existing .glif file, get the glyph name
# from the XML data.
from xml.parsers.expat import ParserCreate
p = ParserCreate()
p.StartElementHandler = _startElementHandler
p.returns_unicode = True
f = open(glyphPath)
try:
p.ParseFile(f)
except _DoneParsing, why:
glyphName = why.args[0]
if glyphName is None:
raise ValueError, (".glif file doen't have a <glyph> top-level "
"element: %r" % glyphPath)
else:
assert 0, "it's not expected that parsing the file ends normally"
return glyphName
def _fetchUnicodes(text):
# Given GLIF text, get a list of all unicode values from the XML data.
parser = _FetchUnicodesParser(text)
return parser.unicodes
class _FetchUnicodesParser(object):
def __init__(self, text):
from xml.parsers.expat import ParserCreate
self.unicodes = []
self._elementStack = []
parser = ParserCreate()
parser.returns_unicode = 0 # XXX, Don't remember why. It sucks, though.
parser.StartElementHandler = self.startElementHandler
parser.EndElementHandler = self.endElementHandler
parser.Parse(text)
def startElementHandler(self, name, attrs):
if name == "unicode" and len(self._elementStack) == 1 and self._elementStack[0] == "glyph":
value = attrs.get("hex")
value = int(value, 16)
self.unicodes.append(value)
self._elementStack.append(name)
def endElementHandler(self, name):
other = self._elementStack.pop(-1)
assert other == name
def buildOutline_Format0(pen, xmlNodes):
# This reads the "old" .glif format, retroactively named "format 0",
# later formats have a "format" attribute in the <glyph> element.
for element, attrs, children in xmlNodes:
if element == "contour":
pen.beginPath()
currentSegmentType = None
for subElement, attrs, dummy in children:
if subElement != "point":
continue
x = _number(attrs["x"])
y = _number(attrs["y"])
pointType = attrs.get("type", "onCurve")
if pointType == "bcp":
currentSegmentType = "curve"
elif pointType == "offCurve":
currentSegmentType = "qcurve"
elif currentSegmentType is None and pointType == "onCurve":
currentSegmentType = "line"
if pointType == "onCurve":
segmentType = currentSegmentType
currentSegmentType = None
else:
segmentType = None
smooth = attrs.get("smooth") == "yes"
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth)
pen.endPath()
elif element == "component":
baseGlyphName = attrs["base"]
transformation = []
for attr, default in _transformationInfo:
value = attrs.get(attr)
if value is None:
value = default
else:
value = _number(value)
transformation.append(value)
pen.addComponent(baseGlyphName, tuple(transformation))
elif element == "anchor":
name, x, y = attrs["name"], _number(attrs["x"]), _number(attrs["y"])
pen.beginPath()
pen.addPoint((x, y), segmentType="move", name=name)
pen.endPath()
def buildOutline_Format1(pen, xmlNodes):
for element, attrs, children in xmlNodes:
if element == "contour":
pen.beginPath()
for subElement, attrs, dummy in children:
if subElement != "point":
continue
x = _number(attrs["x"])
y = _number(attrs["y"])
segmentType = attrs.get("type", "offcurve")
if segmentType == "offcurve":
segmentType = None
smooth = attrs.get("smooth") == "yes"
name = attrs.get("name")
pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
pen.endPath()
elif element == "component":
baseGlyphName = attrs["base"]
transformation = []
for attr, default in _transformationInfo:
value = attrs.get(attr)
if value is None:
value = default
else:
value = _number(value)
transformation.append(value)
pen.addComponent(baseGlyphName, tuple(transformation))
_transformationInfo = [
# field name, default value
("xScale", 1),
("xyScale", 0),
("yxScale", 0),
("yScale", 1),
("xOffset", 0),
("yOffset", 0),
]
class GLIFPointPen(AbstractPointPen):
"""Helper class using the PointPen protocol to write the <outline>
part of .glif files.
"""
def __init__(self, xmlWriter):
self.writer = xmlWriter
def beginPath(self):
self.writer.begintag("contour")
self.writer.newline()
def endPath(self):
self.writer.endtag("contour")
self.writer.newline()
def addPoint(self, pt, segmentType=None, smooth=None, name=None, **kwargs):
attrs = []
if pt is not None:
for coord in pt:
if not isinstance(coord, (int, float)):
raise GlifLibError, "coordinates must be int or float"
attrs.append(("x", repr(pt[0])))
attrs.append(("y", repr(pt[1])))
if segmentType is not None:
attrs.append(("type", segmentType))
if smooth:
attrs.append(("smooth", "yes"))
if name is not None:
attrs.append(("name", name))
self.writer.simpletag("point", attrs)
self.writer.newline()
def addComponent(self, glyphName, transformation):
attrs = [("base", glyphName)]
for (attr, default), value in zip(_transformationInfo, transformation):
if not isinstance(value, (int, float)):
raise GlifLibError, "transformation values must be int or float"
if value != default:
attrs.append((attr, repr(value)))
self.writer.simpletag("component", attrs)
self.writer.newline()
if __name__ == "__main__":
from pprint import pprint
from robofab.pens.pointPen import PrintingPointPen
class TestGlyph: pass
gs = GlyphSet(".")
def drawPoints(pen):
pen.beginPath()
pen.addPoint((100, 200), name="foo")
pen.addPoint((200, 250), segmentType="curve", smooth=True)
pen.endPath()
pen.addComponent("a", (1, 0, 0, 1, 20, 30))
glyph = TestGlyph()
glyph.width = 120
glyph.unicodes = [1, 2, 3, 43215, 66666]
glyph.lib = {"a": "b", "c": [1, 2, 3, True]}
glyph.note = " hallo! "
if 0:
gs.writeGlyph("a", glyph, drawPoints)
g2 = TestGlyph()
gs.readGlyph("a", g2, PrintingPointPen())
pprint(g2.__dict__)
else:
s = writeGlyphToString("a", glyph, drawPoints)
print s
g2 = TestGlyph()
readGlyphFromString(s, g2, PrintingPointPen())
pprint(g2.__dict__)

View file

@ -0,0 +1,14 @@
"""
Directory for interface related modules. Stuff like widgets,
dialog modules. Please keep them sorted by platform.
interfaces/all : modules that are platform independent
interfaces/mac : modules that are mac specific
interfaces/win : modules that are windows specific
"""

View file

@ -0,0 +1,14 @@
"""
Directory for interface related modules. Stuff like widgets,
dialog modules. Please keep them sorted by platform.
interfaces/all : modules that are platform independent
interfaces/mac : modules that are mac specific
interfaces/win : modules that are windows specific
"""

View file

@ -0,0 +1,278 @@
"""
Restructured dialogs for Robofab
dialog file dialogs
* FontLab 5.04 10.6 dialogKit fl internal * theoretically that should work under windows as well, untested
* FontLab 5.1 10.6 dialogKit fl internal
* FontLab 5.1 10.7 raw cocao fl internal
Glyphs any vanilla vanilla
RoboFont any vanilla vanilla
This module does a fair amount of sniffing in order to guess
which dialogs to load. Linux and Windows environments are
underrepresented at the moment. Following the prototypes in dialogs_default.py
it is possible (with knowledge of the platform) to extend support.
The platformApplicationSupport table contains very specific
versions with which certain apps need to work. Moving forward,
it is likely that these versions will change and need to be updated.
# this calls the new dialogs infrastructure:
from robofab.interface.all.dialogs import Message
# this calls the old original legacy dialogs infrastructure:
from robofab.interface.all.dialogs_legacy import Message
"""
# determine platform and application
import sys, os
import platform as _platform
__verbose__ = False
platform = None
platformVersion = None
application = None
applicationVersion = None
if sys.platform in (
'mac',
'darwin',
):
platform = "mac"
v = _platform.mac_ver()[0]
platformVersion = float('.'.join(v.split('.')[:2]))
elif sys.platform in (
'linux1',
'linux2', # Ubuntu = others?
):
platform = "linux"
elif os.name == 'nt':
platform = "win"
# determine application
try:
# FontLab
# alternative syntax to cheat on the FL import filtering in RF
__import__("FL")
application = "fontlab"
#applicationVersion = fl.version
except ImportError:
pass
if application is None:
try:
# RoboFont
import mojo
application = 'robofont'
try:
from AppKit import NSBundle
b = NSBundle.mainBundle()
applicationVersion = b.infoDictionary()["CFBundleVersion"]
except ImportError:
pass
except ImportError:
pass
if application is None:
try:
# Glyphs
import GlyphsApp
application = "glyphs"
except ImportError:
pass
if application is None:
try:
# fontforge
# note that in some configurations, fontforge can be imported in other pythons as well
# so the availability of the fontforge module is no garuantee that we are in fontforge.
import fontforge
application = "fontforge"
except ImportError:
pass
pyVersion = sys.version_info[:3]
# with that out of the way, perhaps we can have a look at where we are
# and which modules we have available. This maps any number of platform / application
# combinations so an independent list of module names. That should make it
# possible to map multiple things to one module.
platformApplicationSupport = [
#
# switchboard for platform, application, python version -> dialog implementations
# platform applicatiom python sub module
# | | | |
('mac', 'fontlab', (2,3,5), "dialogs_fontlab_legacy1"),
# because FontLab 5.01 and earlier on 2.3.5 can run EasyDialogs
# | | | |
# because FontLab 5.1 on mac 10.6 should theoretically be able to run cocoa dialogs,
# but they are very unreliable. So until we know what's going on, FL5.1 on 10.6
# is going to have to live with DialogKit dialogs.
# | | | |
('mac', 'fontlab', None, "dialogs_fontlab_legacy2"),
# because FontLab 5.1 on mac, 10.7+ should run cocoa / vanilla
# | | | |
('mac', None, None, "dialogs_mac_vanilla"),
# perhaps nonelab scripts can run vanilla as well?
# | | | |
('win', None, None, "dialogs_legacy"),
# older windows stuff might be able to use the legacy dialogs
]
platformModule = None
foundPlatformModule = False
dialogs = {}
if __verbose__:
print "robofab.interface.all __init__ - finding out where we were."
# do we have a support module?
for pl, app, py, platformApplicationModuleName in platformApplicationSupport:
if __verbose__:
print "looking at", pl, app, py, platformApplicationModuleName
if pl is None or pl == platform:
if app is None or app == application:
if py is None or py == pyVersion:
break
if __verbose__:
print "nope"
if __verbose__:
print "searched for", pl, app, py, platformApplicationModuleName
# preload the namespace with default functions that do nothing but raise NotImplementedError
from robofab.interface.all.dialogs_default import *
# now import the module we selected.
if platformApplicationModuleName == "dialogs_fontlab_legacy1":
try:
from robofab.interface.all.dialogs_fontlab_legacy1 import *
foundPlatformModule = True
if __verbose__:
print "loaded robofab.interface.all.dialogs_fontlab_legacy1"
if platform == "mac":
from robofab.interface.mac.getFileOrFolder import GetFile, GetFileOrFolder
except ImportError:
print "can't import", platformApplicationModuleName
elif platformApplicationModuleName == "dialogs_fontlab_legacy2":
try:
from robofab.interface.all.dialogs_fontlab_legacy2 import *
foundPlatformModule = True
if __verbose__:
print "loaded robofab.interface.all.dialogs_fontlab_legacy2"
if platform == "mac":
#
#
#
#
#
from robofab.interface.all.dialogs_legacy import AskString, TwoChecks, TwoFields, SelectGlyph, FindGlyph, OneList, SearchList, SelectFont, SelectGlyph
except ImportError:
print "can't import", platformApplicationModuleName
elif platformApplicationModuleName == "dialogs_mac_vanilla":
try:
from robofab.interface.all.dialogs_mac_vanilla import *
foundPlatformModule = True
if __verbose__:
print "loaded robofab.interface.all.dialogs_mac_vanilla"
except ImportError:
print "can't import", platformApplicationModuleName
elif platformApplicationModuleName == "dialogs_legacy":
try:
from robofab.interface.all.dialogs_legacy import *
foundPlatformModule = True
if __verbose__:
print "loaded robofab.interface.all.dialogs_legacy"
except ImportError:
print "can't import", platformApplicationModuleName
__all__ = [
"AskString",
"AskYesNoCancel",
"FindGlyph",
"GetFile",
"GetFolder",
"GetFileOrFolder",
"Message",
"OneList",
"PutFile",
"SearchList",
"SelectFont",
"SelectGlyph",
"TwoChecks",
"TwoFields",
"ProgressBar",
]
def test():
""" This is a test that prints the available functions and where they're imported from.
The report can be useful for debugging.
For instance:
from robofab.interface.all.dialogs import test
test()
testing RoboFab Dialogs:
python version: (2, 7, 1)
platform: mac
application: None
applicationVersion: None
platformVersion: 10.7
looking for module: dialogs_mac_vanilla
did we find it? True
Available dialogs and source:
AskString robofab.interface.all.dialogs_mac_vanilla
AskYesNoCancel robofab.interface.all.dialogs_mac_vanilla
FindGlyph robofab.interface.all.dialogs_mac_vanilla
GetFile robofab.interface.all.dialogs_mac_vanilla
GetFolder robofab.interface.all.dialogs_mac_vanilla
GetFileOrFolder robofab.interface.all.dialogs_mac_vanilla
Message robofab.interface.all.dialogs_mac_vanilla
OneList robofab.interface.all.dialogs_mac_vanilla
PutFile robofab.interface.all.dialogs_mac_vanilla
SearchList robofab.interface.all.dialogs_mac_vanilla
SelectFont robofab.interface.all.dialogs_mac_vanilla
SelectGlyph robofab.interface.all.dialogs_mac_vanilla
TwoChecks robofab.interface.all.dialogs_default
TwoFields robofab.interface.all.dialogs_default
ProgressBar robofab.interface.all.dialogs_mac_vanilla
"""
print
print "testing RoboFab Dialogs:"
print "\tpython version:", pyVersion
print "\tplatform:", platform
print "\tapplication:", application
print "\tapplicationVersion:", applicationVersion
print "\tplatformVersion:", platformVersion
print "\tlooking for module:", platformApplicationModuleName
print "\t\tdid we find it?", foundPlatformModule
print
print "Available dialogs and source:"
for name in __all__:
if name in globals().keys():
print "\t", name, "\t", globals()[name].__module__
else:
print "\t", name, "\t not loaded."
if __name__ == "__main__":
test()

View file

@ -0,0 +1,76 @@
"""
Dialog prototypes.
These are loaded before any others. So if a specific platform implementation doesn't
have all functions, these will make sure a NotImplemtedError is raised.
http://www.robofab.org/tools/dialogs.html
"""
__all__ = [
"AskString",
"AskYesNoCancel",
"FindGlyph",
"GetFile",
"GetFolder",
"GetFileOrFolder",
"Message",
"OneList",
"PutFile",
"SearchList",
"SelectFont",
"SelectGlyph",
"TwoChecks",
"TwoFields",
"ProgressBar",
]
# start with all the defaults.
def AskString(message, value='', title='RoboFab'):
raise NotImplementedError
def AskYesNoCancel(message, title='RoboFab', default=0):
raise NotImplementedError
def FindGlyph(font, message="Search for a glyph:", title='RoboFab'):
raise NotImplementedError
def GetFile(message=None):
raise NotImplementedError
def GetFolder(message=None):
raise NotImplementedError
def GetFileOrFolder(message=None):
raise NotImplementedError
def Message(message, title='RoboFab'):
raise NotImplementedError
def OneList(list, message="Select an item:", title='RoboFab'):
raise PendingDeprecationWarning
def PutFile(message=None, fileName=None):
raise NotImplementedError
def SearchList(list, message="Select an item:", title='RoboFab'):
raise NotImplementedError
def SelectFont(message="Select a font:", title='RoboFab'):
raise NotImplementedError
def SelectGlyph(font, message="Select a glyph:", title='RoboFab'):
raise NotImplementedError
def TwoChecks(title_1="One", title_2="Two", value1=1, value2=1, title='RoboFab'):
raise PendingDeprecationWarning
def TwoFields(title_1="One:", value_1="0", title_2="Two:", value_2="0", title='RoboFab'):
raise PendingDeprecationWarning
class ProgressBar(object):
pass

View file

@ -0,0 +1,73 @@
"""
Dialogs for FontLab < 5.1.
This one should be loaded for various platforms, using dialogKit
http://www.robofab.org/tools/dialogs.html
"""
from FL import *
from dialogKit import ModalDialog, Button, TextBox, EditText
__all__ = [
#"AskString",
#"AskYesNoCancel",
#"FindGlyph",
"GetFile",
"GetFolder",
#"Message",
#"OneList",
#"PutFile",
#"SearchList",
#"SelectFont",
#"SelectGlyph",
#"TwoChecks",
#"TwoFields",
"ProgressBar",
]
def GetFile(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None):
strFilter = "All Files (*.*)|*.*|"
defaultExt = ""
# using fontlab's internal file dialogs
return fl.GetFileName(1, defaultExt, message, strFilter)
def GetFolder(message=None, title=None, directory=None, allowsMultipleSelection=False):
# using fontlab's internal file dialogs
if message is None:
message = ""
return fl.GetPathName(message)
def PutFile(message=None, fileName=None):
# using fontlab's internal file dialogs
# message is not used
if message is None:
message = ""
if fileName is None:
fileName = ""
defaultExt = ""
return fl.GetFileName(0, defaultExt, fileName, '')
class ProgressBar(object):
def __init__(self, title="RoboFab...", ticks=0, label=""):
self._tickValue = 1
fl.BeginProgress(title, ticks)
def getCurrentTick(self):
return self._tickValue
def tick(self, tickValue=None):
if not tickValue:
tickValue = self._tickValue
fl.TickProgress(tickValue)
self._tickValue = tickValue + 1
def label(self, label):
pass
def close(self):
fl.EndProgress()

View file

@ -0,0 +1,373 @@
"""
Dialogs for FontLab 5.1.
This might work in future versions of FontLab as well.
This is basically a butchered version of vanilla.dialogs.
No direct import of, or dependency on Vanilla
March 7 2012
It seems only the dialogs that deal with the file system
need to be replaced, the other dialogs still work.
As we're not entirely sure whether it is worth to maintain
these dialogs, let's fix the imports in dialogs.py.
This is the phenolic aldehyde version of dialogs.
"""
#__import__("FL")
from FL import *
from Foundation import NSObject
from AppKit import NSApplication, NSInformationalAlertStyle, objc, NSAlert, NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, NSAlertThirdButtonReturn, NSSavePanel, NSOKButton, NSOpenPanel
NSApplication.sharedApplication()
__all__ = [
# "AskString",
"AskYesNoCancel",
# "FindGlyph",
"GetFile",
"GetFolder",
"GetFileOrFolder",
"Message",
# "OneList",
"PutFile",
# "SearchList",
# "SelectFont",
# "SelectGlyph",
# "TwoChecks",
# "TwoFields",
"ProgressBar",
]
class BaseMessageDialog(NSObject):
def initWithMessageText_informativeText_alertStyle_buttonTitlesValues_window_resultCallback_(self,
messageText="",
informativeText="",
alertStyle=NSInformationalAlertStyle,
buttonTitlesValues=None,
parentWindow=None,
resultCallback=None):
if buttonTitlesValues is None:
buttonTitlesValues = []
self = super(BaseMessageDialog, self).init()
self.retain()
self._resultCallback = resultCallback
self._buttonTitlesValues = buttonTitlesValues
#
alert = NSAlert.alloc().init()
alert.setMessageText_(messageText)
alert.setInformativeText_(informativeText)
alert.setAlertStyle_(alertStyle)
for buttonTitle, value in buttonTitlesValues:
alert.addButtonWithTitle_(buttonTitle)
self._value = None
code = alert.runModal()
self._translateValue(code)
return self
def _translateValue(self, code):
if code == NSAlertFirstButtonReturn:
value = 1
elif code == NSAlertSecondButtonReturn:
value = 2
elif code == NSAlertThirdButtonReturn:
value = 3
else:
value = code - NSAlertThirdButtonReturn + 3
self._value = self._buttonTitlesValues[value-1][1]
def windowWillClose_(self, notification):
self.autorelease()
class BasePutGetPanel(NSObject):
def initWithWindow_resultCallback_(self, parentWindow=None, resultCallback=None):
self = super(BasePutGetPanel, self).init()
self.retain()
self._parentWindow = parentWindow
self._resultCallback = resultCallback
return self
def windowWillClose_(self, notification):
self.autorelease()
class PutFilePanel(BasePutGetPanel):
def initWithWindow_resultCallback_(self, parentWindow=None, resultCallback=None):
self = super(PutFilePanel, self).initWithWindow_resultCallback_(parentWindow, resultCallback)
self.messageText = None
self.title = None
self.fileTypes = None
self.directory = None
self.fileName = None
self.canCreateDirectories = True
self.accessoryView = None
self._result = None
return self
def run(self):
panel = NSSavePanel.alloc().init()
if self.messageText:
panel.setMessage_(self.messageText)
if self.title:
panel.setTitle_(self.title)
if self.directory:
panel.setDirectory_(self.directory)
if self.fileTypes:
panel.setAllowedFileTypes_(self.fileTypes)
panel.setCanCreateDirectories_(self.canCreateDirectories)
panel.setCanSelectHiddenExtension_(True)
panel.setAccessoryView_(self.accessoryView)
if self._parentWindow is not None:
panel.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
self.directory, self.fileName, self._parentWindow, self, "savePanelDidEnd:returnCode:contextInfo:", 0)
else:
isOK = panel.runModalForDirectory_file_(self.directory, self.fileName)
if isOK == NSOKButton:
self._result = panel.filename()
def savePanelDidEnd_returnCode_contextInfo_(self, panel, returnCode, context):
panel.close()
if returnCode:
self._result = panel.filename()
if self._resultCallback is not None:
self._resultCallback(self._result)
savePanelDidEnd_returnCode_contextInfo_ = objc.selector(savePanelDidEnd_returnCode_contextInfo_, signature="v@:@ii")
class GetFileOrFolderPanel(BasePutGetPanel):
def initWithWindow_resultCallback_(self, parentWindow=None, resultCallback=None):
self = super(GetFileOrFolderPanel, self).initWithWindow_resultCallback_(parentWindow, resultCallback)
self.messageText = None
self.title = None
self.directory = None
self.fileName = None
self.fileTypes = None
self.allowsMultipleSelection = False
self.canChooseDirectories = True
self.canChooseFiles = True
self.resolvesAliases = True
self._result = None
return self
def run(self):
panel = NSOpenPanel.alloc().init()
if self.messageText:
panel.setMessage_(self.messageText)
if self.title:
panel.setTitle_(self.title)
if self.directory:
panel.setDirectory_(self.directory)
if self.fileTypes:
panel.setAllowedFileTypes_(self.fileTypes)
panel.setCanChooseDirectories_(self.canChooseDirectories)
panel.setCanChooseFiles_(self.canChooseFiles)
panel.setAllowsMultipleSelection_(self.allowsMultipleSelection)
panel.setResolvesAliases_(self.resolvesAliases)
if self._parentWindow is not None:
panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
self.directory, self.fileName, self.fileTypes, self._parentWindow, self, "openPanelDidEnd:returnCode:contextInfo:", 0)
else:
isOK = panel.runModalForDirectory_file_types_(self.directory, self.fileName, self.fileTypes)
if isOK == NSOKButton:
self._result = panel.filenames()
def openPanelDidEnd_returnCode_contextInfo_(self, panel, returnCode, context):
panel.close()
if returnCode:
self._result = panel.filenames()
if self._resultCallback is not None:
self._resultCallback(self._result)
openPanelDidEnd_returnCode_contextInfo_ = objc.selector(openPanelDidEnd_returnCode_contextInfo_, signature="v@:@ii")
def Message(message="", title='noLongerUsed', informativeText=""):
"""Legacy robofab dialog compatible wrapper."""
#def _message(messageText="", informativeText="", alertStyle=NSInformationalAlertStyle, parentWindow=None, resultCallback=None):
resultCallback = None
alert = BaseMessageDialog.alloc().initWithMessageText_informativeText_alertStyle_buttonTitlesValues_window_resultCallback_(
messageText=message,
informativeText=informativeText,
alertStyle=NSInformationalAlertStyle,
buttonTitlesValues=[("OK", 1)],
parentWindow=None,
resultCallback=None)
if resultCallback is None:
return 1
def AskYesNoCancel(message, title='noLongerUsed', default=None, informativeText=""):
"""
AskYesNoCancel Dialog
message the string
title* a title of the window
(may not be supported everywhere)
default* index number of which button should be default
(i.e. respond to return)
informativeText* A string with secundary information
* may not be supported everywhere
"""
parentWindow = None
alert = BaseMessageDialog.alloc().initWithMessageText_informativeText_alertStyle_buttonTitlesValues_window_resultCallback_(
messageText=message,
informativeText=informativeText,
alertStyle=NSInformationalAlertStyle,
buttonTitlesValues=[("Cancel", -1), ("Yes", 1), ("No", 0)],
parentWindow=None,
resultCallback=None)
return alert._value
def _askYesNo(messageText="", informativeText="", alertStyle=NSInformationalAlertStyle, parentWindow=None, resultCallback=None):
parentWindow = None
alert = BaseMessageDialog.alloc().initWithMessageText_informativeText_alertStyle_buttonTitlesValues_window_resultCallback_(
messageText=messageText, informativeText=informativeText, alertStyle=alertStyle, buttonTitlesValues=[("Yes", 1), ("No", 0)], parentWindow=parentWindow, resultCallback=resultCallback)
if resultCallback is None:
return alert._value
def GetFile(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None):
""" Legacy robofab dialog compatible wrapper.
This will select UFO on OSX 10.7, FL5.1
"""
parentWindow = None
resultCallback=None
basePanel = GetFileOrFolderPanel.alloc().initWithWindow_resultCallback_(parentWindow, resultCallback)
basePanel.messageText = message
basePanel.title = title
basePanel.directory = directory
basePanel.fileName = fileName
basePanel.fileTypes = fileTypes
basePanel.allowsMultipleSelection = allowsMultipleSelection
basePanel.canChooseDirectories = False
basePanel.canChooseFiles = True
basePanel.run()
if basePanel._result is None:
return None
if not allowsMultipleSelection:
# compatibly return only one as we expect
return str(list(basePanel._result)[0])
else:
# return more if we explicitly expect
return [str(n) for n in list(basePanel._result)]
def GetFolder(message=None, title=None, directory=None, allowsMultipleSelection=False):
parentWindow = None
resultCallback = None
basePanel = GetFileOrFolderPanel.alloc().initWithWindow_resultCallback_(parentWindow, resultCallback)
basePanel.messageText = message
basePanel.title = title
basePanel.directory = directory
basePanel.allowsMultipleSelection = allowsMultipleSelection
basePanel.canChooseDirectories = True
basePanel.canChooseFiles = False
basePanel.run()
if basePanel._result is None:
return None
if not allowsMultipleSelection:
# compatibly return only one as we expect
return str(list(basePanel._result)[0])
else:
# return more if we explicitly expect
return [str(n) for n in list(basePanel._result)]
def GetFileOrFolder(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None, parentWindow=None, resultCallback=None):
parentWindow = None
basePanel = GetFileOrFolderPanel.alloc().initWithWindow_resultCallback_(parentWindow, resultCallback)
basePanel.messageText = message
basePanel.title = title
basePanel.directory = directory
basePanel.fileName = fileName
basePanel.fileTypes = fileTypes
basePanel.allowsMultipleSelection = allowsMultipleSelection
basePanel.canChooseDirectories = True
basePanel.canChooseFiles = True
basePanel.run()
if basePanel._result is None:
return None
if not allowsMultipleSelection:
# compatibly return only one as we expect
return str(list(basePanel._result)[0])
else:
# return more if we explicitly expect
return [str(n) for n in list(basePanel._result)]
def PutFile(message=None, title=None, directory=None, fileName=None, canCreateDirectories=True, fileTypes=None):
parentWindow = None
resultCallback=None
accessoryView=None
basePanel = PutFilePanel.alloc().initWithWindow_resultCallback_(parentWindow, resultCallback)
basePanel.messageText = message
basePanel.title = title
basePanel.directory = directory
basePanel.fileName = fileName
basePanel.fileTypes = fileTypes
basePanel.canCreateDirectories = canCreateDirectories
basePanel.accessoryView = accessoryView
basePanel.run()
return str(basePanel._result)
class ProgressBar(object):
def __init__(self, title="RoboFab...", ticks=0, label=""):
self._tickValue = 1
fl.BeginProgress(title, ticks)
def getCurrentTick(self):
return self._tickValue
def tick(self, tickValue=None):
if not tickValue:
tickValue = self._tickValue
fl.TickProgress(tickValue)
self._tickValue = tickValue + 1
def label(self, label):
pass
def close(self):
fl.EndProgress()
# we seem to have problems importing from here.
# so let's see what happens if we make the robofab compatible wrappers here as well.
# start with all the defaults.
#def AskString(message, value='', title='RoboFab'):
# raise NotImplementedError
#def FindGlyph(aFont, message="Search for a glyph:", title='RoboFab'):
# raise NotImplementedError
#def OneList(list, message="Select an item:", title='RoboFab'):
# raise NotImplementedError
#def PutFile(message=None, fileName=None):
# raise NotImplementedError
#def SearchList(list, message="Select an item:", title='RoboFab'):
# raise NotImplementedError
#def SelectFont(message="Select a font:", title='RoboFab'):
# raise NotImplementedError
#def SelectGlyph(font, message="Select a glyph:", title='RoboFab'):
# raise NotImplementedError
#def TwoChecks(title_1="One", title_2="Two", value1=1, value2=1, title='RoboFab'):
# raise NotImplementedError
#def TwoFields(title_1="One:", value_1="0", title_2="Two:", value_2="0", title='RoboFab'):
# raise NotImplementedError

View file

@ -0,0 +1,737 @@
"""
Dialogs.
Cross-platform and cross-application compatible. Some of them anyway.
(Not all dialogs work on PCs outside of FontLab. Some dialogs are for FontLab only. Sorry.)
Mac and FontLab implementation written by the RoboFab development team.
PC implementation by Eigi Eigendorf and is (C)2002 Eigi Eigendorf.
"""
import os
import sys
from robofab import RoboFabError
from warnings import warn
MAC = False
PC = False
haveMacfs = False
if sys.platform in ('mac', 'darwin'):
MAC = True
elif os.name == 'nt':
PC = True
else:
warn("dialogs.py only supports Mac and PC platforms.")
pyVersion = sys.version_info[:3]
inFontLab = False
try:
from FL import *
inFontLab = True
except ImportError: pass
try:
import W
hasW = True
except ImportError:
hasW = False
try:
import dialogKit
hasDialogKit = True
except ImportError:
hasDialogKit = False
try:
import EasyDialogs
hasEasyDialogs = True
except:
hasEasyDialogs = False
if MAC:
if pyVersion < (2, 3, 0):
import macfs
haveMacfs = True
elif PC and not inFontLab:
from win32com.shell import shell
import win32ui
import win32con
def _raisePlatformError(dialog):
"""error raiser"""
if MAC:
p = 'Macintosh'
elif PC:
p = 'PC'
else:
p = sys.platform
raise RoboFabError("%s is not currently available on the %s platform"%(dialog, p))
class _FontLabDialogOneList:
"""A one list dialog for FontLab. This class should not be called directly. Use the OneList function."""
def __init__(self, list, message, title='RoboFab'):
self.message = message
self.selected = None
self.list = list
self.d = Dialog(self)
self.d.size = Point(250, 250)
self.d.title = title
self.d.Center()
self.d.AddControl(LISTCONTROL, Rect(12, 30, 238, 190), "list", STYLE_LIST, self.message)
self.list_index = 0
def Run(self):
return self.d.Run()
def on_cancel(self, code):
self.selected = None
def on_ok(self, code):
self.d.GetValue('list')
# Since FLS v5.2, the GetValue() method of the Dialog() class returns
# a 'wrong' index value from the specified LISTCONTROL.
# If the selected index is n, it will return n-1. For example, when
# the index is 1, it returns 0; when it's 2, it returns 1, and so on.
# If the selection is empty, FLS v5.2 returns -2, while the old v5.0
# returned None.
# See also:
# - http://forum.fontlab.com/index.php?topic=8807.0
# - http://forum.fontlab.com/index.php?topic=9003.0
#
# Edited based on feedback from Adam Twardoch
if fl.buildnumber > 4600 and sys.platform == 'win32':
if self.list_index == -2:
self.selected = None
else:
self.selected = self.list_index + 1
else:
self.selected = self.list_index
class _FontLabDialogSearchList:
"""A dialog for searching through a list. It contains a text field and a results list FontLab. This class should not be called directly. Use the SearchList function."""
def __init__(self, aList, message, title="RoboFab"):
self.d = Dialog(self)
self.d.size = Point(250, 290)
self.d.title = title
self.d.Center()
self.message = message
self._fullContent = aList
self.possibleHits = list(aList)
self.possibleHits.sort()
self.possibleHits_index = 0
self.entryField = ""
self.selected = None
self.d.AddControl(STATICCONTROL, Rect(10, 10, 240, 30), "message", STYLE_LABEL, message)
self.d.AddControl(EDITCONTROL, Rect(10, 30, 240, aAUTO), "entryField", STYLE_EDIT, "")
self.d.AddControl(LISTCONTROL, Rect(12, 60, 238, 230), "possibleHits", STYLE_LIST, "")
def run(self):
self.d.Run()
def on_entryField(self, code):
self.d.GetValue("entryField")
entry = self.entryField
count = len(entry)
possibleHits = [
i for i in self._fullContent
if len(i) >= count
and i[:count] == entry
]
possibleHits.sort()
self.possibleHits = possibleHits
self.possibleHits_index = 0
self.d.PutValue("possibleHits")
def on_ok(self, code):
self.d.GetValue("possibleHits")
sel = self.possibleHits_index
if sel == -1:
self.selected = None
else:
self.selected = self.possibleHits[sel]
def on_cancel(self, code):
self.selected = None
class _FontLabDialogTwoFields:
"""A two field dialog for FontLab. This class should not be called directly. Use the TwoFields function."""
def __init__(self, title_1, value_1, title_2, value_2, title='RoboFab'):
self.d = Dialog(self)
self.d.size = Point(200, 125)
self.d.title = title
self.d.Center()
self.d.AddControl(EDITCONTROL, Rect(120, 10, aIDENT2, aAUTO), "v1edit", STYLE_EDIT, title_1)
self.d.AddControl(EDITCONTROL, Rect(120, 40, aIDENT2, aAUTO), "v2edit", STYLE_EDIT, title_2)
self.v1edit = value_1
self.v2edit = value_2
def Run(self):
return self.d.Run()
def on_cancel(self, code):
self.v1edit = None
self.v2edit = None
def on_ok(self, code):
self.d.GetValue("v1edit")
self.d.GetValue("v2edit")
self.v1 = self.v1edit
self.v2 = self.v2edit
class _FontLabDialogTwoChecks:
"""A two check box dialog for FontLab. This class should not be called directly. Use the TwoChecks function."""
def __init__(self, title_1, title_2, value1=1, value2=1, title='RoboFab'):
self.d = Dialog(self)
self.d.size = Point(200, 105)
self.d.title = title
self.d.Center()
self.d.AddControl(CHECKBOXCONTROL, Rect(10, 10, aIDENT2, aAUTO), "check1", STYLE_CHECKBOX, title_1)
self.d.AddControl(CHECKBOXCONTROL, Rect(10, 30, aIDENT2, aAUTO), "check2", STYLE_CHECKBOX, title_2)
self.check1 = value1
self.check2 = value2
def Run(self):
return self.d.Run()
def on_cancel(self, code):
self.check1 = None
self.check2 = None
def on_ok(self, code):
self.d.GetValue("check1")
self.d.GetValue("check2")
class _FontLabDialogAskString:
"""A one simple string prompt dialog for FontLab. This class should not be called directly. Use the GetString function."""
def __init__(self, message, value, title='RoboFab'):
self.d = Dialog(self)
self.d.size = Point(350, 130)
self.d.title = title
self.d.Center()
self.d.AddControl(STATICCONTROL, Rect(aIDENT, aIDENT, aIDENT, aAUTO), "label", STYLE_LABEL, message)
self.d.AddControl(EDITCONTROL, Rect(aIDENT, 40, aIDENT, aAUTO), "value", STYLE_EDIT, '')
self.value=value
def Run(self):
return self.d.Run()
def on_cancel(self, code):
self.value = None
def on_ok(self, code):
self.d.GetValue("value")
class _FontLabDialogMessage:
"""A simple message dialog for FontLab. This class should not be called directly. Use the SimpleMessage function."""
def __init__(self, message, title='RoboFab'):
self.d = Dialog(self)
self.d.size = Point(350, 130)
self.d.title = title
self.d.Center()
self.d.AddControl(STATICCONTROL, Rect(aIDENT, aIDENT, aIDENT, 80), "label", STYLE_LABEL, message)
def Run(self):
return self.d.Run()
class _FontLabDialogGetYesNoCancel:
"""A yes no cancel message dialog for FontLab. This class should not be called directly. Use the YesNoCancel function."""
def __init__(self, message, title='RoboFab'):
self.d = Dialog(self)
self.d.size = Point(350, 130)
self.d.title = title
self.d.Center()
self.d.ok = 'Yes'
self.d.AddControl(STATICCONTROL, Rect(aIDENT, aIDENT, aIDENT, 80), "label", STYLE_LABEL, message)
self.d.AddControl(BUTTONCONTROL, Rect(100, 95, 172, 115), "button", STYLE_BUTTON, "No")
self.value = 0
def Run(self):
return self.d.Run()
def on_ok(self, code):
self.value = 1
def on_cancel(self, code):
self.value = -1
def on_button(self, code):
self.value = 0
self.d.End()
class _MacOneListW:
"""A one list dialog for Macintosh. This class should not be called directly. Use the OneList function."""
def __init__(self, list, message='Make a selection'):
import W
self.list = list
self.selected = None
self.w = W.ModalDialog((200, 240))
self.w.message = W.TextBox((10, 10, -10, 30), message)
self.w.list = W.List((10, 35, -10, -50), list)
self.w.l = W.HorizontalLine((10, -40, -10, 1), 1)
self.w.cancel = W.Button((10, -30, 87, -10), 'Cancel', self.cancel)
self.w.ok = W.Button((102, -30, 88, -10), 'OK', self.ok)
self.w.setdefaultbutton(self.w.ok)
self.w.bind('cmd.', self.w.cancel.push)
self.w.open()
def ok(self):
if len(self.w.list.getselection()) == 1:
self.selected = self.w.list.getselection()[0]
self.w.close()
def cancel(self):
self.selected = None
self.w.close()
class _MacTwoChecksW:
""" Version using W """
def __init__(self, title_1, title_2, value1=1, value2=1, title='RoboFab'):
import W
self.check1 = value1
self.check2 = value2
self.w = W.ModalDialog((200, 100))
self.w.check1 = W.CheckBox((10, 10, -10, 16), title_1, value=value1)
self.w.check2 = W.CheckBox((10, 35, -10, 16), title_2, value=value2)
self.w.l = W.HorizontalLine((10, 60, -10, 1), 1)
self.w.cancel = W.Button((10, 70, 85, 20), 'Cancel', self.cancel)
self.w.ok = W.Button((105, 70, 85, 20), 'OK', self.ok)
self.w.setdefaultbutton(self.w.ok)
self.w.bind('cmd.', self.w.cancel.push)
self.w.open()
def ok(self):
self.check1 = self.w.check1.get()
self.check2 = self.w.check2.get()
self.w.close()
def cancel(self):
self.check1 = None
self.check2 = None
self.w.close()
class ProgressBar:
def __init__(self, title='RoboFab...', ticks=0, label=''):
"""
A progress bar.
Availability: FontLab, Mac
"""
self._tickValue = 1
if inFontLab:
fl.BeginProgress(title, ticks)
elif MAC and hasEasyDialogs:
import EasyDialogs
self._bar = EasyDialogs.ProgressBar(title, maxval=ticks, label=label)
else:
_raisePlatformError('Progress')
def getCurrentTick(self):
return self._tickValue
def tick(self, tickValue=None):
"""
Tick the progress bar.
Availability: FontLab, Mac
"""
if not tickValue:
tickValue = self._tickValue
if inFontLab:
fl.TickProgress(tickValue)
elif MAC:
self._bar.set(tickValue)
else:
pass
self._tickValue = tickValue + 1
def label(self, label):
"""
Set the label on the progress bar.
Availability: Mac
"""
if inFontLab:
pass
elif MAC:
self._bar.label(label)
else:
pass
def close(self):
"""
Close the progressbar.
Availability: FontLab, Mac
"""
if inFontLab:
fl.EndProgress()
elif MAC:
del self._bar
else:
pass
def SelectFont(message="Select a font:", title='RoboFab'):
"""
Returns font instance if there is one, otherwise it returns None.
Availability: FontLab
"""
from robofab.world import RFont
if inFontLab:
list = []
for i in range(fl.count):
list.append(fl[i].full_name)
name = OneList(list, message, title)
if name is None:
return None
else:
return RFont(fl[list.index(name)])
else:
_raisePlatformError('SelectFont')
def SelectGlyph(font, message="Select a glyph:", title='RoboFab'):
"""
Returns glyph instance if there is one, otherwise it returns None.
Availability: FontLab
"""
from fontTools.misc.textTools import caselessSort
if inFontLab:
tl = font.keys()
list = caselessSort(tl)
glyphname = OneList(list, message, title)
if glyphname is None:
return None
else:
return font[glyphname]
else:
_raisePlatformError('SelectGlyph')
def FindGlyph(font, message="Search for a glyph:", title='RoboFab'):
"""
Returns glyph instance if there is one, otherwise it returns None.
Availability: FontLab
"""
if inFontLab:
glyphname = SearchList(font.keys(), message, title)
if glyphname is None:
return None
else:
return font[glyphname]
else:
_raisePlatformError('SelectGlyph')
def OneList(list, message="Select an item:", title='RoboFab'):
"""
Returns selected item, otherwise it returns None.
Availability: FontLab, Macintosh
"""
if inFontLab:
ol = _FontLabDialogOneList(list, message)
ol.Run()
selected = ol.selected
if selected is None:
return None
else:
try:
return list[selected]
except:
return None
elif MAC:
if hasW:
d = _MacOneListW(list, message)
sel = d.selected
if sel is None:
return None
else:
return list[sel]
else:
_raisePlatformError('OneList')
elif PC:
_raisePlatformError('OneList')
def SearchList(list, message="Select an item:", title='RoboFab'):
"""
Returns selected item, otherwise it returns None.
Availability: FontLab
"""
if inFontLab:
sl = _FontLabDialogSearchList(list, message, title)
sl.run()
selected = sl.selected
if selected is None:
return None
else:
return selected
else:
_raisePlatformError('SearchList')
def TwoFields(title_1="One:", value_1="0", title_2="Two:", value_2="0", title='RoboFab'):
"""
Returns (value 1, value 2).
Availability: FontLab
"""
if inFontLab:
tf = _FontLabDialogTwoFields(title_1, value_1, title_2, value_2, title)
tf.Run()
try:
v1 = tf.v1
v2 = tf.v2
return (v1, v2)
except:
return None
else:
_raisePlatformError('TwoFields')
def TwoChecks(title_1="One", title_2="Two", value1=1, value2=1, title='RoboFab'):
"""
Returns check value:
1 if check box 1 is checked
2 if check box 2 is checked
3 if both are checked
0 if neither are checked
None if cancel is clicked.
Availability: FontLab, Macintosh
"""
tc = None
if inFontLab:
tc = _FontLabDialogTwoChecks(title_1, title_2, value1, value2, title)
tc.Run()
elif MAC:
if hasW:
tc = _MacTwoChecksW(title_1, title_2, value1, value2, title)
else:
_raisePlatformError('TwoChecks')
else:
_raisePlatformError('TwoChecks')
c1 = tc.check1
c2 = tc.check2
if c1 == 1 and c2 == 0:
return 1
elif c1 == 0 and c2 == 1:
return 2
elif c1 == 1 and c2 == 1:
return 3
elif c1 == 0 and c2 == 0:
return 0
else:
return None
def Message(message, title='RoboFab'):
"""
A simple message dialog.
Availability: FontLab, Macintosh
"""
if inFontLab:
_FontLabDialogMessage(message, title).Run()
elif MAC:
import EasyDialogs
EasyDialogs.Message(message)
else:
_raisePlatformError('Message')
def AskString(message, value='', title='RoboFab'):
"""
Returns entered string.
Availability: FontLab, Macintosh
"""
if inFontLab:
askString = _FontLabDialogAskString(message, value, title)
askString.Run()
v = askString.value
if v is None:
return None
else:
return v
elif MAC:
import EasyDialogs
askString = EasyDialogs.AskString(message)
if askString is None:
return None
if len(askString) == 0:
return None
else:
return askString
else:
_raisePlatformError('GetString')
def AskYesNoCancel(message, title='RoboFab', default=0):
"""
Returns 1 for 'Yes', 0 for 'No' and -1 for 'Cancel'.
Availability: FontLab, Macintosh
("default" argument only available on Macintosh)
"""
if inFontLab:
gync = _FontLabDialogGetYesNoCancel(message, title)
gync.Run()
v = gync.value
return v
elif MAC:
import EasyDialogs
gync = EasyDialogs.AskYesNoCancel(message, default=default)
return gync
else:
_raisePlatformError('GetYesNoCancel')
def GetFile(message=None):
"""
Select file dialog. Returns path if one is selected. Otherwise it returns None.
Availability: FontLab, Macintosh, PC
"""
path = None
if MAC:
if haveMacfs:
fss, ok = macfs.PromptGetFile(message)
if ok:
path = fss.as_pathname()
else:
from robofab.interface.mac.getFileOrFolder import GetFile
path = GetFile(message)
elif PC:
if inFontLab:
if not message:
message = ''
path = fl.GetFileName(1, message, '', '')
else:
openFlags = win32con.OFN_FILEMUSTEXIST|win32con.OFN_EXPLORER
mode_open = 1
myDialog = win32ui.CreateFileDialog(mode_open,None,None,openFlags)
myDialog.SetOFNTitle(message)
is_OK = myDialog.DoModal()
if is_OK == 1:
path = myDialog.GetPathName()
else:
_raisePlatformError('GetFile')
return path
def GetFolder(message=None):
"""
Select folder dialog. Returns path if one is selected. Otherwise it returns None.
Availability: FontLab, Macintosh, PC
"""
path = None
if MAC:
if haveMacfs:
fss, ok = macfs.GetDirectory(message)
if ok:
path = fss.as_pathname()
else:
from robofab.interface.mac.getFileOrFolder import GetFileOrFolder
# This _also_ allows the user to select _files_, but given the
# package/folder dichotomy, I think we have no other choice.
path = GetFileOrFolder(message)
elif PC:
if inFontLab:
if not message:
message = ''
path = fl.GetPathName('', message)
else:
myTuple = shell.SHBrowseForFolder(0, None, message, 64)
try:
path = shell.SHGetPathFromIDList(myTuple[0])
except:
pass
else:
_raisePlatformError('GetFile')
return path
GetDirectory = GetFolder
def PutFile(message=None, fileName=None):
"""
Save file dialog. Returns path if one is entered. Otherwise it returns None.
Availability: FontLab, Macintosh, PC
"""
path = None
if MAC:
if haveMacfs:
fss, ok = macfs.StandardPutFile(message, fileName)
if ok:
path = fss.as_pathname()
else:
import EasyDialogs
path = EasyDialogs.AskFileForSave(message, savedFileName=fileName)
elif PC:
if inFontLab:
if not message:
message = ''
if not fileName:
fileName = ''
path = fl.GetFileName(0, message, fileName, '')
else:
openFlags = win32con.OFN_OVERWRITEPROMPT|win32con.OFN_EXPLORER
mode_save = 0
myDialog = win32ui.CreateFileDialog(mode_save, None, fileName, openFlags)
myDialog.SetOFNTitle(message)
is_OK = myDialog.DoModal()
if is_OK == 1:
path = myDialog.GetPathName()
else:
_raisePlatformError('GetFile')
return path
if __name__=='__main__':
import traceback
print "dialogs hasW", hasW
print "dialogs hasDialogKit", hasDialogKit
print "dialogs MAC", MAC
print "dialogs PC", PC
print "dialogs inFontLab", inFontLab
print "dialogs hasEasyDialogs", hasEasyDialogs
def tryDialog(dialogClass, args=None):
print
print "tryDialog:", dialogClass, "with args:", args
try:
if args is not None:
apply(dialogClass, args)
else:
apply(dialogClass)
except:
traceback.print_exc(limit=0)
tryDialog(TwoChecks, ('hello', 'world', 1, 0, 'ugh'))
tryDialog(TwoFields)
tryDialog(TwoChecks, ('hello', 'world', 1, 0, 'ugh'))
tryDialog(OneList, (['a', 'b', 'c'], 'hello world'))
tryDialog(Message, ('hello world',))
tryDialog(AskString, ('hello world',))
tryDialog(AskYesNoCancel, ('hello world',))
try:
b = ProgressBar('hello', 50, 'world')
for i in range(50):
if i == 25:
b.label('ugh.')
b.tick(i)
b.close()
except:
traceback.print_exc(limit=0)

View file

@ -0,0 +1,267 @@
"""
Dialogs for environments that support cocao / vanilla.
"""
import vanilla.dialogs
from AppKit import NSApp, NSModalPanelWindowLevel, NSWindowCloseButton, NSWindowZoomButton, NSWindowMiniaturizeButton
__all__ = [
"AskString",
"AskYesNoCancel",
"FindGlyph",
"GetFile",
"GetFileOrFolder",
"GetFolder",
"Message",
"OneList",
"PutFile",
"SearchList",
"SelectFont",
"SelectGlyph",
# "TwoChecks",
# "TwoFields",
"ProgressBar",
]
class _ModalWindow(vanilla.Window):
nsWindowLevel = NSModalPanelWindowLevel
def __init__(self, *args, **kwargs):
super(_ModalWindow, self).__init__(*args, **kwargs)
self._window.standardWindowButton_(NSWindowCloseButton).setHidden_(True)
self._window.standardWindowButton_(NSWindowZoomButton).setHidden_(True)
self._window.standardWindowButton_(NSWindowMiniaturizeButton).setHidden_(True)
def open(self):
super(_ModalWindow, self).open()
self.center()
NSApp().runModalForWindow_(self._window)
def windowWillClose_(self, notification):
super(_ModalWindow, self).windowWillClose_(notification)
NSApp().stopModal()
class _baseWindowController(object):
def setUpBaseWindowBehavior(self):
self._getValue = None
self.w.okButton = vanilla.Button((-70, -30, -15, 20), "OK", callback=self.okCallback, sizeStyle="small")
self.w.setDefaultButton(self.w.okButton)
self.w.closeButton = vanilla.Button((-150, -30, -80, 20), "Cancel", callback=self.closeCallback, sizeStyle="small")
self.w.closeButton.bind(".", ["command"])
self.w.closeButton.bind(unichr(27), [])
self.cancelled = False
def okCallback(self, sender):
self.w.close()
def closeCallback(self, sender):
self.cancelled = True
self.w.close()
def get(self):
raise NotImplementedError
class _AskStringController(_baseWindowController):
def __init__(self, message, value, title):
self.w = _ModalWindow((370, 110), title)
self.w.infoText = vanilla.TextBox((15, 10, -15, 22), message)
self.w.input = vanilla.EditText((15, 40, -15, 22))
self.w.input.set(value)
self.setUpBaseWindowBehavior()
self.w.open()
def get(self):
if self.cancelled:
return None
return self.w.input.get()
class _listController(_baseWindowController):
def __init__(self, items, message, title, showSearch=False):
self.items = items
self.w = _ModalWindow((350, 300), title)
y = 10
self.w.infoText = vanilla.TextBox((15, y, -15, 22), message)
y += 25
if showSearch:
self.w.search = vanilla.SearchBox((15, y, -15, 22), callback=self.searchCallback)
y += 25
self.w.itemList = vanilla.List((15, y, -15, -40), self.items, allowsMultipleSelection=False)
self.setUpBaseWindowBehavior()
self.w.open()
def searchCallback(self, sender):
search = sender.get()
newItems = [item for item in self.items if repr(item).startswith(search)]
self.w.itemList.set(newItems)
if newItems:
self.w.itemList.setSelection([0])
def get(self):
index = self.w.itemList.getSelection()
if index:
index = index[0]
return self.w.itemList[index]
return None
def AskString(message, value='', title='RoboFab'):
"""
AskString Dialog
message the string
value a default value
title a title of the window (may not be supported everywhere)
"""
w = _AskStringController(message, value, title)
return w.get()
def AskYesNoCancel(message, title='RoboFab', default=0, informativeText=""):
"""
AskYesNoCancel Dialog
message the string
title* a title of the window
(may not be supported everywhere)
default* index number of which button should be default
(i.e. respond to return)
informativeText* A string with secundary information
* may not be supported everywhere
"""
return vanilla.dialogs.askYesNoCancel(messageText=message, informativeText=informativeText)
def FindGlyph(aFont, message="Search for a glyph:", title='RoboFab'):
items = aFont.keys()
items.sort()
w = _listController(items, message, title, showSearch=True)
glyphName = w.get()
if glyphName is not None:
return aFont[glyphName]
return None
def GetFile(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None):
result = vanilla.dialogs.getFile(messageText=message, title=title, directory=directory, fileName=fileName, allowsMultipleSelection=allowsMultipleSelection, fileTypes=fileTypes)
if result is None:
return None
if not allowsMultipleSelection:
return str(list(result)[0])
else:
return [str(n) for n in list(result)]
def GetFolder(message=None, title=None, directory=None, allowsMultipleSelection=False):
result = vanilla.dialogs.getFolder(messageText=message, title=title, directory=directory, allowsMultipleSelection=allowsMultipleSelection)
if result is None:
return None
if not allowsMultipleSelection:
return str(list(result)[0])
else:
return [str(n) for n in list(result)]
def GetFileOrFolder(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None):
result = vanilla.dialogs.getFileOrFolder(messageText=message, title=title, directory=directory, fileName=fileName, allowsMultipleSelection=allowsMultipleSelection, fileTypes=fileTypes)
if result is None:
return None
if not allowsMultipleSelection:
return str(list(result)[0])
else:
return [str(n) for n in list(result)]
def Message(message, title='RoboFab', informativeText=""):
vanilla.dialogs.message(messageText=message, informativeText=informativeText)
def OneList(items, message="Select an item:", title='RoboFab'):
w = _listController(items, message, title, showSearch=False)
return w.get()
def PutFile(message=None, fileName=None):
return vanilla.dialogs.putFile(messageText=message, fileName=fileName)
def SearchList(list, message="Select an item:", title='RoboFab'):
w = _listController(list, message, title, showSearch=True)
return w.get()
def SelectFont(message="Select a font:", title='RoboFab', allFonts=None):
if allFonts is None:
from robofab.world import AllFonts
fonts = AllFonts()
else:
fonts = allFonts
data = dict()
for font in fonts:
data["%s" %font] = font
items = data.keys()
items.sort()
w = _listController(items, message, title, showSearch=False)
value = w.get()
return data.get(value, None)
def SelectGlyph(aFont, message="Select a glyph:", title='RoboFab'):
items = aFont.keys()
items.sort()
w = _listController(items, message, title, showSearch=False)
glyphName = w.get()
if glyphName is not None:
return aFont[glyphName]
return None
def TwoChecks(title_1="One", title_2="Two", value1=1, value2=1, title='RoboFab'):
raise NotImplementedError
def TwoFields(title_1="One:", value_1="0", title_2="Two:", value_2="0", title='RoboFab'):
raise NotImplementedError
class ProgressBar(object):
def __init__(self, title="RoboFab...", ticks=None, label=""):
self.w = vanilla.Window((250, 60), title)
if ticks is None:
isIndeterminate = True
ticks = 0
else:
isIndeterminate = False
self.w.progress = vanilla.ProgressBar((15, 15, -15, 12), maxValue=ticks, isIndeterminate=isIndeterminate, sizeStyle="small")
self.w.text = vanilla.TextBox((15, 32, -15, 14), label, sizeStyle="small")
self.w.progress.start()
self.w.center()
self.w.open()
def close(self):
self.w.progress.stop()
self.w.close()
def getCurrentTick(self):
return self.w.progress.get()
def label(self, label):
self.w.text.set(label)
self.w.text._nsObject.display()
def tick(self, tickValue=None):
if tickValue is None:
self.w.progress.increment()
else:
self.w.progress.set(tickValue)
if __name__ == "__main__":
pass

View file

@ -0,0 +1,10 @@
"""
Directory for interface related modules.
Stuff for MacOSX, widgets, quartz
"""

View file

@ -0,0 +1,80 @@
"""This module provides two functions, very similar to
EasyDialogs.AskFileForOpen() and EasyDialogs.AskFolder(): GetFile() and
GetFileOrFolder(). The main difference is that the functions here fully
support "packages" or "bundles", ie. folders that appear to be files in
the finder and open/save dialogs. The second difference is that
GetFileOrFolder() allows the user to select a file _or_ a folder.
"""
__all__ = ["GetFile", "GetFileOrFolder"]
from EasyDialogs import _process_Nav_args, _interact
import Nav
import Carbon.File
# Lots of copy/paste from EasyDialogs.py, for one because althought the
# EasyDialogs counterparts take a million options, they don't take the
# one option I need: the flag to support packages...
kNavSupportPackages = 0x00001000
def GetFile(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None):
"""Ask the user to select a file.
Some of these arguments are not supported:
title, directory, fileName, allowsMultipleSelection and fileTypes are here for compatibility reasons.
"""
default_flags = 0x56 | kNavSupportPackages
args, tpwanted = _process_Nav_args(default_flags, message=message)
_interact()
try:
rr = Nav.NavChooseFile(args)
good = 1
except Nav.error, arg:
if arg[0] != -128: # userCancelledErr
raise Nav.error, arg
return None
if not rr.validRecord or not rr.selection:
return None
if issubclass(tpwanted, Carbon.File.FSRef):
return tpwanted(rr.selection_fsr[0])
if issubclass(tpwanted, Carbon.File.FSSpec):
return tpwanted(rr.selection[0])
if issubclass(tpwanted, str):
return tpwanted(rr.selection_fsr[0].as_pathname())
if issubclass(tpwanted, unicode):
return tpwanted(rr.selection_fsr[0].as_pathname(), 'utf8')
raise TypeError, "Unknown value for argument 'wanted': %s" % repr(tpwanted)
def GetFileOrFolder(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None):
"""Ask the user to select a file or a folder.
Some of these arguments are not supported:
title, directory, fileName, allowsMultipleSelection and fileTypes are here for compatibility reasons.
"""
default_flags = 0x17 | kNavSupportPackages
args, tpwanted = _process_Nav_args(default_flags, message=message)
_interact()
try:
rr = Nav.NavChooseObject(args)
good = 1
except Nav.error, arg:
if arg[0] != -128: # userCancelledErr
raise Nav.error, arg
return None
if not rr.validRecord or not rr.selection:
return None
if issubclass(tpwanted, Carbon.File.FSRef):
return tpwanted(rr.selection_fsr[0])
if issubclass(tpwanted, Carbon.File.FSSpec):
return tpwanted(rr.selection[0])
if issubclass(tpwanted, str):
return tpwanted(rr.selection_fsr[0].as_pathname())
if issubclass(tpwanted, unicode):
return tpwanted(rr.selection_fsr[0].as_pathname(), 'utf8')
raise TypeError, "Unknown value for argument 'wanted': %s" % repr(tpwanted)

View file

@ -0,0 +1,10 @@
"""
Directory for interface related modules.
Stuff for Windows
"""

View file

@ -0,0 +1,13 @@
"""
arrayTools and bezierTools, originally from fontTools and using Numpy,
now in a pure python implementation. This should ease the Numpy dependency
for normal UFO input/output and basic scripting tasks.
comparison test and speedtest provided.
"""

View file

@ -0,0 +1,160 @@
#
# Various array and rectangle tools, but mostly rectangles, hence the
# name of this module (not).
#
"""
Rewritten to elimate the numpy dependency
"""
import math
def calcBounds(array):
"""Return the bounding rectangle of a 2D points array as a tuple:
(xMin, yMin, xMax, yMax)
"""
if len(array) == 0:
return 0, 0, 0, 0
xs = [x for x, y in array]
ys = [y for x, y in array]
return min(xs), min(ys), max(xs), max(ys)
def updateBounds(bounds, pt, min=min, max=max):
"""Return the bounding recangle of rectangle bounds and point (x, y)."""
xMin, yMin, xMax, yMax = bounds
x, y = pt
return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y)
def pointInRect(pt, rect):
"""Return True when point (x, y) is inside rect."""
xMin, yMin, xMax, yMax = rect
return (xMin <= pt[0] <= xMax) and (yMin <= pt[1] <= yMax)
def pointsInRect(array, rect):
"""Find out which points or array are inside rect.
Returns an array with a boolean for each point.
"""
if len(array) < 1:
return []
xMin, yMin, xMax, yMax = rect
return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array]
def vectorLength(vector):
"""Return the length of the given vector."""
x, y = vector
return math.sqrt(x**2 + y**2)
def asInt16(array):
"""Round and cast to 16 bit integer."""
return [int(math.floor(i+0.5)) for i in array]
def normRect(box):
"""Normalize the rectangle so that the following holds:
xMin <= xMax and yMin <= yMax
"""
return min(box[0], box[2]), min(box[1], box[3]), max(box[0], box[2]), max(box[1], box[3])
def scaleRect(box, x, y):
"""Scale the rectangle by x, y."""
return box[0] * x, box[1] * y, box[2] * x, box[3] * y
def offsetRect(box, dx, dy):
"""Offset the rectangle by dx, dy."""
return box[0]+dx, box[1]+dy, box[2]+dx, box[3]+dy
def insetRect(box, dx, dy):
"""Inset the rectangle by dx, dy on all sides."""
return box[0]+dx, box[1]+dy, box[2]-dx, box[3]-dy
def sectRect(box1, box2):
"""Return a boolean and a rectangle. If the input rectangles intersect, return
True and the intersecting rectangle. Return False and (0, 0, 0, 0) if the input
rectangles don't intersect.
"""
xMin, yMin, xMax, yMax = (max(box1[0], box2[0]), max(box1[1], box2[1]),
min(box1[2], box2[2]), min(box1[3], box2[3]))
if xMin >= xMax or yMin >= yMax:
return 0, (0, 0, 0, 0)
return 1, (xMin, yMin, xMax, yMax)
def unionRect(box1, box2):
"""Return the smallest rectangle in which both input rectangles are fully
enclosed. In other words, return the total bounding rectangle of both input
rectangles.
"""
return (max(box1[0], box2[0]), max(box1[1], box2[1]),
min(box1[2], box2[2]), min(box1[3], box2[3]))
def rectCenter(box):
"""Return the center of the rectangle as an (x, y) coordinate."""
return (box[0]+box[2])/2, (box[1]+box[3])/2
def intRect(box):
"""Return the rectangle, rounded off to integer values, but guaranteeing that
the resulting rectangle is NOT smaller than the original.
"""
xMin, yMin, xMax, yMax = box
xMin = int(math.floor(xMin))
yMin = int(math.floor(yMin))
xMax = int(math.ceil(xMax))
yMax = int(math.ceil(yMax))
return (xMin, yMin, xMax, yMax)
def _test():
"""
>>> import math
>>> calcBounds([(0, 40), (0, 100), (50, 50), (80, 10)])
(0, 10, 80, 100)
>>> updateBounds((0, 0, 0, 0), (100, 100))
(0, 0, 100, 100)
>>> pointInRect((50, 50), (0, 0, 100, 100))
True
>>> pointInRect((0, 0), (0, 0, 100, 100))
True
>>> pointInRect((100, 100), (0, 0, 100, 100))
True
>>> not pointInRect((101, 100), (0, 0, 100, 100))
True
>>> list(pointsInRect([(50, 50), (0, 0), (100, 100), (101, 100)], (0, 0, 100, 100)))
[True, True, True, False]
>>> vectorLength((3, 4))
5.0
>>> vectorLength((1, 1)) == math.sqrt(2)
True
>>> list(asInt16([0, 0.1, 0.5, 0.9]))
[0, 0, 1, 1]
>>> normRect((0, 10, 100, 200))
(0, 10, 100, 200)
>>> normRect((100, 200, 0, 10))
(0, 10, 100, 200)
>>> scaleRect((10, 20, 50, 150), 1.5, 2)
(15.0, 40, 75.0, 300)
>>> offsetRect((10, 20, 30, 40), 5, 6)
(15, 26, 35, 46)
>>> insetRect((10, 20, 50, 60), 5, 10)
(15, 30, 45, 50)
>>> insetRect((10, 20, 50, 60), -5, -10)
(5, 10, 55, 70)
>>> intersects, rect = sectRect((0, 10, 20, 30), (0, 40, 20, 50))
>>> not intersects
True
>>> intersects, rect = sectRect((0, 10, 20, 30), (5, 20, 35, 50))
>>> intersects
1
>>> rect
(5, 20, 20, 30)
>>> unionRect((0, 10, 20, 30), (0, 40, 20, 50))
(0, 10, 20, 50)
>>> rectCenter((0, 0, 100, 200))
(50, 100)
>>> rectCenter((0, 0, 100, 199.0))
(50, 99.5)
>>> intRect((0.9, 2.9, 3.1, 4.1))
(0, 2, 4, 5)
"""
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,416 @@
"""fontTools.misc.bezierTools.py -- tools for working with bezier path segments.
Rewritten to elimate the numpy dependency
"""
__all__ = [
"calcQuadraticBounds",
"calcCubicBounds",
"splitLine",
"splitQuadratic",
"splitCubic",
"splitQuadraticAtT",
"splitCubicAtT",
"solveQuadratic",
"solveCubic",
]
from robofab.misc.arrayTools import calcBounds
epsilon = 1e-12
def calcQuadraticBounds(pt1, pt2, pt3):
"""Return the bounding rectangle for a qudratic bezier segment.
pt1 and pt3 are the "anchor" points, pt2 is the "handle".
>>> calcQuadraticBounds((0, 0), (50, 100), (100, 0))
(0, 0, 100, 50.0)
>>> calcQuadraticBounds((0, 0), (100, 0), (100, 100))
(0.0, 0.0, 100, 100)
"""
(ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3)
ax2 = ax*2.0
ay2 = ay*2.0
roots = []
if ax2 != 0:
roots.append(-bx/ax2)
if ay2 != 0:
roots.append(-by/ay2)
points = [(ax*t*t + bx*t + cx, ay*t*t + by*t + cy) for t in roots if 0 <= t < 1] + [pt1, pt3]
return calcBounds(points)
def calcCubicBounds(pt1, pt2, pt3, pt4):
"""Return the bounding rectangle for a cubic bezier segment.
pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".
>>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0))
(0, 0, 100, 75.0)
>>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100))
(0.0, 0.0, 100, 100)
>>> calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))
(35.566243270259356, 0, 64.43375672974068, 75.0)
"""
(ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4)
# calc first derivative
ax3 = ax * 3.0
ay3 = ay * 3.0
bx2 = bx * 2.0
by2 = by * 2.0
xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1]
yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1]
roots = xRoots + yRoots
points = [(ax*t*t*t + bx*t*t + cx * t + dx, ay*t*t*t + by*t*t + cy * t + dy) for t in roots] + [pt1, pt4]
return calcBounds(points)
def splitLine(pt1, pt2, where, isHorizontal):
"""Split the line between pt1 and pt2 at position 'where', which
is an x coordinate if isHorizontal is False, a y coordinate if
isHorizontal is True. Return a list of two line segments if the
line was successfully split, or a list containing the original
line.
>>> printSegments(splitLine((0, 0), (100, 200), 50, False))
((0, 0), (50.0, 100.0))
((50.0, 100.0), (100, 200))
>>> printSegments(splitLine((0, 0), (100, 200), 50, True))
((0, 0), (25.0, 50.0))
((25.0, 50.0), (100, 200))
>>> printSegments(splitLine((0, 0), (100, 100), 50, True))
((0, 0), (50.0, 50.0))
((50.0, 50.0), (100, 100))
>>> printSegments(splitLine((0, 0), (100, 100), 100, True))
((0, 0), (100, 100))
>>> printSegments(splitLine((0, 0), (100, 100), 0, True))
((0, 0), (0.0, 0.0))
((0.0, 0.0), (100, 100))
>>> printSegments(splitLine((0, 0), (100, 100), 0, False))
((0, 0), (0.0, 0.0))
((0.0, 0.0), (100, 100))
"""
pt1x, pt1y = pt1
pt2x, pt2y = pt2
ax = (pt2x - pt1x)
ay = (pt2y - pt1y)
bx = pt1x
by = pt1y
ax1 = (ax, ay)[isHorizontal]
if ax1 == 0:
return [(pt1, pt2)]
t = float(where - (bx, by)[isHorizontal]) / ax1
if 0 <= t < 1:
midPt = ax * t + bx, ay * t + by
return [(pt1, midPt), (midPt, pt2)]
else:
return [(pt1, pt2)]
def splitQuadratic(pt1, pt2, pt3, where, isHorizontal):
"""Split the quadratic curve between pt1, pt2 and pt3 at position 'where',
which is an x coordinate if isHorizontal is False, a y coordinate if
isHorizontal is True. Return a list of curve segments.
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False))
((0, 0), (50, 100), (100, 0))
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False))
((0.0, 0.0), (12.5, 25.0), (25.0, 37.5))
((25.0, 37.5), (62.5, 75.0), (100.0, 0.0))
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True))
((0.0, 0.0), (7.32233047034, 14.6446609407), (14.6446609407, 25.0))
((14.6446609407, 25.0), (50.0, 75.0), (85.3553390593, 25.0))
((85.3553390593, 25.0), (92.6776695297, 14.6446609407), (100.0, -7.1054273576e-15))
>>> # XXX I'm not at all sure if the following behavior is desirable:
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (50.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
"""
a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
solutions = solveQuadratic(a[isHorizontal], b[isHorizontal],
c[isHorizontal] - where)
solutions = [t for t in solutions if 0 <= t < 1]
solutions.sort()
if not solutions:
return [(pt1, pt2, pt3)]
return _splitQuadraticAtT(a, b, c, *solutions)
def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal):
"""Split the cubic curve between pt1, pt2, pt3 and pt4 at position 'where',
which is an x coordinate if isHorizontal is False, a y coordinate if
isHorizontal is True. Return a list of curve segments.
>>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False))
((0, 0), (25, 100), (75, 100), (100, 0))
>>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False))
((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
>>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True))
((0.0, 0.0), (2.2937927384, 9.17517095361), (4.79804488188, 17.5085042869), (7.47413641001, 25.0))
((7.47413641001, 25.0), (31.2886200204, 91.6666666667), (68.7113799796, 91.6666666667), (92.52586359, 25.0))
((92.52586359, 25.0), (95.2019551181, 17.5085042869), (97.7062072616, 9.17517095361), (100.0, 1.7763568394e-15))
"""
a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
solutions = solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal],
d[isHorizontal] - where)
solutions = [t for t in solutions if 0 <= t < 1]
solutions.sort()
if not solutions:
return [(pt1, pt2, pt3, pt4)]
return _splitCubicAtT(a, b, c, d, *solutions)
def splitQuadraticAtT(pt1, pt2, pt3, *ts):
"""Split the quadratic curve between pt1, pt2 and pt3 at one or more
values of t. Return a list of curve segments.
>>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
>>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (62.5, 50.0), (75.0, 37.5))
((75.0, 37.5), (87.5, 25.0), (100.0, 0.0))
"""
a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
return _splitQuadraticAtT(a, b, c, *ts)
def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
"""Split the cubic curve between pt1, pt2, pt3 and pt4 at one or more
values of t. Return a list of curve segments.
>>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5))
((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
>>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75))
((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
((50.0, 75.0), (59.375, 75.0), (68.75, 68.75), (77.34375, 56.25))
((77.34375, 56.25), (85.9375, 43.75), (93.75, 25.0), (100.0, 0.0))
"""
a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
return _splitCubicAtT(a, b, c, d, *ts)
def _splitQuadraticAtT(a, b, c, *ts):
ts = list(ts)
segments = []
ts.insert(0, 0.0)
ts.append(1.0)
ax, ay = a
bx, by = b
cx, cy = c
for i in range(len(ts) - 1):
t1 = ts[i]
t2 = ts[i+1]
delta = (t2 - t1)
# calc new a, b and c
a1x = ax * delta**2
a1y = ay * delta**2
b1x = (2*ax*t1 + bx) * delta
b1y = (2*ay*t1 + by) * delta
c1x = ax*t1**2 + bx*t1 + cx
c1y = ay*t1**2 + by*t1 + cy
pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y))
segments.append((pt1, pt2, pt3))
return segments
def _splitCubicAtT(a, b, c, d, *ts):
ts = list(ts)
ts.insert(0, 0.0)
ts.append(1.0)
segments = []
ax, ay = a
bx, by = b
cx, cy = c
dx, dy = d
for i in range(len(ts) - 1):
t1 = ts[i]
t2 = ts[i+1]
delta = (t2 - t1)
# calc new a, b, c and d
a1x = ax * delta**3
a1y = ay * delta**3
b1x = (3*ax*t1 + bx) * delta**2
b1y = (3*ay*t1 + by) * delta**2
c1x = (2*bx*t1 + cx + 3*ax*t1**2) * delta
c1y = (2*by*t1 + cy + 3*ay*t1**2) * delta
d1x = ax*t1**3 + bx*t1**2 + cx*t1 + dx
d1y = ay*t1**3 + by*t1**2 + cy*t1 + dy
pt1, pt2, pt3, pt4 = calcCubicPoints((a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y))
segments.append((pt1, pt2, pt3, pt4))
return segments
#
# Equation solvers.
#
from math import sqrt, acos, cos, pi
def solveQuadratic(a, b, c,
sqrt=sqrt):
"""Solve a quadratic equation where a, b and c are real.
a*x*x + b*x + c = 0
This function returns a list of roots. Note that the returned list
is neither guaranteed to be sorted nor to contain unique values!
"""
if abs(a) < epsilon:
if abs(b) < epsilon:
# We have a non-equation; therefore, we have no valid solution
roots = []
else:
# We have a linear equation with 1 root.
roots = [-c/b]
else:
# We have a true quadratic equation. Apply the quadratic formula to find two roots.
DD = b*b - 4.0*a*c
if DD >= 0.0:
rDD = sqrt(DD)
roots = [(-b+rDD)/2.0/a, (-b-rDD)/2.0/a]
else:
# complex roots, ignore
roots = []
return roots
def solveCubic(a, b, c, d,
abs=abs, pow=pow, sqrt=sqrt, cos=cos, acos=acos, pi=pi):
"""Solve a cubic equation where a, b, c and d are real.
a*x*x*x + b*x*x + c*x + d = 0
This function returns a list of roots. Note that the returned list
is neither guaranteed to be sorted nor to contain unique values!
"""
#
# adapted from:
# CUBIC.C - Solve a cubic polynomial
# public domain by Ross Cottrell
# found at: http://www.strangecreations.com/library/snippets/Cubic.C
#
if abs(a) < epsilon:
# don't just test for zero; for very small values of 'a' solveCubic()
# returns unreliable results, so we fall back to quad.
return solveQuadratic(b, c, d)
a = float(a)
a1 = b/a
a2 = c/a
a3 = d/a
Q = (a1*a1 - 3.0*a2)/9.0
R = (2.0*a1*a1*a1 - 9.0*a1*a2 + 27.0*a3)/54.0
R2_Q3 = R*R - Q*Q*Q
if R2_Q3 < 0:
theta = acos(R/sqrt(Q*Q*Q))
rQ2 = -2.0*sqrt(Q)
x0 = rQ2*cos(theta/3.0) - a1/3.0
x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1/3.0
x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1/3.0
return [x0, x1, x2]
else:
if Q == 0 and R == 0:
x = 0
else:
x = pow(sqrt(R2_Q3)+abs(R), 1/3.0)
x = x + Q/x
if R >= 0.0:
x = -x
x = x - a1/3.0
return [x]
#
# Conversion routines for points to parameters and vice versa
#
def calcQuadraticParameters(pt1, pt2, pt3):
x2, y2 = pt2
x3, y3 = pt3
cx, cy = pt1
bx = (x2 - cx) * 2.0
by = (y2 - cy) * 2.0
ax = x3 - cx - bx
ay = y3 - cy - by
return (ax, ay), (bx, by), (cx, cy)
def calcCubicParameters(pt1, pt2, pt3, pt4):
x2, y2 = pt2
x3, y3 = pt3
x4, y4 = pt4
dx, dy = pt1
cx = (x2 -dx) * 3.0
cy = (y2 -dy) * 3.0
bx = (x3 - x2) * 3.0 - cx
by = (y3 - y2) * 3.0 - cy
ax = x4 - dx - cx - bx
ay = y4 - dy - cy - by
return (ax, ay), (bx, by), (cx, cy), (dx, dy)
def calcQuadraticPoints(a, b, c):
ax, ay = a
bx, by = b
cx, cy = c
x1 = cx
y1 = cy
x2 = (bx * 0.5) + cx
y2 = (by * 0.5) + cy
x3 = ax + bx + cx
y3 = ay + by + cy
return (x1, y1), (x2, y2), (x3, y3)
def calcCubicPoints(a, b, c, d):
ax, ay = a
bx, by = b
cx, cy = c
dx, dy = d
x1 = dx
y1 = dy
x2 = (cx / 3.0) + dx
y2 = (cy / 3.0) + dy
x3 = (bx + cx) / 3.0 + x2
y3 = (by + cy) / 3.0 + y2
x4 = ax + dx + cx + bx
y4 = ay + dy + cy + by
return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
def _segmentrepr(obj):
"""
>>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
'(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
"""
try:
it = iter(obj)
except TypeError:
return str(obj)
else:
return "(%s)" % ", ".join([_segmentrepr(x) for x in it])
def printSegments(segments):
"""Helper for the doctests, displaying each segment in a list of
segments on a single line as a tuple.
"""
for segment in segments:
print _segmentrepr(segment)
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,99 @@
"""
Speed comparison between the fontTools numpy based arrayTools and bezierTools,
and the pure python implementation in robofab.path.arrayTools and robofab.path.bezierTools
"""
import time
from fontTools.misc import arrayTools
from fontTools.misc import bezierTools
import numpy
import robofab.misc.arrayTools as noNumpyArrayTools
import robofab.misc.bezierTools as noNumpyBezierTools
################
pt1 = (100, 100)
pt2 = (200, 20)
pt3 = (30, 580)
pt4 = (153, 654)
rect = [20, 20, 100, 100]
## loops
c = 10000
print "(loop %s)"%c
print "with numpy:"
print "calcQuadraticParameters\t\t",
n = time.time()
for i in range(c):
bezierTools.calcQuadraticParameters(pt1, pt2, pt3)
print time.time() - n
print "calcBounds\t\t\t",
n = time.time()
for i in range(c):
arrayTools.calcBounds([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3])
print time.time() - n
print "pointsInRect\t\t\t",
n = time.time()
for i in range(c):
arrayTools.pointsInRect([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt4], rect)
print time.time() - n
print "calcQuadraticBounds\t\t",
n = time.time()
for i in range(c):
bezierTools.calcQuadraticBounds(pt1, pt2, pt3)
print time.time() - n
print "calcCubicBounds\t\t\t",
n = time.time()
for i in range(c):
bezierTools.calcCubicBounds(pt1, pt2, pt3, pt4)
print time.time() - n
print
##############
print "no-numpy"
print "calcQuadraticParameters\t\t",
n = time.time()
for i in range(c):
noNumpyBezierTools.calcQuadraticParameters(pt1, pt2, pt3)
print time.time() - n
print "calcBounds\t\t\t",
n = time.time()
for i in range(c):
noNumpyArrayTools.calcBounds([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3])
print time.time() - n
print "pointsInRect\t\t\t",
n = time.time()
for i in range(c):
noNumpyArrayTools.pointsInRect([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt4], rect)
print time.time() - n
print "calcQuadraticBounds\t\t",
n = time.time()
for i in range(c):
noNumpyBezierTools.calcQuadraticBounds(pt1, pt2, pt3)
print time.time() - n
print "calcCubicBounds\t\t\t",
n = time.time()
for i in range(c):
noNumpyBezierTools.calcCubicBounds(pt1, pt2, pt3, pt4)
print time.time() - n

View file

@ -0,0 +1,119 @@
"""
doc test requires fontTools to compare and defon to make the test font.
"""
import random
from fontTools.pens.basePen import BasePen
from fontTools.misc import arrayTools
from fontTools.misc import bezierTools
import robofab.misc.arrayTools as noNumpyArrayTools
import robofab.misc.bezierTools as noNumpyBezierTools
def drawMoveTo(pen, maxBox):
pen.moveTo((maxBox*random.random(), maxBox*random.random()))
def drawLineTo(pen, maxBox):
pen.lineTo((maxBox*random.random(), maxBox*random.random()))
def drawCurveTo(pen, maxBox):
pen.curveTo((maxBox*random.random(), maxBox*random.random()),
(maxBox*random.random(), maxBox*random.random()),
(maxBox*random.random(), maxBox*random.random()))
def drawClosePath(pen):
pen.closePath()
def createRandomFont():
from defcon import Font
maxGlyphs = 1000
maxContours = 10
maxSegments = 10
maxBox = 700
drawCallbacks = [drawLineTo, drawCurveTo]
f = Font()
for i in range(maxGlyphs):
name = "%s" %i
f.newGlyph(name)
g = f[name]
g.width = maxBox
pen = g.getPen()
for c in range(maxContours):
drawMoveTo(pen, maxBox)
for s in range(maxSegments):
random.choice(drawCallbacks)(pen, maxBox)
drawClosePath(pen)
return f
class BoundsPen(BasePen):
def __init__(self, glyphSet, at, bt):
BasePen.__init__(self, glyphSet)
self.bounds = None
self._start = None
self._arrayTools = at
self._bezierTools = bt
def _moveTo(self, pt):
self._start = pt
def _addMoveTo(self):
if self._start is None:
return
bounds = self.bounds
if bounds:
self.bounds = self._arrayTools.updateBounds(bounds, self._start)
else:
x, y = self._start
self.bounds = (x, y, x, y)
self._start = None
def _lineTo(self, pt):
self._addMoveTo()
self.bounds = self._arrayTools.updateBounds(self.bounds, pt)
def _curveToOne(self, bcp1, bcp2, pt):
self._addMoveTo()
bounds = self.bounds
bounds = self._arrayTools.updateBounds(bounds, pt)
if not self._arrayTools.pointInRect(bcp1, bounds) or not self._arrayTools.pointInRect(bcp2, bounds):
bounds = self._arrayTools.unionRect(bounds, self._bezierTools.calcCubicBounds(
self._getCurrentPoint(), bcp1, bcp2, pt))
self.bounds = bounds
def _qCurveToOne(self, bcp, pt):
self._addMoveTo()
bounds = self.bounds
bounds = self._arrayTools.updateBounds(bounds, pt)
if not self._arrayTools.pointInRect(bcp, bounds):
bounds = self._arrayToolsunionRect(bounds, self._bezierTools.calcQuadraticBounds(
self._getCurrentPoint(), bcp, pt))
self.bounds = bounds
def _testFont(font):
succes = True
for glyph in font:
fontToolsBoundsPen = BoundsPen(font, arrayTools, bezierTools)
glyph.draw(fontToolsBoundsPen)
noNumpyBoundsPen = BoundsPen(font, noNumpyArrayTools, noNumpyBezierTools)
glyph.draw(noNumpyBoundsPen)
if fontToolsBoundsPen.bounds != noNumpyBoundsPen.bounds:
succes = False
return succes
def testCompareAgainstFontTools():
"""
>>> font = createRandomFont()
>>> _testFont(font)
True
"""
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,15 @@
"""
Directory for modules supporting
Unified
Font
Objects
"""

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
"""
Directory for modules
which do path stuff.
Maybe it should move somewhere else.
"""

View file

@ -0,0 +1,108 @@
from robofab.pens.filterPen import flattenGlyph
from robofab.objects.objectsRF import RGlyph as _RGlyph
import math
_EPSILON = 1e-15
def normalise(a1, a2):
"""Normalise this vector to length 1"""
n = math.sqrt((a1*a1)+(a2*a2))
return (a1/n, a2/n)
def inbetween((a1, a2), (b1, b2), (c1, c2)):
"""Return True if point b is in between points a and c."""
x = (a1-_EPSILON<=b1<=c1+_EPSILON) or (a1+_EPSILON>=b1>=c1-_EPSILON)
y = (a2-_EPSILON<=b2<=c2+_EPSILON) or (a2+_EPSILON>=b2>=c2-_EPSILON)
return x == y == True
def sectlines((a1, a2), (p1, p2), (b1, b2), (q1, q2)):
'''Calculate the intersection point of two straight lines. Result in floats.'''
if (a1, a2) == (p1, p2):
return None
r1 = a1-p1
r2 = a2-p2
r1, r2 = normalise(r1, r2)
s1 = b1-q1
s2 = b2-q2
s1, s2 = normalise(s1, s2)
f = float(s1*r2 - s2*r1)
if f == 0:
return None
mu = (r1*(q2 - p2) + r2*(p1 - q1)) / f
m1 = mu*s1 + q1
m2 = mu*s2 + q2
if (m1, m2) == (a1, a2):
return None
if inbetween((a1, a2), (m1, m2), (p1,p2)) and inbetween((b1, b2), (m1, m2), (q1,q2)):
return m1, m2
else:
return None
def _makeFlat(aGlyph, segmentLength = 10):
"""Helper function to flatten the glyph with a given approximate segment length."""
return flattenGlyph(aGlyph, segmentLength)
def intersect(aGlyph, startPt, endPt, segmentLength=10):
"""Find the intersections between a glyph and a straight line."""
flat = _makeFlat(aGlyph)
return _intersect(flat, startPt, endPt, segmentLength)
def _intersect(flat, startPt, endPt, segmentLength=10):
"""Find the intersections between a flattened glyph and a straight line."""
if len(flat.contours) == 0:
return None
if startPt == endPt:
return None
sect = None
intersections = {}
# new contains the flattened outline
for c in flat:
l =len(c.points)
for i in range(l):
cur = c.points[i]
next = c.points[(i+1)%l]
sect = sectlines((cur.x, cur.y), (next.x, next.y), startPt, endPt)
if sect is None:
continue
intersections[sect] = 1
return intersections.keys()
def intersectGlyphs(glyphA, glyphB, segmentLength=10):
"""Approximate the intersection points between two glyphs by
flattening both glyphs and checking each tiny segment for
intersections. Slow, but perhaps more realistic then
solving the equasions.
Seems to work for basic curves and straights, but untested
for edges cases, alsmost hits, near hits, double points, crap like that.
"""
flatA = _makeFlat(glyphA)
flatB = _makeFlat(glyphB)
intersections = []
for c in flatA:
l =len(c.points)
for i in range(l):
cur = c.points[i]
next = c.points[(i+1)%l]
sect = _intersect(flatB, (cur.x, cur.y), (next.x, next.y))
if sect is None:
continue
intersections = intersections + sect
return intersections
def makeTestGlyph():
g = _RGlyph()
pen = g.getPen()
pen.moveTo((100, 100))
pen.lineTo((800, 100))
pen.curveTo((1000, 300), (1000, 600), (800, 800))
pen.lineTo((100, 800))
pen.lineTo((100, 100))
pen.closePath()
return g
if __name__ == "__main__":
g = makeTestGlyph()
print intersect(g, (-10, 200), (650, 150))
print intersect(g, (100, 100), (600, 600))

View file

@ -0,0 +1,11 @@
"""
Directory for all pen modules.
If you make a pen, put it here so that we can keep track of it.
"""

View file

@ -0,0 +1,293 @@
import math
from fontTools.pens.basePen import AbstractPen
from robofab.pens.pointPen import AbstractPointPen, BasePointToSegmentPen
class FabToFontToolsPenAdapter:
"""Class that covers up the subtle differences between RoboFab
Pens and FontTools Pens. 'Fab should eventually move to FontTools
Pens, this class may help to make the transition smoother.
"""
# XXX The change to FontTools pens has almost been completed. Any
# usage of this class should be highly suspect.
def __init__(self, fontToolsPen):
self.fontToolsPen = fontToolsPen
def moveTo(self, pt, **kargs):
self.fontToolsPen.moveTo(pt)
def lineTo(self, pt, **kargs):
self.fontToolsPen.lineTo(pt)
def curveTo(self, *pts, **kargs):
self.fontToolsPen.curveTo(*pts)
def qCurveTo(self, *pts, **kargs):
self.fontToolsPen.qCurveTo(*pts)
def closePath(self):
self.fontToolsPen.closePath()
def endPath(self):
self.fontToolsPen.endPath()
def addComponent(self, glyphName, offset=(0, 0), scale=(1, 1)):
self.fontToolsPen.addComponent(glyphName,
(scale[0], 0, 0, scale[1], offset[0], offset[1]))
def setWidth(self, width):
self.width = width
def setNote(self, note):
pass
def addAnchor(self, name, pt):
self.fontToolsPen.moveTo(pt)
self.fontToolsPen.endPath()
def doneDrawing(self):
pass
class PointToSegmentPen(BasePointToSegmentPen):
"""Adapter class that converts the PointPen protocol to the
(Segment)Pen protocol.
"""
def __init__(self, segmentPen, outputImpliedClosingLine=False):
BasePointToSegmentPen.__init__(self)
self.pen = segmentPen
self.outputImpliedClosingLine = outputImpliedClosingLine
def _flushContour(self, segments):
assert len(segments) >= 1
pen = self.pen
if segments[0][0] == "move":
# It's an open path.
closed = False
points = segments[0][1]
assert len(points) == 1
movePt, smooth, name, kwargs = points[0]
del segments[0]
else:
# It's a closed path, do a moveTo to the last
# point of the last segment.
closed = True
segmentType, points = segments[-1]
movePt, smooth, name, kwargs = points[-1]
if movePt is None:
# quad special case: a contour with no on-curve points contains
# one "qcurve" segment that ends with a point that's None. We
# must not output a moveTo() in that case.
pass
else:
pen.moveTo(movePt)
outputImpliedClosingLine = self.outputImpliedClosingLine
nSegments = len(segments)
for i in range(nSegments):
segmentType, points = segments[i]
points = [pt for pt, smooth, name, kwargs in points]
if segmentType == "line":
assert len(points) == 1
pt = points[0]
if i + 1 != nSegments or outputImpliedClosingLine or not closed:
pen.lineTo(pt)
elif segmentType == "curve":
pen.curveTo(*points)
elif segmentType == "qcurve":
pen.qCurveTo(*points)
else:
assert 0, "illegal segmentType: %s" % segmentType
if closed:
pen.closePath()
else:
pen.endPath()
def addComponent(self, glyphName, transform):
self.pen.addComponent(glyphName, transform)
class SegmentToPointPen(AbstractPen):
"""Adapter class that converts the (Segment)Pen protocol to the
PointPen protocol.
"""
def __init__(self, pointPen, guessSmooth=True):
if guessSmooth:
self.pen = GuessSmoothPointPen(pointPen)
else:
self.pen = pointPen
self.contour = None
def _flushContour(self):
pen = self.pen
pen.beginPath()
for pt, segmentType in self.contour:
pen.addPoint(pt, segmentType=segmentType)
pen.endPath()
def moveTo(self, pt):
self.contour = []
self.contour.append((pt, "move"))
def lineTo(self, pt):
self.contour.append((pt, "line"))
def curveTo(self, *pts):
for pt in pts[:-1]:
self.contour.append((pt, None))
self.contour.append((pts[-1], "curve"))
def qCurveTo(self, *pts):
if pts[-1] is None:
self.contour = []
for pt in pts[:-1]:
self.contour.append((pt, None))
if pts[-1] is not None:
self.contour.append((pts[-1], "qcurve"))
def closePath(self):
if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
self.contour[0] = self.contour[-1]
del self.contour[-1]
else:
# There's an implied line at the end, replace "move" with "line"
# for the first point
pt, tp = self.contour[0]
if tp == "move":
self.contour[0] = pt, "line"
self._flushContour()
self.contour = None
def endPath(self):
self._flushContour()
self.contour = None
def addComponent(self, glyphName, transform):
assert self.contour is None
self.pen.addComponent(glyphName, transform)
class TransformPointPen(AbstractPointPen):
"""PointPen that transforms all coordinates, and passes them to another
PointPen. It also transforms the transformation given to addComponent().
"""
def __init__(self, outPen, transformation):
if not hasattr(transformation, "transformPoint"):
from fontTools.misc.transform import Transform
transformation = Transform(*transformation)
self._transformation = transformation
self._transformPoint = transformation.transformPoint
self._outPen = outPen
self._stack = []
def beginPath(self):
self._outPen.beginPath()
def endPath(self):
self._outPen.endPath()
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
pt = self._transformPoint(pt)
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
def addComponent(self, glyphName, transformation):
transformation = self._transformation.transform(transformation)
self._outPen.addComponent(glyphName, transformation)
class GuessSmoothPointPen(AbstractPointPen):
"""Filtering PointPen that tries to determine whether an on-curve point
should be "smooth", ie. that it's a "tangent" point or a "curve" point.
"""
def __init__(self, outPen):
self._outPen = outPen
self._points = None
def _flushContour(self):
points = self._points
nPoints = len(points)
if not nPoints:
return
if points[0][1] == "move":
# Open path.
indices = range(1, nPoints - 1)
elif nPoints > 1:
# Closed path. To avoid having to mod the contour index, we
# simply abuse Python's negative index feature, and start at -1
indices = range(-1, nPoints - 1)
else:
# closed path containing 1 point (!), ignore.
indices = []
for i in indices:
pt, segmentType, dummy, name, kwargs = points[i]
if segmentType is None:
continue
prev = i - 1
next = i + 1
if points[prev][1] is not None and points[next][1] is not None:
continue
# At least one of our neighbors is an off-curve point
pt = points[i][0]
prevPt = points[prev][0]
nextPt = points[next][0]
if pt != prevPt and pt != nextPt:
dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
a1 = math.atan2(dx1, dy1)
a2 = math.atan2(dx2, dy2)
if abs(a1 - a2) < 0.05:
points[i] = pt, segmentType, True, name, kwargs
for pt, segmentType, smooth, name, kwargs in points:
self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
def beginPath(self):
assert self._points is None
self._points = []
self._outPen.beginPath()
def endPath(self):
self._flushContour()
self._outPen.endPath()
self._points = None
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._points.append((pt, segmentType, False, name, kwargs))
def addComponent(self, glyphName, transformation):
assert self._points is None
self._outPen.addComponent(glyphName, transformation)
if __name__ == "__main__":
from fontTools.pens.basePen import _TestPen as PSPen
from robofab.pens.pointPen import PrintingPointPen
segmentPen = PSPen(None)
# pen = PointToSegmentPen(SegmentToPointPen(PointToSegmentPen(PSPen(None))))
pen = PointToSegmentPen(SegmentToPointPen(PrintingPointPen()))
# pen = PrintingPointPen()
pen = PointToSegmentPen(PSPen(None), outputImpliedClosingLine=False)
# pen.beginPath()
# pen.addPoint((50, 50), name="an anchor")
# pen.endPath()
pen.beginPath()
pen.addPoint((-100, 0), segmentType="line")
pen.addPoint((0, 0), segmentType="line")
pen.addPoint((0, 100), segmentType="line")
pen.addPoint((30, 200))
pen.addPoint((50, 100), name="superbezcontrolpoint!")
pen.addPoint((70, 200))
pen.addPoint((100, 100), segmentType="curve")
pen.addPoint((100, 0), segmentType="line")
pen.endPath()
# pen.addComponent("a", (1, 0, 0, 1, 100, 200))

View file

@ -0,0 +1,132 @@
from robofab.world import RFont
from fontTools.pens.basePen import BasePen
from robofab.misc.arrayTools import updateBounds, pointInRect, unionRect
from robofab.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
from robofab.pens.filterPen import _estimateCubicCurveLength, _getCubicPoint
import math
__all__ = ["AngledMarginPen", "getAngledMargins",
"setAngledLeftMargin", "setAngledRightMargin",
"centerAngledMargins"]
class AngledMarginPen(BasePen):
"""
Angled Margin Pen
Pen to calculate the margins as if the margin lines were slanted
according to the font.info.italicAngle.
Notes:
- this pen works on the on-curve points, and approximates the distance to curves.
- results will be float.
- when used in FontLab, the resulting margins may be slightly
different from the values originally set, due to rounding errors.
- similar to what RoboFog used to do.
- RoboFog had a special attribute for "italicoffset", horizontal
shift of all glyphs. This is missing in Robofab.
"""
def __init__(self, glyphSet, width, italicAngle):
BasePen.__init__(self, glyphSet)
self.width = width
self._angle = math.radians(90+italicAngle)
self.maxSteps = 100
self.margin = None
self._left = None
self._right = None
self._start = None
self.currentPt = None
def _getAngled(self, pt):
r = (g.width + (pt[1] / math.tan(self._angle)))-pt[0]
l = pt[0]-((pt[1] / math.tan(self._angle)))
if self._right is None:
self._right = r
else:
self._right = min(self._right, r)
if self._left is None:
self._left = l
else:
self._left = min(self._left, l)
#print pt, l, r
self.margin = self._left, self._right
def _moveTo(self, pt):
self._start = self.currentPt = pt
def _addMoveTo(self):
if self._start is None:
return
self._start = self.currentPt = None
def _lineTo(self, pt):
self._addMoveTo()
self._getAngled(pt)
def _curveToOne(self, pt1, pt2, pt3):
step = 1.0/self.maxSteps
factors = range(0, self.maxSteps+1)
for i in factors:
pt = _getCubicPoint(i*step, self.currentPt, pt1, pt2, pt3)
self._getAngled(pt)
self.currentPt = pt3
def _qCurveToOne(self, bcp, pt):
self._addMoveTo()
# add curve tracing magic here.
self._getAngled(pt)
self.currentPt = pt3
def getAngledMargins(glyph, font):
"""Get the angled margins for this glyph."""
pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
glyph.draw(pen)
return pen.margin
def setAngledLeftMargin(glyph, font, value):
"""Set the left angled margin to value, adjusted for font.info.italicAngle."""
pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
g.draw(pen)
isLeft, isRight = pen.margin
glyph.leftMargin += value-isLeft
def setAngledRightMargin(glyph, font, value):
"""Set the right angled margin to value, adjusted for font.info.italicAngle."""
pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
g.draw(pen)
isLeft, isRight = pen.margin
glyph.rightMargin += value-isRight
def centerAngledMargins(glyph, font):
"""Center the glyph on angled margins."""
pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
g.draw(pen)
isLeft, isRight = pen.margin
setAngledLeftMargin(glyph, font, (isLeft+isRight)*.5)
setAngledRightMargin(glyph, font, (isLeft+isRight)*.5)
def guessItalicOffset(glyph, font):
"""Guess the italic offset based on the margins of a symetric glyph.
For instance H or I.
"""
l, r = getAngledMargins(glyph, font)
return l - (l+r)*.5
if __name__ == "__main__":
# example for FontLab, with a glyph open.
from robofab.world import CurrentFont, CurrentGlyph
g = CurrentGlyph()
f = CurrentFont()
print "margins!", getAngledMargins(g, f)
# set the angled margin to a value
m = 50
setAngledLeftMargin(g, f, m)
setAngledRightMargin(g, f, m)
g.update()

View file

@ -0,0 +1,95 @@
from fontTools.pens.basePen import BasePen
from robofab.misc.arrayTools import updateBounds, pointInRect, unionRect
from robofab.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
__all__ = ["BoundsPen", "ControlBoundsPen"]
class ControlBoundsPen(BasePen):
"""Pen to calculate the "control bounds" of a shape. This is the
bounding box of all control points __on closed paths__, so may be larger than the
actual bounding box if there are curves that don't have points
on their extremes.
Single points, or anchors, are ignored.
When the shape has been drawn, the bounds are available as the
'bounds' attribute of the pen object. It's a 4-tuple:
(xMin, yMin, xMax, yMax)
This replaces fontTools/pens/boundsPen (temporarily?)
The fontTools bounds pen takes lose anchor points into account,
this one doesn't.
"""
def __init__(self, glyphSet):
BasePen.__init__(self, glyphSet)
self.bounds = None
self._start = None
def _moveTo(self, pt):
self._start = pt
def _addMoveTo(self):
if self._start is None:
return
bounds = self.bounds
if bounds:
self.bounds = updateBounds(bounds, self._start)
else:
x, y = self._start
self.bounds = (x, y, x, y)
self._start = None
def _lineTo(self, pt):
self._addMoveTo()
self.bounds = updateBounds(self.bounds, pt)
def _curveToOne(self, bcp1, bcp2, pt):
self._addMoveTo()
bounds = self.bounds
bounds = updateBounds(bounds, bcp1)
bounds = updateBounds(bounds, bcp2)
bounds = updateBounds(bounds, pt)
self.bounds = bounds
def _qCurveToOne(self, bcp, pt):
self._addMoveTo()
bounds = self.bounds
bounds = updateBounds(bounds, bcp)
bounds = updateBounds(bounds, pt)
self.bounds = bounds
class BoundsPen(ControlBoundsPen):
"""Pen to calculate the bounds of a shape. It calculates the
correct bounds even when the shape contains curves that don't
have points on their extremes. This is somewhat slower to compute
than the "control bounds".
When the shape has been drawn, the bounds are available as the
'bounds' attribute of the pen object. It's a 4-tuple:
(xMin, yMin, xMax, yMax)
"""
def _curveToOne(self, bcp1, bcp2, pt):
self._addMoveTo()
bounds = self.bounds
bounds = updateBounds(bounds, pt)
if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds):
bounds = unionRect(bounds, calcCubicBounds(
self._getCurrentPoint(), bcp1, bcp2, pt))
self.bounds = bounds
def _qCurveToOne(self, bcp, pt):
self._addMoveTo()
bounds = self.bounds
bounds = updateBounds(bounds, pt)
if not pointInRect(bcp, bounds):
bounds = unionRect(bounds, calcQuadraticBounds(
self._getCurrentPoint(), bcp, pt))
self.bounds = bounds

View file

@ -0,0 +1,106 @@
"""A couple of point pens which return the glyph as a list of basic values."""
from robofab.pens.pointPen import AbstractPointPen
class DigestPointPen(AbstractPointPen):
"""Calculate a digest of all points
AND coordinates
AND components
in a glyph.
"""
def __init__(self, ignoreSmoothAndName=False):
self._data = []
self.ignoreSmoothAndName = ignoreSmoothAndName
def beginPath(self):
self._data.append('beginPath')
def endPath(self):
self._data.append('endPath')
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
if self.ignoreSmoothAndName:
self._data.append((pt, segmentType))
else:
self._data.append((pt, segmentType, smooth, name))
def addComponent(self, baseGlyphName, transformation):
t = []
for v in transformation:
if int(v) == v:
t.append(int(v))
else:
t.append(v)
self._data.append((baseGlyphName, tuple(t)))
def getDigest(self):
return tuple(self._data)
def getDigestPointsOnly(self, needSort=True):
""" Return a tuple with all coordinates of all points,
but without smooth info or drawing instructions.
For instance if you want to compare 2 glyphs in shape,
but not interpolatability.
"""
points = []
from types import TupleType
for item in self._data:
if type(item) == TupleType:
points.append(item[0])
if needSort:
points.sort()
return tuple(points)
class DigestPointStructurePen(DigestPointPen):
"""Calculate a digest of the structure of the glyph
NOT coordinates
NOT values.
"""
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._data.append(segmentType)
def addComponent(self, baseGlyphName, transformation):
self._data.append(baseGlyphName)
if __name__ == "__main__":
"""
beginPath
((112, 651), 'line', False, None)
((112, 55), 'line', False, None)
((218, 55), 'line', False, None)
((218, 651), 'line', False, None)
endPath
"""
# a test
from robofab.objects.objectsRF import RGlyph
g = RGlyph()
p = g.getPen()
p.moveTo((112, 651))
p.lineTo((112, 55))
p.lineTo((218, 55))
p.lineTo((218, 651))
p.closePath()
print g, len(g)
digestPen = DigestPointPen()
g.drawPoints(digestPen)
print
print "getDigest", digestPen.getDigest()
print
print "getDigestPointsOnly", digestPen.getDigestPointsOnly()

View file

@ -0,0 +1,407 @@
"""A couple of point pens to filter contours in various ways."""
from fontTools.pens.basePen import AbstractPen, BasePen
from robofab.pens.pointPen import AbstractPointPen
from robofab.objects.objectsRF import RGlyph as _RGlyph
from robofab.objects.objectsBase import _interpolatePt
import math
#
# threshold filtering
#
def distance(pt1, pt2):
return math.hypot(pt1[0]-pt2[0], pt1[1]-pt2[1])
class ThresholdPointPen(AbstractPointPen):
"""
Rewrite of the ThresholdPen as a PointPen
so that we can preserve named points and other arguments.
This pen will add components from the original glyph, but
but it won't filter those components.
"move", "line", "curve" or "qcurve"
"""
def __init__(self, otherPointPen, threshold=10):
self.threshold = threshold
self._lastPt = None
self._offCurveBuffer = []
self.otherPointPen = otherPointPen
def beginPath(self):
"""Start a new sub path."""
self.otherPointPen.beginPath()
self._lastPt = None
def endPath(self):
"""End the current sub path."""
self.otherPointPen.endPath()
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
"""Add a point to the current sub path."""
if segmentType in ['curve', 'qcurve']:
# it's an offcurve, let's buffer them until we get another oncurve
# and we know what to do with them
self._offCurveBuffer.append((pt, segmentType, smooth, name, kwargs))
return
elif segmentType == "move":
# start of an open contour
self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs?
self._lastPt = pt
self._offCurveBuffer = []
elif segmentType == "line":
if self._lastPt is None:
self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs?
self._lastPt = pt
elif distance(pt, self._lastPt) >= self.threshold:
# we're oncurve and far enough from the last oncurve
if self._offCurveBuffer:
# empty any buffered offcurves
for buf_pt, buf_segmentType, buf_smooth, buf_name, buf_kwargs in self._offCurveBuffer:
self.otherPointPen.addPoint(buf_pt, buf_segmentType, buf_smooth, buf_name) # how to add kwargs?
self._offCurveBuffer = []
# finally add the oncurve.
self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs?
self._lastPt = pt
else:
# we're too short, so we're not going to make it.
# we need to clear out the offcurve buffer.
self._offCurveBuffer = []
def addComponent(self, baseGlyphName, transformation):
"""Add a sub glyph. Note: this way components are not filtered."""
self.otherPointPen.addComponent(baseGlyphName, transformation)
class ThresholdPen(AbstractPen):
"""Removes segments shorter in length than the threshold value."""
def __init__(self, otherPen, threshold=10):
self.threshold = threshold
self._lastPt = None
self.otherPen = otherPen
def moveTo(self, pt):
self._lastPt = pt
self.otherPen.moveTo(pt)
def lineTo(self, pt, smooth=False):
if self.threshold <= distance(pt, self._lastPt):
self.otherPen.lineTo(pt)
self._lastPt = pt
def curveTo(self, pt1, pt2, pt3):
if self.threshold <= distance(pt3, self._lastPt):
self.otherPen.curveTo(pt1, pt2, pt3)
self._lastPt = pt3
def qCurveTo(self, *points):
if self.threshold <= distance(points[-1], self._lastPt):
self.otherPen.qCurveTo(*points)
self._lastPt = points[-1]
def closePath(self):
self.otherPen.closePath()
def endPath(self):
self.otherPen.endPath()
def addComponent(self, glyphName, transformation):
self.otherPen.addComponent(glyphName, transformation)
def thresholdGlyph(aGlyph, threshold=10):
""" Convenience function that handles the filtering. """
from robofab.pens.adapterPens import PointToSegmentPen
new = _RGlyph()
filterpen = ThresholdPen(new.getPen(), threshold)
wrappedPen = PointToSegmentPen(filterpen)
aGlyph.drawPoints(wrappedPen)
aGlyph.clear()
aGlyph.appendGlyph(new)
aGlyph.update()
return aGlyph
def thresholdGlyphPointPen(aGlyph, threshold=10):
""" Same a thresholdGlyph, but using the ThresholdPointPen, which should respect anchors."""
from robofab.pens.adapterPens import PointToSegmentPen
new = _RGlyph()
wrappedPen = new.getPointPen()
filterpen = ThresholdPointPen(wrappedPen, threshold)
aGlyph.drawPoints(filterpen)
aGlyph.clear()
new.drawPoints(aGlyph.getPointPen())
aGlyph.update()
return aGlyph
#
# Curve flattening
#
def _estimateCubicCurveLength(pt0, pt1, pt2, pt3, precision=10):
"""Estimate the length of this curve by iterating
through it and averaging the length of the flat bits.
"""
points = []
length = 0
step = 1.0/precision
factors = range(0, precision+1)
for i in factors:
points.append(_getCubicPoint(i*step, pt0, pt1, pt2, pt3))
for i in range(len(points)-1):
pta = points[i]
ptb = points[i+1]
length += distance(pta, ptb)
return length
def _mid((x0, y0), (x1, y1)):
"""(Point, Point) -> Point\nReturn the point that lies in between the two input points."""
return 0.5 * (x0 + x1), 0.5 * (y0 + y1)
def _getCubicPoint(t, pt0, pt1, pt2, pt3):
if t == 0:
return pt0
if t == 1:
return pt3
if t == 0.5:
a = _mid(pt0, pt1)
b = _mid(pt1, pt2)
c = _mid(pt2, pt3)
d = _mid(a, b)
e = _mid(b, c)
return _mid(d, e)
else:
cx = (pt1[0] - pt0[0]) * 3
cy = (pt1[1] - pt0[1]) * 3
bx = (pt2[0] - pt1[0]) * 3 - cx
by = (pt2[1] - pt1[1]) * 3 - cy
ax = pt3[0] - pt0[0] - cx - bx
ay = pt3[1] - pt0[1] - cy - by
t3 = t ** 3
t2 = t * t
x = ax * t3 + bx * t2 + cx * t + pt0[0]
y = ay * t3 + by * t2 + cy * t + pt0[1]
return x, y
class FlattenPen(BasePen):
"""Process the contours into a series of straight lines by flattening the curves.
"""
def __init__(self, otherPen, approximateSegmentLength=5, segmentLines=False, filterDoubles=True):
self.approximateSegmentLength = approximateSegmentLength
BasePen.__init__(self, {})
self.otherPen = otherPen
self.currentPt = None
self.firstPt = None
self.segmentLines = segmentLines
self.filterDoubles = filterDoubles
def _moveTo(self, pt):
self.otherPen.moveTo(pt)
self.currentPt = pt
self.firstPt = pt
def _lineTo(self, pt):
if self.filterDoubles:
if pt == self.currentPt:
return
if not self.segmentLines:
self.otherPen.lineTo(pt)
self.currentPt = pt
return
d = distance(self.currentPt, pt)
maxSteps = int(round(d / self.approximateSegmentLength))
if maxSteps < 1:
self.otherPen.lineTo(pt)
self.currentPt = pt
return
step = 1.0/maxSteps
factors = range(0, maxSteps+1)
for i in factors[1:]:
self.otherPen.lineTo(_interpolatePt(self.currentPt, pt, i*step))
self.currentPt = pt
def _curveToOne(self, pt1, pt2, pt3):
est = _estimateCubicCurveLength(self.currentPt, pt1, pt2, pt3)/self.approximateSegmentLength
maxSteps = int(round(est))
falseCurve = (pt1==self.currentPt) and (pt2==pt3)
if maxSteps < 1 or falseCurve:
self.otherPen.lineTo(pt3)
self.currentPt = pt3
return
step = 1.0/maxSteps
factors = range(0, maxSteps+1)
for i in factors[1:]:
pt = _getCubicPoint(i*step, self.currentPt, pt1, pt2, pt3)
self.otherPen.lineTo(pt)
self.currentPt = pt3
def _closePath(self):
self.lineTo(self.firstPt)
self.otherPen.closePath()
self.currentPt = None
def _endPath(self):
self.otherPen.endPath()
self.currentPt = None
def addComponent(self, glyphName, transformation):
self.otherPen.addComponent(glyphName, transformation)
def flattenGlyph(aGlyph, threshold=10, segmentLines=True):
"""Replace curves with series of straight l ines."""
from robofab.pens.adapterPens import PointToSegmentPen
if len(aGlyph.contours) == 0:
return
new = _RGlyph()
writerPen = new.getPen()
filterpen = FlattenPen(writerPen, threshold, segmentLines)
wrappedPen = PointToSegmentPen(filterpen)
aGlyph.drawPoints(wrappedPen)
aGlyph.clear()
aGlyph.appendGlyph(new)
aGlyph.update()
return aGlyph
def spikeGlyph(aGlyph, segmentLength=20, spikeLength=40, patternFunc=None):
"""Add narly spikes or dents to the glyph.
patternFunc is an optional function which recalculates the offset."""
from math import atan2, sin, cos, pi
new = _RGlyph()
new.appendGlyph(aGlyph)
new.width = aGlyph.width
if len(new.contours) == 0:
return
flattenGlyph(new, segmentLength, segmentLines=True)
for contour in new:
l = len(contour.points)
lastAngle = None
for i in range(0, len(contour.points), 2):
prev = contour.points[i-1]
cur = contour.points[i]
next = contour.points[(i+1)%l]
angle = atan2(prev.x - next.x, prev.y - next.y)
lastAngle = angle
if patternFunc is not None:
thisSpikeLength = patternFunc(spikeLength)
else:
thisSpikeLength = spikeLength
cur.x -= sin(angle+.5*pi)*thisSpikeLength
cur.y -= cos(angle+.5*pi)*thisSpikeLength
new.update()
aGlyph.clear()
aGlyph.appendGlyph(new)
aGlyph.update()
return aGlyph
def halftoneGlyph(aGlyph, invert=False):
"""Convert the glyph into some sort of halftoning pattern.
Measure a bunch of inside/outside points to simulate grayscale levels.
Slow.
"""
print 'halftoneGlyph is running...'
grid = {}
drawing = {}
dataDistance = 10
scan = 2
preload = 0
cellDistance = dataDistance * 5
overshoot = dataDistance * 2
(xMin, yMin, xMax, yMax) = aGlyph.box
for x in range(xMin-overshoot, xMax+overshoot, dataDistance):
print 'scanning..', x
for y in range(yMin-overshoot, yMax+overshoot, dataDistance):
if aGlyph.pointInside((x, y)):
grid[(x, y)] = True
else:
grid[(x, y)] = False
#print 'gathering data', x, y, grid[(x, y)]
print 'analyzing..'
for x in range(xMin-overshoot, xMax+overshoot, cellDistance):
for y in range(yMin-overshoot, yMax+overshoot, cellDistance):
total = preload
for scanx in range(-scan, scan):
for scany in range(-scan, scan):
if grid.get((x+scanx*dataDistance, y+scany*dataDistance)):
total += 1
if invert:
drawing[(x, y)] = 2*scan**2 - float(total)
else:
drawing[(x, y)] = float(total)
aGlyph.clear()
print drawing
for (x,y) in drawing.keys():
size = drawing[(x,y)] / float(2*scan**2) * 5
pen = aGlyph.getPen()
pen.moveTo((x-size, y-size))
pen.lineTo((x+size, y-size))
pen.lineTo((x+size, y+size))
pen.lineTo((x-size, y+size))
pen.lineTo((x-size, y-size))
pen.closePath()
aGlyph.update()
if __name__ == "__main__":
from robofab.pens.pointPen import PrintingPointPen
pp = PrintingPointPen()
#pp.beginPath()
#pp.addPoint((100, 100))
#pp.endPath()
tpp = ThresholdPointPen(pp, threshold=20)
tpp.beginPath()
#segmentType=None, smooth=False, name=None
tpp.addPoint((100, 100), segmentType="line", smooth=True)
# section that should be too small
tpp.addPoint((100, 102), segmentType="line", smooth=True)
tpp.addPoint((200, 200), segmentType="line", smooth=True)
# curve section with final point that's far enough, but with offcurves that are under the threshold
tpp.addPoint((200, 205), segmentType="curve", smooth=True)
tpp.addPoint((300, 295), segmentType="curve", smooth=True)
tpp.addPoint((300, 300), segmentType="line", smooth=True)
# curve section with final point that is not far enough
tpp.addPoint((550, 350), segmentType="curve", smooth=True)
tpp.addPoint((360, 760), segmentType="curve", smooth=True)
tpp.addPoint((310, 310), segmentType="line", smooth=True)
tpp.addPoint((400, 400), segmentType="line", smooth=True)
tpp.addPoint((100, 100), segmentType="line", smooth=True)
tpp.endPath()
# couple of single points with names
tpp.beginPath()
tpp.addPoint((500, 500), segmentType="move", smooth=True, name="named point")
tpp.addPoint((600, 500), segmentType="move", smooth=True, name="named point")
tpp.addPoint((601, 501), segmentType="move", smooth=True, name="named point")
tpp.endPath()
# open path
tpp.beginPath()
tpp.addPoint((500, 500), segmentType="move", smooth=True)
tpp.addPoint((501, 500), segmentType="line", smooth=True)
tpp.addPoint((101, 500), segmentType="line", smooth=True)
tpp.addPoint((101, 100), segmentType="line", smooth=True)
tpp.addPoint((498, 498), segmentType="line", smooth=True)
tpp.endPath()

274
misc/pylib/robofab/pens/flPen.py Executable file
View file

@ -0,0 +1,274 @@
"""Pens for creating glyphs in FontLab."""
__all__ = ["FLPen", "FLPointPen", "drawFLGlyphOntoPointPen"]
from FL import *
try:
from fl_cmd import *
except ImportError:
print "The fl_cmd module is not available here. flPen.py"
from robofab.tools.toolsFL import NewGlyph
from robofab.pens.pointPen import AbstractPointPen
from robofab.pens.adapterPens import SegmentToPointPen
def roundInt(x):
return int(round(x))
class FLPen(SegmentToPointPen):
def __init__(self, glyph):
SegmentToPointPen.__init__(self, FLPointPen(glyph))
class FLPointPen(AbstractPointPen):
def __init__(self, glyph):
if hasattr(glyph, "isRobofab"):
self.glyph = glyph.naked()
else:
self.glyph = glyph
self.currentPath = None
def beginPath(self):
self.currentPath = []
def endPath(self):
# Love is... abstracting away FL's madness.
path = self.currentPath
self.currentPath = None
glyph = self.glyph
if len(path) == 1 and path[0][3] is not None:
# Single point on the contour, it has a name. Make it an anchor.
x, y = path[0][0]
name = path[0][3]
anchor = Anchor(name, roundInt(x), roundInt(y))
glyph.anchors.append(anchor)
return
firstOnCurveIndex = None
for i in range(len(path)):
if path[i][1] is not None:
firstOnCurveIndex = i
break
if firstOnCurveIndex is None:
# TT special case: on-curve-less contour. FL doesn't support that,
# so we insert an implied point at the end.
x1, y1 = path[0][0]
x2, y2 = path[-1][0]
impliedPoint = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
path.append((impliedPoint, "qcurve", True, None))
firstOnCurveIndex = 0
path = path[firstOnCurveIndex + 1:] + path[:firstOnCurveIndex + 1]
firstPoint, segmentType, smooth, name = path[-1]
closed = True
if segmentType == "move":
path = path[:-1]
closed = False
# XXX The contour is not closed, but I can't figure out how to
# create an open contour in FL. Creating one by hand shows type"0x8011"
# for a move node in an open contour, but I'm not able to access
# that flag.
elif segmentType == "line":
# The contour is closed and ends in a lineto, which is redundant
# as it's implied by closepath.
path = path[:-1]
x, y = firstPoint
node = Node(nMOVE, Point(roundInt(x), roundInt(y)))
if smooth and closed:
if segmentType == "line" or path[0][1] == "line":
node.alignment = nFIXED
else:
node.alignment = nSMOOTH
glyph.Insert(node, len(glyph))
segment = []
nPoints = len(path)
for i in range(nPoints):
pt, segmentType, smooth, name = path[i]
segment.append(pt)
if segmentType is None:
continue
if segmentType == "curve":
if len(segment) < 2:
segmentType = "line"
elif len(segment) == 2:
segmentType = "qcurve"
if segmentType == "qcurve":
for x, y in segment[:-1]:
glyph.Insert(Node(nOFF, Point(roundInt(x), roundInt(y))), len(glyph))
x, y = segment[-1]
node = Node(nLINE, Point(roundInt(x), roundInt(y)))
glyph.Insert(node, len(glyph))
elif segmentType == "curve":
if len(segment) == 3:
cubicSegments = [segment]
else:
from fontTools.pens.basePen import decomposeSuperBezierSegment
cubicSegments = decomposeSuperBezierSegment(segment)
nSegments = len(cubicSegments)
for i in range(nSegments):
pt1, pt2, pt3 = cubicSegments[i]
x, y = pt3
node = Node(nCURVE, Point(roundInt(x), roundInt(y)))
node.points[1].x, node.points[1].y = roundInt(pt1[0]), roundInt(pt1[1])
node.points[2].x, node.points[2].y = roundInt(pt2[0]), roundInt(pt2[1])
if i != nSegments - 1:
node.alignment = nSMOOTH
glyph.Insert(node, len(self.glyph))
elif segmentType == "line":
assert len(segment) == 1, segment
x, y = segment[0]
node = Node(nLINE, Point(roundInt(x), roundInt(y)))
glyph.Insert(node, len(glyph))
else:
assert 0, "unsupported curve type (%s)" % segmentType
if smooth:
if i + 1 < nPoints or closed:
# Can't use existing node, as you can't change node attributes
# AFTER it's been appended to the glyph.
node = glyph[-1]
if segmentType == "line" or path[(i+1) % nPoints][1] == "line":
# tangent
node.alignment = nFIXED
else:
# curve
node.alignment = nSMOOTH
segment = []
if closed:
# we may have output a node too much
node = glyph[-1]
if node.type == nLINE and (node.x, node.y) == (roundInt(firstPoint[0]), roundInt(firstPoint[1])):
glyph.DeleteNode(len(glyph) - 1)
def addPoint(self, pt, segmentType=None, smooth=None, name=None, **kwargs):
self.currentPath.append((pt, segmentType, smooth, name))
def addComponent(self, baseName, transformation):
assert self.currentPath is None
# make base glyph if needed, Component() needs the index
NewGlyph(self.glyph.parent, baseName, updateFont=False)
baseIndex = self.glyph.parent.FindGlyph(baseName)
if baseIndex == -1:
raise KeyError, "couldn't find or make base glyph"
xx, xy, yx, yy, dx, dy = transformation
# XXX warn when xy or yx != 0
new = Component(baseIndex, Point(dx, dy), Point(xx, yy))
self.glyph.components.append(new)
def drawFLGlyphOntoPointPen(flGlyph, pen):
"""Draw a FontLab glyph onto a PointPen."""
for anchor in flGlyph.anchors:
pen.beginPath()
pen.addPoint((anchor.x, anchor.y), name=anchor.name)
pen.endPath()
for contour in _getContours(flGlyph):
pen.beginPath()
for pt, segmentType, smooth in contour:
pen.addPoint(pt, segmentType=segmentType, smooth=smooth)
pen.endPath()
for baseGlyph, tranform in _getComponents(flGlyph):
pen.addComponent(baseGlyph, tranform)
class FLPointContourPen(FLPointPen):
"""Same as FLPointPen, except that it ignores components."""
def addComponent(self, baseName, transformation):
pass
NODE_TYPES = {nMOVE: "move", nLINE: "line", nCURVE: "curve",
nOFF: None}
def _getContours(glyph):
contours = []
for i in range(len(glyph)):
node = glyph[i]
segmentType = NODE_TYPES[node.type]
if segmentType == "move":
contours.append([])
for pt in node.points[1:]:
contours[-1].append(((pt.x, pt.y), None, False))
smooth = node.alignment != nSHARP
contours[-1].append(((node.x, node.y), segmentType, smooth))
for contour in contours:
# filter out or change the move
movePt, segmentType, smooth = contour[0]
assert segmentType == "move"
lastSegmentType = contour[-1][1]
if movePt == contour[-1][0] and lastSegmentType == "curve":
contour[0] = contour[-1]
contour.pop()
elif lastSegmentType is None:
contour[0] = movePt, "qcurve", smooth
else:
assert lastSegmentType in ("line", "curve")
contour[0] = movePt, "line", smooth
# change "line" to "qcurve" if appropriate
previousSegmentType = "ArbitraryValueOtherThanNone"
for i in range(len(contour)):
pt, segmentType, smooth = contour[i]
if segmentType == "line" and previousSegmentType is None:
contour[i] = pt, "qcurve", smooth
previousSegmentType = segmentType
return contours
def _simplifyValues(*values):
"""Given a set of numbers, convert items to ints if they are
integer float values, eg. 0.0, 1.0."""
newValues = []
for v in values:
i = int(v)
if v == i:
v = i
newValues.append(v)
return newValues
def _getComponents(glyph):
components = []
for comp in glyph.components:
baseName = glyph.parent[comp.index].name
dx, dy = comp.delta.x, comp.delta.y
sx, sy = comp.scale.x, comp.scale.y
dx, dy, sx, sy = _simplifyValues(dx, dy, sx, sy)
components.append((baseName, (sx, 0, 0, sy, dx, dy)))
return components
def test():
g = fl.glyph
g.Clear()
p = PLPen(g)
p.moveTo((50, 50))
p.lineTo((150,50))
p.lineTo((170, 200), smooth=2)
p.curveTo((173, 225), (150, 250), (120, 250), smooth=1)
p.curveTo((85, 250), (50, 200), (50, 200))
p.closePath()
p.moveTo((300, 300))
p.lineTo((400, 300))
p.curveTo((450, 325), (450, 375), (400, 400))
p.qCurveTo((400, 500), (350, 550), (300, 500), (300, 400))
p.closePath()
p.setWidth(600)
p.setNote("Hello, this is a note")
p.addAnchor("top", (250, 600))
fl.UpdateGlyph(-1)
fl.UpdateFont(-1)
if __name__ == "__main__":
test()

View file

@ -0,0 +1,155 @@
from fontTools.pens.basePen import AbstractPen, BasePen
from robofab.misc.bezierTools import splitLine, splitCubic
from sets import Set
class MarginPen(BasePen):
"""
Pen to calculate the margins at a given value.
When isHorizontal is True, the margins at <value> are horizontal.
When isHorizontal is False, the margins at <value> are vertical.
When a glyphset or font is given, MarginPen will also calculate for glyphs with components.
pen.getMargins() returns the minimum and maximum intersections of the glyph.
pen.getContourMargins() returns the minimum and maximum intersections for each contour.
Possible optimisation:
Initialise the pen object with a list of points we want to measure,
then draw the glyph once, but do the splitLine() math for all measure points.
"""
def __init__(self, glyphSet, value, isHorizontal=True):
BasePen.__init__(self, glyphSet)
self.value = value
self.hits = {}
self.filterDoubles = True
self.contourIndex = None
self.startPt = None
self.isHorizontal = isHorizontal
def _moveTo(self, pt):
self.currentPt = pt
self.startPt = pt
if self.contourIndex is None:
self.contourIndex = 0
else:
self.contourIndex += 1
def _lineTo(self, pt):
if self.filterDoubles:
if pt == self.currentPt:
return
hits = splitLine(self.currentPt, pt, self.value, self.isHorizontal)
if len(hits)>1:
# result will be 2 tuples of 2 coordinates
# first two points: start to intersect
# second two points: intersect to end
# so, second point in first tuple is the intersect
# then, the first coordinate of that point is the x.
if not self.contourIndex in self.hits:
self.hits[self.contourIndex] = []
if self.isHorizontal:
self.hits[self.contourIndex].append(round(hits[0][-1][0], 4))
else:
self.hits[self.contourIndex].append(round(hits[0][-1][1], 4))
if self.isHorizontal and pt[1] == self.value:
# it could happen
if not self.contourIndex in self.hits:
self.hits[self.contourIndex] = []
self.hits[self.contourIndex].append(pt[0])
elif (not self.isHorizontal) and (pt[0] == self.value):
# it could happen
if not self.contourIndex in self.hits:
self.hits[self.contourIndex] = []
self.hits[self.contourIndex].append(pt[1])
self.currentPt = pt
def _curveToOne(self, pt1, pt2, pt3):
hits = splitCubic(self.currentPt, pt1, pt2, pt3, self.value, self.isHorizontal)
for i in range(len(hits)-1):
# a number of intersections is possible. Just take the
# last point of each segment.
if not self.contourIndex in self.hits:
self.hits[self.contourIndex] = []
if self.isHorizontal:
self.hits[self.contourIndex].append(round(hits[i][-1][0], 4))
else:
self.hits[self.contourIndex].append(round(hits[i][-1][1], 4))
if self.isHorizontal and pt3[1] == self.value:
# it could happen
if not self.contourIndex in self.hits:
self.hits[self.contourIndex] = []
self.hits[self.contourIndex].append(pt3[0])
if (not self.isHorizontal) and (pt3[0] == self.value):
# it could happen
if not self.contourIndex in self.hits:
self.hits[self.contourIndex] = []
self.hits[self.contourIndex].append(pt3[1])
self.currentPt = pt3
def _closePath(self):
if self.currentPt != self.startPt:
self._lineTo(self.startPt)
self.currentPt = self.startPt = None
def _endPath(self):
self.currentPt = None
def addComponent(self, baseGlyph, transformation):
from fontTools.pens.transformPen import TransformPen
if self.glyphSet is None:
return
if baseGlyph in self.glyphSet:
glyph = self.glyphSet[baseGlyph]
if glyph is None:
return
tPen = TransformPen(self, transformation)
glyph.draw(tPen)
def getMargins(self):
"""Get the horizontal margins for all contours combined, i.e. the whole glyph."""
allHits = []
for index, pts in self.hits.items():
allHits.extend(pts)
if allHits:
return min(allHits), max(allHits)
return None
def getContourMargins(self):
"""Get the horizontal margins for each contour."""
allHits = {}
for index, pts in self.hits.items():
unique = list(Set(pts))
unique.sort()
allHits[index] = unique
return allHits
def getAll(self):
"""Get all the slices."""
allHits = []
for index, pts in self.hits.items():
allHits.extend(pts)
unique = list(Set(allHits))
unique = list(unique)
unique.sort()
return unique
if __name__ == "__main__":
from robofab.world import CurrentGlyph, CurrentFont
f = CurrentFont()
g = CurrentGlyph()
pt = (74, 216)
pen = MarginPen(f, pt[1], isHorizontal=False)
g.draw(pen)
print 'glyph Y margins', pen.getMargins()
print pen.getContourMargins()

View file

@ -0,0 +1,185 @@
"""Some pens that are needed during glyph math"""
from robofab.pens.pointPen import BasePointToSegmentPen, AbstractPointPen
class GetMathDataPointPen(AbstractPointPen):
"""
Point pen that converts all "line" segments into
curve segments containing two off curve points.
"""
def __init__(self):
self.contours = []
self.components = []
self.anchors = []
self._points = []
def _flushContour(self):
points = self._points
if len(points) == 1:
segmentType, pt, smooth, name = points[0]
self.anchors.append((pt, name))
else:
self.contours.append([])
prevOnCurve = None
offCurves = []
# deal with the first point
segmentType, pt, smooth, name = points[0]
# if it is an offcurve, add it to the offcurve list
if segmentType is None:
offCurves.append((segmentType, pt, smooth, name))
# if it is a line, change the type to curve and add it to the contour
# create offcurves corresponding with the last oncurve and
# this point and add them to the points list
elif segmentType == "line":
prevOnCurve = pt
self.contours[-1].append(("curve", pt, False, name))
lastPoint = points[-1][1]
points.append((None, lastPoint, False, None))
points.append((None, pt, False, None))
# a move, curve or qcurve. simple append the data.
else:
self.contours[-1].append((segmentType, pt, smooth, name))
prevOnCurve = pt
# now go through the rest of the points
for segmentType, pt, smooth, name in points[1:]:
# store the off curves
if segmentType is None:
offCurves.append((segmentType, pt, smooth, name))
continue
# make off curve corresponding the the previous
# on curve an dthis point
if segmentType == "line":
segmentType = "curve"
offCurves.append((None, prevOnCurve, False, None))
offCurves.append((None, pt, False, None))
# add the offcurves to the contour
for offCurve in offCurves:
self.contours[-1].append(offCurve)
# add the oncurve to the contour
self.contours[-1].append((segmentType, pt, smooth, name))
# reset the stored data
prevOnCurve = pt
offCurves = []
# catch offcurves that belong to the first
if len(offCurves) != 0:
self.contours[-1].extend(offCurves)
def beginPath(self):
self._points = []
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._points.append((segmentType, pt, smooth, name))
def endPath(self):
self._flushContour()
def addComponent(self, baseGlyphName, transformation):
self.components.append((baseGlyphName, transformation))
def getData(self):
return {
'contours':list(self.contours),
'components':list(self.components),
'anchors':list(self.anchors)
}
class CurveSegmentFilterPointPen(AbstractPointPen):
"""
Point pen that turns curve segments that contain
unnecessary anchor points into line segments.
"""
# XXX it would be great if this also checked to see if the
# off curves are on the segment and therefre unneeded
def __init__(self, anotherPointPen):
self._pen = anotherPointPen
self._points = []
def _flushContour(self):
points = self._points
# an anchor
if len(points) == 1:
pt, segmentType, smooth, name = points[0]
self._pen.addPoint(pt, segmentType, smooth, name)
else:
prevOnCurve = None
offCurves = []
pointsToDraw = []
# deal with the first point
pt, segmentType, smooth, name = points[0]
# if it is an offcurve, add it to the offcurve list
if segmentType is None:
offCurves.append((pt, segmentType, smooth, name))
else:
# potential redundancy
if segmentType == "curve":
# gather preceding off curves
testOffCurves = []
lastPoint = None
for i in xrange(len(points)):
i = -i - 1
testPoint = points[i]
testSegmentType = testPoint[1]
if testSegmentType is not None:
lastPoint = testPoint[0]
break
testOffCurves.append(testPoint[0])
# if two offcurves exist we can test for redundancy
if len(testOffCurves) == 2:
if testOffCurves[1] == lastPoint and testOffCurves[0] == pt:
segmentType = "line"
# remove the last two points
points = points[:-2]
# add the point to the contour
pointsToDraw.append((pt, segmentType, smooth, name))
prevOnCurve = pt
for pt, segmentType, smooth, name in points[1:]:
# store offcurves
if segmentType is None:
offCurves.append((pt, segmentType, smooth, name))
continue
# curves are a potential redundancy
elif segmentType == "curve":
if len(offCurves) == 2:
# test for redundancy
if offCurves[0][0] == prevOnCurve and offCurves[1][0] == pt:
offCurves = []
segmentType = "line"
# add all offcurves
for offCurve in offCurves:
pointsToDraw.append(offCurve)
# add the on curve
pointsToDraw.append((pt, segmentType, smooth, name))
# reset the stored data
prevOnCurve = pt
offCurves = []
# catch any remaining offcurves
if len(offCurves) != 0:
for offCurve in offCurves:
pointsToDraw.append(offCurve)
# draw to the pen
for pt, segmentType, smooth, name in pointsToDraw:
self._pen.addPoint(pt, segmentType, smooth, name)
def beginPath(self):
self._points = []
self._pen.beginPath()
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self._points.append((pt, segmentType, smooth, name))
def endPath(self):
self._flushContour()
self._pen.endPath()
def addComponent(self, baseGlyphName, transformation):
self._pen.addComponent(baseGlyphName, transformation)

View file

@ -0,0 +1,173 @@
__all__ = ["AbstractPointPen", "BasePointToSegmentPen", "PrintingPointPen",
"PrintingSegmentPen", "SegmentPrintingPointPen"]
class AbstractPointPen:
def beginPath(self):
"""Start a new sub path."""
raise NotImplementedError
def endPath(self):
"""End the current sub path."""
raise NotImplementedError
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
"""Add a point to the current sub path."""
raise NotImplementedError
def addComponent(self, baseGlyphName, transformation):
"""Add a sub glyph."""
raise NotImplementedError
class BasePointToSegmentPen(AbstractPointPen):
"""Base class for retrieving the outline in a segment-oriented
way. The PointPen protocol is simple yet also a little tricky,
so when you need an outline presented as segments but you have
as points, do use this base implementation as it properly takes
care of all the edge cases.
"""
def __init__(self):
self.currentPath = None
def beginPath(self):
assert self.currentPath is None
self.currentPath = []
def _flushContour(self, segments):
"""Override this method.
It will be called for each non-empty sub path with a list
of segments: the 'segments' argument.
The segments list contains tuples of length 2:
(segmentType, points)
segmentType is one of "move", "line", "curve" or "qcurve".
"move" may only occur as the first segment, and it signifies
an OPEN path. A CLOSED path does NOT start with a "move", in
fact it will not contain a "move" at ALL.
The 'points' field in the 2-tuple is a list of point info
tuples. The list has 1 or more items, a point tuple has
four items:
(point, smooth, name, kwargs)
'point' is an (x, y) coordinate pair.
For a closed path, the initial moveTo point is defined as
the last point of the last segment.
The 'points' list of "move" and "line" segments always contains
exactly one point tuple.
"""
raise NotImplementedError
def endPath(self):
assert self.currentPath is not None
points = self.currentPath
self.currentPath = None
if not points:
return
if len(points) == 1:
# Not much more we can do than output a single move segment.
pt, segmentType, smooth, name, kwargs = points[0]
segments = [("move", [(pt, smooth, name, kwargs)])]
self._flushContour(segments)
return
segments = []
if points[0][1] == "move":
# It's an open contour, insert a "move" segment for the first
# point and remove that first point from the point list.
pt, segmentType, smooth, name, kwargs = points[0]
segments.append(("move", [(pt, smooth, name, kwargs)]))
points.pop(0)
else:
# It's a closed contour. Locate the first on-curve point, and
# rotate the point list so that it _ends_ with an on-curve
# point.
firstOnCurve = None
for i in range(len(points)):
segmentType = points[i][1]
if segmentType is not None:
firstOnCurve = i
break
if firstOnCurve is None:
# Special case for quadratics: a contour with no on-curve
# points. Add a "None" point. (See also the Pen protocol's
# qCurveTo() method and fontTools.pens.basePen.py.)
points.append((None, "qcurve", None, None, None))
else:
points = points[firstOnCurve+1:] + points[:firstOnCurve+1]
currentSegment = []
for pt, segmentType, smooth, name, kwargs in points:
currentSegment.append((pt, smooth, name, kwargs))
if segmentType is None:
continue
segments.append((segmentType, currentSegment))
currentSegment = []
self._flushContour(segments)
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self.currentPath.append((pt, segmentType, smooth, name, kwargs))
class PrintingPointPen(AbstractPointPen):
def __init__(self):
self.havePath = False
def beginPath(self):
self.havePath = True
print "pen.beginPath()"
def endPath(self):
self.havePath = False
print "pen.endPath()"
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
assert self.havePath
args = ["(%s, %s)" % (pt[0], pt[1])]
if segmentType is not None:
args.append("segmentType=%r" % segmentType)
if smooth:
args.append("smooth=True")
if name is not None:
args.append("name=%r" % name)
if kwargs:
args.append("**%s" % kwargs)
print "pen.addPoint(%s)" % ", ".join(args)
def addComponent(self, baseGlyphName, transformation):
assert not self.havePath
print "pen.addComponent(%r, %s)" % (baseGlyphName, tuple(transformation))
from fontTools.pens.basePen import AbstractPen
class PrintingSegmentPen(AbstractPen):
def moveTo(self, pt):
print "pen.moveTo(%s)" % (pt,)
def lineTo(self, pt):
print "pen.lineTo(%s)" % (pt,)
def curveTo(self, *pts):
print "pen.curveTo%s" % (pts,)
def qCurveTo(self, *pts):
print "pen.qCurveTo%s" % (pts,)
def closePath(self):
print "pen.closePath()"
def endPath(self):
print "pen.endPath()"
def addComponent(self, baseGlyphName, transformation):
print "pen.addComponent(%r, %s)" % (baseGlyphName, tuple(transformation))
class SegmentPrintingPointPen(BasePointToSegmentPen):
def _flushContour(self, segments):
from pprint import pprint
pprint(segments)
if __name__ == "__main__":
p = SegmentPrintingPointPen()
from robofab.test.test_pens import TestShapes
TestShapes.onCurveLessQuadShape(p)

View file

@ -0,0 +1,21 @@
from fontTools.pens.basePen import BasePen
class QuartzPen(BasePen):
"""Pen to draw onto a Quartz drawing context (Carbon.CG)."""
def __init__(self, glyphSet, quartzContext):
BasePen.__init__(self, glyphSet)
self._context = quartzContext
def _moveTo(self, (x, y)):
self._context.CGContextMoveToPoint(x, y)
def _lineTo(self, (x, y)):
self._context.CGContextAddLineToPoint(x, y)
def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
self._context.CGContextAddCurveToPoint(x1, y1, x2, y2, x3, y3)
def _closePath(self):
self._context.closePath()

View file

@ -0,0 +1,125 @@
"""PointPen for reversing the winding direction of contours."""
__all__ = ["ReverseContourPointPen"]
from robofab.pens.pointPen import AbstractPointPen
class ReverseContourPointPen(AbstractPointPen):
"""This is a PointPen that passes outline data to another PointPen, but
reversing the winding direction of all contours. Components are simply
passed through unchanged.
Closed contours are reversed in such a way that the first point remains
the first point.
"""
def __init__(self, outputPointPen):
self.pen = outputPointPen
self.currentContour = None # a place to store the points for the current sub path
def _flushContour(self):
pen = self.pen
contour = self.currentContour
if not contour:
pen.beginPath()
pen.endPath()
return
closed = contour[0][1] != "move"
if not closed:
lastSegmentType = "move"
else:
# Remove the first point and insert it at the end. When
# the list of points gets reversed, this point will then
# again be at the start. In other words, the following
# will hold:
# for N in range(len(originalContour)):
# originalContour[N] == reversedContour[-N]
contour.append(contour.pop(0))
# Find the first on-curve point.
firstOnCurve = None
for i in range(len(contour)):
if contour[i][1] is not None:
firstOnCurve = i
break
if firstOnCurve is None:
# There are no on-curve points, be basically have to
# do nothing but contour.reverse().
lastSegmentType = None
else:
lastSegmentType = contour[firstOnCurve][1]
contour.reverse()
if not closed:
# Open paths must start with a move, so we simply dump
# all off-curve points leading up to the first on-curve.
while contour[0][1] is None:
contour.pop(0)
pen.beginPath()
for pt, nextSegmentType, smooth, name in contour:
if nextSegmentType is not None:
segmentType = lastSegmentType
lastSegmentType = nextSegmentType
else:
segmentType = None
pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name)
pen.endPath()
def beginPath(self):
assert self.currentContour is None
self.currentContour = []
self.onCurve = []
def endPath(self):
assert self.currentContour is not None
self._flushContour()
self.currentContour = None
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
self.currentContour.append((pt, segmentType, smooth, name))
def addComponent(self, glyphName, transform):
assert self.currentContour is None
self.pen.addComponent(glyphName, transform)
if __name__ == "__main__":
from robofab.pens.pointPen import PrintingPointPen
pP = PrintingPointPen()
rP = ReverseContourPointPen(pP)
rP.beginPath()
rP.addPoint((339, -8), "curve")
rP.addPoint((502, -8))
rP.addPoint((635, 65))
rP.addPoint((635, 305), "curve")
rP.addPoint((635, 545))
rP.addPoint((504, 623))
rP.addPoint((340, 623), "curve")
rP.addPoint((177, 623))
rP.addPoint((43, 545))
rP.addPoint((43, 305), "curve")
rP.addPoint((43, 65))
rP.addPoint((176, -8))
rP.endPath()
rP.beginPath()
rP.addPoint((100, 100), "move", smooth=False, name='a')
rP.addPoint((150, 150))
rP.addPoint((200, 200))
rP.addPoint((250, 250), "curve", smooth=True, name='b')
rP.addPoint((300, 300), "line", smooth=False, name='c')
rP.addPoint((350, 350))
rP.addPoint((400, 400))
rP.addPoint((450, 450))
rP.addPoint((500, 500), "curve", smooth=True, name='d')
rP.addPoint((550, 550))
rP.addPoint((600, 600))
rP.addPoint((650, 650))
rP.addPoint((700, 700))
rP.addPoint((750, 750), "qcurve", smooth=False, name='e')
rP.endPath()

View file

@ -0,0 +1,103 @@
"""Pens for creating UFO glyphs."""
from robofab.objects.objectsBase import MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE
from robofab.objects.objectsRF import RContour, RSegment, RPoint
from robofab.pens.pointPen import BasePointToSegmentPen
from robofab.pens.adapterPens import SegmentToPointPen
class RFUFOPen(SegmentToPointPen):
def __init__(self, glyph):
SegmentToPointPen.__init__(self, RFUFOPointPen(glyph))
class RFUFOPointPen(BasePointToSegmentPen):
"""Point pen for building objectsRF glyphs"""
def __init__(self, glyph):
BasePointToSegmentPen.__init__(self)
self.glyph = glyph
def _flushContour(self, segments):
#
# adapted from robofab.pens.adapterPens.PointToSegmentPen
#
assert len(segments) >= 1
# if we only have one point and it has a name, we must have an anchor
first = segments[0]
segmentType, points = first
pt, smooth, name, kwargs = points[0]
if len(segments) == 1 and name != None:
self.glyph.appendAnchor(name, pt)
return
# we must have a contour
contour = RContour()
contour.setParent(self.glyph)
if segments[0][0] == "move":
# It's an open path.
closed = False
points = segments[0][1]
assert len(points) == 1
movePt, smooth, name, kwargs = points[0]
del segments[0]
else:
# It's a closed path, do a moveTo to the last
# point of the last segment. only if it isn't a qcurve
closed = True
segmentType, points = segments[-1]
movePt, smooth, name, kwargs = points[-1]
## THIS IS STILL UNDECIDED!!!
# since objectsRF currently follows the FL model of not
# allowing open contours, remove the last segment
# since it is being replaced by a move
if segmentType == 'line':
del segments[-1]
# construct a move segment and apply it to the contour if we aren't dealing with a qcurve
segment = RSegment()
segment.setParent(contour)
segment.smooth = smooth
rPoint = RPoint(x=movePt[0], y=movePt[1], pointType=MOVE, name=name)
rPoint.setParent(segment)
segment.points = [rPoint]
contour.segments.append(segment)
# do the rest of the segments
for segmentType, points in segments:
points = [(pt, name) for pt, smooth, name, kwargs in points]
if segmentType == "line":
assert len(points) == 1
sType = LINE
elif segmentType == "curve":
sType = CURVE
elif segmentType == "qcurve":
sType = QCURVE
else:
assert 0, "illegal segmentType: %s" % segmentType
segment = RSegment()
segment.setParent(contour)
segment.smooth = smooth
rPoints = []
# handle the offCurves
for point in points[:-1]:
point, name = point
rPoint = RPoint(x=point[0], y=point[1], pointType=OFFCURVE, name=name)
rPoint.setParent(segment)
rPoints.append(rPoint)
# now the onCurve
point, name = points[-1]
rPoint = RPoint(x=point[0], y=point[1], pointType=sType, name=name)
rPoint.setParent(segment)
rPoints.append(rPoint)
# apply them to the segment
segment.points = rPoints
contour.segments.append(segment)
if contour.segments[-1].type == "curve":
contour.segments[-1].points[-1].name = None
self.glyph.contours.append(contour)
def addComponent(self, glyphName, transform):
xx, xy, yx, yy, dx, dy = transform
self.glyph.appendComponent(baseGlyph=glyphName, offset=(dx, dy), scale=(xx, yy))

View file

@ -0,0 +1,43 @@
"""Small helper module to parse Plist-formatted data from trees as created
by xmlTreeBuilder.
"""
__all__ = "readPlistFromTree"
from plistlib import PlistParser
def readPlistFromTree(tree):
"""Given a (sub)tree created by xmlTreeBuilder, interpret it
as Plist-formatted data, and return the root object.
"""
parser = PlistTreeParser()
return parser.parseTree(tree)
class PlistTreeParser(PlistParser):
def parseTree(self, tree):
element, attributes, children = tree
self.parseElement(element, attributes, children)
return self.root
def parseElement(self, element, attributes, children):
self.handleBeginElement(element, attributes)
for child in children:
if isinstance(child, tuple):
self.parseElement(child[0], child[1], child[2])
else:
if not isinstance(child, unicode):
# ugh, xmlTreeBuilder returns utf-8 :-(
child = unicode(child, "utf-8")
self.handleData(child)
self.handleEndElement(element)
if __name__ == "__main__":
from xmlTreeBuilder import buildTree
tree = buildTree("xxx.plist", stripData=0)
print readPlistFromTree(tree)

495
misc/pylib/robofab/plistlib.py Executable file
View file

@ -0,0 +1,495 @@
"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
The PropertList (.plist) file format is a simple XML pickle supporting
basic object types, like dictionaries, lists, numbers and strings.
Usually the top level object is a dictionary.
To write out a plist file, use the writePlist(rootObject, pathOrFile)
function. 'rootObject' is the top level object, 'pathOrFile' is a
filename or a (writable) file object.
To parse a plist from a file, use the readPlist(pathOrFile) function,
with a file name or a (readable) file object as the only argument. It
returns the top level object (again, usually a dictionary).
To work with plist data in strings, you can use readPlistFromString()
and writePlistToString().
Values can be strings, integers, floats, booleans, tuples, lists,
dictionaries, Data or datetime.datetime objects. String values (including
dictionary keys) may be unicode strings -- they will be written out as
UTF-8.
The <data> plist type is supported through the Data class. This is a
thin wrapper around a Python string.
Generate Plist example:
pl = dict(
aString="Doodah",
aList=["A", "B", 12, 32.1, [1, 2, 3]],
aFloat = 0.1,
anInt = 728,
aDict=dict(
anotherString="<hello & hi there!>",
aUnicodeValue=u'M\xe4ssig, Ma\xdf',
aTrueValue=True,
aFalseValue=False,
),
someData = Data("<binary gunk>"),
someMoreData = Data("<lots of binary gunk>" * 10),
aDate = datetime.fromtimestamp(time.mktime(time.gmtime())),
)
# unicode keys are possible, but a little awkward to use:
pl[u'\xc5benraa'] = "That was a unicode key."
writePlist(pl, fileName)
Parse Plist example:
pl = readPlist(pathOrFile)
print pl["aKey"]
"""
__all__ = [
"readPlist", "writePlist", "readPlistFromString", "writePlistToString",
"readPlistFromResource", "writePlistToResource",
"Plist", "Data", "Dict"
]
# Note: the Plist and Dict classes have been deprecated.
import binascii
from cStringIO import StringIO
import re
try:
from datetime import datetime
except ImportError:
# We're running on Python < 2.3, we don't support dates here,
# yet we provide a stub class so type dispatching works.
class datetime(object):
def __init__(self, *args, **kwargs):
raise ValueError("datetime is not supported")
def readPlist(pathOrFile):
"""Read a .plist file. 'pathOrFile' may either be a file name or a
(readable) file object. Return the unpacked root object (which
usually is a dictionary).
"""
didOpen = 0
if isinstance(pathOrFile, (str, unicode)):
pathOrFile = open(pathOrFile)
didOpen = 1
p = PlistParser()
rootObject = p.parse(pathOrFile)
if didOpen:
pathOrFile.close()
return rootObject
def writePlist(rootObject, pathOrFile):
"""Write 'rootObject' to a .plist file. 'pathOrFile' may either be a
file name or a (writable) file object.
"""
didOpen = 0
if isinstance(pathOrFile, (str, unicode)):
pathOrFile = open(pathOrFile, "w")
didOpen = 1
writer = PlistWriter(pathOrFile)
writer.writeln("<plist version=\"1.0\">")
writer.writeValue(rootObject)
writer.writeln("</plist>")
if didOpen:
pathOrFile.close()
def readPlistFromString(data):
"""Read a plist data from a string. Return the root object.
"""
return readPlist(StringIO(data))
def writePlistToString(rootObject):
"""Return 'rootObject' as a plist-formatted string.
"""
f = StringIO()
writePlist(rootObject, f)
return f.getvalue()
def readPlistFromResource(path, restype='plst', resid=0):
"""Read plst resource from the resource fork of path.
"""
from Carbon.File import FSRef, FSGetResourceForkName
from Carbon.Files import fsRdPerm
from Carbon import Res
fsRef = FSRef(path)
resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdPerm)
Res.UseResFile(resNum)
plistData = Res.Get1Resource(restype, resid).data
Res.CloseResFile(resNum)
return readPlistFromString(plistData)
def writePlistToResource(rootObject, path, restype='plst', resid=0):
"""Write 'rootObject' as a plst resource to the resource fork of path.
"""
from Carbon.File import FSRef, FSGetResourceForkName
from Carbon.Files import fsRdWrPerm
from Carbon import Res
plistData = writePlistToString(rootObject)
fsRef = FSRef(path)
resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdWrPerm)
Res.UseResFile(resNum)
try:
Res.Get1Resource(restype, resid).RemoveResource()
except Res.Error:
pass
res = Res.Resource(plistData)
res.AddResource(restype, resid, '')
res.WriteResource()
Res.CloseResFile(resNum)
class DumbXMLWriter:
def __init__(self, file, indentLevel=0, indent="\t"):
self.file = file
self.stack = []
self.indentLevel = indentLevel
self.indent = indent
def beginElement(self, element):
self.stack.append(element)
self.writeln("<%s>" % element)
self.indentLevel += 1
def endElement(self, element):
assert self.indentLevel > 0
assert self.stack.pop() == element
self.indentLevel -= 1
self.writeln("</%s>" % element)
def simpleElement(self, element, value=None):
if value is not None:
value = _escapeAndEncode(value)
self.writeln("<%s>%s</%s>" % (element, value, element))
else:
self.writeln("<%s/>" % element)
def writeln(self, line):
if line:
self.file.write(self.indentLevel * self.indent + line + "\n")
else:
self.file.write("\n")
# Contents should conform to a subset of ISO 8601
# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with
# a loss of precision)
_dateParser = re.compile(r"(?P<year>\d\d\d\d)(?:-(?P<month>\d\d)(?:-(?P<day>\d\d)(?:T(?P<hour>\d\d)(?::(?P<minute>\d\d)(?::(?P<second>\d\d))?)?)?)?)?Z")
def _dateFromString(s):
order = ('year', 'month', 'day', 'hour', 'minute', 'second')
gd = _dateParser.match(s).groupdict()
lst = []
for key in order:
val = gd[key]
if val is None:
break
lst.append(int(val))
return datetime(*lst)
def _dateToString(d):
return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
d.year, d.month, d.day,
d.hour, d.minute, d.second
)
# Regex to find any control chars, except for \t \n and \r
_controlCharPat = re.compile(
r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
def _escapeAndEncode(text):
m = _controlCharPat.search(text)
if m is not None:
raise ValueError("strings can't contains control characters; "
"use plistlib.Data instead")
text = text.replace("\r\n", "\n") # convert DOS line endings
text = text.replace("\r", "\n") # convert Mac line endings
text = text.replace("&", "&amp;") # escape '&'
text = text.replace("<", "&lt;") # escape '<'
text = text.replace(">", "&gt;") # escape '>'
return text.encode("utf-8") # encode as UTF-8
PLISTHEADER = """\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
"""
class PlistWriter(DumbXMLWriter):
def __init__(self, file, indentLevel=0, indent="\t", writeHeader=1):
if writeHeader:
file.write(PLISTHEADER)
DumbXMLWriter.__init__(self, file, indentLevel, indent)
def writeValue(self, value):
if isinstance(value, (str, unicode)):
self.simpleElement("string", value)
elif isinstance(value, bool):
# must switch for bool before int, as bool is a
# subclass of int...
if value:
self.simpleElement("true")
else:
self.simpleElement("false")
elif isinstance(value, (int, long)):
self.simpleElement("integer", "%d" % value)
elif isinstance(value, float):
self.simpleElement("real", repr(value))
elif isinstance(value, dict):
self.writeDict(value)
elif isinstance(value, Data):
self.writeData(value)
elif isinstance(value, datetime):
self.simpleElement("date", _dateToString(value))
elif isinstance(value, (tuple, list)):
self.writeArray(value)
else:
raise TypeError("unsuported type: %s" % type(value))
def writeData(self, data):
self.beginElement("data")
self.indentLevel -= 1
maxlinelength = 76 - len(self.indent.replace("\t", " " * 8) *
self.indentLevel)
for line in data.asBase64(maxlinelength).split("\n"):
if line:
self.writeln(line)
self.indentLevel += 1
self.endElement("data")
def writeDict(self, d):
self.beginElement("dict")
items = d.items()
items.sort()
for key, value in items:
if not isinstance(key, (str, unicode)):
raise TypeError("keys must be strings")
self.simpleElement("key", key)
self.writeValue(value)
self.endElement("dict")
def writeArray(self, array):
self.beginElement("array")
for value in array:
self.writeValue(value)
self.endElement("array")
class _InternalDict(dict):
# This class is needed while Dict is scheduled for deprecation:
# we only need to warn when a *user* instantiates Dict or when
# the "attribute notation for dict keys" is used.
def __getattr__(self, attr):
try:
value = self[attr]
except KeyError:
raise AttributeError, attr
from warnings import warn
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", PendingDeprecationWarning)
return value
def __setattr__(self, attr, value):
from warnings import warn
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", PendingDeprecationWarning)
self[attr] = value
def __delattr__(self, attr):
try:
del self[attr]
except KeyError:
raise AttributeError, attr
from warnings import warn
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", PendingDeprecationWarning)
class Dict(_InternalDict):
def __init__(self, **kwargs):
from warnings import warn
warn("The plistlib.Dict class is deprecated, use builtin dict instead",
PendingDeprecationWarning)
super(Dict, self).__init__(**kwargs)
class Plist(_InternalDict):
"""This class has been deprecated. Use readPlist() and writePlist()
functions instead, together with regular dict objects.
"""
def __init__(self, **kwargs):
from warnings import warn
warn("The Plist class is deprecated, use the readPlist() and "
"writePlist() functions instead", PendingDeprecationWarning)
super(Plist, self).__init__(**kwargs)
def fromFile(cls, pathOrFile):
"""Deprecated. Use the readPlist() function instead."""
rootObject = readPlist(pathOrFile)
plist = cls()
plist.update(rootObject)
return plist
fromFile = classmethod(fromFile)
def write(self, pathOrFile):
"""Deprecated. Use the writePlist() function instead."""
writePlist(self, pathOrFile)
def _encodeBase64(s, maxlinelength=76):
# copied from base64.encodestring(), with added maxlinelength argument
maxbinsize = (maxlinelength//4)*3
pieces = []
for i in range(0, len(s), maxbinsize):
chunk = s[i : i + maxbinsize]
pieces.append(binascii.b2a_base64(chunk))
return "".join(pieces)
class Data:
"""Wrapper for binary data."""
def __init__(self, data):
self.data = data
def fromBase64(cls, data):
# base64.decodestring just calls binascii.a2b_base64;
# it seems overkill to use both base64 and binascii.
return cls(binascii.a2b_base64(data))
fromBase64 = classmethod(fromBase64)
def asBase64(self, maxlinelength=76):
return _encodeBase64(self.data, maxlinelength)
def __cmp__(self, other):
if isinstance(other, self.__class__):
return cmp(self.data, other.data)
elif isinstance(other, str):
return cmp(self.data, other)
else:
return cmp(id(self), id(other))
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
class PlistParser:
def __init__(self):
self.stack = []
self.currentKey = None
self.root = None
def parse(self, fileobj):
from xml.parsers.expat import ParserCreate
parser = ParserCreate()
parser.StartElementHandler = self.handleBeginElement
parser.EndElementHandler = self.handleEndElement
parser.CharacterDataHandler = self.handleData
parser.ParseFile(fileobj)
return self.root
def handleBeginElement(self, element, attrs):
self.data = []
handler = getattr(self, "begin_" + element, None)
if handler is not None:
handler(attrs)
def handleEndElement(self, element):
handler = getattr(self, "end_" + element, None)
if handler is not None:
handler()
def handleData(self, data):
self.data.append(data)
def addObject(self, value):
if self.currentKey is not None:
self.stack[-1][self.currentKey] = value
self.currentKey = None
elif not self.stack:
# this is the root object
self.root = value
else:
self.stack[-1].append(value)
def getData(self):
data = "".join(self.data)
try:
data = data.encode("ascii")
except UnicodeError:
pass
self.data = []
return data
# element handlers
def begin_dict(self, attrs):
d = _InternalDict()
self.addObject(d)
self.stack.append(d)
def end_dict(self):
self.stack.pop()
def end_key(self):
self.currentKey = self.getData()
def begin_array(self, attrs):
a = []
self.addObject(a)
self.stack.append(a)
def end_array(self):
self.stack.pop()
def end_true(self):
self.addObject(True)
def end_false(self):
self.addObject(False)
def end_integer(self):
self.addObject(int(self.getData()))
def end_real(self):
self.addObject(float(self.getData()))
def end_string(self):
self.addObject(self.getData())
def end_data(self):
self.addObject(Data.fromBase64(self.getData()))
def end_date(self):
self.addObject(_dateFromString(self.getData()))
# cruft to support booleans in Python <= 2.3
import sys
if sys.version_info[:2] < (2, 3):
# Python 2.2 and earlier: no booleans
# Python 2.2.x: booleans are ints
class bool(int):
"""Imitation of the Python 2.3 bool object."""
def __new__(cls, value):
return int.__new__(cls, not not value)
def __repr__(self):
if self:
return "True"
else:
return "False"
True = bool(1)
False = bool(0)

19
misc/pylib/robofab/setup.py Executable file
View file

@ -0,0 +1,19 @@
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [
Extension("objects.objectsBase", ["objects/objectsBase.pyx"]),
Extension("objects.objectsRF", ["objects/objectsRF.pyx"]),
Extension("pens.rfUFOPen", ["pens/rfUFOPen.pyx"]),
Extension("pens.boundsPen", ["pens/boundsPen.pyx"]),
Extension("xmlTreeBuilder", ["xmlTreeBuilder.pyx"]),
Extension("misc.arrayTools", ["misc/arrayTools.pyx"]),
Extension("glifLib", ["glifLib.pyx"]),
]
setup(
name = 'robofab',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)

View file

@ -0,0 +1,8 @@
"""Directory for unit tests.
Modules here are typically named text_<something>.py, where <something> is
usually a module name, for example "test_flPen.py", but it can also be the name
of an area or concept to be tested, for example "test_drawing.py".
Testmodules should use the unittest framework.
"""

View file

@ -0,0 +1,27 @@
import os
import glob
import unittest
import robofab.test
if __name__ == "__main__":
testDir = os.path.dirname(robofab.test.__file__)
testFiles = glob.glob1(testDir, "test_*.py")
loader = unittest.TestLoader()
suites = []
for fileName in testFiles:
modName = "robofab.test." + fileName[:-3]
print "importing", fileName
try:
mod = __import__(modName, {}, {}, ["*"])
except ImportError:
print "*** skipped", fileName
continue
suites.append(loader.loadTestsFromModule(mod))
print "running tests..."
testRunner = unittest.TextTestRunner(verbosity=0)
testSuite = unittest.TestSuite(suites)
testRunner.run(testSuite)

View file

@ -0,0 +1,278 @@
"""Miscellaneous helpers for our test suite."""
import sys
import os
import types
import unittest
def getDemoFontPath():
"""Return the path to Data/DemoFont.ufo/."""
import robofab
root = os.path.dirname(os.path.dirname(os.path.dirname(robofab.__file__)))
return os.path.join(root, "Data", "DemoFont.ufo")
def getDemoFontGlyphSetPath():
"""Return the path to Data/DemoFont.ufo/glyphs/."""
return os.path.join(getDemoFontPath(), "glyphs")
def _gatherTestCasesFromCallerByMagic():
# UGLY magic: fetch TestClass subclasses from the globals of our
# caller's caller.
frame = sys._getframe(2)
return _gatherTestCasesFromDict(frame.f_globals)
def _gatherTestCasesFromDict(d):
testCases = []
for ob in d.values():
if isinstance(ob, type) and issubclass(ob, unittest.TestCase):
testCases.append(ob)
return testCases
def runTests(testCases=None, verbosity=1):
"""Run a series of tests."""
if testCases is None:
testCases = _gatherTestCasesFromCallerByMagic()
loader = unittest.TestLoader()
suites = []
for testCase in testCases:
suites.append(loader.loadTestsFromTestCase(testCase))
testRunner = unittest.TextTestRunner(verbosity=verbosity)
testSuite = unittest.TestSuite(suites)
testRunner.run(testSuite)
# font info values used by several tests
fontInfoVersion1 = {
"familyName" : "Some Font (Family Name)",
"styleName" : "Regular (Style Name)",
"fullName" : "Some Font-Regular (Postscript Full Name)",
"fontName" : "SomeFont-Regular (Postscript Font Name)",
"menuName" : "Some Font Regular (Style Map Family Name)",
"fontStyle" : 64,
"note" : "A note.",
"versionMajor" : 1,
"versionMinor" : 0,
"year" : 2008,
"copyright" : "Copyright Some Foundry.",
"notice" : "Some Font by Some Designer for Some Foundry.",
"trademark" : "Trademark Some Foundry",
"license" : "License info for Some Foundry.",
"licenseURL" : "http://somefoundry.com/license",
"createdBy" : "Some Foundry",
"designer" : "Some Designer",
"designerURL" : "http://somedesigner.com",
"vendorURL" : "http://somefoundry.com",
"unitsPerEm" : 1000,
"ascender" : 750,
"descender" : -250,
"capHeight" : 750,
"xHeight" : 500,
"defaultWidth" : 400,
"slantAngle" : -12.5,
"italicAngle" : -12.5,
"widthName" : "Medium (normal)",
"weightName" : "Medium",
"weightValue" : 500,
"fondName" : "SomeFont Regular (FOND Name)",
"otFamilyName" : "Some Font (Preferred Family Name)",
"otStyleName" : "Regular (Preferred Subfamily Name)",
"otMacName" : "Some Font Regular (Compatible Full Name)",
"msCharSet" : 0,
"fondID" : 15000,
"uniqueID" : 4000000,
"ttVendor" : "SOME",
"ttUniqueID" : "OpenType name Table Unique ID",
"ttVersion" : "OpenType name Table Version",
}
fontInfoVersion2 = {
"familyName" : "Some Font (Family Name)",
"styleName" : "Regular (Style Name)",
"styleMapFamilyName" : "Some Font Regular (Style Map Family Name)",
"styleMapStyleName" : "regular",
"versionMajor" : 1,
"versionMinor" : 0,
"year" : 2008,
"copyright" : "Copyright Some Foundry.",
"trademark" : "Trademark Some Foundry",
"unitsPerEm" : 1000,
"descender" : -250,
"xHeight" : 500,
"capHeight" : 750,
"ascender" : 750,
"italicAngle" : -12.5,
"note" : "A note.",
"openTypeHeadCreated" : "2000/01/01 00:00:00",
"openTypeHeadLowestRecPPEM" : 10,
"openTypeHeadFlags" : [0, 1],
"openTypeHheaAscender" : 750,
"openTypeHheaDescender" : -250,
"openTypeHheaLineGap" : 200,
"openTypeHheaCaretSlopeRise" : 1,
"openTypeHheaCaretSlopeRun" : 0,
"openTypeHheaCaretOffset" : 0,
"openTypeNameDesigner" : "Some Designer",
"openTypeNameDesignerURL" : "http://somedesigner.com",
"openTypeNameManufacturer" : "Some Foundry",
"openTypeNameManufacturerURL" : "http://somefoundry.com",
"openTypeNameLicense" : "License info for Some Foundry.",
"openTypeNameLicenseURL" : "http://somefoundry.com/license",
"openTypeNameVersion" : "OpenType name Table Version",
"openTypeNameUniqueID" : "OpenType name Table Unique ID",
"openTypeNameDescription" : "Some Font by Some Designer for Some Foundry.",
"openTypeNamePreferredFamilyName" : "Some Font (Preferred Family Name)",
"openTypeNamePreferredSubfamilyName" : "Regular (Preferred Subfamily Name)",
"openTypeNameCompatibleFullName" : "Some Font Regular (Compatible Full Name)",
"openTypeNameSampleText" : "Sample Text for Some Font.",
"openTypeNameWWSFamilyName" : "Some Font (WWS Family Name)",
"openTypeNameWWSSubfamilyName" : "Regular (WWS Subfamily Name)",
"openTypeOS2WidthClass" : 5,
"openTypeOS2WeightClass" : 500,
"openTypeOS2Selection" : [3],
"openTypeOS2VendorID" : "SOME",
"openTypeOS2Panose" : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"openTypeOS2FamilyClass" : [1, 1],
"openTypeOS2UnicodeRanges" : [0, 1],
"openTypeOS2CodePageRanges" : [0, 1],
"openTypeOS2TypoAscender" : 750,
"openTypeOS2TypoDescender" : -250,
"openTypeOS2TypoLineGap" : 200,
"openTypeOS2WinAscent" : 750,
"openTypeOS2WinDescent" : -250,
"openTypeOS2Type" : [],
"openTypeOS2SubscriptXSize" : 200,
"openTypeOS2SubscriptYSize" : 400,
"openTypeOS2SubscriptXOffset" : 0,
"openTypeOS2SubscriptYOffset" : -100,
"openTypeOS2SuperscriptXSize" : 200,
"openTypeOS2SuperscriptYSize" : 400,
"openTypeOS2SuperscriptXOffset" : 0,
"openTypeOS2SuperscriptYOffset" : 200,
"openTypeOS2StrikeoutSize" : 20,
"openTypeOS2StrikeoutPosition" : 300,
"openTypeVheaVertTypoAscender" : 750,
"openTypeVheaVertTypoDescender" : -250,
"openTypeVheaVertTypoLineGap" : 200,
"openTypeVheaCaretSlopeRise" : 0,
"openTypeVheaCaretSlopeRun" : 1,
"openTypeVheaCaretOffset" : 0,
"postscriptFontName" : "SomeFont-Regular (Postscript Font Name)",
"postscriptFullName" : "Some Font-Regular (Postscript Full Name)",
"postscriptSlantAngle" : -12.5,
"postscriptUniqueID" : 4000000,
"postscriptUnderlineThickness" : 20,
"postscriptUnderlinePosition" : -200,
"postscriptIsFixedPitch" : False,
"postscriptBlueValues" : [500, 510],
"postscriptOtherBlues" : [-250, -260],
"postscriptFamilyBlues" : [500, 510],
"postscriptFamilyOtherBlues" : [-250, -260],
"postscriptStemSnapH" : [100, 120],
"postscriptStemSnapV" : [80, 90],
"postscriptBlueFuzz" : 1,
"postscriptBlueShift" : 7,
"postscriptBlueScale" : 0.039625,
"postscriptForceBold" : True,
"postscriptDefaultWidthX" : 400,
"postscriptNominalWidthX" : 400,
"postscriptWeightName" : "Medium",
"postscriptDefaultCharacter" : ".notdef",
"postscriptWindowsCharacterSet" : 1,
"macintoshFONDFamilyID" : 15000,
"macintoshFONDName" : "SomeFont Regular (FOND Name)",
}
expectedFontInfo1To2Conversion = {
"familyName" : "Some Font (Family Name)",
"styleMapFamilyName" : "Some Font Regular (Style Map Family Name)",
"styleMapStyleName" : "regular",
"styleName" : "Regular (Style Name)",
"unitsPerEm" : 1000,
"ascender" : 750,
"capHeight" : 750,
"xHeight" : 500,
"descender" : -250,
"italicAngle" : -12.5,
"versionMajor" : 1,
"versionMinor" : 0,
"year" : 2008,
"copyright" : "Copyright Some Foundry.",
"trademark" : "Trademark Some Foundry",
"note" : "A note.",
"macintoshFONDFamilyID" : 15000,
"macintoshFONDName" : "SomeFont Regular (FOND Name)",
"openTypeNameCompatibleFullName" : "Some Font Regular (Compatible Full Name)",
"openTypeNameDescription" : "Some Font by Some Designer for Some Foundry.",
"openTypeNameDesigner" : "Some Designer",
"openTypeNameDesignerURL" : "http://somedesigner.com",
"openTypeNameLicense" : "License info for Some Foundry.",
"openTypeNameLicenseURL" : "http://somefoundry.com/license",
"openTypeNameManufacturer" : "Some Foundry",
"openTypeNameManufacturerURL" : "http://somefoundry.com",
"openTypeNamePreferredFamilyName" : "Some Font (Preferred Family Name)",
"openTypeNamePreferredSubfamilyName": "Regular (Preferred Subfamily Name)",
"openTypeNameCompatibleFullName" : "Some Font Regular (Compatible Full Name)",
"openTypeNameUniqueID" : "OpenType name Table Unique ID",
"openTypeNameVersion" : "OpenType name Table Version",
"openTypeOS2VendorID" : "SOME",
"openTypeOS2WeightClass" : 500,
"openTypeOS2WidthClass" : 5,
"postscriptDefaultWidthX" : 400,
"postscriptFontName" : "SomeFont-Regular (Postscript Font Name)",
"postscriptFullName" : "Some Font-Regular (Postscript Full Name)",
"postscriptSlantAngle" : -12.5,
"postscriptUniqueID" : 4000000,
"postscriptWeightName" : "Medium",
"postscriptWindowsCharacterSet" : 1
}
expectedFontInfo2To1Conversion = {
"familyName" : "Some Font (Family Name)",
"menuName" : "Some Font Regular (Style Map Family Name)",
"fontStyle" : 64,
"styleName" : "Regular (Style Name)",
"unitsPerEm" : 1000,
"ascender" : 750,
"capHeight" : 750,
"xHeight" : 500,
"descender" : -250,
"italicAngle" : -12.5,
"versionMajor" : 1,
"versionMinor" : 0,
"copyright" : "Copyright Some Foundry.",
"trademark" : "Trademark Some Foundry",
"note" : "A note.",
"fondID" : 15000,
"fondName" : "SomeFont Regular (FOND Name)",
"fullName" : "Some Font Regular (Compatible Full Name)",
"notice" : "Some Font by Some Designer for Some Foundry.",
"designer" : "Some Designer",
"designerURL" : "http://somedesigner.com",
"license" : "License info for Some Foundry.",
"licenseURL" : "http://somefoundry.com/license",
"createdBy" : "Some Foundry",
"vendorURL" : "http://somefoundry.com",
"otFamilyName" : "Some Font (Preferred Family Name)",
"otStyleName" : "Regular (Preferred Subfamily Name)",
"otMacName" : "Some Font Regular (Compatible Full Name)",
"ttUniqueID" : "OpenType name Table Unique ID",
"ttVersion" : "OpenType name Table Version",
"ttVendor" : "SOME",
"weightValue" : 500,
"widthName" : "Medium (normal)",
"defaultWidth" : 400,
"fontName" : "SomeFont-Regular (Postscript Font Name)",
"fullName" : "Some Font-Regular (Postscript Full Name)",
"slantAngle" : -12.5,
"uniqueID" : 4000000,
"weightName" : "Medium",
"msCharSet" : 0,
"year" : 2008
}

View file

@ -0,0 +1,111 @@
import unittest
from cStringIO import StringIO
import sys
from robofab import ufoLib
from robofab.objects.objectsFL import NewFont
from robofab.test.testSupport import fontInfoVersion1, fontInfoVersion2
class RInfoRFTestCase(unittest.TestCase):
def testRoundTripVersion2(self):
font = NewFont()
infoObject = font.info
for attr, value in fontInfoVersion2.items():
if attr in infoObject._ufoToFLAttrMapping and infoObject._ufoToFLAttrMapping[attr]["nakedAttribute"] is None:
continue
setattr(infoObject, attr, value)
newValue = getattr(infoObject, attr)
self.assertEqual((attr, newValue), (attr, value))
font.close()
def testVersion2UnsupportedSet(self):
saveStderr = sys.stderr
saveStdout = sys.stdout
tempStderr = StringIO()
sys.stderr = tempStderr
sys.stdout = tempStderr
font = NewFont()
infoObject = font.info
requiredWarnings = []
try:
for attr, value in fontInfoVersion2.items():
if attr in infoObject._ufoToFLAttrMapping and infoObject._ufoToFLAttrMapping[attr]["nakedAttribute"] is not None:
continue
setattr(infoObject, attr, value)
s = "The attribute %s is not supported by FontLab." % attr
requiredWarnings.append((attr, s))
finally:
sys.stderr = saveStderr
sys.stdout = saveStdout
tempStderr = tempStderr.getvalue()
for attr, line in requiredWarnings:
self.assertEquals((attr, line in tempStderr), (attr, True))
font.close()
def testVersion2UnsupportedGet(self):
saveStderr = sys.stderr
saveStdout = sys.stdout
tempStderr = StringIO()
sys.stderr = tempStderr
sys.stdout = tempStderr
font = NewFont()
infoObject = font.info
requiredWarnings = []
try:
for attr, value in fontInfoVersion2.items():
if attr in infoObject._ufoToFLAttrMapping and infoObject._ufoToFLAttrMapping[attr]["nakedAttribute"] is not None:
continue
getattr(infoObject, attr, value)
s = "The attribute %s is not supported by FontLab." % attr
requiredWarnings.append((attr, s))
finally:
sys.stderr = saveStderr
sys.stdout = saveStdout
tempStderr = tempStderr.getvalue()
for attr, line in requiredWarnings:
self.assertEquals((attr, line in tempStderr), (attr, True))
font.close()
def testRoundTripVersion1(self):
font = NewFont()
infoObject = font.info
for attr, value in fontInfoVersion1.items():
if attr not in ufoLib.deprecatedFontInfoAttributesVersion2:
setattr(infoObject, attr, value)
for attr, expectedValue in fontInfoVersion1.items():
if attr not in ufoLib.deprecatedFontInfoAttributesVersion2:
value = getattr(infoObject, attr)
self.assertEqual((attr, expectedValue), (attr, value))
font.close()
def testVersion1DeprecationRoundTrip(self):
saveStderr = sys.stderr
saveStdout = sys.stdout
tempStderr = StringIO()
sys.stderr = tempStderr
sys.stdout = tempStderr
font = NewFont()
infoObject = font.info
requiredWarnings = []
try:
for attr, value in fontInfoVersion1.items():
if attr in ufoLib.deprecatedFontInfoAttributesVersion2:
setattr(infoObject, attr, value)
v = getattr(infoObject, attr)
self.assertEquals((attr, value), (attr, v))
s = "DeprecationWarning: The %s attribute has been deprecated." % attr
requiredWarnings.append((attr, s))
finally:
sys.stderr = saveStderr
sys.stdout = saveStdout
tempStderr = tempStderr.getvalue()
for attr, line in requiredWarnings:
self.assertEquals((attr, line in tempStderr), (attr, True))
font.close()
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,56 @@
import unittest
from cStringIO import StringIO
import sys
from robofab import ufoLib
from robofab.objects.objectsRF import RInfo
from robofab.test.testSupport import fontInfoVersion1, fontInfoVersion2
class RInfoRFTestCase(unittest.TestCase):
def testRoundTripVersion2(self):
infoObject = RInfo()
for attr, value in fontInfoVersion2.items():
setattr(infoObject, attr, value)
newValue = getattr(infoObject, attr)
self.assertEqual((attr, newValue), (attr, value))
def testRoundTripVersion1(self):
infoObject = RInfo()
for attr, value in fontInfoVersion1.items():
if attr not in ufoLib.deprecatedFontInfoAttributesVersion2:
setattr(infoObject, attr, value)
for attr, expectedValue in fontInfoVersion1.items():
if attr not in ufoLib.deprecatedFontInfoAttributesVersion2:
value = getattr(infoObject, attr)
self.assertEqual((attr, expectedValue), (attr, value))
def testVersion1DeprecationRoundTrip(self):
"""
unittest doesn't catch warnings in self.assertRaises,
so some hackery is required to catch the warnings
that are raised when setting deprecated attributes.
"""
saveStderr = sys.stderr
tempStderr = StringIO()
sys.stderr = tempStderr
infoObject = RInfo()
requiredWarnings = []
try:
for attr, value in fontInfoVersion1.items():
if attr in ufoLib.deprecatedFontInfoAttributesVersion2:
setattr(infoObject, attr, value)
v = getattr(infoObject, attr)
self.assertEquals((attr, value), (attr, v))
s = "DeprecationWarning: The %s attribute has been deprecated." % attr
requiredWarnings.append((attr, s))
finally:
sys.stderr = saveStderr
tempStderr = tempStderr.getvalue()
for attr, line in requiredWarnings:
self.assertEquals((attr, line in tempStderr), (attr, True))
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,218 @@
import robofab.interface.all.dialogs
reload(robofab.interface.all.dialogs)
from robofab.interface.all.dialogs import *
import unittest
__all__ = [
"AskString", #x
"AskYesNoCancel", #x
"FindGlyph",
"GetFile", #x
"GetFolder", #x
"GetFileOrFolder", #x
"Message", #x
"OneList",
"PutFile", #x
"SearchList",
"SelectFont",
"SelectGlyph",
"TwoChecks",
"TwoFields",
"ProgressBar",
]
class DialogRunner(object):
def __init__(self):
prompt = "The prompt for %s."
message = "The message for %s."
title = "The title for %s."
informativeText = "The informative text for %s."
fileTypes = ['ufo']
fileName = "The_filename.txt"
self.fonts = fonts = [self.makeTestFont(n) for n in range(4)]
t = "AskString"
try:
print "About to try", t
print "\t>>>", AskString(
message=prompt%t,
value='',
title=title%t
)
except NotImplementedError:
print t, "is not implemented."
t = "AskYesNoCancel"
try:
print "About to try", t
print "\t>>>", AskYesNoCancel(
message=prompt%t+" default set to 0",
title=title%t,
default=0,
informativeText=informativeText%t
)
print "\t>>>", AskYesNoCancel(
message=prompt%t+" default set to 1",
title=title%t,
default=1,
informativeText=informativeText%t
)
except NotImplementedError:
print t, "is not implemented."
t = "GetFile"
try:
print "About to try", t
print "\t>>>", GetFile(
message=message%t+" Only fileTypes "+`fileTypes`,
title=title%t,
directory=None,
fileName=fileName,
allowsMultipleSelection=False,
fileTypes=fileTypes
)
print "\t>>>", GetFile(
message=message%t+" All filetypes, allow multiple selection.",
title=title%t,
directory=None,
fileName=fileName,
allowsMultipleSelection=True,
fileTypes=None
)
except NotImplementedError:
print t, "is not implemented."
t = "GetFolder"
try:
print "About to try", t
print "\t>>>", GetFolder(
message=message%t,
title=title%t,
directory=None,
allowsMultipleSelection=False
)
print "\t>>>", GetFolder(
message=message%t + " Allow multiple selection.",
title=title%t,
directory=None,
allowsMultipleSelection=True
)
except NotImplementedError:
print t, "is not implemented."
t = "GetFileOrFolder"
try:
print "About to try", t
print "\t>>>", GetFileOrFolder(
message=message%t+" Only fileTypes "+`fileTypes`,
title=title%t,
directory=None,
fileName=fileName,
allowsMultipleSelection=False,
fileTypes=fileTypes
)
print "\t>>>", GetFileOrFolder(
message=message%t + " Allow multiple selection.",
title=title%t,
directory=None,
fileName=fileName,
allowsMultipleSelection=True,
fileTypes=None
)
except NotImplementedError:
print t, "is not implemented."
t = "Message"
try:
print "About to try", t
print "\t>>>", Message(
message=message%t,
title=title%t,
informativeText=informativeText%t
)
except NotImplementedError:
print t, "is not implemented."
t = "PutFile"
try:
print "About to try", t
print "\t>>>", PutFile(
message=message%t,
fileName=fileName,
)
except NotImplementedError:
print t, "is not implemented."
# t = "SelectFont"
# try:
#print "About to try", t
# print "\t>>>", SelectFont(
# message=message%t,
# title=title%t,
# allFonts=fonts,
# )
# except NotImplementedError:
# print t, "is not implemented."
# t = 'SelectGlyph'
# try:
#print "About to try", t
# print "\t>>>", SelectGlyph(
# font=fonts[0],
# message=message%t,
# title=title%t,
# )
# except NotImplementedError:
# print t, "is not implemented."
print 'No more tests.'
def makeTestFont(self, number):
from robofab.objects.objectsRF import RFont as _RFont
f = _RFont()
f.info.familyName = "TestFamily"
f.info.styleName = "weight%d"%number
f.info.postscriptFullName = "%s %s"%(f.info.familyName, f.info.styleName)
# make some glyphs
for name in ['A', 'B', 'C']:
g = f.newGlyph(name)
pen = g.getPen()
pen.moveTo((0,0))
pen.lineTo((500, 0))
pen.lineTo((500, 800))
pen.lineTo((0, 800))
pen.closePath()
return f
class DialogTests(unittest.TestCase):
def setUp(self):
from robofab.interface.all.dialogs import test
test()
def tearDown(self):
pass
def testDialogs(self):
import robofab.interface.all.dialogs
dialogModuleName = robofab.interface.all.dialogs.platformApplicationModuleName
application = robofab.interface.all.dialogs.application
if application is None and dialogModuleName == "dialogs_mac_vanilla":
# in vanilla, but not in a host application, run with executeVanillaTest
print
print "I'm running these tests with executeVanillaTest"
from vanilla.test.testTools import executeVanillaTest
executeVanillaTest(DialogRunner)
else:
print
print "I'm running these tests natively in"
DialogRunner()
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,565 @@
import os
import shutil
import unittest
import tempfile
from robofab.plistlib import readPlist
import robofab
from robofab.ufoLib import UFOReader, UFOWriter
from robofab.test.testSupport import fontInfoVersion2, expectedFontInfo1To2Conversion
from robofab.objects.objectsFL import NewFont, OpenFont
vfbPath = os.path.dirname(robofab.__file__)
vfbPath = os.path.dirname(vfbPath)
vfbPath = os.path.dirname(vfbPath)
vfbPath = os.path.join(vfbPath, "TestData", "TestFont1.vfb")
ufoPath1 = os.path.dirname(robofab.__file__)
ufoPath1 = os.path.dirname(ufoPath1)
ufoPath1 = os.path.dirname(ufoPath1)
ufoPath1 = os.path.join(ufoPath1, "TestData", "TestFont1 (UFO1).ufo")
ufoPath2 = ufoPath1.replace("TestFont1 (UFO1).ufo", "TestFont1 (UFO2).ufo")
expectedFormatVersion1Features = """@myClass = [A B];
feature liga {
sub A A by b;
} liga;
"""
# robofab should remove these from the lib after a load.
removeFromFormatVersion1Lib = [
"org.robofab.opentype.classes",
"org.robofab.opentype.features",
"org.robofab.opentype.featureorder",
"org.robofab.postScriptHintData"
]
class ReadUFOFormatVersion1TestCase(unittest.TestCase):
def setUpFont(self, doInfo=False, doKerning=False, doGroups=False, doLib=False, doFeatures=False):
self.font = NewFont()
self.ufoPath = ufoPath1
self.font.readUFO(ufoPath1, doInfo=doInfo, doKerning=doKerning, doGroups=doGroups, doLib=doLib, doFeatures=doFeatures)
self.font.update()
def tearDownFont(self):
self.font.close()
self.font = None
def compareToUFO(self, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True):
reader = UFOReader(self.ufoPath)
results = {}
if doInfo:
infoMatches = True
info = self.font.info
for attr, expectedValue in expectedFontInfo1To2Conversion.items():
writtenValue = getattr(info, attr)
if expectedValue != writtenValue:
infoMatches = False
break
results["info"]= infoMatches
if doKerning:
kerning = self.font.kerning.asDict()
expectedKerning = reader.readKerning()
results["kerning"] = expectedKerning == kerning
if doGroups:
groups = dict(self.font.groups)
expectedGroups = reader.readGroups()
results["groups"] = expectedGroups == groups
if doFeatures:
features = self.font.features.text
expectedFeatures = expectedFormatVersion1Features
# FontLab likes to add lines to the features, so skip blank lines.
features = [line for line in features.splitlines() if line]
expectedFeatures = [line for line in expectedFeatures.splitlines() if line]
results["features"] = expectedFeatures == features
if doLib:
lib = dict(self.font.lib)
expectedLib = reader.readLib()
for key in removeFromFormatVersion1Lib:
if key in expectedLib:
del expectedLib[key]
results["lib"] = expectedLib == lib
return results
def testFull(self):
self.setUpFont(doInfo=True, doKerning=True, doGroups=True, doFeatures=True, doLib=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
def testInfo(self):
self.setUpFont(doInfo=True)
otherResults = self.compareToUFO(doInfo=False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
info = self.font.info
for attr, expectedValue in expectedFontInfo1To2Conversion.items():
writtenValue = getattr(info, attr)
self.assertEqual((attr, expectedValue), (attr, writtenValue))
self.tearDownFont()
def testFeatures(self):
self.setUpFont(doFeatures=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testKerning(self):
self.setUpFont(doKerning=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testGroups(self):
self.setUpFont(doGroups=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testLib(self):
self.setUpFont(doLib=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
class ReadUFOFormatVersion2TestCase(unittest.TestCase):
def setUpFont(self, doInfo=False, doKerning=False, doGroups=False, doLib=False, doFeatures=False):
self.font = NewFont()
self.ufoPath = ufoPath2
self.font.readUFO(ufoPath2, doInfo=doInfo, doKerning=doKerning, doGroups=doGroups, doLib=doLib, doFeatures=doFeatures)
self.font.update()
def tearDownFont(self):
self.font.close()
self.font = None
def compareToUFO(self, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True):
reader = UFOReader(self.ufoPath)
results = {}
if doInfo:
infoMatches = True
info = self.font.info
for attr, expectedValue in fontInfoVersion2.items():
# cheat by skipping attrs that aren't supported
if info._ufoToFLAttrMapping[attr]["nakedAttribute"] is None:
continue
writtenValue = getattr(info, attr)
if expectedValue != writtenValue:
infoMatches = False
break
results["info"]= infoMatches
if doKerning:
kerning = self.font.kerning.asDict()
expectedKerning = reader.readKerning()
results["kerning"] = expectedKerning == kerning
if doGroups:
groups = dict(self.font.groups)
expectedGroups = reader.readGroups()
results["groups"] = expectedGroups == groups
if doFeatures:
features = self.font.features.text
expectedFeatures = reader.readFeatures()
results["features"] = expectedFeatures == features
if doLib:
lib = dict(self.font.lib)
expectedLib = reader.readLib()
results["lib"] = expectedLib == lib
return results
def testFull(self):
self.setUpFont(doInfo=True, doKerning=True, doGroups=True, doFeatures=True, doLib=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
def testInfo(self):
self.setUpFont(doInfo=True)
otherResults = self.compareToUFO(doInfo=False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
info = self.font.info
for attr, expectedValue in fontInfoVersion2.items():
# cheat by skipping attrs that aren't supported
if info._ufoToFLAttrMapping[attr]["nakedAttribute"] is None:
continue
writtenValue = getattr(info, attr)
self.assertEqual((attr, expectedValue), (attr, writtenValue))
self.tearDownFont()
def testFeatures(self):
self.setUpFont(doFeatures=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testKerning(self):
self.setUpFont(doKerning=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testGroups(self):
self.setUpFont(doGroups=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testLib(self):
self.setUpFont(doLib=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
class WriteUFOFormatVersion1TestCase(unittest.TestCase):
def setUpFont(self, doInfo=False, doKerning=False, doGroups=False):
self.dstDir = tempfile.mktemp()
os.mkdir(self.dstDir)
self.font = OpenFont(vfbPath)
self.font.writeUFO(self.dstDir, doInfo=doInfo, doKerning=doKerning, doGroups=doGroups, formatVersion=1)
self.font.close()
def tearDownFont(self):
shutil.rmtree(self.dstDir)
def compareToUFO(self, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True):
readerExpected = UFOReader(ufoPath1)
readerWritten = UFOReader(self.dstDir)
results = {}
if doInfo:
matches = True
expectedPath = os.path.join(ufoPath1, "fontinfo.plist")
writtenPath = os.path.join(self.dstDir, "fontinfo.plist")
if not os.path.exists(writtenPath):
matches = False
else:
expected = readPlist(expectedPath)
written = readPlist(writtenPath)
for attr, expectedValue in expected.items():
if expectedValue != written[attr]:
matches = False
break
results["info"] = matches
if doKerning:
matches = True
expectedPath = os.path.join(ufoPath1, "kerning.plist")
writtenPath = os.path.join(self.dstDir, "kerning.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["kerning"] = matches
if doGroups:
matches = True
expectedPath = os.path.join(ufoPath1, "groups.plist")
writtenPath = os.path.join(self.dstDir, "groups.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["groups"] = matches
if doFeatures:
matches = True
featuresPath = os.path.join(self.dstDir, "features.fea")
libPath = os.path.join(self.dstDir, "lib.plist")
if os.path.exists(featuresPath):
matches = False
else:
fontLib = readPlist(libPath)
writtenText = [fontLib.get("org.robofab.opentype.classes", "")]
features = fontLib.get("org.robofab.opentype.features", {})
featureOrder= fontLib.get("org.robofab.opentype.featureorder", [])
for featureName in featureOrder:
writtenText.append(features.get(featureName, ""))
writtenText = "\n".join(writtenText)
# FontLab likes to add lines to the features, so skip blank lines.
expectedText = [line for line in expectedFormatVersion1Features.splitlines() if line]
writtenText = [line for line in writtenText.splitlines() if line]
matches = "\n".join(expectedText) == "\n".join(writtenText)
results["features"] = matches
if doLib:
matches = True
expectedPath = os.path.join(ufoPath1, "lib.plist")
writtenPath = os.path.join(self.dstDir, "lib.plist")
if not os.path.exists(writtenPath):
matches = False
else:
# the test file doesn't have the glyph order
# so purge it from the written
writtenLib = readPlist(writtenPath)
del writtenLib["org.robofab.glyphOrder"]
matches = readPlist(expectedPath) == writtenLib
results["lib"] = matches
return results
def testFull(self):
self.setUpFont(doInfo=True, doKerning=True, doGroups=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
def testInfo(self):
self.setUpFont(doInfo=True)
otherResults = self.compareToUFO(doInfo=False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
expectedPath = os.path.join(ufoPath1, "fontinfo.plist")
writtenPath = os.path.join(self.dstDir, "fontinfo.plist")
expected = readPlist(expectedPath)
written = readPlist(writtenPath)
for attr, expectedValue in expected.items():
self.assertEqual((attr, expectedValue), (attr, written[attr]))
self.tearDownFont()
def testFeatures(self):
self.setUpFont()
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], True)
self.tearDownFont()
def testKerning(self):
self.setUpFont(doKerning=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], False)
self.tearDownFont()
def testGroups(self):
self.setUpFont(doGroups=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], True)
self.tearDownFont()
def testLib(self):
self.setUpFont()
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
class WriteUFOFormatVersion2TestCase(unittest.TestCase):
def setUpFont(self, doInfo=False, doKerning=False, doGroups=False, doLib=False, doFeatures=False):
self.dstDir = tempfile.mktemp()
os.mkdir(self.dstDir)
self.font = OpenFont(vfbPath)
self.font.writeUFO(self.dstDir, doInfo=doInfo, doKerning=doKerning, doGroups=doGroups, doLib=doLib, doFeatures=doFeatures)
self.font.close()
def tearDownFont(self):
shutil.rmtree(self.dstDir)
def compareToUFO(self, doInfo=True, doKerning=True, doGroups=True, doLib=True, doFeatures=True):
readerExpected = UFOReader(ufoPath2)
readerWritten = UFOReader(self.dstDir)
results = {}
if doInfo:
matches = True
expectedPath = os.path.join(ufoPath2, "fontinfo.plist")
writtenPath = os.path.join(self.dstDir, "fontinfo.plist")
if not os.path.exists(writtenPath):
matches = False
else:
dummyFont = NewFont()
_ufoToFLAttrMapping = dict(dummyFont.info._ufoToFLAttrMapping)
dummyFont.close()
expected = readPlist(expectedPath)
written = readPlist(writtenPath)
for attr, expectedValue in expected.items():
# cheat by skipping attrs that aren't supported
if _ufoToFLAttrMapping[attr]["nakedAttribute"] is None:
continue
if expectedValue != written[attr]:
matches = False
break
results["info"] = matches
if doKerning:
matches = True
expectedPath = os.path.join(ufoPath2, "kerning.plist")
writtenPath = os.path.join(self.dstDir, "kerning.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["kerning"] = matches
if doGroups:
matches = True
expectedPath = os.path.join(ufoPath2, "groups.plist")
writtenPath = os.path.join(self.dstDir, "groups.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["groups"] = matches
if doFeatures:
matches = True
expectedPath = os.path.join(ufoPath2, "features.fea")
writtenPath = os.path.join(self.dstDir, "features.fea")
if not os.path.exists(writtenPath):
matches = False
else:
f = open(expectedPath, "r")
expectedText = f.read()
f.close()
f = open(writtenPath, "r")
writtenText = f.read()
f.close()
# FontLab likes to add lines to the features, so skip blank lines.
expectedText = [line for line in expectedText.splitlines() if line]
writtenText = [line for line in writtenText.splitlines() if line]
matches = "\n".join(expectedText) == "\n".join(writtenText)
results["features"] = matches
if doLib:
matches = True
expectedPath = os.path.join(ufoPath2, "lib.plist")
writtenPath = os.path.join(self.dstDir, "lib.plist")
if not os.path.exists(writtenPath):
matches = False
else:
# the test file doesn't have the glyph order
# so purge it from the written
writtenLib = readPlist(writtenPath)
del writtenLib["org.robofab.glyphOrder"]
matches = readPlist(expectedPath) == writtenLib
results["lib"] = matches
return results
def testFull(self):
self.setUpFont(doInfo=True, doKerning=True, doGroups=True, doFeatures=True, doLib=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
def testInfo(self):
self.setUpFont(doInfo=True)
otherResults = self.compareToUFO(doInfo=False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
expectedPath = os.path.join(ufoPath2, "fontinfo.plist")
writtenPath = os.path.join(self.dstDir, "fontinfo.plist")
expected = readPlist(expectedPath)
written = readPlist(writtenPath)
dummyFont = NewFont()
_ufoToFLAttrMapping = dict(dummyFont.info._ufoToFLAttrMapping)
dummyFont.close()
for attr, expectedValue in expected.items():
# cheat by skipping attrs that aren't supported
if _ufoToFLAttrMapping[attr]["nakedAttribute"] is None:
continue
self.assertEqual((attr, expectedValue), (attr, written[attr]))
self.tearDownFont()
def testFeatures(self):
self.setUpFont(doFeatures=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testKerning(self):
self.setUpFont(doKerning=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testGroups(self):
self.setUpFont(doGroups=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], False)
self.tearDownFont()
def testLib(self):
self.setUpFont(doLib=True)
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], False)
self.assertEqual(otherResults["kerning"], False)
self.assertEqual(otherResults["groups"], False)
self.assertEqual(otherResults["features"], False)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,150 @@
import os
import tempfile
import shutil
import unittest
from robofab.test.testSupport import getDemoFontGlyphSetPath
from robofab.glifLib import GlyphSet, glyphNameToFileName, READ_MODE
from robofab.tools.glyphNameSchemes import glyphNameToShortFileName
GLYPHSETDIR = getDemoFontGlyphSetPath()
class GlyphSetTests(unittest.TestCase):
def setUp(self):
self.dstDir = tempfile.mktemp()
os.mkdir(self.dstDir)
def tearDown(self):
shutil.rmtree(self.dstDir)
def testRoundTrip(self):
srcDir = GLYPHSETDIR
dstDir = self.dstDir
src = GlyphSet(srcDir)
dst = GlyphSet(dstDir)
for glyphName in src.keys():
g = src[glyphName]
g.drawPoints(None) # load attrs
dst.writeGlyph(glyphName, g, g.drawPoints)
# compare raw file data:
for glyphName in src.keys():
fileName = src.contents[glyphName]
org = file(os.path.join(srcDir, fileName), READ_MODE).read()
new = file(os.path.join(dstDir, fileName), READ_MODE).read()
self.assertEqual(org, new, "%r .glif file differs after round tripping" % glyphName)
def testRebuildContents(self):
gset = GlyphSet(GLYPHSETDIR)
contents = gset.contents
gset.rebuildContents()
self.assertEqual(contents, gset.contents)
def testReverseContents(self):
gset = GlyphSet(GLYPHSETDIR)
d = {}
for k, v in gset.getReverseContents().items():
d[v] = k
org = {}
for k, v in gset.contents.items():
org[k] = v.lower()
self.assertEqual(d, org)
def testReverseContents2(self):
src = GlyphSet(GLYPHSETDIR)
dst = GlyphSet(self.dstDir)
dstMap = dst.getReverseContents()
self.assertEqual(dstMap, {})
for glyphName in src.keys():
g = src[glyphName]
g.drawPoints(None) # load attrs
dst.writeGlyph(glyphName, g, g.drawPoints)
self.assertNotEqual(dstMap, {})
srcMap = dict(src.getReverseContents()) # copy
self.assertEqual(dstMap, srcMap)
del srcMap["a.glif"]
dst.deleteGlyph("a")
self.assertEqual(dstMap, srcMap)
def testCustomFileNamingScheme(self):
def myGlyphNameToFileName(glyphName, glyphSet):
return "prefix" + glyphNameToFileName(glyphName, glyphSet)
src = GlyphSet(GLYPHSETDIR)
dst = GlyphSet(self.dstDir, myGlyphNameToFileName)
for glyphName in src.keys():
g = src[glyphName]
g.drawPoints(None) # load attrs
dst.writeGlyph(glyphName, g, g.drawPoints)
d = {}
for k, v in src.contents.items():
print k, v
d[k] = "prefix" + v
self.assertEqual(d, dst.contents)
def testGetUnicodes(self):
src = GlyphSet(GLYPHSETDIR)
unicodes = src.getUnicodes()
for glyphName in src.keys():
g = src[glyphName]
g.drawPoints(None) # load attrs
if not hasattr(g, "unicodes"):
self.assertEqual(unicodes[glyphName], [])
else:
self.assertEqual(g.unicodes, unicodes[glyphName])
class FileNameTests(unittest.TestCase):
def testDefaultFileNameScheme(self):
self.assertEqual(glyphNameToFileName("a", None), "a.glif")
self.assertEqual(glyphNameToFileName("A", None), "A_.glif")
self.assertEqual(glyphNameToFileName("Aring", None), "Aring_.glif")
self.assertEqual(glyphNameToFileName("F_A_B", None), "F__A__B_.glif")
self.assertEqual(glyphNameToFileName("A.alt", None), "A_.alt.glif")
self.assertEqual(glyphNameToFileName("A.Alt", None), "A_.Alt_.glif")
self.assertEqual(glyphNameToFileName(".notdef", None), "_notdef.glif")
self.assertEqual(glyphNameToFileName("T_H", None), "T__H_.glif")
self.assertEqual(glyphNameToFileName("T_h", None), "T__h.glif")
self.assertEqual(glyphNameToFileName("t_h", None), "t_h.glif")
self.assertEqual(glyphNameToFileName('F_F_I', None), "F__F__I_.glif")
self.assertEqual(glyphNameToFileName('f_f_i', None), "f_f_i.glif")
def testShortFileNameScheme(self):
print "testShortFileNameScheme"
self.assertEqual(glyphNameToShortFileName("a", None), "a.glif")
self.assertEqual(glyphNameToShortFileName("A", None), "A_.glif")
self.assertEqual(glyphNameToShortFileName("aE", None), "aE_.glif")
self.assertEqual(glyphNameToShortFileName("AE", None), "A_E_.glif")
self.assertEqual(glyphNameToShortFileName("a.alt", None), "a_alt.glif")
self.assertEqual(glyphNameToShortFileName("A.alt", None), "A__alt.glif")
self.assertEqual(glyphNameToShortFileName("a.alt#swash", None), "a_alt_swash.glif")
self.assertEqual(glyphNameToShortFileName("A.alt", None), "A__alt.glif")
self.assertEqual(glyphNameToShortFileName(".notdef", None), "_notdef.glif")
self.assertEqual(glyphNameToShortFileName("f_f_i", None), "f_f_i.glif")
self.assertEqual(glyphNameToShortFileName("F_F_I", None), "F__F__I_.glif")
self.assertEqual(glyphNameToShortFileName("acircumflexdieresis.swash.alt1", None), "acircumflexdieresi0cfc8352.glif")
self.assertEqual(glyphNameToShortFileName("acircumflexdieresis.swash.alt2", None), "acircumflexdieresi95f5d2e8.glif")
self.assertEqual(glyphNameToShortFileName("Acircumflexdieresis.swash.alt1", None), "A_circumflexdieresed24fb56.glif")
self.assertEqual(glyphNameToShortFileName("F#weight0.800_width0.425", None), "F__weight0_800_width0_425.glif")
self.assertEqual(glyphNameToShortFileName("F#weight0.83245511_width0.425693567", None), "F__weight0_8324551c9a4143c.glif")
self.assertEqual(len(glyphNameToShortFileName("F#weight0.83245511_width0.425693567", None)), 31)
def testShortFileNameScheme_clashes(self):
# test for the condition in code.robofab.com ticket #5
name1 = glyphNameToShortFileName('Adieresis', None)
name2 = glyphNameToShortFileName('a_dieresis', None)
self.assertNotEqual(name1, name2)
name1 = glyphNameToShortFileName('AE', None)
name2 = glyphNameToShortFileName('aE', None)
self.assertNotEqual(name1, name2)
if __name__ == "__main__":
from robofab.test.testSupport import runTests
import sys
if len(sys.argv) > 1 and os.path.isdir(sys.argv[-1]):
GLYPHSETDIR = sys.argv.pop()
runTests()

View file

@ -0,0 +1,321 @@
import os
import shutil
import unittest
import tempfile
from robofab.plistlib import readPlist
import robofab
from robofab.test.testSupport import fontInfoVersion2, expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion
from robofab.objects.objectsRF import NewFont, OpenFont
from robofab.ufoLib import UFOReader
ufoPath1 = os.path.dirname(robofab.__file__)
ufoPath1 = os.path.dirname(ufoPath1)
ufoPath1 = os.path.dirname(ufoPath1)
ufoPath1 = os.path.join(ufoPath1, "TestData", "TestFont1 (UFO1).ufo")
ufoPath2 = ufoPath1.replace("TestFont1 (UFO1).ufo", "TestFont1 (UFO2).ufo")
# robofab should remove these from the lib after a load.
removeFromFormatVersion1Lib = [
"org.robofab.opentype.classes",
"org.robofab.opentype.features",
"org.robofab.opentype.featureorder",
"org.robofab.postScriptHintData"
]
class ReadUFOFormatVersion1TestCase(unittest.TestCase):
def setUpFont(self):
self.font = OpenFont(ufoPath1)
self.font.update()
def tearDownFont(self):
self.font.close()
self.font = None
def compareToUFO(self, doInfo=True):
reader = UFOReader(ufoPath1)
results = {}
# info
infoMatches = True
info = self.font.info
for attr, expectedValue in expectedFontInfo1To2Conversion.items():
writtenValue = getattr(info, attr)
if expectedValue != writtenValue:
infoMatches = False
break
results["info"]= infoMatches
# kerning
kerning = self.font.kerning.asDict()
expectedKerning = reader.readKerning()
results["kerning"] = expectedKerning == kerning
# groups
groups = dict(self.font.groups)
expectedGroups = reader.readGroups()
results["groups"] = expectedGroups == groups
# features
features = self.font.features.text
f = open(os.path.join(ufoPath2, "features.fea"), "r")
expectedFeatures = f.read()
f.close()
match = True
features = [line for line in features.splitlines() if line]
expectedFeatures = [line for line in expectedFeatures.splitlines() if line]
if expectedFeatures != features or reader.readFeatures() != "":
match = False
results["features"] = match
# lib
lib = dict(self.font.lib)
expectedLib = reader.readLib()
for key in removeFromFormatVersion1Lib:
if key in expectedLib:
del expectedLib[key]
results["lib"] = expectedLib == lib
return results
def testFull(self):
self.setUpFont()
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
def testInfo(self):
self.setUpFont()
info = self.font.info
for attr, expectedValue in expectedFontInfo1To2Conversion.items():
writtenValue = getattr(info, attr)
self.assertEqual((attr, expectedValue), (attr, writtenValue))
self.tearDownFont()
class ReadUFOFormatVersion2TestCase(unittest.TestCase):
def setUpFont(self):
self.font = OpenFont(ufoPath2)
self.font.update()
def tearDownFont(self):
self.font.close()
self.font = None
def compareToUFO(self, doInfo=True):
reader = UFOReader(ufoPath2)
results = {}
# info
infoMatches = True
info = self.font.info
for attr, expectedValue in fontInfoVersion2.items():
writtenValue = getattr(info, attr)
if expectedValue != writtenValue:
infoMatches = False
break
results["info"]= infoMatches
# kerning
kerning = self.font.kerning.asDict()
expectedKerning = reader.readKerning()
results["kerning"] = expectedKerning == kerning
# groups
groups = dict(self.font.groups)
expectedGroups = reader.readGroups()
results["groups"] = expectedGroups == groups
# features
features = self.font.features.text
expectedFeatures = reader.readFeatures()
results["features"] = expectedFeatures == features
# lib
lib = dict(self.font.lib)
expectedLib = reader.readLib()
results["lib"] = expectedLib == lib
return results
def testFull(self):
self.setUpFont()
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
def testInfo(self):
self.setUpFont()
info = self.font.info
for attr, expectedValue in fontInfoVersion2.items():
writtenValue = getattr(info, attr)
self.assertEqual((attr, expectedValue), (attr, writtenValue))
self.tearDownFont()
class WriteUFOFormatVersion1TestCase(unittest.TestCase):
def setUpFont(self):
self.dstDir = tempfile.mktemp()
os.mkdir(self.dstDir)
self.font = OpenFont(ufoPath2)
self.font.save(self.dstDir, formatVersion=1)
def tearDownFont(self):
shutil.rmtree(self.dstDir)
def compareToUFO(self):
readerExpected = UFOReader(ufoPath1)
readerWritten = UFOReader(self.dstDir)
results = {}
# info
matches = True
expectedPath = os.path.join(ufoPath1, "fontinfo.plist")
writtenPath = os.path.join(self.dstDir, "fontinfo.plist")
if not os.path.exists(writtenPath):
matches = False
else:
expected = readPlist(expectedPath)
written = readPlist(writtenPath)
for attr, expectedValue in expected.items():
if expectedValue != written.get(attr):
matches = False
break
results["info"] = matches
# kerning
matches = True
expectedPath = os.path.join(ufoPath1, "kerning.plist")
writtenPath = os.path.join(self.dstDir, "kerning.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["kerning"] = matches
# groups
matches = True
expectedPath = os.path.join(ufoPath1, "groups.plist")
writtenPath = os.path.join(self.dstDir, "groups.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["groups"] = matches
# features
matches = True
expectedPath = os.path.join(ufoPath1, "features.fea")
writtenPath = os.path.join(self.dstDir, "features.fea")
if os.path.exists(writtenPath):
matches = False
results["features"] = matches
# lib
matches = True
expectedPath = os.path.join(ufoPath1, "lib.plist")
writtenPath = os.path.join(self.dstDir, "lib.plist")
if not os.path.exists(writtenPath):
matches = False
else:
writtenLib = readPlist(writtenPath)
matches = readPlist(expectedPath) == writtenLib
results["lib"] = matches
return results
def testFull(self):
self.setUpFont()
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
class WriteUFOFormatVersion2TestCase(unittest.TestCase):
def setUpFont(self):
self.dstDir = tempfile.mktemp()
os.mkdir(self.dstDir)
self.font = OpenFont(ufoPath2)
self.font.save(self.dstDir)
def tearDownFont(self):
shutil.rmtree(self.dstDir)
def compareToUFO(self):
readerExpected = UFOReader(ufoPath2)
readerWritten = UFOReader(self.dstDir)
results = {}
# info
matches = True
expectedPath = os.path.join(ufoPath2, "fontinfo.plist")
writtenPath = os.path.join(self.dstDir, "fontinfo.plist")
if not os.path.exists(writtenPath):
matches = False
else:
expected = readPlist(expectedPath)
written = readPlist(writtenPath)
for attr, expectedValue in expected.items():
if expectedValue != written[attr]:
matches = False
break
results["info"] = matches
# kerning
matches = True
expectedPath = os.path.join(ufoPath2, "kerning.plist")
writtenPath = os.path.join(self.dstDir, "kerning.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["kerning"] = matches
# groups
matches = True
expectedPath = os.path.join(ufoPath2, "groups.plist")
writtenPath = os.path.join(self.dstDir, "groups.plist")
if not os.path.exists(writtenPath):
matches = False
else:
matches = readPlist(expectedPath) == readPlist(writtenPath)
results["groups"] = matches
# features
matches = True
expectedPath = os.path.join(ufoPath2, "features.fea")
writtenPath = os.path.join(self.dstDir, "features.fea")
if not os.path.exists(writtenPath):
matches = False
else:
f = open(expectedPath, "r")
expectedText = f.read()
f.close()
f = open(writtenPath, "r")
writtenText = f.read()
f.close()
# FontLab likes to add lines to the features, so skip blank lines.
expectedText = [line for line in expectedText.splitlines() if line]
writtenText = [line for line in writtenText.splitlines() if line]
matches = "\n".join(expectedText) == "\n".join(writtenText)
results["features"] = matches
# lib
matches = True
expectedPath = os.path.join(ufoPath2, "lib.plist")
writtenPath = os.path.join(self.dstDir, "lib.plist")
if not os.path.exists(writtenPath):
matches = False
else:
writtenLib = readPlist(writtenPath)
matches = readPlist(expectedPath) == writtenLib
results["lib"] = matches
return results
def testFull(self):
self.setUpFont()
otherResults = self.compareToUFO()
self.assertEqual(otherResults["info"], True)
self.assertEqual(otherResults["kerning"], True)
self.assertEqual(otherResults["groups"], True)
self.assertEqual(otherResults["features"], True)
self.assertEqual(otherResults["lib"], True)
self.tearDownFont()
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,54 @@
"""This test suite for various FontLab-specific tests."""
import FL # needed to quickly raise ImportError if run outside of FL
import os
import tempfile
import unittest
from robofab.world import NewFont
from robofab.test.testSupport import getDemoFontPath, getDemoFontGlyphSetPath
from robofab.tools.glifImport import importAllGlifFiles
from robofab.pens.digestPen import DigestPointPen
from robofab.pens.adapterPens import SegmentToPointPen
def getDigests(font):
digests = {}
for glyphName in font.keys():
pen = DigestPointPen()
font[glyphName].drawPoints(pen)
digests[glyphName] = pen.getDigest()
return digests
class FLTestCase(unittest.TestCase):
def testUFOVersusGlifImport(self):
font = NewFont()
font.readUFO(getDemoFontPath(), doProgress=False)
d1 = getDigests(font)
font.close(False)
font = NewFont()
importAllGlifFiles(font.naked(), getDemoFontGlyphSetPath(), doProgress=False)
d2 = getDigests(font)
self.assertEqual(d1, d2)
font.close(False)
def testTwoUntitledFonts(self):
font1 = NewFont()
font2 = NewFont()
font1.unitsPerEm = 1024
font2.unitsPerEm = 2048
self.assertNotEqual(font1.unitsPerEm, font2.unitsPerEm)
font1.update()
font2.update()
font1.close(False)
font2.close(False)
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,203 @@
"""This test suite for ufo glyph methods"""
import unittest
import os
import tempfile
import shutil
from robofab.objects.objectsRF import RFont
from robofab.test.testSupport import getDemoFontPath
from robofab.pens.digestPen import DigestPointPen
from robofab.pens.adapterPens import SegmentToPointPen, FabToFontToolsPenAdapter
class ContourMethodsTestCase(unittest.TestCase):
def setUp(self):
self.font = RFont(getDemoFontPath())
def testReverseContour(self):
for glyph in self.font:
pen = DigestPointPen()
glyph.drawPoints(pen)
digest1 = pen.getDigest()
for contour in glyph:
contour.reverseContour()
contour.reverseContour()
pen = DigestPointPen()
glyph.drawPoints(pen)
digest2 = pen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same after reversing twice" % glyph.name)
def testStartSegment(self):
for glyph in self.font:
pen = DigestPointPen()
glyph.drawPoints(pen)
digest1 = pen.getDigest()
for contour in glyph:
contour.setStartSegment(2)
contour.setStartSegment(-2)
pen = DigestPointPen()
glyph.drawPoints(pen)
digest2 = pen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same after seting start segment twice" % glyph.name)
def testAppendSegment(self):
for glyph in self.font:
pen = DigestPointPen()
glyph.drawPoints(pen)
digest1 = pen.getDigest()
for contour in glyph:
contour.insertSegment(2, "curve", [(100, 100), (200, 200), (300, 300)])
contour.removeSegment(2)
pen = DigestPointPen()
glyph.drawPoints(pen)
digest2 = pen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same after inserting and removing segment" % glyph.name)
class GlyphsMethodsTestCase(ContourMethodsTestCase):
def testCopyGlyph(self):
for glyph in self.font:
pen = DigestPointPen()
glyph.drawPoints(pen)
digest1 = pen.getDigest()
copy = glyph.copy()
pen = DigestPointPen()
copy.drawPoints(pen)
digest2 = pen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same after copying" % glyph.name)
self.assertEqual(glyph.lib, copy.lib, "%r's lib not the same after copying" % glyph.name)
self.assertEqual(glyph.width, copy.width, "%r's width not the same after copying" % glyph.name)
self.assertEqual(glyph.unicodes, copy.unicodes, "%r's unicodes not the same after copying" % glyph.name)
def testMoveGlyph(self):
for glyph in self.font:
pen = DigestPointPen()
glyph.drawPoints(pen)
digest1 = pen.getDigest()
glyph.move((100, 200))
glyph.move((-100, -200))
pen = DigestPointPen()
glyph.drawPoints(pen)
digest2 = pen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same after moving twice" % glyph.name)
def testScaleGlyph(self):
for glyph in self.font:
pen = DigestPointPen()
glyph.drawPoints(pen)
digest1 = pen.getDigest()
glyph.scale((2, 2))
glyph.scale((.5, .5))
pen = DigestPointPen()
glyph.drawPoints(pen)
digest2 = pen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same after scaling twice" % glyph.name)
def testSegmentPenInterface(self):
for glyph in self.font:
digestPen = DigestPointPen(ignoreSmoothAndName=True)
pen = SegmentToPointPen(digestPen)
glyph.draw(pen)
digest1 = digestPen.getDigest()
digestPen = DigestPointPen(ignoreSmoothAndName=True)
glyph.drawPoints(digestPen)
digest2 = digestPen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same for gl.draw() and gl.drawPoints()" % glyph.name)
def testFabPenCompatibility(self):
for glyph in self.font:
digestPen = DigestPointPen(ignoreSmoothAndName=True)
pen = FabToFontToolsPenAdapter(SegmentToPointPen(digestPen))
glyph.draw(pen)
digest1 = digestPen.getDigest()
digestPen = DigestPointPen(ignoreSmoothAndName=True)
glyph.drawPoints(digestPen)
digest2 = digestPen.getDigest()
self.assertEqual(digest1, digest2, "%r not the same for gl.draw() and gl.drawPoints()" % glyph.name)
def testComponentTransformations(self):
from robofab.objects.objectsRF import RComponent
name = "baseGlyphName"
c = RComponent(name, transform=(1,0,0,1,0,0))
# get values
assert c.baseGlyph == "baseGlyphName"
assert c.transformation == c.transformation
assert c.scale == (1,1)
assert c.offset == (0,0)
# set values
c.offset = (12,34)
assert c.transformation == (1, 0, 0, 1, 12, 34)
c.offset = (0,0)
assert c.transformation == (1,0,0,1,0,0)
c.scale = (12,34)
assert c.transformation == (12, 0, 0, 34, 0, 0)
class SaveTestCase(ContourMethodsTestCase):
def testSaveAs(self):
path = tempfile.mktemp(".ufo")
try:
keys1 = self.font.keys()
self.font.save(path)
keys2 = self.font.keys()
keys1.sort()
keys2.sort()
self.assertEqual(keys1, keys2)
self.assertEqual(self.font.path, path)
font2 = RFont(path)
keys3 = font2.keys()
keys3.sort()
self.assertEqual(keys1, keys3)
finally:
if os.path.exists(path):
shutil.rmtree(path)
def testSaveAs2(self):
path = tempfile.mktemp(".ufo")
# copy a glyph
self.font["X"] = self.font["a"].copy()
# self.assertEqual(self.font["X"].name, "X")
# remove a glyph
self.font.removeGlyph("a")
keys1 = self.font.keys()
try:
self.font.save(path)
self.assertEqual(self.font.path, path)
keys2 = self.font.keys()
keys1.sort()
keys2.sort()
self.assertEqual(keys1, keys2)
font2 = RFont(path)
keys3 = font2.keys()
keys3.sort()
self.assertEqual(keys1, keys3)
finally:
if os.path.exists(path):
shutil.rmtree(path)
def testCustomFileNameScheme(self):
path = tempfile.mktemp(".ufo")
libKey = "org.robofab.glyphNameToFileNameFuncName"
self.font.lib[libKey] = "robofab.test.test_objectsUFO.testGlyphNameToFileName"
try:
self.font.save(path)
self.assertEqual(os.path.exists(os.path.join(path,
"glyphs", "test_a.glif")), True)
finally:
if os.path.exists(path):
shutil.rmtree(path)
def testGlyphNameToFileName(glyphName, glyphSet):
from robofab.glifLib import glyphNameToFileName
return "test_" + glyphNameToFileName(glyphName, glyphSet)
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,149 @@
"""This test suite test general Pen stuff, it should not contain
FontLab-specific code.
"""
import unittest
from robofab.pens.digestPen import DigestPointPen
from robofab.pens.adapterPens import SegmentToPointPen, PointToSegmentPen
from robofab.pens.adapterPens import GuessSmoothPointPen
from robofab.pens.reverseContourPointPen import ReverseContourPointPen
from robofab.test.testSupport import getDemoFontGlyphSetPath
from robofab.glifLib import GlyphSet
class TestShapes:
# Collection of test shapes. It's probably better to add these as
# glyphs to the demo font.
def square(pen):
# a simple square as a closed path (100, 100, 600, 600)
pen.beginPath()
pen.addPoint((100, 100), "line")
pen.addPoint((100, 600), "line")
pen.addPoint((600, 600), "line")
pen.addPoint((600, 100), "line")
pen.endPath()
square = staticmethod(square)
def onCurveLessQuadShape(pen):
pen.beginPath()
pen.addPoint((100, 100))
pen.addPoint((100, 600))
pen.addPoint((600, 600))
pen.addPoint((600, 100))
pen.endPath()
onCurveLessQuadShape = staticmethod(onCurveLessQuadShape)
def openPath(pen):
# a simple square as a closed path (100, 100, 600, 600)
pen.beginPath()
pen.addPoint((100, 100), "move")
pen.addPoint((100, 600), "line")
pen.addPoint((600, 600), "line")
pen.addPoint((600, 100), "line")
pen.endPath()
openPath = staticmethod(openPath)
def circle(pen):
pen.beginPath()
pen.addPoint((0, 500), "curve")
pen.addPoint((0, 800))
pen.addPoint((200, 1000))
pen.addPoint((500, 1000), "curve")
pen.addPoint((800, 1000))
pen.addPoint((1000, 800))
pen.addPoint((1000, 500), "curve")
pen.addPoint((1000, 200))
pen.addPoint((800, 0))
pen.addPoint((500, 0), "curve")
pen.addPoint((200, 0))
pen.addPoint((0, 200))
pen.endPath()
circle = staticmethod(circle)
class RoundTripTestCase(unittest.TestCase):
def _doTest(self, shapeFunc, shapeName):
pen = DigestPointPen(ignoreSmoothAndName=True)
shapeFunc(pen)
digest1 = pen.getDigest()
digestPen = DigestPointPen(ignoreSmoothAndName=True)
pen = PointToSegmentPen(SegmentToPointPen(digestPen))
shapeFunc(pen)
digest2 = digestPen.getDigest()
self.assertEqual(digest1, digest2, "%r failed round tripping" % shapeName)
def testShapes(self):
for name in dir(TestShapes):
if name[0] != "_":
self._doTest(getattr(TestShapes, name), name)
def testShapesFromGlyphSet(self):
glyphSet = GlyphSet(getDemoFontGlyphSetPath())
for name in glyphSet.keys():
self._doTest(glyphSet[name].drawPoints, name)
def testGuessSmoothPen(self):
glyphSet = GlyphSet(getDemoFontGlyphSetPath())
for name in glyphSet.keys():
digestPen = DigestPointPen()
glyphSet[name].drawPoints(digestPen)
digest1 = digestPen.getDigest()
digestPen = DigestPointPen()
pen = GuessSmoothPointPen(digestPen)
glyphSet[name].drawPoints(pen)
digest2 = digestPen.getDigest()
self.assertEqual(digest1, digest2)
class ReverseContourTestCase(unittest.TestCase):
def testReverseContourClosedPath(self):
digestPen = DigestPointPen()
TestShapes.square(digestPen)
d1 = digestPen.getDigest()
digestPen = DigestPointPen()
pen = ReverseContourPointPen(digestPen)
pen.beginPath()
pen.addPoint((100, 100), "line")
pen.addPoint((600, 100), "line")
pen.addPoint((600, 600), "line")
pen.addPoint((100, 600), "line")
pen.endPath()
d2 = digestPen.getDigest()
self.assertEqual(d1, d2)
def testReverseContourOpenPath(self):
digestPen = DigestPointPen()
TestShapes.openPath(digestPen)
d1 = digestPen.getDigest()
digestPen = DigestPointPen()
pen = ReverseContourPointPen(digestPen)
pen.beginPath()
pen.addPoint((600, 100), "move")
pen.addPoint((600, 600), "line")
pen.addPoint((100, 600), "line")
pen.addPoint((100, 100), "line")
pen.endPath()
d2 = digestPen.getDigest()
self.assertEqual(d1, d2)
def testReversContourFromGlyphSet(self):
glyphSet = GlyphSet(getDemoFontGlyphSetPath())
digestPen = DigestPointPen()
glyphSet["testglyph1"].drawPoints(digestPen)
digest1 = digestPen.getDigest()
digestPen = DigestPointPen()
pen = ReverseContourPointPen(digestPen)
glyphSet["testglyph1.reversed"].drawPoints(pen)
digest2 = digestPen.getDigest()
self.assertEqual(digest1, digest2)
if __name__ == "__main__":
from robofab.test.testSupport import runTests
runTests()

View file

@ -0,0 +1,110 @@
def test():
"""
# some tests for the ps Hints operations
>>> from robofab.world import RFont, RGlyph
>>> g = RGlyph()
>>> g.psHints.isEmpty()
True
>>> h = RGlyph()
>>> i = g + h
>>> i.psHints.isEmpty()
True
>>> i = g - h
>>> i.psHints.isEmpty()
True
>>> i = g * 2
>>> i.psHints.isEmpty()
True
>>> i = g / 2
>>> i.psHints.isEmpty()
True
>>> g.psHints.vHints = [(100, 50), (200, 50)]
>>> g.psHints.hHints = [(100, 50), (200, 5)]
>>> not g.psHints.isEmpty()
True
>>> gc = g.copy()
>>> gc.psHints.asDict() == g.psHints.asDict()
True
# multiplication
>>> v = g.psHints * 2
>>> v.asDict() == {'vHints': [[200, 100], [400, 100]], 'hHints': [[200, 100], [400, 10]]}
True
# division
>>> v = g.psHints / 2
>>> v.asDict() == {'vHints': [[50.0, 25.0], [100.0, 25.0]], 'hHints': [[50.0, 25.0], [100.0, 2.5]]}
True
# multiplication with x, y, factor
# vertically oriented values should respond different
>>> v = g.psHints * (.5, 10)
>>> v.asDict() == {'vHints': [[1000, 500], [2000, 500]], 'hHints': [[50.0, 25.0], [100.0, 2.5]]}
True
# division with x, y, factor
# vertically oriented values should respond different
>>> v = g.psHints / (.5, 10)
>>> v.asDict() == {'vHints': [[10.0, 5.0], [20.0, 5.0]], 'hHints': [[200.0, 100.0], [400.0, 10.0]]}
True
# rounding to integer
>>> v = g.psHints / 2
>>> v.round()
>>> v.asDict() == {'vHints': [(50, 25), (100, 25)], 'hHints': [(50, 25), (100, 3)]}
True
# "ps hint values calculating with a glyph"
# ps hint values as part of glyphmath operations.
# multiplication
>>> h = g * 10
>>> h.psHints.asDict() == {'vHints': [[1000, 500], [2000, 500]], 'hHints': [[1000, 500], [2000, 50]]}
True
# division
>>> h = g / 2
>>> h.psHints.asDict() == {'vHints': [[50.0, 25.0], [100.0, 25.0]], 'hHints': [[50.0, 25.0], [100.0, 2.5]]}
True
# x, y factor multiplication
>>> h = g * (.5, 10)
>>> h.psHints.asDict() == {'vHints': [[1000, 500], [2000, 500]], 'hHints': [[50.0, 25.0], [100.0, 2.5]]}
True
# x, y factor division
>>> h = g / (.5, 10)
>>> h.psHints.asDict() == {'vHints': [[10.0, 5.0], [20.0, 5.0]], 'hHints': [[200.0, 100.0], [400.0, 10.0]]}
True
# "font ps hint values"
>>> f = RFont()
>>> f.psHints.isEmpty()
True
>>> f.psHints.blueScale = .5
>>> f.psHints.blueShift = 1
>>> f.psHints.blueFuzz = 1
>>> f.psHints.forceBold = True
>>> f.psHints.hStems = (100, 90)
>>> f.psHints.vStems = (500, 10)
>>> not f.psHints.isEmpty()
True
>>> f.insertGlyph(g, name="new")
<RGlyph for None.new>
>>> f["new"].psHints.asDict() == g.psHints.asDict()
True
"""
if __name__ == "__main__":
import doctest
doctest.testmod()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
"""
Directory for all tool like code.
Stuff that doesn't really belong to objects, pens, compilers etc.
The code is split up into sections.
"""

View file

@ -0,0 +1,348 @@
"""A simple set of tools for building accented glyphs.
# Hey look! A demonstration:
from robofab.accentBuilder import AccentTools, buildRelatedAccentList
font = CurrentFont
# a list of accented glyphs that you want to build
myList=['Aacute', 'aacute']
# search for glyphs related to glyphs in myList and add them to myList
myList=buildRelatedAccentList(font, myList)+myList
# start the class
at=AccentTools(font, myList)
# clear away any anchors that exist (this is optional)
at.clearAnchors()
# add necessary anchors if you want to
at.buildAnchors(ucXOffset=20, ucYOffset=40, lcXOffset=15, lcYOffset=30)
# print a report of any errors that occured
at.printAnchorErrors()
# build the accented glyphs if you want to
at.buildAccents()
# print a report of any errors that occured
at.printAccentErrors()
"""
#XXX! This is *very* experimental! I think it works, but you never know.
from robofab.gString import lowercase_plain, accents, uppercase_plain, splitAccent, findAccentBase
from robofab.tools.toolsAll import readGlyphConstructions
import robofab
from robofab.interface.all.dialogs import ProgressBar
from robofab.world import RFWorld
inFontLab = RFWorld().inFontLab
anchorColor=125
accentColor=75
def stripSuffix(glyphName):
"""strip away unnecessary suffixes from a glyph name"""
if glyphName.find('.') != -1:
baseName = glyphName.split('.')[0]
if glyphName.find('.sc') != -1:
baseName = '.'.join([baseName, 'sc'])
return baseName
else:
return glyphName
def buildRelatedAccentList(font, list):
"""build a list of related glyphs suitable for use with AccentTools"""
searchList = []
baseGlyphs = {}
foundList = []
for glyphName in list:
splitNames = splitAccent(glyphName)
baseName = splitNames[0]
accentNames = splitNames[1]
if baseName not in searchList:
searchList.append(baseName)
if baseName not in baseGlyphs.keys():
baseGlyphs[baseName] = [accentNames]
else:
baseGlyphs[baseName].append(accentNames)
foundGlyphs = findRelatedGlyphs(font, searchList, doAccents=0)
for baseGlyph in foundGlyphs.keys():
for foundGlyph in foundGlyphs[baseGlyph]:
for accentNames in baseGlyphs[baseGlyph]:
foundList.append(makeAccentName(foundGlyph, accentNames))
return foundList
def findRelatedGlyphs(font, searchItem, doAccents=True):
"""Gather up a bunch of related glyph names. Send it either a
single glyph name 'a', or a list of glyph names ['a', 'x'] and it
returns a dict like: {'a': ['atilde', 'a.alt', 'a.swash']}. if doAccents
is False it will skip accented glyph names.
This is a relatively slow operation!"""
relatedGlyphs = {}
for name in font.keys():
base = name.split('.')[0]
if name not in relatedGlyphs.keys():
relatedGlyphs[name] = []
if base not in relatedGlyphs.keys():
relatedGlyphs[base] = []
if doAccents:
accentBase = findAccentBase(name)
if accentBase not in relatedGlyphs.keys():
relatedGlyphs[accentBase] = []
baseAccentBase = findAccentBase(base)
if baseAccentBase not in relatedGlyphs.keys():
relatedGlyphs[baseAccentBase] = []
if base != name and name not in relatedGlyphs[base]:
relatedGlyphs[base].append(name)
if doAccents:
if accentBase != name and name not in relatedGlyphs[accentBase]:
relatedGlyphs[accentBase].append(name)
if baseAccentBase != name and name not in relatedGlyphs[baseAccentBase]:
relatedGlyphs[baseAccentBase].append(name)
foundGlyphs = {}
if isinstance(searchItem, str):
searchList = [searchItem]
else:
searchList = searchItem
for glyph in searchList:
foundGlyphs[glyph] = relatedGlyphs[glyph]
return foundGlyphs
def makeAccentName(baseName, accentNames):
"""make an accented glyph name"""
if isinstance(accentNames, str):
accentNames = [accentNames]
build = []
if baseName.find('.') != -1:
base = baseName.split('.')[0]
suffix = baseName.split('.')[1]
else:
base = baseName
suffix = ''
build.append(base)
for accent in accentNames:
build.append(accent)
buildJoin = ''.join(build)
name = '.'.join([buildJoin, suffix])
return name
def nameBuster(glyphName, glyphConstruct):
stripedSuffixName = stripSuffix(glyphName)
suffix = None
errors = []
accentNames = []
baseName = glyphName
if glyphName.find('.') != -1:
suffix = glyphName.split('.')[1]
if glyphName.find('.sc') != -1:
suffix = glyphName.split('.sc')[1]
if stripedSuffixName not in glyphConstruct.keys():
errors.append('%s: %s not in glyph construction database'%(glyphName, stripedSuffixName))
else:
if suffix is None:
baseName = glyphConstruct[stripedSuffixName][0]
else:
if glyphName.find('.sc') != -1:
baseName = ''.join([glyphConstruct[stripedSuffixName][0], suffix])
else:
baseName = '.'.join([glyphConstruct[stripedSuffixName][0], suffix])
accentNames = glyphConstruct[stripedSuffixName][1:]
return (baseName, stripedSuffixName, accentNames, errors)
class AccentTools:
def __init__(self, font, accentList):
"""several tools for working with anchors and building accents"""
self.glyphConstructions = readGlyphConstructions()
self.accentList = accentList
self.anchorErrors = ['ANCHOR ERRORS:']
self.accentErrors = ['ACCENT ERRORS:']
self.font = font
def clearAnchors(self, doProgress=True):
"""clear all anchors in the font"""
tickCount = len(self.font)
if doProgress:
bar = ProgressBar("Cleaning all anchors...", tickCount)
tick = 0
for glyphName in self.accentList:
if doProgress:
bar.label(glyphName)
baseName, stripedSuffixName, accentNames, errors = nameBuster(glyphName, self.glyphConstructions)
existError = False
if len(errors) > 0:
existError = True
if not existError:
toClear = [baseName]
for accent, position in accentNames:
toClear.append(accent)
for glyphName in toClear:
try:
self.font[glyphName].clearAnchors()
except IndexError: pass
if doProgress:
bar.tick(tick)
tick = tick+1
if doProgress:
bar.close()
def buildAnchors(self, ucXOffset=0, ucYOffset=0, lcXOffset=0, lcYOffset=0, markGlyph=True, doProgress=True):
"""add the necessary anchors to the glyphs if they don't exist
some flag definitions:
uc/lc/X/YOffset=20 offset values for the anchors
markGlyph=1 mark the glyph that is created
doProgress=1 show a progress bar"""
accentOffset = 10
tickCount = len(self.accentList)
if doProgress:
bar = ProgressBar('Adding anchors...', tickCount)
tick = 0
for glyphName in self.accentList:
if doProgress:
bar.label(glyphName)
previousPositions = {}
baseName, stripedSuffixName, accentNames, errors = nameBuster(glyphName, self.glyphConstructions)
existError = False
if len(errors) > 0:
existError = True
for anchorError in errors:
self.anchorErrors.append(anchorError)
if not existError:
existError = False
try:
self.font[baseName]
except IndexError:
self.anchorErrors.append(' '.join([glyphName, ':', baseName, 'does not exist.']))
existError = True
for accentName, accentPosition in accentNames:
try:
self.font[accentName]
except IndexError:
self.anchorErrors.append(' '.join([glyphName, ':', accentName, 'does not exist.']))
existError = True
if not existError:
#glyph = self.font.newGlyph(glyphName, clear=True)
for accentName, accentPosition in accentNames:
if baseName.split('.')[0] in lowercase_plain:
xOffset = lcXOffset-accentOffset
yOffset = lcYOffset-accentOffset
else:
xOffset = ucXOffset-accentOffset
yOffset = ucYOffset-accentOffset
# should I add a cedilla and ogonek yoffset override here?
if accentPosition not in previousPositions.keys():
self._dropAnchor(self.font[baseName], accentPosition, xOffset, yOffset)
if markGlyph:
self.font[baseName].mark = anchorColor
if inFontLab:
self.font[baseName].update()
else:
self._dropAnchor(self.font[previousPositions[accentPosition]], accentPosition, xOffset, yOffset)
self._dropAnchor(self.font[accentName], accentPosition, accentOffset, accentOffset, doAccentPosition=1)
previousPositions[accentPosition] = accentName
if markGlyph:
self.font[accentName].mark = anchorColor
if inFontLab:
self.font[accentName].update()
if inFontLab:
self.font.update()
if doProgress:
bar.tick(tick)
tick = tick+1
if doProgress:
bar.close()
def printAnchorErrors(self):
"""print errors encounted during buildAnchors"""
if len(self.anchorErrors) == 1:
print 'No anchor errors encountered'
else:
for i in self.anchorErrors:
print i
def _dropAnchor(self, glyph, positionName, xOffset=0, yOffset=0, doAccentPosition=False):
"""anchor adding method. for internal use only."""
existingAnchorNames = []
for anchor in glyph.getAnchors():
existingAnchorNames.append(anchor.name)
if doAccentPosition:
positionName = ''.join(['_', positionName])
if positionName not in existingAnchorNames:
glyphLeft, glyphBottom, glyphRight, glyphTop = glyph.box
glyphXCenter = glyph.width/2
if positionName == 'top':
glyph.appendAnchor(positionName, (glyphXCenter, glyphTop+yOffset))
elif positionName == 'bottom':
glyph.appendAnchor(positionName, (glyphXCenter, glyphBottom-yOffset))
elif positionName == 'left':
glyph.appendAnchor(positionName, (glyphLeft-xOffset, glyphTop))
elif positionName == 'right':
glyph.appendAnchor(positionName, (glyphRight+xOffset, glyphTop))
elif positionName == '_top':
glyph.appendAnchor(positionName, (glyphXCenter, glyphBottom-yOffset))
elif positionName == '_bottom':
glyph.appendAnchor(positionName, (glyphXCenter, glyphTop+yOffset))
elif positionName == '_left':
glyph.appendAnchor(positionName, (glyphRight+xOffset, glyphTop))
elif positionName == '_right':
glyph.appendAnchor(positionName, (glyphLeft-xOffset, glyphTop))
if inFontLab:
glyph.update()
def buildAccents(self, clear=True, adjustWidths=True, markGlyph=True, doProgress=True):
"""build accented glyphs. some flag definitions:
clear=1 clear the glyphs if they already exist
markGlyph=1 mark the glyph that is created
doProgress=1 show a progress bar
adjustWidths=1 will fix right and left margins when left or right accents are added"""
tickCount = len(self.accentList)
if doProgress:
bar = ProgressBar('Building accented glyphs...', tickCount)
tick = 0
for glyphName in self.accentList:
if doProgress:
bar.label(glyphName)
existError = False
anchorError = False
baseName, stripedSuffixName, accentNames, errors = nameBuster(glyphName, self.glyphConstructions)
if len(errors) > 0:
existError = True
for accentError in errors:
self.accentErrors.append(accentError)
if not existError:
baseAnchors = []
try:
self.font[baseName]
except IndexError:
self.accentErrors.append('%s: %s does not exist.'%(glyphName, baseName))
existError = True
else:
for anchor in self.font[baseName].anchors:
baseAnchors.append(anchor.name)
for accentName, accentPosition in accentNames:
accentAnchors = []
try:
self.font[accentName]
except IndexError:
self.accentErrors.append('%s: %s does not exist.'%(glyphName, accentName))
existError = True
else:
for anchor in self.font[accentName].getAnchors():
accentAnchors.append(anchor.name)
if accentPosition not in baseAnchors:
self.accentErrors.append('%s: %s not in %s anchors.'%(glyphName, accentPosition, baseName))
anchorError = True
if ''.join(['_', accentPosition]) not in accentAnchors:
self.accentErrors.append('%s: %s not in %s anchors.'%(glyphName, ''.join(['_', accentPosition]), accentName))
anchorError = True
if not existError and not anchorError:
destination = self.font.compileGlyph(glyphName, baseName, self.glyphConstructions[stripedSuffixName][1:], adjustWidths)
if markGlyph:
destination.mark = accentColor
if doProgress:
bar.tick(tick)
tick = tick+1
if doProgress:
bar.close()
def printAccentErrors(self):
"""print errors encounted during buildAccents"""
if len(self.accentErrors) == 1:
print 'No accent errors encountered'
else:
for i in self.accentErrors:
print i

View file

@ -0,0 +1,85 @@
import re
featureRE = re.compile(
"^" # start of line
"\s*" #
"feature" # feature
"\s+" #
"(\w{4})" # four alphanumeric characters
"\s*" #
"\{" # {
, re.MULTILINE # run in multiline to preserve line seps
)
def splitFeaturesForFontLab(text):
"""
>>> result = splitFeaturesForFontLab(testText)
>>> result == expectedTestResult
True
"""
classes = ""
features = []
while text:
m = featureRE.search(text)
if m is None:
classes = text
text = ""
else:
start, end = m.span()
# if start is not zero, this is the first match
# and all previous lines are part of the "classes"
if start > 0:
assert not classes
classes = text[:start]
# extract the current feature
featureName = m.group(1)
featureText = text[start:end]
text = text[end:]
# grab all text before the next feature definition
# and add it to the current definition
if text:
m = featureRE.search(text)
if m is not None:
start, end = m.span()
featureText += text[:start]
text = text[start:]
else:
featureText += text
text = ""
# store the feature
features.append((featureName, featureText))
return classes, features
testText = """
@class1 = [a b c d];
feature liga {
sub f i by fi;
} liga;
@class2 = [x y z];
feature salt {
sub a by a.alt;
} salt; feature ss01 {sub x by x.alt} ss01;
feature ss02 {sub y by y.alt} ss02;
# feature calt {
# sub a b' by b.alt;
# } calt;
"""
expectedTestResult = (
"\n@class1 = [a b c d];\n",
[
("liga", "\nfeature liga {\n sub f i by fi;\n} liga;\n\n@class2 = [x y z];\n"),
("salt", "\nfeature salt {\n sub a by a.alt;\n} salt; feature ss01 {sub x by x.alt} ss01;\n"),
("ss02", "\nfeature ss02 {sub y by y.alt} ss02;\n\n# feature calt {\n# sub a b' by b.alt;\n# } calt;\n")
]
)
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,95 @@
"""Tool for exporting GLIFs from FontLab"""
import FL
import os
from robofab.interface.all.dialogs import ProgressBar
from robofab.glifLib import GlyphSet
from robofab.tools.glifImport import GlyphPlaceholder
from robofab.pens.flPen import drawFLGlyphOntoPointPen
def exportGlyph(glyphName, flGlyph, glyphSet):
"""Export a FontLab glyph."""
glyph = GlyphPlaceholder()
glyph.width = flGlyph.width
glyph.unicodes = flGlyph.unicodes
if flGlyph.note:
glyph.note = flGlyph.note
customdata = flGlyph.customdata
if customdata:
from cStringIO import StringIO
from robofab.plistlib import readPlist, Data
f = StringIO(customdata)
try:
glyph.lib = readPlist(f)
except: # XXX ugh, plistlib can raise lots of things
# Anyway, customdata does not contain valid plist data,
# but we don't need to toss it!
glyph.lib = {"org.robofab.fontlab.customdata": Data(customdata)}
def drawPoints(pen):
# whoohoo, nested scopes are cool.
drawFLGlyphOntoPointPen(flGlyph, pen)
glyphSet.writeGlyph(glyphName, glyph, drawPoints)
def exportGlyphs(font, glyphs=None, dest=None, doProgress=True, bar=None):
"""Export all glyphs in a FontLab font"""
if dest is None:
dir, base = os.path.split(font.file_name)
base = base.split(".")[0] + ".glyphs"
dest = os.path.join(dir, base)
if not os.path.exists(dest):
os.makedirs(dest)
glyphSet = GlyphSet(dest)
if glyphs is None:
indices = range(len(font))
else:
indices = []
for glyphName in glyphs:
indices.append(font.FindGlyph(glyphName))
barStart = 0
closeBar = False
if doProgress:
if not bar:
bar = ProgressBar("Exporting Glyphs", len(indices))
closeBar = True
else:
barStart = bar.getCurrentTick()
else:
bar = None
try:
done = {}
for i in range(len(indices)):
#if not (i % 10) and not bar.tick(i + barStart):
# raise KeyboardInterrupt
index = indices[i]
flGlyph = font[index]
if flGlyph is None:
continue
glyphName = flGlyph.name
if not glyphName:
print "can't dump glyph #%s, it has no glyph name" % i
else:
if glyphName in done:
n = 1
while ("%s#%s" % (glyphName, n)) in done:
n += 1
glyphName = "%s#%s" % (glyphName, n)
done[glyphName] = None
exportGlyph(glyphName, flGlyph, glyphSet)
if bar and not i % 10:
bar.tick(barStart + i)
# Write out contents.plist
glyphSet.writeContents()
except KeyboardInterrupt:
if bar:
bar.close()
bar = None
if bar and closeBar:
bar.close()

View file

@ -0,0 +1,74 @@
"""Tools for importing GLIFs into FontLab"""
import os
from FL import fl
from robofab.tools.toolsFL import NewGlyph, FontIndex
from robofab.pens.flPen import FLPointPen
from robofab.glifLib import GlyphSet
from robofab.interface.all.dialogs import ProgressBar, GetFolder
class GlyphPlaceholder:
pass
def importAllGlifFiles(font, dirName=None, doProgress=True, bar=None):
"""import all GLIFs into a FontLab font"""
if dirName is None:
if font.file_name:
dir, base = os.path.split(font.file_name)
base = base.split(".")[0] + ".glyphs"
dirName = os.path.join(dir, base)
else:
dirName = GetFolder("Please select a folder with .glif files")
glyphSet = GlyphSet(dirName)
glyphNames = glyphSet.keys()
glyphNames.sort()
barStart = 0
closeBar = False
if doProgress:
if not bar:
bar = ProgressBar("Importing Glyphs", len(glyphNames))
closeBar = True
else:
barStart = bar.getCurrentTick()
else:
bar = None
try:
for i in range(len(glyphNames)):
#if not (i % 10) and not bar.tick(barStart + i):
# raise KeyboardInterrupt
glyphName = glyphNames[i]
flGlyph = NewGlyph(font, glyphName, clear=True)
pen = FLPointPen(flGlyph)
glyph = GlyphPlaceholder()
glyphSet.readGlyph(glyphName, glyph, pen)
if hasattr(glyph, "width"):
flGlyph.width = int(round(glyph.width))
if hasattr(glyph, "unicodes"):
flGlyph.unicodes = glyph.unicodes
if hasattr(glyph, "note"):
flGlyph.note = glyph.note # XXX must encode
if hasattr(glyph, "lib"):
from cStringIO import StringIO
from robofab.plistlib import writePlist
lib = glyph.lib
if lib:
if len(lib) == 1 and "org.robofab.fontlab.customdata" in lib:
data = lib["org.robofab.fontlab.customdata"].data
else:
f = StringIO()
writePlist(glyph.lib, f)
data = f.getvalue()
flGlyph.customdata = data
# XXX the next bit is only correct when font is the current font :-(
fl.UpdateGlyph(font.FindGlyph(glyphName))
if bar and not i % 10:
bar.tick(barStart + i)
except KeyboardInterrupt:
if bar:
bar.close()
bar = None
fl.UpdateFont(FontIndex(font))
if bar and closeBar:
bar.close()

View file

@ -0,0 +1,565 @@
_glyphConstruction = """\
#
# RoboFab Glyph Construction Database
#
# format:
# Glyphname: BaseGlyph Accent.RelativePosition* Accent.RelativePosition*
# *RelativePosition can be top, bottom, left, right
#
# NOTE: this is not a comprehensive, or even accurate, glyph list.
# It was built by Python robots and, in many cases, by tired human hands.
# Please report any omissions, errors or praise to the local RoboFab authorities.
#
##: Uppercase
AEacute: AE acute.top
AEmacron: AE macron.top
Aacute: A acute.top
Abreve: A breve.top
Abreveacute: A breve.top acute.top
Abrevedotaccent: A breve.top dotaccent.bottom
Abrevegrave: A breve.top grave.top
Abrevetilde: A breve.top tilde.top
Acaron: A caron.top
Acircumflex: A circumflex.top
Acircumflexacute: A circumflex.top acute.top
Acircumflexdotaccent: A circumflex.top dotaccent.bottom
Acircumflexgrave: A circumflex.top grave.top
Acircumflextilde: A circumflex.top tilde.top
Adblgrave: A dblgrave.top
Adieresis: A dieresis.top
Adieresismacron: A dieresis.top macron.top
Adotaccent: A dotaccent.top
Adotaccentmacron: A dotaccent.top macron.top
Agrave: A grave.top
Amacron: A macron.top
Aogonek: A ogonek.bottom
Aring: A ring.top
Aringacute: A ring.top acute.top
Atilde: A tilde.top
Bdotaccent: B dotaccent.top
Cacute: C acute.top
Ccaron: C caron.top
Ccedilla: C cedilla.bottom
Ccedillaacute: C cedilla.bottom acute.top
Ccircumflex: C circumflex.top
Cdotaccent: C dotaccent.top
Dcaron: D caron.top
Dcedilla: D cedilla.bottom
Ddotaccent: D dotaccent.top
Eacute: E acute.top
Ebreve: E breve.top
Ecaron: E caron.top
Ecedilla: E cedilla.bottom
Ecedillabreve: E cedilla.bottom breve.top
Ecircumflex: E circumflex.top
Ecircumflexacute: E circumflex.top acute.top
Ecircumflexdotaccent: E circumflex.top dotaccent.bottom
Ecircumflexgrave: E circumflex.top grave.top
Ecircumflextilde: E circumflex.top tilde.top
Edblgrave: E dblgrave.top
Edieresis: E dieresis.top
Edotaccent: E dotaccent.top
Egrave: E grave.top
Emacron: E macron.top
Emacronacute: E macron.top acute.top
Emacrongrave: E macron.top grave.top
Eogonek: E ogonek.bottom
Etilde: E tilde.top
Fdotaccent: F dotaccent.top
Gacute: G acute.top
Gbreve: G breve.top
Gcaron: G caron.top
Gcedilla: G cedilla.bottom
Gcircumflex: G circumflex.top
Gcommaaccent: G commaaccent.bottom
Gdotaccent: G dotaccent.top
Gmacron: G macron.top
Hcaron: H caron.top
Hcedilla: H cedilla.top
Hcircumflex: H circumflex.top
Hdieresis: H dieresis.top
Hdotaccent: H dotaccent.top
Iacute: I acute.top
Ibreve: I breve.top
Icaron: I caron.top
Icircumflex: I circumflex.top
Idblgrave: I dblgrave.top
Idieresis: I dieresis.top
Idieresisacute: I dieresis.top acute.top
Idotaccent: I dotaccent.top
Igrave: I grave.top
Imacron: I macron.top
Iogonek: I ogonek.bottom
Itilde: I tilde.top
Jcircumflex: J circumflex.top
Kacute: K acute.top
Kcaron: K caron.top
Kcedilla: K cedilla.bottom
Kcommaaccent: K commaaccent.bottom
Lacute: L acute.top
Lcaron: L commaaccent.right
Lcedilla: L cedilla.bottom
Lcommaaccent: L commaaccent.bottom
Ldot: L dot.right
Ldotaccent: L dotaccent.bottom
Ldotaccentmacron: L dotaccent.bottom macron.top
Macute: M acute.top
Mdotaccent: M dotaccent.top
Nacute: N acute.top
Ncaron: N caron.top
Ncedilla: N cedilla.bottom
Ncommaaccent: N commaaccent.bottom
Ndotaccent: N dotaccent.top
Ngrave: N grave.top
Ntilde: N tilde.top
Oacute: O acute.top
Obreve: O breve.top
Ocaron: O caron.top
Ocircumflex: O circumflex.top
Ocircumflexacute: O circumflex.top acute.top
Ocircumflexdotaccent: O circumflex.top dotaccent.bottom
Ocircumflexgrave: O circumflex.top grave.top
Ocircumflextilde: O circumflex.top tilde.top
Odblgrave: O dblgrave.top
Odieresis: O dieresis.top
Odieresismacron: O dieresis.top macron.top
Ograve: O grave.top
Ohungarumlaut: O hungarumlaut.top
Omacron: O macron.top
Omacronacute: O macron.top acute.top
Omacrongrave: O macron.top grave.top
Oogonek: O ogonek.bottom
Oogonekmacron: O ogonek.bottom macron.top
Oslashacute: Oslash acute.top
Otilde: O tilde.top
Otildeacute: O tilde.top acute.top
Otildedieresis: O tilde.top dieresis.top
Otildemacron: O tilde.top macron.top
Pacute: P acute.top
Pdotaccent: P dotaccent.top
Racute: R acute.top
Rcaron: R caron.top
Rcedilla: R cedilla.bottom
Rcommaaccent: R commaaccent.bottom
Rdblgrave: R dblgrave.top
Rdotaccent: R dotaccent.top
Rdotaccentmacron: R dotaccent.top macron.top
Sacute: S acute.top
Sacutedotaccent: S acute.top dotaccent.top
Scaron: S caron.top
Scarondotaccent: S caron.top dotaccent.top
Scedilla: S cedilla.bottom
Scircumflex: S circumflex.top
Scommaaccent: S commaaccent.bottom
Sdotaccent: S dotaccent.top
Tcaron: T caron.top
Tcedilla: T cedilla.bottom
Tcommaaccent: T commaaccent.bottom
Tdotaccent: T dotaccent.top
Uacute: U acute.top
Ubreve: U breve.top
Ucaron: U caron.top
Ucircumflex: U circumflex.top
Udblgrave: U dblgrave.top
Udieresis: U dieresis.top
Udieresisacute: U dieresis.top acute.top
Udieresiscaron: U dieresis.top caron.top
Udieresisgrave: U dieresis.top grave.top
Udieresismacron: U dieresis.top macron.top
Ugrave: U grave.top
Uhungarumlaut: U hungarumlaut.top
Umacron: U macron.top
Umacrondieresis: U macron.top dieresis.top
Uogonek: U ogonek.bottom
Uring: U ring.top
Utilde: U tilde.top
Utildeacute: U tilde.top acute.top
Vtilde: V tilde.top
Wacute: W acute.top
Wcircumflex: W circumflex.top
Wdieresis: W dieresis.top
Wdotaccent: W dotaccent.top
Wgrave: W grave.top
Xdieresis: X dieresis.top
Xdotaccent: X dotaccent.top
Yacute: Y acute.top
Ycircumflex: Y circumflex.top
Ydieresis: Y dieresis.top
Ydotaccent: Y dotaccent.top
Ygrave: Y grave.top
Ytilde: Y tilde.top
Zacute: Z acute.top
Zcaron: Z caron.top
Zcircumflex: Z circumflex.top
Zdotaccent: Z dotaccent.top
##: Lowercase
aacute: a acute.top
abreve: a breve.top
abreveacute: a breve.top acute.top
abrevedotaccent: a breve.top dotaccent.bottom
abrevegrave: a breve.top grave.top
abrevetilde: a breve.top tilde.top
acaron: a caron.top
acircumflex: a circumflex.top
acircumflexacute: a circumflex.top acute.top
acircumflexdotaccent: a circumflex.top dotaccent.bottom
acircumflexgrave: a circumflex.top grave.top
acircumflextilde: a circumflex.top tilde.top
adblgrave: a dblgrave.top
adieresis: a dieresis.top
adieresismacron: a dieresis.top macron.top
adotaccent: a dotaccent.top
adotaccentmacron: a dotaccent.top macron.top
aeacute: ae acute.top
aemacron: ae macron.top
agrave: a grave.top
amacron: a macron.top
aogonek: a ogonek.bottom
aring: a ring.top
aringacute: a ring.top acute.top
atilde: a tilde.top
bdotaccent: b dotaccent.top
cacute: c acute.top
ccaron: c caron.top
ccedilla: c cedilla.bottom
ccedillaacute: c cedilla.bottom acute.top
ccircumflex: c circumflex.top
cdotaccent: c dotaccent.top
dcaron: d commaaccent.right
dcedilla: d cedilla.bottom
ddotaccent: d dotaccent.top
dmacron: d macron.top
eacute: e acute.top
ebreve: e breve.top
ecaron: e caron.top
ecedilla: e cedilla.bottom
ecedillabreve: e cedilla.bottom breve.top
ecircumflex: e circumflex.top
ecircumflexacute: e circumflex.top acute.top
ecircumflexdotaccent: e circumflex.top dotaccent.bottom
ecircumflexgrave: e circumflex.top grave.top
ecircumflextilde: e circumflex.top tilde.top
edblgrave: e dblgrave.top
edieresis: e dieresis.top
edotaccent: e dotaccent.top
egrave: e grave.top
emacron: e macron.top
emacronacute: e macron.top acute.top
emacrongrave: e macron.top grave.top
eogonek: e ogonek.bottom
etilde: e tilde.top
fdotaccent: f dotaccent.top
gacute: g acute.top
gbreve: g breve.top
gcaron: g caron.top
gcedilla: g cedilla.top
gcircumflex: g circumflex.top
gcommaaccent: g commaaccent.top
gdotaccent: g dotaccent.top
gmacron: g macron.top
hcaron: h caron.top
hcedilla: h cedilla.bottom
hcircumflex: h circumflex.top
hdieresis: h dieresis.top
hdotaccent: h dotaccent.top
iacute: dotlessi acute.top
ibreve: dotlessi breve.top
icaron: dotlessi caron.top
icircumflex: dotlessi circumflex.top
idblgrave: dotlessi dblgrave.top
idieresis: dotlessi dieresis.top
idieresisacute: dotlessi dieresis.top acute.top
igrave: dotlessi grave.top
imacron: dotlessi macron.top
iogonek: i ogonek.bottom
itilde: dotlessi tilde.top
jcaron: dotlessj caron.top
jcircumflex: dotlessj circumflex.top
jacute: dotlessj acute.top
kacute: k acute.top
kcaron: k caron.top
kcedilla: k cedilla.bottom
kcommaaccent: k commaaccent.bottom
lacute: l acute.top
lcaron: l commaaccent.right
lcedilla: l cedilla.bottom
lcommaaccent: l commaaccent.bottom
ldot: l dot.right
ldotaccent: l dotaccent.bottom
ldotaccentmacron: l dotaccent.bottom macron.top
macute: m acute.top
mdotaccent: m dotaccent.top
nacute: n acute.top
ncaron: n caron.top
ncedilla: n cedilla.bottom
ncommaaccent: n commaaccent.bottom
ndotaccent: n dotaccent.top
ngrave: n grave.top
ntilde: n tilde.top
oacute: o acute.top
obreve: o breve.top
ocaron: o caron.top
ocircumflex: o circumflex.top
ocircumflexacute: o circumflex.top acute.top
ocircumflexdotaccent: o circumflex.top dotaccent.bottom
ocircumflexgrave: o circumflex.top grave.top
ocircumflextilde: o circumflex.top tilde.top
odblgrave: o dblgrave.top
odieresis: o dieresis.top
odieresismacron: o dieresis.top macron.top
ograve: o grave.top
ohungarumlaut: o hungarumlaut.top
omacron: o macron.top
omacronacute: o macron.top acute.top
omacrongrave: o macron.top grave.top
oogonek: o ogonek.bottom
oogonekmacron: o ogonek.bottom macron.top
oslashacute: oslash acute.top
otilde: o tilde.top
otildeacute: o tilde.top acute.top
otildedieresis: o tilde.top dieresis.top
otildemacron: o tilde.top macron.top
pacute: p acute.top
pdotaccent: p dotaccent.top
racute: r acute.top
rcaron: r caron.top
rcedilla: r cedilla.bottom
rcommaaccent: r commaaccent.bottom
rdblgrave: r dblgrave.top
rdotaccent: r dotaccent.top
rdotaccentmacron: r dotaccent.top macron.top
sacute: s acute.top
sacutedotaccent: s acute.top dotaccent.top
scaron: s caron.top
scarondotaccent: s caron.top dotaccent.top
scedilla: s cedilla.bottom
scircumflex: s circumflex.top
scommaaccent: s commaaccent.bottom
sdotaccent: s dotaccent.top
tcaron: t commaaccent.right
tcedilla: t cedilla.bottom
tcommaaccent: t commaaccent.bottom
tdieresis: t dieresis.top
tdotaccent: t dotaccent.top
uacute: u acute.top
ubreve: u breve.top
ucaron: u caron.top
ucircumflex: u circumflex.top
udblgrave: u dblgrave.top
udieresis: u dieresis.top
udieresisacute: u dieresis.top acute.top
udieresiscaron: u dieresis.top caron.top
udieresisgrave: u dieresis.top grave.top
udieresismacron: u dieresis.top macron.top
ugrave: u grave.top
uhungarumlaut: u hungarumlaut.top
umacron: u macron.top
umacrondieresis: u macron.top dieresis.top
uogonek: u ogonek.bottom
uring: u ring.top
utilde: u tilde.top
utildeacute: u tilde.top acute.top
vtilde: v tilde.top
wacute: w acute.top
wcircumflex: w circumflex.top
wdieresis: w dieresis.top
wdotaccent: w dotaccent.top
wgrave: w grave.top
wring: w ring.top
xdieresis: x dieresis.top
xdotaccent: x dotaccent.top
yacute: y acute.top
ycircumflex: y circumflex.top
ydieresis: y dieresis.top
ydotaccent: y dotaccent.top
ygrave: y grave.top
yring: y ring.top
ytilde: y tilde.top
zacute: z acute.top
zcaron: z caron.top
zcircumflex: z circumflex.top
zdotaccent: z dotaccent.top
##: Small: Caps
AEacute.sc: AE.sc acute.top
AEmacron.sc: AE.sc macron.top
Aacute.sc: A.sc acute.top
Abreve.sc: A.sc breve.top
Abreveacute.sc: A.sc breve.top acute.top
Abrevedotaccent.sc: A.sc breve.top dotaccent.bottom
Abrevegrave.sc: A.sc breve.top grave.top
Abrevetilde.sc: A.sc breve.top tilde.top
Acaron.sc: A.sc caron.top
Acircumflex.sc: A.sc circumflex.top
Acircumflexacute.sc: A.sc circumflex.top acute.top
Acircumflexdotaccent.sc: A.sc circumflex.top dotaccent.bottom
Acircumflexgrave.sc: A.sc circumflex.top grave.top
Acircumflextilde.sc: A.sc circumflex.top tilde.top
Adblgrave.sc: A.sc dblgrave.top
Adieresis.sc: A.sc dieresis.top
Adieresismacron.sc: A.sc dieresis.top macron.top
Adotaccent.sc: A.sc dotaccent.top
Adotaccentmacron.sc: A.sc dotaccent.top macron.top
Agrave.sc: A.sc grave.top
Amacron.sc: A.sc macron.top
Aogonek.sc: A.sc ogonek.bottom
Aring.sc: A.sc ring.top
Aringacute.sc: A.sc ring.top acute.top
Atilde.sc: A.sc tilde.top
Bdotaccent.sc: B.sc dotaccent.top
Cacute.sc: C.sc acute.top
Ccaron.sc: C.sc caron.top
Ccedilla.sc: C.sc cedilla.bottom
Ccedillaacute.sc: C.sc cedilla.bottom acute.top
Ccircumflex.sc: C.sc circumflex.top
Cdotaccent.sc: C.sc dotaccent.top
Dcaron.sc: D.sc caron.top
Dcedilla.sc: D.sc cedilla.bottom
Ddotaccent.sc: D.sc dotaccent.top
Eacute.sc: E.sc acute.top
Ebreve.sc: E.sc breve.top
Ecaron.sc: E.sc caron.top
Ecedilla.sc: E.sc cedilla.bottom
Ecedillabreve.sc: E.sc cedilla.bottom breve.top
Ecircumflex.sc: E.sc circumflex.top
Ecircumflexacute.sc: E.sc circumflex.top acute.top
Ecircumflexdotaccent.sc: E.sc circumflex.top dotaccent.bottom
Ecircumflexgrave.sc: E.sc circumflex.top grave.top
Ecircumflextilde.sc: E.sc circumflex.top tilde.top
Edblgrave.sc: E.sc dblgrave.top
Edieresis.sc: E.sc dieresis.top
Edotaccent.sc: E.sc dotaccent.top
Egrave.sc: E.sc grave.top
Emacron.sc: E.sc macron.top
Emacronacute.sc: E.sc macron.top acute.top
Emacrongrave.sc: E.sc macron.top grave.top
Eogonek.sc: E.sc ogonek.bottom
Etilde.sc: E.sc tilde.top
Fdotaccent.sc: F.sc dotaccent.top
Gacute.sc: G.sc acute.top
Gbreve.sc: G.sc breve.top
Gcaron.sc: G.sc caron.top
Gcedilla.sc: G.sc cedilla.bottom
Gcircumflex.sc: G.sc circumflex.top
Gcommaaccent.sc: G.sc commaaccent.bottom
Gdotaccent.sc: G.sc dotaccent.top
Gmacron.sc: G.sc macron.top
Hcaron.sc: H.sc caron.top
Hcedilla.sc: H.sc cedilla.top
Hcircumflex.sc: H.sc circumflex.top
Hdieresis.sc: H.sc dieresis.top
Hdotaccent.sc: H.sc dotaccent.top
Iacute.sc: I.sc acute.top
Ibreve.sc: I.sc breve.top
Icaron.sc: I.sc caron.top
Icircumflex.sc: I.sc circumflex.top
Idblgrave.sc: I.sc dblgrave.top
Idieresis.sc: I.sc dieresis.top
Idieresisacute.sc: I.sc dieresis.top acute.top
Idotaccent.sc: I.sc dotaccent.top
Igrave.sc: I.sc grave.top
Imacron.sc: I.sc macron.top
Iogonek.sc: I.sc ogonek.bottom
Itilde.sc: I.sc tilde.top
Jcircumflex.sc: J.sc circumflex.top
Kacute.sc: K.sc acute.top
Kcaron.sc: K.sc caron.top
Kcedilla.sc: K.sc cedilla.bottom
Kcommaaccent.sc: K.sc commaaccent.bottom
Lacute.sc: L.sc acute.top
Lcaron.sc: L.sc commaaccent.right
Lcedilla.sc: L.sc cedilla.bottom
Lcommaaccent.sc: L.sc commaaccent.bottom
Ldot.sc: L.sc dot.right
Ldotaccent.sc: L.sc dotaccent.bottom
Ldotaccentmacron.sc: L.sc dotaccent.bottom macron.top
Macute.sc: M.sc acute.top
Mdotaccent.sc: M.sc dotaccent.top
Nacute.sc: N.sc acute.top
Ncaron.sc: N.sc caron.top
Ncedilla.sc: N.sc cedilla.bottom
Ncommaaccent.sc: N.sc commaaccent.bottom
Ndotaccent.sc: N.sc dotaccent.top
Ngrave.sc: N.sc grave.top
Ntilde.sc: N.sc tilde.top
Oacute.sc: O.sc acute.top
Obreve.sc: O.sc breve.top
Ocaron.sc: O.sc caron.top
Ocircumflex.sc: O.sc circumflex.top
Ocircumflexacute.sc: O.sc circumflex.top acute.top
Ocircumflexdotaccent.sc: O.sc circumflex.top dotaccent.bottom
Ocircumflexgrave.sc: O.sc circumflex.top grave.top
Ocircumflextilde.sc: O.sc circumflex.top tilde.top
Odblgrave.sc: O.sc dblgrave.top
Odieresis.sc: O.sc dieresis.top
Odieresismacron.sc: O.sc dieresis.top macron.top
Ograve.sc: O.sc grave.top
Ohungarumlaut.sc: O.sc hungarumlaut.top
Omacron.sc: O.sc macron.top
Omacronacute.sc: O.sc macron.top acute.top
Omacrongrave.sc: O.sc macron.top grave.top
Oogonek.sc: O.sc ogonek.bottom
Oogonekmacron.sc: O.sc ogonek.bottom macron.top
Oslashacute.sc: Oslash.sc acute.top
Otilde.sc: O.sc tilde.top
Otildeacute.sc: O.sc tilde.top acute.top
Otildedieresis.sc: O.sc tilde.top dieresis.top
Otildemacron.sc: O.sc tilde.top macron.top
Pacute.sc: P.sc acute.top
Pdotaccent.sc: P.sc dotaccent.top
Racute.sc: R.sc acute.top
Rcaron.sc: R.sc caron.top
Rcedilla.sc: R.sc cedilla.bottom
Rcommaaccent.sc: R.sc commaaccent.bottom
Rdblgrave.sc: R.sc dblgrave.top
Rdotaccent.sc: R.sc dotaccent.top
Rdotaccentmacron.sc: R.sc dotaccent.top macron.top
Sacute.sc: S.sc acute.top
Sacutedotaccent.sc: S.sc acute.top dotaccent.top
Scaron.sc: S.sc caron.top
Scarondotaccent.sc: S.sc caron.top dotaccent.top
Scedilla.sc: S.sc cedilla.bottom
Scircumflex.sc: S.sc circumflex.top
Scommaaccent.sc: S.sc commaaccent.bottom
Sdotaccent.sc: S.sc dotaccent.top
Tcaron.sc: T.sc caron.top
Tcedilla.sc: T.sc cedilla.bottom
Tcommaaccent.sc: T.sc commaaccent.bottom
Tdotaccent.sc: T.sc dotaccent.top
Uacute.sc: U.sc acute.top
Ubreve.sc: U.sc breve.top
Ucaron.sc: U.sc caron.top
Ucircumflex.sc: U.sc circumflex.top
Udblgrave.sc: U.sc dblgrave.top
Udieresis.sc: U.sc dieresis.top
Udieresisacute.sc: U.sc dieresis.top acute.top
Udieresiscaron.sc: U.sc dieresis.top caron.top
Udieresisgrave.sc: U.sc dieresis.top grave.top
Udieresismacron.sc: U.sc dieresis.top macron.top
Ugrave.sc: U.sc grave.top
Uhungarumlaut.sc: U.sc hungarumlaut.top
Umacron.sc: U.sc macron.top
Umacrondieresis.sc: U.sc macron.top dieresis.top
Uogonek.sc: U.sc ogonek.bottom
Uring.sc: U.sc ring.top
Utilde.sc: U.sc tilde.top
Utildeacute.sc: U.sc tilde.top acute.top
Vtilde.sc: V.sc tilde.top
Wacute.sc: W.sc acute.top
Wcircumflex.sc: W.sc circumflex.top
Wdieresis.sc: W.sc dieresis.top
Wdotaccent.sc: W.sc dotaccent.top
Wgrave.sc: W.sc grave.top
Xdieresis.sc: X.sc dieresis.top
Xdotaccent.sc: X.sc dotaccent.top
Yacute.sc: Y.sc acute.top
Ycircumflex.sc: Y.sc circumflex.top
Ydieresis.sc: Y.sc dieresis.top
Ydotaccent.sc: Y.sc dotaccent.top
Ygrave.sc: Y.sc grave.top
Ytilde.sc: Y.sc tilde.top
Zacute.sc: Z.sc acute.top
Zcaron.sc: Z.sc caron.top
Zcircumflex.sc: Z.sc circumflex.top
Zdotaccent.sc: Z.sc dotaccent.top
"""

View file

@ -0,0 +1,41 @@
"""A separate module for glyphname to filename functions.
glyphNameToShortFileName() generates a non-clashing filename for systems with
filename-length limitations.
"""
MAXLEN = 31
def glyphNameToShortFileName(glyphName, glyphSet):
"""Alternative glyphname to filename function.
Features a garuanteed maximum filename for really long glyphnames, and clash testing.
- all non-ascii characters are converted to "_" (underscore), including "."
- all glyphnames which are too long are truncated and a hash is added at the end
- the hash is generated from the whole glyphname
- finally, the candidate glyphname is checked against the contents.plist
and a incrementing number is added at the end if there is a clash.
"""
import binascii, struct, string
ext = ".glif"
ok = string.ascii_letters + string.digits + " _"
h = binascii.hexlify(struct.pack(">l", binascii.crc32(glyphName)))
n = ''
for c in glyphName:
if c in ok:
if c != c.lower():
n += c + "_"
else:
n += c
else:
n += "_"
if len(n + ext) < MAXLEN:
return n + ext
count = 0
candidate = n[:MAXLEN - len(h + ext)] + h + ext
if glyphSet is not None:
names = glyphSet.getReverseContents()
while candidate.lower() in names:
candidate = n[:MAXLEN - len(h + ext + str(count))] + h + str(count) + ext
count += 1
return candidate

View file

@ -0,0 +1,55 @@
"""Simple and ugly way to print some attributes and properties of an object to stdout.
FontLab doesn't have an object browser and sometimes we do need to look inside"""
from pprint import pprint
def classname(object, modname):
"""Get a class name and qualify it with a module name if necessary."""
name = object.__name__
if object.__module__ != modname:
name = object.__module__ + '.' + name
return name
def _objectDumper(object, indent=0, private=False):
"""Collect a dict with the contents of the __dict__ as a quick means of peeking inside
an instance. Some RoboFab locations do not support PyBrowser and still need debugging."""
data = {}
data['__class__'] = "%s at %d"%(classname(object.__class__, object.__module__), id(object))
for k in object.__class__.__dict__.keys():
if private and k[0] == "_":
continue
x = object.__class__.__dict__[k]
if hasattr(x, "fget"): #other means of recognising a property?
try:
try:
value = _objectDumper(x.fget(self), 1)
except:
value = x.fget(self)
data[k] = "[property, %s] %s"%(type(x.fget(self)).__name__, value)
except:
data[k] = "[property] (Error getting property value)"
for k in object.__dict__.keys():
if private and k[0] == "_":
continue
try:
data[k] = "[attribute, %s] %s"%(type(object.__dict__[k]).__name__, `object.__dict__[k]`)
except:
data[k] = "[attribute] (Error getting attribute value)"
return data
def flattenDict(dict, indent=0):
t = []
k = dict.keys()
k.sort()
print
print '---RoboFab Object Dump---'
for key in k:
value = dict[key]
t.append(indent*"\t"+"%s: %s"%(key, value))
t.append('')
return "\r".join(t)
def dumpObject(object, private=False):
print pprint(_objectDumper(object, private=private))

View file

@ -0,0 +1,190 @@
"""Simple module to write features to font"""
import string
from types import StringType, ListType, TupleType
from robofab.world import world
if world.inFontLab:
from FL import *
from fl_cmd import *
from robofab.tools.toolsFL import FontIndex
#feat = []
#feat.append('feature smcp {')
#feat.append('\tlookup SMALLCAPS {')
#feat.append('\t\tsub @LETTERS_LC by @LETTERS_LC;')
#feat.append('\t} SMALLCAPS;')
#feat.append('} smcp;')
class FeatureWriter:
"""Make properly formatted feature code"""
def __init__(self, type):
self.type = type
self.data = []
def add(self, src, dst):
"""Add a substitution: change src to dst."""
self.data.append((src, dst))
def write(self, group=0):
"""Write the whole thing to string"""
t = []
if len(self.data) == 0:
return None
t.append('feature %s {' % self.type)
for src, dst in self.data:
if isinstance(src, (list, tuple)):
if group:
src = "[%s]" % string.join(src, ' ')
else:
src = string.join(src, ' ')
if isinstance(dst, (list, tuple)):
if group:
dst = "[%s]" % string.join(dst, ' ')
else:
dst = string.join(dst, ' ')
src = string.strip(src)
dst = string.strip(dst)
t.append("\tsub %s by %s;" % (src, dst))
t.append('}%s;' % self.type)
return string.join(t, '\n')
class GlyphName:
"""Simple class that splits a glyphname in handy parts,
access the parts as attributes of the name."""
def __init__(self, name):
self.suffix = []
self.ligs = []
self.name = self.base = name
if '.' in name:
self.bits = name.split('.')
self.base = self.bits[0]
self.suffix = self.bits[1:]
if '_' in name:
self.ligs = self.base.split('_')
def GetAlternates(font, flavor="alt", match=0):
"""Sort the glyphs of this font by the parts of the name.
flavor is the bit to look for, i.e. 'alt' in a.alt
match = 1 if you want a exact match: alt1 != alt
match = 0 if the flavor is a partial match: alt == alt1
"""
names = {}
for c in font.glyphs:
name = GlyphName(c.name)
if not names.has_key(name.base):
names[name.base] = []
if match:
# only include if there is an exact match
if flavor in name.suffix:
names[name.base].append(c.name)
else:
# include if there is a partial match
for a in name.suffix:
if a.find(flavor) != -1:
names[name.base].append(c.name)
return names
# XXX there should be a more generic glyph finder.
def MakeCapsFeature(font):
"""Build a feature for smallcaps based on .sc glyphnames"""
names = GetAlternates(font, 'sc', match=1)
fw = FeatureWriter('smcp')
k = names.keys()
k.sort()
for p in k:
if names[p]:
fw.add(p, names[p])
feat = fw.write()
if feat:
font.features.append(Feature('smcp', feat))
return feat
def MakeAlternatesFeature(font):
"""Build a aalt feature based on glyphnames"""
names = GetAlternates(font, 'alt', match=0)
fw = FeatureWriter('aalt')
k = names.keys()
k.sort()
for p in k:
if names[p]:
fw.add(p, names[p])
feat = fw.write(group=1)
if feat:
font.features.append(Feature('aalt', feat))
return feat
def MakeSwashFeature(font):
"""Build a swash feature based on glyphnames"""
names = GetAlternates(font, 'swash', match=0)
fw = FeatureWriter('swsh')
k=names.keys()
k.sort()
for p in k:
if names[p]:
l=names[p]
l.sort()
fw.add(p, l[0])
feat=fw.write()
if feat:
font.features.append(Feature('swsh', feat))
return feat
def MakeLigaturesFeature(font):
"""Build a liga feature based on glyphnames"""
from robofab.gString import ligatures
ligCountDict = {}
for glyph in font.glyphs:
if glyph.name in ligatures:
if len(glyph.name) not in ligCountDict.keys():
ligCountDict[len(glyph.name)] = [glyph.name]
else:
ligCountDict[len(glyph.name)].append(glyph.name)
elif glyph.name.find('_') != -1:
usCounter=1
for i in glyph.name:
if i =='_':
usCounter=usCounter+1
if usCounter not in ligCountDict.keys():
ligCountDict[usCounter] = [glyph.name]
else:
ligCountDict[usCounter].append(glyph.name)
ligCount=ligCountDict.keys()
ligCount.sort()
foundLigs=[]
for i in ligCount:
l = ligCountDict[i]
l.sort()
foundLigs=foundLigs+l
fw=FeatureWriter('liga')
for i in foundLigs:
if i.find('_') != -1:
sub=i.split('_')
else:
sub=[]
for c in i:
sub.append(c)
fw.add(sub, i)
feat=fw.write()
if feat:
font.features.append(Feature('liga', feat))
return feat
if __name__ == "__main__":
fw = FeatureWriter('liga')
fw.add(['f', 'f', 'i'], ['f_f_i'])
fw.add('f f ', 'f_f')
fw.add(['f', 'i'], 'f_i')
print fw.write()

119
misc/pylib/robofab/tools/proof.py Executable file
View file

@ -0,0 +1,119 @@
"""This is the place for stuff that makes proofs and test text settings etc"""
import string
idHeader = """<ASCII-MAC>
<Version:2.000000><FeatureSet:InDesign-Roman><ColorTable:=<Black:COLOR:CMYK:Process:0.000000,0.000000,0.000000,1.000000>>"""
idColor = """<cColor:COLOR\:%(model)s\:Process\:%(c)f\,%(m)f\,%(y)f\,%(k)f>"""
idParaStyle = """<ParaStyle:><cTypeface:%(weight)s><cSize:%(size)f><cLeading:%(leading)f><cFont:%(family)s>"""
idGlyphStyle = """<cTypeface:%(weight)s><cSize:%(size)f><cLeading:%(leading)f><cFont:%(family)s>"""
seperator = ''
autoLinespaceFactor = 1.2
class IDTaggedText:
"""Export a text as a XML tagged text file for InDesign (2.0?).
The tags can contain information about
- family: font family i.e. "Times"
- weight: font weight "Bold"
- size: typesize in points
- leading: leading in points
- color: a CMYK color, as a 4 tuple of floats between 0 and 1
- insert special glyphs based on glyphindex
(which is why it only makes sense if you use this in FontLab,
otherwise there is no other way to get the indices)
"""
def __init__(self, family, weight, size=36, leading=None):
self.family = family
self.weight = weight
self.size = size
if not leading:
self.leading = autoLinespaceFactor*size
self.text = []
self.data = []
self.addHeader()
def add(self, text):
"""Method to add text to the file."""
t = self.charToGlyph(text)
self.data.append(t)
def charToGlyph(self, text):
return text
def addHeader(self):
"""Add the standard header."""
# set colors too?
self.data.append(idHeader)
def replace(self, old, new):
"""Replace occurances of 'old' with 'new' in all content."""
d = []
for i in self.data:
d.append(i.replace(old, new))
self.data = d
def save(self, path):
"""Save the tagged text here."""
f = open(path, 'w')
f.write(string.join(self.data, seperator))
f.close()
def addGlyph(self, index):
"""Add a special glyph, index is the glyphIndex in an OpenType font."""
self.addStyle()
self.data.append("<cSpecialGlyph:%d><0xFFFD>"%index)
def addStyle(self, family=None, weight=None, size=None, leading=None, color=None):
"""Set the paragraph style for the following text."""
if not family:
family = self.family
if not weight:
weight = self.weight
if not size:
size = self.size
if not leading:
leading = autoLinespaceFactor*self.size
self.data.append(idGlyphStyle%({'weight': weight, 'size': size, 'family': family, 'leading':leading}))
if color:
self.data.append(idColor%({'model': 'CMYK', 'c': color[0], 'm': color[1], 'y': color[2], 'k': color[3]}))
if __name__ == "__main__":
from random import randint
id = IDTaggedText("Minion", "Regular", size=40, leading=50)
id.addStyle(color=(0,0,0,1))
id.add("Hello")
id.addStyle(weight="Bold", color=(0,0.5,1,0))
id.add(" Everybody")
id.addStyle(weight="Regular", size=100, color=(0,1,1,0))
id.addGlyph(102)
id.addGlyph(202)
from robofab.interface.all.dialogs import PutFile
path = PutFile("Save the tagged file:", "TaggedText.txt")
if path:
id.save(path)
# then: open a document in Adobe InDesign
# select "Place" (cmd-D on Mac)
# select the text file you just generated
# place the text
#

View file

@ -0,0 +1,175 @@
"""Remote control for MacOS FontLab.
initFontLabRemote() registers a callback for appleevents and
runFontLabRemote() sends the code from a different application,
such as a Mac Python IDE or Python interpreter.
"""
from robofab.world import world
if world.inFontLab and world.mac is not None:
from Carbon import AE as _AE
else:
import sys
from aetools import TalkTo
class FontLab(TalkTo):
pass
__all__ = ['initFontLabRemote', 'runFontLabRemote']
def _executePython(theAppleEvent, theReply):
import aetools
import cStringIO
import traceback
import sys
parms, attrs = aetools.unpackevent(theAppleEvent)
source = parms.get("----")
if source is None:
return
stdout = cStringIO.StringIO()
#print "<executing remote command>"
save = sys.stdout, sys.stderr
sys.stdout = sys.stderr = stdout
namespace = {}
try:
try:
exec source in namespace
except:
traceback.print_exc()
finally:
sys.stdout, sys.stderr = save
output = stdout.getvalue()
aetools.packevent(theReply, {"----": output})
_imported = False
def initFontLabRemote():
"""Call this in FontLab at startup of the application to switch on the remote."""
print "FontLabRemote is on."
_AE.AEInstallEventHandler("Rfab", "exec", _executePython)
if world.inFontLab and world.mac is not None:
initFontLabRemote()
def runFontLabRemote(code):
"""Call this in the MacOS Python IDE to make FontLab execute the code."""
fl = FontLab("FLab", start=1)
ae, parms, attrs = fl.send("Rfab", "exec", {"----": code})
output = parms.get("----")
return output
# GlyphTransmit
# Convert a glyph to a string using digestPen, transmit string, unpack string with pointpen.
#
def Glyph2String(glyph):
from robofab.pens.digestPen import DigestPointPen
import pickle
p = DigestPointPen(glyph)
glyph.drawPoints(p)
info = {}
info['name'] = glyph.name
info['width'] = glyph.width
info['points'] = p.getDigest()
return str(pickle.dumps(info))
def String2Glyph(gString, penClass, font):
import pickle
if gString is None:
return None
info = pickle.loads(gString)
name = info['name']
if not name in font.keys():
glyph = font.newGlyph(name)
else:
glyph = font[name]
pen = penClass(glyph)
for p in info['points']:
if p == "beginPath":
pen.beginPath()
elif p == "endPath":
pen.endPath()
else:
pt, type = p
pen.addPoint(pt, type)
glyph.width = info['width']
glyph.update()
return glyph
_makeFLGlyph = """
from robofab.world import CurrentFont
from robofab.tools.remote import receiveGlyph
code = '''%s'''
receiveGlyph(code, CurrentFont())
"""
def transmitGlyph(glyph):
from robofab.world import world
if world.inFontLab and world.mac is not None:
# we're in fontlab, on a mac
print Glyph2String(glyph)
pass
else:
remoteProgram = _makeFLGlyph%Glyph2String(glyph)
print "remoteProgram", remoteProgram
return runFontLabRemote(remoteProgram)
def receiveGlyph(glyphString, font=None):
from robofab.world import world
if world.inFontLab and world.mac is not None:
# we're in fontlab, on a mac
from robofab.pens.flPen import FLPointPen
print String2Glyph(glyphString, FLPointPen, font)
pass
else:
from robofab.pens.rfUFOPen import RFUFOPointPen
print String2Glyph(glyphString, RFUFOPointPen, font)
#
# command to tell FontLab to open a UFO and save it as a vfb
def os9PathConvert(path):
"""Attempt to convert a unix style path to a Mac OS9 style path.
No support for relative paths!
"""
if path.find("/Volumes") == 0:
# it's on the volumes list, some sort of external volume
path = path[len("/Volumes")+1:]
elif path[0] == "/":
# a dir on the root volume
path = path[1:]
new = path.replace("/", ":")
return new
_remoteUFOImportProgram = """
from robofab.objects.objectsFL import NewFont
import os.path
destinationPathVFB = "%(destinationPathVFB)s"
font = NewFont()
font.readUFO("%(sourcePathUFO)s", doProgress=True)
font.update()
font.save(destinationPathVFB)
print font, "done"
font.close()
"""
def makeVFB(sourcePathUFO, destinationPathVFB=None):
"""FontLab convenience function to import a UFO and save it as a VFB"""
import os
fl = FontLab("FLab", start=1)
if destinationPathVFB is None:
destinationPathVFB = os.path.splitext(sourcePathUFO)[0]+".vfb"
src9 = os9PathConvert(sourcePathUFO)
dst9 = os9PathConvert(destinationPathVFB)
code = _remoteUFOImportProgram%{'sourcePathUFO': src9, 'destinationPathVFB':dst9}
ae, parms, attrs = fl.send("Rfab", "exec", {"----": code})
output = parms.get("----")
return output

View file

@ -0,0 +1,122 @@
"""A simple module for dealing with preferences that are used by scripts. Based almost entirely on MacPrefs.
To save some preferences:
myPrefs = RFPrefs(drive/directory/directory/myPrefs.plist)
myPrefs.myString = 'xyz'
myPrefs.myInteger = 1234
myPrefs.myList = ['a', 'b', 'c']
myPrefs.myDict = {'a':1, 'b':2}
myPrefs.save()
To retrieve some preferences:
myPrefs = RFPrefs(drive/directory/directory/myPrefs.plist)
myString = myPrefs.myString
myInteger = myPrefs.myInteger
myList = myPrefs.myList
myDict = myPrefs.myDict
When using this module within FontLab, it is not necessary to
provide the RFPrefs class with a path. If a path is not given,
it will look for a file in FontLab/RoboFab Data/RFPrefs.plist.
If that file does not exist, it will make it.
"""
from robofab import RoboFabError
from robofab.plistlib import Plist
from cStringIO import StringIO
import os
class _PrefObject:
def __init__(self, dict=None):
if not dict:
self._prefs = {}
else:
self._prefs = dict
def __len__(self):
return len(self._prefs)
def __delattr__(self, attr):
if self._prefs.has_key(attr):
del self._prefs[attr]
else:
raise AttributeError, 'delete non-existing instance attribute'
def __getattr__(self, attr):
if attr == '__members__':
keys = self._prefs.keys()
keys.sort()
return keys
try:
return self._prefs[attr]
except KeyError:
raise AttributeError, attr
def __setattr__(self, attr, value):
if attr[0] != '_':
self._prefs[attr] = value
else:
self.__dict__[attr] = value
def asDict(self):
return self._prefs
class RFPrefs(_PrefObject):
"""The main preferences object to call"""
def __init__(self, path=None):
from robofab.world import world
self.__path = path
self._prefs = {}
if world.inFontLab:
#we don't have a path, but we know where we can put it
if not path:
from robofab.tools.toolsFL import makeDataFolder
settingsPath = makeDataFolder()
path = os.path.join(settingsPath, 'RFPrefs.plist')
self.__path = path
self._makePrefsFile()
#we do have a path, make sure it exists and load it
else:
self._makePrefsFile()
else:
#no path, raise error
if not path:
raise RoboFabError, "no preferences path defined"
#we do have a path, make sure it exists and load it
else:
self._makePrefsFile()
self._prefs = Plist.fromFile(path)
def _makePrefsFile(self):
if not os.path.exists(self.__path):
self.save()
def __getattr__(self, attr):
if attr[0] == '__members__':
keys = self._prefs.keys()
keys.sort()
return keys
try:
return self._prefs[attr]
except KeyError:
raise AttributeError, attr
#if attr[0] != '_':
# self._prefs[attr] = _PrefObject()
# return self._prefs[attr]
#else:
# raise AttributeError, attr
def save(self):
"""save the plist file"""
f = StringIO()
pl = Plist()
for i in self._prefs.keys():
pl[i] = self._prefs[i]
pl.write(f)
data = f.getvalue()
f = open(self.__path, 'wb')
f.write(data)
f.close()

Some files were not shown because too many files have changed in this diff Show more