aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.appveyor.yml22
-rw-r--r--.travis.yml38
-rwxr-xr-x.travis/after_success.sh5
-rwxr-xr-x.travis/before_deploy.sh15
-rwxr-xr-x.travis/install.sh17
-rwxr-xr-x.travis/run.sh4
-rw-r--r--Doc/source/ufoLib/converters.rst9
-rw-r--r--Doc/source/ufoLib/filenames.rst9
-rw-r--r--Doc/source/ufoLib/glifLib.rst9
-rw-r--r--Doc/source/ufoLib/pointPen.rst9
-rw-r--r--Doc/source/ufoLib/ufoLib.rst9
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/cffLib/specializer.py4
-rw-r--r--Lib/fontTools/designspaceLib/__init__.py160
-rw-r--r--Lib/fontTools/feaLib/ast.py3
-rw-r--r--Lib/fontTools/feaLib/builder.py66
-rw-r--r--Lib/fontTools/feaLib/parser.py4
-rw-r--r--Lib/fontTools/misc/etree.py482
-rw-r--r--Lib/fontTools/misc/loggingTools.py35
-rw-r--r--Lib/fontTools/misc/plistlib.py540
-rw-r--r--Lib/fontTools/misc/psCharStrings.py25
-rw-r--r--Lib/fontTools/misc/psLib.py7
-rw-r--r--Lib/fontTools/misc/testTools.py7
-rw-r--r--Lib/fontTools/misc/xmlWriter.py6
-rw-r--r--Lib/fontTools/pens/pointPen.py413
-rw-r--r--Lib/fontTools/subset/__init__.py63
-rw-r--r--Lib/fontTools/svgLib/path/__init__.py12
-rw-r--r--Lib/fontTools/ttLib/tables/C_P_A_L_.py6
-rw-r--r--Lib/fontTools/ttLib/tables/DefaultTable.py2
-rw-r--r--Lib/fontTools/ttLib/tables/G_P_K_G_.py12
-rw-r--r--Lib/fontTools/ttLib/tables/G__l_o_c.py14
-rw-r--r--Lib/fontTools/ttLib/tables/S__i_l_f.py6
-rw-r--r--Lib/fontTools/ttLib/tables/T_S_I_C_.py7
-rw-r--r--Lib/fontTools/ttLib/tables/T_S_I__5.py6
-rw-r--r--Lib/fontTools/ttLib/tables/TupleVariation.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_c_m_a_p.py22
-rw-r--r--Lib/fontTools/ttLib/tables/_c_v_t.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_g_l_y_f.py64
-rw-r--r--Lib/fontTools/ttLib/tables/_g_v_a_r.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_h_m_t_x.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_k_e_r_n.py3
-rw-r--r--Lib/fontTools/ttLib/tables/_l_o_c_a.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_p_o_s_t.py12
-rw-r--r--Lib/fontTools/ttLib/tables/otBase.py12
-rw-r--r--Lib/fontTools/ttLib/tables/otConverters.py62
-rwxr-xr-xLib/fontTools/ttLib/tables/otData.py30
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py152
-rw-r--r--Lib/fontTools/ttLib/ttFont.py20
-rw-r--r--Lib/fontTools/ttLib/woff2.py6
-rwxr-xr-xLib/fontTools/ufoLib/__init__.py2246
-rw-r--r--Lib/fontTools/ufoLib/converters.py336
-rw-r--r--Lib/fontTools/ufoLib/errors.py9
-rw-r--r--Lib/fontTools/ufoLib/etree.py5
-rw-r--r--Lib/fontTools/ufoLib/filenames.py214
-rwxr-xr-xLib/fontTools/ufoLib/glifLib.py1622
-rw-r--r--Lib/fontTools/ufoLib/kerning.py90
-rw-r--r--Lib/fontTools/ufoLib/plistlib.py45
-rw-r--r--Lib/fontTools/ufoLib/pointPen.py5
-rw-r--r--Lib/fontTools/ufoLib/utils.py86
-rw-r--r--Lib/fontTools/ufoLib/validators.py1066
-rw-r--r--Lib/fontTools/unicodedata/Blocks.py70
-rw-r--r--Lib/fontTools/unicodedata/ScriptExtensions.py140
-rw-r--r--Lib/fontTools/unicodedata/Scripts.py397
-rw-r--r--Lib/fontTools/unicodedata/__init__.py2
-rw-r--r--Lib/fontTools/varLib/__init__.py218
-rw-r--r--Lib/fontTools/varLib/designspace.py113
-rw-r--r--Lib/fontTools/varLib/interpolate_layout.py20
-rw-r--r--Lib/fontTools/varLib/iup.py4
-rw-r--r--Lib/fontTools/varLib/merger.py31
-rw-r--r--Lib/fontTools/varLib/models.py20
-rw-r--r--Lib/fontTools/varLib/mutator.py8
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO286
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt106
-rw-r--r--Lib/fonttools.egg-info/requires.txt66
-rw-r--r--METADATA8
-rw-r--r--NEWS.rst80
-rw-r--r--PKG-INFO286
-rw-r--r--README.rst194
-rwxr-xr-xSnippets/otf2ttf.py54
-rw-r--r--Tests/cffLib/specializer_test.py4
-rw-r--r--Tests/designspaceLib/data/test.designspace210
-rw-r--r--Tests/designspaceLib/designspace_test.py2
-rw-r--r--Tests/feaLib/builder_test.py46
-rw-r--r--Tests/feaLib/data/PairPosSubtable.fea13
-rw-r--r--Tests/feaLib/data/PairPosSubtable.ttx54
-rw-r--r--Tests/feaLib/data/bug1307.fea65
-rw-r--r--Tests/feaLib/data/bug1307.ttx215
-rw-r--r--Tests/feaLib/parser_test.py7
-rw-r--r--Tests/misc/etree_test.py55
-rw-r--r--Tests/misc/plistlib_test.py536
-rw-r--r--Tests/misc/psCharStrings_test.py21
-rw-r--r--Tests/misc/testdata/test.plist87
-rw-r--r--Tests/pens/reverseContourPen_test.py7
-rw-r--r--Tests/subset/subset_test.py4
-rw-r--r--Tests/ttLib/tables/_g_l_y_f_test.py61
-rw-r--r--Tests/ttLib/tables/_m_o_r_x_test.py174
-rw-r--r--Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.glyf.binbin0 -> 54 bytes
-rw-r--r--Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.head.binbin0 -> 54 bytes
-rw-r--r--Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.loca.binbin0 -> 10 bytes
-rw-r--r--Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.maxp.binbin0 -> 32 bytes
-rw-r--r--Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.ttx88
-rw-r--r--Tests/ttLib/tables/otTables_test.py106
-rw-r--r--Tests/ufoLib/GLIF1_test.py1337
-rw-r--r--Tests/ufoLib/GLIF2_test.py2372
-rw-r--r--Tests/ufoLib/UFO1_test.py152
-rw-r--r--Tests/ufoLib/UFO2_test.py1414
-rw-r--r--Tests/ufoLib/UFO3_test.py4686
-rw-r--r--Tests/ufoLib/UFOConversion_test.py347
-rw-r--r--Tests/ufoLib/UFOZ_test.py99
-rw-r--r--Tests/ufoLib/__init__.py0
-rw-r--r--Tests/ufoLib/filenames_test.py98
-rw-r--r--Tests/ufoLib/glifLib_test.py164
-rwxr-xr-xTests/ufoLib/testSupport.py672
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/fontinfo.plist46
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/A_.glif40
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/B_.glif46
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F_.glif22
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F__A__B_.glif9
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/G_.glif45
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/O_.glif41
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/R_.glif37
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/a.glif6
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/contents.plist26
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.glif18
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif18
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/lib.plist6
-rw-r--r--Tests/ufoLib/testdata/DemoFont.ufo/metainfo.plist10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/fontinfo.plist87
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif13
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif21
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/groups.plist15
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/kerning.plist16
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/lib.plist72
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/metainfo.plist10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/features.fea5
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/fontinfo.plist239
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif13
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif21
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/groups.plist15
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/kerning.plist16
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/lib.plist8
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/metainfo.plist10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/fontinfo.plist338
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif18
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif12
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif13
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif16
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist34
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif21
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif21
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif21
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif8
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif9
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif17
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif17
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif9
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif9
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif7
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/kerning.plist20
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/layercontents.plist10
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/lib.plist25
-rw-r--r--Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/metainfo.plist10
-rw-r--r--Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt1
-rw-r--r--Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt1
-rw-r--r--Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt1
-rw-r--r--Tests/ufoLib/testdata/UFO3-Read Data.ufo/metainfo.plist10
-rw-r--r--Tests/unicodedata_test.py8
-rw-r--r--Tests/varLib/data/Designspace.designspace39
-rw-r--r--Tests/varLib/data/Designspace2.designspace8
-rw-r--r--Tests/varLib/data/FeatureVars.designspace71
-rw-r--r--Tests/varLib/data/test_results/Build3.ttx725
-rw-r--r--Tests/varLib/data/test_results/FeatureVars.ttx181
-rw-r--r--Tests/varLib/designspace_test.py69
-rw-r--r--Tests/varLib/interpolate_layout_test.py20
-rw-r--r--Tests/varLib/varLib_test.py33
-rw-r--r--requirements.txt6
-rwxr-xr-xrun-tests.sh10
-rw-r--r--setup.cfg12
-rwxr-xr-xsetup.py81
-rw-r--r--tox.ini39
183 files changed, 24081 insertions, 2194 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index f04db29e..e6d53c66 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -2,23 +2,12 @@ environment:
matrix:
- JOB: "2.7 32-bit"
PYTHON_HOME: "C:\\Python27"
- TOXENV: "py27-cov"
- TOXPYTHON: "C:\\Python27\\python.exe"
-
- - JOB: "3.6 32-bit"
- PYTHON_HOME: "C:\\Python36"
- TOXENV: "py36-cov"
- TOXPYTHON: "C:\\Python36\\python.exe"
-
- - JOB: "2.7 64-bit"
- PYTHON_HOME: "C:\\Python27-x64"
- TOXENV: "py27-cov"
- TOXPYTHON: "C:\\Python27-x64\\python.exe"
- JOB: "3.6 64-bit"
PYTHON_HOME: "C:\\Python36-x64"
- TOXENV: "py36-cov"
- TOXPYTHON: "C:\\Python36-x64\\python.exe"
+
+ - JOB: "3.7 64-bit"
+ PYTHON_HOME: "C:\\Python37-x64"
install:
# If there is a newer build queued for the same PR, cancel this one.
@@ -44,13 +33,14 @@ install:
# install the dependencies to run the tests
- "python -m pip install tox"
-
build: false
test_script:
- - "tox"
+ # run tests with the current 'python' in %PATH%, and measure test coverage
+ - "tox -e py-cov"
after_test:
+ # upload test coverage to Codecov.io
- "tox -e codecov"
notifications:
diff --git a/.travis.yml b/.travis.yml
index a1cd77dd..4223e959 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,10 @@
-sudo: false
-
language: python
python: 3.5
-# empty "env:" is needed for 'allow_failures'
-# https://docs.travis-ci.com/user/customizing-the-build/#Rows-that-are-Allowed-to-Fail
env:
+ global:
+ - TWINE_USERNAME="anthrotype"
+ - secure: PJuCmlDuwnojiw3QuDhfNAaU4f/yeJcEcRzJAudA66bwZK7hvxV7Tiy9A17Bm6yO0HbJmmyjsIr8h2e7/PyY6QCaV8RqcMDkQ0UraU16pRsihp0giVXJoWscj2sCP4cNDOBVwSaGAX8yZ2OONc5srESywghzcy8xmgw6O+XFqx4=
matrix:
fast_finish: true
@@ -15,27 +14,30 @@ matrix:
include:
- python: 2.7
env: TOXENV=py27-cov
- - python: 3.4
- env: TOXENV=py34-cov
- python: 3.5
env: TOXENV=py35-cov
- python: 3.6
env:
- TOXENV=py36-cov
- BUILD_DIST=true
+ - python: 3.7
+ env: TOXENV=py37-cov
+ # required to run python3.7 on Travis CI
+ # https://github.com/travis-ci/travis-ci/issues/9815
+ dist: xenial
- python: pypy2.7-5.8.0
# disable coverage.py on pypy because of performance problems
- env: TOXENV=pypy-nocov
+ env: TOXENV=pypy
- language: generic
os: osx
env: TOXENV=py27-cov
- language: generic
os: osx
env:
- - TOXENV=py36-cov
+ - TOXENV=py3-cov
- HOMEBREW_NO_AUTO_UPDATE=1
- env:
- - TOXENV=py27-nocov
+ - TOXENV=py27
- PYENV_VERSION='2.7.6'
- PYENV_VERSION_STRING='Python 2.7.6'
- PYENV_ROOT=$HOME/.travis-pyenv
@@ -49,7 +51,7 @@ matrix:
- language: generic
os: osx
env:
- - TOXENV=py36-cov
+ - TOXENV=py3-cov
- HOMEBREW_NO_AUTO_UPDATE=1
cache:
@@ -69,9 +71,6 @@ script:
after_success:
- ./.travis/after_success.sh
-before_deploy:
- - ./.travis/before_deploy.sh
-
notifications:
irc: "irc.freenode.org##fonts"
email: fonttools-dev@googlegroups.com
@@ -89,16 +88,3 @@ deploy:
repo: fonttools/fonttools
all_branches: true
condition: "$BUILD_DIST == true"
- # deploy to PyPI on tags
- - provider: pypi
- server: https://upload.pypi.org/legacy/
- user: anthrotype
- password:
- secure: Dz3x8kh4ergBV6qZUgcGVDOEzjoCEFzzQiO5WVw4Zfi04DD8+d1ghmMz2BY4UvoVKSsFrfKDuEB3MCWyqewJsf/zoZQczk/vnWVFjERROieyO1Ckzpz/WkCvbjtniIE0lxzB7zorSV+kGI9VigGAaRlXJyU7mCFojeAFqD6cjS4=
- skip_cleanup: true
- distributions: pass
- on:
- tags: true
- repo: fonttools/fonttools
- all_branches: true
- condition: "$BUILD_DIST == true"
diff --git a/.travis/after_success.sh b/.travis/after_success.sh
index d113fe7b..07bcab5e 100755
--- a/.travis/after_success.sh
+++ b/.travis/after_success.sh
@@ -9,3 +9,8 @@ fi
# upload coverage data to Codecov.io
[[ ${TOXENV} == *"-cov"* ]] && tox -e codecov
+
+# if tagged commit, create distribution packages and deploy to PyPI
+if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "fonttools/fonttools" ] && [ "$BUILD_DIST" == true ]; then
+ tox -e pypi
+fi
diff --git a/.travis/before_deploy.sh b/.travis/before_deploy.sh
deleted file mode 100755
index 1ded8f0f..00000000
--- a/.travis/before_deploy.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash
-
-set -e
-set -x
-
-# build sdist and wheel distribution packages in ./dist folder.
-# Travis runs the `before_deploy` stage before each deployment, but
-# we only want to build them once, as we want to use the same
-# files for both Github and PyPI
-if $(ls ./dist/fonttools*.zip > /dev/null 2>&1) && \
- $(ls ./dist/fonttools*.whl > /dev/null 2>&1); then
- echo "Distribution packages already exists; skipping"
-else
- tox -e bdist
-fi
diff --git a/.travis/install.sh b/.travis/install.sh
index 03cc0b3d..f2a0717f 100755
--- a/.travis/install.sh
+++ b/.travis/install.sh
@@ -10,23 +10,20 @@ if [ "$TRAVIS_OS_NAME" == "osx" ]; then
# install pip on the system python
curl -O https://bootstrap.pypa.io/get-pip.py
python get-pip.py --user
- # install virtualenv and create virtual environment
python -m pip install --user virtualenv
python -m virtualenv .venv/
elif [[ ${TOXENV} == *"py3"* ]]; then
- # install/upgrade current python3 with homebrew
- if brew list --versions python3 > /dev/null; then
- brew upgrade python3
- else
- brew install python3
- fi
- # create virtual environment
- python3 -m venv .venv/
+ # install current python3 with homebrew
+ # NOTE: the formula is now named just "python"
+ brew install python
+ command -v python3
+ python3 --version
+ python3 -m pip install virtualenv
+ python3 -m virtualenv .venv/
else
echo "unsupported $TOXENV: "${TOXENV}
exit 1
fi
- # activate virtual environment
source .venv/bin/activate
fi
diff --git a/.travis/run.sh b/.travis/run.sh
index 6804f7dc..c6c1fea9 100755
--- a/.travis/run.sh
+++ b/.travis/run.sh
@@ -8,3 +8,7 @@ if [ "$TRAVIS_OS_NAME" == "osx" ]; then
fi
tox
+
+# re-run all the XML-related tests, this time without lxml but using the
+# built-in ElementTree library.
+tox -e ${TOXENV:-py}-nolxml -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py
diff --git a/Doc/source/ufoLib/converters.rst b/Doc/source/ufoLib/converters.rst
new file mode 100644
index 00000000..74aafbf8
--- /dev/null
+++ b/Doc/source/ufoLib/converters.rst
@@ -0,0 +1,9 @@
+.. highlight:: python
+
+==========
+converters
+==========
+
+.. automodule:: ufoLib.converters
+ :inherited-members:
+ :members:
diff --git a/Doc/source/ufoLib/filenames.rst b/Doc/source/ufoLib/filenames.rst
new file mode 100644
index 00000000..d604c4c5
--- /dev/null
+++ b/Doc/source/ufoLib/filenames.rst
@@ -0,0 +1,9 @@
+.. highlight:: python
+
+=========
+filenames
+=========
+
+.. automodule:: ufoLib.filenames
+ :inherited-members:
+ :members:
diff --git a/Doc/source/ufoLib/glifLib.rst b/Doc/source/ufoLib/glifLib.rst
new file mode 100644
index 00000000..ffe4672f
--- /dev/null
+++ b/Doc/source/ufoLib/glifLib.rst
@@ -0,0 +1,9 @@
+.. highlight:: python
+
+=======
+glifLib
+=======
+
+.. automodule:: ufoLib.glifLib
+ :inherited-members:
+ :members:
diff --git a/Doc/source/ufoLib/pointPen.rst b/Doc/source/ufoLib/pointPen.rst
new file mode 100644
index 00000000..6cb1ca17
--- /dev/null
+++ b/Doc/source/ufoLib/pointPen.rst
@@ -0,0 +1,9 @@
+.. highlight:: python
+
+========
+pointPen
+========
+
+.. automodule:: ufoLib.pointPen
+ :inherited-members:
+ :members:
diff --git a/Doc/source/ufoLib/ufoLib.rst b/Doc/source/ufoLib/ufoLib.rst
new file mode 100644
index 00000000..3b3b0b1a
--- /dev/null
+++ b/Doc/source/ufoLib/ufoLib.rst
@@ -0,0 +1,9 @@
+.. highlight:: python
+
+======
+ufoLib
+======
+
+.. automodule:: ufoLib
+ :inherited-members:
+ :members:
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index fb2b35cd..10eab303 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -5,6 +5,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "3.28.0"
+version = __version__ = "3.31.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index 7f19faf3..5a6942d3 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -493,7 +493,9 @@ def specializeCommands(commands,
if d0 is None: continue
new_op = d0+d+'curveto'
- if new_op and len(args1) + len(args2) <= maxstack:
+ # Make sure the stack depth does not exceed (maxstack - 1), so
+ # that subroutinizer can insert subroutine calls at any point.
+ if new_op and len(args1) + len(args2) < maxstack:
commands[i-1] = (new_op, args1+args2)
del commands[i]
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
index 8063ac57..24c2247f 100644
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -6,12 +6,8 @@ from fontTools.misc.loggingTools import LogMixin
import collections
import os
import posixpath
-import plistlib
-
-try:
- import xml.etree.cElementTree as ET
-except ImportError:
- import xml.etree.ElementTree as ET
+from fontTools.misc import etree as ET
+from fontTools.misc import plistlib
"""
designSpaceDocument
@@ -31,30 +27,6 @@ XML_NS = "{http://www.w3.org/XML/1998/namespace}"
XML_LANG = XML_NS + "lang"
-def to_plist(value):
- try:
- # Python 2
- string = plistlib.writePlistToString(value)
- except AttributeError:
- # Python 3
- string = plistlib.dumps(value).decode()
- return ET.fromstring(string)[0]
-
-
-def from_plist(element):
- if element is None:
- return {}
- plist = ET.Element('plist')
- plist.append(element)
- string = ET.tostring(plist)
- try:
- # Python 2
- return plistlib.readPlistFromString(string)
- except AttributeError:
- # Python 3
- return plistlib.loads(string, fmt=plistlib.FMT_XML)
-
-
def posix(path):
"""Normalize paths using forward slash to work also on Windows."""
new_path = posixpath.join(*path.split(os.path.sep))
@@ -88,24 +60,24 @@ class DesignSpaceDocumentError(Exception):
": %r" % self.obj if self.obj is not None else "")
-def _indent(elem, whitespace=" ", level=0):
- # taken from http://effbot.org/zone/element-lib.htm#prettyprint
- i = "\n" + level * whitespace
- if len(elem):
- if not elem.text or not elem.text.strip():
- elem.text = i + whitespace
- if not elem.tail or not elem.tail.strip():
- elem.tail = i
- for elem in elem:
- _indent(elem, whitespace, level+1)
- if not elem.tail or not elem.tail.strip():
- elem.tail = i
- else:
- if level and (not elem.tail or not elem.tail.strip()):
- elem.tail = i
+class AsDictMixin(object):
+
+ def asdict(self):
+ d = {}
+ for attr, value in self.__dict__.items():
+ if attr.startswith("_"):
+ continue
+ if hasattr(value, "asdict"):
+ value = value.asdict()
+ elif isinstance(value, list):
+ value = [
+ v.asdict() if hasattr(v, "asdict") else v for v in value
+ ]
+ d[attr] = value
+ return d
-class SimpleDescriptor(object):
+class SimpleDescriptor(AsDictMixin):
""" Containers for a bunch of attributes"""
# XXX this is ugly. The 'print' is inappropriate here, and instead of
@@ -346,6 +318,20 @@ class AxisDescriptor(SimpleDescriptor):
map=self.map,
)
+ def map_forward(self, v):
+ from fontTools.varLib.models import piecewiseLinearMap
+
+ if not self.map:
+ return v
+ return piecewiseLinearMap(v, {k: v for k, v in self.map})
+
+ def map_backward(self, v):
+ from fontTools.varLib.models import piecewiseLinearMap
+
+ if not self.map:
+ return v
+ return piecewiseLinearMap(v, {v: k for k, v in self.map})
+
class BaseDocWriter(object):
_whiteSpace = " "
@@ -379,7 +365,7 @@ class BaseDocWriter(object):
self._axes = [] # for use by the writer only
self._rules = [] # for use by the writer only
- def write(self, pretty=True):
+ def write(self, pretty=True, encoding="UTF-8", xml_declaration=True):
if self.documentObject.axes:
self.root.append(ET.Element("axes"))
for axisObject in self.documentObject.axes:
@@ -403,10 +389,14 @@ class BaseDocWriter(object):
if self.documentObject.lib:
self._addLib(self.documentObject.lib)
- if pretty:
- _indent(self.root, whitespace=self._whiteSpace)
tree = ET.ElementTree(self.root)
- tree.write(self.path, encoding="utf-8", method='xml', xml_declaration=True)
+ tree.write(
+ self.path,
+ encoding=encoding,
+ method='xml',
+ xml_declaration=xml_declaration,
+ pretty_print=pretty,
+ )
def _makeLocationElement(self, locationObject, name=None):
""" Convert Location dict to a locationElement."""
@@ -475,7 +465,7 @@ class BaseDocWriter(object):
axisElement.attrib['hidden'] = "1"
for languageCode, labelName in sorted(axisObject.labelNames.items()):
languageElement = ET.Element('labelname')
- languageElement.attrib[u'xml:lang'] = languageCode
+ languageElement.attrib[XML_LANG] = languageCode
languageElement.text = labelName
axisElement.append(languageElement)
if axisObject.map:
@@ -502,7 +492,7 @@ class BaseDocWriter(object):
if code == "en":
continue # already stored in the element attribute
localisedStyleNameElement = ET.Element('stylename')
- localisedStyleNameElement.attrib["xml:lang"] = code
+ localisedStyleNameElement.attrib[XML_LANG] = code
localisedStyleNameElement.text = instanceObject.getStyleName(code)
instanceElement.append(localisedStyleNameElement)
if instanceObject.localisedFamilyName:
@@ -512,7 +502,7 @@ class BaseDocWriter(object):
if code == "en":
continue # already stored in the element attribute
localisedFamilyNameElement = ET.Element('familyname')
- localisedFamilyNameElement.attrib["xml:lang"] = code
+ localisedFamilyNameElement.attrib[XML_LANG] = code
localisedFamilyNameElement.text = instanceObject.getFamilyName(code)
instanceElement.append(localisedFamilyNameElement)
if instanceObject.localisedStyleMapStyleName:
@@ -522,7 +512,7 @@ class BaseDocWriter(object):
if code == "en":
continue
localisedStyleMapStyleNameElement = ET.Element('stylemapstylename')
- localisedStyleMapStyleNameElement.attrib["xml:lang"] = code
+ localisedStyleMapStyleNameElement.attrib[XML_LANG] = code
localisedStyleMapStyleNameElement.text = instanceObject.getStyleMapStyleName(code)
instanceElement.append(localisedStyleMapStyleNameElement)
if instanceObject.localisedStyleMapFamilyName:
@@ -532,7 +522,7 @@ class BaseDocWriter(object):
if code == "en":
continue
localisedStyleMapFamilyNameElement = ET.Element('stylemapfamilyname')
- localisedStyleMapFamilyNameElement.attrib["xml:lang"] = code
+ localisedStyleMapFamilyNameElement.attrib[XML_LANG] = code
localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code)
instanceElement.append(localisedStyleMapFamilyNameElement)
@@ -563,7 +553,7 @@ class BaseDocWriter(object):
instanceElement.append(infoElement)
if instanceObject.lib:
libElement = ET.Element('lib')
- libElement.append(to_plist(instanceObject.lib))
+ libElement.append(plistlib.totree(instanceObject.lib, indent_level=4))
instanceElement.append(libElement)
self.root.findall('.instances')[0].append(instanceElement)
@@ -616,7 +606,7 @@ class BaseDocWriter(object):
def _addLib(self, dict):
libElement = ET.Element('lib')
- libElement.append(to_plist(dict))
+ libElement.append(plistlib.totree(dict, indent_level=2))
self.root.append(libElement)
def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data):
@@ -669,6 +659,13 @@ class BaseDocReader(LogMixin):
self.axisDefaults = {}
self._strictAxisNames = True
+ @classmethod
+ def fromstring(cls, string, documentObject):
+ f = BytesIO(tobytes(string, encoding="utf-8"))
+ self = cls(f, documentObject)
+ self.path = None
+ return self
+
def read(self):
self.readAxes()
self.readRules()
@@ -741,10 +738,10 @@ class BaseDocReader(LogMixin):
def readAxes(self):
# read the axes elements, including the warp map.
- if len(self.root.findall(".axes/axis")) == 0:
- self._strictAxisNames = False
+ axisElements = self.root.findall(".axes/axis")
+ if not axisElements:
return
- for axisElement in self.root.findall(".axes/axis"):
+ for axisElement in axisElements:
axisObject = self.axisDescriptorClass()
axisObject.name = axisElement.attrib.get("name")
axisObject.minimum = float(axisElement.attrib.get("minimum"))
@@ -758,7 +755,7 @@ class BaseDocReader(LogMixin):
b = float(mapElement.attrib['output'])
axisObject.map.append((a, b))
for labelNameElement in axisElement.findall('labelname'):
- # Note: elementtree reads the xml:lang attribute name as
+ # Note: elementtree reads the "xml:lang" attribute name as
# '{http://www.w3.org/XML/1998/namespace}lang'
for key, lang in labelNameElement.items():
if key == XML_LANG:
@@ -827,7 +824,7 @@ class BaseDocReader(LogMixin):
def readLocationElement(self, locationElement):
""" Format 0 location reader """
- if not self.documentObject.axes:
+ if self._strictAxisNames and not self.documentObject.axes:
raise DesignSpaceDocumentError("No axes defined")
loc = {}
for dimensionElement in locationElement.findall(".dimension"):
@@ -861,7 +858,7 @@ class BaseDocReader(LogMixin):
def _readSingleInstanceElement(self, instanceElement, makeGlyphs=True, makeKerning=True, makeInfo=True):
filename = instanceElement.attrib.get('filename')
- if filename is not None:
+ if filename is not None and self.documentObject.path is not None:
instancePath = os.path.join(os.path.dirname(self.documentObject.path), filename)
else:
instancePath = None
@@ -920,7 +917,7 @@ class BaseDocReader(LogMixin):
def readLibElement(self, libElement, instanceObject):
"""Read the lib element for the given instance."""
- instanceObject.lib = from_plist(libElement[0])
+ instanceObject.lib = plistlib.fromtree(libElement[0])
def readInfoElement(self, infoElement, instanceObject):
""" Read the info element."""
@@ -987,10 +984,10 @@ class BaseDocReader(LogMixin):
def readLib(self):
"""Read the lib element for the whole document."""
for libElement in self.root.findall(".lib"):
- self.documentObject.lib = from_plist(libElement[0])
+ self.documentObject.lib = plistlib.fromtree(libElement[0])
-class DesignSpaceDocument(LogMixin):
+class DesignSpaceDocument(LogMixin, AsDictMixin):
""" Read, write data from the designspace file"""
def __init__(self, readerClass=None, writerClass=None):
self.path = None
@@ -1024,6 +1021,37 @@ class DesignSpaceDocument(LogMixin):
else:
self.writerClass = BaseDocWriter
+ @classmethod
+ def fromfile(cls, path, readerClass=None, writerClass=None):
+ self = cls(readerClass=readerClass, writerClass=writerClass)
+ self.read(path)
+ return self
+
+ @classmethod
+ def fromstring(cls, string, readerClass=None, writerClass=None):
+ self = cls(readerClass=readerClass, writerClass=writerClass)
+ reader = self.readerClass.fromstring(string, self)
+ reader.read()
+ if self.sources:
+ self.findDefault()
+ return self
+
+ def tostring(self, encoding=None):
+ if encoding is unicode or (
+ encoding is not None and encoding.lower() == "unicode"
+ ):
+ f = UnicodeIO()
+ xml_declaration = False
+ elif encoding is None or encoding == "utf-8":
+ f = BytesIO()
+ encoding = "UTF-8"
+ xml_declaration = True
+ else:
+ raise ValueError("unsupported encoding: '%s'" % encoding)
+ writer = self.writerClass(f, self)
+ writer.write(encoding=encoding, xml_declaration=xml_declaration)
+ return f.getvalue()
+
def read(self, path):
self.path = path
self.filename = os.path.basename(path)
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index 3c2c5f66..7f33ca41 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -1077,6 +1077,9 @@ class SubtableStatement(Statement):
def __init__(self, location=None):
Statement.__init__(self, location)
+ def build(self, builder):
+ builder.add_subtable_break(self.location)
+
def asFea(self, indent=""):
return indent + "subtable;"
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 447ded59..957b01d6 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -59,6 +59,7 @@ class Builder(object):
self.lookupflag_ = 0
self.lookupflag_markFilterSet_ = None
self.language_systems = set()
+ self.seen_non_DFLT_script_ = False
self.named_lookups_ = {}
self.cur_lookup_ = None
self.cur_lookup_name_ = None
@@ -289,10 +290,14 @@ class Builder(object):
else:
params.SubfamilyNameID = 0
elif tag in self.featureNames_:
- assert tag in self.featureNames_ids_
- params = otTables.FeatureParamsStylisticSet()
- params.Version = 0
- params.UINameID = self.featureNames_ids_[tag]
+ if not self.featureNames_ids_:
+ # name table wasn't selected among the tables to build; skip
+ pass
+ else:
+ assert tag in self.featureNames_ids_
+ params = otTables.FeatureParamsStylisticSet()
+ params.Version = 0
+ params.UINameID = self.featureNames_ids_[tag]
elif tag in self.cv_parameters_:
params = otTables.FeatureParamsCharacterVariants()
params.Format = 0
@@ -616,6 +621,15 @@ class Builder(object):
raise FeatureLibError(
'If "languagesystem DFLT dflt" is present, it must be '
'the first of the languagesystem statements', location)
+ if script == "DFLT":
+ if self.seen_non_DFLT_script_:
+ raise FeatureLibError(
+ 'languagesystems using the "DFLT" script tag must '
+ "precede all other languagesystems",
+ location
+ )
+ else:
+ self.seen_non_DFLT_script_ = True
if (script, language) in self.default_language_systems_:
raise FeatureLibError(
'"languagesystem %s %s" has already been specified' %
@@ -688,31 +702,15 @@ class Builder(object):
raise FeatureLibError(
"Language statements are not allowed "
"within \"feature %s\"" % self.cur_feature_name_, location)
- if language != 'dflt' and self.script_ == 'DFLT':
- raise FeatureLibError("Need non-DFLT script when using non-dflt "
- "language (was: \"%s\")" % language, location)
self.cur_lookup_ = None
key = (self.script_, language, self.cur_feature_name_)
- if not include_default:
- # don't include any lookups added by script DFLT in this feature
- self.features_[key] = []
- elif language != 'dflt':
- # add rules defined between script statement and its first following
- # language statement to each of its explicitly specified languages:
- # http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html#4.b.ii
- lookups = self.features_.get((key[0], 'dflt', key[2]))
- dflt_lookups = self.features_.get(('DFLT', 'dflt', key[2]), [])
- if lookups:
- if key[:2] in self.get_default_language_systems_():
- lookups = [l for l in lookups if l not in dflt_lookups]
- self.features_.setdefault(key, []).extend(lookups)
- if self.script_ == 'DFLT':
- langsys = set(self.get_default_language_systems_())
+ lookups = self.features_.get((key[0], 'dflt', key[2]))
+ if (language == 'dflt' or include_default) and lookups:
+ self.features_[key] = lookups[:]
else:
- langsys = set()
- langsys.add((self.script_, language))
- self.language_systems = frozenset(langsys)
+ self.features_[key] = []
+ self.language_systems = frozenset([(self.script_, language)])
if required:
key = (self.script_, language)
@@ -997,6 +995,16 @@ class Builder(object):
lookup = self.get_lookup_(location, PairPosBuilder)
lookup.addClassPair(location, glyphclass1, value1, glyphclass2, value2)
+ def add_subtable_break(self, location):
+ if type(self.cur_lookup_) is not PairPosBuilder:
+ raise FeatureLibError(
+ 'explicit "subtable" statement is intended for use with only '
+ "Pair Adjustment Positioning Format 2 (i.e. pair class kerning)",
+ location
+ )
+ lookup = self.get_lookup_(location, PairPosBuilder)
+ lookup.add_subtable_break(location)
+
def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
lookup = self.get_lookup_(location, PairPosBuilder)
lookup.addGlyphPair(location, glyph1, value1, glyph2, value2)
@@ -1486,7 +1494,10 @@ class ClassPairPosSubtableBuilder(object):
return
st = otl.buildPairPosClassesSubtable(self.values_,
self.builder_.glyphMap)
+ if st.Coverage is None:
+ return
self.subtables_.append(st)
+ self.forceSubtableBreak_ = False
class PairPosBuilder(LookupBuilder):
@@ -1506,10 +1517,9 @@ class PairPosBuilder(LookupBuilder):
oldValue = self.glyphPairs.get(key, None)
if oldValue is not None:
# the Feature File spec explicitly allows specific pairs generated
- # by an 'enum' rule to be overridden by preceding single pairs;
- # we emit a warning and use the previously defined value
+ # by an 'enum' rule to be overridden by preceding single pairs
otherLoc = self.locations[key]
- log.warning(
+ log.debug(
'Already defined position for pair %s %s at %s:%d:%d; '
'choosing the first value',
glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2])
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index 2b2f2d47..30426987 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -1207,10 +1207,6 @@ class Parser(object):
script = self.expect_script_tag_()
language = self.expect_language_tag_()
self.expect_symbol_(";")
- if script == "DFLT" and language != "dflt":
- raise FeatureLibError(
- 'For script "DFLT", the language must be "dflt"',
- self.cur_token_location_)
return self.ast.LanguageSystemStatement(script, language,
location=location)
diff --git a/Lib/fontTools/misc/etree.py b/Lib/fontTools/misc/etree.py
new file mode 100644
index 00000000..08e43f0a
--- /dev/null
+++ b/Lib/fontTools/misc/etree.py
@@ -0,0 +1,482 @@
+"""Shim module exporting the same ElementTree API for lxml and
+xml.etree backends.
+
+When lxml is installed, it is automatically preferred over the built-in
+xml.etree module.
+On Python 2.7, the cElementTree module is preferred over the pure-python
+ElementTree module.
+
+Besides exporting a unified interface, this also defines extra functions
+or subclasses built-in ElementTree classes to add features that are
+only availble in lxml, like OrderedDict for attributes, pretty_print and
+iterwalk.
+"""
+from __future__ import absolute_import, unicode_literals
+from fontTools.misc.py23 import basestring, unicode, tounicode, open
+
+
+XML_DECLARATION = """<?xml version='1.0' encoding='%s'?>"""
+
+__all__ = [
+ # public symbols
+ "Comment",
+ "dump",
+ "Element",
+ "ElementTree",
+ "fromstring",
+ "fromstringlist",
+ "iselement",
+ "iterparse",
+ "parse",
+ "ParseError",
+ "PI",
+ "ProcessingInstruction",
+ "QName",
+ "SubElement",
+ "tostring",
+ "tostringlist",
+ "TreeBuilder",
+ "XML",
+ "XMLParser",
+ "register_namespace",
+]
+
+try:
+ from lxml.etree import *
+
+ _have_lxml = True
+except ImportError:
+ try:
+ from xml.etree.cElementTree import *
+
+ # the cElementTree version of XML function doesn't support
+ # the optional 'parser' keyword argument
+ from xml.etree.ElementTree import XML
+ except ImportError: # pragma: no cover
+ from xml.etree.ElementTree import *
+ _have_lxml = False
+
+ import sys
+
+ # dict is always ordered in python >= 3.6 and on pypy
+ PY36 = sys.version_info >= (3, 6)
+ try:
+ import __pypy__
+ except ImportError:
+ __pypy__ = None
+ _dict_is_ordered = bool(PY36 or __pypy__)
+ del PY36, __pypy__
+
+ if _dict_is_ordered:
+ _Attrib = dict
+ else:
+ from collections import OrderedDict as _Attrib
+
+ if isinstance(Element, type):
+ _Element = Element
+ else:
+ # in py27, cElementTree.Element cannot be subclassed, so
+ # we need to import the pure-python class
+ from xml.etree.ElementTree import Element as _Element
+
+ class Element(_Element):
+ """Element subclass that keeps the order of attributes."""
+
+ def __init__(self, tag, attrib=_Attrib(), **extra):
+ super(Element, self).__init__(tag)
+ self.attrib = _Attrib()
+ if attrib:
+ self.attrib.update(attrib)
+ if extra:
+ self.attrib.update(extra)
+
+ def SubElement(parent, tag, attrib=_Attrib(), **extra):
+ """Must override SubElement as well otherwise _elementtree.SubElement
+ fails if 'parent' is a subclass of Element object.
+ """
+ element = parent.__class__(tag, attrib, **extra)
+ parent.append(element)
+ return element
+
+ def _iterwalk(element, events, tag):
+ include = tag is None or element.tag == tag
+ if include and "start" in events:
+ yield ("start", element)
+ for e in element:
+ for item in _iterwalk(e, events, tag):
+ yield item
+ if include:
+ yield ("end", element)
+
+ def iterwalk(element_or_tree, events=("end",), tag=None):
+ """A tree walker that generates events from an existing tree as
+ if it was parsing XML data with iterparse().
+ Drop-in replacement for lxml.etree.iterwalk.
+ """
+ if iselement(element_or_tree):
+ element = element_or_tree
+ else:
+ element = element_or_tree.getroot()
+ if tag == "*":
+ tag = None
+ for item in _iterwalk(element, events, tag):
+ yield item
+
+ _ElementTree = ElementTree
+
+ class ElementTree(_ElementTree):
+ """ElementTree subclass that adds 'pretty_print' and 'doctype'
+ arguments to the 'write' method.
+ Currently these are only supported for the default XML serialization
+ 'method', and not also for "html" or "text", for these are delegated
+ to the base class.
+ """
+
+ def write(
+ self,
+ file_or_filename,
+ encoding=None,
+ xml_declaration=False,
+ method=None,
+ doctype=None,
+ pretty_print=False,
+ ):
+ if method and method != "xml":
+ # delegate to super-class
+ super(ElementTree, self).write(
+ file_or_filename,
+ encoding=encoding,
+ xml_declaration=xml_declaration,
+ method=method,
+ )
+ return
+
+ if encoding is unicode or (
+ encoding is not None and encoding.lower() == "unicode"
+ ):
+ if xml_declaration:
+ raise ValueError(
+ "Serialisation to unicode must not request an XML declaration"
+ )
+ write_declaration = False
+ encoding = "unicode"
+ elif xml_declaration is None:
+ # by default, write an XML declaration only for non-standard encodings
+ write_declaration = encoding is not None and encoding.upper() not in (
+ "ASCII",
+ "UTF-8",
+ "UTF8",
+ "US-ASCII",
+ )
+ else:
+ write_declaration = xml_declaration
+
+ if encoding is None:
+ encoding = "ASCII"
+
+ if pretty_print:
+ # NOTE this will modify the tree in-place
+ _indent(self._root)
+
+ with _get_writer(file_or_filename, encoding) as write:
+ if write_declaration:
+ write(XML_DECLARATION % encoding.upper())
+ if pretty_print:
+ write("\n")
+ if doctype:
+ write(_tounicode(doctype))
+ if pretty_print:
+ write("\n")
+
+ qnames, namespaces = _namespaces(self._root)
+ _serialize_xml(write, self._root, qnames, namespaces)
+
+ import io
+
+ def tostring(
+ element,
+ encoding=None,
+ xml_declaration=None,
+ method=None,
+ doctype=None,
+ pretty_print=False,
+ ):
+ """Custom 'tostring' function that uses our ElementTree subclass, with
+ pretty_print support.
+ """
+ stream = io.StringIO() if encoding == "unicode" else io.BytesIO()
+ ElementTree(element).write(
+ stream,
+ encoding=encoding,
+ xml_declaration=xml_declaration,
+ method=method,
+ doctype=doctype,
+ pretty_print=pretty_print,
+ )
+ return stream.getvalue()
+
+ # serialization support
+
+ import re
+
+ # Valid XML strings can include any Unicode character, excluding control
+ # characters, the surrogate blocks, FFFE, and FFFF:
+ # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ # Here we reversed the pattern to match only the invalid characters.
+ # For the 'narrow' python builds supporting only UCS-2, which represent
+ # characters beyond BMP as UTF-16 surrogate pairs, we need to pass through
+ # the surrogate block. I haven't found a more elegant solution...
+ UCS2 = sys.maxunicode < 0x10FFFF
+ if UCS2:
+ _invalid_xml_string = re.compile(
+ "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uFFFE-\uFFFF]"
+ )
+ else:
+ _invalid_xml_string = re.compile(
+ "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]"
+ )
+
+ def _tounicode(s):
+ """Test if a string is valid user input and decode it to unicode string
+ using ASCII encoding if it's a bytes string.
+ Reject all bytes/unicode input that contains non-XML characters.
+ Reject all bytes input that contains non-ASCII characters.
+ """
+ try:
+ s = tounicode(s, encoding="ascii", errors="strict")
+ except UnicodeDecodeError:
+ raise ValueError(
+ "Bytes strings can only contain ASCII characters. "
+ "Use unicode strings for non-ASCII characters.")
+ except AttributeError:
+ _raise_serialization_error(s)
+ if s and _invalid_xml_string.search(s):
+ raise ValueError(
+ "All strings must be XML compatible: Unicode or ASCII, "
+ "no NULL bytes or control characters"
+ )
+ return s
+
+ import contextlib
+
+ @contextlib.contextmanager
+ def _get_writer(file_or_filename, encoding):
+ # returns text write method and release all resources after using
+ try:
+ write = file_or_filename.write
+ except AttributeError:
+ # file_or_filename is a file name
+ f = open(
+ file_or_filename,
+ "w",
+ encoding="utf-8" if encoding == "unicode" else encoding,
+ errors="xmlcharrefreplace",
+ )
+ with f:
+ yield f.write
+ else:
+ # file_or_filename is a file-like object
+ # encoding determines if it is a text or binary writer
+ if encoding == "unicode":
+ # use a text writer as is
+ yield write
+ else:
+ # wrap a binary writer with TextIOWrapper
+ detach_buffer = False
+ if isinstance(file_or_filename, io.BufferedIOBase):
+ buf = file_or_filename
+ elif isinstance(file_or_filename, io.RawIOBase):
+ buf = io.BufferedWriter(file_or_filename)
+ detach_buffer = True
+ else:
+ # This is to handle passed objects that aren't in the
+ # IOBase hierarchy, but just have a write method
+ buf = io.BufferedIOBase()
+ buf.writable = lambda: True
+ buf.write = write
+ try:
+ # TextIOWrapper uses this methods to determine
+ # if BOM (for UTF-16, etc) should be added
+ buf.seekable = file_or_filename.seekable
+ buf.tell = file_or_filename.tell
+ except AttributeError:
+ pass
+ wrapper = io.TextIOWrapper(
+ buf,
+ encoding=encoding,
+ errors="xmlcharrefreplace",
+ newline="\n",
+ )
+ try:
+ yield wrapper.write
+ finally:
+ # Keep the original file open when the TextIOWrapper and
+ # the BufferedWriter are destroyed
+ wrapper.detach()
+ if detach_buffer:
+ buf.detach()
+
+ from xml.etree.ElementTree import _namespace_map
+
+ def _namespaces(elem):
+ # identify namespaces used in this tree
+
+ # maps qnames to *encoded* prefix:local names
+ qnames = {None: None}
+
+ # maps uri:s to prefixes
+ namespaces = {}
+
+ def add_qname(qname):
+ # calculate serialized qname representation
+ try:
+ qname = _tounicode(qname)
+ if qname[:1] == "{":
+ uri, tag = qname[1:].rsplit("}", 1)
+ prefix = namespaces.get(uri)
+ if prefix is None:
+ prefix = _namespace_map.get(uri)
+ if prefix is None:
+ prefix = "ns%d" % len(namespaces)
+ else:
+ prefix = _tounicode(prefix)
+ if prefix != "xml":
+ namespaces[uri] = prefix
+ if prefix:
+ qnames[qname] = "%s:%s" % (prefix, tag)
+ else:
+ qnames[qname] = tag # default element
+ else:
+ qnames[qname] = qname
+ except TypeError:
+ _raise_serialization_error(qname)
+
+ # populate qname and namespaces table
+ for elem in elem.iter():
+ tag = elem.tag
+ if isinstance(tag, QName):
+ if tag.text not in qnames:
+ add_qname(tag.text)
+ elif isinstance(tag, basestring):
+ if tag not in qnames:
+ add_qname(tag)
+ elif tag is not None and tag is not Comment and tag is not PI:
+ _raise_serialization_error(tag)
+ for key, value in elem.items():
+ if isinstance(key, QName):
+ key = key.text
+ if key not in qnames:
+ add_qname(key)
+ if isinstance(value, QName) and value.text not in qnames:
+ add_qname(value.text)
+ text = elem.text
+ if isinstance(text, QName) and text.text not in qnames:
+ add_qname(text.text)
+ return qnames, namespaces
+
+ def _serialize_xml(write, elem, qnames, namespaces, **kwargs):
+ tag = elem.tag
+ text = elem.text
+ if tag is Comment:
+ write("<!--%s-->" % _tounicode(text))
+ elif tag is ProcessingInstruction:
+ write("<?%s?>" % _tounicode(text))
+ else:
+ tag = qnames[_tounicode(tag) if tag is not None else None]
+ if tag is None:
+ if text:
+ write(_escape_cdata(text))
+ for e in elem:
+ _serialize_xml(write, e, qnames, None)
+ else:
+ write("<" + tag)
+ if namespaces:
+ for uri, prefix in sorted(
+ namespaces.items(), key=lambda x: x[1]
+ ): # sort on prefix
+ if prefix:
+ prefix = ":" + prefix
+ write(' xmlns%s="%s"' % (prefix, _escape_attrib(uri)))
+ attrs = elem.attrib
+ if attrs:
+ # try to keep existing attrib order
+ if len(attrs) <= 1 or type(attrs) is _Attrib:
+ items = attrs.items()
+ else:
+ # if plain dict, use lexical order
+ items = sorted(attrs.items())
+ for k, v in items:
+ if isinstance(k, QName):
+ k = _tounicode(k.text)
+ else:
+ k = _tounicode(k)
+ if isinstance(v, QName):
+ v = qnames[_tounicode(v.text)]
+ else:
+ v = _escape_attrib(v)
+ write(' %s="%s"' % (qnames[k], v))
+ if text is not None or len(elem):
+ write(">")
+ if text:
+ write(_escape_cdata(text))
+ for e in elem:
+ _serialize_xml(write, e, qnames, None)
+ write("</" + tag + ">")
+ else:
+ write("/>")
+ if elem.tail:
+ write(_escape_cdata(elem.tail))
+
+ def _raise_serialization_error(text):
+ raise TypeError(
+ "cannot serialize %r (type %s)" % (text, type(text).__name__)
+ )
+
+ def _escape_cdata(text):
+ # escape character data
+ try:
+ text = _tounicode(text)
+ # it's worth avoiding do-nothing calls for short strings
+ if "&" in text:
+ text = text.replace("&", "&amp;")
+ if "<" in text:
+ text = text.replace("<", "&lt;")
+ if ">" in text:
+ text = text.replace(">", "&gt;")
+ return text
+ except (TypeError, AttributeError):
+ _raise_serialization_error(text)
+
+ def _escape_attrib(text):
+ # escape attribute value
+ try:
+ text = _tounicode(text)
+ if "&" in text:
+ text = text.replace("&", "&amp;")
+ if "<" in text:
+ text = text.replace("<", "&lt;")
+ if ">" in text:
+ text = text.replace(">", "&gt;")
+ if '"' in text:
+ text = text.replace('"', "&quot;")
+ if "\n" in text:
+ text = text.replace("\n", "&#10;")
+ return text
+ except (TypeError, AttributeError):
+ _raise_serialization_error(text)
+
+ def _indent(elem, level=0):
+ # From http://effbot.org/zone/element-lib.htm#prettyprint
+ i = "\n" + level * " "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for elem in elem:
+ _indent(elem, level + 1)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
diff --git a/Lib/fontTools/misc/loggingTools.py b/Lib/fontTools/misc/loggingTools.py
index ebc0d0a7..aacc0858 100644
--- a/Lib/fontTools/misc/loggingTools.py
+++ b/Lib/fontTools/misc/loggingTools.py
@@ -8,7 +8,10 @@ import sys
import logging
import timeit
from functools import wraps
-import collections
+try:
+ from collections.abc import Mapping, Callable
+except ImportError: # python < 3.3
+ from collections import Mapping, Callable
import warnings
try:
@@ -64,7 +67,7 @@ class LevelFormatter(logging.Formatter):
if isinstance(fmt, basestring):
default_format = fmt
custom_formats = {}
- elif isinstance(fmt, collections.Mapping):
+ elif isinstance(fmt, Mapping):
custom_formats = dict(fmt)
default_format = custom_formats.pop("*", None)
else:
@@ -360,7 +363,7 @@ class Timer(object):
Timer instance, referencing the same logger.
A 'level' keyword can also be passed to override self.level.
"""
- if isinstance(func_or_msg, collections.Callable):
+ if isinstance(func_or_msg, Callable):
func = func_or_msg
# use the function name when no explicit 'msg' is provided
if not self.msg:
@@ -431,8 +434,8 @@ class ChannelsFilter(logging.Filter):
class CapturingLogHandler(logging.Handler):
def __init__(self, logger, level):
+ super(CapturingLogHandler, self).__init__(level=level)
self.records = []
- self.level = logging._checkLevel(level)
if isinstance(logger, basestring):
self.logger = logging.getLogger(logger)
else:
@@ -441,35 +444,35 @@ class CapturingLogHandler(logging.Handler):
def __enter__(self):
self.original_disabled = self.logger.disabled
self.original_level = self.logger.level
+ self.original_propagate = self.logger.propagate
self.logger.addHandler(self)
- self.logger.level = self.level
+ self.logger.setLevel(self.level)
self.logger.disabled = False
+ self.logger.propagate = False
return self
def __exit__(self, type, value, traceback):
self.logger.removeHandler(self)
- self.logger.level = self.original_level
- self.logger.disabled = self.logger.disabled
- return self
+ self.logger.setLevel(self.original_level)
+ self.logger.disabled = self.original_disabled
+ self.logger.propagate = self.original_propagate
- def handle(self, record):
- self.records.append(record)
+ return self
def emit(self, record):
- pass
-
- def createLock(self):
- self.lock = None
+ self.records.append(record)
- def assertRegex(self, regexp):
+ def assertRegex(self, regexp, msg=None):
import re
pattern = re.compile(regexp)
for r in self.records:
if pattern.search(r.getMessage()):
return True
- assert 0, "Pattern '%s' not found in logger records" % regexp
+ if msg is None:
+ msg = "Pattern '%s' not found in logger records" % regexp
+ assert 0, msg
class LogMixin(object):
diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py
new file mode 100644
index 00000000..fe695936
--- /dev/null
+++ b/Lib/fontTools/misc/plistlib.py
@@ -0,0 +1,540 @@
+from __future__ import absolute_import, unicode_literals
+import sys
+import re
+from io import BytesIO
+from datetime import datetime
+from base64 import b64encode, b64decode
+from numbers import Integral
+
+try:
+ from functools import singledispatch
+except ImportError:
+ try:
+ from singledispatch import singledispatch
+ except ImportError:
+ singledispatch = None
+
+from fontTools.misc import etree
+
+from fontTools.misc.py23 import (
+ unicode,
+ basestring,
+ tounicode,
+ tobytes,
+ SimpleNamespace,
+ range,
+)
+
+# On python3, by default we deserialize <data> elements as bytes, whereas on
+# python2 we deserialize <data> elements as plistlib.Data objects, in order
+# to distinguish them from the built-in str type (which is bytes on python2).
+# Similarly, by default on python3 we serialize bytes as <data> elements;
+# however, on python2 we serialize bytes as <string> elements (they must
+# only contain ASCII characters in this case).
+# You can pass use_builtin_types=[True|False] to load/dump etc. functions to
+# enforce the same treatment of bytes across python 2 and 3.
+# NOTE that unicode type always maps to <string> element, and plistlib.Data
+# always maps to <data> element, regardless of use_builtin_types.
+PY3 = sys.version_info[0] > 2
+if PY3:
+ USE_BUILTIN_TYPES = True
+else:
+ USE_BUILTIN_TYPES = False
+
+XML_DECLARATION = b"""<?xml version='1.0' encoding='UTF-8'?>"""
+
+PLIST_DOCTYPE = (
+ b'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
+ b'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
+)
+
+# Date should conform to a subset of ISO 8601:
+# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'
+_date_parser = re.compile(
+ r"(?P<year>\d\d\d\d)"
+ r"(?:-(?P<month>\d\d)"
+ r"(?:-(?P<day>\d\d)"
+ r"(?:T(?P<hour>\d\d)"
+ r"(?::(?P<minute>\d\d)"
+ r"(?::(?P<second>\d\d))"
+ r"?)?)?)?)?Z",
+ getattr(re, "ASCII", 0), # py3-only
+)
+
+
+def _date_from_string(s):
+ order = ("year", "month", "day", "hour", "minute", "second")
+ gd = _date_parser.match(s).groupdict()
+ lst = []
+ for key in order:
+ val = gd[key]
+ if val is None:
+ break
+ lst.append(int(val))
+ return datetime(*lst)
+
+
+def _date_to_string(d):
+ return "%04d-%02d-%02dT%02d:%02d:%02dZ" % (
+ d.year,
+ d.month,
+ d.day,
+ d.hour,
+ d.minute,
+ d.second,
+ )
+
+
+def _encode_base64(data, maxlinelength=76, indent_level=1):
+ data = b64encode(data)
+ if data and maxlinelength:
+ # split into multiple lines right-justified to 'maxlinelength' chars
+ indent = b"\n" + b" " * indent_level
+ max_length = max(16, maxlinelength - len(indent))
+ chunks = []
+ for i in range(0, len(data), max_length):
+ chunks.append(indent)
+ chunks.append(data[i : i + max_length])
+ chunks.append(indent)
+ data = b"".join(chunks)
+ return data
+
+
+class Data:
+ """Wrapper for binary data returned in place of the built-in bytes type
+ when loading property list data with use_builtin_types=False.
+ """
+
+ def __init__(self, data):
+ if not isinstance(data, bytes):
+ raise TypeError("Expected bytes, found %s" % type(data).__name__)
+ self.data = data
+
+ @classmethod
+ def fromBase64(cls, data):
+ return cls(b64decode(data))
+
+ def asBase64(self, maxlinelength=76, indent_level=1):
+ return _encode_base64(
+ self.data, maxlinelength=maxlinelength, indent_level=indent_level
+ )
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.data == other.data
+ elif isinstance(other, bytes):
+ return self.data == other
+ else:
+ return NotImplemented
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, repr(self.data))
+
+
+class PlistTarget(object):
+ """ Event handler using the ElementTree Target API that can be
+ passed to a XMLParser to produce property list objects from XML.
+ It is based on the CPython plistlib module's _PlistParser class,
+ but does not use the expat parser.
+
+ >>> from fontTools.misc import etree
+ >>> parser = etree.XMLParser(target=PlistTarget())
+ >>> result = etree.XML(
+ ... "<dict>"
+ ... " <key>something</key>"
+ ... " <string>blah</string>"
+ ... "</dict>",
+ ... parser=parser)
+ >>> result == {"something": "blah"}
+ True
+
+ Links:
+ https://github.com/python/cpython/blob/master/Lib/plistlib.py
+ http://lxml.de/parsing.html#the-target-parser-interface
+ """
+
+ def __init__(self, use_builtin_types=None, dict_type=dict):
+ self.stack = []
+ self.current_key = None
+ self.root = None
+ if use_builtin_types is None:
+ self._use_builtin_types = USE_BUILTIN_TYPES
+ else:
+ self._use_builtin_types = use_builtin_types
+ self._dict_type = dict_type
+
+ def start(self, tag, attrib):
+ self._data = []
+ handler = _TARGET_START_HANDLERS.get(tag)
+ if handler is not None:
+ handler(self)
+
+ def end(self, tag):
+ handler = _TARGET_END_HANDLERS.get(tag)
+ if handler is not None:
+ handler(self)
+
+ def data(self, data):
+ self._data.append(data)
+
+ def close(self):
+ return self.root
+
+ # helpers
+
+ def add_object(self, value):
+ if self.current_key is not None:
+ if not isinstance(self.stack[-1], type({})):
+ raise ValueError("unexpected element: %r" % self.stack[-1])
+ self.stack[-1][self.current_key] = value
+ self.current_key = None
+ elif not self.stack:
+ # this is the root object
+ self.root = value
+ else:
+ if not isinstance(self.stack[-1], type([])):
+ raise ValueError("unexpected element: %r" % self.stack[-1])
+ self.stack[-1].append(value)
+
+ def get_data(self):
+ data = "".join(self._data)
+ self._data = []
+ return data
+
+
+# event handlers
+
+
+def start_dict(self):
+ d = self._dict_type()
+ self.add_object(d)
+ self.stack.append(d)
+
+
+def end_dict(self):
+ if self.current_key:
+ raise ValueError("missing value for key '%s'" % self.current_key)
+ self.stack.pop()
+
+
+def end_key(self):
+ if self.current_key or not isinstance(self.stack[-1], type({})):
+ raise ValueError("unexpected key")
+ self.current_key = self.get_data()
+
+
+def start_array(self):
+ a = []
+ self.add_object(a)
+ self.stack.append(a)
+
+
+def end_array(self):
+ self.stack.pop()
+
+
+def end_true(self):
+ self.add_object(True)
+
+
+def end_false(self):
+ self.add_object(False)
+
+
+def end_integer(self):
+ self.add_object(int(self.get_data()))
+
+
+def end_real(self):
+ self.add_object(float(self.get_data()))
+
+
+def end_string(self):
+ self.add_object(self.get_data())
+
+
+def end_data(self):
+ if self._use_builtin_types:
+ self.add_object(b64decode(self.get_data()))
+ else:
+ self.add_object(Data.fromBase64(self.get_data()))
+
+
+def end_date(self):
+ self.add_object(_date_from_string(self.get_data()))
+
+
+_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array}
+
+_TARGET_END_HANDLERS = {
+ "dict": end_dict,
+ "array": end_array,
+ "key": end_key,
+ "true": end_true,
+ "false": end_false,
+ "integer": end_integer,
+ "real": end_real,
+ "string": end_string,
+ "data": end_data,
+ "date": end_date,
+}
+
+
+# functions to build element tree from plist data
+
+
+def _string_element(value, ctx):
+ el = etree.Element("string")
+ el.text = value
+ return el
+
+
+def _bool_element(value, ctx):
+ if value:
+ return etree.Element("true")
+ else:
+ return etree.Element("false")
+
+
+def _integer_element(value, ctx):
+ if -1 << 63 <= value < 1 << 64:
+ el = etree.Element("integer")
+ el.text = "%d" % value
+ return el
+ else:
+ raise OverflowError(value)
+
+
+def _real_element(value, ctx):
+ el = etree.Element("real")
+ el.text = repr(value)
+ return el
+
+
+def _dict_element(d, ctx):
+ el = etree.Element("dict")
+ items = d.items()
+ if ctx.sort_keys:
+ items = sorted(items)
+ ctx.indent_level += 1
+ for key, value in items:
+ if not isinstance(key, basestring):
+ if ctx.skipkeys:
+ continue
+ raise TypeError("keys must be strings")
+ k = etree.SubElement(el, "key")
+ k.text = tounicode(key, "utf-8")
+ el.append(_make_element(value, ctx))
+ ctx.indent_level -= 1
+ return el
+
+
+def _array_element(array, ctx):
+ el = etree.Element("array")
+ if len(array) == 0:
+ return el
+ ctx.indent_level += 1
+ for value in array:
+ el.append(_make_element(value, ctx))
+ ctx.indent_level -= 1
+ return el
+
+
+def _date_element(date, ctx):
+ el = etree.Element("date")
+ el.text = _date_to_string(date)
+ return el
+
+
+def _data_element(data, ctx):
+ el = etree.Element("data")
+ el.text = _encode_base64(
+ data,
+ maxlinelength=(76 if ctx.pretty_print else None),
+ indent_level=ctx.indent_level,
+ )
+ return el
+
+
+def _string_or_data_element(raw_bytes, ctx):
+ if ctx.use_builtin_types:
+ return _data_element(raw_bytes, ctx)
+ else:
+ try:
+ string = raw_bytes.decode(encoding="ascii", errors="strict")
+ except UnicodeDecodeError:
+ raise ValueError(
+ "invalid non-ASCII bytes; use unicode string instead: %r"
+ % raw_bytes
+ )
+ return _string_element(string, ctx)
+
+
+# if singledispatch is available, we use a generic '_make_element' function
+# and register overloaded implementations that are run based on the type of
+# the first argument
+
+if singledispatch is not None:
+
+ @singledispatch
+ def _make_element(value, ctx):
+ raise TypeError("unsupported type: %s" % type(value))
+
+ _make_element.register(unicode)(_string_element)
+ _make_element.register(bool)(_bool_element)
+ _make_element.register(Integral)(_integer_element)
+ _make_element.register(float)(_real_element)
+ _make_element.register(dict)(_dict_element)
+ _make_element.register(list)(_array_element)
+ _make_element.register(tuple)(_array_element)
+ _make_element.register(datetime)(_date_element)
+ _make_element.register(bytes)(_string_or_data_element)
+ _make_element.register(bytearray)(_data_element)
+ _make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx))
+
+else:
+ # otherwise we use a long switch-like if statement
+
+ def _make_element(value, ctx):
+ if isinstance(value, unicode):
+ return _string_element(value, ctx)
+ elif isinstance(value, bool):
+ return _bool_element(value, ctx)
+ elif isinstance(value, Integral):
+ return _integer_element(value, ctx)
+ elif isinstance(value, float):
+ return _real_element(value, ctx)
+ elif isinstance(value, dict):
+ return _dict_element(value, ctx)
+ elif isinstance(value, (list, tuple)):
+ return _array_element(value, ctx)
+ elif isinstance(value, datetime):
+ return _date_element(value, ctx)
+ elif isinstance(value, bytes):
+ return _string_or_data_element(value, ctx)
+ elif isinstance(value, bytearray):
+ return _data_element(value, ctx)
+ elif isinstance(value, Data):
+ return _data_element(value.data, ctx)
+
+
+# Public functions to create element tree from plist-compatible python
+# data structures and viceversa, for use when (de)serializing GLIF xml.
+
+
+def totree(
+ value,
+ sort_keys=True,
+ skipkeys=False,
+ use_builtin_types=None,
+ pretty_print=True,
+ indent_level=1,
+):
+ if use_builtin_types is None:
+ use_builtin_types = USE_BUILTIN_TYPES
+ else:
+ use_builtin_types = use_builtin_types
+ context = SimpleNamespace(
+ sort_keys=sort_keys,
+ skipkeys=skipkeys,
+ use_builtin_types=use_builtin_types,
+ pretty_print=pretty_print,
+ indent_level=indent_level,
+ )
+ return _make_element(value, context)
+
+
+def fromtree(tree, use_builtin_types=None, dict_type=dict):
+ target = PlistTarget(
+ use_builtin_types=use_builtin_types, dict_type=dict_type
+ )
+ for action, element in etree.iterwalk(tree, events=("start", "end")):
+ if action == "start":
+ target.start(element.tag, element.attrib)
+ elif action == "end":
+ # if there are no children, parse the leaf's data
+ if not len(element):
+ # always pass str, not None
+ target.data(element.text or "")
+ target.end(element.tag)
+ return target.close()
+
+
+# python3 plistlib API
+
+
+def load(fp, use_builtin_types=None, dict_type=dict):
+ if not hasattr(fp, "read"):
+ raise AttributeError(
+ "'%s' object has no attribute 'read'" % type(fp).__name__
+ )
+ target = PlistTarget(
+ use_builtin_types=use_builtin_types, dict_type=dict_type
+ )
+ parser = etree.XMLParser(target=target)
+ result = etree.parse(fp, parser=parser)
+ # lxml returns the target object directly, while ElementTree wraps
+ # it as the root of an ElementTree object
+ try:
+ return result.getroot()
+ except AttributeError:
+ return result
+
+
+def loads(value, use_builtin_types=None, dict_type=dict):
+ fp = BytesIO(value)
+ return load(fp, use_builtin_types=use_builtin_types, dict_type=dict_type)
+
+
+def dump(
+ value,
+ fp,
+ sort_keys=True,
+ skipkeys=False,
+ use_builtin_types=None,
+ pretty_print=True,
+):
+ if not hasattr(fp, "write"):
+ raise AttributeError(
+ "'%s' object has no attribute 'write'" % type(fp).__name__
+ )
+ root = etree.Element("plist", version="1.0")
+ el = totree(
+ value,
+ sort_keys=sort_keys,
+ skipkeys=skipkeys,
+ use_builtin_types=use_builtin_types,
+ pretty_print=pretty_print,
+ )
+ root.append(el)
+ tree = etree.ElementTree(root)
+ # we write the doctype ourselves instead of using the 'doctype' argument
+ # of 'write' method, becuse lxml will force adding a '\n' even when
+ # pretty_print is False.
+ if pretty_print:
+ header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b""))
+ else:
+ header = XML_DECLARATION + PLIST_DOCTYPE
+ fp.write(header)
+ tree.write(
+ fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False
+ )
+
+
+def dumps(
+ value,
+ sort_keys=True,
+ skipkeys=False,
+ use_builtin_types=None,
+ pretty_print=True,
+):
+ fp = BytesIO()
+ dump(
+ value,
+ fp,
+ sort_keys=sort_keys,
+ skipkeys=skipkeys,
+ use_builtin_types=use_builtin_types,
+ pretty_print=pretty_print,
+ )
+ return fp.getvalue()
diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py
index ff782716..a92802b0 100644
--- a/Lib/fontTools/misc/psCharStrings.py
+++ b/Lib/fontTools/misc/psCharStrings.py
@@ -216,8 +216,12 @@ encodeIntT1 = getIntEncoder("t1")
encodeIntT2 = getIntEncoder("t2")
def encodeFixed(f, pack=struct.pack):
- # For T2 only
- return b"\xff" + pack(">l", otRound(f * 65536))
+ """For T2 only"""
+ value = otRound(f * 65536) # convert the float to fixed point
+ if value & 0xFFFF == 0: # check if the fractional part is zero
+ return encodeIntT2(value >> 16) # encode only the integer part
+ else:
+ return b"\xff" + pack(">l", value) # encode the entire fixed point value
def encodeFloat(f):
# For CFF only, used in cffLib
@@ -944,7 +948,7 @@ class T2CharString(object):
decompilerClass = SimpleT2Decompiler
outlineExtractor = T2OutlineExtractor
isCFF2 = False
-
+
def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None):
if program is None:
program = []
@@ -1006,8 +1010,7 @@ class T2CharString(object):
while i < end:
token = program[i]
i = i + 1
- tp = type(token)
- if issubclass(tp, basestring):
+ if isinstance(token, basestring):
try:
bytecode.extend(bytechr(b) for b in opcodes[token])
except KeyError:
@@ -1015,12 +1018,12 @@ class T2CharString(object):
if token in ('hintmask', 'cntrmask'):
bytecode.append(program[i]) # hint mask
i = i + 1
- elif tp == int:
+ elif isinstance(token, int):
bytecode.append(encodeInt(token))
- elif tp == float:
+ elif isinstance(token, float):
bytecode.append(encodeFixed(token))
else:
- assert 0, "unsupported type: %s" % tp
+ assert 0, "unsupported type: %s" % type(token)
try:
bytecode = bytesjoin(bytecode)
except TypeError:
@@ -1259,12 +1262,12 @@ class DictDecompiler(object):
"""
There may be non-blend args at the top of the stack. We first calculate
where the blend args start in the stack. These are the last
- numMasters*numBlends) +1 args.
+ numMasters*numBlends) +1 args.
The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by
numBlends list of values. Each of value in one of these lists is the
Variable Font delta for the matching region.
-
- We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by
+
+ We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by
the delta values. We then convert the default values, the first item in each entry, to an absolute value.
"""
vsindex = self.dict.get('vsindex', 0)
diff --git a/Lib/fontTools/misc/psLib.py b/Lib/fontTools/misc/psLib.py
index aa01eed7..e069e7b6 100644
--- a/Lib/fontTools/misc/psLib.py
+++ b/Lib/fontTools/misc/psLib.py
@@ -3,7 +3,10 @@ from fontTools.misc.py23 import *
from fontTools.misc import eexec
from .psOperators import *
import re
-import collections
+try:
+ from collections.abc import Callable
+except ImportError: # python < 3.3
+ from collections import Callable
from string import whitespace
import logging
@@ -169,7 +172,7 @@ class PSInterpreter(PSOperators):
def suckoperators(self, systemdict, klass):
for name in dir(klass):
attr = getattr(self, name)
- if isinstance(attr, collections.Callable) and name[:3] == 'ps_':
+ if isinstance(attr, Callable) and name[:3] == 'ps_':
name = name[3:]
systemdict[name] = ps_operator(name, attr)
for baseclass in klass.__bases__:
diff --git a/Lib/fontTools/misc/testTools.py b/Lib/fontTools/misc/testTools.py
index 8a37b7e2..3759d4ed 100644
--- a/Lib/fontTools/misc/testTools.py
+++ b/Lib/fontTools/misc/testTools.py
@@ -2,7 +2,10 @@
from __future__ import (print_function, division, absolute_import,
unicode_literals)
-import collections
+try:
+ from collections.abc import Iterable
+except ImportError: # python < 3.3
+ from collections import Iterable
import os
import shutil
import sys
@@ -29,7 +32,7 @@ def parseXML(xmlSnippet):
xml += xmlSnippet
elif isinstance(xmlSnippet, unicode):
xml += tobytes(xmlSnippet, 'utf-8')
- elif isinstance(xmlSnippet, collections.Iterable):
+ elif isinstance(xmlSnippet, Iterable):
xml += b"".join(tobytes(s, 'utf-8') for s in xmlSnippet)
else:
raise TypeError("expected string or sequence of strings; found %r"
diff --git a/Lib/fontTools/misc/xmlWriter.py b/Lib/fontTools/misc/xmlWriter.py
index 3f30ab5d..d625d3ae 100644
--- a/Lib/fontTools/misc/xmlWriter.py
+++ b/Lib/fontTools/misc/xmlWriter.py
@@ -50,6 +50,12 @@ class XMLWriter(object):
self._writeraw('<?xml version="1.0" encoding="UTF-8"?>')
self.newline()
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ self.close()
+
def close(self):
if self._closeStream:
self.file.close()
diff --git a/Lib/fontTools/pens/pointPen.py b/Lib/fontTools/pens/pointPen.py
new file mode 100644
index 00000000..641eb446
--- /dev/null
+++ b/Lib/fontTools/pens/pointPen.py
@@ -0,0 +1,413 @@
+"""
+=========
+PointPens
+=========
+
+Where **SegmentPens** have an intuitive approach to drawing
+(if you're familiar with postscript anyway), the **PointPen**
+is geared towards accessing all the data in the contours of
+the glyph. A PointPen has a very simple interface, it just
+steps through all the points in a call from glyph.drawPoints().
+This allows the caller to provide more data for each point.
+For instance, whether or not a point is smooth, and its name.
+"""
+from __future__ import absolute_import, unicode_literals
+from fontTools.pens.basePen import AbstractPen
+import math
+
+__all__ = [
+ "AbstractPointPen",
+ "BasePointToSegmentPen",
+ "PointToSegmentPen",
+ "SegmentToPointPen",
+ "GuessSmoothPointPen",
+ "ReverseContourPointPen",
+]
+
+
+class AbstractPointPen(object):
+ """
+ Baseclass for all PointPens.
+ """
+
+ def beginPath(self, identifier=None, **kwargs):
+ """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,
+ identifier=None, **kwargs):
+ """Add a point to the current sub path."""
+ raise NotImplementedError
+
+ def addComponent(self, baseGlyphName, transformation, identifier=None,
+ **kwargs):
+ """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, **kwargs):
+ 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 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, "illegal move segment point count: %d" % len(points)
+ 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, "illegal line segment point count: %d" % len(points)
+ 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, **kwargs):
+ 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 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)
+
+
+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
+ # a place to store the points for the current sub path
+ self.currentContour = None
+
+ def _flushContour(self):
+ pen = self.pen
+ contour = self.currentContour
+ if not contour:
+ pen.beginPath(identifier=self.currentContourIdentifier)
+ 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(identifier=self.currentContourIdentifier)
+ for pt, nextSegmentType, smooth, name, kwargs in contour:
+ if nextSegmentType is not None:
+ segmentType = lastSegmentType
+ lastSegmentType = nextSegmentType
+ else:
+ segmentType = None
+ pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs)
+ pen.endPath()
+
+ def beginPath(self, identifier=None, **kwargs):
+ assert self.currentContour is None
+ self.currentContour = []
+ self.currentContourIdentifier = identifier
+ 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, kwargs))
+
+ def addComponent(self, glyphName, transform, identifier=None, **kwargs):
+ assert self.currentContour is None
+ self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 8468b7a3..6ed54e5f 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -145,6 +145,10 @@ Glyph set expansion:
--no-recommended-glyphs
Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in
glyph set. [default]
+ --no-layout-closure
+ Do not expand glyph set to add glyphs produced by OpenType layout
+ features. Instead, OpenType layout features will be subset to only
+ rules that are relevant to the otherwise-specified glyph set.
--layout-features[+|-]=<feature>[,<feature>...]
Specify (=), add to (+=) or exclude from (-=) the comma-separated
set of OpenType layout feature tags that will be preserved.
@@ -168,6 +172,10 @@ Glyph set expansion:
* Keep all features.
--layout-features+=aalt --layout-features-=vrt2
* Keep default set of features plus 'aalt', but drop 'vrt2'.
+ --layout-scripts[+|-]=<script>[,<script>...]
+ Specify (=), add to (+=) or exclude from (-=) the comma-separated
+ set of OpenType layout script tags that will be preserved. By
+ default all scripts are retained ('*').
Hinting options:
--hinting
@@ -464,10 +472,10 @@ def closure_glyphs(self, s, cur_glyphs):
@_add_method(otTables.AlternateSubst)
def subset_glyphs(self, s):
- self.alternates = {g:vlist
+ self.alternates = {g:[v for v in vlist if v in s.glyphs]
for g,vlist in self.alternates.items()
if g in s.glyphs and
- all(v in s.glyphs for v in vlist)}
+ any(v in s.glyphs for v in vlist)}
return bool(self.alternates)
@_add_method(otTables.LigatureSubst)
@@ -1408,7 +1416,7 @@ def closure_glyphs(self, s):
del s.table
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def subset_glyphs(self, s):
s.glyphs = s.glyphs_gsubed
if self.table.LookupList:
@@ -1419,14 +1427,14 @@ def subset_glyphs(self, s):
return True
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def retain_empty_scripts(self):
# https://github.com/behdad/fonttools/issues/518
# https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15
return self.__class__ == ttLib.getTableClass('GSUB')
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def subset_lookups(self, lookup_indices):
"""Retains specified lookups, then removes empty features, language
systems, and scripts."""
@@ -1447,14 +1455,14 @@ def subset_lookups(self, lookup_indices):
self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts())
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def neuter_lookups(self, lookup_indices):
"""Sets lookups not in lookup_indices to None."""
if self.table.LookupList:
self.table.LookupList.neuter_lookups(lookup_indices)
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def prune_lookups(self, remap=True):
"""Remove (default) or neuter unreferenced lookups"""
if self.table.ScriptList:
@@ -1478,7 +1486,7 @@ def prune_lookups(self, remap=True):
self.neuter_lookups(lookup_indices)
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def subset_feature_tags(self, feature_tags):
if self.table.FeatureList:
feature_indices = \
@@ -1493,6 +1501,15 @@ def subset_feature_tags(self, feature_tags):
self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts())
@_add_method(ttLib.getTableClass('GSUB'),
+ ttLib.getTableClass('GPOS'))
+def subset_script_tags(self, script_tags):
+ if self.table.ScriptList:
+ self.table.ScriptList.ScriptRecord = \
+ [s for s in self.table.ScriptList.ScriptRecord
+ if s.ScriptTag in script_tags]
+ self.table.ScriptList.ScriptCount = len(self.table.ScriptList.ScriptRecord)
+
+@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def prune_features(self):
"""Remove unreferenced features"""
@@ -1508,9 +1525,11 @@ def prune_features(self):
self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts())
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def prune_pre_subset(self, font, options):
# Drop undesired features
+ if '*' not in options.layout_scripts:
+ self.subset_script_tags(options.layout_scripts)
if '*' not in options.layout_features:
self.subset_feature_tags(options.layout_features)
# Neuter unreferenced lookups
@@ -1518,7 +1537,7 @@ def prune_pre_subset(self, font, options):
return True
@_add_method(ttLib.getTableClass('GSUB'),
- ttLib.getTableClass('GPOS'))
+ ttLib.getTableClass('GPOS'))
def remove_redundant_langsys(self):
table = self.table
if not table.ScriptList or not table.FeatureList:
@@ -2503,7 +2522,8 @@ def prune_post_subset(self, font, options):
for k in ['BlueValues', 'OtherBlues',
'FamilyBlues', 'FamilyOtherBlues',
'BlueScale', 'BlueShift', 'BlueFuzz',
- 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']:
+ 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW',
+ 'ForceBold', 'LanguageGroup', 'ExpansionFactor']:
if hasattr(priv, k):
setattr(priv, k, None)
@@ -2669,6 +2689,10 @@ def prune_pre_subset(self, font, options):
nameIDs.update([inst.subfamilyNameID for inst in fvar.instances])
nameIDs.update([inst.postscriptNameID for inst in fvar.instances
if inst.postscriptNameID != 0xFFFF])
+ stat = font.get('STAT')
+ if stat:
+ nameIDs.update([val_rec.ValueNameID for val_rec in stat.table.AxisValueArray.AxisValue])
+ nameIDs.update([axis_rec.AxisNameID for axis_rec in stat.table.DesignAxisRecord.Axis])
if '*' not in options.name_IDs:
self.names = [n for n in self.names if n.nameID in nameIDs]
if not options.name_legacy:
@@ -2754,7 +2778,9 @@ class Options(object):
self.passthrough_tables = False # keep/drop tables we can't subset
self.hinting_tables = self._hinting_tables_default[:]
self.legacy_kern = False # drop 'kern' table if GPOS available
+ self.layout_closure = True
self.layout_features = self._layout_features_default[:]
+ self.layout_scripts = ['*']
self.ignore_missing_glyphs = False
self.ignore_missing_unicodes = True
self.hinting = True
@@ -2963,7 +2989,7 @@ class Subsetter(object):
self.glyphs.add(font.getGlyphName(i))
log.info("Added first four glyphs to subset")
- if 'GSUB' in font:
+ if self.options.layout_closure and 'GSUB' in font:
with timer("close glyph list over 'GSUB'"):
log.info("Closing glyph list over 'GSUB': %d glyphs before",
len(self.glyphs))
@@ -3137,8 +3163,6 @@ def load_font(fontFile,
@timer("compile and save font")
def save_font(font, outfile, options):
- if options.flavor and not hasattr(font, 'flavor'):
- raise Exception("fonttools version does not support flavors.")
if options.with_zopfli and options.flavor == "woff":
from fontTools.ttLib import sfnt
sfnt.USE_ZOPFLI = True
@@ -3215,8 +3239,7 @@ def main(args=None):
args = args[1:]
subsetter = Subsetter(options=options)
- basename, extension = splitext(fontfile)
- outfile = basename + '.subset' + extension
+ outfile = None
glyphs = []
gids = []
unicodes = []
@@ -3268,6 +3291,14 @@ def main(args=None):
dontLoadGlyphNames = not options.glyph_names and not glyphs
font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames)
+ if outfile is None:
+ basename, _ = splitext(fontfile)
+ if options.flavor is not None:
+ ext = "." + options.flavor.lower()
+ else:
+ ext = ".ttf" if font.sfntVersion == "\0\1\0\0" else ".otf"
+ outfile = basename + ".subset" + ext
+
with timer("compile glyph list"):
if wildcard_glyphs:
glyphs.extend(font.getGlyphOrder())
diff --git a/Lib/fontTools/svgLib/path/__init__.py b/Lib/fontTools/svgLib/path/__init__.py
index 4f17e766..690475f2 100644
--- a/Lib/fontTools/svgLib/path/__init__.py
+++ b/Lib/fontTools/svgLib/path/__init__.py
@@ -3,13 +3,9 @@ from __future__ import (
from fontTools.misc.py23 import *
from fontTools.pens.transformPen import TransformPen
+from fontTools.misc import etree
from .parser import parse_path
-try:
- from xml.etree import cElementTree as ElementTree # python 2
-except ImportError: # pragma nocover
- from xml.etree import ElementTree # python 3
-
__all__ = [tostr(s) for s in ("SVGPath", "parse_path")]
@@ -39,16 +35,16 @@ class SVGPath(object):
def __init__(self, filename=None, transform=None):
if filename is None:
- self.root = ElementTree.ElementTree()
+ self.root = etree.ElementTree()
else:
- tree = ElementTree.parse(filename)
+ tree = etree.parse(filename)
self.root = tree.getroot()
self.transform = transform
@classmethod
def fromstring(cls, data, transform=None):
self = cls(transform=transform)
- self.root = ElementTree.fromstring(data)
+ self.root = etree.fromstring(data)
return self
def draw(self, pen):
diff --git a/Lib/fontTools/ttLib/tables/C_P_A_L_.py b/Lib/fontTools/ttLib/tables/C_P_A_L_.py
index 25d50a5e..c687c7a1 100644
--- a/Lib/fontTools/ttLib/tables/C_P_A_L_.py
+++ b/Lib/fontTools/ttLib/tables/C_P_A_L_.py
@@ -56,8 +56,7 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
if offset == 0:
return [0] * numElements
result = array.array("H", data[offset : offset + 2 * numElements])
- if sys.byteorder != "big":
- result.byteswap()
+ if sys.byteorder != "big": result.byteswap()
assert len(result) == numElements, result
return result.tolist()
@@ -65,8 +64,7 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
if offset == 0:
return [0] * numElements
result = array.array("I", data[offset : offset + 4 * numElements])
- if sys.byteorder != "big":
- result.byteswap()
+ if sys.byteorder != "big": result.byteswap()
assert len(result) == numElements, result
return result.tolist()
diff --git a/Lib/fontTools/ttLib/tables/DefaultTable.py b/Lib/fontTools/ttLib/tables/DefaultTable.py
index f0e82f5d..8ad36e16 100644
--- a/Lib/fontTools/ttLib/tables/DefaultTable.py
+++ b/Lib/fontTools/ttLib/tables/DefaultTable.py
@@ -17,7 +17,7 @@ class DefaultTable(object):
def compile(self, ttFont):
return self.data
- def toXML(self, writer, ttFont):
+ def toXML(self, writer, ttFont, **kwargs):
if hasattr(self, "ERROR"):
writer.comment("An error occurred during the decompilation of this table")
writer.newline()
diff --git a/Lib/fontTools/ttLib/tables/G_P_K_G_.py b/Lib/fontTools/ttLib/tables/G_P_K_G_.py
index 4e13830b..b835f4af 100644
--- a/Lib/fontTools/ttLib/tables/G_P_K_G_.py
+++ b/Lib/fontTools/ttLib/tables/G_P_K_G_.py
@@ -25,8 +25,7 @@ class table_G_P_K_G_(DefaultTable.DefaultTable):
GMAPoffsets = array.array("I")
endPos = (self.numGMAPs+1) * 4
GMAPoffsets.fromstring(newData[:endPos])
- if sys.byteorder != "big":
- GMAPoffsets.byteswap()
+ if sys.byteorder != "big": GMAPoffsets.byteswap()
self.GMAPs = []
for i in range(self.numGMAPs):
start = GMAPoffsets[i]
@@ -36,8 +35,7 @@ class table_G_P_K_G_(DefaultTable.DefaultTable):
endPos = pos + (self.numGlyplets + 1)*4
glyphletOffsets = array.array("I")
glyphletOffsets.fromstring(newData[pos:endPos])
- if sys.byteorder != "big":
- glyphletOffsets.byteswap()
+ if sys.byteorder != "big": glyphletOffsets.byteswap()
self.glyphlets = []
for i in range(self.numGlyplets):
start = glyphletOffsets[i]
@@ -58,8 +56,7 @@ class table_G_P_K_G_(DefaultTable.DefaultTable):
pos += len(self.GMAPs[i-1])
GMAPoffsets[i] = pos
gmapArray = array.array("I", GMAPoffsets)
- if sys.byteorder != "big":
- gmapArray.byteswap()
+ if sys.byteorder != "big": gmapArray.byteswap()
dataList.append(gmapArray.tostring())
glyphletOffsets[0] = pos
@@ -67,8 +64,7 @@ class table_G_P_K_G_(DefaultTable.DefaultTable):
pos += len(self.glyphlets[i-1])
glyphletOffsets[i] = pos
glyphletArray = array.array("I", glyphletOffsets)
- if sys.byteorder != "big":
- glyphletArray.byteswap()
+ if sys.byteorder != "big": glyphletArray.byteswap()
dataList.append(glyphletArray.tostring())
dataList += self.GMAPs
dataList += self.glyphlets
diff --git a/Lib/fontTools/ttLib/tables/G__l_o_c.py b/Lib/fontTools/ttLib/tables/G__l_o_c.py
index d77c4837..188637d8 100644
--- a/Lib/fontTools/ttLib/tables/G__l_o_c.py
+++ b/Lib/fontTools/ttLib/tables/G__l_o_c.py
@@ -4,6 +4,8 @@ from fontTools.misc import sstruct
from fontTools.misc.textTools import safeEval
from . import DefaultTable
import array
+import sys
+
Gloc_header = '''
> # big endian
@@ -30,23 +32,23 @@ class table_G__l_o_c(DefaultTable.DefaultTable):
del self.flags
self.locations = array.array('I' if flags & 1 else 'H')
self.locations.fromstring(data[:len(data) - self.numAttribs * (flags & 2)])
- self.locations.byteswap()
+ if sys.byteorder != "big": self.locations.byteswap()
self.attribIds = array.array('H')
if flags & 2:
self.attribIds.fromstring(data[-self.numAttribs * 2:])
- self.attribIds.byteswap()
+ if sys.byteorder != "big": self.attribIds.byteswap()
def compile(self, ttFont):
data = sstruct.pack(Gloc_header, dict(version=1.0,
flags=(bool(self.attribIds) << 1) + (self.locations.typecode == 'I'),
numAttribs=self.numAttribs))
- self.locations.byteswap()
+ if sys.byteorder != "big": self.locations.byteswap()
data += self.locations.tostring()
- self.locations.byteswap()
+ if sys.byteorder != "big": self.locations.byteswap()
if self.attribIds:
- self.attribIds.byteswap()
+ if sys.byteorder != "big": self.attribIds.byteswap()
data += self.attribIds.tostring()
- self.attribIds.byteswap()
+ if sys.byteorder != "big": self.attribIds.byteswap()
return data
def set(self, locations):
diff --git a/Lib/fontTools/ttLib/tables/S__i_l_f.py b/Lib/fontTools/ttLib/tables/S__i_l_f.py
index 2afd71ea..44dd69b0 100644
--- a/Lib/fontTools/ttLib/tables/S__i_l_f.py
+++ b/Lib/fontTools/ttLib/tables/S__i_l_f.py
@@ -717,7 +717,7 @@ class Pass(object):
data = data[2 * self.numRules + 2:]
for i in range(self.numTransitional):
a = array("H", data[i*self.numColumns*2:(i+1)*self.numColumns*2])
- a.byteswap()
+ if sys.byteorder != "big": a.byteswap()
self.stateTrans.append(a)
data = data[self.numTransitional * self.numColumns * 2 + 1:]
self.passConstraints = data[:pConstraint]
@@ -738,9 +738,9 @@ class Pass(object):
constraintCode = "\000" + "".join(self.ruleConstraints)
transes = []
for t in self.stateTrans:
- t.byteswap()
+ if sys.byteorder != "big": t.byteswap()
transes.append(t.tostring())
- t.byteswap()
+ if sys.byteorder != "big": t.byteswap()
if not len(transes):
self.startStates = [0]
oRuleMap = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.rules+[[]], (0, []))[1]
diff --git a/Lib/fontTools/ttLib/tables/T_S_I_C_.py b/Lib/fontTools/ttLib/tables/T_S_I_C_.py
new file mode 100644
index 00000000..b4684a46
--- /dev/null
+++ b/Lib/fontTools/ttLib/tables/T_S_I_C_.py
@@ -0,0 +1,7 @@
+from __future__ import print_function, division, absolute_import
+from fontTools.misc.py23 import *
+from .otBase import BaseTTXConverter
+
+
+class table_T_S_I_C_(BaseTTXConverter):
+ pass
diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py
index dbf9e5a2..61b76044 100644
--- a/Lib/fontTools/ttLib/tables/T_S_I__5.py
+++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py
@@ -18,8 +18,7 @@ class table_T_S_I__5(DefaultTable.DefaultTable):
assert len(data) == 2 * numGlyphs
a = array.array("H")
a.fromstring(data)
- if sys.byteorder != "big":
- a.byteswap()
+ if sys.byteorder != "big": a.byteswap()
self.glyphGrouping = {}
for i in range(numGlyphs):
self.glyphGrouping[ttFont.getGlyphName(i)] = a[i]
@@ -29,8 +28,7 @@ class table_T_S_I__5(DefaultTable.DefaultTable):
a = array.array("H")
for i in range(len(glyphNames)):
a.append(self.glyphGrouping.get(glyphNames[i], 0))
- if sys.byteorder != "big":
- a.byteswap()
+ if sys.byteorder != "big": a.byteswap()
return a.tostring()
def toXML(self, writer, ttFont):
diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py
index 5fa71c84..92d07a11 100644
--- a/Lib/fontTools/ttLib/tables/TupleVariation.py
+++ b/Lib/fontTools/ttLib/tables/TupleVariation.py
@@ -267,8 +267,7 @@ class TupleVariation(object):
points = array.array("B")
pointsSize = numPointsInRun
points.fromstring(data[pos:pos+pointsSize])
- if sys.byteorder != "big":
- points.byteswap()
+ if sys.byteorder != "big": points.byteswap()
assert len(points) == numPointsInRun
pos += pointsSize
@@ -425,8 +424,7 @@ class TupleVariation(object):
deltas = array.array("b")
deltasSize = numDeltasInRun
deltas.fromstring(data[pos:pos+deltasSize])
- if sys.byteorder != "big":
- deltas.byteswap()
+ if sys.byteorder != "big": deltas.byteswap()
assert len(deltas) == numDeltasInRun
pos += deltasSize
result.extend(deltas)
diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
index eccf69e0..5bc9354f 100644
--- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
@@ -340,8 +340,7 @@ class cmap_format_2(CmapSubtable):
allKeys = array.array("H")
allKeys.fromstring(data[:512])
data = data[512:]
- if sys.byteorder != "big":
- allKeys.byteswap()
+ if sys.byteorder != "big": allKeys.byteswap()
subHeaderKeys = [ key//8 for key in allKeys]
maxSubHeaderindex = max(subHeaderKeys)
@@ -356,8 +355,7 @@ class cmap_format_2(CmapSubtable):
giDataPos = pos + subHeader.idRangeOffset-2
giList = array.array("H")
giList.fromstring(data[giDataPos:giDataPos + subHeader.entryCount*2])
- if sys.byteorder != "big":
- giList.byteswap()
+ if sys.byteorder != "big": giList.byteswap()
subHeader.glyphIndexArray = giList
subHeaderList.append(subHeader)
# How this gets processed.
@@ -702,8 +700,7 @@ class cmap_format_4(CmapSubtable):
allCodes.fromstring(data)
self.data = data = None
- if sys.byteorder != "big":
- allCodes.byteswap()
+ if sys.byteorder != "big": allCodes.byteswap()
# divide the data
endCode = allCodes[:segCount]
@@ -829,10 +826,9 @@ class cmap_format_4(CmapSubtable):
charCodeArray = array.array("H", endCode + [0] + startCode)
idDeltaArray = array.array("H", idDelta)
restArray = array.array("H", idRangeOffset + glyphIndexArray)
- if sys.byteorder != "big":
- charCodeArray.byteswap()
- idDeltaArray.byteswap()
- restArray.byteswap()
+ if sys.byteorder != "big": charCodeArray.byteswap()
+ if sys.byteorder != "big": idDeltaArray.byteswap()
+ if sys.byteorder != "big": restArray.byteswap()
data = charCodeArray.tostring() + idDeltaArray.tostring() + restArray.tostring()
length = struct.calcsize(cmap_format_4_format) + len(data)
@@ -872,8 +868,7 @@ class cmap_format_6(CmapSubtable):
#assert len(data) == 2 * entryCount # XXX not true in Apple's Helvetica!!!
gids = array.array("H")
gids.fromstring(data[:2 * int(entryCount)])
- if sys.byteorder != "big":
- gids.byteswap()
+ if sys.byteorder != "big": gids.byteswap()
self.data = data = None
charCodes = list(range(firstCode, firstCode + len(gids)))
@@ -892,8 +887,7 @@ class cmap_format_6(CmapSubtable):
for code in codes
]
gids = array.array("H", valueList)
- if sys.byteorder != "big":
- gids.byteswap()
+ if sys.byteorder != "big": gids.byteswap()
data = gids.tostring()
else:
data = b""
diff --git a/Lib/fontTools/ttLib/tables/_c_v_t.py b/Lib/fontTools/ttLib/tables/_c_v_t.py
index 4fbee7bf..21df0bad 100644
--- a/Lib/fontTools/ttLib/tables/_c_v_t.py
+++ b/Lib/fontTools/ttLib/tables/_c_v_t.py
@@ -10,14 +10,12 @@ class table__c_v_t(DefaultTable.DefaultTable):
def decompile(self, data, ttFont):
values = array.array("h")
values.fromstring(data)
- if sys.byteorder != "big":
- values.byteswap()
+ if sys.byteorder != "big": values.byteswap()
self.values = values
def compile(self, ttFont):
values = self.values[:]
- if sys.byteorder != "big":
- values.byteswap()
+ if sys.byteorder != "big": values.byteswap()
return values.tostring()
def toXML(self, writer, ttFont):
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
index 58c1eb2c..8b360504 100644
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -260,8 +260,11 @@ flagYShort = 0x04
flagRepeat = 0x08
flagXsame = 0x10
flagYsame = 0x20
-flagReserved1 = 0x40
-flagReserved2 = 0x80
+flagOverlapSimple = 0x40
+flagReserved = 0x80
+
+# These flags are kept for XML output after decompiling the coordinates
+keepFlags = flagOnCurve + flagOverlapSimple
_flagSignBytes = {
0: 2,
@@ -408,10 +411,15 @@ class Glyph(object):
writer.begintag("contour")
writer.newline()
for j in range(last, self.endPtsOfContours[i] + 1):
- writer.simpletag("pt", [
+ attrs = [
("x", self.coordinates[j][0]),
("y", self.coordinates[j][1]),
- ("on", self.flags[j] & flagOnCurve)])
+ ("on", self.flags[j] & flagOnCurve),
+ ]
+ if self.flags[j] & flagOverlapSimple:
+ # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours
+ attrs.append(("overlap", 1))
+ writer.simpletag("pt", attrs)
writer.newline()
last = self.endPtsOfContours[i] + 1
writer.endtag("contour")
@@ -441,7 +449,10 @@ class Glyph(object):
if name != "pt":
continue # ignore anything but "pt"
coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"])))
- flags.append(not not safeEval(attrs["on"]))
+ flag = not not safeEval(attrs["on"])
+ if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
+ flag |= flagOverlapSimple
+ flags.append(flag)
flags = array.array("B", flags)
if not hasattr(self, "coordinates"):
self.coordinates = coordinates
@@ -512,8 +523,7 @@ class Glyph(object):
def decompileCoordinates(self, data):
endPtsOfContours = array.array("h")
endPtsOfContours.fromstring(data[:2*self.numberOfContours])
- if sys.byteorder != "big":
- endPtsOfContours.byteswap()
+ if sys.byteorder != "big": endPtsOfContours.byteswap()
self.endPtsOfContours = endPtsOfContours.tolist()
data = data[2*self.numberOfContours:]
@@ -561,8 +571,8 @@ class Glyph(object):
assert xIndex == len(xCoordinates)
assert yIndex == len(yCoordinates)
coordinates.relativeToAbsolute()
- # discard all flags but for "flagOnCurve"
- self.flags = array.array("B", (f & flagOnCurve for f in flags))
+ # discard all flags except "keepFlags"
+ self.flags = array.array("B", (f & keepFlags for f in flags))
def decompileCoordinatesRaw(self, nCoordinates, data):
# unpack flags and prepare unpacking of coordinates
@@ -625,8 +635,7 @@ class Glyph(object):
assert len(self.coordinates) == len(self.flags)
data = []
endPtsOfContours = array.array("h", self.endPtsOfContours)
- if sys.byteorder != "big":
- endPtsOfContours.byteswap()
+ if sys.byteorder != "big": endPtsOfContours.byteswap()
data.append(endPtsOfContours.tostring())
instructions = self.program.getBytecode()
data.append(struct.pack(">h", len(instructions)))
@@ -1028,6 +1037,39 @@ class Glyph(object):
cFlags = cFlags[nextOnCurve:]
pen.closePath()
+ def drawPoints(self, pen, glyfTable, offset=0):
+ """Draw the glyph using the supplied pointPen. Opposed to Glyph.draw(),
+ this will not change the point indices.
+ """
+
+ if self.isComposite():
+ for component in self.components:
+ glyphName, transform = component.getComponentInfo()
+ pen.addComponent(glyphName, transform)
+ return
+
+ coordinates, endPts, flags = self.getCoordinates(glyfTable)
+ if offset:
+ coordinates = coordinates.copy()
+ coordinates.translate((offset, 0))
+ start = 0
+ for end in endPts:
+ end = end + 1
+ contour = coordinates[start:end]
+ cFlags = flags[start:end]
+ start = end
+ pen.beginPath()
+ # Start with the appropriate segment type based on the final segment
+ segmentType = "line" if cFlags[-1] == 1 else "qcurve"
+ for i, pt in enumerate(contour):
+ if cFlags[i] == 1:
+ pen.addPoint(pt, segmentType=segmentType)
+ segmentType = "line"
+ else:
+ pen.addPoint(pt)
+ segmentType = "qcurve"
+ pen.endPath()
+
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
index 9f97c31a..608b6a2d 100644
--- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
@@ -121,8 +121,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
offsets = array.array("I")
offsetsSize = (glyphCount + 1) * 4
offsets.fromstring(data[0 : offsetsSize])
- if sys.byteorder != "big":
- offsets.byteswap()
+ if sys.byteorder != "big": offsets.byteswap()
# In the short format, offsets need to be multiplied by 2.
# This is not documented in Apple's TrueType specification,
@@ -152,8 +151,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
else:
packed = array.array("I", offsets)
tableFormat = 1
- if sys.byteorder != "big":
- packed.byteswap()
+ if sys.byteorder != "big": packed.byteswap()
return (packed.tostring(), tableFormat)
def toXML(self, writer, ttFont):
diff --git a/Lib/fontTools/ttLib/tables/_h_m_t_x.py b/Lib/fontTools/ttLib/tables/_h_m_t_x.py
index e6b8ea9b..6f8bb972 100644
--- a/Lib/fontTools/ttLib/tables/_h_m_t_x.py
+++ b/Lib/fontTools/ttLib/tables/_h_m_t_x.py
@@ -40,8 +40,7 @@ class table__h_m_t_x(DefaultTable.DefaultTable):
sideBearings = array.array("h", data[:2 * numberOfSideBearings])
data = data[2 * numberOfSideBearings:]
- if sys.byteorder != "big":
- sideBearings.byteswap()
+ if sys.byteorder != "big": sideBearings.byteswap()
if data:
log.warning("too much '%s' table data" % self.tableTag)
self.metrics = {}
@@ -98,8 +97,7 @@ class table__h_m_t_x(DefaultTable.DefaultTable):
else:
raise
additionalMetrics = array.array("h", additionalMetrics)
- if sys.byteorder != "big":
- additionalMetrics.byteswap()
+ if sys.byteorder != "big": additionalMetrics.byteswap()
data = data + additionalMetrics.tostring()
return data
diff --git a/Lib/fontTools/ttLib/tables/_k_e_r_n.py b/Lib/fontTools/ttLib/tables/_k_e_r_n.py
index 6e21a4bd..98eb7092 100644
--- a/Lib/fontTools/ttLib/tables/_k_e_r_n.py
+++ b/Lib/fontTools/ttLib/tables/_k_e_r_n.py
@@ -130,8 +130,7 @@ class KernTable_format_0(object):
nPairs = min(nPairs, len(data) // 6)
datas = array.array("H", data[:6 * nPairs])
- if sys.byteorder != "big": # pragma: no cover
- datas.byteswap()
+ if sys.byteorder != "big": datas.byteswap()
it = iter(datas)
glyphOrder = ttFont.getGlyphOrder()
for k in range(nPairs):
diff --git a/Lib/fontTools/ttLib/tables/_l_o_c_a.py b/Lib/fontTools/ttLib/tables/_l_o_c_a.py
index 2fcd5284..6aa53030 100644
--- a/Lib/fontTools/ttLib/tables/_l_o_c_a.py
+++ b/Lib/fontTools/ttLib/tables/_l_o_c_a.py
@@ -21,8 +21,7 @@ class table__l_o_c_a(DefaultTable.DefaultTable):
format = "H"
locations = array.array(format)
locations.fromstring(data)
- if sys.byteorder != "big":
- locations.byteswap()
+ if sys.byteorder != "big": locations.byteswap()
if not longFormat:
l = array.array("I")
for i in range(len(locations)):
@@ -47,8 +46,7 @@ class table__l_o_c_a(DefaultTable.DefaultTable):
else:
locations = array.array("I", self.locations)
ttFont['head'].indexToLocFormat = 1
- if sys.byteorder != "big":
- locations.byteswap()
+ if sys.byteorder != "big": locations.byteswap()
return locations.tostring()
def set(self, locations):
diff --git a/Lib/fontTools/ttLib/tables/_p_o_s_t.py b/Lib/fontTools/ttLib/tables/_p_o_s_t.py
index ede62dab..4874ecd9 100644
--- a/Lib/fontTools/ttLib/tables/_p_o_s_t.py
+++ b/Lib/fontTools/ttLib/tables/_p_o_s_t.py
@@ -84,8 +84,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
data = data[2:]
indices = array.array("H")
indices.fromstring(data[:2*numGlyphs])
- if sys.byteorder != "big":
- indices.byteswap()
+ if sys.byteorder != "big": indices.byteswap()
data = data[2*numGlyphs:]
self.extraNames = extraNames = unpackPStrings(data)
self.glyphOrder = glyphOrder = [""] * int(ttFont['maxp'].numGlyphs)
@@ -134,8 +133,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
numGlyphs = ttFont['maxp'].numGlyphs
indices = array.array("H")
indices.fromstring(data)
- if sys.byteorder != "big":
- indices.byteswap()
+ if sys.byteorder != "big": indices.byteswap()
# In some older fonts, the size of the post table doesn't match
# the number of glyphs. Sometimes it's bigger, sometimes smaller.
self.glyphOrder = glyphOrder = [''] * int(numGlyphs)
@@ -173,8 +171,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
extraDict[psName] = len(extraNames)
extraNames.append(psName)
indices.append(index)
- if sys.byteorder != "big":
- indices.byteswap()
+ if sys.byteorder != "big": indices.byteswap()
return struct.pack(">H", numGlyphs) + indices.tostring() + packPStrings(extraNames)
def encode_format_4_0(self, ttFont):
@@ -191,8 +188,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
indices.append(int(glyphID[3:],16))
else:
indices.append(0xFFFF)
- if sys.byteorder != "big":
- indices.byteswap()
+ if sys.byteorder != "big": indices.byteswap()
return indices.tostring()
def toXML(self, writer, ttFont):
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 2064e24c..c71cc8db 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -87,7 +87,13 @@ class BaseTTXConverter(DefaultTable):
from .otTables import fixSubTableOverFlows
ok = fixSubTableOverFlows(font, overflowRecord)
if not ok:
- raise
+ # Try upgrading lookup to Extension and hope
+ # that cross-lookup sharing not happening would
+ # fix overflow...
+ from .otTables import fixLookupOverFlows
+ ok = fixLookupOverFlows(font, overflowRecord)
+ if not ok:
+ raise
def toXML(self, writer, font):
self.table.toXML2(writer, font)
@@ -139,8 +145,7 @@ class OTTableReader(object):
pos = self.pos
newpos = pos + count * 2
value = array.array("H", self.data[pos:newpos])
- if sys.byteorder != "big":
- value.byteswap()
+ if sys.byteorder != "big": value.byteswap()
self.pos = newpos
return value
@@ -331,7 +336,6 @@ class OTTableWriter(object):
iRange.reverse()
isExtension = hasattr(self, "Extension")
- dontShare = hasattr(self, 'DontShare')
selfTables = tables
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index ac659455..d1712db8 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -9,7 +9,7 @@ from .otBase import (CountReference, FormatSwitchingBaseTable,
OTTableReader, OTTableWriter, ValueRecordFactory)
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
ContextualMorphAction, LigatureMorphAction,
- MorxSubtable)
+ InsertionMorphAction, MorxSubtable)
from functools import partial
import struct
import logging
@@ -130,7 +130,7 @@ class BaseConverter(object):
self.tableClass = tableClass
self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
self.isLookupType = name.endswith("LookupType") or name == "MorphType"
- self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount", 'DesignAxisCount', 'DesignAxisRecordSize', 'AxisValueCount', 'ValueRecordSize']
+ self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount", 'DesignAxisCount', 'DesignAxisRecordSize', 'AxisValueCount', 'ValueRecordSize', 'AxisCount']
def readArray(self, reader, font, tableDict, count):
"""Read an array of values from the reader."""
@@ -1055,6 +1055,9 @@ class STXHeader(BaseConverter):
table.LigComponents = \
ligComponentReader.readUShortArray(numLigComponents)
table.Ligatures = self._readLigatures(ligaturesReader, font)
+ elif issubclass(self.tableClass, InsertionMorphAction):
+ actionReader = reader.getSubReader(0)
+ actionReader.seek(pos + reader.readULong())
table.GlyphClasses = self.classLookup.read(classTableReader,
font, tableDict)
numStates = int((entryTableReader.pos - stateArrayReader.pos)
@@ -1119,19 +1122,15 @@ class STXHeader(BaseConverter):
glyphClassWriter = OTTableWriter()
self.classLookup.write(glyphClassWriter, font, tableDict,
value.GlyphClasses, repeatIndex=None)
- glyphClassData = pad(glyphClassWriter.getAllData(), 4)
+ glyphClassData = pad(glyphClassWriter.getAllData(), 2)
glyphClassCount = max(value.GlyphClasses.values()) + 1
glyphClassTableOffset = 16 # size of STXHeader
if self.perGlyphLookup is not None:
glyphClassTableOffset += 4
- actionData, actionIndex = None, None
- if issubclass(self.tableClass, LigatureMorphAction):
- glyphClassTableOffset += 12
- actionData, actionIndex = \
- self._compileLigActions(value, font)
- actionData = pad(actionData, 4)
-
+ glyphClassTableOffset += self.tableClass.actionHeaderSize
+ actionData, actionIndex = \
+ self.tableClass.compileActions(font, value.States)
stateArrayData, entryTableData = self._compileStates(
font, value.States, glyphClassCount, actionIndex)
stateArrayOffset = glyphClassTableOffset + len(glyphClassData)
@@ -1139,17 +1138,19 @@ class STXHeader(BaseConverter):
perGlyphOffset = entryTableOffset + len(entryTableData)
perGlyphData = \
pad(self._compilePerGlyphLookups(value, font), 4)
+ if actionData is not None:
+ actionOffset = entryTableOffset + len(entryTableData)
+ else:
+ actionOffset = None
+
+ ligaturesOffset, ligComponentsOffset = None, None
ligComponentsData = self._compileLigComponents(value, font)
ligaturesData = self._compileLigatures(value, font)
- if actionData is None:
- actionOffset = None
- ligComponentsOffset = None
- ligaturesOffset = None
- else:
+ if ligComponentsData is not None:
assert len(perGlyphData) == 0
- actionOffset = entryTableOffset + len(entryTableData)
ligComponentsOffset = actionOffset + len(actionData)
ligaturesOffset = ligComponentsOffset + len(ligComponentsData)
+
writer.writeULong(glyphClassCount)
writer.writeULong(glyphClassTableOffset)
writer.writeULong(stateArrayOffset)
@@ -1158,6 +1159,7 @@ class STXHeader(BaseConverter):
writer.writeULong(perGlyphOffset)
if actionOffset is not None:
writer.writeULong(actionOffset)
+ if ligComponentsOffset is not None:
writer.writeULong(ligComponentsOffset)
writer.writeULong(ligaturesOffset)
writer.writeData(glyphClassData)
@@ -1214,34 +1216,6 @@ class STXHeader(BaseConverter):
writer.writeSubTable(lookupWriter)
return writer.getAllData()
- def _compileLigActions(self, table, font):
- assert issubclass(self.tableClass, LigatureMorphAction)
- actions = set()
- for state in table.States:
- for _glyphClass, trans in state.Transitions.items():
- actions.add(trans.compileLigActions())
- result, actionIndex = b"", {}
- # Sort the compiled actions in decreasing order of
- # length, so that the longer sequence come before the
- # shorter ones. For each compiled action ABCD, its
- # suffixes BCD, CD, and D do not be encoded separately
- # (in case they occur); instead, we can just store an
- # index that points into the middle of the longer
- # sequence. Every compiled AAT ligature sequence is
- # terminated with an end-of-sequence flag, which can
- # only be set on the last element of the sequence.
- # Therefore, it is sufficient to consider just the
- # suffixes.
- for a in sorted(actions, key=lambda x:(-len(x), x)):
- if a not in actionIndex:
- for i in range(0, len(a), 4):
- suffix = a[i:]
- suffixIndex = (len(result) + i) // 4
- actionIndex.setdefault(
- suffix, suffixIndex)
- result += a
- return (result, actionIndex)
-
def _compileLigComponents(self, table, font):
if not hasattr(table, "LigComponents"):
return None
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index a0d0552c..d08bcc57 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -1445,8 +1445,7 @@ otData = [
]),
('InsertionMorph', [
- ('struct', 'StateHeader', None, None, 'Header.'),
- # TODO: Add missing parts.
+ ('STXHeader(InsertionMorphAction)', 'StateTable', None, None, 'Finite-state transducer for glyph insertion.'),
]),
('MorphClass', [
@@ -1513,4 +1512,31 @@ otData = [
('int16', 'Bottom', None, None, 'Control point index for the bottom-side optical edge, or -1 if this glyph has none.'),
]),
+ #
+ # TSIC
+ #
+ ('TSIC', [
+ ('Version', 'Version', None, None, 'Version of table initially set to 0x00010000.'),
+ ('uint16', 'Flags', None, None, 'TSIC flags - set to 0'),
+ ('uint16', 'AxisCount', None, None, 'Axis count from fvar'),
+ ('uint16', 'RecordCount', None, None, 'TSIC record count'),
+ ('uint16', 'Reserved', None, None, 'Set to 0'),
+ ('Tag', 'AxisArray', 'AxisCount', 0, 'Array of axis tags in fvar order'),
+ ('LocationRecord', 'RecordLocations', 'RecordCount', 0, 'Location in variation space of TSIC record'),
+ ('TSICRecord', 'Record', 'RecordCount', 0, 'Array of TSIC records'),
+ ]),
+
+ ('LocationRecord', [
+ ('F2Dot14', 'Axis', 'AxisCount', 0, 'Axis record'),
+ ]),
+
+ ('TSICRecord', [
+ ('uint16', 'Flags', None, None, 'Record flags - set to 0'),
+ ('uint16', 'NumCVTEntries', None, None, 'Number of CVT number value pairs'),
+ ('uint16', 'NameLength', None, None, 'Length of optional user record name'),
+ ('uint16', 'NameArray', 'NameLength', 0, 'Unicode 16 name'),
+ ('uint16', 'CVTArray', 'NumCVTEntries', 0, 'CVT number array'),
+ ('int16', 'CVTValueArray', 'NumCVTEntries', 0, 'CVT value'),
+ ]),
+
]
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index d00c2336..4d2bc574 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -7,7 +7,7 @@ converter objects from otConverters.py.
"""
from __future__ import print_function, division, absolute_import, unicode_literals
from fontTools.misc.py23 import *
-from fontTools.misc.textTools import safeEval
+from fontTools.misc.textTools import pad, safeEval
from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord
import operator
import logging
@@ -32,6 +32,10 @@ class AATState(object):
class AATAction(object):
_FLAGS = None
+ @staticmethod
+ def compileActions(font, states):
+ return (None, None)
+
def _writeFlagsToXML(self, xmlWriter):
flags = [f for f in self._FLAGS if self.__dict__[f]]
if flags:
@@ -50,6 +54,7 @@ class AATAction(object):
class RearrangementMorphAction(AATAction):
staticSize = 4
+ actionHeaderSize = 0
_FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
_VERBS = {
@@ -131,6 +136,7 @@ class RearrangementMorphAction(AATAction):
class ContextualMorphAction(AATAction):
staticSize = 8
+ actionHeaderSize = 0
_FLAGS = ["SetMark", "DontAdvance"]
def __init__(self):
@@ -209,6 +215,10 @@ class LigAction(object):
class LigatureMorphAction(AATAction):
staticSize = 6
+
+ # 4 bytes for each of {action,ligComponents,ligatures}Offset
+ actionHeaderSize = 12
+
_FLAGS = ["SetComponent", "DontAdvance"]
def __init__(self):
@@ -251,6 +261,34 @@ class LigatureMorphAction(AATAction):
else:
self.Actions = []
+ @staticmethod
+ def compileActions(font, states):
+ result, actions, actionIndex = b"", set(), {}
+ for state in states:
+ for _glyphClass, trans in state.Transitions.items():
+ actions.add(trans.compileLigActions())
+ # Sort the compiled actions in decreasing order of
+ # length, so that the longer sequence come before the
+ # shorter ones. For each compiled action ABCD, its
+ # suffixes BCD, CD, and D do not be encoded separately
+ # (in case they occur); instead, we can just store an
+ # index that points into the middle of the longer
+ # sequence. Every compiled AAT ligature sequence is
+ # terminated with an end-of-sequence flag, which can
+ # only be set on the last element of the sequence.
+ # Therefore, it is sufficient to consider just the
+ # suffixes.
+ for a in sorted(actions, key=lambda x:(-len(x), x)):
+ if a not in actionIndex:
+ for i in range(0, len(a), 4):
+ suffix = a[i:]
+ suffixIndex = (len(result) + i) // 4
+ actionIndex.setdefault(
+ suffix, suffixIndex)
+ result += a
+ result = pad(result, 4)
+ return (result, actionIndex)
+
def compileLigActions(self):
result = []
for i, action in enumerate(self.Actions):
@@ -319,7 +357,7 @@ class LigatureMorphAction(AATAction):
class InsertionMorphAction(AATAction):
staticSize = 8
-
+ actionHeaderSize = 4 # 4 bytes for actionOffset
_FLAGS = ["SetMark", "DontAdvance",
"CurrentIsKashidaLike", "MarkedIsKashidaLike",
"CurrentInsertBefore", "MarkedInsertBefore"]
@@ -417,6 +455,37 @@ class InsertionMorphAction(AATAction):
else:
assert False, eltName
+ @staticmethod
+ def compileActions(font, states):
+ actions, actionIndex, result = set(), {}, b""
+ for state in states:
+ for _glyphClass, trans in state.Transitions.items():
+ if trans.CurrentInsertionAction is not None:
+ actions.add(tuple(trans.CurrentInsertionAction))
+ if trans.MarkedInsertionAction is not None:
+ actions.add(tuple(trans.MarkedInsertionAction))
+ # Sort the compiled actions in decreasing order of
+ # length, so that the longer sequence come before the
+ # shorter ones.
+ for action in sorted(actions, key=lambda x:(-len(x), x)):
+ # We insert all sub-sequences of the action glyph sequence
+ # into actionIndex. For example, if one action triggers on
+ # glyph sequence [A, B, C, D, E] and another action triggers
+ # on [C, D], we return result=[A, B, C, D, E] (as list of
+ # encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0,
+ # ('C','D'): 2}.
+ if action in actionIndex:
+ continue
+ for start in range(0, len(action)):
+ startIndex = (len(result) // 2) + start
+ for limit in range(start, len(action)):
+ glyphs = action[start : limit + 1]
+ actionIndex.setdefault(glyphs, startIndex)
+ for glyph in action:
+ glyphID = font.getGlyphID(glyph)
+ result += struct.pack(">H", glyphID)
+ return result, actionIndex
+
class FeatureParams(BaseTable):
@@ -1281,6 +1350,67 @@ def splitPairPos(oldSubTable, newSubTable, overflowRecord):
return ok
+def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
+ # split half of the mark classes to the new subtable
+ classCount = oldSubTable.ClassCount
+ if classCount < 2:
+ # oh well, not much left to split...
+ return False
+
+ oldClassCount = classCount // 2
+ newClassCount = classCount - oldClassCount
+
+ oldMarkCoverage, oldMarkRecords = [], []
+ newMarkCoverage, newMarkRecords = [], []
+ for glyphName, markRecord in zip(
+ oldSubTable.MarkCoverage.glyphs,
+ oldSubTable.MarkArray.MarkRecord
+ ):
+ if markRecord.Class < oldClassCount:
+ oldMarkCoverage.append(glyphName)
+ oldMarkRecords.append(markRecord)
+ else:
+ newMarkCoverage.append(glyphName)
+ newMarkRecords.append(markRecord)
+
+ oldBaseRecords, newBaseRecords = [], []
+ for rec in oldSubTable.BaseArray.BaseRecord:
+ oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
+ oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
+ newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
+ oldBaseRecords.append(oldBaseRecord)
+ newBaseRecords.append(newBaseRecord)
+
+ newSubTable.Format = oldSubTable.Format
+
+ oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
+ newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
+ newSubTable.MarkCoverage.Format = oldSubTable.MarkCoverage.Format
+ newSubTable.MarkCoverage.glyphs = newMarkCoverage
+
+ # share the same BaseCoverage in both halves
+ newSubTable.BaseCoverage = oldSubTable.BaseCoverage
+
+ oldSubTable.ClassCount = oldClassCount
+ newSubTable.ClassCount = newClassCount
+
+ oldSubTable.MarkArray.MarkRecord = oldMarkRecords
+ newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
+ newSubTable.MarkArray.MarkRecord = newMarkRecords
+
+ oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
+ newSubTable.MarkArray.MarkCount = len(newMarkRecords)
+
+ oldSubTable.BaseArray.BaseRecord = oldBaseRecords
+ newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
+ newSubTable.BaseArray.BaseRecord = newBaseRecords
+
+ oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
+ newSubTable.BaseArray.BaseCount = len(newBaseRecords)
+
+ return True
+
+
splitTable = { 'GSUB': {
# 1: splitSingleSubst,
# 2: splitMultipleSubst,
@@ -1295,7 +1425,7 @@ splitTable = { 'GSUB': {
# 1: splitSinglePos,
2: splitPairPos,
# 3: splitCursivePos,
-# 4: splitMarkBasePos,
+ 4: splitMarkBasePos,
# 5: splitMarkLigPos,
# 6: splitMarkMarkPos,
# 7: splitContextPos,
@@ -1309,7 +1439,6 @@ def fixSubTableOverFlows(ttf, overflowRecord):
"""
An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
"""
- ok = 0
table = ttf[overflowRecord.tableType].table
lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
subIndex = overflowRecord.SubTableIndex
@@ -1330,7 +1459,7 @@ def fixSubTableOverFlows(ttf, overflowRecord):
newExtSubTableClass = lookupTypes[overflowRecord.tableType][extSubTable.__class__.LookupType]
newExtSubTable = newExtSubTableClass()
newExtSubTable.Format = extSubTable.Format
- lookup.SubTable.insert(subIndex + 1, newExtSubTable)
+ toInsert = newExtSubTable
newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
newSubTable = newSubTableClass()
@@ -1339,7 +1468,7 @@ def fixSubTableOverFlows(ttf, overflowRecord):
subTableType = subtable.__class__.LookupType
newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
newSubTable = newSubTableClass()
- lookup.SubTable.insert(subIndex + 1, newSubTable)
+ toInsert = newSubTable
if hasattr(lookup, 'SubTableCount'): # may not be defined yet.
lookup.SubTableCount = lookup.SubTableCount + 1
@@ -1347,9 +1476,16 @@ def fixSubTableOverFlows(ttf, overflowRecord):
try:
splitFunc = splitTable[overflowRecord.tableType][subTableType]
except KeyError:
- return ok
+ log.error(
+ "Don't know how to split %s lookup type %s",
+ overflowRecord.tableType,
+ subTableType,
+ )
+ return False
ok = splitFunc(subtable, newSubTable, overflowRecord)
+ if ok:
+ lookup.SubTable.insert(subIndex + 1, toInsert)
return ok
# End of OverFlow logic
@@ -1414,7 +1550,7 @@ def _buildClasses():
2: LigatureMorph,
# 3: Reserved,
4: NoncontextualMorph,
- # 5: InsertionMorph,
+ 5: InsertionMorph,
},
}
lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
index 87538fb1..3a89592f 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -2,6 +2,7 @@ from __future__ import print_function, division, absolute_import
from fontTools.misc import xmlWriter
from fontTools.misc.py23 import *
from fontTools.misc.loggingTools import deprecateArgument
+from fontTools.ttLib import TTLibError
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
import os
import logging
@@ -731,9 +732,9 @@ class _TTGlyphSet(object):
class _TTGlyph(object):
"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
- that it has a .draw() method that takes a pen object as its only
- argument. Additionally there are 'width' and 'lsb' attributes, read from
- the 'hmtx' table.
+ that it has .draw() and .drawPoints() methods that take a pen object as
+ their only argument. Additionally there are 'width' and 'lsb' attributes,
+ read from the 'hmtx' table.
If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
attributes.
@@ -754,6 +755,10 @@ class _TTGlyph(object):
"""
self._glyph.draw(pen)
+ def drawPoints(self, pen):
+ # drawPoints is only implemented for _TTGlyphGlyf at this time.
+ raise NotImplementedError()
+
class _TTGlyphCFF(_TTGlyph):
pass
@@ -768,6 +773,15 @@ class _TTGlyphGlyf(_TTGlyph):
offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
glyph.draw(pen, glyfTable, offset)
+ def drawPoints(self, pen):
+ """Draw the glyph onto PointPen. See ufoLib.pointPen for details
+ how that works.
+ """
+ glyfTable = self._glyphset._glyphs
+ glyph = self._glyph
+ offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
+ glyph.drawPoints(pen, glyfTable, offset)
+
class GlyphOrder(object):
diff --git a/Lib/fontTools/ttLib/woff2.py b/Lib/fontTools/ttLib/woff2.py
index 1952682c..c0c0e704 100644
--- a/Lib/fontTools/ttLib/woff2.py
+++ b/Lib/fontTools/ttLib/woff2.py
@@ -582,8 +582,7 @@ class WOFF2LocaTable(getTableClass('loca')):
locations.append(self.locations[i] // 2)
else:
locations = array.array("I", self.locations)
- if sys.byteorder != "big":
- locations.byteswap()
+ if sys.byteorder != "big": locations.byteswap()
data = locations.tostring()
else:
# use the most compact indexFormat given the current glyph offsets
@@ -627,8 +626,7 @@ class WOFF2GlyfTable(getTableClass('glyf')):
self.bboxStream = self.bboxStream[bboxBitmapSize:]
self.nContourStream = array.array("h", self.nContourStream)
- if sys.byteorder != "big":
- self.nContourStream.byteswap()
+ if sys.byteorder != "big": self.nContourStream.byteswap()
assert len(self.nContourStream) == self.numGlyphs
if 'head' in ttFont:
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
new file mode 100755
index 00000000..b1ece9cf
--- /dev/null
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -0,0 +1,2246 @@
+from __future__ import absolute_import, unicode_literals
+import sys
+import os
+from copy import deepcopy
+import logging
+import zipfile
+import enum
+import fs
+import fs.base
+import fs.subfs
+import fs.errors
+import fs.copy
+import fs.osfs
+import fs.zipfs
+import fs.tempfs
+import fs.tools
+from fontTools.misc.py23 import basestring, unicode, tounicode
+from fontTools.misc import plistlib
+from fontTools.ufoLib.validators import *
+from fontTools.ufoLib.filenames import userNameToFileName
+from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
+from fontTools.ufoLib.errors import UFOLibError
+from fontTools.ufoLib.utils import datetimeAsTimestamp, fsdecode, numberTypes
+
+"""
+A library for importing .ufo files and their descendants.
+Refer to http://unifiedfontobject.com for the UFO specification.
+
+The UFOReader and UFOWriter classes support versions 1, 2 and 3
+of the specification.
+
+Sets that list the font info attribute names for the fontinfo.plist
+formats are available for external use. These are:
+ fontInfoAttributesVersion1
+ fontInfoAttributesVersion2
+ fontInfoAttributesVersion3
+
+A set listing the fontinfo.plist attributes that were deprecated
+in version 2 is available for external use:
+ deprecatedFontInfoAttributesVersion2
+
+Functions that do basic validation on values for fontinfo.plist
+are available for external use. These are
+ validateFontInfoVersion2ValueForAttribute
+ validateFontInfoVersion3ValueForAttribute
+
+Value conversion functions are available for converting
+fontinfo.plist values between the possible format versions.
+ convertFontInfoValueForAttributeFromVersion1ToVersion2
+ convertFontInfoValueForAttributeFromVersion2ToVersion1
+ convertFontInfoValueForAttributeFromVersion2ToVersion3
+ convertFontInfoValueForAttributeFromVersion3ToVersion2
+"""
+
+__all__ = [
+ "makeUFOPath",
+ "UFOLibError",
+ "UFOReader",
+ "UFOWriter",
+ "UFOFileStructure",
+ "fontInfoAttributesVersion1",
+ "fontInfoAttributesVersion2",
+ "fontInfoAttributesVersion3",
+ "deprecatedFontInfoAttributesVersion2",
+ "validateFontInfoVersion2ValueForAttribute",
+ "validateFontInfoVersion3ValueForAttribute",
+ "convertFontInfoValueForAttributeFromVersion1ToVersion2",
+ "convertFontInfoValueForAttributeFromVersion2ToVersion1"
+]
+
+__version__ = "3.0.0"
+
+
+logger = logging.getLogger(__name__)
+
+
+# ---------
+# Constants
+# ---------
+
+DEFAULT_GLYPHS_DIRNAME = "glyphs"
+DATA_DIRNAME = "data"
+IMAGES_DIRNAME = "images"
+METAINFO_FILENAME = "metainfo.plist"
+FONTINFO_FILENAME = "fontinfo.plist"
+LIB_FILENAME = "lib.plist"
+GROUPS_FILENAME = "groups.plist"
+KERNING_FILENAME = "kerning.plist"
+FEATURES_FILENAME = "features.fea"
+LAYERCONTENTS_FILENAME = "layercontents.plist"
+LAYERINFO_FILENAME = "layerinfo.plist"
+
+DEFAULT_LAYER_NAME = "public.default"
+
+supportedUFOFormatVersions = [1, 2, 3]
+
+
+class UFOFileStructure(enum.Enum):
+ ZIP = "zip"
+ PACKAGE = "package"
+
+
+# --------------
+# Shared Methods
+# --------------
+
+
+def _getFileModificationTime(self, path):
+ """
+ Returns the modification time for the file at the given path, as a
+ floating point number giving the number of seconds since the epoch.
+ The path must be relative to the UFO path.
+ Returns None if the file does not exist.
+ """
+ try:
+ dt = self.fs.getinfo(fsdecode(path), namespaces=["details"]).modified
+ except (fs.errors.MissingInfoNamespace, fs.errors.ResourceNotFound):
+ return None
+ else:
+ return datetimeAsTimestamp(dt)
+
+
+def _readBytesFromPath(self, path):
+ """
+ Returns the bytes in the file at the given path.
+ The path must be relative to the UFO's filesystem root.
+ Returns None if the file does not exist.
+ """
+ try:
+ return self.fs.getbytes(fsdecode(path))
+ except fs.errors.ResourceNotFound:
+ return None
+
+
+def _getPlist(self, fileName, default=None):
+ """
+ Read a property list relative to the UFO filesystem's root.
+ Raises UFOLibError if the file is missing and default is None,
+ otherwise default is returned.
+
+ The errors that could be raised during the reading of a plist are
+ unpredictable and/or too large to list, so, a blind try: except:
+ is done. If an exception occurs, a UFOLibError will be raised.
+ """
+ try:
+ with self.fs.open(fileName, "rb") as f:
+ return plistlib.load(f)
+ except fs.errors.ResourceNotFound:
+ if default is None:
+ raise UFOLibError(
+ "'%s' is missing on %s. This file is required"
+ % (fileName, self.fs)
+ )
+ else:
+ return default
+ except Exception as e:
+ # TODO(anthrotype): try to narrow this down a little
+ raise UFOLibError(
+ "'%s' could not be read on %s: %s" % (fileName, self.fs, e)
+ )
+
+
+def _writePlist(self, fileName, obj):
+ """
+ Write a property list to a file relative to the UFO filesystem's root.
+
+ Do this sort of atomically, making it harder to corrupt existing files,
+ for example when plistlib encounters an error halfway during write.
+ This also checks to see if text matches the text that is already in the
+ file at path. If so, the file is not rewritten so that the modification
+ date is preserved.
+
+ The errors that could be raised during the writing of a plist are
+ unpredictable and/or too large to list, so, a blind try: except: is done.
+ If an exception occurs, a UFOLibError will be raised.
+ """
+ if self._havePreviousFile:
+ try:
+ data = plistlib.dumps(obj)
+ except Exception as e:
+ raise UFOLibError(
+ "'%s' could not be written on %s because "
+ "the data is not properly formatted: %s"
+ % (fileName, self.fs, e)
+ )
+ if self.fs.exists(fileName) and data == self.fs.getbytes(fileName):
+ return
+ self.fs.setbytes(fileName, data)
+ else:
+ with self.fs.openbin(fileName, mode="w") as fp:
+ try:
+ plistlib.dump(obj, fp)
+ except Exception as e:
+ raise UFOLibError(
+ "'%s' could not be written on %s because "
+ "the data is not properly formatted: %s"
+ % (fileName, self.fs, e)
+ )
+
+
+# ----------
+# UFO Reader
+# ----------
+
+class UFOReader(object):
+
+ """
+ Read the various components of the .ufo.
+
+ By default read data is validated. Set ``validate`` to
+ ``False`` to not validate the data.
+ """
+
+ def __init__(self, path, validate=True):
+ if hasattr(path, "__fspath__"): # support os.PathLike objects
+ path = path.__fspath__()
+
+ if isinstance(path, basestring):
+ structure = _sniffFileStructure(path)
+ try:
+ if structure is UFOFileStructure.ZIP:
+ parentFS = fs.zipfs.ZipFS(path, write=False, encoding="utf-8")
+ else:
+ parentFS = fs.osfs.OSFS(path)
+ except fs.errors.CreateFailed as e:
+ raise UFOLibError("unable to open '%s': %s" % (path, e))
+
+ if structure is UFOFileStructure.ZIP:
+ # .ufoz zip files must contain a single root directory, with arbitrary
+ # name, containing all the UFO files
+ rootDirs = [
+ p.name for p in parentFS.scandir("/")
+ # exclude macOS metadata contained in zip file
+ if p.is_dir and p.name != "__MACOSX"
+ ]
+ if len(rootDirs) == 1:
+ # 'ClosingSubFS' ensures that the parent zip file is closed when
+ # its root subdirectory is closed
+ self.fs = parentFS.opendir(
+ rootDirs[0], factory=fs.subfs.ClosingSubFS
+ )
+ else:
+ raise UFOLibError(
+ "Expected exactly 1 root directory, found %d" % len(rootDirs)
+ )
+ else:
+ # normal UFO 'packages' are just a single folder
+ self.fs = parentFS
+ # when passed a path string, we make sure we close the newly opened fs
+ # upon calling UFOReader.close method or context manager's __exit__
+ self._shouldClose = True
+ self._fileStructure = structure
+ elif isinstance(path, fs.base.FS):
+ filesystem = path
+ try:
+ filesystem.check()
+ except fs.errors.FilesystemClosed:
+ raise UFOLibError("the filesystem '%s' is closed" % path)
+ else:
+ self.fs = filesystem
+ try:
+ path = filesystem.getsyspath("/")
+ except fs.errors.NoSysPath:
+ # network or in-memory FS may not map to the local one
+ path = unicode(filesystem)
+ # when user passed an already initialized fs instance, it is her
+ # responsibility to close it, thus UFOReader.close/__exit__ are no-op
+ self._shouldClose = False
+ # default to a 'package' structure
+ self._fileStructure = UFOFileStructure.PACKAGE
+ else:
+ raise TypeError(
+ "Expected a path string or fs.base.FS object, found '%s'"
+ % type(path).__name__
+ )
+ self._path = fsdecode(path)
+ self._validate = validate
+ self.readMetaInfo(validate=validate)
+ self._upConvertedKerningData = None
+
+ # properties
+
+ def _get_formatVersion(self):
+ return self._formatVersion
+
+ formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")
+
+ def _get_fileStructure(self):
+ return self._fileStructure
+
+ fileStructure = property(
+ _get_fileStructure,
+ doc=(
+ "The current file structure of the UFO: "
+ "either UFOFileStructure.ZIP or UFOFileStructure.PACKAGE"
+ )
+ )
+
+ # up conversion
+
+ def _upConvertKerning(self, validate):
+ """
+ Up convert kerning and groups in UFO 1 and 2.
+ The data will be held internally until each bit of data
+ has been retrieved. The conversion of both must be done
+ at once, so the raw data is cached and an error is raised
+ if one bit of data becomes obsolete before it is called.
+
+ ``validate`` will validate the data.
+ """
+ if self._upConvertedKerningData:
+ testKerning = self._readKerning()
+ if testKerning != self._upConvertedKerningData["originalKerning"]:
+ raise UFOLibError("The data in kerning.plist has been modified since it was converted to UFO 3 format.")
+ testGroups = self._readGroups()
+ if testGroups != self._upConvertedKerningData["originalGroups"]:
+ raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.")
+ else:
+ groups = self._readGroups()
+ if validate:
+ invalidFormatMessage = "groups.plist is not properly formatted."
+ if not isinstance(groups, dict):
+ raise UFOLibError(invalidFormatMessage)
+ for groupName, glyphList in groups.items():
+ if not isinstance(groupName, basestring):
+ raise UFOLibError(invalidFormatMessage)
+ elif not isinstance(glyphList, list):
+ raise UFOLibError(invalidFormatMessage)
+ for glyphName in glyphList:
+ if not isinstance(glyphName, basestring):
+ raise UFOLibError(invalidFormatMessage)
+ self._upConvertedKerningData = dict(
+ kerning={},
+ originalKerning=self._readKerning(),
+ groups={},
+ originalGroups=groups
+ )
+ # convert kerning and groups
+ kerning, groups, conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning(
+ self._upConvertedKerningData["originalKerning"],
+ deepcopy(self._upConvertedKerningData["originalGroups"])
+ )
+ # store
+ self._upConvertedKerningData["kerning"] = kerning
+ self._upConvertedKerningData["groups"] = groups
+ self._upConvertedKerningData["groupRenameMaps"] = conversionMaps
+
+ # support methods
+
+ _getPlist = _getPlist
+ getFileModificationTime = _getFileModificationTime
+ readBytesFromPath = _readBytesFromPath
+
+ def getReadFileForPath(self, path, encoding=None):
+ """
+ Returns a file (or file-like) object for the file at the given path.
+ The path must be relative to the UFO path.
+ Returns None if the file does not exist.
+ By default the file is opened in binary mode (reads bytes).
+ If encoding is passed, the file is opened in text mode (reads unicode).
+
+ Note: The caller is responsible for closing the open file.
+ """
+ path = fsdecode(path)
+ try:
+ if encoding is None:
+ return self.fs.openbin(path)
+ else:
+ return self.fs.open(path, mode="r", encoding=encoding)
+ except fs.errors.ResourceNotFound:
+ return None
+ # metainfo.plist
+
+ def readMetaInfo(self, validate=None):
+ """
+ Read metainfo.plist. Only used for internal operations.
+
+ ``validate`` will validate the read data, by default it is set
+ to the class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ data = self._getPlist(METAINFO_FILENAME)
+ if validate and not isinstance(data, dict):
+ raise UFOLibError("metainfo.plist is not properly formatted.")
+ formatVersion = data["formatVersion"]
+ if validate:
+ if not isinstance(formatVersion, int):
+ raise UFOLibError(
+ "formatVersion must be specified as an integer in '%s' on %s"
+ % (METAINFO_FILENAME, self.fs)
+ )
+ if formatVersion not in supportedUFOFormatVersions:
+ raise UFOLibError(
+ "Unsupported UFO format (%d) in '%s' on %s"
+ % (formatVersion, METAINFO_FILENAME, self.fs)
+ )
+ self._formatVersion = formatVersion
+
+ # groups.plist
+
+ def _readGroups(self):
+ return self._getPlist(GROUPS_FILENAME, {})
+
+ def readGroups(self, validate=None):
+ """
+ Read groups.plist. Returns a dict.
+ ``validate`` will validate the read data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ # handle up conversion
+ if self._formatVersion < 3:
+ self._upConvertKerning(validate)
+ groups = self._upConvertedKerningData["groups"]
+ # normal
+ else:
+ groups = self._readGroups()
+ if validate:
+ valid, message = groupsValidator(groups)
+ if not valid:
+ raise UFOLibError(message)
+ return groups
+
+ def getKerningGroupConversionRenameMaps(self, validate=None):
+ """
+ Get maps defining the renaming that was done during any
+ needed kerning group conversion. This method returns a
+ dictionary of this form:
+
+ {
+ "side1" : {"old group name" : "new group name"},
+ "side2" : {"old group name" : "new group name"}
+ }
+
+ When no conversion has been performed, the side1 and side2
+ dictionaries will be empty.
+
+ ``validate`` will validate the groups, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ if self._formatVersion >= 3:
+ return dict(side1={}, side2={})
+ # use the public group reader to force the load and
+ # conversion of the data if it hasn't happened yet.
+ self.readGroups(validate=validate)
+ return self._upConvertedKerningData["groupRenameMaps"]
+
+ # fontinfo.plist
+
+ def _readInfo(self, validate):
+ data = self._getPlist(FONTINFO_FILENAME, {})
+ if validate and not isinstance(data, dict):
+ raise UFOLibError("fontinfo.plist is not properly formatted.")
+ return data
+
+ def readInfo(self, info, validate=None):
+ """
+ Read fontinfo.plist. It requires an object that allows
+ setting attributes with names that follow the fontinfo.plist
+ version 3 specification. This will write the attributes
+ defined in the file into the object.
+
+ ``validate`` will validate the read data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ infoDict = self._readInfo(validate)
+ infoDataToSet = {}
+ # version 1
+ if self._formatVersion == 1:
+ for attr in fontInfoAttributesVersion1:
+ value = infoDict.get(attr)
+ if value is not None:
+ infoDataToSet[attr] = value
+ infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
+ infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
+ # version 2
+ elif self._formatVersion == 2:
+ for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()):
+ value = infoDict.get(attr)
+ if value is None:
+ continue
+ infoDataToSet[attr] = value
+ infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
+ # version 3
+ elif self._formatVersion == 3:
+ for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()):
+ value = infoDict.get(attr)
+ if value is None:
+ continue
+ infoDataToSet[attr] = value
+ # unsupported version
+ else:
+ raise NotImplementedError
+ # validate data
+ if validate:
+ infoDataToSet = validateInfoVersion3Data(infoDataToSet)
+ # populate the object
+ for attr, value in list(infoDataToSet.items()):
+ try:
+ setattr(info, attr, value)
+ except AttributeError:
+ raise UFOLibError("The supplied info object does not support setting a necessary attribute (%s)." % attr)
+
+ # kerning.plist
+
+ def _readKerning(self):
+ data = self._getPlist(KERNING_FILENAME, {})
+ return data
+
+ def readKerning(self, validate=None):
+ """
+ Read kerning.plist. Returns a dict.
+
+ ``validate`` will validate the kerning data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ # handle up conversion
+ if self._formatVersion < 3:
+ self._upConvertKerning(validate)
+ kerningNested = self._upConvertedKerningData["kerning"]
+ # normal
+ else:
+ kerningNested = self._readKerning()
+ if validate:
+ valid, message = kerningValidator(kerningNested)
+ if not valid:
+ raise UFOLibError(message)
+ # flatten
+ kerning = {}
+ for left in kerningNested:
+ for right in kerningNested[left]:
+ value = kerningNested[left][right]
+ kerning[left, right] = value
+ return kerning
+
+ # lib.plist
+
+ def readLib(self, validate=None):
+ """
+ Read lib.plist. Returns a dict.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ data = self._getPlist(LIB_FILENAME, {})
+ if validate:
+ valid, message = fontLibValidator(data)
+ if not valid:
+ raise UFOLibError(message)
+ return data
+
+ # features.fea
+
+ def readFeatures(self):
+ """
+ Read features.fea. Return a unicode string.
+ The returned string is empty if the file is missing.
+ """
+ try:
+ with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8") as f:
+ return f.read()
+ except fs.errors.ResourceNotFound:
+ return ""
+
+ # glyph sets & layers
+
+ def _readLayerContents(self, validate):
+ """
+ Rebuild the layer contents list by checking what glyphsets
+ are available on disk.
+
+ ``validate`` will validate the layer contents.
+ """
+ if self._formatVersion < 3:
+ return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
+ contents = self._getPlist(LAYERCONTENTS_FILENAME)
+ if validate:
+ valid, error = layerContentsValidator(contents, self.fs)
+ if not valid:
+ raise UFOLibError(error)
+ return contents
+
+ def getLayerNames(self, validate=None):
+ """
+ Get the ordered layer names from layercontents.plist.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ layerContents = self._readLayerContents(validate)
+ layerNames = [layerName for layerName, directoryName in layerContents]
+ return layerNames
+
+ def getDefaultLayerName(self, validate=None):
+ """
+ Get the default layer name from layercontents.plist.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ layerContents = self._readLayerContents(validate)
+ for layerName, layerDirectory in layerContents:
+ if layerDirectory == DEFAULT_GLYPHS_DIRNAME:
+ return layerName
+ # this will already have been raised during __init__
+ raise UFOLibError("The default layer is not defined in layercontents.plist.")
+
+ def getGlyphSet(self, layerName=None, validateRead=None, validateWrite=None):
+ """
+ Return the GlyphSet associated with the
+ glyphs directory mapped to layerName
+ in the UFO. If layerName is not provided,
+ the name retrieved with getDefaultLayerName
+ will be used.
+
+ ``validateRead`` will validate the read data, by default it is set to the
+ class's validate value, can be overridden.
+ ``validateWrte`` will validate the written data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ from fontTools.ufoLib.glifLib import GlyphSet
+
+ if validateRead is None:
+ validateRead = self._validate
+ if validateWrite is None:
+ validateWrite = self._validate
+ if layerName is None:
+ layerName = self.getDefaultLayerName(validate=validateRead)
+ directory = None
+ layerContents = self._readLayerContents(validateRead)
+ for storedLayerName, storedLayerDirectory in layerContents:
+ if layerName == storedLayerName:
+ directory = storedLayerDirectory
+ break
+ if directory is None:
+ raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName)
+ try:
+ glyphSubFS = self.fs.opendir(directory)
+ except fs.errors.ResourceNotFound:
+ raise UFOLibError(
+ "No '%s' directory for layer '%s'" % (directory, layerName)
+ )
+ return GlyphSet(
+ glyphSubFS,
+ ufoFormatVersion=self._formatVersion,
+ validateRead=validateRead,
+ validateWrite=validateWrite,
+ )
+
+ def getCharacterMapping(self, layerName=None, validate=None):
+ """
+ Return a dictionary that maps unicode values (ints) to
+ lists of glyph names.
+ """
+ if validate is None:
+ validate = self._validate
+ glyphSet = self.getGlyphSet(layerName, validateRead=validate, validateWrite=True)
+ allUnicodes = glyphSet.getUnicodes()
+ cmap = {}
+ for glyphName, unicodes in allUnicodes.items():
+ for code in unicodes:
+ if code in cmap:
+ cmap[code].append(glyphName)
+ else:
+ cmap[code] = [glyphName]
+ return cmap
+
+ # /data
+
+ def getDataDirectoryListing(self):
+ """
+ Returns a list of all files in the data directory.
+ The returned paths will be relative to the UFO.
+ This will not list directory names, only file names.
+ Thus, empty directories will be skipped.
+ """
+ try:
+ self._dataFS = self.fs.opendir(DATA_DIRNAME)
+ except fs.errors.ResourceNotFound:
+ return []
+ except fs.errors.DirectoryExpected:
+ raise UFOLibError("The UFO contains a \"data\" file instead of a directory.")
+ try:
+ # fs Walker.files method returns "absolute" paths (in terms of the
+ # root of the 'data' SubFS), so we strip the leading '/' to make
+ # them relative
+ return [
+ p.lstrip("/") for p in self._dataFS.walk.files()
+ ]
+ except fs.errors.ResourceError:
+ return []
+
+ def getImageDirectoryListing(self, validate=None):
+ """
+ Returns a list of all image file names in
+ the images directory. Each of the images will
+ have been verified to have the PNG signature.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if self._formatVersion < 3:
+ return []
+ if validate is None:
+ validate = self._validate
+ try:
+ self._imagesFS = imagesFS = self.fs.opendir(IMAGES_DIRNAME)
+ except fs.errors.ResourceNotFound:
+ return []
+ except fs.errors.DirectoryExpected:
+ raise UFOLibError("The UFO contains an \"images\" file instead of a directory.")
+ result = []
+ for path in imagesFS.scandir("/"):
+ if path.is_dir:
+ # silently skip this as version control
+ # systems often have hidden directories
+ continue
+ if validate:
+ with imagesFS.openbin(path.name) as fp:
+ valid, error = pngValidator(fileObj=fp)
+ if valid:
+ result.append(path.name)
+ else:
+ result.append(path.name)
+ return result
+
+ def readData(self, fileName):
+ """
+ Return bytes for the file named 'fileName' inside the 'data/' directory.
+ """
+ fileName = fsdecode(fileName)
+ try:
+ try:
+ dataFS = self._dataFS
+ except AttributeError:
+ # in case readData is called before getDataDirectoryListing
+ dataFS = self.fs.opendir(DATA_DIRNAME)
+ data = dataFS.getbytes(fileName)
+ except fs.errors.ResourceNotFound:
+ raise UFOLibError("No data file named '%s' on %s" % (fileName, self.fs))
+ return data
+
+ def readImage(self, fileName, validate=None):
+ """
+ Return image data for the file named fileName.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ if self._formatVersion < 3:
+ raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion)
+ fileName = fsdecode(fileName)
+ try:
+ try:
+ imagesFS = self._imagesFS
+ except AttributeError:
+ # in case readImage is called before getImageDirectoryListing
+ imagesFS = self.fs.opendir(IMAGES_DIRNAME)
+ data = imagesFS.getbytes(fileName)
+ except fs.errors.ResourceNotFound:
+ raise UFOLibError("No image file named '%s' on %s" % (fileName, self.fs))
+ if validate:
+ valid, error = pngValidator(data=data)
+ if not valid:
+ raise UFOLibError(error)
+ return data
+
+ def close(self):
+ if self._shouldClose:
+ self.fs.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ self.close()
+
+
+# ----------
+# UFO Writer
+# ----------
+
+class UFOWriter(object):
+
+ """
+ Write the various components of the .ufo.
+
+ By default, the written data will be validated before writing. Set ``validate`` to
+ ``False`` if you do not want to validate the data. Validation can also be overriden
+ on a per method level if desired.
+ """
+
+ def __init__(
+ self,
+ path,
+ formatVersion=3,
+ fileCreator="com.github.fonttools.ufoLib",
+ structure=None,
+ validate=True,
+ ):
+ if formatVersion not in supportedUFOFormatVersions:
+ raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
+
+ if hasattr(path, "__fspath__"): # support os.PathLike objects
+ path = path.__fspath__()
+
+ if isinstance(path, basestring):
+ havePreviousFile = os.path.exists(path)
+ if havePreviousFile:
+ # ensure we use the same structure as the destination
+ existingStructure = _sniffFileStructure(path)
+ if structure is not None:
+ try:
+ structure = UFOFileStructure(structure)
+ except ValueError:
+ raise UFOLibError(
+ "Invalid or unsupported structure: '%s'" % structure
+ )
+ if structure is not existingStructure:
+ raise UFOLibError(
+ "A UFO with a different structure (%s) already exists "
+ "at the given path: '%s'" % (existingStructure, path)
+ )
+ else:
+ structure = existingStructure
+ else:
+ # if not exists, default to 'package' structure
+ if structure is None:
+ structure = UFOFileStructure.PACKAGE
+ dirName = os.path.dirname(path)
+ if dirName and not os.path.isdir(dirName):
+ raise UFOLibError(
+ "Cannot write to '%s': directory does not exist" % path
+ )
+ if structure is UFOFileStructure.ZIP:
+ if havePreviousFile:
+ # we can't write a zip in-place, so we have to copy its
+ # contents to a temporary location and work from there, then
+ # upon closing UFOWriter we create the final zip file
+ parentFS = fs.tempfs.TempFS()
+ with fs.zipfs.ZipFS(path, encoding="utf-8") as origFS:
+ fs.copy.copy_fs(origFS, parentFS)
+ # if output path is an existing zip, we require that it contains
+ # one, and only one, root directory (with arbitrary name), in turn
+ # containing all the existing UFO contents
+ rootDirs = [
+ p.name for p in parentFS.scandir("/")
+ # exclude macOS metadata contained in zip file
+ if p.is_dir and p.name != "__MACOSX"
+ ]
+ if len(rootDirs) != 1:
+ raise UFOLibError(
+ "Expected exactly 1 root directory, found %d" % len(rootDirs)
+ )
+ else:
+ # 'ClosingSubFS' ensures that the parent filesystem is closed
+ # when its root subdirectory is closed
+ self.fs = parentFS.opendir(
+ rootDirs[0], factory=fs.subfs.ClosingSubFS
+ )
+ else:
+ # if the output zip file didn't exist, we create the root folder;
+ # we name it the same as input 'path', but with '.ufo' extension
+ rootDir = os.path.splitext(os.path.basename(path))[0] + ".ufo"
+ parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8")
+ parentFS.makedir(rootDir)
+ self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS)
+ else:
+ self.fs = fs.osfs.OSFS(path, create=True)
+ self._fileStructure = structure
+ self._havePreviousFile = havePreviousFile
+ self._shouldClose = True
+ elif isinstance(path, fs.base.FS):
+ filesystem = path
+ try:
+ filesystem.check()
+ except fs.errors.FilesystemClosed:
+ raise UFOLibError("the filesystem '%s' is closed" % path)
+ else:
+ self.fs = filesystem
+ try:
+ path = filesystem.getsyspath("/")
+ except fs.errors.NoSysPath:
+ # network or in-memory FS may not map to the local one
+ path = unicode(filesystem)
+ # if passed an FS object, always use 'package' structure
+ if structure and structure is not UFOFileStructure.PACKAGE:
+ import warnings
+
+ warnings.warn(
+ "The 'structure' argument is not used when input is an FS object",
+ UserWarning,
+ stacklevel=2,
+ )
+ self._fileStructure = UFOFileStructure.PACKAGE
+ # if FS contains a "metainfo.plist", we consider it non-empty
+ self._havePreviousFile = filesystem.exists(METAINFO_FILENAME)
+ # the user is responsible for closing the FS object
+ self._shouldClose = False
+ else:
+ raise TypeError(
+ "Expected a path string or fs object, found %s"
+ % type(path).__name__
+ )
+
+ # establish some basic stuff
+ self._path = fsdecode(path)
+ self._formatVersion = formatVersion
+ self._fileCreator = fileCreator
+ self._downConversionKerningData = None
+ self._validate = validate
+ # if the file already exists, get the format version.
+ # this will be needed for up and down conversion.
+ previousFormatVersion = None
+ if self._havePreviousFile:
+ metaInfo = self._getPlist(METAINFO_FILENAME)
+ previousFormatVersion = metaInfo.get("formatVersion")
+ try:
+ previousFormatVersion = int(previousFormatVersion)
+ except (ValueError, TypeError):
+ self.fs.close()
+ raise UFOLibError("The existing metainfo.plist is not properly formatted.")
+ if previousFormatVersion not in supportedUFOFormatVersions:
+ self.fs.close()
+ raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
+ # catch down conversion
+ if previousFormatVersion is not None and previousFormatVersion > formatVersion:
+ raise UFOLibError("The UFO located at this path is a higher version (%d) than the version (%d) that is trying to be written. This is not supported." % (previousFormatVersion, formatVersion))
+ # handle the layer contents
+ self.layerContents = {}
+ if previousFormatVersion is not None and previousFormatVersion >= 3:
+ # already exists
+ self._readLayerContents(validate=validate)
+ else:
+ # previous < 3
+ # imply the layer contents
+ if self.fs.exists(DEFAULT_GLYPHS_DIRNAME):
+ self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME}
+ # write the new metainfo
+ self._writeMetaInfo()
+
+ # properties
+
+ def _get_path(self):
+ import warnings
+
+ warnings.warn(
+ "The 'path' attribute is deprecated; use the 'fs' attribute instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._path
+
+ path = property(_get_path, doc="The path the UFO is being written to (DEPRECATED).")
+
+ def _get_formatVersion(self):
+ return self._formatVersion
+
+ formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.")
+
+ def _get_fileCreator(self):
+ return self._fileCreator
+
+ fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.")
+
+ def _get_fileStructure(self):
+ return self._fileStructure
+
+ fileStructure = property(
+ _get_fileStructure,
+ doc=(
+ "The file structure of the destination UFO: "
+ "either UFOFileStrucure.ZIP or UFOFileStructure.PACKAGE"
+ )
+ )
+
+ # support methods for file system interaction
+
+ _getPlist = _getPlist
+ _writePlist = _writePlist
+ readBytesFromPath = _readBytesFromPath
+ getFileModificationTime = _getFileModificationTime
+
+ def copyFromReader(self, reader, sourcePath, destPath):
+ """
+ Copy the sourcePath in the provided UFOReader to destPath
+ in this writer. The paths must be relative. This works with
+ both individual files and directories.
+ """
+ if not isinstance(reader, UFOReader):
+ raise UFOLibError("The reader must be an instance of UFOReader.")
+ sourcePath = fsdecode(sourcePath)
+ destPath = fsdecode(destPath)
+ if not reader.fs.exists(sourcePath):
+ raise UFOLibError("The reader does not have data located at \"%s\"." % sourcePath)
+ if self.fs.exists(destPath):
+ raise UFOLibError("A file named \"%s\" already exists." % destPath)
+ # create the destination directory if it doesn't exist
+ self.fs.makedirs(fs.path.dirname(destPath), recreate=True)
+ if reader.fs.isdir(sourcePath):
+ fs.copy.copy_dir(reader.fs, sourcePath, self.fs, destPath)
+ else:
+ fs.copy.copy_file(reader.fs, sourcePath, self.fs, destPath)
+
+ def writeBytesToPath(self, path, data):
+ """
+ Write bytes to a path relative to the UFO filesystem's root.
+ If writing to an existing UFO, check to see if data matches the data
+ that is already in the file at path; if so, the file is not rewritten
+ so that the modification date is preserved.
+ If needed, the directory tree for the given path will be built.
+ """
+ path = fsdecode(path)
+ if self._havePreviousFile:
+ if self.fs.isfile(path) and data == self.fs.getbytes(path):
+ return
+ try:
+ self.fs.setbytes(path, data)
+ except fs.errors.FileExpected:
+ raise UFOLibError("A directory exists at '%s'" % path)
+ except fs.errors.ResourceNotFound:
+ self.fs.makedirs(fs.path.dirname(path), recreate=True)
+ self.fs.setbytes(path, data)
+
+ def getFileObjectForPath(self, path, mode="w", encoding=None):
+ """
+ Returns a file (or file-like) object for the
+ file at the given path. The path must be relative
+ to the UFO path. Returns None if the file does
+ not exist and the mode is "r" or "rb.
+ An encoding may be passed if the file is opened in text mode.
+
+ Note: The caller is responsible for closing the open file.
+ """
+ path = fsdecode(path)
+ try:
+ return self.fs.open(path, mode=mode, encoding=encoding)
+ except fs.errors.ResourceNotFound as e:
+ m = mode[0]
+ if m == "r":
+ # XXX I think we should just let it raise. The docstring,
+ # however, says that this returns None if mode is 'r'
+ return None
+ elif m == "w" or m == "a" or m == "x":
+ self.fs.makedirs(fs.path.dirname(path), recreate=True)
+ return self.fs.open(path, mode=mode, encoding=encoding)
+ except fs.errors.ResourceError as e:
+ return UFOLibError(
+ "unable to open '%s' on %s: %s" % (path, self.fs, e)
+ )
+
+ def removePath(self, path, force=False, removeEmptyParents=True):
+ """
+ Remove the file (or directory) at path. The path
+ must be relative to the UFO.
+ Raises UFOLibError if the path doesn't exist.
+ If force=True, ignore non-existent paths.
+ If the directory where 'path' is located becomes empty, it will
+ be automatically removed, unless 'removeEmptyParents' is False.
+ """
+ path = fsdecode(path)
+ try:
+ self.fs.remove(path)
+ except fs.errors.FileExpected:
+ self.fs.removetree(path)
+ except fs.errors.ResourceNotFound:
+ if not force:
+ raise UFOLibError(
+ "'%s' does not exist on %s" % (path, self.fs)
+ )
+ if removeEmptyParents:
+ parent = fs.path.dirname(path)
+ if parent:
+ fs.tools.remove_empty(self.fs, parent)
+
+ # alias kept for backward compatibility with old API
+ removeFileForPath = removePath
+
+ # UFO mod time
+
+ def setModificationTime(self):
+ """
+ Set the UFO modification time to the current time.
+ This is never called automatically. It is up to the
+ caller to call this when finished working on the UFO.
+ """
+ path = self._path
+ if path is not None and os.path.exists(path):
+ try:
+ # this may fail on some filesystems (e.g. SMB servers)
+ os.utime(path, None)
+ except OSError as e:
+ logger.warning("Failed to set modified time: %s", e)
+
+ # metainfo.plist
+
+ def _writeMetaInfo(self):
+ metaInfo = dict(
+ creator=self._fileCreator,
+ formatVersion=self._formatVersion
+ )
+ self._writePlist(METAINFO_FILENAME, metaInfo)
+
+ # groups.plist
+
+ def setKerningGroupConversionRenameMaps(self, maps):
+ """
+ Set maps defining the renaming that should be done
+ when writing groups and kerning in UFO 1 and UFO 2.
+ This will effectively undo the conversion done when
+ UFOReader reads this data. The dictionary should have
+ this form:
+
+ {
+ "side1" : {"group name to use when writing" : "group name in data"},
+ "side2" : {"group name to use when writing" : "group name in data"}
+ }
+
+ This is the same form returned by UFOReader's
+ getKerningGroupConversionRenameMaps method.
+ """
+ if self._formatVersion >= 3:
+ return # XXX raise an error here
+ # flip the dictionaries
+ remap = {}
+ for side in ("side1", "side2"):
+ for writeName, dataName in list(maps[side].items()):
+ remap[dataName] = writeName
+ self._downConversionKerningData = dict(groupRenameMap=remap)
+
+ def writeGroups(self, groups, validate=None):
+ """
+ Write groups.plist. This method requires a
+ dict of glyph groups as an argument.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ # validate the data structure
+ if validate:
+ valid, message = groupsValidator(groups)
+ if not valid:
+ raise UFOLibError(message)
+ # down convert
+ if self._formatVersion < 3 and self._downConversionKerningData is not None:
+ remap = self._downConversionKerningData["groupRenameMap"]
+ remappedGroups = {}
+ # there are some edge cases here that are ignored:
+ # 1. if a group is being renamed to a name that
+ # already exists, the existing group is always
+ # overwritten. (this is why there are two loops
+ # below.) there doesn't seem to be a logical
+ # solution to groups mismatching and overwriting
+ # with the specifiecd group seems like a better
+ # solution than throwing an error.
+ # 2. if side 1 and side 2 groups are being renamed
+ # to the same group name there is no check to
+ # ensure that the contents are identical. that
+ # is left up to the caller.
+ for name, contents in list(groups.items()):
+ if name in remap:
+ continue
+ remappedGroups[name] = contents
+ for name, contents in list(groups.items()):
+ if name not in remap:
+ continue
+ name = remap[name]
+ remappedGroups[name] = contents
+ groups = remappedGroups
+ # pack and write
+ groupsNew = {}
+ for key, value in groups.items():
+ groupsNew[key] = list(value)
+ if groupsNew:
+ self._writePlist(GROUPS_FILENAME, groupsNew)
+ elif self._havePreviousFile:
+ self.removePath(GROUPS_FILENAME, force=True, removeEmptyParents=False)
+
+ # fontinfo.plist
+
+ def writeInfo(self, info, validate=None):
+ """
+ Write info.plist. This method requires an object
+ that supports getting attributes that follow the
+ fontinfo.plist version 2 specification. Attributes
+ will be taken from the given object and written
+ into the file.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ # gather version 3 data
+ infoData = {}
+ for attr in list(fontInfoAttributesVersion3ValueData.keys()):
+ if hasattr(info, attr):
+ try:
+ value = getattr(info, attr)
+ except AttributeError:
+ raise UFOLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
+ if value is None:
+ continue
+ infoData[attr] = value
+ # down convert data if necessary and validate
+ if self._formatVersion == 3:
+ if validate:
+ infoData = validateInfoVersion3Data(infoData)
+ elif self._formatVersion == 2:
+ infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
+ if validate:
+ infoData = validateInfoVersion2Data(infoData)
+ elif self._formatVersion == 1:
+ infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
+ if validate:
+ infoData = validateInfoVersion2Data(infoData)
+ infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
+ # write file
+ self._writePlist(FONTINFO_FILENAME, infoData)
+
+ # kerning.plist
+
+ def writeKerning(self, kerning, validate=None):
+ """
+ Write kerning.plist. This method requires a
+ dict of kerning pairs as an argument.
+
+ This performs basic structural validation of the kerning,
+ but it does not check for compliance with the spec in
+ regards to conflicting pairs. The assumption is that the
+ kerning data being passed is standards compliant.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ # validate the data structure
+ if validate:
+ invalidFormatMessage = "The kerning is not properly formatted."
+ if not isDictEnough(kerning):
+ raise UFOLibError(invalidFormatMessage)
+ for pair, value in list(kerning.items()):
+ if not isinstance(pair, (list, tuple)):
+ raise UFOLibError(invalidFormatMessage)
+ if not len(pair) == 2:
+ raise UFOLibError(invalidFormatMessage)
+ if not isinstance(pair[0], basestring):
+ raise UFOLibError(invalidFormatMessage)
+ if not isinstance(pair[1], basestring):
+ raise UFOLibError(invalidFormatMessage)
+ if not isinstance(value, numberTypes):
+ raise UFOLibError(invalidFormatMessage)
+ # down convert
+ if self._formatVersion < 3 and self._downConversionKerningData is not None:
+ remap = self._downConversionKerningData["groupRenameMap"]
+ remappedKerning = {}
+ for (side1, side2), value in list(kerning.items()):
+ side1 = remap.get(side1, side1)
+ side2 = remap.get(side2, side2)
+ remappedKerning[side1, side2] = value
+ kerning = remappedKerning
+ # pack and write
+ kerningDict = {}
+ for left, right in kerning.keys():
+ value = kerning[left, right]
+ if left not in kerningDict:
+ kerningDict[left] = {}
+ kerningDict[left][right] = value
+ if kerningDict:
+ self._writePlist(KERNING_FILENAME, kerningDict)
+ elif self._havePreviousFile:
+ self.removePath(KERNING_FILENAME, force=True, removeEmptyParents=False)
+
+ # lib.plist
+
+ def writeLib(self, libDict, validate=None):
+ """
+ Write lib.plist. This method requires a
+ lib dict as an argument.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validate
+ if validate:
+ valid, message = fontLibValidator(libDict)
+ if not valid:
+ raise UFOLibError(message)
+ if libDict:
+ self._writePlist(LIB_FILENAME, libDict)
+ elif self._havePreviousFile:
+ self.removePath(LIB_FILENAME, force=True, removeEmptyParents=False)
+
+ # features.fea
+
+ def writeFeatures(self, features, validate=None):
+ """
+ Write features.fea. This method requires a
+ features string as an argument.
+ """
+ if validate is None:
+ validate = self._validate
+ if self._formatVersion == 1:
+ raise UFOLibError("features.fea is not allowed in UFO Format Version 1.")
+ if validate:
+ if not isinstance(features, basestring):
+ raise UFOLibError("The features are not text.")
+ if features:
+ self.writeBytesToPath(FEATURES_FILENAME, features.encode("utf8"))
+ elif self._havePreviousFile:
+ self.removePath(FEATURES_FILENAME, force=True, removeEmptyParents=False)
+
+ # glyph sets & layers
+
+ def _readLayerContents(self, validate):
+ """
+ Rebuild the layer contents list by checking what glyph sets
+ are available on disk.
+
+ ``validate`` will validate the data.
+ """
+ # read the file on disk
+ raw = self._getPlist(LAYERCONTENTS_FILENAME)
+ contents = {}
+ if validate:
+ valid, error = layerContentsValidator(raw, self.fs)
+ if not valid:
+ raise UFOLibError(error)
+ for entry in raw:
+ layerName, directoryName = entry
+ contents[layerName] = directoryName
+ self.layerContents = contents
+
+ def writeLayerContents(self, layerOrder=None, validate=None):
+ """
+ Write the layercontents.plist file. This method *must* be called
+ after all glyph sets have been written.
+ """
+ if validate is None:
+ validate = self._validate
+ if self.formatVersion < 3:
+ return
+ if layerOrder is not None:
+ newOrder = []
+ for layerName in layerOrder:
+ if layerName is None:
+ layerName = DEFAULT_LAYER_NAME
+ else:
+ layerName = tounicode(layerName)
+ newOrder.append(layerName)
+ layerOrder = newOrder
+ else:
+ layerOrder = list(self.layerContents.keys())
+ if validate and set(layerOrder) != set(self.layerContents.keys()):
+ raise UFOLibError("The layer order content does not match the glyph sets that have been created.")
+ layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder]
+ self._writePlist(LAYERCONTENTS_FILENAME, layerContents)
+
+ def _findDirectoryForLayerName(self, layerName):
+ foundDirectory = None
+ for existingLayerName, directoryName in list(self.layerContents.items()):
+ if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME:
+ foundDirectory = directoryName
+ break
+ elif existingLayerName == layerName:
+ foundDirectory = directoryName
+ break
+ if not foundDirectory:
+ raise UFOLibError("Could not locate a glyph set directory for the layer named %s." % layerName)
+ return foundDirectory
+
+ def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None, validateRead=None, validateWrite=None):
+ """
+ Return the GlyphSet object associated with the
+ appropriate glyph directory in the .ufo.
+ If layerName is None, the default glyph set
+ will be used. The defaultLayer flag indictes
+ that the layer should be saved into the default
+ glyphs directory.
+
+ ``validateRead`` will validate the read data, by default it is set to the
+ class's validate value, can be overridden.
+ ``validateWrte`` will validate the written data, by default it is set to the
+ class's validate value, can be overridden.
+ """
+ if validateRead is None:
+ validateRead = self._validate
+ if validateWrite is None:
+ validateWrite = self._validate
+ # only default can be written in < 3
+ if self._formatVersion < 3 and (not defaultLayer or layerName is not None):
+ raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion)
+ # locate a layer name when None has been given
+ if layerName is None and defaultLayer:
+ for existingLayerName, directory in list(self.layerContents.items()):
+ if directory == DEFAULT_GLYPHS_DIRNAME:
+ layerName = existingLayerName
+ if layerName is None:
+ layerName = DEFAULT_LAYER_NAME
+ elif layerName is None and not defaultLayer:
+ raise UFOLibError("A layer name must be provided for non-default layers.")
+ # move along to format specific writing
+ if self.formatVersion == 1:
+ return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
+ elif self.formatVersion == 2:
+ return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
+ elif self.formatVersion == 3:
+ return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
+ else:
+ raise AssertionError(self.formatVersion)
+
+ def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
+ from fontTools.ufoLib.glifLib import GlyphSet
+
+ glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True),
+ return GlyphSet(
+ glyphSubFS,
+ glyphNameToFileNameFunc=glyphNameToFileNameFunc,
+ ufoFormatVersion=1,
+ validateRead=validateRead,
+ validateWrite=validateWrite,
+ )
+
+ def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
+ from fontTools.ufoLib.glifLib import GlyphSet
+
+ glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
+ return GlyphSet(
+ glyphSubFS,
+ glyphNameToFileNameFunc=glyphNameToFileNameFunc,
+ ufoFormatVersion=2,
+ validateRead=validateRead,
+ validateWrite=validateWrite,
+ )
+
+ def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None):
+ from fontTools.ufoLib.glifLib import GlyphSet
+
+ # if the default flag is on, make sure that the default in the file
+ # matches the default being written. also make sure that this layer
+ # name is not already linked to a non-default layer.
+ if defaultLayer:
+ for existingLayerName, directory in list(self.layerContents.items()):
+ if directory == DEFAULT_GLYPHS_DIRNAME:
+ if existingLayerName != layerName:
+ raise UFOLibError("Another layer is already mapped to the default directory.")
+ elif existingLayerName == layerName:
+ raise UFOLibError("The layer name is already mapped to a non-default layer.")
+ # get an existing directory name
+ if layerName in self.layerContents:
+ directory = self.layerContents[layerName]
+ # get a new directory name
+ else:
+ if defaultLayer:
+ directory = DEFAULT_GLYPHS_DIRNAME
+ else:
+ # not caching this could be slightly expensive,
+ # but caching it will be cumbersome
+ existing = [d.lower() for d in list(self.layerContents.values())]
+ if not isinstance(layerName, unicode):
+ try:
+ layerName = unicode(layerName)
+ except UnicodeDecodeError:
+ raise UFOLibError("The specified layer name is not a Unicode string.")
+ directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.")
+ # make the directory
+ glyphSubFS = self.fs.makedir(directory, recreate=True)
+ # store the mapping
+ self.layerContents[layerName] = directory
+ # load the glyph set
+ return GlyphSet(
+ glyphSubFS,
+ glyphNameToFileNameFunc=glyphNameToFileNameFunc,
+ ufoFormatVersion=3,
+ validateRead=validateRead,
+ validateWrite=validateWrite,
+ )
+
+ def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False):
+ """
+ Rename a glyph set.
+
+ Note: if a GlyphSet object has already been retrieved for
+ layerName, it is up to the caller to inform that object that
+ the directory it represents has changed.
+ """
+ if self._formatVersion < 3:
+ # ignore renaming glyph sets for UFO1 UFO2
+ # just write the data from the default layer
+ return
+ # the new and old names can be the same
+ # as long as the default is being switched
+ if layerName == newLayerName:
+ # if the default is off and the layer is already not the default, skip
+ if self.layerContents[layerName] != DEFAULT_GLYPHS_DIRNAME and not defaultLayer:
+ return
+ # if the default is on and the layer is already the default, skip
+ if self.layerContents[layerName] == DEFAULT_GLYPHS_DIRNAME and defaultLayer:
+ return
+ else:
+ # make sure the new layer name doesn't already exist
+ if newLayerName is None:
+ newLayerName = DEFAULT_LAYER_NAME
+ if newLayerName in self.layerContents:
+ raise UFOLibError("A layer named %s already exists." % newLayerName)
+ # make sure the default layer doesn't already exist
+ if defaultLayer and DEFAULT_GLYPHS_DIRNAME in list(self.layerContents.values()):
+ raise UFOLibError("A default layer already exists.")
+ # get the paths
+ oldDirectory = self._findDirectoryForLayerName(layerName)
+ if defaultLayer:
+ newDirectory = DEFAULT_GLYPHS_DIRNAME
+ else:
+ existing = [name.lower() for name in list(self.layerContents.values())]
+ newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.")
+ # update the internal mapping
+ del self.layerContents[layerName]
+ self.layerContents[newLayerName] = newDirectory
+ # do the file system copy
+ self.fs.movedir(oldDirectory, newDirectory, create=True)
+
+ def deleteGlyphSet(self, layerName):
+ """
+ Remove the glyph set matching layerName.
+ """
+ if self._formatVersion < 3:
+ # ignore deleting glyph sets for UFO1 UFO2 as there are no layers
+ # just write the data from the default layer
+ return
+ foundDirectory = self._findDirectoryForLayerName(layerName)
+ self.removePath(foundDirectory, removeEmptyParents=False)
+ del self.layerContents[layerName]
+
+ def writeData(self, fileName, data):
+ """
+ Write data to fileName in the 'data' directory.
+ The data must be a bytes string.
+ """
+ self.writeBytesToPath("%s/%s" % (DATA_DIRNAME, fsdecode(fileName)), data)
+
+ def removeData(self, fileName):
+ """
+ Remove the file named fileName from the data directory.
+ """
+ self.removePath("%s/%s" % (DATA_DIRNAME, fsdecode(fileName)))
+
+ # /images
+
+ def writeImage(self, fileName, data, validate=None):
+ """
+ Write data to fileName in the images directory.
+ The data must be a valid PNG.
+ """
+ if validate is None:
+ validate = self._validate
+ if self._formatVersion < 3:
+ raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
+ fileName = fsdecode(fileName)
+ if validate:
+ valid, error = pngValidator(data=data)
+ if not valid:
+ raise UFOLibError(error)
+ self.writeBytesToPath("%s/%s" % (IMAGES_DIRNAME, fileName), data)
+
+ def removeImage(self, fileName, validate=None): # XXX remove unused 'validate'?
+ """
+ Remove the file named fileName from the
+ images directory.
+ """
+ if self._formatVersion < 3:
+ raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
+ self.removePath("%s/%s" % (IMAGES_DIRNAME, fsdecode(fileName)))
+
+ def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None):
+ """
+ Copy the sourceFileName in the provided UFOReader to destFileName
+ in this writer. This uses the most memory efficient method possible
+ for copying the data possible.
+ """
+ if validate is None:
+ validate = self._validate
+ if self._formatVersion < 3:
+ raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
+ sourcePath = "%s/%s" % (IMAGES_DIRNAME, fsdecode(sourceFileName))
+ destPath = "%s/%s" % (IMAGES_DIRNAME, fsdecode(destFileName))
+ self.copyFromReader(reader, sourcePath, destPath)
+
+ def close(self):
+ if self._havePreviousFile and self._fileStructure is UFOFileStructure.ZIP:
+ # if we are updating an existing zip file, we can now compress the
+ # contents of the temporary filesystem in the destination path
+ rootDir = os.path.splitext(os.path.basename(self._path))[0] + ".ufo"
+ with fs.zipfs.ZipFS(self._path, write=True, encoding="utf-8") as destFS:
+ fs.copy.copy_fs(self.fs, destFS.makedir(rootDir))
+ if self._shouldClose:
+ self.fs.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ self.close()
+
+
+# ----------------
+# Helper Functions
+# ----------------
+
+
+def _sniffFileStructure(ufo_path):
+ """Return UFOFileStructure.ZIP if the UFO at path 'ufo_path' (basestring)
+ is a zip file, else return UFOFileStructure.PACKAGE if 'ufo_path' is a
+ directory.
+ Raise UFOLibError if it is a file with unknown structure, or if the path
+ does not exist.
+ """
+ if zipfile.is_zipfile(ufo_path):
+ return UFOFileStructure.ZIP
+ elif os.path.isdir(ufo_path):
+ return UFOFileStructure.PACKAGE
+ elif os.path.isfile(ufo_path):
+ raise UFOLibError(
+ "The specified UFO does not have a known structure: '%s'" % ufo_path
+ )
+ else:
+ raise UFOLibError("No such file or directory: '%s'" % ufo_path)
+
+
+def makeUFOPath(path):
+ """
+ Return a .ufo pathname.
+
+ >>> makeUFOPath("directory/something.ext") == (
+ ... os.path.join('directory', 'something.ufo'))
+ True
+ >>> makeUFOPath("directory/something.another.thing.ext") == (
+ ... os.path.join('directory', 'something.another.thing.ufo'))
+ True
+ """
+ dir, name = os.path.split(path)
+ name = ".".join([".".join(name.split(".")[:-1]), "ufo"])
+ return os.path.join(dir, name)
+
+# ----------------------
+# fontinfo.plist Support
+# ----------------------
+
+# Version Validators
+
+# There is no version 1 validator and there shouldn't be.
+# The version 1 spec was very loose and there were numerous
+# cases of invalid values.
+
+def validateFontInfoVersion2ValueForAttribute(attr, value):
+ """
+ This performs very basic validation of the value for attribute
+ following the UFO 2 fontinfo.plist specification. The results
+ of this should not be interpretted as *correct* for the font
+ that they are part of. This merely indicates that the value
+ is of the proper type and, where the specification defines
+ a set range of possible values for an attribute, that the
+ value is in the accepted range.
+ """
+ dataValidationDict = fontInfoAttributesVersion2ValueData[attr]
+ valueType = dataValidationDict.get("type")
+ validator = dataValidationDict.get("valueValidator")
+ valueOptions = dataValidationDict.get("valueOptions")
+ # have specific options for the validator
+ if valueOptions is not None:
+ isValidValue = validator(value, valueOptions)
+ # no specific options
+ else:
+ if validator == genericTypeValidator:
+ isValidValue = validator(value, valueType)
+ else:
+ isValidValue = validator(value)
+ return isValidValue
+
+def validateInfoVersion2Data(infoData):
+ """
+ This performs very basic validation of the value for infoData
+ following the UFO 2 fontinfo.plist specification. The results
+ of this should not be interpretted as *correct* for the font
+ that they are part of. This merely indicates that the values
+ are of the proper type and, where the specification defines
+ a set range of possible values for an attribute, that the
+ value is in the accepted range.
+ """
+ validInfoData = {}
+ for attr, value in list(infoData.items()):
+ isValidValue = validateFontInfoVersion2ValueForAttribute(attr, value)
+ if not isValidValue:
+ raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
+ else:
+ validInfoData[attr] = value
+ return validInfoData
+
+def validateFontInfoVersion3ValueForAttribute(attr, value):
+ """
+ This performs very basic validation of the value for attribute
+ following the UFO 3 fontinfo.plist specification. The results
+ of this should not be interpretted as *correct* for the font
+ that they are part of. This merely indicates that the value
+ is of the proper type and, where the specification defines
+ a set range of possible values for an attribute, that the
+ value is in the accepted range.
+ """
+ dataValidationDict = fontInfoAttributesVersion3ValueData[attr]
+ valueType = dataValidationDict.get("type")
+ validator = dataValidationDict.get("valueValidator")
+ valueOptions = dataValidationDict.get("valueOptions")
+ # have specific options for the validator
+ if valueOptions is not None:
+ isValidValue = validator(value, valueOptions)
+ # no specific options
+ else:
+ if validator == genericTypeValidator:
+ isValidValue = validator(value, valueType)
+ else:
+ isValidValue = validator(value)
+ return isValidValue
+
+def validateInfoVersion3Data(infoData):
+ """
+ This performs very basic validation of the value for infoData
+ following the UFO 3 fontinfo.plist specification. The results
+ of this should not be interpretted as *correct* for the font
+ that they are part of. This merely indicates that the values
+ are of the proper type and, where the specification defines
+ a set range of possible values for an attribute, that the
+ value is in the accepted range.
+ """
+ validInfoData = {}
+ for attr, value in list(infoData.items()):
+ isValidValue = validateFontInfoVersion3ValueForAttribute(attr, value)
+ if not isValidValue:
+ raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
+ else:
+ validInfoData[attr] = value
+ return validInfoData
+
+# Value Options
+
+fontInfoOpenTypeHeadFlagsOptions = list(range(0, 15))
+fontInfoOpenTypeOS2SelectionOptions = [1, 2, 3, 4, 7, 8, 9]
+fontInfoOpenTypeOS2UnicodeRangesOptions = list(range(0, 128))
+fontInfoOpenTypeOS2CodePageRangesOptions = list(range(0, 64))
+fontInfoOpenTypeOS2TypeOptions = [0, 1, 2, 3, 8, 9]
+
+# Version Attribute Definitions
+# This defines the attributes, types and, in some
+# cases the possible values, that can exist is
+# fontinfo.plist.
+
+fontInfoAttributesVersion1 = set([
+ "familyName",
+ "styleName",
+ "fullName",
+ "fontName",
+ "menuName",
+ "fontStyle",
+ "note",
+ "versionMajor",
+ "versionMinor",
+ "year",
+ "copyright",
+ "notice",
+ "trademark",
+ "license",
+ "licenseURL",
+ "createdBy",
+ "designer",
+ "designerURL",
+ "vendorURL",
+ "unitsPerEm",
+ "ascender",
+ "descender",
+ "capHeight",
+ "xHeight",
+ "defaultWidth",
+ "slantAngle",
+ "italicAngle",
+ "widthName",
+ "weightName",
+ "weightValue",
+ "fondName",
+ "otFamilyName",
+ "otStyleName",
+ "otMacName",
+ "msCharSet",
+ "fondID",
+ "uniqueID",
+ "ttVendor",
+ "ttUniqueID",
+ "ttVersion",
+])
+
+fontInfoAttributesVersion2ValueData = {
+ "familyName" : dict(type=basestring),
+ "styleName" : dict(type=basestring),
+ "styleMapFamilyName" : dict(type=basestring),
+ "styleMapStyleName" : dict(type=basestring, valueValidator=fontInfoStyleMapStyleNameValidator),
+ "versionMajor" : dict(type=int),
+ "versionMinor" : dict(type=int),
+ "year" : dict(type=int),
+ "copyright" : dict(type=basestring),
+ "trademark" : dict(type=basestring),
+ "unitsPerEm" : dict(type=(int, float)),
+ "descender" : dict(type=(int, float)),
+ "xHeight" : dict(type=(int, float)),
+ "capHeight" : dict(type=(int, float)),
+ "ascender" : dict(type=(int, float)),
+ "italicAngle" : dict(type=(float, int)),
+ "note" : dict(type=basestring),
+ "openTypeHeadCreated" : dict(type=basestring, valueValidator=fontInfoOpenTypeHeadCreatedValidator),
+ "openTypeHeadLowestRecPPEM" : dict(type=(int, float)),
+ "openTypeHeadFlags" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeHeadFlagsOptions),
+ "openTypeHheaAscender" : dict(type=(int, float)),
+ "openTypeHheaDescender" : dict(type=(int, float)),
+ "openTypeHheaLineGap" : dict(type=(int, float)),
+ "openTypeHheaCaretSlopeRise" : dict(type=int),
+ "openTypeHheaCaretSlopeRun" : dict(type=int),
+ "openTypeHheaCaretOffset" : dict(type=(int, float)),
+ "openTypeNameDesigner" : dict(type=basestring),
+ "openTypeNameDesignerURL" : dict(type=basestring),
+ "openTypeNameManufacturer" : dict(type=basestring),
+ "openTypeNameManufacturerURL" : dict(type=basestring),
+ "openTypeNameLicense" : dict(type=basestring),
+ "openTypeNameLicenseURL" : dict(type=basestring),
+ "openTypeNameVersion" : dict(type=basestring),
+ "openTypeNameUniqueID" : dict(type=basestring),
+ "openTypeNameDescription" : dict(type=basestring),
+ "openTypeNamePreferredFamilyName" : dict(type=basestring),
+ "openTypeNamePreferredSubfamilyName" : dict(type=basestring),
+ "openTypeNameCompatibleFullName" : dict(type=basestring),
+ "openTypeNameSampleText" : dict(type=basestring),
+ "openTypeNameWWSFamilyName" : dict(type=basestring),
+ "openTypeNameWWSSubfamilyName" : dict(type=basestring),
+ "openTypeOS2WidthClass" : dict(type=int, valueValidator=fontInfoOpenTypeOS2WidthClassValidator),
+ "openTypeOS2WeightClass" : dict(type=int, valueValidator=fontInfoOpenTypeOS2WeightClassValidator),
+ "openTypeOS2Selection" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2SelectionOptions),
+ "openTypeOS2VendorID" : dict(type=basestring),
+ "openTypeOS2Panose" : dict(type="integerList", valueValidator=fontInfoVersion2OpenTypeOS2PanoseValidator),
+ "openTypeOS2FamilyClass" : dict(type="integerList", valueValidator=fontInfoOpenTypeOS2FamilyClassValidator),
+ "openTypeOS2UnicodeRanges" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2UnicodeRangesOptions),
+ "openTypeOS2CodePageRanges" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2CodePageRangesOptions),
+ "openTypeOS2TypoAscender" : dict(type=(int, float)),
+ "openTypeOS2TypoDescender" : dict(type=(int, float)),
+ "openTypeOS2TypoLineGap" : dict(type=(int, float)),
+ "openTypeOS2WinAscent" : dict(type=(int, float)),
+ "openTypeOS2WinDescent" : dict(type=(int, float)),
+ "openTypeOS2Type" : dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2TypeOptions),
+ "openTypeOS2SubscriptXSize" : dict(type=(int, float)),
+ "openTypeOS2SubscriptYSize" : dict(type=(int, float)),
+ "openTypeOS2SubscriptXOffset" : dict(type=(int, float)),
+ "openTypeOS2SubscriptYOffset" : dict(type=(int, float)),
+ "openTypeOS2SuperscriptXSize" : dict(type=(int, float)),
+ "openTypeOS2SuperscriptYSize" : dict(type=(int, float)),
+ "openTypeOS2SuperscriptXOffset" : dict(type=(int, float)),
+ "openTypeOS2SuperscriptYOffset" : dict(type=(int, float)),
+ "openTypeOS2StrikeoutSize" : dict(type=(int, float)),
+ "openTypeOS2StrikeoutPosition" : dict(type=(int, float)),
+ "openTypeVheaVertTypoAscender" : dict(type=(int, float)),
+ "openTypeVheaVertTypoDescender" : dict(type=(int, float)),
+ "openTypeVheaVertTypoLineGap" : dict(type=(int, float)),
+ "openTypeVheaCaretSlopeRise" : dict(type=int),
+ "openTypeVheaCaretSlopeRun" : dict(type=int),
+ "openTypeVheaCaretOffset" : dict(type=(int, float)),
+ "postscriptFontName" : dict(type=basestring),
+ "postscriptFullName" : dict(type=basestring),
+ "postscriptSlantAngle" : dict(type=(float, int)),
+ "postscriptUniqueID" : dict(type=int),
+ "postscriptUnderlineThickness" : dict(type=(int, float)),
+ "postscriptUnderlinePosition" : dict(type=(int, float)),
+ "postscriptIsFixedPitch" : dict(type=bool),
+ "postscriptBlueValues" : dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator),
+ "postscriptOtherBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator),
+ "postscriptFamilyBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator),
+ "postscriptFamilyOtherBlues" : dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator),
+ "postscriptStemSnapH" : dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator),
+ "postscriptStemSnapV" : dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator),
+ "postscriptBlueFuzz" : dict(type=(int, float)),
+ "postscriptBlueShift" : dict(type=(int, float)),
+ "postscriptBlueScale" : dict(type=(float, int)),
+ "postscriptForceBold" : dict(type=bool),
+ "postscriptDefaultWidthX" : dict(type=(int, float)),
+ "postscriptNominalWidthX" : dict(type=(int, float)),
+ "postscriptWeightName" : dict(type=basestring),
+ "postscriptDefaultCharacter" : dict(type=basestring),
+ "postscriptWindowsCharacterSet" : dict(type=int, valueValidator=fontInfoPostscriptWindowsCharacterSetValidator),
+ "macintoshFONDFamilyID" : dict(type=int),
+ "macintoshFONDName" : dict(type=basestring),
+}
+fontInfoAttributesVersion2 = set(fontInfoAttributesVersion2ValueData.keys())
+
+fontInfoAttributesVersion3ValueData = deepcopy(fontInfoAttributesVersion2ValueData)
+fontInfoAttributesVersion3ValueData.update({
+ "versionMinor" : dict(type=int, valueValidator=genericNonNegativeIntValidator),
+ "unitsPerEm" : dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator),
+ "openTypeHeadLowestRecPPEM" : dict(type=int, valueValidator=genericNonNegativeNumberValidator),
+ "openTypeHheaAscender" : dict(type=int),
+ "openTypeHheaDescender" : dict(type=int),
+ "openTypeHheaLineGap" : dict(type=int),
+ "openTypeHheaCaretOffset" : dict(type=int),
+ "openTypeOS2Panose" : dict(type="integerList", valueValidator=fontInfoVersion3OpenTypeOS2PanoseValidator),
+ "openTypeOS2TypoAscender" : dict(type=int),
+ "openTypeOS2TypoDescender" : dict(type=int),
+ "openTypeOS2TypoLineGap" : dict(type=int),
+ "openTypeOS2WinAscent" : dict(type=int, valueValidator=genericNonNegativeNumberValidator),
+ "openTypeOS2WinDescent" : dict(type=int, valueValidator=genericNonNegativeNumberValidator),
+ "openTypeOS2SubscriptXSize" : dict(type=int),
+ "openTypeOS2SubscriptYSize" : dict(type=int),
+ "openTypeOS2SubscriptXOffset" : dict(type=int),
+ "openTypeOS2SubscriptYOffset" : dict(type=int),
+ "openTypeOS2SuperscriptXSize" : dict(type=int),
+ "openTypeOS2SuperscriptYSize" : dict(type=int),
+ "openTypeOS2SuperscriptXOffset" : dict(type=int),
+ "openTypeOS2SuperscriptYOffset" : dict(type=int),
+ "openTypeOS2StrikeoutSize" : dict(type=int),
+ "openTypeOS2StrikeoutPosition" : dict(type=int),
+ "openTypeGaspRangeRecords" : dict(type="dictList", valueValidator=fontInfoOpenTypeGaspRangeRecordsValidator),
+ "openTypeNameRecords" : dict(type="dictList", valueValidator=fontInfoOpenTypeNameRecordsValidator),
+ "openTypeVheaVertTypoAscender" : dict(type=int),
+ "openTypeVheaVertTypoDescender" : dict(type=int),
+ "openTypeVheaVertTypoLineGap" : dict(type=int),
+ "openTypeVheaCaretOffset" : dict(type=int),
+ "woffMajorVersion" : dict(type=int, valueValidator=genericNonNegativeIntValidator),
+ "woffMinorVersion" : dict(type=int, valueValidator=genericNonNegativeIntValidator),
+ "woffMetadataUniqueID" : dict(type=dict, valueValidator=fontInfoWOFFMetadataUniqueIDValidator),
+ "woffMetadataVendor" : dict(type=dict, valueValidator=fontInfoWOFFMetadataVendorValidator),
+ "woffMetadataCredits" : dict(type=dict, valueValidator=fontInfoWOFFMetadataCreditsValidator),
+ "woffMetadataDescription" : dict(type=dict, valueValidator=fontInfoWOFFMetadataDescriptionValidator),
+ "woffMetadataLicense" : dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseValidator),
+ "woffMetadataCopyright" : dict(type=dict, valueValidator=fontInfoWOFFMetadataCopyrightValidator),
+ "woffMetadataTrademark" : dict(type=dict, valueValidator=fontInfoWOFFMetadataTrademarkValidator),
+ "woffMetadataLicensee" : dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseeValidator),
+ "woffMetadataExtensions" : dict(type=list, valueValidator=fontInfoWOFFMetadataExtensionsValidator),
+ "guidelines" : dict(type=list, valueValidator=guidelinesValidator)
+})
+fontInfoAttributesVersion3 = set(fontInfoAttributesVersion3ValueData.keys())
+
+# insert the type validator for all attrs that
+# have no defined validator.
+for attr, dataDict in list(fontInfoAttributesVersion2ValueData.items()):
+ if "valueValidator" not in dataDict:
+ dataDict["valueValidator"] = genericTypeValidator
+
+for attr, dataDict in list(fontInfoAttributesVersion3ValueData.items()):
+ if "valueValidator" not in dataDict:
+ dataDict["valueValidator"] = genericTypeValidator
+
+# Version Conversion Support
+# These are used from converting from version 1
+# to version 2 or vice-versa.
+
+def _flipDict(d):
+ flipped = {}
+ for key, value in list(d.items()):
+ flipped[value] = key
+ return flipped
+
+fontInfoAttributesVersion1To2 = {
+ "menuName" : "styleMapFamilyName",
+ "designer" : "openTypeNameDesigner",
+ "designerURL" : "openTypeNameDesignerURL",
+ "createdBy" : "openTypeNameManufacturer",
+ "vendorURL" : "openTypeNameManufacturerURL",
+ "license" : "openTypeNameLicense",
+ "licenseURL" : "openTypeNameLicenseURL",
+ "ttVersion" : "openTypeNameVersion",
+ "ttUniqueID" : "openTypeNameUniqueID",
+ "notice" : "openTypeNameDescription",
+ "otFamilyName" : "openTypeNamePreferredFamilyName",
+ "otStyleName" : "openTypeNamePreferredSubfamilyName",
+ "otMacName" : "openTypeNameCompatibleFullName",
+ "weightName" : "postscriptWeightName",
+ "weightValue" : "openTypeOS2WeightClass",
+ "ttVendor" : "openTypeOS2VendorID",
+ "uniqueID" : "postscriptUniqueID",
+ "fontName" : "postscriptFontName",
+ "fondID" : "macintoshFONDFamilyID",
+ "fondName" : "macintoshFONDName",
+ "defaultWidth" : "postscriptDefaultWidthX",
+ "slantAngle" : "postscriptSlantAngle",
+ "fullName" : "postscriptFullName",
+ # require special value conversion
+ "fontStyle" : "styleMapStyleName",
+ "widthName" : "openTypeOS2WidthClass",
+ "msCharSet" : "postscriptWindowsCharacterSet"
+}
+fontInfoAttributesVersion2To1 = _flipDict(fontInfoAttributesVersion1To2)
+deprecatedFontInfoAttributesVersion2 = set(fontInfoAttributesVersion1To2.keys())
+
+_fontStyle1To2 = {
+ 64 : "regular",
+ 1 : "italic",
+ 32 : "bold",
+ 33 : "bold italic"
+}
+_fontStyle2To1 = _flipDict(_fontStyle1To2)
+# Some UFO 1 files have 0
+_fontStyle1To2[0] = "regular"
+
+_widthName1To2 = {
+ "Ultra-condensed" : 1,
+ "Extra-condensed" : 2,
+ "Condensed" : 3,
+ "Semi-condensed" : 4,
+ "Medium (normal)" : 5,
+ "Semi-expanded" : 6,
+ "Expanded" : 7,
+ "Extra-expanded" : 8,
+ "Ultra-expanded" : 9
+}
+_widthName2To1 = _flipDict(_widthName1To2)
+# FontLab's default width value is "Normal".
+# Many format version 1 UFOs will have this.
+_widthName1To2["Normal"] = 5
+# FontLab has an "All" width value. In UFO 1
+# move this up to "Normal".
+_widthName1To2["All"] = 5
+# "medium" appears in a lot of UFO 1 files.
+_widthName1To2["medium"] = 5
+# "Medium" appears in a lot of UFO 1 files.
+_widthName1To2["Medium"] = 5
+
+_msCharSet1To2 = {
+ 0 : 1,
+ 1 : 2,
+ 2 : 3,
+ 77 : 4,
+ 128 : 5,
+ 129 : 6,
+ 130 : 7,
+ 134 : 8,
+ 136 : 9,
+ 161 : 10,
+ 162 : 11,
+ 163 : 12,
+ 177 : 13,
+ 178 : 14,
+ 186 : 15,
+ 200 : 16,
+ 204 : 17,
+ 222 : 18,
+ 238 : 19,
+ 255 : 20
+}
+_msCharSet2To1 = _flipDict(_msCharSet1To2)
+
+# 1 <-> 2
+
+def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
+ """
+ Convert value from version 1 to version 2 format.
+ Returns the new attribute name and the converted value.
+ If the value is None, None will be returned for the new value.
+ """
+ # convert floats to ints if possible
+ if isinstance(value, float):
+ if int(value) == value:
+ value = int(value)
+ if value is not None:
+ if attr == "fontStyle":
+ v = _fontStyle1To2.get(value)
+ if v is None:
+ raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
+ value = v
+ elif attr == "widthName":
+ v = _widthName1To2.get(value)
+ if v is None:
+ raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
+ value = v
+ elif attr == "msCharSet":
+ v = _msCharSet1To2.get(value)
+ if v is None:
+ raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
+ value = v
+ attr = fontInfoAttributesVersion1To2.get(attr, attr)
+ return attr, value
+
+def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
+ """
+ Convert value from version 2 to version 1 format.
+ Returns the new attribute name and the converted value.
+ If the value is None, None will be returned for the new value.
+ """
+ if value is not None:
+ if attr == "styleMapStyleName":
+ value = _fontStyle2To1.get(value)
+ elif attr == "openTypeOS2WidthClass":
+ value = _widthName2To1.get(value)
+ elif attr == "postscriptWindowsCharacterSet":
+ value = _msCharSet2To1.get(value)
+ attr = fontInfoAttributesVersion2To1.get(attr, attr)
+ return attr, value
+
+def _convertFontInfoDataVersion1ToVersion2(data):
+ converted = {}
+ for attr, value in list(data.items()):
+ # FontLab gives -1 for the weightValue
+ # for fonts wil no defined value. Many
+ # format version 1 UFOs will have this.
+ if attr == "weightValue" and value == -1:
+ continue
+ newAttr, newValue = convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value)
+ # skip if the attribute is not part of version 2
+ if newAttr not in fontInfoAttributesVersion2:
+ continue
+ # catch values that can't be converted
+ if value is None:
+ raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr))
+ # store
+ converted[newAttr] = newValue
+ return converted
+
+def _convertFontInfoDataVersion2ToVersion1(data):
+ converted = {}
+ for attr, value in list(data.items()):
+ newAttr, newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value)
+ # only take attributes that are registered for version 1
+ if newAttr not in fontInfoAttributesVersion1:
+ continue
+ # catch values that can't be converted
+ if value is None:
+ raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr))
+ # store
+ converted[newAttr] = newValue
+ return converted
+
+# 2 <-> 3
+
+_ufo2To3NonNegativeInt = set((
+ "versionMinor",
+ "openTypeHeadLowestRecPPEM",
+ "openTypeOS2WinAscent",
+ "openTypeOS2WinDescent"
+))
+_ufo2To3NonNegativeIntOrFloat = set((
+ "unitsPerEm"
+))
+_ufo2To3FloatToInt = set(((
+ "openTypeHeadLowestRecPPEM",
+ "openTypeHheaAscender",
+ "openTypeHheaDescender",
+ "openTypeHheaLineGap",
+ "openTypeHheaCaretOffset",
+ "openTypeOS2TypoAscender",
+ "openTypeOS2TypoDescender",
+ "openTypeOS2TypoLineGap",
+ "openTypeOS2WinAscent",
+ "openTypeOS2WinDescent",
+ "openTypeOS2SubscriptXSize",
+ "openTypeOS2SubscriptYSize",
+ "openTypeOS2SubscriptXOffset",
+ "openTypeOS2SubscriptYOffset",
+ "openTypeOS2SuperscriptXSize",
+ "openTypeOS2SuperscriptYSize",
+ "openTypeOS2SuperscriptXOffset",
+ "openTypeOS2SuperscriptYOffset",
+ "openTypeOS2StrikeoutSize",
+ "openTypeOS2StrikeoutPosition",
+ "openTypeVheaVertTypoAscender",
+ "openTypeVheaVertTypoDescender",
+ "openTypeVheaVertTypoLineGap",
+ "openTypeVheaCaretOffset"
+)))
+
+def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
+ """
+ Convert value from version 2 to version 3 format.
+ Returns the new attribute name and the converted value.
+ If the value is None, None will be returned for the new value.
+ """
+ if attr in _ufo2To3FloatToInt:
+ try:
+ v = int(round(value))
+ except (ValueError, TypeError):
+ raise UFOLibError("Could not convert value for %s." % attr)
+ if v != value:
+ value = v
+ if attr in _ufo2To3NonNegativeInt:
+ try:
+ v = int(abs(value))
+ except (ValueError, TypeError):
+ raise UFOLibError("Could not convert value for %s." % attr)
+ if v != value:
+ value = v
+ elif attr in _ufo2To3NonNegativeIntOrFloat:
+ try:
+ v = float(abs(value))
+ except (ValueError, TypeError):
+ raise UFOLibError("Could not convert value for %s." % attr)
+ if v == int(v):
+ v = int(v)
+ if v != value:
+ value = v
+ return attr, value
+
+def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value):
+ """
+ Convert value from version 3 to version 2 format.
+ Returns the new attribute name and the converted value.
+ If the value is None, None will be returned for the new value.
+ """
+ return attr, value
+
+def _convertFontInfoDataVersion3ToVersion2(data):
+ converted = {}
+ for attr, value in list(data.items()):
+ newAttr, newValue = convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value)
+ if newAttr not in fontInfoAttributesVersion2:
+ continue
+ converted[newAttr] = newValue
+ return converted
+
+def _convertFontInfoDataVersion2ToVersion3(data):
+ converted = {}
+ for attr, value in list(data.items()):
+ attr, value = convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value)
+ converted[attr] = value
+ return converted
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/Lib/fontTools/ufoLib/converters.py b/Lib/fontTools/ufoLib/converters.py
new file mode 100644
index 00000000..c9ec9906
--- /dev/null
+++ b/Lib/fontTools/ufoLib/converters.py
@@ -0,0 +1,336 @@
+"""
+Conversion functions.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+
+# adapted from the UFO spec
+
+def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups):
+ # gather known kerning groups based on the prefixes
+ firstReferencedGroups, secondReferencedGroups = findKnownKerningGroups(groups)
+ # Make lists of groups referenced in kerning pairs.
+ for first, seconds in list(kerning.items()):
+ if first in groups:
+ if not first.startswith("public.kern1."):
+ firstReferencedGroups.add(first)
+ for second in list(seconds.keys()):
+ if second in groups:
+ if not second.startswith("public.kern2."):
+ secondReferencedGroups.add(second)
+ # Create new names for these groups.
+ firstRenamedGroups = {}
+ for first in firstReferencedGroups:
+ # Make a list of existing group names.
+ existingGroupNames = list(groups.keys()) + list(firstRenamedGroups.keys())
+ # Remove the old prefix from the name
+ newName = first.replace("@MMK_L_", "")
+ # Add the new prefix to the name.
+ newName = "public.kern1." + newName
+ # Make a unique group name.
+ newName = makeUniqueGroupName(newName, existingGroupNames)
+ # Store for use later.
+ firstRenamedGroups[first] = newName
+ secondRenamedGroups = {}
+ for second in secondReferencedGroups:
+ # Make a list of existing group names.
+ existingGroupNames = list(groups.keys()) + list(secondRenamedGroups.keys())
+ # Remove the old prefix from the name
+ newName = second.replace("@MMK_R_", "")
+ # Add the new prefix to the name.
+ newName = "public.kern2." + newName
+ # Make a unique group name.
+ newName = makeUniqueGroupName(newName, existingGroupNames)
+ # Store for use later.
+ secondRenamedGroups[second] = newName
+ # Populate the new group names into the kerning dictionary as needed.
+ newKerning = {}
+ for first, seconds in list(kerning.items()):
+ first = firstRenamedGroups.get(first, first)
+ newSeconds = {}
+ for second, value in list(seconds.items()):
+ second = secondRenamedGroups.get(second, second)
+ newSeconds[second] = value
+ newKerning[first] = newSeconds
+ # Make copies of the referenced groups and store them
+ # under the new names in the overall groups dictionary.
+ allRenamedGroups = list(firstRenamedGroups.items())
+ allRenamedGroups += list(secondRenamedGroups.items())
+ for oldName, newName in allRenamedGroups:
+ group = list(groups[oldName])
+ groups[newName] = group
+ # Return the kerning and the groups.
+ return newKerning, groups, dict(side1=firstRenamedGroups, side2=secondRenamedGroups)
+
+def findKnownKerningGroups(groups):
+ """
+ This will find kerning groups with known prefixes.
+ In some cases not all kerning groups will be referenced
+ by the kerning pairs. The algorithm for locating groups
+ in convertUFO1OrUFO2KerningToUFO3Kerning will miss these
+ unreferenced groups. By scanning for known prefixes
+ this function will catch all of the prefixed groups.
+
+ These are the prefixes and sides that are handled:
+ @MMK_L_ - side 1
+ @MMK_R_ - side 2
+
+ >>> testGroups = {
+ ... "@MMK_L_1" : None,
+ ... "@MMK_L_2" : None,
+ ... "@MMK_L_3" : None,
+ ... "@MMK_R_1" : None,
+ ... "@MMK_R_2" : None,
+ ... "@MMK_R_3" : None,
+ ... "@MMK_l_1" : None,
+ ... "@MMK_r_1" : None,
+ ... "@MMK_X_1" : None,
+ ... "foo" : None,
+ ... }
+ >>> first, second = findKnownKerningGroups(testGroups)
+ >>> sorted(first) == ['@MMK_L_1', '@MMK_L_2', '@MMK_L_3']
+ True
+ >>> sorted(second) == ['@MMK_R_1', '@MMK_R_2', '@MMK_R_3']
+ True
+ """
+ knownFirstGroupPrefixes = [
+ "@MMK_L_"
+ ]
+ knownSecondGroupPrefixes = [
+ "@MMK_R_"
+ ]
+ firstGroups = set()
+ secondGroups = set()
+ for groupName in list(groups.keys()):
+ for firstPrefix in knownFirstGroupPrefixes:
+ if groupName.startswith(firstPrefix):
+ firstGroups.add(groupName)
+ break
+ for secondPrefix in knownSecondGroupPrefixes:
+ if groupName.startswith(secondPrefix):
+ secondGroups.add(groupName)
+ break
+ return firstGroups, secondGroups
+
+
+def makeUniqueGroupName(name, groupNames, counter=0):
+ # Add a number to the name if the counter is higher than zero.
+ newName = name
+ if counter > 0:
+ newName = "%s%d" % (newName, counter)
+ # If the new name is in the existing group names, recurse.
+ if newName in groupNames:
+ return makeUniqueGroupName(name, groupNames, counter + 1)
+ # Otherwise send back the new name.
+ return newName
+
+def test():
+ """
+ No known prefixes.
+
+ >>> testKerning = {
+ ... "A" : {
+ ... "A" : 1,
+ ... "B" : 2,
+ ... "CGroup" : 3,
+ ... "DGroup" : 4
+ ... },
+ ... "BGroup" : {
+ ... "A" : 5,
+ ... "B" : 6,
+ ... "CGroup" : 7,
+ ... "DGroup" : 8
+ ... },
+ ... "CGroup" : {
+ ... "A" : 9,
+ ... "B" : 10,
+ ... "CGroup" : 11,
+ ... "DGroup" : 12
+ ... },
+ ... }
+ >>> testGroups = {
+ ... "BGroup" : ["B"],
+ ... "CGroup" : ["C"],
+ ... "DGroup" : ["D"],
+ ... }
+ >>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning(
+ ... testKerning, testGroups)
+ >>> expected = {
+ ... "A" : {
+ ... "A": 1,
+ ... "B": 2,
+ ... "public.kern2.CGroup": 3,
+ ... "public.kern2.DGroup": 4
+ ... },
+ ... "public.kern1.BGroup": {
+ ... "A": 5,
+ ... "B": 6,
+ ... "public.kern2.CGroup": 7,
+ ... "public.kern2.DGroup": 8
+ ... },
+ ... "public.kern1.CGroup": {
+ ... "A": 9,
+ ... "B": 10,
+ ... "public.kern2.CGroup": 11,
+ ... "public.kern2.DGroup": 12
+ ... }
+ ... }
+ >>> kerning == expected
+ True
+ >>> expected = {
+ ... "BGroup": ["B"],
+ ... "CGroup": ["C"],
+ ... "DGroup": ["D"],
+ ... "public.kern1.BGroup": ["B"],
+ ... "public.kern1.CGroup": ["C"],
+ ... "public.kern2.CGroup": ["C"],
+ ... "public.kern2.DGroup": ["D"],
+ ... }
+ >>> groups == expected
+ True
+
+ Known prefixes.
+
+ >>> testKerning = {
+ ... "A" : {
+ ... "A" : 1,
+ ... "B" : 2,
+ ... "@MMK_R_CGroup" : 3,
+ ... "@MMK_R_DGroup" : 4
+ ... },
+ ... "@MMK_L_BGroup" : {
+ ... "A" : 5,
+ ... "B" : 6,
+ ... "@MMK_R_CGroup" : 7,
+ ... "@MMK_R_DGroup" : 8
+ ... },
+ ... "@MMK_L_CGroup" : {
+ ... "A" : 9,
+ ... "B" : 10,
+ ... "@MMK_R_CGroup" : 11,
+ ... "@MMK_R_DGroup" : 12
+ ... },
+ ... }
+ >>> testGroups = {
+ ... "@MMK_L_BGroup" : ["B"],
+ ... "@MMK_L_CGroup" : ["C"],
+ ... "@MMK_L_XGroup" : ["X"],
+ ... "@MMK_R_CGroup" : ["C"],
+ ... "@MMK_R_DGroup" : ["D"],
+ ... "@MMK_R_XGroup" : ["X"],
+ ... }
+ >>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning(
+ ... testKerning, testGroups)
+ >>> expected = {
+ ... "A" : {
+ ... "A": 1,
+ ... "B": 2,
+ ... "public.kern2.CGroup": 3,
+ ... "public.kern2.DGroup": 4
+ ... },
+ ... "public.kern1.BGroup": {
+ ... "A": 5,
+ ... "B": 6,
+ ... "public.kern2.CGroup": 7,
+ ... "public.kern2.DGroup": 8
+ ... },
+ ... "public.kern1.CGroup": {
+ ... "A": 9,
+ ... "B": 10,
+ ... "public.kern2.CGroup": 11,
+ ... "public.kern2.DGroup": 12
+ ... }
+ ... }
+ >>> kerning == expected
+ True
+ >>> expected = {
+ ... "@MMK_L_BGroup": ["B"],
+ ... "@MMK_L_CGroup": ["C"],
+ ... "@MMK_L_XGroup": ["X"],
+ ... "@MMK_R_CGroup": ["C"],
+ ... "@MMK_R_DGroup": ["D"],
+ ... "@MMK_R_XGroup": ["X"],
+ ... "public.kern1.BGroup": ["B"],
+ ... "public.kern1.CGroup": ["C"],
+ ... "public.kern1.XGroup": ["X"],
+ ... "public.kern2.CGroup": ["C"],
+ ... "public.kern2.DGroup": ["D"],
+ ... "public.kern2.XGroup": ["X"],
+ ... }
+ >>> groups == expected
+ True
+
+ >>> from .validators import kerningValidator
+ >>> kerningValidator(kerning)
+ (True, None)
+
+ Mixture of known prefixes and groups without prefixes.
+
+ >>> testKerning = {
+ ... "A" : {
+ ... "A" : 1,
+ ... "B" : 2,
+ ... "@MMK_R_CGroup" : 3,
+ ... "DGroup" : 4
+ ... },
+ ... "BGroup" : {
+ ... "A" : 5,
+ ... "B" : 6,
+ ... "@MMK_R_CGroup" : 7,
+ ... "DGroup" : 8
+ ... },
+ ... "@MMK_L_CGroup" : {
+ ... "A" : 9,
+ ... "B" : 10,
+ ... "@MMK_R_CGroup" : 11,
+ ... "DGroup" : 12
+ ... },
+ ... }
+ >>> testGroups = {
+ ... "BGroup" : ["B"],
+ ... "@MMK_L_CGroup" : ["C"],
+ ... "@MMK_R_CGroup" : ["C"],
+ ... "DGroup" : ["D"],
+ ... }
+ >>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning(
+ ... testKerning, testGroups)
+ >>> expected = {
+ ... "A" : {
+ ... "A": 1,
+ ... "B": 2,
+ ... "public.kern2.CGroup": 3,
+ ... "public.kern2.DGroup": 4
+ ... },
+ ... "public.kern1.BGroup": {
+ ... "A": 5,
+ ... "B": 6,
+ ... "public.kern2.CGroup": 7,
+ ... "public.kern2.DGroup": 8
+ ... },
+ ... "public.kern1.CGroup": {
+ ... "A": 9,
+ ... "B": 10,
+ ... "public.kern2.CGroup": 11,
+ ... "public.kern2.DGroup": 12
+ ... }
+ ... }
+ >>> kerning == expected
+ True
+ >>> expected = {
+ ... "BGroup": ["B"],
+ ... "@MMK_L_CGroup": ["C"],
+ ... "@MMK_R_CGroup": ["C"],
+ ... "DGroup": ["D"],
+ ... "public.kern1.BGroup": ["B"],
+ ... "public.kern1.CGroup": ["C"],
+ ... "public.kern2.CGroup": ["C"],
+ ... "public.kern2.DGroup": ["D"],
+ ... }
+ >>> groups == expected
+ True
+ """
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/Lib/fontTools/ufoLib/errors.py b/Lib/fontTools/ufoLib/errors.py
new file mode 100644
index 00000000..fb048d1c
--- /dev/null
+++ b/Lib/fontTools/ufoLib/errors.py
@@ -0,0 +1,9 @@
+from __future__ import absolute_import, unicode_literals
+
+
+class UFOLibError(Exception):
+ pass
+
+
+class GlifLibError(UFOLibError):
+ pass
diff --git a/Lib/fontTools/ufoLib/etree.py b/Lib/fontTools/ufoLib/etree.py
new file mode 100644
index 00000000..5054f816
--- /dev/null
+++ b/Lib/fontTools/ufoLib/etree.py
@@ -0,0 +1,5 @@
+"""DEPRECATED - This module is kept here only as a backward compatibility shim
+for the old ufoLib.etree module, which was moved to fontTools.misc.etree.
+Please use the latter instead.
+"""
+from fontTools.misc.etree import *
diff --git a/Lib/fontTools/ufoLib/filenames.py b/Lib/fontTools/ufoLib/filenames.py
new file mode 100644
index 00000000..98f53b1f
--- /dev/null
+++ b/Lib/fontTools/ufoLib/filenames.py
@@ -0,0 +1,214 @@
+"""
+User name to file name conversion.
+This was taken form the UFO 3 spec.
+"""
+from __future__ import absolute_import, unicode_literals
+from fontTools.misc.py23 import basestring, unicode
+
+
+illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ")
+illegalCharacters += [chr(i) for i in range(1, 32)]
+illegalCharacters += [chr(0x7F)]
+reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ")
+reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ")
+maxFileNameLength = 255
+
+
+class NameTranslationError(Exception):
+ pass
+
+
+def userNameToFileName(userName, existing=[], prefix="", suffix=""):
+ """
+ existing should be a case-insensitive list
+ of all existing file names.
+
+ >>> userNameToFileName("a") == "a"
+ True
+ >>> userNameToFileName("A") == "A_"
+ True
+ >>> userNameToFileName("AE") == "A_E_"
+ True
+ >>> userNameToFileName("Ae") == "A_e"
+ True
+ >>> userNameToFileName("ae") == "ae"
+ True
+ >>> userNameToFileName("aE") == "aE_"
+ True
+ >>> userNameToFileName("a.alt") == "a.alt"
+ True
+ >>> userNameToFileName("A.alt") == "A_.alt"
+ True
+ >>> userNameToFileName("A.Alt") == "A_.A_lt"
+ True
+ >>> userNameToFileName("A.aLt") == "A_.aL_t"
+ True
+ >>> userNameToFileName(u"A.alT") == "A_.alT_"
+ True
+ >>> userNameToFileName("T_H") == "T__H_"
+ True
+ >>> userNameToFileName("T_h") == "T__h"
+ True
+ >>> userNameToFileName("t_h") == "t_h"
+ True
+ >>> userNameToFileName("F_F_I") == "F__F__I_"
+ True
+ >>> userNameToFileName("f_f_i") == "f_f_i"
+ True
+ >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
+ True
+ >>> userNameToFileName(".notdef") == "_notdef"
+ True
+ >>> userNameToFileName("con") == "_con"
+ True
+ >>> userNameToFileName("CON") == "C_O_N_"
+ True
+ >>> userNameToFileName("con.alt") == "_con.alt"
+ True
+ >>> userNameToFileName("alt.con") == "alt._con"
+ True
+ """
+ # the incoming name must be a unicode string
+ if not isinstance(userName, unicode):
+ raise ValueError("The value for userName must be a unicode string.")
+ # establish the prefix and suffix lengths
+ prefixLength = len(prefix)
+ suffixLength = len(suffix)
+ # replace an initial period with an _
+ # if no prefix is to be added
+ if not prefix and userName[0] == ".":
+ userName = "_" + userName[1:]
+ # filter the user name
+ filteredUserName = []
+ for character in userName:
+ # replace illegal characters with _
+ if character in illegalCharacters:
+ character = "_"
+ # add _ to all non-lower characters
+ elif character != character.lower():
+ character += "_"
+ filteredUserName.append(character)
+ userName = "".join(filteredUserName)
+ # clip to 255
+ sliceLength = maxFileNameLength - prefixLength - suffixLength
+ userName = userName[:sliceLength]
+ # test for illegal files names
+ parts = []
+ for part in userName.split("."):
+ if part.lower() in reservedFileNames:
+ part = "_" + part
+ parts.append(part)
+ userName = ".".join(parts)
+ # test for clash
+ fullName = prefix + userName + suffix
+ if fullName.lower() in existing:
+ fullName = handleClash1(userName, existing, prefix, suffix)
+ # finished
+ return fullName
+
+def handleClash1(userName, existing=[], prefix="", suffix=""):
+ """
+ existing should be a case-insensitive list
+ of all existing file names.
+
+ >>> prefix = ("0" * 5) + "."
+ >>> suffix = "." + ("0" * 10)
+ >>> existing = ["a" * 5]
+
+ >>> e = list(existing)
+ >>> handleClash1(userName="A" * 5, existing=e,
+ ... prefix=prefix, suffix=suffix) == (
+ ... '00000.AAAAA000000000000001.0000000000')
+ True
+
+ >>> e = list(existing)
+ >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
+ >>> handleClash1(userName="A" * 5, existing=e,
+ ... prefix=prefix, suffix=suffix) == (
+ ... '00000.AAAAA000000000000002.0000000000')
+ True
+
+ >>> e = list(existing)
+ >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
+ >>> handleClash1(userName="A" * 5, existing=e,
+ ... prefix=prefix, suffix=suffix) == (
+ ... '00000.AAAAA000000000000001.0000000000')
+ True
+ """
+ # if the prefix length + user name length + suffix length + 15 is at
+ # or past the maximum length, silce 15 characters off of the user name
+ prefixLength = len(prefix)
+ suffixLength = len(suffix)
+ if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
+ l = (prefixLength + len(userName) + suffixLength + 15)
+ sliceLength = maxFileNameLength - l
+ userName = userName[:sliceLength]
+ finalName = None
+ # try to add numbers to create a unique name
+ counter = 1
+ while finalName is None:
+ name = userName + str(counter).zfill(15)
+ fullName = prefix + name + suffix
+ if fullName.lower() not in existing:
+ finalName = fullName
+ break
+ else:
+ counter += 1
+ if counter >= 999999999999999:
+ break
+ # if there is a clash, go to the next fallback
+ if finalName is None:
+ finalName = handleClash2(existing, prefix, suffix)
+ # finished
+ return finalName
+
+def handleClash2(existing=[], prefix="", suffix=""):
+ """
+ existing should be a case-insensitive list
+ of all existing file names.
+
+ >>> prefix = ("0" * 5) + "."
+ >>> suffix = "." + ("0" * 10)
+ >>> existing = [prefix + str(i) + suffix for i in range(100)]
+
+ >>> e = list(existing)
+ >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
+ ... '00000.100.0000000000')
+ True
+
+ >>> e = list(existing)
+ >>> e.remove(prefix + "1" + suffix)
+ >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
+ ... '00000.1.0000000000')
+ True
+
+ >>> e = list(existing)
+ >>> e.remove(prefix + "2" + suffix)
+ >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
+ ... '00000.2.0000000000')
+ True
+ """
+ # calculate the longest possible string
+ maxLength = maxFileNameLength - len(prefix) - len(suffix)
+ maxValue = int("9" * maxLength)
+ # try to find a number
+ finalName = None
+ counter = 1
+ while finalName is None:
+ fullName = prefix + str(counter) + suffix
+ if fullName.lower() not in existing:
+ finalName = fullName
+ break
+ else:
+ counter += 1
+ if counter >= maxValue:
+ break
+ # raise an error if nothing has been found
+ if finalName is None:
+ raise NameTranslationError("No unique name could be found.")
+ # finished
+ return finalName
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
new file mode 100755
index 00000000..eae300b2
--- /dev/null
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -0,0 +1,1622 @@
+# -*- 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.
+"""
+
+from __future__ import absolute_import, unicode_literals
+from warnings import warn
+from collections import OrderedDict
+import fs
+import fs.base
+import fs.errors
+import fs.osfs
+import fs.path
+from fontTools.misc.py23 import basestring, unicode, tobytes, tounicode
+from fontTools.misc import plistlib
+from fontTools.ufoLib.errors import GlifLibError
+from fontTools.ufoLib.pointPen import AbstractPointPen, PointToSegmentPen
+from fontTools.ufoLib.filenames import userNameToFileName
+from fontTools.ufoLib.validators import (
+ genericTypeValidator,
+ colorValidator,
+ guidelinesValidator,
+ anchorsValidator,
+ identifierValidator,
+ imageValidator,
+ glyphLibValidator,
+)
+from fontTools.misc import etree
+from fontTools.ufoLib.utils import integerTypes, numberTypes
+
+
+__all__ = [
+ "GlyphSet",
+ "GlifLibError",
+ "readGlyphFromString", "writeGlyphToString",
+ "glyphNameToFileName"
+]
+
+
+# ---------
+# Constants
+# ---------
+
+CONTENTS_FILENAME = "contents.plist"
+LAYERINFO_FILENAME = "layerinfo.plist"
+supportedUFOFormatVersions = [1, 2, 3]
+supportedGLIFFormatVersions = [1, 2]
+
+
+# ------------
+# Simple Glyph
+# ------------
+
+class Glyph(object):
+
+ """
+ Minimal glyph object. It has no glyph attributes until either
+ the draw() or the drawPoints() 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.
+ """
+ pointPen = PointToSegmentPen(pen)
+ self.drawPoints(pointPen)
+
+ def drawPoints(self, pointPen):
+ """
+ Draw this glyph onto a PointPen.
+ """
+ self.glyphSet.readGlyph(self.glyphName, self, pointPen)
+
+
+# ---------
+# Glyph Set
+# ---------
+
+class GlyphSet(object):
+
+ """
+ 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,
+ path,
+ glyphNameToFileNameFunc=None,
+ ufoFormatVersion=3,
+ validateRead=True,
+ validateWrite=True,
+ ):
+ """
+ 'path' should be a path (string) to an existing local directory, or
+ an instance of fs.base.FS class.
+
+ The optional 'glyphNameToFileNameFunc' argument must be a callback
+ function that takes two arguments: a glyph name and a list of all
+ existing filenames (if any exist). 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.
+
+ ``validateRead`` will validate read operations. Its default is ``True``.
+ ``validateWrite`` will validate write operations. Its default is ``True``.
+ """
+ if ufoFormatVersion not in supportedUFOFormatVersions:
+ raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
+ if isinstance(path, basestring):
+ try:
+ filesystem = fs.osfs.OSFS(path)
+ except fs.errors.CreateFailed:
+ raise GlifLibError("No glyphs directory '%s'" % path)
+ self._shouldClose = True
+ elif isinstance(path, fs.base.FS):
+ filesystem = path
+ try:
+ filesystem.check()
+ except fs.errors.FilesystemClosed:
+ raise GlifLibError("the filesystem '%s' is closed" % filesystem)
+ self._shouldClose = False
+ else:
+ raise TypeError(
+ "Expected a path string or fs object, found %s"
+ % type(path).__name__
+ )
+ try:
+ path = filesystem.getsyspath("/")
+ except fs.errors.NoSysPath:
+ # network or in-memory FS may not map to the local one
+ path = unicode(filesystem)
+ # 'dirName' is kept for backward compatibility only, but it's DEPRECATED
+ # as it's not guaranteed that it maps to an existing OSFS directory.
+ # Client could use the FS api via the `self.fs` attribute instead.
+ self.dirName = fs.path.parts(path)[-1]
+ self.fs = filesystem
+ # if glyphSet contains no 'contents.plist', we consider it empty
+ self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME)
+ self.ufoFormatVersion = ufoFormatVersion
+ if glyphNameToFileNameFunc is None:
+ glyphNameToFileNameFunc = glyphNameToFileName
+ self.glyphNameToFileName = glyphNameToFileNameFunc
+ self._validateRead = validateRead
+ self._validateWrite = validateWrite
+ self._existingFileNames = None
+ self._reverseContents = None
+
+ self.rebuildContents()
+
+ # here we reuse the same methods from UFOReader/UFOWriter
+ from fontTools.ufoLib import _getPlist, _writePlist, _getFileModificationTime
+
+ def rebuildContents(self, validateRead=None):
+ """
+ Rebuild the contents dict by loading contents.plist.
+
+ ``validateRead`` will validate the data, by default it is set to the
+ class's ``validateRead`` value, can be overridden.
+ """
+ if validateRead is None:
+ validateRead = self._validateRead
+ contents = self._getPlist(CONTENTS_FILENAME, {})
+ # validate the contents
+ if validateRead:
+ invalidFormat = False
+ if not isinstance(contents, dict):
+ invalidFormat = True
+ else:
+ for name, fileName in contents.items():
+ if not isinstance(name, basestring):
+ invalidFormat = True
+ if not isinstance(fileName, basestring):
+ invalidFormat = True
+ elif not self.fs.exists(fileName):
+ raise GlifLibError(
+ "%s references a file that does not exist: %s"
+ % (CONTENTS_FILENAME, fileName)
+ )
+ if invalidFormat:
+ raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME)
+ self.contents = contents
+ self._existingFileNames = None
+ 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.items():
+ 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.
+ """
+ self._writePlist(CONTENTS_FILENAME, self.contents)
+
+ # layer info
+
+ def readLayerInfo(self, info, validateRead=None):
+ """
+ ``validateRead`` will validate the data, by default it is set to the
+ class's ``validateRead`` value, can be overridden.
+ """
+ if validateRead is None:
+ validateRead = self._validateRead
+ infoDict = self._getPlist(LAYERINFO_FILENAME, {})
+ if validateRead:
+ if not isinstance(infoDict, dict):
+ raise GlifLibError("layerinfo.plist is not properly formatted.")
+ infoDict = validateLayerInfoVersion3Data(infoDict)
+ # populate the object
+ for attr, value in infoDict.items():
+ try:
+ setattr(info, attr, value)
+ except AttributeError:
+ raise GlifLibError("The supplied layer info object does not support setting a necessary attribute (%s)." % attr)
+
+ def writeLayerInfo(self, info, validateWrite=None):
+ """
+ ``validateWrite`` will validate the data, by default it is set to the
+ class's ``validateWrite`` value, can be overridden.
+ """
+ if validateWrite is None:
+ validateWrite = self._validateWrite
+ if self.ufoFormatVersion < 3:
+ raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion)
+ # gather data
+ infoData = {}
+ for attr in layerInfoVersion3ValueData.keys():
+ if hasattr(info, attr):
+ try:
+ value = getattr(info, attr)
+ except AttributeError:
+ raise GlifLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
+ if value is None or (attr == 'lib' and not value):
+ continue
+ infoData[attr] = value
+ if infoData:
+ # validate
+ if validateWrite:
+ infoData = validateLayerInfoVersion3Data(infoData)
+ # write file
+ self._writePlist(LAYERINFO_FILENAME, infoData)
+ elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME):
+ # data empty, remove existing file
+ self.fs.remove(LAYERINFO_FILENAME)
+
+ 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.
+
+ Raises KeyError if 'glyphName' is not in contents.plist, or
+ GlifLibError if the file associated with can't be found.
+ """
+ fileName = self.contents[glyphName]
+ try:
+ return self.fs.getbytes(fileName)
+ except fs.errors.ResourceNotFound:
+ raise GlifLibError(
+ "The file '%s' associated with glyph '%s' in contents.plist "
+ "does not exist on %s" % (fileName, glyphName, self.fs)
+ )
+
+ def getGLIFModificationTime(self, glyphName):
+ """
+ Returns the modification time for the GLIF file with 'glyphName', as
+ a floating point number giving the number of seconds since the epoch.
+ Return None if the associated file does not exist or the underlying
+ filesystem does not support getting modified times.
+ Raises KeyError if the glyphName is not in contents.plist.
+ """
+ fileName = self.contents[glyphName]
+ return self._getFileModificationTime(fileName)
+
+ # reading/writing API
+
+ def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=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
+ "height" the advance height of the glyph
+ "unicodes" a list of unicode values for this glyph
+ "note" a string
+ "lib" a dictionary containing custom data
+ "image" a dictionary containing image data
+ "guidelines" a list of guideline data dictionaries
+ "anchors" a list of anchor data dictionaries
+
+ 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.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's ``validateRead`` value, can be overridden.
+ """
+ if validate is None:
+ validate = self._validateRead
+ text = self.getGLIF(glyphName)
+ tree = _glifTreeFromString(text)
+ if self.ufoFormatVersion < 3:
+ formatVersions = (1,)
+ else:
+ formatVersions = (1, 2)
+ _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
+
+ def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=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
+ "height" the advance height of the glyph
+ "unicodes" a list of unicode values for this glyph
+ "note" a string
+ "lib" a dictionary containing custom data
+ "image" a dictionary containing image data
+ "guidelines" a list of guideline data dictionaries
+ "anchors" a list of anchor data dictionaries
+
+ 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.
+
+ The GLIF format version will be chosen based on the ufoFormatVersion
+ passed during the creation of this object. If a particular format
+ version is desired, it can be passed with the formatVersion argument.
+
+ ``validate`` will validate the data, by default it is set to the
+ class's ``validateWrite`` value, can be overridden.
+ """
+ if formatVersion is None:
+ if self.ufoFormatVersion >= 3:
+ formatVersion = 2
+ else:
+ formatVersion = 1
+ if formatVersion not in supportedGLIFFormatVersions:
+ raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
+ if formatVersion == 2 and self.ufoFormatVersion < 3:
+ raise GlifLibError(
+ "Unsupported GLIF format version (%d) for UFO format version %d."
+ % (formatVersion, self.ufoFormatVersion)
+ )
+ if validate is None:
+ validate = self._validateWrite
+ fileName = self.contents.get(glyphName)
+ if fileName is None:
+ if self._existingFileNames is None:
+ self._existingFileNames = {}
+ for fileName in self.contents.values():
+ self._existingFileNames[fileName] = fileName.lower()
+ fileName = self.glyphNameToFileName(glyphName, self._existingFileNames)
+ self.contents[glyphName] = fileName
+ self._existingFileNames[fileName] = fileName.lower()
+ if self._reverseContents is not None:
+ self._reverseContents[fileName.lower()] = glyphName
+ data = _writeGlyphToBytes(
+ glyphName,
+ glyphObject,
+ drawPointsFunc,
+ formatVersion=formatVersion,
+ validate=validate,
+ )
+ if (
+ self._havePreviousFile
+ and self.fs.exists(fileName)
+ and data == self.fs.getbytes(fileName)
+ ):
+ return
+ self.fs.setbytes(fileName, data)
+
+ 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]
+ self.fs.remove(fileName)
+ if self._existingFileNames is not None:
+ del self._existingFileNames[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 list(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 fetch unicode values
+
+ def getUnicodes(self, glyphNames=None):
+ """
+ Return a dictionary that maps glyph names to lists containing
+ the unicode value[s] for that glyph, if any. This parses the .glif
+ files partially, so it is a lot faster than parsing all files completely.
+ By default this checks all glyphs, but a subset can be passed with glyphNames.
+ """
+ unicodes = {}
+ if glyphNames is None:
+ glyphNames = self.contents.keys()
+ for glyphName in glyphNames:
+ text = self.getGLIF(glyphName)
+ unicodes[glyphName] = _fetchUnicodes(text)
+ return unicodes
+
+ def getComponentReferences(self, glyphNames=None):
+ """
+ Return a dictionary that maps glyph names to lists containing the
+ base glyph name of components in the glyph. This parses the .glif
+ files partially, so it is a lot faster than parsing all files completely.
+ By default this checks all glyphs, but a subset can be passed with glyphNames.
+ """
+ components = {}
+ if glyphNames is None:
+ glyphNames = self.contents.keys()
+ for glyphName in glyphNames:
+ text = self.getGLIF(glyphName)
+ components[glyphName] = _fetchComponentBases(text)
+ return components
+
+ def getImageReferences(self, glyphNames=None):
+ """
+ Return a dictionary that maps glyph names to the file name of the image
+ referenced by the glyph. This parses the .glif files partially, so it is a
+ lot faster than parsing all files completely.
+ By default this checks all glyphs, but a subset can be passed with glyphNames.
+ """
+ images = {}
+ if glyphNames is None:
+ glyphNames = self.contents.keys()
+ for glyphName in glyphNames:
+ text = self.getGLIF(glyphName)
+ images[glyphName] = _fetchImageFileName(text)
+ return images
+
+ def close(self):
+ if self._shouldClose:
+ self.fs.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ self.close()
+
+
+# -----------------------
+# Glyph Name to File Name
+# -----------------------
+
+def glyphNameToFileName(glyphName, existingFileNames):
+ """
+ Wrapper around the userNameToFileName function in filenames.py
+ """
+ if existingFileNames is None:
+ existingFileNames = []
+ if not isinstance(glyphName, unicode):
+ try:
+ new = unicode(glyphName)
+ glyphName = new
+ except UnicodeDecodeError:
+ pass
+ return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif")
+
+# -----------------------
+# GLIF To and From String
+# -----------------------
+
+def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
+ """
+ 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
+ "height" the advance height of the glyph
+ "unicodes" a list of unicode values for this glyph
+ "note" a string
+ "lib" a dictionary containing custom data
+ "image" a dictionary containing image data
+ "guidelines" a list of guideline data dictionaries
+ "anchors" a list of anchor data dictionaries
+
+ 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.
+
+ The formatVersions argument defined the GLIF format versions
+ that are allowed to be read.
+
+ ``validate`` will validate the read data. It is set to ``True`` by default.
+ """
+ tree = _glifTreeFromString(aString)
+ _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
+
+
+def _writeGlyphToBytes(
+ glyphName, glyphObject=None, drawPointsFunc=None, writer=None,
+ formatVersion=2, validate=True):
+ """Return .glif data for a glyph as a UTF-8 encoded bytes string."""
+ # start
+ if validate and not isinstance(glyphName, basestring):
+ raise GlifLibError("The glyph name is not properly formatted.")
+ if validate and len(glyphName) == 0:
+ raise GlifLibError("The glyph name is empty.")
+ root = etree.Element("glyph", OrderedDict([("name", glyphName), ("format", repr(formatVersion))]))
+ identifiers = set()
+ # advance
+ _writeAdvance(glyphObject, root, validate)
+ # unicodes
+ if getattr(glyphObject, "unicodes", None):
+ _writeUnicodes(glyphObject, root, validate)
+ # note
+ if getattr(glyphObject, "note", None):
+ _writeNote(glyphObject, root, validate)
+ # image
+ if formatVersion >= 2 and getattr(glyphObject, "image", None):
+ _writeImage(glyphObject, root, validate)
+ # guidelines
+ if formatVersion >= 2 and getattr(glyphObject, "guidelines", None):
+ _writeGuidelines(glyphObject, root, identifiers, validate)
+ # anchors
+ anchors = getattr(glyphObject, "anchors", None)
+ if formatVersion >= 2 and anchors:
+ _writeAnchors(glyphObject, root, identifiers, validate)
+ # outline
+ if drawPointsFunc is not None:
+ outline = etree.SubElement(root, "outline")
+ pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
+ drawPointsFunc(pen)
+ if formatVersion == 1 and anchors:
+ _writeAnchorsFormat1(pen, anchors, validate)
+ # prevent lxml from writing self-closing tags
+ if not len(outline):
+ outline.text = "\n "
+ # lib
+ if getattr(glyphObject, "lib", None):
+ _writeLib(glyphObject, root, validate)
+ # return the text
+ data = etree.tostring(
+ root, encoding="UTF-8", xml_declaration=True, pretty_print=True
+ )
+ return data
+
+
+def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=2, validate=True):
+ """
+ Return .glif data for a glyph as a Unicode string (`unicode` in py2, `str`
+ in py3). The XML declaration's encoding is always set to "UTF-8".
+ 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 width of the glyph
+ "height" the advance height of the glyph
+ "unicodes" a list of unicode values for this glyph
+ "note" a string
+ "lib" a dictionary containing custom data
+ "image" a dictionary containing image data
+ "guidelines" a list of guideline data dictionaries
+ "anchors" a list of anchor data dictionaries
+
+ 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.
+
+ The GLIF format version can be specified with the formatVersion argument.
+
+ ``validate`` will validate the written data. It is set to ``True`` by default.
+ """
+ data = _writeGlyphToBytes(
+ glyphName,
+ glyphObject=glyphObject,
+ drawPointsFunc=drawPointsFunc,
+ formatVersion=formatVersion,
+ validate=validate,
+ )
+ return data.decode("utf-8")
+
+
+def _writeAdvance(glyphObject, element, validate):
+ width = getattr(glyphObject, "width", None)
+ if width is not None:
+ if validate and not isinstance(width, numberTypes):
+ raise GlifLibError("width attribute must be int or float")
+ if width == 0:
+ width = None
+ height = getattr(glyphObject, "height", None)
+ if height is not None:
+ if validate and not isinstance(height, numberTypes):
+ raise GlifLibError("height attribute must be int or float")
+ if height == 0:
+ height = None
+ if width is not None and height is not None:
+ etree.SubElement(element, "advance", OrderedDict([("height", repr(height)), ("width", repr(width))]))
+ elif width is not None:
+ etree.SubElement(element, "advance", dict(width=repr(width)))
+ elif height is not None:
+ etree.SubElement(element, "advance", dict(height=repr(height)))
+
+def _writeUnicodes(glyphObject, element, validate):
+ unicodes = getattr(glyphObject, "unicodes", None)
+ if validate and isinstance(unicodes, integerTypes):
+ unicodes = [unicodes]
+ seen = set()
+ for code in unicodes:
+ if validate and not isinstance(code, integerTypes):
+ raise GlifLibError("unicode values must be int")
+ if code in seen:
+ continue
+ seen.add(code)
+ hexCode = "%04X" % code
+ etree.SubElement(element, "unicode", dict(hex=hexCode))
+
+def _writeNote(glyphObject, element, validate):
+ note = getattr(glyphObject, "note", None)
+ if validate and not isinstance(note, basestring):
+ raise GlifLibError("note attribute must be str or unicode")
+ note = note.strip()
+ note = "\n" + note + "\n"
+ # ensure text is unicode, if it's bytes decode as ASCII
+ etree.SubElement(element, "note").text = tounicode(note)
+
+def _writeImage(glyphObject, element, validate):
+ image = getattr(glyphObject, "image", None)
+ if validate and not imageValidator(image):
+ raise GlifLibError("image attribute must be a dict or dict-like object with the proper structure.")
+ attrs = OrderedDict([("fileName", image["fileName"])])
+ for attr, default in _transformationInfo:
+ value = image.get(attr, default)
+ if value != default:
+ attrs[attr] = repr(value)
+ color = image.get("color")
+ if color is not None:
+ attrs["color"] = color
+ etree.SubElement(element, "image", attrs)
+
+def _writeGuidelines(glyphObject, element, identifiers, validate):
+ guidelines = getattr(glyphObject, "guidelines", [])
+ if validate and not guidelinesValidator(guidelines):
+ raise GlifLibError("guidelines attribute does not have the proper structure.")
+ for guideline in guidelines:
+ attrs = OrderedDict()
+ x = guideline.get("x")
+ if x is not None:
+ attrs["x"] = repr(x)
+ y = guideline.get("y")
+ if y is not None:
+ attrs["y"] = repr(y)
+ angle = guideline.get("angle")
+ if angle is not None:
+ attrs["angle"] = repr(angle)
+ name = guideline.get("name")
+ if name is not None:
+ attrs["name"] = name
+ color = guideline.get("color")
+ if color is not None:
+ attrs["color"] = color
+ identifier = guideline.get("identifier")
+ if identifier is not None:
+ if validate and identifier in identifiers:
+ raise GlifLibError("identifier used more than once: %s" % identifier)
+ attrs["identifier"] = identifier
+ identifiers.add(identifier)
+ etree.SubElement(element, "guideline", attrs)
+
+def _writeAnchorsFormat1(pen, anchors, validate):
+ if validate and not anchorsValidator(anchors):
+ raise GlifLibError("anchors attribute does not have the proper structure.")
+ for anchor in anchors:
+ attrs = {}
+ x = anchor["x"]
+ attrs["x"] = repr(x)
+ y = anchor["y"]
+ attrs["y"] = repr(y)
+ name = anchor.get("name")
+ if name is not None:
+ attrs["name"] = name
+ pen.beginPath()
+ pen.addPoint((x, y), segmentType="move", name=name)
+ pen.endPath()
+
+def _writeAnchors(glyphObject, element, identifiers, validate):
+ anchors = getattr(glyphObject, "anchors", [])
+ if validate and not anchorsValidator(anchors):
+ raise GlifLibError("anchors attribute does not have the proper structure.")
+ for anchor in anchors:
+ attrs = OrderedDict()
+ x = anchor["x"]
+ attrs["x"] = repr(x)
+ y = anchor["y"]
+ attrs["y"] = repr(y)
+ name = anchor.get("name")
+ if name is not None:
+ attrs["name"] = name
+ color = anchor.get("color")
+ if color is not None:
+ attrs["color"] = color
+ identifier = anchor.get("identifier")
+ if identifier is not None:
+ if validate and identifier in identifiers:
+ raise GlifLibError("identifier used more than once: %s" % identifier)
+ attrs["identifier"] = identifier
+ identifiers.add(identifier)
+ etree.SubElement(element, "anchor", attrs)
+
+def _writeLib(glyphObject, element, validate):
+ lib = getattr(glyphObject, "lib", None)
+ if not lib:
+ # don't write empty lib
+ return
+ if validate:
+ valid, message = glyphLibValidator(lib)
+ if not valid:
+ raise GlifLibError(message)
+ if not isinstance(lib, dict):
+ lib = dict(lib)
+ # plist inside GLIF begins with 2 levels of indentation
+ e = plistlib.totree(lib, indent_level=2)
+ etree.SubElement(element, "lib").append(e)
+
+# -----------------------
+# layerinfo.plist Support
+# -----------------------
+
+layerInfoVersion3ValueData = {
+ "color" : dict(type=basestring, valueValidator=colorValidator),
+ "lib" : dict(type=dict, valueValidator=genericTypeValidator)
+}
+
+def validateLayerInfoVersion3ValueForAttribute(attr, value):
+ """
+ This performs very basic validation of the value for attribute
+ following the UFO 3 fontinfo.plist specification. The results
+ of this should not be interpretted as *correct* for the font
+ that they are part of. This merely indicates that the value
+ is of the proper type and, where the specification defines
+ a set range of possible values for an attribute, that the
+ value is in the accepted range.
+ """
+ if attr not in layerInfoVersion3ValueData:
+ return False
+ dataValidationDict = layerInfoVersion3ValueData[attr]
+ valueType = dataValidationDict.get("type")
+ validator = dataValidationDict.get("valueValidator")
+ valueOptions = dataValidationDict.get("valueOptions")
+ # have specific options for the validator
+ if valueOptions is not None:
+ isValidValue = validator(value, valueOptions)
+ # no specific options
+ else:
+ if validator == genericTypeValidator:
+ isValidValue = validator(value, valueType)
+ else:
+ isValidValue = validator(value)
+ return isValidValue
+
+def validateLayerInfoVersion3Data(infoData):
+ """
+ This performs very basic validation of the value for infoData
+ following the UFO 3 layerinfo.plist specification. The results
+ of this should not be interpretted as *correct* for the font
+ that they are part of. This merely indicates that the values
+ are of the proper type and, where the specification defines
+ a set range of possible values for an attribute, that the
+ value is in the accepted range.
+ """
+ for attr, value in infoData.items():
+ if attr not in layerInfoVersion3ValueData:
+ raise GlifLibError("Unknown attribute %s." % attr)
+ isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value)
+ if not isValidValue:
+ raise GlifLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
+ return infoData
+
+# -----------------
+# GLIF Tree Support
+# -----------------
+
+def _glifTreeFromFile(aFile):
+ root = etree.parse(aFile).getroot()
+ if root.tag != "glyph":
+ raise GlifLibError("The GLIF is not properly formatted.")
+ if root.text and root.text.strip() != '':
+ raise GlifLibError("Invalid GLIF structure.")
+ return root
+
+
+def _glifTreeFromString(aString):
+ data = tobytes(aString, encoding="utf-8")
+ root = etree.fromstring(data)
+ if root.tag != "glyph":
+ raise GlifLibError("The GLIF is not properly formatted.")
+ if root.text and root.text.strip() != '':
+ raise GlifLibError("Invalid GLIF structure.")
+ return root
+
+def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
+ # check the format version
+ formatVersion = tree.get("format")
+ if validate and formatVersion is None:
+ raise GlifLibError("Unspecified format version in GLIF.")
+ try:
+ v = int(formatVersion)
+ formatVersion = v
+ except ValueError:
+ pass
+ if validate and formatVersion not in formatVersions:
+ raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion)
+ if formatVersion == 1:
+ _readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
+ elif formatVersion == 2:
+ _readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
+ else:
+ raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
+
+
+def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None):
+ # get the name
+ _readName(glyphObject, tree, validate)
+ # populate the sub elements
+ unicodes = []
+ haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False
+ for element in tree:
+ if element.tag == "outline":
+ if validate:
+ if haveSeenOutline:
+ raise GlifLibError("The outline element occurs more than once.")
+ if element.attrib:
+ raise GlifLibError("The outline element contains unknown attributes.")
+ if element.text and element.text.strip() != '':
+ raise GlifLibError("Invalid outline structure.")
+ haveSeenOutline = True
+ buildOutlineFormat1(glyphObject, pointPen, element, validate)
+ elif glyphObject is None:
+ continue
+ elif element.tag == "advance":
+ if validate and haveSeenAdvance:
+ raise GlifLibError("The advance element occurs more than once.")
+ haveSeenAdvance = True
+ _readAdvance(glyphObject, element)
+ elif element.tag == "unicode":
+ try:
+ v = element.get("hex")
+ v = int(v, 16)
+ if v not in unicodes:
+ unicodes.append(v)
+ except ValueError:
+ raise GlifLibError("Illegal value for hex attribute of unicode element.")
+ elif element.tag == "note":
+ if validate and haveSeenNote:
+ raise GlifLibError("The note element occurs more than once.")
+ haveSeenNote = True
+ _readNote(glyphObject, element)
+ elif element.tag == "lib":
+ if validate and haveSeenLib:
+ raise GlifLibError("The lib element occurs more than once.")
+ haveSeenLib = True
+ _readLib(glyphObject, element, validate)
+ else:
+ raise GlifLibError("Unknown element in GLIF: %s" % element)
+ # set the collected unicodes
+ if unicodes:
+ _relaxedSetattr(glyphObject, "unicodes", unicodes)
+
+def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=None):
+ # get the name
+ _readName(glyphObject, tree, validate)
+ # populate the sub elements
+ unicodes = []
+ guidelines = []
+ anchors = []
+ haveSeenAdvance = haveSeenImage = haveSeenOutline = haveSeenLib = haveSeenNote = False
+ identifiers = set()
+ for element in tree:
+ if element.tag == "outline":
+ if validate:
+ if haveSeenOutline:
+ raise GlifLibError("The outline element occurs more than once.")
+ if element.attrib:
+ raise GlifLibError("The outline element contains unknown attributes.")
+ if element.text and element.text.strip() != '':
+ raise GlifLibError("Invalid outline structure.")
+ haveSeenOutline = True
+ if pointPen is not None:
+ buildOutlineFormat2(glyphObject, pointPen, element, identifiers, validate)
+ elif glyphObject is None:
+ continue
+ elif element.tag == "advance":
+ if validate and haveSeenAdvance:
+ raise GlifLibError("The advance element occurs more than once.")
+ haveSeenAdvance = True
+ _readAdvance(glyphObject, element)
+ elif element.tag == "unicode":
+ try:
+ v = element.get("hex")
+ v = int(v, 16)
+ if v not in unicodes:
+ unicodes.append(v)
+ except ValueError:
+ raise GlifLibError("Illegal value for hex attribute of unicode element.")
+ elif element.tag == "guideline":
+ if validate and len(element):
+ raise GlifLibError("Unknown children in guideline element.")
+ attrib = dict(element.attrib)
+ for attr in ("x", "y", "angle"):
+ if attr in attrib:
+ attrib[attr] = _number(attrib[attr])
+ guidelines.append(attrib)
+ elif element.tag == "anchor":
+ if validate and len(element):
+ raise GlifLibError("Unknown children in anchor element.")
+ attrib = dict(element.attrib)
+ for attr in ("x", "y"):
+ if attr in element.attrib:
+ attrib[attr] = _number(attrib[attr])
+ anchors.append(attrib)
+ elif element.tag == "image":
+ if validate:
+ if haveSeenImage:
+ raise GlifLibError("The image element occurs more than once.")
+ if len(element):
+ raise GlifLibError("Unknown children in image element.")
+ haveSeenImage = True
+ _readImage(glyphObject, element, validate)
+ elif element.tag == "note":
+ if validate and haveSeenNote:
+ raise GlifLibError("The note element occurs more than once.")
+ haveSeenNote = True
+ _readNote(glyphObject, element)
+ elif element.tag == "lib":
+ if validate and haveSeenLib:
+ raise GlifLibError("The lib element occurs more than once.")
+ haveSeenLib = True
+ _readLib(glyphObject, element, validate)
+ else:
+ raise GlifLibError("Unknown element in GLIF: %s" % element)
+ # set the collected unicodes
+ if unicodes:
+ _relaxedSetattr(glyphObject, "unicodes", unicodes)
+ # set the collected guidelines
+ if guidelines:
+ if validate and not guidelinesValidator(guidelines, identifiers):
+ raise GlifLibError("The guidelines are improperly formatted.")
+ _relaxedSetattr(glyphObject, "guidelines", guidelines)
+ # set the collected anchors
+ if anchors:
+ if validate and not anchorsValidator(anchors, identifiers):
+ raise GlifLibError("The anchors are improperly formatted.")
+ _relaxedSetattr(glyphObject, "anchors", anchors)
+
+def _readName(glyphObject, root, validate):
+ glyphName = root.get("name")
+ if validate and not glyphName:
+ raise GlifLibError("Empty glyph name in GLIF.")
+ if glyphName and glyphObject is not None:
+ _relaxedSetattr(glyphObject, "name", glyphName)
+
+def _readAdvance(glyphObject, advance):
+ width = _number(advance.get("width", 0))
+ _relaxedSetattr(glyphObject, "width", width)
+ height = _number(advance.get("height", 0))
+ _relaxedSetattr(glyphObject, "height", height)
+
+def _readNote(glyphObject, note):
+ lines = note.text.split("\n")
+ note = "\n".join(line.strip() for line in lines if line.strip())
+ _relaxedSetattr(glyphObject, "note", note)
+
+def _readLib(glyphObject, lib, validate):
+ assert len(lib) == 1
+ child = lib[0]
+ plist = plistlib.fromtree(child)
+ if validate:
+ valid, message = glyphLibValidator(plist)
+ if not valid:
+ raise GlifLibError(message)
+ _relaxedSetattr(glyphObject, "lib", plist)
+
+def _readImage(glyphObject, image, validate):
+ imageData = dict(image.attrib)
+ for attr, default in _transformationInfo:
+ value = imageData.get(attr, default)
+ imageData[attr] = _number(value)
+ if validate and not imageValidator(imageData):
+ raise GlifLibError("The image element is not properly formatted.")
+ _relaxedSetattr(glyphObject, "image", imageData)
+
+# ----------------
+# GLIF to PointPen
+# ----------------
+
+contourAttributesFormat2 = set(["identifier"])
+componentAttributesFormat1 = set(["base", "xScale", "xyScale", "yxScale", "yScale", "xOffset", "yOffset"])
+componentAttributesFormat2 = componentAttributesFormat1 | set(["identifier"])
+pointAttributesFormat1 = set(["x", "y", "type", "smooth", "name"])
+pointAttributesFormat2 = pointAttributesFormat1 | set(["identifier"])
+pointSmoothOptions = set(("no", "yes"))
+pointTypeOptions = set(["move", "line", "offcurve", "curve", "qcurve"])
+
+# format 1
+
+def buildOutlineFormat1(glyphObject, pen, outline, validate):
+ anchors = []
+ for element in outline:
+ if element.tag == "contour":
+ if len(element) == 1:
+ point = element[0]
+ if point.tag == "point":
+ anchor = _buildAnchorFormat1(point, validate)
+ if anchor is not None:
+ anchors.append(anchor)
+ continue
+ if pen is not None:
+ _buildOutlineContourFormat1(pen, element, validate)
+ elif element.tag == "component":
+ if pen is not None:
+ _buildOutlineComponentFormat1(pen, element, validate)
+ else:
+ raise GlifLibError("Unknown element in outline element: %s" % element)
+ if glyphObject is not None and anchors:
+ if validate and not anchorsValidator(anchors):
+ raise GlifLibError("GLIF 1 anchors are not properly formatted.")
+ _relaxedSetattr(glyphObject, "anchors", anchors)
+
+def _buildAnchorFormat1(point, validate):
+ if point.get("type") != "move":
+ return None
+ name = point.get("name")
+ if name is None:
+ return None
+ x = point.get("x")
+ y = point.get("y")
+ if validate and x is None:
+ raise GlifLibError("Required x attribute is missing in point element.")
+ if validate and y is None:
+ raise GlifLibError("Required y attribute is missing in point element.")
+ x = _number(x)
+ y = _number(y)
+ anchor = dict(x=x, y=y, name=name)
+ return anchor
+
+def _buildOutlineContourFormat1(pen, contour, validate):
+ if validate and contour.attrib:
+ raise GlifLibError("Unknown attributes in contour element.")
+ pen.beginPath()
+ if len(contour):
+ massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat1, openContourOffCurveLeniency=True, validate=validate)
+ _buildOutlinePointsFormat1(pen, massaged)
+ pen.endPath()
+
+def _buildOutlinePointsFormat1(pen, contour):
+ for point in contour:
+ x = point["x"]
+ y = point["y"]
+ segmentType = point["segmentType"]
+ smooth = point["smooth"]
+ name = point["name"]
+ pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
+
+def _buildOutlineComponentFormat1(pen, component, validate):
+ if validate:
+ if len(component):
+ raise GlifLibError("Unknown child elements of component element.")
+ for attr in component.attrib.keys():
+ if attr not in componentAttributesFormat1:
+ raise GlifLibError("Unknown attribute in component element: %s" % attr)
+ baseGlyphName = component.get("base")
+ if validate and baseGlyphName is None:
+ raise GlifLibError("The base attribute is not defined in the component.")
+ transformation = []
+ for attr, default in _transformationInfo:
+ value = component.get(attr)
+ if value is None:
+ value = default
+ else:
+ value = _number(value)
+ transformation.append(value)
+ pen.addComponent(baseGlyphName, tuple(transformation))
+
+# format 2
+
+def buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate):
+ for element in outline:
+ if element.tag == "contour":
+ _buildOutlineContourFormat2(pen, element, identifiers, validate)
+ elif element.tag == "component":
+ _buildOutlineComponentFormat2(pen, element, identifiers, validate)
+ else:
+ raise GlifLibError("Unknown element in outline element: %s" % element.tag)
+
+def _buildOutlineContourFormat2(pen, contour, identifiers, validate):
+ if validate:
+ for attr in contour.attrib.keys():
+ if attr not in contourAttributesFormat2:
+ raise GlifLibError("Unknown attribute in contour element: %s" % attr)
+ identifier = contour.get("identifier")
+ if identifier is not None:
+ if validate:
+ if identifier in identifiers:
+ raise GlifLibError("The identifier %s is used more than once." % identifier)
+ if not identifierValidator(identifier):
+ raise GlifLibError("The contour identifier %s is not valid." % identifier)
+ identifiers.add(identifier)
+ try:
+ pen.beginPath(identifier=identifier)
+ except TypeError:
+ pen.beginPath()
+ warn("The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.", DeprecationWarning)
+ if len(contour):
+ massaged = _validateAndMassagePointStructures(contour, pointAttributesFormat2, validate=validate)
+ _buildOutlinePointsFormat2(pen, massaged, identifiers, validate)
+ pen.endPath()
+
+def _buildOutlinePointsFormat2(pen, contour, identifiers, validate):
+ for point in contour:
+ x = point["x"]
+ y = point["y"]
+ segmentType = point["segmentType"]
+ smooth = point["smooth"]
+ name = point["name"]
+ identifier = point.get("identifier")
+ if identifier is not None:
+ if validate:
+ if identifier in identifiers:
+ raise GlifLibError("The identifier %s is used more than once." % identifier)
+ if not identifierValidator(identifier):
+ raise GlifLibError("The identifier %s is not valid." % identifier)
+ identifiers.add(identifier)
+ try:
+ pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name, identifier=identifier)
+ except TypeError:
+ pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
+ warn("The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.", DeprecationWarning)
+
+def _buildOutlineComponentFormat2(pen, component, identifiers, validate):
+ if validate:
+ if len(component):
+ raise GlifLibError("Unknown child elements of component element.")
+ for attr in component.attrib.keys():
+ if attr not in componentAttributesFormat2:
+ raise GlifLibError("Unknown attribute in component element: %s" % attr)
+ baseGlyphName = component.get("base")
+ if validate and baseGlyphName is None:
+ raise GlifLibError("The base attribute is not defined in the component.")
+ transformation = []
+ for attr, default in _transformationInfo:
+ value = component.get(attr)
+ if value is None:
+ value = default
+ else:
+ value = _number(value)
+ transformation.append(value)
+ identifier = component.get("identifier")
+ if identifier is not None:
+ if validate:
+ if identifier in identifiers:
+ raise GlifLibError("The identifier %s is used more than once." % identifier)
+ if validate and not identifierValidator(identifier):
+ raise GlifLibError("The identifier %s is not valid." % identifier)
+ identifiers.add(identifier)
+ try:
+ pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier)
+ except TypeError:
+ pen.addComponent(baseGlyphName, tuple(transformation))
+ warn("The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.", DeprecationWarning)
+
+# all formats
+
+def _validateAndMassagePointStructures(contour, pointAttributes, openContourOffCurveLeniency=False, validate=True):
+ if not len(contour):
+ return
+ # store some data for later validation
+ lastOnCurvePoint = None
+ haveOffCurvePoint = False
+ # validate and massage the individual point elements
+ massaged = []
+ for index, element in enumerate(contour):
+ # not <point>
+ if element.tag != "point":
+ raise GlifLibError("Unknown child element (%s) of contour element." % element.tag)
+ point = dict(element.attrib)
+ massaged.append(point)
+ if validate:
+ # unknown attributes
+ for attr in point.keys():
+ if attr not in pointAttributes:
+ raise GlifLibError("Unknown attribute in point element: %s" % attr)
+ # search for unknown children
+ if len(element):
+ raise GlifLibError("Unknown child elements in point element.")
+ # x and y are required
+ for attr in ("x", "y"):
+ value = element.get(attr)
+ if validate and value is None:
+ raise GlifLibError("Required %s attribute is missing in point element." % attr)
+ point[attr] = _number(value)
+ # segment type
+ pointType = point.pop("type", "offcurve")
+ if validate and pointType not in pointTypeOptions:
+ raise GlifLibError("Unknown point type: %s" % pointType)
+ if pointType == "offcurve":
+ pointType = None
+ point["segmentType"] = pointType
+ if pointType is None:
+ haveOffCurvePoint = True
+ else:
+ lastOnCurvePoint = index
+ # move can only occur as the first point
+ if validate and pointType == "move" and index != 0:
+ raise GlifLibError("A move point occurs after the first point in the contour.")
+ # smooth is optional
+ smooth = point.get("smooth", "no")
+ if validate and smooth is not None:
+ if smooth not in pointSmoothOptions:
+ raise GlifLibError("Unknown point smooth value: %s" % smooth)
+ smooth = smooth == "yes"
+ point["smooth"] = smooth
+ # smooth can only be applied to curve and qcurve
+ if validate and smooth and pointType is None:
+ raise GlifLibError("smooth attribute set in an offcurve point.")
+ # name is optional
+ if "name" not in element.attrib:
+ point["name"] = None
+ if openContourOffCurveLeniency:
+ # remove offcurves that precede a move. this is technically illegal,
+ # but we let it slide because there are fonts out there in the wild like this.
+ if massaged[0]["segmentType"] == "move":
+ count = 0
+ for point in reversed(massaged):
+ if point["segmentType"] is None:
+ count += 1
+ else:
+ break
+ if count:
+ massaged = massaged[:-count]
+ # validate the off-curves in the segments
+ if validate and haveOffCurvePoint and lastOnCurvePoint is not None:
+ # we only care about how many offCurves there are before an onCurve
+ # filter out the trailing offCurves
+ offCurvesCount = len(massaged) - 1 - lastOnCurvePoint
+ for point in massaged:
+ segmentType = point["segmentType"]
+ if segmentType is None:
+ offCurvesCount += 1
+ else:
+ if offCurvesCount:
+ # move and line can't be preceded by off-curves
+ if segmentType == "move":
+ # this will have been filtered out already
+ raise GlifLibError("move can not have an offcurve.")
+ elif segmentType == "line":
+ raise GlifLibError("line can not have an offcurve.")
+ elif segmentType == "curve":
+ if offCurvesCount > 2:
+ raise GlifLibError("Too many offcurves defined for curve.")
+ elif segmentType == "qcurve":
+ pass
+ else:
+ # unknown segment type. it'll be caught later.
+ pass
+ offCurvesCount = 0
+ return massaged
+
+# ---------------------
+# Misc Helper Functions
+# ---------------------
+
+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.
+
+ >>> _number("1")
+ 1
+ >>> _number("1.0")
+ 1.0
+ >>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ GlifLibError: Could not convert a to an int or float.
+ """
+ try:
+ n = int(s)
+ return n
+ except ValueError:
+ pass
+ try:
+ n = float(s)
+ return n
+ except ValueError:
+ raise GlifLibError("Could not convert %s to an int or float." % s)
+
+# --------------------
+# Rapid Value Fetching
+# --------------------
+
+# base
+
+class _DoneParsing(Exception): pass
+
+class _BaseParser(object):
+
+ def __init__(self):
+ self._elementStack = []
+
+ def parse(self, text):
+ from xml.parsers.expat import ParserCreate
+ parser = ParserCreate()
+ parser.StartElementHandler = self.startElementHandler
+ parser.EndElementHandler = self.endElementHandler
+ parser.Parse(text)
+
+ def startElementHandler(self, name, attrs):
+ self._elementStack.append(name)
+
+ def endElementHandler(self, name):
+ other = self._elementStack.pop(-1)
+ assert other == name
+
+
+# unicodes
+
+def _fetchUnicodes(glif):
+ """
+ Get a list of unicodes listed in glif.
+ """
+ parser = _FetchUnicodesParser()
+ parser.parse(glif)
+ return parser.unicodes
+
+class _FetchUnicodesParser(_BaseParser):
+
+ def __init__(self):
+ self.unicodes = []
+ super(_FetchUnicodesParser, self).__init__()
+
+ def startElementHandler(self, name, attrs):
+ if name == "unicode" and self._elementStack and self._elementStack[-1] == "glyph":
+ value = attrs.get("hex")
+ if value is not None:
+ try:
+ value = int(value, 16)
+ if value not in self.unicodes:
+ self.unicodes.append(value)
+ except ValueError:
+ pass
+ super(_FetchUnicodesParser, self).startElementHandler(name, attrs)
+
+# image
+
+def _fetchImageFileName(glif):
+ """
+ The image file name (if any) from glif.
+ """
+ parser = _FetchImageFileNameParser()
+ try:
+ parser.parse(glif)
+ except _DoneParsing:
+ pass
+ return parser.fileName
+
+class _FetchImageFileNameParser(_BaseParser):
+
+ def __init__(self):
+ self.fileName = None
+ super(_FetchImageFileNameParser, self).__init__()
+
+ def startElementHandler(self, name, attrs):
+ if name == "image" and self._elementStack and self._elementStack[-1] == "glyph":
+ self.fileName = attrs.get("fileName")
+ raise _DoneParsing
+ super(_FetchImageFileNameParser, self).startElementHandler(name, attrs)
+
+# component references
+
+def _fetchComponentBases(glif):
+ """
+ Get a list of component base glyphs listed in glif.
+ """
+ parser = _FetchComponentBasesParser()
+ try:
+ parser.parse(glif)
+ except _DoneParsing:
+ pass
+ return list(parser.bases)
+
+class _FetchComponentBasesParser(_BaseParser):
+
+ def __init__(self):
+ self.bases = []
+ super(_FetchComponentBasesParser, self).__init__()
+
+ def startElementHandler(self, name, attrs):
+ if name == "component" and self._elementStack and self._elementStack[-1] == "outline":
+ base = attrs.get("base")
+ if base is not None:
+ self.bases.append(base)
+ super(_FetchComponentBasesParser, self).startElementHandler(name, attrs)
+
+ def endElementHandler(self, name):
+ if name == "outline":
+ raise _DoneParsing
+ super(_FetchComponentBasesParser, self).endElementHandler(name)
+
+# --------------
+# GLIF Point Pen
+# --------------
+
+_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, element, formatVersion=2, identifiers=None, validate=True):
+ if identifiers is None:
+ identifiers = set()
+ self.formatVersion = formatVersion
+ self.identifiers = identifiers
+ self.outline = element
+ self.contour = None
+ self.prevOffCurveCount = 0
+ self.prevPointTypes = []
+ self.validate = validate
+
+ def beginPath(self, identifier=None, **kwargs):
+ attrs = OrderedDict()
+ if identifier is not None and self.formatVersion >= 2:
+ if self.validate:
+ if identifier in self.identifiers:
+ raise GlifLibError("identifier used more than once: %s" % identifier)
+ if not identifierValidator(identifier):
+ raise GlifLibError("identifier not formatted properly: %s" % identifier)
+ attrs["identifier"] = identifier
+ self.identifiers.add(identifier)
+ self.contour = etree.SubElement(self.outline, "contour", attrs)
+ self.prevOffCurveCount = 0
+
+ def endPath(self):
+ if self.prevPointTypes and self.prevPointTypes[0] == "move":
+ if self.validate and self.prevPointTypes[-1] == "offcurve":
+ raise GlifLibError("open contour has loose offcurve point")
+ # prevent lxml from writing self-closing tags
+ if not len(self.contour):
+ self.contour.text = "\n "
+ self.contour = None
+ self.prevPointType = None
+ self.prevOffCurveCount = 0
+ self.prevPointTypes = []
+
+ def addPoint(self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs):
+ attrs = OrderedDict()
+ # coordinates
+ if pt is not None:
+ if self.validate:
+ for coord in pt:
+ if not isinstance(coord, numberTypes):
+ raise GlifLibError("coordinates must be int or float")
+ attrs["x"] = repr(pt[0])
+ attrs["y"] = repr(pt[1])
+ # segment type
+ if segmentType == "offcurve":
+ segmentType = None
+ if self.validate:
+ if segmentType == "move" and self.prevPointTypes:
+ raise GlifLibError("move occurs after a point has already been added to the contour.")
+ if segmentType in ("move", "line") and self.prevPointTypes and self.prevPointTypes[-1] == "offcurve":
+ raise GlifLibError("offcurve occurs before %s point." % segmentType)
+ if segmentType == "curve" and self.prevOffCurveCount > 2:
+ raise GlifLibError("too many offcurve points before curve point.")
+ if segmentType is not None:
+ attrs["type"] = segmentType
+ else:
+ segmentType = "offcurve"
+ if segmentType == "offcurve":
+ self.prevOffCurveCount += 1
+ else:
+ self.prevOffCurveCount = 0
+ self.prevPointTypes.append(segmentType)
+ # smooth
+ if smooth:
+ if self.validate and segmentType == "offcurve":
+ raise GlifLibError("can't set smooth in an offcurve point.")
+ attrs["smooth"] = "yes"
+ # name
+ if name is not None:
+ attrs["name"] = name
+ # identifier
+ if identifier is not None and self.formatVersion >= 2:
+ if self.validate:
+ if identifier in self.identifiers:
+ raise GlifLibError("identifier used more than once: %s" % identifier)
+ if not identifierValidator(identifier):
+ raise GlifLibError("identifier not formatted properly: %s" % identifier)
+ attrs["identifier"] = identifier
+ self.identifiers.add(identifier)
+ etree.SubElement(self.contour, "point", attrs)
+
+ def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
+ attrs = OrderedDict([("base", glyphName)])
+ for (attr, default), value in zip(_transformationInfo, transformation):
+ if self.validate and not isinstance(value, numberTypes):
+ raise GlifLibError("transformation values must be int or float")
+ if value != default:
+ attrs[attr] = repr(value)
+ if identifier is not None and self.formatVersion >= 2:
+ if self.validate:
+ if identifier in self.identifiers:
+ raise GlifLibError("identifier used more than once: %s" % identifier)
+ if self.validate and not identifierValidator(identifier):
+ raise GlifLibError("identifier not formatted properly: %s" % identifier)
+ attrs["identifier"] = identifier
+ self.identifiers.add(identifier)
+ etree.SubElement(self.outline, "component", attrs)
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/Lib/fontTools/ufoLib/kerning.py b/Lib/fontTools/ufoLib/kerning.py
new file mode 100644
index 00000000..03e413fa
--- /dev/null
+++ b/Lib/fontTools/ufoLib/kerning.py
@@ -0,0 +1,90 @@
+from __future__ import absolute_import, unicode_literals
+
+
+def lookupKerningValue(pair, kerning, groups, fallback=0, glyphToFirstGroup=None, glyphToSecondGroup=None):
+ """
+ Note: This expects kerning to be a flat dictionary
+ of kerning pairs, not the nested structure used
+ in kerning.plist.
+
+ >>> groups = {
+ ... "public.kern1.O" : ["O", "D", "Q"],
+ ... "public.kern2.E" : ["E", "F"]
+ ... }
+ >>> kerning = {
+ ... ("public.kern1.O", "public.kern2.E") : -100,
+ ... ("public.kern1.O", "F") : -200,
+ ... ("D", "F") : -300
+ ... }
+ >>> lookupKerningValue(("D", "F"), kerning, groups)
+ -300
+ >>> lookupKerningValue(("O", "F"), kerning, groups)
+ -200
+ >>> lookupKerningValue(("O", "E"), kerning, groups)
+ -100
+ >>> lookupKerningValue(("O", "O"), kerning, groups)
+ 0
+ >>> lookupKerningValue(("E", "E"), kerning, groups)
+ 0
+ >>> lookupKerningValue(("E", "O"), kerning, groups)
+ 0
+ >>> lookupKerningValue(("X", "X"), kerning, groups)
+ 0
+ >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"),
+ ... kerning, groups)
+ -100
+ >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups)
+ -200
+ >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups)
+ -100
+ >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups)
+ 0
+ """
+ # quickly check to see if the pair is in the kerning dictionary
+ if pair in kerning:
+ return kerning[pair]
+ # create glyph to group mapping
+ if glyphToFirstGroup is not None:
+ assert glyphToSecondGroup is not None
+ if glyphToSecondGroup is not None:
+ assert glyphToFirstGroup is not None
+ if glyphToFirstGroup is None:
+ glyphToFirstGroup = {}
+ glyphToSecondGroup = {}
+ for group, groupMembers in groups.items():
+ if group.startswith("public.kern1."):
+ for glyph in groupMembers:
+ glyphToFirstGroup[glyph] = group
+ elif group.startswith("public.kern2."):
+ for glyph in groupMembers:
+ glyphToSecondGroup[glyph] = group
+ # get group names and make sure first and second are glyph names
+ first, second = pair
+ firstGroup = secondGroup = None
+ if first.startswith("public.kern1."):
+ firstGroup = first
+ first = None
+ else:
+ firstGroup = glyphToFirstGroup.get(first)
+ if second.startswith("public.kern2."):
+ secondGroup = second
+ second = None
+ else:
+ secondGroup = glyphToSecondGroup.get(second)
+ # make an ordered list of pairs to look up
+ pairs = [
+ (first, second),
+ (first, secondGroup),
+ (firstGroup, second),
+ (firstGroup, secondGroup)
+ ]
+ # look up the pairs and return any matches
+ for pair in pairs:
+ if pair in kerning:
+ return kerning[pair]
+ # use the fallback value
+ return fallback
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/Lib/fontTools/ufoLib/plistlib.py b/Lib/fontTools/ufoLib/plistlib.py
new file mode 100644
index 00000000..d0247bf9
--- /dev/null
+++ b/Lib/fontTools/ufoLib/plistlib.py
@@ -0,0 +1,45 @@
+"""DEPRECATED - This module is kept here only as a backward compatibility shim
+for the old ufoLib.plistlib module, which was moved to fontTools.misc.plistlib.
+Please use the latter instead.
+"""
+from fontTools.misc.plistlib import *
+
+# The following functions were part of the old py2-like ufoLib.plistlib API.
+# They are kept only for backward compatiblity.
+from fontTools.ufoLib.utils import deprecated
+
+
+@deprecated("Use 'fontTools.misc.plistlib.load' instead")
+def readPlist(path_or_file):
+ did_open = False
+ if isinstance(path_or_file, basestring):
+ path_or_file = open(path_or_file, "rb")
+ did_open = True
+ try:
+ return load(path_or_file, use_builtin_types=False)
+ finally:
+ if did_open:
+ path_or_file.close()
+
+
+@deprecated("Use 'fontTools.misc.plistlib.dump' instead")
+def writePlist(value, path_or_file):
+ did_open = False
+ if isinstance(path_or_file, basestring):
+ path_or_file = open(path_or_file, "wb")
+ did_open = True
+ try:
+ dump(value, path_or_file, use_builtin_types=False)
+ finally:
+ if did_open:
+ path_or_file.close()
+
+
+@deprecated("Use 'fontTools.misc.plistlib.loads' instead")
+def readPlistFromString(data):
+ return loads(tobytes(data, encoding="utf-8"), use_builtin_types=False)
+
+
+@deprecated("Use 'fontTools.misc.plistlib.dumps' instead")
+def writePlistToString(value):
+ return dumps(value, use_builtin_types=False)
diff --git a/Lib/fontTools/ufoLib/pointPen.py b/Lib/fontTools/ufoLib/pointPen.py
new file mode 100644
index 00000000..3433fdbc
--- /dev/null
+++ b/Lib/fontTools/ufoLib/pointPen.py
@@ -0,0 +1,5 @@
+"""DEPRECATED - This module is kept here only as a backward compatibility shim
+for the old ufoLib.pointPen module, which was moved to fontTools.pens.pointPen.
+Please use the latter instead.
+"""
+from fontTools.pens.pointPen import *
diff --git a/Lib/fontTools/ufoLib/utils.py b/Lib/fontTools/ufoLib/utils.py
new file mode 100644
index 00000000..77e2f92a
--- /dev/null
+++ b/Lib/fontTools/ufoLib/utils.py
@@ -0,0 +1,86 @@
+"""The module contains miscellaneous helpers.
+It's not considered part of the public ufoLib API.
+"""
+from __future__ import absolute_import, unicode_literals
+import sys
+import warnings
+import functools
+from datetime import datetime
+from fontTools.misc.py23 import tounicode
+
+
+if hasattr(datetime, "timestamp"): # python >= 3.3
+
+ def datetimeAsTimestamp(dt):
+ return dt.timestamp()
+
+else:
+ from datetime import tzinfo, timedelta
+
+ ZERO = timedelta(0)
+
+ class UTC(tzinfo):
+
+ def utcoffset(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return ZERO
+
+ utc = UTC()
+
+ EPOCH = datetime.fromtimestamp(0, tz=utc)
+
+ def datetimeAsTimestamp(dt):
+ return (dt - EPOCH).total_seconds()
+
+
+# TODO: should import from fontTools.misc.py23
+try:
+ long = long
+except NameError:
+ long = int
+
+integerTypes = (int, long)
+numberTypes = (int, float, long)
+
+
+def deprecated(msg=""):
+ """Decorator factory to mark functions as deprecated with given message.
+
+ >>> @deprecated("Enough!")
+ ... def some_function():
+ ... "I just print 'hello world'."
+ ... print("hello world")
+ >>> some_function()
+ hello world
+ >>> some_function.__doc__ == "I just print 'hello world'."
+ True
+ """
+
+ def deprecated_decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ warnings.warn(
+ "{} function is a deprecated. {}".format(func.__name__, msg),
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+ return func(*args, **kwargs)
+
+ return wrapper
+
+ return deprecated_decorator
+
+
+def fsdecode(path, encoding=sys.getfilesystemencoding()):
+ return tounicode(path, encoding=encoding)
+
+
+if __name__ == "__main__":
+ import doctest
+
+ doctest.testmod()
diff --git a/Lib/fontTools/ufoLib/validators.py b/Lib/fontTools/ufoLib/validators.py
new file mode 100644
index 00000000..844b7f1b
--- /dev/null
+++ b/Lib/fontTools/ufoLib/validators.py
@@ -0,0 +1,1066 @@
+"""Various low level data validators."""
+
+from __future__ import absolute_import, unicode_literals
+import calendar
+from io import open
+import fs.base
+import fs.osfs
+
+try:
+ from collections.abc import Mapping # python >= 3.3
+except ImportError:
+ from collections import Mapping
+
+from fontTools.misc.py23 import basestring
+from fontTools.ufoLib.utils import integerTypes, numberTypes
+
+
+# -------
+# Generic
+# -------
+
+def isDictEnough(value):
+ """
+ Some objects will likely come in that aren't
+ dicts but are dict-ish enough.
+ """
+ if isinstance(value, Mapping):
+ return True
+ for attr in ("keys", "values", "items"):
+ if not hasattr(value, attr):
+ return False
+ return True
+
+def genericTypeValidator(value, typ):
+ """
+ Generic. (Added at version 2.)
+ """
+ return isinstance(value, typ)
+
+def genericIntListValidator(values, validValues):
+ """
+ Generic. (Added at version 2.)
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ valuesSet = set(values)
+ validValuesSet = set(validValues)
+ if valuesSet - validValuesSet:
+ return False
+ for value in values:
+ if not isinstance(value, integerTypes):
+ return False
+ return True
+
+def genericNonNegativeIntValidator(value):
+ """
+ Generic. (Added at version 3.)
+ """
+ if not isinstance(value, integerTypes):
+ return False
+ if value < 0:
+ return False
+ return True
+
+def genericNonNegativeNumberValidator(value):
+ """
+ Generic. (Added at version 3.)
+ """
+ if not isinstance(value, numberTypes):
+ return False
+ if value < 0:
+ return False
+ return True
+
+def genericDictValidator(value, prototype):
+ """
+ Generic. (Added at version 3.)
+ """
+ # not a dict
+ if not isinstance(value, Mapping):
+ return False
+ # missing required keys
+ for key, (typ, required) in prototype.items():
+ if not required:
+ continue
+ if key not in value:
+ return False
+ # unknown keys
+ for key in value.keys():
+ if key not in prototype:
+ return False
+ # incorrect types
+ for key, v in value.items():
+ prototypeType, required = prototype[key]
+ if v is None and not required:
+ continue
+ if not isinstance(v, prototypeType):
+ return False
+ return True
+
+# --------------
+# fontinfo.plist
+# --------------
+
+# Data Validators
+
+def fontInfoStyleMapStyleNameValidator(value):
+ """
+ Version 2+.
+ """
+ options = ["regular", "italic", "bold", "bold italic"]
+ return value in options
+
+def fontInfoOpenTypeGaspRangeRecordsValidator(value):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if len(value) == 0:
+ return True
+ validBehaviors = [0, 1, 2, 3]
+ dictPrototype = dict(rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True))
+ ppemOrder = []
+ for rangeRecord in value:
+ if not genericDictValidator(rangeRecord, dictPrototype):
+ return False
+ ppem = rangeRecord["rangeMaxPPEM"]
+ behavior = rangeRecord["rangeGaspBehavior"]
+ ppemValidity = genericNonNegativeIntValidator(ppem)
+ if not ppemValidity:
+ return False
+ behaviorValidity = genericIntListValidator(behavior, validBehaviors)
+ if not behaviorValidity:
+ return False
+ ppemOrder.append(ppem)
+ if ppemOrder != sorted(ppemOrder):
+ return False
+ return True
+
+def fontInfoOpenTypeHeadCreatedValidator(value):
+ """
+ Version 2+.
+ """
+ # format: 0000/00/00 00:00:00
+ if not isinstance(value, basestring):
+ return False
+ # basic formatting
+ if not len(value) == 19:
+ return False
+ if value.count(" ") != 1:
+ return False
+ date, time = value.split(" ")
+ if date.count("/") != 2:
+ return False
+ if time.count(":") != 2:
+ return False
+ # date
+ year, month, day = date.split("/")
+ if len(year) != 4:
+ return False
+ if len(month) != 2:
+ return False
+ if len(day) != 2:
+ return False
+ try:
+ year = int(year)
+ month = int(month)
+ day = int(day)
+ except ValueError:
+ return False
+ if month < 1 or month > 12:
+ return False
+ monthMaxDay = calendar.monthrange(year, month)[1]
+ if day < 1 or day > monthMaxDay:
+ return False
+ # time
+ hour, minute, second = time.split(":")
+ if len(hour) != 2:
+ return False
+ if len(minute) != 2:
+ return False
+ if len(second) != 2:
+ return False
+ try:
+ hour = int(hour)
+ minute = int(minute)
+ second = int(second)
+ except ValueError:
+ return False
+ if hour < 0 or hour > 23:
+ return False
+ if minute < 0 or minute > 59:
+ return False
+ if second < 0 or second > 59:
+ return False
+ # fallback
+ return True
+
+def fontInfoOpenTypeNameRecordsValidator(value):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ dictPrototype = dict(nameID=(int, True), platformID=(int, True), encodingID=(int, True), languageID=(int, True), string=(basestring, True))
+ for nameRecord in value:
+ if not genericDictValidator(nameRecord, dictPrototype):
+ return False
+ return True
+
+def fontInfoOpenTypeOS2WeightClassValidator(value):
+ """
+ Version 2+.
+ """
+ if not isinstance(value, integerTypes):
+ return False
+ if value < 0:
+ return False
+ return True
+
+def fontInfoOpenTypeOS2WidthClassValidator(value):
+ """
+ Version 2+.
+ """
+ if not isinstance(value, integerTypes):
+ return False
+ if value < 1:
+ return False
+ if value > 9:
+ return False
+ return True
+
+def fontInfoVersion2OpenTypeOS2PanoseValidator(values):
+ """
+ Version 2.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) != 10:
+ return False
+ for value in values:
+ if not isinstance(value, integerTypes):
+ return False
+ # XXX further validation?
+ return True
+
+def fontInfoVersion3OpenTypeOS2PanoseValidator(values):
+ """
+ Version 3+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) != 10:
+ return False
+ for value in values:
+ if not isinstance(value, integerTypes):
+ return False
+ if value < 0:
+ return False
+ # XXX further validation?
+ return True
+
+def fontInfoOpenTypeOS2FamilyClassValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) != 2:
+ return False
+ for value in values:
+ if not isinstance(value, integerTypes):
+ return False
+ classID, subclassID = values
+ if classID < 0 or classID > 14:
+ return False
+ if subclassID < 0 or subclassID > 15:
+ return False
+ return True
+
+def fontInfoPostscriptBluesValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) > 14:
+ return False
+ if len(values) % 2:
+ return False
+ for value in values:
+ if not isinstance(value, numberTypes):
+ return False
+ return True
+
+def fontInfoPostscriptOtherBluesValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) > 10:
+ return False
+ if len(values) % 2:
+ return False
+ for value in values:
+ if not isinstance(value, numberTypes):
+ return False
+ return True
+
+def fontInfoPostscriptStemsValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) > 12:
+ return False
+ for value in values:
+ if not isinstance(value, numberTypes):
+ return False
+ return True
+
+def fontInfoPostscriptWindowsCharacterSetValidator(value):
+ """
+ Version 2+.
+ """
+ validValues = list(range(1, 21))
+ if value not in validValues:
+ return False
+ return True
+
+def fontInfoWOFFMetadataUniqueIDValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(id=(basestring, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ return True
+
+def fontInfoWOFFMetadataVendorValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {"name" : (basestring, True), "url" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+def fontInfoWOFFMetadataCreditsValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(credits=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if not len(value["credits"]):
+ return False
+ dictPrototype = {"name" : (basestring, True), "url" : (basestring, False), "role" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
+ for credit in value["credits"]:
+ if not genericDictValidator(credit, dictPrototype):
+ return False
+ if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+def fontInfoWOFFMetadataDescriptionValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(url=(basestring, False), text=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+def fontInfoWOFFMetadataLicenseValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(url=(basestring, False), text=(list, False), id=(basestring, False))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "text" in value:
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+def fontInfoWOFFMetadataTrademarkValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(text=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+def fontInfoWOFFMetadataCopyrightValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(text=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+def fontInfoWOFFMetadataLicenseeValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {"name" : (basestring, True), "dir" : (basestring, False), "class" : (basestring, False)}
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+def fontInfoWOFFMetadataTextValue(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {"text" : (basestring, True), "language" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+def fontInfoWOFFMetadataExtensionsValidator(value):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if not value:
+ return False
+ for extension in value:
+ if not fontInfoWOFFMetadataExtensionValidator(extension):
+ return False
+ return True
+
+def fontInfoWOFFMetadataExtensionValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(names=(list, False), items=(list, True), id=(basestring, False))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "names" in value:
+ for name in value["names"]:
+ if not fontInfoWOFFMetadataExtensionNameValidator(name):
+ return False
+ for item in value["items"]:
+ if not fontInfoWOFFMetadataExtensionItemValidator(item):
+ return False
+ return True
+
+def fontInfoWOFFMetadataExtensionItemValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(id=(basestring, False), names=(list, True), values=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for name in value["names"]:
+ if not fontInfoWOFFMetadataExtensionNameValidator(name):
+ return False
+ for val in value["values"]:
+ if not fontInfoWOFFMetadataExtensionValueValidator(val):
+ return False
+ return True
+
+def fontInfoWOFFMetadataExtensionNameValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {"text" : (basestring, True), "language" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+def fontInfoWOFFMetadataExtensionValueValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {"text" : (basestring, True), "language" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+# ----------
+# Guidelines
+# ----------
+
+def guidelinesValidator(value, identifiers=None):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if identifiers is None:
+ identifiers = set()
+ for guide in value:
+ if not guidelineValidator(guide):
+ return False
+ identifier = guide.get("identifier")
+ if identifier is not None:
+ if identifier in identifiers:
+ return False
+ identifiers.add(identifier)
+ return True
+
+_guidelineDictPrototype = dict(
+ x=((int, float), False), y=((int, float), False), angle=((int, float), False),
+ name=(basestring, False), color=(basestring, False), identifier=(basestring, False)
+)
+
+def guidelineValidator(value):
+ """
+ Version 3+.
+ """
+ if not genericDictValidator(value, _guidelineDictPrototype):
+ return False
+ x = value.get("x")
+ y = value.get("y")
+ angle = value.get("angle")
+ # x or y must be present
+ if x is None and y is None:
+ return False
+ # if x or y are None, angle must not be present
+ if x is None or y is None:
+ if angle is not None:
+ return False
+ # if x and y are defined, angle must be defined
+ if x is not None and y is not None and angle is None:
+ return False
+ # angle must be between 0 and 360
+ if angle is not None:
+ if angle < 0:
+ return False
+ if angle > 360:
+ return False
+ # identifier must be 1 or more characters
+ identifier = value.get("identifier")
+ if identifier is not None and not identifierValidator(identifier):
+ return False
+ # color must follow the proper format
+ color = value.get("color")
+ if color is not None and not colorValidator(color):
+ return False
+ return True
+
+# -------
+# Anchors
+# -------
+
+def anchorsValidator(value, identifiers=None):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if identifiers is None:
+ identifiers = set()
+ for anchor in value:
+ if not anchorValidator(anchor):
+ return False
+ identifier = anchor.get("identifier")
+ if identifier is not None:
+ if identifier in identifiers:
+ return False
+ identifiers.add(identifier)
+ return True
+
+_anchorDictPrototype = dict(
+ x=((int, float), False), y=((int, float), False),
+ name=(basestring, False), color=(basestring, False),
+ identifier=(basestring, False)
+)
+
+def anchorValidator(value):
+ """
+ Version 3+.
+ """
+ if not genericDictValidator(value, _anchorDictPrototype):
+ return False
+ x = value.get("x")
+ y = value.get("y")
+ # x and y must be present
+ if x is None or y is None:
+ return False
+ # identifier must be 1 or more characters
+ identifier = value.get("identifier")
+ if identifier is not None and not identifierValidator(identifier):
+ return False
+ # color must follow the proper format
+ color = value.get("color")
+ if color is not None and not colorValidator(color):
+ return False
+ return True
+
+# ----------
+# Identifier
+# ----------
+
+def identifierValidator(value):
+ """
+ Version 3+.
+
+ >>> identifierValidator("a")
+ True
+ >>> identifierValidator("")
+ False
+ >>> identifierValidator("a" * 101)
+ False
+ """
+ validCharactersMin = 0x20
+ validCharactersMax = 0x7E
+ if not isinstance(value, basestring):
+ return False
+ if not value:
+ return False
+ if len(value) > 100:
+ return False
+ for c in value:
+ c = ord(c)
+ if c < validCharactersMin or c > validCharactersMax:
+ return False
+ return True
+
+# -----
+# Color
+# -----
+
+def colorValidator(value):
+ """
+ Version 3+.
+
+ >>> colorValidator("0,0,0,0")
+ True
+ >>> colorValidator(".5,.5,.5,.5")
+ True
+ >>> colorValidator("0.5,0.5,0.5,0.5")
+ True
+ >>> colorValidator("1,1,1,1")
+ True
+
+ >>> colorValidator("2,0,0,0")
+ False
+ >>> colorValidator("0,2,0,0")
+ False
+ >>> colorValidator("0,0,2,0")
+ False
+ >>> colorValidator("0,0,0,2")
+ False
+
+ >>> colorValidator("1r,1,1,1")
+ False
+ >>> colorValidator("1,1g,1,1")
+ False
+ >>> colorValidator("1,1,1b,1")
+ False
+ >>> colorValidator("1,1,1,1a")
+ False
+
+ >>> colorValidator("1 1 1 1")
+ False
+ >>> colorValidator("1 1,1,1")
+ False
+ >>> colorValidator("1,1 1,1")
+ False
+ >>> colorValidator("1,1,1 1")
+ False
+
+ >>> colorValidator("1, 1, 1, 1")
+ True
+ """
+ if not isinstance(value, basestring):
+ return False
+ parts = value.split(",")
+ if len(parts) != 4:
+ return False
+ for part in parts:
+ part = part.strip()
+ converted = False
+ try:
+ part = int(part)
+ converted = True
+ except ValueError:
+ pass
+ if not converted:
+ try:
+ part = float(part)
+ converted = True
+ except ValueError:
+ pass
+ if not converted:
+ return False
+ if part < 0:
+ return False
+ if part > 1:
+ return False
+ return True
+
+# -----
+# image
+# -----
+
+pngSignature = b"\x89PNG\r\n\x1a\n"
+
+_imageDictPrototype = dict(
+ fileName=(basestring, True),
+ xScale=((int, float), False), xyScale=((int, float), False),
+ yxScale=((int, float), False), yScale=((int, float), False),
+ xOffset=((int, float), False), yOffset=((int, float), False),
+ color=(basestring, False)
+)
+
+def imageValidator(value):
+ """
+ Version 3+.
+ """
+ if not genericDictValidator(value, _imageDictPrototype):
+ return False
+ # fileName must be one or more characters
+ if not value["fileName"]:
+ return False
+ # color must follow the proper format
+ color = value.get("color")
+ if color is not None and not colorValidator(color):
+ return False
+ return True
+
+def pngValidator(path=None, data=None, fileObj=None):
+ """
+ Version 3+.
+
+ This checks the signature of the image data.
+ """
+ assert path is not None or data is not None or fileObj is not None
+ if path is not None:
+ with open(path, "rb") as f:
+ signature = f.read(8)
+ elif data is not None:
+ signature = data[:8]
+ elif fileObj is not None:
+ pos = fileObj.tell()
+ signature = fileObj.read(8)
+ fileObj.seek(pos)
+ if signature != pngSignature:
+ return False, "Image does not begin with the PNG signature."
+ return True, None
+
+# -------------------
+# layercontents.plist
+# -------------------
+
+def layerContentsValidator(value, ufoPathOrFileSystem):
+ """
+ Check the validity of layercontents.plist.
+ Version 3+.
+ """
+ if isinstance(ufoPathOrFileSystem, fs.base.FS):
+ fileSystem = ufoPathOrFileSystem
+ else:
+ fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem)
+
+ bogusFileMessage = "layercontents.plist in not in the correct format."
+ # file isn't in the right format
+ if not isinstance(value, list):
+ return False, bogusFileMessage
+ # work through each entry
+ usedLayerNames = set()
+ usedDirectories = set()
+ contents = {}
+ for entry in value:
+ # layer entry in the incorrect format
+ if not isinstance(entry, list):
+ return False, bogusFileMessage
+ if not len(entry) == 2:
+ return False, bogusFileMessage
+ for i in entry:
+ if not isinstance(i, basestring):
+ return False, bogusFileMessage
+ layerName, directoryName = entry
+ # check directory naming
+ if directoryName != "glyphs":
+ if not directoryName.startswith("glyphs."):
+ return False, "Invalid directory name (%s) in layercontents.plist." % directoryName
+ if len(layerName) == 0:
+ return False, "Empty layer name in layercontents.plist."
+ # directory doesn't exist
+ if not fileSystem.exists(directoryName):
+ return False, "A glyphset does not exist at %s." % directoryName
+ # default layer name
+ if layerName == "public.default" and directoryName != "glyphs":
+ return False, "The name public.default is being used by a layer that is not the default."
+ # check usage
+ if layerName in usedLayerNames:
+ return False, "The layer name %s is used by more than one layer." % layerName
+ usedLayerNames.add(layerName)
+ if directoryName in usedDirectories:
+ return False, "The directory %s is used by more than one layer." % directoryName
+ usedDirectories.add(directoryName)
+ # store
+ contents[layerName] = directoryName
+ # missing default layer
+ foundDefault = "glyphs" in contents.values()
+ if not foundDefault:
+ return False, "The required default glyph set is not in the UFO."
+ return True, None
+
+# ------------
+# groups.plist
+# ------------
+
+def groupsValidator(value):
+ """
+ Check the validity of the groups.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> groups = {"A" : ["A", "A"], "A2" : ["A"]}
+ >>> groupsValidator(groups)
+ (True, None)
+
+ >>> groups = {"" : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ A group has an empty name.
+
+ >>> groups = {"public.awesome" : ["A"]}
+ >>> groupsValidator(groups)
+ (True, None)
+
+ >>> groups = {"public.kern1." : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ The group data contains a kerning group with an incomplete name.
+ >>> groups = {"public.kern2." : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ The group data contains a kerning group with an incomplete name.
+
+ >>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]}
+ >>> groupsValidator(groups)
+ (True, None)
+
+ >>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ The glyph "A" occurs in too many kerning groups.
+ """
+ bogusFormatMessage = "The group data is not in the correct format."
+ if not isDictEnough(value):
+ return False, bogusFormatMessage
+ firstSideMapping = {}
+ secondSideMapping = {}
+ for groupName, glyphList in value.items():
+ if not isinstance(groupName, (basestring)):
+ return False, bogusFormatMessage
+ if not isinstance(glyphList, (list, tuple)):
+ return False, bogusFormatMessage
+ if not groupName:
+ return False, "A group has an empty name."
+ if groupName.startswith("public."):
+ if not groupName.startswith("public.kern1.") and not groupName.startswith("public.kern2."):
+ # unknown pubic.* name. silently skip.
+ continue
+ else:
+ if len("public.kernN.") == len(groupName):
+ return False, "The group data contains a kerning group with an incomplete name."
+ if groupName.startswith("public.kern1."):
+ d = firstSideMapping
+ else:
+ d = secondSideMapping
+ for glyphName in glyphList:
+ if not isinstance(glyphName, basestring):
+ return False, "The group data %s contains an invalid member." % groupName
+ if glyphName in d:
+ return False, "The glyph \"%s\" occurs in too many kerning groups." % glyphName
+ d[glyphName] = groupName
+ return True, None
+
+# -------------
+# kerning.plist
+# -------------
+
+def kerningValidator(data):
+ """
+ Check the validity of the kerning data structure.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> kerning = {"A" : {"B" : 100}}
+ >>> kerningValidator(kerning)
+ (True, None)
+
+ >>> kerning = {"A" : ["B"]}
+ >>> valid, msg = kerningValidator(kerning)
+ >>> valid
+ False
+ >>> print(msg)
+ The kerning data is not in the correct format.
+
+ >>> kerning = {"A" : {"B" : "100"}}
+ >>> valid, msg = kerningValidator(kerning)
+ >>> valid
+ False
+ >>> print(msg)
+ The kerning data is not in the correct format.
+ """
+ bogusFormatMessage = "The kerning data is not in the correct format."
+ if not isinstance(data, Mapping):
+ return False, bogusFormatMessage
+ for first, secondDict in data.items():
+ if not isinstance(first, basestring):
+ return False, bogusFormatMessage
+ elif not isinstance(secondDict, Mapping):
+ return False, bogusFormatMessage
+ for second, value in secondDict.items():
+ if not isinstance(second, basestring):
+ return False, bogusFormatMessage
+ elif not isinstance(value, numberTypes):
+ return False, bogusFormatMessage
+ return True, None
+
+# -------------
+# lib.plist/lib
+# -------------
+
+_bogusLibFormatMessage = "The lib data is not in the correct format: %s"
+
+def fontLibValidator(value):
+ """
+ Check the validity of the lib.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> lib = {"foo" : "bar"}
+ >>> fontLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.awesome" : "hello"}
+ >>> fontLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.glyphOrder" : ["A", "C", "B"]}
+ >>> fontLibValidator(lib)
+ (True, None)
+
+ >>> lib = "hello"
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg) # doctest: +ELLIPSIS
+ The lib data is not in the correct format: expected a dictionary, ...
+
+ >>> lib = {1: "hello"}
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg)
+ The lib key is not properly formatted: expected basestring, found int: 1
+
+ >>> lib = {"public.glyphOrder" : "hello"}
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg) # doctest: +ELLIPSIS
+ public.glyphOrder is not properly formatted: expected list or tuple,...
+
+ >>> lib = {"public.glyphOrder" : ["A", 1, "B"]}
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg) # doctest: +ELLIPSIS
+ public.glyphOrder is not properly formatted: expected basestring,...
+ """
+ if not isDictEnough(value):
+ reason = "expected a dictionary, found %s" % type(value).__name__
+ return False, _bogusLibFormatMessage % reason
+ for key, value in value.items():
+ if not isinstance(key, basestring):
+ return False, (
+ "The lib key is not properly formatted: expected basestring, found %s: %r" %
+ (type(key).__name__, key))
+ # public.glyphOrder
+ if key == "public.glyphOrder":
+ bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s"
+ if not isinstance(value, (list, tuple)):
+ reason = "expected list or tuple, found %s" % type(value).__name__
+ return False, bogusGlyphOrderMessage % reason
+ for glyphName in value:
+ if not isinstance(glyphName, basestring):
+ reason = "expected basestring, found %s" % type(glyphName).__name__
+ return False, bogusGlyphOrderMessage % reason
+ return True, None
+
+# --------
+# GLIF lib
+# --------
+
+def glyphLibValidator(value):
+ """
+ Check the validity of the lib.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> lib = {"foo" : "bar"}
+ >>> glyphLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.awesome" : "hello"}
+ >>> glyphLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.markColor" : "1,0,0,0.5"}
+ >>> glyphLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.markColor" : 1}
+ >>> valid, msg = glyphLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg)
+ public.markColor is not properly formatted.
+ """
+ if not isDictEnough(value):
+ reason = "expected a dictionary, found %s" % type(value).__name__
+ return False, _bogusLibFormatMessage % reason
+ for key, value in value.items():
+ if not isinstance(key, basestring):
+ reason = "key (%s) should be a string" % key
+ return False, _bogusLibFormatMessage % reason
+ # public.markColor
+ if key == "public.markColor":
+ if not colorValidator(value):
+ return False, "public.markColor is not properly formatted."
+ return True, None
+
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/Lib/fontTools/unicodedata/Blocks.py b/Lib/fontTools/unicodedata/Blocks.py
index cba4e9e9..692fca89 100644
--- a/Lib/fontTools/unicodedata/Blocks.py
+++ b/Lib/fontTools/unicodedata/Blocks.py
@@ -4,8 +4,8 @@
# Source: https://unicode.org/Public/UNIDATA/Blocks.txt
# License: http://unicode.org/copyright.html#License
#
-# Blocks-10.0.0.txt
-# Date: 2017-04-12, 17:30:00 GMT [KW]
+# Blocks-11.0.0.txt
+# Date: 2017-10-16, 24:39:00 GMT [KW]
# © 2017 Unicode®, Inc.
# For terms of use, see http://www.unicode.org/terms_of_use.html
#
@@ -81,7 +81,7 @@ RANGES = [
0x1C00, # .. 0x1C4F ; Lepcha
0x1C50, # .. 0x1C7F ; Ol Chiki
0x1C80, # .. 0x1C8F ; Cyrillic Extended-C
- 0x1C90, # .. 0x1CBF ; No_Block
+ 0x1C90, # .. 0x1CBF ; Georgian Extended
0x1CC0, # .. 0x1CCF ; Sundanese Supplement
0x1CD0, # .. 0x1CFF ; Vedic Extensions
0x1D00, # .. 0x1D7F ; Phonetic Extensions
@@ -231,9 +231,13 @@ RANGES = [
0x10C00, # .. 0x10C4F ; Old Turkic
0x10C50, # .. 0x10C7F ; No_Block
0x10C80, # .. 0x10CFF ; Old Hungarian
- 0x10D00, # .. 0x10E5F ; No_Block
+ 0x10D00, # .. 0x10D3F ; Hanifi Rohingya
+ 0x10D40, # .. 0x10E5F ; No_Block
0x10E60, # .. 0x10E7F ; Rumi Numeral Symbols
- 0x10E80, # .. 0x10FFF ; No_Block
+ 0x10E80, # .. 0x10EFF ; No_Block
+ 0x10F00, # .. 0x10F2F ; Old Sogdian
+ 0x10F30, # .. 0x10F6F ; Sogdian
+ 0x10F70, # .. 0x10FFF ; No_Block
0x11000, # .. 0x1107F ; Brahmi
0x11080, # .. 0x110CF ; Kaithi
0x110D0, # .. 0x110FF ; Sora Sompeng
@@ -256,7 +260,9 @@ RANGES = [
0x11680, # .. 0x116CF ; Takri
0x116D0, # .. 0x116FF ; No_Block
0x11700, # .. 0x1173F ; Ahom
- 0x11740, # .. 0x1189F ; No_Block
+ 0x11740, # .. 0x117FF ; No_Block
+ 0x11800, # .. 0x1184F ; Dogra
+ 0x11850, # .. 0x1189F ; No_Block
0x118A0, # .. 0x118FF ; Warang Citi
0x11900, # .. 0x119FF ; No_Block
0x11A00, # .. 0x11A4F ; Zanabazar Square
@@ -268,7 +274,10 @@ RANGES = [
0x11C70, # .. 0x11CBF ; Marchen
0x11CC0, # .. 0x11CFF ; No_Block
0x11D00, # .. 0x11D5F ; Masaram Gondi
- 0x11D60, # .. 0x11FFF ; No_Block
+ 0x11D60, # .. 0x11DAF ; Gunjala Gondi
+ 0x11DB0, # .. 0x11EDF ; No_Block
+ 0x11EE0, # .. 0x11EFF ; Makasar
+ 0x11F00, # .. 0x11FFF ; No_Block
0x12000, # .. 0x123FF ; Cuneiform
0x12400, # .. 0x1247F ; Cuneiform Numbers and Punctuation
0x12480, # .. 0x1254F ; Early Dynastic Cuneiform
@@ -282,7 +291,9 @@ RANGES = [
0x16A70, # .. 0x16ACF ; No_Block
0x16AD0, # .. 0x16AFF ; Bassa Vah
0x16B00, # .. 0x16B8F ; Pahawh Hmong
- 0x16B90, # .. 0x16EFF ; No_Block
+ 0x16B90, # .. 0x16E3F ; No_Block
+ 0x16E40, # .. 0x16E9F ; Medefaidrin
+ 0x16EA0, # .. 0x16EFF ; No_Block
0x16F00, # .. 0x16F9F ; Miao
0x16FA0, # .. 0x16FDF ; No_Block
0x16FE0, # .. 0x16FFF ; Ideographic Symbols and Punctuation
@@ -300,7 +311,8 @@ RANGES = [
0x1D000, # .. 0x1D0FF ; Byzantine Musical Symbols
0x1D100, # .. 0x1D1FF ; Musical Symbols
0x1D200, # .. 0x1D24F ; Ancient Greek Musical Notation
- 0x1D250, # .. 0x1D2FF ; No_Block
+ 0x1D250, # .. 0x1D2DF ; No_Block
+ 0x1D2E0, # .. 0x1D2FF ; Mayan Numerals
0x1D300, # .. 0x1D35F ; Tai Xuan Jing Symbols
0x1D360, # .. 0x1D37F ; Counting Rod Numerals
0x1D380, # .. 0x1D3FF ; No_Block
@@ -312,7 +324,9 @@ RANGES = [
0x1E800, # .. 0x1E8DF ; Mende Kikakui
0x1E8E0, # .. 0x1E8FF ; No_Block
0x1E900, # .. 0x1E95F ; Adlam
- 0x1E960, # .. 0x1EDFF ; No_Block
+ 0x1E960, # .. 0x1EC6F ; No_Block
+ 0x1EC70, # .. 0x1ECBF ; Indic Siyaq Numbers
+ 0x1ECC0, # .. 0x1EDFF ; No_Block
0x1EE00, # .. 0x1EEFF ; Arabic Mathematical Alphabetic Symbols
0x1EF00, # .. 0x1EFFF ; No_Block
0x1F000, # .. 0x1F02F ; Mahjong Tiles
@@ -328,7 +342,8 @@ RANGES = [
0x1F780, # .. 0x1F7FF ; Geometric Shapes Extended
0x1F800, # .. 0x1F8FF ; Supplemental Arrows-C
0x1F900, # .. 0x1F9FF ; Supplemental Symbols and Pictographs
- 0x1FA00, # .. 0x1FFFF ; No_Block
+ 0x1FA00, # .. 0x1FA6F ; Chess Symbols
+ 0x1FA70, # .. 0x1FFFF ; No_Block
0x20000, # .. 0x2A6DF ; CJK Unified Ideographs Extension B
0x2A6E0, # .. 0x2A6FF ; No_Block
0x2A700, # .. 0x2B73F ; CJK Unified Ideographs Extension C
@@ -411,7 +426,7 @@ VALUES = [
'Lepcha', # 1C00..1C4F
'Ol Chiki', # 1C50..1C7F
'Cyrillic Extended-C', # 1C80..1C8F
- 'No_Block', # 1C90..1CBF
+ 'Georgian Extended', # 1C90..1CBF
'Sundanese Supplement', # 1CC0..1CCF
'Vedic Extensions', # 1CD0..1CFF
'Phonetic Extensions', # 1D00..1D7F
@@ -561,9 +576,13 @@ VALUES = [
'Old Turkic', # 10C00..10C4F
'No_Block', # 10C50..10C7F
'Old Hungarian', # 10C80..10CFF
- 'No_Block', # 10D00..10E5F
+ 'Hanifi Rohingya', # 10D00..10D3F
+ 'No_Block', # 10D40..10E5F
'Rumi Numeral Symbols', # 10E60..10E7F
- 'No_Block', # 10E80..10FFF
+ 'No_Block', # 10E80..10EFF
+ 'Old Sogdian', # 10F00..10F2F
+ 'Sogdian', # 10F30..10F6F
+ 'No_Block', # 10F70..10FFF
'Brahmi', # 11000..1107F
'Kaithi', # 11080..110CF
'Sora Sompeng', # 110D0..110FF
@@ -586,7 +605,9 @@ VALUES = [
'Takri', # 11680..116CF
'No_Block', # 116D0..116FF
'Ahom', # 11700..1173F
- 'No_Block', # 11740..1189F
+ 'No_Block', # 11740..117FF
+ 'Dogra', # 11800..1184F
+ 'No_Block', # 11850..1189F
'Warang Citi', # 118A0..118FF
'No_Block', # 11900..119FF
'Zanabazar Square', # 11A00..11A4F
@@ -598,7 +619,10 @@ VALUES = [
'Marchen', # 11C70..11CBF
'No_Block', # 11CC0..11CFF
'Masaram Gondi', # 11D00..11D5F
- 'No_Block', # 11D60..11FFF
+ 'Gunjala Gondi', # 11D60..11DAF
+ 'No_Block', # 11DB0..11EDF
+ 'Makasar', # 11EE0..11EFF
+ 'No_Block', # 11F00..11FFF
'Cuneiform', # 12000..123FF
'Cuneiform Numbers and Punctuation', # 12400..1247F
'Early Dynastic Cuneiform', # 12480..1254F
@@ -612,7 +636,9 @@ VALUES = [
'No_Block', # 16A70..16ACF
'Bassa Vah', # 16AD0..16AFF
'Pahawh Hmong', # 16B00..16B8F
- 'No_Block', # 16B90..16EFF
+ 'No_Block', # 16B90..16E3F
+ 'Medefaidrin', # 16E40..16E9F
+ 'No_Block', # 16EA0..16EFF
'Miao', # 16F00..16F9F
'No_Block', # 16FA0..16FDF
'Ideographic Symbols and Punctuation', # 16FE0..16FFF
@@ -630,7 +656,8 @@ VALUES = [
'Byzantine Musical Symbols', # 1D000..1D0FF
'Musical Symbols', # 1D100..1D1FF
'Ancient Greek Musical Notation', # 1D200..1D24F
- 'No_Block', # 1D250..1D2FF
+ 'No_Block', # 1D250..1D2DF
+ 'Mayan Numerals', # 1D2E0..1D2FF
'Tai Xuan Jing Symbols', # 1D300..1D35F
'Counting Rod Numerals', # 1D360..1D37F
'No_Block', # 1D380..1D3FF
@@ -642,7 +669,9 @@ VALUES = [
'Mende Kikakui', # 1E800..1E8DF
'No_Block', # 1E8E0..1E8FF
'Adlam', # 1E900..1E95F
- 'No_Block', # 1E960..1EDFF
+ 'No_Block', # 1E960..1EC6F
+ 'Indic Siyaq Numbers', # 1EC70..1ECBF
+ 'No_Block', # 1ECC0..1EDFF
'Arabic Mathematical Alphabetic Symbols', # 1EE00..1EEFF
'No_Block', # 1EF00..1EFFF
'Mahjong Tiles', # 1F000..1F02F
@@ -658,7 +687,8 @@ VALUES = [
'Geometric Shapes Extended', # 1F780..1F7FF
'Supplemental Arrows-C', # 1F800..1F8FF
'Supplemental Symbols and Pictographs', # 1F900..1F9FF
- 'No_Block', # 1FA00..1FFFF
+ 'Chess Symbols', # 1FA00..1FA6F
+ 'No_Block', # 1FA70..1FFFF
'CJK Unified Ideographs Extension B', # 20000..2A6DF
'No_Block', # 2A6E0..2A6FF
'CJK Unified Ideographs Extension C', # 2A700..2B73F
diff --git a/Lib/fontTools/unicodedata/ScriptExtensions.py b/Lib/fontTools/unicodedata/ScriptExtensions.py
index a92cc80c..bfcdbec3 100644
--- a/Lib/fontTools/unicodedata/ScriptExtensions.py
+++ b/Lib/fontTools/unicodedata/ScriptExtensions.py
@@ -4,9 +4,9 @@
# Source: https://unicode.org/Public/UNIDATA/ScriptExtensions.txt
# License: http://unicode.org/copyright.html#License
#
-# ScriptExtensions-10.0.0.txt
-# Date: 2017-05-31, 01:07:00 GMT [RP]
-# © 2017 Unicode®, Inc.
+# ScriptExtensions-11.0.0.txt
+# Date: 2018-02-04, 20:04:00 GMT
+# © 2018 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use, see http://www.unicode.org/terms_of_use.html
#
@@ -55,39 +55,38 @@ RANGES = [
0x0488, # .. 0x0588 ; None
0x0589, # .. 0x0589 ; {'Armn', 'Geor'}
0x058A, # .. 0x060B ; None
- 0x060C, # .. 0x060C ; {'Arab', 'Syrc', 'Thaa'}
+ 0x060C, # .. 0x060C ; {'Arab', 'Rohg', 'Syrc', 'Thaa'}
0x060D, # .. 0x061A ; None
- 0x061B, # .. 0x061C ; {'Arab', 'Syrc', 'Thaa'}
+ 0x061B, # .. 0x061B ; {'Arab', 'Rohg', 'Syrc', 'Thaa'}
+ 0x061C, # .. 0x061C ; {'Arab', 'Syrc', 'Thaa'}
0x061D, # .. 0x061E ; None
- 0x061F, # .. 0x061F ; {'Arab', 'Syrc', 'Thaa'}
+ 0x061F, # .. 0x061F ; {'Arab', 'Rohg', 'Syrc', 'Thaa'}
0x0620, # .. 0x063F ; None
- 0x0640, # .. 0x0640 ; {'Adlm', 'Arab', 'Mand', 'Mani', 'Phlp', 'Syrc'}
+ 0x0640, # .. 0x0640 ; {'Adlm', 'Arab', 'Mand', 'Mani', 'Phlp', 'Rohg', 'Sogd', 'Syrc'}
0x0641, # .. 0x064A ; None
0x064B, # .. 0x0655 ; {'Arab', 'Syrc'}
0x0656, # .. 0x065F ; None
0x0660, # .. 0x0669 ; {'Arab', 'Thaa'}
0x066A, # .. 0x066F ; None
0x0670, # .. 0x0670 ; {'Arab', 'Syrc'}
- 0x0671, # .. 0x0950 ; None
- 0x0951, # .. 0x0951 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Shrd', 'Taml', 'Telu'}
- 0x0952, # .. 0x0952 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Taml', 'Telu'}
+ 0x0671, # .. 0x06D3 ; None
+ 0x06D4, # .. 0x06D4 ; {'Arab', 'Rohg'}
+ 0x06D5, # .. 0x0950 ; None
+ 0x0951, # .. 0x0951 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Shrd', 'Taml', 'Telu', 'Tirh'}
+ 0x0952, # .. 0x0952 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Taml', 'Telu', 'Tirh'}
0x0953, # .. 0x0963 ; None
- 0x0964, # .. 0x0964 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}
- 0x0965, # .. 0x0965 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Limb', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}
- 0x0966, # .. 0x096F ; {'Deva', 'Kthi', 'Mahj'}
+ 0x0964, # .. 0x0964 ; {'Beng', 'Deva', 'Dogr', 'Gong', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}
+ 0x0965, # .. 0x0965 ; {'Beng', 'Deva', 'Dogr', 'Gong', 'Gran', 'Gujr', 'Guru', 'Knda', 'Limb', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}
+ 0x0966, # .. 0x096F ; {'Deva', 'Dogr', 'Kthi', 'Mahj'}
0x0970, # .. 0x09E5 ; None
0x09E6, # .. 0x09EF ; {'Beng', 'Cakm', 'Sylo'}
0x09F0, # .. 0x0A65 ; None
0x0A66, # .. 0x0A6F ; {'Guru', 'Mult'}
0x0A70, # .. 0x0AE5 ; None
0x0AE6, # .. 0x0AEF ; {'Gujr', 'Khoj'}
- 0x0AF0, # .. 0x0BA9 ; None
- 0x0BAA, # .. 0x0BAA ; {'Gran', 'Taml'}
- 0x0BAB, # .. 0x0BB4 ; None
- 0x0BB5, # .. 0x0BB5 ; {'Gran', 'Taml'}
- 0x0BB6, # .. 0x0BE5 ; None
- 0x0BE6, # .. 0x0BF2 ; {'Gran', 'Taml'}
- 0x0BF3, # .. 0x103F ; None
+ 0x0AF0, # .. 0x0BE5 ; None
+ 0x0BE6, # .. 0x0BF3 ; {'Gran', 'Taml'}
+ 0x0BF4, # .. 0x103F ; None
0x1040, # .. 0x1049 ; {'Cakm', 'Mymr', 'Tale'}
0x104A, # .. 0x10FA ; None
0x10FB, # .. 0x10FB ; {'Geor', 'Latn'}
@@ -98,22 +97,29 @@ RANGES = [
0x1804, # .. 0x1804 ; None
0x1805, # .. 0x1805 ; {'Mong', 'Phag'}
0x1806, # .. 0x1CCF ; None
- 0x1CD0, # .. 0x1CD0 ; {'Deva', 'Gran'}
+ 0x1CD0, # .. 0x1CD0 ; {'Beng', 'Deva', 'Gran', 'Knda'}
0x1CD1, # .. 0x1CD1 ; {'Deva'}
- 0x1CD2, # .. 0x1CD3 ; {'Deva', 'Gran'}
- 0x1CD4, # .. 0x1CD6 ; {'Deva'}
+ 0x1CD2, # .. 0x1CD2 ; {'Beng', 'Deva', 'Gran', 'Knda'}
+ 0x1CD3, # .. 0x1CD3 ; {'Deva', 'Gran'}
+ 0x1CD4, # .. 0x1CD4 ; {'Deva'}
+ 0x1CD5, # .. 0x1CD6 ; {'Beng', 'Deva'}
0x1CD7, # .. 0x1CD7 ; {'Deva', 'Shrd'}
- 0x1CD8, # .. 0x1CD8 ; {'Deva'}
+ 0x1CD8, # .. 0x1CD8 ; {'Beng', 'Deva'}
0x1CD9, # .. 0x1CD9 ; {'Deva', 'Shrd'}
- 0x1CDA, # .. 0x1CDA ; {'Deva', 'Knda', 'Mlym', 'Taml', 'Telu'}
+ 0x1CDA, # .. 0x1CDA ; {'Deva', 'Knda', 'Mlym', 'Orya', 'Taml', 'Telu'}
0x1CDB, # .. 0x1CDB ; {'Deva'}
0x1CDC, # .. 0x1CDD ; {'Deva', 'Shrd'}
0x1CDE, # .. 0x1CDF ; {'Deva'}
0x1CE0, # .. 0x1CE0 ; {'Deva', 'Shrd'}
- 0x1CE1, # .. 0x1CF1 ; {'Deva'}
- 0x1CF2, # .. 0x1CF4 ; {'Deva', 'Gran'}
- 0x1CF5, # .. 0x1CF5 ; {'Deva', 'Knda'}
- 0x1CF6, # .. 0x1CF6 ; {'Deva'}
+ 0x1CE1, # .. 0x1CE1 ; {'Beng', 'Deva'}
+ 0x1CE2, # .. 0x1CE9 ; {'Deva'}
+ 0x1CEA, # .. 0x1CEA ; {'Beng', 'Deva'}
+ 0x1CEB, # .. 0x1CEC ; {'Deva'}
+ 0x1CED, # .. 0x1CED ; {'Beng', 'Deva'}
+ 0x1CEE, # .. 0x1CF1 ; {'Deva'}
+ 0x1CF2, # .. 0x1CF3 ; {'Deva', 'Gran'}
+ 0x1CF4, # .. 0x1CF4 ; {'Deva', 'Gran', 'Knda'}
+ 0x1CF5, # .. 0x1CF6 ; {'Beng', 'Deva'}
0x1CF7, # .. 0x1CF7 ; {'Beng'}
0x1CF8, # .. 0x1CF9 ; {'Deva', 'Gran'}
0x1CFA, # .. 0x1DBF ; None
@@ -169,8 +175,9 @@ RANGES = [
0x33FF, # .. 0xA66E ; None
0xA66F, # .. 0xA66F ; {'Cyrl', 'Glag'}
0xA670, # .. 0xA82F ; None
- 0xA830, # .. 0xA835 ; {'Deva', 'Gujr', 'Guru', 'Knda', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}
- 0xA836, # .. 0xA839 ; {'Deva', 'Gujr', 'Guru', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}
+ 0xA830, # .. 0xA832 ; {'Deva', 'Dogr', 'Gujr', 'Guru', 'Khoj', 'Knda', 'Kthi', 'Mahj', 'Mlym', 'Modi', 'Sind', 'Takr', 'Tirh'}
+ 0xA833, # .. 0xA835 ; {'Deva', 'Dogr', 'Gujr', 'Guru', 'Khoj', 'Knda', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}
+ 0xA836, # .. 0xA839 ; {'Deva', 'Dogr', 'Gujr', 'Guru', 'Khoj', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}
0xA83A, # .. 0xA8F0 ; None
0xA8F1, # .. 0xA8F1 ; {'Beng', 'Deva'}
0xA8F2, # .. 0xA8F2 ; None
@@ -203,8 +210,8 @@ RANGES = [
0x11301, # .. 0x11301 ; {'Gran', 'Taml'}
0x11302, # .. 0x11302 ; None
0x11303, # .. 0x11303 ; {'Gran', 'Taml'}
- 0x11304, # .. 0x1133B ; None
- 0x1133C, # .. 0x1133C ; {'Gran', 'Taml'}
+ 0x11304, # .. 0x1133A ; None
+ 0x1133B, # .. 0x1133C ; {'Gran', 'Taml'}
0x1133D, # .. 0x1BC9F ; None
0x1BCA0, # .. 0x1BCA3 ; {'Dupl'}
0x1BCA4, # .. 0x1D35F ; None
@@ -229,39 +236,38 @@ VALUES = [
None, # 0488..0588
{'Armn', 'Geor'}, # 0589..0589
None, # 058A..060B
- {'Arab', 'Syrc', 'Thaa'}, # 060C..060C
+ {'Arab', 'Rohg', 'Syrc', 'Thaa'}, # 060C..060C
None, # 060D..061A
- {'Arab', 'Syrc', 'Thaa'}, # 061B..061C
+ {'Arab', 'Rohg', 'Syrc', 'Thaa'}, # 061B..061B
+ {'Arab', 'Syrc', 'Thaa'}, # 061C..061C
None, # 061D..061E
- {'Arab', 'Syrc', 'Thaa'}, # 061F..061F
+ {'Arab', 'Rohg', 'Syrc', 'Thaa'}, # 061F..061F
None, # 0620..063F
- {'Adlm', 'Arab', 'Mand', 'Mani', 'Phlp', 'Syrc'}, # 0640..0640
+ {'Adlm', 'Arab', 'Mand', 'Mani', 'Phlp', 'Rohg', 'Sogd', 'Syrc'}, # 0640..0640
None, # 0641..064A
{'Arab', 'Syrc'}, # 064B..0655
None, # 0656..065F
{'Arab', 'Thaa'}, # 0660..0669
None, # 066A..066F
{'Arab', 'Syrc'}, # 0670..0670
- None, # 0671..0950
- {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Shrd', 'Taml', 'Telu'}, # 0951..0951
- {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Taml', 'Telu'}, # 0952..0952
+ None, # 0671..06D3
+ {'Arab', 'Rohg'}, # 06D4..06D4
+ None, # 06D5..0950
+ {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Shrd', 'Taml', 'Telu', 'Tirh'}, # 0951..0951
+ {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Taml', 'Telu', 'Tirh'}, # 0952..0952
None, # 0953..0963
- {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}, # 0964..0964
- {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Limb', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}, # 0965..0965
- {'Deva', 'Kthi', 'Mahj'}, # 0966..096F
+ {'Beng', 'Deva', 'Dogr', 'Gong', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}, # 0964..0964
+ {'Beng', 'Deva', 'Dogr', 'Gong', 'Gran', 'Gujr', 'Guru', 'Knda', 'Limb', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}, # 0965..0965
+ {'Deva', 'Dogr', 'Kthi', 'Mahj'}, # 0966..096F
None, # 0970..09E5
{'Beng', 'Cakm', 'Sylo'}, # 09E6..09EF
None, # 09F0..0A65
{'Guru', 'Mult'}, # 0A66..0A6F
None, # 0A70..0AE5
{'Gujr', 'Khoj'}, # 0AE6..0AEF
- None, # 0AF0..0BA9
- {'Gran', 'Taml'}, # 0BAA..0BAA
- None, # 0BAB..0BB4
- {'Gran', 'Taml'}, # 0BB5..0BB5
- None, # 0BB6..0BE5
- {'Gran', 'Taml'}, # 0BE6..0BF2
- None, # 0BF3..103F
+ None, # 0AF0..0BE5
+ {'Gran', 'Taml'}, # 0BE6..0BF3
+ None, # 0BF4..103F
{'Cakm', 'Mymr', 'Tale'}, # 1040..1049
None, # 104A..10FA
{'Geor', 'Latn'}, # 10FB..10FB
@@ -272,22 +278,29 @@ VALUES = [
None, # 1804..1804
{'Mong', 'Phag'}, # 1805..1805
None, # 1806..1CCF
- {'Deva', 'Gran'}, # 1CD0..1CD0
+ {'Beng', 'Deva', 'Gran', 'Knda'}, # 1CD0..1CD0
{'Deva'}, # 1CD1..1CD1
- {'Deva', 'Gran'}, # 1CD2..1CD3
- {'Deva'}, # 1CD4..1CD6
+ {'Beng', 'Deva', 'Gran', 'Knda'}, # 1CD2..1CD2
+ {'Deva', 'Gran'}, # 1CD3..1CD3
+ {'Deva'}, # 1CD4..1CD4
+ {'Beng', 'Deva'}, # 1CD5..1CD6
{'Deva', 'Shrd'}, # 1CD7..1CD7
- {'Deva'}, # 1CD8..1CD8
+ {'Beng', 'Deva'}, # 1CD8..1CD8
{'Deva', 'Shrd'}, # 1CD9..1CD9
- {'Deva', 'Knda', 'Mlym', 'Taml', 'Telu'}, # 1CDA..1CDA
+ {'Deva', 'Knda', 'Mlym', 'Orya', 'Taml', 'Telu'}, # 1CDA..1CDA
{'Deva'}, # 1CDB..1CDB
{'Deva', 'Shrd'}, # 1CDC..1CDD
{'Deva'}, # 1CDE..1CDF
{'Deva', 'Shrd'}, # 1CE0..1CE0
- {'Deva'}, # 1CE1..1CF1
- {'Deva', 'Gran'}, # 1CF2..1CF4
- {'Deva', 'Knda'}, # 1CF5..1CF5
- {'Deva'}, # 1CF6..1CF6
+ {'Beng', 'Deva'}, # 1CE1..1CE1
+ {'Deva'}, # 1CE2..1CE9
+ {'Beng', 'Deva'}, # 1CEA..1CEA
+ {'Deva'}, # 1CEB..1CEC
+ {'Beng', 'Deva'}, # 1CED..1CED
+ {'Deva'}, # 1CEE..1CF1
+ {'Deva', 'Gran'}, # 1CF2..1CF3
+ {'Deva', 'Gran', 'Knda'}, # 1CF4..1CF4
+ {'Beng', 'Deva'}, # 1CF5..1CF6
{'Beng'}, # 1CF7..1CF7
{'Deva', 'Gran'}, # 1CF8..1CF9
None, # 1CFA..1DBF
@@ -343,8 +356,9 @@ VALUES = [
None, # 33FF..A66E
{'Cyrl', 'Glag'}, # A66F..A66F
None, # A670..A82F
- {'Deva', 'Gujr', 'Guru', 'Knda', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A830..A835
- {'Deva', 'Gujr', 'Guru', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A836..A839
+ {'Deva', 'Dogr', 'Gujr', 'Guru', 'Khoj', 'Knda', 'Kthi', 'Mahj', 'Mlym', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A830..A832
+ {'Deva', 'Dogr', 'Gujr', 'Guru', 'Khoj', 'Knda', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A833..A835
+ {'Deva', 'Dogr', 'Gujr', 'Guru', 'Khoj', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A836..A839
None, # A83A..A8F0
{'Beng', 'Deva'}, # A8F1..A8F1
None, # A8F2..A8F2
@@ -377,8 +391,8 @@ VALUES = [
{'Gran', 'Taml'}, # 11301..11301
None, # 11302..11302
{'Gran', 'Taml'}, # 11303..11303
- None, # 11304..1133B
- {'Gran', 'Taml'}, # 1133C..1133C
+ None, # 11304..1133A
+ {'Gran', 'Taml'}, # 1133B..1133C
None, # 1133D..1BC9F
{'Dupl'}, # 1BCA0..1BCA3
None, # 1BCA4..1D35F
diff --git a/Lib/fontTools/unicodedata/Scripts.py b/Lib/fontTools/unicodedata/Scripts.py
index f39b430f..30cd8f5e 100644
--- a/Lib/fontTools/unicodedata/Scripts.py
+++ b/Lib/fontTools/unicodedata/Scripts.py
@@ -4,9 +4,9 @@
# Source: https://unicode.org/Public/UNIDATA/Scripts.txt
# License: http://unicode.org/copyright.html#License
#
-# Scripts-10.0.0.txt
-# Date: 2017-03-11, 06:40:37 GMT
-# © 2017 Unicode®, Inc.
+# Scripts-11.0.0.txt
+# Date: 2018-02-21, 05:34:31 GMT
+# © 2018 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use, see http://www.unicode.org/terms_of_use.html
#
@@ -68,10 +68,7 @@ RANGES = [
0x0530, # .. 0x0530 ; Unknown
0x0531, # .. 0x0556 ; Armenian
0x0557, # .. 0x0558 ; Unknown
- 0x0559, # .. 0x055F ; Armenian
- 0x0560, # .. 0x0560 ; Unknown
- 0x0561, # .. 0x0587 ; Armenian
- 0x0588, # .. 0x0588 ; Unknown
+ 0x0559, # .. 0x0588 ; Armenian
0x0589, # .. 0x0589 ; Common
0x058A, # .. 0x058A ; Armenian
0x058B, # .. 0x058C ; Unknown
@@ -80,8 +77,8 @@ RANGES = [
0x0591, # .. 0x05C7 ; Hebrew
0x05C8, # .. 0x05CF ; Unknown
0x05D0, # .. 0x05EA ; Hebrew
- 0x05EB, # .. 0x05EF ; Unknown
- 0x05F0, # .. 0x05F4 ; Hebrew
+ 0x05EB, # .. 0x05EE ; Unknown
+ 0x05EF, # .. 0x05F4 ; Hebrew
0x05F5, # .. 0x05FF ; Unknown
0x0600, # .. 0x0604 ; Arabic
0x0605, # .. 0x0605 ; Common
@@ -111,7 +108,8 @@ RANGES = [
0x0780, # .. 0x07B1 ; Thaana
0x07B2, # .. 0x07BF ; Unknown
0x07C0, # .. 0x07FA ; Nko
- 0x07FB, # .. 0x07FF ; Unknown
+ 0x07FB, # .. 0x07FC ; Unknown
+ 0x07FD, # .. 0x07FF ; Nko
0x0800, # .. 0x082D ; Samaritan
0x082E, # .. 0x082F ; Unknown
0x0830, # .. 0x083E ; Samaritan
@@ -125,8 +123,8 @@ RANGES = [
0x08A0, # .. 0x08B4 ; Arabic
0x08B5, # .. 0x08B5 ; Unknown
0x08B6, # .. 0x08BD ; Arabic
- 0x08BE, # .. 0x08D3 ; Unknown
- 0x08D4, # .. 0x08E1 ; Arabic
+ 0x08BE, # .. 0x08D2 ; Unknown
+ 0x08D3, # .. 0x08E1 ; Arabic
0x08E2, # .. 0x08E2 ; Common
0x08E3, # .. 0x08FF ; Arabic
0x0900, # .. 0x0950 ; Devanagari
@@ -160,8 +158,8 @@ RANGES = [
0x09DE, # .. 0x09DE ; Unknown
0x09DF, # .. 0x09E3 ; Bengali
0x09E4, # .. 0x09E5 ; Unknown
- 0x09E6, # .. 0x09FD ; Bengali
- 0x09FE, # .. 0x0A00 ; Unknown
+ 0x09E6, # .. 0x09FE ; Bengali
+ 0x09FF, # .. 0x0A00 ; Unknown
0x0A01, # .. 0x0A03 ; Gurmukhi
0x0A04, # .. 0x0A04 ; Unknown
0x0A05, # .. 0x0A0A ; Gurmukhi
@@ -192,8 +190,8 @@ RANGES = [
0x0A5D, # .. 0x0A5D ; Unknown
0x0A5E, # .. 0x0A5E ; Gurmukhi
0x0A5F, # .. 0x0A65 ; Unknown
- 0x0A66, # .. 0x0A75 ; Gurmukhi
- 0x0A76, # .. 0x0A80 ; Unknown
+ 0x0A66, # .. 0x0A76 ; Gurmukhi
+ 0x0A77, # .. 0x0A80 ; Unknown
0x0A81, # .. 0x0A83 ; Gujarati
0x0A84, # .. 0x0A84 ; Unknown
0x0A85, # .. 0x0A8D ; Gujarati
@@ -282,9 +280,7 @@ RANGES = [
0x0BD8, # .. 0x0BE5 ; Unknown
0x0BE6, # .. 0x0BFA ; Tamil
0x0BFB, # .. 0x0BFF ; Unknown
- 0x0C00, # .. 0x0C03 ; Telugu
- 0x0C04, # .. 0x0C04 ; Unknown
- 0x0C05, # .. 0x0C0C ; Telugu
+ 0x0C00, # .. 0x0C0C ; Telugu
0x0C0D, # .. 0x0C0D ; Unknown
0x0C0E, # .. 0x0C10 ; Telugu
0x0C11, # .. 0x0C11 ; Unknown
@@ -307,9 +303,7 @@ RANGES = [
0x0C66, # .. 0x0C6F ; Telugu
0x0C70, # .. 0x0C77 ; Unknown
0x0C78, # .. 0x0C7F ; Telugu
- 0x0C80, # .. 0x0C83 ; Kannada
- 0x0C84, # .. 0x0C84 ; Unknown
- 0x0C85, # .. 0x0C8C ; Kannada
+ 0x0C80, # .. 0x0C8C ; Kannada
0x0C8D, # .. 0x0C8D ; Unknown
0x0C8E, # .. 0x0C90 ; Kannada
0x0C91, # .. 0x0C91 ; Unknown
@@ -517,8 +511,8 @@ RANGES = [
0x180F, # .. 0x180F ; Unknown
0x1810, # .. 0x1819 ; Mongolian
0x181A, # .. 0x181F ; Unknown
- 0x1820, # .. 0x1877 ; Mongolian
- 0x1878, # .. 0x187F ; Unknown
+ 0x1820, # .. 0x1878 ; Mongolian
+ 0x1879, # .. 0x187F ; Unknown
0x1880, # .. 0x18AA ; Mongolian
0x18AB, # .. 0x18AF ; Unknown
0x18B0, # .. 0x18F5 ; Canadian_Aboriginal
@@ -574,7 +568,10 @@ RANGES = [
0x1C4D, # .. 0x1C4F ; Lepcha
0x1C50, # .. 0x1C7F ; Ol_Chiki
0x1C80, # .. 0x1C88 ; Cyrillic
- 0x1C89, # .. 0x1CBF ; Unknown
+ 0x1C89, # .. 0x1C8F ; Unknown
+ 0x1C90, # .. 0x1CBA ; Georgian
+ 0x1CBB, # .. 0x1CBC ; Unknown
+ 0x1CBD, # .. 0x1CBF ; Georgian
0x1CC0, # .. 0x1CC7 ; Sundanese
0x1CC8, # .. 0x1CCF ; Unknown
0x1CD0, # .. 0x1CD2 ; Inherited
@@ -675,14 +672,10 @@ RANGES = [
0x2B74, # .. 0x2B75 ; Unknown
0x2B76, # .. 0x2B95 ; Common
0x2B96, # .. 0x2B97 ; Unknown
- 0x2B98, # .. 0x2BB9 ; Common
- 0x2BBA, # .. 0x2BBC ; Unknown
- 0x2BBD, # .. 0x2BC8 ; Common
+ 0x2B98, # .. 0x2BC8 ; Common
0x2BC9, # .. 0x2BC9 ; Unknown
- 0x2BCA, # .. 0x2BD2 ; Common
- 0x2BD3, # .. 0x2BEB ; Unknown
- 0x2BEC, # .. 0x2BEF ; Common
- 0x2BF0, # .. 0x2BFF ; Unknown
+ 0x2BCA, # .. 0x2BFE ; Common
+ 0x2BFF, # .. 0x2BFF ; Unknown
0x2C00, # .. 0x2C2E ; Glagolitic
0x2C2F, # .. 0x2C2F ; Unknown
0x2C30, # .. 0x2C5E ; Glagolitic
@@ -721,8 +714,8 @@ RANGES = [
0x2DD8, # .. 0x2DDE ; Ethiopic
0x2DDF, # .. 0x2DDF ; Unknown
0x2DE0, # .. 0x2DFF ; Cyrillic
- 0x2E00, # .. 0x2E49 ; Common
- 0x2E4A, # .. 0x2E7F ; Unknown
+ 0x2E00, # .. 0x2E4E ; Common
+ 0x2E4F, # .. 0x2E7F ; Unknown
0x2E80, # .. 0x2E99 ; Han
0x2E9A, # .. 0x2E9A ; Unknown
0x2E9B, # .. 0x2EF3 ; Han
@@ -753,8 +746,8 @@ RANGES = [
0x30FB, # .. 0x30FC ; Common
0x30FD, # .. 0x30FF ; Katakana
0x3100, # .. 0x3104 ; Unknown
- 0x3105, # .. 0x312E ; Bopomofo
- 0x312F, # .. 0x3130 ; Unknown
+ 0x3105, # .. 0x312F ; Bopomofo
+ 0x3130, # .. 0x3130 ; Unknown
0x3131, # .. 0x318E ; Hangul
0x318F, # .. 0x318F ; Unknown
0x3190, # .. 0x319F ; Common
@@ -775,8 +768,8 @@ RANGES = [
0x3400, # .. 0x4DB5 ; Han
0x4DB6, # .. 0x4DBF ; Unknown
0x4DC0, # .. 0x4DFF ; Common
- 0x4E00, # .. 0x9FEA ; Han
- 0x9FEB, # .. 0x9FFF ; Unknown
+ 0x4E00, # .. 0x9FEF ; Han
+ 0x9FF0, # .. 0x9FFF ; Unknown
0xA000, # .. 0xA48C ; Yi
0xA48D, # .. 0xA48F ; Unknown
0xA490, # .. 0xA4C6 ; Yi
@@ -790,10 +783,8 @@ RANGES = [
0xA700, # .. 0xA721 ; Common
0xA722, # .. 0xA787 ; Latin
0xA788, # .. 0xA78A ; Common
- 0xA78B, # .. 0xA7AE ; Latin
- 0xA7AF, # .. 0xA7AF ; Unknown
- 0xA7B0, # .. 0xA7B7 ; Latin
- 0xA7B8, # .. 0xA7F6 ; Unknown
+ 0xA78B, # .. 0xA7B9 ; Latin
+ 0xA7BA, # .. 0xA7F6 ; Unknown
0xA7F7, # .. 0xA7FF ; Latin
0xA800, # .. 0xA82B ; Syloti_Nagri
0xA82C, # .. 0xA82F ; Unknown
@@ -805,8 +796,7 @@ RANGES = [
0xA8C6, # .. 0xA8CD ; Unknown
0xA8CE, # .. 0xA8D9 ; Saurashtra
0xA8DA, # .. 0xA8DF ; Unknown
- 0xA8E0, # .. 0xA8FD ; Devanagari
- 0xA8FE, # .. 0xA8FF ; Unknown
+ 0xA8E0, # .. 0xA8FF ; Devanagari
0xA900, # .. 0xA92D ; Kayah_Li
0xA92E, # .. 0xA92E ; Common
0xA92F, # .. 0xA92F ; Kayah_Li
@@ -1050,12 +1040,12 @@ RANGES = [
0x10A14, # .. 0x10A14 ; Unknown
0x10A15, # .. 0x10A17 ; Kharoshthi
0x10A18, # .. 0x10A18 ; Unknown
- 0x10A19, # .. 0x10A33 ; Kharoshthi
- 0x10A34, # .. 0x10A37 ; Unknown
+ 0x10A19, # .. 0x10A35 ; Kharoshthi
+ 0x10A36, # .. 0x10A37 ; Unknown
0x10A38, # .. 0x10A3A ; Kharoshthi
0x10A3B, # .. 0x10A3E ; Unknown
- 0x10A3F, # .. 0x10A47 ; Kharoshthi
- 0x10A48, # .. 0x10A4F ; Unknown
+ 0x10A3F, # .. 0x10A48 ; Kharoshthi
+ 0x10A49, # .. 0x10A4F ; Unknown
0x10A50, # .. 0x10A58 ; Kharoshthi
0x10A59, # .. 0x10A5F ; Unknown
0x10A60, # .. 0x10A7F ; Old_South_Arabian
@@ -1087,24 +1077,33 @@ RANGES = [
0x10CC0, # .. 0x10CF2 ; Old_Hungarian
0x10CF3, # .. 0x10CF9 ; Unknown
0x10CFA, # .. 0x10CFF ; Old_Hungarian
- 0x10D00, # .. 0x10E5F ; Unknown
+ 0x10D00, # .. 0x10D27 ; Hanifi_Rohingya
+ 0x10D28, # .. 0x10D2F ; Unknown
+ 0x10D30, # .. 0x10D39 ; Hanifi_Rohingya
+ 0x10D3A, # .. 0x10E5F ; Unknown
0x10E60, # .. 0x10E7E ; Arabic
- 0x10E7F, # .. 0x10FFF ; Unknown
+ 0x10E7F, # .. 0x10EFF ; Unknown
+ 0x10F00, # .. 0x10F27 ; Old_Sogdian
+ 0x10F28, # .. 0x10F2F ; Unknown
+ 0x10F30, # .. 0x10F59 ; Sogdian
+ 0x10F5A, # .. 0x10FFF ; Unknown
0x11000, # .. 0x1104D ; Brahmi
0x1104E, # .. 0x11051 ; Unknown
0x11052, # .. 0x1106F ; Brahmi
0x11070, # .. 0x1107E ; Unknown
0x1107F, # .. 0x1107F ; Brahmi
0x11080, # .. 0x110C1 ; Kaithi
- 0x110C2, # .. 0x110CF ; Unknown
+ 0x110C2, # .. 0x110CC ; Unknown
+ 0x110CD, # .. 0x110CD ; Kaithi
+ 0x110CE, # .. 0x110CF ; Unknown
0x110D0, # .. 0x110E8 ; Sora_Sompeng
0x110E9, # .. 0x110EF ; Unknown
0x110F0, # .. 0x110F9 ; Sora_Sompeng
0x110FA, # .. 0x110FF ; Unknown
0x11100, # .. 0x11134 ; Chakma
0x11135, # .. 0x11135 ; Unknown
- 0x11136, # .. 0x11143 ; Chakma
- 0x11144, # .. 0x1114F ; Unknown
+ 0x11136, # .. 0x11146 ; Chakma
+ 0x11147, # .. 0x1114F ; Unknown
0x11150, # .. 0x11176 ; Mahajani
0x11177, # .. 0x1117F ; Unknown
0x11180, # .. 0x111CD ; Sharada
@@ -1144,7 +1143,8 @@ RANGES = [
0x11332, # .. 0x11333 ; Grantha
0x11334, # .. 0x11334 ; Unknown
0x11335, # .. 0x11339 ; Grantha
- 0x1133A, # .. 0x1133B ; Unknown
+ 0x1133A, # .. 0x1133A ; Unknown
+ 0x1133B, # .. 0x1133B ; Inherited
0x1133C, # .. 0x11344 ; Grantha
0x11345, # .. 0x11346 ; Unknown
0x11347, # .. 0x11348 ; Grantha
@@ -1165,8 +1165,8 @@ RANGES = [
0x1145A, # .. 0x1145A ; Unknown
0x1145B, # .. 0x1145B ; Newa
0x1145C, # .. 0x1145C ; Unknown
- 0x1145D, # .. 0x1145D ; Newa
- 0x1145E, # .. 0x1147F ; Unknown
+ 0x1145D, # .. 0x1145E ; Newa
+ 0x1145F, # .. 0x1147F ; Unknown
0x11480, # .. 0x114C7 ; Tirhuta
0x114C8, # .. 0x114CF ; Unknown
0x114D0, # .. 0x114D9 ; Tirhuta
@@ -1185,12 +1185,14 @@ RANGES = [
0x116B8, # .. 0x116BF ; Unknown
0x116C0, # .. 0x116C9 ; Takri
0x116CA, # .. 0x116FF ; Unknown
- 0x11700, # .. 0x11719 ; Ahom
- 0x1171A, # .. 0x1171C ; Unknown
+ 0x11700, # .. 0x1171A ; Ahom
+ 0x1171B, # .. 0x1171C ; Unknown
0x1171D, # .. 0x1172B ; Ahom
0x1172C, # .. 0x1172F ; Unknown
0x11730, # .. 0x1173F ; Ahom
- 0x11740, # .. 0x1189F ; Unknown
+ 0x11740, # .. 0x117FF ; Unknown
+ 0x11800, # .. 0x1183B ; Dogra
+ 0x1183C, # .. 0x1189F ; Unknown
0x118A0, # .. 0x118F2 ; Warang_Citi
0x118F3, # .. 0x118FE ; Unknown
0x118FF, # .. 0x118FF ; Warang_Citi
@@ -1199,9 +1201,7 @@ RANGES = [
0x11A48, # .. 0x11A4F ; Unknown
0x11A50, # .. 0x11A83 ; Soyombo
0x11A84, # .. 0x11A85 ; Unknown
- 0x11A86, # .. 0x11A9C ; Soyombo
- 0x11A9D, # .. 0x11A9D ; Unknown
- 0x11A9E, # .. 0x11AA2 ; Soyombo
+ 0x11A86, # .. 0x11AA2 ; Soyombo
0x11AA3, # .. 0x11ABF ; Unknown
0x11AC0, # .. 0x11AF8 ; Pau_Cin_Hau
0x11AF9, # .. 0x11BFF ; Unknown
@@ -1232,7 +1232,21 @@ RANGES = [
0x11D3F, # .. 0x11D47 ; Masaram_Gondi
0x11D48, # .. 0x11D4F ; Unknown
0x11D50, # .. 0x11D59 ; Masaram_Gondi
- 0x11D5A, # .. 0x11FFF ; Unknown
+ 0x11D5A, # .. 0x11D5F ; Unknown
+ 0x11D60, # .. 0x11D65 ; Gunjala_Gondi
+ 0x11D66, # .. 0x11D66 ; Unknown
+ 0x11D67, # .. 0x11D68 ; Gunjala_Gondi
+ 0x11D69, # .. 0x11D69 ; Unknown
+ 0x11D6A, # .. 0x11D8E ; Gunjala_Gondi
+ 0x11D8F, # .. 0x11D8F ; Unknown
+ 0x11D90, # .. 0x11D91 ; Gunjala_Gondi
+ 0x11D92, # .. 0x11D92 ; Unknown
+ 0x11D93, # .. 0x11D98 ; Gunjala_Gondi
+ 0x11D99, # .. 0x11D9F ; Unknown
+ 0x11DA0, # .. 0x11DA9 ; Gunjala_Gondi
+ 0x11DAA, # .. 0x11EDF ; Unknown
+ 0x11EE0, # .. 0x11EF8 ; Makasar
+ 0x11EF9, # .. 0x11FFF ; Unknown
0x12000, # .. 0x12399 ; Cuneiform
0x1239A, # .. 0x123FF ; Unknown
0x12400, # .. 0x1246E ; Cuneiform
@@ -1266,7 +1280,9 @@ RANGES = [
0x16B63, # .. 0x16B77 ; Pahawh_Hmong
0x16B78, # .. 0x16B7C ; Unknown
0x16B7D, # .. 0x16B8F ; Pahawh_Hmong
- 0x16B90, # .. 0x16EFF ; Unknown
+ 0x16B90, # .. 0x16E3F ; Unknown
+ 0x16E40, # .. 0x16E9A ; Medefaidrin
+ 0x16E9B, # .. 0x16EFF ; Unknown
0x16F00, # .. 0x16F44 ; Miao
0x16F45, # .. 0x16F4F ; Unknown
0x16F50, # .. 0x16F7E ; Miao
@@ -1276,8 +1292,8 @@ RANGES = [
0x16FE0, # .. 0x16FE0 ; Tangut
0x16FE1, # .. 0x16FE1 ; Nushu
0x16FE2, # .. 0x16FFF ; Unknown
- 0x17000, # .. 0x187EC ; Tangut
- 0x187ED, # .. 0x187FF ; Unknown
+ 0x17000, # .. 0x187F1 ; Tangut
+ 0x187F2, # .. 0x187FF ; Unknown
0x18800, # .. 0x18AF2 ; Tangut
0x18AF3, # .. 0x1AFFF ; Unknown
0x1B000, # .. 0x1B000 ; Katakana
@@ -1311,11 +1327,13 @@ RANGES = [
0x1D1AE, # .. 0x1D1E8 ; Common
0x1D1E9, # .. 0x1D1FF ; Unknown
0x1D200, # .. 0x1D245 ; Greek
- 0x1D246, # .. 0x1D2FF ; Unknown
+ 0x1D246, # .. 0x1D2DF ; Unknown
+ 0x1D2E0, # .. 0x1D2F3 ; Common
+ 0x1D2F4, # .. 0x1D2FF ; Unknown
0x1D300, # .. 0x1D356 ; Common
0x1D357, # .. 0x1D35F ; Unknown
- 0x1D360, # .. 0x1D371 ; Common
- 0x1D372, # .. 0x1D3FF ; Unknown
+ 0x1D360, # .. 0x1D378 ; Common
+ 0x1D379, # .. 0x1D3FF ; Unknown
0x1D400, # .. 0x1D454 ; Common
0x1D455, # .. 0x1D455 ; Unknown
0x1D456, # .. 0x1D49C ; Common
@@ -1382,7 +1400,9 @@ RANGES = [
0x1E950, # .. 0x1E959 ; Adlam
0x1E95A, # .. 0x1E95D ; Unknown
0x1E95E, # .. 0x1E95F ; Adlam
- 0x1E960, # .. 0x1EDFF ; Unknown
+ 0x1E960, # .. 0x1EC70 ; Unknown
+ 0x1EC71, # .. 0x1ECB4 ; Common
+ 0x1ECB5, # .. 0x1EDFF ; Unknown
0x1EE00, # .. 0x1EE03 ; Arabic
0x1EE04, # .. 0x1EE04 ; Unknown
0x1EE05, # .. 0x1EE1F ; Arabic
@@ -1465,9 +1485,7 @@ RANGES = [
0x1F0F6, # .. 0x1F0FF ; Unknown
0x1F100, # .. 0x1F10C ; Common
0x1F10D, # .. 0x1F10F ; Unknown
- 0x1F110, # .. 0x1F12E ; Common
- 0x1F12F, # .. 0x1F12F ; Unknown
- 0x1F130, # .. 0x1F16B ; Common
+ 0x1F110, # .. 0x1F16B ; Common
0x1F16C, # .. 0x1F16F ; Unknown
0x1F170, # .. 0x1F1AC ; Common
0x1F1AD, # .. 0x1F1E5 ; Unknown
@@ -1487,12 +1505,12 @@ RANGES = [
0x1F6D5, # .. 0x1F6DF ; Unknown
0x1F6E0, # .. 0x1F6EC ; Common
0x1F6ED, # .. 0x1F6EF ; Unknown
- 0x1F6F0, # .. 0x1F6F8 ; Common
- 0x1F6F9, # .. 0x1F6FF ; Unknown
+ 0x1F6F0, # .. 0x1F6F9 ; Common
+ 0x1F6FA, # .. 0x1F6FF ; Unknown
0x1F700, # .. 0x1F773 ; Common
0x1F774, # .. 0x1F77F ; Unknown
- 0x1F780, # .. 0x1F7D4 ; Common
- 0x1F7D5, # .. 0x1F7FF ; Unknown
+ 0x1F780, # .. 0x1F7D8 ; Common
+ 0x1F7D9, # .. 0x1F7FF ; Unknown
0x1F800, # .. 0x1F80B ; Common
0x1F80C, # .. 0x1F80F ; Unknown
0x1F810, # .. 0x1F847 ; Common
@@ -1507,16 +1525,22 @@ RANGES = [
0x1F90C, # .. 0x1F90F ; Unknown
0x1F910, # .. 0x1F93E ; Common
0x1F93F, # .. 0x1F93F ; Unknown
- 0x1F940, # .. 0x1F94C ; Common
- 0x1F94D, # .. 0x1F94F ; Unknown
- 0x1F950, # .. 0x1F96B ; Common
- 0x1F96C, # .. 0x1F97F ; Unknown
- 0x1F980, # .. 0x1F997 ; Common
- 0x1F998, # .. 0x1F9BF ; Unknown
- 0x1F9C0, # .. 0x1F9C0 ; Common
- 0x1F9C1, # .. 0x1F9CF ; Unknown
- 0x1F9D0, # .. 0x1F9E6 ; Common
- 0x1F9E7, # .. 0x1FFFF ; Unknown
+ 0x1F940, # .. 0x1F970 ; Common
+ 0x1F971, # .. 0x1F972 ; Unknown
+ 0x1F973, # .. 0x1F976 ; Common
+ 0x1F977, # .. 0x1F979 ; Unknown
+ 0x1F97A, # .. 0x1F97A ; Common
+ 0x1F97B, # .. 0x1F97B ; Unknown
+ 0x1F97C, # .. 0x1F9A2 ; Common
+ 0x1F9A3, # .. 0x1F9AF ; Unknown
+ 0x1F9B0, # .. 0x1F9B9 ; Common
+ 0x1F9BA, # .. 0x1F9BF ; Unknown
+ 0x1F9C0, # .. 0x1F9C2 ; Common
+ 0x1F9C3, # .. 0x1F9CF ; Unknown
+ 0x1F9D0, # .. 0x1F9FF ; Common
+ 0x1FA00, # .. 0x1FA5F ; Unknown
+ 0x1FA60, # .. 0x1FA6D ; Common
+ 0x1FA6E, # .. 0x1FFFF ; Unknown
0x20000, # .. 0x2A6D6 ; Han
0x2A6D7, # .. 0x2A6FF ; Unknown
0x2A700, # .. 0x2B734 ; Han
@@ -1585,10 +1609,7 @@ VALUES = [
'Zzzz', # 0530..0530 ; Unknown
'Armn', # 0531..0556 ; Armenian
'Zzzz', # 0557..0558 ; Unknown
- 'Armn', # 0559..055F ; Armenian
- 'Zzzz', # 0560..0560 ; Unknown
- 'Armn', # 0561..0587 ; Armenian
- 'Zzzz', # 0588..0588 ; Unknown
+ 'Armn', # 0559..0588 ; Armenian
'Zyyy', # 0589..0589 ; Common
'Armn', # 058A..058A ; Armenian
'Zzzz', # 058B..058C ; Unknown
@@ -1597,8 +1618,8 @@ VALUES = [
'Hebr', # 0591..05C7 ; Hebrew
'Zzzz', # 05C8..05CF ; Unknown
'Hebr', # 05D0..05EA ; Hebrew
- 'Zzzz', # 05EB..05EF ; Unknown
- 'Hebr', # 05F0..05F4 ; Hebrew
+ 'Zzzz', # 05EB..05EE ; Unknown
+ 'Hebr', # 05EF..05F4 ; Hebrew
'Zzzz', # 05F5..05FF ; Unknown
'Arab', # 0600..0604 ; Arabic
'Zyyy', # 0605..0605 ; Common
@@ -1628,7 +1649,8 @@ VALUES = [
'Thaa', # 0780..07B1 ; Thaana
'Zzzz', # 07B2..07BF ; Unknown
'Nkoo', # 07C0..07FA ; Nko
- 'Zzzz', # 07FB..07FF ; Unknown
+ 'Zzzz', # 07FB..07FC ; Unknown
+ 'Nkoo', # 07FD..07FF ; Nko
'Samr', # 0800..082D ; Samaritan
'Zzzz', # 082E..082F ; Unknown
'Samr', # 0830..083E ; Samaritan
@@ -1642,8 +1664,8 @@ VALUES = [
'Arab', # 08A0..08B4 ; Arabic
'Zzzz', # 08B5..08B5 ; Unknown
'Arab', # 08B6..08BD ; Arabic
- 'Zzzz', # 08BE..08D3 ; Unknown
- 'Arab', # 08D4..08E1 ; Arabic
+ 'Zzzz', # 08BE..08D2 ; Unknown
+ 'Arab', # 08D3..08E1 ; Arabic
'Zyyy', # 08E2..08E2 ; Common
'Arab', # 08E3..08FF ; Arabic
'Deva', # 0900..0950 ; Devanagari
@@ -1677,8 +1699,8 @@ VALUES = [
'Zzzz', # 09DE..09DE ; Unknown
'Beng', # 09DF..09E3 ; Bengali
'Zzzz', # 09E4..09E5 ; Unknown
- 'Beng', # 09E6..09FD ; Bengali
- 'Zzzz', # 09FE..0A00 ; Unknown
+ 'Beng', # 09E6..09FE ; Bengali
+ 'Zzzz', # 09FF..0A00 ; Unknown
'Guru', # 0A01..0A03 ; Gurmukhi
'Zzzz', # 0A04..0A04 ; Unknown
'Guru', # 0A05..0A0A ; Gurmukhi
@@ -1709,8 +1731,8 @@ VALUES = [
'Zzzz', # 0A5D..0A5D ; Unknown
'Guru', # 0A5E..0A5E ; Gurmukhi
'Zzzz', # 0A5F..0A65 ; Unknown
- 'Guru', # 0A66..0A75 ; Gurmukhi
- 'Zzzz', # 0A76..0A80 ; Unknown
+ 'Guru', # 0A66..0A76 ; Gurmukhi
+ 'Zzzz', # 0A77..0A80 ; Unknown
'Gujr', # 0A81..0A83 ; Gujarati
'Zzzz', # 0A84..0A84 ; Unknown
'Gujr', # 0A85..0A8D ; Gujarati
@@ -1799,9 +1821,7 @@ VALUES = [
'Zzzz', # 0BD8..0BE5 ; Unknown
'Taml', # 0BE6..0BFA ; Tamil
'Zzzz', # 0BFB..0BFF ; Unknown
- 'Telu', # 0C00..0C03 ; Telugu
- 'Zzzz', # 0C04..0C04 ; Unknown
- 'Telu', # 0C05..0C0C ; Telugu
+ 'Telu', # 0C00..0C0C ; Telugu
'Zzzz', # 0C0D..0C0D ; Unknown
'Telu', # 0C0E..0C10 ; Telugu
'Zzzz', # 0C11..0C11 ; Unknown
@@ -1824,9 +1844,7 @@ VALUES = [
'Telu', # 0C66..0C6F ; Telugu
'Zzzz', # 0C70..0C77 ; Unknown
'Telu', # 0C78..0C7F ; Telugu
- 'Knda', # 0C80..0C83 ; Kannada
- 'Zzzz', # 0C84..0C84 ; Unknown
- 'Knda', # 0C85..0C8C ; Kannada
+ 'Knda', # 0C80..0C8C ; Kannada
'Zzzz', # 0C8D..0C8D ; Unknown
'Knda', # 0C8E..0C90 ; Kannada
'Zzzz', # 0C91..0C91 ; Unknown
@@ -2034,8 +2052,8 @@ VALUES = [
'Zzzz', # 180F..180F ; Unknown
'Mong', # 1810..1819 ; Mongolian
'Zzzz', # 181A..181F ; Unknown
- 'Mong', # 1820..1877 ; Mongolian
- 'Zzzz', # 1878..187F ; Unknown
+ 'Mong', # 1820..1878 ; Mongolian
+ 'Zzzz', # 1879..187F ; Unknown
'Mong', # 1880..18AA ; Mongolian
'Zzzz', # 18AB..18AF ; Unknown
'Cans', # 18B0..18F5 ; Canadian_Aboriginal
@@ -2091,7 +2109,10 @@ VALUES = [
'Lepc', # 1C4D..1C4F ; Lepcha
'Olck', # 1C50..1C7F ; Ol_Chiki
'Cyrl', # 1C80..1C88 ; Cyrillic
- 'Zzzz', # 1C89..1CBF ; Unknown
+ 'Zzzz', # 1C89..1C8F ; Unknown
+ 'Geor', # 1C90..1CBA ; Georgian
+ 'Zzzz', # 1CBB..1CBC ; Unknown
+ 'Geor', # 1CBD..1CBF ; Georgian
'Sund', # 1CC0..1CC7 ; Sundanese
'Zzzz', # 1CC8..1CCF ; Unknown
'Zinh', # 1CD0..1CD2 ; Inherited
@@ -2192,14 +2213,10 @@ VALUES = [
'Zzzz', # 2B74..2B75 ; Unknown
'Zyyy', # 2B76..2B95 ; Common
'Zzzz', # 2B96..2B97 ; Unknown
- 'Zyyy', # 2B98..2BB9 ; Common
- 'Zzzz', # 2BBA..2BBC ; Unknown
- 'Zyyy', # 2BBD..2BC8 ; Common
+ 'Zyyy', # 2B98..2BC8 ; Common
'Zzzz', # 2BC9..2BC9 ; Unknown
- 'Zyyy', # 2BCA..2BD2 ; Common
- 'Zzzz', # 2BD3..2BEB ; Unknown
- 'Zyyy', # 2BEC..2BEF ; Common
- 'Zzzz', # 2BF0..2BFF ; Unknown
+ 'Zyyy', # 2BCA..2BFE ; Common
+ 'Zzzz', # 2BFF..2BFF ; Unknown
'Glag', # 2C00..2C2E ; Glagolitic
'Zzzz', # 2C2F..2C2F ; Unknown
'Glag', # 2C30..2C5E ; Glagolitic
@@ -2238,8 +2255,8 @@ VALUES = [
'Ethi', # 2DD8..2DDE ; Ethiopic
'Zzzz', # 2DDF..2DDF ; Unknown
'Cyrl', # 2DE0..2DFF ; Cyrillic
- 'Zyyy', # 2E00..2E49 ; Common
- 'Zzzz', # 2E4A..2E7F ; Unknown
+ 'Zyyy', # 2E00..2E4E ; Common
+ 'Zzzz', # 2E4F..2E7F ; Unknown
'Hani', # 2E80..2E99 ; Han
'Zzzz', # 2E9A..2E9A ; Unknown
'Hani', # 2E9B..2EF3 ; Han
@@ -2270,8 +2287,8 @@ VALUES = [
'Zyyy', # 30FB..30FC ; Common
'Kana', # 30FD..30FF ; Katakana
'Zzzz', # 3100..3104 ; Unknown
- 'Bopo', # 3105..312E ; Bopomofo
- 'Zzzz', # 312F..3130 ; Unknown
+ 'Bopo', # 3105..312F ; Bopomofo
+ 'Zzzz', # 3130..3130 ; Unknown
'Hang', # 3131..318E ; Hangul
'Zzzz', # 318F..318F ; Unknown
'Zyyy', # 3190..319F ; Common
@@ -2292,8 +2309,8 @@ VALUES = [
'Hani', # 3400..4DB5 ; Han
'Zzzz', # 4DB6..4DBF ; Unknown
'Zyyy', # 4DC0..4DFF ; Common
- 'Hani', # 4E00..9FEA ; Han
- 'Zzzz', # 9FEB..9FFF ; Unknown
+ 'Hani', # 4E00..9FEF ; Han
+ 'Zzzz', # 9FF0..9FFF ; Unknown
'Yiii', # A000..A48C ; Yi
'Zzzz', # A48D..A48F ; Unknown
'Yiii', # A490..A4C6 ; Yi
@@ -2307,10 +2324,8 @@ VALUES = [
'Zyyy', # A700..A721 ; Common
'Latn', # A722..A787 ; Latin
'Zyyy', # A788..A78A ; Common
- 'Latn', # A78B..A7AE ; Latin
- 'Zzzz', # A7AF..A7AF ; Unknown
- 'Latn', # A7B0..A7B7 ; Latin
- 'Zzzz', # A7B8..A7F6 ; Unknown
+ 'Latn', # A78B..A7B9 ; Latin
+ 'Zzzz', # A7BA..A7F6 ; Unknown
'Latn', # A7F7..A7FF ; Latin
'Sylo', # A800..A82B ; Syloti_Nagri
'Zzzz', # A82C..A82F ; Unknown
@@ -2322,8 +2337,7 @@ VALUES = [
'Zzzz', # A8C6..A8CD ; Unknown
'Saur', # A8CE..A8D9 ; Saurashtra
'Zzzz', # A8DA..A8DF ; Unknown
- 'Deva', # A8E0..A8FD ; Devanagari
- 'Zzzz', # A8FE..A8FF ; Unknown
+ 'Deva', # A8E0..A8FF ; Devanagari
'Kali', # A900..A92D ; Kayah_Li
'Zyyy', # A92E..A92E ; Common
'Kali', # A92F..A92F ; Kayah_Li
@@ -2567,12 +2581,12 @@ VALUES = [
'Zzzz', # 10A14..10A14 ; Unknown
'Khar', # 10A15..10A17 ; Kharoshthi
'Zzzz', # 10A18..10A18 ; Unknown
- 'Khar', # 10A19..10A33 ; Kharoshthi
- 'Zzzz', # 10A34..10A37 ; Unknown
+ 'Khar', # 10A19..10A35 ; Kharoshthi
+ 'Zzzz', # 10A36..10A37 ; Unknown
'Khar', # 10A38..10A3A ; Kharoshthi
'Zzzz', # 10A3B..10A3E ; Unknown
- 'Khar', # 10A3F..10A47 ; Kharoshthi
- 'Zzzz', # 10A48..10A4F ; Unknown
+ 'Khar', # 10A3F..10A48 ; Kharoshthi
+ 'Zzzz', # 10A49..10A4F ; Unknown
'Khar', # 10A50..10A58 ; Kharoshthi
'Zzzz', # 10A59..10A5F ; Unknown
'Sarb', # 10A60..10A7F ; Old_South_Arabian
@@ -2604,24 +2618,33 @@ VALUES = [
'Hung', # 10CC0..10CF2 ; Old_Hungarian
'Zzzz', # 10CF3..10CF9 ; Unknown
'Hung', # 10CFA..10CFF ; Old_Hungarian
- 'Zzzz', # 10D00..10E5F ; Unknown
+ 'Rohg', # 10D00..10D27 ; Hanifi_Rohingya
+ 'Zzzz', # 10D28..10D2F ; Unknown
+ 'Rohg', # 10D30..10D39 ; Hanifi_Rohingya
+ 'Zzzz', # 10D3A..10E5F ; Unknown
'Arab', # 10E60..10E7E ; Arabic
- 'Zzzz', # 10E7F..10FFF ; Unknown
+ 'Zzzz', # 10E7F..10EFF ; Unknown
+ 'Sogo', # 10F00..10F27 ; Old_Sogdian
+ 'Zzzz', # 10F28..10F2F ; Unknown
+ 'Sogd', # 10F30..10F59 ; Sogdian
+ 'Zzzz', # 10F5A..10FFF ; Unknown
'Brah', # 11000..1104D ; Brahmi
'Zzzz', # 1104E..11051 ; Unknown
'Brah', # 11052..1106F ; Brahmi
'Zzzz', # 11070..1107E ; Unknown
'Brah', # 1107F..1107F ; Brahmi
'Kthi', # 11080..110C1 ; Kaithi
- 'Zzzz', # 110C2..110CF ; Unknown
+ 'Zzzz', # 110C2..110CC ; Unknown
+ 'Kthi', # 110CD..110CD ; Kaithi
+ 'Zzzz', # 110CE..110CF ; Unknown
'Sora', # 110D0..110E8 ; Sora_Sompeng
'Zzzz', # 110E9..110EF ; Unknown
'Sora', # 110F0..110F9 ; Sora_Sompeng
'Zzzz', # 110FA..110FF ; Unknown
'Cakm', # 11100..11134 ; Chakma
'Zzzz', # 11135..11135 ; Unknown
- 'Cakm', # 11136..11143 ; Chakma
- 'Zzzz', # 11144..1114F ; Unknown
+ 'Cakm', # 11136..11146 ; Chakma
+ 'Zzzz', # 11147..1114F ; Unknown
'Mahj', # 11150..11176 ; Mahajani
'Zzzz', # 11177..1117F ; Unknown
'Shrd', # 11180..111CD ; Sharada
@@ -2661,7 +2684,8 @@ VALUES = [
'Gran', # 11332..11333 ; Grantha
'Zzzz', # 11334..11334 ; Unknown
'Gran', # 11335..11339 ; Grantha
- 'Zzzz', # 1133A..1133B ; Unknown
+ 'Zzzz', # 1133A..1133A ; Unknown
+ 'Zinh', # 1133B..1133B ; Inherited
'Gran', # 1133C..11344 ; Grantha
'Zzzz', # 11345..11346 ; Unknown
'Gran', # 11347..11348 ; Grantha
@@ -2682,8 +2706,8 @@ VALUES = [
'Zzzz', # 1145A..1145A ; Unknown
'Newa', # 1145B..1145B ; Newa
'Zzzz', # 1145C..1145C ; Unknown
- 'Newa', # 1145D..1145D ; Newa
- 'Zzzz', # 1145E..1147F ; Unknown
+ 'Newa', # 1145D..1145E ; Newa
+ 'Zzzz', # 1145F..1147F ; Unknown
'Tirh', # 11480..114C7 ; Tirhuta
'Zzzz', # 114C8..114CF ; Unknown
'Tirh', # 114D0..114D9 ; Tirhuta
@@ -2702,12 +2726,14 @@ VALUES = [
'Zzzz', # 116B8..116BF ; Unknown
'Takr', # 116C0..116C9 ; Takri
'Zzzz', # 116CA..116FF ; Unknown
- 'Ahom', # 11700..11719 ; Ahom
- 'Zzzz', # 1171A..1171C ; Unknown
+ 'Ahom', # 11700..1171A ; Ahom
+ 'Zzzz', # 1171B..1171C ; Unknown
'Ahom', # 1171D..1172B ; Ahom
'Zzzz', # 1172C..1172F ; Unknown
'Ahom', # 11730..1173F ; Ahom
- 'Zzzz', # 11740..1189F ; Unknown
+ 'Zzzz', # 11740..117FF ; Unknown
+ 'Dogr', # 11800..1183B ; Dogra
+ 'Zzzz', # 1183C..1189F ; Unknown
'Wara', # 118A0..118F2 ; Warang_Citi
'Zzzz', # 118F3..118FE ; Unknown
'Wara', # 118FF..118FF ; Warang_Citi
@@ -2716,9 +2742,7 @@ VALUES = [
'Zzzz', # 11A48..11A4F ; Unknown
'Soyo', # 11A50..11A83 ; Soyombo
'Zzzz', # 11A84..11A85 ; Unknown
- 'Soyo', # 11A86..11A9C ; Soyombo
- 'Zzzz', # 11A9D..11A9D ; Unknown
- 'Soyo', # 11A9E..11AA2 ; Soyombo
+ 'Soyo', # 11A86..11AA2 ; Soyombo
'Zzzz', # 11AA3..11ABF ; Unknown
'Pauc', # 11AC0..11AF8 ; Pau_Cin_Hau
'Zzzz', # 11AF9..11BFF ; Unknown
@@ -2749,7 +2773,21 @@ VALUES = [
'Gonm', # 11D3F..11D47 ; Masaram_Gondi
'Zzzz', # 11D48..11D4F ; Unknown
'Gonm', # 11D50..11D59 ; Masaram_Gondi
- 'Zzzz', # 11D5A..11FFF ; Unknown
+ 'Zzzz', # 11D5A..11D5F ; Unknown
+ 'Gong', # 11D60..11D65 ; Gunjala_Gondi
+ 'Zzzz', # 11D66..11D66 ; Unknown
+ 'Gong', # 11D67..11D68 ; Gunjala_Gondi
+ 'Zzzz', # 11D69..11D69 ; Unknown
+ 'Gong', # 11D6A..11D8E ; Gunjala_Gondi
+ 'Zzzz', # 11D8F..11D8F ; Unknown
+ 'Gong', # 11D90..11D91 ; Gunjala_Gondi
+ 'Zzzz', # 11D92..11D92 ; Unknown
+ 'Gong', # 11D93..11D98 ; Gunjala_Gondi
+ 'Zzzz', # 11D99..11D9F ; Unknown
+ 'Gong', # 11DA0..11DA9 ; Gunjala_Gondi
+ 'Zzzz', # 11DAA..11EDF ; Unknown
+ 'Maka', # 11EE0..11EF8 ; Makasar
+ 'Zzzz', # 11EF9..11FFF ; Unknown
'Xsux', # 12000..12399 ; Cuneiform
'Zzzz', # 1239A..123FF ; Unknown
'Xsux', # 12400..1246E ; Cuneiform
@@ -2783,7 +2821,9 @@ VALUES = [
'Hmng', # 16B63..16B77 ; Pahawh_Hmong
'Zzzz', # 16B78..16B7C ; Unknown
'Hmng', # 16B7D..16B8F ; Pahawh_Hmong
- 'Zzzz', # 16B90..16EFF ; Unknown
+ 'Zzzz', # 16B90..16E3F ; Unknown
+ 'Medf', # 16E40..16E9A ; Medefaidrin
+ 'Zzzz', # 16E9B..16EFF ; Unknown
'Plrd', # 16F00..16F44 ; Miao
'Zzzz', # 16F45..16F4F ; Unknown
'Plrd', # 16F50..16F7E ; Miao
@@ -2793,8 +2833,8 @@ VALUES = [
'Tang', # 16FE0..16FE0 ; Tangut
'Nshu', # 16FE1..16FE1 ; Nushu
'Zzzz', # 16FE2..16FFF ; Unknown
- 'Tang', # 17000..187EC ; Tangut
- 'Zzzz', # 187ED..187FF ; Unknown
+ 'Tang', # 17000..187F1 ; Tangut
+ 'Zzzz', # 187F2..187FF ; Unknown
'Tang', # 18800..18AF2 ; Tangut
'Zzzz', # 18AF3..1AFFF ; Unknown
'Kana', # 1B000..1B000 ; Katakana
@@ -2828,11 +2868,13 @@ VALUES = [
'Zyyy', # 1D1AE..1D1E8 ; Common
'Zzzz', # 1D1E9..1D1FF ; Unknown
'Grek', # 1D200..1D245 ; Greek
- 'Zzzz', # 1D246..1D2FF ; Unknown
+ 'Zzzz', # 1D246..1D2DF ; Unknown
+ 'Zyyy', # 1D2E0..1D2F3 ; Common
+ 'Zzzz', # 1D2F4..1D2FF ; Unknown
'Zyyy', # 1D300..1D356 ; Common
'Zzzz', # 1D357..1D35F ; Unknown
- 'Zyyy', # 1D360..1D371 ; Common
- 'Zzzz', # 1D372..1D3FF ; Unknown
+ 'Zyyy', # 1D360..1D378 ; Common
+ 'Zzzz', # 1D379..1D3FF ; Unknown
'Zyyy', # 1D400..1D454 ; Common
'Zzzz', # 1D455..1D455 ; Unknown
'Zyyy', # 1D456..1D49C ; Common
@@ -2899,7 +2941,9 @@ VALUES = [
'Adlm', # 1E950..1E959 ; Adlam
'Zzzz', # 1E95A..1E95D ; Unknown
'Adlm', # 1E95E..1E95F ; Adlam
- 'Zzzz', # 1E960..1EDFF ; Unknown
+ 'Zzzz', # 1E960..1EC70 ; Unknown
+ 'Zyyy', # 1EC71..1ECB4 ; Common
+ 'Zzzz', # 1ECB5..1EDFF ; Unknown
'Arab', # 1EE00..1EE03 ; Arabic
'Zzzz', # 1EE04..1EE04 ; Unknown
'Arab', # 1EE05..1EE1F ; Arabic
@@ -2982,9 +3026,7 @@ VALUES = [
'Zzzz', # 1F0F6..1F0FF ; Unknown
'Zyyy', # 1F100..1F10C ; Common
'Zzzz', # 1F10D..1F10F ; Unknown
- 'Zyyy', # 1F110..1F12E ; Common
- 'Zzzz', # 1F12F..1F12F ; Unknown
- 'Zyyy', # 1F130..1F16B ; Common
+ 'Zyyy', # 1F110..1F16B ; Common
'Zzzz', # 1F16C..1F16F ; Unknown
'Zyyy', # 1F170..1F1AC ; Common
'Zzzz', # 1F1AD..1F1E5 ; Unknown
@@ -3004,12 +3046,12 @@ VALUES = [
'Zzzz', # 1F6D5..1F6DF ; Unknown
'Zyyy', # 1F6E0..1F6EC ; Common
'Zzzz', # 1F6ED..1F6EF ; Unknown
- 'Zyyy', # 1F6F0..1F6F8 ; Common
- 'Zzzz', # 1F6F9..1F6FF ; Unknown
+ 'Zyyy', # 1F6F0..1F6F9 ; Common
+ 'Zzzz', # 1F6FA..1F6FF ; Unknown
'Zyyy', # 1F700..1F773 ; Common
'Zzzz', # 1F774..1F77F ; Unknown
- 'Zyyy', # 1F780..1F7D4 ; Common
- 'Zzzz', # 1F7D5..1F7FF ; Unknown
+ 'Zyyy', # 1F780..1F7D8 ; Common
+ 'Zzzz', # 1F7D9..1F7FF ; Unknown
'Zyyy', # 1F800..1F80B ; Common
'Zzzz', # 1F80C..1F80F ; Unknown
'Zyyy', # 1F810..1F847 ; Common
@@ -3024,16 +3066,22 @@ VALUES = [
'Zzzz', # 1F90C..1F90F ; Unknown
'Zyyy', # 1F910..1F93E ; Common
'Zzzz', # 1F93F..1F93F ; Unknown
- 'Zyyy', # 1F940..1F94C ; Common
- 'Zzzz', # 1F94D..1F94F ; Unknown
- 'Zyyy', # 1F950..1F96B ; Common
- 'Zzzz', # 1F96C..1F97F ; Unknown
- 'Zyyy', # 1F980..1F997 ; Common
- 'Zzzz', # 1F998..1F9BF ; Unknown
- 'Zyyy', # 1F9C0..1F9C0 ; Common
- 'Zzzz', # 1F9C1..1F9CF ; Unknown
- 'Zyyy', # 1F9D0..1F9E6 ; Common
- 'Zzzz', # 1F9E7..1FFFF ; Unknown
+ 'Zyyy', # 1F940..1F970 ; Common
+ 'Zzzz', # 1F971..1F972 ; Unknown
+ 'Zyyy', # 1F973..1F976 ; Common
+ 'Zzzz', # 1F977..1F979 ; Unknown
+ 'Zyyy', # 1F97A..1F97A ; Common
+ 'Zzzz', # 1F97B..1F97B ; Unknown
+ 'Zyyy', # 1F97C..1F9A2 ; Common
+ 'Zzzz', # 1F9A3..1F9AF ; Unknown
+ 'Zyyy', # 1F9B0..1F9B9 ; Common
+ 'Zzzz', # 1F9BA..1F9BF ; Unknown
+ 'Zyyy', # 1F9C0..1F9C2 ; Common
+ 'Zzzz', # 1F9C3..1F9CF ; Unknown
+ 'Zyyy', # 1F9D0..1F9FF ; Common
+ 'Zzzz', # 1FA00..1FA5F ; Unknown
+ 'Zyyy', # 1FA60..1FA6D ; Common
+ 'Zzzz', # 1FA6E..1FFFF ; Unknown
'Hani', # 20000..2A6D6 ; Han
'Zzzz', # 2A6D7..2A6FF ; Unknown
'Hani', # 2A700..2B734 ; Han
@@ -3082,6 +3130,7 @@ NAMES = {
'Cprt': 'Cypriot',
'Cyrl': 'Cyrillic',
'Deva': 'Devanagari',
+ 'Dogr': 'Dogra',
'Dsrt': 'Deseret',
'Dupl': 'Duployan',
'Egyp': 'Egyptian_Hieroglyphs',
@@ -3089,6 +3138,7 @@ NAMES = {
'Ethi': 'Ethiopic',
'Geor': 'Georgian',
'Glag': 'Glagolitic',
+ 'Gong': 'Gunjala_Gondi',
'Gonm': 'Masaram_Gondi',
'Goth': 'Gothic',
'Gran': 'Grantha',
@@ -3125,9 +3175,11 @@ NAMES = {
'Lyci': 'Lycian',
'Lydi': 'Lydian',
'Mahj': 'Mahajani',
+ 'Maka': 'Makasar',
'Mand': 'Mandaic',
'Mani': 'Manichaean',
'Marc': 'Marchen',
+ 'Medf': 'Medefaidrin',
'Mend': 'Mende_Kikakui',
'Merc': 'Meroitic_Cursive',
'Mero': 'Meroitic_Hieroglyphs',
@@ -3159,6 +3211,7 @@ NAMES = {
'Plrd': 'Miao',
'Prti': 'Inscriptional_Parthian',
'Rjng': 'Rejang',
+ 'Rohg': 'Hanifi_Rohingya',
'Runr': 'Runic',
'Samr': 'Samaritan',
'Sarb': 'Old_South_Arabian',
@@ -3169,6 +3222,8 @@ NAMES = {
'Sidd': 'Siddham',
'Sind': 'Khudawadi',
'Sinh': 'Sinhala',
+ 'Sogd': 'Sogdian',
+ 'Sogo': 'Old_Sogdian',
'Sora': 'Sora_Sompeng',
'Soyo': 'Soyombo',
'Sund': 'Sundanese',
diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py
index 0a69dbd4..79462c76 100644
--- a/Lib/fontTools/unicodedata/__init__.py
+++ b/Lib/fontTools/unicodedata/__init__.py
@@ -75,7 +75,7 @@ def script_extension(char):
>>> script_extension("a") == {'Latn'}
True
- >>> script_extension(unichr(0x060C)) == {'Arab', 'Syrc', 'Thaa'}
+ >>> script_extension(unichr(0x060C)) == {'Arab', 'Rohg', 'Syrc', 'Thaa'}
True
>>> script_extension(unichr(0x10FFFF)) == {'Zzzz'}
True
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 5f0f9bd2..437324e0 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -31,11 +31,13 @@ from fontTools.ttLib.tables.ttProgram import Program
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import OTTableWriter
-from fontTools.varLib import builder, designspace, models, varStore
+from fontTools.varLib import builder, models, varStore
from fontTools.varLib.merger import VariationMerger, _all_equal
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.iup import iup_delta_optimize
-from collections import OrderedDict
+from fontTools.varLib.featureVars import addFeatureVariations
+from fontTools.designspaceLib import DesignSpaceDocument, AxisDescriptor
+from collections import OrderedDict, namedtuple
import os.path
import logging
from pprint import pformat
@@ -73,7 +75,7 @@ def _add_fvar(font, axes, instances):
axis.axisTag = Tag(a.tag)
# TODO Skip axes that have no variation.
axis.minValue, axis.defaultValue, axis.maxValue = a.minimum, a.default, a.maximum
- axis.axisNameID = nameTable.addName(tounicode(a.labelname['en']))
+ axis.axisNameID = nameTable.addName(tounicode(a.labelNames['en']))
# TODO:
# Replace previous line with the following when the following issues are resolved:
# https://github.com/fonttools/fonttools/issues/930
@@ -82,9 +84,9 @@ def _add_fvar(font, axes, instances):
fvar.axes.append(axis)
for instance in instances:
- coordinates = instance['location']
- name = tounicode(instance['stylename'])
- psname = instance.get('postscriptfontname')
+ coordinates = instance.location
+ name = tounicode(instance.styleName)
+ psname = instance.postScriptFontName
inst = NamedInstance()
inst.subfamilyNameID = nameTable.addName(name)
@@ -104,7 +106,7 @@ def _add_avar(font, axes):
"""
Add 'avar' table to font.
- axes is an ordered dictionary of DesignspaceAxis objects.
+ axes is an ordered dictionary of AxisDescriptor objects.
"""
assert axes
@@ -127,7 +129,7 @@ def _add_avar(font, axes):
if not axis.map:
continue
- items = sorted(axis.map.items())
+ items = sorted(axis.map)
keys = [item[0] for item in items]
vals = [item[1] for item in items]
@@ -566,52 +568,59 @@ def _merge_OTL(font, model, master_fonts, axisTags):
font['GPOS'].table.remap_device_varidxes(varidx_map)
+def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules):
+
+ def normalize(name, value):
+ return models.normalizeLocation(
+ {name: value}, internal_axis_supports
+ )[name]
+
+ log.info("Generating GSUB FeatureVariations")
+
+ axis_tags = {name: axis.tag for name, axis in axes.items()}
+
+ conditional_subs = []
+ for rule in rules:
+
+ region = []
+ for conditions in rule.conditionSets:
+ space = {}
+ for condition in conditions:
+ axis_name = condition["name"]
+ minimum = normalize(axis_name, condition["minimum"])
+ maximum = normalize(axis_name, condition["maximum"])
+ tag = axis_tags[axis_name]
+ space[tag] = (minimum, maximum)
+ region.append(space)
+
+ subs = {k: v for k, v in rule.subs}
+
+ conditional_subs.append((region, subs))
+
+ addFeatureVariations(font, conditional_subs)
+
-# Pretty much all of this file should be redesigned and moved inot submodules...
-# Such a mess right now, but kludging along...
-class _DesignspaceAxis(object):
-
- def __repr__(self):
- return repr(self.__dict__)
-
- @staticmethod
- def _map(v, map):
- keys = map.keys()
- if not keys:
- return v
- if v in keys:
- return map[v]
- k = min(keys)
- if v < k:
- return v + map[k] - k
- k = max(keys)
- if v > k:
- return v + map[k] - k
- # Interpolate
- a = max(k for k in keys if k < v)
- b = min(k for k in keys if k > v)
- va = map[a]
- vb = map[b]
- return va + (vb - va) * (v - a) / (b - a)
-
- def map_forward(self, v):
- if self.map is None: return v
- return self._map(v, self.map)
-
- def map_backward(self, v):
- if self.map is None: return v
- map = {v:k for k,v in self.map.items()}
- return self._map(v, map)
+_DesignSpaceData = namedtuple(
+ "_DesignSpaceData",
+ [
+ "axes",
+ "internal_axis_supports",
+ "base_idx",
+ "normalized_master_locs",
+ "masters",
+ "instances",
+ "rules",
+ ],
+)
def load_designspace(designspace_filename):
- ds = designspace.load(designspace_filename)
- axes = ds.get('axes')
- masters = ds.get('sources')
+ ds = DesignSpaceDocument.fromfile(designspace_filename)
+ masters = ds.sources
if not masters:
raise VarLibError("no sources found in .designspace")
- instances = ds.get('instances', [])
+ instances = ds.instances
standard_axis_map = OrderedDict([
('weight', ('wght', {'en':'Weight'})),
@@ -620,70 +629,31 @@ def load_designspace(designspace_filename):
('optical', ('opsz', {'en':'Optical Size'})),
])
-
# Setup axes
- axis_objects = OrderedDict()
- if axes is not None:
- for axis_dict in axes:
- axis_name = axis_dict.get('name')
- if not axis_name:
- axis_name = axis_dict['name'] = axis_dict['tag']
- if 'map' not in axis_dict:
- axis_dict['map'] = None
- else:
- axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']}
-
- if axis_name in standard_axis_map:
- if 'tag' not in axis_dict:
- axis_dict['tag'] = standard_axis_map[axis_name][0]
- if 'labelname' not in axis_dict:
- axis_dict['labelname'] = standard_axis_map[axis_name][1].copy()
-
- axis = _DesignspaceAxis()
- for item in ['name', 'tag', 'minimum', 'default', 'maximum', 'map']:
- assert item in axis_dict, 'Axis does not have "%s"' % item
- if 'labelname' not in axis_dict:
- axis_dict['labelname'] = {'en': axis_name}
- axis.__dict__ = axis_dict
- axis_objects[axis_name] = axis
- else:
- # No <axes> element. Guess things...
- base_idx = None
- for i,m in enumerate(masters):
- if 'info' in m and m['info']['copy']:
- assert base_idx is None
- base_idx = i
- assert base_idx is not None, "Cannot find 'base' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document."
-
- master_locs = [o['location'] for o in masters]
- base_loc = master_locs[base_idx]
- axis_names = set(base_loc.keys())
- assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no <axes> element."
-
- for name,(tag,labelname) in standard_axis_map.items():
- if name not in axis_names:
- continue
-
- axis = _DesignspaceAxis()
- axis.name = name
- axis.tag = tag
- axis.labelname = labelname.copy()
- axis.default = base_loc[name]
- axis.minimum = min(m[name] for m in master_locs if name in m)
- axis.maximum = max(m[name] for m in master_locs if name in m)
- axis.map = None
- # TODO Fill in weight / width mapping from OS/2 table? Need loading fonts...
- axis_objects[name] = axis
- del base_idx, base_loc, axis_names, master_locs
- axes = axis_objects
- del axis_objects
- log.info("Axes:\n%s", pformat(axes))
+ axes = OrderedDict()
+ for axis in ds.axes:
+ axis_name = axis.name
+ if not axis_name:
+ assert axis.tag is not None
+ axis_name = axis.name = axis.tag
+
+ if axis_name in standard_axis_map:
+ if axis.tag is None:
+ axis.tag = standard_axis_map[axis_name][0]
+ if not axis.labelNames:
+ axis.labelNames.update(standard_axis_map[axis_name][1])
+ else:
+ assert axis.tag is not None
+ if not axis.labelNames:
+ axis.labelNames["en"] = axis_name
+ axes[axis_name] = axis
+ log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))
# Check all master and instance locations are valid and fill in defaults
for obj in masters+instances:
- obj_name = obj.get('name', obj.get('stylename', ''))
- loc = obj['location']
+ obj_name = obj.name or obj.styleName or ''
+ loc = obj.location
for axis_name in loc.keys():
assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
for axis_name,axis in axes.items():
@@ -693,10 +663,9 @@ def load_designspace(designspace_filename):
v = axis.map_backward(loc[axis_name])
assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)
-
# Normalize master locations
- internal_master_locs = [o['location'] for o in masters]
+ internal_master_locs = [o.location for o in masters]
log.info("Internal master locations:\n%s", pformat(internal_master_locs))
# TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
@@ -709,7 +678,6 @@ def load_designspace(designspace_filename):
normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs]
log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))
-
# Find base master
base_idx = None
for i,m in enumerate(normalized_master_locs):
@@ -719,7 +687,15 @@ def load_designspace(designspace_filename):
assert base_idx is not None, "Base master not found; no master at default location?"
log.info("Index of base master: %s", base_idx)
- return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances
+ return _DesignSpaceData(
+ axes,
+ internal_axis_supports,
+ base_idx,
+ normalized_master_locs,
+ masters,
+ instances,
+ ds.rules,
+ )
def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=True):
@@ -731,33 +707,33 @@ def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=T
binary as to be opened (eg. .ttf or .otf).
"""
- axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances = load_designspace(designspace_filename)
+ ds = load_designspace(designspace_filename)
log.info("Building variable font")
log.info("Loading master fonts")
basedir = os.path.dirname(designspace_filename)
- master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters]
+ master_ttfs = [master_finder(os.path.join(basedir, m.filename)) for m in ds.masters]
master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
# Reload base font as target font
- vf = TTFont(master_ttfs[base_idx])
+ vf = TTFont(master_ttfs[ds.base_idx])
# TODO append masters as named-instances as well; needs .designspace change.
- fvar = _add_fvar(vf, axes, instances)
+ fvar = _add_fvar(vf, ds.axes, ds.instances)
if 'STAT' not in exclude:
- _add_stat(vf, axes)
+ _add_stat(vf, ds.axes)
if 'avar' not in exclude:
- _add_avar(vf, axes)
- del instances
+ _add_avar(vf, ds.axes)
# Map from axis names to axis tags...
- normalized_master_locs = [{axes[k].tag:v for k,v in loc.items()} for loc in normalized_master_locs]
- #del axes
+ normalized_master_locs = [
+ {ds.axes[k].tag: v for k,v in loc.items()} for loc in ds.normalized_master_locs
+ ]
# From here on, we use fvar axes only
axisTags = [axis.axisTag for axis in fvar.axes]
# Assume single-model for now.
model = models.VariationModel(normalized_master_locs, axisOrder=axisTags)
- assert 0 == model.mapping[base_idx]
+ assert 0 == model.mapping[ds.base_idx]
log.info("Building variations tables")
if 'MVAR' not in exclude:
@@ -770,6 +746,8 @@ def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=T
_add_gvar(vf, model, master_fonts, optimize=optimize)
if 'cvar' not in exclude and 'glyf' in vf:
_merge_TTHinting(vf, model, master_fonts)
+ if 'GSUB' not in exclude and ds.rules:
+ _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules)
for tag in exclude:
if tag in vf:
diff --git a/Lib/fontTools/varLib/designspace.py b/Lib/fontTools/varLib/designspace.py
deleted file mode 100644
index 7f235af0..00000000
--- a/Lib/fontTools/varLib/designspace.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""Rudimentary support for loading MutatorMath .designspace files."""
-from __future__ import print_function, division, absolute_import
-from fontTools.misc.py23 import *
-try:
- import xml.etree.cElementTree as ET
-except ImportError:
- import xml.etree.ElementTree as ET
-
-__all__ = ['load', 'loads']
-
-namespaces = {'xml': '{http://www.w3.org/XML/1998/namespace}'}
-
-
-def _xml_parse_location(et):
- loc = {}
- for dim in et.find('location'):
- assert dim.tag == 'dimension'
- name = dim.attrib['name']
- value = float(dim.attrib['xvalue'])
- assert name not in loc
- loc[name] = value
- return loc
-
-
-def _load_item(et):
- item = dict(et.attrib)
- for element in et:
- if element.tag == 'location':
- value = _xml_parse_location(et)
- else:
- value = {}
- if 'copy' in element.attrib:
- value['copy'] = bool(int(element.attrib['copy']))
- # TODO load more?!
- item[element.tag] = value
- return item
-
-
-def _xml_parse_axis_or_map(element):
- dic = {}
- for name in element.attrib:
- if name in ['name', 'tag']:
- dic[name] = element.attrib[name]
- else:
- dic[name] = float(element.attrib[name])
- return dic
-
-
-def _load_axis(et):
- item = _xml_parse_axis_or_map(et)
- maps = []
- labelnames = {}
- for element in et:
- assert element.tag in ['labelname', 'map']
- if element.tag == 'labelname':
- lang = element.attrib["{0}lang".format(namespaces['xml'])]
- labelnames[lang] = element.text
- elif element.tag == 'map':
- maps.append(_xml_parse_axis_or_map(element))
- if labelnames:
- item['labelname'] = labelnames
- if maps:
- item['map'] = maps
- return item
-
-
-def _load(et):
- designspace = {}
- ds = et.getroot()
-
- axes_element = ds.find('axes')
- if axes_element is not None:
- axes = []
- for et in axes_element:
- axes.append(_load_axis(et))
- designspace['axes'] = axes
-
- sources_element = ds.find('sources')
- if sources_element is not None:
- sources = []
- for et in sources_element:
- sources.append(_load_item(et))
- designspace['sources'] = sources
-
- instances_element = ds.find('instances')
- if instances_element is not None:
- instances = []
- for et in instances_element:
- instances.append(_load_item(et))
- designspace['instances'] = instances
-
- return designspace
-
-
-def load(filename):
- """Load designspace from a file name or object.
- Returns a dictionary containing three (optional) items:
- - list of "axes"
- - list of "sources" (aka masters)
- - list of "instances"
- """
- return _load(ET.parse(filename))
-
-
-def loads(string):
- """Load designspace from a string."""
- return _load(ET.fromstring(string))
-
-if __name__ == '__main__':
- import sys
- from pprint import pprint
- for f in sys.argv[1:]:
- pprint(load(f))
diff --git a/Lib/fontTools/varLib/interpolate_layout.py b/Lib/fontTools/varLib/interpolate_layout.py
index ca9ccfeb..1ce3fafb 100644
--- a/Lib/fontTools/varLib/interpolate_layout.py
+++ b/Lib/fontTools/varLib/interpolate_layout.py
@@ -26,29 +26,29 @@ def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapp
it is assumed that location is in designspace's internal space and
no mapping is performed.
"""
-
- axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances = load_designspace(designspace_filename)
-
+ ds = load_designspace(designspace_filename)
log.info("Building interpolated font")
log.info("Loading master fonts")
basedir = os.path.dirname(designspace_filename)
- master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters]
+ master_ttfs = [
+ master_finder(os.path.join(basedir, m.filename)) for m in ds.masters
+ ]
master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
- #font = master_fonts[base_idx]
- font = TTFont(master_ttfs[base_idx])
+ #font = master_fonts[ds.base_idx]
+ font = TTFont(master_ttfs[ds.base_idx])
log.info("Location: %s", pformat(loc))
if not mapped:
- loc = {name:axes[name].map_forward(v) for name,v in loc.items()}
+ loc = {name: ds.axes[name].map_forward(v) for name,v in loc.items()}
log.info("Internal location: %s", pformat(loc))
- loc = models.normalizeLocation(loc, internal_axis_supports)
+ loc = models.normalizeLocation(loc, ds.internal_axis_supports)
log.info("Normalized location: %s", pformat(loc))
# Assume single-model for now.
- model = models.VariationModel(normalized_master_locs)
- assert 0 == model.mapping[base_idx]
+ model = models.VariationModel(ds.normalized_master_locs)
+ assert 0 == model.mapping[ds.base_idx]
merger = InstancerMerger(font, model, loc)
diff --git a/Lib/fontTools/varLib/iup.py b/Lib/fontTools/varLib/iup.py
index 912ff0a4..fc36a9f5 100644
--- a/Lib/fontTools/varLib/iup.py
+++ b/Lib/fontTools/varLib/iup.py
@@ -170,8 +170,8 @@ def _iup_contour_bound_forced_set(delta, coords, tolerance=0):
def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=None):
"""Straightforward Dynamic-Programming. For each index i, find least-costly encoding of
- points i to n-1 where i is explicitly encoded. We find this by considering all next
- explicit points j and check whether interpolation can fill points between i and j.
+ points 0 to i where i is explicitly encoded. We find this by considering all previous
+ explicit points j and check whether interpolation can fill points between j and i.
Note that solution always encodes last point explicitly. Higher-level is responsible
for removing that restriction.
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
index 0e4095db..aaee3118 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -705,6 +705,37 @@ class MutatorMerger(AligningMerger):
def instantiate(self):
font = self.font
+ for tableTag in 'GSUB','GPOS':
+ if not tableTag in font:
+ continue
+ table = font[tableTag].table
+ if not hasattr(table, 'FeatureVariations'):
+ continue
+ variations = table.FeatureVariations
+ for record in variations.FeatureVariationRecord:
+ applies = True
+ for condition in record.ConditionSet.ConditionTable:
+ if condition.Format == 1:
+ axisIdx = condition.AxisIndex
+ axisTag = self.font['fvar'].axes[axisIdx].axisTag
+ Min = condition.FilterRangeMinValue
+ Max = condition.FilterRangeMaxValue
+ loc = self.location[axisTag]
+ if not (Min <= loc <= Max):
+ applies = False
+ else:
+ applies = False
+ if not applies:
+ break
+
+ if applies:
+ assert record.FeatureTableSubstitution.Version == 0x00010000
+ for rec in record.FeatureTableSubstitution.SubstitutionRecord:
+ table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
+ break
+ del table.FeatureVariations
+
+
self.mergeTables(font, [font], ['GPOS'])
if 'GDEF' in font:
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py
index 8ea1432a..653b75ff 100644
--- a/Lib/fontTools/varLib/models.py
+++ b/Lib/fontTools/varLib/models.py
@@ -339,6 +339,26 @@ class VariationModel(object):
return self.interpolateFromDeltasAndScalars(deltas, scalars)
+def piecewiseLinearMap(v, mapping):
+ keys = mapping.keys()
+ if not keys:
+ return v
+ if v in keys:
+ return mapping[v]
+ k = min(keys)
+ if v < k:
+ return v + mapping[k] - k
+ k = max(keys)
+ if v > k:
+ return v + mapping[k] - k
+ # Interpolate
+ a = max(k for k in keys if k < v)
+ b = min(k for k in keys if k > v)
+ va = mapping[a]
+ vb = mapping[b]
+ return va + (vb - va) * (v - a) / (b - a)
+
+
def main(args):
from fontTools import configLogger
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
index ac771e8a..7a03e454 100644
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -8,8 +8,10 @@ from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import floatToFixedToFloat, otRound
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
-from fontTools.varLib import _GetCoordinates, _SetCoordinates, _DesignspaceAxis
-from fontTools.varLib.models import supportScalar, normalizeLocation
+from fontTools.varLib import _GetCoordinates, _SetCoordinates
+from fontTools.varLib.models import (
+ supportScalar, normalizeLocation, piecewiseLinearMap
+)
from fontTools.varLib.merger import MutatorMerger
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.varLib.mvar import MVAR_ENTRIES
@@ -50,7 +52,7 @@ def instantiateVariableFont(varfont, location, inplace=False):
loc = normalizeLocation(location, axes)
if 'avar' in varfont:
maps = varfont['avar'].segments
- loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()}
+ loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()}
# Quantize to F2Dot14, to avoid surprise interpolations.
loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
# Location is normalized now
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index c92fa098..ee3a3772 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.2
+Metadata-Version: 2.1
Name: fonttools
-Version: 3.28.0
+Version: 3.31.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -18,7 +18,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
project includes the TTX tool, that can convert TrueType and OpenType
fonts to and from an XML text format, which is also called TTX. It
supports TrueType, OpenType, AFM and to an extent Type 1 and some
- Mac-specific formats. The project has a `MIT open-source
+ Mac-specific formats. The project has an `MIT open-source
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
@@ -36,7 +36,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
pip install fonttools
If you would like to contribute to its development, you can clone the
- repository from Github, install the package in 'editable' mode and
+ repository from GitHub, install the package in 'editable' mode and
modify the source code in place. We recommend creating a virtual
environment, using `virtualenv <https://virtualenv.pypa.io>`__ or
Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module.
@@ -50,7 +50,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
# create new virtual environment called e.g. 'fonttools-venv', or anything you like
python -m virtualenv fonttools-venv
- # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate`
+ # source the `activate` shell script to enter the environment (Un*x); to exit, just type `deactivate`
. fonttools-venv/bin/activate
# to activate the virtual environment in Windows `cmd.exe`, do
@@ -63,7 +63,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Once installed you can use the ``ttx`` command to convert binary font
- files (``.otf``, ``.ttf``, etc) to the TTX xml format, edit them, and
+ files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
convert them back to binary format. TTX files have a .ttx file
extension.
@@ -72,11 +72,11 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
ttx /path/to/font.otf
ttx /path/to/font.ttx
- The TTX application works can be used in two ways, depending on what
+ The TTX application can be used in two ways, depending on what
platform you run it on:
- - As a command line tool (Windows/DOS, Unix, MacOSX)
- - By dropping files onto the application (Windows, MacOS)
+ - As a command line tool (Windows/DOS, Unix, macOS)
+ - By dropping files onto the application (Windows, macOS)
TTX detects what kind of files it is fed: it will output a ``.ttx`` file
when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
@@ -87,8 +87,8 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
number to the output filename (before the extension) such as
``Arial#1.ttf``
- When using TTX from the command line there are a bunch of extra options,
- these are explained in the help text, as displayed when typing
+ When using TTX from the command line there are a bunch of extra options.
+ These are explained in the help text, as displayed when typing
``ttx -h`` at the command prompt. These additional options include:
- specifying the folder where the output files are created
@@ -152,7 +152,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
fontTools Python Module
~~~~~~~~~~~~~~~~~~~~~~~
- The fontTools python module provides a convenient way to
+ The fontTools Python module provides a convenient way to
programmatically edit font files.
.. code:: py
@@ -163,7 +163,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
<fontTools.ttLib.TTFont object at 0x10c34ed50>
>>>
- A selection of sample python programs is in the
+ A selection of sample Python programs is in the
`Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
directory.
@@ -174,105 +174,143 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
besides the modules included in the Python Standard Library.
However, a few extra dependencies are required by some of its modules, which
are needed to unlock optional features.
+ The ``fonttools`` PyPI distribution also supports so-called "extras", i.e. a
+ set of keywords that describe a group of additional dependencies, which can be
+ used when installing via pip, or when specifying a requirement.
+ For example:
- - ``Lib/fontTools/ttLib/woff2.py``
+ .. code:: sh
- Module to compress/decompress WOFF 2.0 web fonts; it requires:
+ pip install fonttools[ufo,lxml,woff,unicode]
- - `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of
- the Brotli compression library.
+ This command will install fonttools, as well as the optional dependencies that
+ are required to unlock the extra features named "ufo", etc.
- - ``Lib/fontTools/ttLib/sfnt.py``
+ - ``Lib/fontTools/misc/etree.py``
- To better compress WOFF 1.0 web fonts, the following module can be used
- instead of the built-in ``zlib`` library:
+ The module exports a ElementTree-like API for reading/writing XML files, and
+ allows to use as the backend either the built-in ``xml.etree`` module or
+ `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+ as it is generally faster and more secure.
- - `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of
- the Zopfli compression library.
+ *Extra:* ``lxml``
- - ``Lib/fontTools/unicode.py``
+ - ``Lib/fontTools/ufoLib``
- To display the Unicode character names when dumping the ``cmap`` table
- with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
- The version included in there varies between different Python versions.
- To use the latest available data, you can install:
+ Package for reading and writing UFO source files; it requires:
- - `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__:
- ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest
- Unicode version 9.0. Note this is not necessary if you use Python 3.6
- as the latter already comes with an up-to-date ``unicodedata``.
+ * `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem
+ abstraction layer.
- - ``Lib/fontTools/varLib/interpolatable.py``
+ * `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum``
+ module (only required on Python < 3.4).
- Module for finding wrong contour/component order between different masters.
- It requires one of the following packages in order to solve the so-called
- "minimum weight perfect matching problem in bipartite graphs", or
- the Assignment problem:
+ *Extra:* ``ufo``
- * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library
- for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__
- arrays and hence is very fast;
- * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python
- module that implements the Hungarian or Kuhn-Munkres algorithm.
+ - ``Lib/fontTools/ttLib/woff2.py``
- - ``Lib/fontTools/misc/symfont.py``
+ Module to compress/decompress WOFF 2.0 web fonts; it requires:
- Advanced module for symbolic font statistics analysis; it requires:
+ * `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of
+ the Brotli compression library.
- * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for
- symbolic mathematics.
+ *Extra:* ``woff``
- - ``Lib/fontTools/t1Lib.py``
+ - ``Lib/fontTools/ttLib/sfnt.py``
- To get the file creator and type of Macintosh PostScript Type 1 fonts
- on Python 3 you need to install the following module, as the old ``MacOS``
- module is no longer included in Mac Python:
+ To better compress WOFF 1.0 web fonts, the following module can be used
+ instead of the built-in ``zlib`` library:
- * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for
- extended filesystem attributes (macOS platform only).
+ * `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of
+ the Zopfli compression library.
- - ``Lib/fontTools/pens/cocoaPen.py``
+ *Extra:* ``woff``
- Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+ - ``Lib/fontTools/unicode.py``
- * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between
- Python and the Objective-C runtime (macOS platform only).
+ To display the Unicode character names when dumping the ``cmap`` table
+ with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
+ The version included in there varies between different Python versions.
+ To use the latest available data, you can install:
- - ``Lib/fontTools/pens/qtPen.py``
+ * `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__:
+ ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest
+ Unicode version 9.0. Note this is not necessary if you use Python 3.6
+ as the latter already comes with an up-to-date ``unicodedata``.
- Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+ *Extra:* ``unicode``
- * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for
- the Qt cross platform UI and application toolkit.
+ - ``Lib/fontTools/varLib/interpolatable.py``
- - ``Lib/fontTools/pens/reportLabPen.py``
+ Module for finding wrong contour/component order between different masters.
+ It requires one of the following packages in order to solve the so-called
+ "minimum weight perfect matching problem in bipartite graphs", or
+ the Assignment problem:
- Pen to drawing glyphs as PNG images, requires:
+ * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library
+ for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__
+ arrays and hence is very fast;
+ * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python
+ module that implements the Hungarian or Kuhn-Munkres algorithm.
- * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
- for generating PDFs and graphics.
+ *Extra:* ``interpolatable``
- - ``Lib/fontTools/inspect.py``
+ - ``Lib/fontTools/misc/symfont.py``
- A GUI font inspector, requires one of the following packages:
+ Advanced module for symbolic font statistics analysis; it requires:
- * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for
- GTK  2.x (only works with Python 2).
- * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ :
- Python bindings for GTK 3.x and gobject-introspection libraries (also
- compatible with Python 3).
+ * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for
+ symbolic mathematics.
- Testing
- ~~~~~~~
+ *Extra:* ``symfont``
- To run the test suite, you can do:
+ - ``Lib/fontTools/t1Lib.py``
- .. code:: sh
+ To get the file creator and type of Macintosh PostScript Type 1 fonts
+ on Python 3 you need to install the following module, as the old ``MacOS``
+ module is no longer included in Mac Python:
+
+ * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for
+ extended filesystem attributes (macOS platform only).
+
+ *Extra:* ``type1``
+
+ - ``Lib/fontTools/pens/cocoaPen.py``
+
+ Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+
+ * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between
+ Python and the Objective-C runtime (macOS platform only).
+
+ - ``Lib/fontTools/pens/qtPen.py``
+
+ Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+
+ * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for
+ the Qt cross platform UI and application toolkit.
+
+ - ``Lib/fontTools/pens/reportLabPen.py``
+
+ Pen to drawing glyphs as PNG images, requires:
+
+ * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
+ for generating PDFs and graphics.
- python setup.py test
+ - ``Lib/fontTools/inspect.py``
- If you have `pytest <http://docs.pytest.org/en/latest/>`__, you can run
- the ``pytest`` command directly. The tests will run against the
+ A GUI font inspector, requires one of the following packages:
+
+ * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for
+ GTK  2.x (only works with Python 2).
+ * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ :
+ Python bindings for GTK 3.x and gobject-introspection libraries (also
+ compatible with Python 3).
+
+ Testing
+ ~~~~~~~
+
+ To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
+ When you run the ``pytest`` command, the tests will run against the
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
@@ -287,15 +325,15 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Note that when you run ``tox`` without arguments, the tests are executed
for all the environments listed in tox.ini's ``envlist``. In our case,
- this includes Python 2.7 and 3.6, so for this to work the ``python2.7``
- and ``python3.6`` executables must be available in your ``PATH``.
+ this includes Python 2.7 and 3.7, so for this to work the ``python2.7``
+ and ``python3.7`` executables must be available in your ``PATH``.
You can specify an alternative environment list via the ``-e`` option,
or the ``TOXENV`` environment variable:
.. code:: sh
- tox -e py27-nocov
+ tox -e py27
TOXENV="py36-cov,htmlcov" tox
Development Community
@@ -377,6 +415,86 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Changelog
~~~~~~~~~
+ 3.31.0 (released 2018-10-21)
+ ----------------------------
+
+ - [ufoLib] Merged the `ufoLib <https://github.com/unified-font-objects/ufoLib>`__
+ master branch into a new ``fontTools.ufoLib`` package (#1335, #1095).
+ Moved ``ufoLib.pointPen`` module to ``fontTools.pens.pointPen``.
+ Moved ``ufoLib.etree`` module to ``fontTools.misc.etree``.
+ Moved ``ufoLib.plistlib`` module to ``fontTools.misc.plistlib``.
+ To use the new ``fontTools.ufoLib`` module you need to install fonttools
+ with the ``[ufo]`` extra, or you can manually install the required additional
+ dependencies (cf. README.rst).
+ - [morx] Support AAT action type to insert glyphs and clean up compilation
+ of AAT action tables (4a1871f, 2011ccf).
+ - [subset] The ``--no-hinting`` on a CFF font now also drops the optional
+ hinting keys in Private dict: ``ForceBold``, ``LanguageGroup``, and
+ ``ExpansionFactor`` (#1322).
+ - [subset] Include nameIDs referenced by STAT table (#1327).
+ - [loggingTools] Added ``msg=None`` argument to
+ ``CapturingLogHandler.assertRegex`` (0245f2c).
+ - [varLib.mutator] Implemented ``FeatureVariations`` instantiation (#1244).
+ - [g_l_y_f] Added PointPen support to ``_TTGlyph`` objects (#1334).
+
+ 3.30.0 (released 2018-09-18)
+ ----------------------------
+
+ - [feaLib] Skip building noop class PairPos subtables when Coverage is NULL
+ (#1318).
+ - [ttx] Expose the previously reserved bit flag ``OVERLAP_SIMPLE`` of
+ glyf table's contour points in the TTX dump. This is used in some
+ implementations to specify a non-zero fill with overlapping contours (#1316).
+ - [ttLib] Added support for decompiling/compiling ``TS1C`` tables containing
+ VTT sources for ``cvar`` variation table (#1310).
+ - [varLib] Use ``fontTools.designspaceLib`` to read DesignSpaceDocument. The
+ ``fontTools.varLib.designspace`` module is now deprecated and will be removed
+ in future versions. The presence of an explicit ``axes`` element is now
+ required in order to build a variable font (#1224, #1313).
+ - [varLib] Implemented building GSUB FeatureVariations table from the ``rules``
+ element of DesignSpace document (#1240, #713, #1314).
+ - [subset] Added ``--no-layout-closure`` option to not expand the subset with
+ the glyphs produced by OpenType layout features. Instead, OpenType features
+ will be subset to only rules that are relevant to the otherwise-specified
+ glyph set (#43, #1121).
+
+ 3.29.1 (released 2018-09-10)
+ ----------------------------
+
+ - [feaLib] Fixed issue whereby lookups from DFLT/dflt were not included in the
+ DFLT/non-dflt language systems (#1307).
+ - [graphite] Fixed issue on big-endian architectures (e.g. ppc64) (#1311).
+ - [subset] Added ``--layout-scripts`` option to add/exclude set of OpenType
+ layout scripts that will be preserved. By default all scripts are retained
+ (``'*'``) (#1303).
+
+ 3.29.0 (released 2018-07-26)
+ ----------------------------
+
+ - [feaLib] In the OTL table builder, when the ``name`` table is excluded
+ from the list of tables to be build, skip compiling ``featureNames`` blocks,
+ as the records referenced in ``FeatureParams`` table don't exist (68951b7).
+ - [otBase] Try ``ExtensionLookup`` if other offset-overflow methods fail
+ (05f95f0).
+ - [feaLib] Added support for explicit ``subtable;`` break statements in
+ PairPos lookups; previously these were ignored (#1279, #1300, #1302).
+ - [cffLib.specializer] Make sure the stack depth does not exceed maxstack - 1,
+ so that a subroutinizer can insert subroutine calls (#1301,
+ https://github.com/googlei18n/ufo2ft/issues/266).
+ - [otTables] Added support for fixing offset overflow errors occurring inside
+ ``MarkBasePos`` subtables (#1297).
+ - [subset] Write the default output file extension based on ``--flavor`` option,
+ or the value of ``TTFont.sfntVersion`` (d7ac0ad).
+ - [unicodedata] Updated Blocks, Scripts and ScriptExtensions for Unicode 11
+ (452c85e).
+ - [xmlWriter] Added context manager to XMLWriter class to autoclose file
+ descriptor on exit (#1290).
+ - [psCharStrings] Optimize the charstring's bytecode by encoding as integers
+ all float values that have no decimal portion (8d7774a).
+ - [ttFont] Fixed missing import of ``TTLibError`` exception (#1285).
+ - [feaLib] Allow any languages other than ``dflt`` under ``DFLT`` script
+ (#1278, #1292).
+
3.28.0 (released 2018-06-19)
----------------------------
@@ -1373,3 +1491,11 @@ Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
+Provides-Extra: type1
+Provides-Extra: lxml
+Provides-Extra: unicode
+Provides-Extra: symfont
+Provides-Extra: all
+Provides-Extra: ufo
+Provides-Extra: woff
+Provides-Extra: interpolatable
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index fa92e3eb..ef8491df 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -16,7 +16,6 @@ setup.cfg
setup.py
tox.ini
.travis/after_success.sh
-.travis/before_deploy.sh
.travis/before_install.sh
.travis/install.sh
.travis/run.sh
@@ -72,6 +71,11 @@ Doc/source/ttLib/macUtils.rst
Doc/source/ttLib/sfnt.rst
Doc/source/ttLib/tables.rst
Doc/source/ttLib/woff2.rst
+Doc/source/ufoLib/converters.rst
+Doc/source/ufoLib/filenames.rst
+Doc/source/ufoLib/glifLib.rst
+Doc/source/ufoLib/pointPen.rst
+Doc/source/ufoLib/ufoLib.rst
Doc/source/varLib/designspace.rst
Doc/source/varLib/index.rst
Doc/source/varLib/interpolatable.rst
@@ -109,11 +113,13 @@ Lib/fontTools/misc/classifyTools.py
Lib/fontTools/misc/cliTools.py
Lib/fontTools/misc/eexec.py
Lib/fontTools/misc/encodingTools.py
+Lib/fontTools/misc/etree.py
Lib/fontTools/misc/filenames.py
Lib/fontTools/misc/fixedTools.py
Lib/fontTools/misc/loggingTools.py
Lib/fontTools/misc/macCreatorType.py
Lib/fontTools/misc/macRes.py
+Lib/fontTools/misc/plistlib.py
Lib/fontTools/misc/psCharStrings.py
Lib/fontTools/misc/psLib.py
Lib/fontTools/misc/psOperators.py
@@ -139,6 +145,7 @@ Lib/fontTools/pens/filterPen.py
Lib/fontTools/pens/momentsPen.py
Lib/fontTools/pens/perimeterPen.py
Lib/fontTools/pens/pointInsidePen.py
+Lib/fontTools/pens/pointPen.py
Lib/fontTools/pens/qtPen.py
Lib/fontTools/pens/recordingPen.py
Lib/fontTools/pens/reportLabPen.py
@@ -197,6 +204,7 @@ Lib/fontTools/ttLib/tables/S_V_G_.py
Lib/fontTools/ttLib/tables/S__i_l_f.py
Lib/fontTools/ttLib/tables/S__i_l_l.py
Lib/fontTools/ttLib/tables/T_S_I_B_.py
+Lib/fontTools/ttLib/tables/T_S_I_C_.py
Lib/fontTools/ttLib/tables/T_S_I_D_.py
Lib/fontTools/ttLib/tables/T_S_I_J_.py
Lib/fontTools/ttLib/tables/T_S_I_P_.py
@@ -258,6 +266,17 @@ Lib/fontTools/ttLib/tables/sbixGlyph.py
Lib/fontTools/ttLib/tables/sbixStrike.py
Lib/fontTools/ttLib/tables/table_API_readme.txt
Lib/fontTools/ttLib/tables/ttProgram.py
+Lib/fontTools/ufoLib/__init__.py
+Lib/fontTools/ufoLib/converters.py
+Lib/fontTools/ufoLib/errors.py
+Lib/fontTools/ufoLib/etree.py
+Lib/fontTools/ufoLib/filenames.py
+Lib/fontTools/ufoLib/glifLib.py
+Lib/fontTools/ufoLib/kerning.py
+Lib/fontTools/ufoLib/plistlib.py
+Lib/fontTools/ufoLib/pointPen.py
+Lib/fontTools/ufoLib/utils.py
+Lib/fontTools/ufoLib/validators.py
Lib/fontTools/unicodedata/Blocks.py
Lib/fontTools/unicodedata/OTTags.py
Lib/fontTools/unicodedata/ScriptExtensions.py
@@ -266,7 +285,6 @@ Lib/fontTools/unicodedata/__init__.py
Lib/fontTools/varLib/__init__.py
Lib/fontTools/varLib/__main__.py
Lib/fontTools/varLib/builder.py
-Lib/fontTools/varLib/designspace.py
Lib/fontTools/varLib/featureVars.py
Lib/fontTools/varLib/interpolatable.py
Lib/fontTools/varLib/interpolate_layout.py
@@ -286,6 +304,7 @@ Lib/fonttools.egg-info/PKG-INFO
Lib/fonttools.egg-info/SOURCES.txt
Lib/fonttools.egg-info/dependency_links.txt
Lib/fonttools.egg-info/entry_points.txt
+Lib/fonttools.egg-info/requires.txt
Lib/fonttools.egg-info/top_level.txt
MetaTools/buildTableList.py
MetaTools/buildUCD.py
@@ -371,6 +390,8 @@ Tests/feaLib/data/ZeroValue_SinglePos_vertical.fea
Tests/feaLib/data/ZeroValue_SinglePos_vertical.ttx
Tests/feaLib/data/baseClass.fea
Tests/feaLib/data/baseClass.feax
+Tests/feaLib/data/bug1307.fea
+Tests/feaLib/data/bug1307.ttx
Tests/feaLib/data/bug453.fea
Tests/feaLib/data/bug453.ttx
Tests/feaLib/data/bug457.fea
@@ -504,10 +525,12 @@ Tests/misc/bezierTools_test.py
Tests/misc/classifyTools_test.py
Tests/misc/eexec_test.py
Tests/misc/encodingTools_test.py
+Tests/misc/etree_test.py
Tests/misc/filenames_test.py
Tests/misc/fixedTools_test.py
Tests/misc/loggingTools_test.py
Tests/misc/macRes_test.py
+Tests/misc/plistlib_test.py
Tests/misc/psCharStrings_test.py
Tests/misc/py23_test.py
Tests/misc/testTools_test.py
@@ -516,6 +539,7 @@ Tests/misc/timeTools_test.py
Tests/misc/transform_test.py
Tests/misc/xmlReader_test.py
Tests/misc/xmlWriter_test.py
+Tests/misc/testdata/test.plist
Tests/mtiLib/mti_test.py
Tests/mtiLib/data/featurename-backward.ttx.GSUB
Tests/mtiLib/data/featurename-backward.txt
@@ -689,6 +713,11 @@ Tests/ttLib/tables/data/C_F_F_.bin
Tests/ttLib/tables/data/C_F_F_.ttx
Tests/ttLib/tables/data/C_F_F__2.bin
Tests/ttLib/tables/data/C_F_F__2.ttx
+Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.glyf.bin
+Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.head.bin
+Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.loca.bin
+Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.maxp.bin
+Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.ttx
Tests/ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx
Tests/ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx
Tests/ttLib/tables/data/_h_h_e_a_recalc_empty.ttx
@@ -1285,9 +1314,75 @@ Tests/ttx/data/TestTTF.ttf
Tests/ttx/data/TestTTF.ttx
Tests/ttx/data/TestWOFF.woff
Tests/ttx/data/TestWOFF2.woff2
+Tests/ufoLib/GLIF1_test.py
+Tests/ufoLib/GLIF2_test.py
+Tests/ufoLib/UFO1_test.py
+Tests/ufoLib/UFO2_test.py
+Tests/ufoLib/UFO3_test.py
+Tests/ufoLib/UFOConversion_test.py
+Tests/ufoLib/UFOZ_test.py
+Tests/ufoLib/__init__.py
+Tests/ufoLib/filenames_test.py
+Tests/ufoLib/glifLib_test.py
+Tests/ufoLib/testSupport.py
+Tests/ufoLib/testdata/DemoFont.ufo/fontinfo.plist
+Tests/ufoLib/testdata/DemoFont.ufo/lib.plist
+Tests/ufoLib/testdata/DemoFont.ufo/metainfo.plist
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/A_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/B_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F__A__B_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/G_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/O_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/R_.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/a.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/contents.plist
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.glif
+Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/fontinfo.plist
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/groups.plist
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/kerning.plist
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/lib.plist
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/metainfo.plist
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif
+Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/features.fea
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/fontinfo.plist
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/groups.plist
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/kerning.plist
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/lib.plist
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/metainfo.plist
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif
+Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/fontinfo.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/kerning.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/layercontents.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/lib.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/metainfo.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif
+Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif
+Tests/ufoLib/testdata/UFO3-Read Data.ufo/metainfo.plist
+Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt
+Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt
+Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt
Tests/varLib/__init__.py
Tests/varLib/builder_test.py
-Tests/varLib/designspace_test.py
Tests/varLib/interpolatable_test.py
Tests/varLib/interpolate_layout_test.py
Tests/varLib/models_test.py
@@ -1297,8 +1392,7 @@ Tests/varLib/data/Build.designspace
Tests/varLib/data/BuildAvarEmptyAxis.designspace
Tests/varLib/data/BuildAvarIdentityMaps.designspace
Tests/varLib/data/BuildAvarSingleAxis.designspace
-Tests/varLib/data/Designspace.designspace
-Tests/varLib/data/Designspace2.designspace
+Tests/varLib/data/FeatureVars.designspace
Tests/varLib/data/InterpolateLayout.designspace
Tests/varLib/data/InterpolateLayout2.designspace
Tests/varLib/data/InterpolateLayout3.designspace
@@ -1542,11 +1636,11 @@ Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif
Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif
Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif
Tests/varLib/data/test_results/Build.ttx
-Tests/varLib/data/test_results/Build3.ttx
Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx
Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx
Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx
Tests/varLib/data/test_results/BuildMain.ttx
+Tests/varLib/data/test_results/FeatureVars.ttx
Tests/varLib/data/test_results/InterpolateLayout.ttx
Tests/varLib/data/test_results/InterpolateLayout2.ttx
Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx
diff --git a/Lib/fonttools.egg-info/requires.txt b/Lib/fonttools.egg-info/requires.txt
new file mode 100644
index 00000000..c7eb2e13
--- /dev/null
+++ b/Lib/fonttools.egg-info/requires.txt
@@ -0,0 +1,66 @@
+
+[all]
+fs<3,>=2.1.1
+lxml<5,>=4.0
+zopfli>=0.1.4
+sympy
+
+[all:platform_python_implementation != "PyPy"]
+brotli>=1.0.1
+scipy
+
+[all:platform_python_implementation == "PyPy"]
+brotlipy>=0.7.0
+munkres
+
+[all:python_version < "3.4"]
+enum34>=1.1.6
+singledispatch>=3.4.0.3
+
+[all:python_version < "3.7" and platform_python_implementation != "PyPy"]
+unicodedata2>=11.0.0
+
+[all:sys_platform == "darwin"]
+xattr
+
+[interpolatable]
+
+[interpolatable:platform_python_implementation != "PyPy"]
+scipy
+
+[interpolatable:platform_python_implementation == "PyPy"]
+munkres
+
+[lxml]
+lxml<5,>=4.0
+
+[lxml:python_version < "3.4"]
+singledispatch>=3.4.0.3
+
+[symfont]
+sympy
+
+[type1]
+
+[type1:sys_platform == "darwin"]
+xattr
+
+[ufo]
+fs<3,>=2.1.1
+
+[ufo:python_version < "3.4"]
+enum34>=1.1.6
+
+[unicode]
+
+[unicode:python_version < "3.7" and platform_python_implementation != "PyPy"]
+unicodedata2>=11.0.0
+
+[woff]
+zopfli>=0.1.4
+
+[woff:platform_python_implementation != "PyPy"]
+brotli>=1.0.1
+
+[woff:platform_python_implementation == "PyPy"]
+brotlipy>=0.7.0
diff --git a/METADATA b/METADATA
index f5503e3c..f017a666 100644
--- a/METADATA
+++ b/METADATA
@@ -7,12 +7,12 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/3.28.0/fonttools-3.28.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/3.31.0/fonttools-3.31.0.zip"
}
- version: "3.28.0"
+ version: "3.31.0"
last_upgrade_date {
year: 2018
- month: 7
- day: 3
+ month: 10
+ day: 30
}
}
diff --git a/NEWS.rst b/NEWS.rst
index 5341ca52..16c2a906 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,83 @@
+3.31.0 (released 2018-10-21)
+----------------------------
+
+- [ufoLib] Merged the `ufoLib <https://github.com/unified-font-objects/ufoLib>`__
+ master branch into a new ``fontTools.ufoLib`` package (#1335, #1095).
+ Moved ``ufoLib.pointPen`` module to ``fontTools.pens.pointPen``.
+ Moved ``ufoLib.etree`` module to ``fontTools.misc.etree``.
+ Moved ``ufoLib.plistlib`` module to ``fontTools.misc.plistlib``.
+ To use the new ``fontTools.ufoLib`` module you need to install fonttools
+ with the ``[ufo]`` extra, or you can manually install the required additional
+ dependencies (cf. README.rst).
+- [morx] Support AAT action type to insert glyphs and clean up compilation
+ of AAT action tables (4a1871f, 2011ccf).
+- [subset] The ``--no-hinting`` on a CFF font now also drops the optional
+ hinting keys in Private dict: ``ForceBold``, ``LanguageGroup``, and
+ ``ExpansionFactor`` (#1322).
+- [subset] Include nameIDs referenced by STAT table (#1327).
+- [loggingTools] Added ``msg=None`` argument to
+ ``CapturingLogHandler.assertRegex`` (0245f2c).
+- [varLib.mutator] Implemented ``FeatureVariations`` instantiation (#1244).
+- [g_l_y_f] Added PointPen support to ``_TTGlyph`` objects (#1334).
+
+3.30.0 (released 2018-09-18)
+----------------------------
+
+- [feaLib] Skip building noop class PairPos subtables when Coverage is NULL
+ (#1318).
+- [ttx] Expose the previously reserved bit flag ``OVERLAP_SIMPLE`` of
+ glyf table's contour points in the TTX dump. This is used in some
+ implementations to specify a non-zero fill with overlapping contours (#1316).
+- [ttLib] Added support for decompiling/compiling ``TS1C`` tables containing
+ VTT sources for ``cvar`` variation table (#1310).
+- [varLib] Use ``fontTools.designspaceLib`` to read DesignSpaceDocument. The
+ ``fontTools.varLib.designspace`` module is now deprecated and will be removed
+ in future versions. The presence of an explicit ``axes`` element is now
+ required in order to build a variable font (#1224, #1313).
+- [varLib] Implemented building GSUB FeatureVariations table from the ``rules``
+ element of DesignSpace document (#1240, #713, #1314).
+- [subset] Added ``--no-layout-closure`` option to not expand the subset with
+ the glyphs produced by OpenType layout features. Instead, OpenType features
+ will be subset to only rules that are relevant to the otherwise-specified
+ glyph set (#43, #1121).
+
+3.29.1 (released 2018-09-10)
+----------------------------
+
+- [feaLib] Fixed issue whereby lookups from DFLT/dflt were not included in the
+ DFLT/non-dflt language systems (#1307).
+- [graphite] Fixed issue on big-endian architectures (e.g. ppc64) (#1311).
+- [subset] Added ``--layout-scripts`` option to add/exclude set of OpenType
+ layout scripts that will be preserved. By default all scripts are retained
+ (``'*'``) (#1303).
+
+3.29.0 (released 2018-07-26)
+----------------------------
+
+- [feaLib] In the OTL table builder, when the ``name`` table is excluded
+ from the list of tables to be build, skip compiling ``featureNames`` blocks,
+ as the records referenced in ``FeatureParams`` table don't exist (68951b7).
+- [otBase] Try ``ExtensionLookup`` if other offset-overflow methods fail
+ (05f95f0).
+- [feaLib] Added support for explicit ``subtable;`` break statements in
+ PairPos lookups; previously these were ignored (#1279, #1300, #1302).
+- [cffLib.specializer] Make sure the stack depth does not exceed maxstack - 1,
+ so that a subroutinizer can insert subroutine calls (#1301,
+ https://github.com/googlei18n/ufo2ft/issues/266).
+- [otTables] Added support for fixing offset overflow errors occurring inside
+ ``MarkBasePos`` subtables (#1297).
+- [subset] Write the default output file extension based on ``--flavor`` option,
+ or the value of ``TTFont.sfntVersion`` (d7ac0ad).
+- [unicodedata] Updated Blocks, Scripts and ScriptExtensions for Unicode 11
+ (452c85e).
+- [xmlWriter] Added context manager to XMLWriter class to autoclose file
+ descriptor on exit (#1290).
+- [psCharStrings] Optimize the charstring's bytecode by encoding as integers
+ all float values that have no decimal portion (8d7774a).
+- [ttFont] Fixed missing import of ``TTLibError`` exception (#1285).
+- [feaLib] Allow any languages other than ``dflt`` under ``DFLT`` script
+ (#1278, #1292).
+
3.28.0 (released 2018-06-19)
----------------------------
diff --git a/PKG-INFO b/PKG-INFO
index c92fa098..ee3a3772 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.2
+Metadata-Version: 2.1
Name: fonttools
-Version: 3.28.0
+Version: 3.31.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -18,7 +18,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
project includes the TTX tool, that can convert TrueType and OpenType
fonts to and from an XML text format, which is also called TTX. It
supports TrueType, OpenType, AFM and to an extent Type 1 and some
- Mac-specific formats. The project has a `MIT open-source
+ Mac-specific formats. The project has an `MIT open-source
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
@@ -36,7 +36,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
pip install fonttools
If you would like to contribute to its development, you can clone the
- repository from Github, install the package in 'editable' mode and
+ repository from GitHub, install the package in 'editable' mode and
modify the source code in place. We recommend creating a virtual
environment, using `virtualenv <https://virtualenv.pypa.io>`__ or
Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module.
@@ -50,7 +50,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
# create new virtual environment called e.g. 'fonttools-venv', or anything you like
python -m virtualenv fonttools-venv
- # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate`
+ # source the `activate` shell script to enter the environment (Un*x); to exit, just type `deactivate`
. fonttools-venv/bin/activate
# to activate the virtual environment in Windows `cmd.exe`, do
@@ -63,7 +63,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Once installed you can use the ``ttx`` command to convert binary font
- files (``.otf``, ``.ttf``, etc) to the TTX xml format, edit them, and
+ files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
convert them back to binary format. TTX files have a .ttx file
extension.
@@ -72,11 +72,11 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
ttx /path/to/font.otf
ttx /path/to/font.ttx
- The TTX application works can be used in two ways, depending on what
+ The TTX application can be used in two ways, depending on what
platform you run it on:
- - As a command line tool (Windows/DOS, Unix, MacOSX)
- - By dropping files onto the application (Windows, MacOS)
+ - As a command line tool (Windows/DOS, Unix, macOS)
+ - By dropping files onto the application (Windows, macOS)
TTX detects what kind of files it is fed: it will output a ``.ttx`` file
when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
@@ -87,8 +87,8 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
number to the output filename (before the extension) such as
``Arial#1.ttf``
- When using TTX from the command line there are a bunch of extra options,
- these are explained in the help text, as displayed when typing
+ When using TTX from the command line there are a bunch of extra options.
+ These are explained in the help text, as displayed when typing
``ttx -h`` at the command prompt. These additional options include:
- specifying the folder where the output files are created
@@ -152,7 +152,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
fontTools Python Module
~~~~~~~~~~~~~~~~~~~~~~~
- The fontTools python module provides a convenient way to
+ The fontTools Python module provides a convenient way to
programmatically edit font files.
.. code:: py
@@ -163,7 +163,7 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
<fontTools.ttLib.TTFont object at 0x10c34ed50>
>>>
- A selection of sample python programs is in the
+ A selection of sample Python programs is in the
`Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
directory.
@@ -174,105 +174,143 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
besides the modules included in the Python Standard Library.
However, a few extra dependencies are required by some of its modules, which
are needed to unlock optional features.
+ The ``fonttools`` PyPI distribution also supports so-called "extras", i.e. a
+ set of keywords that describe a group of additional dependencies, which can be
+ used when installing via pip, or when specifying a requirement.
+ For example:
- - ``Lib/fontTools/ttLib/woff2.py``
+ .. code:: sh
- Module to compress/decompress WOFF 2.0 web fonts; it requires:
+ pip install fonttools[ufo,lxml,woff,unicode]
- - `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of
- the Brotli compression library.
+ This command will install fonttools, as well as the optional dependencies that
+ are required to unlock the extra features named "ufo", etc.
- - ``Lib/fontTools/ttLib/sfnt.py``
+ - ``Lib/fontTools/misc/etree.py``
- To better compress WOFF 1.0 web fonts, the following module can be used
- instead of the built-in ``zlib`` library:
+ The module exports a ElementTree-like API for reading/writing XML files, and
+ allows to use as the backend either the built-in ``xml.etree`` module or
+ `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+ as it is generally faster and more secure.
- - `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of
- the Zopfli compression library.
+ *Extra:* ``lxml``
- - ``Lib/fontTools/unicode.py``
+ - ``Lib/fontTools/ufoLib``
- To display the Unicode character names when dumping the ``cmap`` table
- with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
- The version included in there varies between different Python versions.
- To use the latest available data, you can install:
+ Package for reading and writing UFO source files; it requires:
- - `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__:
- ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest
- Unicode version 9.0. Note this is not necessary if you use Python 3.6
- as the latter already comes with an up-to-date ``unicodedata``.
+ * `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem
+ abstraction layer.
- - ``Lib/fontTools/varLib/interpolatable.py``
+ * `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum``
+ module (only required on Python < 3.4).
- Module for finding wrong contour/component order between different masters.
- It requires one of the following packages in order to solve the so-called
- "minimum weight perfect matching problem in bipartite graphs", or
- the Assignment problem:
+ *Extra:* ``ufo``
- * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library
- for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__
- arrays and hence is very fast;
- * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python
- module that implements the Hungarian or Kuhn-Munkres algorithm.
+ - ``Lib/fontTools/ttLib/woff2.py``
- - ``Lib/fontTools/misc/symfont.py``
+ Module to compress/decompress WOFF 2.0 web fonts; it requires:
- Advanced module for symbolic font statistics analysis; it requires:
+ * `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of
+ the Brotli compression library.
- * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for
- symbolic mathematics.
+ *Extra:* ``woff``
- - ``Lib/fontTools/t1Lib.py``
+ - ``Lib/fontTools/ttLib/sfnt.py``
- To get the file creator and type of Macintosh PostScript Type 1 fonts
- on Python 3 you need to install the following module, as the old ``MacOS``
- module is no longer included in Mac Python:
+ To better compress WOFF 1.0 web fonts, the following module can be used
+ instead of the built-in ``zlib`` library:
- * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for
- extended filesystem attributes (macOS platform only).
+ * `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of
+ the Zopfli compression library.
- - ``Lib/fontTools/pens/cocoaPen.py``
+ *Extra:* ``woff``
- Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+ - ``Lib/fontTools/unicode.py``
- * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between
- Python and the Objective-C runtime (macOS platform only).
+ To display the Unicode character names when dumping the ``cmap`` table
+ with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
+ The version included in there varies between different Python versions.
+ To use the latest available data, you can install:
- - ``Lib/fontTools/pens/qtPen.py``
+ * `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__:
+ ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest
+ Unicode version 9.0. Note this is not necessary if you use Python 3.6
+ as the latter already comes with an up-to-date ``unicodedata``.
- Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+ *Extra:* ``unicode``
- * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for
- the Qt cross platform UI and application toolkit.
+ - ``Lib/fontTools/varLib/interpolatable.py``
- - ``Lib/fontTools/pens/reportLabPen.py``
+ Module for finding wrong contour/component order between different masters.
+ It requires one of the following packages in order to solve the so-called
+ "minimum weight perfect matching problem in bipartite graphs", or
+ the Assignment problem:
- Pen to drawing glyphs as PNG images, requires:
+ * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library
+ for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__
+ arrays and hence is very fast;
+ * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python
+ module that implements the Hungarian or Kuhn-Munkres algorithm.
- * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
- for generating PDFs and graphics.
+ *Extra:* ``interpolatable``
- - ``Lib/fontTools/inspect.py``
+ - ``Lib/fontTools/misc/symfont.py``
- A GUI font inspector, requires one of the following packages:
+ Advanced module for symbolic font statistics analysis; it requires:
- * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for
- GTK  2.x (only works with Python 2).
- * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ :
- Python bindings for GTK 3.x and gobject-introspection libraries (also
- compatible with Python 3).
+ * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for
+ symbolic mathematics.
- Testing
- ~~~~~~~
+ *Extra:* ``symfont``
- To run the test suite, you can do:
+ - ``Lib/fontTools/t1Lib.py``
- .. code:: sh
+ To get the file creator and type of Macintosh PostScript Type 1 fonts
+ on Python 3 you need to install the following module, as the old ``MacOS``
+ module is no longer included in Mac Python:
+
+ * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for
+ extended filesystem attributes (macOS platform only).
+
+ *Extra:* ``type1``
+
+ - ``Lib/fontTools/pens/cocoaPen.py``
+
+ Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+
+ * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between
+ Python and the Objective-C runtime (macOS platform only).
+
+ - ``Lib/fontTools/pens/qtPen.py``
+
+ Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+
+ * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for
+ the Qt cross platform UI and application toolkit.
+
+ - ``Lib/fontTools/pens/reportLabPen.py``
+
+ Pen to drawing glyphs as PNG images, requires:
+
+ * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
+ for generating PDFs and graphics.
- python setup.py test
+ - ``Lib/fontTools/inspect.py``
- If you have `pytest <http://docs.pytest.org/en/latest/>`__, you can run
- the ``pytest`` command directly. The tests will run against the
+ A GUI font inspector, requires one of the following packages:
+
+ * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for
+ GTK  2.x (only works with Python 2).
+ * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ :
+ Python bindings for GTK 3.x and gobject-introspection libraries (also
+ compatible with Python 3).
+
+ Testing
+ ~~~~~~~
+
+ To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
+ When you run the ``pytest`` command, the tests will run against the
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
@@ -287,15 +325,15 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Note that when you run ``tox`` without arguments, the tests are executed
for all the environments listed in tox.ini's ``envlist``. In our case,
- this includes Python 2.7 and 3.6, so for this to work the ``python2.7``
- and ``python3.6`` executables must be available in your ``PATH``.
+ this includes Python 2.7 and 3.7, so for this to work the ``python2.7``
+ and ``python3.7`` executables must be available in your ``PATH``.
You can specify an alternative environment list via the ``-e`` option,
or the ``TOXENV`` environment variable:
.. code:: sh
- tox -e py27-nocov
+ tox -e py27
TOXENV="py36-cov,htmlcov" tox
Development Community
@@ -377,6 +415,86 @@ Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage St
Changelog
~~~~~~~~~
+ 3.31.0 (released 2018-10-21)
+ ----------------------------
+
+ - [ufoLib] Merged the `ufoLib <https://github.com/unified-font-objects/ufoLib>`__
+ master branch into a new ``fontTools.ufoLib`` package (#1335, #1095).
+ Moved ``ufoLib.pointPen`` module to ``fontTools.pens.pointPen``.
+ Moved ``ufoLib.etree`` module to ``fontTools.misc.etree``.
+ Moved ``ufoLib.plistlib`` module to ``fontTools.misc.plistlib``.
+ To use the new ``fontTools.ufoLib`` module you need to install fonttools
+ with the ``[ufo]`` extra, or you can manually install the required additional
+ dependencies (cf. README.rst).
+ - [morx] Support AAT action type to insert glyphs and clean up compilation
+ of AAT action tables (4a1871f, 2011ccf).
+ - [subset] The ``--no-hinting`` on a CFF font now also drops the optional
+ hinting keys in Private dict: ``ForceBold``, ``LanguageGroup``, and
+ ``ExpansionFactor`` (#1322).
+ - [subset] Include nameIDs referenced by STAT table (#1327).
+ - [loggingTools] Added ``msg=None`` argument to
+ ``CapturingLogHandler.assertRegex`` (0245f2c).
+ - [varLib.mutator] Implemented ``FeatureVariations`` instantiation (#1244).
+ - [g_l_y_f] Added PointPen support to ``_TTGlyph`` objects (#1334).
+
+ 3.30.0 (released 2018-09-18)
+ ----------------------------
+
+ - [feaLib] Skip building noop class PairPos subtables when Coverage is NULL
+ (#1318).
+ - [ttx] Expose the previously reserved bit flag ``OVERLAP_SIMPLE`` of
+ glyf table's contour points in the TTX dump. This is used in some
+ implementations to specify a non-zero fill with overlapping contours (#1316).
+ - [ttLib] Added support for decompiling/compiling ``TS1C`` tables containing
+ VTT sources for ``cvar`` variation table (#1310).
+ - [varLib] Use ``fontTools.designspaceLib`` to read DesignSpaceDocument. The
+ ``fontTools.varLib.designspace`` module is now deprecated and will be removed
+ in future versions. The presence of an explicit ``axes`` element is now
+ required in order to build a variable font (#1224, #1313).
+ - [varLib] Implemented building GSUB FeatureVariations table from the ``rules``
+ element of DesignSpace document (#1240, #713, #1314).
+ - [subset] Added ``--no-layout-closure`` option to not expand the subset with
+ the glyphs produced by OpenType layout features. Instead, OpenType features
+ will be subset to only rules that are relevant to the otherwise-specified
+ glyph set (#43, #1121).
+
+ 3.29.1 (released 2018-09-10)
+ ----------------------------
+
+ - [feaLib] Fixed issue whereby lookups from DFLT/dflt were not included in the
+ DFLT/non-dflt language systems (#1307).
+ - [graphite] Fixed issue on big-endian architectures (e.g. ppc64) (#1311).
+ - [subset] Added ``--layout-scripts`` option to add/exclude set of OpenType
+ layout scripts that will be preserved. By default all scripts are retained
+ (``'*'``) (#1303).
+
+ 3.29.0 (released 2018-07-26)
+ ----------------------------
+
+ - [feaLib] In the OTL table builder, when the ``name`` table is excluded
+ from the list of tables to be build, skip compiling ``featureNames`` blocks,
+ as the records referenced in ``FeatureParams`` table don't exist (68951b7).
+ - [otBase] Try ``ExtensionLookup`` if other offset-overflow methods fail
+ (05f95f0).
+ - [feaLib] Added support for explicit ``subtable;`` break statements in
+ PairPos lookups; previously these were ignored (#1279, #1300, #1302).
+ - [cffLib.specializer] Make sure the stack depth does not exceed maxstack - 1,
+ so that a subroutinizer can insert subroutine calls (#1301,
+ https://github.com/googlei18n/ufo2ft/issues/266).
+ - [otTables] Added support for fixing offset overflow errors occurring inside
+ ``MarkBasePos`` subtables (#1297).
+ - [subset] Write the default output file extension based on ``--flavor`` option,
+ or the value of ``TTFont.sfntVersion`` (d7ac0ad).
+ - [unicodedata] Updated Blocks, Scripts and ScriptExtensions for Unicode 11
+ (452c85e).
+ - [xmlWriter] Added context manager to XMLWriter class to autoclose file
+ descriptor on exit (#1290).
+ - [psCharStrings] Optimize the charstring's bytecode by encoding as integers
+ all float values that have no decimal portion (8d7774a).
+ - [ttFont] Fixed missing import of ``TTLibError`` exception (#1285).
+ - [feaLib] Allow any languages other than ``dflt`` under ``DFLT`` script
+ (#1278, #1292).
+
3.28.0 (released 2018-06-19)
----------------------------
@@ -1373,3 +1491,11 @@ Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Fonts
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
+Provides-Extra: type1
+Provides-Extra: lxml
+Provides-Extra: unicode
+Provides-Extra: symfont
+Provides-Extra: all
+Provides-Extra: ufo
+Provides-Extra: woff
+Provides-Extra: interpolatable
diff --git a/README.rst b/README.rst
index 664ad8fc..5f39ed85 100644
--- a/README.rst
+++ b/README.rst
@@ -8,7 +8,7 @@ What is this?
project includes the TTX tool, that can convert TrueType and OpenType
fonts to and from an XML text format, which is also called TTX. It
supports TrueType, OpenType, AFM and to an extent Type 1 and some
- Mac-specific formats. The project has a `MIT open-source
+ Mac-specific formats. The project has an `MIT open-source
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
@@ -26,7 +26,7 @@ install it with `pip <https://pip.pypa.io>`__:
pip install fonttools
If you would like to contribute to its development, you can clone the
-repository from Github, install the package in 'editable' mode and
+repository from GitHub, install the package in 'editable' mode and
modify the source code in place. We recommend creating a virtual
environment, using `virtualenv <https://virtualenv.pypa.io>`__ or
Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module.
@@ -40,7 +40,7 @@ Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module.
# create new virtual environment called e.g. 'fonttools-venv', or anything you like
python -m virtualenv fonttools-venv
- # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate`
+ # source the `activate` shell script to enter the environment (Un*x); to exit, just type `deactivate`
. fonttools-venv/bin/activate
# to activate the virtual environment in Windows `cmd.exe`, do
@@ -53,7 +53,7 @@ TTX – From OpenType and TrueType to XML and Back
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Once installed you can use the ``ttx`` command to convert binary font
-files (``.otf``, ``.ttf``, etc) to the TTX xml format, edit them, and
+files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
convert them back to binary format. TTX files have a .ttx file
extension.
@@ -62,11 +62,11 @@ extension.
ttx /path/to/font.otf
ttx /path/to/font.ttx
-The TTX application works can be used in two ways, depending on what
+The TTX application can be used in two ways, depending on what
platform you run it on:
-- As a command line tool (Windows/DOS, Unix, MacOSX)
-- By dropping files onto the application (Windows, MacOS)
+- As a command line tool (Windows/DOS, Unix, macOS)
+- By dropping files onto the application (Windows, macOS)
TTX detects what kind of files it is fed: it will output a ``.ttx`` file
when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
@@ -77,8 +77,8 @@ same name as the input file but with a different extension. TTX will
number to the output filename (before the extension) such as
``Arial#1.ttf``
-When using TTX from the command line there are a bunch of extra options,
-these are explained in the help text, as displayed when typing
+When using TTX from the command line there are a bunch of extra options.
+These are explained in the help text, as displayed when typing
``ttx -h`` at the command prompt. These additional options include:
- specifying the folder where the output files are created
@@ -142,7 +142,7 @@ available:
fontTools Python Module
~~~~~~~~~~~~~~~~~~~~~~~
-The fontTools python module provides a convenient way to
+The fontTools Python module provides a convenient way to
programmatically edit font files.
.. code:: py
@@ -153,7 +153,7 @@ programmatically edit font files.
<fontTools.ttLib.TTFont object at 0x10c34ed50>
>>>
-A selection of sample python programs is in the
+A selection of sample Python programs is in the
`Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
directory.
@@ -164,105 +164,143 @@ The ``fontTools`` package currently has no (required) external dependencies
besides the modules included in the Python Standard Library.
However, a few extra dependencies are required by some of its modules, which
are needed to unlock optional features.
+The ``fonttools`` PyPI distribution also supports so-called "extras", i.e. a
+set of keywords that describe a group of additional dependencies, which can be
+used when installing via pip, or when specifying a requirement.
+For example:
-- ``Lib/fontTools/ttLib/woff2.py``
+.. code:: sh
- Module to compress/decompress WOFF 2.0 web fonts; it requires:
+ pip install fonttools[ufo,lxml,woff,unicode]
- - `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of
- the Brotli compression library.
+This command will install fonttools, as well as the optional dependencies that
+are required to unlock the extra features named "ufo", etc.
-- ``Lib/fontTools/ttLib/sfnt.py``
+- ``Lib/fontTools/misc/etree.py``
- To better compress WOFF 1.0 web fonts, the following module can be used
- instead of the built-in ``zlib`` library:
+ The module exports a ElementTree-like API for reading/writing XML files, and
+ allows to use as the backend either the built-in ``xml.etree`` module or
+ `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+ as it is generally faster and more secure.
- - `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of
- the Zopfli compression library.
+ *Extra:* ``lxml``
-- ``Lib/fontTools/unicode.py``
+- ``Lib/fontTools/ufoLib``
- To display the Unicode character names when dumping the ``cmap`` table
- with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
- The version included in there varies between different Python versions.
- To use the latest available data, you can install:
+ Package for reading and writing UFO source files; it requires:
- - `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__:
- ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest
- Unicode version 9.0. Note this is not necessary if you use Python 3.6
- as the latter already comes with an up-to-date ``unicodedata``.
+ * `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem
+ abstraction layer.
-- ``Lib/fontTools/varLib/interpolatable.py``
+ * `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum``
+ module (only required on Python < 3.4).
- Module for finding wrong contour/component order between different masters.
- It requires one of the following packages in order to solve the so-called
- "minimum weight perfect matching problem in bipartite graphs", or
- the Assignment problem:
+ *Extra:* ``ufo``
- * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library
- for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__
- arrays and hence is very fast;
- * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python
- module that implements the Hungarian or Kuhn-Munkres algorithm.
+- ``Lib/fontTools/ttLib/woff2.py``
-- ``Lib/fontTools/misc/symfont.py``
+ Module to compress/decompress WOFF 2.0 web fonts; it requires:
- Advanced module for symbolic font statistics analysis; it requires:
+ * `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of
+ the Brotli compression library.
- * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for
- symbolic mathematics.
+ *Extra:* ``woff``
-- ``Lib/fontTools/t1Lib.py``
+- ``Lib/fontTools/ttLib/sfnt.py``
- To get the file creator and type of Macintosh PostScript Type 1 fonts
- on Python 3 you need to install the following module, as the old ``MacOS``
- module is no longer included in Mac Python:
+ To better compress WOFF 1.0 web fonts, the following module can be used
+ instead of the built-in ``zlib`` library:
- * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for
- extended filesystem attributes (macOS platform only).
+ * `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of
+ the Zopfli compression library.
-- ``Lib/fontTools/pens/cocoaPen.py``
+ *Extra:* ``woff``
- Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+- ``Lib/fontTools/unicode.py``
- * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between
- Python and the Objective-C runtime (macOS platform only).
+ To display the Unicode character names when dumping the ``cmap`` table
+ with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
+ The version included in there varies between different Python versions.
+ To use the latest available data, you can install:
-- ``Lib/fontTools/pens/qtPen.py``
+ * `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__:
+ ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest
+ Unicode version 9.0. Note this is not necessary if you use Python 3.6
+ as the latter already comes with an up-to-date ``unicodedata``.
- Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+ *Extra:* ``unicode``
- * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for
- the Qt cross platform UI and application toolkit.
+- ``Lib/fontTools/varLib/interpolatable.py``
-- ``Lib/fontTools/pens/reportLabPen.py``
+ Module for finding wrong contour/component order between different masters.
+ It requires one of the following packages in order to solve the so-called
+ "minimum weight perfect matching problem in bipartite graphs", or
+ the Assignment problem:
- Pen to drawing glyphs as PNG images, requires:
+ * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library
+ for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__
+ arrays and hence is very fast;
+ * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python
+ module that implements the Hungarian or Kuhn-Munkres algorithm.
- * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
- for generating PDFs and graphics.
+ *Extra:* ``interpolatable``
-- ``Lib/fontTools/inspect.py``
+- ``Lib/fontTools/misc/symfont.py``
- A GUI font inspector, requires one of the following packages:
+ Advanced module for symbolic font statistics analysis; it requires:
- * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for
- GTK  2.x (only works with Python 2).
- * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ :
- Python bindings for GTK 3.x and gobject-introspection libraries (also
- compatible with Python 3).
+ * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for
+ symbolic mathematics.
-Testing
-~~~~~~~
+ *Extra:* ``symfont``
-To run the test suite, you can do:
+- ``Lib/fontTools/t1Lib.py``
-.. code:: sh
+ To get the file creator and type of Macintosh PostScript Type 1 fonts
+ on Python 3 you need to install the following module, as the old ``MacOS``
+ module is no longer included in Mac Python:
+
+ * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for
+ extended filesystem attributes (macOS platform only).
+
+ *Extra:* ``type1``
+
+- ``Lib/fontTools/pens/cocoaPen.py``
+
+ Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+
+ * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between
+ Python and the Objective-C runtime (macOS platform only).
+
+- ``Lib/fontTools/pens/qtPen.py``
- python setup.py test
+ Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+
+ * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for
+ the Qt cross platform UI and application toolkit.
+
+- ``Lib/fontTools/pens/reportLabPen.py``
+
+ Pen to drawing glyphs as PNG images, requires:
+
+ * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
+ for generating PDFs and graphics.
+
+- ``Lib/fontTools/inspect.py``
+
+ A GUI font inspector, requires one of the following packages:
+
+ * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for
+ GTK  2.x (only works with Python 2).
+ * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ :
+ Python bindings for GTK 3.x and gobject-introspection libraries (also
+ compatible with Python 3).
+
+Testing
+~~~~~~~
-If you have `pytest <http://docs.pytest.org/en/latest/>`__, you can run
-the ``pytest`` command directly. The tests will run against the
+To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
+When you run the ``pytest`` command, the tests will run against the
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
@@ -277,15 +315,15 @@ environments.
Note that when you run ``tox`` without arguments, the tests are executed
for all the environments listed in tox.ini's ``envlist``. In our case,
-this includes Python 2.7 and 3.6, so for this to work the ``python2.7``
-and ``python3.6`` executables must be available in your ``PATH``.
+this includes Python 2.7 and 3.7, so for this to work the ``python2.7``
+and ``python3.7`` executables must be available in your ``PATH``.
You can specify an alternative environment list via the ``-e`` option,
or the ``TOXENV`` environment variable:
.. code:: sh
- tox -e py27-nocov
+ tox -e py27
TOXENV="py36-cov,htmlcov" tox
Development Community
diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py
index 60acd546..62b4f735 100755
--- a/Snippets/otf2ttf.py
+++ b/Snippets/otf2ttf.py
@@ -1,13 +1,20 @@
#!/usr/bin/env python
from __future__ import print_function, division, absolute_import
+
+import argparse
+import logging
+import os
import sys
-from fontTools.ttLib import TTFont, newTable
+
from cu2qu.pens import Cu2QuPen
+from fontTools import configLogger
+from fontTools.misc.cliTools import makeOutputFileName
from fontTools.pens.ttGlyphPen import TTGlyphPen
-from fontTools.ttx import makeOutputFileName
-import argparse
+from fontTools.ttLib import TTFont, newTable
+log = logging.getLogger()
+
# default approximation error, measured in UPEM
MAX_ERR = 1.0
@@ -43,6 +50,7 @@ def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs):
glyf.glyphOrder = glyphOrder
glyf.glyphs = glyphs_to_quadratic(ttFont.getGlyphSet(), **kwargs)
del ttFont["CFF "]
+ glyf.compile(ttFont)
ttFont["maxp"] = maxp = newTable("maxp")
maxp.tableVersion = 0x00010000
@@ -56,35 +64,55 @@ def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs):
maxp.maxComponentElements = max(
len(g.components if hasattr(g, 'components') else [])
for g in glyf.glyphs.values())
+ maxp.compile(ttFont)
post = ttFont["post"]
post.formatType = post_format
post.extraNames = []
post.mapping = {}
post.glyphOrder = glyphOrder
+ try:
+ post.compile(ttFont)
+ except OverflowError:
+ post.formatType = 3
+ log.warning("Dropping glyph names, they do not fit in 'post' table.")
ttFont.sfntVersion = "\000\001\000\000"
def main(args=None):
+ configLogger(logger=log)
+
parser = argparse.ArgumentParser()
- parser.add_argument("input", metavar="INPUT")
+ parser.add_argument("input", nargs='+', metavar="INPUT")
parser.add_argument("-o", "--output")
parser.add_argument("-e", "--max-error", type=float, default=MAX_ERR)
parser.add_argument("--post-format", type=float, default=POST_FORMAT)
parser.add_argument(
"--keep-direction", dest='reverse_direction', action='store_false')
+ parser.add_argument("--face-index", type=int, default=0)
+ parser.add_argument("--overwrite", action='store_true')
options = parser.parse_args(args)
- output = options.output or makeOutputFileName(options.input,
- outputDir=None,
- extension='.ttf')
- font = TTFont(options.input)
- otf_to_ttf(font,
- post_format=options.post_format,
- max_err=options.max_error,
- reverse_direction=options.reverse_direction)
- font.save(output)
+ if options.output and len(options.input) > 1:
+ if not os.path.isdir(options.output):
+ parser.error("-o/--output option must be a directory when "
+ "processing multiple fonts")
+
+ for path in options.input:
+ if options.output and not os.path.isdir(options.output):
+ output = options.output
+ else:
+ output = makeOutputFileName(path, outputDir=options.output,
+ extension='.ttf',
+ overWrite=options.overwrite)
+
+ font = TTFont(path, fontNumber=options.face_index)
+ otf_to_ttf(font,
+ post_format=options.post_format,
+ max_err=options.max_error,
+ reverse_direction=options.reverse_direction)
+ font.save(output)
if __name__ == "__main__":
diff --git a/Tests/cffLib/specializer_test.py b/Tests/cffLib/specializer_test.py
index aa506119..912e66ee 100644
--- a/Tests/cffLib/specializer_test.py
+++ b/Tests/cffLib/specializer_test.py
@@ -904,12 +904,12 @@ class CFFSpecializeProgramTest(unittest.TestCase):
xpct_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 rlinecurve'
self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr)
-# maxstack CFF=48
+# maxstack CFF=48, specializer uses up to 47
def test_maxstack(self):
operands = '1 2 3 4 5 6 '
operator = 'rrcurveto '
test_charstr = (operands + operator)*9
- xpct_charstr = (operands + operator + operands*8 + operator).rstrip()
+ xpct_charstr = (operands*2 + operator + operands*7 + operator).rstrip()
self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr)
diff --git a/Tests/designspaceLib/data/test.designspace b/Tests/designspaceLib/data/test.designspace
index cf7056b3..9f97ab57 100644
--- a/Tests/designspaceLib/data/test.designspace
+++ b/Tests/designspaceLib/data/test.designspace
@@ -1,107 +1,111 @@
-<?xml version='1.0' encoding='utf-8'?>
+<?xml version='1.0' encoding='UTF-8'?>
<designspace format="4.0">
- <axes>
- <axis default="0" maximum="1000" minimum="0" name="weight" tag="wght">
- <labelname xml:lang="en">Wéíght</labelname>
- <labelname xml:lang="fa-IR">قطر</labelname>
- </axis>
- <axis default="20" hidden="1" maximum="1000" minimum="0" name="width" tag="wdth">
- <labelname xml:lang="fr">Chasse</labelname>
- <map input="0" output="10" />
- <map input="401" output="66" />
- <map input="1000" output="990" />
- </axis>
- </axes>
- <rules>
- <rule name="named.rule.1">
- <conditionset>
- <condition maximum="1" minimum="0" name="axisName_a" />
- <condition maximum="3" minimum="2" name="axisName_b" />
- </conditionset>
- <sub name="a" with="a.alt" />
- </rule>
- </rules>
- <sources>
- <source familyname="MasterFamilyName" filename="masters/masterTest1.ufo" name="master.ufo1" stylename="MasterStyleNameOne">
- <lib copy="1" />
- <features copy="1" />
- <info copy="1" />
- <glyph mute="1" name="A" />
- <glyph mute="1" name="Z" />
- <location>
- <dimension name="weight" xvalue="0" />
- <dimension name="width" xvalue="20" />
- </location>
- </source>
- <source familyname="MasterFamilyName" filename="masters/masterTest2.ufo" name="master.ufo2" stylename="MasterStyleNameTwo">
- <kerning mute="1" />
- <location>
- <dimension name="weight" xvalue="1000" />
- <dimension name="width" xvalue="20" />
- </location>
- </source>
- <source familyname="MasterFamilyName" filename="masters/masterTest2.ufo" layer="supports" name="master.ufo2" stylename="Supports">
- <location>
- <dimension name="weight" xvalue="1000" />
- <dimension name="width" xvalue="20" />
- </location>
- </source>
- </sources>
- <instances>
- <instance familyname="InstanceFamilyName" filename="instances/instanceTest1.ufo" name="instance.ufo1" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
- <location>
- <dimension name="weight" xvalue="500" />
- <dimension name="width" xvalue="20" />
- </location>
- <glyphs>
- <glyph mute="1" name="arrow" unicode="0x123 0x124 0x125" />
- </glyphs>
- <kerning />
- <info />
- <lib>
- <dict>
- <key>com.coolDesignspaceApp.specimenText</key>
- <string>Hamburgerwhatever</string>
- </dict>
- </lib>
- </instance>
- <instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
- <location>
- <dimension name="weight" xvalue="500" />
- <dimension name="width" xvalue="400" yvalue="300" />
- </location>
- <glyphs>
- <glyph name="arrow" unicode="0x65 0xc9 0x12d">
- <location>
- <dimension name="weight" xvalue="120" />
- <dimension name="width" xvalue="100" />
- </location>
- <note>A note about this glyph</note>
- <masters>
- <master glyphname="BB" source="master.ufo1">
- <location>
- <dimension name="weight" xvalue="20" />
- <dimension name="width" xvalue="20" />
- </location>
- </master>
- <master glyphname="CC" source="master.ufo2">
- <location>
- <dimension name="weight" xvalue="900" />
- <dimension name="width" xvalue="900" />
- </location>
- </master>
- </masters>
- </glyph>
- <glyph name="arrow2" />
- </glyphs>
- <kerning />
- <info />
- </instance>
- </instances>
- <lib>
+ <axes>
+ <axis tag="wght" name="weight" minimum="0" maximum="1000" default="0">
+ <labelname xml:lang="en">Wéíght</labelname>
+ <labelname xml:lang="fa-IR">قطر</labelname>
+ </axis>
+ <axis tag="wdth" name="width" minimum="0" maximum="1000" default="20" hidden="1">
+ <labelname xml:lang="fr">Chasse</labelname>
+ <map input="0" output="10"/>
+ <map input="401" output="66"/>
+ <map input="1000" output="990"/>
+ </axis>
+ </axes>
+ <rules>
+ <rule name="named.rule.1">
+ <conditionset>
+ <condition name="axisName_a" minimum="0" maximum="1"/>
+ <condition name="axisName_b" minimum="2" maximum="3"/>
+ </conditionset>
+ <sub name="a" with="a.alt"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="masters/masterTest1.ufo" name="master.ufo1" familyname="MasterFamilyName" stylename="MasterStyleNameOne">
+ <lib copy="1"/>
+ <features copy="1"/>
+ <info copy="1"/>
+ <glyph name="A" mute="1"/>
+ <glyph name="Z" mute="1"/>
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="masters/masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="MasterStyleNameTwo">
+ <kerning mute="1"/>
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="masters/masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="Supports" layer="supports">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="20"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="instance.ufo1" familyname="InstanceFamilyName" stylename="InstanceStyleName" filename="instances/instanceTest1.ufo" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="20"/>
+ </location>
+ <glyphs>
+ <glyph mute="1" unicode="0x123 0x124 0x125" name="arrow"/>
+ </glyphs>
+ <kerning/>
+ <info/>
+ <lib>
<dict>
- <key>com.coolDesignspaceApp.previewSize</key>
- <integer>30</integer>
+ <key>com.coolDesignspaceApp.binaryData</key>
+ <data>
+ PGJpbmFyeSBndW5rPg==
+ </data>
+ <key>com.coolDesignspaceApp.specimenText</key>
+ <string>Hamburgerwhatever</string>
</dict>
- </lib>
+ </lib>
+ </instance>
+ <instance name="instance.ufo2" familyname="InstanceFamilyName" stylename="InstanceStyleName" filename="instances/instanceTest2.ufo" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="400" yvalue="300"/>
+ </location>
+ <glyphs>
+ <glyph unicode="0x65 0xc9 0x12d" name="arrow">
+ <location>
+ <dimension name="weight" xvalue="120"/>
+ <dimension name="width" xvalue="100"/>
+ </location>
+ <note>A note about this glyph</note>
+ <masters>
+ <master glyphname="BB" source="master.ufo1">
+ <location>
+ <dimension name="weight" xvalue="20"/>
+ <dimension name="width" xvalue="20"/>
+ </location>
+ </master>
+ <master glyphname="CC" source="master.ufo2">
+ <location>
+ <dimension name="weight" xvalue="900"/>
+ <dimension name="width" xvalue="900"/>
+ </location>
+ </master>
+ </masters>
+ </glyph>
+ <glyph name="arrow2"/>
+ </glyphs>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.previewSize</key>
+ <integer>30</integer>
+ </dict>
+ </lib>
</designspace>
diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py
index 1d9b841a..b5d0b70d 100644
--- a/Tests/designspaceLib/designspace_test.py
+++ b/Tests/designspaceLib/designspace_test.py
@@ -8,6 +8,7 @@ import pytest
import warnings
from fontTools.misc.py23 import open
+from fontTools.misc import plistlib
from fontTools.designspaceLib import (
DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor,
InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError)
@@ -123,6 +124,7 @@ def test_fill_document(tmpdir):
i1.styleMapStyleName = "InstanceStyleMapStyleName"
glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125])
i1.glyphs['arrow'] = glyphData
+ i1.lib['com.coolDesignspaceApp.binaryData'] = plistlib.Data(b'<binary gunk>')
i1.lib['com.coolDesignspaceApp.specimenText'] = "Hamburgerwhatever"
doc.addInstance(i1)
# add instance 2
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index e831f977..18ece3b1 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -65,7 +65,7 @@ class BuilderTest(unittest.TestCase):
spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g
spec10
bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509
- bug512 bug514 bug568 bug633
+ bug512 bug514 bug568 bug633 bug1307
name size size2 multiple_feature_blocks omitted_GlyphClassDef
ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
@@ -202,7 +202,7 @@ class BuilderTest(unittest.TestCase):
def test_pairPos_redefinition_warning(self):
# https://github.com/fonttools/fonttools/issues/1147
logger = logging.getLogger("fontTools.feaLib.builder")
- with CapturingLogHandler(logger, "WARNING") as captor:
+ with CapturingLogHandler(logger, "DEBUG") as captor:
# the pair "yacute semicolon" is redefined in the enum pos
font = self.build(
"@Y_LC = [y yacute ydieresis];"
@@ -285,6 +285,17 @@ class BuilderTest(unittest.TestCase):
"it must be the first of the languagesystem statements",
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
+ def test_languagesystem_DFLT_not_preceding(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "languagesystems using the \"DFLT\" script tag must "
+ "precede all other languagesystems",
+ self.build,
+ "languagesystem DFLT dflt; "
+ "languagesystem latn dflt; "
+ "languagesystem DFLT fooo; "
+ )
+
def test_script(self):
builder = Builder(makeTTFont(), (None, None))
builder.start_feature(location=None, name='test')
@@ -316,11 +327,7 @@ class BuilderTest(unittest.TestCase):
self.assertEqual(builder.language_systems,
{('cyrl', 'BGR ')})
builder.start_feature(location=None, name='test2')
- self.assertRaisesRegex(
- FeatureLibError,
- "Need non-DFLT script when using non-dflt language "
- "\(was: \"FRA \"\)",
- builder.set_language, None, 'FRA ', True, False)
+ self.assertEqual(builder.language_systems, {('latn', 'FRA ')})
def test_language_in_aalt_feature(self):
self.assertRaisesRegex(
@@ -505,6 +512,31 @@ class BuilderTest(unittest.TestCase):
addOpenTypeFeatures(font, tree)
assert "GSUB" in font
+ def test_unsupported_subtable_break(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ 'explicit "subtable" statement is intended for .* class kerning',
+ self.build,
+ "feature liga {"
+ " sub f f by f_f;"
+ " subtable;"
+ " sub f i by f_i;"
+ "} liga;"
+ )
+
+ def test_skip_featureNames_if_no_name_table(self):
+ features = (
+ "feature ss01 {"
+ " featureNames {"
+ ' name "ignored as we request to skip name table";'
+ " };"
+ " sub A by A.alt1;"
+ "} ss01;"
+ )
+ font = self.build(features, tables=["GSUB"])
+ self.assertIn("GSUB", font)
+ self.assertNotIn("name", font)
+
def generate_feature_file_test(name):
return lambda self: self.check_feature_file(name)
diff --git a/Tests/feaLib/data/PairPosSubtable.fea b/Tests/feaLib/data/PairPosSubtable.fea
index 021f3cc0..cb78801c 100644
--- a/Tests/feaLib/data/PairPosSubtable.fea
+++ b/Tests/feaLib/data/PairPosSubtable.fea
@@ -1,12 +1,21 @@
languagesystem DFLT dflt;
languagesystem latn dflt;
+@group1 = [b o];
+@group2 = [c d];
+@group3 = [v w];
+@group4 = [];
+
lookup kernlookup {
pos A V -34;
subtable;
- @group1 = [b o];
- @group2 = [c d];
pos @group1 @group2 -12;
+ subtable;
+ pos @group1 @group3 -10;
+ pos @group3 @group2 -20;
+ subtable;
+ pos @group4 @group1 -10;
+ pos @group4 @group4 -10;
} kernlookup;
feature kern {
diff --git a/Tests/feaLib/data/PairPosSubtable.ttx b/Tests/feaLib/data/PairPosSubtable.ttx
index 13a77aaa..4b76f991 100644
--- a/Tests/feaLib/data/PairPosSubtable.ttx
+++ b/Tests/feaLib/data/PairPosSubtable.ttx
@@ -22,14 +22,14 @@
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
- <FeatureIndex index="0" value="1"/>
+ <FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
- <!-- FeatureCount=2 -->
+ <!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="kern"/>
<Feature>
@@ -37,21 +37,13 @@
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
- <FeatureRecord index="1">
- <FeatureTag value="kern"/>
- <Feature>
- <!-- LookupCount=2 -->
- <LookupListIndex index="0" value="0"/>
- <LookupListIndex index="1" value="0"/>
- </Feature>
- </FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<Lookup index="0">
<LookupType value="2"/>
<LookupFlag value="0"/>
- <!-- SubTableCount=2 -->
+ <!-- SubTableCount=3 -->
<PairPos index="0" Format="1">
<Coverage>
<Glyph value="A"/>
@@ -90,6 +82,46 @@
</Class2Record>
</Class1Record>
</PairPos>
+ <PairPos index="2" Format="2">
+ <Coverage>
+ <Glyph value="b"/>
+ <Glyph value="o"/>
+ <Glyph value="v"/>
+ <Glyph value="w"/>
+ </Coverage>
+ <ValueFormat1 value="4"/>
+ <ValueFormat2 value="0"/>
+ <ClassDef1>
+ <ClassDef glyph="b" class="1"/>
+ <ClassDef glyph="o" class="1"/>
+ </ClassDef1>
+ <ClassDef2>
+ <ClassDef glyph="c" class="2"/>
+ <ClassDef glyph="d" class="2"/>
+ <ClassDef glyph="v" class="1"/>
+ <ClassDef glyph="w" class="1"/>
+ </ClassDef2>
+ <!-- Class1Count=2 -->
+ <!-- Class2Count=3 -->
+ <Class1Record index="0">
+ <Class2Record index="0">
+ </Class2Record>
+ <Class2Record index="1">
+ </Class2Record>
+ <Class2Record index="2">
+ <Value1 XAdvance="-20"/>
+ </Class2Record>
+ </Class1Record>
+ <Class1Record index="1">
+ <Class2Record index="0">
+ </Class2Record>
+ <Class2Record index="1">
+ <Value1 XAdvance="-10"/>
+ </Class2Record>
+ <Class2Record index="2">
+ </Class2Record>
+ </Class1Record>
+ </PairPos>
</Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/feaLib/data/bug1307.fea b/Tests/feaLib/data/bug1307.fea
new file mode 100644
index 00000000..dbc4ae36
--- /dev/null
+++ b/Tests/feaLib/data/bug1307.fea
@@ -0,0 +1,65 @@
+# Test of features and languagesystems
+
+lookup a {
+ sub a by A;
+} a;
+
+lookup b {
+ sub b by B;
+} b;
+
+lookup c {
+ sub c by C;
+} c;
+
+lookup d {
+ sub d by D;
+} d;
+
+lookup e {
+ sub e by E;
+} e;
+
+lookup f {
+ sub f by F;
+} f;
+
+lookup g {
+ sub g by G;
+} g;
+
+lookup h {
+ sub h by H;
+} h;
+
+lookup i {
+ sub i by I;
+} i;
+
+languagesystem DFLT dflt;
+languagesystem DFLT FRE;
+languagesystem DFLT ABC;
+languagesystem latn dflt;
+languagesystem latn ABC;
+
+feature smcp {
+ lookup a;
+} smcp;
+
+feature liga {
+ lookup b;
+ script DFLT;
+ lookup c;
+ language dflt;
+ lookup d;
+ language FRE;
+ lookup e;
+ script latn;
+ lookup f;
+ language dflt;
+ lookup g;
+ language FRE;
+ lookup h;
+ language DEF exclude_dflt;
+ lookup i;
+} liga;
diff --git a/Tests/feaLib/data/bug1307.ttx b/Tests/feaLib/data/bug1307.ttx
new file mode 100644
index 00000000..1ecbf03f
--- /dev/null
+++ b/Tests/feaLib/data/bug1307.ttx
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="4"/>
+ <FeatureIndex index="1" value="6"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=2 -->
+ <LangSysRecord index="0">
+ <LangSysTag value="ABC "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="6"/>
+ </LangSys>
+ </LangSysRecord>
+ <LangSysRecord index="1">
+ <LangSysTag value="FRE "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="2"/>
+ <FeatureIndex index="1" value="6"/>
+ </LangSys>
+ </LangSysRecord>
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="5"/>
+ <FeatureIndex index="1" value="6"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=3 -->
+ <LangSysRecord index="0">
+ <LangSysTag value="ABC "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="6"/>
+ </LangSys>
+ </LangSysRecord>
+ <LangSysRecord index="1">
+ <LangSysTag value="DEF "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="1"/>
+ </LangSys>
+ </LangSysRecord>
+ <LangSysRecord index="2">
+ <LangSysTag value="FRE "/>
+ <LangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="3"/>
+ </LangSys>
+ </LangSysRecord>
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=7 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="8"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="2">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=4 -->
+ <LookupListIndex index="0" value="1"/>
+ <LookupListIndex index="1" value="2"/>
+ <LookupListIndex index="2" value="3"/>
+ <LookupListIndex index="3" value="4"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="3">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=4 -->
+ <LookupListIndex index="0" value="1"/>
+ <LookupListIndex index="1" value="5"/>
+ <LookupListIndex index="2" value="6"/>
+ <LookupListIndex index="3" value="7"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="4">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=3 -->
+ <LookupListIndex index="0" value="1"/>
+ <LookupListIndex index="1" value="2"/>
+ <LookupListIndex index="2" value="3"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="5">
+ <FeatureTag value="liga"/>
+ <Feature>
+ <!-- LookupCount=3 -->
+ <LookupListIndex index="0" value="1"/>
+ <LookupListIndex index="1" value="5"/>
+ <LookupListIndex index="2" value="6"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="6">
+ <FeatureTag value="smcp"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=9 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="a" out="A"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="b" out="B"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="c" out="C"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="3">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="d" out="D"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="4">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="e" out="E"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="5">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="f" out="F"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="6">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="g" out="G"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="7">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="h" out="H"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="8">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="i" out="I"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index 91744f07..f9b1063b 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -1560,10 +1560,9 @@ class ParserTest(unittest.TestCase):
[langsys] = self.parse("languagesystem latn DEU;").statements
self.assertEqual(langsys.script, "latn")
self.assertEqual(langsys.language, "DEU ")
- self.assertRaisesRegex(
- FeatureLibError,
- 'For script "DFLT", the language must be "dflt"',
- self.parse, "languagesystem DFLT DEU;")
+ [langsys] = self.parse("languagesystem DFLT DEU;").statements
+ self.assertEqual(langsys.script, "DFLT")
+ self.assertEqual(langsys.language, "DEU ")
self.assertRaisesRegex(
FeatureLibError,
'"dflt" is not a valid script tag; use "DFLT" instead',
diff --git a/Tests/misc/etree_test.py b/Tests/misc/etree_test.py
new file mode 100644
index 00000000..d2f585cf
--- /dev/null
+++ b/Tests/misc/etree_test.py
@@ -0,0 +1,55 @@
+# coding: utf-8
+from __future__ import absolute_import, unicode_literals
+from fontTools.misc import etree
+from collections import OrderedDict
+import io
+import pytest
+
+
+@pytest.mark.parametrize(
+ "xml",
+ [
+ (
+ "<root>"
+ '<element key="value">text</element>'
+ "<element>text</element>tail"
+ "<empty-element/>"
+ "</root>"
+ ),
+ (
+ "<root>\n"
+ ' <element key="value">text</element>\n'
+ " <element>text</element>tail\n"
+ " <empty-element/>\n"
+ "</root>"
+ ),
+ (
+ '<axis default="400" maximum="1000" minimum="1" name="weight" tag="wght">'
+ '<labelname xml:lang="fa-IR">قطر</labelname>'
+ "</axis>"
+ ),
+ ],
+ ids=["simple_xml_no_indent", "simple_xml_indent", "xml_ns_attrib_utf_8"],
+)
+def test_roundtrip_string(xml):
+ root = etree.fromstring(xml.encode("utf-8"))
+ result = etree.tostring(root, encoding="utf-8").decode("utf-8")
+ assert result == xml
+
+
+def test_pretty_print():
+ root = etree.Element("root")
+ attrs = OrderedDict([("c", "2"), ("b", "1"), ("a", "0")])
+ etree.SubElement(root, "element", attrs).text = "text"
+ etree.SubElement(root, "element").text = "text"
+ root.append(etree.Element("empty-element"))
+
+ result = etree.tostring(root, encoding="unicode", pretty_print=True)
+
+ assert result == (
+ "<root>\n"
+ ' <element c="2" b="1" a="0">text</element>\n'
+ " <element>text</element>\n"
+ " <empty-element/>\n"
+ "</root>\n"
+ )
diff --git a/Tests/misc/plistlib_test.py b/Tests/misc/plistlib_test.py
new file mode 100644
index 00000000..041f3328
--- /dev/null
+++ b/Tests/misc/plistlib_test.py
@@ -0,0 +1,536 @@
+from __future__ import absolute_import, unicode_literals
+import sys
+import os
+import datetime
+import codecs
+import collections
+from io import BytesIO
+from numbers import Integral
+from fontTools.misc.py23 import tounicode, unicode
+from fontTools.misc import etree
+from fontTools.misc import plistlib
+from fontTools.ufoLib.plistlib import (
+ readPlist, readPlistFromString, writePlist, writePlistToString,
+)
+import pytest
+
+
+PY2 = sys.version_info < (3,)
+if PY2:
+ # This is a ResourceWarning that only happens on py27 at interpreter
+ # finalization, and only when coverage is enabled. We can ignore it.
+ # https://github.com/numpy/numpy/issues/3778#issuecomment-24885336
+ pytestmark = pytest.mark.filterwarnings(
+ "ignore:tp_compare didn't return -1 or -2 for exception"
+ )
+
+# The testdata is generated using https://github.com/python/cpython/...
+# Mac/Tools/plistlib_generate_testdata.py
+# which uses PyObjC to control the Cocoa classes for generating plists
+datadir = os.path.join(os.path.dirname(__file__), "testdata")
+with open(os.path.join(datadir, "test.plist"), "rb") as fp:
+ TESTDATA = fp.read()
+
+
+def _test_pl(use_builtin_types):
+ DataClass = bytes if use_builtin_types else plistlib.Data
+ pl = dict(
+ aString="Doodah",
+ aList=["A", "B", 12, 32.5, [1, 2, 3]],
+ aFloat=0.5,
+ anInt=728,
+ aBigInt=2 ** 63 - 44,
+ aBigInt2=2 ** 63 + 44,
+ aNegativeInt=-5,
+ aNegativeBigInt=-80000000000,
+ aDict=dict(
+ anotherString="<hello & 'hi' there!>",
+ aUnicodeValue="M\xe4ssig, Ma\xdf",
+ aTrueValue=True,
+ aFalseValue=False,
+ deeperDict=dict(a=17, b=32.5, c=[1, 2, "text"]),
+ ),
+ someData=DataClass(b"<binary gunk>"),
+ someMoreData=DataClass(b"<lots of binary gunk>\0\1\2\3" * 10),
+ nestedData=[DataClass(b"<lots of binary gunk>\0\1\2\3" * 10)],
+ aDate=datetime.datetime(2004, 10, 26, 10, 33, 33),
+ anEmptyDict=dict(),
+ anEmptyList=list(),
+ )
+ pl["\xc5benraa"] = "That was a unicode key."
+ return pl
+
+
+@pytest.fixture
+def pl():
+ return _test_pl(use_builtin_types=True)
+
+
+@pytest.fixture
+def pl_no_builtin_types():
+ return _test_pl(use_builtin_types=False)
+
+
+@pytest.fixture(
+ params=[True, False],
+ ids=["builtin=True", "builtin=False"],
+)
+def use_builtin_types(request):
+ return request.param
+
+
+@pytest.fixture
+def parametrized_pl(use_builtin_types):
+ return _test_pl(use_builtin_types), use_builtin_types
+
+
+def test__test_pl():
+ # sanity test that checks that the two values are equivalent
+ # (plistlib.Data implements __eq__ against bytes values)
+ pl = _test_pl(use_builtin_types=False)
+ pl2 = _test_pl(use_builtin_types=True)
+ assert pl == pl2
+
+
+def test_io(tmpdir, parametrized_pl):
+ pl, use_builtin_types = parametrized_pl
+ testpath = tmpdir / "test.plist"
+ with testpath.open("wb") as fp:
+ plistlib.dump(pl, fp, use_builtin_types=use_builtin_types)
+
+ with testpath.open("rb") as fp:
+ pl2 = plistlib.load(fp, use_builtin_types=use_builtin_types)
+
+ assert pl == pl2
+
+ with pytest.raises(AttributeError):
+ plistlib.dump(pl, "filename")
+
+ with pytest.raises(AttributeError):
+ plistlib.load("filename")
+
+
+def test_invalid_type():
+ pl = [object()]
+
+ with pytest.raises(TypeError):
+ plistlib.dumps(pl)
+
+
+@pytest.mark.parametrize(
+ "pl",
+ [
+ 0,
+ 2 ** 8 - 1,
+ 2 ** 8,
+ 2 ** 16 - 1,
+ 2 ** 16,
+ 2 ** 32 - 1,
+ 2 ** 32,
+ 2 ** 63 - 1,
+ 2 ** 64 - 1,
+ 1,
+ -2 ** 63,
+ ],
+)
+def test_int(pl):
+ data = plistlib.dumps(pl)
+ pl2 = plistlib.loads(data)
+ assert isinstance(pl2, Integral)
+ assert pl == pl2
+ data2 = plistlib.dumps(pl2)
+ assert data == data2
+
+
+@pytest.mark.parametrize(
+ "pl", [2 ** 64 + 1, 2 ** 127 - 1, -2 ** 64, -2 ** 127]
+)
+def test_int_overflow(pl):
+ with pytest.raises(OverflowError):
+ plistlib.dumps(pl)
+
+
+def test_bytearray(use_builtin_types):
+ DataClass = bytes if use_builtin_types else plistlib.Data
+ pl = DataClass(b"<binary gunk\0\1\2\3>")
+ array = bytearray(pl) if use_builtin_types else bytearray(pl.data)
+ data = plistlib.dumps(array)
+ pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types)
+ assert isinstance(pl2, DataClass)
+ assert pl2 == pl
+ data2 = plistlib.dumps(pl2, use_builtin_types=use_builtin_types)
+ assert data == data2
+
+
+@pytest.mark.parametrize(
+ "DataClass, use_builtin_types",
+ [(bytes, True), (plistlib.Data, True), (plistlib.Data, False)],
+ ids=[
+ "bytes|builtin_types=True",
+ "Data|builtin_types=True",
+ "Data|builtin_types=False",
+ ],
+)
+def test_bytes_data(DataClass, use_builtin_types):
+ pl = DataClass(b"<binary gunk\0\1\2\3>")
+ data = plistlib.dumps(pl, use_builtin_types=use_builtin_types)
+ pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types)
+ assert isinstance(pl2, bytes if use_builtin_types else plistlib.Data)
+ assert pl2 == pl
+ data2 = plistlib.dumps(pl2, use_builtin_types=use_builtin_types)
+ assert data == data2
+
+
+def test_bytes_string(use_builtin_types):
+ pl = b"some ASCII bytes"
+ data = plistlib.dumps(pl, use_builtin_types=False)
+ pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types)
+ assert isinstance(pl2, unicode) # it's always a <string>
+ assert pl2 == pl.decode()
+
+
+def test_indentation_array():
+ data = [[[[[[[[{"test": "aaaaaa"}]]]]]]]]
+ assert plistlib.loads(plistlib.dumps(data)) == data
+
+
+def test_indentation_dict():
+ data = {
+ "1": {"2": {"3": {"4": {"5": {"6": {"7": {"8": {"9": "aaaaaa"}}}}}}}}
+ }
+ assert plistlib.loads(plistlib.dumps(data)) == data
+
+
+def test_indentation_dict_mix():
+ data = {"1": {"2": [{"3": [[[[[{"test": "aaaaaa"}]]]]]}]}}
+ assert plistlib.loads(plistlib.dumps(data)) == data
+
+
+@pytest.mark.xfail(reason="we use two spaces, Apple uses tabs")
+def test_apple_formatting(parametrized_pl):
+ # we also split base64 data into multiple lines differently:
+ # both right-justify data to 76 chars, but Apple's treats tabs
+ # as 8 spaces, whereas we use 2 spaces
+ pl, use_builtin_types = parametrized_pl
+ pl = plistlib.loads(TESTDATA, use_builtin_types=use_builtin_types)
+ data = plistlib.dumps(pl, use_builtin_types=use_builtin_types)
+ assert data == TESTDATA
+
+
+def test_apple_formatting_fromliteral(parametrized_pl):
+ pl, use_builtin_types = parametrized_pl
+ pl2 = plistlib.loads(TESTDATA, use_builtin_types=use_builtin_types)
+ assert pl == pl2
+
+
+def test_apple_roundtrips(use_builtin_types):
+ pl = plistlib.loads(TESTDATA, use_builtin_types=use_builtin_types)
+ data = plistlib.dumps(pl, use_builtin_types=use_builtin_types)
+ pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types)
+ data2 = plistlib.dumps(pl2, use_builtin_types=use_builtin_types)
+ assert data == data2
+
+
+def test_bytesio(parametrized_pl):
+ pl, use_builtin_types = parametrized_pl
+ b = BytesIO()
+ plistlib.dump(pl, b, use_builtin_types=use_builtin_types)
+ pl2 = plistlib.load(
+ BytesIO(b.getvalue()), use_builtin_types=use_builtin_types
+ )
+ assert pl == pl2
+
+
+@pytest.mark.parametrize("sort_keys", [False, True])
+def test_keysort_bytesio(sort_keys):
+ pl = collections.OrderedDict()
+ pl["b"] = 1
+ pl["a"] = 2
+ pl["c"] = 3
+
+ b = BytesIO()
+
+ plistlib.dump(pl, b, sort_keys=sort_keys)
+ pl2 = plistlib.load(
+ BytesIO(b.getvalue()), dict_type=collections.OrderedDict
+ )
+
+ assert dict(pl) == dict(pl2)
+ if sort_keys:
+ assert list(pl2.keys()) == ["a", "b", "c"]
+ else:
+ assert list(pl2.keys()) == ["b", "a", "c"]
+
+
+@pytest.mark.parametrize("sort_keys", [False, True])
+def test_keysort(sort_keys):
+ pl = collections.OrderedDict()
+ pl["b"] = 1
+ pl["a"] = 2
+ pl["c"] = 3
+
+ data = plistlib.dumps(pl, sort_keys=sort_keys)
+ pl2 = plistlib.loads(data, dict_type=collections.OrderedDict)
+
+ assert dict(pl) == dict(pl2)
+ if sort_keys:
+ assert list(pl2.keys()) == ["a", "b", "c"]
+ else:
+ assert list(pl2.keys()) == ["b", "a", "c"]
+
+
+def test_keys_no_string():
+ pl = {42: "aNumber"}
+
+ with pytest.raises(TypeError):
+ plistlib.dumps(pl)
+
+ b = BytesIO()
+ with pytest.raises(TypeError):
+ plistlib.dump(pl, b)
+
+
+def test_skipkeys():
+ pl = {42: "aNumber", "snake": "aWord"}
+
+ data = plistlib.dumps(pl, skipkeys=True, sort_keys=False)
+
+ pl2 = plistlib.loads(data)
+ assert pl2 == {"snake": "aWord"}
+
+ fp = BytesIO()
+ plistlib.dump(pl, fp, skipkeys=True, sort_keys=False)
+ data = fp.getvalue()
+ pl2 = plistlib.loads(fp.getvalue())
+ assert pl2 == {"snake": "aWord"}
+
+
+def test_tuple_members():
+ pl = {"first": (1, 2), "second": (1, 2), "third": (3, 4)}
+
+ data = plistlib.dumps(pl)
+ pl2 = plistlib.loads(data)
+ assert pl2 == {"first": [1, 2], "second": [1, 2], "third": [3, 4]}
+ assert pl2["first"] is not pl2["second"]
+
+
+def test_list_members():
+ pl = {"first": [1, 2], "second": [1, 2], "third": [3, 4]}
+
+ data = plistlib.dumps(pl)
+ pl2 = plistlib.loads(data)
+ assert pl2 == {"first": [1, 2], "second": [1, 2], "third": [3, 4]}
+ assert pl2["first"] is not pl2["second"]
+
+
+def test_dict_members():
+ pl = {"first": {"a": 1}, "second": {"a": 1}, "third": {"b": 2}}
+
+ data = plistlib.dumps(pl)
+ pl2 = plistlib.loads(data)
+ assert pl2 == {"first": {"a": 1}, "second": {"a": 1}, "third": {"b": 2}}
+ assert pl2["first"] is not pl2["second"]
+
+
+def test_controlcharacters():
+ for i in range(128):
+ c = chr(i)
+ testString = "string containing %s" % c
+ if i >= 32 or c in "\r\n\t":
+ # \r, \n and \t are the only legal control chars in XML
+ data = plistlib.dumps(testString)
+ # the stdlib's plistlib writer, as well as the elementtree
+ # parser, always replace \r with \n inside string values;
+ # lxml doesn't (the ctrl character is escaped), so it roundtrips
+ if c != "\r" or etree._have_lxml:
+ assert plistlib.loads(data) == testString
+ else:
+ with pytest.raises(ValueError):
+ plistlib.dumps(testString)
+
+
+def test_non_bmp_characters():
+ pl = {"python": "\U0001f40d"}
+ data = plistlib.dumps(pl)
+ assert plistlib.loads(data) == pl
+
+
+def test_nondictroot():
+ test1 = "abc"
+ test2 = [1, 2, 3, "abc"]
+ result1 = plistlib.loads(plistlib.dumps(test1))
+ result2 = plistlib.loads(plistlib.dumps(test2))
+ assert test1 == result1
+ assert test2 == result2
+
+
+def test_invalidarray():
+ for i in [
+ "<key>key inside an array</key>",
+ "<key>key inside an array2</key><real>3</real>",
+ "<true/><key>key inside an array3</key>",
+ ]:
+ with pytest.raises(ValueError):
+ plistlib.loads(
+ ("<plist><array>%s</array></plist>" % i).encode("utf-8")
+ )
+
+
+def test_invaliddict():
+ for i in [
+ "<key><true/>k</key><string>compound key</string>",
+ "<key>single key</key>",
+ "<string>missing key</string>",
+ "<key>k1</key><string>v1</string><real>5.3</real>"
+ "<key>k1</key><key>k2</key><string>double key</string>",
+ ]:
+ with pytest.raises(ValueError):
+ plistlib.loads(("<plist><dict>%s</dict></plist>" % i).encode())
+ with pytest.raises(ValueError):
+ plistlib.loads(
+ ("<plist><array><dict>%s</dict></array></plist>" % i).encode()
+ )
+
+
+def test_invalidinteger():
+ with pytest.raises(ValueError):
+ plistlib.loads(b"<plist><integer>not integer</integer></plist>")
+
+
+def test_invalidreal():
+ with pytest.raises(ValueError):
+ plistlib.loads(b"<plist><integer>not real</integer></plist>")
+
+
+@pytest.mark.parametrize(
+ "xml_encoding, encoding, bom",
+ [
+ (b"utf-8", "utf-8", codecs.BOM_UTF8),
+ (b"utf-16", "utf-16-le", codecs.BOM_UTF16_LE),
+ (b"utf-16", "utf-16-be", codecs.BOM_UTF16_BE),
+ # expat parser (used by ElementTree) does't support UTF-32
+ # (b"utf-32", "utf-32-le", codecs.BOM_UTF32_LE),
+ # (b"utf-32", "utf-32-be", codecs.BOM_UTF32_BE),
+ ],
+)
+def test_xml_encodings(parametrized_pl, xml_encoding, encoding, bom):
+ pl, use_builtin_types = parametrized_pl
+ data = TESTDATA.replace(b"UTF-8", xml_encoding)
+ data = bom + data.decode("utf-8").encode(encoding)
+ pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types)
+ assert pl == pl2
+
+
+def test_fromtree(parametrized_pl):
+ pl, use_builtin_types = parametrized_pl
+ tree = etree.fromstring(TESTDATA)
+ pl2 = plistlib.fromtree(tree, use_builtin_types=use_builtin_types)
+ assert pl == pl2
+
+
+def _strip(txt):
+ return (
+ "".join(l.strip() for l in tounicode(txt, "utf-8").splitlines())
+ if txt is not None
+ else ""
+ )
+
+
+def test_totree(parametrized_pl):
+ pl, use_builtin_types = parametrized_pl
+ tree = etree.fromstring(TESTDATA)[0] # ignore root 'plist' element
+ tree2 = plistlib.totree(pl, use_builtin_types=use_builtin_types)
+ assert tree.tag == tree2.tag == "dict"
+ for (_, e1), (_, e2) in zip(etree.iterwalk(tree), etree.iterwalk(tree2)):
+ assert e1.tag == e2.tag
+ assert e1.attrib == e2.attrib
+ assert len(e1) == len(e2)
+ # ignore whitespace
+ assert _strip(e1.text) == _strip(e2.text)
+
+
+def test_no_pretty_print(use_builtin_types):
+ data = plistlib.dumps(
+ {"data": b"hello" if use_builtin_types else plistlib.Data(b"hello")},
+ pretty_print=False,
+ use_builtin_types=use_builtin_types,
+ )
+ assert data == (
+ plistlib.XML_DECLARATION
+ + plistlib.PLIST_DOCTYPE
+ + b'<plist version="1.0">'
+ b"<dict>"
+ b"<key>data</key>"
+ b"<data>aGVsbG8=</data>"
+ b"</dict>"
+ b"</plist>"
+ )
+
+
+def test_readPlist_from_path(pl):
+ path = os.path.join(datadir, "test.plist")
+ pl2 = readPlist(path)
+ assert isinstance(pl2["someData"], plistlib.Data)
+ assert pl2 == pl
+
+
+def test_readPlist_from_file(pl):
+ with open(os.path.join(datadir, "test.plist"), "rb") as f:
+ pl2 = readPlist(f)
+ assert isinstance(pl2["someData"], plistlib.Data)
+ assert pl2 == pl
+ assert not f.closed
+
+
+def test_readPlistFromString(pl):
+ pl2 = readPlistFromString(TESTDATA)
+ assert isinstance(pl2["someData"], plistlib.Data)
+ assert pl2 == pl
+
+
+def test_writePlist_to_path(tmpdir, pl_no_builtin_types):
+ testpath = tmpdir / "test.plist"
+ writePlist(pl_no_builtin_types, str(testpath))
+ with testpath.open("rb") as fp:
+ pl2 = plistlib.load(fp, use_builtin_types=False)
+ assert pl2 == pl_no_builtin_types
+
+
+def test_writePlist_to_file(tmpdir, pl_no_builtin_types):
+ testpath = tmpdir / "test.plist"
+ with testpath.open("wb") as fp:
+ writePlist(pl_no_builtin_types, fp)
+ with testpath.open("rb") as fp:
+ pl2 = plistlib.load(fp, use_builtin_types=False)
+ assert pl2 == pl_no_builtin_types
+
+
+def test_writePlistToString(pl_no_builtin_types):
+ data = writePlistToString(pl_no_builtin_types)
+ pl2 = plistlib.loads(data)
+ assert pl2 == pl_no_builtin_types
+
+
+def test_load_use_builtin_types_default():
+ pl = plistlib.loads(TESTDATA)
+ expected = plistlib.Data if PY2 else bytes
+ assert isinstance(pl["someData"], expected)
+
+
+def test_dump_use_builtin_types_default(pl_no_builtin_types):
+ data = plistlib.dumps(pl_no_builtin_types)
+ pl2 = plistlib.loads(data)
+ expected = plistlib.Data if PY2 else bytes
+ assert isinstance(pl2["someData"], expected)
+ assert pl2 == pl_no_builtin_types
+
+
+def test_non_ascii_bytes():
+ with pytest.raises(ValueError, match="invalid non-ASCII bytes"):
+ plistlib.dumps("\U0001f40d".encode("utf-8"), use_builtin_types=False)
+
+
+if __name__ == "__main__":
+ import sys
+
+ sys.exit(pytest.main(sys.argv))
diff --git a/Tests/misc/psCharStrings_test.py b/Tests/misc/psCharStrings_test.py
index 1e36246d..3f35ac85 100644
--- a/Tests/misc/psCharStrings_test.py
+++ b/Tests/misc/psCharStrings_test.py
@@ -26,6 +26,27 @@ class T2CharStringTest(unittest.TestCase):
bounds = cs.calcBounds(None)
self.assertEqual(bounds, (91.90524980688875, -12.5, 208.09475019311125, 100))
+ def test_charstring_bytecode_optimization(self):
+ cs = self.stringToT2CharString(
+ "100.0 100 rmoveto -50.0 -150 200.5 0.0 -50 150 rrcurveto endchar")
+ cs.isCFF2 = False
+ cs.private._isCFF2 = False
+ cs.compile()
+ cs.decompile()
+ self.assertEqual(
+ cs.program, [100, 100, 'rmoveto', -50, -150, 200.5, 0, -50, 150,
+ 'rrcurveto', 'endchar'])
+
+ cs2 = self.stringToT2CharString(
+ "100.0 rmoveto -50.0 -150 200.5 0.0 -50 150 rrcurveto")
+ cs2.isCFF2 = True
+ cs2.private._isCFF2 = True
+ cs2.compile(isCFF2=True)
+ cs2.decompile()
+ self.assertEqual(
+ cs2.program, [100, 'rmoveto', -50, -150, 200.5, 0, -50, 150,
+ 'rrcurveto'])
+
if __name__ == "__main__":
import sys
diff --git a/Tests/misc/testdata/test.plist b/Tests/misc/testdata/test.plist
new file mode 100644
index 00000000..864605f7
--- /dev/null
+++ b/Tests/misc/testdata/test.plist
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>aBigInt</key>
+ <integer>9223372036854775764</integer>
+ <key>aBigInt2</key>
+ <integer>9223372036854775852</integer>
+ <key>aDate</key>
+ <date>2004-10-26T10:33:33Z</date>
+ <key>aDict</key>
+ <dict>
+ <key>aFalseValue</key>
+ <false/>
+ <key>aTrueValue</key>
+ <true/>
+ <key>aUnicodeValue</key>
+ <string>Mässig, Maß</string>
+ <key>anotherString</key>
+ <string>&lt;hello &amp; 'hi' there!&gt;</string>
+ <key>deeperDict</key>
+ <dict>
+ <key>a</key>
+ <integer>17</integer>
+ <key>b</key>
+ <real>32.5</real>
+ <key>c</key>
+ <array>
+ <integer>1</integer>
+ <integer>2</integer>
+ <string>text</string>
+ </array>
+ </dict>
+ </dict>
+ <key>aFloat</key>
+ <real>0.5</real>
+ <key>aList</key>
+ <array>
+ <string>A</string>
+ <string>B</string>
+ <integer>12</integer>
+ <real>32.5</real>
+ <array>
+ <integer>1</integer>
+ <integer>2</integer>
+ <integer>3</integer>
+ </array>
+ </array>
+ <key>aNegativeBigInt</key>
+ <integer>-80000000000</integer>
+ <key>aNegativeInt</key>
+ <integer>-5</integer>
+ <key>aString</key>
+ <string>Doodah</string>
+ <key>anEmptyDict</key>
+ <dict/>
+ <key>anEmptyList</key>
+ <array/>
+ <key>anInt</key>
+ <integer>728</integer>
+ <key>nestedData</key>
+ <array>
+ <data>
+ PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r
+ PgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5
+ IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi
+ aW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz
+ IG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID
+ PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==
+ </data>
+ </array>
+ <key>someData</key>
+ <data>
+ PGJpbmFyeSBndW5rPg==
+ </data>
+ <key>someMoreData</key>
+ <data>
+ PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8
+ bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs
+ b3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv
+ dHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90
+ cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==
+ </data>
+ <key>Ã…benraa</key>
+ <string>That was a unicode key.</string>
+</dict>
+</plist>
diff --git a/Tests/pens/reverseContourPen_test.py b/Tests/pens/reverseContourPen_test.py
index bace8068..6dbc5e00 100644
--- a/Tests/pens/reverseContourPen_test.py
+++ b/Tests/pens/reverseContourPen_test.py
@@ -293,11 +293,8 @@ def test_reverse_pen(contour, expected):
@pytest.mark.parametrize("contour, expected", TEST_DATA)
def test_reverse_point_pen(contour, expected):
- try:
- from ufoLib.pointPen import (
- ReverseContourPointPen, PointToSegmentPen, SegmentToPointPen)
- except ImportError:
- pytest.skip("ufoLib not installed")
+ from fontTools.ufoLib.pointPen import (
+ ReverseContourPointPen, PointToSegmentPen, SegmentToPointPen)
recpen = RecordingPen()
pt2seg = PointToSegmentPen(recpen, outputImpliedClosingLine=True)
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index e9b3cba9..76d89c22 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -333,10 +333,8 @@ class SubsetTest(unittest.TestCase):
subsetter.populate(text='ABC')
font = TTFont(fontpath)
with CapturingLogHandler('fontTools.subset.timer', logging.DEBUG) as captor:
- captor.logger.propagate = False
subsetter.subset(font)
- logs = captor.records
- captor.logger.propagate = True
+ logs = captor.records
self.assertTrue(len(logs) > 5)
self.assertEqual(len(logs), len([l for l in logs if 'msg' in l.args and 'time' in l.args]))
diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py
index 205af843..d0786924 100644
--- a/Tests/ttLib/tables/_g_l_y_f_test.py
+++ b/Tests/ttLib/tables/_g_l_y_f_test.py
@@ -1,10 +1,14 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound
+from fontTools.ttLib import TTFont, newTable
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
import sys
import array
import pytest
+import re
+import os
+import unittest
class GlyphCoordinatesTest(object):
@@ -158,3 +162,60 @@ class GlyphCoordinatesTest(object):
g.append((0x8000, 0))
assert g.array.typecode == "d"
assert g.array == array.array("d", [1.0, 1.0, 32768.0, 0.0])
+
+
+CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+DATA_DIR = os.path.join(CURR_DIR, 'data')
+
+GLYF_TTX = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.ttx")
+GLYF_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.glyf.bin")
+HEAD_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.head.bin")
+LOCA_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.loca.bin")
+MAXP_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.maxp.bin")
+
+
+def strip_ttLibVersion(string):
+ return re.sub(' ttLibVersion=".*"', '', string)
+
+
+class glyfTableTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ with open(GLYF_BIN, 'rb') as f:
+ cls.glyfData = f.read()
+ with open(HEAD_BIN, 'rb') as f:
+ cls.headData = f.read()
+ with open(LOCA_BIN, 'rb') as f:
+ cls.locaData = f.read()
+ with open(MAXP_BIN, 'rb') as f:
+ cls.maxpData = f.read()
+ with open(GLYF_TTX, 'r') as f:
+ cls.glyfXML = strip_ttLibVersion(f.read()).splitlines()
+
+ def test_toXML(self):
+ font = TTFont(sfntVersion="\x00\x01\x00\x00")
+ glyfTable = font['glyf'] = newTable('glyf')
+ font['head'] = newTable('head')
+ font['loca'] = newTable('loca')
+ font['maxp'] = newTable('maxp')
+ font['maxp'].decompile(self.maxpData, font)
+ font['head'].decompile(self.headData, font)
+ font['loca'].decompile(self.locaData, font)
+ glyfTable.decompile(self.glyfData, font)
+ out = UnicodeIO()
+ font.saveXML(out)
+ glyfXML = strip_ttLibVersion(out.getvalue()).splitlines()
+ self.assertEqual(glyfXML, self.glyfXML)
+
+ def test_fromXML(self):
+ font = TTFont(sfntVersion="\x00\x01\x00\x00")
+ font.importXML(GLYF_TTX)
+ glyfTable = font['glyf']
+ glyfData = glyfTable.compile(font)
+ self.assertEqual(glyfData, self.glyfData)
+
+
+if __name__ == "__main__":
+ import sys
+ sys.exit(unittest.main())
diff --git a/Tests/ttLib/tables/_m_o_r_x_test.py b/Tests/ttLib/tables/_m_o_r_x_test.py
index 7deb39f9..f4e7fb89 100644
--- a/Tests/ttLib/tables/_m_o_r_x_test.py
+++ b/Tests/ttLib/tables/_m_o_r_x_test.py
@@ -708,6 +708,160 @@ MORX_LIGATURE_XML = [
]
+# Taken from the `morx` table of the second font in DevanagariSangamMN.ttc
+# on macOS X 10.12.6; manually pruned to just contain the insertion lookup.
+MORX_INSERTION_DATA = deHexStr(
+ '0002 0000 ' # 0: Version=2, Reserved=0
+ '0000 0001 ' # 4: MorphChainCount=1
+ '0000 0001 ' # 8: DefaultFlags=1
+ '0000 00A4 ' # 12: StructLength=164 (+8=172)
+ '0000 0000 ' # 16: MorphFeatureCount=0
+ '0000 0001 ' # 20: MorphSubtableCount=1
+ '0000 0094 ' # 24: Subtable[0].StructLength=148 (+24=172)
+ '00 ' # 28: Subtable[0].CoverageFlags=0x00
+ '00 00 ' # 29: Subtable[0].Reserved=0
+ '05 ' # 31: Subtable[0].MorphType=5/InsertionMorph
+ '0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1
+ '0000 0006 ' # 36: STXHeader.ClassCount=6
+ '0000 0014 ' # 40: STXHeader.ClassTableOffset=20 (+36=56)
+ '0000 004A ' # 44: STXHeader.StateArrayOffset=74 (+36=110)
+ '0000 006E ' # 48: STXHeader.EntryTableOffset=110 (+36=146)
+ '0000 0086 ' # 52: STXHeader.InsertionActionOffset=134 (+36=170)
+ # Glyph class table.
+ '0002 0006 ' # 56: ClassTable.LookupFormat=2, .UnitSize=6
+ '0006 0018 ' # 60: .NUnits=6, .SearchRange=24
+ '0002 000C ' # 64: .EntrySelector=2, .RangeShift=12
+ '00AC 00AC 0005 ' # 68: GlyphID 172..172 -> GlyphClass 5
+ '01EB 01E6 0005 ' # 74: GlyphID 486..491 -> GlyphClass 5
+ '01F0 01F0 0004 ' # 80: GlyphID 496..496 -> GlyphClass 4
+ '01F8 01F6 0004 ' # 88: GlyphID 502..504 -> GlyphClass 4
+ '01FC 01FA 0004 ' # 92: GlyphID 506..508 -> GlyphClass 4
+ '0250 0250 0005 ' # 98: GlyphID 592..592 -> GlyphClass 5
+ 'FFFF FFFF 0000 ' # 104: <end of lookup>
+ # State array.
+ '0000 0000 0000 0000 0001 0000 ' # 110: State[0][0..5]
+ '0000 0000 0000 0000 0001 0000 ' # 122: State[1][0..5]
+ '0000 0000 0001 0000 0001 0002 ' # 134: State[2][0..5]
+ # Entry table.
+ '0000 0000 ' # 146: Entries[0].NewState=0, .Flags=0
+ 'FFFF ' # 150: Entries[0].CurrentInsertIndex=<None>
+ 'FFFF ' # 152: Entries[0].MarkedInsertIndex=<None>
+ '0002 0000 ' # 154: Entries[1].NewState=0, .Flags=0
+ 'FFFF ' # 158: Entries[1].CurrentInsertIndex=<None>
+ 'FFFF ' # 160: Entries[1].MarkedInsertIndex=<None>
+ '0000 ' # 162: Entries[2].NewState=0
+ '2820 ' # 164: .Flags=CurrentIsKashidaLike,CurrentInsertBefore
+ # .CurrentInsertCount=1, .MarkedInsertCount=0
+ '0000 ' # 166: Entries[1].CurrentInsertIndex=0
+ 'FFFF ' # 168: Entries[1].MarkedInsertIndex=<None>
+ # Insertion action table.
+ '022F' # 170: InsertionActionTable[0]=GlyphID 559
+) # 172: <end>
+assert len(MORX_INSERTION_DATA) == 172, len(MORX_INSERTION_DATA)
+
+
+MORX_INSERTION_XML = [
+ '<Version value="2"/>',
+ '<Reserved value="0"/>',
+ '<!-- MorphChainCount=1 -->',
+ '<MorphChain index="0">',
+ ' <DefaultFlags value="0x00000001"/>',
+ ' <!-- StructLength=164 -->',
+ ' <!-- MorphFeatureCount=0 -->',
+ ' <!-- MorphSubtableCount=1 -->',
+ ' <MorphSubtable index="0">',
+ ' <!-- StructLength=148 -->',
+ ' <TextDirection value="Horizontal"/>',
+ ' <ProcessingOrder value="LayoutOrder"/>',
+ ' <!-- MorphType=5 -->',
+ ' <SubFeatureFlags value="0x00000001"/>',
+ ' <InsertionMorph>',
+ ' <StateTable>',
+ ' <!-- GlyphClassCount=6 -->',
+ ' <GlyphClass glyph="g.172" value="5"/>',
+ ' <GlyphClass glyph="g.486" value="5"/>',
+ ' <GlyphClass glyph="g.487" value="5"/>',
+ ' <GlyphClass glyph="g.488" value="5"/>',
+ ' <GlyphClass glyph="g.489" value="5"/>',
+ ' <GlyphClass glyph="g.490" value="5"/>',
+ ' <GlyphClass glyph="g.491" value="5"/>',
+ ' <GlyphClass glyph="g.496" value="4"/>',
+ ' <GlyphClass glyph="g.502" value="4"/>',
+ ' <GlyphClass glyph="g.503" value="4"/>',
+ ' <GlyphClass glyph="g.504" value="4"/>',
+ ' <GlyphClass glyph="g.506" value="4"/>',
+ ' <GlyphClass glyph="g.507" value="4"/>',
+ ' <GlyphClass glyph="g.508" value="4"/>',
+ ' <GlyphClass glyph="g.592" value="5"/>',
+ ' <State index="0">',
+ ' <Transition onGlyphClass="0">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="1">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="2">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="3">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="4">',
+ ' <NewState value="2"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="5">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' </State>',
+ ' <State index="1">',
+ ' <Transition onGlyphClass="0">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="1">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="2">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="3">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="4">',
+ ' <NewState value="2"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="5">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' </State>',
+ ' <State index="2">',
+ ' <Transition onGlyphClass="0">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="1">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="2">',
+ ' <NewState value="2"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="3">',
+ ' <NewState value="0"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="4">',
+ ' <NewState value="2"/>',
+ ' </Transition>',
+ ' <Transition onGlyphClass="5">',
+ ' <NewState value="0"/>',
+ ' <Flags value="CurrentIsKashidaLike,CurrentInsertBefore"/>',
+ ' <CurrentInsertionAction glyph="g.559"/>',
+ ' </Transition>',
+ ' </State>',
+ ' </StateTable>',
+ ' </InsertionMorph>',
+ ' </MorphSubtable>',
+ '</MorphChain>',
+]
+
+
class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase):
@classmethod
@@ -802,6 +956,26 @@ class MORXLigatureSubstitutionTest(unittest.TestCase):
hexStr(MORX_LIGATURE_DATA))
+class MORXGlyphInsertionTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.maxDiff = None
+ cls.font = FakeFont(['.notdef'] + ['g.%d' % i for i in range (1, 910)])
+
+ def test_decompile_toXML(self):
+ table = newTable('morx')
+ table.decompile(MORX_INSERTION_DATA, self.font)
+ self.assertEqual(getXML(table.toXML), MORX_INSERTION_XML)
+
+ def test_compile_fromXML(self):
+ table = newTable('morx')
+ for name, attrs, content in parseXML(MORX_INSERTION_XML):
+ table.fromXML(name, attrs, content, font=self.font)
+ self.assertEqual(hexStr(table.compile(self.font)),
+ hexStr(MORX_INSERTION_DATA))
+
+
class MORXCoverageFlagsTest(unittest.TestCase):
@classmethod
diff --git a/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.glyf.bin b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.glyf.bin
new file mode 100644
index 00000000..4e18ddbd
--- /dev/null
+++ b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.glyf.bin
Binary files differ
diff --git a/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.head.bin b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.head.bin
new file mode 100644
index 00000000..3114f38b
--- /dev/null
+++ b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.head.bin
Binary files differ
diff --git a/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.loca.bin b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.loca.bin
new file mode 100644
index 00000000..d0a95fd5
--- /dev/null
+++ b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.loca.bin
Binary files differ
diff --git a/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.maxp.bin b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.maxp.bin
new file mode 100644
index 00000000..7fbd12eb
--- /dev/null
+++ b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.maxp.bin
Binary files differ
diff --git a/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.ttx b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.ttx
new file mode 100644
index 00000000..d0194d62
--- /dev/null
+++ b/Tests/ttLib/tables/data/_g_l_y_f_outline_flag_bit6.ttx
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.29">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="glyph00001"/>
+ <GlyphID id="2" name="glyph00002"/>
+ <GlyphID id="3" name="glyph00003"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x6e4f1ccf"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00100000 00011011"/>
+ <unitsPerEm value="2048"/>
+ <created value="Thu Sep 13 14:22:20 2018"/>
+ <modified value="Fri Sep 14 09:25:13 2018"/>
+ <xMin value="12"/>
+ <yMin value="0"/>
+ <xMax value="1172"/>
+ <yMax value="1430"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="9"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="4"/>
+ <maxPoints value="11"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="10"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="512"/>
+ <maxSizeOfInstructions value="353"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef"/><!-- contains no outline data -->
+
+ <TTGlyph name="glyph00001"/><!-- contains no outline data -->
+
+ <TTGlyph name="glyph00002"/><!-- contains no outline data -->
+
+ <TTGlyph name="glyph00003" xMin="12" yMin="0" xMax="1172" yMax="1430">
+ <contour>
+ <pt x="501" y="1430" on="1" overlap="1"/>
+ <pt x="683" y="1430" on="1"/>
+ <pt x="1172" y="0" on="1"/>
+ <pt x="983" y="0" on="1"/>
+ <pt x="591" y="1193" on="1"/>
+ <pt x="199" y="0" on="1"/>
+ <pt x="12" y="0" on="1"/>
+ </contour>
+ <contour>
+ <pt x="249" y="514" on="1"/>
+ <pt x="935" y="514" on="1"/>
+ <pt x="935" y="352" on="1"/>
+ <pt x="249" y="352" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+</ttFont>
diff --git a/Tests/ttLib/tables/otTables_test.py b/Tests/ttLib/tables/otTables_test.py
index e02b22f8..5a771e61 100644
--- a/Tests/ttLib/tables/otTables_test.py
+++ b/Tests/ttLib/tables/otTables_test.py
@@ -383,6 +383,10 @@ class RearrangementMorphActionTest(unittest.TestCase):
r.compile(writer, self.font, actionIndex=None)
self.assertEqual(hexStr(writer.getAllData()), "1234fffd")
+ def testCompileActions(self):
+ act = otTables.RearrangementMorphAction()
+ self.assertEqual(act.compileActions(self.font, []), (None, None))
+
def testDecompileToXML(self):
r = otTables.RearrangementMorphAction()
r.decompile(OTTableReader(deHexStr("1234fffd")),
@@ -411,6 +415,10 @@ class ContextualMorphActionTest(unittest.TestCase):
a.compile(writer, self.font, actionIndex=None)
self.assertEqual(hexStr(writer.getAllData()), "1234f117deadbeef")
+ def testCompileActions(self):
+ act = otTables.ContextualMorphAction()
+ self.assertEqual(act.compileActions(self.font, []), (None, None))
+
def testDecompileToXML(self):
a = otTables.ContextualMorphAction()
a.decompile(OTTableReader(deHexStr("1234f117deadbeef")),
@@ -447,6 +455,32 @@ class LigatureMorphActionTest(unittest.TestCase):
'</Transition>',
])
+ def testCompileActions_empty(self):
+ act = otTables.LigatureMorphAction()
+ actions, actionIndex = act.compileActions(self.font, [])
+ self.assertEqual(actions, b'')
+ self.assertEqual(actionIndex, {})
+
+ def testCompileActions_shouldShareSubsequences(self):
+ state = otTables.AATState()
+ t = state.Transitions = {i: otTables.LigatureMorphAction()
+ for i in range(3)}
+ ligs = [otTables.LigAction() for _ in range(3)]
+ for i, lig in enumerate(ligs):
+ lig.GlyphIndexDelta = i
+ t[0].Actions = ligs[1:2]
+ t[1].Actions = ligs[0:3]
+ t[2].Actions = ligs[1:3]
+ actions, actionIndex = t[0].compileActions(self.font, [state])
+ self.assertEqual(actions,
+ deHexStr("00000000 00000001 80000002 80000001"))
+ self.assertEqual(actionIndex, {
+ deHexStr("00000000 00000001 80000002"): 0,
+ deHexStr("00000001 80000002"): 1,
+ deHexStr("80000002"): 2,
+ deHexStr("80000001"): 3,
+ })
+
class InsertionMorphActionTest(unittest.TestCase):
MORPH_ACTION_XML = [
@@ -484,6 +518,78 @@ class InsertionMorphActionTest(unittest.TestCase):
actionIndex={('B', 'C'): 9, ('B', 'A', 'D'): 7})
self.assertEqual(hexStr(writer.getAllData()), "1234fc4300090007")
+ def testCompileActions_empty(self):
+ act = otTables.InsertionMorphAction()
+ actions, actionIndex = act.compileActions(self.font, [])
+ self.assertEqual(actions, b'')
+ self.assertEqual(actionIndex, {})
+
+ def testCompileActions_shouldShareSubsequences(self):
+ state = otTables.AATState()
+ t = state.Transitions = {i: otTables.InsertionMorphAction()
+ for i in range(3)}
+ t[1].CurrentInsertionAction = []
+ t[0].MarkedInsertionAction = ['A']
+ t[1].CurrentInsertionAction = ['C', 'D']
+ t[1].MarkedInsertionAction = ['B']
+ t[2].CurrentInsertionAction = ['B', 'C', 'D']
+ t[2].MarkedInsertionAction = ['C', 'D']
+ actions, actionIndex = t[0].compileActions(self.font, [state])
+ self.assertEqual(actions, deHexStr('0002 0003 0004 0001'))
+ self.assertEqual(actionIndex, {
+ ('A',): 3,
+ ('B',): 0,
+ ('B', 'C'): 0,
+ ('B', 'C', 'D'): 0,
+ ('C',): 1,
+ ('C', 'D'): 1,
+ ('D',): 2,
+ })
+
+
+def test_splitMarkBasePos():
+ from fontTools.otlLib.builder import buildAnchor, buildMarkBasePosSubtable
+
+ marks = {
+ "acutecomb": (0, buildAnchor(0, 600)),
+ "gravecomb": (0, buildAnchor(0, 590)),
+ "cedillacomb": (1, buildAnchor(0, 0)),
+ }
+ bases = {
+ "a": {
+ 0: buildAnchor(350, 500),
+ 1: None,
+ },
+ "c": {
+ 0: buildAnchor(300, 700),
+ 1: buildAnchor(300, 0),
+ },
+ }
+ glyphOrder = ["a", "c", "acutecomb", "gravecomb", "cedillacomb"]
+ glyphMap = {g: i for i, g in enumerate(glyphOrder)}
+
+ oldSubTable = buildMarkBasePosSubtable(marks, bases, glyphMap)
+ oldSubTable.MarkCoverage.Format = oldSubTable.BaseCoverage.Format = 1
+ newSubTable = otTables.MarkBasePos()
+
+ ok = otTables.splitMarkBasePos(oldSubTable, newSubTable, overflowRecord=None)
+
+ assert ok
+ assert oldSubTable.Format == newSubTable.Format
+ assert oldSubTable.MarkCoverage.glyphs == [
+ "acutecomb", "gravecomb"
+ ]
+ assert newSubTable.MarkCoverage.glyphs == ["cedillacomb"]
+ assert newSubTable.MarkCoverage.Format == 1
+ assert oldSubTable.BaseCoverage.glyphs == newSubTable.BaseCoverage.glyphs
+ assert newSubTable.BaseCoverage.Format == 1
+ assert oldSubTable.ClassCount == newSubTable.ClassCount == 1
+ assert oldSubTable.MarkArray.MarkCount == 2
+ assert newSubTable.MarkArray.MarkCount == 1
+ assert oldSubTable.BaseArray.BaseCount == newSubTable.BaseArray.BaseCount
+ assert newSubTable.BaseArray.BaseRecord[0].BaseAnchor[0] is None
+ assert newSubTable.BaseArray.BaseRecord[1].BaseAnchor[0] == buildAnchor(300, 0)
+
if __name__ == "__main__":
import sys
diff --git a/Tests/ufoLib/GLIF1_test.py b/Tests/ufoLib/GLIF1_test.py
new file mode 100644
index 00000000..707c2097
--- /dev/null
+++ b/Tests/ufoLib/GLIF1_test.py
@@ -0,0 +1,1337 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import unittest
+from fontTools.ufoLib.glifLib import GlifLibError, readGlyphFromString, writeGlyphToString
+from .testSupport import Glyph, stripText
+from itertools import islice
+
+try:
+ basestring
+except NameError:
+ basestring = str
+# ----------
+# Test Cases
+# ----------
+
+class TestGLIF1(unittest.TestCase):
+
+ def assertEqual(self, first, second, msg=None):
+ if isinstance(first, basestring):
+ first = stripText(first)
+ if isinstance(second, basestring):
+ second = stripText(second)
+ return super(TestGLIF1, self).assertEqual(first, second, msg=msg)
+
+ def pyToGLIF(self, py):
+ py = stripText(py)
+ glyph = Glyph()
+ exec(py, {"glyph" : glyph, "pointPen" : glyph})
+ glif = writeGlyphToString(glyph.name, glyphObject=glyph, drawPointsFunc=glyph.drawPoints, formatVersion=1, validate=True)
+ # discard the first line containing the xml declaration
+ return "\n".join(islice(glif.splitlines(), 1, None))
+
+ def glifToPy(self, glif):
+ glif = stripText(glif)
+ glif = "<?xml version=\"1.0\"?>\n" + glif
+ glyph = Glyph()
+ readGlyphFromString(glif, glyphObject=glyph, pointPen=glyph, validate=True)
+ return glyph.py()
+
+ def testTopElement(self):
+ # not glyph
+ glif = """
+ <notglyph name="a" format="1">
+ <outline>
+ </outline>
+ </notglyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testName_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testName_empty(self):
+ # empty
+ glif = """
+ <glyph name="" format="1">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = ""
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testName_not_a_string(self):
+ # not a string
+ py = """
+ glyph.name = 1
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+
+ def testFormat_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testFormat_wrong_number(self):
+ # wrong number
+ glif = """
+ <glyph name="a" format="-1">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testFormat_not_an_int(self):
+ # not an int
+ glif = """
+ <glyph name="a" format="A">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testBogusGlyphStructure_unknown_element(self):
+ # unknown element
+ glif = """
+ <glyph name="a" format="1">
+ <unknown />
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testBogusGlyphStructure_content(self):
+ # content
+ glif = """
+ <glyph name="a" format="1">
+ Hello World.
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAdvance_legal_width_and_height(self):
+ # legal: width and height
+ glif = """
+ <glyph name="a" format="1">
+ <advance height="200" width="100"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = 100
+ glyph.height = 200
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_legal_width_and_height_floats(self):
+ # legal: width and height floats
+ glif = """
+ <glyph name="a" format="1">
+ <advance height="200.1" width="100.1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = 100.1
+ glyph.height = 200.1
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_legal_width(self):
+ # legal: width
+ glif = """
+ <glyph name="a" format="1">
+ <advance width="100"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = 100
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_legal_height(self):
+ # legal: height
+ glif = """
+ <glyph name="a" format="1">
+ <advance height="200"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.height = 200
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_illegal_width(self):
+ # illegal: not a number
+ glif = """
+ <glyph name="a" format="1">
+ <advance width="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = "a"
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAdvance_illegal_height(self):
+ glif = """
+ <glyph name="a" format="1">
+ <advance height="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.height = "a"
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testUnicodes_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <unicode hex="0061"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.unicodes = [97]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testUnicodes_legal_multiple(self):
+ glif = """
+ <glyph name="a" format="1">
+ <unicode hex="0062"/>
+ <unicode hex="0063"/>
+ <unicode hex="0061"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.unicodes = [98, 99, 97]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testUnicodes_illegal(self):
+ # illegal
+ glif = """
+ <glyph name="a" format="1">
+ <unicode hex="1.1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "zzzzzz"
+ glyph.unicodes = ["1.1"]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testNote(self):
+ glif = """
+ <glyph name="a" format="1">
+ <note>
+ \U0001F4A9
+ </note>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.note = "💩"
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testLib_legal(self):
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ </outline>
+ <lib>
+ <dict>
+ <key>dict</key>
+ <dict>
+ <key>hello</key>
+ <string>world</string>
+ </dict>
+ <key>float</key>
+ <real>2.5</real>
+ <key>int</key>
+ <integer>1</integer>
+ <key>list</key>
+ <array>
+ <string>a</string>
+ <string>b</string>
+ <integer>1</integer>
+ <real>2.5</real>
+ </array>
+ <key>string</key>
+ <string>a</string>
+ </dict>
+ </lib>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.lib = {"dict" : {"hello" : "world"}, "float" : 2.5, "int" : 1, "list" : ["a", "b", 1, 2.5], "string" : "a"}
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testOutline_unknown_element(self):
+ # unknown element
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <unknown/>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testOutline_content(self):
+ # content
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ hello
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testComponent_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, 5, 1, 4)])
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testComponent_illegal_no_base(self):
+ # no base
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testComponent_bogus_transformation(self):
+ # bogus values in transformation
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="a" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", ("a", 3, 6, 5, 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="a" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, "a", 6, 5, 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="a" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, "a", 5, 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="a" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, "a", 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="a" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, 5, "a", 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="a"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, 5, 1, "a")])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testContour_legal_one_contour(self):
+ # legal: one contour
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testContour_legal_two_contours(self):
+ # legal: two contours
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ <point x="10" y="20" type="line"/>
+ </contour>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ <point x="10" y="20" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(10, 20)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(10, 20)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testContour_illegal_unkonwn_element(self):
+ # unknown element
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <unknown/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointCoordinates_legal_int(self):
+ # legal: int
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="0" y="0" type="line" name="this is here so that the contour isn't seen as an anchor"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"name" : "this is here so that the contour isn't seen as an anchor", "segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointCoordinates_legal_float(self):
+ # legal: float
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1.1" y="-2.2" type="move"/>
+ <point x="0" y="0" type="line" name="this is here so that the contour isn't seen as an anchor"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1.1, -2.2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"name" : "this is here so that the contour isn't seen as an anchor", "segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointCoordinates_illegal_x(self):
+ # illegal: string
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="a" y="2" type="move"/>
+ <point x="0" y="0" type="line" name="this is here so that the contour isn't seen as an anchor"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[("a", 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"name" : "this is here so that the contour isn't seen as an anchor", "segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointCoordinates_illegal_y(self):
+ # legal: int
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="a" type="move"/>
+ <point x="0" y="0" type="line" name="this is here so that the contour isn't seen as an anchor"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, "a")], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"name" : "this is here so that the contour isn't seen as an anchor", "segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeMove_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeMove_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move" smooth="yes"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : True})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeMove_illegal_not_at_start(self):
+ # illegal: not at start
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="3" y="-4" type="line"/>
+ <point x="1" y="-2" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeLine_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeLine_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="line"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeLine_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="3" y="-4" type="line" smooth="yes"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : True})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="100" y="200" type="curve"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve" smooth="yes"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : True})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_no_off_curves(self):
+ # legal: no off-curves
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_1_off_curve(self):
+ # legal: 1 off-curve
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="50" y="100"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(50, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_illegal_3_off_curves(self):
+ # illegal: 3 off-curves
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="100"/>
+ <point x="35" y="125"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(35, 125)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointQCurve_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="100" y="200" type="qcurve"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="qcurve" smooth="yes"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : True})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_no_off_curves(self):
+ # legal: no off-curves
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_one_off_curve(self):
+ # legal: 1 off-curve
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="50" y="100"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(50, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_3_off_curves(self):
+ # legal: 3 off-curves
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="100"/>
+ <point x="35" y="125"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(35, 125)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testSpecialCaseQCurve(self):
+ # contour with no on curve
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0"/>
+ <point x="0" y="100"/>
+ <point x="100" y="100"/>
+ <point x="100" y="0"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"smooth" : False})
+ pointPen.addPoint(*[(0, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 0)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeOffCurve_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeOffCurve_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeOffCurve_illegal_before_move(self):
+ # before move
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="65"/>
+ <point x="0" y="0" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeOffCurve_illegal_before_line(self):
+ # before line
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="65"/>
+ <point x="0" y="0" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeOffCurve_illegal_smooth(self):
+ # smooth=True
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="0" y="65" smooth="yes"/>
+ <point x="0" y="0" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : True})
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testSinglePoint_legal_without_name(self):
+ # legal
+ # glif format 1 single point without a name was not an anchor
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAnchor_legal_with_name(self):
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="2" type="move" name="test"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.anchors = [{"name" : "test", "x" : 1, "y" : 2}]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testOpenContourLooseOffCurves_legal(self):
+ # a piece of software was writing this kind of structure
+ glif = """
+ <glyph name="a" format="1">
+ <outline>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ <point x="1" y="2"/>
+ <point x="1" y="2"/>
+ <point x="1" y="2" type="curve"/>
+ <point x="1" y="2"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ expectedPy = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(resultPy, expectedPy)
+
+ def testOpenContourLooseOffCurves_illegal(self):
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
diff --git a/Tests/ufoLib/GLIF2_test.py b/Tests/ufoLib/GLIF2_test.py
new file mode 100644
index 00000000..2daa4533
--- /dev/null
+++ b/Tests/ufoLib/GLIF2_test.py
@@ -0,0 +1,2372 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import unittest
+from fontTools.ufoLib.glifLib import GlifLibError, readGlyphFromString, writeGlyphToString
+from .testSupport import Glyph, stripText
+from itertools import islice
+
+try:
+ basestring
+except NameError:
+ basestring = str
+# ----------
+# Test Cases
+# ----------
+
+class TestGLIF2(unittest.TestCase):
+
+ def assertEqual(self, first, second, msg=None):
+ if isinstance(first, basestring):
+ first = stripText(first)
+ if isinstance(second, basestring):
+ second = stripText(second)
+ return super(TestGLIF2, self).assertEqual(first, second, msg=msg)
+
+ def pyToGLIF(self, py):
+ py = stripText(py)
+ glyph = Glyph()
+ exec(py, {"glyph" : glyph, "pointPen" : glyph})
+ glif = writeGlyphToString(glyph.name, glyphObject=glyph, drawPointsFunc=glyph.drawPoints, formatVersion=2, validate=True)
+ # discard the first line containing the xml declaration
+ return "\n".join(islice(glif.splitlines(), 1, None))
+
+ def glifToPy(self, glif):
+ glif = stripText(glif)
+ glif = "<?xml version=\"1.0\"?>\n" + glif
+ glyph = Glyph()
+ readGlyphFromString(glif, glyphObject=glyph, pointPen=glyph, validate=True)
+ return glyph.py()
+
+ def testTopElement(self):
+ # not glyph
+ glif = """
+ <notglyph name="a" format="2">
+ <outline>
+ </outline>
+ </notglyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testName_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testName_empty(self):
+ # empty
+ glif = """
+ <glyph name="" format="2">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = ""
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testName_not_a_string(self):
+ # not a string
+ py = """
+ glyph.name = 1
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+
+ def testFormat_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testFormat_illegal_wrong_number(self):
+ # wrong number
+ glif = """
+ <glyph name="a" format="-1">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testFormat_illegal_not_int(self):
+ # not an int
+ glif = """
+ <glyph name="a" format="A">
+ <outline>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testBogusGlyphStructure_unknown_element(self):
+ # unknown element
+ glif = """
+ <glyph name="a" format="2">
+ <unknown />
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testBogusGlyphStructure_content(self):
+ # content
+ glif = """
+ <glyph name="a" format="2">
+ Hello World.
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAdvance_legal_widht_and_height(self):
+ # legal: width and height
+ glif = """
+ <glyph name="a" format="2">
+ <advance height="200" width="100"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = 100
+ glyph.height = 200
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_legal_width_and_height_floats(self):
+ # legal: width and height floats
+ glif = """
+ <glyph name="a" format="2">
+ <advance height="200.1" width="100.1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = 100.1
+ glyph.height = 200.1
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_legal_width(self):
+ # legal: width
+ glif = """
+ <glyph name="a" format="2">
+ <advance width="100"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = 100
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_legal_height(self):
+ # legal: height
+ glif = """
+ <glyph name="a" format="2">
+ <advance height="200"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.height = 200
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAdvance_illegal_width(self):
+ # illegal: not a number
+ glif = """
+ <glyph name="a" format="2">
+ <advance width="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.width = "a"
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAdvance_illegal_height(self):
+ glif = """
+ <glyph name="a" format="2">
+ <advance height="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.height = "a"
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testUnicodes_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <unicode hex="0061"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.unicodes = [97]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testUnicodes_legal_multiple(self):
+ glif = """
+ <glyph name="a" format="2">
+ <unicode hex="0062"/>
+ <unicode hex="0063"/>
+ <unicode hex="0061"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.unicodes = [98, 99, 97]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testUnicodes_illegal(self):
+ # illegal
+ glif = """
+ <glyph name="a" format="2">
+ <unicode hex="1.1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "zzzzzz"
+ glyph.unicodes = ["1.1"]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testNote(self):
+ glif = """
+ <glyph name="a" format="2">
+ <note>
+ hëllö
+ </note>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.note = "hëllö"
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testLib(self):
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ </outline>
+ <lib>
+ <dict>
+ <key>dict</key>
+ <dict>
+ <key>hello</key>
+ <string>world</string>
+ </dict>
+ <key>float</key>
+ <real>2.5</real>
+ <key>int</key>
+ <integer>1</integer>
+ <key>list</key>
+ <array>
+ <string>a</string>
+ <string>b</string>
+ <integer>1</integer>
+ <real>2.5</real>
+ </array>
+ <key>string</key>
+ <string>a</string>
+ </dict>
+ </lib>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.lib = {"dict" : {"hello" : "world"}, "float" : 2.5, "int" : 1, "list" : ["a", "b", 1, 2.5], "string" : "a"}
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testGuidelines_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1"/>
+ <guideline y="1"/>
+ <guideline x="1" y="1" angle="0"/>
+ <guideline x="1" y="1" angle="360"/>
+ <guideline x="1.1" y="1.1" angle="45.5"/>
+ <guideline x="1" name="a"/>
+ <guideline x="1" color="1,1,1,1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"x" : 1}, {"y" : 1}, {"angle" : 0, "x" : 1, "y" : 1}, {"angle" : 360, "x" : 1, "y" : 1}, {"angle" : 45.5, "x" : 1.1, "y" : 1.1}, {"name" : "a", "x" : 1}, {"color" : "1,1,1,1", "x" : 1}]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testGuidelines_illegal_x(self):
+ # x not an int or float
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="a" y="1" angle="45"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : 45, "x" : "a", "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testGuidelines_illegal_y(self):
+ # y not an int or float
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1" y="y" angle="45"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : 45, "x" : 1, "y" : "a"}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testGuidelines_illegal_angle(self):
+ # angle not an int or float
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1" y="1" angle="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : "a", "x" : 1, "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testGuidelines_illegal_x_missing(self):
+ # x missing
+ glif = """
+ <glyph name="a" format="2">
+ <guideline y="1" angle="45"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : 45, "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testGuidelines_illegal_y_missing(self):
+ # y missing
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1" angle="45"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : 45, "x" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testGuidelines_illegal_angle_missing(self):
+ # angle missing
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1" y="1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"x" : 1, "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testGuidelines_illegal_angle_out_of_range(self):
+ # angle out of range
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1" y="1" angle="-1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : -1, "x" : "1", "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="1" y="1" angle="361"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"angle" : 361, "x" : "1", "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAnchors_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <anchor x="1" y="2" name="test" color="1,0,0,1"/>
+ <anchor x="1" y="2"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.anchors = [{"color" : "1,0,0,1", "name" : "test", "x" : 1, "y" : 2}, {"x" : 1, "y" : 2}]
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testAnchors_illegal_x(self):
+ # x not an int or float
+ glif = """
+ <glyph name="a" format="2">
+ <anchor x="a" y="1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.anchors = [{"x" : "a", "y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAnchors_illegal_y(self):
+ # y not an int or float
+ glif = """
+ <glyph name="a" format="2">
+ <anchor x="1" y="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.anchors = [{"x" : 1, "y" : "a"}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAnchors_illegal_x_missing(self):
+ # x missing
+ glif = """
+ <glyph name="a" format="2">
+ <anchor y="1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.anchors = [{"y" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testAnchors_illegal_y_missing(self):
+ # y missing
+ glif = """
+ <glyph name="a" format="2">
+ <anchor x="1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.anchors = [{"x" : 1}]
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testImage_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4" color="1,1,1,1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"color" : "1,1,1,1", "fileName" : "test.png", "xOffset" : 1, "xScale" : 2, "xyScale" : 3, "yOffset" : 4, "yScale" : 5, "yxScale" : 6}
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testImage_legal_no_color_or_transformation(self):
+ # legal: no color or transformation
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : 0, "xScale" : 1, "xyScale" : 0, "yOffset" : 0, "yScale" : 1, "yxScale" : 0}
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testImage_illegal_no_file_name(self):
+ # no file name
+ glif = """
+ <glyph name="a" format="2">
+ <image xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4" color="1,1,1,1"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"color" : "1,1,1,1", "xOffset" : 1, "xScale" : 2, "xyScale" : 3, "yOffset" : 4, "yScale" : 5, "yxScale" : 6}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testImage_bogus_transformation(self):
+ # bogus transformation
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="a" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : 1, "xScale" : "a", "xyScale" : 3, "yOffset" : 4, "yScale" : 5, "yxScale" : 6}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="2" xyScale="a" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : 1, "xScale" : 2, "xyScale" : "a", "yOffset" : 4, "yScale" : 5, "yxScale" : 6}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="2" xyScale="3" yxScale="a" yScale="5" xOffset="1" yOffset="4"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : 1, "xScale" : 2, "xyScale" : 3, "yOffset" : 4, "yScale" : 5, "yxScale" : "a"}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="2" xyScale="3" yxScale="6" yScale="a" xOffset="1" yOffset="4"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : 1, "xScale" : 2, "xyScale" : 3, "yOffset" : 4, "yScale" : "a", "yxScale" : 6}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="a" yOffset="4"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : "a", "xScale" : 2, "xyScale" : 3, "yOffset" : 4, "yScale" : 5, "yxScale" : 6}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="a"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"fileName" : "test.png", "xOffset" : 1, "xScale" : 2, "xyScale" : 3, "yOffset" : "a", "yScale" : 5, "yxScale" : 6}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testImage_bogus_color(self):
+ # bogus color
+ glif = """
+ <glyph name="a" format="2">
+ <image fileName="test.png" color="1,1,1,x"/>
+ <outline>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.image = {"color" : "1,1,1,x"}
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testOutline_unknown_element(self):
+ # unknown element
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <unknown/>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testOutline_content(self):
+ # content
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ hello
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testComponent_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, 5, 1, 4)])
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testComponent_illegal_no_base(self):
+ # no base
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testComponent_illegal_bogus_transformation(self):
+ # bogus values in transformation
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="a" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", ("a", 3, 6, 5, 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="a" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, "a", 6, 5, 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="a" yScale="5" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, "a", 5, 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="a" xOffset="1" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, "a", 1, 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="a" yOffset="4"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, 5, "a", 4)])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <component base="x" xScale="2" xyScale="3" yxScale="6" yScale="5" xOffset="1" yOffset="a"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.addComponent(*["x", (2, 3, 6, 5, 1, "a")])
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testContour_legal_one_contour(self):
+ # legal: one contour
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testContour_legal_two_contours(self):
+ # legal: two contours
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ </contour>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ <point x="10" y="20" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(10, 20)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testContour_illegal_unkonwn_element(self):
+ # unknown element
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <unknown/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testContourIdentifier(self):
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour identifier="foo">
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath(**{"identifier" : "foo"})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointCoordinates_legal_int(self):
+ # legal: int
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointCoordinates_legal_float(self):
+ # legal: float
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1.1" y="-2.2" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1.1, -2.2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointCoordinates_illegal_x(self):
+ # illegal: x as string
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="a" y="2" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[("a", 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointCoordinates_illegal_y(self):
+ # illegal: y as string
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="a" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, "a")], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeMove_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeMove_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move" smooth="yes"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : True})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeMove_illegal_not_at_start(self):
+ # illegal: not at start
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="3" y="-4" type="line"/>
+ <point x="1" y="-2" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeLine_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeLine_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="line"/>
+ <point x="3" y="-4" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeLine_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move"/>
+ <point x="3" y="-4" type="line" smooth="yes"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(3, -4)], **{"segmentType" : "line", "smooth" : True})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="100" y="200" type="curve"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve" smooth="yes"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : True})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_no_off_curves(self):
+ # legal: no off-curves
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_legal_1_off_curve(self):
+ # legal: 1 off-curve
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="50" y="100"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(50, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeCurve_illegal_3_off_curves(self):
+ # illegal: 3 off-curves
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="100"/>
+ <point x="35" y="125"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(35, 125)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointQCurve_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="100" y="200" type="qcurve"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_smooth(self):
+ # legal: smooth=True
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="qcurve" smooth="yes"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : True})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_no_off_curves(self):
+ # legal: no off-curves
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_one_off_curve(self):
+ # legal: 1 off-curve
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="50" y="100"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(50, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointQCurve_legal_3_off_curves(self):
+ # legal: 3 off-curves
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="100"/>
+ <point x="35" y="125"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="qcurve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(35, 125)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testSpecialCaseQCurve_legal_no_on_curve(self):
+ # contour with no on curve
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0"/>
+ <point x="0" y="100"/>
+ <point x="100" y="100"/>
+ <point x="100" y="0"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"smooth" : False})
+ pointPen.addPoint(*[(0, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 100)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 0)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeOffCurve_legal(self):
+ # legal
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="0" type="move"/>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeOffCurve_legal_start_of_contour(self):
+ # legal: start of contour
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="65"/>
+ <point x="65" y="200"/>
+ <point x="100" y="200" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(65, 200)], **{"smooth" : False})
+ pointPen.addPoint(*[(100, 200)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testPointTypeOffCurve_illegal_before_move(self):
+ # before move
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="65"/>
+ <point x="0" y="0" type="move"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeOffCurve_illegal_before_line(self):
+ # before line
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="65"/>
+ <point x="0" y="0" type="line"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : False})
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "line", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testPointTypeOffCurve_illegal_smooth(self):
+ # smooth=True
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="0" y="65" smooth="yess"/>
+ <point x="0" y="0" type="curve"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(0, 65)], **{"smooth" : True})
+ pointPen.addPoint(*[(0, 0)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testOpenContourLooseOffCurves(self):
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="2" type="move"/>
+ <point x="1" y="2"/>
+ <point x="1" y="2"/>
+ <point x="1" y="2" type="curve"/>
+ <point x="1" y="2"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, 2)], **{"smooth" : False})
+ pointPen.endPath()
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+
+ def testPointIdentifier(self):
+ glif = """
+ <glyph name="a" format="2">
+ <outline>
+ <contour>
+ <point x="1" y="-2" type="move" identifier="1"/>
+ <point x="1" y="-2" type="line" identifier="2"/>
+ <point x="1" y="-2" type="curve" identifier="3"/>
+ <point x="1" y="-2" type="qcurve" identifier="4"/>
+ </contour>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ pointPen.beginPath()
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testIdentifierConflict_legal_no_conflict(self):
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ resultGlif = self.pyToGLIF(py)
+ resultPy = self.glifToPy(glif)
+ self.assertEqual(glif, resultGlif)
+ self.assertEqual(py, resultPy)
+
+ def testIdentifierConflict_point_point(self):
+ # point - point
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point1"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_point_contour(self):
+ # point - contour
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="contour1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "contour1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_point_component(self):
+ # point - component
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="component1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "component1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_point_guideline(self):
+ # point - guideline
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="guideline1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "guideline1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_point_anchor(self):
+ # point - anchor
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="anchor1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "anchor1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_contour_contour(self):
+ # contour - contour
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_contour_component(self):
+ # contour - component
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="contour1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "contour1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_contour_guideline(self):
+ # contour - guideline
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="contour1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "contour1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_contour_anchor(self):
+ # contour - anchor
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="anchor1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "anchor1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_component_component(self):
+ # component - component
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_component_guideline(self):
+ # component - guideline
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="component1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "component1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_component_anchor(self):
+ # component - anchor
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="anchor1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "anchor1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_guideline_guideline(self):
+ # guideline - guideline
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline1"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline1", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_guideline_anchor(self):
+ # guideline - anchor
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="anchor1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor2"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "anchor1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor2", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
+
+ def testIdentifierConflict_anchor_anchor(self):
+ # anchor - anchor
+ glif = """
+ <glyph name="a" format="2">
+ <guideline x="0" identifier="guideline1"/>
+ <guideline x="0" identifier="guideline2"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <anchor x="0" y="0" identifier="anchor1"/>
+ <outline>
+ <contour identifier="contour1">
+ <point x="1" y="-2" type="move" identifier="point1"/>
+ <point x="1" y="-2" type="line" identifier="point2"/>
+ <point x="1" y="-2" type="curve" identifier="point3"/>
+ <point x="1" y="-2" type="qcurve" identifier="point4"/>
+ </contour>
+ <contour identifier="contour2">
+ <point x="1" y="-2" type="move" identifier="point5"/>
+ </contour>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component1"/>
+ <component base="x" xyScale="1" yxScale="1" xOffset="1" yOffset="1" identifier="component2"/>
+ </outline>
+ </glyph>
+ """
+ py = """
+ glyph.name = "a"
+ glyph.guidelines = [{"identifier" : "guideline1", "x" : 0}, {"identifier" : "guideline2", "x" : 0}]
+ glyph.anchors = [{"identifier" : "anchor1", "x" : 0, "y" : 0}, {"identifier" : "anchor1", "x" : 0, "y" : 0}]
+ pointPen.beginPath(**{"identifier" : "contour1"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point1", "segmentType" : "move", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point2", "segmentType" : "line", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point3", "segmentType" : "curve", "smooth" : False})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point4", "segmentType" : "qcurve", "smooth" : False})
+ pointPen.endPath()
+ pointPen.beginPath(**{"identifier" : "contour2"})
+ pointPen.addPoint(*[(1, -2)], **{"identifier" : "point5", "segmentType" : "move", "smooth" : False})
+ pointPen.endPath()
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component1"})
+ pointPen.addComponent(*["x", (1, 1, 1, 1, 1, 1)], **{"identifier" : "component2"})
+ """
+ self.assertRaises(GlifLibError, self.pyToGLIF, py)
+ self.assertRaises(GlifLibError, self.glifToPy, glif)
diff --git a/Tests/ufoLib/UFO1_test.py b/Tests/ufoLib/UFO1_test.py
new file mode 100644
index 00000000..6194270d
--- /dev/null
+++ b/Tests/ufoLib/UFO1_test.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import os
+import shutil
+import unittest
+import tempfile
+from io import open
+from fontTools.ufoLib import UFOReader, UFOWriter, UFOLibError
+from fontTools.ufoLib import plistlib
+from .testSupport import fontInfoVersion1, fontInfoVersion2
+
+
+class TestInfoObject(object): pass
+
+
+class ReadFontInfoVersion1TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.dstDir = tempfile.mktemp()
+ os.mkdir(self.dstDir)
+ metaInfo = {
+ "creator": "test",
+ "formatVersion": 1
+ }
+ path = os.path.join(self.dstDir, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+
+ def tearDown(self):
+ shutil.rmtree(self.dstDir)
+
+ def _writeInfoToPlist(self, info):
+ path = os.path.join(self.dstDir, "fontinfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(info, f)
+
+ def testRead(self):
+ originalData = dict(fontInfoVersion1)
+ self._writeInfoToPlist(originalData)
+ infoObject = TestInfoObject()
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(infoObject)
+ for attr in dir(infoObject):
+ if attr not in fontInfoVersion2:
+ continue
+ originalValue = fontInfoVersion2[attr]
+ readValue = getattr(infoObject, attr)
+ self.assertEqual(originalValue, readValue)
+
+ def testFontStyleConversion(self):
+ fontStyle1To2 = {
+ 64 : "regular",
+ 1 : "italic",
+ 32 : "bold",
+ 33 : "bold italic"
+ }
+ for old, new in list(fontStyle1To2.items()):
+ info = dict(fontInfoVersion1)
+ info["fontStyle"] = old
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ infoObject = TestInfoObject()
+ reader.readInfo(infoObject)
+ self.assertEqual(new, infoObject.styleMapStyleName)
+
+ def testWidthNameConversion(self):
+ widthName1To2 = {
+ "Ultra-condensed" : 1,
+ "Extra-condensed" : 2,
+ "Condensed" : 3,
+ "Semi-condensed" : 4,
+ "Medium (normal)" : 5,
+ "Semi-expanded" : 6,
+ "Expanded" : 7,
+ "Extra-expanded" : 8,
+ "Ultra-expanded" : 9
+ }
+ for old, new in list(widthName1To2.items()):
+ info = dict(fontInfoVersion1)
+ info["widthName"] = old
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ infoObject = TestInfoObject()
+ reader.readInfo(infoObject)
+ self.assertEqual(new, infoObject.openTypeOS2WidthClass)
+
+
+class WriteFontInfoVersion1TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.dstDir = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeInfoObject(self):
+ infoObject = TestInfoObject()
+ for attr, value in list(fontInfoVersion2.items()):
+ setattr(infoObject, attr, value)
+ return infoObject
+
+ def readPlist(self):
+ path = os.path.join(self.dstDir, "fontinfo.plist")
+ with open(path, "rb") as f:
+ plist = plistlib.load(f)
+ return plist
+
+ def testWrite(self):
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=1)
+ writer.writeInfo(infoObject)
+ writtenData = self.readPlist()
+ for attr, originalValue in list(fontInfoVersion1.items()):
+ newValue = writtenData[attr]
+ self.assertEqual(newValue, originalValue)
+
+ def testFontStyleConversion(self):
+ fontStyle1To2 = {
+ 64 : "regular",
+ 1 : "italic",
+ 32 : "bold",
+ 33 : "bold italic"
+ }
+ for old, new in list(fontStyle1To2.items()):
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapStyleName = new
+ writer = UFOWriter(self.dstDir, formatVersion=1)
+ writer.writeInfo(infoObject)
+ writtenData = self.readPlist()
+ self.assertEqual(writtenData["fontStyle"], old)
+
+ def testWidthNameConversion(self):
+ widthName1To2 = {
+ "Ultra-condensed" : 1,
+ "Extra-condensed" : 2,
+ "Condensed" : 3,
+ "Semi-condensed" : 4,
+ "Medium (normal)" : 5,
+ "Semi-expanded" : 6,
+ "Expanded" : 7,
+ "Extra-expanded" : 8,
+ "Ultra-expanded" : 9
+ }
+ for old, new in list(widthName1To2.items()):
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WidthClass = new
+ writer = UFOWriter(self.dstDir, formatVersion=1)
+ writer.writeInfo(infoObject)
+ writtenData = self.readPlist()
+ self.assertEqual(writtenData["widthName"], old)
diff --git a/Tests/ufoLib/UFO2_test.py b/Tests/ufoLib/UFO2_test.py
new file mode 100644
index 00000000..04309ba7
--- /dev/null
+++ b/Tests/ufoLib/UFO2_test.py
@@ -0,0 +1,1414 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import os
+import shutil
+import unittest
+import tempfile
+from io import open
+from fontTools.ufoLib import UFOReader, UFOWriter, UFOLibError
+from fontTools.ufoLib import plistlib
+from .testSupport import fontInfoVersion2
+
+
+class TestInfoObject(object): pass
+
+
+class ReadFontInfoVersion2TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.dstDir = tempfile.mktemp()
+ os.mkdir(self.dstDir)
+ metaInfo = {
+ "creator": "test",
+ "formatVersion": 2
+ }
+ path = os.path.join(self.dstDir, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+
+ def tearDown(self):
+ shutil.rmtree(self.dstDir)
+
+ def _writeInfoToPlist(self, info):
+ path = os.path.join(self.dstDir, "fontinfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(info, f)
+
+ def testRead(self):
+ originalData = dict(fontInfoVersion2)
+ self._writeInfoToPlist(originalData)
+ infoObject = TestInfoObject()
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(infoObject)
+ readData = {}
+ for attr in list(fontInfoVersion2.keys()):
+ readData[attr] = getattr(infoObject, attr)
+ self.assertEqual(originalData, readData)
+
+ def testGenericRead(self):
+ # familyName
+ info = dict(fontInfoVersion2)
+ info["familyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # styleName
+ info = dict(fontInfoVersion2)
+ info["styleName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # styleMapFamilyName
+ info = dict(fontInfoVersion2)
+ info["styleMapFamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # styleMapStyleName
+ ## not a string
+ info = dict(fontInfoVersion2)
+ info["styleMapStyleName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion2)
+ info["styleMapStyleName"] = "REGULAR"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # versionMajor
+ info = dict(fontInfoVersion2)
+ info["versionMajor"] = "1"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # versionMinor
+ info = dict(fontInfoVersion2)
+ info["versionMinor"] = "0"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # copyright
+ info = dict(fontInfoVersion2)
+ info["copyright"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # trademark
+ info = dict(fontInfoVersion2)
+ info["trademark"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # unitsPerEm
+ info = dict(fontInfoVersion2)
+ info["unitsPerEm"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # descender
+ info = dict(fontInfoVersion2)
+ info["descender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # xHeight
+ info = dict(fontInfoVersion2)
+ info["xHeight"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # capHeight
+ info = dict(fontInfoVersion2)
+ info["capHeight"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # ascender
+ info = dict(fontInfoVersion2)
+ info["ascender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # italicAngle
+ info = dict(fontInfoVersion2)
+ info["italicAngle"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testHeadRead(self):
+ # openTypeHeadCreated
+ ## not a string
+ info = dict(fontInfoVersion2)
+ info["openTypeHeadCreated"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## invalid format
+ info = dict(fontInfoVersion2)
+ info["openTypeHeadCreated"] = "2000-Jan-01 00:00:00"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHeadLowestRecPPEM
+ info = dict(fontInfoVersion2)
+ info["openTypeHeadLowestRecPPEM"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHeadFlags
+ info = dict(fontInfoVersion2)
+ info["openTypeHeadFlags"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testHheaRead(self):
+ # openTypeHheaAscender
+ info = dict(fontInfoVersion2)
+ info["openTypeHheaAscender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaDescender
+ info = dict(fontInfoVersion2)
+ info["openTypeHheaDescender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaLineGap
+ info = dict(fontInfoVersion2)
+ info["openTypeHheaLineGap"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaCaretSlopeRise
+ info = dict(fontInfoVersion2)
+ info["openTypeHheaCaretSlopeRise"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaCaretSlopeRun
+ info = dict(fontInfoVersion2)
+ info["openTypeHheaCaretSlopeRun"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaCaretOffset
+ info = dict(fontInfoVersion2)
+ info["openTypeHheaCaretOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testNameRead(self):
+ # openTypeNameDesigner
+ info = dict(fontInfoVersion2)
+ info["openTypeNameDesigner"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameDesignerURL
+ info = dict(fontInfoVersion2)
+ info["openTypeNameDesignerURL"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameManufacturer
+ info = dict(fontInfoVersion2)
+ info["openTypeNameManufacturer"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameManufacturerURL
+ info = dict(fontInfoVersion2)
+ info["openTypeNameManufacturerURL"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameLicense
+ info = dict(fontInfoVersion2)
+ info["openTypeNameLicense"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameLicenseURL
+ info = dict(fontInfoVersion2)
+ info["openTypeNameLicenseURL"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameVersion
+ info = dict(fontInfoVersion2)
+ info["openTypeNameVersion"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameUniqueID
+ info = dict(fontInfoVersion2)
+ info["openTypeNameUniqueID"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameDescription
+ info = dict(fontInfoVersion2)
+ info["openTypeNameDescription"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNamePreferredFamilyName
+ info = dict(fontInfoVersion2)
+ info["openTypeNamePreferredFamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNamePreferredSubfamilyName
+ info = dict(fontInfoVersion2)
+ info["openTypeNamePreferredSubfamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameCompatibleFullName
+ info = dict(fontInfoVersion2)
+ info["openTypeNameCompatibleFullName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameSampleText
+ info = dict(fontInfoVersion2)
+ info["openTypeNameSampleText"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameWWSFamilyName
+ info = dict(fontInfoVersion2)
+ info["openTypeNameWWSFamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameWWSSubfamilyName
+ info = dict(fontInfoVersion2)
+ info["openTypeNameWWSSubfamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testOS2Read(self):
+ # openTypeOS2WidthClass
+ ## not an int
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2WidthClass"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out or range
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2WidthClass"] = 15
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2WeightClass
+ info = dict(fontInfoVersion2)
+ ## not an int
+ info["openTypeOS2WeightClass"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info["openTypeOS2WeightClass"] = -50
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2Selection
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2Selection"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2VendorID
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2VendorID"] = 1234
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2Panose
+ ## not an int
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3, 4, 5, 6, 7, 8, str(9)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too few values
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2FamilyClass
+ ## not an int
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2FamilyClass"] = [1, str(1)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too few values
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2FamilyClass"] = [1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2FamilyClass"] = [1, 1, 1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2FamilyClass"] = [1, 201]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2UnicodeRanges
+ ## not an int
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2UnicodeRanges"] = ["0"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2UnicodeRanges"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2CodePageRanges
+ ## not an int
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2CodePageRanges"] = ["0"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2CodePageRanges"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2TypoAscender
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2TypoAscender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2TypoDescender
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2TypoDescender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2TypoLineGap
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2TypoLineGap"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2WinAscent
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2WinAscent"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2WinDescent
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2WinDescent"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2Type
+ ## not an int
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2Type"] = ["1"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2Type"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptXSize
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SubscriptXSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptYSize
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SubscriptYSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptXOffset
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SubscriptXOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptYOffset
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SubscriptYOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptXSize
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SuperscriptXSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptYSize
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SuperscriptYSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptXOffset
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SuperscriptXOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptYOffset
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2SuperscriptYOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2StrikeoutSize
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2StrikeoutSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2StrikeoutPosition
+ info = dict(fontInfoVersion2)
+ info["openTypeOS2StrikeoutPosition"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testVheaRead(self):
+ # openTypeVheaVertTypoAscender
+ info = dict(fontInfoVersion2)
+ info["openTypeVheaVertTypoAscender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaVertTypoDescender
+ info = dict(fontInfoVersion2)
+ info["openTypeVheaVertTypoDescender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaVertTypoLineGap
+ info = dict(fontInfoVersion2)
+ info["openTypeVheaVertTypoLineGap"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaCaretSlopeRise
+ info = dict(fontInfoVersion2)
+ info["openTypeVheaCaretSlopeRise"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaCaretSlopeRun
+ info = dict(fontInfoVersion2)
+ info["openTypeVheaCaretSlopeRun"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaCaretOffset
+ info = dict(fontInfoVersion2)
+ info["openTypeVheaCaretOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testFONDRead(self):
+ # macintoshFONDFamilyID
+ info = dict(fontInfoVersion2)
+ info["macintoshFONDFamilyID"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # macintoshFONDName
+ info = dict(fontInfoVersion2)
+ info["macintoshFONDName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testPostscriptRead(self):
+ # postscriptFontName
+ info = dict(fontInfoVersion2)
+ info["postscriptFontName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # postscriptFullName
+ info = dict(fontInfoVersion2)
+ info["postscriptFullName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # postscriptSlantAngle
+ info = dict(fontInfoVersion2)
+ info["postscriptSlantAngle"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # postscriptUniqueID
+ info = dict(fontInfoVersion2)
+ info["postscriptUniqueID"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptUnderlineThickness
+ info = dict(fontInfoVersion2)
+ info["postscriptUnderlineThickness"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptUnderlinePosition
+ info = dict(fontInfoVersion2)
+ info["postscriptUnderlinePosition"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptIsFixedPitch
+ info = dict(fontInfoVersion2)
+ info["postscriptIsFixedPitch"] = 2
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueValues
+ ## not a list
+ info = dict(fontInfoVersion2)
+ info["postscriptBlueValues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion2)
+ info["postscriptBlueValues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["postscriptBlueValues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptOtherBlues
+ ## not a list
+ info = dict(fontInfoVersion2)
+ info["postscriptOtherBlues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion2)
+ info["postscriptOtherBlues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["postscriptOtherBlues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptFamilyBlues
+ ## not a list
+ info = dict(fontInfoVersion2)
+ info["postscriptFamilyBlues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion2)
+ info["postscriptFamilyBlues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["postscriptFamilyBlues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptFamilyOtherBlues
+ ## not a list
+ info = dict(fontInfoVersion2)
+ info["postscriptFamilyOtherBlues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion2)
+ info["postscriptFamilyOtherBlues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["postscriptFamilyOtherBlues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptStemSnapH
+ ## not list
+ info = dict(fontInfoVersion2)
+ info["postscriptStemSnapH"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["postscriptStemSnapH"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptStemSnapV
+ ## not list
+ info = dict(fontInfoVersion2)
+ info["postscriptStemSnapV"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion2)
+ info["postscriptStemSnapV"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueFuzz
+ info = dict(fontInfoVersion2)
+ info["postscriptBlueFuzz"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueShift
+ info = dict(fontInfoVersion2)
+ info["postscriptBlueShift"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueScale
+ info = dict(fontInfoVersion2)
+ info["postscriptBlueScale"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptForceBold
+ info = dict(fontInfoVersion2)
+ info["postscriptForceBold"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptDefaultWidthX
+ info = dict(fontInfoVersion2)
+ info["postscriptDefaultWidthX"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptNominalWidthX
+ info = dict(fontInfoVersion2)
+ info["postscriptNominalWidthX"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptWeightName
+ info = dict(fontInfoVersion2)
+ info["postscriptWeightName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptDefaultCharacter
+ info = dict(fontInfoVersion2)
+ info["postscriptDefaultCharacter"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptWindowsCharacterSet
+ info = dict(fontInfoVersion2)
+ info["postscriptWindowsCharacterSet"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # macintoshFONDFamilyID
+ info = dict(fontInfoVersion2)
+ info["macintoshFONDFamilyID"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # macintoshFONDName
+ info = dict(fontInfoVersion2)
+ info["macintoshFONDName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+
+
+class WriteFontInfoVersion2TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.dstDir = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeInfoObject(self):
+ infoObject = TestInfoObject()
+ for attr, value in list(fontInfoVersion2.items()):
+ setattr(infoObject, attr, value)
+ return infoObject
+
+ def readPlist(self):
+ path = os.path.join(self.dstDir, "fontinfo.plist")
+ with open(path, "rb") as f:
+ plist = plistlib.load(f)
+ return plist
+
+ def testWrite(self):
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ writer.writeInfo(infoObject)
+ writtenData = self.readPlist()
+ for attr, originalValue in list(fontInfoVersion2.items()):
+ newValue = writtenData[attr]
+ self.assertEqual(newValue, originalValue)
+
+ def testGenericWrite(self):
+ # familyName
+ infoObject = self.makeInfoObject()
+ infoObject.familyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # styleName
+ infoObject = self.makeInfoObject()
+ infoObject.styleName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # styleMapFamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapFamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # styleMapStyleName
+ ## not a string
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapStyleName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapStyleName = "REGULAR"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # versionMajor
+ infoObject = self.makeInfoObject()
+ infoObject.versionMajor = "1"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # versionMinor
+ infoObject = self.makeInfoObject()
+ infoObject.versionMinor = "0"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # copyright
+ infoObject = self.makeInfoObject()
+ infoObject.copyright = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # trademark
+ infoObject = self.makeInfoObject()
+ infoObject.trademark = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # unitsPerEm
+ infoObject = self.makeInfoObject()
+ infoObject.unitsPerEm = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # descender
+ infoObject = self.makeInfoObject()
+ infoObject.descender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # xHeight
+ infoObject = self.makeInfoObject()
+ infoObject.xHeight = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # capHeight
+ infoObject = self.makeInfoObject()
+ infoObject.capHeight = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # ascender
+ infoObject = self.makeInfoObject()
+ infoObject.ascender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # italicAngle
+ infoObject = self.makeInfoObject()
+ infoObject.italicAngle = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testHeadWrite(self):
+ # openTypeHeadCreated
+ ## not a string
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadCreated = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## invalid format
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadCreated = "2000-Jan-01 00:00:00"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHeadLowestRecPPEM
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadLowestRecPPEM = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHeadFlags
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadFlags = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testHheaWrite(self):
+ # openTypeHheaAscender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaAscender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHheaDescender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaDescender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHheaLineGap
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaLineGap = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHheaCaretSlopeRise
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaCaretSlopeRise = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHheaCaretSlopeRun
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaCaretSlopeRun = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeHheaCaretOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaCaretOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testNameWrite(self):
+ # openTypeNameDesigner
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameDesigner = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameDesignerURL
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameDesignerURL = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameManufacturer
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameManufacturer = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameManufacturerURL
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameManufacturerURL = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameLicense
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameLicense = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameLicenseURL
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameLicenseURL = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameVersion
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameVersion = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameUniqueID
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameUniqueID = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameDescription
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameDescription = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNamePreferredFamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNamePreferredFamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNamePreferredSubfamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNamePreferredSubfamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameCompatibleFullName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameCompatibleFullName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameSampleText
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameSampleText = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameWWSFamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameWWSFamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeNameWWSSubfamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameWWSSubfamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testOS2Write(self):
+ # openTypeOS2WidthClass
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WidthClass = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out or range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WidthClass = 15
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2WeightClass
+ infoObject = self.makeInfoObject()
+ ## not an int
+ infoObject.openTypeOS2WeightClass = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out of range
+ infoObject.openTypeOS2WeightClass = -50
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2Selection
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Selection = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2VendorID
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2VendorID = 1234
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2Panose
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Panose = [0, 1, 2, 3, 4, 5, 6, 7, 8, str(9)]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too few values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Panose = [0, 1, 2, 3]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Panose = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2FamilyClass
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [0, str(1)]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too few values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [1, 1, 1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [1, 20]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2UnicodeRanges
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2UnicodeRanges = ["0"]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2UnicodeRanges = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2CodePageRanges
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2CodePageRanges = ["0"]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2CodePageRanges = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2TypoAscender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2TypoAscender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2TypoDescender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2TypoDescender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2TypoLineGap
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2TypoLineGap = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2WinAscent
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WinAscent = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2WinDescent
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WinDescent = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2Type
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Type = ["1"]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Type = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SubscriptXSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptXSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SubscriptYSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptYSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SubscriptXOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptXOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SubscriptYOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptYOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SuperscriptXSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptXSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SuperscriptYSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptYSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SuperscriptXOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptXOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2SuperscriptYOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptYOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2StrikeoutSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2StrikeoutSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeOS2StrikeoutPosition
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2StrikeoutPosition = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testVheaWrite(self):
+ # openTypeVheaVertTypoAscender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaVertTypoAscender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeVheaVertTypoDescender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaVertTypoDescender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeVheaVertTypoLineGap
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaVertTypoLineGap = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeVheaCaretSlopeRise
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaCaretSlopeRise = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeVheaCaretSlopeRun
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaCaretSlopeRun = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # openTypeVheaCaretOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaCaretOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testFONDWrite(self):
+ # macintoshFONDFamilyID
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDFamilyID = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # macintoshFONDName
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+
+ def testPostscriptWrite(self):
+ # postscriptFontName
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFontName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptFullName
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFullName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptSlantAngle
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptSlantAngle = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptUniqueID
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptUniqueID = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptUnderlineThickness
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptUnderlineThickness = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptUnderlinePosition
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptUnderlinePosition = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptIsFixedPitch
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptIsFixedPitch = 2
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptBlueValues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueValues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueValues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueValues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptOtherBlues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptOtherBlues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptOtherBlues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptOtherBlues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptFamilyBlues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyBlues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyBlues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyBlues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptFamilyOtherBlues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyOtherBlues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyOtherBlues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyOtherBlues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptStemSnapH
+ ## not list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapH = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapH = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptStemSnapV
+ ## not list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapV = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapV = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptBlueFuzz
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueFuzz = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptBlueShift
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueShift = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptBlueScale
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueScale = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptForceBold
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptForceBold = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptDefaultWidthX
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptDefaultWidthX = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptNominalWidthX
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptNominalWidthX = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptWeightName
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptWeightName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptDefaultCharacter
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptDefaultCharacter = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # postscriptWindowsCharacterSet
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptWindowsCharacterSet = -1
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # macintoshFONDFamilyID
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDFamilyID = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ # macintoshFONDName
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
diff --git a/Tests/ufoLib/UFO3_test.py b/Tests/ufoLib/UFO3_test.py
new file mode 100644
index 00000000..3cfd7c8f
--- /dev/null
+++ b/Tests/ufoLib/UFO3_test.py
@@ -0,0 +1,4686 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import os
+import shutil
+import unittest
+import tempfile
+from io import open
+from fontTools.misc.py23 import unicode
+from fontTools.ufoLib import UFOReader, UFOWriter, UFOLibError
+from fontTools.ufoLib.glifLib import GlifLibError
+from fontTools.misc import plistlib
+from .testSupport import fontInfoVersion3
+
+
+class TestInfoObject(object): pass
+
+
+# --------------
+# fontinfo.plist
+# --------------
+
+class ReadFontInfoVersion3TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.dstDir = tempfile.mktemp()
+ os.mkdir(self.dstDir)
+ metaInfo = {
+ "creator": "test",
+ "formatVersion": 3
+ }
+ path = os.path.join(self.dstDir, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+
+ def tearDown(self):
+ shutil.rmtree(self.dstDir)
+
+ def _writeInfoToPlist(self, info):
+ path = os.path.join(self.dstDir, "fontinfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(info, f)
+
+ def testRead(self):
+ originalData = dict(fontInfoVersion3)
+ self._writeInfoToPlist(originalData)
+ infoObject = TestInfoObject()
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(infoObject)
+ readData = {}
+ for attr in list(fontInfoVersion3.keys()):
+ readData[attr] = getattr(infoObject, attr)
+ self.assertEqual(originalData, readData)
+
+ def testGenericRead(self):
+ # familyName
+ info = dict(fontInfoVersion3)
+ info["familyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # styleName
+ info = dict(fontInfoVersion3)
+ info["styleName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # styleMapFamilyName
+ info = dict(fontInfoVersion3)
+ info["styleMapFamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # styleMapStyleName
+ ## not a string
+ info = dict(fontInfoVersion3)
+ info["styleMapStyleName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion3)
+ info["styleMapStyleName"] = "REGULAR"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # versionMajor
+ info = dict(fontInfoVersion3)
+ info["versionMajor"] = "1"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # versionMinor
+ info = dict(fontInfoVersion3)
+ info["versionMinor"] = "0"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["versionMinor"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # copyright
+ info = dict(fontInfoVersion3)
+ info["copyright"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # trademark
+ info = dict(fontInfoVersion3)
+ info["trademark"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # unitsPerEm
+ info = dict(fontInfoVersion3)
+ info["unitsPerEm"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["unitsPerEm"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["unitsPerEm"] = -1.0
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # descender
+ info = dict(fontInfoVersion3)
+ info["descender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # xHeight
+ info = dict(fontInfoVersion3)
+ info["xHeight"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # capHeight
+ info = dict(fontInfoVersion3)
+ info["capHeight"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # ascender
+ info = dict(fontInfoVersion3)
+ info["ascender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # italicAngle
+ info = dict(fontInfoVersion3)
+ info["italicAngle"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testGaspRead(self):
+ # not a list
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # empty list
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = []
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ # not a dict
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = ["abc"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # dict not properly formatted
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(rangeMaxPPEM=0xFFFF, notTheRightKey=1)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(notTheRightKey=1, rangeGaspBehavior=[0])]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # not an int for ppem
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(rangeMaxPPEM="abc", rangeGaspBehavior=[0]), dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # not a list for behavior
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(rangeMaxPPEM=10, rangeGaspBehavior="abc"), dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # invalid behavior value
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(rangeMaxPPEM=10, rangeGaspBehavior=[-1]), dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # not sorted
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0]), dict(rangeMaxPPEM=10, rangeGaspBehavior=[0])]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # no 0xFFFF
+ info = dict(fontInfoVersion3)
+ info["openTypeGaspRangeRecords"] = [dict(rangeMaxPPEM=10, rangeGaspBehavior=[0]), dict(rangeMaxPPEM=20, rangeGaspBehavior=[0])]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+
+ def testHeadRead(self):
+ # openTypeHeadCreated
+ ## not a string
+ info = dict(fontInfoVersion3)
+ info["openTypeHeadCreated"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## invalid format
+ info = dict(fontInfoVersion3)
+ info["openTypeHeadCreated"] = "2000-Jan-01 00:00:00"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHeadLowestRecPPEM
+ info = dict(fontInfoVersion3)
+ info["openTypeHeadLowestRecPPEM"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeHeadLowestRecPPEM"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHeadFlags
+ info = dict(fontInfoVersion3)
+ info["openTypeHeadFlags"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testHheaRead(self):
+ # openTypeHheaAscender
+ info = dict(fontInfoVersion3)
+ info["openTypeHheaAscender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaDescender
+ info = dict(fontInfoVersion3)
+ info["openTypeHheaDescender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaLineGap
+ info = dict(fontInfoVersion3)
+ info["openTypeHheaLineGap"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaCaretSlopeRise
+ info = dict(fontInfoVersion3)
+ info["openTypeHheaCaretSlopeRise"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaCaretSlopeRun
+ info = dict(fontInfoVersion3)
+ info["openTypeHheaCaretSlopeRun"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeHheaCaretOffset
+ info = dict(fontInfoVersion3)
+ info["openTypeHheaCaretOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testNameRead(self):
+ # openTypeNameDesigner
+ info = dict(fontInfoVersion3)
+ info["openTypeNameDesigner"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameDesignerURL
+ info = dict(fontInfoVersion3)
+ info["openTypeNameDesignerURL"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameManufacturer
+ info = dict(fontInfoVersion3)
+ info["openTypeNameManufacturer"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameManufacturerURL
+ info = dict(fontInfoVersion3)
+ info["openTypeNameManufacturerURL"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameLicense
+ info = dict(fontInfoVersion3)
+ info["openTypeNameLicense"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameLicenseURL
+ info = dict(fontInfoVersion3)
+ info["openTypeNameLicenseURL"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameVersion
+ info = dict(fontInfoVersion3)
+ info["openTypeNameVersion"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameUniqueID
+ info = dict(fontInfoVersion3)
+ info["openTypeNameUniqueID"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameDescription
+ info = dict(fontInfoVersion3)
+ info["openTypeNameDescription"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNamePreferredFamilyName
+ info = dict(fontInfoVersion3)
+ info["openTypeNamePreferredFamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNamePreferredSubfamilyName
+ info = dict(fontInfoVersion3)
+ info["openTypeNamePreferredSubfamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameCompatibleFullName
+ info = dict(fontInfoVersion3)
+ info["openTypeNameCompatibleFullName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameSampleText
+ info = dict(fontInfoVersion3)
+ info["openTypeNameSampleText"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameWWSFamilyName
+ info = dict(fontInfoVersion3)
+ info["openTypeNameWWSFamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameWWSSubfamilyName
+ info = dict(fontInfoVersion3)
+ info["openTypeNameWWSSubfamilyName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeNameRecords
+ ## not a list
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## not a dict
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = ["abc"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## invalid dict structure
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [dict(foo="bar")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## incorrect keys
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record.", foo="bar")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1)
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## invalid values
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID="1", platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID="1", encodingID=1, languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID="1", languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID="1", string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string=1)
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## duplicate
+ info = dict(fontInfoVersion3)
+ info["openTypeNameRecords"] = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record."),
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+
+ def testOS2Read(self):
+ # openTypeOS2WidthClass
+ ## not an int
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2WidthClass"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out or range
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2WidthClass"] = 15
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2WeightClass
+ info = dict(fontInfoVersion3)
+ ## not an int
+ info["openTypeOS2WeightClass"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info["openTypeOS2WeightClass"] = -50
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2Selection
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Selection"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2VendorID
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2VendorID"] = 1234
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2Panose
+ ## not an int
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3, 4, 5, 6, 7, 8, str(9)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## negative
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3, 4, 5, 6, 7, 8, -9]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too few values
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Panose"] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2FamilyClass
+ ## not an int
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2FamilyClass"] = [1, str(1)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too few values
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2FamilyClass"] = [1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2FamilyClass"] = [1, 1, 1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2FamilyClass"] = [1, 201]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2UnicodeRanges
+ ## not an int
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2UnicodeRanges"] = ["0"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2UnicodeRanges"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2CodePageRanges
+ ## not an int
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2CodePageRanges"] = ["0"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2CodePageRanges"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2TypoAscender
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2TypoAscender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2TypoDescender
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2TypoDescender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2TypoLineGap
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2TypoLineGap"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2WinAscent
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2WinAscent"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2WinAscent"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2WinDescent
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2WinDescent"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2WinDescent"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2Type
+ ## not an int
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Type"] = ["1"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ ## out of range
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2Type"] = [-1]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptXSize
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SubscriptXSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptYSize
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SubscriptYSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptXOffset
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SubscriptXOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SubscriptYOffset
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SubscriptYOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptXSize
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SuperscriptXSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptYSize
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SuperscriptYSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptXOffset
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SuperscriptXOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2SuperscriptYOffset
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2SuperscriptYOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2StrikeoutSize
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2StrikeoutSize"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeOS2StrikeoutPosition
+ info = dict(fontInfoVersion3)
+ info["openTypeOS2StrikeoutPosition"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testVheaRead(self):
+ # openTypeVheaVertTypoAscender
+ info = dict(fontInfoVersion3)
+ info["openTypeVheaVertTypoAscender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaVertTypoDescender
+ info = dict(fontInfoVersion3)
+ info["openTypeVheaVertTypoDescender"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaVertTypoLineGap
+ info = dict(fontInfoVersion3)
+ info["openTypeVheaVertTypoLineGap"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaCaretSlopeRise
+ info = dict(fontInfoVersion3)
+ info["openTypeVheaCaretSlopeRise"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaCaretSlopeRun
+ info = dict(fontInfoVersion3)
+ info["openTypeVheaCaretSlopeRun"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # openTypeVheaCaretOffset
+ info = dict(fontInfoVersion3)
+ info["openTypeVheaCaretOffset"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testFONDRead(self):
+ # macintoshFONDFamilyID
+ info = dict(fontInfoVersion3)
+ info["macintoshFONDFamilyID"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # macintoshFONDName
+ info = dict(fontInfoVersion3)
+ info["macintoshFONDName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+
+ def testPostscriptRead(self):
+ # postscriptFontName
+ info = dict(fontInfoVersion3)
+ info["postscriptFontName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # postscriptFullName
+ info = dict(fontInfoVersion3)
+ info["postscriptFullName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # postscriptSlantAngle
+ info = dict(fontInfoVersion3)
+ info["postscriptSlantAngle"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, info=TestInfoObject())
+ # postscriptUniqueID
+ info = dict(fontInfoVersion3)
+ info["postscriptUniqueID"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptUnderlineThickness
+ info = dict(fontInfoVersion3)
+ info["postscriptUnderlineThickness"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptUnderlinePosition
+ info = dict(fontInfoVersion3)
+ info["postscriptUnderlinePosition"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptIsFixedPitch
+ info = dict(fontInfoVersion3)
+ info["postscriptIsFixedPitch"] = 2
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueValues
+ ## not a list
+ info = dict(fontInfoVersion3)
+ info["postscriptBlueValues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion3)
+ info["postscriptBlueValues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["postscriptBlueValues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptOtherBlues
+ ## not a list
+ info = dict(fontInfoVersion3)
+ info["postscriptOtherBlues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion3)
+ info["postscriptOtherBlues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["postscriptOtherBlues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptFamilyBlues
+ ## not a list
+ info = dict(fontInfoVersion3)
+ info["postscriptFamilyBlues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion3)
+ info["postscriptFamilyBlues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["postscriptFamilyBlues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptFamilyOtherBlues
+ ## not a list
+ info = dict(fontInfoVersion3)
+ info["postscriptFamilyOtherBlues"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## uneven value count
+ info = dict(fontInfoVersion3)
+ info["postscriptFamilyOtherBlues"] = [500]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["postscriptFamilyOtherBlues"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptStemSnapH
+ ## not list
+ info = dict(fontInfoVersion3)
+ info["postscriptStemSnapH"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["postscriptStemSnapH"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptStemSnapV
+ ## not list
+ info = dict(fontInfoVersion3)
+ info["postscriptStemSnapV"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many values
+ info = dict(fontInfoVersion3)
+ info["postscriptStemSnapV"] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueFuzz
+ info = dict(fontInfoVersion3)
+ info["postscriptBlueFuzz"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueShift
+ info = dict(fontInfoVersion3)
+ info["postscriptBlueShift"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptBlueScale
+ info = dict(fontInfoVersion3)
+ info["postscriptBlueScale"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptForceBold
+ info = dict(fontInfoVersion3)
+ info["postscriptForceBold"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptDefaultWidthX
+ info = dict(fontInfoVersion3)
+ info["postscriptDefaultWidthX"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptNominalWidthX
+ info = dict(fontInfoVersion3)
+ info["postscriptNominalWidthX"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptWeightName
+ info = dict(fontInfoVersion3)
+ info["postscriptWeightName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptDefaultCharacter
+ info = dict(fontInfoVersion3)
+ info["postscriptDefaultCharacter"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # postscriptWindowsCharacterSet
+ info = dict(fontInfoVersion3)
+ info["postscriptWindowsCharacterSet"] = -1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # macintoshFONDFamilyID
+ info = dict(fontInfoVersion3)
+ info["macintoshFONDFamilyID"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # macintoshFONDName
+ info = dict(fontInfoVersion3)
+ info["macintoshFONDName"] = 123
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+
+ def testWOFFRead(self):
+ # woffMajorVersion
+ info = dict(fontInfoVersion3)
+ info["woffMajorVersion"] = 1.0
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["woffMajorVersion"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMinorVersion
+ info = dict(fontInfoVersion3)
+ info["woffMinorVersion"] = 1.0
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["woffMinorVersion"] = "abc"
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMetadataUniqueID
+ ## none
+ info = dict(fontInfoVersion3)
+ del info["woffMetadataUniqueID"]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## not a dict
+ info = dict(fontInfoVersion3)
+ info["woffMetadataUniqueID"] = 1
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## unknown key
+ info = dict(fontInfoVersion3)
+ info["woffMetadataUniqueID"] = dict(id="foo", notTheRightKey=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no id
+ info = dict(fontInfoVersion3)
+ info["woffMetadataUniqueID"] = dict()
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## not a string for id
+ info = dict(fontInfoVersion3)
+ info["woffMetadataUniqueID"] = dict(id=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## empty string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataUniqueID"] = dict(id="")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ # woffMetadataVendor
+ ## no name
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(url="foo")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## name not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name=1, url="foo")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## name an empty string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="", url="foo")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## no URL
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo", url=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## url empty string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo", url="")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## have dir
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo", url="bar", dir="ltr")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo", url="bar", dir="rtl")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## dir not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo", url="bar", dir=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = dict(name="foo", url="bar", dir="utd")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## have class
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = {"name" : "foo", "url" : "bar", "class" : "hello"}
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = {"name" : "foo", "url" : "bar", "class" : 1}
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## class empty string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataVendor"] = {"name" : "foo", "url" : "bar", "class" : ""}
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ # woffMetadataCredits
+ ## no credits attribute
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = {}
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## unknown attribute
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name="foo")], notTheRightKey=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## not a list
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits="abc")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no elements in credits
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## credit not a dict
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=["abc"])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## unknown key
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name="foo", notTheRightKey=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no name
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(url="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## name not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name="foo", url=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## role not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name="foo", role=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name="foo", dir=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[dict(name="foo", dir="utd")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCredits"] = dict(credits=[{"name" : "foo", "class" : 1}])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMetadataDescription
+ ## no url
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text="foo")], url=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(url="foo")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a list
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text="abc")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item not a dict
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=["abc"])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item unknown key
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text="foo", notTheRightKey=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item missing text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(language="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text="foo", url=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## language not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text="foo", language=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[dict(text="foo", dir="utd")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataDescription"] = dict(text=[{"text" : "foo", "class" : 1}])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMetadataLicense
+ ## no url
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo")], url=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## id not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo")], id=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(url="foo")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## text not a list
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text="abc")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item not a dict
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=["abc"])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item unknown key
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo", notTheRightKey=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item missing text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(language="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo", url=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## language not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo", language=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[dict(text="foo", dir="utd")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicense"] = dict(text=[{"text" : "foo", "class" : 1}])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMetadataCopyright
+ ## unknown attribute
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(text="foo")], notTheRightKey=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict()
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a list
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text="abc")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item not a dict
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=["abc"])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item unknown key
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(text="foo", notTheRightKey=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item missing text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(language="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(text=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(text="foo", url=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## language not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(text="foo", language=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[dict(text="foo", dir="utd")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataCopyright"] = dict(text=[{"text" : "foo", "class" : 1}])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMetadataTrademark
+ ## unknown attribute
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(text="foo")], notTheRightKey=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## no text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict()
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a list
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text="abc")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item not a dict
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=["abc"])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item unknown key
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(text="foo", notTheRightKey=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text item missing text
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(language="foo")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## text not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(text=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## url not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(text="foo", url=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## language not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(text="foo", language=1)])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[dict(text="foo", dir="utd")])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataTrademark"] = dict(text=[{"text" : "foo", "class" : 1}])
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # woffMetadataLicensee
+ ## no name
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = dict()
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## unknown attribute
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = dict(name="foo", notTheRightKey=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## name not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = dict(name=1)
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## dir options
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = dict(name="foo", dir="ltr")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = dict(name="foo", dir="rtl")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## dir not ltr or rtl
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = dict(name="foo", dir="utd")
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## have class
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = {"name" : "foo", "class" : "hello"}
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ reader.readInfo(TestInfoObject())
+ ## class not a string
+ info = dict(fontInfoVersion3)
+ info["woffMetadataLicensee"] = {"name" : "foo", "class" : 1}
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+
+ def testGuidelinesRead(self):
+ # x
+ ## not an int or float
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x="1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # y
+ ## not an int or float
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(y="1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # angle
+ ## < 0
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, y=0, angle=-1)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## > 360
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, y=0, angle=361)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # name
+ ## not a string
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, name=1)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # color
+ ## not a string
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color=1)]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## not enough commas
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1 0, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1 0 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1 0 0 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## not enough parts
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color=", 0, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, , 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, 0, , 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, 0, 0, ")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color=", , , ")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## not a number in all positions
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="r, 1, 1, 1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, g, 1, 1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, 1, b, 1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, 1, 1, a")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## too many parts
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="1, 0, 0, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## < 0 in each position
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="-1, 0, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="0, -1, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="0, 0, -1, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="0, 0, 0, -1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ ## > 1 in each position
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="2, 0, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="0, 2, 0, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="0, 0, 2, 0")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, color="0, 0, 0, 2")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+ # identifier
+ ## duplicate
+ info = dict(fontInfoVersion3)
+ info["guidelines"] = [dict(x=0, identifier="guide1"), dict(y=0, identifier="guide1")]
+ self._writeInfoToPlist(info)
+ reader = UFOReader(self.dstDir, validate=True)
+ self.assertRaises(UFOLibError, reader.readInfo, TestInfoObject())
+
+
+class WriteFontInfoVersion3TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.dstDir = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def tearDownUFO(self):
+ if os.path.exists(self.dstDir):
+ shutil.rmtree(self.dstDir)
+
+ def makeInfoObject(self):
+ infoObject = TestInfoObject()
+ for attr, value in list(fontInfoVersion3.items()):
+ setattr(infoObject, attr, value)
+ return infoObject
+
+ def readPlist(self):
+ path = os.path.join(self.dstDir, "fontinfo.plist")
+ with open(path, "rb") as f:
+ plist = plistlib.load(f)
+ return plist
+
+ def testWrite(self):
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ writtenData = self.readPlist()
+ for attr, originalValue in list(fontInfoVersion3.items()):
+ newValue = writtenData[attr]
+ self.assertEqual(newValue, originalValue)
+ self.tearDownUFO()
+
+ def testGenericWrite(self):
+ # familyName
+ infoObject = self.makeInfoObject()
+ infoObject.familyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # styleName
+ infoObject = self.makeInfoObject()
+ infoObject.styleName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # styleMapFamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapFamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # styleMapStyleName
+ ## not a string
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapStyleName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.styleMapStyleName = "REGULAR"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # versionMajor
+ infoObject = self.makeInfoObject()
+ infoObject.versionMajor = "1"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # versionMinor
+ infoObject = self.makeInfoObject()
+ infoObject.versionMinor = "0"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # copyright
+ infoObject = self.makeInfoObject()
+ infoObject.copyright = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # trademark
+ infoObject = self.makeInfoObject()
+ infoObject.trademark = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # unitsPerEm
+ infoObject = self.makeInfoObject()
+ infoObject.unitsPerEm = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # descender
+ infoObject = self.makeInfoObject()
+ infoObject.descender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # xHeight
+ infoObject = self.makeInfoObject()
+ infoObject.xHeight = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # capHeight
+ infoObject = self.makeInfoObject()
+ infoObject.capHeight = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # ascender
+ infoObject = self.makeInfoObject()
+ infoObject.ascender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # italicAngle
+ infoObject = self.makeInfoObject()
+ infoObject.italicAngle = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testGaspWrite(self):
+ # not a list
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # empty list
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = []
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ # not a dict
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = ["abc"]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # dict not properly formatted
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(rangeMaxPPEM=0xFFFF, notTheRightKey=1)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(notTheRightKey=1, rangeGaspBehavior=[0])]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # not an int for ppem
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(rangeMaxPPEM="abc", rangeGaspBehavior=[0]), dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # not a list for behavior
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(rangeMaxPPEM=10, rangeGaspBehavior="abc"), dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # invalid behavior value
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(rangeMaxPPEM=10, rangeGaspBehavior=[-1]), dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # not sorted
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0]), dict(rangeMaxPPEM=10, rangeGaspBehavior=[0])]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # no 0xFFFF
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeGaspRangeRecords = [dict(rangeMaxPPEM=10, rangeGaspBehavior=[0]), dict(rangeMaxPPEM=20, rangeGaspBehavior=[0])]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+
+ def testHeadWrite(self):
+ # openTypeHeadCreated
+ ## not a string
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadCreated = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## invalid format
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadCreated = "2000-Jan-01 00:00:00"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHeadLowestRecPPEM
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadLowestRecPPEM = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHeadFlags
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHeadFlags = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testHheaWrite(self):
+ # openTypeHheaAscender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaAscender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHheaDescender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaDescender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHheaLineGap
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaLineGap = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHheaCaretSlopeRise
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaCaretSlopeRise = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHheaCaretSlopeRun
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaCaretSlopeRun = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeHheaCaretOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeHheaCaretOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testNameWrite(self):
+ # openTypeNameDesigner
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameDesigner = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameDesignerURL
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameDesignerURL = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameManufacturer
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameManufacturer = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameManufacturerURL
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameManufacturerURL = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameLicense
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameLicense = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameLicenseURL
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameLicenseURL = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameVersion
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameVersion = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameUniqueID
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameUniqueID = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameDescription
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameDescription = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNamePreferredFamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNamePreferredFamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNamePreferredSubfamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNamePreferredSubfamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameCompatibleFullName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameCompatibleFullName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameSampleText
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameSampleText = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameWWSFamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameWWSFamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameWWSSubfamilyName
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameWWSSubfamilyName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeNameRecords
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## not a dict
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = ["abc"]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## invalid dict structure
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [dict(foo="bar")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## incorrect keys
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record.", foo="bar")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1)
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## invalid values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID="1", platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID="1", encodingID=1, languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID="1", languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID="1", string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string=1)
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## duplicate
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeNameRecords = [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record."),
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+
+ def testOS2Write(self):
+ # openTypeOS2WidthClass
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WidthClass = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out or range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WidthClass = 15
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2WeightClass
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WeightClass = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WeightClass = -50
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2Selection
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Selection = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2VendorID
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2VendorID = 1234
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2Panose
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Panose = [0, 1, 2, 3, 4, 5, 6, 7, 8, str(9)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too few values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Panose = [0, 1, 2, 3]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Panose = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2FamilyClass
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [0, str(1)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too few values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [1, 1, 1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2FamilyClass = [1, 20]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2UnicodeRanges
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2UnicodeRanges = ["0"]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2UnicodeRanges = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2CodePageRanges
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2CodePageRanges = ["0"]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2CodePageRanges = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2TypoAscender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2TypoAscender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2TypoDescender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2TypoDescender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2TypoLineGap
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2TypoLineGap = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2WinAscent
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WinAscent = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WinAscent = -1
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2WinDescent
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WinDescent = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2WinDescent = -1
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2Type
+ ## not an int
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Type = ["1"]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## out of range
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2Type = [-1]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SubscriptXSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptXSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SubscriptYSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptYSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SubscriptXOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptXOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SubscriptYOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SubscriptYOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SuperscriptXSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptXSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SuperscriptYSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptYSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SuperscriptXOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptXOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2SuperscriptYOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2SuperscriptYOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2StrikeoutSize
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2StrikeoutSize = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeOS2StrikeoutPosition
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeOS2StrikeoutPosition = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testVheaWrite(self):
+ # openTypeVheaVertTypoAscender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaVertTypoAscender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeVheaVertTypoDescender
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaVertTypoDescender = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeVheaVertTypoLineGap
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaVertTypoLineGap = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeVheaCaretSlopeRise
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaCaretSlopeRise = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeVheaCaretSlopeRun
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaCaretSlopeRun = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # openTypeVheaCaretOffset
+ infoObject = self.makeInfoObject()
+ infoObject.openTypeVheaCaretOffset = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testFONDWrite(self):
+ # macintoshFONDFamilyID
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDFamilyID = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # macintoshFONDName
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testPostscriptWrite(self):
+ # postscriptFontName
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFontName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptFullName
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFullName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptSlantAngle
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptSlantAngle = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptUniqueID
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptUniqueID = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptUnderlineThickness
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptUnderlineThickness = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptUnderlinePosition
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptUnderlinePosition = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptIsFixedPitch
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptIsFixedPitch = 2
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptBlueValues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueValues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueValues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueValues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptOtherBlues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptOtherBlues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptOtherBlues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptOtherBlues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptFamilyBlues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyBlues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyBlues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyBlues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptFamilyOtherBlues
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyOtherBlues = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## uneven value count
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyOtherBlues = [500]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptFamilyOtherBlues = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptStemSnapH
+ ## not list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapH = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapH = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptStemSnapV
+ ## not list
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapV = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many values
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptStemSnapV = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptBlueFuzz
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueFuzz = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptBlueShift
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueShift = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptBlueScale
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptBlueScale = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptForceBold
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptForceBold = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptDefaultWidthX
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptDefaultWidthX = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptNominalWidthX
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptNominalWidthX = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptWeightName
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptWeightName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptDefaultCharacter
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptDefaultCharacter = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # postscriptWindowsCharacterSet
+ infoObject = self.makeInfoObject()
+ infoObject.postscriptWindowsCharacterSet = -1
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # macintoshFONDFamilyID
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDFamilyID = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # macintoshFONDName
+ infoObject = self.makeInfoObject()
+ infoObject.macintoshFONDName = 123
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testWOFFWrite(self):
+ # woffMajorVersion
+ infoObject = self.makeInfoObject()
+ infoObject.woffMajorVersion = 1.0
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.woffMajorVersion = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMinorVersion
+ infoObject = self.makeInfoObject()
+ infoObject.woffMinorVersion = 1.0
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.woffMinorVersion = "abc"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMetadataUniqueID
+ ## none
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataUniqueID = None
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## not a dict
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataUniqueID = 1
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## unknown key
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataUniqueID = dict(id="foo", notTheRightKey=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no id
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataUniqueID = dict()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## not a string for id
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataUniqueID = dict(id=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## empty string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataUniqueID = dict(id="")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ # woffMetadataVendor
+ ## no name
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(url="foo")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## name not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name=1, url="foo")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## name an empty string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="", url="foo")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## no URL
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo", url=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## url empty string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo", url="")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## have dir
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo", url="bar", dir="ltr")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo", url="bar", dir="rtl")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## dir not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo", url="bar", dir=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = dict(name="foo", url="bar", dir="utd")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## have class
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = {"name" : "foo", "url" : "bar", "class" : "hello"}
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = {"name" : "foo", "url" : "bar", "class" : 1}
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## class empty string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataVendor = {"name" : "foo", "url" : "bar", "class" : ""}
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ # woffMetadataCredits
+ ## no credits attribute
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = {}
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## unknown attribute
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name="foo")], notTheRightKey=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## not a list
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits="abc")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no elements in credits
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## credit not a dict
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=["abc"])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## unknown key
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name="foo", notTheRightKey=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no name
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(url="foo")])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## name not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name="foo", url=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## role not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name="foo", role=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name="foo", dir=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[dict(name="foo", dir="utd")])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCredits = dict(credits=[{"name" : "foo", "class" : 1}])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMetadataDescription
+ ## no url
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text="foo")])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text="foo")], url=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no text
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(url="foo")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a list
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text="abc")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item not a dict
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=["abc"])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item unknown key
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text="foo", notTheRightKey=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item missing text
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(language="foo")])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text="foo", url=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## language not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text="foo", language=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[dict(text="foo", dir="utd")])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataDescription = dict(text=[{"text" : "foo", "class" : 1}])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMetadataLicense
+ ## no url
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo")])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo")], url=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## id not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo")], id=1)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no text
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(url="foo")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## text not a list
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(text="abc")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item not a dict
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(text=["abc"])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item unknown key
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo", notTheRightKey=1)])
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item missing text
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicense = dict(text=[dict(language="foo")])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicense = dict(text=[dict(text=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo", url=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## language not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo", language=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicense = dict(text=[dict(text="foo", dir="utd")])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicense = dict(text=[{"text" : "foo", "class" : 1}])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMetadataCopyright
+ ## unknown attribute
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[dict(text="foo")], notTheRightKey=1)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no text
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict()
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a list
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text="abc")
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item not a dict
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=["abc"])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item unknown key
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[dict(text="foo", notTheRightKey=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item missing text
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataCopyright = dict(text=[dict(language="foo")])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[dict(text=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[dict(text="foo", url=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## language not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[dict(text="foo", language=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[dict(text="foo", dir="utd")])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataCopyright = dict(text=[{"text" : "foo", "class" : 1}])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMetadataTrademark
+ ## unknown attribute
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(text="foo")], notTheRightKey=1)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## no text
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict()
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a list
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text="abc")
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item not a dict
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=["abc"])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item unknown key
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(text="foo", notTheRightKey=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text item missing text
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(language="foo")])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## text not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(text=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## url not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(text="foo", url=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## language not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(text="foo", language=1)])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[dict(text="foo", dir="utd")])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataTrademark = dict(text=[{"text" : "foo", "class" : 1}])
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # woffMetadataLicensee
+ ## no name
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicensee = dict()
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## unknown attribute
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicensee = dict(name="foo", notTheRightKey=1)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## name not a string
+ infoObject = self.makeInfoObject()
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ infoObject.woffMetadataLicensee = dict(name=1)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## dir options
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicensee = dict(name="foo", dir="ltr")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicensee = dict(name="foo", dir="rtl")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## dir not ltr or rtl
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicensee = dict(name="foo", dir="utd")
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## have class
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicensee = {"name" : "foo", "class" : "hello"}
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeInfo(infoObject)
+ self.tearDownUFO()
+ ## class not a string
+ infoObject = self.makeInfoObject()
+ infoObject.woffMetadataLicensee = {"name" : "foo", "class" : 1}
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+ def testGuidelinesWrite(self):
+ # x
+ ## not an int or float
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x="1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # y
+ ## not an int or float
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(y="1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # angle
+ ## < 0
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, y=0, angle=-1)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## > 360
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, y=0, angle=361)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # name
+ ## not a string
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, name=1)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # color
+ ## not a string
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color=1)]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## not enough commas
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1 0, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1 0 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1 0 0 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## not enough parts
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color=", 0, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, , 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, 0, , 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, 0, 0, ")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color=", , , ")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## not a number in all positions
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="r, 1, 1, 1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, g, 1, 1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, 1, b, 1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, 1, 1, a")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## too many parts
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="1, 0, 0, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## < 0 in each position
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="-1, 0, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="0, -1, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="0, 0, -1, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="0, 0, 0, -1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## > 1 in each position
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="2, 0, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="0, 2, 0, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="0, 0, 2, 0")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, color="0, 0, 0, 2")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ # identifier
+ ## duplicate
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, identifier="guide1"), dict(y=0, identifier="guide1")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## below min
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, identifier="\0x1F")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+ ## above max
+ infoObject = self.makeInfoObject()
+ infoObject.guidelines = [dict(x=0, identifier="\0x7F")]
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ self.assertRaises(UFOLibError, writer.writeInfo, info=infoObject)
+ self.tearDownUFO()
+
+
+# ------
+# layers
+# ------
+
+class UFO3ReadLayersTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.ufoPath = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeUFO(self, metaInfo=None, layerContents=None):
+ self.clearUFO()
+ if not os.path.exists(self.ufoPath):
+ os.mkdir(self.ufoPath)
+ # metainfo.plist
+ if metaInfo is None:
+ metaInfo = dict(creator="test", formatVersion=3)
+ path = os.path.join(self.ufoPath, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+ # layers
+ if layerContents is None:
+ layerContents = [
+ ("public.default", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2"),
+ ]
+ if layerContents:
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ else:
+ layerContents = [("", "glyphs")]
+ for name, directory in layerContents:
+ glyphsPath = os.path.join(self.ufoPath, directory)
+ os.mkdir(glyphsPath)
+ contents = dict(a="a.glif")
+ path = os.path.join(glyphsPath, "contents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(contents, f)
+ path = os.path.join(glyphsPath, "a.glif")
+ with open(path, "w") as f:
+ f.write(" ")
+
+ def clearUFO(self):
+ if os.path.exists(self.ufoPath):
+ shutil.rmtree(self.ufoPath)
+
+ # valid
+
+ def testValidRead(self):
+ # UFO 1
+ self.makeUFO(
+ metaInfo=dict(creator="test", formatVersion=1),
+ layerContents=dict()
+ )
+ reader = UFOReader(self.ufoPath, validate=True)
+ reader.getGlyphSet()
+ # UFO 2
+ self.makeUFO(
+ metaInfo=dict(creator="test", formatVersion=2),
+ layerContents=dict()
+ )
+ reader = UFOReader(self.ufoPath, validate=True)
+ reader.getGlyphSet()
+ # UFO 3
+ self.makeUFO()
+ reader = UFOReader(self.ufoPath, validate=True)
+ reader.getGlyphSet()
+
+ # missing layer contents
+
+ def testMissingLayerContents(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # layer contents invalid format
+
+ def testInvalidLayerContentsFormat(self):
+ # bogus
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ with open(path, "w") as f:
+ f.write("test")
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+ # dict
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = {
+ "public.default" : "glyphs",
+ "layer 1" : "glyphs.layer 1",
+ "layer 2" : "glyphs.layer 2",
+ }
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # layer contents invalid name format
+
+ def testInvalidLayerContentsNameFormat(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ (1, "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # layer contents invalid directory format
+
+ def testInvalidLayerContentsDirectoryFormat(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", 1),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # directory listed in contents not on disk
+
+ def testLayerContentsHasMissingDirectory(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.doesnotexist"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # # directory on disk not listed in contents
+ # XXX should this raise an error?
+ #
+ # def testLayerContentsHasMissingDirectory(self):
+ # self.makeUFO()
+ # path = os.path.join(self.ufoPath, "layercontents.plist")
+ # os.remove(path)
+ # layerContents = [
+ # ("public.foregound", "glyphs"),
+ # ("layer 1", "glyphs.layer 2")
+ # ]
+ # with open(path, "wb") as f:
+ # plistlib.dump(layerContents, f)
+ # reader = UFOReader(self.ufoPath, validate=True)
+ # with self.assertRaises(UFOLibError):
+ # reader.getGlyphSet()
+
+ # no default layer on disk
+
+ def testMissingDefaultLayer(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # duplicate layer name
+
+ def testDuplicateLayerName(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 1", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # directory referenced by two layer names
+
+ def testDuplicateLayerDirectory(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 1")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ self.assertRaises(UFOLibError, reader.getGlyphSet)
+
+ # default without a name
+
+ def testDefaultLayerNoName(self):
+ # get the glyph set
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ reader.getGlyphSet()
+
+ # default with a name
+
+ def testDefaultLayerName(self):
+ # get the name
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("custom name", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ expected = layerContents[0][0]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ result = reader.getDefaultLayerName()
+ self.assertEqual(expected, result)
+ # get the glyph set
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("custom name", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ reader.getGlyphSet(expected)
+
+ # layer order
+
+ def testLayerOrder(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ expected = [name for (name, directory) in layerContents]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ result = reader.getLayerNames()
+ self.assertEqual(expected, result)
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("layer 1", "glyphs.layer 1"),
+ ("public.foregound", "glyphs"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ expected = [name for (name, directory) in layerContents]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ result = reader.getLayerNames()
+ self.assertEqual(expected, result)
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("layer 2", "glyphs.layer 2"),
+ ("layer 1", "glyphs.layer 1"),
+ ("public.foregound", "glyphs")
+ ]
+ expected = [name for (name, directory) in layerContents]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ reader = UFOReader(self.ufoPath, validate=True)
+ result = reader.getLayerNames()
+ self.assertEqual(expected, result)
+
+
+class UFO3WriteLayersTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.ufoPath = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeUFO(self, metaInfo=None, layerContents=None):
+ self.clearUFO()
+ if not os.path.exists(self.ufoPath):
+ os.mkdir(self.ufoPath)
+ # metainfo.plist
+ if metaInfo is None:
+ metaInfo = dict(creator="test", formatVersion=3)
+ path = os.path.join(self.ufoPath, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+ # layers
+ if layerContents is None:
+ layerContents = [
+ ("public.default", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2"),
+ ]
+ if layerContents:
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ else:
+ layerContents = [("", "glyphs")]
+ for name, directory in layerContents:
+ glyphsPath = os.path.join(self.ufoPath, directory)
+ os.mkdir(glyphsPath)
+ contents = dict(a="a.glif")
+ path = os.path.join(glyphsPath, "contents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(contents, f)
+ path = os.path.join(glyphsPath, "a.glif")
+ with open(path, "w") as f:
+ f.write(" ")
+
+ def clearUFO(self):
+ if os.path.exists(self.ufoPath):
+ shutil.rmtree(self.ufoPath)
+
+ # __init__: missing layer contents
+
+ def testMissingLayerContents(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: layer contents invalid format
+
+ def testInvalidLayerContentsFormat(self):
+ # bogus
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ with open(path, "w") as f:
+ f.write("test")
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+ # dict
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = {
+ "public.default" : "glyphs",
+ "layer 1" : "glyphs.layer 1",
+ "layer 2" : "glyphs.layer 2",
+ }
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: layer contents invalid name format
+
+ def testInvalidLayerContentsNameFormat(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ (1, "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: layer contents invalid directory format
+
+ def testInvalidLayerContentsDirectoryFormat(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", 1),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: directory listed in contents not on disk
+
+ def testLayerContentsHasMissingDirectory(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.doesnotexist"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: no default layer on disk
+
+ def testMissingDefaultLayer(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: duplicate layer name
+
+ def testDuplicateLayerName(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 1", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: directory referenced by two layer names
+
+ def testDuplicateLayerDirectory(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 1")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath)
+
+ # __init__: default without a name
+
+ def testDefaultLayerNoName(self):
+ # get the glyph set
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("public.foregound", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ writer = UFOWriter(self.ufoPath)
+
+ # __init__: default with a name
+
+ def testDefaultLayerName(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ os.remove(path)
+ layerContents = [
+ ("custom name", "glyphs"),
+ ("layer 1", "glyphs.layer 1"),
+ ("layer 2", "glyphs.layer 2")
+ ]
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ writer = UFOWriter(self.ufoPath)
+
+ # __init__: up convert 1 > 3
+
+ def testUpConvert1To3(self):
+ self.makeUFO(
+ metaInfo=dict(creator="test", formatVersion=1),
+ layerContents=dict()
+ )
+ writer = UFOWriter(self.ufoPath)
+ writer.writeLayerContents(["public.default"])
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [["public.default", "glyphs"]]
+ self.assertEqual(expected, result)
+
+ # __init__: up convert 2 > 3
+
+ def testUpConvert2To3(self):
+ self.makeUFO(
+ metaInfo=dict(creator="test", formatVersion=2),
+ layerContents=dict()
+ )
+ writer = UFOWriter(self.ufoPath)
+ writer.writeLayerContents(["public.default"])
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [["public.default", "glyphs"]]
+ self.assertEqual(expected, result)
+
+ # __init__: down convert 3 > 1
+
+ def testDownConvert3To1(self):
+ self.makeUFO()
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath, formatVersion=1)
+
+ # __init__: down convert 3 > 2
+
+ def testDownConvert3To2(self):
+ self.makeUFO()
+ self.assertRaises(UFOLibError, UFOWriter, self.ufoPath, formatVersion=2)
+
+ # get glyph sets
+
+ def testGetGlyphSets(self):
+ self.makeUFO()
+ # hack contents.plist
+ path = os.path.join(self.ufoPath, "glyphs.layer 1", "contents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(dict(b="a.glif"), f)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2", "contents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(dict(c="a.glif"), f)
+ # now test
+ writer = UFOWriter(self.ufoPath)
+ # default
+ expected = ["a"]
+ result = list(writer.getGlyphSet().keys())
+ self.assertEqual(expected, result)
+ # layer 1
+ expected = ["b"]
+ result = list(writer.getGlyphSet("layer 1", defaultLayer=False).keys())
+ self.assertEqual(expected, result)
+ # layer 2
+ expected = ["c"]
+ result = list(writer.getGlyphSet("layer 2", defaultLayer=False).keys())
+ self.assertEqual(expected, result)
+
+ # make a new font with two layers
+
+ def testNewFontOneLayer(self):
+ self.clearUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.getGlyphSet()
+ writer.writeLayerContents(["public.default"])
+ # directory
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [["public.default", "glyphs"]]
+ self.assertEqual(expected, result)
+
+ def testNewFontThreeLayers(self):
+ self.clearUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.getGlyphSet("layer 1", defaultLayer=False)
+ writer.getGlyphSet()
+ writer.getGlyphSet("layer 2", defaultLayer=False)
+ writer.writeLayerContents(["layer 1", "public.default", "layer 2"])
+ # directories
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 1")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [["layer 1", "glyphs.layer 1"], ["public.default", "glyphs"], ["layer 2", "glyphs.layer 2"]]
+ self.assertEqual(expected, result)
+
+ # add a layer to an existing font
+
+ def testAddLayerToExistingFont(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.getGlyphSet("layer 3", defaultLayer=False)
+ writer.writeLayerContents(["public.default", "layer 1", "layer 2", "layer 3"])
+ # directories
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 1")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 3")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [['public.default', 'glyphs'], ['layer 1', 'glyphs.layer 1'], ['layer 2', 'glyphs.layer 2'], ["layer 3", "glyphs.layer 3"]]
+ self.assertEqual(expected, result)
+
+ # rename valid name
+
+ def testRenameLayer(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.renameGlyphSet("layer 1", "layer 3")
+ writer.writeLayerContents(["public.default", "layer 3", "layer 2"])
+ # directories
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 1")
+ exists = os.path.exists(path)
+ self.assertEqual(False, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 3")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [['public.default', 'glyphs'], ['layer 3', 'glyphs.layer 3'], ['layer 2', 'glyphs.layer 2']]
+ self.assertEqual(expected, result)
+
+ def testRenameLayerDefault(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.renameGlyphSet("public.default", "layer xxx")
+ writer.renameGlyphSet("layer 1", "layer 1", defaultLayer=True)
+ writer.writeLayerContents(["layer xxx", "layer 1", "layer 2"])
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 1")
+ exists = os.path.exists(path)
+ self.assertEqual(False, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer xxx")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [['layer xxx', 'glyphs.layer xxx'], ['layer 1', 'glyphs'], ['layer 2', 'glyphs.layer 2']]
+ self.assertEqual(expected, result)
+
+ # rename duplicate name
+
+ def testRenameLayerDuplicateName(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ self.assertRaises(UFOLibError, writer.renameGlyphSet, "layer 1", "layer 2")
+
+ # rename unknown layer
+
+ def testRenameLayerUnknownName(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ self.assertRaises(UFOLibError, writer.renameGlyphSet, "does not exist", "layer 2")
+
+ # remove valid layer
+
+ def testRemoveLayer(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.deleteGlyphSet("layer 1")
+ writer.writeLayerContents(["public.default", "layer 2"])
+ # directories
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 1")
+ exists = os.path.exists(path)
+ self.assertEqual(False, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [["public.default", "glyphs"], ["layer 2", "glyphs.layer 2"]]
+ self.assertEqual(expected, result)
+
+ # remove default layer
+
+ def testRemoveDefaultLayer(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ writer.deleteGlyphSet("public.default")
+ # directories
+ path = os.path.join(self.ufoPath, "glyphs")
+ exists = os.path.exists(path)
+ self.assertEqual(False, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 1")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ path = os.path.join(self.ufoPath, "glyphs.layer 2")
+ exists = os.path.exists(path)
+ self.assertEqual(True, exists)
+ # layer contents
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [["layer 1", "glyphs.layer 1"], ["layer 2", "glyphs.layer 2"]]
+ self.assertEqual(expected, result)
+
+ # remove unknown layer
+
+ def testRemoveDefaultLayer(self):
+ self.makeUFO()
+ writer = UFOWriter(self.ufoPath)
+ self.assertRaises(UFOLibError, writer.deleteGlyphSet, "does not exist")
+
+ def testWriteAsciiLayerOrder(self):
+ self.makeUFO(
+ layerContents=[
+ ["public.default", "glyphs"],
+ ["layer 1", "glyphs.layer 1"],
+ ["layer 2", "glyphs.layer 2"],
+ ]
+ )
+ writer = UFOWriter(self.ufoPath)
+ # if passed bytes string, it'll be decoded to ASCII unicode string
+ writer.writeLayerContents(["public.default", "layer 2", b"layer 1"])
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ expected = [
+ ["public.default", "glyphs"],
+ ["layer 2", "glyphs.layer 2"],
+ ["layer 1", "glyphs.layer 1"],
+ ]
+ self.assertEqual(expected, result)
+ for layerName, directory in result:
+ assert isinstance(layerName, unicode)
+
+# -----
+# /data
+# -----
+
+
+class UFO3ReadDataTestCase(unittest.TestCase):
+
+ def getFontPath(self):
+ testdata = os.path.join(os.path.dirname(__file__), "testdata")
+ return os.path.join(testdata, "UFO3-Read Data.ufo")
+
+ def testUFOReaderDataDirectoryListing(self):
+ reader = UFOReader(self.getFontPath())
+ found = reader.getDataDirectoryListing()
+ expected = [
+ 'org.unifiedfontobject.directory/bar/lol.txt',
+ 'org.unifiedfontobject.directory/foo.txt',
+ 'org.unifiedfontobject.file.txt'
+ ]
+ self.assertEqual(set(found), set(expected))
+
+ def testUFOReaderBytesFromPath(self):
+ reader = UFOReader(self.getFontPath())
+ found = reader.readBytesFromPath("data/org.unifiedfontobject.file.txt")
+ expected = b"file.txt"
+ self.assertEqual(found, expected)
+ found = reader.readBytesFromPath("data/org.unifiedfontobject.directory/bar/lol.txt")
+ expected = b"lol.txt"
+ self.assertEqual(found, expected)
+ found = reader.readBytesFromPath("data/org.unifiedfontobject.doesNotExist")
+ expected = None
+ self.assertEqual(found, expected)
+
+ def testUFOReaderReadFileFromPath(self):
+ reader = UFOReader(self.getFontPath())
+ fileObject = reader.getReadFileForPath("data/org.unifiedfontobject.file.txt")
+ self.assertNotEqual(fileObject, None)
+ hasRead = hasattr(fileObject, "read")
+ self.assertEqual(hasRead, True)
+ fileObject.close()
+ fileObject = reader.getReadFileForPath("data/org.unifiedfontobject.doesNotExist")
+ self.assertEqual(fileObject, None)
+
+
+class UFO3WriteDataTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.dstDir = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def tearDownUFO(self):
+ if os.path.exists(self.dstDir):
+ shutil.rmtree(self.dstDir)
+
+ def testUFOWriterWriteBytesToPath(self):
+ # basic file
+ path = "data/org.unifiedfontobject.writebytesbasicfile.txt"
+ testBytes = b"test"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeBytesToPath(path, testBytes)
+ path = os.path.join(self.dstDir, path)
+ self.assertEqual(os.path.exists(path), True)
+ with open(path, "rb") as f:
+ written = f.read()
+ self.assertEqual(testBytes, written)
+ self.tearDownUFO()
+ # basic file with unicode text
+ path = "data/org.unifiedfontobject.writebytesbasicunicodefile.txt"
+ text = b"t\xeb\xdft"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeBytesToPath(path, text)
+ path = os.path.join(self.dstDir, path)
+ self.assertEqual(os.path.exists(path), True)
+ with open(path, "rb") as f:
+ written = f.read()
+ self.assertEqual(text, written)
+ self.tearDownUFO()
+ # basic directory
+ path = "data/org.unifiedfontobject.writebytesdirectory/level1/level2/file.txt"
+ testBytes = b"test"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeBytesToPath(path, testBytes)
+ path = os.path.join(self.dstDir, path)
+ self.assertEqual(os.path.exists(path), True)
+ with open(path, "rb") as f:
+ written = f.read()
+ self.assertEqual(testBytes, written)
+ self.tearDownUFO()
+
+ def testUFOWriterWriteFileToPath(self):
+ # basic file
+ path = "data/org.unifiedfontobject.getwritefile.txt"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ fileObject = writer.getFileObjectForPath(path)
+ self.assertNotEqual(fileObject, None)
+ hasRead = hasattr(fileObject, "read")
+ self.assertEqual(hasRead, True)
+ fileObject.close()
+ self.tearDownUFO()
+
+ def testUFOWriterRemoveFile(self):
+ path1 = "data/org.unifiedfontobject.removefile/level1/level2/file1.txt"
+ path2 = "data/org.unifiedfontobject.removefile/level1/level2/file2.txt"
+ path3 = "data/org.unifiedfontobject.removefile/level1/file3.txt"
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.writeBytesToPath(path1, b"test")
+ writer.writeBytesToPath(path2, b"test")
+ writer.writeBytesToPath(path3, b"test")
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path1)), True)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path2)), True)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), True)
+ writer.removeFileForPath(path1)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path1)), False)
+ self.assertEqual(os.path.exists(os.path.dirname(os.path.join(self.dstDir, path1))), True)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path2)), True)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), True)
+ writer.removeFileForPath(path2)
+ self.assertEqual(os.path.exists(os.path.dirname(os.path.join(self.dstDir, path1))), False)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path2)), False)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), True)
+ writer.removeFileForPath(path3)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, path3)), False)
+ self.assertEqual(os.path.exists(os.path.dirname(os.path.join(self.dstDir, path2))), False)
+ self.assertEqual(os.path.exists(os.path.join(self.dstDir, "data/org.unifiedfontobject.removefile")), False)
+ self.assertRaises(UFOLibError, writer.removeFileForPath, path="data/org.unifiedfontobject.doesNotExist.txt")
+ self.tearDownUFO()
+
+ def testUFOWriterCopy(self):
+ sourceDir = self.dstDir.replace(".ufo", "") + "-copy source" + ".ufo"
+ dataPath = "data/org.unifiedfontobject.copy/level1/level2/file1.txt"
+ writer = UFOWriter(sourceDir, formatVersion=3)
+ writer.writeBytesToPath(dataPath, b"test")
+ # copy a file
+ reader = UFOReader(sourceDir)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ writer.copyFromReader(reader, dataPath, dataPath)
+ path = os.path.join(self.dstDir, dataPath)
+ self.assertEqual(os.path.exists(path), True)
+ self.tearDownUFO()
+ # copy a directory
+ reader = UFOReader(sourceDir)
+ writer = UFOWriter(self.dstDir, formatVersion=3)
+ p = "data/org.unifiedfontobject.copy"
+ writer.copyFromReader(reader, p, p)
+ path = os.path.join(self.dstDir, dataPath)
+ self.assertEqual(os.path.exists(path), True)
+ self.tearDownUFO()
+
+# ---------------
+# layerinfo.plist
+# ---------------
+
+class TestLayerInfoObject(object):
+
+ color = guidelines = lib = None
+
+
+class UFO3ReadLayerInfoTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.ufoPath = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeUFO(self, formatVersion=3, layerInfo=None):
+ self.clearUFO()
+ if not os.path.exists(self.ufoPath):
+ os.mkdir(self.ufoPath)
+ # metainfo.plist
+ metaInfo = dict(creator="test", formatVersion=formatVersion)
+ path = os.path.join(self.ufoPath, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+ # layercontents.plist
+ layerContents = [("public.default", "glyphs")]
+ path = os.path.join(self.ufoPath, "layercontents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(layerContents, f)
+ # glyphs
+ glyphsPath = os.path.join(self.ufoPath, "glyphs")
+ os.mkdir(glyphsPath)
+ contents = dict(a="a.glif")
+ path = os.path.join(glyphsPath, "contents.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(contents, f)
+ path = os.path.join(glyphsPath, "a.glif")
+ with open(path, "w") as f:
+ f.write(" ")
+ # layerinfo.plist
+ if layerInfo is None:
+ layerInfo = dict(
+ color="0,0,0,1",
+ lib={"foo" : "bar"}
+ )
+ path = os.path.join(glyphsPath, "layerinfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(layerInfo, f)
+
+ def clearUFO(self):
+ if os.path.exists(self.ufoPath):
+ shutil.rmtree(self.ufoPath)
+
+ def testValidLayerInfo(self):
+ self.makeUFO()
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ info = TestLayerInfoObject()
+ glyphSet.readLayerInfo(info)
+ expectedColor = "0,0,0,1"
+ self.assertEqual(expectedColor, info.color)
+ expectedLib = {"foo": "bar"}
+ self.assertEqual(expectedLib, info.lib)
+
+ def testMissingLayerInfo(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "glyphs", "layerinfo.plist")
+ os.remove(path)
+ # read
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ info = TestLayerInfoObject()
+ glyphSet.readLayerInfo(info)
+ self.assertEqual(None, info.color)
+ self.assertEqual(None, info.guidelines)
+ self.assertEqual(None, info.lib)
+
+ def testBogusLayerInfo(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "glyphs", "layerinfo.plist")
+ os.remove(path)
+ with open(path, "w") as f:
+ f.write("test")
+ # read
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ info = TestLayerInfoObject()
+ self.assertRaises(UFOLibError, glyphSet.readLayerInfo, info)
+
+ def testInvalidFormatLayerInfo(self):
+ self.makeUFO()
+ path = os.path.join(self.ufoPath, "glyphs", "layerinfo.plist")
+ info = [("color", "0,0,0,0")]
+ with open(path, "wb") as f:
+ plistlib.dump(info, f)
+ # read
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ info = TestLayerInfoObject()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, info)
+
+ def testColor(self):
+ ## not a string
+ info = {}
+ info["color"] = 1
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ ## not enough commas
+ info = {}
+ info["color"] = "1 0, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1 0 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1 0 0 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ ## not enough parts
+ info = {}
+ info["color"] = ", 0, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1, , 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1, 0, , 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1, 0, 0, "
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = ", , , "
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ ## not a number in all positions
+ info = {}
+ info["color"] = "r, 1, 1, 1"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1, g, 1, 1"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1, 1, b, 1"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "1, 1, 1, a"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ ## too many parts
+ info = {}
+ info["color"] = "1, 0, 0, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ ## < 0 in each position
+ info = {}
+ info["color"] = "-1, 0, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "0, -1, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "0, 0, -1, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "0, 0, 0, -1"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ ## > 1 in each position
+ info = {}
+ info["color"] = "2, 0, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "0, 2, 0, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "0, 0, 2, 0"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+ info = {}
+ info["color"] = "0, 0, 0, 2"
+ self.makeUFO(layerInfo=info)
+ reader = UFOReader(self.ufoPath, validate=True)
+ glyphSet = reader.getGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.readLayerInfo, TestLayerInfoObject())
+
+
+class UFO3WriteLayerInfoTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.ufoPath = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeGlyphSet(self):
+ self.clearUFO()
+ writer = UFOWriter(self.ufoPath)
+ return writer.getGlyphSet()
+
+ def clearUFO(self):
+ if os.path.exists(self.ufoPath):
+ shutil.rmtree(self.ufoPath)
+
+ def testValidWrite(self):
+ expected = dict(
+ color="0,0,0,1",
+ lib={"foo" : "bar"}
+ )
+ info = TestLayerInfoObject()
+ info.color = expected["color"]
+ info.lib = expected["lib"]
+ glyphSet = self.makeGlyphSet()
+ glyphSet.writeLayerInfo(info)
+ path = os.path.join(self.ufoPath, "glyphs", "layerinfo.plist")
+ with open(path, "rb") as f:
+ result = plistlib.load(f)
+ self.assertEqual(expected, result)
+
+ def testColor(self):
+ ## not a string
+ info = TestLayerInfoObject()
+ info.color = 1
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ ## not enough commas
+ info = TestLayerInfoObject()
+ info.color = "1 0, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1 0 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1 0 0 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ ## not enough parts
+ info = TestLayerInfoObject()
+ info.color = ", 0, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1, , 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1, 0, , 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1, 0, 0, "
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = ", , , "
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ ## not a number in all positions
+ info = TestLayerInfoObject()
+ info.color = "r, 1, 1, 1"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1, g, 1, 1"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1, 1, b, 1"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "1, 1, 1, a"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ ## too many parts
+ info = TestLayerInfoObject()
+ info.color = "1, 0, 0, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ ## < 0 in each position
+ info = TestLayerInfoObject()
+ info.color = "-1, 0, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "0, -1, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "0, 0, -1, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "0, 0, 0, -1"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ ## > 1 in each position
+ info = TestLayerInfoObject()
+ info.color = "2, 0, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "0, 2, 0, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "0, 0, 2, 0"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
+ info = TestLayerInfoObject()
+ info.color = "0, 0, 0, 2"
+ glyphSet = self.makeGlyphSet()
+ self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info)
diff --git a/Tests/ufoLib/UFOConversion_test.py b/Tests/ufoLib/UFOConversion_test.py
new file mode 100644
index 00000000..2e288b94
--- /dev/null
+++ b/Tests/ufoLib/UFOConversion_test.py
@@ -0,0 +1,347 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import os
+import shutil
+import unittest
+import tempfile
+from io import open
+from fontTools.ufoLib import UFOReader, UFOWriter
+from fontTools.ufoLib import plistlib
+from .testSupport import expectedFontInfo1To2Conversion, expectedFontInfo2To1Conversion
+
+
+# the format version 1 lib.plist contains some data
+# that these tests shouldn't be concerned about.
+removeFromFormatVersion1Lib = [
+ "org.robofab.opentype.classes",
+ "org.robofab.opentype.features",
+ "org.robofab.opentype.featureorder",
+ "org.robofab.postScriptHintData"
+]
+
+
+class ConversionFunctionsTestCase(unittest.TestCase):
+
+ def tearDown(self):
+ path = self.getFontPath("TestFont1 (UFO1) converted.ufo")
+ if os.path.exists(path):
+ shutil.rmtree(path)
+ path = self.getFontPath("TestFont1 (UFO2) converted.ufo")
+ if os.path.exists(path):
+ shutil.rmtree(path)
+
+ def getFontPath(self, fileName):
+ testdata = os.path.join(os.path.dirname(__file__), "testdata")
+ return os.path.join(testdata, fileName)
+
+ def compareFileStructures(self, path1, path2, expectedInfoData, testFeatures):
+ # result
+ metainfoPath1 = os.path.join(path1, "metainfo.plist")
+ fontinfoPath1 = os.path.join(path1, "fontinfo.plist")
+ kerningPath1 = os.path.join(path1, "kerning.plist")
+ groupsPath1 = os.path.join(path1, "groups.plist")
+ libPath1 = os.path.join(path1, "lib.plist")
+ featuresPath1 = os.path.join(path1, "features.plist")
+ glyphsPath1 = os.path.join(path1, "glyphs")
+ glyphsPath1_contents = os.path.join(glyphsPath1, "contents.plist")
+ glyphsPath1_A = os.path.join(glyphsPath1, "A_.glif")
+ glyphsPath1_B = os.path.join(glyphsPath1, "B_.glif")
+ # expected result
+ metainfoPath2 = os.path.join(path2, "metainfo.plist")
+ fontinfoPath2 = os.path.join(path2, "fontinfo.plist")
+ kerningPath2 = os.path.join(path2, "kerning.plist")
+ groupsPath2 = os.path.join(path2, "groups.plist")
+ libPath2 = os.path.join(path2, "lib.plist")
+ featuresPath2 = os.path.join(path2, "features.plist")
+ glyphsPath2 = os.path.join(path2, "glyphs")
+ glyphsPath2_contents = os.path.join(glyphsPath2, "contents.plist")
+ glyphsPath2_A = os.path.join(glyphsPath2, "A_.glif")
+ glyphsPath2_B = os.path.join(glyphsPath2, "B_.glif")
+ # look for existence
+ self.assertEqual(os.path.exists(metainfoPath1), True)
+ self.assertEqual(os.path.exists(fontinfoPath1), True)
+ self.assertEqual(os.path.exists(kerningPath1), True)
+ self.assertEqual(os.path.exists(groupsPath1), True)
+ self.assertEqual(os.path.exists(libPath1), True)
+ self.assertEqual(os.path.exists(glyphsPath1), True)
+ self.assertEqual(os.path.exists(glyphsPath1_contents), True)
+ self.assertEqual(os.path.exists(glyphsPath1_A), True)
+ self.assertEqual(os.path.exists(glyphsPath1_B), True)
+ if testFeatures:
+ self.assertEqual(os.path.exists(featuresPath1), True)
+ # look for aggrement
+ with open(metainfoPath1, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(metainfoPath2, "rb") as f:
+ data2 = plistlib.load(f)
+ self.assertEqual(data1, data2)
+ with open(fontinfoPath1, "rb") as f:
+ data1 = plistlib.load(f)
+ self.assertEqual(sorted(data1.items()), sorted(expectedInfoData.items()))
+ with open(kerningPath1, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(kerningPath2, "rb") as f:
+ data2 = plistlib.load(f)
+ self.assertEqual(data1, data2)
+ with open(groupsPath1, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(groupsPath2, "rb") as f:
+ data2 = plistlib.load(f)
+ self.assertEqual(data1, data2)
+ with open(libPath1, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(libPath2, "rb") as f:
+ data2 = plistlib.load(f)
+ if "UFO1" in libPath1:
+ for key in removeFromFormatVersion1Lib:
+ if key in data1:
+ del data1[key]
+ if "UFO1" in libPath2:
+ for key in removeFromFormatVersion1Lib:
+ if key in data2:
+ del data2[key]
+ self.assertEqual(data1, data2)
+ with open(glyphsPath1_contents, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(glyphsPath2_contents, "rb") as f:
+ data2 = plistlib.load(f)
+ self.assertEqual(data1, data2)
+ with open(glyphsPath1_A, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(glyphsPath2_A, "rb") as f:
+ data2 = plistlib.load(f)
+ self.assertEqual(data1, data2)
+ with open(glyphsPath1_B, "rb") as f:
+ data1 = plistlib.load(f)
+ with open(glyphsPath2_B, "rb") as f:
+ data2 = plistlib.load(f)
+ self.assertEqual(data1, data2)
+
+
+# ---------------------
+# kerning up conversion
+# ---------------------
+
+class TestInfoObject(object): pass
+
+
+class KerningUpConversionTestCase(unittest.TestCase):
+
+ expectedKerning = {
+ ("public.kern1.BGroup", "public.kern2.CGroup"): 7,
+ ("public.kern1.BGroup", "public.kern2.DGroup"): 8,
+ ("public.kern1.BGroup", "A"): 5,
+ ("public.kern1.BGroup", "B"): 6,
+ ("public.kern1.CGroup", "public.kern2.CGroup"): 11,
+ ("public.kern1.CGroup", "public.kern2.DGroup"): 12,
+ ("public.kern1.CGroup", "A"): 9,
+ ("public.kern1.CGroup", "B"): 10,
+ ("A", "public.kern2.CGroup"): 3,
+ ("A", "public.kern2.DGroup"): 4,
+ ("A", "A"): 1,
+ ("A", "B"): 2
+ }
+
+ expectedGroups = {
+ "BGroup": ["B"],
+ "CGroup": ["C", "Ccedilla"],
+ "DGroup": ["D"],
+ "public.kern1.BGroup": ["B"],
+ "public.kern1.CGroup": ["C", "Ccedilla"],
+ "public.kern2.CGroup": ["C", "Ccedilla"],
+ "public.kern2.DGroup": ["D"],
+ "Not A Kerning Group" : ["A"]
+ }
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.ufoPath = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def makeUFO(self, formatVersion):
+ self.clearUFO()
+ if not os.path.exists(self.ufoPath):
+ os.mkdir(self.ufoPath)
+ # metainfo.plist
+ metaInfo = dict(creator="test", formatVersion=formatVersion)
+ path = os.path.join(self.ufoPath, "metainfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(metaInfo, f)
+ # kerning
+ kerning = {
+ "A" : {
+ "A" : 1,
+ "B" : 2,
+ "CGroup" : 3,
+ "DGroup" : 4
+ },
+ "BGroup" : {
+ "A" : 5,
+ "B" : 6,
+ "CGroup" : 7,
+ "DGroup" : 8
+ },
+ "CGroup" : {
+ "A" : 9,
+ "B" : 10,
+ "CGroup" : 11,
+ "DGroup" : 12
+ }
+ }
+ path = os.path.join(self.ufoPath, "kerning.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(kerning, f)
+ # groups
+ groups = {
+ "BGroup" : ["B"],
+ "CGroup" : ["C", "Ccedilla"],
+ "DGroup" : ["D"],
+ "Not A Kerning Group" : ["A"]
+ }
+ path = os.path.join(self.ufoPath, "groups.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(groups, f)
+ # font info
+ fontInfo = {
+ "familyName" : "Test"
+ }
+ path = os.path.join(self.ufoPath, "fontinfo.plist")
+ with open(path, "wb") as f:
+ plistlib.dump(fontInfo, f)
+
+ def clearUFO(self):
+ if os.path.exists(self.ufoPath):
+ shutil.rmtree(self.ufoPath)
+
+ def testUFO1(self):
+ self.makeUFO(formatVersion=2)
+ reader = UFOReader(self.ufoPath, validate=True)
+ kerning = reader.readKerning()
+ self.assertEqual(self.expectedKerning, kerning)
+ groups = reader.readGroups()
+ self.assertEqual(self.expectedGroups, groups)
+ info = TestInfoObject()
+ reader.readInfo(info)
+
+ def testUFO2(self):
+ self.makeUFO(formatVersion=2)
+ reader = UFOReader(self.ufoPath, validate=True)
+ kerning = reader.readKerning()
+ self.assertEqual(self.expectedKerning, kerning)
+ groups = reader.readGroups()
+ self.assertEqual(self.expectedGroups, groups)
+ info = TestInfoObject()
+ reader.readInfo(info)
+
+
+class KerningDownConversionTestCase(unittest.TestCase):
+
+ expectedKerning = {
+ ("public.kern1.BGroup", "public.kern2.CGroup"): 7,
+ ("public.kern1.BGroup", "public.kern2.DGroup"): 8,
+ ("public.kern1.BGroup", "A"): 5,
+ ("public.kern1.BGroup", "B"): 6,
+ ("public.kern1.CGroup", "public.kern2.CGroup"): 11,
+ ("public.kern1.CGroup", "public.kern2.DGroup"): 12,
+ ("public.kern1.CGroup", "A"): 9,
+ ("public.kern1.CGroup", "B"): 10,
+ ("A", "public.kern2.CGroup"): 3,
+ ("A", "public.kern2.DGroup"): 4,
+ ("A", "A"): 1,
+ ("A", "B"): 2
+ }
+
+ groups = {
+ "BGroup": ["B"],
+ "CGroup": ["C"],
+ "DGroup": ["D"],
+ "public.kern1.BGroup": ["B"],
+ "public.kern1.CGroup": ["C", "Ccedilla"],
+ "public.kern2.CGroup": ["C", "Ccedilla"],
+ "public.kern2.DGroup": ["D"],
+ "Not A Kerning Group" : ["A"]
+ }
+ expectedWrittenGroups = {
+ "BGroup": ["B"],
+ "CGroup": ["C", "Ccedilla"],
+ "DGroup": ["D"],
+ "Not A Kerning Group" : ["A"]
+ }
+
+ kerning = {
+ ("public.kern1.BGroup", "public.kern2.CGroup"): 7,
+ ("public.kern1.BGroup", "public.kern2.DGroup"): 8,
+ ("public.kern1.BGroup", "A"): 5,
+ ("public.kern1.BGroup", "B"): 6,
+ ("public.kern1.CGroup", "public.kern2.CGroup"): 11,
+ ("public.kern1.CGroup", "public.kern2.DGroup"): 12,
+ ("public.kern1.CGroup", "A"): 9,
+ ("public.kern1.CGroup", "B"): 10,
+ ("A", "public.kern2.CGroup"): 3,
+ ("A", "public.kern2.DGroup"): 4,
+ ("A", "A"): 1,
+ ("A", "B"): 2
+ }
+ expectedWrittenKerning = {
+ "BGroup" : {
+ "CGroup" : 7,
+ "DGroup" : 8,
+ "A" : 5,
+ "B" : 6
+ },
+ "CGroup" : {
+ "CGroup" : 11,
+ "DGroup" : 12,
+ "A" : 9,
+ "B" : 10
+ },
+ "A" : {
+ "CGroup" : 3,
+ "DGroup" : 4,
+ "A" : 1,
+ "B" : 2
+ }
+ }
+
+
+ downConversionMapping = {
+ "side1" : {
+ "BGroup" : "public.kern1.BGroup",
+ "CGroup" : "public.kern1.CGroup"
+ },
+ "side2" : {
+ "CGroup" : "public.kern2.CGroup",
+ "DGroup" : "public.kern2.DGroup"
+ }
+ }
+
+ def setUp(self):
+ self.tempDir = tempfile.mktemp()
+ os.mkdir(self.tempDir)
+ self.dstDir = os.path.join(self.tempDir, "test.ufo")
+
+ def tearDown(self):
+ shutil.rmtree(self.tempDir)
+
+ def tearDownUFO(self):
+ shutil.rmtree(self.dstDir)
+
+ def testWrite(self):
+ writer = UFOWriter(self.dstDir, formatVersion=2)
+ writer.setKerningGroupConversionRenameMaps(self.downConversionMapping)
+ writer.writeKerning(self.kerning)
+ writer.writeGroups(self.groups)
+ # test groups
+ path = os.path.join(self.dstDir, "groups.plist")
+ with open(path, "rb") as f:
+ writtenGroups = plistlib.load(f)
+ self.assertEqual(writtenGroups, self.expectedWrittenGroups)
+ # test kerning
+ path = os.path.join(self.dstDir, "kerning.plist")
+ with open(path, "rb") as f:
+ writtenKerning = plistlib.load(f)
+ self.assertEqual(writtenKerning, self.expectedWrittenKerning)
+ self.tearDownUFO()
diff --git a/Tests/ufoLib/UFOZ_test.py b/Tests/ufoLib/UFOZ_test.py
new file mode 100644
index 00000000..b32bba38
--- /dev/null
+++ b/Tests/ufoLib/UFOZ_test.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+from fontTools.misc.py23 import tostr
+from fontTools.ufoLib import UFOReader, UFOWriter, UFOFileStructure
+from fontTools.ufoLib.errors import UFOLibError, GlifLibError
+from fontTools.misc import plistlib
+import sys
+import os
+import fs.osfs
+import fs.tempfs
+import fs.memoryfs
+import fs.copy
+import pytest
+import warnings
+
+
+TESTDATA = fs.osfs.OSFS(
+ os.path.join(os.path.dirname(__file__), "testdata")
+)
+TEST_UFO3 = "TestFont1 (UFO3).ufo"
+TEST_UFOZ = "TestFont1 (UFO3).ufoz"
+
+
+@pytest.fixture(params=[TEST_UFO3, TEST_UFOZ])
+def testufo(request):
+ name = request.param
+ with fs.tempfs.TempFS() as tmp:
+ if TESTDATA.isdir(name):
+ fs.copy.copy_dir(TESTDATA, name, tmp, name)
+ else:
+ fs.copy.copy_file(TESTDATA, name, tmp, name)
+ yield tmp.getsyspath(name)
+
+
+@pytest.fixture
+def testufoz():
+ with fs.tempfs.TempFS() as tmp:
+ fs.copy.copy_file(TESTDATA, TEST_UFOZ, tmp, TEST_UFOZ)
+ yield tmp.getsyspath(TEST_UFOZ)
+
+
+class TestUFOZ(object):
+
+ def test_read(self, testufoz):
+ with UFOReader(testufoz) as reader:
+ assert reader.fileStructure == UFOFileStructure.ZIP
+ assert reader.formatVersion == 3
+
+ def test_write(self, testufoz):
+ with UFOWriter(testufoz, structure="zip") as writer:
+ writer.writeLib({"hello world": 123})
+ with UFOReader(testufoz) as reader:
+ assert reader.readLib() == {"hello world": 123}
+
+
+def test_pathlike(testufo):
+
+ class PathLike(object):
+
+ def __init__(self, s):
+ self._path = s
+
+ def __fspath__(self):
+ return tostr(self._path, sys.getfilesystemencoding())
+
+ path = PathLike(testufo)
+
+ with UFOReader(path) as reader:
+ assert reader._path == path.__fspath__()
+
+ with UFOWriter(path) as writer:
+ assert writer._path == path.__fspath__()
+
+
+def test_path_attribute_deprecated(testufo):
+ with UFOWriter(testufo) as writer:
+ with pytest.warns(DeprecationWarning, match="The 'path' attribute"):
+ writer.path
+
+
+@pytest.fixture
+def memufo():
+ m = fs.memoryfs.MemoryFS()
+ fs.copy.copy_dir(TESTDATA, TEST_UFO3, m, "/")
+ return m
+
+
+class TestMemoryFS(object):
+
+ def test_init_reader(self, memufo):
+ with UFOReader(memufo) as reader:
+ assert reader.formatVersion == 3
+ assert reader.fileStructure == UFOFileStructure.PACKAGE
+
+ def test_init_writer(self):
+ m = fs.memoryfs.MemoryFS()
+ with UFOWriter(m) as writer:
+ assert m.exists("metainfo.plist")
+ assert writer._path == "<memfs>"
diff --git a/Tests/ufoLib/__init__.py b/Tests/ufoLib/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/Tests/ufoLib/__init__.py
diff --git a/Tests/ufoLib/filenames_test.py b/Tests/ufoLib/filenames_test.py
new file mode 100644
index 00000000..a1d6d92a
--- /dev/null
+++ b/Tests/ufoLib/filenames_test.py
@@ -0,0 +1,98 @@
+from __future__ import absolute_import, unicode_literals
+import unittest
+from fontTools.ufoLib.filenames import userNameToFileName, handleClash1, handleClash2
+
+
+class TestFilenames(unittest.TestCase):
+
+ def test_userNameToFileName(self):
+ self.assertEqual(userNameToFileName("a"), "a")
+ self.assertEqual(userNameToFileName("A"), "A_")
+ self.assertEqual(userNameToFileName("AE"), "A_E_")
+ self.assertEqual(userNameToFileName("Ae"), "A_e")
+ self.assertEqual(userNameToFileName("ae"), "ae")
+ self.assertEqual(userNameToFileName("aE"), "aE_")
+ self.assertEqual(userNameToFileName("a.alt"), "a.alt")
+ self.assertEqual(userNameToFileName("A.alt"), "A_.alt")
+ self.assertEqual(userNameToFileName("A.Alt"), "A_.A_lt")
+ self.assertEqual(userNameToFileName("A.aLt"), "A_.aL_t")
+ self.assertEqual(userNameToFileName("A.alT"), "A_.alT_")
+ self.assertEqual(userNameToFileName("T_H"), "T__H_")
+ self.assertEqual(userNameToFileName("T_h"), "T__h")
+ self.assertEqual(userNameToFileName("t_h"), "t_h")
+ self.assertEqual(userNameToFileName("F_F_I"), "F__F__I_")
+ self.assertEqual(userNameToFileName("f_f_i"), "f_f_i")
+ self.assertEqual(userNameToFileName("Aacute_V.swash"),
+ "A_acute_V_.swash")
+ self.assertEqual(userNameToFileName(".notdef"), "_notdef")
+ self.assertEqual(userNameToFileName("con"), "_con")
+ self.assertEqual(userNameToFileName("CON"), "C_O_N_")
+ self.assertEqual(userNameToFileName("con.alt"), "_con.alt")
+ self.assertEqual(userNameToFileName("alt.con"), "alt._con")
+
+ def test_userNameToFileName_ValueError(self):
+ with self.assertRaises(ValueError):
+ userNameToFileName(b"a")
+ with self.assertRaises(ValueError):
+ userNameToFileName({"a"})
+ with self.assertRaises(ValueError):
+ userNameToFileName(("a",))
+ with self.assertRaises(ValueError):
+ userNameToFileName(["a"])
+ with self.assertRaises(ValueError):
+ userNameToFileName(["a"])
+ with self.assertRaises(ValueError):
+ userNameToFileName(b"\xd8\x00")
+
+ def test_handleClash1(self):
+ prefix = ("0" * 5) + "."
+ suffix = "." + ("0" * 10)
+ existing = ["a" * 5]
+
+ e = list(existing)
+ self.assertEqual(
+ handleClash1(userName="A" * 5, existing=e, prefix=prefix,
+ suffix=suffix),
+ '00000.AAAAA000000000000001.0000000000'
+ )
+
+ e = list(existing)
+ e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
+ self.assertEqual(
+ handleClash1(userName="A" * 5, existing=e, prefix=prefix,
+ suffix=suffix),
+ '00000.AAAAA000000000000002.0000000000'
+ )
+
+ e = list(existing)
+ e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
+ self.assertEqual(
+ handleClash1(userName="A" * 5, existing=e, prefix=prefix,
+ suffix=suffix),
+ '00000.AAAAA000000000000001.0000000000'
+ )
+
+ def test_handleClash2(self):
+ prefix = ("0" * 5) + "."
+ suffix = "." + ("0" * 10)
+ existing = [prefix + str(i) + suffix for i in range(100)]
+
+ e = list(existing)
+ self.assertEqual(
+ handleClash2(existing=e, prefix=prefix, suffix=suffix),
+ '00000.100.0000000000'
+ )
+
+ e = list(existing)
+ e.remove(prefix + "1" + suffix)
+ self.assertEqual(
+ handleClash2(existing=e, prefix=prefix, suffix=suffix),
+ '00000.1.0000000000'
+ )
+
+ e = list(existing)
+ e.remove(prefix + "2" + suffix)
+ self.assertEqual(
+ handleClash2(existing=e, prefix=prefix, suffix=suffix),
+ '00000.2.0000000000'
+ )
diff --git a/Tests/ufoLib/glifLib_test.py b/Tests/ufoLib/glifLib_test.py
new file mode 100644
index 00000000..3bcd07c1
--- /dev/null
+++ b/Tests/ufoLib/glifLib_test.py
@@ -0,0 +1,164 @@
+from __future__ import absolute_import, unicode_literals
+import os
+import tempfile
+import shutil
+import unittest
+from io import open
+from .testSupport import getDemoFontGlyphSetPath
+from fontTools.ufoLib.glifLib import (
+ GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString,
+)
+from fontTools.misc.etree import XML_DECLARATION
+
+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):
+ import difflib
+ srcDir = GLYPHSETDIR
+ dstDir = self.dstDir
+ src = GlyphSet(srcDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
+ dst = GlyphSet(dstDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
+ 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 sorted(src.keys()):
+ fileName = src.contents[glyphName]
+ with open(os.path.join(srcDir, fileName), "r") as f:
+ org = f.read()
+ with open(os.path.join(dstDir, fileName), "r") as f:
+ new = f.read()
+ added = []
+ removed = []
+ for line in difflib.unified_diff(
+ org.split("\n"), new.split("\n")):
+ if line.startswith("+ "):
+ added.append(line[1:])
+ elif line.startswith("- "):
+ removed.append(line[1:])
+ self.assertEqual(
+ added, removed,
+ "%s.glif file differs after round tripping" % glyphName)
+
+ def testRebuildContents(self):
+ gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
+ contents = gset.contents
+ gset.rebuildContents()
+ self.assertEqual(contents, gset.contents)
+
+ def testReverseContents(self):
+ gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
+ 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, validateRead=True, validateWrite=True)
+ dst = GlyphSet(self.dstDir, validateRead=True, validateWrite=True)
+ 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, validateRead=True, validateWrite=True)
+ dst = GlyphSet(self.dstDir, myGlyphNameToFileName, validateRead=True, validateWrite=True)
+ 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():
+ d[k] = "prefix" + v
+ self.assertEqual(d, dst.contents)
+
+ def testGetUnicodes(self):
+ src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
+ 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), "A_ring.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_.A_lt.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")
+ self.assertEqual(glyphNameToFileName("AE", None), "A_E_.glif")
+ self.assertEqual(glyphNameToFileName("Ae", None), "A_e.glif")
+ self.assertEqual(glyphNameToFileName("ae", None), "ae.glif")
+ self.assertEqual(glyphNameToFileName("aE", None), "aE_.glif")
+ self.assertEqual(glyphNameToFileName("a.alt", None), "a.alt.glif")
+ self.assertEqual(glyphNameToFileName("A.aLt", None), "A_.aL_t.glif")
+ self.assertEqual(glyphNameToFileName("A.alT", None), "A_.alT_.glif")
+ self.assertEqual(glyphNameToFileName("Aacute_V.swash", None), "A_acute_V_.swash.glif")
+ self.assertEqual(glyphNameToFileName(".notdef", None), "_notdef.glif")
+ self.assertEqual(glyphNameToFileName("con", None), "_con.glif")
+ self.assertEqual(glyphNameToFileName("CON", None), "C_O_N_.glif")
+ self.assertEqual(glyphNameToFileName("con.alt", None), "_con.alt.glif")
+ self.assertEqual(glyphNameToFileName("alt.con", None), "alt._con.glif")
+
+
+class _Glyph(object):
+ pass
+
+
+class ReadWriteFuncTest(unittest.TestCase):
+
+ def testRoundTrip(self):
+ glyph = _Glyph()
+ glyph.name = "a"
+ glyph.unicodes = [0x0061]
+
+ s1 = writeGlyphToString(glyph.name, glyph)
+
+ glyph2 = _Glyph()
+ readGlyphFromString(s1, glyph2)
+ self.assertEqual(glyph.__dict__, glyph2.__dict__)
+
+ s2 = writeGlyphToString(glyph2.name, glyph2)
+ self.assertEqual(s1, s2)
+
+ def testXmlDeclaration(self):
+ s = writeGlyphToString("a", _Glyph())
+ self.assertTrue(s.startswith(XML_DECLARATION % "UTF-8"))
diff --git a/Tests/ufoLib/testSupport.py b/Tests/ufoLib/testSupport.py
new file mode 100755
index 00000000..2982ce84
--- /dev/null
+++ b/Tests/ufoLib/testSupport.py
@@ -0,0 +1,672 @@
+"""Miscellaneous helpers for our test suite."""
+
+from __future__ import absolute_import, unicode_literals
+import os
+from fontTools.ufoLib.utils import numberTypes
+
+try:
+ basestring
+except NameError:
+ basestring = str
+
+def getDemoFontPath():
+ """Return the path to Data/DemoFont.ufo/."""
+ testdata = os.path.join(os.path.dirname(__file__), "testdata")
+ return os.path.join(testdata, "DemoFont.ufo")
+
+
+def getDemoFontGlyphSetPath():
+ """Return the path to Data/DemoFont.ufo/glyphs/."""
+ return os.path.join(getDemoFontPath(), "glyphs")
+
+
+# GLIF test tools
+
+class Glyph(object):
+
+ def __init__(self):
+ self.name = None
+ self.width = None
+ self.height = None
+ self.unicodes = None
+ self.note = None
+ self.lib = None
+ self.image = None
+ self.guidelines = None
+ self.anchors = None
+ self.outline = []
+
+ def _writePointPenCommand(self, command, args, kwargs):
+ args = _listToString(args)
+ kwargs = _dictToString(kwargs)
+ if args and kwargs:
+ return "pointPen.%s(*%s, **%s)" % (command, args, kwargs)
+ elif len(args):
+ return "pointPen.%s(*%s)" % (command, args)
+ elif len(kwargs):
+ return "pointPen.%s(**%s)" % (command, kwargs)
+ else:
+ return "pointPen.%s()" % command
+
+ def beginPath(self, **kwargs):
+ self.outline.append(self._writePointPenCommand("beginPath", [], kwargs))
+
+ def endPath(self):
+ self.outline.append(self._writePointPenCommand("endPath", [], {}))
+
+ def addPoint(self, *args, **kwargs):
+ self.outline.append(self._writePointPenCommand("addPoint", args, kwargs))
+
+ def addComponent(self, *args, **kwargs):
+ self.outline.append(self._writePointPenCommand("addComponent", args, kwargs))
+
+ def drawPoints(self, pointPen):
+ if self.outline:
+ py = "\n".join(self.outline)
+ exec(py, {"pointPen" : pointPen})
+
+ def py(self):
+ text = []
+ if self.name is not None:
+ text.append("glyph.name = \"%s\"" % self.name)
+ if self.width:
+ text.append("glyph.width = %r" % self.width)
+ if self.height:
+ text.append("glyph.height = %r" % self.height)
+ if self.unicodes is not None:
+ text.append("glyph.unicodes = [%s]" % ", ".join([str(i) for i in self.unicodes]))
+ if self.note is not None:
+ text.append("glyph.note = \"%s\"" % self.note)
+ if self.lib is not None:
+ text.append("glyph.lib = %s" % _dictToString(self.lib))
+ if self.image is not None:
+ text.append("glyph.image = %s" % _dictToString(self.image))
+ if self.guidelines is not None:
+ text.append("glyph.guidelines = %s" % _listToString(self.guidelines))
+ if self.anchors is not None:
+ text.append("glyph.anchors = %s" % _listToString(self.anchors))
+ if self.outline:
+ text += self.outline
+ return "\n".join(text)
+
+def _dictToString(d):
+ text = []
+ for key, value in sorted(d.items()):
+ if value is None:
+ continue
+ key = "\"%s\"" % key
+ if isinstance(value, dict):
+ value = _dictToString(value)
+ elif isinstance(value, list):
+ value = _listToString(value)
+ elif isinstance(value, tuple):
+ value = _tupleToString(value)
+ elif isinstance(value, numberTypes):
+ value = repr(value)
+ elif isinstance(value, basestring):
+ value = "\"%s\"" % value
+ text.append("%s : %s" % (key, value))
+ if not text:
+ return ""
+ return "{%s}" % ", ".join(text)
+
+def _listToString(l):
+ text = []
+ for value in l:
+ if isinstance(value, dict):
+ value = _dictToString(value)
+ elif isinstance(value, list):
+ value = _listToString(value)
+ elif isinstance(value, tuple):
+ value = _tupleToString(value)
+ elif isinstance(value, numberTypes):
+ value = repr(value)
+ elif isinstance(value, basestring):
+ value = "\"%s\"" % value
+ text.append(value)
+ if not text:
+ return ""
+ return "[%s]" % ", ".join(text)
+
+def _tupleToString(t):
+ text = []
+ for value in t:
+ if isinstance(value, dict):
+ value = _dictToString(value)
+ elif isinstance(value, list):
+ value = _listToString(value)
+ elif isinstance(value, tuple):
+ value = _tupleToString(value)
+ elif isinstance(value, numberTypes):
+ value = repr(value)
+ elif isinstance(value, basestring):
+ value = "\"%s\"" % value
+ text.append(value)
+ if not text:
+ return ""
+ return "(%s)" % ", ".join(text)
+
+def stripText(text):
+ new = []
+ for line in text.strip().splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ new.append(line)
+ return "\n".join(new)
+
+# 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)",
+}
+
+fontInfoVersion3 = {
+ "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.",
+ "openTypeGaspRangeRecords" : [
+ dict(rangeMaxPPEM=10, rangeGaspBehavior=[0]),
+ dict(rangeMaxPPEM=20, rangeGaspBehavior=[1]),
+ dict(rangeMaxPPEM=30, rangeGaspBehavior=[2]),
+ dict(rangeMaxPPEM=40, rangeGaspBehavior=[3]),
+ dict(rangeMaxPPEM=50, rangeGaspBehavior=[0, 1, 2, 3]),
+ dict(rangeMaxPPEM=0xFFFF, rangeGaspBehavior=[0])
+ ],
+ "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)",
+ "openTypeNameRecords" : [
+ dict(nameID=1, platformID=1, encodingID=1, languageID=1, string="Name Record."),
+ dict(nameID=2, platformID=1, encodingID=1, languageID=1, string="Name Record.")
+ ],
+ "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)",
+ "woffMajorVersion" : 1,
+ "woffMinorVersion" : 0,
+ "woffMetadataUniqueID" : dict(id="string"),
+ "woffMetadataVendor" : dict(name="Some Foundry", url="http://somefoundry.com"),
+ "woffMetadataCredits" : dict(
+ credits=[
+ dict(name="Some Designer"),
+ dict(name=""),
+ dict(name="Some Designer", url="http://somedesigner.com"),
+ dict(name="Some Designer", url=""),
+ dict(name="Some Designer", role="Designer"),
+ dict(name="Some Designer", role=""),
+ dict(name="Some Designer", dir="ltr"),
+ dict(name="rengiseD emoS", dir="rtl"),
+ {"name" : "Some Designer", "class" : "hello"},
+ {"name" : "Some Designer", "class" : ""},
+ ]
+ ),
+ "woffMetadataDescription" : dict(
+ url="http://somefoundry.com/foo/description",
+ text=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "foo"},
+ {"text" : "foo", "class" : ""},
+ ]
+ ),
+ "woffMetadataLicense" : dict(
+ url="http://somefoundry.com/foo/license",
+ id="foo",
+ text=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "foo"},
+ {"text" : "foo", "class" : ""},
+ ]
+ ),
+ "woffMetadataCopyright" : dict(
+ text=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "foo"},
+ {"text" : "foo", "class" : ""},
+ ]
+ ),
+ "woffMetadataTrademark" : dict(
+ text=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "foo"},
+ {"text" : "foo", "class" : ""},
+ ]
+ ),
+ "woffMetadataLicensee" : dict(
+ name="Some Licensee"
+ ),
+ "woffMetadataExtensions" : [
+ dict(
+ # everything
+ names=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "hello"},
+ {"text" : "foo", "class" : ""},
+ ],
+ items=[
+ # everything
+ dict(
+ id="foo",
+ names=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "hello"},
+ {"text" : "foo", "class" : ""},
+ ],
+ values=[
+ dict(text="foo"),
+ dict(text=""),
+ dict(text="foo", language="bar"),
+ dict(text="foo", language=""),
+ dict(text="foo", dir="ltr"),
+ dict(text="foo", dir="rtl"),
+ {"text" : "foo", "class" : "hello"},
+ {"text" : "foo", "class" : ""},
+ ]
+ ),
+ # no id
+ dict(
+ names=[
+ dict(text="foo")
+ ],
+ values=[
+ dict(text="foo")
+ ]
+ )
+ ]
+ ),
+ # no names
+ dict(
+ items=[
+ dict(
+ id="foo",
+ names=[
+ dict(text="foo")
+ ],
+ values=[
+ dict(text="foo")
+ ]
+ )
+ ]
+ ),
+ ],
+ "guidelines" : [
+ # ints
+ dict(x=100, y=200, angle=45),
+ # floats
+ dict(x=100.5, y=200.5, angle=45.5),
+ # edges
+ dict(x=0, y=0, angle=0),
+ dict(x=0, y=0, angle=360),
+ dict(x=0, y=0, angle=360.0),
+ # no y
+ dict(x=100),
+ # no x
+ dict(y=200),
+ # name
+ dict(x=100, y=200, angle=45, name="foo"),
+ dict(x=100, y=200, angle=45, name=""),
+ # identifier
+ dict(x=100, y=200, angle=45, identifier="guide1"),
+ dict(x=100, y=200, angle=45, identifier="guide2"),
+ dict(x=100, y=200, angle=45, identifier="\x20"),
+ dict(x=100, y=200, angle=45, identifier="\x7E"),
+ # colors
+ dict(x=100, y=200, angle=45, color="0,0,0,0"),
+ dict(x=100, y=200, angle=45, color="1,0,0,0"),
+ dict(x=100, y=200, angle=45, color="1,1,1,1"),
+ dict(x=100, y=200, angle=45, color="0,1,0,0"),
+ dict(x=100, y=200, angle=45, color="0,0,1,0"),
+ dict(x=100, y=200, angle=45, color="0,0,0,1"),
+ dict(x=100, y=200, angle=45, color="1, 0, 0, 0"),
+ dict(x=100, y=200, angle=45, color="0, 1, 0, 0"),
+ dict(x=100, y=200, angle=45, color="0, 0, 1, 0"),
+ dict(x=100, y=200, angle=45, color="0, 0, 0, 1"),
+ dict(x=100, y=200, angle=45, color=".5,0,0,0"),
+ dict(x=100, y=200, angle=45, color="0,.5,0,0"),
+ dict(x=100, y=200, angle=45, color="0,0,.5,0"),
+ dict(x=100, y=200, angle=45, color="0,0,0,.5"),
+ dict(x=100, y=200, angle=45, color=".5,1,1,1"),
+ dict(x=100, y=200, angle=45, color="1,.5,1,1"),
+ dict(x=100, y=200, angle=45, color="1,1,.5,1"),
+ dict(x=100, y=200, angle=45, color="1,1,1,.5"),
+ ],
+}
+
+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
+}
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/fontinfo.plist b/Tests/ufoLib/testdata/DemoFont.ufo/fontinfo.plist
new file mode 100644
index 00000000..501e77f7
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/fontinfo.plist
@@ -0,0 +1,46 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>ascender</key>
+ <integer>750</integer>
+ <key>capHeight</key>
+ <integer>527</integer>
+ <key>copyright</key>
+ <string>TOKEN COPYRIGHT STRING. COPYRIGHT SAME AS PACKAGE. </string>
+ <key>defaultWidth</key>
+ <integer>500</integer>
+ <key>descender</key>
+ <integer>-170</integer>
+ <key>designer</key>
+ <string>Various</string>
+ <key>designerURL</key>
+ <string></string>
+ <key>familyName</key>
+ <string>UFODEMOFONT</string>
+ <key>fontStyle</key>
+ <integer>64</integer>
+ <key>license</key>
+ <string>LICENSE SAME AS PACKAGE.</string>
+ <key>notice</key>
+ <string>TOKEN DESCRIPTION</string>
+ <key>styleName</key>
+ <string>JUSTADEMO</string>
+ <key>trademark</key>
+ <string>NO TRADEMARKS</string>
+ <key>ttVendor</key>
+ <string>NONE</string>
+ <key>ttVersion</key>
+ <string>Version 1.000;PS development 5;hotconv 1.0.38</string>
+ <key>unitsPerEm</key>
+ <integer>1000</integer>
+ <key>vendorURL</key>
+ <string></string>
+ <key>versionMajor</key>
+ <integer>1</integer>
+ <key>xHeight</key>
+ <integer>456</integer>
+ <key>year</key>
+ <integer>2003</integer>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/A_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/A_.glif
new file mode 100644
index 00000000..54b23d27
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/A_.glif
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="A" format="1">
+ <advance width="487"/>
+ <unicode hex="0041"/>
+ <outline>
+ <contour>
+ <point x="243" y="681" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="243" y="739" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="243" y="-75" type="move" name="bottom"/>
+ </contour>
+ <contour>
+ <point x="243" y="739" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="243" y="-75" type="move" name="bottom"/>
+ </contour>
+ <contour>
+ <point x="460" y="0" type="line"/>
+ <point x="318" y="664" type="line"/>
+ <point x="169" y="664" type="line"/>
+ <point x="27" y="0" type="line"/>
+ <point x="129" y="0" type="line"/>
+ <point x="150" y="94" type="line"/>
+ <point x="328" y="94" type="line"/>
+ <point x="348" y="0" type="line"/>
+ </contour>
+ <contour>
+ <point x="307" y="189" type="line"/>
+ <point x="172" y="189" type="line"/>
+ <point x="214" y="398" type="line"/>
+ <point x="239" y="541" type="line"/>
+ <point x="249" y="541" type="line"/>
+ <point x="264" y="399" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/B_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/B_.glif
new file mode 100644
index 00000000..97fed73d
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/B_.glif
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="B" format="1">
+ <advance width="460"/>
+ <unicode hex="0042"/>
+ <outline>
+ <contour>
+ <point x="201" y="681" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="412" y="474" type="curve" smooth="yes"/>
+ <point x="412" y="606"/>
+ <point x="323" y="664"/>
+ <point x="170" y="664" type="curve" smooth="yes"/>
+ <point x="47" y="664" type="line"/>
+ <point x="47" y="0" type="line"/>
+ <point x="170" y="0" type="line" smooth="yes"/>
+ <point x="340" y="0"/>
+ <point x="421" y="70"/>
+ <point x="421" y="189" type="curve" smooth="yes"/>
+ <point x="421" y="265"/>
+ <point x="358" y="330"/>
+ <point x="285" y="330" type="curve"/>
+ <point x="285" y="340" type="line"/>
+ <point x="358" y="340"/>
+ <point x="412" y="392"/>
+ </contour>
+ <contour>
+ <point x="151" y="284" type="line"/>
+ <point x="264" y="284"/>
+ <point x="314" y="253"/>
+ <point x="314" y="189" type="curve" smooth="yes"/>
+ <point x="314" y="130"/>
+ <point x="265" y="95"/>
+ <point x="151" y="95" type="curve"/>
+ </contour>
+ <contour>
+ <point x="151" y="569" type="line"/>
+ <point x="259" y="569"/>
+ <point x="304" y="551"/>
+ <point x="304" y="474" type="curve" smooth="yes"/>
+ <point x="304" y="409"/>
+ <point x="261" y="379"/>
+ <point x="151" y="379" type="curve"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F_.glif
new file mode 100644
index 00000000..4baff853
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F_.glif
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="F" format="1">
+ <advance width="417"/>
+ <unicode hex="0046"/>
+ <outline>
+ <contour>
+ <point x="213" y="681" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="332" y="350" type="line"/>
+ <point x="151" y="350" type="line"/>
+ <point x="151" y="560" type="line"/>
+ <point x="379" y="560" type="line"/>
+ <point x="379" y="664" type="line"/>
+ <point x="47" y="664" type="line"/>
+ <point x="47" y="0" type="line"/>
+ <point x="151" y="0" type="line"/>
+ <point x="151" y="250" type="line"/>
+ <point x="332" y="250" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F__A__B_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F__A__B_.glif
new file mode 100644
index 00000000..f3cbe3ff
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/F__A__B_.glif
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="F_A_B" format="1">
+ <advance width="900"/>
+ <outline>
+ <component base="A"/>
+ <component base="B" xOffset="350"/>
+ <component base="F" xScale="0.965925826289" xyScale="-0.258819045103" yxScale="0.258819045103" yScale="0.965925826289" xOffset="-50" yOffset="500"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/G_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/G_.glif
new file mode 100644
index 00000000..3531c305
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/G_.glif
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="G" format="1">
+ <advance width="494"/>
+ <unicode hex="0047"/>
+ <outline>
+ <contour>
+ <point x="301" y="681" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="272" y="0" type="move" name="bottom"/>
+ </contour>
+ <contour>
+ <point x="446" y="379" type="line"/>
+ <point x="231" y="379" type="line"/>
+ <point x="231" y="284" type="line"/>
+ <point x="350" y="284" type="line"/>
+ <point x="350" y="98" type="line"/>
+ <point x="338" y="95"/>
+ <point x="300" y="94"/>
+ <point x="288" y="94" type="curve" smooth="yes"/>
+ <point x="197" y="94"/>
+ <point x="142" y="130"/>
+ <point x="142" y="322" type="curve" smooth="yes"/>
+ <point x="142" y="514"/>
+ <point x="177" y="569"/>
+ <point x="300" y="569" type="curve" smooth="yes"/>
+ <point x="324" y="569"/>
+ <point x="387" y="567"/>
+ <point x="417" y="563" type="curve"/>
+ <point x="427" y="653" type="line"/>
+ <point x="401" y="663"/>
+ <point x="338" y="674"/>
+ <point x="300" y="674" type="curve" smooth="yes"/>
+ <point x="120" y="674"/>
+ <point x="37" y="570"/>
+ <point x="37" y="322" type="curve" smooth="yes"/>
+ <point x="37" y="71"/>
+ <point x="134" y="-9"/>
+ <point x="272" y="-9" type="curve" smooth="yes"/>
+ <point x="353" y="-9"/>
+ <point x="396" y="-1"/>
+ <point x="446" y="18" type="curve"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/O_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/O_.glif
new file mode 100644
index 00000000..d0cdba60
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/O_.glif
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="O" format="1">
+ <advance width="513"/>
+ <unicode hex="004F"/>
+ <outline>
+ <contour>
+ <point x="259" y="681" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="257" y="0" type="move" name="bottom"/>
+ </contour>
+ <contour>
+ <point x="474" y="332" type="curve" smooth="yes"/>
+ <point x="474" y="579"/>
+ <point x="414" y="674"/>
+ <point x="257" y="674" type="curve" smooth="yes"/>
+ <point x="106" y="674"/>
+ <point x="37" y="567"/>
+ <point x="37" y="332" type="curve" smooth="yes"/>
+ <point x="37" y="85"/>
+ <point x="98" y="-9"/>
+ <point x="256" y="-9" type="curve" smooth="yes"/>
+ <point x="405" y="-9"/>
+ <point x="474" y="98"/>
+ </contour>
+ <contour>
+ <point x="257" y="574" type="curve" smooth="yes"/>
+ <point x="336" y="574"/>
+ <point x="367" y="511"/>
+ <point x="367" y="332" type="curve" smooth="yes"/>
+ <point x="367" y="163"/>
+ <point x="332" y="91"/>
+ <point x="256" y="91" type="curve" smooth="yes"/>
+ <point x="176" y="91"/>
+ <point x="145" y="153"/>
+ <point x="145" y="332" type="curve" smooth="yes"/>
+ <point x="145" y="501"/>
+ <point x="180" y="574"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/R_.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/R_.glif
new file mode 100644
index 00000000..ba45f0ee
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/R_.glif
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="R" format="1">
+ <advance width="463"/>
+ <unicode hex="0052"/>
+ <outline>
+ <contour>
+ <point x="208" y="681" type="move" name="top"/>
+ </contour>
+ <contour>
+ <point x="445" y="0" type="line"/>
+ <point x="319" y="249" type="line"/>
+ <point x="380" y="286"/>
+ <point x="417" y="349"/>
+ <point x="417" y="436" type="curve" smooth="yes"/>
+ <point x="417" y="590"/>
+ <point x="315" y="664"/>
+ <point x="151" y="664" type="curve" smooth="yes"/>
+ <point x="47" y="664" type="line"/>
+ <point x="47" y="0" type="line"/>
+ <point x="151" y="0" type="line"/>
+ <point x="151" y="208" type="line"/>
+ <point x="180" y="208"/>
+ <point x="197" y="210"/>
+ <point x="221" y="214" type="curve"/>
+ <point x="331" y="0" type="line"/>
+ </contour>
+ <contour>
+ <point x="313" y="436" type="curve" smooth="yes"/>
+ <point x="313" y="345"/>
+ <point x="250" y="303"/>
+ <point x="151" y="303" type="curve"/>
+ <point x="151" y="569" type="line"/>
+ <point x="251" y="569"/>
+ <point x="313" y="535"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/a.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/a.glif
new file mode 100644
index 00000000..dc1cbdf0
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/a.glif
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="a" format="1">
+ <unicode hex="0061"/>
+ <outline>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/contents.plist b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/contents.plist
new file mode 100644
index 00000000..8607953b
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/contents.plist
@@ -0,0 +1,26 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>A</key>
+ <string>A_.glif</string>
+ <key>B</key>
+ <string>B_.glif</string>
+ <key>F</key>
+ <string>F_.glif</string>
+ <key>F_A_B</key>
+ <string>F__A__B_.glif</string>
+ <key>G</key>
+ <string>G_.glif</string>
+ <key>O</key>
+ <string>O_.glif</string>
+ <key>R</key>
+ <string>R_.glif</string>
+ <key>a</key>
+ <string>a.glif</string>
+ <key>testglyph1</key>
+ <string>testglyph1.glif</string>
+ <key>testglyph1.reversed</key>
+ <string>testglyph1.reversed.glif</string>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.glif
new file mode 100644
index 00000000..0559cacb
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.glif
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="testglyph1" format="1">
+ <advance width="500"/>
+ <outline>
+ <contour>
+ <point x="58" y="443" type="move"/>
+ <point x="84" y="667" type="line"/>
+ <point x="313" y="632" type="line"/>
+ <point x="354" y="380" type="line"/>
+ </contour>
+ <contour>
+ <point x="328" y="238" type="line"/>
+ <point x="328" y="32" type="line"/>
+ <point x="90" y="29" type="line"/>
+ <point x="87" y="235" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif
new file mode 100644
index 00000000..91c33775
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/glyphs/testglyph1.reversed.glif
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="testglyph1.reversed" format="1">
+ <advance width="500"/>
+ <outline>
+ <contour>
+ <point x="354" y="380" type="move"/>
+ <point x="313" y="632" type="line"/>
+ <point x="84" y="667" type="line"/>
+ <point x="58" y="443" type="line"/>
+ </contour>
+ <contour>
+ <point x="328" y="238" type="line"/>
+ <point x="87" y="235" type="line"/>
+ <point x="90" y="29" type="line"/>
+ <point x="328" y="32" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/lib.plist b/Tests/ufoLib/testdata/DemoFont.ufo/lib.plist
new file mode 100644
index 00000000..6056e7d1
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/lib.plist
@@ -0,0 +1,6 @@
+<?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">
+<plist version="1.0">
+<dict>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/DemoFont.ufo/metainfo.plist b/Tests/ufoLib/testdata/DemoFont.ufo/metainfo.plist
new file mode 100644
index 00000000..c044a5ff
--- /dev/null
+++ b/Tests/ufoLib/testdata/DemoFont.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>creator</key>
+ <string>org.robofab.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>1</integer>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/fontinfo.plist b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/fontinfo.plist
new file mode 100644
index 00000000..bab3aa2f
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/fontinfo.plist
@@ -0,0 +1,87 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>ascender</key>
+ <integer>750</integer>
+ <key>capHeight</key>
+ <integer>750</integer>
+ <key>copyright</key>
+ <string>Copyright Some Foundry.</string>
+ <key>createdBy</key>
+ <string>Some Foundry</string>
+ <key>defaultWidth</key>
+ <integer>400</integer>
+ <key>descender</key>
+ <integer>-250</integer>
+ <key>designer</key>
+ <string>Some Designer</string>
+ <key>designerURL</key>
+ <string>http://somedesigner.com</string>
+ <key>familyName</key>
+ <string>Some Font (Family Name)</string>
+ <key>fondID</key>
+ <integer>15000</integer>
+ <key>fondName</key>
+ <string>SomeFont Regular (FOND Name)</string>
+ <key>fontName</key>
+ <string>SomeFont-Regular (Postscript Font Name)</string>
+ <key>fontStyle</key>
+ <integer>64</integer>
+ <key>fullName</key>
+ <string>Some Font-Regular (Postscript Full Name)</string>
+ <key>italicAngle</key>
+ <real>-12.5</real>
+ <key>license</key>
+ <string>License info for Some Foundry.</string>
+ <key>licenseURL</key>
+ <string>http://somefoundry.com/license</string>
+ <key>menuName</key>
+ <string>Some Font Regular (Style Map Family Name)</string>
+ <key>msCharSet</key>
+ <integer>0</integer>
+ <key>note</key>
+ <string>A note.</string>
+ <key>notice</key>
+ <string>Some Font by Some Designer for Some Foundry.</string>
+ <key>otFamilyName</key>
+ <string>Some Font (Preferred Family Name)</string>
+ <key>otMacName</key>
+ <string>Some Font Regular (Compatible Full Name)</string>
+ <key>otStyleName</key>
+ <string>Regular (Preferred Subfamily Name)</string>
+ <key>slantAngle</key>
+ <real>-12.5</real>
+ <key>styleName</key>
+ <string>Regular (Style Name)</string>
+ <key>trademark</key>
+ <string>Trademark Some Foundry</string>
+ <key>ttUniqueID</key>
+ <string>OpenType name Table Unique ID</string>
+ <key>ttVendor</key>
+ <string>SOME</string>
+ <key>ttVersion</key>
+ <string>OpenType name Table Version</string>
+ <key>uniqueID</key>
+ <integer>4000000</integer>
+ <key>unitsPerEm</key>
+ <integer>1000</integer>
+ <key>vendorURL</key>
+ <string>http://somefoundry.com</string>
+ <key>versionMajor</key>
+ <integer>1</integer>
+ <key>versionMinor</key>
+ <integer>0</integer>
+ <key>weightName</key>
+ <string>Medium</string>
+ <key>weightValue</key>
+ <integer>500</integer>
+ <key>widthName</key>
+ <string>Medium (normal)</string>
+ <key>xHeight</key>
+ <integer>500</integer>
+ <key>year</key>
+ <integer>2008</integer>
+</dict>
+</plist>
+
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif
new file mode 100644
index 00000000..36afaccc
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/A_.glif
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="A" format="1">
+ <advance width="740"/>
+ <unicode hex="0041"/>
+ <outline>
+ <contour>
+ <point x="20" y="0" type="line"/>
+ <point x="720" y="0" type="line"/>
+ <point x="720" y="700" type="line"/>
+ <point x="20" y="700" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif
new file mode 100644
index 00000000..ddcf3b22
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/B_.glif
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="B" format="1">
+ <advance width="740"/>
+ <unicode hex="0042"/>
+ <outline>
+ <contour>
+ <point x="20" y="350" type="curve" smooth="yes"/>
+ <point x="20" y="157"/>
+ <point x="177" y="0"/>
+ <point x="370" y="0" type="curve" smooth="yes"/>
+ <point x="563" y="0"/>
+ <point x="720" y="157"/>
+ <point x="720" y="350" type="curve" smooth="yes"/>
+ <point x="720" y="543"/>
+ <point x="563" y="700"/>
+ <point x="370" y="700" type="curve" smooth="yes"/>
+ <point x="177" y="700"/>
+ <point x="20" y="543"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist
new file mode 100644
index 00000000..08f7bba8
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/glyphs/contents.plist
@@ -0,0 +1,10 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>A</key>
+ <string>A_.glif</string>
+ <key>B</key>
+ <string>B_.glif</string>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/groups.plist b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/groups.plist
new file mode 100644
index 00000000..40d17d9f
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/groups.plist
@@ -0,0 +1,15 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>group1</key>
+ <array>
+ <string>A</string>
+ </array>
+ <key>group2</key>
+ <array>
+ <string>A</string>
+ <string>B</string>
+ </array>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/kerning.plist b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/kerning.plist
new file mode 100644
index 00000000..c07bf22a
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/kerning.plist
@@ -0,0 +1,16 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>A</key>
+ <dict>
+ <key>B</key>
+ <integer>100</integer>
+ </dict>
+ <key>B</key>
+ <dict>
+ <key>A</key>
+ <integer>-200</integer>
+ </dict>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/lib.plist b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/lib.plist
new file mode 100644
index 00000000..df50b299
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/lib.plist
@@ -0,0 +1,72 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>org.robofab.opentype.classes</key>
+ <string>@myClass = [A B];
+</string>
+ <key>org.robofab.opentype.featureorder</key>
+ <array>
+ <string>liga</string>
+ </array>
+ <key>org.robofab.opentype.features</key>
+ <dict>
+ <key>liga</key>
+ <string>feature liga {
+ sub A A by b;
+} liga;
+</string>
+ </dict>
+ <key>org.robofab.postScriptHintData</key>
+ <dict>
+ <key>blueFuzz</key>
+ <integer>1</integer>
+ <key>blueScale</key>
+ <real>0.039625</real>
+ <key>blueShift</key>
+ <integer>7</integer>
+ <key>blueValues</key>
+ <array>
+ <array>
+ <integer>500</integer>
+ <integer>510</integer>
+ </array>
+ </array>
+ <key>familyBlues</key>
+ <array>
+ <array>
+ <integer>500</integer>
+ <integer>510</integer>
+ </array>
+ </array>
+ <key>familyOtherBlues</key>
+ <array>
+ <array>
+ <integer>-260</integer>
+ <integer>-250</integer>
+ </array>
+ </array>
+ <key>forceBold</key>
+ <true/>
+ <key>hStems</key>
+ <array>
+ <integer>100</integer>
+ <integer>120</integer>
+ </array>
+ <key>otherBlues</key>
+ <array>
+ <array>
+ <integer>-260</integer>
+ <integer>-250</integer>
+ </array>
+ </array>
+ <key>vStems</key>
+ <array>
+ <integer>80</integer>
+ <integer>90</integer>
+ </array>
+ </dict>
+ <key>org.robofab.testFontLibData</key>
+ <string>Foo Bar</string>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/metainfo.plist b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/metainfo.plist
new file mode 100644
index 00000000..c044a5ff
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO1).ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>creator</key>
+ <string>org.robofab.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>1</integer>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/features.fea b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/features.fea
new file mode 100644
index 00000000..40188d9a
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/features.fea
@@ -0,0 +1,5 @@
+@myClass = [A B];
+
+feature liga {
+ sub A A by b;
+} liga;
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/fontinfo.plist b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/fontinfo.plist
new file mode 100644
index 00000000..021d46d6
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/fontinfo.plist
@@ -0,0 +1,239 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>ascender</key>
+ <integer>750</integer>
+ <key>capHeight</key>
+ <integer>750</integer>
+ <key>copyright</key>
+ <string>Copyright Some Foundry.</string>
+ <key>descender</key>
+ <integer>-250</integer>
+ <key>familyName</key>
+ <string>Some Font (Family Name)</string>
+ <key>italicAngle</key>
+ <real>-12.5</real>
+ <key>macintoshFONDFamilyID</key>
+ <integer>15000</integer>
+ <key>macintoshFONDName</key>
+ <string>SomeFont Regular (FOND Name)</string>
+ <key>note</key>
+ <string>A note.</string>
+ <key>openTypeHeadCreated</key>
+ <string>2000/01/01 00:00:00</string>
+ <key>openTypeHeadFlags</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeHeadLowestRecPPEM</key>
+ <integer>10</integer>
+ <key>openTypeHheaAscender</key>
+ <integer>750</integer>
+ <key>openTypeHheaCaretOffset</key>
+ <integer>0</integer>
+ <key>openTypeHheaCaretSlopeRise</key>
+ <integer>1</integer>
+ <key>openTypeHheaCaretSlopeRun</key>
+ <integer>0</integer>
+ <key>openTypeHheaDescender</key>
+ <integer>-250</integer>
+ <key>openTypeHheaLineGap</key>
+ <integer>200</integer>
+ <key>openTypeNameCompatibleFullName</key>
+ <string>Some Font Regular (Compatible Full Name)</string>
+ <key>openTypeNameDescription</key>
+ <string>Some Font by Some Designer for Some Foundry.</string>
+ <key>openTypeNameDesigner</key>
+ <string>Some Designer</string>
+ <key>openTypeNameDesignerURL</key>
+ <string>http://somedesigner.com</string>
+ <key>openTypeNameLicense</key>
+ <string>License info for Some Foundry.</string>
+ <key>openTypeNameLicenseURL</key>
+ <string>http://somefoundry.com/license</string>
+ <key>openTypeNameManufacturer</key>
+ <string>Some Foundry</string>
+ <key>openTypeNameManufacturerURL</key>
+ <string>http://somefoundry.com</string>
+ <key>openTypeNamePreferredFamilyName</key>
+ <string>Some Font (Preferred Family Name)</string>
+ <key>openTypeNamePreferredSubfamilyName</key>
+ <string>Regular (Preferred Subfamily Name)</string>
+ <key>openTypeNameSampleText</key>
+ <string>Sample Text for Some Font.</string>
+ <key>openTypeNameUniqueID</key>
+ <string>OpenType name Table Unique ID</string>
+ <key>openTypeNameVersion</key>
+ <string>OpenType name Table Version</string>
+ <key>openTypeNameWWSFamilyName</key>
+ <string>Some Font (WWS Family Name)</string>
+ <key>openTypeNameWWSSubfamilyName</key>
+ <string>Regular (WWS Subfamily Name)</string>
+ <key>openTypeOS2CodePageRanges</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeOS2Panose</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ <integer>2</integer>
+ <integer>3</integer>
+ <integer>4</integer>
+ <integer>5</integer>
+ <integer>6</integer>
+ <integer>7</integer>
+ <integer>8</integer>
+ <integer>9</integer>
+ </array>
+ <key>openTypeOS2FamilyClass</key>
+ <array>
+ <integer>1</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeOS2Selection</key>
+ <array>
+ <integer>3</integer>
+ </array>
+ <key>openTypeOS2StrikeoutPosition</key>
+ <integer>300</integer>
+ <key>openTypeOS2StrikeoutSize</key>
+ <integer>20</integer>
+ <key>openTypeOS2SubscriptXOffset</key>
+ <integer>0</integer>
+ <key>openTypeOS2SubscriptXSize</key>
+ <integer>200</integer>
+ <key>openTypeOS2SubscriptYOffset</key>
+ <integer>-100</integer>
+ <key>openTypeOS2SubscriptYSize</key>
+ <integer>400</integer>
+ <key>openTypeOS2SuperscriptXOffset</key>
+ <integer>0</integer>
+ <key>openTypeOS2SuperscriptXSize</key>
+ <integer>200</integer>
+ <key>openTypeOS2SuperscriptYOffset</key>
+ <integer>200</integer>
+ <key>openTypeOS2SuperscriptYSize</key>
+ <integer>400</integer>
+ <key>openTypeOS2Type</key>
+ <array>
+ </array>
+ <key>openTypeOS2TypoAscender</key>
+ <integer>750</integer>
+ <key>openTypeOS2TypoDescender</key>
+ <integer>-250</integer>
+ <key>openTypeOS2TypoLineGap</key>
+ <integer>200</integer>
+ <key>openTypeOS2UnicodeRanges</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeOS2VendorID</key>
+ <string>SOME</string>
+ <key>openTypeOS2WeightClass</key>
+ <integer>500</integer>
+ <key>openTypeOS2WidthClass</key>
+ <integer>5</integer>
+ <key>openTypeOS2WinAscent</key>
+ <integer>750</integer>
+ <key>openTypeOS2WinDescent</key>
+ <integer>-250</integer>
+ <key>openTypeVheaCaretOffset</key>
+ <integer>0</integer>
+ <key>openTypeVheaCaretSlopeRise</key>
+ <integer>0</integer>
+ <key>openTypeVheaCaretSlopeRun</key>
+ <integer>1</integer>
+ <key>openTypeVheaVertTypoAscender</key>
+ <integer>750</integer>
+ <key>openTypeVheaVertTypoDescender</key>
+ <integer>-250</integer>
+ <key>openTypeVheaVertTypoLineGap</key>
+ <integer>200</integer>
+ <key>postscriptBlueFuzz</key>
+ <integer>1</integer>
+ <key>postscriptBlueScale</key>
+ <real>0.039625</real>
+ <key>postscriptBlueShift</key>
+ <integer>7</integer>
+ <key>postscriptBlueValues</key>
+ <array>
+ <integer>500</integer>
+ <integer>510</integer>
+ </array>
+ <key>postscriptDefaultCharacter</key>
+ <string>.notdef</string>
+ <key>postscriptDefaultWidthX</key>
+ <integer>400</integer>
+ <key>postscriptFamilyBlues</key>
+ <array>
+ <integer>500</integer>
+ <integer>510</integer>
+ </array>
+ <key>postscriptFamilyOtherBlues</key>
+ <array>
+ <integer>-250</integer>
+ <integer>-260</integer>
+ </array>
+ <key>postscriptFontName</key>
+ <string>SomeFont-Regular (Postscript Font Name)</string>
+ <key>postscriptForceBold</key>
+ <true/>
+ <key>postscriptFullName</key>
+ <string>Some Font-Regular (Postscript Full Name)</string>
+ <key>postscriptIsFixedPitch</key>
+ <false/>
+ <key>postscriptNominalWidthX</key>
+ <integer>400</integer>
+ <key>postscriptOtherBlues</key>
+ <array>
+ <integer>-250</integer>
+ <integer>-260</integer>
+ </array>
+ <key>postscriptSlantAngle</key>
+ <real>-12.5</real>
+ <key>postscriptStemSnapH</key>
+ <array>
+ <integer>100</integer>
+ <integer>120</integer>
+ </array>
+ <key>postscriptStemSnapV</key>
+ <array>
+ <integer>80</integer>
+ <integer>90</integer>
+ </array>
+ <key>postscriptUnderlinePosition</key>
+ <integer>-200</integer>
+ <key>postscriptUnderlineThickness</key>
+ <integer>20</integer>
+ <key>postscriptUniqueID</key>
+ <integer>4000000</integer>
+ <key>postscriptWeightName</key>
+ <string>Medium</string>
+ <key>postscriptWindowsCharacterSet</key>
+ <integer>1</integer>
+ <key>styleMapFamilyName</key>
+ <string>Some Font Regular (Style Map Family Name)</string>
+ <key>styleMapStyleName</key>
+ <string>regular</string>
+ <key>styleName</key>
+ <string>Regular (Style Name)</string>
+ <key>trademark</key>
+ <string>Trademark Some Foundry</string>
+ <key>unitsPerEm</key>
+ <integer>1000</integer>
+ <key>versionMajor</key>
+ <integer>1</integer>
+ <key>versionMinor</key>
+ <integer>0</integer>
+ <key>xHeight</key>
+ <integer>500</integer>
+ <key>year</key>
+ <integer>2008</integer>
+</dict>
+</plist>
+
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif
new file mode 100644
index 00000000..36afaccc
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/A_.glif
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="A" format="1">
+ <advance width="740"/>
+ <unicode hex="0041"/>
+ <outline>
+ <contour>
+ <point x="20" y="0" type="line"/>
+ <point x="720" y="0" type="line"/>
+ <point x="720" y="700" type="line"/>
+ <point x="20" y="700" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif
new file mode 100644
index 00000000..ddcf3b22
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/B_.glif
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="B" format="1">
+ <advance width="740"/>
+ <unicode hex="0042"/>
+ <outline>
+ <contour>
+ <point x="20" y="350" type="curve" smooth="yes"/>
+ <point x="20" y="157"/>
+ <point x="177" y="0"/>
+ <point x="370" y="0" type="curve" smooth="yes"/>
+ <point x="563" y="0"/>
+ <point x="720" y="157"/>
+ <point x="720" y="350" type="curve" smooth="yes"/>
+ <point x="720" y="543"/>
+ <point x="563" y="700"/>
+ <point x="370" y="700" type="curve" smooth="yes"/>
+ <point x="177" y="700"/>
+ <point x="20" y="543"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist
new file mode 100644
index 00000000..08f7bba8
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/glyphs/contents.plist
@@ -0,0 +1,10 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>A</key>
+ <string>A_.glif</string>
+ <key>B</key>
+ <string>B_.glif</string>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/groups.plist b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/groups.plist
new file mode 100644
index 00000000..40d17d9f
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/groups.plist
@@ -0,0 +1,15 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>group1</key>
+ <array>
+ <string>A</string>
+ </array>
+ <key>group2</key>
+ <array>
+ <string>A</string>
+ <string>B</string>
+ </array>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/kerning.plist b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/kerning.plist
new file mode 100644
index 00000000..c07bf22a
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/kerning.plist
@@ -0,0 +1,16 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>A</key>
+ <dict>
+ <key>B</key>
+ <integer>100</integer>
+ </dict>
+ <key>B</key>
+ <dict>
+ <key>A</key>
+ <integer>-200</integer>
+ </dict>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/lib.plist b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/lib.plist
new file mode 100644
index 00000000..0931165b
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/lib.plist
@@ -0,0 +1,8 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>org.robofab.testFontLibData</key>
+ <string>Foo Bar</string>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/metainfo.plist b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/metainfo.plist
new file mode 100644
index 00000000..f5ec4e54
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO2).ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>creator</key>
+ <string>org.robofab.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>2</integer>
+</dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx
new file mode 100644
index 00000000..51847fd5
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/data/com.github.fonttools.ttx/CUST.ttx
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.13">
+
+ <CUST raw="True">
+ <hexdata>
+ 0001beef
+ </hexdata>
+ </CUST>
+
+</ttFont>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/fontinfo.plist b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/fontinfo.plist
new file mode 100644
index 00000000..78dd88e0
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/fontinfo.plist
@@ -0,0 +1,338 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>ascender</key>
+ <integer>750</integer>
+ <key>capHeight</key>
+ <integer>750</integer>
+ <key>copyright</key>
+ <string>Copyright © Some Foundry.</string>
+ <key>descender</key>
+ <integer>-250</integer>
+ <key>familyName</key>
+ <string>Some Font (Family Name)</string>
+ <key>guidelines</key>
+ <array>
+ <dict>
+ <key>x</key>
+ <integer>250</integer>
+ </dict>
+ <dict>
+ <key>x</key>
+ <integer>-20</integer>
+ </dict>
+ <dict>
+ <key>x</key>
+ <integer>30</integer>
+ </dict>
+ <dict>
+ <key>y</key>
+ <integer>500</integer>
+ </dict>
+ <dict>
+ <key>y</key>
+ <integer>-200</integer>
+ </dict>
+ <dict>
+ <key>y</key>
+ <integer>700</integer>
+ </dict>
+ <dict>
+ <key>angle</key>
+ <integer>135</integer>
+ <key>x</key>
+ <integer>0</integer>
+ <key>y</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>angle</key>
+ <integer>45</integer>
+ <key>x</key>
+ <integer>0</integer>
+ <key>y</key>
+ <integer>700</integer>
+ </dict>
+ <dict>
+ <key>angle</key>
+ <integer>135</integer>
+ <key>x</key>
+ <integer>20</integer>
+ <key>y</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>italicAngle</key>
+ <real>-12.5</real>
+ <key>macintoshFONDFamilyID</key>
+ <integer>15000</integer>
+ <key>macintoshFONDName</key>
+ <string>SomeFont Regular (FOND Name)</string>
+ <key>note</key>
+ <string>A note.</string>
+ <key>openTypeGaspRangeRecords</key>
+ <array>
+ <dict>
+ <key>rangeGaspBehavior</key>
+ <array>
+ <integer>1</integer>
+ <integer>3</integer>
+ </array>
+ <key>rangeMaxPPEM</key>
+ <integer>7</integer>
+ </dict>
+ <dict>
+ <key>rangeGaspBehavior</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ <integer>2</integer>
+ <integer>3</integer>
+ </array>
+ <key>rangeMaxPPEM</key>
+ <integer>65535</integer>
+ </dict>
+ </array>
+ <key>openTypeHeadCreated</key>
+ <string>2000/01/01 00:00:00</string>
+ <key>openTypeHeadFlags</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeHeadLowestRecPPEM</key>
+ <integer>10</integer>
+ <key>openTypeHheaAscender</key>
+ <integer>750</integer>
+ <key>openTypeHheaCaretOffset</key>
+ <integer>0</integer>
+ <key>openTypeHheaCaretSlopeRise</key>
+ <integer>1</integer>
+ <key>openTypeHheaCaretSlopeRun</key>
+ <integer>0</integer>
+ <key>openTypeHheaDescender</key>
+ <integer>-250</integer>
+ <key>openTypeHheaLineGap</key>
+ <integer>200</integer>
+ <key>openTypeNameCompatibleFullName</key>
+ <string>Some Font Regular (Compatible Full Name)</string>
+ <key>openTypeNameDescription</key>
+ <string>Some Font by Some Designer for Some Foundry.</string>
+ <key>openTypeNameDesigner</key>
+ <string>Some Designer</string>
+ <key>openTypeNameDesignerURL</key>
+ <string>http://somedesigner.com</string>
+ <key>openTypeNameLicense</key>
+ <string>License info for Some Foundry.</string>
+ <key>openTypeNameLicenseURL</key>
+ <string>http://somefoundry.com/license</string>
+ <key>openTypeNameManufacturer</key>
+ <string>Some Foundry</string>
+ <key>openTypeNameManufacturerURL</key>
+ <string>http://somefoundry.com</string>
+ <key>openTypeNamePreferredFamilyName</key>
+ <string>Some Font (Preferred Family Name)</string>
+ <key>openTypeNamePreferredSubfamilyName</key>
+ <string>Regular (Preferred Subfamily Name)</string>
+ <key>openTypeNameRecords</key>
+ <array>
+ <dict>
+ <key>encodingID</key>
+ <integer>0</integer>
+ <key>languageID</key>
+ <integer>0</integer>
+ <key>nameID</key>
+ <integer>3</integer>
+ <key>platformID</key>
+ <integer>1</integer>
+ <key>string</key>
+ <string>Unique Font Identifier</string>
+ </dict>
+ <dict>
+ <key>encodingID</key>
+ <integer>1</integer>
+ <key>languageID</key>
+ <integer>1033</integer>
+ <key>nameID</key>
+ <integer>8</integer>
+ <key>platformID</key>
+ <integer>3</integer>
+ <key>string</key>
+ <string>Some Foundry (Manufacturer Name)</string>
+ </dict>
+ </array>
+ <key>openTypeNameSampleText</key>
+ <string>Sample Text for Some Font.</string>
+ <key>openTypeNameUniqueID</key>
+ <string>OpenType name Table Unique ID</string>
+ <key>openTypeNameVersion</key>
+ <string>OpenType name Table Version</string>
+ <key>openTypeNameWWSFamilyName</key>
+ <string>Some Font (WWS Family Name)</string>
+ <key>openTypeNameWWSSubfamilyName</key>
+ <string>Regular (WWS Subfamily Name)</string>
+ <key>openTypeOS2CodePageRanges</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeOS2FamilyClass</key>
+ <array>
+ <integer>1</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeOS2Panose</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ <integer>2</integer>
+ <integer>3</integer>
+ <integer>4</integer>
+ <integer>5</integer>
+ <integer>6</integer>
+ <integer>7</integer>
+ <integer>8</integer>
+ <integer>9</integer>
+ </array>
+ <key>openTypeOS2Selection</key>
+ <array>
+ <integer>3</integer>
+ </array>
+ <key>openTypeOS2StrikeoutPosition</key>
+ <integer>300</integer>
+ <key>openTypeOS2StrikeoutSize</key>
+ <integer>20</integer>
+ <key>openTypeOS2SubscriptXOffset</key>
+ <integer>0</integer>
+ <key>openTypeOS2SubscriptXSize</key>
+ <integer>200</integer>
+ <key>openTypeOS2SubscriptYOffset</key>
+ <integer>-100</integer>
+ <key>openTypeOS2SubscriptYSize</key>
+ <integer>400</integer>
+ <key>openTypeOS2SuperscriptXOffset</key>
+ <integer>0</integer>
+ <key>openTypeOS2SuperscriptXSize</key>
+ <integer>200</integer>
+ <key>openTypeOS2SuperscriptYOffset</key>
+ <integer>200</integer>
+ <key>openTypeOS2SuperscriptYSize</key>
+ <integer>400</integer>
+ <key>openTypeOS2Type</key>
+ <array/>
+ <key>openTypeOS2TypoAscender</key>
+ <integer>750</integer>
+ <key>openTypeOS2TypoDescender</key>
+ <integer>-250</integer>
+ <key>openTypeOS2TypoLineGap</key>
+ <integer>200</integer>
+ <key>openTypeOS2UnicodeRanges</key>
+ <array>
+ <integer>0</integer>
+ <integer>1</integer>
+ </array>
+ <key>openTypeOS2VendorID</key>
+ <string>SOME</string>
+ <key>openTypeOS2WeightClass</key>
+ <integer>500</integer>
+ <key>openTypeOS2WidthClass</key>
+ <integer>5</integer>
+ <key>openTypeOS2WinAscent</key>
+ <integer>750</integer>
+ <key>openTypeOS2WinDescent</key>
+ <integer>250</integer>
+ <key>openTypeVheaCaretOffset</key>
+ <integer>0</integer>
+ <key>openTypeVheaCaretSlopeRise</key>
+ <integer>0</integer>
+ <key>openTypeVheaCaretSlopeRun</key>
+ <integer>1</integer>
+ <key>openTypeVheaVertTypoAscender</key>
+ <integer>750</integer>
+ <key>openTypeVheaVertTypoDescender</key>
+ <integer>-250</integer>
+ <key>openTypeVheaVertTypoLineGap</key>
+ <integer>200</integer>
+ <key>postscriptBlueFuzz</key>
+ <integer>1</integer>
+ <key>postscriptBlueScale</key>
+ <real>0.039625</real>
+ <key>postscriptBlueShift</key>
+ <integer>7</integer>
+ <key>postscriptBlueValues</key>
+ <array>
+ <integer>500</integer>
+ <integer>510</integer>
+ </array>
+ <key>postscriptDefaultCharacter</key>
+ <string>.notdef</string>
+ <key>postscriptDefaultWidthX</key>
+ <integer>400</integer>
+ <key>postscriptFamilyBlues</key>
+ <array>
+ <integer>500</integer>
+ <integer>510</integer>
+ </array>
+ <key>postscriptFamilyOtherBlues</key>
+ <array>
+ <integer>-250</integer>
+ <integer>-260</integer>
+ </array>
+ <key>postscriptFontName</key>
+ <string>SomeFont-Regular (Postscript Font Name)</string>
+ <key>postscriptForceBold</key>
+ <true/>
+ <key>postscriptFullName</key>
+ <string>Some Font-Regular (Postscript Full Name)</string>
+ <key>postscriptIsFixedPitch</key>
+ <false/>
+ <key>postscriptNominalWidthX</key>
+ <integer>400</integer>
+ <key>postscriptOtherBlues</key>
+ <array>
+ <integer>-250</integer>
+ <integer>-260</integer>
+ </array>
+ <key>postscriptSlantAngle</key>
+ <real>-12.5</real>
+ <key>postscriptStemSnapH</key>
+ <array>
+ <integer>100</integer>
+ <integer>120</integer>
+ </array>
+ <key>postscriptStemSnapV</key>
+ <array>
+ <integer>80</integer>
+ <integer>90</integer>
+ </array>
+ <key>postscriptUnderlinePosition</key>
+ <integer>-200</integer>
+ <key>postscriptUnderlineThickness</key>
+ <integer>20</integer>
+ <key>postscriptUniqueID</key>
+ <integer>4000000</integer>
+ <key>postscriptWeightName</key>
+ <string>Medium</string>
+ <key>postscriptWindowsCharacterSet</key>
+ <integer>1</integer>
+ <key>styleMapFamilyName</key>
+ <string>Some Font Regular (Style Map Family Name)</string>
+ <key>styleMapStyleName</key>
+ <string>regular</string>
+ <key>styleName</key>
+ <string>Regular (Style Name)</string>
+ <key>trademark</key>
+ <string>Trademark Some Foundry</string>
+ <key>unitsPerEm</key>
+ <integer>1000</integer>
+ <key>versionMajor</key>
+ <integer>1</integer>
+ <key>versionMinor</key>
+ <integer>0</integer>
+ <key>xHeight</key>
+ <integer>500</integer>
+ <key>year</key>
+ <integer>2008</integer>
+ </dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif
new file mode 100644
index 00000000..630ec6b0
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/_notdef.glif
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name=".notdef" format="2">
+ <advance height="1000" width="500"/>
+ <outline>
+ <contour>
+ <point x="450" y="0" type="line"/>
+ <point x="450" y="750" type="line"/>
+ <point x="50" y="750" type="line"/>
+ <point x="50" y="0" type="line"/>
+ </contour>
+ <contour>
+ <point x="400" y="50" type="line"/>
+ <point x="100" y="50" type="line"/>
+ <point x="100" y="700" type="line"/>
+ <point x="400" y="700" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif
new file mode 100644
index 00000000..a751d870
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/a.glif
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="a" format="2">
+ <advance height="750" width="388"/>
+ <unicode hex="0061"/>
+ <outline>
+ <contour>
+ <point x="66" y="0" type="line"/>
+ <point x="322" y="0" type="line"/>
+ <point x="194" y="510" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif
new file mode 100644
index 00000000..54066a25
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/b.glif
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="b" format="2">
+ <advance height="750" width="410"/>
+ <unicode hex="0062"/>
+ <outline>
+ <contour>
+ <point x="100" y="505" type="line"/>
+ <point x="100" y="-5" type="line"/>
+ <point x="310" y="-5" type="line"/>
+ <point x="310" y="505" type="line"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif
new file mode 100644
index 00000000..7abf451b
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/c.glif
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="c" format="2">
+ <advance height="750" width="374"/>
+ <unicode hex="0063"/>
+ <outline>
+ <contour>
+ <point x="300" y="-10" type="curve"/>
+ <point x="300" y="500" type="line"/>
+ <point x="150" y="500"/>
+ <point x="100" y="450"/>
+ <point x="100" y="245" type="curve"/>
+ <point x="100" y="40"/>
+ <point x="150" y="-10"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist
new file mode 100644
index 00000000..f730b93b
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/contents.plist
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>.notdef</key>
+ <string>_notdef.glif</string>
+ <key>a</key>
+ <string>a.glif</string>
+ <key>b</key>
+ <string>b.glif</string>
+ <key>c</key>
+ <string>c.glif</string>
+ <key>d</key>
+ <string>d.glif</string>
+ <key>e</key>
+ <string>e.glif</string>
+ <key>f</key>
+ <string>f.glif</string>
+ <key>g</key>
+ <string>g.glif</string>
+ <key>h</key>
+ <string>h.glif</string>
+ <key>i</key>
+ <string>i.glif</string>
+ <key>j</key>
+ <string>j.glif</string>
+ <key>k</key>
+ <string>k.glif</string>
+ <key>l</key>
+ <string>l.glif</string>
+ <key>space</key>
+ <string>space.glif</string>
+ </dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif
new file mode 100644
index 00000000..09b619b4
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/d.glif
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="d" format="2">
+ <advance height="750" width="374"/>
+ <unicode hex="0064"/>
+ <outline>
+ <contour>
+ <point x="150.66" y="197.32" type="curve"/>
+ <point x="117" y="197.32"/>
+ <point x="90.33" y="170.33"/>
+ <point x="90.33" y="137" type="curve"/>
+ <point x="90.33" y="103.67"/>
+ <point x="117" y="77.01"/>
+ <point x="150.66" y="77.01" type="curve"/>
+ <point x="183.99" y="77.01"/>
+ <point x="210.65" y="103.67"/>
+ <point x="210.65" y="137" type="curve"/>
+ <point x="210.65" y="170.33"/>
+ <point x="183.99" y="197.32"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif
new file mode 100644
index 00000000..52abd0a7
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/e.glif
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="e" format="2">
+ <advance height="750" width="388"/>
+ <unicode hex="0065"/>
+ <outline>
+ <contour>
+ <point x="66" y="510" type="line"/>
+ <point x="194" y="75" type="line"/>
+ <point x="322" y="510" type="line"/>
+ </contour>
+ <contour>
+ <point x="-55" y="23" type="curve"/>
+ <point x="454" y="23" type="line"/>
+ <point x="454" y="173"/>
+ <point x="404" y="223"/>
+ <point x="199" y="223" type="curve"/>
+ <point x="-5" y="223"/>
+ <point x="-55" y="173"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif
new file mode 100644
index 00000000..2c13b95a
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/f.glif
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="f" format="2">
+ <advance height="750" width="410"/>
+ <unicode hex="0066"/>
+ <outline>
+ <contour>
+ <point x="66" y="510" type="line"/>
+ <point x="322" y="510" type="line"/>
+ <point x="194" y="75" type="line"/>
+ </contour>
+ <contour>
+ <point x="-55" y="23" type="curve"/>
+ <point x="454" y="23" type="line"/>
+ <point x="454" y="173"/>
+ <point x="404" y="223"/>
+ <point x="199" y="223" type="curve"/>
+ <point x="-5" y="223"/>
+ <point x="-55" y="173"/>
+ </contour>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif
new file mode 100644
index 00000000..fdbe8ac1
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/g.glif
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="g" format="2">
+ <advance height="750" width="388"/>
+ <unicode hex="0067"/>
+ <outline>
+ <component base="a"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif
new file mode 100644
index 00000000..561563fd
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/h.glif
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="h" format="2">
+ <advance height="1000" width="410"/>
+ <unicode hex="0068"/>
+ <outline>
+ <component base="d" xOffset="60" yOffset="460"/>
+ <component base="b"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif
new file mode 100644
index 00000000..84ef89bb
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/i.glif
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="i" format="2">
+ <advance height="750" width="600"/>
+ <unicode hex="0069"/>
+ <outline>
+ <contour>
+ <point x="-55" y="-80" type="curve"/>
+ <point x="454" y="-80" type="line"/>
+ <point x="454" y="69"/>
+ <point x="404" y="119"/>
+ <point x="199" y="119" type="curve"/>
+ <point x="-5" y="119"/>
+ <point x="-55" y="69"/>
+ </contour>
+ <component base="a"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif
new file mode 100644
index 00000000..550ee9b5
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/j.glif
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="j" format="2">
+ <advance height="1000" width="600"/>
+ <unicode hex="006A"/>
+ <outline>
+ <contour>
+ <point x="-55" y="-80" type="curve"/>
+ <point x="454" y="-80" type="line"/>
+ <point x="454" y="69"/>
+ <point x="404" y="119"/>
+ <point x="199" y="119" type="curve"/>
+ <point x="-5" y="119"/>
+ <point x="-55" y="69"/>
+ </contour>
+ <component base="a" yScale="-1" yOffset="230"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif
new file mode 100644
index 00000000..c09ac3ae
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/k.glif
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="k" format="2">
+ <advance height="1000" width="600"/>
+ <unicode hex="006B"/>
+ <outline>
+ <component base="a"/>
+ <component base="a" xOffset="100"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif
new file mode 100644
index 00000000..f71c34f3
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/l.glif
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="l" format="2">
+ <advance height="1000" width="600"/>
+ <unicode hex="006C"/>
+ <outline>
+ <component base="a" xScale="-1" xOffset="400"/>
+ <component base="a" xOffset="100"/>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif
new file mode 100644
index 00000000..eaa0d169
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/glyphs/space.glif
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glyph name="space" format="2">
+ <advance height="250" width="250"/>
+ <unicode hex="0020"/>
+ <outline>
+ </outline>
+</glyph>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/kerning.plist b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/kerning.plist
new file mode 100644
index 00000000..1dd116ec
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/kerning.plist
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>a</key>
+ <dict>
+ <key>a</key>
+ <integer>5</integer>
+ <key>b</key>
+ <integer>-10</integer>
+ <key>space</key>
+ <integer>1</integer>
+ </dict>
+ <key>b</key>
+ <dict>
+ <key>a</key>
+ <integer>-7</integer>
+ </dict>
+ </dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/layercontents.plist b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/layercontents.plist
new file mode 100644
index 00000000..963df400
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/layercontents.plist
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <array>
+ <array>
+ <string>public.default</string>
+ <string>glyphs</string>
+ </array>
+ </array>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/lib.plist b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/lib.plist
new file mode 100644
index 00000000..f257b3a2
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/lib.plist
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>public.glyphOrder</key>
+ <array>
+ <string>.notdef</string>
+ <string>glyph1</string>
+ <string>glyph2</string>
+ <string>space</string>
+ <string>a</string>
+ <string>b</string>
+ <string>c</string>
+ <string>d</string>
+ <string>e</string>
+ <string>f</string>
+ <string>g</string>
+ <string>h</string>
+ <string>i</string>
+ <string>j</string>
+ <string>k</string>
+ <string>l</string>
+ </array>
+ </dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/metainfo.plist b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/metainfo.plist
new file mode 100644
index 00000000..8e836fbf
--- /dev/null
+++ b/Tests/ufoLib/testdata/TestFont1 (UFO3).ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>creator</key>
+ <string>org.robofab.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>3</integer>
+ </dict>
+</plist>
diff --git a/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt
new file mode 100644
index 00000000..8449c6b6
--- /dev/null
+++ b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/bar/lol.txt
@@ -0,0 +1 @@
+lol.txt \ No newline at end of file
diff --git a/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt
new file mode 100644
index 00000000..996f1789
--- /dev/null
+++ b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.directory/foo.txt
@@ -0,0 +1 @@
+foo.txt \ No newline at end of file
diff --git a/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt
new file mode 100644
index 00000000..4c330738
--- /dev/null
+++ b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/data/org.unifiedfontobject.file.txt
@@ -0,0 +1 @@
+file.txt \ No newline at end of file
diff --git a/Tests/ufoLib/testdata/UFO3-Read Data.ufo/metainfo.plist b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/metainfo.plist
new file mode 100644
index 00000000..b584ee25
--- /dev/null
+++ b/Tests/ufoLib/testdata/UFO3-Read Data.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?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">
+<plist version="1.0">
+<dict>
+ <key>creator</key>
+ <string>org.robofab.ufoLib</string>
+ <key>formatVersion</key>
+ <integer>3</integer>
+</dict>
+</plist>
diff --git a/Tests/unicodedata_test.py b/Tests/unicodedata_test.py
index 96c0f01d..190576fd 100644
--- a/Tests/unicodedata_test.py
+++ b/Tests/unicodedata_test.py
@@ -165,8 +165,9 @@ def test_script_extension():
assert unicodedata.script_extension("\u0660") == {'Arab', 'Thaa'}
assert unicodedata.script_extension("\u0964") == {
- 'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym',
- 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}
+ 'Beng', 'Deva', 'Dogr', 'Gong', 'Gran', 'Gujr', 'Guru', 'Knda',
+ 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml',
+ 'Telu', 'Tirh'}
def test_script_name():
@@ -200,7 +201,8 @@ def test_block():
assert unicodedata.block("\x00") == "Basic Latin"
assert unicodedata.block("\x7F") == "Basic Latin"
assert unicodedata.block("\x80") == "Latin-1 Supplement"
- assert unicodedata.block("\u1c90") == "No_Block"
+ assert unicodedata.block("\u1c90") == "Georgian Extended"
+ assert unicodedata.block("\u0870") == "No_Block"
def test_ot_tags_from_script():
diff --git a/Tests/varLib/data/Designspace.designspace b/Tests/varLib/data/Designspace.designspace
deleted file mode 100644
index df1036ea..00000000
--- a/Tests/varLib/data/Designspace.designspace
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0"?>
-<designspace format="3">
- <axes>
- <axis default="0.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght">
- <map input="0.0" output="10.0" />
- <map input="401.0" output="66.0" />
- <map input="1000.0" output="990.0" />
- </axis>
- <axis default="250.0" maximum="1000.0" minimum="0.0" name="width" tag="wdth" />
- <axis default="0.0" maximum="100.0" minimum="0.0" name="contrast" tag="cntr">
- <labelname xml:lang="en">Contrast</labelname>
- <labelname xml:lang="de">Kontrast</labelname>
- </axis>
- </axes>
- <sources>
- <source filename="DesignspaceTest-Light.ufo" name="master_1">
- <lib copy="1"/>
- <groups copy="1"/>
- <info copy="1"/>
- <location>
- <dimension name="weight" xvalue="0.0"/>
- </location>
- </source>
- <source filename="DesignspaceTest-Bold.ufo" name="master_2">
- <location>
- <dimension name="weight" xvalue="1.0"/>
- </location>
- </source>
- </sources>
- <instances>
- <instance familyname="DesignspaceTest" filename="instance/DesignspaceTest-Medium.ufo" stylename="Medium">
- <location>
- <dimension name="weight" xvalue="0.5"/>
- </location>
- <info/>
- <kerning/>
- </instance>
- </instances>
-</designspace>
diff --git a/Tests/varLib/data/Designspace2.designspace b/Tests/varLib/data/Designspace2.designspace
deleted file mode 100644
index ac7d403f..00000000
--- a/Tests/varLib/data/Designspace2.designspace
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0"?>
-<designspace format="3">
- <!-- no <axes> element -->
- <sources/><!-- empty <sources> element -->
- <instances>
- <instance/><!-- bare-bones <instance> element -->
- </instances>
-</designspace>
diff --git a/Tests/varLib/data/FeatureVars.designspace b/Tests/varLib/data/FeatureVars.designspace
new file mode 100644
index 00000000..d641ba21
--- /dev/null
+++ b/Tests/varLib/data/FeatureVars.designspace
@@ -0,0 +1,71 @@
+<?xml version='1.0' encoding='utf-8'?>
+<designspace format="3">
+ <axes>
+ <axis default="368.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght" />
+ <axis default="0.0" maximum="100.0" minimum="0.0" name="contrast" tag="cntr">
+ <labelname xml:lang="en">Contrast</labelname>
+ </axis>
+ </axes>
+ <rules>
+ <rule name="dollar-stroke">
+ <conditionset>
+ <condition name="weight" minimum="500" maximum="1000" />
+ </conditionset>
+ <sub name="uni0024" with="uni0024.nostroke" />
+ </rule>
+ <rule name="to-lowercase">
+ <conditionset>
+ <condition name="contrast" minimum="75" maximum="100" />
+ </conditionset>
+ <sub name="uni0041" with="uni0061" />
+ </rule>
+ <rule name="to-uppercase">
+ <conditionset>
+ <condition name="weight" minimum="0" maximum="200" />
+ <condition name="contrast" minimum="0" maximum="25" />
+ </conditionset>
+ <sub name="uni0061" with="uni0041" />
+ </rule>
+ </rules>
+ <sources>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master0.ufo" name="master_0" stylename="Master0">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ <dimension name="contrast" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master1.ufo" name="master_1" stylename="Master1">
+ <lib copy="1" />
+ <groups copy="1" />
+ <info copy="1" />
+ <location>
+ <dimension name="weight" xvalue="368" />
+ <dimension name="contrast" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master2.ufo" name="master_2" stylename="Master2">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ <dimension name="contrast" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master3.ufo" name="master_3" stylename="Master3">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ <dimension name="contrast" xvalue="100" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master0.ufo" name="master_0" stylename="Master0">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ <dimension name="contrast" xvalue="100" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master4.ufo" name="master_4" stylename="Master4">
+ <location>
+ <dimension name="weight" xvalue="368" />
+ <dimension name="contrast" xvalue="100" />
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/test_results/Build3.ttx b/Tests/varLib/data/test_results/Build3.ttx
deleted file mode 100644
index a6ae23ed..00000000
--- a/Tests/varLib/data/test_results/Build3.ttx
+++ /dev/null
@@ -1,725 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.14">
-
- <HVAR>
- <Version value="0x00010000"/>
- <VarStore Format="1">
- <Format value="1"/>
- <VarRegionList>
- <!-- RegionAxisCount=1 -->
- <!-- RegionCount=1 -->
- <Region index="0">
- <VarRegionAxis index="0">
- <StartCoord value="0.0"/>
- <PeakCoord value="1.0"/>
- <EndCoord value="1.0"/>
- </VarRegionAxis>
- </Region>
- </VarRegionList>
- <!-- VarDataCount=1 -->
- <VarData index="0">
- <!-- ItemCount=18 -->
- <NumShorts value="1"/>
- <!-- VarRegionCount=1 -->
- <VarRegionIndex index="0" value="0"/>
- <Item index="0" value="[80]"/>
- <Item index="1" value="[0]"/>
- <Item index="2" value="[64]"/>
- <Item index="3" value="[50]"/>
- <Item index="4" value="[40]"/>
- <Item index="5" value="[108]"/>
- <Item index="6" value="[56]"/>
- <Item index="7" value="[98]"/>
- <Item index="8" value="[206]"/>
- <Item index="9" value="[40]"/>
- <Item index="10" value="[72]"/>
- <Item index="11" value="[50]"/>
- <Item index="12" value="[128]"/>
- <Item index="13" value="[-18]"/>
- <Item index="14" value="[0]"/>
- <Item index="15" value="[0]"/>
- <Item index="16" value="[0]"/>
- <Item index="17" value="[0]"/>
- </VarData>
- </VarStore>
- </HVAR>
-
- <MVAR>
- <Version value="0x00010000"/>
- <Reserved value="0"/>
- <ValueRecordSize value="8"/>
- <!-- ValueRecordCount=2 -->
- <VarStore Format="1">
- <Format value="1"/>
- <VarRegionList>
- <!-- RegionAxisCount=1 -->
- <!-- RegionCount=1 -->
- <Region index="0">
- <VarRegionAxis index="0">
- <StartCoord value="0.0"/>
- <PeakCoord value="1.0"/>
- <EndCoord value="1.0"/>
- </VarRegionAxis>
- </Region>
- </VarRegionList>
- <!-- VarDataCount=1 -->
- <VarData index="0">
- <!-- ItemCount=2 -->
- <NumShorts value="0"/>
- <!-- VarRegionCount=1 -->
- <VarRegionIndex index="0" value="0"/>
- <Item index="0" value="[13]"/>
- <Item index="1" value="[22]"/>
- </VarData>
- </VarStore>
- <ValueRecord index="0">
- <ValueTag value="stro"/>
- <VarIdx value="0"/>
- </ValueRecord>
- <ValueRecord index="1">
- <ValueTag value="xhgt"/>
- <VarIdx value="1"/>
- </ValueRecord>
- </MVAR>
-
- <fvar>
-
- <!-- Weight -->
- <Axis>
- <AxisTag>wght</AxisTag>
- <Flags>0x0</Flags>
- <MinValue>0.0</MinValue>
- <DefaultValue>0.0</DefaultValue>
- <MaxValue>1000.0</MaxValue>
- <AxisNameID>257</AxisNameID>
- </Axis>
-
- <!-- ExtraLight -->
- <!-- PostScript: TestFamily2-ExtraLight -->
- <NamedInstance flags="0x0" postscriptNameID="259" subfamilyNameID="258">
- <coord axis="wght" value="0.0"/>
- </NamedInstance>
-
- <!-- Light -->
- <!-- PostScript: TestFamily2-Light -->
- <NamedInstance flags="0x0" postscriptNameID="261" subfamilyNameID="260">
- <coord axis="wght" value="100.0"/>
- </NamedInstance>
-
- <!-- Regular -->
- <!-- PostScript: TestFamily2-Regular -->
- <NamedInstance flags="0x0" postscriptNameID="263" subfamilyNameID="262">
- <coord axis="wght" value="368.0"/>
- </NamedInstance>
-
- <!-- Semibold -->
- <!-- PostScript: TestFamily2-Semibold -->
- <NamedInstance flags="0x0" postscriptNameID="265" subfamilyNameID="264">
- <coord axis="wght" value="600.0"/>
- </NamedInstance>
-
- <!-- Bold -->
- <!-- PostScript: TestFamily2-Bold -->
- <NamedInstance flags="0x0" postscriptNameID="267" subfamilyNameID="266">
- <coord axis="wght" value="824.0"/>
- </NamedInstance>
-
- <!-- Black -->
- <!-- PostScript: TestFamily2-Black -->
- <NamedInstance flags="0x0" postscriptNameID="269" subfamilyNameID="268">
- <coord axis="wght" value="1000.0"/>
- </NamedInstance>
- </fvar>
-
- <gvar>
- <version value="1"/>
- <reserved value="0"/>
- <glyphVariations glyph=".notdef">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-20" y="0"/>
- <delta pt="1" x="-20" y="0"/>
- <delta pt="2" x="100" y="0"/>
- <delta pt="3" x="100" y="0"/>
- <delta pt="4" x="144" y="72"/>
- <delta pt="5" x="-60" y="72"/>
- <delta pt="6" x="14" y="-48"/>
- <delta pt="7" x="40" y="-58"/>
- <delta pt="8" x="40" y="-58"/>
- <delta pt="9" x="68" y="-48"/>
- <delta pt="10" x="40" y="58"/>
- <delta pt="11" x="40" y="58"/>
- <delta pt="12" x="26" y="62"/>
- <delta pt="13" x="-50" y="-70"/>
- <delta pt="14" x="132" y="-70"/>
- <delta pt="15" x="56" y="62"/>
- <delta pt="16" x="54" y="98"/>
- <delta pt="17" x="-18" y="0"/>
- <delta pt="18" x="54" y="-102"/>
- <delta pt="19" x="28" y="98"/>
- <delta pt="20" x="28" y="-102"/>
- <delta pt="21" x="98" y="0"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="80" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="A">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-20" y="0"/>
- <delta pt="1" x="-58" y="-10"/>
- <delta pt="2" x="122" y="-10"/>
- <delta pt="3" x="84" y="0"/>
- <delta pt="4" x="-64" y="0"/>
- <delta pt="5" x="0" y="-80"/>
- <delta pt="6" x="9" y="-94"/>
- <delta pt="7" x="22" y="-91"/>
- <delta pt="8" x="28" y="-104"/>
- <delta pt="9" x="28" y="-104"/>
- <delta pt="10" x="36" y="-92"/>
- <delta pt="11" x="49" y="-94"/>
- <delta pt="12" x="58" y="-80"/>
- <delta pt="13" x="124" y="0"/>
- <delta pt="14" x="20" y="-98"/>
- <delta pt="15" x="20" y="7"/>
- <delta pt="16" x="45" y="7"/>
- <delta pt="17" x="45" y="-98"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="64" y="0"/>
- <delta pt="20" x="0" y="-10"/>
- <delta pt="21" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="a">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-10" y="0"/>
- <delta pt="1" x="-24" y="0"/>
- <delta pt="2" x="-22" y="31"/>
- <delta pt="3" x="-22" y="25"/>
- <delta pt="4" x="-22" y="23"/>
- <delta pt="5" x="-46" y="29"/>
- <delta pt="6" x="-64" y="26"/>
- <delta pt="7" x="-69" y="-5"/>
- <delta pt="8" x="-58" y="-86"/>
- <delta pt="9" x="-16" y="-86"/>
- <delta pt="10" x="8" y="-86"/>
- <delta pt="11" x="31" y="-67"/>
- <delta pt="12" x="14" y="-70"/>
- <delta pt="13" x="-30" y="18"/>
- <delta pt="14" x="0" y="34"/>
- <delta pt="15" x="16" y="22"/>
- <delta pt="16" x="16" y="22"/>
- <delta pt="17" x="30" y="22"/>
- <delta pt="18" x="78" y="19"/>
- <delta pt="19" x="78" y="-32"/>
- <delta pt="20" x="78" y="0"/>
- <delta pt="21" x="-36" y="0"/>
- <delta pt="22" x="-44" y="-16"/>
- <delta pt="23" x="-46" y="-16"/>
- <delta pt="24" x="-38" y="-13"/>
- <delta pt="25" x="-18" y="0"/>
- <delta pt="26" x="48" y="104"/>
- <delta pt="27" x="25" y="104"/>
- <delta pt="28" x="-30" y="81"/>
- <delta pt="29" x="-64" y="56"/>
- <delta pt="30" x="-64" y="-50"/>
- <delta pt="31" x="32" y="-41"/>
- <delta pt="32" x="110" y="-3"/>
- <delta pt="33" x="110" y="38"/>
- <delta pt="34" x="110" y="77"/>
- <delta pt="35" x="70" y="104"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="50" y="0"/>
- <delta pt="38" x="0" y="22"/>
- <delta pt="39" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="d">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-12" y="0"/>
- <delta pt="1" x="-15" y="0"/>
- <delta pt="2" x="-18" y="12"/>
- <delta pt="3" x="-18" y="12"/>
- <delta pt="4" x="-18" y="19"/>
- <delta pt="5" x="-17" y="22"/>
- <delta pt="6" x="-28" y="22"/>
- <delta pt="7" x="-32" y="22"/>
- <delta pt="8" x="-44" y="26"/>
- <delta pt="9" x="-60" y="32"/>
- <delta pt="10" x="-64" y="14"/>
- <delta pt="11" x="-64" y="-26"/>
- <delta pt="12" x="78" y="-26"/>
- <delta pt="13" x="78" y="0"/>
- <delta pt="14" x="-36" y="0"/>
- <delta pt="15" x="-44" y="-18"/>
- <delta pt="16" x="-46" y="-18"/>
- <delta pt="17" x="-42" y="-14"/>
- <delta pt="18" x="-29" y="0"/>
- <delta pt="19" x="32" y="112"/>
- <delta pt="20" x="10" y="112"/>
- <delta pt="21" x="-38" y="83"/>
- <delta pt="22" x="-64" y="64"/>
- <delta pt="23" x="-64" y="-48"/>
- <delta pt="24" x="-39" y="-70"/>
- <delta pt="25" x="-6" y="-90"/>
- <delta pt="26" x="16" y="-90"/>
- <delta pt="27" x="65" y="-90"/>
- <delta pt="28" x="126" y="-13"/>
- <delta pt="29" x="126" y="14"/>
- <delta pt="30" x="126" y="45"/>
- <delta pt="31" x="79" y="112"/>
- <delta pt="32" x="0" y="0"/>
- <delta pt="33" x="40" y="0"/>
- <delta pt="34" x="0" y="-26"/>
- <delta pt="35" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="f">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-12" y="0"/>
- <delta pt="1" x="-12" y="-90"/>
- <delta pt="2" x="-12" y="-80"/>
- <delta pt="3" x="16" y="-26"/>
- <delta pt="4" x="76" y="-26"/>
- <delta pt="5" x="95" y="-26"/>
- <delta pt="6" x="116" y="-30"/>
- <delta pt="7" x="116" y="-28"/>
- <delta pt="8" x="96" y="-128"/>
- <delta pt="9" x="97" y="-132"/>
- <delta pt="10" x="104" y="-132"/>
- <delta pt="11" x="120" y="-132"/>
- <delta pt="12" x="130" y="-99"/>
- <delta pt="13" x="130" y="-80"/>
- <delta pt="14" x="130" y="0"/>
- <delta pt="15" x="-12" y="-84"/>
- <delta pt="16" x="-12" y="20"/>
- <delta pt="17" x="-2" y="22"/>
- <delta pt="18" x="100" y="22"/>
- <delta pt="19" x="100" y="-84"/>
- <delta pt="20" x="0" y="0"/>
- <delta pt="21" x="108" y="0"/>
- <delta pt="22" x="0" y="-26"/>
- <delta pt="23" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="n">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-38" y="0"/>
- <delta pt="1" x="-38" y="22"/>
- <delta pt="2" x="76" y="22"/>
- <delta pt="3" x="84" y="38"/>
- <delta pt="4" x="86" y="38"/>
- <delta pt="5" x="78" y="28"/>
- <delta pt="6" x="77" y="22"/>
- <delta pt="7" x="78" y="22"/>
- <delta pt="8" x="86" y="22"/>
- <delta pt="9" x="90" y="0"/>
- <delta pt="10" x="90" y="0"/>
- <delta pt="11" x="90" y="0"/>
- <delta pt="12" x="-52" y="0"/>
- <delta pt="13" x="-52" y="-18"/>
- <delta pt="14" x="-52" y="-50"/>
- <delta pt="15" x="-22" y="-96"/>
- <delta pt="16" x="14" y="-96"/>
- <delta pt="17" x="35" y="-96"/>
- <delta pt="18" x="78" y="-68"/>
- <delta pt="19" x="104" y="-38"/>
- <delta pt="20" x="104" y="0"/>
- <delta pt="21" x="0" y="0"/>
- <delta pt="22" x="56" y="0"/>
- <delta pt="23" x="0" y="22"/>
- <delta pt="24" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="t">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="46" y="0"/>
- <delta pt="1" x="14" y="0"/>
- <delta pt="2" x="-26" y="36"/>
- <delta pt="3" x="-26" y="66"/>
- <delta pt="4" x="-26" y="-84"/>
- <delta pt="5" x="-16" y="-84"/>
- <delta pt="6" x="-16" y="20"/>
- <delta pt="7" x="-16" y="22"/>
- <delta pt="8" x="0" y="12"/>
- <delta pt="9" x="116" y="12"/>
- <delta pt="10" x="116" y="22"/>
- <delta pt="11" x="88" y="22"/>
- <delta pt="12" x="88" y="-84"/>
- <delta pt="13" x="116" y="-84"/>
- <delta pt="14" x="116" y="73"/>
- <delta pt="15" x="116" y="78"/>
- <delta pt="16" x="120" y="106"/>
- <delta pt="17" x="92" y="106"/>
- <delta pt="18" x="90" y="106"/>
- <delta pt="19" x="79" y="101"/>
- <delta pt="20" x="74" y="98"/>
- <delta pt="21" x="90" y="0"/>
- <delta pt="22" x="91" y="2"/>
- <delta pt="23" x="75" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="98" y="0"/>
- <delta pt="26" x="0" y="12"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="f_t">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-12" y="0"/>
- <delta pt="1" x="-12" y="-90"/>
- <delta pt="2" x="-12" y="-80"/>
- <delta pt="3" x="16" y="-26"/>
- <delta pt="4" x="76" y="-26"/>
- <delta pt="5" x="95" y="-26"/>
- <delta pt="6" x="116" y="-30"/>
- <delta pt="7" x="116" y="-28"/>
- <delta pt="8" x="96" y="-128"/>
- <delta pt="9" x="97" y="-132"/>
- <delta pt="10" x="104" y="-132"/>
- <delta pt="11" x="120" y="-132"/>
- <delta pt="12" x="130" y="-99"/>
- <delta pt="13" x="130" y="-80"/>
- <delta pt="14" x="130" y="0"/>
- <delta pt="15" x="154" y="0"/>
- <delta pt="16" x="122" y="0"/>
- <delta pt="17" x="82" y="36"/>
- <delta pt="18" x="82" y="66"/>
- <delta pt="19" x="82" y="-84"/>
- <delta pt="20" x="-12" y="-84"/>
- <delta pt="21" x="-12" y="20"/>
- <delta pt="22" x="-2" y="22"/>
- <delta pt="23" x="92" y="22"/>
- <delta pt="24" x="108" y="12"/>
- <delta pt="25" x="224" y="12"/>
- <delta pt="26" x="224" y="22"/>
- <delta pt="27" x="196" y="22"/>
- <delta pt="28" x="196" y="-84"/>
- <delta pt="29" x="224" y="-84"/>
- <delta pt="30" x="224" y="73"/>
- <delta pt="31" x="224" y="78"/>
- <delta pt="32" x="228" y="106"/>
- <delta pt="33" x="200" y="106"/>
- <delta pt="34" x="198" y="106"/>
- <delta pt="35" x="187" y="101"/>
- <delta pt="36" x="182" y="98"/>
- <delta pt="37" x="198" y="0"/>
- <delta pt="38" x="199" y="2"/>
- <delta pt="39" x="183" y="0"/>
- <delta pt="40" x="0" y="0"/>
- <delta pt="41" x="206" y="0"/>
- <delta pt="42" x="0" y="-26"/>
- <delta pt="43" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="a.alt">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-12" y="0"/>
- <delta pt="1" x="-15" y="0"/>
- <delta pt="2" x="-18" y="12"/>
- <delta pt="3" x="-18" y="12"/>
- <delta pt="4" x="-18" y="19"/>
- <delta pt="5" x="-13" y="22"/>
- <delta pt="6" x="-24" y="22"/>
- <delta pt="7" x="-32" y="22"/>
- <delta pt="8" x="-36" y="24"/>
- <delta pt="9" x="-42" y="18"/>
- <delta pt="10" x="-40" y="18"/>
- <delta pt="11" x="-28" y="22"/>
- <delta pt="12" x="78" y="22"/>
- <delta pt="13" x="78" y="0"/>
- <delta pt="14" x="-36" y="0"/>
- <delta pt="15" x="-44" y="-18"/>
- <delta pt="16" x="-46" y="-18"/>
- <delta pt="17" x="-42" y="-14"/>
- <delta pt="18" x="-29" y="0"/>
- <delta pt="19" x="32" y="112"/>
- <delta pt="20" x="10" y="112"/>
- <delta pt="21" x="-38" y="83"/>
- <delta pt="22" x="-64" y="64"/>
- <delta pt="23" x="-64" y="-48"/>
- <delta pt="24" x="-39" y="-70"/>
- <delta pt="25" x="-6" y="-90"/>
- <delta pt="26" x="16" y="-90"/>
- <delta pt="27" x="65" y="-90"/>
- <delta pt="28" x="126" y="-13"/>
- <delta pt="29" x="126" y="14"/>
- <delta pt="30" x="126" y="45"/>
- <delta pt="31" x="79" y="112"/>
- <delta pt="32" x="0" y="0"/>
- <delta pt="33" x="40" y="0"/>
- <delta pt="34" x="0" y="22"/>
- <delta pt="35" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="A.sc">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-20" y="0"/>
- <delta pt="1" x="-52" y="22"/>
- <delta pt="2" x="125" y="22"/>
- <delta pt="3" x="92" y="0"/>
- <delta pt="4" x="-54" y="0"/>
- <delta pt="5" x="5" y="-60"/>
- <delta pt="6" x="14" y="-71"/>
- <delta pt="7" x="26" y="-58"/>
- <delta pt="8" x="32" y="-66"/>
- <delta pt="9" x="32" y="-66"/>
- <delta pt="10" x="40" y="-58"/>
- <delta pt="11" x="52" y="-70"/>
- <delta pt="12" x="61" y="-60"/>
- <delta pt="13" x="122" y="0"/>
- <delta pt="14" x="21" y="-82"/>
- <delta pt="15" x="21" y="12"/>
- <delta pt="16" x="52" y="12"/>
- <delta pt="17" x="52" y="-82"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="72" y="0"/>
- <delta pt="20" x="0" y="22"/>
- <delta pt="21" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="atilde">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="0" y="0"/>
- <delta pt="1" x="24" y="0"/>
- <delta pt="2" x="0" y="0"/>
- <delta pt="3" x="50" y="0"/>
- <delta pt="4" x="0" y="40"/>
- <delta pt="5" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="ampersand">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="22" y="0"/>
- <delta pt="1" x="-16" y="0"/>
- <delta pt="2" x="-16" y="18"/>
- <delta pt="3" x="-16" y="10"/>
- <delta pt="4" x="-16" y="22"/>
- <delta pt="5" x="-14" y="24"/>
- <delta pt="6" x="-11" y="9"/>
- <delta pt="7" x="-8" y="-13"/>
- <delta pt="8" x="-6" y="-37"/>
- <delta pt="9" x="-6" y="-46"/>
- <delta pt="10" x="-6" y="-58"/>
- <delta pt="11" x="9" y="-94"/>
- <delta pt="12" x="32" y="-94"/>
- <delta pt="13" x="58" y="-94"/>
- <delta pt="14" x="79" y="-58"/>
- <delta pt="15" x="79" y="-44"/>
- <delta pt="16" x="79" y="-23"/>
- <delta pt="17" x="87" y="22"/>
- <delta pt="18" x="100" y="62"/>
- <delta pt="19" x="108" y="77"/>
- <delta pt="20" x="111" y="87"/>
- <delta pt="21" x="120" y="102"/>
- <delta pt="22" x="120" y="110"/>
- <delta pt="23" x="92" y="0"/>
- <delta pt="24" x="72" y="-3"/>
- <delta pt="25" x="36" y="-4"/>
- <delta pt="26" x="23" y="-7"/>
- <delta pt="27" x="2" y="-11"/>
- <delta pt="28" x="-23" y="-20"/>
- <delta pt="29" x="-32" y="-33"/>
- <delta pt="30" x="-32" y="-42"/>
- <delta pt="31" x="-32" y="-35"/>
- <delta pt="32" x="-4" y="-10"/>
- <delta pt="33" x="26" y="-10"/>
- <delta pt="34" x="53" y="-10"/>
- <delta pt="35" x="94" y="-28"/>
- <delta pt="36" x="94" y="-48"/>
- <delta pt="37" x="94" y="-52"/>
- <delta pt="38" x="100" y="-48"/>
- <delta pt="39" x="108" y="-31"/>
- <delta pt="40" x="114" y="-3"/>
- <delta pt="41" x="114" y="18"/>
- <delta pt="42" x="114" y="57"/>
- <delta pt="43" x="66" y="102"/>
- <delta pt="44" x="42" y="102"/>
- <delta pt="45" x="28" y="102"/>
- <delta pt="46" x="11" y="88"/>
- <delta pt="47" x="10" y="78"/>
- <delta pt="48" x="6" y="66"/>
- <delta pt="49" x="8" y="40"/>
- <delta pt="50" x="4" y="32"/>
- <delta pt="51" x="130" y="32"/>
- <delta pt="52" x="129" y="32"/>
- <delta pt="53" x="118" y="30"/>
- <delta pt="54" x="106" y="20"/>
- <delta pt="55" x="96" y="10"/>
- <delta pt="56" x="51" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="128" y="0"/>
- <delta pt="59" x="0" y="-10"/>
- <delta pt="60" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni25CC">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-6" y="-1"/>
- <delta pt="1" x="-10" y="-1"/>
- <delta pt="2" x="-20" y="4"/>
- <delta pt="3" x="-20" y="12"/>
- <delta pt="4" x="-20" y="18"/>
- <delta pt="5" x="-10" y="26"/>
- <delta pt="6" x="-6" y="26"/>
- <delta pt="9" x="8" y="-1"/>
- <delta pt="10" x="-6" y="-3"/>
- <delta pt="12" x="-22" y="4"/>
- <delta pt="13" x="-22" y="12"/>
- <delta pt="14" x="-22" y="17"/>
- <delta pt="16" x="-6" y="25"/>
- <delta pt="18" x="8" y="12"/>
- <delta pt="20" x="-6" y="-5"/>
- <delta pt="21" x="-10" y="-5"/>
- <delta pt="22" x="-20" y="3"/>
- <delta pt="23" x="-20" y="9"/>
- <delta pt="25" x="-10" y="23"/>
- <delta pt="26" x="-6" y="23"/>
- <delta pt="29" x="8" y="-5"/>
- <delta pt="31" x="-13" y="-1"/>
- <delta pt="33" x="-23" y="12"/>
- <delta pt="35" x="-13" y="27"/>
- <delta pt="36" x="-7" y="27"/>
- <delta pt="37" x="-2" y="27"/>
- <delta pt="39" x="8" y="12"/>
- <delta pt="42" x="-13" y="-5"/>
- <delta pt="43" x="-23" y="2"/>
- <delta pt="44" x="-23" y="9"/>
- <delta pt="45" x="-23" y="14"/>
- <delta pt="46" x="-13" y="23"/>
- <delta pt="47" x="-7" y="23"/>
- <delta pt="48" x="-2" y="23"/>
- <delta pt="49" x="8" y="14"/>
- <delta pt="50" x="8" y="9"/>
- <delta pt="53" x="-13" y="-1"/>
- <delta pt="54" x="-22" y="8"/>
- <delta pt="56" x="-22" y="20"/>
- <delta pt="57" x="-13" y="27"/>
- <delta pt="60" x="6" y="14"/>
- <delta pt="63" x="-13" y="-5"/>
- <delta pt="65" x="-22" y="10"/>
- <delta pt="67" x="-13" y="22"/>
- <delta pt="70" x="6" y="10"/>
- <delta pt="73" x="-15" y="-1"/>
- <delta pt="75" x="-25" y="12"/>
- <delta pt="78" x="-9" y="27"/>
- <delta pt="79" x="-3" y="27"/>
- <delta pt="81" x="7" y="12"/>
- <delta pt="84" x="-15" y="-5"/>
- <delta pt="85" x="-25" y="1"/>
- <delta pt="86" x="-25" y="9"/>
- <delta pt="87" x="-25" y="15"/>
- <delta pt="89" x="-9" y="24"/>
- <delta pt="90" x="-3" y="24"/>
- <delta pt="91" x="7" y="15"/>
- <delta pt="92" x="7" y="9"/>
- <delta pt="94" x="-8" y="-1"/>
- <delta pt="95" x="-16" y="-1"/>
- <delta pt="96" x="-25" y="4"/>
- <delta pt="97" x="-25" y="12"/>
- <delta pt="98" x="-25" y="18"/>
- <delta pt="99" x="-16" y="26"/>
- <delta pt="100" x="-8" y="26"/>
- <delta pt="103" x="6" y="-1"/>
- <delta pt="105" x="-25" y="-3"/>
- <delta pt="106" x="-25" y="12"/>
- <delta pt="108" x="-10" y="25"/>
- <delta pt="109" x="-3" y="25"/>
- <delta pt="110" x="5" y="17"/>
- <delta pt="111" x="5" y="12"/>
- <delta pt="113" x="-8" y="-4"/>
- <delta pt="114" x="-16" y="-4"/>
- <delta pt="116" x="-25" y="10"/>
- <delta pt="118" x="-16" y="24"/>
- <delta pt="119" x="-8" y="24"/>
- <delta pt="122" x="6" y="-4"/>
- <delta pt="124" x="-18" y="0"/>
- <delta pt="125" x="0" y="22"/>
- <delta pt="126" x="0" y="1"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni0303">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-13" y="-8"/>
- <delta pt="1" x="-4" y="-8"/>
- <delta pt="2" x="-4" y="-30"/>
- <delta pt="3" x="2" y="-52"/>
- <delta pt="4" x="18" y="-52"/>
- <delta pt="5" x="34" y="-52"/>
- <delta pt="6" x="45" y="-13"/>
- <delta pt="7" x="44" y="0"/>
- <delta pt="8" x="-36" y="4"/>
- <delta pt="9" x="-37" y="46"/>
- <delta pt="10" x="0" y="40"/>
- <delta pt="11" x="12" y="40"/>
- <delta pt="12" x="4" y="40"/>
- <delta pt="13" x="3" y="62"/>
- <delta pt="14" x="-3" y="84"/>
- <delta pt="15" x="-19" y="84"/>
- <delta pt="16" x="-35" y="84"/>
- <delta pt="17" x="-45" y="44"/>
- <delta pt="18" x="-44" y="32"/>
- <delta pt="19" x="36" y="28"/>
- <delta pt="20" x="37" y="-15"/>
- <delta pt="21" x="0" y="-8"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="40"/>
- <delta pt="25" x="0" y="8"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni0308">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="1" x="-49" y="-40"/>
- <delta pt="2" x="-76" y="-12"/>
- <delta pt="4" x="-76" y="28"/>
- <delta pt="7" x="-7" y="56"/>
- <delta pt="8" x="20" y="28"/>
- <delta pt="10" x="20" y="-12"/>
- <delta pt="13" x="7" y="-40"/>
- <delta pt="14" x="-20" y="-12"/>
- <delta pt="16" x="-20" y="28"/>
- <delta pt="19" x="49" y="56"/>
- <delta pt="20" x="76" y="28"/>
- <delta pt="22" x="76" y="-12"/>
- <delta pt="26" x="0" y="56"/>
- <delta pt="27" x="0" y="40"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni0330">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="0" y="0"/>
- <delta pt="1" x="0" y="0"/>
- <delta pt="2" x="0" y="0"/>
- <delta pt="3" x="0" y="40"/>
- <delta pt="4" x="0" y="8"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni0324">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="0" y="4"/>
- <delta pt="1" x="0" y="0"/>
- <delta pt="2" x="0" y="0"/>
- <delta pt="3" x="0" y="60"/>
- <delta pt="4" x="0" y="36"/>
- </tuple>
- </glyphVariations>
- </gvar>
-
-</ttFont>
diff --git a/Tests/varLib/data/test_results/FeatureVars.ttx b/Tests/varLib/data/test_results/FeatureVars.ttx
new file mode 100644
index 00000000..0764bb84
--- /dev/null
+++ b/Tests/varLib/data/test_results/FeatureVars.ttx
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.29">
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>368.0</DefaultValue>
+ <MaxValue>1000.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- Contrast -->
+ <Axis>
+ <AxisTag>cntr</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>100.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+ </fvar>
+
+ <GSUB>
+ <Version value="0x00010001"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="rvrn"/>
+ <Feature>
+ <!-- LookupCount=0 -->
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=3 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0024" out="uni0024.nostroke"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0041" out="uni0061"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0061" out="uni0041"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ <FeatureVariations>
+ <Version value="0x00010000"/>
+ <!-- FeatureVariationCount=4 -->
+ <FeatureVariationRecord index="0">
+ <ConditionSet>
+ <!-- ConditionCount=2 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.75"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ <ConditionTable index="1" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="0.20886"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010001"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=2 -->
+ <LookupListIndex index="0" value="0"/>
+ <LookupListIndex index="1" value="1"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ <FeatureVariationRecord index="1">
+ <ConditionSet>
+ <!-- ConditionCount=1 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="0.20886"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010001"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ <FeatureVariationRecord index="2">
+ <ConditionSet>
+ <!-- ConditionCount=1 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.75"/>
+ <FilterRangeMaxValue value="1.0"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010001"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ <FeatureVariationRecord index="3">
+ <ConditionSet>
+ <!-- ConditionCount=2 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="1"/>
+ <FilterRangeMinValue value="0.0"/>
+ <FilterRangeMaxValue value="0.25"/>
+ </ConditionTable>
+ <ConditionTable index="1" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="-1.0"/>
+ <FilterRangeMaxValue value="-0.45654"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010001"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="2"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ </FeatureVariations>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/varLib/designspace_test.py b/Tests/varLib/designspace_test.py
deleted file mode 100644
index fbdaab37..00000000
--- a/Tests/varLib/designspace_test.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from __future__ import print_function, division, absolute_import
-from __future__ import unicode_literals
-from fontTools.varLib import designspace
-import os
-import unittest
-
-
-class DesignspaceTest(unittest.TestCase):
- def test_load(self):
- self.maxDiff = None
- self.assertEqual(
- designspace.load(_getpath("Designspace.designspace")),
-
- {'sources':
- [{'location': {'weight': 0.0},
- 'groups': {'copy': True},
- 'filename': 'DesignspaceTest-Light.ufo',
- 'info': {'copy': True},
- 'name': 'master_1',
- 'lib': {'copy': True}},
- {'location': {'weight': 1.0},
- 'name': 'master_2',
- 'filename': 'DesignspaceTest-Bold.ufo'}],
-
- 'instances':
- [{'location': {'weight': 0.5},
- 'familyname': 'DesignspaceTest',
- 'filename': 'instance/DesignspaceTest-Medium.ufo',
- 'kerning': {},
- 'info': {},
- 'stylename': 'Medium'}],
-
- 'axes':
- [{'name': 'weight',
- 'map': [{'input': 0.0, 'output': 10.0},
- {'input': 401.0, 'output': 66.0},
- {'input': 1000.0, 'output': 990.0}],
- 'tag': 'wght',
- 'maximum': 1000.0,
- 'minimum': 0.0,
- 'default': 0.0},
- {'maximum': 1000.0,
- 'default': 250.0,
- 'minimum': 0.0,
- 'name': 'width',
- 'tag': 'wdth'},
- {'name': 'contrast',
- 'tag': 'cntr',
- 'maximum': 100.0,
- 'minimum': 0.0,
- 'default': 0.0,
- 'labelname': {'de': 'Kontrast', 'en': 'Contrast'}}]
- }
- )
-
- def test_load2(self):
- self.assertEqual(
- designspace.load(_getpath("Designspace2.designspace")),
- {'sources': [], 'instances': [{}]})
-
-
-def _getpath(testfile):
- path, _ = os.path.split(__file__)
- return os.path.join(path, "data", testfile)
-
-
-if __name__ == "__main__":
- import sys
- sys.exit(unittest.main())
diff --git a/Tests/varLib/interpolate_layout_test.py b/Tests/varLib/interpolate_layout_test.py
index 6f6efe06..00039295 100644
--- a/Tests/varLib/interpolate_layout_test.py
+++ b/Tests/varLib/interpolate_layout_test.py
@@ -4,6 +4,7 @@ from fontTools.ttLib import TTFont
from fontTools.varLib import build
from fontTools.varLib.interpolate_layout import interpolate_layout
from fontTools.varLib.interpolate_layout import main as interpolate_layout_main
+from fontTools.designspaceLib import DesignSpaceDocumentError
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
import difflib
import os
@@ -157,24 +158,9 @@ class InterpolateLayoutTest(unittest.TestCase):
The variable font will inherit the GSUB table from the
base master.
"""
- suffix = '.ttf'
ds_path = self.get_test_input('InterpolateLayout3.designspace')
- ufo_dir = self.get_test_input('master_ufo')
- ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf')
-
- self.temp_dir()
- ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-')
- for path in ttx_paths:
- self.compile_font(path, suffix, self.tempdir)
-
- finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix)
- instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
-
- tables = ['GSUB']
- expected_ttx_path = self.get_test_output('InterpolateLayout.ttx')
- self.expect_ttx(instfont, expected_ttx_path, tables)
- self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
-
+ with self.assertRaisesRegex(DesignSpaceDocumentError, "No axes defined"):
+ instfont = interpolate_layout(ds_path, {'weight': 500})
def test_varlib_interpolate_layout_GPOS_only_size_feat_same_val_ttf(self):
"""Only GPOS; 'size' feature; same values in all masters.
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
index 12c4874d..2bfe0f2d 100644
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -3,6 +3,7 @@ from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont
from fontTools.varLib import build
from fontTools.varLib import main as varLib_main
+from fontTools.designspaceLib import DesignSpaceDocumentError
import difflib
import os
import shutil
@@ -94,7 +95,7 @@ class BuildTest(unittest.TestCase):
return font, savepath
def _run_varlib_build_test(self, designspace_name, font_name, tables,
- expected_ttx_name):
+ expected_ttx_name, save_before_dump=False):
suffix = '.ttf'
ds_path = self.get_test_input(designspace_name + '.designspace')
ufo_dir = self.get_test_input('master_ufo')
@@ -108,6 +109,15 @@ class BuildTest(unittest.TestCase):
finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix)
varfont, model, _ = build(ds_path, finder)
+ if save_before_dump:
+ # some data (e.g. counts printed in TTX inline comments) is only
+ # calculated at compile time, so before we can compare the TTX
+ # dumps we need to save to a temporary stream, and realod the font
+ buf = BytesIO()
+ varfont.save(buf)
+ buf.seek(0)
+ varfont = TTFont(buf)
+
expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
self.expect_ttx(varfont, expected_ttx_path, tables)
self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
@@ -126,12 +136,9 @@ class BuildTest(unittest.TestCase):
def test_varlib_build_no_axes_ttf(self):
"""Designspace file does not contain an <axes> element."""
- self._run_varlib_build_test(
- designspace_name='InterpolateLayout3',
- font_name='TestFamily2',
- tables=['GDEF', 'HVAR', 'MVAR', 'fvar', 'gvar'],
- expected_ttx_name='Build3'
- )
+ ds_path = self.get_test_input('InterpolateLayout3.designspace')
+ with self.assertRaisesRegex(DesignSpaceDocumentError, "No axes defined"):
+ build(ds_path)
def test_varlib_avar_single_axis(self):
"""Designspace file contains a 'weight' axis with <map> elements
@@ -185,6 +192,18 @@ class BuildTest(unittest.TestCase):
expected_ttx_name=test_name
)
+ def test_varlib_build_feature_variations(self):
+ """Designspace file contains <rules> element, used to build
+ GSUB FeatureVariations table.
+ """
+ self._run_varlib_build_test(
+ designspace_name="FeatureVars",
+ font_name="TestFamily",
+ tables=["fvar", "GSUB"],
+ expected_ttx_name="FeatureVars",
+ save_before_dump=True,
+ )
+
def test_varlib_main_ttf(self):
"""Mostly for testing varLib.main()
"""
diff --git a/requirements.txt b/requirements.txt
index 5592626d..0e5f5bab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,8 @@
# extension 'brotlipy' on PyPy
brotli==1.0.1; platform_python_implementation != "PyPy"
brotlipy==0.7.0; platform_python_implementation == "PyPy"
-unicodedata2==10.0.0; python_version < '3.7' and platform_python_implementation != "PyPy"
-munkres==1.0.10
+unicodedata2==11.0.0; python_version < '3.7' and platform_python_implementation != "PyPy"
+scipy==1.1.0; platform_python_implementation != "PyPy"
+munkres==1.0.12; platform_python_implementation == "PyPy"
zopfli==0.1.4
+fs==2.1.1
diff --git a/run-tests.sh b/run-tests.sh
index 0eb84de0..f10c1b01 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -5,13 +5,13 @@ set -e
# Choose python version
if test "x$1" = x-3; then
- PYTHON=python3
+ PYTHON=py3
shift
elif test "x$1" = x-2; then
- PYTHON=python2
+ PYTHON=py2
shift
fi
-test "x$PYTHON" = x && PYTHON=python
+test "x$PYTHON" = x && PYTHON=py
# Find tests
FILTERS=
@@ -22,7 +22,7 @@ done
# Run tests
if [ -z "$FILTERS" ]; then
- $PYTHON setup.py test
+ tox --develop -e $PYTHON
else
- $PYTHON setup.py test --addopts="-k \"$FILTERS\""
+ tox --develop -e $PYTHON -- -k "$FILTERS"
fi
diff --git a/setup.cfg b/setup.cfg
index 232b34a6..0d36f4c8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 3.28.0
+current_version = 3.31.0
commit = True
tag = False
tag_name = {new_version}
@@ -30,9 +30,6 @@ universal = 1
[sdist]
formats = zip
-[aliases]
-test = pytest
-
[metadata]
license_file = LICENSE
@@ -50,6 +47,13 @@ addopts =
--doctest-modules
--doctest-ignore-import-errors
--pyargs
+filterwarnings =
+ ignore:tostring:DeprecationWarning
+ ignore:fromstring:DeprecationWarning
+ ignore:readPlist:DeprecationWarning:plistlib_test
+ ignore:writePlist:DeprecationWarning:plistlib_test
+ ignore:some_function:DeprecationWarning:fontTools.ufoLib.utils
+ ignore::DeprecationWarning:fontTools.varLib.designspace
[egg_info]
tag_build =
diff --git a/setup.py b/setup.py
index c843da0b..e975f63d 100755
--- a/setup.py
+++ b/setup.py
@@ -23,13 +23,61 @@ def doraise_py_compile(file, cfile=None, dfile=None, doraise=False):
py_compile.compile = doraise_py_compile
-needs_pytest = {'pytest', 'test'}.intersection(sys.argv)
-pytest_runner = ['pytest_runner'] if needs_pytest else []
needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
wheel = ['wheel'] if needs_wheel else []
needs_bumpversion = {'release'}.intersection(sys.argv)
bumpversion = ['bump2version'] if needs_bumpversion else []
+extras_require = {
+ # for fontTools.ufoLib: to read/write UFO fonts
+ "ufo": [
+ "fs >= 2.1.1, < 3",
+ "enum34 >= 1.1.6; python_version < '3.4'",
+ ],
+ # for fontTools.misc.etree and fontTools.misc.plistlib: use lxml to
+ # read/write XML files (faster/safer than built-in ElementTree)
+ "lxml": [
+ "lxml >= 4.0, < 5",
+ "singledispatch >= 3.4.0.3; python_version < '3.4'",
+ ],
+ # for fontTools.sfnt and fontTools.woff2: to compress/uncompress
+ # WOFF 1.0 and WOFF 2.0 webfonts.
+ "woff": [
+ "brotli >= 1.0.1; platform_python_implementation != 'PyPy'",
+ "brotlipy >= 0.7.0; platform_python_implementation == 'PyPy'",
+ "zopfli >= 0.1.4",
+ ],
+ # for fontTools.unicode and fontTools.unicodedata: to use the latest version
+ # of the Unicode Character Database instead of the built-in unicodedata
+ # which varies between python versions and may be outdated.
+ "unicode": [
+ # the unicodedata2 extension module doesn't work on PyPy.
+ # Python 3.7 already has Unicode 11, so the backport is not needed.
+ (
+ "unicodedata2 >= 11.0.0; "
+ "python_version < '3.7' and platform_python_implementation != 'PyPy'"
+ ),
+ ],
+ # for fontTools.interpolatable: to solve the "minimum weight perfect
+ # matching problem in bipartite graphs" (aka Assignment problem)
+ "interpolatable": [
+ # use pure-python alternative on pypy
+ "scipy; platform_python_implementation != 'PyPy'",
+ "munkres; platform_python_implementation == 'PyPy'",
+ ],
+ # for fontTools.misc.symfont, module for symbolic font statistics analysis
+ "symfont": [
+ "sympy",
+ ],
+ # To get file creator and type of Macintosh PostScript Type 1 fonts (macOS only)
+ "type1": [
+ "xattr; sys_platform == 'darwin'",
+ ],
+}
+# use a special 'all' key as shorthand to includes all the extra dependencies
+extras_require["all"] = sum(extras_require.values(), [])
+
+
# Trove classifiers for PyPI
classifiers = {"classifiers": [
"Development Status :: 5 - Production/Stable",
@@ -244,26 +292,6 @@ class release(Command):
return u"".join(changes)
-class PassCommand(Command):
- """ This is used with Travis `dpl` tool so that it skips creating sdist
- and wheel packages, but simply uploads to PyPI the files found in ./dist
- folder, that were previously built inside the tox 'bdist' environment.
- This ensures that the same files are uploaded to Github Releases and PyPI.
- """
-
- description = "do nothing"
- user_options = []
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
-
- def run(self):
- pass
-
-
def find_data_files(manpath="share/man"):
""" Find FontTools's data_files (just man pages at this point).
@@ -309,7 +337,7 @@ def find_data_files(manpath="share/man"):
setup(
name="fonttools",
- version="3.28.0",
+ version="3.31.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",
@@ -323,10 +351,8 @@ setup(
packages=find_packages("Lib"),
include_package_data=True,
data_files=find_data_files(),
- setup_requires=pytest_runner + wheel + bumpversion,
- tests_require=[
- 'pytest>=3.0',
- ],
+ setup_requires=wheel + bumpversion,
+ extras_require=extras_require,
entry_points={
'console_scripts': [
"fonttools = fontTools.__main__:main",
@@ -338,7 +364,6 @@ setup(
},
cmdclass={
"release": release,
- 'pass': PassCommand,
},
**classifiers
)
diff --git a/tox.ini b/tox.ini
index cb1ab915..63619e3d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,29 +1,24 @@
[tox]
-envlist = py{27,36}-cov, htmlcov
+minversion = 3.0
+envlist = py{27,37}-cov, htmlcov
[testenv]
-basepython =
- py27: {env:TOXPYTHON:python2.7}
- pypy: {env:TOXPYTHON:pypy}
- py34: {env:TOXPYTHON:python3.4}
- py35: {env:TOXPYTHON:python3.5}
- py36: {env:TOXPYTHON:python3.6}
deps =
cov: coverage>=4.3
pytest
-rrequirements.txt
-install_command =
- pip install -v {opts} {packages}
+extras =
+ ufo
+ woff
+ unicode
+ interpolatable
+ !nolxml: lxml
commands =
- # run the test suite against the package installed inside tox env.
- # We use parallel mode and then combine later so that coverage.py will take
- # paths like .tox/py36/lib/python3.6/site-packages/fontTools and collapse
- # them into Lib/fontTools.
+ # test with or without coverage, passing extra positonal args to pytest
cov: coverage run --parallel-mode -m pytest {posargs}
- nocov: pytest {posargs}
+ !cov: pytest {posargs}
[testenv:htmlcov]
-basepython = {env:TOXPYTHON:python3.6}
deps =
coverage>=4.3
skip_install = true
@@ -33,7 +28,6 @@ commands =
[testenv:codecov]
passenv = *
-basepython = {env:TOXPYTHON:python}
deps =
coverage>=4.3
codecov
@@ -44,7 +38,6 @@ commands =
codecov --env TOXENV
[testenv:bdist]
-basepython = {env:TOXPYTHON:python3.6}
deps =
pygments
docutils
@@ -60,9 +53,19 @@ commands =
# check metadata and rst long_description
python setup.py check --restructuredtext --strict
# clean up build/ and dist/ folders
- rm -rf {toxinidir}/dist
+ python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)'
python setup.py clean --all
# build sdist
python setup.py sdist --dist-dir {toxinidir}/dist
# build wheel from sdist
pip wheel -v --no-deps --no-index --wheel-dir {toxinidir}/dist --find-links {toxinidir}/dist fonttools
+
+[testenv:pypi]
+deps =
+ {[testenv:bdist]deps}
+ twine
+skip_install = true
+passenv = TWINE_USERNAME TWINE_PASSWORD
+commands =
+ {[testenv:bdist]commands}
+ twine upload dist/*.whl dist/*.zip