Major overhaul, moving from UFO2 to Glyphs and UFO3, plus a brand new and much simpler fontbuild

This commit is contained in:
Rasmus Andersson 2018-09-03 12:55:49 -07:00
parent 8c1a4c181e
commit c833e252c9
5246 changed files with 346953 additions and 163499 deletions

1
.gitignore vendored
View file

@ -11,7 +11,6 @@
_*.ignore
*~
.DS_Store
*.sparseimage
nohup.out
build

119
Makefile
View file

@ -10,50 +10,18 @@
# install Build all (web, ttf and otf) and install. Mac-only for now.
# dist Create a new release distribution. Does everything.
#
all: all_ttf all_otf
$(MAKE) all_web -j
all: all_fonts
all_unhinted: all_ttf all_otf all_web
all_hinted: all_ttf_hinted all_web_hinted
all_hinted: all_ttf all_ttf_hinted all_otf
$(MAKE) all_web_hinted -j
VERSION := $(shell cat version.txt)
VERSION := $(shell misc/version.py)
export PATH := $(PWD)/build/venv/bin:$(PATH)
# generated.make is automatically generated by init.sh and defines depenencies for
# all styles and alias targets
include build/etc/generated.make
res_files := src/fontbuild.cfg src/diacritics.txt src/glyphlist.txt \
src/features.fea src/glyphorder.txt
# UFO -> TTF & OTF (note that UFO deps are defined by generated.make)
build/tmp/InterUITTF/InterUI-%.ttf: $(res_files)
misc/ufocompile --otf $*
build/tmp/InterUIOTF/InterUI-%.otf: build/tmp/InterUITTF/InterUI-%.ttf $(res_files)
@true
# tmp/ttf -> dist
build/dist-unhinted/Inter-UI-%.ttf: build/tmp/InterUITTF/InterUI-%.ttf
@mkdir -p build/dist-unhinted
cp -a "$<" "$@"
# tmp/otf -> dist
build/dist-unhinted/Inter-UI-%.otf: build/tmp/InterUIOTF/InterUI-%.otf
@mkdir -p build/dist-unhinted
cp -a "$<" "$@"
# autohint
build/dist-hinted/Inter-UI-%.ttf: build/dist-unhinted/Inter-UI-%.ttf
@mkdir -p build/dist-hinted
ttfautohint \
--hinting-limit=256 \
--hinting-range-min=8 \
--hinting-range-max=64 \
--fallback-stem-width=256 \
--strong-stem-width=D \
--no-info \
--verbose \
"$<" "$@"
# TTF -> WOFF2
build/%.woff2: build/%.ttf
@ -67,14 +35,63 @@ build/%.woff: build/%.ttf
# build/%.eot: build/%.ttf
# ttf2eot "$<" > "$@"
test: all_otf
@misc/check-font.py build/dist-unhinted/*.otf
# UFO -> OTF, TTF
build/unhinted/Inter-UI-Regular.%: master_ufo_regular
misc/fontbuild compile -o $@ src/Inter-UI-Regular.ufo
build/unhinted/Inter-UI-Black.%: master_ufo_black
misc/fontbuild compile -o $@ src/Inter-UI-Black.ufo
build/unhinted/Inter-UI-%.otf: build/ufo/Inter-UI-%.ufo
misc/fontbuild compile -o $@ $<
build/unhinted/Inter-UI-%.ttf: build/ufo/Inter-UI-%.ufo
misc/fontbuild compile -o $@ $<
# designspace <- glyphs file
src/Inter-UI.designspace: src/Inter-UI.glyphs
misc/fontbuild glyphsync $<
# instance UFOs <- master UFOs
build/ufo/Inter-UI-%.ufo: master_ufo_regular master_ufo_black
misc/fontbuild instancegen src/Inter-UI.designspace $*
# master UFOs <- designspace
master_ufo_regular: src/Inter-UI.designspace $(Regular_ufo_d)
master_ufo_black: src/Inter-UI.designspace $(Black_ufo_d)
# Note: The seemingly convoluted dependency graph above is required to
# make sure that glyphsync and instancegen are not run in parallel.
.PHONY: master_ufo_regular master_ufo_black
# hinted TTF files via autohint
build/hinted/%.ttf: build/unhinted/%.ttf
@mkdir -p build/hinted
@echo ttfautohint "$<" "$@"
@ttfautohint \
--hinting-limit=256 \
--hinting-range-min=8 \
--hinting-range-max=64 \
--fallback-stem-width=256 \
--no-info \
--verbose \
"$<" "$@"
# test runs all tests
# Note: all_check is generated by init.sh and runs "fontbuild checkfont"
# on all otf and ttf files.
test: all_check
ZIP_FILE_DIST := build/release/Inter-UI-${VERSION}.zip
ZIP_FILE_DEV := build/release/Inter-UI-${VERSION}-$(shell git rev-parse --short=10 HEAD).zip
# zip intermediate
build/.zip.zip: all_otf
build/.zip.zip: all_otf all_ttf
$(MAKE) all_web all_web_hinted -j
@rm -rf build/.zip
@rm -f build/.zip.zip
@ -84,15 +101,15 @@ build/.zip.zip: all_otf
"build/.zip/Inter UI (TTF)" \
"build/.zip/Inter UI (TTF hinted)" \
"build/.zip/Inter UI (OTF)"
@cp -a build/dist-unhinted/*.woff build/dist-unhinted/*.woff2 \
@cp -a build/unhinted/*.woff build/unhinted/*.woff2 \
"build/.zip/Inter UI (web)/"
@cp -a misc/doc/inter-ui.css "build/.zip/Inter UI (web)/"
@cp -a build/dist-hinted/*.woff build/dist-hinted/*.woff2 \
@cp -a build/hinted/*.woff build/hinted/*.woff2 \
"build/.zip/Inter UI (web hinted)/"
@cp -a misc/doc/inter-ui.css "build/.zip/Inter UI (web hinted)/"
@cp -a build/dist-unhinted/*.ttf "build/.zip/Inter UI (TTF)/"
@cp -a build/dist-hinted/*.ttf "build/.zip/Inter UI (TTF hinted)/"
@cp -a build/dist-unhinted/*.otf "build/.zip/Inter UI (OTF)/"
@cp -a build/unhinted/*.ttf "build/.zip/Inter UI (TTF)/"
@cp -a build/hinted/*.ttf "build/.zip/Inter UI (TTF hinted)/"
@cp -a build/unhinted/*.otf "build/.zip/Inter UI (OTF)/"
@cp -a misc/doc/*.txt "build/.zip/"
@cp -a LICENSE.txt "build/.zip/"
cd build/.zip && zip -v -X -r "../../build/.zip.zip" * >/dev/null && cd ../..
@ -133,28 +150,28 @@ dist: zip_dist
copy_docs_fonts:
rm -rf docs/font-files
mkdir docs/font-files
cp -a build/dist-unhinted/*.woff build/dist-unhinted/*.woff2 build/dist-unhinted/*.otf docs/font-files/
cp -a build/unhinted/*.woff build/unhinted/*.woff2 build/unhinted/*.otf docs/font-files/
install_ttf: all_ttf_unhinted
$(MAKE) all_web -j
@echo "Installing TTF files locally at ~/Library/Fonts/Inter UI"
rm -rf ~/'Library/Fonts/Inter UI'
mkdir -p ~/'Library/Fonts/Inter UI'
cp -va build/dist-unhinted/*.ttf ~/'Library/Fonts/Inter UI'
cp -va build/unhinted/*.ttf ~/'Library/Fonts/Inter UI'
install_ttf_hinted: all_ttf
$(MAKE) all_web -j
@echo "Installing autohinted TTF files locally at ~/Library/Fonts/Inter UI"
rm -rf ~/'Library/Fonts/Inter UI'
mkdir -p ~/'Library/Fonts/Inter UI'
cp -va build/dist-hinted/*.ttf ~/'Library/Fonts/Inter UI'
cp -va build/hinted/*.ttf ~/'Library/Fonts/Inter UI'
install_otf: all_otf
$(MAKE) all_web -j
@echo "Installing OTF files locally at ~/Library/Fonts/Inter UI"
rm -rf ~/'Library/Fonts/Inter UI'
mkdir -p ~/'Library/Fonts/Inter UI'
cp -va build/dist-unhinted/*.otf ~/'Library/Fonts/Inter UI'
cp -va build/unhinted/*.otf ~/'Library/Fonts/Inter UI'
install: install_otf
@ -182,6 +199,6 @@ _local/UnicodeData.txt:
http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
clean:
rm -rf build/tmp/* build/dist-hinted build/dist-unhinted
rm -rvf build/tmp build/hinted build/unhinted
.PHONY: all web clean install install_otf install_ttf deploy zip zip_dist pre_dist dist geninfo copy_docs_fonts all_hinted test
.PHONY: all web clean install install_otf install_ttf deploy zip zip_dist pre_dist dist geninfo copy_docs_fonts all_hinted test glyphsync

267
init.sh
View file

@ -7,7 +7,7 @@ if [[ "${BUILD_DIR:0:2}" == "./" ]]; then
BUILD_DIR=${BUILD_DIR:2}
fi
DIST_DIR=$BUILD_DIR/dist #-hinted|-unhinted
DIST_DIR=$BUILD_DIR/ # hinted | unhinted
BUILD_TMP_DIR=$BUILD_DIR/tmp
VENV_DIR=$BUILD_DIR/venv
@ -86,14 +86,26 @@ else
if [[ ! -d "$VENV_DIR/bin" ]]; then
echo "Setting up virtualenv in '$VENV_DIR'"
rm -f "$VENV_DIR/lib/python"
require_virtualenv
$virtualenv "$VENV_DIR"
elif [[ ! -z $VIRTUAL_ENV ]] && [[ "$VIRTUAL_ENV" != "$VENV_DIR_ABS" ]]; then
echo "Looks like the repository has moved location -- updating virtualenv"
rm -f "$VENV_DIR/lib/python"
require_virtualenv
$virtualenv "$VENV_DIR"
fi
# symlink lib/python -> lib/python<version>
if [[ ! -d "$VENV_DIR/lib/python" ]]; then
for f in "$VENV_DIR/lib/"python*; do
if [[ -d "$f" ]]; then
ln -svf $(basename "$f") "$VENV_DIR/lib/python"
break
fi
done
fi
source "$VENV_DIR/bin/activate"
UPDATE_TIMESTAMP_FILE="$VENV_DIR/last-pip-run.mark"
@ -113,44 +125,65 @@ else
PATCH_DIR=$(pwd)/misc/patches
mkdir -p "$DEPS_DIR"
check_dep() {
function fetch() {
URL=$1
DSTFILE=$2
echo "Fetching $URL"
curl '-#' -o "$DSTFILE" -L "$URL"
}
function check_dep() {
NAME=$1
REPO_URL=$2
BRANCH=$3
TREE_REF=$4
set -e
REPODIR=$DEPS_DIR/$NAME
CHANGED=false
if [[ ! -d "$REPODIR/.git" ]]; then
rm -rf "$REPODIR"
echo "Fetching $NAME from $REPO_URL"
if ! (git clone --recursive --single-branch -b $BRANCH -- "$REPO_URL" "$REPODIR"); then
exit 1
fi
CHANGED=true
elif [[ "$(git -C "$REPODIR" rev-parse HEAD)" != "$TREE_REF" ]]; then
CHANGED=true
git -C "$REPODIR" fetch origin
fi
if $CHANGED; then
if [[ ! -z $TREE_REF ]]; then
git -C "$REPODIR" checkout "$TREE_REF"
git -C "$REPODIR" submodule update
fi
return 1
fi
# TODO: check that source matches tree ref
return 0
}
if ! (check_dep \
woff2 https://github.com/google/woff2.git master 36e6555b92a1519c927ebd43b79621810bf17c1a )
# woff2
LINK=false
if ! (check_dep woff2 \
https://github.com/google/woff2.git master \
a0d0ed7da27b708c0a4e96ad7a998bddc933c06e )
then
echo "Building woff2"
git -C "$DEPS_DIR/woff2" apply "$PATCH_DIR/woff2.patch"
if !(make -C "$DEPS_DIR/woff2" -j8 clean all); then
make -C "$DEPS_DIR/woff2" -j8 clean
if !(make -C "$DEPS_DIR/woff2" -j8 all); then
rm -rf "$DEPS_DIR/woff2"
exit 1
fi
elif [[ ! -f "$VENV_DIR/bin/woff2_compress" ]]; then
LINK=true
fi
if [[ ! -f "$VENV_DIR/bin/woff2_compress" ]]; then
ln -vfs ../../deps/woff2/woff2_compress "$VENV_DIR/bin"
if $LINK; then
ln -vfs ../../deps/woff2/woff2_compress "$VENV_DIR/bin/woff2_compress"
fi
# EOT is disabled
# # EOT (disabled)
# if ! (check_dep \
# ttf2eot https://github.com/rsms/ttf2eot.git master )
# then
@ -161,15 +194,95 @@ else
# ln -vfs ../../deps/ttf2eot/ttf2eot "$VENV_DIR/bin"
# fi
if [[ ! -f "$DEPS_DIR/ttfautohint" ]]; then
URL=https://download.savannah.gnu.org/releases/freetype/ttfautohint-1.6-tty-osx.tar.gz
echo "Fetching $URL"
curl '-#' -o "$DEPS_DIR/ttfautohint.tar.gz" -L "$URL"
# # meson (internal requirement of ots)
# MESON_VERSION=0.47.2
# pushd "$DEPS_DIR" >/dev/null
# MESON_BIN=$PWD/meson-${MESON_VERSION}/meson.py
# popd >/dev/null
# if [[ ! -f "$MESON_BIN" ]]; then
# fetch \
# https://github.com/mesonbuild/meson/releases/download/${MESON_VERSION}/meson-${MESON_VERSION}.tar.gz \
# "$DEPS_DIR/meson.tar.gz"
# tar -C "$DEPS_DIR" -xzf "$DEPS_DIR/meson.tar.gz"
# rm "$DEPS_DIR/meson.tar.gz"
# fi
# # ninja
# NINJA_VERSION=1.8.2
# pushd "$DEPS_DIR" >/dev/null
# NINJA_BIN=$PWD/ninja-${NINJA_VERSION}
# if [[ ! -f "$NINJA_BIN" ]]; then
# fetch \
# https://github.com/ninja-build/ninja/releases/download/v${NINJA_VERSION}/ninja-mac.zip \
# ninja.zip
# rm -f ninja
# unzip -q -o ninja.zip
# rm ninja.zip
# mv ninja "$NINJA_BIN"
# fi
# popd >/dev/null
# # ots (from source)
# LINK=false
# if ! (check_dep ots \
# https://github.com/khaledhosny/ots.git master \
# cad0b4f9405d03ddf801f977f2f65892192ad047 )
# then
# echo "Building ots"
# pushd "$DEPS_DIR/ots" >/dev/null
# "$MESON_BIN" build
# "$NINJA_BIN" -C build
# popd >/dev/null
# fi
# ots (from dist)
OTS_VERSION=7.1.7
OTS_DIR=$DEPS_DIR/ots-${OTS_VERSION}
LINK=false
if [[ ! -f "$OTS_DIR/ots-sanitize" ]]; then
mkdir -p "$OTS_DIR"
fetch \
https://github.com/khaledhosny/ots/releases/download/v${OTS_VERSION}/ots-${OTS_VERSION}-osx.zip \
"$OTS_DIR/ots.zip"
pushd "$OTS_DIR" >/dev/null
unzip ots.zip
rm ots.zip
mv ots-* xx-ots
mv xx-ots/* .
rm -rf xx-ots
popd >/dev/null
LINK=true
fi
if $LINK || [[ ! -f "$VENV_DIR/bin/ots-sanitize" ]]; then
pushd "$OTS_DIR" >/dev/null
for f in ots-*; do
popd >/dev/null
ln -vfs ../../deps/ots-${OTS_VERSION}/$f "$VENV_DIR/bin/$f"
pushd "$OTS_DIR" >/dev/null
done
popd >/dev/null
fi
AUTOHINT_VERSION=1.8.2
LINK=false
if [[ ! -f "$DEPS_DIR/ttfautohint-${AUTOHINT_VERSION}" ]]; then
fetch \
https://download.savannah.gnu.org/releases/freetype/ttfautohint-${AUTOHINT_VERSION}-tty-osx.tar.gz
"$DEPS_DIR/ttfautohint.tar.gz"
tar -C "$DEPS_DIR" -xzf "$DEPS_DIR/ttfautohint.tar.gz"
rm "$DEPS_DIR/ttfautohint.tar.gz"
mv -f "$DEPS_DIR/ttfautohint" "$DEPS_DIR/ttfautohint-${AUTOHINT_VERSION}"
LINK=true
elif [[ ! -f "$VENV_DIR/bin/ttfautohint" ]]; then
LINK=true
fi
if [[ ! -f "$VENV_DIR/bin/ttfautohint" ]]; then
ln -vfs ../../deps/ttfautohint "$VENV_DIR/bin"
if $LINK; then
ln -vfs ../../deps/ttfautohint-1.8.2 "$VENV_DIR/bin/ttfautohint"
fi
if [[ ! -f "$VENV_DIR/bin/ttf2woff" ]] || [[ ! -f "$SRCDIR/misc/ttf2woff/ttf2woff" ]]; then
@ -189,50 +302,6 @@ else
return 1
}
check_cython_dep() {
DIR=$1
REF_FILE=$DIR/$2
set -e
if $clean || $PY_REQUIREMENTS_CHANGED || [ ! -f "$REF_FILE" ] || has_newer "$DIR" "$REF_FILE"; then
pushd "$DIR" >/dev/null
if $clean; then
find . \
-type f \
-name '*.c' -or \
-name '*.o' -or \
-name '*.pyc' -or \
-name '*.pyo' \
| xargs rm
fi
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
python setup.py build_ext --inplace
popd >/dev/null
touch "$REF_FILE"
PY_REQUIREMENTS_CHANGED=true
fi
}
# native booleanOperations module
check_cython_dep misc/pylib/booleanOperations flatten.so
check_cython_dep misc/pylib/copy copy.so
check_cython_dep misc/pylib/fontbuild mix.so
check_cython_dep misc/pylib/robofab glifLib.so
# ————————————————————————————————————————————————————————————————————————————————————————————————
# $BUILD_TMP_DIR
# create and mount spare disk image needed on macOS to support case-sensitive filenames
if [[ "$(uname)" = *Darwin* ]]; then
bash misc/mac-tmp-disk-mount.sh
if $clean; then
echo "[clean] rm -rf '$BUILD_TMP_DIR'/*"
rm -rf "$BUILD_TMP_DIR"/*
fi
else
mkdir -p "$BUILD_TMP_DIR"
fi
# ————————————————————————————————————————————————————————————————————————————————————————————————
# $BUILD_DIR/etc/generated.make
master_styles=( \
@ -273,55 +342,53 @@ else
all_styles=()
# add master styles to style array
for style in "${master_styles[@]}"; do
all_styles+=( $style )
echo "${style}_ufo_d := " \
"\$(wildcard src/Inter-UI-${style}.ufo/* src/Inter-UI-${style}.ufo/*/*)" >> "$GEN_MAKE_FILE"
echo "$BUILD_TMP_DIR/InterUITTF/InterUI-${style}.ttf: \$(${style}_ufo_d)" >> "$GEN_MAKE_FILE"
echo "$BUILD_TMP_DIR/InterUIOTF/InterUI-${style}.otf: \$(${style}_ufo_d)" >> "$GEN_MAKE_FILE"
done
echo -n "all_ufo:" >> "$GEN_MAKE_FILE"
for style in "${master_styles[@]}"; do
echo -n " \$(${style}_ufo_d)" >> "$GEN_MAKE_FILE"
done
# generate all_ufo: <master_ufos>
# echo -n "all_ufo:" >> "$GEN_MAKE_FILE"
# for style in "${master_styles[@]}"; do
# echo -n " \$(${style}_ufo_d)" >> "$GEN_MAKE_FILE"
# done
# echo "" >> "$GEN_MAKE_FILE"
echo "" >> "$GEN_MAKE_FILE"
# add derived styles to style array
for e in "${derived_styles[@]}"; do
style=$(echo "${e%%:*}" | xargs)
dependent_styles=$(echo "${e#*:}" | xargs)
all_styles+=( $style )
echo -n "$BUILD_TMP_DIR/InterUITTF/InterUI-${style}.ttf:" >> "$GEN_MAKE_FILE"
for depstyle in $dependent_styles; do
echo -n " \$(${depstyle}_ufo_d)" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
echo -n "$BUILD_TMP_DIR/InterUIOTF/InterUI-${style}.otf:" >> "$GEN_MAKE_FILE"
for depstyle in $dependent_styles; do
echo -n " \$(${depstyle}_ufo_d)" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
done
# STYLE and STYLE_ttf targets
for style in "${all_styles[@]}"; do
echo "${style}_ttf_hinted: $DIST_DIR-hinted/Inter-UI-${style}.ttf" >> "$GEN_MAKE_FILE"
echo "${style}_ttf: $DIST_DIR-unhinted/Inter-UI-${style}.ttf" >> "$GEN_MAKE_FILE"
echo "${style}_otf: $DIST_DIR-unhinted/Inter-UI-${style}.otf" >> "$GEN_MAKE_FILE"
echo "${style}: ${style}_otf ${style}_ttf ${style}_ttf_hinted ${style}_web ${style}_web_hinted" >> "$GEN_MAKE_FILE"
echo -n "${style}: ${style}_otf" >> "$GEN_MAKE_FILE"
echo "${style}_ttf_hinted: ${DIST_DIR}hinted/Inter-UI-${style}.ttf" >> "$GEN_MAKE_FILE"
echo "${style}_ttf: ${DIST_DIR}unhinted/Inter-UI-${style}.ttf" >> "$GEN_MAKE_FILE"
echo "${style}_otf: ${DIST_DIR}unhinted/Inter-UI-${style}.otf" >> "$GEN_MAKE_FILE"
echo -n "${style}_web:" >> "$GEN_MAKE_FILE"
for format in "${web_formats[@]}"; do
echo -n " $DIST_DIR-unhinted/Inter-UI-${style}.${format}" >> "$GEN_MAKE_FILE"
echo -n " ${DIST_DIR}unhinted/Inter-UI-${style}.${format}" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
echo -n "${style}_hinted: ${style}_ttf_hinted" >> "$GEN_MAKE_FILE"
echo -n "${style}_web_hinted:" >> "$GEN_MAKE_FILE"
for format in "${web_formats[@]}"; do
echo -n " $DIST_DIR-hinted/Inter-UI-${style}.${format}" >> "$GEN_MAKE_FILE"
echo -n " ${DIST_DIR}hinted/Inter-UI-${style}.${format}" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
echo "${style}_check: ${DIST_DIR}unhinted/Inter-UI-${style}.otf ${DIST_DIR}unhinted/Inter-UI-${style}.ttf" >> "$GEN_MAKE_FILE"
echo -e "\t misc/fontbuild checkfont $^" >> "$GEN_MAKE_FILE"
echo "" >> "$GEN_MAKE_FILE"
done
# all_otf target
@ -348,21 +415,35 @@ else
# all_web target
echo -n "all_web:" >> "$GEN_MAKE_FILE"
for style in "${all_styles[@]}"; do
echo -n " ${style}" >> "$GEN_MAKE_FILE"
echo -n " ${style}_web" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
# all_web_hinted target
echo -n "all_web_hinted:" >> "$GEN_MAKE_FILE"
for style in "${all_styles[@]}"; do
echo -n " ${style}_hinted" >> "$GEN_MAKE_FILE"
echo -n " ${style}_web_hinted" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
# all_check target
echo -n "all_check:" >> "$GEN_MAKE_FILE"
for style in "${all_styles[@]}"; do
echo -n " ${style}_check" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
# all_fonts target
echo -n "all_fonts:" >> "$GEN_MAKE_FILE"
for style in "${all_styles[@]}"; do
echo -n " ${style}" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
echo -n ".PHONY: all_otf all_ttf_hinted all_ttf all_web all_web_hinted all_ufo" >> "$GEN_MAKE_FILE"
echo -n ".PHONY: all_otf all_ttf_hinted all_ttf all_web all_web_hinted all_ufo all_check" >> "$GEN_MAKE_FILE"
for style in "${all_styles[@]}"; do
echo -n " ${style} ${style}_ttf ${style}_ttf_hinted ${style}_otf" >> "$GEN_MAKE_FILE"
echo -n " ${style} ${style}_ttf ${style}_ttf_hinted ${style}_otf ${style}_check" >> "$GEN_MAKE_FILE"
done
echo "" >> "$GEN_MAKE_FILE"
fi
@ -370,14 +451,12 @@ else
# ————————————————————————————————————————————————————————————————————————————————————————————————
# summary
if ! $VENV_ACTIVE; then
echo "You now need to activate virtualenv by:"
echo -n "You can activate virtualenv by running "
if [ "$0" == "./init.sh" ]; then
# pretty format for common case
echo " source init.sh"
echo '`source init.sh`'
else
echo " source '$0'"
echo "\`source \"$0\"\`"
fi
echo "Or directly by sourcing the activate script:"
echo " source '$VENV_DIR/bin/activate'"
fi
fi

