This project provides a Pythonic mechanism to construct SVG graphics, termed as "glyphs". Glyphs can be parameterized and leverage inheritance to promote reuse. The ability to construct many variations of glyphs programmatically is a powerful tool for creativity.
First, install using pip:
pip install glyphsynth
The user is intended to develop glyphs using their own Python modules. A typical workflow might be to create a number of BaseGlyph
subclasses, set them in __all__
, and invoke glyphsynth-export
passing in the module and output path. See below for more details.
Glyphs can be constructed in two ways, or a combination of both:
- Subclass
BaseGlyph
and implementdraw()
- Parameterize with a subclass of
BaseParams
corresponding to the glyph
- Parameterize with a subclass of
- Create an instance of
EmptyGlyph
(or any otherBaseGlyph
subclass) and invoke draw APIs
In its draw()
method, a BaseGlyph
subclass can invoke drawing APIs which create corresponding SVG objects. SVG properties are automatically propagated to SVG objects from the glyph's properties, BaseGlyph.properties
, which can be provided at runtime with defaults being specified by the subclass.
A simple example of implementing draw()
to draw a blue square:
from glyphsynth import BaseParams, BaseGlyph, ShapeProperties
# Glyph params
class MySquareParams(BaseParams):
color: str
# Glyph subclass
class MySquareGlyph(BaseGlyph[MySquareParams]):
# Canonical size for glyph construction, can be rescaled upon creation
size_canon = (100.0, 100.0)
def draw(self):
# Draw a centered square using the provided color
self.draw_rect(
(25.0, 25.0),
(50.0, 50.0),
properties=ShapeProperties(fill=self.params.color),
)
# Draw a black border around the perimeter
self.draw_polyline(
[(0.0, 0.0), (0.0, 100.0), (100.0, 100.0), (100.0, 0), (0.0, 0.0)],
properties=ShapeProperties(
stroke="black",
fill="none",
stroke_width="5",
),
)
# Create glyph instance
blue_square = MySquareGlyph(
glyph_id="blue-square", params=MySquareParams(color="blue")
)
# Render as image
blue_square.export_png(Path("my_glyph_renders"))
This is rendered as:
Equivalently, the same glyph can be constructed from an EmptyGlyph
:
from glyphsynth import EmptyGlyph
blue_square = EmptyGlyph(glyph_id="blue-square", size=(100, 100))
# Draw a centered square
blue_square.draw_rect(
(25.0, 25.0), (50.0, 50.0), properties=ShapeProperties(fill="blue")
)
# Draw a black border around the perimeter
blue_square.draw_polyline(
[(0.0, 0.0), (0.0, 100.0), (100.0, 100.0), (100.0, 0), (0.0, 0.0)],
properties=ShapeProperties(
stroke="black",
fill="none",
stroke_width="5",
),
)
A glyph is exported as an .svg
file. Rasterizing to .png
is supported on Linux and requires the following packages:
sudo apt install librsvg2-bin libmagickwand-dev
A glyph can be exported using BaseGlyph.export()
, BaseGlyph.export_svg()
, or BaseGlyph.export_png()
. If a folder is passed as the output path, the glyph's glyph_id
will be used to derive the filename.
from pathlib import Path
# Export to specific path
blue_square.export(Path("my_glyph_renders/blue-square.svg"))
blue_square.export(Path("my_glyph_renders/blue-square.png"))
# Export using class name as filename
blue_square.export_svg(Path("my_glyph_renders")) # blue-square.svg
blue_square.export_png(Path("my_glyph_renders")) # blue-square.png
The CLI tool glyphsynth-export
exports glyphs by importing a Python object. See glyphsynth-export --help
for full details.
The object can be any of the following:
- Module, from which objects will be extracted via
__all__
BaseGlyph
subclassBaseGlyph
instance- Iterable
- Callable
Any BaseGlyph
subclasses found will be instantiated using their respective default parameters. For Iterable
and Callable
, the object is traversed or invoked recursively until glyph subclasses or instances are found.
Assuming the above code containing the blue_square
is placed in my_glyphs.py
, the glyph can be exported to my_glyph_renders/
via the following command:
glyphsynth-export my_glyphs.blue_square my_glyph_renders --svg --png
This glyph is composed of 4 nested squares, each with a color parameter.
from glyphsynth import BaseParams, BaseGlyph
# Definitions
ZERO: float = 0.0
UNIT: float = 100.0
HALF: float = UNIT / 2
UNIT_SIZE: tuple[float, float] = (UNIT, UNIT)
ORIGIN: tuple[float, float] = (ZERO, ZERO)
# Multi-square parameters
class MultiSquareParams(BaseParams):
color_upper_left: str
color_upper_right: str
color_lower_left: str
color_lower_right: str
# Multi-square glyph class
class MultiSquareGlyph(BaseGlyph[MultiSquareParams]):
size_canon = UNIT_SIZE
def draw(self):
# Each nested square should occupy 1/4 of the area
size: tuple[float, float] = (HALF, HALF)
# Draw upper left
self.draw_rect(
ORIGIN,
size,
properties=ShapeProperties(fill=self.params.color_upper_left),
)
# Draw upper right
self.draw_rect(
(HALF, ZERO),
size,
properties=ShapeProperties(fill=self.params.color_upper_right),
)
# Draw lower left
self.draw_rect(
(ZERO, HALF),
size,
properties=ShapeProperties(fill=self.params.color_lower_left),
)
# Draw lower right
self.draw_rect(
(HALF, HALF),
size,
properties=ShapeProperties(fill=self.params.color_lower_right),
)
# Create parameters
multi_square_params = MultiSquareParams(
color_upper_left="red",
color_upper_right="orange",
color_lower_right="green",
color_lower_left="blue",
)
# Create glyph
multi_square = MultiSquareGlyph(glyph_id="multi-square", params=multi_square_params)
This glyph nests a square glyph recursively up to a certain depth.
from glyphsynth import BaseParams, BaseGlyph
# Maximum recursion depth for creating fractal
FRACTAL_DEPTH = 10
class SquareFractalParams(BaseParams):
square_params: MultiSquareParams
depth: int = FRACTAL_DEPTH
class SquareFractalGlyph(BaseGlyph[SquareFractalParams]):
size_canon = UNIT_SIZE
def draw(self):
# Draw square
self.insert_glyph(MultiSquareGlyph(params=self.params.square_params))
if self.params.depth > 1:
# Draw another fractal glyph, half the size and rotated 90 degrees
child_params = SquareFractalParams(
square_params=self.params.square_params,
depth=self.params.depth - 1,
)
child_glyph = SquareFractalGlyph(
params=child_params, size=(HALF, HALF)
)
# Rotate and insert in center
child_glyph.rotate(90.0)
self.insert_glyph(child_glyph, insert=(HALF / 2, HALF / 2))
multi_square_params = MultiSquareParams(
color_upper_left="rgb(250, 50, 0)",
color_upper_right="rgb(250, 250, 0)",
color_lower_right="rgb(0, 250, 50)",
color_lower_left="rgb(0, 50, 250)",
)
fractal = SquareFractalGlyph(
glyph_id="multi-square-fractal",
params=SquareFractalParams(square_params=multi_square_params),
)
This illustrates the use of letter glyphs, provided by this package as a library, to create parameterized geometric designs. Permutations of pairs of letters A
, M
, and T
are selected for a range of color variants, with the second letter being rotated 180 degrees.
from glyphsynth.lib.alphabet.minimal import (
UNIT,
LetterParams,
BaseLetterGlyph,
LetterComboParams,
BaseLetterComboGlyph,
A,
M,
T,
)
# Letters to use for variants
LETTERS = [
A,
M,
T,
]
# Colors to use for variants
COLORS = [
"black",
"red",
"green",
"blue",
]
# Params containing 2 letters which are overlayed
class AMTComboParams(LetterComboParams):
letter1: type[BaseLetterGlyph]
letter2: type[BaseLetterGlyph]
# Glyph class
class AMTComboGlyph(BaseLetterComboGlyph[AMTComboParams]):
def draw(self):
# draw letters given by params
self.draw_letter(self.params.letter1)
letter2 = self.draw_letter(self.params.letter2)
# additionally rotate letter2
letter2.rotate(180)
A subclass of BaseVariantExportFactory
can be used as a convenience for generating variants:
from typing import Generator
import itertools
from glyphsynth.lib.variants import BaseVariantExportFactory
# Factory to create variants of AMTComboGlyph
class VariantFactory(BaseVariantExportFactory[AMTComboGlyph]):
# Width of resulting matrix
MATRIX_WIDTH = len(COLORS)
# Top-level padding and space between glyph variants
SPACING = UNIT / 10
# Glyph subclass from which to generate variants
glyph_cls = AMTComboGlyph
# Generate variants of colors and letter combinations
def get_params_variants(self) -> Generator[AMTComboParams, None, None]:
for color, letter1, letter2 in itertools.product(
COLORS, LETTERS, LETTERS
):
yield AMTComboParams(
letter_params=LetterParams(color=color),
letter1=letter1,
letter2=letter2,
)
The fully-qualified class name of VariantFactory
can be passed as an argument to glyphsynth-export
. This will result in a folder structure containing each variant individually, as well as the variant matrix and each individual row/column.