_ _ (_) ___ | |_ ____ | |/ _ \| __|_ / | | (_) | |_ / / _/ |\___/ \__/___| |__/
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.
Technical notes:
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.
<!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>
<!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>
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
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:
#!/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()
//
// 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;
}
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/
To send a PDF file (e.g. the single-page font summary "fontpage.pdf") to the default system printer:
lp fontpage.pdf