View file

@ -1,34 +0,0 @@
#!/usr/bin/env python
# encoding: utf8
from __future__ import print_function
import os, sys
from argparse import ArgumentParser
from multiprocessing import Pool
import extractor, defcon
def check_font(filename):
print('check %s' % filename)
ufo = defcon.Font()
extractor.extractUFO(filename, ufo, doGlyphs=True, doInfo=True, doKerning=True)
def main(argv=None):
opts = ArgumentParser(description='Check')
opts.add_argument(
'fontFiles', metavar='<file>', type=str, nargs='+',
help='Font files (otf, ttf, woff, woff2, pfa, pfb, ttx)')
args = opts.parse_args(argv)
if len(args.fontFiles) == 1:
check_font(args.fontFiles[0])
else:
p = Pool(8)
p.map(check_font, args.fontFiles)
p.terminate()
if __name__ == '__main__':
main()

462
misc/fontbuild Executable file
View file

@ -0,0 +1,462 @@
#!/usr/bin/env python
from __future__ import print_function
import sys, os
# patch PYTHONPATH to include $BASEDIR/build/venv/python/site-packages
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
VENVDIR = os.path.join(BASEDIR, 'build', 'venv')
sys.path.append(os.path.join(VENVDIR, 'lib', 'python', 'site-packages'))
import argparse
import datetime
import glyphsLib
import logging
import re
import signal
import subprocess
from fontmake.font_project import FontProject
from fontTools import designspaceLib
from glyphsLib.interpolation import apply_instance_data
from mutatorMath.ufo.document import DesignSpaceDocumentReader
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
_gitHash = None
def getGitHash():
global _gitHash
if _gitHash is None:
_gitHash = ''
try:
_gitHash = subprocess.check_output(
['git', '-C', BASEDIR, 'rev-parse', '--short', 'HEAD'],
shell=False
).strip()
except:
pass
return _gitHash
_version = None
def getVersion():
global _version
if _version is None:
_version = open(os.path.join(BASEDIR, 'version.txt'), 'r').read().strip()
return _version
subfamily_re = re.compile(r'^\s*([^\s]+)(?:\s*italic|)\s*$', re.I | re.U)
def sighandler(signum, frame):
sys.stdout.write('\n')
sys.stdout.flush()
sys.exit(1)
def mkdirs(path):
if not os.access(path, os.F_OK):
os.makedirs(path)
# setFontInfo patches font.info
#
def setFontInfo(font, weight):
#
# For UFO3 names, see
# https://github.com/unified-font-object/ufo-spec/blob/gh-pages/versions/
# ufo3/fontinfo.plist.md
# For OpenType NAME table IDs, see
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
#
version = getVersion()
buildtag = getGitHash()
versionMajor, versionMinor = [int(num) for num in version.split(".")]
now = datetime.datetime.utcnow()
family = font.info.familyName # i.e. "Inter UI"
style = font.info.styleName # e.g. "Medium Italic"
isitalic = font.info.italicAngle != 0
# weight
font.info.openTypeOS2WeightClass = weight
# creation date & time (YYYY/MM/DD HH:MM:SS)
font.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S")
# version
font.info.version = version
font.info.versionMajor = versionMajor
font.info.versionMinor = versionMinor
font.info.year = now.year
font.info.openTypeNameVersion = "%s;%s" % (version, buildtag)
font.info.openTypeNameUniqueID = "%s %s:%d:%s" % (family, style, now.year, buildtag)
# Names
family_nosp = re.sub(r'\s', '', family)
style_nosp = re.sub(r'\s', '', style)
font.info.macintoshFONDName = "%s %s" % (family_nosp, style_nosp)
font.info.postscriptFontName = "%s-%s" % (family_nosp, style_nosp)
# name ID 16 "Typographic Family name"
font.info.openTypeNamePreferredFamilyName = family
# name ID 17 "Typographic Subfamily name"
subfamily = subfamily_re.sub('\\1', style) # "A Italic" => "A", "A" => "A"
if len(subfamily) == 0:
subfamily = "Regular"
font.info.openTypeNamePreferredSubfamilyName = subfamily
# Legacy family name (full name except "italic")
subfamily_lc = subfamily.lower()
if subfamily_lc != "regular" and subfamily_lc != "bold":
font.info.styleMapFamilyName = "%s %s" % (family, subfamily)
else:
font.info.styleMapFamilyName = family
# Legacy style name. Must be one of these case-sensitive strings:
# "regular", "italic", "bold", "bold italic"
font.info.styleMapStyleName = "regular"
if style.strip().lower().find('bold') != -1:
if isitalic:
font.info.styleMapStyleName = "bold italic"
else:
font.info.styleMapStyleName = "bold"
elif isitalic:
font.info.styleMapStyleName = "italic"
class Main(object):
def __init__(self):
self.tmpdir = os.path.join(BASEDIR,'build','tmp')
def main(self, argv):
# make ^C instantly exit program
signal.signal(signal.SIGINT, sighandler)
# update environment
os.environ['PATH'] = '%s:%s' % (
os.path.join(VENVDIR, 'bin'), os.environ['PATH'])
argparser = argparse.ArgumentParser(
description='',
usage='''
%(prog)s [options] <command> [<args>]
Commands:
compile Build font files
glyphsync Generate designspace and UFOs from Glyphs file
instancegen Generate instance UFOs for designspace
'''.strip().replace('\n ', '\n'))
argparser.add_argument('-v', '--verbose', action='store_true',
help='Print more details')
argparser.add_argument('command', metavar='<command>')
args = argparser.parse_args(argv[1:2])
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.ERROR)
cmd = 'cmd_' + args.command.replace('-', '_')
if not hasattr(self, cmd):
print('Unrecognized command %s. Try --help' % args.command,
file=sys.stderr)
exit(1)
getattr(self, cmd)(argv[2:])
def cmd_compile(self, argv):
argparser = argparse.ArgumentParser(
usage='%(prog)s compile [-h] [-o <file>] <ufo>',
description='Compile font files')
argparser.add_argument('ufo', metavar='<ufo>',
help='UFO source file')
argparser.add_argument('-o', '--output', metavar='<fontfile>',
help='Output font file (.otf, .ttf, .ufo or .gx)')
argparser.add_argument('--validate', action='store_true',
help='Enable ufoLib validation on reading/writing UFO files')
# argparser.add_argument('-f', '--formats', nargs='+', metavar='<format>',
# help='Output formats. Any of %(choices)s. Defaults to "otf".',
# choices=tuple(all_formats),
# default=('otf'))
args = argparser.parse_args(argv)
ext_to_format = {
'.ufo': 'ufo',
'.otf': 'otf',
'.ttf': 'ttf',
# 'ttf-interpolatable',
'.gx': 'variable',
}
formats = []
filename = args.output
if filename is None or filename == '':
ufoname = os.path.basename(args.ufo)
name, ext = os.path.splitext(ufoname)
filename = name + '.otf'
logging.info('setting --output %r' % filename)
# for filename in args.output:
ext = os.path.splitext(filename)[1]
ext_lc = ext.lower()
if ext_lc in ext_to_format:
formats.append(ext_to_format[ext_lc])
else:
print('Unsupported output format %s' % ext, file=sys.stderr)
exit(1)
mkdirs(self.tmpdir)
project = FontProject(
timing=None,
verbose='WARNING',
validate_ufo=args.validate
)
project.run_from_ufos(
[args.ufo],
output_dir=self.tmpdir,
output=formats
)
# run through ots-sanitize
# for filename in args.output:
tmpfile = os.path.join(self.tmpdir, os.path.basename(filename))
mkdirs(os.path.dirname(filename))
success = True
try:
otssan_res = subprocess.check_output(
['ots-sanitize', tmpfile, filename],
shell=False
).strip()
# Note: ots-sanitize does not exit with an error in many cases where
# it fails to sanitize the font.
success = otssan_res.find('Failed') == -1
except:
success = False
otssan_res = 'error'
if success:
os.unlink(tmpfile)
else:
print('ots-sanitize failed for %s: %s' % (
tmpfile, otssan_res), file=sys.stderr)
exit(1)
def cmd_glyphsync(self, argv):
argparser = argparse.ArgumentParser(
usage='%(prog)s glyphsync <glyphsfile> [options]',
description='Generates designspace and UFOs from Glyphs file')
argparser.add_argument('glyphsfile', metavar='<glyphsfile>',
help='Glyphs source file')
argparser.add_argument('-o', '--outdir', metavar='<dir>',
help='''Write output to <dir>. If omitted, designspace and UFOs are
written to the directory of the glyphs file.
'''.strip().replace('\n ', ''))
args = argparser.parse_args(argv)
outdir = args.outdir
if outdir is None:
outdir = os.path.dirname(args.glyphsfile)
# files
master_dir = outdir
glyphsfile = args.glyphsfile
designspace_file = os.path.join(outdir, 'Inter-UI.designspace')
instance_dir = os.path.join(BASEDIR, 'build', 'ufo')
# load glyphs project file
print("generating %s from %s" % (
os.path.relpath(designspace_file, os.getcwd()),
os.path.relpath(glyphsfile, os.getcwd())
))
font = glyphsLib.GSFont(glyphsfile)
# generate designspace from glyphs project
designspace = glyphsLib.to_designspace(
font,
propagate_anchors=False,
instance_dir=os.path.relpath(instance_dir, master_dir)
)
# strip lib data
designspace.lib.clear()
# fixup axes
for axis in designspace.axes:
if axis.tag == "wght":
axis.map = []
axis.minimum = 100
axis.maximum = 900
axis.default = 400
# patch and write UFO files
# TODO: Only write out-of-date UFOs
for source in designspace.sources:
# source : fontTools.designspaceLib.SourceDescriptor
# source.font : defcon.objects.font.Font
ufo_path = os.path.join(master_dir, source.filename.replace('InterUI', 'Inter-UI'))
# no need to also set the relative 'filename' attribute as that
# will be auto-updated on writing the designspace document
# name "Inter UI Black" => "black"
source.name = source.styleName.lower().replace(' ', '')
# fixup font info
weight = int(source.location['Weight'])
setFontInfo(source.font, weight)
# cleanup lib
lib = dict()
for key, value in source.font.lib.iteritems():
if key.startswith('com.schriftgestaltung'):
continue
if key == 'public.postscriptNames':
continue
lib[key] = value
source.font.lib.clear()
source.font.lib.update(lib)
# write UFO file
source.path = ufo_path
print("write %s" % os.path.relpath(ufo_path, os.getcwd()))
source.font.save(ufo_path)
# patch instance names
for instance in designspace.instances:
# name "Inter UI Black Italic" => "blackitalic"
instance.name = instance.styleName.lower().replace(' ', '')
instance.filename = instance.filename.replace('InterUI', 'Inter-UI')
print("write %s" % os.path.relpath(designspace_file, os.getcwd()))
designspace.write(designspace_file)
def cmd_instancegen(self, argv):
argparser = argparse.ArgumentParser(
description='Generate UFO instances from designspace')
argparser.add_argument('designspacefile', metavar='<designspace>',
help='Designspace file')
argparser.add_argument('instances', metavar='<instance-name>', nargs='*',
help='Style instances to generate. Omit to generate all.')
args = argparser.parse_args(argv)
instances = set([s.lower() for s in args.instances])
all_instances = len(instances) == 0
# files
designspace_file = args.designspacefile
instance_dir = os.path.join(BASEDIR, 'build', 'ufo')
# DesignSpaceDocumentReader generates UFOs
gen = DesignSpaceDocumentReader(
designspace_file,
ufoVersion=3,
roundGeometry=True,
verbose=True
)
designspace = designspaceLib.DesignSpaceDocument()
designspace.read(designspace_file)
# Generate UFOs for instances
instance_weight = dict()
instance_files = set()
for instance in designspace.instances:
if all_instances or instance.name in instances:
filebase = os.path.basename(instance.filename)
relname = os.path.relpath(
os.path.join(os.path.dirname(designspace_file), instance.filename),
os.getcwd()
)
print('generating %s' % relname)
gen.readInstance(("name", instance.name))
instance_files.add(instance.filename)
instance_weight[filebase] = int(instance.location['Weight'])
if not all_instances:
instances.remove(instance.name)
if len(instances) > 0:
print('unknown style(s): %s' % ', '.join(list(instances)), file=sys.stderr)
sys.exit(1)
ufos = apply_instance_data(designspace_file, instance_files)
# patch ufos (list of defcon.Font instances)
italicAngleKey = 'com.schriftgestaltung.customParameter.' +\
'InstanceDescriptorAsGSInstance.italicAngle'
for font in ufos:
# move italicAngle from lib to info
italicAngle = font.lib.get(italicAngleKey)
if italicAngle != None:
italicAngle = float(italicAngle)
del font.lib[italicAngleKey]
font.info.italicAngle = italicAngle
# update font info
weight = instance_weight[os.path.basename(font.path)]
setFontInfo(font, weight)
font.save()
def checkfont(self, fontfile):
try:
res = subprocess.check_output(
['ots-idempotent', fontfile],
shell=False
).strip()
# Note: ots-idempotent does not exit with an error in many cases where
# it fails to sanitize the font.
if res.find('Failed') != -1:
logging.error('[checkfont] ots-idempotent failed for %r: %s' % (
fontfile, res))
return False
except:
logging.error('[checkfont] ots-idempotent failed for %r' % fontfile)
return False
return True
def cmd_checkfont(self, argv):
argparser = argparse.ArgumentParser(
usage='%(prog)s checkfont <file> ...',
description='Verify integrity of font files')
argparser.add_argument('files', metavar='<file>', nargs='+',
help='Font files')
args = argparser.parse_args(argv)
for fontfile in args.files:
if not self.checkfont(fontfile):
sys.exit(1)
# could use from multiprocessing import Pool
# p = Pool(8)
# p.map(self.checkfont, args.files)
# p.terminate()
if __name__ == '__main__':
Main().main(sys.argv)

View file

@ -1,25 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
# Create if needed
if [[ ! -f build/tmp.sparseimage ]]; then
echo "Creating sparse disk image with case-sensitive file system build/tmp.sparseimage"
mkdir -p build
hdiutil create build/tmp.sparseimage \
-size 1g \
-type SPARSE \
-fs JHFS+X \
-volname tmp
fi
# Mount if needed
if ! (diskutil info build/tmp >/dev/null); then
echo "Mounting sparse disk image with case-sensitive file system at build/tmp"
hdiutil attach build/tmp.sparseimage \
-readwrite \
-mountpoint "$(pwd)/build/tmp" \
-nobrowse \
-noautoopen \
-noidmereveal
fi

View file

@ -1,5 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
diskutil unmount build/tmp

View file

@ -1,33 +0,0 @@
From 25319cecf58faf9d92f489ad52323fe4762b8087 Mon Sep 17 00:00:00 2001
From: Rasmus Andersson <rasmus@notion.se>
Date: Mon, 21 Aug 2017 07:53:23 -0700
Subject: [PATCH] Exclude invalid ar flag in makefile on macOS
---
Makefile | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 4b3bd7b..5bf878f 100644
--- a/Makefile
+++ b/Makefile
@@ -11,13 +11,15 @@ CANONICAL_PREFIXES ?= -no-canonical-prefixes
NOISY_LOGGING ?= -DFONT_COMPRESSION_BIN
COMMON_FLAGS = -fno-omit-frame-pointer $(CANONICAL_PREFIXES) $(NOISY_LOGGING) -D __STDC_FORMAT_MACROS
+ARFLAGS = cr
+
ifeq ($(OS), Darwin)
CPPFLAGS += -DOS_MACOSX
else
COMMON_FLAGS += -fno-tree-vrp
+ ARFLAGS += f
endif
-ARFLAGS = crf
CFLAGS += $(COMMON_FLAGS)
CXXFLAGS += $(COMMON_FLAGS) -std=c++11
--
2.11.0

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,50 +0,0 @@
|Travis Build Status| |PyPI Version| |Python Versions|
UFO Extractor
=============
Tools for extracting data from font binaries into UFO objects.
Features
--------
Import data into a `Defcon <https://github.com/typesupply/defcon>`__ ``Font``
instance:
.. code:: python
>>> import extractor
>>> import defcon
>>> ufo = defcon.Font()
>>> extractor.extractUFO("/path/to/MyFont.ttf", ufo)
>>> ufo.save("/path/to/MyFont.ufo")
Supported input formats:
- CFF or TrueType-flavored OpenType fonts (``*.otf``, ``*.ttf``)
- `FontTools <https://github.com/fonttools/fonttools>`__ TTX files
(``*.ttx``)
- WOFF 1.0/2.0 (``*.woff``, ``*.woff2``)
- PostScript Type1 fonts (``*.pfa``, ``*.pfb``, etc.)
Installation
------------
You can install ``extractor`` with ``pip``:
.. code::
$ pip install ufo-extractor
Note that, for historical reasons, the package is listed on the
`Python Package Index <https://travis-ci.org/typesupply/extractor>`__ under the name
``ufo-extractor``, to disambiguate it from another package also called "extractor".
However, the import name for the package remains ``extractor``, without prefix.
.. |Travis Build Status| image:: https://travis-ci.org/typesupply/extractor.svg?branch=master
:target: https://travis-ci.org/typesupply/extractor
.. |PyPI Version| image:: https://img.shields.io/pypi/v/ufo-extractor.svg
:target: https://pypi.org/project/ufo-extractor/
.. |Python Versions| image:: https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg

