_       _       
  (_) ___ | |_ ____
  | |/ _ \| __|_  /
  | | (_) | |_ / / 
 _/ |\___/ \__/___|
|__/               

Font Foundry - Dublin Maker 2023

Introduction

My project for Dublin Maker 2023 (2 Sept 2023 in Richmond Barracks, Inchicore) is Font Foundry, which lets you design your own custom font. Draw some example alphabet letters on paper and then Font Foundry transforms your design into a complete set of characters - your very own custom font! You will receive a printout of your complete font with a web link to download the font file so that you can use it happily ever after for all your most important documents.

Workflow illustration for Font Foundry art installation

Visitor experience

Equipment

Software

Technical notes:

Some useful / informative videos

Although I've been using Inkscape for years, it's such a rich and powerful tool that you could be using it for a lifetime and still be learning new features. One of my personal objectives for the Font Foundry project was to reach outside my Inkscape comfort zone and experiment with features I hadn't used before. There are now several YouTube channels providing first-rate instructional videos about Inkscape. The following are some of the videos I found particularly useful for learning about specific features of Inkscape, FontForge, etc. that are used in Font Foundry.

Make Your Own Font in Inkscape - Create for Free

Inkscape 1.2 Trace Bitmap - Create for Free

FontForge Master Class Part 1 - Install & Intro - Michael Harmon

Another useful YouTube channel with a lot of great Inkscape content is Logos by Nick.

Notes

Development notes

Required items / actions

  1. tedz.eu/ff/index.php - With URL parameter "?font=FONTNAME" this displays the file downloads for FONTNAME. Without URL param, it lists the fonts on the server as links.
  2. tedz.eu/ff/upload/index.php - Used to upload a new 'b' photo. This can be accessed from browser on phone as "tedz.eu/ff/upload".
  3. tedz.eu/ff/storephoto.php
  4. font_template.svg - Inkscape SVG template with sublayers for font glyphs, guides, etc.
  5. Either ff_new.py (standalone script) or ff_import.inx, ff_import.py (Inkscape extension) to clean and trace 'b' photo and load it into font_template.svg, creating multiple copies with predefined ids ready to be carved into standard primitive strokes.
  6. MANUAL Carve multiple copies of 'b' glyph into standard primitive strokes with pre-defined XML ids
  7. ff_generate.inx, ff_generate.py - Inkscape extension to populate font sublayers with glyphs (build from primitive strokes)
  8. MANUAL Review and save FONTNAME.svg, tweaking glyphs if necessary.
  9. font_page_template.svg - template for creating FONTNAME_page.svg.
  10. ff_complete.sh - Shell script to convert FONTNAME.svg to FONTNAME.ttf, copy FONTNAME.ttf to "~/.fonts/", create FONTNAME_page.svg, export to FONTNAME.pdf, ftp upload .
  11. tedz.eu/ff/index.php

Font Foundry main page "tedz.eu/ff/index.php"

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Font Foundry</title>
</head>
<body>
<pre class="asciiart">
 _____           _     _____                     _            
