Major overhaul, moving from UFO2 to Glyphs and UFO3, plus a brand new and much simpler fontbuild
This commit is contained in:
parent
8c1a4c181e
commit
c833e252c9
5246 changed files with 346953 additions and 163499 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,7 +11,6 @@
|
|||
_*.ignore
|
||||
*~
|
||||
.DS_Store
|
||||
*.sparseimage
|
||||
nohup.out
|
||||
|
||||
build
|
||||
|
|
|
|||
119
Makefile
119
Makefile
|
|
@ -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
267
init.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
462
misc/fontbuild
Executable 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)
|
||||
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
diskutil unmount build/tmp
|
||||
|
|
@ -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
|
||||
|
||||
2
misc/pylib/booleanOperations/.gitignore
vendored
2
misc/pylib/booleanOperations/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*.c
|
||||
build
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
# pyclipper==1.0.5
|
||||
# fonttools==3.1.2
|
||||
# ufoLib==2.0.0
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
try:
|
||||
__version__ = __import__('pkg_resources').require('booleanOperations')[0].version
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
2
misc/pylib/copy/.gitignore
vendored
2
misc/pylib/copy/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*.c
|
||||
build
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from __future__ import absolute_import
|
||||
from .copy import copy, deepcopy, Error
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
class ExtractorError(Exception): pass
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fonttools==3.3.1
|
||||
ufoLib==2.0.0
|
||||
|
|
@ -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)
|
||||
1
misc/pylib/fontbuild/.gitignore
vendored
1
misc/pylib/fontbuild/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
*.c
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -1 +0,0 @@
|
|||
https://github.com/google/roboto/tree/master/scripts/lib/fontbuild
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
"""
|
||||
fontbuild
|
||||
|
||||
A collection of font production tools written for FontLab
|
||||
"""
|
||||
version = "0.1"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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])
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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$"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
2
misc/pylib/robofab/.gitignore
vendored
2
misc/pylib/robofab/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*.c
|
||||
build
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
"""
|
||||
|
||||
Directory for contributed packages.
|
||||
Packages stored here can be imported from
|
||||
robofab.contrib.<packagename>
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
class RoboFabError(Exception): pass
|
||||
|
||||
class RoboFabWarning(Warning): pass
|
||||
|
|
@ -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()
|
||||
|
|
@ -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__)
|
||||
|
||||
|
|
@ -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__)
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
"""
|
||||
|
||||
Directory for interface related modules.
|
||||
Stuff for MacOSX, widgets, quartz
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
"""
|
||||
|
||||
Directory for interface related modules.
|
||||
Stuff for Windows
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
|
@ -1,12 +0,0 @@
|
|||
"""
|
||||
|
||||
Directory for modules
|
||||
which do path stuff.
|
||||
Maybe it should move somewhere else.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
@ -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.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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("&", "&") # escape '&'
|
||||
text = text.replace("<", "<") # escape '<'
|
||||
text = text.replace(">", ">") # escape '>'
|
||||
return text.encode("utf-8") # encode as UTF-8
|
||||
|
||||
|
||||
PLISTHEADER = """\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
"""
|
||||
|
||||
class PlistWriter(DumbXMLWriter):
|
||||
|
||||
def __init__(self, file, indentLevel=0, indent="\t", writeHeader=1):
|
||||
if writeHeader:
|
||||
file.write(PLISTHEADER)
|
||||
DumbXMLWriter.__init__(self, file, indentLevel, indent)
|
||||
|
||||
def writeValue(self, value):
|
||||
if isinstance(value, (str, unicode)):
|
||||
self.simpleElement("string", value)
|
||||
elif isinstance(value, bool):
|
||||
# must switch for bool before int, as bool is a
|
||||
# subclass of int...
|
||||
if value:
|
||||
self.simpleElement("true")
|
||||
else:
|
||||
self.simpleElement("false")
|
||||
elif isinstance(value, (int, long)):
|
||||
self.simpleElement("integer", "%d" % value)
|
||||
elif isinstance(value, float):
|
||||
self.simpleElement("real", repr(value))
|
||||
elif isinstance(value, dict):
|
||||
self.writeDict(value)
|
||||
elif isinstance(value, Data):
|
||||
self.writeData(value)
|
||||
elif isinstance(value, datetime):
|
||||
self.simpleElement("date", _dateToString(value))
|
||||
elif isinstance(value, (tuple, list)):
|
||||
self.writeArray(value)
|
||||
else:
|
||||
raise TypeError("unsuported type: %s" % type(value))
|
||||
|
||||
def writeData(self, data):
|
||||
self.beginElement("data")
|
||||
self.indentLevel -= 1
|
||||
maxlinelength = 76 - len(self.indent.replace("\t", " " * 8) *
|
||||
self.indentLevel)
|
||||
for line in data.asBase64(maxlinelength).split("\n"):
|
||||
if line:
|
||||
self.writeln(line)
|
||||
self.indentLevel += 1
|
||||
self.endElement("data")
|
||||
|
||||
def writeDict(self, d):
|
||||
self.beginElement("dict")
|
||||
items = d.items()
|
||||
items.sort()
|
||||
for key, value in items:
|
||||
if not isinstance(key, (str, unicode)):
|
||||
raise TypeError("keys must be strings")
|
||||
self.simpleElement("key", key)
|
||||
self.writeValue(value)
|
||||
self.endElement("dict")
|
||||
|
||||
def writeArray(self, array):
|
||||
self.beginElement("array")
|
||||
for value in array:
|
||||
self.writeValue(value)
|
||||
self.endElement("array")
|
||||
|
||||
|
||||
class _InternalDict(dict):
|
||||
|
||||
# This class is needed while Dict is scheduled for deprecation:
|
||||
# we only need to warn when a *user* instantiates Dict or when
|
||||
# the "attribute notation for dict keys" is used.
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
value = self[attr]
|
||||
except KeyError:
|
||||
raise AttributeError, attr
|
||||
from warnings import warn
|
||||
warn("Attribute access from plist dicts is deprecated, use d[key] "
|
||||
"notation instead", PendingDeprecationWarning)
|
||||
return value
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
from warnings import warn
|
||||
warn("Attribute access from plist dicts is deprecated, use d[key] "
|
||||
"notation instead", PendingDeprecationWarning)
|
||||
self[attr] = value
|
||||
|
||||
def __delattr__(self, attr):
|
||||
try:
|
||||
del self[attr]
|
||||
except KeyError:
|
||||
raise AttributeError, attr
|
||||
from warnings import warn
|
||||
warn("Attribute access from plist dicts is deprecated, use d[key] "
|
||||
"notation instead", PendingDeprecationWarning)
|
||||
|
||||
class Dict(_InternalDict):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from warnings import warn
|
||||
warn("The plistlib.Dict class is deprecated, use builtin dict instead",
|
||||
PendingDeprecationWarning)
|
||||
super(Dict, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class Plist(_InternalDict):
|
||||
|
||||
"""This class has been deprecated. Use readPlist() and writePlist()
|
||||
functions instead, together with regular dict objects.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from warnings import warn
|
||||
warn("The Plist class is deprecated, use the readPlist() and "
|
||||
"writePlist() functions instead", PendingDeprecationWarning)
|
||||
super(Plist, self).__init__(**kwargs)
|
||||
|
||||
def fromFile(cls, pathOrFile):
|
||||
"""Deprecated. Use the readPlist() function instead."""
|
||||
rootObject = readPlist(pathOrFile)
|
||||
plist = cls()
|
||||
plist.update(rootObject)
|
||||
return plist
|
||||
fromFile = classmethod(fromFile)
|
||||
|
||||
def write(self, pathOrFile):
|
||||
"""Deprecated. Use the writePlist() function instead."""
|
||||
writePlist(self, pathOrFile)
|
||||
|
||||
|
||||
def _encodeBase64(s, maxlinelength=76):
|
||||
# copied from base64.encodestring(), with added maxlinelength argument
|
||||
maxbinsize = (maxlinelength//4)*3
|
||||
pieces = []
|
||||
for i in range(0, len(s), maxbinsize):
|
||||
chunk = s[i : i + maxbinsize]
|
||||
pieces.append(binascii.b2a_base64(chunk))
|
||||
return "".join(pieces)
|
||||
|
||||
class Data:
|
||||
|
||||
"""Wrapper for binary data."""
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def fromBase64(cls, data):
|
||||
# base64.decodestring just calls binascii.a2b_base64;
|
||||
# it seems overkill to use both base64 and binascii.
|
||||
return cls(binascii.a2b_base64(data))
|
||||
fromBase64 = classmethod(fromBase64)
|
||||
|
||||
def asBase64(self, maxlinelength=76):
|
||||
return _encodeBase64(self.data, maxlinelength)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return cmp(self.data, other.data)
|
||||
elif isinstance(other, str):
|
||||
return cmp(self.data, other)
|
||||
else:
|
||||
return cmp(id(self), id(other))
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
|
||||
|
||||
|
||||
class PlistParser:
|
||||
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
self.currentKey = None
|
||||
self.root = None
|
||||
|
||||
def parse(self, fileobj):
|
||||
from xml.parsers.expat import ParserCreate
|
||||
parser = ParserCreate()
|
||||
parser.StartElementHandler = self.handleBeginElement
|
||||
parser.EndElementHandler = self.handleEndElement
|
||||
parser.CharacterDataHandler = self.handleData
|
||||
parser.ParseFile(fileobj)
|
||||
return self.root
|
||||
|
||||
def handleBeginElement(self, element, attrs):
|
||||
self.data = []
|
||||
handler = getattr(self, "begin_" + element, None)
|
||||
if handler is not None:
|
||||
handler(attrs)
|
||||
|
||||
def handleEndElement(self, element):
|
||||
handler = getattr(self, "end_" + element, None)
|
||||
if handler is not None:
|
||||
handler()
|
||||
|
||||
def handleData(self, data):
|
||||
self.data.append(data)
|
||||
|
||||
def addObject(self, value):
|
||||
if self.currentKey is not None:
|
||||
self.stack[-1][self.currentKey] = value
|
||||
self.currentKey = None
|
||||
elif not self.stack:
|
||||
# this is the root object
|
||||
self.root = value
|
||||
else:
|
||||
self.stack[-1].append(value)
|
||||
|
||||
def getData(self):
|
||||
data = "".join(self.data)
|
||||
try:
|
||||
data = data.encode("ascii")
|
||||
except UnicodeError:
|
||||
pass
|
||||
self.data = []
|
||||
return data
|
||||
|
||||
# element handlers
|
||||
|
||||
def begin_dict(self, attrs):
|
||||
d = _InternalDict()
|
||||
self.addObject(d)
|
||||
self.stack.append(d)
|
||||
def end_dict(self):
|
||||
self.stack.pop()
|
||||
|
||||
def end_key(self):
|
||||
self.currentKey = self.getData()
|
||||
|
||||
def begin_array(self, attrs):
|
||||
a = []
|
||||
self.addObject(a)
|
||||
self.stack.append(a)
|
||||
def end_array(self):
|
||||
self.stack.pop()
|
||||
|
||||
def end_true(self):
|
||||
self.addObject(True)
|
||||
def end_false(self):
|
||||
self.addObject(False)
|
||||
def end_integer(self):
|
||||
self.addObject(int(self.getData()))
|
||||
def end_real(self):
|
||||
self.addObject(float(self.getData()))
|
||||
def end_string(self):
|
||||
self.addObject(self.getData())
|
||||
def end_data(self):
|
||||
self.addObject(Data.fromBase64(self.getData()))
|
||||
def end_date(self):
|
||||
self.addObject(_dateFromString(self.getData()))
|
||||
|
||||
|
||||
# cruft to support booleans in Python <= 2.3
|
||||
import sys
|
||||
if sys.version_info[:2] < (2, 3):
|
||||
# Python 2.2 and earlier: no booleans
|
||||
# Python 2.2.x: booleans are ints
|
||||
class bool(int):
|
||||
"""Imitation of the Python 2.3 bool object."""
|
||||
def __new__(cls, value):
|
||||
return int.__new__(cls, not not value)
|
||||
def __repr__(self):
|
||||
if self:
|
||||
return "True"
|
||||
else:
|
||||
return "False"
|
||||
True = bool(1)
|
||||
False = bool(0)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
Reference in a new issue