View file

@ -1,45 +0,0 @@
from extractor.exceptions import ExtractorError
from extractor.formats.opentype import isOpenType, extractFontFromOpenType
from extractor.formats.woff import isWOFF, extractFontFromWOFF
from extractor.formats.type1 import isType1, extractFontFromType1
from extractor.formats.ttx import isTTX, extractFontFromTTX
__version__ = "0.2.1.dev0"
_extractFunctions = dict(
OTF=extractFontFromOpenType,
Type1=extractFontFromType1,
WOFF=extractFontFromWOFF,
ttx=extractFontFromTTX,
)
def extractFormat(pathOrFile):
if isType1(pathOrFile):
return "Type1"
elif isWOFF(pathOrFile):
return "WOFF"
elif isOpenType(pathOrFile):
return "OTF"
elif isTTX(pathOrFile):
return "ttx"
return None
def extractUFO(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, format=None, customFunctions={}):
if format is None:
format = extractFormat(pathOrFile)
if format not in _extractFunctions:
raise ExtractorError("Unknown file format.")
func = _extractFunctions[format]
# wrap the extraction in a try: except: so that
# callers don't need to worry about lower level
# (fontTools, etc.) errors. if an error
# occurs, print the traceback for debugging and
# raise an ExtractorError.
try:
func(pathOrFile, destination, doGlyphs=doGlyphs, doInfo=doInfo, doKerning=doKerning, customFunctions=customFunctions.get(format, []))
except:
import sys
import traceback
traceback.print_exc(file=sys.stdout)
raise ExtractorError("There was an error reading the %s file." % format)

View file

@ -1 +0,0 @@
class ExtractorError(Exception): pass

View file