|  ___|__  _ __ | |_  |  ___|__  _   _ _ __   __| |_ __ _   _ 
| |_ / _ \| '_ \| __| | |_ / _ \| | | | '_ \ / _` | '__| | | |
|  _| (_) | | | | |_  |  _| (_) | |_| | | | | (_| | |  | |_| |
|_|  \___/|_| |_|\__| |_|  \___/ \__,_|_| |_|\__,_|_|   \__, |
                                                        |___/ 
</pre>

<p><a href="https://tedz.eu/ff">Home</a>  <a href="https://tedz.eu/ff/upload">Upload</a></p>

<?php
if ($_SERVER['REQUEST_METHOD'] === 'GET' and !empty($_GET["font"]))
{
  $font_name = $_GET["font"];
  if (!is_dir($font_name))
  {
    echo("<p>Error: Font \"$font_name\" not found.</p>" . PHP_EOL);
  }
  else
  {
    // Display file download links
    echo("<h2>Font Download</h2>" . PHP_EOL);
    echo("<h3>Font name: \"$font_name\"</h3>" . PHP_EOL);
    $photo_filename = glob("$font_name/b_photo.*")[0];
    if (!empty($photo_filename))
      echo("<a href=\"$photo_filename\"><img src=\"$photo_filename\" style=\"width:200px;\"></a><br>" . PHP_EOL);

    // TTF font file
    if (file_exists("$font_name/$font_name.ttf"))
      echo("<p>TrueType font file: <a href=\"$font_name/$font_name.ttf\">$font_name.ttf</a></p>" . PHP_EOL);
    else
      echo("<p>TrueType font file: <s>$font_name.ttf</s></p>" . PHP_EOL);

    // SVG font file
    if (file_exists("$font_name/$font_name.svg"))
      echo("<p>SVG font file: <a href=\"$font_name/$font_name.svg\">$font_name.svg</a></p>" . PHP_EOL);
    else
      echo("<p>SVG font file: <s>$font_name.svg</s></p>" . PHP_EOL);

    // Font summary page PDF
    if (file_exists("$font_name/$font_name_page.pdf"))
      echo("<p>Font summary page PDF: <a href=\"$font_name/$font_name_page.pdf\">$font_name_page.pdf</a></p>" . PHP_EOL);
    else
      echo("<p>Font summary page PDF: <s>$font_name_page.pdf</s></p>" . PHP_EOL);

    // Font summary page SVG
    if (file_exists("$font_name/$font_name_page.svg"))
      echo("<p>Font summary page SVG: <a href=\"$font_name/$font_name_page.svg\">$font_name_page.svg</a></p>" . PHP_EOL);
    else
      echo("<p>Font summary page SVG: <s>$font_name_page.svg</s></p>" . PHP_EOL);
  }
}

// Display list of font directories
echo("<h2>Fonts</h2>" . PHP_EOL);
echo("<ul>" . PHP_EOL);
foreach (glob('*') as $file)
{
  if (is_dir($file) and $file != 'upload')
  {
    echo("<li><a href=\"index.php?font=$file\">$file</a></li>" . PHP_EOL);
  }
}
echo("</ul>" . PHP_EOL);
?>

</body>
</html>

Font Foundry photo uploader "tedz.eu/ff/upload/index.php"

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Font Foundry photo uploader</title>
</head>
<body>
<pre class="asciiart">
 _____           _     _____                     _            
|  ___|__  _ __ | |_  |  ___|__  _   _ _ __   __| |_ __ _   _ 
| |_ / _ \| '_ \| __| | |_ / _ \| | | | '_ \ / _` | '__| | | |
|  _| (_) | | | | |_  |  _| (_) | |_| | | | | (_| | |  | |_| |
|_|  \___/|_| |_|\__| |_|  \___/ \__,_|_| |_|\__,_|_|   \__, |
                                                        |___/ 
</pre>

<p><a href="https://tedz.eu/ff">Home</a>  <a href="https://tedz.eu/ff/upload">Upload</a></p>

<h1>Font Foundry photo uploader</h1>

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
  // Get font name, photo file name, temp file name
  $font_name = $_POST["font_name"];
  $file_name = basename($_FILES["file_to_upload"]["name"]);
  $tmp_name = $_FILES["file_to_upload"]["tmp_name"];

  // Remove spaces and special characters
  $font_name = str_replace( array('.', '\'', '"', ',' , ';', '<', '>' ), '', $font_name);
  $font_name = preg_replace("/\s+/", "", $font_name);
  $file_name = str_replace( array('\'', '"', ',' , ';', '<', '>' ), '', $file_name);
  $file_name = preg_replace("/\s+/", "", $file_name);
  $file_name = trim($file_name, ".");
  $file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
  $target_file = "../$font_name/" . "b_photo." . $file_ext;

  if (empty($font_name))
  {
    echo("<p>Error: Font name not specified.</p>" . PHP_EOL);
  }
  else if (is_dir("../$font_name") or file_exists("../$font_name"))
  {
    echo("<p>Error: Font name already in use.</p>" . PHP_EOL);
  }
  else if (!mkdir("../$font_name", 0755))
  {
    echo("<p>Error: Failed to create directory \"$font_name\".</p>" . PHP_EOL);
  }
  else if (!move_uploaded_file($tmp_name, $target_file))
  {
    echo("<p>Error: Failed to copy file to target location.</p>" . PHP_EOL);
  }
  else
  {
    echo("<h3>Upload successful!</h3>" . PHP_EOL);
  }

  // Print information
  echo("<p>font_name = $font_name</p>" . PHP_EOL);
  echo("<p>file_name = $file_name</p>" . PHP_EOL);
  //echo("<p>tmp_name = $tmp_name</p>" . PHP_EOL);
  echo("<p>target_file = $target_file</p>" . PHP_EOL);

  echo("<p><a href=\"$target_file\"><img src=\"$target_file\" alt=\"Hand-drawn letter b photo\" style=\"width:200px;\"></a></p>" . PHP_EOL);
  echo("<p><a href=\"../index.php?font=$font_name\">Link to font page</a></p><br>" . PHP_EOL);
  echo("<h2>Upload another photo</h2>");
}
else
{
  echo("<h2>Upload photo</h2>");
}
?>

