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
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()
|
||||
145
misc/pylib/robofab/tools/toolsAll.py
Executable file
145
misc/pylib/robofab/tools/toolsAll.py
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
"""A collection of non-environment specific tools"""
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
from robofab.objects.objectsRF import RInfo
|
||||
|
||||
if sys.platform == "darwin" and sys.version_info[:3] == (2, 2, 0):
|
||||
# the Mac support of Jaguar's Python 2.2 is broken
|
||||
have_broken_macsupport = 1
|
||||
else:
|
||||
have_broken_macsupport = 0
|
||||
|
||||
|
||||
def readGlyphConstructions():
|
||||
"""read GlyphConstruction and turn it into a dict"""
|
||||
from robofab.tools.glyphConstruction import _glyphConstruction
|
||||
data = _glyphConstruction.split("\n")
|
||||
glyphConstructions = {}
|
||||
for i in data:
|
||||
if len(i) == 0: continue
|
||||
if i[0] != '#':
|
||||
name = i.split(': ')[0]
|
||||
construction = i.split(': ')[1].split(' ')
|
||||
build = [construction[0]]
|
||||
for c in construction[1:]:
|
||||
accent = c.split('.')[0]
|
||||
position = c.split('.')[1]
|
||||
build.append((accent, position))
|
||||
glyphConstructions[name] = build
|
||||
return glyphConstructions
|
||||
|
||||
#
|
||||
#
|
||||
# glyph.unicode: ttFont["cmap"].getcmap(3, 1)
|
||||
#
|
||||
#
|
||||
|
||||
def guessFileType(fileName):
|
||||
if not os.path.exists(fileName):
|
||||
return None
|
||||
base, ext = os.path.splitext(fileName)
|
||||
ext = ext.lower()
|
||||
if not have_broken_macsupport:
|
||||
try:
|
||||
import MacOS
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
cr, tp = MacOS.GetCreatorAndType(fileName)
|
||||
if tp in ("sfnt", "FFIL"):
|
||||
return "TTF"
|
||||
if tp == "LWFN":
|
||||
return "Type 1"
|
||||
if ext == ".dfont":
|
||||
return "TTF"
|
||||
if ext in (".otf", ".ttf"):
|
||||
return "TTF"
|
||||
if ext in (".pfb", ".pfa"):
|
||||
return "Type 1"
|
||||
return None
|
||||
|
||||
def extractTTFFontInfo(font):
|
||||
# UFO.info attribute name / index.
|
||||
# name table entries index according to http://www.microsoft.com/typography/otspec/name.htm
|
||||
attrs = [
|
||||
('copyright', 0),
|
||||
('familyName', 1),
|
||||
('styleMapStyleName', 2),
|
||||
('postscriptFullName', 4),
|
||||
('trademark', 7),
|
||||
('openTypeNameDesigner', 9),
|
||||
('openTypeNameLicenseURL', 14),
|
||||
('openTypeNameDesignerURL', 12),
|
||||
]
|
||||
info = RInfo()
|
||||
names = font['name']
|
||||
info.ascender = font['hhea'].ascent
|
||||
info.descender = font['hhea'].descent
|
||||
info.unitsPerEm = font['head'].unitsPerEm
|
||||
for name, index in attrs:
|
||||
entry = font["name"].getName(index, 3, 1, 0x409)
|
||||
if entry is not None:
|
||||
try:
|
||||
value = unicode(entry.string, "utf_16_be")
|
||||
if name == "styleMapStyleName":
|
||||
value = value.lower()
|
||||
setattr(info, name, value)
|
||||
except Exception, e:
|
||||
print "Error importing value %s: %s: %s"%(str(name), value, e.message)
|
||||
return info
|
||||
|
||||
def extractT1FontInfo(font):
|
||||
info = RInfo()
|
||||
src = font.font['FontInfo']
|
||||
factor = font.font['FontMatrix'][0]
|
||||
assert factor > 0
|
||||
info.unitsPerEm = int(round(1/factor, 0))
|
||||
# assume something for ascender descender
|
||||
info.ascender = (info.unitsPerEm / 5) * 4
|
||||
info.descender = info.ascender - info.unitsPerEm
|
||||
info.versionMajor = font.font['FontInfo']['version']
|
||||
info.fullName = font.font['FontInfo']['FullName']
|
||||
info.familyName = font.font['FontInfo']['FullName'].split("-")[0]
|
||||
info.notice = unicode(font.font['FontInfo']['Notice'], "macroman")
|
||||
info.italicAngle = font.font['FontInfo']['ItalicAngle']
|
||||
info.uniqueID = font['UniqueID']
|
||||
return info
|
||||
|
||||
def fontToUFO(src, dst, fileType=None):
|
||||
from robofab.ufoLib import UFOWriter
|
||||
from robofab.pens.adapterPens import SegmentToPointPen
|
||||
if fileType is None:
|
||||
fileType = guessFileType(src)
|
||||
if fileType is None:
|
||||
raise ValueError, "Can't determine input file type"
|
||||
ufoWriter = UFOWriter(dst)
|
||||
if fileType == "TTF":
|
||||
from fontTools.ttLib import TTFont
|
||||
font = TTFont(src, 0)
|
||||
elif fileType == "Type 1":
|
||||
from fontTools.t1Lib import T1Font
|
||||
font = T1Font(src)
|
||||
else:
|
||||
assert 0, "unknown file type: %r" % fileType
|
||||
inGlyphSet = font.getGlyphSet()
|
||||
outGlyphSet = ufoWriter.getGlyphSet()
|
||||
for glyphName in inGlyphSet.keys():
|
||||
print "-", glyphName
|
||||
glyph = inGlyphSet[glyphName]
|
||||
def drawPoints(pen):
|
||||
pen = SegmentToPointPen(pen)
|
||||
glyph.draw(pen)
|
||||
outGlyphSet.writeGlyph(glyphName, glyph, drawPoints)
|
||||
outGlyphSet.writeContents()
|
||||
if fileType == "TTF":
|
||||
info = extractTTFFontInfo(font)
|
||||
elif fileType == "Type 1":
|
||||
info = extractT1FontInfo(font)
|
||||
ufoWriter.writeInfo(info)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print readGlyphConstructions()
|
||||
|
||||
|
||||
339
misc/pylib/robofab/tools/toolsFL.py
Executable file
339
misc/pylib/robofab/tools/toolsFL.py
Executable file
|
|
@ -0,0 +1,339 @@
|
|||
"""
|
||||
T.O.O.L.S.: Things Other Objects Lack (Sometimes)
|
||||
-assorted raw tools.
|
||||
|
||||
This is an assorted colection of raw tools that do
|
||||
things inside of FontLab. Many of these functions
|
||||
form the bedrock of objectsFL. In short, use these
|
||||
tools only if you need the raw functions and they are
|
||||
not supported by the objects.
|
||||
|
||||
Object model:
|
||||
Most of these tools were written before
|
||||
objectsFL. Some of these tools are used by
|
||||
objectsFL. That means that if you want to
|
||||
use functions from robofab.tools you can always
|
||||
feed them FontLab objects (like Font, Glyps,
|
||||
etc.). If the functions also accept Robjects from
|
||||
robofab.objects it is usually mentioned in the
|
||||
doc string.
|
||||
|
||||
This is a simple way to convert a robofab Font
|
||||
object back to a FL Font object. Even if you don't
|
||||
know which particular faith an object belongs to
|
||||
you can use this:
|
||||
|
||||
font = unwrapFont(font)
|
||||
"""
|
||||
|
||||
|
||||
from FL import *
|
||||
from warnings import warn
|
||||
|
||||
try:
|
||||
from fl_cmd import *
|
||||
except ImportError:
|
||||
print "The fl_cmd module is not available here. toolsFL.py"
|
||||
|
||||
import os
|
||||
|
||||
from robofab import RoboFabError
|
||||
|
||||
# local encoding
|
||||
if os.name == "mac":
|
||||
LOCAL_ENCODING = "macroman"
|
||||
else:
|
||||
LOCAL_ENCODING = "latin-1"
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
# stuff for fontlab app
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def AppFolderRenamer():
|
||||
"""This function will rename the folder that contains the
|
||||
FontLab application to a more specific name that includes
|
||||
the version of the application
|
||||
Warning: it messes with the paths of your app, if you have
|
||||
items that hardwired to this path you'd be in trouble.
|
||||
"""
|
||||
if fl.count > 0:
|
||||
warn("Close all fonts before running AppFolderRenamer")
|
||||
return
|
||||
old = fl.path[:-1]
|
||||
root = os.path.dirname(old)
|
||||
new = "FontLab " + fl.version.replace('/', '_')
|
||||
path = os.path.join(root, new)
|
||||
if path != old:
|
||||
try:
|
||||
os.rename(old, path)
|
||||
except OSError:
|
||||
pass
|
||||
warn("Please quit and restart FontLab")
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
# stuff for fonts
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def GetFont(full_name):
|
||||
"""Return fontobjects which match full_name.
|
||||
Note: result is a list.
|
||||
Returns: a list of FL Font objects
|
||||
"""
|
||||
found = []
|
||||
for f in AllFonts():
|
||||
if f.full_name == full_name:
|
||||
found.append(f)
|
||||
return found
|
||||
|
||||
def AllFonts():
|
||||
"""Collect a list of all open fonts.
|
||||
Returns: a list of FL Font objects.
|
||||
"""
|
||||
fontcount = len(fl)
|
||||
af = []
|
||||
for i in range(fontcount):
|
||||
af.append(fl[i])
|
||||
return af
|
||||
|
||||
def FontIndex(font):
|
||||
"""return the index of a specified FL Font"""
|
||||
font = unwrapFont(font)
|
||||
a = AllFonts()
|
||||
p = []
|
||||
for f in a:
|
||||
p.append(f.file_name)
|
||||
if font.file_name in p:
|
||||
return p.index(font.file_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
def unwrapFont(font):
|
||||
"""Unwrap the font if it happens to be a RoboFab Font"""
|
||||
if hasattr(font, 'isRobofab'):
|
||||
return font.naked()
|
||||
return font
|
||||
|
||||
def MakeTempFont(font, dupemark=None, removeOverlap=True, decompose=True):
|
||||
"""Save the current FL Font,
|
||||
- close the file,
|
||||
- duplicate the file in the finder (icon looks weird, but it works)
|
||||
- open the duplicate
|
||||
- decompose the glyphs
|
||||
- remove overlaps
|
||||
- return the fontobject
|
||||
|
||||
font is either a FL Font or RF RFont object.
|
||||
|
||||
Problems: doesn't check if the filename is getting too long.
|
||||
Note: it will overwrite older files with the same name.
|
||||
"""
|
||||
import string
|
||||
f = unwrapFont(font)
|
||||
if not dupemark or dupemark == "":
|
||||
dupemark = "_tmp_"
|
||||
path = f.file_name
|
||||
a = f.file_name.split('.')
|
||||
a.insert(len(a)-1, dupemark)
|
||||
newpath = string.join(a, '.')
|
||||
f.Save(path)
|
||||
fl.Close(FontIndex(f))
|
||||
file = open(path, 'rb')
|
||||
data = file.read()
|
||||
file.close()
|
||||
file = open(newpath, 'wb')
|
||||
file.write(data)
|
||||
file.close()
|
||||
fl.Open(newpath, 1)
|
||||
nf = fl.font
|
||||
if nf is None:
|
||||
print 'uh oh, sup?'
|
||||
return None
|
||||
else:
|
||||
for g in nf.glyphs:
|
||||
if decompose:
|
||||
g.Decompose()
|
||||
if removeOverlap:
|
||||
g.RemoveOverlap()
|
||||
return nf
|
||||
|
||||
def makePSFontName(name):
|
||||
"""Create a postscript filename out of a regular postscript fontname,
|
||||
using the old fashioned macintosh 5:3:3 convention.
|
||||
"""
|
||||
import string
|
||||
parts = []
|
||||
current = []
|
||||
final = []
|
||||
notAllowed = '-_+=,-'
|
||||
index = 0
|
||||
for c in name:
|
||||
if c in notAllowed:
|
||||
continue
|
||||
if c in string.uppercase or index == 0:
|
||||
c = string.upper(c)
|
||||
if current:
|
||||
parts.append("".join(current))
|
||||
current = [c]
|
||||
else:
|
||||
current.append(c)
|
||||
index = index + 1
|
||||
if current:
|
||||
parts.append("".join(current))
|
||||
final.append(parts[0][:5])
|
||||
for p in parts[1:]:
|
||||
final.append(p[:3])
|
||||
return "".join(final)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
# stuff for glyphs
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def NewGlyph(font, glyphName, clear=False, updateFont=True):
|
||||
"""Make a new glyph if it doesn't already exist, return the glyph.
|
||||
font is either a FL Font or RF RFont object. If updateFont is True
|
||||
the (very slow) fl.UpdateFont function will be called.
|
||||
"""
|
||||
font = unwrapFont(font)
|
||||
if isinstance(glyphName, unicode):
|
||||
glyphName = glyphName.encode(LOCAL_ENCODING)
|
||||
glyph = font[glyphName]
|
||||
if glyph is None:
|
||||
new = Glyph()
|
||||
new.name = glyphName
|
||||
font.glyphs.append(new)
|
||||
if updateFont:
|
||||
fl.UpdateFont(FontIndex(font))
|
||||
glyph = font[glyphName]
|
||||
elif clear:
|
||||
glyph.Clear()
|
||||
glyph.anchors.clean()
|
||||
glyph.components.clean()
|
||||
glyph.note = ""
|
||||
return glyph
|
||||
|
||||
|
||||
def AddToAlias(additions, sep='+'):
|
||||
"""additions is a dict with glyphnames as keys
|
||||
and glyphConstruction as values. In order to make
|
||||
a bunch of additions in one go rather than open
|
||||
and close the file for each name. Add a glyph
|
||||
to the alias.dat file if it doesn't already exist.
|
||||
additions = {'Gcircumflex': ['G','circumflex'], }
|
||||
Returns a list of only the added glyphnames."""
|
||||
import string
|
||||
glyphs = {}
|
||||
data = []
|
||||
new = []
|
||||
path = os.path.join(fl.path, 'Mapping', 'alias.dat')
|
||||
if os.path.exists(path):
|
||||
file = open(path, 'r')
|
||||
data = file.read().split('\n')
|
||||
file.close()
|
||||
for i in data:
|
||||
if len(i) == 0: continue
|
||||
if i[0] != '%':
|
||||
glyphs[i.split(' ')[0]] = i.split(' ')[1]
|
||||
for glyphName, glyphConstruction in additions.items():
|
||||
if glyphName not in glyphs.keys():
|
||||
new.append(glyphName)
|
||||
glyphs[glyphName] = string.join(glyphConstruction, sep)
|
||||
newNames = ['%%FONTLAB ALIASES']
|
||||
l = glyphs.keys()
|
||||
l.sort()
|
||||
for i in l:
|
||||
newNames.append(string.join([i, glyphs[i]], ' '))
|
||||
file = open(path, 'w')
|
||||
file.write(string.join(newNames, '\n'))
|
||||
file.close()
|
||||
return new
|
||||
|
||||
|
||||
def GlyphIndexTable(font):
|
||||
"""Make a glyph index table for font"""
|
||||
font = unwrapFont(font)
|
||||
idx = {}
|
||||
for i in range(len(font)):
|
||||
g = font.glyphs[i]
|
||||
idx[g.name] = i
|
||||
return idx
|
||||
|
||||
def MakeReverseCompoMapping(font):
|
||||
"""Return a dict that maps glyph names to lists containing tuples
|
||||
of the form:
|
||||
(clientGlyphName, componentIndex)
|
||||
"""
|
||||
font = unwrapFont(font)
|
||||
reverseCompoMapping = {}
|
||||
for g in font.glyphs:
|
||||
for i, c in zip(range(len(g.components)), g.components):
|
||||
base = font[c.index].name
|
||||
if not base in reverseCompoMapping:
|
||||
reverseCompoMapping[base] = []
|
||||
reverseCompoMapping[base].append((g.name, i))
|
||||
return reverseCompoMapping
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
# stuff for text files
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def textPrinter(text, name=None, path=None):
|
||||
"""Write a string to a text file. If no name is given it becomes
|
||||
Untitled_hour_minute_second.txt . If no path is given it goes
|
||||
into the FontLab/RoboFab Data directory."""
|
||||
if not name:
|
||||
import time
|
||||
tm_year,tm_mon,tm_day,tm_hour,tm_min,tm_sec,tm_wday,tm_yday,tm_isdst = time.localtime()
|
||||
now = '_'.join((`tm_hour`, `tm_min`, `tm_sec`))
|
||||
name = 'Untitled_%s.txt'%now
|
||||
if not path:
|
||||
path = os.path.join(makeDataFolder(), name)
|
||||
f = open(path, 'wb')
|
||||
f.write(text)
|
||||
f.close()
|
||||
|
||||
def makeDataFolder():
|
||||
"""Make the RoboFab data folder"""
|
||||
folderPath = os.path.join(fl.path, "RoboFab Data")
|
||||
if not os.path.exists(folderPath):
|
||||
try:
|
||||
os.makedirs(folderPath)
|
||||
except:
|
||||
pass
|
||||
return folderPath
|
||||
|
||||
|
||||
def Log(text=None):
|
||||
"""Make an entry in the default log file."""
|
||||
now = str(time.asctime(time.localtime(time.time())))
|
||||
if not text:
|
||||
text = "-"
|
||||
entry = "%s: %s\r"%(now, text)
|
||||
path = os.path.join(os.getcwd(), "Logs")
|
||||
new = 0
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
new = 1
|
||||
log = os.path.join(path, "log.txt")
|
||||
f = open(log, 'a')
|
||||
if new:
|
||||
f.write("# log file for FL\r")
|
||||
f.write(entry)
|
||||
f.close()
|
||||
6
misc/pylib/robofab/tools/toolsRF.py
Executable file
6
misc/pylib/robofab/tools/toolsRF.py
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
|
||||
Module for rf specific tool like code.
|
||||
|
||||
"""
|
||||
|
||||
1084
misc/pylib/robofab/ufoLib.py
Executable file
1084
misc/pylib/robofab/ufoLib.py
Executable file
File diff suppressed because it is too large
Load diff
108
misc/pylib/robofab/world.py
Normal file
108
misc/pylib/robofab/world.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import os, sys
|
||||
from robofab import RoboFabError, version, numberVersion
|
||||
|
||||
|
||||
class RFWorld:
|
||||
|
||||
"""All parameters about platforms, versions and environments included in one object."""
|
||||
|
||||
def __init__(self):
|
||||
self.mac = None
|
||||
self.pc = None
|
||||
self.platform = sys.platform
|
||||
self.applicationName = None # name of the application we're running in
|
||||
self.name = os.name
|
||||
self.version = version # the robofab version
|
||||
self.numberVersion = numberVersion
|
||||
self.run = True
|
||||
|
||||
# get some platform information
|
||||
if self.name == 'mac' or self.name == 'posix':
|
||||
if self.platform == "darwin":
|
||||
self.mac = "X"
|
||||
else:
|
||||
self.mac = "pre-X"
|
||||
elif self.name == 'nt':
|
||||
# if you know more about PC & win stuff, add it here!
|
||||
self.pc = True
|
||||
else:
|
||||
raise RoboFabError, "We're running on an unknown platform."
|
||||
|
||||
# collect versions
|
||||
self.pyVersion = sys.version[:3]
|
||||
self.inPython = False
|
||||
self.inFontLab = False
|
||||
self.flVersion = None
|
||||
self.inGlyphs = False
|
||||
self.glyphsVersion = None
|
||||
self.inRoboFont = False
|
||||
self.roboFontVersion = None
|
||||
|
||||
# are we in Glyphs?
|
||||
try:
|
||||
import objectsGS
|
||||
from AppKit import NSBundle
|
||||
bundle = NSBundle.mainBundle()
|
||||
self.applicationName = bundle.infoDictionary()["CFBundleName"]
|
||||
self.inGlyphs = True
|
||||
self.glyphsVersion = bundle.infoDictionary()["CFBundleVersion"]
|
||||
except ImportError:
|
||||
# are we in RoboFont
|
||||
try:
|
||||
import mojo
|
||||
from AppKit import NSBundle
|
||||
bundle = NSBundle.mainBundle()
|
||||
self.applicationName = bundle.infoDictionary()["CFBundleName"]
|
||||
self.inRoboFont = True
|
||||
self.roboFontVersion = bundle.infoDictionary()["CFBundleVersion"]
|
||||
except ImportError:
|
||||
# are we in FontLab?
|
||||
try:
|
||||
from FL import fl
|
||||
self.applicationName = fl.filename
|
||||
self.inFontLab = True
|
||||
self.flVersion = fl.version
|
||||
except ImportError: pass
|
||||
# we are in NoneLab
|
||||
if not self.inFontLab:
|
||||
self.inPython = True
|
||||
|
||||
# see if we have DialogKit
|
||||
self.supportsDialogKit = False
|
||||
|
||||
def __repr__(self):
|
||||
s = [
|
||||
"Robofab is running on %s" % self.platform,
|
||||
"Python version: %s" % self.pyVersion,
|
||||
"Mac stuff: %s" % self.mac,
|
||||
"PC stuff: %s" % self.pc,
|
||||
"FontLab stuff: %s" % self.inFontLab,
|
||||
"FLversion: %s" % self.flVersion,
|
||||
"Glyphs stuff: %s" % self.inGlyphs,
|
||||
"Glyphs version: %s" % self.glyphsVersion,
|
||||
"RoboFont stuff: %s" %self.inRoboFont,
|
||||
"RoboFont version: %s" %self.roboFontVersion,
|
||||
]
|
||||
return ", ".join(s)
|
||||
|
||||
|
||||
world = RFWorld()
|
||||
|
||||
lineBreak = os.linesep
|
||||
|
||||
if world.inFontLab:
|
||||
from robofab.interface.all.dialogs import SelectFont, SelectGlyph
|
||||
from robofab.objects.objectsFL import CurrentFont, CurrentGlyph, RFont, RGlyph, OpenFont, NewFont, AllFonts
|
||||
lineBreak = "\n"
|
||||
elif world.inRoboFont:
|
||||
from mojo.roboFont import CurrentFont, CurrentGlyph, RFont, RGlyph, OpenFont, NewFont, AllFonts
|
||||
elif world.inGlyphs:
|
||||
from objectsGS import CurrentFont, CurrentGlyph, RFont, RGlyph, OpenFont, NewFont, AllFonts
|
||||
elif world.inPython:
|
||||
from robofab.objects.objectsRF import CurrentFont, CurrentGlyph, RFont, RGlyph, OpenFont, NewFont, AllFonts
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
f = RFWorld()
|
||||
print f
|
||||
116
misc/pylib/robofab/xmlTreeBuilder.pyx
Normal file
116
misc/pylib/robofab/xmlTreeBuilder.pyx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import os
|
||||
try:
|
||||
from xml.parsers.expat import ParserCreate
|
||||
except ImportError:
|
||||
_haveExpat = 0
|
||||
from xml.parsers.xmlproc.xmlproc import XMLProcessor
|
||||
else:
|
||||
_haveExpat = 1
|
||||
|
||||
|
||||
class XMLParser:
|
||||
|
||||
def __init__(self):
|
||||
self.root = []
|
||||
self.current = (self.root, None)
|
||||
|
||||
def getRoot(self):
|
||||
assert len(self.root) == 1
|
||||
return self.root[0]
|
||||
|
||||
def startElementHandler(self, name, attrs):
|
||||
children = []
|
||||
self.current = (children, name, attrs, self.current)
|
||||
|
||||
def endElementHandler(self, name):
|
||||
children, name, attrs, previous = self.current
|
||||
previous[0].append((name, attrs, children))
|
||||
self.current = previous
|
||||
|
||||
def characterDataHandler(self, data):
|
||||
nodes = self.current[0]
|
||||
if nodes and type(nodes[-1]) == type(data):
|
||||
nodes[-1] = nodes[-1] + data
|
||||
else:
|
||||
nodes.append(data)
|
||||
|
||||
def _expatParseFile(self, pathOrFile):
|
||||
parser = ParserCreate()
|
||||
parser.returns_unicode = 0 # XXX, Don't remember why. It sucks, though.
|
||||
parser.StartElementHandler = self.startElementHandler
|
||||
parser.EndElementHandler = self.endElementHandler
|
||||
parser.CharacterDataHandler = self.characterDataHandler
|
||||
if isinstance(pathOrFile, (str, unicode)):
|
||||
f = open(pathOrFile)
|
||||
didOpen = 1
|
||||
else:
|
||||
didOpen = 0
|
||||
f = pathOrFile
|
||||
parser.ParseFile(f)
|
||||
if didOpen:
|
||||
f.close()
|
||||
return self.getRoot()
|
||||
|
||||
def _xmlprocDataHandler(self, data, begin, end):
|
||||
self.characterDataHandler(data[begin:end])
|
||||
|
||||
def _xmlprocParseFile(self, pathOrFile):
|
||||
proc = XMLProcessor()
|
||||
proc.app.handle_start_tag = self.startElementHandler
|
||||
proc.app.handle_end_tag = self.endElementHandler
|
||||
proc.app.handle_data = self._xmlprocDataHandler
|
||||
if isinstance(pathOrFile, (str, unicode)):
|
||||
f = open(pathOrFile)
|
||||
didOpen = 1
|
||||
else:
|
||||
didOpen = 0
|
||||
f = pathOrFile
|
||||
proc.parseStart()
|
||||
proc.read_from(f)
|
||||
proc.flush()
|
||||
proc.parseEnd()
|
||||
proc.deref()
|
||||
if didOpen:
|
||||
f.close()
|
||||
return self.getRoot()
|
||||
|
||||
if _haveExpat:
|
||||
parseFile = _expatParseFile
|
||||
else:
|
||||
parseFile = _xmlprocParseFile
|
||||
|
||||
|
||||
def stripCharacterData(nodes, recursive=True):
|
||||
i = 0
|
||||
while 1:
|
||||
try:
|
||||
node = nodes[i]
|
||||
except IndexError:
|
||||
break
|
||||
if isinstance(node, tuple):
|
||||
if recursive:
|
||||
stripCharacterData(node[2])
|
||||
i = i + 1
|
||||
else:
|
||||
node = node.strip()
|
||||
if node:
|
||||
nodes[i] = node
|
||||
i = i + 1
|
||||
else:
|
||||
del nodes[i]
|
||||
|
||||
|
||||
def buildTree(pathOrFile, stripData=1):
|
||||
parser = XMLParser()
|
||||
tree = parser.parseFile(pathOrFile)
|
||||
if stripData:
|
||||
stripCharacterData(tree[2])
|
||||
return tree
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pprint import pprint
|
||||
import sys
|
||||
strip = bool(sys.argv[2:])
|
||||
tree = buildTree(sys.argv[1], strip)
|
||||
pprint(tree)
|
||||
Reference in a new issue