@ -1,806 +0,0 @@
import time
from fontTools.ttLib import TTFont, TTLibError
from fontTools.ttLib.tables._h_e_a_d import mac_epoch_diff
from fontTools.misc.textTools import num2binary
from fontTools.pens.boundsPen import ControlBoundsPen
from extractor.exceptions import ExtractorError
from extractor.tools import RelaxedInfo, copyAttr
# ----------------
# Public Functions
# ----------------
def isOpenType(pathOrFile):
try:
font = TTFont(pathOrFile)
del font
except TTLibError:
return False
return True
def extractFontFromOpenType(pathOrFile, destination, doGlyphOrder=True, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
source = TTFont(pathOrFile)
if doInfo:
extractOpenTypeInfo(source, destination)
if doGlyphs:
extractOpenTypeGlyphs(source, destination)
if doGlyphOrder:
extractGlyphOrder(source, destination)
if doKerning:
kerning, groups = extractOpenTypeKerning(source, destination)
destination.groups.update(groups)
destination.kerning.clear()
destination.kerning.update(kerning)
for function in customFunctions:
function(source, destination)
source.close()
def extractGlyphOrder(source, destination):
glyphOrder = source.getGlyphOrder()
if len(glyphOrder):
destination.lib["public.glyphOrder"] = glyphOrder
# ----
# Info
# ----
def extractOpenTypeInfo(source, destination):
info = RelaxedInfo(destination.info)
_extractInfoHead(source, info)
_extractInfoName(source, info)
_extracInfoOS2(source, info)
_extractInfoHhea(source, info)
_extractInfoVhea(source, info)
_extractInfoPost(source, info)
_extractInfoCFF(source, info)
_extractInfoGasp(source, info)
def _extractInfoHead(source, info):
head = source["head"]
# version
version = str(round(head.fontRevision, 3))
versionMajor, versionMinor = version.split(".")
info.versionMajor = int(versionMajor)
info.versionMinor = int(versionMinor)
# upm
info.unitsPerEm = head.unitsPerEm
# created
format = "%Y/%m/%d %H:%M:%S"
created = head.created
created = time.gmtime(max(0, created + mac_epoch_diff))
info.openTypeHeadCreated = time.strftime(format, created)
# lowestRecPPEM
info.openTypeHeadLowestRecPPEM = head.lowestRecPPEM
# flags
info.openTypeHeadFlags = binaryToIntList(head.flags)
# styleMapStyleName
macStyle = binaryToIntList(head.macStyle)
styleMapStyleName = "regular"
if 0 in macStyle and 1 in macStyle:
styleMapStyleName = "bold italic"
elif 0 in macStyle:
styleMapStyleName = "bold"
elif 1 in macStyle:
styleMapStyleName = "italic"
info.styleMapStyleName = styleMapStyleName
def _extractInfoName(source, info):
records = []
nameIDs = {}
for record in source["name"].names:
nameID = record.nameID
platformID = record.platformID
encodingID = record.platEncID
languageID = record.langID
string = record.toUnicode()
nameIDs[nameID, platformID, encodingID, languageID] = string
records.append((nameID, platformID, encodingID, languageID,
dict(
nameID=nameID,
platformID=platformID,
encodingID=encodingID,
languageID=languageID,
string=string
)
))
attributes = dict(
familyName=_priorityOrder(16) + _priorityOrder(1),
styleName=_priorityOrder(17) + _priorityOrder(2),
styleMapFamilyName=_priorityOrder(1),
# styleMapStyleName will be handled in head extraction
copyright=_priorityOrder(0),
trademark=_priorityOrder(7),
openTypeNameDesigner=_priorityOrder(9),
openTypeNameDesignerURL=_priorityOrder(12),
openTypeNameManufacturer=_priorityOrder(8),
openTypeNameManufacturerURL=_priorityOrder(11),
openTypeNameLicense=_priorityOrder(13),
openTypeNameLicenseURL=_priorityOrder(14),
openTypeNameVersion=_priorityOrder(5),
openTypeNameUniqueID=_priorityOrder(3),
openTypeNameDescription=_priorityOrder(10),
openTypeNamePreferredFamilyName=_priorityOrder(16),
openTypeNamePreferredSubfamilyName=_priorityOrder(17),
openTypeNameCompatibleFullName=_priorityOrder(18),
openTypeNameSampleText=_priorityOrder(20),
openTypeNameWWSFamilyName=_priorityOrder(21),
openTypeNameWWSSubfamilyName=_priorityOrder(22)
)
for attr, priority in attributes.items():
value = _skimNameIDs(nameIDs, priority)
if value is not None:
setattr(info, attr, value)
info.openTypeNameRecords = [record[-1] for record in sorted(records)]
def _priorityOrder(nameID):
priority = [
(nameID, 1, 0, 0),
(nameID, 1, None, None),
(nameID, None, None, None)
]
return priority
def _skimNameIDs(nameIDs, priority):
for (nameID, platformID, platEncID, langID) in priority:
for (nID, pID, pEID, lID), text in nameIDs.items():
if nID != nameID:
continue
if pID != platformID and platformID is not None:
continue
if pEID != platEncID and platEncID is not None:
continue
if lID != langID and langID is not None:
continue
return text
def _extracInfoOS2(source, info):
os2 = source["OS/2"]
# openTypeOS2WidthClass
copyAttr(os2, "usWidthClass", info, "openTypeOS2WidthClass")
# openTypeOS2WeightClass
copyAttr(os2, "usWeightClass", info, "openTypeOS2WeightClass")
# openTypeOS2Selection
if hasattr(os2, "fsSelection"):
fsSelection = binaryToIntList(os2.fsSelection)
fsSelection = [i for i in fsSelection if i in (1, 2, 3, 4)]
info.openTypeOS2Selection = fsSelection
# openTypeOS2VendorID
copyAttr(os2, "achVendID", info, "openTypeOS2VendorID")
## the string could be padded with null bytes. strip those.
if info.openTypeOS2VendorID.endswith("\x00"):
r = []
for c in reversed(info.openTypeOS2VendorID):
if r or c != "\x00":
r.insert(0, c)
info.openTypeOS2VendorID = "".join(r)
# openTypeOS2Panose
if hasattr(os2, "panose"):
panose = os2.panose
info.openTypeOS2Panose = [
os2.panose.bFamilyType,
os2.panose.bSerifStyle,
os2.panose.bWeight,
os2.panose.bProportion,
os2.panose.bContrast,
os2.panose.bStrokeVariation,
os2.panose.bArmStyle,
os2.panose.bLetterForm,
os2.panose.bMidline,
os2.panose.bXHeight
]
# openTypeOS2FamilyClass
# XXX info.openTypeOS2FamilyClass
if hasattr(os2, "ulUnicodeRange1"):
info.openTypeOS2UnicodeRanges = binaryToIntList(os2.ulUnicodeRange1) + binaryToIntList(os2.ulUnicodeRange2, 32) + binaryToIntList(os2.ulUnicodeRange3, 64) + binaryToIntList(os2.ulUnicodeRange4, 96)
if hasattr(os2, "ulCodePageRange1"):
info.openTypeOS2CodePageRanges = binaryToIntList(os2.ulCodePageRange1) + binaryToIntList(os2.ulCodePageRange2, 32)
copyAttr(os2, "sxHeight", info, "xHeight")
copyAttr(os2, "sCapHeight", info, "capHeight")
copyAttr(os2, "sTypoAscender", info, "ascender")
copyAttr(os2, "sTypoDescender", info, "descender")
copyAttr(os2, "sTypoAscender", info, "openTypeOS2TypoAscender")
copyAttr(os2, "sTypoDescender", info, "openTypeOS2TypoDescender")
copyAttr(os2, "sTypoLineGap", info, "openTypeOS2TypoLineGap")
copyAttr(os2, "usWinAscent", info, "openTypeOS2WinAscent")
copyAttr(os2, "usWinDescent", info, "openTypeOS2WinDescent")
if hasattr(os2, "fsType"):
info.openTypeOS2Type = binaryToIntList(os2.fsType)
copyAttr(os2, "ySubscriptXSize", info, "openTypeOS2SubscriptXSize")
copyAttr(os2, "ySubscriptYSize", info, "openTypeOS2SubscriptYSize")
copyAttr(os2, "ySubscriptXOffset", info, "openTypeOS2SubscriptXOffset")
copyAttr(os2, "ySubscriptYOffset", info, "openTypeOS2SubscriptYOffset")
copyAttr(os2, "ySuperscriptXSize", info, "openTypeOS2SuperscriptXSize")
copyAttr(os2, "ySuperscriptYSize", info, "openTypeOS2SuperscriptYSize")
copyAttr(os2, "ySuperscriptXOffset", info, "openTypeOS2SuperscriptXOffset")
copyAttr(os2, "ySuperscriptYOffset", info, "openTypeOS2SuperscriptYOffset")
copyAttr(os2, "yStrikeoutSize", info, "openTypeOS2StrikeoutSize")
copyAttr(os2, "yStrikeoutPosition", info, "openTypeOS2StrikeoutPosition")
def _extractInfoHhea(source, info):
if "hhea" not in source:
return
hhea = source["hhea"]
info.openTypeHheaAscender = hhea.ascent
info.openTypeHheaDescender = hhea.descent
info.openTypeHheaLineGap = hhea.lineGap
info.openTypeHheaCaretSlopeRise = hhea.caretSlopeRise
info.openTypeHheaCaretSlopeRun = hhea.caretSlopeRun
info.openTypeHheaCaretOffset = hhea.caretOffset
def _extractInfoVhea(source, info):
if "vhea" not in source:
return
vhea = source["vhea"]
info.openTypeVheaVertTypoAscender = vhea.ascent
info.openTypeVheaVertTypoDescender = vhea.descent
info.openTypeVheaVertTypoLineGap = vhea.lineGap
info.openTypeVheaCaretSlopeRise = vhea.caretSlopeRise
info.openTypeVheaCaretSlopeRun = vhea.caretSlopeRun
if hasattr(vhea, "caretOffset"):
info.openTypeVheaCaretOffset = vhea.caretOffset
def _extractInfoPost(source, info):
post = source["post"]
info.italicAngle = post.italicAngle
info.postscriptUnderlineThickness = post.underlineThickness
info.postscriptUnderlinePosition = post.underlinePosition
info.postscriptIsFixedPitch = bool(post.isFixedPitch)
def _extractInfoCFF(source, info):
if "CFF " not in source:
return
cff = source["CFF "].cff
info.postscriptFontName = cff.fontNames[0]
# TopDict
topDict = cff.topDictIndex[0]
info.postscriptFullName = topDict.rawDict.get("FullName", None)
info.postscriptWeightName = topDict.rawDict.get("Weight", None)
# Private
# CID doesn't have this, so safely extract.
if hasattr(topDict, "Private"):
private = topDict.Private
info.postscriptBlueValues = private.rawDict.get("BlueValues", [])
info.postscriptOtherBlues = private.rawDict.get("OtherBlues", [])
info.postscriptFamilyBlues = private.rawDict.get("FamilyBlues", [])
info.postscriptFamilyOtherBlues = private.rawDict.get("FamilyOtherBlues", [])
info.postscriptStemSnapH = private.rawDict.get("StemSnapH", [])
info.postscriptStemSnapV = private.rawDict.get("StemSnapV", [])
info.postscriptBlueFuzz = private.rawDict.get("BlueFuzz", None)
info.postscriptBlueShift = private.rawDict.get("BlueShift", None)
info.postscriptBlueScale = private.rawDict.get("BlueScale", None)
info.postscriptForceBold = bool(private.rawDict.get("ForceBold", None))
info.postscriptNominalWidthX = private.rawDict.get("nominalWidthX", None)
info.postscriptDefaultWidthX = private.rawDict.get("defaultWidthX", None)
# XXX postscriptSlantAngle
# XXX postscriptUniqueID
def _extractInfoGasp(source, info):
if "gasp" not in source:
return
gasp = source["gasp"]
records = []
for size, bits in sorted(gasp.gaspRange.items()):
behavior = []
if bits & 0x0001:
behavior.append(0)
if bits & 0x0002:
behavior.append(1)
if bits & 0x0004:
behavior.append(2)
if bits & 0x0008:
behavior.append(3)
record = dict(
rangeMaxPPEM=size,
rangeGaspBehavior=behavior
)
records.append(record)
info.openTypeGaspRangeRecords = records
# Tools
def binaryToIntList(value, start=0):
intList = []
counter = start
while value:
if value & 1:
intList.append(counter)
value >>= 1
counter += 1
return intList
# --------
# Outlines
# --------
def extractOpenTypeGlyphs(source, destination):
# grab the cmap
cmap = source["cmap"]
vmtx = source.get("vmtx")
vorg = source.get("VORG")
preferred = [
(3, 10, 12),
(3, 10, 4),
(3, 1, 12),
(3, 1, 4),
(0, 3, 12),
(0, 3, 4),
(3, 0, 12),
(3, 0, 4),
(1, 0, 12),
(1, 0, 4)
]
found = {}
for table in cmap.tables:
found[table.platformID, table.platEncID, table.format] = table
table = None
for key in preferred:
if key not in found:
continue
table = found[key]
break
reversedMapping = {}
if table is not None:
for uniValue, glyphName in table.cmap.items():
reversedMapping[glyphName] = uniValue
# grab the glyphs
glyphSet = source.getGlyphSet()
for glyphName in glyphSet.keys():
sourceGlyph = glyphSet[glyphName]
# make the new glyph
destination.newGlyph(glyphName)
destinationGlyph = destination[glyphName]
# outlines
pen = destinationGlyph.getPen()
sourceGlyph.draw(pen)
# width
destinationGlyph.width = sourceGlyph.width
# height and vertical origin
if vmtx is not None and glyphName in vmtx.metrics:
destinationGlyph.height = vmtx[glyphName][0]
if vorg is not None:
if glyphName in vorg.VOriginRecords:
destinationGlyph.verticalOrigin = vorg[glyphName]
else:
destinationGlyph.verticalOrigin = vorg.defaultVertOriginY
else:
tsb = vmtx[glyphName][1]
bounds_pen = ControlBoundsPen(glyphSet)
sourceGlyph.draw(bounds_pen)
if bounds_pen.bounds is None:
continue
xMin, yMin, xMax, yMax = bounds_pen.bounds
destinationGlyph.verticalOrigin = tsb + yMax
# unicode
destinationGlyph.unicode = reversedMapping.get(glyphName)
# -------
# Kerning
# -------
def extractOpenTypeKerning(source, destination):
kerning = {}
groups = {}
if "GPOS" in source:
kerning, groups = _extractOpenTypeKerningFromGPOS(source)
elif "kern" in source:
kerning = _extractOpenTypeKerningFromKern(source)
groups = {}
for name, group in groups.items():
groups[name] = list(sorted(group))
return kerning, groups
def _extractOpenTypeKerningFromGPOS(source):
gpos = source["GPOS"].table
# get an ordered list of scripts
scriptOrder = _makeScriptOrder(gpos)
# extract kerning and classes from each applicable lookup
kerningDictionaries, leftClassDictionaries, rightClassDictionaries = _gatherDataFromLookups(gpos, scriptOrder)
# merge all kerning pairs
kerning = _mergeKerningDictionaries(kerningDictionaries)
# get rid of groups that have only one member
leftSingleMemberGroups = _findSingleMemberGroups(leftClassDictionaries)
rightSingleMemberGroups = _findSingleMemberGroups(rightClassDictionaries)
# filter out the single glyph groups from the kerning
kerning = _removeSingleMemberGroupReferences(kerning, leftSingleMemberGroups, rightSingleMemberGroups)
# merge groups that have the exact same member list
leftClasses, leftClassRename = _mergeClasses(leftClassDictionaries)
rightClasses, rightClassRename = _mergeClasses(rightClassDictionaries)
# search for overlapping groups and raise an error if any were found
_validateClasses(leftClasses)
_validateClasses(rightClasses)
# populate the class marging into the kerning
kerning = _replaceRenamedPairMembers(kerning, leftClassRename, rightClassRename)
# rename the groups to final names
leftClassRename = _renameClasses(leftClasses, "public.kern1.")
rightClassRename = _renameClasses(rightClasses, "public.kern2.")
# populate the final group names
kerning = _replaceRenamedPairMembers(kerning, leftClassRename, rightClassRename)
leftGroups = _setGroupNames(leftClasses, leftClassRename)
rightGroups = _setGroupNames(rightClasses, rightClassRename)
# combine the side groups
groups = {}
groups.update(leftGroups)
groups.update(rightGroups)
# done.
return kerning, groups
def _makeScriptOrder(gpos):
"""
Run therough GPOS and make an alphabetically
ordered list of scripts. If DFLT is in the list,
move it to the front.
"""
scripts = []
for scriptRecord in gpos.ScriptList.ScriptRecord:
scripts.append(scriptRecord.ScriptTag)
if "DFLT" in scripts:
scripts.remove("DFLT")
scripts.insert(0, "DFLT")
return sorted(scripts)
def _gatherDataFromLookups(gpos, scriptOrder):
"""
Gather kerning and classes from the applicable lookups
and return them in script order.
"""
lookupIndexes = _gatherLookupIndexes(gpos)
seenLookups = set()
kerningDictionaries = []
leftClassDictionaries = []
rightClassDictionaries = []
for script in scriptOrder:
kerning = []
leftClasses = []
rightClasses = []
for lookupIndex in lookupIndexes[script]:
if lookupIndex in seenLookups:
continue
seenLookups.add(lookupIndex)
result = _gatherKerningForLookup(gpos, lookupIndex)
if result is None:
continue
k, lG, rG = result
kerning.append(k)
leftClasses.append(lG)
rightClasses.append(rG)
if kerning:
kerningDictionaries.append(kerning)
leftClassDictionaries.append(leftClasses)
rightClassDictionaries.append(rightClasses)
return kerningDictionaries, leftClassDictionaries, rightClassDictionaries
def _gatherLookupIndexes(gpos):
"""
Gather a mapping of script to lookup indexes
referenced by the kern feature for each script.
Returns a dictionary of this structure:
{
"latn" : [0],
"DFLT" : [0]
}
"""
# gather the indexes of the kern features
kernFeatureIndexes = [index for index, featureRecord in enumerate(gpos.FeatureList.FeatureRecord) if featureRecord.FeatureTag == "kern"]
# find scripts and languages that have kern features
scriptKernFeatureIndexes = {}
for scriptRecord in gpos.ScriptList.ScriptRecord:
script = scriptRecord.ScriptTag
thisScriptKernFeatureIndexes = []
defaultLangSysRecord = scriptRecord.Script.DefaultLangSys
if defaultLangSysRecord is not None:
f = []
for featureIndex in defaultLangSysRecord.FeatureIndex:
if featureIndex not in kernFeatureIndexes:
continue
f.append(featureIndex)
if f:
thisScriptKernFeatureIndexes.append((None, f))
if scriptRecord.Script.LangSysRecord is not None:
for langSysRecord in scriptRecord.Script.LangSysRecord:
langSys = langSysRecord.LangSysTag
f = []
for featureIndex in langSysRecord.LangSys.FeatureIndex:
if featureIndex not in kernFeatureIndexes:
continue
f.append(featureIndex)
if f:
thisScriptKernFeatureIndexes.append((langSys, f))
scriptKernFeatureIndexes[script] = thisScriptKernFeatureIndexes
# convert the feature indexes to lookup indexes
scriptLookupIndexes = {}
for script, featureDefinitions in scriptKernFeatureIndexes.items():
lookupIndexes = scriptLookupIndexes[script] = []
for language, featureIndexes in featureDefinitions:
for featureIndex in featureIndexes:
featureRecord = gpos.FeatureList.FeatureRecord[featureIndex]
for lookupIndex in featureRecord.Feature.LookupListIndex:
if lookupIndex not in lookupIndexes:
lookupIndexes.append(lookupIndex)
# done
return scriptLookupIndexes
def _gatherKerningForLookup(gpos, lookupIndex):
"""
Gather the kerning and class data for a particular lookup.
Returns kerning, left clases, right classes.
The kerning dictionary is of this structure:
{
("a", "a") : 10,
((1, 1, 3), "a") : -20
}
The class dictionaries have this structure:
{
(1, 1, 3) : ["x", "y", "z"]
}
Where the tuple means this:
(lookup index, subtable index, class index)
"""
allKerning = {}
allLeftClasses = {}
allRightClasses = {}
lookup = gpos.LookupList.Lookup[lookupIndex]
# only handle pair positioning and extension
if lookup.LookupType not in (2, 9):
return
for subtableIndex, subtable in enumerate(lookup.SubTable):
if lookup.LookupType == 2:
format = subtable.Format
lookupType = subtable.LookupType
if (lookupType, format) == (2, 1):
kerning = _handleLookupType2Format1(subtable)
allKerning.update(kerning)
elif (lookupType, format) == (2, 2):
kerning, leftClasses, rightClasses = _handleLookupType2Format2(subtable, lookupIndex, subtableIndex)
allKerning.update(kerning)
allLeftClasses.update(leftClasses)
allRightClasses.update(rightClasses)
elif lookup.LookupType == 9:
extSubtable = subtable.ExtSubTable
format = extSubtable.Format
lookupType = extSubtable.LookupType
if (lookupType, format) == (2, 1):
kerning = _handleLookupType2Format1(extSubtable)
allKerning.update(kerning)
elif (lookupType, format) == (2, 2):
kerning, leftClasses, rightClasses = _handleLookupType2Format2(extSubtable, lookupIndex, subtableIndex)
allKerning.update(kerning)
allLeftClasses.update(leftClasses)
allRightClasses.update(rightClasses)
# done
return allKerning, allLeftClasses, allRightClasses
def _handleLookupType2Format1(subtable):
"""
Extract a kerning dictionary from a Lookup Type 2 Format 1.
"""
kerning = {}
coverage = subtable.Coverage.glyphs
valueFormat1 = subtable.ValueFormat1
pairSets = subtable.PairSet
for index, leftGlyphName in enumerate(coverage):
pairSet = pairSets[index]
for pairValueRecord in pairSet.PairValueRecord:
rightGlyphName = pairValueRecord.SecondGlyph
if valueFormat1:
value = pairValueRecord.Value1
else:
value = pairValueRecord.Value2
if hasattr(value, "XAdvance"):
value = value.XAdvance
kerning[leftGlyphName, rightGlyphName] = value
return kerning
def _handleLookupType2Format2(subtable, lookupIndex, subtableIndex):
"""
Extract kerning, left class and right class dictionaries from a Lookup Type 2 Format 2.
"""
# extract the classes
leftClasses = _extractFeatureClasses(lookupIndex=lookupIndex, subtableIndex=subtableIndex, classDefs=subtable.ClassDef1.classDefs, coverage=subtable.Coverage.glyphs)
rightClasses = _extractFeatureClasses(lookupIndex=lookupIndex, subtableIndex=subtableIndex, classDefs=subtable.ClassDef2.classDefs)
# extract the pairs
kerning = {}
for class1RecordIndex, class1Record in enumerate(subtable.Class1Record):
for class2RecordIndex, class2Record in enumerate(class1Record.Class2Record):
leftClass = (lookupIndex, subtableIndex, class1RecordIndex)
rightClass = (lookupIndex, subtableIndex, class2RecordIndex)
valueFormat1 = subtable.ValueFormat1
if valueFormat1:
value = class2Record.Value1
else:
value = class2Record.Value2
if hasattr(value, "XAdvance") and value.XAdvance != 0:
value = value.XAdvance
kerning[leftClass, rightClass] = value
return kerning, leftClasses, rightClasses
def _mergeKerningDictionaries(kerningDictionaries):
"""
Merge all of the kerning dictionaries found into
one flat dictionary.
"""
# work through the dictionaries backwards since
# this uses an update to load the kerning. this
# will ensure that the script order is honored.
kerning = {}
for dictionaryGroup in reversed(kerningDictionaries):
for dictionary in dictionaryGroup:
kerning.update(dictionary)
# done.
return kerning
def _findSingleMemberGroups(classDictionaries):
"""
Find all classes that have only one member.
"""
toRemove = {}
for classDictionaryGroup in classDictionaries:
for classDictionary in classDictionaryGroup:
for name, members in list(classDictionary.items()):
if len(members) == 1:
toRemove[name] = list(members)[0]
del classDictionary[name]
return toRemove
def _removeSingleMemberGroupReferences(kerning, leftGroups, rightGroups):
"""
Translate group names into glyph names in pairs
if the group only contains one glyph.
"""
new = {}
for (left, right), value in kerning.items():
left = leftGroups.get(left, left)
right = rightGroups.get(right, right)
new[left, right] = value
return new
def _mergeClasses(classDictionaries):
"""
Look for classes that have the exact same list
of members and flag them for removal.
This returns left classes, left rename map,
right classes and right rename map.
The classes have the standard class structure.
The rename maps have this structure:
{
(1, 1, 3) : (2, 3, 4),
old name : new name
}
Where the key is the class that should be
preserved and the value is a list of classes
that should be removed.
"""
# build a mapping of members to names
memberTree = {}
for classDictionaryGroup in classDictionaries:
for classDictionary in classDictionaryGroup:
for name, members in classDictionary.items():
if members not in memberTree:
memberTree[members] = set()
memberTree[members].add(name)
# find members that have more than one name
classes = {}
rename = {}
for members, names in memberTree.items():
name = names.pop()
if len(names) > 0:
for otherName in names:
rename[otherName] = name
classes[name] = members
return classes, rename
def _setGroupNames(classes, classRename):
"""
Set the final names into the groups.
"""
groups = {}
for groupName, glyphList in classes.items():
groupName = classRename.get(groupName, groupName)
# if the glyph list has only one member,
# the glyph name will be used in the pairs.
# no group is needed.
if len(glyphList) == 1:
continue
groups[groupName] = glyphList
return groups
def _validateClasses(classes):
"""
Check to make sure that a glyph is not part of more than
one class. If this is found, an ExtractorError is raised.
"""
glyphToClass = {}
for className, glyphList in classes.items():
for glyphName in glyphList:
if glyphName not in glyphToClass:
glyphToClass[glyphName] = set()
glyphToClass[glyphName].add(className)
conflicts = 0
for glyphName, groupList in glyphToClass.items():
if len(groupList) > 1:
print('Conflicting kerning classes for %s:' % glyphName)
for groupId in groupList:
group = classes[groupId]
print(' %r => %s' % (groupId, ', '.join(group)))
conflicts += 1
if conflicts > 0:
raise ExtractorError("Kerning classes are in an conflicting state")
def _replaceRenamedPairMembers(kerning, leftRename, rightRename):
"""
Populate the renamed pair members into the kerning.
"""
renamedKerning = {}
for (left, right), value in kerning.items():
left = leftRename.get(left, left)
right = rightRename.get(right, right)
renamedKerning[left, right] = value
return renamedKerning
def _renameClasses(classes, prefix):
"""
Replace class IDs with nice strings.
"""
renameMap = {}
for classID, glyphList in classes.items():
if len(glyphList) == 0:
groupName = "%s_empty_lu.%d_st.%d_cl.%d" % (prefix, classID[0], classID[1], classID[2])
elif len(glyphList) == 1:
groupName = list(glyphList)[0]
else:
glyphList = list(sorted(glyphList))
groupName = prefix + glyphList[0]
renameMap[classID] = groupName
return renameMap
def _extractFeatureClasses(lookupIndex, subtableIndex, classDefs, coverage=None):
"""
Extract classes for a specific lookup in a specific subtable.
This is relatively straightforward, except for class 0 interpretation.
Some fonts don't have class 0. Some fonts have a list of class
members that are clearly not all to be used in kerning pairs.
In the case of a missing class 0, the coverage is used as a basis
for the class and glyph names used in classed 1+ are filtered out.
In the case of class 0 having glyph names that are not part of the
kerning pairs, the coverage is used to filter out the unnecessary
glyph names.
"""
# gather the class members
classDict = {}
for glyphName, classIndex in classDefs.items():
if classIndex not in classDict:
classDict[classIndex] = set()
classDict[classIndex].add(glyphName)
# specially handle class index 0
revisedClass0 = set()
if coverage is not None and 0 in classDict:
for glyphName in classDict[0]:
if glyphName in coverage:
revisedClass0.add(glyphName)
elif coverage is not None and 0 not in classDict:
revisedClass0 = set(coverage)
for glyphList in classDict.values():
revisedClass0 = revisedClass0 - glyphList
classDict[0] = revisedClass0
# flip the class map around
classes = {}
for classIndex, glyphList in classDict.items():
classes[lookupIndex, subtableIndex, classIndex] = frozenset(glyphList)
return classes
def _extractOpenTypeKerningFromKern(source):
kern = source["kern"]
kerning = {}
for subtable in kern.kernTables:
if subtable.version != 0:
raise ExtractorError("Unknown kern table formst: %d" % subtable.version)
# XXX the spec defines coverage values for
# kerning direction (horizontal or vertical)
# minimum (some sort of kerning restriction)
# cross-stream (direction of the kerns within the direction of the table. odd.)
# override (if the values in this subtable should override the values of others)
# however, it is vague about how these should be stored.
# as such, we just assume that the direction is horizontal,
# that the values of all subtables are additive and that
# there are no minimum values.
kerning.update(subtable.kernTable)
return kerning

View file

@ -1,28 +0,0 @@
from extractor.formats.opentype import extractOpenTypeInfo, extractOpenTypeGlyphs, extractOpenTypeKerning
def isTTX(pathOrFile):
from fontTools.ttLib import TTFont, TTLibError
try:
font = TTFont()
font.importXML(pathOrFile)
del font
except TTLibError:
return False
return True
def extractFontFromTTX(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
from fontTools.ttLib import TTFont, TTLibError
source = TTFont()
source.importXML(pathOrFile)
if doInfo:
extractOpenTypeInfo(source, destination)
if doGlyphs:
extractOpenTypeGlyphs(source, destination)
if doKerning:
kerning, groups = extractOpenTypeKerning(source, destination)
destination.groups.update(groups)
destination.kerning.clear()
destination.kerning.update(kerning)
for function in customFunctions:
function(source, destination)
source.close()

View file

@ -1,164 +0,0 @@
from fontTools.t1Lib import T1Font, T1Error
from fontTools.agl import AGL2UV
from fontTools.misc.psLib import PSInterpreter
from fontTools.misc.transform import Transform
from extractor.tools import RelaxedInfo
# specification: http://partners.adobe.com/public/developer/en/font/T1_SPEC.PDF
# ----------------
# Public Functions
# ----------------
def isType1(pathOrFile):
try:
font = T1Font(pathOrFile)
del font
except T1Error:
return False
return True
def extractFontFromType1(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
source = T1Font(pathOrFile)
destination.lib["public.glyphOrder"] = _extractType1GlyphOrder(source)
if doInfo:
extractType1Info(source, destination)
if doGlyphs:
extractType1Glyphs(source, destination)
if doKerning:
# kerning extraction is not supported yet.
# in theory, it could be retried from an AFM.
# we need to find the AFM naming rules so that we can sniff for the file.
pass
for function in customFunctions:
function(source, destination)
def extractType1Info(source, destination):
info = RelaxedInfo(destination.info)
_extractType1FontInfo(source, info)
_extractType1Private(source, info)
_extractType1FontMatrix(source, info)
# ----
# Info
# ----
def _extractType1FontInfo(source, info):
sourceInfo = source["FontInfo"]
# FontName
info.postscriptFontName = source["FontName"]
# version
version = sourceInfo.get("version")
if version is not None:
# the spec says that version will be a string and no formatting info is given.
# so, only move forward if the string can actually be parsed.
try:
# 1. convert to a float
version = float(version)
# 2. convert it back to a string
version = "%.3f" % version
# 3. split.
versionMajor, versionMinor = version.split(".")
# 4. convert.
versionMajor = int(versionMajor)
versionMinor = int(versionMinor)
# 5. set.
info.versionMajor = int(versionMajor)
info.versionMinor = int(versionMinor)
except ValueError:
# couldn't parse. leve the object with the default values.
pass
# Notice
notice = sourceInfo.get("Notice")
if notice:
info.copyright = notice
# FullName
fullName = sourceInfo.get("FullName")
if fullName:
info.postscriptFullName = fullName
# FamilyName
familyName = sourceInfo.get("FamilyName")
if familyName:
info.familyName = familyName
# Weight
postscriptWeightName = sourceInfo.get("Weight")
if postscriptWeightName:
info.postscriptWeightName = postscriptWeightName
# ItalicAngle
info.italicAngle = sourceInfo.get("ItalicAngle")
# IsFixedPitch
info.postscriptIsFixedPitch = sourceInfo.get("isFixedPitch")
# UnderlinePosition/Thickness
info.postscriptUnderlinePosition = sourceInfo.get("UnderlinePosition")
info.postscriptUnderlineThickness = sourceInfo.get("UnderlineThickness")
def _extractType1FontMatrix(source, info):
# units per em
matrix = source["FontMatrix"]
matrix = Transform(*matrix).inverse()
info.unitsPerEm = int(round(matrix[3]))
def _extractType1Private(source, info):
private = source["Private"]
# UniqueID
info.openTypeNameUniqueID = private.get("UniqueID", None)
# BlueValues and OtherBlues
info.postscriptBlueValues = private.get("BlueValues", [])
info.postscriptOtherBlues = private.get("OtherBlues", [])
# FamilyBlues and FamilyOtherBlues
info.postscriptFamilyBlues = private.get("FamilyBlues", [])
info.postscriptFamilyOtherBlues = private.get("FamilyOtherBlues", [])
# BlueScale/Shift/Fuzz
info.postscriptBlueScale = private.get("BlueScale", None)
info.postscriptBlueShift = private.get("BlueShift", None)
info.postscriptBlueFuzz = private.get("BlueFuzz", None)
# StemSnapH/V
info.postscriptStemSnapH = private.get("StemSnapH", [])
info.postscriptStemSnapV = private.get("StemSnapV", [])
# ForceBold
info.postscriptForceBold = bool(private.get("ForceBold", None))
# --------
# Outlines
# --------
def extractType1Glyphs(source, destination):
glyphSet = source.getGlyphSet()
for glyphName in sorted(glyphSet.keys()):
sourceGlyph = glyphSet[glyphName]
# make the new glyph
destination.newGlyph(glyphName)
destinationGlyph = destination[glyphName]
# outlines
pen = destinationGlyph.getPen()
sourceGlyph.draw(pen)
# width
destinationGlyph.width = sourceGlyph.width
# synthesize the unicode value
destinationGlyph.unicode = AGL2UV.get(glyphName)
# -----------
# Glyph order
# -----------
class GlyphOrderPSInterpreter(PSInterpreter):
def __init__(self):
PSInterpreter.__init__(self)
self.glyphOrder = []
self.collectTokenForGlyphOrder = False
def do_literal(self, token):
result = PSInterpreter.do_literal(self, token)
if token == "/FontName":
self.collectTokenForGlyphOrder = False
if self.collectTokenForGlyphOrder:
self.glyphOrder.append(result.value)
if token == "/CharStrings":
self.collectTokenForGlyphOrder = True
return result
def _extractType1GlyphOrder(t1Font):
interpreter = GlyphOrderPSInterpreter()
interpreter.interpret(t1Font.data)
return interpreter.glyphOrder

View file

@ -1,222 +0,0 @@
from xml.sax.saxutils import quoteattr
from fontTools.ttLib import TTFont, TTLibError
from extractor.tools import RelaxedInfo
from extractor.formats.opentype import extractOpenTypeInfo, extractOpenTypeGlyphs, extractOpenTypeKerning
try:
from xml.etree import cElementTree as ElementTree
except ImportError:
from xml.etree import ElementTree
# ----------------
# Public Functions
# ----------------
def isWOFF(pathOrFile):
flavor = None
try:
font = TTFont(pathOrFile)
flavor = font.flavor
del font
except TTLibError:
return False
return flavor in ("woff", "woff2")
def extractFontFromWOFF(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
source = TTFont(pathOrFile)
if doInfo:
extractWOFFInfo(source, destination)
if doGlyphs:
extractWOFFGlyphs(source, destination)
if doKerning:
kerning, groups = extractWOFFKerning(source, destination)
destination.groups.update(groups)
destination.kerning.clear()
destination.kerning.update(kerning)
for function in customFunctions:
function(source, destination)
source.close()
# ----------------
# Specific Imports
# ----------------
def extractWOFFInfo(source, destination):
info = RelaxedInfo(destination.info)
info.woffMajorVersion = source.flavorData.majorVersion
info.woffMinorVersion = source.flavorData.minorVersion
_extractWOFFMetadata(source.flavorData, info)
return extractOpenTypeInfo(source, destination)
def extractWOFFGlyphs(source, destination):
return extractOpenTypeGlyphs(source, destination)
def extractWOFFKerning(source, destination):
return extractOpenTypeKerning(source, destination)
# --------
# Metadata
# --------
def _extractWOFFMetadata(source, destination):
if source.metaData is None:
return
metadata = ElementTree.fromstring(source.metaData)
for element in metadata:
if element.tag == "uniqueid":
_extractWOFFMetadataUniqueID(element, destination)
elif element.tag == "vendor":
_extractWOFFMetadataVendor(element, destination)
elif element.tag == "credits":
_extractWOFFMetadataCredits(element, destination)
elif element.tag == "description":
_extractWOFFMetadataDescription(element, destination)
elif element.tag == "license":
_extractWOFFMetadataLicense(element, destination)
elif element.tag == "copyright":
_extractWOFFMetadataCopyright(element, destination)
elif element.tag == "trademark":
_extractWOFFMetadataTrademark(element, destination)
elif element.tag == "licensee":
_extractWOFFMetadataLicensee(element, destination)
elif element.tag == "extension":
_extractWOFFMetadataExtension(element, destination)
def _extractWOFFMetadataUniqueID(element, destination):
destination.woffMetadataUniqueID = _extractWOFFMetadataDict(element, ("id",))
def _extractWOFFMetadataVendor(element, destination):
attributes = ("name", "url", "dir", "class")
record = _extractWOFFMetadataDict(element, attributes)
destination.woffMetadataVendor = record
def _extractWOFFMetadataCredits(element, destination):
attributes = ("name", "url", "role", "dir", "class")
credits = []
for subElement in element:
if subElement.tag == "credit":
record = _extractWOFFMetadataDict(subElement, attributes)
credits.append(record)
destination.woffMetadataCredits = dict(credits=credits)
def _extractWOFFMetadataDescription(element, destination):
description = _extractWOFFMetadataDict(element, ("url",))
textRecords = _extractWOFFMetadataText(element)
if textRecords:
description["text"] = textRecords
destination.woffMetadataDescription = description
def _extractWOFFMetadataLicense(element, destination):
license = _extractWOFFMetadataDict(element, ("url", "id"))
textRecords = _extractWOFFMetadataText(element)
if textRecords:
license["text"] = textRecords
destination.woffMetadataLicense = license
def _extractWOFFMetadataCopyright(element, destination):
copyright = {}
textRecords = _extractWOFFMetadataText(element)
if textRecords:
copyright["text"] = textRecords
destination.woffMetadataCopyright = copyright
def _extractWOFFMetadataTrademark(element, destination):
trademark = {}
textRecords = _extractWOFFMetadataText(element)
if textRecords:
trademark["text"] = textRecords
destination.woffMetadataTrademark = trademark
def _extractWOFFMetadataLicensee(element, destination):
destination.woffMetadataLicensee = _extractWOFFMetadataDict(element, ("name", "dir", "class"))
def _extractWOFFMetadataExtension(element, destination):
extension = _extractWOFFMetadataDict(element, ("id",))
for subElement in element:
if subElement.tag == "name":
if "names" not in extension:
extension["names"] = []
name = _extractWOFFMetadataExtensionName(subElement)
extension["names"].append(name)
elif subElement.tag == "item":
if "items" not in extension:
extension["items"] = []
item = _extractWOFFMetadataExtensionItem(subElement)
extension["items"].append(item)
extensions = []
if destination.woffMetadataExtensions:
extensions.extend(destination.woffMetadataExtensions)
destination.woffMetadataExtensions = extensions + [extension]
def _extractWOFFMetadataExtensionItem(element):
item = _extractWOFFMetadataDict(element, ("id",))
for subElement in element:
if subElement.tag == "name":
if "names" not in item:
item["names"] = []
name = _extractWOFFMetadataExtensionName(subElement)
item["names"].append(name)
elif subElement.tag == "value":
if "values" not in item:
item["values"] = []
name = _extractWOFFMetadataExtensionValue(subElement)
item["values"].append(name)
return item
def _extractWOFFMetadataExtensionName(element):
name = _extractWOFFMetadataDict(element, ("dir", "class"))
language = _extractWOFFMetadataLanguage(element)
if language is not None:
name["language"] = language
name["text"] = _flattenWOFFMetadataString(element)
return name
def _extractWOFFMetadataExtensionValue(element):
return _extractWOFFMetadataExtensionName(element)
# support
def _extractWOFFMetadataDict(element, attributes):
record = {}
for attribute in attributes:
value = element.attrib.get(attribute)
if value is not None:
record[attribute] = value
return record
def _extractWOFFMetadataText(element):
records = []
attributes = ("dir", "class")
for subElement in element:
record = _extractWOFFMetadataDict(subElement, attributes)
# text
record["text"] = _flattenWOFFMetadataString(subElement)
# language
language = _extractWOFFMetadataLanguage(subElement)
if language is not None:
record["language"] = language
records.append(record)
return records
def _extractWOFFMetadataLanguage(element):
language = element.attrib.get("{http://www.w3.org/XML/1998/namespace}lang")
if language is None:
language = element.attrib.get("lang")
return language
def _flattenWOFFMetadataString(element, sub=False):
text = element.text.strip()
for subElement in element:
text += _flattenWOFFMetadataString(subElement, sub=True)
if element.tail:
text += element.tail.strip()
if sub:
attrib = ["%s=%s" % (key, quoteattr(value)) for key, value in element.attrib.items()]
attrib = " ".join(attrib)
if attrib:
start = "<%s %s>" % (element.tag, attrib)
else:
start = "<%s>" % (element.tag)
end = "</%s>" % (element.tag)
text = start + text + end
return text

View file

@ -1,2 +0,0 @@
fonttools==3.3.1
ufoLib==2.0.0

View file

@ -1,32 +0,0 @@
from ufoLib import fontInfoAttributesVersion3, validateFontInfoVersion3ValueForAttribute
class RelaxedInfo(object):
"""
This object that sets only valid info values
into the given info object.
"""
def __init__(self, info):
self._object = info
def __getattr__(self, attr):
if attr in fontInfoAttributesVersion3:
return getattr(self._object, attr)
else:
return super(RelaxedInfo, self).__getattr__(attr)
def __setattr__(self, attr, value):
if attr in fontInfoAttributesVersion3:
if validateFontInfoVersion3ValueForAttribute(attr, value):
setattr(self._object, attr, value)
else:
super(RelaxedInfo, self).__setattr__(attr, value)
def copyAttr(src, srcAttr, dest, destAttr):
if not hasattr(src, srcAttr):
return
value = getattr(src, srcAttr)
setattr(dest, destAttr, value)

View file

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

View file

@ -1,365 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ConfigParser
import os
import sys
import math
import warnings
from booleanOperations import BooleanOperationManager
from cu2qu.ufo import fonts_to_quadratic
from fontTools.misc.transform import Transform
from robofab.world import OpenFont
from ufo2ft import compileOTF, compileTTF
from fontbuild.decomposeGlyph import decomposeGlyph
from fontbuild.features import readFeatureFile, writeFeatureFile
from fontbuild.generateGlyph import generateGlyph
from fontbuild.instanceNames import setInfoRF
from fontbuild.italics import italicizeGlyph
from fontbuild.markFeature import RobotoFeatureCompiler, RobotoKernWriter
from fontbuild.mitreGlyph import mitreGlyph
from fontbuild.mix import Mix,Master,narrowFLGlyph
class FontProject:
def __init__(self, basefont, basedir, configfile, buildTag=''):
self.basefont = basefont
self.basedir = basedir
self.config = ConfigParser.RawConfigParser()
self.configfile = os.path.join(self.basedir, configfile)
self.config.read(self.configfile)
self.buildTag = buildTag
self.diacriticList = [
line.strip() for line in self.openResource("diacriticfile")
if not line.startswith("#")]
self.adobeGlyphList = dict(
line.split(";") for line in self.openResource("agl_glyphlistfile")
if not line.startswith("#"))
self.glyphOrder = self.openResource("glyphorder")
# map exceptional glyph names in Roboto to names in the AGL
roboNames = (
('Obar', 'Ocenteredtilde'), ('obar', 'obarred'),
('eturn', 'eturned'), ('Iota1', 'Iotaafrican'))
for roboName, aglName in roboNames:
self.adobeGlyphList[roboName] = self.adobeGlyphList[aglName]
self.builddir = "out"
self.decompose = self.config.get("glyphs","decompose").split()
self.predecompose = set(self.config.get("glyphs","predecompose").split())
self.predecompose_auto = 1 # unless 0, automatically populate predecompose
self.lessItalic = set(self.config.get("glyphs","lessitalic").split())
self.deleteList = set(self.config.get("glyphs","delete").split())
self.noItalic = set(self.config.get("glyphs","noitalic").split())
self.buildOTF = False
self.compatible = False
self.generatedFonts = []
self.justCompileUFO = False
def openResource(self, name):
with open(os.path.join(
self.basedir, self.config.get("res", name))) as resourceFile:
resource = resourceFile.read()
return resource.splitlines()
def generateOutputPath(self, font, ext):
family = font.info.familyName.replace(" ", "")
style = font.info.styleName.replace(" ", "")
path = os.path.join(self.basedir, self.builddir, family + ext.upper())
if not os.path.exists(path):
os.makedirs(path)
return os.path.join(path, "%s-%s.%s" % (family, style, ext))
def generateFont(self, mix, names, italic=False, swapSuffixes=None, stemWidth=185,
italicMeanYCenter=-825, scaleX=1, panose=[]):
n = names.split("/")
log("---------------------\n%s, %s\n----------------------" %(n[0],n[1]))
if isinstance( mix, Mix):
log(">> Mixing masters")
f = mix.generateFont(self.basefont, self.deleteList)
else:
f = mix.copy()
if not self.justCompileUFO:
deleteGlyphs(f, self.deleteList)
if italic == True:
log(">> Italicizing")
i = 0
for g in f:
decomposeGlyph(f, g)
removeGlyphOverlap(g)
for g in f:
i += 1
if i % 10 == 0: print g.name
if g.name not in self.noItalic:
if g.name in self.lessItalic:
italicizeGlyph(f, g, 10, stemWidth=stemWidth,
meanYCenter=italicMeanYCenter,
scaleX=scaleX)
else:
italicizeGlyph(f, g, 12, stemWidth=stemWidth,
meanYCenter=italicMeanYCenter,
scaleX=scaleX)
# set the oblique flag in fsSelection
f.info.openTypeOS2Selection.append(9)
if swapSuffixes != None:
for swap in swapSuffixes:
swapList = [g.name for g in f if g.name.endswith(swap)]
for gname in swapList:
print gname
swapContours(f, gname.replace(swap,""), gname)
if self.predecompose_auto == 1:
self.predecompose_auto = 2
for g in self.basefont:
if len(g.components) > 0:
self.predecompose.add(g.name)
if not self.justCompileUFO:
for gname in self.predecompose:
if f.has_key(gname):
decomposeGlyph(f, f[gname])
log(">> Generating glyphs")
self.generateGlyphs(f, self.diacriticList, self.adobeGlyphList)
# log(">> Reading features")
# readFeatureFile(f, f.features.text)
# adjust width of italic glyphs
if italic == True:
widthAdjustment = -8
if widthAdjustment != 0:
leftAdjustment = math.floor(widthAdjustment / 2)
rightAdjustment = math.ceil(widthAdjustment / 2)
for g in f:
if g.name not in self.noItalic:
if g.width != 0:
if g.box is None:
g.width += widthAdjustment
else:
newLeftMargin = int(g.leftMargin + leftAdjustment)
newRightMargin = int(g.rightMargin + rightAdjustment)
g.leftMargin = newLeftMargin
g.rightMargin = newRightMargin
if not self.justCompileUFO:
log(">> Decomposing")
# for g in f:
# if len(g.components) > 0:
# decomposeGlyph(f, g)
for gname in self.decompose:
if f.has_key(gname):
decomposeGlyph(f, f[gname])
copyrightHolderName = ''
if self.config.has_option('main', 'copyrightHolderName'):
copyrightHolderName = self.config.get('main', 'copyrightHolderName')
def getcfg(name, fallback=''):
if self.config.has_option('main', name):
return self.config.get('main', name)
else:
return fallback
setInfoRF(f, n, {
'foundry': getcfg('foundry'),
'foundryURL': getcfg('foundryURL'),
'designer': getcfg('designer'),
'copyrightHolderName': getcfg('copyrightHolderName'),
'build': self.buildTag,
'version': getcfg('version'),
'license': getcfg('license'),
'licenseURL': getcfg('licenseURL'),
'italicAngle': float(getcfg('italicAngle', '-12')),
}, panose)
if not self.compatible and not self.justCompileUFO:
# Remove overlaps etc
cleanCurves(f)
# deleteGlyphs(f, self.deleteList)
log(">> Generating font files")
ufoName = self.generateOutputPath(f, "ufo")
f.save(ufoName)
self.generatedFonts.append(ufoName)
if self.justCompileUFO:
print("wrote %s" % ufoName)
return
# filter glyphorder -- only include glyphs that exists in font
glyphOrder = []
seenGlyphNames = set()
missingGlyphs = []
for glyphName in self.glyphOrder:
if glyphName in f:
if glyphName in seenGlyphNames:
raise Exception('Duplicate glyphname %r in glyphorder' % glyphName)
seenGlyphNames.add(glyphName)
glyphOrder.append(glyphName)
if self.buildOTF:
newFont = OpenFont(ufoName)
otfName = self.generateOutputPath(f, "otf")
log(">> Generating OTF file %s" % otfName)
saveOTF(newFont, otfName, glyphOrder)
def generateTTFs(self):
"""Build TTF for each font generated since last call to generateTTFs."""
fonts = [OpenFont(ufo) for ufo in self.generatedFonts]
self.generatedFonts = []
log(">> Converting curves to quadratic")
# using a slightly higher max error (e.g. 0.0025 em), dots will have
# fewer control points and look noticeably different
max_err = 0.001
if self.compatible:
fonts_to_quadratic(fonts, max_err_em=max_err, dump_stats=True, reverse_direction=True)
else:
for font in fonts:
fonts_to_quadratic([font], max_err_em=max_err, dump_stats=True, reverse_direction=True)
for font in fonts:
ttfName = self.generateOutputPath(font, "ttf")
log(">> Generating TTF file %s" % ttfName)
log(os.path.basename(ttfName))
glyphOrder = [n for n in self.glyphOrder if n in font]
saveOTF(font, ttfName, glyphOrder, truetype=True)
def generateGlyphs(self, f, glyphNames, glyphList={}):
log(">> Generating diacritics")
glyphnames = [gname for gname in glyphNames if not gname.startswith("#") and gname != ""]
for glyphName in glyphNames:
generateGlyph(f, glyphName, glyphList)
# def transformGlyphMembers(g, m):
# g.width = int(g.width * m.a)
# g.Transform(m)
# for a in g.anchors:
# p = Point(a.p)
# p.Transform(m)
# a.p = p
# for c in g.components:
# # Assumes that components have also been individually transformed
# p = Point(0,0)
# d = Point(c.deltas[0])
# d.Transform(m)
# p.Transform(m)
# d1 = d - p
# c.deltas[0].x = d1.x
# c.deltas[0].y = d1.y
# s = Point(c.scale)
# s.Transform(m)
# #c.scale = s
def swapContours(f,gName1,gName2):
try:
g1 = f[gName1]
g2 = f[gName2]
except KeyError:
log("swapGlyphs failed for %s %s" % (gName1, gName2))
return
g3 = g1.copy()
while g1.contours:
g1.removeContour(0)
for contour in g2.contours:
g1.appendContour(contour)
g1.width = g2.width
while g2.contours:
g2.removeContour(0)
for contour in g3.contours:
g2.appendContour(contour)
g2.width = g3.width
def log(msg):
print msg
def deleteGlyphs(f, deleteList):
for name in deleteList:
if f.has_key(name):
f.removeGlyph(name)
def cleanCurves(f):
log(">> Removing overlaps")
for g in f:
if len(g.components) > 0:
decomposeGlyph(f, g)
removeGlyphOverlap(g)
# log(">> Mitring sharp corners")
# for g in f:
# mitreGlyph(g, 3., .7)
# log(">> Converting curves to quadratic")
# for g in f:
# glyphCurvesToQuadratic(g)
def removeGlyphOverlap(g):
"""Remove overlaps in contours from a glyph."""
# Note: Although it theoretically would be more efficient to first check
# if contours has overlap before applying clipping, boolean ops and
# re-drawing the shapes, the booleanOperations library's getIntersections
# function adds more overhead in the real world, compared to bluntly
# computing the union for every single glyph.
#
# If we can find a really cheap/efficient way to check if there's any
# overlap, then we should do that before going ahead and computing the
# union. Keep in mind that some glyphs have just a single contour that
# intersects itself, for instance "e".
contours = g.contours
g.clearContours()
BooleanOperationManager.union(contours, g.getPointPen())
def saveOTF(font, destFile, glyphOrder, truetype=False):
"""Save a RoboFab font as an OTF binary using ufo2fdk."""
with warnings.catch_warnings():
# Note: ignore warnings produced by compreffor, used by ufo2ft:
# 'Compreffor' class is deprecated; use 'compress' function instead
# which is triggered by ufo2ft which we can't update because newer
# versions completely break the API of ufo2ft.
warnings.simplefilter("ignore")
if truetype:
otf = compileTTF(font, featureCompilerClass=RobotoFeatureCompiler,
kernWriter=RobotoKernWriter, glyphOrder=glyphOrder,
convertCubics=False,
useProductionNames=False)
else:
otf = compileOTF(font, featureCompilerClass=RobotoFeatureCompiler,
kernWriter=RobotoKernWriter, glyphOrder=glyphOrder,
useProductionNames=False)
otf.save(destFile)

View file

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1 +0,0 @@
https://github.com/google/roboto/tree/master/scripts/lib/fontbuild

View file

@ -1,6 +0,0 @@
"""
fontbuild
A collection of font production tools written for FontLab
"""
version = "0.1"

View file

@ -1,178 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import numpy as np
from numpy.linalg import lstsq
def alignCorners(glyph, va, subsegments):
out = va.copy()
# for i,c in enumerate(subsegments):
# segmentCount = len(glyph.contours[i].segments) - 1
# n = len(c)
# for j,s in enumerate(c):
# if j < segmentCount:
# seg = glyph.contours[i].segments[j]
# if seg.type == "line":
# subIndex = subsegmentIndex(i,j,subsegments)
# out[subIndex] = alignPoints(va[subIndex])
for i,c in enumerate(subsegments):
segmentCount = len(glyph.contours[i].segments)
n = len(c)
for j,s in enumerate(c):
if j < segmentCount - 1:
segType = glyph.contours[i].segments[j].type
segnextType = glyph.contours[i].segments[j+1].type
next = j+1
elif j == segmentCount -1 and s[1] > 3:
segType = glyph.contours[i].segments[j].type
segNextType = "line"
next = j+1
elif j == segmentCount:
segType = "line"
segnextType = glyph.contours[i].segments[1].type
if glyph.name == "J":
print s[1]
print segnextType
next = 1
else:
break
if segType == "line" and segnextType == "line":
subIndex = subsegmentIndex(i,j,subsegments)
pts = va[subIndex]
ptsnext = va[subsegmentIndex(i,next,subsegments)]
# out[subIndex[-1]] = (out[subIndex[-1]] - 500) * 3 + 500 #findCorner(pts, ptsnext)
# print subIndex[-1], subIndex, subsegmentIndex(i,next,subsegments)
try:
out[subIndex[-1]] = findCorner(pts, ptsnext)
except:
pass
# print glyph.name, "Can't find corner: parallel lines"
return out
def subsegmentIndex(contourIndex, segmentIndex, subsegments):
# This whole thing is so dumb. Need a better data model for subsegments
contourOffset = 0
for i,c in enumerate(subsegments):
if i == contourIndex:
break
contourOffset += c[-1][0]
n = subsegments[contourIndex][-1][0]
# print contourIndex, contourOffset, n
startIndex = subsegments[contourIndex][segmentIndex-1][0]
segmentCount = subsegments[contourIndex][segmentIndex][1]
endIndex = (startIndex + segmentCount + 1) % (n)
indices = np.array([(startIndex + i) % (n) + contourOffset for i in range(segmentCount + 1)])
return indices
def alignPoints(pts, start=None, end=None):
if start == None or end == None:
start, end = fitLine(pts)
out = pts.copy()
for i,p in enumerate(pts):
out[i] = nearestPoint(start, end, p)
return out
def findCorner(pp, nn):
if len(pp) < 4 or len(nn) < 4:
assert 0, "line too short to fit"
pStart,pEnd = fitLine(pp)
nStart,nEnd = fitLine(nn)
prev = pEnd - pStart
next = nEnd - nStart
# print int(np.arctan2(prev[1],prev[0]) / math.pi * 180),
# print int(np.arctan2(next[1],next[0]) / math.pi * 180)
# if lines are parallel, return simple average of end and start points
if np.dot(prev / np.linalg.norm(prev),
next / np.linalg.norm(next)) > .999999:
# print "parallel lines", np.arctan2(prev[1],prev[0]), np.arctan2(next[1],next[0])
# print prev, next
assert 0, "parallel lines"
# if glyph.name is None:
# # Never happens, but here to fix a bug in Python 2.7 with -OO
# print ''
return lineIntersect(pStart, pEnd, nStart, nEnd)
def lineIntersect(p1, p2, p3, p4):
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
x4, y4 = p4
x12 = x1 - x2
x34 = x3 - x4
y12 = y1 - y2
y34 = y3 - y4
det = x12 * y34 - y12 * x34
if det == 0:
print "parallel!"
a = x1 * y2 - y1 * x2
b = x3 * y4 - y3 * x4
x = (a * x34 - b * x12) / det
y = (a * y34 - b * y12) / det
return (x,y)
def fitLineLSQ(pts):
"returns a line fit with least squares. Fails for vertical lines"
n = len(pts)
a = np.ones((n,2))
for i in range(n):
a[i,0] = pts[i,0]
line = lstsq(a,pts[:,1])[0]
return line
def fitLine(pts):
"""returns a start vector and direction vector
Assumes points segments that already form a somewhat smooth line
"""
n = len(pts)
if n < 1:
return (0,0),(0,0)
a = np.zeros((n-1,2))
for i in range(n-1):
v = pts[i] - pts[i+1]
a[i] = v / np.linalg.norm(v)
direction = np.mean(a[1:-1], axis=0)
start = np.mean(pts[1:-1], axis=0)
return start, start+direction
def nearestPoint(a,b,c):
"nearest point to point c on line a_b"
magnitude = np.linalg.norm(b-a)
if magnitude == 0:
raise Exception, "Line segment cannot be 0 length"
return (b-a) * np.dot((c-a) / magnitude, (b-a) / magnitude) + a
# pts = np.array([[1,1],[2,2],[3,3],[4,4]])
# pts2 = np.array([[1,0],[2,0],[3,0],[4,0]])
# print alignPoints(pts2, start = pts[0], end = pts[0]+pts[0])
# # print findCorner(pts,pts2)

View file

@ -1,77 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def getGlyph(gname, font):
return font[gname] if font.has_key(gname) else None
def getComponentByName(f, g, componentName):
for c in g.components:
if c.baseGlyph == componentName:
return c
def getAnchorByName(g,anchorName):
for a in g.anchors:
if a.name == anchorName:
return a
def moveMarkAnchors(f, g, anchorName, accentName, dx, dy):
if "top"==anchorName:
anchors = f[accentName].anchors
for anchor in anchors:
if "mkmktop_acc" == anchor.name:
for anc in g.anchors:
if anc.name == "top":
g.removeAnchor(anc)
break
g.appendAnchor("top", (anchor.x + int(dx), anchor.y + int(dy)))
elif anchorName in ["bottom", "bottomu"]:
anchors = f[accentName].anchors
for anchor in anchors:
if "mkmkbottom_acc" == anchor.name:
for anc in g.anchors:
if anc.name == "bottom":
g.removeAnchor(anc)
break
x = anchor.x + int(dx)
for anc in anchors:
if "top" == anc.name:
x = anc.x + int(dx)
g.appendAnchor("bottom", (x, anchor.y + int(dy)))
def alignComponentToAnchor(f,glyphName,baseName,accentName,anchorName):
g = getGlyph(glyphName,f)
base = getGlyph(baseName,f)
accent = getGlyph(accentName,f)
if g == None or base == None or accent == None:
return
a1 = getAnchorByName(base,anchorName)
a2 = getAnchorByName(accent,"_" + anchorName)
if a1 == None or a2 == None:
return
offset = (a1.x - a2.x, a1.y - a2.y)
c = getComponentByName(f, g, accentName)
c.offset = offset
moveMarkAnchors(f, g, anchorName, accentName, offset[0], offset[1])
def alignComponentsToAnchors(f,glyphName,baseName,accentNames):
for a in accentNames:
if len(a) == 1:
continue
alignComponentToAnchor(f,glyphName,baseName,a[0],a[1])

View file

@ -1,102 +0,0 @@
#! /usr/bin/env python
#
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Converts a cubic bezier curve to a quadratic spline with
exactly two off curve points.
"""
import numpy
from numpy import array,cross,dot
from fontTools.misc import bezierTools
from robofab.objects.objectsRF import RSegment
def replaceSegments(contour, segments):
while len(contour):
contour.removeSegment(0)
for s in segments:
contour.appendSegment(s.type, [(p.x, p.y) for p in s.points], s.smooth)
def calcIntersect(a,b,c,d):
numpy.seterr(all='raise')
e = b-a
f = d-c
p = array([-e[1], e[0]])
try:
h = dot((a-c),p) / dot(f,p)
except:
print a,b,c,d
raise
return c + dot(f,h)
def simpleConvertToQuadratic(p0,p1,p2,p3):
p = [array(i.x,i.y) for i in [p0,p1,p2,p3]]
off = calcIntersect(p[0],p[1],p[2],p[3])
# OFFCURVE_VECTOR_CORRECTION = -.015
OFFCURVE_VECTOR_CORRECTION = 0
def convertToQuadratic(p0,p1,p2,p3):
# TODO: test for accuracy and subdivide further if needed
p = [(i.x,i.y) for i in [p0,p1,p2,p3]]
# if p[0][0] == p[1][0] and p[0][0] == p[2][0] and p[0][0] == p[2][0] and p[0][0] == p[3][0]:
# return (p[0],p[1],p[2],p[3])
# if p[0][1] == p[1][1] and p[0][1] == p[2][1] and p[0][1] == p[2][1] and p[0][1] == p[3][1]:
# return (p[0],p[1],p[2],p[3])
seg1,seg2 = bezierTools.splitCubicAtT(p[0], p[1], p[2], p[3], .5)
pts1 = [array([i[0], i[1]]) for i in seg1]
pts2 = [array([i[0], i[1]]) for i in seg2]
on1 = seg1[0]
on2 = seg2[3]
try:
off1 = calcIntersect(pts1[0], pts1[1], pts1[2], pts1[3])
off2 = calcIntersect(pts2[0], pts2[1], pts2[2], pts2[3])
except:
return (p[0],p[1],p[2],p[3])
off1 = (on1 - off1) * OFFCURVE_VECTOR_CORRECTION + off1
off2 = (on2 - off2) * OFFCURVE_VECTOR_CORRECTION + off2
return (on1,off1,off2,on2)
def cubicSegmentToQuadratic(c,sid):
segment = c[sid]
if (segment.type != "curve"):
print "Segment type not curve"
return
#pSegment,junk = getPrevAnchor(c,sid)
pSegment = c[sid-1] #assumes that a curve type will always be proceeded by another point on the same contour
points = convertToQuadratic(pSegment.points[-1],segment.points[0],
segment.points[1],segment.points[2])
return RSegment(
'qcurve', [[int(i) for i in p] for p in points[1:]], segment.smooth)
def glyphCurvesToQuadratic(g):
for c in g:
segments = []
for i in range(len(c)):
s = c[i]
if s.type == "curve":
try:
segments.append(cubicSegmentToQuadratic(c, i))
except Exception:
print g.name, i
raise
else:
segments.append(s)
replaceSegments(c, segments)

View file

@ -1,422 +0,0 @@
#! /opt/local/bin/pythonw2.7
#
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = ["SubsegmentPen","SubsegmentsToCurvesPen", "segmentGlyph", "fitGlyph"]
from fontTools.pens.basePen import BasePen
import numpy as np
from numpy import array as v
from numpy.linalg import norm
from robofab.pens.adapterPens import GuessSmoothPointPen
from robofab.pens.pointPen import BasePointToSegmentPen
class SubsegmentsToCurvesPointPen(BasePointToSegmentPen):
def __init__(self, glyph, subsegmentGlyph, subsegments):
BasePointToSegmentPen.__init__(self)
self.glyph = glyph
self.subPen = SubsegmentsToCurvesPen(None, glyph.getPen(), subsegmentGlyph, subsegments)
def setMatchTangents(self, b):
self.subPen.matchTangents = b
def _flushContour(self, segments):
#
# adapted from robofab.pens.adapterPens.rfUFOPointPen
#
assert len(segments) >= 1
# if we only have one point and it has a name, we must have an anchor
first = segments[0]
segmentType, points = first
pt, smooth, name, kwargs = points[0]
if len(segments) == 1 and name != None:
self.glyph.appendAnchor(name, pt)
return
else:
segmentType, points = segments[-1]
movePt, smooth, name, kwargs = points[-1]
if smooth:
# last point is smooth, set pen to start smooth
self.subPen.setLastSmooth(True)
if segmentType == 'line':
del segments[-1]
self.subPen.moveTo(movePt)
# do the rest of the segments
for segmentType, points in segments:
isSmooth = True in [smooth for pt, smooth, name, kwargs in points]
pp = [pt for pt, smooth, name, kwargs in points]
if segmentType == "line":
assert len(pp) == 1
if isSmooth:
self.subPen.smoothLineTo(pp[0])
else:
self.subPen.lineTo(pp[0])
elif segmentType == "curve":
assert len(pp) == 3
if isSmooth:
self.subPen.smoothCurveTo(*pp)
else:
self.subPen.curveTo(*pp)
elif segmentType == "qcurve":
assert 0, "qcurve not supported"
else:
assert 0, "illegal segmentType: %s" % segmentType
self.subPen.closePath()
def addComponent(self, glyphName, transform):
self.subPen.addComponent(glyphName, transform)
class SubsegmentsToCurvesPen(BasePen):
def __init__(self, glyphSet, otherPen, subsegmentGlyph, subsegments):
BasePen.__init__(self, None)
self.otherPen = otherPen
self.ssglyph = subsegmentGlyph
self.subsegments = subsegments
self.contourIndex = -1
self.segmentIndex = -1
self.lastPoint = (0,0)
self.lastSmooth = False
self.nextSmooth = False
def setLastSmooth(self, b):
self.lastSmooth = b
def _moveTo(self, a):
self.contourIndex += 1
self.segmentIndex = 0
self.startPoint = a
p = self.ssglyph.contours[self.contourIndex][0].points[0]
self.otherPen.moveTo((p.x, p.y))
self.lastPoint = a
def _lineTo(self, a):
self.segmentIndex += 1
index = self.subsegments[self.contourIndex][self.segmentIndex][0]
p = self.ssglyph.contours[self.contourIndex][index].points[0]
self.otherPen.lineTo((p.x, p.y))
self.lastPoint = a
self.lastSmooth = False
def smoothLineTo(self, a):
self.lineTo(a)
self.lastSmooth = True
def smoothCurveTo(self, a, b, c):
self.nextSmooth = True
self.curveTo(a, b, c)
self.nextSmooth = False
self.lastSmooth = True
def _curveToOne(self, a, b, c):
self.segmentIndex += 1
c = self.ssglyph.contours[self.contourIndex]
n = len(c)
startIndex = (self.subsegments[self.contourIndex][self.segmentIndex-1][0])
segmentCount = (self.subsegments[self.contourIndex][self.segmentIndex][1])
endIndex = (startIndex + segmentCount + 1) % (n)
indices = [(startIndex + i) % (n) for i in range(segmentCount + 1)]
points = np.array([(c[i].points[0].x, c[i].points[0].y) for i in indices])
prevPoint = (c[(startIndex - 1)].points[0].x, c[(startIndex - 1)].points[0].y)
nextPoint = (c[(endIndex) % n].points[0].x, c[(endIndex) % n].points[0].y)
prevTangent = prevPoint - points[0]
nextTangent = nextPoint - points[-1]
tangent1 = points[1] - points[0]
tangent3 = points[-2] - points[-1]
prevTangent /= np.linalg.norm(prevTangent)
nextTangent /= np.linalg.norm(nextTangent)
tangent1 /= np.linalg.norm(tangent1)
tangent3 /= np.linalg.norm(tangent3)
tangent1, junk = self.smoothTangents(tangent1, prevTangent, self.lastSmooth)
tangent3, junk = self.smoothTangents(tangent3, nextTangent, self.nextSmooth)
if self.matchTangents == True:
cp = fitBezier(points, tangent1, tangent3)
cp[1] = norm(cp[1] - cp[0]) * tangent1 / norm(tangent1) + cp[0]
cp[2] = norm(cp[2] - cp[3]) * tangent3 / norm(tangent3) + cp[3]
else:
cp = fitBezier(points)
# if self.ssglyph.name == 'r':
# print "-----------"
# print self.lastSmooth, self.nextSmooth
# print "%i %i : %i %i \n %i %i : %i %i \n %i %i : %i %i"%(x1,y1, cp[1,0], cp[1,1], x2,y2, cp[2,0], cp[2,1], x3,y3, cp[3,0], cp[3,1])
self.otherPen.curveTo((cp[1,0], cp[1,1]), (cp[2,0], cp[2,1]), (cp[3,0], cp[3,1]))
self.lastPoint = c
self.lastSmooth = False
def smoothTangents(self,t1,t2,forceSmooth = False):
if forceSmooth or (abs(t1.dot(t2)) > .95 and norm(t1-t2) > 1):
# print t1,t2,
t1 = (t1 - t2) / 2
t2 = -t1
# print t1,t2
return t1 / norm(t1), t2 / norm(t2)
def _closePath(self):
self.otherPen.closePath()
def _endPath(self):
self.otherPen.endPath()
def addComponent(self, glyphName, transformation):
self.otherPen.addComponent(glyphName, transformation)
class SubsegmentPointPen(BasePointToSegmentPen):
def __init__(self, glyph, resolution):
BasePointToSegmentPen.__init__(self)
self.glyph = glyph
self.resolution = resolution
self.subPen = SubsegmentPen(None, glyph.getPen())
def getSubsegments(self):
return self.subPen.subsegments[:]
def _flushContour(self, segments):
#
# adapted from robofab.pens.adapterPens.rfUFOPointPen
#
assert len(segments) >= 1
# if we only have one point and it has a name, we must have an anchor
first = segments[0]
segmentType, points = first
pt, smooth, name, kwargs = points[0]
if len(segments) == 1 and name != None:
self.glyph.appendAnchor(name, pt)
return
else:
segmentType, points = segments[-1]
movePt, smooth, name, kwargs = points[-1]
if segmentType == 'line':
del segments[-1]
self.subPen.moveTo(movePt)
# do the rest of the segments
for segmentType, points in segments:
points = [pt for pt, smooth, name, kwargs in points]
if segmentType == "line":
assert len(points) == 1
self.subPen.lineTo(points[0])
elif segmentType == "curve":
assert len(points) == 3
self.subPen.curveTo(*points)
elif segmentType == "qcurve":
assert 0, "qcurve not supported"
else:
assert 0, "illegal segmentType: %s" % segmentType
self.subPen.closePath()
def addComponent(self, glyphName, transform):
self.subPen.addComponent(glyphName, transform)
class SubsegmentPen(BasePen):
def __init__(self, glyphSet, otherPen, resolution=25):
BasePen.__init__(self,glyphSet)
self.resolution = resolution
self.otherPen = otherPen
self.subsegments = []
self.startContour = (0,0)
self.contourIndex = -1
def _moveTo(self, a):
self.contourIndex += 1
self.segmentIndex = 0
self.subsegments.append([])
self.subsegmentCount = 0
self.subsegments[self.contourIndex].append([self.subsegmentCount, 0])
self.startContour = a
self.lastPoint = a
self.otherPen.moveTo(a)
def _lineTo(self, a):
count = self.stepsForSegment(a,self.lastPoint)
if count < 1:
count = 1
self.subsegmentCount += count
self.subsegments[self.contourIndex].append([self.subsegmentCount, count])
for i in range(1,count+1):
x1 = self.lastPoint[0] + (a[0] - self.lastPoint[0]) * i/float(count)
y1 = self.lastPoint[1] + (a[1] - self.lastPoint[1]) * i/float(count)
self.otherPen.lineTo((x1,y1))
self.lastPoint = a
def _curveToOne(self, a, b, c):
count = self.stepsForSegment(c, self.lastPoint)
if count < 2:
count = 2
self.subsegmentCount += count
self.subsegments[self.contourIndex].append([self.subsegmentCount,count])
x = self.renderCurve((self.lastPoint[0],a[0],b[0],c[0]),count)
y = self.renderCurve((self.lastPoint[1],a[1],b[1],c[1]),count)
assert len(x) == count
if (c[0] == self.startContour[0] and c[1] == self.startContour[1]):
count -= 1
for i in range(count):
self.otherPen.lineTo((x[i], y[i]))
self.lastPoint = c
def _closePath(self):
if not (self.lastPoint[0] == self.startContour[0] and self.lastPoint[1] == self.startContour[1]):
self._lineTo(self.startContour)
# round values used by otherPen (a RoboFab SegmentToPointPen) to decide
# whether to delete duplicate points at start and end of contour
#TODO(jamesgk) figure out why we have to do this hack, then remove it
c = self.otherPen.contour
for i in [0, -1]:
c[i] = [[round(n, 5) for n in c[i][0]]] + list(c[i][1:])
self.otherPen.closePath()
def _endPath(self):
self.otherPen.endPath()
def addComponent(self, glyphName, transformation):
self.otherPen.addComponent(glyphName, transformation)
def stepsForSegment(self, p1, p2):
dist = np.linalg.norm(v(p1) - v(p2))
out = int(dist / self.resolution)
return out
def renderCurve(self,p,count):
curvePoints = []
t = 1.0 / float(count)
temp = t * t
f = p[0]
fd = 3 * (p[1] - p[0]) * t
fdd_per_2 = 3 * (p[0] - 2 * p[1] + p[2]) * temp
fddd_per_2 = 3 * (3 * (p[1] - p[2]) + p[3] - p[0]) * temp * t
fddd = fddd_per_2 + fddd_per_2
fdd = fdd_per_2 + fdd_per_2
fddd_per_6 = fddd_per_2 * (1.0 / 3)
for i in range(count):
f = f + fd + fdd_per_2 + fddd_per_6
fd = fd + fdd + fddd_per_2
fdd = fdd + fddd
fdd_per_2 = fdd_per_2 + fddd_per_2
curvePoints.append(f)
return curvePoints
def fitBezierSimple(pts):
T = [np.linalg.norm(pts[i]-pts[i-1]) for i in range(1,len(pts))]
tsum = np.sum(T)
T = [0] + T
T = [np.sum(T[0:i+1])/tsum for i in range(len(pts))]
T = [[t**3, t**2, t, 1] for t in T]
T = np.array(T)
M = np.array([[-1, 3, -3, 1],
[ 3, -6, 3, 0],
[-3, 3, 0, 0],
[ 1, 0, 0, 0]])
T = T.dot(M)
T = np.concatenate((T, np.array([[100,0,0,0], [0,0,0,100]])))
# pts = np.vstack((pts, pts[0] * 100, pts[-1] * 100))
C = np.linalg.lstsq(T, pts, rcond=-1)
return C[0]
def subdivideLineSegment(pts):
out = [pts[0]]
for i in range(1, len(pts)):
out.append(pts[i-1] + (pts[i] - pts[i-1]) * .5)
out.append(pts[i])
return np.array(out)
def fitBezier(pts, tangent0=None, tangent3=None):
if len(pts < 4):
pts = subdivideLineSegment(pts)
T = [np.linalg.norm(pts[i]-pts[i-1]) for i in range(1,len(pts))]
tsum = np.sum(T)
T = [0] + T
T = [np.sum(T[0:i+1])/tsum for i in range(len(pts))]
T = [[t**3, t**2, t, 1] for t in T]
T = np.array(T)
M = np.array([[-1, 3, -3, 1],
[ 3, -6, 3, 0],
[-3, 3, 0, 0],
[ 1, 0, 0, 0]])
T = T.dot(M)
n = len(pts)
pout = pts.copy()
pout[:,0] -= (T[:,0] * pts[0,0]) + (T[:,3] * pts[-1,0])
pout[:,1] -= (T[:,0] * pts[0,1]) + (T[:,3] * pts[-1,1])
TT = np.zeros((n*2,4))
for i in range(n):
for j in range(2):
TT[i*2,j*2] = T[i,j+1]
TT[i*2+1,j*2+1] = T[i,j+1]
pout = pout.reshape((n*2,1),order="C")
if tangent0 is not None and tangent3 is not None:
tangentConstraintsT = np.array([
[tangent0[1], -tangent0[0], 0, 0],
[0, 0, tangent3[1], -tangent3[0]]
])
tangentConstraintsP = np.array([
[pts[0][1] * -tangent0[0] + pts[0][0] * tangent0[1]],
[pts[-1][1] * -tangent3[0] + pts[-1][0] * tangent3[1]]
])
TT = np.concatenate((TT, tangentConstraintsT * 1000))
pout = np.concatenate((pout, tangentConstraintsP * 1000))
C = np.linalg.lstsq(TT, pout, rcond=-1)[0].reshape((2,2))
return np.array([pts[0], C[0], C[1], pts[-1]])
def segmentGlyph(glyph, resolution=50):
g1 = glyph.copy()
g1.clear()
dp = SubsegmentPointPen(g1, resolution)
glyph.drawPoints(dp)
return g1, dp.getSubsegments()
def fitGlyph(glyph, subsegmentGlyph, subsegmentIndices, matchTangents=True):
outGlyph = glyph.copy()
outGlyph.clear()
fitPen = SubsegmentsToCurvesPointPen(outGlyph, subsegmentGlyph, subsegmentIndices)
fitPen.setMatchTangents(matchTangents)
# smoothPen = GuessSmoothPointPen(fitPen)
glyph.drawPoints(fitPen)
outGlyph.width = subsegmentGlyph.width
return outGlyph
if __name__ == '__main__':
p = SubsegmentPen(None, None)
pts = np.array([
[0,0],
[.5,.5],
[.5,.5],
[1,1]
])
print np.array(p.renderCurve(pts,10)) * 10

View file

@ -1,23 +0,0 @@
def decomposeGlyph(font, glyph):
"""Moves the components of a glyph to its outline."""
if len(glyph.components):
deepCopyContours(font, glyph, glyph, (0, 0), (1, 1))
glyph.clearComponents()
def deepCopyContours(font, parent, component, offset, scale):
"""Copy contours to parent from component, including nested components."""
for nested in component.components:
deepCopyContours(
font, parent, font[nested.baseGlyph],
(offset[0] + nested.offset[0], offset[1] + nested.offset[1]),
(scale[0] * nested.scale[0], scale[1] * nested.scale[1]))
if component == parent:
return
for contour in component:
contour = contour.copy()
contour.scale(scale)
contour.move(offset)
parent.appendContour(contour)

View file

@ -1,189 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from feaTools import parser
from feaTools.writers.fdkSyntaxWriter import FDKSyntaxFeatureWriter
class FilterFeatureWriter(FDKSyntaxFeatureWriter):
"""Feature writer to detect invalid references and duplicate definitions."""
def __init__(self, refs=set(), name=None, isFeature=False):
"""Initializes the set of known references, empty by default."""
self.refs = refs
self.featureNames = set()
self.lookupNames = set()
self.tableNames = set()
self.languageSystems = set()
super(FilterFeatureWriter, self).__init__(
name=name, isFeature=isFeature)
# error to print when undefined reference is found in glyph class
self.classErr = ('Undefined reference "%s" removed from glyph class '
'definition %s.')
# error to print when undefined reference is found in sub or pos rule
subErr = ['Substitution rule with undefined reference "%s" removed']
if self._name:
subErr.append(" from ")
subErr.append("feature" if self._isFeature else "lookup")
subErr.append(' "%s"' % self._name)
subErr.append(".")
self.subErr = "".join(subErr)
self.posErr = self.subErr.replace("Substitution", "Positioning")
def _subwriter(self, name, isFeature):
"""Use this class for nested expressions e.g. in feature definitions."""
return FilterFeatureWriter(self.refs, name, isFeature)
def _flattenRefs(self, refs, flatRefs):
"""Flatten a list of references."""
for ref in refs:
if type(ref) == list:
self._flattenRefs(ref, flatRefs)
elif ref != "'": # ignore contextual class markings
flatRefs.append(ref)
def _checkRefs(self, refs, errorMsg):
"""Check a list of references found in a sub or pos rule."""
flatRefs = []
self._flattenRefs(refs, flatRefs)
for ref in flatRefs:
# trailing apostrophes should be ignored
if ref[-1] == "'":
ref = ref[:-1]
if ref not in self.refs:
print errorMsg % ref
# insert an empty instruction so that we can't end up with an
# empty block, which is illegal syntax
super(FilterFeatureWriter, self).rawText(";")
return False
return True
def classDefinition(self, name, contents):
"""Check that contents are valid, then add name to known references."""
if name in self.refs:
return
newContents = []
for ref in contents:
if ref not in self.refs and ref != "-":
print self.classErr % (ref, name)
else:
newContents.append(ref)
self.refs.add(name)
super(FilterFeatureWriter, self).classDefinition(name, newContents)
def gsubType1(self, target, replacement):
"""Check a sub rule with one-to-one replacement."""
if self._checkRefs([target, replacement], self.subErr):
super(FilterFeatureWriter, self).gsubType1(target, replacement)
def gsubType4(self, target, replacement):
"""Check a sub rule with many-to-one replacement."""
if self._checkRefs([target, replacement], self.subErr):
super(FilterFeatureWriter, self).gsubType4(target, replacement)
def gsubType6(self, precedingContext, target, trailingContext, replacement):
"""Check a sub rule with contextual replacement."""
refs = [precedingContext, target, trailingContext, replacement]
if self._checkRefs(refs, self.subErr):
super(FilterFeatureWriter, self).gsubType6(
precedingContext, target, trailingContext, replacement)
def gposType1(self, target, value):
"""Check a single positioning rule."""
if self._checkRefs([target], self.posErr):
super(FilterFeatureWriter, self).gposType1(target, value)
def gposType2(self, target, value, needEnum=False):
"""Check a pair positioning rule."""
if self._checkRefs(target, self.posErr):
super(FilterFeatureWriter, self).gposType2(target, value, needEnum)
# these rules may contain references, but they aren't present in Roboto
def gsubType3(self, target, replacement):
raise NotImplementedError
def feature(self, name):
"""Adds a feature definition only once."""
if name not in self.featureNames:
self.featureNames.add(name)
return super(FilterFeatureWriter, self).feature(name)
# we must return a new writer even if we don't add it to this one
return FDKSyntaxFeatureWriter(name, True)
def lookup(self, name):
"""Adds a lookup block only once."""
if name not in self.lookupNames:
self.lookupNames.add(name)
return super(FilterFeatureWriter, self).lookup(name)
# we must return a new writer even if we don't add it to this one
return FDKSyntaxFeatureWriter(name, False)
def languageSystem(self, langTag, scriptTag):
"""Adds a language system instruction only once."""
system = (langTag, scriptTag)
if system not in self.languageSystems:
self.languageSystems.add(system)
super(FilterFeatureWriter, self).languageSystem(langTag, scriptTag)
def table(self, name, data):
"""Adds a table only once."""
if name in self.tableNames:
return
self.tableNames.add(name)
self._instructions.append("table %s {" % name)
self._instructions.extend([" %s %s;" % line for line in data])
self._instructions.append("} %s;" % name)
def compileFeatureRE(name):
"""Compiles a feature-matching regex."""
# this is the pattern used internally by feaTools:
# https://github.com/typesupply/feaTools/blob/master/Lib/feaTools/parser.py
featureRE = list(parser.featureContentRE)
featureRE.insert(2, name)
featureRE.insert(6, name)
return re.compile("".join(featureRE))
def updateFeature(font, name, value):
"""Add a feature definition, or replace existing one."""
featureRE = compileFeatureRE(name)
if featureRE.search(font.features.text):
font.features.text = featureRE.sub(value, font.features.text)
else:
font.features.text += "\n" + value
def readFeatureFile(font, text, prepend=True):
"""Incorporate valid definitions from feature text into font."""
writer = FilterFeatureWriter(set(font.keys()))
if prepend:
text += font.features.text
else:
text = font.features.text + text
parser.parseFeatures(writer, text)
font.features.text = writer.write()
def writeFeatureFile(font, path):
"""Write the font's features to an external file."""
fout = open(path, "w")
fout.write(font.features.text)
fout.close()

View file

@ -1,97 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from string import find
from anchors import alignComponentsToAnchors, getAnchorByName
def parseComposite(composite):
c = composite.split("=")
d = c[1].split("/")
glyphName = d[0]
if len(d) == 1:
offset = [0, 0]
else:
offset = [int(i) for i in d[1].split(",")]
accentString = c[0]
accents = accentString.split("+")
baseName = accents.pop(0)
accentNames = [i.split(":") for i in accents]
return (glyphName, baseName, accentNames, offset)
def copyMarkAnchors(f, g, srcname, width):
for anchor in f[srcname].anchors:
if anchor.name in ("top_dd", "bottom_dd", "top0315"):
g.appendAnchor(anchor.name, (anchor.x + width, anchor.y))
if ("top" == anchor.name and
not any(a.name == "parent_top" for a in g.anchors)):
g.appendAnchor("parent_top", anchor.position)
if ("bottom" == anchor.name and
not any(a.name == "bottom" for a in g.anchors)):
g.appendAnchor("bottom", anchor.position)
if any(a.name == "top" for a in g.anchors):
return
anchor_parent_top = getAnchorByName(g, "parent_top")
if anchor_parent_top is not None:
g.appendAnchor("top", anchor_parent_top.position)
def generateGlyph(f,gname,glyphList={}):
glyphName, baseName, accentNames, offset = parseComposite(gname)
if f.has_key(glyphName):
print('Existing glyph "%s" found in font, ignoring composition rule '
'"%s"' % (glyphName, gname))
return
if baseName.find("_") != -1:
g = f.newGlyph(glyphName)
for componentName in baseName.split("_"):
g.appendComponent(componentName, (g.width, 0))
g.width += f[componentName].width
setUnicodeValue(g, glyphList)
else:
try:
f.compileGlyph(glyphName, baseName, accentNames)
except KeyError as e:
print('KeyError raised for composition rule "%s", likely "%s" '
'anchor not found in glyph "%s"' % (gname, e, baseName))
return
g = f[glyphName]
setUnicodeValue(g, glyphList)
copyMarkAnchors(f, g, baseName, offset[1] + offset[0])
if len(accentNames) > 0:
alignComponentsToAnchors(f, glyphName, baseName, accentNames)
if offset[0] != 0 or offset[1] != 0:
g.width += offset[1] + offset[0]
g.move((offset[0], 0), anchors=False)
def setUnicodeValue(glyph, glyphList):
"""Try to ensure glyph has a unicode value -- used by FDK to make OTFs."""
if glyph.name in glyphList:
glyph.unicode = int(glyphList[glyph.name], 16)
else:
uvNameMatch = re.match("uni([\dA-F]{4})$", glyph.name)
if uvNameMatch:
glyph.unicode = int(uvNameMatch.group(1), 16)

View file

@ -1,248 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import date
import re
from random import randint
import string
class InstanceNames:
"Class that allows easy setting of FontLab name fields. TODO: Add proper italic flags"
foundry = ""
foundryURL = ""
copyrightHolderName = ""
build = ""
version = "1.0"
year = date.today().year
designer = ""
designerURL = ""
license = ""
licenseURL = ""
italicAngle = -12
def __init__(self,names):
if type(names) == type(" "):
names = names.split("/")
#print names
self.longfamily = names[0]
self.longstyle = names[1]
self.shortstyle = names[2]
self.subfamilyAbbrev = names[3]
self.width = self._getWidth()
self.italic = self._getItalic()
self.weight = self._getWeight()
self.fullname = "%s %s" %(self.longfamily, self.longstyle)
self.postscript = re.sub(' ','', self.longfamily) + "-" + re.sub(' ','',self.longstyle)
if self.subfamilyAbbrev != "" and self.subfamilyAbbrev != None and self.subfamilyAbbrev != "Rg":
self.shortfamily = "%s %s" %(self.longfamily, self.longstyle.split()[0])
else:
self.shortfamily = self.longfamily
def setRFNames(self,f, version=1, versionMinor=0, panose={}):
f.info.familyName = self.longfamily
f.info.styleName = self.longstyle
f.info.styleMapFamilyName = self.shortfamily
f.info.styleMapStyleName = self.shortstyle.lower()
f.info.versionMajor = version
f.info.versionMinor = versionMinor
f.info.year = self.year
if len(self.copyrightHolderName) > 0:
f.info.copyright = "Copyright %s %s" % (self.year, self.copyrightHolderName)
f.info.trademark = "%s is a trademark of %s." %(self.longfamily, self.foundry.rstrip('.'))
if len(self.designer) > 0:
f.info.openTypeNameDesigner = self.designer
if len(self.designerURL) > 0:
f.info.openTypeNameDesignerURL = self.designerURL
f.info.openTypeNameManufacturer = self.foundry
f.info.openTypeNameManufacturerURL = self.foundryURL
f.info.openTypeNameLicense = self.license
f.info.openTypeNameLicenseURL = self.licenseURL
if self.build is not None and len(self.build):
f.info.openTypeNameVersion = "%i.%i;%s" %(version, versionMinor, self.build)
f.info.openTypeNameUniqueID = "%s:%s:%s" %(self.fullname, self.year, self.build)
else:
f.info.openTypeNameVersion = "%i.%i" %(version, versionMinor)
f.info.openTypeNameUniqueID = "%s:%s" %(self.fullname, self.year)
# f.info.openTypeNameDescription = ""
# f.info.openTypeNameCompatibleFullName = ""
# f.info.openTypeNameSampleText = ""
if (self.subfamilyAbbrev != "Rg"):
f.info.openTypeNamePreferredFamilyName = self.longfamily
f.info.openTypeNamePreferredSubfamilyName = self.longstyle
f.info.openTypeOS2WeightClass = self._getWeightCode(self.weight)
f.info.macintoshFONDName = re.sub(' ','',self.longfamily) + " " + re.sub(' ','',self.longstyle)
f.info.postscriptFontName = f.info.macintoshFONDName.replace(" ", "-")
if self.italic:
f.info.italicAngle = self.italicAngle
if len(panose) > 0:
f.info.openTypeOS2Panose = [
panose.get('bFamilyType', panose.get('familyType', 0)),
panose.get('bSerifStyle', panose.get('serifStyle', 0)),
panose.get('bWeight', panose.get('weight', 0)),
panose.get('bProportion', panose.get('proportion', 0)),
panose.get('bContrast', panose.get('contrast', 0)),
panose.get('bStrokeVariation', panose.get('strokeVariation', 0)),
panose.get('bArmStyle', panose.get('armStyle', 0)),
panose.get('bLetterform', panose.get('letterform', 0)),
panose.get('bMidline', panose.get('midline', 0)),
panose.get('bXHeight', panose.get('xHeight', 0)),
]
def setFLNames(self,flFont):
from FL import NameRecord
flFont.family_name = self.shortfamily
flFont.mac_compatible = self.fullname
flFont.style_name = self.longstyle
flFont.full_name = self.fullname
flFont.font_name = self.postscript
flFont.font_style = self._getStyleCode()
flFont.menu_name = self.shortfamily
flFont.apple_name = re.sub(' ','',self.longfamily) + " " + re.sub(' ','',self.longstyle)
flFont.fond_id = randint(1000,9999)
flFont.pref_family_name = self.longfamily
flFont.pref_style_name = self.longstyle
flFont.weight = self.weight
flFont.weight_code = self._getWeightCode(self.weight)
flFont.width = self.width
if len(self.italic):
flFont.italic_angle = -12
fn = flFont.fontnames
fn.clean()
#fn.append(NameRecord(0,1,0,0, "Font data copyright %s %s" %(self.foundry, self.year) ))
#fn.append(NameRecord(0,3,1,1033, "Font data copyright %s %s" %(self.foundry, self.year) ))
copyrightHolderName = self.copyrightHolderName if len(self.copyrightHolderName) > 0 else self.foundry
fn.append(NameRecord(0,1,0,0, "Copyright %s %s" %(self.year, copyrightHolderName) ))
fn.append(NameRecord(0,3,1,1033, "Copyright %s %s" %(self.year, copyrightHolderName) ))
fn.append(NameRecord(1,1,0,0, self.longfamily ))
fn.append(NameRecord(1,3,1,1033, self.shortfamily ))
fn.append(NameRecord(2,1,0,0, self.longstyle ))
fn.append(NameRecord(2,3,1,1033, self.longstyle ))
#fn.append(NameRecord(3,1,0,0, "%s:%s:%s" %(self.foundry, self.longfamily, self.year) ))
#fn.append(NameRecord(3,3,1,1033, "%s:%s:%s" %(self.foundry, self.longfamily, self.year) ))
fn.append(NameRecord(3,1,0,0, "%s:%s:%s" %(self.foundry, self.fullname, self.year) ))
fn.append(NameRecord(3,3,1,1033, "%s:%s:%s" %(self.foundry, self.fullname, self.year) ))
fn.append(NameRecord(4,1,0,0, self.fullname ))
fn.append(NameRecord(4,3,1,1033, self.fullname ))
if len(self.build) > 0:
fn.append(NameRecord(5,1,0,0, "Version %s%s; %s" %(self.version, self.build, self.year) ))
fn.append(NameRecord(5,3,1,1033, "Version %s%s; %s" %(self.version, self.build, self.year) ))
else:
fn.append(NameRecord(5,1,0,0, "Version %s; %s" %(self.version, self.year) ))
fn.append(NameRecord(5,3,1,1033, "Version %s; %s" %(self.version, self.year) ))
fn.append(NameRecord(6,1,0,0, self.postscript ))
fn.append(NameRecord(6,3,1,1033, self.postscript ))
fn.append(NameRecord(7,1,0,0, "%s is a trademark of %s." %(self.longfamily, self.foundry) ))
fn.append(NameRecord(7,3,1,1033, "%s is a trademark of %s." %(self.longfamily, self.foundry) ))
fn.append(NameRecord(9,1,0,0, self.foundry ))
fn.append(NameRecord(9,3,1,1033, self.foundry ))
fn.append(NameRecord(11,1,0,0, self.foundryURL ))
fn.append(NameRecord(11,3,1,1033, self.foundryURL ))
fn.append(NameRecord(12,1,0,0, self.designer ))
fn.append(NameRecord(12,3,1,1033, self.designer ))
fn.append(NameRecord(13,1,0,0, self.license ))
fn.append(NameRecord(13,3,1,1033, self.license ))
fn.append(NameRecord(14,1,0,0, self.licenseURL ))
fn.append(NameRecord(14,3,1,1033, self.licenseURL ))
if (self.subfamilyAbbrev != "Rg"):
fn.append(NameRecord(16,3,1,1033, self.longfamily ))
fn.append(NameRecord(17,3,1,1033, self.longstyle))
#else:
#fn.append(NameRecord(17,3,1,1033,""))
#fn.append(NameRecord(18,1,0,0, re.sub("Italic","It", self.fullname)))
def _getSubstyle(self, regex):
substyle = re.findall(regex, self.longstyle)
if len(substyle) > 0:
return substyle[0]
else:
return ""
def _getItalic(self):
return self._getSubstyle(r"Italic|Oblique|Obliq")
def _getWeight(self):
w = self._getSubstyle(r"Extrabold|Superbold|Super|Fat|Black|Bold|Semibold|Demibold|Medium|Light|Thin")
if w == "":
w = "Regular"
return w
def _getWidth(self):
w = self._getSubstyle(r"Condensed|Extended|Narrow|Wide")
if w == "":
w = "Normal"
return w
def _getStyleCode(self):
#print "shortstyle:", self.shortstyle
styleCode = 0
if self.shortstyle == "Bold":
styleCode = 32
if self.shortstyle == "Italic":
styleCode = 1
if self.shortstyle == "Bold Italic":
styleCode = 33
if self.longstyle == "Regular":
styleCode = 64
return styleCode
def _getWeightCode(self,weight):
if weight == "Thin":
return 250
elif weight == "Light":
return 300
elif weight == "Bold":
return 700
elif weight == "Medium":
return 500
elif weight == "Semibold":
return 600
elif weight == "Black":
return 900
elif weight == "Fat":
return 900
return 400
def setNames(f,names,foundry="",version="1.0",build=""):
InstanceNames.foundry = foundry
InstanceNames.version = version
InstanceNames.build = build
i = InstanceNames(names)
i.setFLNames(f)
def setInfoRF(f, names, attrs={}, panose={}):
i = InstanceNames(names)
version, versionMinor = (1, 0)
for k,v in attrs.iteritems():
if k == 'version':
if v.find('.') != -1:
version, versionMinor = [int(num) for num in v.split(".")]
else:
version = int(v)
setattr(i, k, v)
i.setRFNames(f, version=version, versionMinor=versionMinor, panose=panose)

View file

@ -1,308 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
from fontTools.misc.transform import Transform
import numpy as np
from numpy.linalg import norm
from scipy.sparse.linalg import cg
from scipy.ndimage.filters import gaussian_filter1d as gaussian
from scipy.cluster.vq import vq, whiten
from fontbuild.alignpoints import alignCorners
from fontbuild.curveFitPen import fitGlyph, segmentGlyph
def italicizeGlyph(f, g, angle=10, stemWidth=185, meanYCenter=-825, scaleX=1):
unic = g.unicode #save unicode
glyph = f[g.name]
slope = np.tanh(math.pi * angle / 180)
# determine how far on the x axis the glyph should slide
# to compensate for the slant.
# meanYCenter:
# -600 is a magic number that assumes a 2048 unit em square,
# and -825 for a 2816 unit em square. (UPM*0.29296875)
m = Transform(1, 0, slope, 1, 0, 0)
xoffset, junk = m.transformPoint((0, meanYCenter))
m = Transform(scaleX, 0, slope, 1, xoffset, 0)
if len(glyph) > 0:
g2 = italicize(f[g.name], angle, xoffset=xoffset, stemWidth=stemWidth)
f.insertGlyph(g2, g.name)
transformFLGlyphMembers(f[g.name], m)
if unic > 0xFFFF: #restore unicode
g.unicode = unic
def italicize(glyph, angle=12, stemWidth=180, xoffset=-50):
CURVE_CORRECTION_WEIGHT = .03
CORNER_WEIGHT = 10
# decompose the glyph into smaller segments
ga, subsegments = segmentGlyph(glyph,25)
va, e = glyphToMesh(ga)
n = len(va)
grad = mapEdges(lambda a, pn: normalize(pn[0]-a), va, e)
cornerWeights = mapEdges(lambda a, pn: normalize(pn[0]-a).dot(normalize(a-pn[1])), grad, e)[:,0].reshape((-1,1))
smooth = np.ones((n,1)) * CURVE_CORRECTION_WEIGHT
controlPoints = findControlPointsInMesh(glyph, va, subsegments)
smooth[controlPoints > 0] = 1
smooth[cornerWeights < .6] = CORNER_WEIGHT
# smooth[cornerWeights >= .9999] = 1
out = va.copy()
hascurves = False
for c in glyph.contours:
for s in c.segments:
if s.type == "curve":
hascurves = True
break
if hascurves:
break
if stemWidth > 100:
outCorrected = skewMesh(recompose(skewMesh(out, angle * 1.6), grad, e, smooth=smooth), -angle * 1.6)
# out = copyMeshDetails(va, out, e, 6)
else:
outCorrected = out
# create a transform for italicizing
normals = edgeNormals(out, e)
center = va + normals * stemWidth * .4
if stemWidth > 130:
center[:, 0] = va[:, 0] * .7 + center[:,0] * .3
centerSkew = skewMesh(center.dot(np.array([[.97,0],[0,1]])), angle * .9)
# apply the transform
out = outCorrected + (centerSkew - center)
out[:,1] = outCorrected[:,1]
# make some corrections
smooth = np.ones((n,1)) * .1
out = alignCorners(glyph, out, subsegments)
out = copyMeshDetails(skewMesh(va, angle), out, e, 7, smooth=smooth)
# grad = mapEdges(lambda a,(p,n): normalize(p-a), skewMesh(outCorrected, angle*.9), e)
# out = recompose(out, grad, e, smooth=smooth)
out = skewMesh(out, angle * .1)
out[:,0] += xoffset
# out[:,1] = outCorrected[:,1]
out[va[:,1] == 0, 1] = 0
gOut = meshToGlyph(out, ga)
# gOut.width *= .97
# gOut.width += 10
# return gOut
# recompose the glyph into original segments
return fitGlyph(glyph, gOut, subsegments)
def transformFLGlyphMembers(g, m, transformAnchors = True):
# g.transform(m)
g.width = g.width * m[0]
p = m.transformPoint((0,0))
for c in g.components:
d = m.transformPoint(c.offset)
c.offset = (d[0] - p[0], d[1] - p[1])
if transformAnchors:
for a in g.anchors:
aa = m.transformPoint((a.x,a.y))
a.x = aa[0]
# a.x,a.y = (aa[0] - p[0], aa[1] - p[1])
# a.x = a.x - m[4]
def glyphToMesh(g):
points = []
edges = {}
offset = 0
for c in g.contours:
if len(c) < 2:
continue
for i,prev,next in rangePrevNext(len(c)):
points.append((c[i].points[0].x, c[i].points[0].y))
edges[i + offset] = np.array([prev + offset, next + offset], dtype=int)
offset += len(c)
return np.array(points), edges
def meshToGlyph(points, g):
g1 = g.copy()
j = 0
for c in g1.contours:
if len(c) < 2:
continue
for i in range(len(c)):
c[i].points[0].x = points[j][0]
c[i].points[0].y = points[j][1]
j += 1
return g1
def quantizeGradient(grad, book=None):
if book == None:
book = np.array([(1,0),(0,1),(0,-1),(-1,0)])
indexArray = vq(whiten(grad), book)[0]
out = book[indexArray]
for i,v in enumerate(out):
out[i] = normalize(v)
return out
def findControlPointsInMesh(glyph, va, subsegments):
controlPointIndices = np.zeros((len(va),1))
index = 0
for i,c in enumerate(subsegments):
segmentCount = len(glyph.contours[i].segments) - 1
for j,s in enumerate(c):
if j < segmentCount:
if glyph.contours[i].segments[j].type == "line":
controlPointIndices[index] = 1
index += s[1]
return controlPointIndices
def recompose(v, grad, e, smooth=1, P=None, distance=None):
n = len(v)
if distance == None:
distance = mapEdges(lambda a, pn: norm(pn[0] - a), v, e)
if (P == None):
P = mP(v,e)
P += np.identity(n) * smooth
f = v.copy()
for i,(prev,next) in e.iteritems():
f[i] = (grad[next] * distance[next] - grad[i] * distance[i])
out = v.copy()
f += v * smooth
for i in range(len(out[0,:])):
out[:,i] = cg(P, f[:,i])[0]
return out
def mP(v,e):
n = len(v)
M = np.zeros((n,n))
for i, edges in e.iteritems():
w = -2 / float(len(edges))
for index in edges:
M[i,index] = w
M[i,i] = 2
return M
def normalize(v):
n = np.linalg.norm(v)
if n == 0:
return v
return v/n
def mapEdges(func,v,e,*args):
b = v.copy()
for i, edges in e.iteritems():
b[i] = func(v[i], [v[j] for j in edges], *args)
return b
def getNormal(a,b,c):
"Assumes TT winding direction"
p = np.roll(normalize(b - a), 1)
n = -np.roll(normalize(c - a), 1)
p[1] *= -1
n[1] *= -1
# print p, n, normalize((p + n) * .5)
return normalize((p + n) * .5)
def edgeNormals(v,e):
"Assumes a mesh where each vertex has exactly least two edges"
return mapEdges(lambda a, pn : getNormal(a,pn[0],pn[1]),v,e)
def rangePrevNext(count):
c = np.arange(count,dtype=int)
r = np.vstack((c, np.roll(c, 1), np.roll(c, -1)))
return r.T
def skewMesh(v,angle):
slope = np.tanh([math.pi * angle / 180])
return v.dot(np.array([[1,0],[slope,1]]))
def labelConnected(e):
label = 0
labels = np.zeros((len(e),1))
for i,(prev,next) in e.iteritems():
labels[i] = label
if next <= i:
label += 1
return labels
def copyGradDetails(a,b,e,scale=15):
n = len(a)
labels = labelConnected(e)
out = a.astype(float).copy()
for i in range(labels[-1]+1):
mask = (labels==i).flatten()
out[mask,:] = gaussian(b[mask,:], scale, mode="wrap", axis=0) + a[mask,:] - gaussian(a[mask,:], scale, mode="wrap", axis=0)
return out
def copyMeshDetails(va,vb,e,scale=5,smooth=.01):
gradA = mapEdges(lambda a, pn: normalize(pn[0]-a), va, e)
gradB = mapEdges(lambda a, pn: normalize(pn[0]-a), vb, e)
grad = copyGradDetails(gradA, gradB, e, scale)
grad = mapEdges(lambda a, pn: normalize(a), grad, e)
return recompose(vb, grad, e, smooth=smooth)
def condenseGlyph(glyph, scale=.8, stemWidth=185):
ga, subsegments = segmentGlyph(glyph, 25)
va, e = glyphToMesh(ga)
n = len(va)
normals = edgeNormals(va,e)
cn = va.dot(np.array([[scale, 0],[0,1]]))
grad = mapEdges(lambda a, pn: normalize(pn[0]-a), cn, e)
# ograd = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
cn[:,0] -= normals[:,0] * stemWidth * .5 * (1 - scale)
out = recompose(cn, grad, e, smooth=.5)
# out = recompose(out, grad, e, smooth=.1)
out = recompose(out, grad, e, smooth=.01)
# cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
# smooth = np.ones((n,1)) * .1
# smooth[cornerWeights < .6] = 10
#
# grad2 = quantizeGradient(grad).astype(float)
# grad2 = copyGradDetails(grad, grad2, e, scale=10)
# grad2 = mapEdges(lambda a,e: normalize(a), grad2, e)
# out = recompose(out, grad2, e, smooth=smooth)
out[:,0] += 15
out[:,1] = va[:,1]
# out = recompose(out, grad, e, smooth=.5)
gOut = meshToGlyph(out, ga)
gOut = fitGlyph(glyph, gOut, subsegments)
for i,seg in enumerate(gOut):
gOut[i].points[0].y = glyph[i].points[0].y
return gOut

View file

@ -1,52 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ufo2ft.kernFeatureWriter import KernFeatureWriter
from ufo2ft.makeotfParts import FeatureOTFCompiler
class RobotoFeatureCompiler(FeatureOTFCompiler):
def precompile(self):
self.overwriteFeatures = True
def setupAnchorPairs(self):
self.anchorPairs = [
["top", "_marktop"],
["bottom", "_markbottom"],
["top_dd", "_marktop_dd"],
["bottom_dd", "_markbottom_dd"],
["rhotichook", "_markrhotichook"],
["top0315", "_marktop0315"],
]
self.mkmkAnchorPairs = [
["mkmktop", "_marktop"],
["mkmkbottom_acc", "_markbottom"],
# By providing a pair with accent anchor _bottom and no base anchor,
# we designate all glyphs with _bottom as accents (so that they will
# be used as base glyphs for mkmk features) without generating any
# positioning rules actually using this anchor (which is instead
# used to generate composite glyphs). This is all for consistency
# with older roboto versions.
["", "_bottom"],
]
self.ligaAnchorPairs = []
class RobotoKernWriter(KernFeatureWriter):
leftFeaClassRe = r"@_(.+)_L$"
rightFeaClassRe = r"@_(.+)_R$"

View file

@ -1,111 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Mitre Glyph:
mitreSize : Length of the segment created by the mitre. The default is 4.
maxAngle : Maximum angle in radians at which segments will be mitred. The default is .9 (about 50 degrees).
Works for both inside and outside angles
"""
import math
from robofab.objects.objectsRF import RPoint, RSegment
from fontbuild.convertCurves import replaceSegments
def getTangents(contours):
tmap = []
for c in contours:
clen = len(c)
for i in range(clen):
s = c[i]
p = s.points[-1]
ns = c[(i + 1) % clen]
ps = c[(clen + i - 1) % clen]
np = ns.points[1] if ns.type == 'curve' else ns.points[-1]
pp = s.points[2] if s.type == 'curve' else ps.points[-1]
tmap.append((pp - p, np - p))
return tmap
def normalizeVector(p):
m = getMagnitude(p);
if m != 0:
return p*(1/m)
else:
return RPoint(0,0)
def getMagnitude(p):
return math.sqrt(p.x*p.x + p.y*p.y)
def getDistance(v1,v2):
return getMagnitude(RPoint(v1.x - v2.x, v1.y - v2.y))
def getAngle(v1,v2):
angle = math.atan2(v1.y,v1.x) - math.atan2(v2.y,v2.x)
return (angle + (2*math.pi)) % (2*math.pi)
def angleDiff(a,b):
return math.pi - abs((abs(a - b) % (math.pi*2)) - math.pi)
def getAngle2(v1,v2):
return abs(angleDiff(math.atan2(v1.y, v1.x), math.atan2(v2.y, v2.x)))
def getMitreOffset(n,v1,v2,mitreSize=4,maxAngle=.9):
# dont mitre if segment is too short
if abs(getMagnitude(v1)) < mitreSize * 2 or abs(getMagnitude(v2)) < mitreSize * 2:
return
angle = getAngle2(v2,v1)
v1 = normalizeVector(v1)
v2 = normalizeVector(v2)
if v1.x == v2.x and v1.y == v2.y:
return
# only mitre corners sharper than maxAngle
if angle > maxAngle:
return
radius = mitreSize / abs(getDistance(v1,v2))
offset1 = RPoint(round(v1.x * radius), round(v1.y * radius))
offset2 = RPoint(round(v2.x * radius), round(v2.y * radius))
return offset1, offset2
def mitreGlyph(g,mitreSize,maxAngle):
if g == None:
return
tangents = getTangents(g.contours)
sid = -1
for c in g.contours:
segments = []
needsMitring = False
for s in c:
sid += 1
v1, v2 = tangents[sid]
off = getMitreOffset(s,v1,v2,mitreSize,maxAngle)
s1 = s.copy()
if off != None:
offset1, offset2 = off
p2 = s.points[-1] + offset2
s2 = RSegment('line', [(p2.x, p2.y)])
s1.points[0] += offset1
segments.append(s1)
segments.append(s2)
needsMitring = True
else:
segments.append(s1)
if needsMitring:
replaceSegments(c, segments)

View file

@ -1,358 +0,0 @@
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from numpy import array, append
import copy
import json
from robofab.objects.objectsRF import RGlyph
from robofab.world import OpenFont
from decomposeGlyph import decomposeGlyph
class Vec2:
def __init__(self, x, y):
self.x = x
self.y = y
class FFont:
"Font wrapper for floating point operations"
def __init__(self,f=None):
self.glyphs = {}
self.hstems = []
self.vstems = []
self.kerning = {}
if isinstance(f,FFont):
#self.glyphs = [g.copy() for g in f.glyphs]
for key,g in f.glyphs.iteritems():
self.glyphs[key] = g.copy()
self.hstems = list(f.hstems)
self.vstems = list(f.vstems)
self.kerning = dict(f.kerning)
elif f != None:
self.copyFromFont(f)
def copyFromFont(self, f):
for g in f:
self.glyphs[g.name] = FGlyph(g)
self.hstems = [s for s in f.info.postscriptStemSnapH]
self.vstems = [s for s in f.info.postscriptStemSnapV]
self.kerning = f.kerning.asDict()
def copyToFont(self, f):
for g in f:
try:
gF = self.glyphs[g.name]
gF.copyToGlyph(g)
except:
print "Copy to glyph failed for" + g.name
f.info.postscriptStemSnapH = self.hstems
f.info.postscriptStemSnapV = self.vstems
for pair in self.kerning:
f.kerning[pair] = self.kerning[pair]
def getGlyph(self, gname):
try:
return self.glyphs[gname]
except:
return None
def setGlyph(self, gname, glyph):
self.glyphs[gname] = glyph
def addDiff(self,b,c):
newFont = FFont(self)
for key,g in newFont.glyphs.iteritems():
gB = b.getGlyph(key)
gC = c.getGlyph(key)
try:
newFont.glyphs[key] = g.addDiff(gB,gC)
except:
print "Add diff failed for '%s'" %key
return newFont
class FGlyph:
"provides a temporary floating point compatible glyph data structure"
def __init__(self, g=None):
self.contours = []
self.width = 0.
self.components = []
self.anchors = []
if g != None:
self.copyFromGlyph(g)
def copyFromGlyph(self,g):
self.name = g.name
valuesX = []
valuesY = []
self.width = len(valuesX)
valuesX.append(g.width)
for c in g.components:
self.components.append((len(valuesX), len(valuesY)))
valuesX.append(c.scale[0])
valuesY.append(c.scale[1])
valuesX.append(c.offset[0])
valuesY.append(c.offset[1])
for a in g.anchors:
self.anchors.append((len(valuesX), len(valuesY)))
valuesX.append(a.x)
valuesY.append(a.y)
for i in range(len(g)):
self.contours.append([])
for j in range (len(g[i].points)):
self.contours[i].append((len(valuesX), len(valuesY)))
valuesX.append(g[i].points[j].x)
valuesY.append(g[i].points[j].y)
self.dataX = array(valuesX, dtype=float)
self.dataY = array(valuesY, dtype=float)
def copyToGlyph(self,g):
g.width = self._derefX(self.width)
if len(g.components) == len(self.components):
for i in range(len(self.components)):
g.components[i].scale = (self._derefX(self.components[i][0] + 0, asInt=False),
self._derefY(self.components[i][1] + 0, asInt=False))
g.components[i].offset = (self._derefX(self.components[i][0] + 1),
self._derefY(self.components[i][1] + 1))
if len(g.anchors) == len(self.anchors):
for i in range(len(self.anchors)):
g.anchors[i].x = self._derefX( self.anchors[i][0])
g.anchors[i].y = self._derefY( self.anchors[i][1])
for i in range(len(g)) :
for j in range (len(g[i].points)):
g[i].points[j].x = self._derefX(self.contours[i][j][0])
g[i].points[j].y = self._derefY(self.contours[i][j][1])
def isCompatible(self, g):
return (len(self.dataX) == len(g.dataX) and
len(self.dataY) == len(g.dataY) and
len(g.contours) == len(self.contours))
def __add__(self,g):
if self.isCompatible(g):
newGlyph = self.copy()
newGlyph.dataX = self.dataX + g.dataX
newGlyph.dataY = self.dataY + g.dataY
return newGlyph
else:
print "Add failed for '%s'" %(self.name)
raise Exception
def __sub__(self,g):
if self.isCompatible(g):
newGlyph = self.copy()
newGlyph.dataX = self.dataX - g.dataX
newGlyph.dataY = self.dataY - g.dataY
return newGlyph
else:
print "Subtract failed for '%s'" %(self.name)
raise Exception
def __mul__(self,scalar):
newGlyph = self.copy()
newGlyph.dataX = self.dataX * scalar
newGlyph.dataY = self.dataY * scalar
return newGlyph
def scaleX(self,scalar):
newGlyph = self.copy()
if len(self.dataX) > 0:
newGlyph.dataX = self.dataX * scalar
for i in range(len(newGlyph.components)):
newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]]
return newGlyph
def shift(self,ammount):
newGlyph = self.copy()
newGlyph.dataX = self.dataX + ammount
for i in range(len(newGlyph.components)):
newGlyph.dataX[newGlyph.components[i][0]] = self.dataX[newGlyph.components[i][0]]
return newGlyph
def interp(self, g, v):
gF = self.copy()
if not self.isCompatible(g):
print "Interpolate failed for '%s'; outlines incompatible" %(self.name)
raise Exception
gF.dataX += (g.dataX - gF.dataX) * v.x
gF.dataY += (g.dataY - gF.dataY) * v.y
return gF
def copy(self):
ng = FGlyph()
ng.contours = list(self.contours)
ng.width = self.width
ng.components = list(self.components)
ng.anchors = list(self.anchors)
ng.dataX = self.dataX.copy()
ng.dataY = self.dataY.copy()
ng.name = self.name
return ng
def _derefX(self,id, asInt=True):
val = self.dataX[id]
return int(round(val)) if asInt else val
def _derefY(self,id, asInt=True):
val = self.dataY[id]
return int(round(val)) if asInt else val
def addDiff(self,gB,gC):
newGlyph = self + (gB - gC)
return newGlyph
class Master:
def __init__(self, font=None, v=0, kernlist=None, overlay=None):
if isinstance(font, FFont):
self.font = None
self.ffont = font
elif isinstance(font,str):
self.openFont(font,overlay)
elif isinstance(font,Mix):
self.font = font
else:
self.font = font
self.ffont = FFont(font)
if isinstance(v,float) or isinstance(v,int):
self.v = Vec2(v, v)
else:
self.v = v
if kernlist != None:
kerns = [i.strip().split() for i in open(kernlist).readlines()]
self.kernlist = [{'left':k[0], 'right':k[1], 'value': k[2]}
for k in kerns
if not k[0].startswith("#")
and not k[0] == ""]
#TODO implement class based kerning / external kerning file
def openFont(self, path, overlayPath=None):
self.font = OpenFont(path)
for g in self.font:
size = len(g)
csize = len(g.components)
if (size > 0 and csize > 0):
decomposeGlyph(self.font, self.font[g.name])
if overlayPath != None:
overlayFont = OpenFont(overlayPath)
font = self.font
for overlayGlyph in overlayFont:
font.insertGlyph(overlayGlyph)
self.ffont = FFont(self.font)
class Mix:
def __init__(self,masters,v):
self.masters = masters
if isinstance(v,float) or isinstance(v,int):
self.v = Vec2(v,v)
else:
self.v = v
def getFGlyph(self, master, gname):
if isinstance(master.font, Mix):
return master.font.mixGlyphs(gname)
return master.ffont.getGlyph(gname)
def getGlyphMasters(self,gname):
masters = self.masters
if len(masters) <= 2:
return self.getFGlyph(masters[0], gname), self.getFGlyph(masters[-1], gname)
def generateFFont(self):
ffont = FFont(self.masters[0].ffont)
for key,g in ffont.glyphs.iteritems():
ffont.glyphs[key] = self.mixGlyphs(key)
ffont.kerning = self.mixKerns()
return ffont
def generateFont(self, baseFont, ignoreGlyphs=None):
newFont = baseFont.copy()
#self.mixStems(newFont) todo _ fix stems code
for g in newFont:
if not ignoreGlyphs or g.name not in ignoreGlyphs:
gF = self.mixGlyphs(g.name)
if gF == None:
g.mark = True
elif isinstance(gF, RGlyph):
newFont[g.name] = gF.copy()
else:
gF.copyToGlyph(g)
newFont.kerning.clear()
newFont.kerning.update(self.mixKerns() or {})
return newFont
def mixGlyphs(self,gname):
gA,gB = self.getGlyphMasters(gname)
try:
return gA.interp(gB,self.v)
except:
print "mixglyph failed for %s" %(gname)
if gA != None:
return gA.copy()
def getKerning(self, master):
if isinstance(master.font, Mix):
return master.font.mixKerns()
return master.ffont.kerning
def mixKerns(self):
masters = self.masters
kA, kB = self.getKerning(masters[0]), self.getKerning(masters[-1])
return interpolateKerns(kA, kB, self.v)
def narrowFLGlyph(g, gThin, factor=.75):
gF = FGlyph(g)
if not isinstance(gThin,FGlyph):
gThin = FGlyph(gThin)
gCondensed = gThin.scaleX(factor)
try:
gNarrow = gF + (gCondensed - gThin)
gNarrow.copyToGlyph(g)
except:
print "No dice for: " + g.name
def interpolate(a,b,v,e=0):
if e == 0:
return a+(b-a)*v
qe = (b-a)*v*v*v + a #cubic easing
le = a+(b-a)*v # linear easing
return le + (qe-le) * e
def interpolateKerns(kA, kB, v):
kerns = {}
for pair, val in kA.items():
kerns[pair] = interpolate(val, kB.get(pair, 0), v.x)
for pair, val in kB.items():
lerped_val = interpolate(val, kA.get(pair, 0), 1 - v.x)
if pair in kerns:
assert abs(kerns[pair] - lerped_val) < 1e-6
else:
kerns[pair] = lerped_val
return kerns

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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