<form action="index.php" method="post" enctype="multipart/form-data">
  Font name: <input type="text" name="font_name"><br><br>
  Select file to upload (max file size 4MB):<br><br>
  Photo file: <input type="file" name="file_to_upload" id="file_to_upload" accept="image/*" capture><br><br>
  <input type="submit" value="Upload File" name="submit"><br>
</form>

</body>
</html>

Running Inkscape 1.3 (using appimage):

First, download the Inkscape 1.3 appimage from the download page on inkscape.org (update the filename if necessary):

curl -O https://media.inkscape.org/dl/resources/file/Inkscape-0e150ed-x86_64.AppImage

Now, to run the appimage...

sudo apt install libfuse2                   # Enable fuse2 compatibility (fuse3 is active by default in Debian 12.0)
chmod u+x Inkscape-0e150ed-x86_64.AppImage  # Make the appimage executable (updating the appimage filename if necessary)
./Inkscape-0e150ed-x86_64.AppImage          # Run Inkscape 1.3

Font Foundry Inkscape extension

File "font_foundry_extension.inx":

<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
  <name>Font Foundry</name>
  <id>org.inkscape.fontfoundry.effect</id>
  <param name="int_param" type="int" gui-text="An integer:"></param>
  <effect>
    <!--object-type>path</object-type-->
    <effects-menu>
      <submenu name="Font Foundry"/>
    </effects-menu>
  </effect>
  <script>
    <command location="inx" interpreter="python">font_foundry_extension.py</command>
  </script>
</inkscape-extension>

File "font_foundry_extension.py" (NOTE Close the SVG Font Editor dialog before running the Font Foundry extension):

#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2023 Ted Burke, ted.burke@tudublin.ie
#
"""
This is the Font Foundry extension for Inkscape, written by Ted Burke for the Font Foundry exhibit at Dublin Maker 2023. Last updated Aug 2023.
"""
import sys
import inkex
from inkex.elements import Group, PathElement, Glyph, Defs 
from lxml import etree

