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:
parent
31ae014e0c
commit
8234b62ab7
108 changed files with 26933 additions and 110 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
*.so
|
||||
*.ttx
|
||||
*.o
|
||||
*.d
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -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
34
init.sh
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
misc/pylib/booleanOperations/.gitignore
vendored
Normal file
2
misc/pylib/booleanOperations/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.c
|
||||
build
|
||||
20
misc/pylib/booleanOperations/LICENSE
Normal file
20
misc/pylib/booleanOperations/LICENSE
Normal 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.
|
||||
11
misc/pylib/booleanOperations/__init__.py
Normal file
11
misc/pylib/booleanOperations/__init__.py
Normal 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
|
||||
257
misc/pylib/booleanOperations/booleanGlyph.pyx
Normal file
257
misc/pylib/booleanOperations/booleanGlyph.pyx
Normal 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)
|
||||
137
misc/pylib/booleanOperations/booleanOperationManager.pyx
Normal file
137
misc/pylib/booleanOperations/booleanOperationManager.pyx
Normal 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)
|
||||
21
misc/pylib/booleanOperations/exceptions.py
Normal file
21
misc/pylib/booleanOperations/exceptions.py
Normal 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"""
|
||||
1247
misc/pylib/booleanOperations/flatten.pyx
Normal file
1247
misc/pylib/booleanOperations/flatten.pyx
Normal file
File diff suppressed because it is too large
Load diff
3
misc/pylib/booleanOperations/requirements.txt
Normal file
3
misc/pylib/booleanOperations/requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pyclipper==1.0.5
|
||||
fonttools==3.1.2
|
||||
ufoLib==2.0.0
|
||||
15
misc/pylib/booleanOperations/setup.py
Normal file
15
misc/pylib/booleanOperations/setup.py
Normal 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
|
||||
)
|
||||
4
misc/pylib/booleanOperations/version.py
Normal file
4
misc/pylib/booleanOperations/version.py
Normal 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
2
misc/pylib/copy/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.c
|
||||
build
|
||||
254
misc/pylib/copy/LICENSE.txt
Normal file
254
misc/pylib/copy/LICENSE.txt
Normal 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.
|
||||
2
misc/pylib/copy/__init__.py
Normal file
2
misc/pylib/copy/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from __future__ import absolute_import
|
||||
from .copy import copy, deepcopy, Error
|
||||
433
misc/pylib/copy/copy.pyx
Normal file
433
misc/pylib/copy/copy.pyx
Normal 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
13
misc/pylib/copy/setup.py
Normal 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
1
misc/pylib/fontbuild/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.c
|
||||
|
|
@ -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):
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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):
|
||||
19
misc/pylib/fontbuild/setup.py
Normal file
19
misc/pylib/fontbuild/setup.py
Normal 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
2
misc/pylib/robofab/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.c
|
||||
build
|
||||
22
misc/pylib/robofab/LICENSE.txt
Normal file
22
misc/pylib/robofab/LICENSE.txt
Normal 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
82
misc/pylib/robofab/__init__.py
Executable 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"
|
||||
11
misc/pylib/robofab/contrib/__init__.py
Executable file
11
misc/pylib/robofab/contrib/__init__.py
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
|
||||
Directory for contributed packages.
|
||||
Packages stored here can be imported from
|
||||
robofab.contrib.<packagename>
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
3
misc/pylib/robofab/exceptions.py
Normal file
3
misc/pylib/robofab/exceptions.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class RoboFabError(Exception): pass
|
||||
|
||||
class RoboFabWarning(Warning): pass
|
||||
625
misc/pylib/robofab/gString.py
Executable file
625
misc/pylib/robofab/gString.py
Executable 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
718
misc/pylib/robofab/glifLib.pyx
Executable 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
747
misc/pylib/robofab/glifLib2.py
Executable 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__)
|
||||
|
||||
14
misc/pylib/robofab/interface/__init__.py
Executable file
14
misc/pylib/robofab/interface/__init__.py
Executable 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
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
14
misc/pylib/robofab/interface/all/__init__.py
Executable file
14
misc/pylib/robofab/interface/all/__init__.py
Executable 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
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
278
misc/pylib/robofab/interface/all/dialogs.py
Executable file
278
misc/pylib/robofab/interface/all/dialogs.py
Executable 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()
|
||||
|
||||
76
misc/pylib/robofab/interface/all/dialogs_default.py
Normal file
76
misc/pylib/robofab/interface/all/dialogs_default.py
Normal 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
|
||||
|
||||
73
misc/pylib/robofab/interface/all/dialogs_fontlab_legacy1.py
Normal file
73
misc/pylib/robofab/interface/all/dialogs_fontlab_legacy1.py
Normal 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()
|
||||
|
||||
373
misc/pylib/robofab/interface/all/dialogs_fontlab_legacy2.py
Normal file
373
misc/pylib/robofab/interface/all/dialogs_fontlab_legacy2.py
Normal 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
|
||||
|
||||
737
misc/pylib/robofab/interface/all/dialogs_legacy.py
Executable file
737
misc/pylib/robofab/interface/all/dialogs_legacy.py
Executable 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)
|
||||
267
misc/pylib/robofab/interface/all/dialogs_mac_vanilla.py
Normal file
267
misc/pylib/robofab/interface/all/dialogs_mac_vanilla.py
Normal 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
|
||||
10
misc/pylib/robofab/interface/mac/__init__.py
Executable file
10
misc/pylib/robofab/interface/mac/__init__.py
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
"""
|
||||
|
||||
Directory for interface related modules.
|
||||
Stuff for MacOSX, widgets, quartz
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
80
misc/pylib/robofab/interface/mac/getFileOrFolder.py
Executable file
80
misc/pylib/robofab/interface/mac/getFileOrFolder.py
Executable 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)
|
||||
10
misc/pylib/robofab/interface/win/__init__.py
Executable file
10
misc/pylib/robofab/interface/win/__init__.py
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
"""
|
||||
|
||||
Directory for interface related modules.
|
||||
Stuff for Windows
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
13
misc/pylib/robofab/misc/__init__.py
Executable file
13
misc/pylib/robofab/misc/__init__.py
Executable 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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
160
misc/pylib/robofab/misc/arrayTools.pyx
Normal file
160
misc/pylib/robofab/misc/arrayTools.pyx
Normal 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()
|
||||
416
misc/pylib/robofab/misc/bezierTools.py
Normal file
416
misc/pylib/robofab/misc/bezierTools.py
Normal 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()
|
||||
99
misc/pylib/robofab/misc/speedTestCase.py
Normal file
99
misc/pylib/robofab/misc/speedTestCase.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
119
misc/pylib/robofab/misc/test.py
Normal file
119
misc/pylib/robofab/misc/test.py
Normal 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()
|
||||
15
misc/pylib/robofab/objects/__init__.py
Executable file
15
misc/pylib/robofab/objects/__init__.py
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
"""
|
||||
|
||||
Directory for modules supporting
|
||||
|
||||
Unified
|
||||
|
||||
Font
|
||||
|
||||
Objects
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
3426
misc/pylib/robofab/objects/objectsBase.pyx
Executable file
3426
misc/pylib/robofab/objects/objectsBase.pyx
Executable file
File diff suppressed because it is too large
Load diff
1253
misc/pylib/robofab/objects/objectsFF.py
Normal file
1253
misc/pylib/robofab/objects/objectsFF.py
Normal file
File diff suppressed because it is too large
Load diff
3112
misc/pylib/robofab/objects/objectsFL.py
Executable file
3112
misc/pylib/robofab/objects/objectsFL.py
Executable file
File diff suppressed because it is too large
Load diff
1233
misc/pylib/robofab/objects/objectsRF.pyx
Executable file
1233
misc/pylib/robofab/objects/objectsRF.pyx
Executable file
File diff suppressed because it is too large
Load diff
12
misc/pylib/robofab/path/__init__.py
Executable file
12
misc/pylib/robofab/path/__init__.py
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
"""
|
||||
|
||||
Directory for modules
|
||||
which do path stuff.
|
||||
Maybe it should move somewhere else.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
108
misc/pylib/robofab/path/intersect.py
Normal file
108
misc/pylib/robofab/path/intersect.py
Normal 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))
|
||||
11
misc/pylib/robofab/pens/__init__.py
Executable file
11
misc/pylib/robofab/pens/__init__.py
Executable 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.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
293
misc/pylib/robofab/pens/adapterPens.py
Normal file
293
misc/pylib/robofab/pens/adapterPens.py
Normal 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))
|
||||
132
misc/pylib/robofab/pens/angledMarginPen.py
Normal file
132
misc/pylib/robofab/pens/angledMarginPen.py
Normal 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()
|
||||
|
||||
95
misc/pylib/robofab/pens/boundsPen.pyx
Normal file
95
misc/pylib/robofab/pens/boundsPen.pyx
Normal 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
|
||||
|
||||
106
misc/pylib/robofab/pens/digestPen.py
Executable file
106
misc/pylib/robofab/pens/digestPen.py
Executable 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()
|
||||
|
||||
|
||||
407
misc/pylib/robofab/pens/filterPen.py
Executable file
407
misc/pylib/robofab/pens/filterPen.py
Executable 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
274
misc/pylib/robofab/pens/flPen.py
Executable 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()
|
||||
155
misc/pylib/robofab/pens/marginPen.py
Normal file
155
misc/pylib/robofab/pens/marginPen.py
Normal 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()
|
||||
|
||||
185
misc/pylib/robofab/pens/mathPens.py
Executable file
185
misc/pylib/robofab/pens/mathPens.py
Executable 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)
|
||||
|
||||
173
misc/pylib/robofab/pens/pointPen.py
Normal file
173
misc/pylib/robofab/pens/pointPen.py
Normal 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)
|
||||
21
misc/pylib/robofab/pens/quartzPen.py
Normal file
21
misc/pylib/robofab/pens/quartzPen.py
Normal 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()
|
||||
125
misc/pylib/robofab/pens/reverseContourPointPen.py
Executable file
125
misc/pylib/robofab/pens/reverseContourPointPen.py
Executable 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()
|
||||
103
misc/pylib/robofab/pens/rfUFOPen.pyx
Executable file
103
misc/pylib/robofab/pens/rfUFOPen.pyx
Executable 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))
|
||||
|
||||
|
||||
43
misc/pylib/robofab/plistFromTree.py
Executable file
43
misc/pylib/robofab/plistFromTree.py
Executable 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
495
misc/pylib/robofab/plistlib.py
Executable 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("&", "&") # escape '&'
|
||||
text = text.replace("<", "<") # escape '<'
|
||||
text = text.replace(">", ">") # 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
19
misc/pylib/robofab/setup.py
Executable 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
|
||||
)
|
||||
8
misc/pylib/robofab/test/__init__.py
Executable file
8
misc/pylib/robofab/test/__init__.py
Executable 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.
|
||||
"""
|
||||
27
misc/pylib/robofab/test/runAll.py
Normal file
27
misc/pylib/robofab/test/runAll.py
Normal 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)
|
||||
278
misc/pylib/robofab/test/testSupport.py
Executable file
278
misc/pylib/robofab/test/testSupport.py
Executable 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
|
||||
}
|
||||
111
misc/pylib/robofab/test/test_RInfoFL.py
Normal file
111
misc/pylib/robofab/test/test_RInfoFL.py
Normal 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()
|
||||
|
||||
56
misc/pylib/robofab/test/test_RInfoRF.py
Normal file
56
misc/pylib/robofab/test/test_RInfoRF.py
Normal 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()
|
||||
218
misc/pylib/robofab/test/test_dialogs.py
Normal file
218
misc/pylib/robofab/test/test_dialogs.py
Normal 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()
|
||||
565
misc/pylib/robofab/test/test_fontLabUFOReadWrite.py
Normal file
565
misc/pylib/robofab/test/test_fontLabUFOReadWrite.py
Normal 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()
|
||||
150
misc/pylib/robofab/test/test_glifLib.py
Normal file
150
misc/pylib/robofab/test/test_glifLib.py
Normal 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()
|
||||
321
misc/pylib/robofab/test/test_noneLabUFOReadWrite.py
Normal file
321
misc/pylib/robofab/test/test_noneLabUFOReadWrite.py
Normal 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()
|
||||
54
misc/pylib/robofab/test/test_objectsFL.py
Executable file
54
misc/pylib/robofab/test/test_objectsFL.py
Executable 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()
|
||||
203
misc/pylib/robofab/test/test_objectsUFO.py
Executable file
203
misc/pylib/robofab/test/test_objectsUFO.py
Executable 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()
|
||||
149
misc/pylib/robofab/test/test_pens.py
Executable file
149
misc/pylib/robofab/test/test_pens.py
Executable 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()
|
||||
110
misc/pylib/robofab/test/test_psHints.py
Normal file
110
misc/pylib/robofab/test/test_psHints.py
Normal 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()
|
||||
|
||||
1659
misc/pylib/robofab/test/test_ufoLib.py
Normal file
1659
misc/pylib/robofab/test/test_ufoLib.py
Normal file
File diff suppressed because it is too large
Load diff
12
misc/pylib/robofab/tools/__init__.py
Executable file
12
misc/pylib/robofab/tools/__init__.py
Executable 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.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
348
misc/pylib/robofab/tools/accentBuilder.py
Executable file
348
misc/pylib/robofab/tools/accentBuilder.py
Executable 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
|
||||
|
||||
|
||||
85
misc/pylib/robofab/tools/fontlabFeatureSplitter.py
Normal file
85
misc/pylib/robofab/tools/fontlabFeatureSplitter.py
Normal 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()
|
||||
95
misc/pylib/robofab/tools/glifExport.py
Executable file
95
misc/pylib/robofab/tools/glifExport.py
Executable 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()
|
||||
74
misc/pylib/robofab/tools/glifImport.py
Executable file
74
misc/pylib/robofab/tools/glifImport.py
Executable 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()
|
||||
565
misc/pylib/robofab/tools/glyphConstruction.py
Normal file
565
misc/pylib/robofab/tools/glyphConstruction.py
Normal 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
|
||||
"""
|
||||
41
misc/pylib/robofab/tools/glyphNameSchemes.py
Executable file
41
misc/pylib/robofab/tools/glyphNameSchemes.py
Executable 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
|
||||
55
misc/pylib/robofab/tools/objectDumper.py
Executable file
55
misc/pylib/robofab/tools/objectDumper.py
Executable 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))
|
||||
|
||||
|
||||
190
misc/pylib/robofab/tools/otFeatures.py
Executable file
190
misc/pylib/robofab/tools/otFeatures.py
Executable 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
119
misc/pylib/robofab/tools/proof.py
Executable 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
175
misc/pylib/robofab/tools/remote.py
Executable file
175
misc/pylib/robofab/tools/remote.py
Executable 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
|
||||
|
||||
|
||||
122
misc/pylib/robofab/tools/rfPrefs.py
Executable file
122
misc/pylib/robofab/tools/rfPrefs.py
Executable 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
Reference in a new issue