Skip to content

Latest commit

 

History

History
356 lines (261 loc) · 10.8 KB

README.md

File metadata and controls

356 lines (261 loc) · 10.8 KB

GlyphSynth: Pythonic vector graphics synthesis toolkit

Python versions PyPI Tests Coverage Code style: black

Motivation

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.

Getting started

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.

Interface

Glyphs can be constructed in two ways, or a combination of both:

  • Subclass BaseGlyph and implement draw()
    • Parameterize with a subclass of BaseParams corresponding to the glyph
  • Create an instance of EmptyGlyph (or any other BaseGlyph 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:

Blue-square

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",
    ),
)

Exporting

Programmatically

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

CLI

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 subclass
  • BaseGlyph 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

Examples

Multi-square

Multi-square

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)

Multi-square fractal

Multi-square fractal

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),
)

Letter combination variants

Variants matrix

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.