class FontFoundryExtension(inkex.EffectExtension):
    def add_arguments(self, pars):
        pars.add_argument("--int_param", type=int, help="An example int param")

    def effect(self):
        self.debug('Start of effect method')
        self.msg("This is the Font Foundry extension.")

        # Print out some guideline positions
        #self.debug(self.svg.getElementById('guide_baseline').position)

        # Iterate over groups looking for font sublayer that matches a given character
        for lyr in self.svg.xpath('//svg:g'):
            self.debug(lyr.get('id') + ' ' + lyr.get('inkscape:groupmode'))
            if lyr.get('inkscape:label') == 'U+000063 c c':
                p = etree.Element(inkex.addNS('path', 'svg'))
                p.set('d','M 100,100 v 100 h 100 v -100 z')
                p.set('style', "color:#0000FF;display:inline;fill:#000000;-inkscape-stroke:none")
                lyr.append(p)

        # Iterate over glyph elements looking for the one that matches a given character
        for gl in self.svg.xpath('//svg:glyph'):
            self.debug(gl.get('id') + ' ' + gl.get('glyph-name'))
            if gl.get('glyph-name') == 'c':
                gl.set('d','M 100,100 v 100 h 100 v -100 z')

        # Useful list of inkex.elements
        self.debug(dir(inkex.elements))

        # Style selected elements (just as an experiment)
        for elem in self.svg.selection:
            #elem.style['fill'] = 'red'
            #elem.style['fill-opacity'] = 1
            #elem.style['opacity'] = 1
            pass

        # Create layers
        for i in [1, 2, 3]:
            self.msg("Creating path for glyph " + str(i))
            newpath = PathElement()
            newpath.path = "M 10 10 L 20 20 C 20 20 10 10 10 0 z"
            newpath.transform.add_translate(self.svg.namedview.center)
            self.msg("Creating layer glyph_" + str(i))
            newlayer = self.svg.add(Group.new('glyph_' + str(i), is_layer=True))
            newlayer.append(newpath)

        # Write document to Inkscape SVG file and plain SVG file
        inkex.command.write_svg(self.svg, "ff_temp.svg")
        #kwargs = {"export-plain-svg": "ff_plain.svg"}
        #inkex.command.inkscape("ff_temp.svg", **kwargs)

if __name__ == '__main__':
    FontFoundryExtension().run()

NOT REQUIRED Convert an Inkscape SVG document to plain SVG (example from here, which also provides many other interesting example command lines):

inkscape --export-plain-svg --export-filename=filename2.svg filename1.svg

Inkex code to style selected elements:

for elem in self.svg.selection:
    elem.style['fill'] = 'red'
    elem.style['fill-opacity'] = 1
    elem.style['opacity'] = 1

Idea for exporting plain SVG file from document open in existing Inkscape window:

inkscape --active-window --batch-process --export-plain-svg --export-filename=plain.svg

Image files (jpg, svg) for testing:

ff_new.py - Python script to start the font creation process

#!/usr/bin/python3

import sys
import os
import urllib3
import numpy as np
from PIL import Image, ImageFilter, ImageChops

# Get font name from command line argument
if len(sys.argv) < 2:
    print("Font name must be specified as first command line argument. Exiting.")
    quit()

font_name = sys.argv[1]
print("Font name: " + font_name)

# Download b_photo image filename
http = urllib3.PoolManager()
url = "https://tedz.eu/ff/" + font_name + "/b_photo.txt"
print("Retrieving url: " + url)
resp = http.request("GET", url)
print("urllib3 response status: " + str(resp.status))
b_photo_filename = resp.data.decode('utf-8')
print("b_photo filename: " + b_photo_filename)

# Download b_photo image file
url = "https://tedz.eu/ff/" + font_name + "/" + b_photo_filename
resp = http.request("GET", url)
print("urllib3 response status: " + str(resp.status))
open(b_photo_filename, 'wb').write(resp.data)

# Load image, convert to grey scale, then save to file
im = Image.open(b_photo_filename) # Load image from file
im = im.convert("L")
w,h = im.size
marg = 10 # margin to crop from perimeter of photo
im = im.crop((marg,marg,w-marg,h-marg))
w,h = im.size
im.save('b_photo_grey.png')
os.system('./photo_cleaner ' + str(w) + ' ' + str(h))

quit()

# Load b_photo using pillow library
im_orig = Image.open(b_photo_filename) # Load image from file
w,h = im_orig.size
#mgn = 10
#left,right,top,bottom = mgn,w-mgn,mgn,h-mgn
#im_orig = im_orig.crop((left, top, right, bottom))
#w,h = im_orig.size

# Build background image by setting each pixel equal to the
# brightest value in a scattering of nearby neighbours on
# a cross centred on the point

im_grey = im_orig.convert("L")           # Convert to greyscale

im_bgnd = im_grey.filter(ImageFilter.MaxFilter(size=25)) # size must be odd number

