mf2ff
is a tool to create vector fonts from METAFONT code using the Python API of FontForge.
It is based on a concept of how METAFONT can work together with a vector font generator without the detour of bitmap tracing, described below in the mf2vec concept section. mf2ff
is the first and, to the developer's knowledge, the only implementation of this concept to date.
Below you will find some help on how to set it up and how to use it.
The tool has not yet been thoroughly tested, but most common METAFONT commands are supported. Besides filling and drawing, also kerning and some ligature commands are supported. Please take a look at the limitations listed below.
You need to install FontForge and METAFONT to use mf2ff
.
You may have problems running the mf2ff
script. One issue may be that you need to run Python 3 with the fontforge module. Below are some tips on how to get it to work. I can't guaranty that this will work on your system, but you should give it a try.
On different operating systems or system configurations where the tips below do not work, check if you use Python and FontForge's Python module is present:
- Run your Python 3 command (
ffpython
,python3
,python
). Check that the version info states that you are runningPython 3
. import fontforge
within the interpreter should work without errors.fontforge.font()
should not raise an error. If this only works in some directories, you should check yourPATH
orPYTHONPATH
variable.
- Download Fontforge from its website: https://fontforge.org/
- Install Fontforge
For temporary access to FontForge's Python version ffpython
, FontForge comes with a batch file:
- Go to your installation folder of FontForge (e.g.
C:\Program Files (x86)\FontForgeBuilds
) and executefontforge-console.bat
Since Windows'set
command changes thePATH
variable only in the current Command Prompt session, ffpython is only available in every location in the Command Prompt in which the batch file was executed.
For permanent access, you need to edit the PATH variable permanently:
- In Windows, search for
environment variables
and openEdit the system environment variables
. Click onEnvironment variables
and edit either thePATH
variable of your user account or the one of the system if you need access toffpython
on multiple user accounts. Now, add FontForge's path (e.g.C:\Program Files (x86)\FontForgeBuilds\bin
, mind the\bin
!) to the list. ffpython
is now available in all new Command Prompt windows and you should be able to useffpython path/to/mf2ff.py ...
To easily access the mf2ff
script from everywhere, do the following:
- As described above, go to the environment variables and add the path of
mf2ff
to thePYTHONPATH
variable. If there is noPYTHONPATH
variable yet, add it to the list. - Now, you should be able to run
mf2ff
withffpython -m mf2ff ...
in new Command Prompt sessions.
Ubuntu is used in this example.
Some repositories ship old versions of FontForge with Python 2 support. You may want to build FontForge from source:
- Get the tools to build FontForge:
sudo apt install libjpeg-dev libtiff5-dev libpng-dev libfreetype6-dev libgif-dev libgtk-3-dev libxml2-dev libpango1.0-dev libcairo2-dev libspiro-dev libuninameslist-dev python3-dev ninja-build cmake build-essential
- If Git is not installed, install Git:
sudo apt install git
- Clone FontForge's source:
git clone https://github.com/fontforge/fontforge
. This creates afontforge/
directory. - Move to a separate directory inside the
fontforge/
directory:
cd fontforge; mkdir build; cd build
- Create the Makefile:
cmake -GNinja ..
- Build FontForge:
ninja
- Install FontForge:
sudo ninja install
Note: Check FontFore's documentation if you have problems with this process.
To easily access FontForge's Python module, add it to your PYTHONPATH:
- Put the following line at the end of your
~/.profile
file:export PYTHONPATH=$PYTHONPATH:/path/to/fontforge/
, where the/path/to/fontforge/
directory is the one created by cloning FontForge with Git, e.g.$HOME/fontforge
. - Reboot.
- Now, you should be able to always use
python3 path/to/mf2ff.py ...
in new shell sessions.
Note: Depending on your system's configuration you need to type python
instead of python3
to run Python 3.
To easily access the mf2ff
script from everywhere, add its location to the PYTHONPATH variable:
- In your home directory, put this at the end of your
~/.profile
file:export PYTHONPATH=$PYTHONPATH:/path/to/mf2ff/
, where/path/to/mf2ff/
directory is the one where you put themf2ff.py
file, e.g.$HOME/mf2ff/mf2ff
. - Reboot.
- Now, you can always run
mf2ff
easily withpython3 -m mf2ff ...
in new shell sessions.
Note: Depending on which dotfile you are using, a restart of the shell instead of a reboot may be sufficient.
The solution below uses Homebrew. This way Fontforge is also accessible with Python:
- Install Homebrew as explained at its website: https://brew.sh/. You may need to enter your password multiple times. This will also install Xcode with Python 3.
- On the Terminal, run
brew install fontforge
. - Now, you can always use
python3 path/to/mf2ff.py ...
in new shell sessions.
Note: Depending on your system's configuration you need to type python
instead of python3
to run Python 3.
To easily access the mf2ff
script from everywhere, add its location to the PYTHONPATH
variable:
- Edit the appropriate dotfile for your shell and append it with export
PYTHONPATH=$PYTHONPATH:/path/to/mf2ff/
.- If you have a macOS Catalina 10.15 or newer, you probably use
zsh
. In your home directory, create or modify the file.zshenv
. - If you have a Mac OS X 10.2 Jaguar or newer, you probably use
bash
. In your home directory, create or modify the file.bash_profile
. - If you have a Mac OS X 10.1 Puma or older, you probably use
tcsh
. In your home directory, create or modify the corresponding file of thetcsh
shell. - If you don't use the shells listed above, you probably changed it by yourself.
- If you have a macOS Catalina 10.15 or newer, you probably use
- For some shells you may need to reboot.
- Now, you can always run
mf2ff
withpython3 -m mf2ff ...
in new shell sessions.
By default, mf2ff
will generate a Spline Font Database (.sfd) file. You can use the options -ttf
/ mf2ff.options['ttf'] = True
or -otf
/ mf2ff.options['otf'] = True
to generate fonts directly.
mf2ff
doesn't do much cleanup by default, as you may want to manually rework the glyphs. You can use the options -cull-at-shipout
/ mf2ff.options['cull-at-shipout'] = True
or -remove-artifacts
/ mf2ff.options['remove-artifacts'] = True
to perform some automated cleanup. Note that cull commands that are part of a glyph definition may result in the cull-at-shipout
option not making any further changes for some glyphs.
Please take a look at the limitations listed below.
The main idea of the mf2vec concept is to make METAFONT to redirect the glyphs' geometries to another program (in case of mf2ff
this program is FontForge) instead of using them to generate a bitmap font. This is possible due to the fact that METAFONT internally uses the same geometrical description which is subsequently needed in the vector font files—the Bézier curves.
The detour used by some alternatives of converting the Bézier curves to raster graphics and back to Bézier curves is circumvented. By using directly the geometry, no information gets lost.
Since METAFONT is used instead of other tools like METAPOST to read .mf
files, unmodified METAFONT code files can be used and not only the geometry but also character encoding, box sizes, kerning and ligature information etc. is shipped to the vector font without manual rework.
This interception of the information is done by redefining METAFONT commands. As METAFONT can't interact with other programs during runtime, the interpretation of the .mf file and the generation of the font need to be separated in time. Therefore, the information METAFONT would use to generate bitmap graphics is saved in METAFONT's log file. METAFONT's commands are redefined so that METAFONT writes the geometry and font properties in its log file. Once METAFONT has processed all commands of the METAFONT files, the information is read from the log file, processed to create a font and then removed from the log file to keep it clear.
The idea to this approach came up in October 2018. To check for practicability an implementation for FontForge was immediately realized in Python and the range of supported commands was progressively expanded. When initial tests showed that the idea worked, it was decided to make mf2ff
available to other interested users. This has been happening since March 2019. After a few improvements and bug fixes, development was paused for a few years. Due to community feedback, mf2ff
development was continued in July 2023.
Basic idea
The concept is based on redefining basic METAFONT commands. They are defined in such a way that the information METAFONT normally uses to create the bitmap font is written into the log file. Subsequently, this log file is then read and the instructions are passed to the program generating the vector font from it. For example, mf2ff
uses FontForge for this purpose. Afterwards, the information added by modified METAFONT commands is removed from the log file to keep it clear.
The following diagram illustrates the concept using mf2ff
as an example.
┌──────────┐
│ font.mf │
└──────────┘
v
┌──────────┐ ┌───────────┐
│ │ > │ METAFONT │
│ │ └───────────┘
│ │ v
│ │ ┌───────────┐
│ mf2ff │ < │ font.log │
│ │ └───────────┘
│ │
│ │ ┌───────────┐
│ │ > │ FontForge │
└──────────┘ └───────────┘
v v
┌──────────┐ ┌─────────────────────┐
│ font.log │ │ font.ttf / font.otf │
└──────────┘ └─────────────────────┘
Simple Example
Adding the following code to a METAFONT file will cause METAFONT not to fill
the contour c
, but to write the operations to the log file:
def fill expr c =
message "fill"; show c;
enddef;
After METAFONT finished processing the file, a script analyzes the log file and knows from that there was a fill
command and it knows the contour to be filled. This information can be passed to the font processing program to add it to the glyph.
In fact, this process is much more complicated. To be able to process METAFONT files not based on plain METAFONT, commands like addto
instead of commands like fill
need to be redefined. But those addto
commands are more complex; they also form the basis for other plain METAFONT commands like unfill
, (un
)draw
, (un
)drawdot
and erase
. These commands expand to several keywords separating the different parts of information needed to do these different operations (addto
, also
, contour
, doublepath
, withpen
, withweight
).
Another challenge is the colon in conditions and loops. These structures can appear in any other command, even in commands that use the colon as a separator (ligtable
and fontdimen
), so :
has to be redefined like other keywords to output information to the log file. A sophisticated switching between different redefinitions of the colon is required within these commands.
In addition to that, a process needs to be implemented to make it easy to find all the commands in the log file even if there are message commands in the METAFONT file not related to the redefinition of the METAFONT primitives. Those should not be interpreted as information for font generation. Therefore all information written in the log file needs to be enclosed in special keywords which are unlikely to be used in the METAFONT files' message commands.
METAFONT, developed by D. E. Knuth since 1977, is a program which generates bitmap fonts from files written in the METAFONT language. Bitmap fonts have the disadvantage that they become blurred under magnification. METAFONT was developed so that for the particular resolution of the printer a separate font was generated. Nowadays, vector fonts are standard, which do not have this problem under magnification. Therefore, they are more suitable for use on displays. Moreover, they can be used with every printer without any restrictions.
Besides the mf2vec approach with the mf2ff
presented here, the following scripts for converting METAFONT files to vector fonts are available:
Name | Method |
---|---|
MetaType1 | METAPOST |
mf2pt1 | METAPOST |
mftrace | Bitmap tracing |
TeXtrace | Bitmap tracing |
In this context, METAPOST means that the program METAPOST is used to convert every single character of the METAFONT file to a vector graphic. After that, the vector graphics are put together to get a vector font. This method has the disadvantage that METAPOST only can process a part of the METAFONT language.
Bitmap tracing means that METAFONT generates a bitmap font first. In a separate program, the bitmap of every glyph is traced and then put together to get a vector font.
Each of the methods have specific downsides. Please take a look at the comparison below for more details.
In the following table shows a comparison of the available scripts to convert METAFONT files to vector fonts.
Characteristic | METAFONT | mf2ff |
MetaType1 | mf2pt1 | mftrace | TeXtrace |
---|---|---|---|---|---|---|
Script is written in | – | Python 3 | Perl | Perl | Python 2 | Perl |
METAFONT file processing | METAFONT | METAFONT | METAPOST | METAPOST | METAFONT | METAFONT |
Subsequent processing | – | FontForge | AWK / t1asm | t1asm | autotrace or Potrace / t1asm | autotrace |
Output format | ❌ GF / TFM | ✅ TTF, OTF, SFD | 🤔 PFB | 🤔 PFB | ✅ AFM / PFA / PFB / TTF / SVG | 🤔 PFB |
Output quality | ❌ bitmap | ✅ vector graphic | ✅ vector graphic | ✅ vector graphic | 🤔 traced bitmap | 🤔 traced bitmap |
Redefines non-primitives / requires non-primitives | ✅ No | ✅ No | ❌ Yes | ❌ Yes | ✅ No | ✅ No |
Unicode support | ❌ No | ✅ Yes | ❔ | ❌ No | ❌ No | ❌ No |
Supports pen-commands | ✅ Yes | 🤔 Limited | ❌ No | 🤔 Limited | ✅ Yes | ✅ Yes |
Supports ligature and kerning commands | ✅ Yes | 🤔 Limited | ❔ | ❔ | ❔ | ❌ No |
Supports variable fonts | ❌ No | ❌ No, possibly in the future | ❌ No | ❌ No | ❔ | ❌ No |
The following are some examples created with mf2ff
. The outlines and filled characters are shown as they are displayed in FontForge. Note that the results are not perfect yet.
Some glyphs of the Computer Modern typefaces are not correctly processed yet, i.e. the middle part of capital S and sloped_serif
in lowercase letters.
The image on the left was created by deactivating cull
commands.
The stylized coast redwood tree El Palo Alto which is presented on pages 124-126 of The METAFONTbook. In the left version, the option cull-at-shipout
is activated. Since the trunk and the topmost branch share an on-curve point, the current implementation causes wrong results there.
The logo, which is presented on page 138, The METAFONTbook. In the left version, the option cull-at-shipout
is activated. The filled logo is the same in both cases.
Since mf2ff
is still under development and not thoroughly tested, there are a few limitations. They may get addressed in future updates.
If a specific limitation is holding your project back, open an issue so that future updates can focus on the needs of users.
- Pen commands
Only round, elliptical and polygonal pens are supported. It is assumed that a path of length 8 (8 points) is an ellipse. Four of these points are used to calculate the axis lengths and the angle. All paths with other lengths are interpreted as polygons. Thereby only points on the Bézier curve are processed.penrazor
is not supported (see dangerous_bend_symbol example), FontForge: "Stroke width cannot be zero"- The use of
penspeck
raises a warning but the output seems to be ok in some cases.
- The support of
cull
commands is limited. - Ligature commands
Only:
,::
,kern
,skipto
as well as the ligature operators=:
,|=:
,=:|
and|=:|
are supported. The ligtable command ignores>
in operators. Moreover, the operator||:
is not supported. - Nether
charlist
norextensible
commands are supported yet. - picture type test
Thepicture
keyword has to be redefined to initialize the picture variables as a FontForge vector layer. As METAFONT uses the type keywords for both, variable declaration and type test in boolean expressions, thepicture
keyword cannot be used to test if a variable is of typepicture
. - System of equations of picture variables
METAFONT's pictures can be interpreted as arrays representing the value of the pixels*. A system of equations of these pictures can be solved by solving systems of equations for each and every pixel. This method cannot be transferred to vector graphics. Therefore systems of equations of pictures are not supported, but simple assignments and assignment-like equations should work fine.
* However, METAFONT stores picture variables by storing the difference between successive pixels. - Pen and path types are the same
In fact, every pen variable is a path variable. Therefore, commands likemakepen
andmakepath
have no effect. Pens and path cannot be distinguished usingpen
orpath
in a boolean expression. There is a optionis_type
that introducesis_pen
oris_path
which might help you to circumvent this problem. - nested conditions or loops inside of
ligtable
andfontdimen
In some situations,mf2ff
needs to redefine the colon (:
). This may cause problems in processing multiple nestedif
...(elseif
)...(end
)...fi
and/orfor
/forsuffixes
/forever
...endfor
inside of commands that use the colon in their own syntax, i.e.ligtable
andfontdimen
. charlist
andextensible
- FontForge sometimes hangs
FontForge hangs in certain situations while stroking a contour. So far, no particular trigger has been identified. This may be a bug in FontForge. - FontForge sometimes raises errors
FontForge raises errors in certain situations while processing cull commands or addto commands with a pen. Nevertheless, the results—especially those of cull commands—often seem to be ok. This may be a bug in FontForge. The errors are hardcoded in FontForge so they cannot be caught or suppressed.
There are a few things, that might help. Keep in mind that I don't understand them 100%:
- add
/usr/local/lib/python3/dist-packages
to your$PYTHONPATH
(addexport PYTHONPATH=$PYTHONPATH:/usr/local/lib/python3/dist-packages
to your~/.profile
)- seems to be relevant for Debian systems (I used it on Ubuntu)
- seems only needed for Python 3
- seems only needed if FontForge is built from source
- inspired by Update 2 of this comment of FontForge issue #2966 which suggests to add
/usr/local/lib/python3.4/site-packages
to Python's path