#im_diff = ImageChops.subtract(im_bgnd, im_grey, offset=0)

# A different way to calculate a difference image
print("im_grey.size = " + str(im_grey.size))
print("im_bgnd.size = " + str(im_bgnd.size))
buf_grey = np.asarray(im_grey)
buf_bgnd = np.asarray(im_bgnd)
buf_diff = buf_bgnd - buf_grey
print("Before colour scaling...")
print("buf_diff.max() = " + str(buf_diff.max()))
print("buf_diff.min() = " + str(buf_diff.min()))
buf_diff = (255.0 / buf_diff.max()) * buf_diff
print("After colour scaling...")
print("buf_diff.max() = " + str(buf_diff.max()))
print("buf_diff.min() = " + str(buf_diff.min()))
buf_diff = 255 * (buf_diff > 64)
im_diff = Image.fromarray(np.rint(buf_diff).astype(np.uint8))

im_grey.save('b_grey.jpg')
im_bgnd.save('b_bgnd.jpg')
im_diff.save('b_biff.jpg')
im_diff.show()

photo_cleaner.c - A faster C program to help with the 'b' image processing

//
// b_photo cleaning utility using FFmpeg to read and write image files
// Written by Ted Burke - last updated 2-9-2023
//
// To compile:
//     gcc photo_cleaner.c -o photo_cleaner
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Video resolution
#define W 1280
#define H 720

int main(int argc, char** argv)
{
    unsigned char *p, *q;
    int x, y, count;

    char command[1024];

    // Read image width and height from command line args
    int w, h;
    w = atoi(argv[1]);
    h = atoi(argv[2]);

    // Allocate input and output pixel buffers
    p = malloc(w*h);
    q = malloc(w*h);

    // Open an input pipe from ffmpeg
    FILE *pipein = popen("ffmpeg -hide_banner -loglevel error -i b_photo_grey.png -f image2pipe -vcodec rawvideo -frames:v 1 -pix_fmt gray -", "r");

    // Open an output pipe to a second instance of ffmpeg
    sprintf(command, "ffmpeg -hide_banner -loglevel error -y -f rawvideo -pix_fmt gray -s %dx%d -i - -update 1 b_photo_clean.png", w, h);
    FILE *pipeout = popen(command, "w");

    // Process image
    // Read image from the input pipe into the buffer
    count = fread(p, 1, h*w, pipein);

    // Check we got the expected number of pixels
    if (count != h*w)
    {
        fprintf(stderr, "Failed to read expected number of pixels.\n");
        return 1;
    }

    // Process the frame
    for (y=0 ; y<h ; ++y) for (x=0 ; x<w ; ++x)
    {
        // Invert each colour component in every pixel
        q[y*w+x] = 255 - p[y*w+x];
    }

    // Write this frame to the output pipe
    fwrite(q, 1, w*h, pipeout);

    FILE *f=fopen("b_photo_clean.pgm", "wb");
    fprintf(f, "P5\n%d %d\n255\n", w, h);
    fwrite(q, 1, w*h, f);
    fclose(f);

    // Flush and close input and output pipes
    fflush(pipein);
    pclose(pipein);
    fflush(pipeout);
    pclose(pipeout);

    // Free pixel buffer
    free(p);
    free(q);

    return 0;
}

Converting the SVG font to TrueType and installing in user fonts directory

This shell command converts an Inkscape SVG file (containing a font) to a TrueType font file (.ttf):

fontforge -lang=ff -c 'Open($1); Generate($2)' font.svg font.ttf

To create a script to perform font conversion similar to above, but taking two filenames as command line arguments, create a file called "fontconvert" containing the following:

#!/bin/bash
fontforge -lang=ff -c 'Open($1); Generate($2)' "$1" "$2"

Make it executable:

chmod u+x fontconvert

Use it as follows:

./fontconvert myfont.svg myfont.ttf

To install the TrueType font in the user's font directory:

mv myfont.ttf ~/.fonts/

Printing PDF

To send a PDF file (e.g. the single-page font summary "fontpage.pdf") to the default system printer:

lp fontpage.pdf