Skip to content

Instantly share code, notes, and snippets.

@p3t3r67x0
Last active November 26, 2023 16:54
Show Gist options
  • Save p3t3r67x0/a35e9e0e9f6f22053e8f7a5543b59724 to your computer and use it in GitHub Desktop.
Save p3t3r67x0/a35e9e0e9f6f22053e8f7a5543b59724 to your computer and use it in GitHub Desktop.
Create a svg from truetype font with freetype and svgpathtools in python

Create a svg from truetype font in python

I am trying to create a svg file from a truetype font with freetype-py and svgpathtools. I have a working python code which generates the svg file but unfortunately there are some corners and edges where instead should be smooth curves. Here in this example I try to render an special char which does not look good at all.

Question

What do I need to change in order to get curve symbols redered correctly?

Prerequisites

pip3 install freetype-py
pip3 install svgpathtools
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

import sys

from freetype import Face, FT_Curve_Tag, FT_Curve_Tag_On
from svgpathtools import (wsvg, Line, CubicBezier, QuadraticBezier, Path)



fontpath = sys.argv[1]

face = Face(fontpath)
face.set_char_size(128 * 256)

for char in face.get_chars():
    # char = chr(char[0])
    char = 'ȸ'
    non_chars = [32, 160, 847, 858, 1995, 8192, 8193, 8194, 8195, 8196,
                 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205,
                 8206, 8207, 8209, 8232, 8233, 8234, 8235, 8236, 8237,
                 8238, 8239, 8287, 8288, 8289, 8290, 8291, 8292, 8293,
                 8294, 8295, 8296, 8297, 8298, 8299, 8300, 8301, 8302,
                 8303, 9864, 10240, 11604, 65024, 65025, 65026, 65027,
                 65028, 65029, 65030, 65031, 65032, 65033, 65034, 65035,
                 65036, 65037, 65038, 65039, 65279, 65529, 65530, 65531,
                 65532, 65533, 66319]

    if ord(char) in non_chars:
        continue

    face.load_char(char)

    outline = face.glyph.outline

    y = [t[1] for t in outline.points]

    # flip the points
    outline_points = [(p[0], max(y) - p[1]) for p in outline.points]
    start, end = 0, 0
    paths = []

    for i in range(len(outline.contours)):
        end = outline.contours[i]

        points = outline_points[start:end + 1]
        print(points)
        points.append(points[0])

        tags = outline.tags[start:end + 1]
        tags.append(tags[0])

        segments = [[points[0], ], ]

        for j in range(1, len(points)):
            segments[-1].append(points[j])

            if (FT_Curve_Tag(tags[j]) == FT_Curve_Tag_On) and j < (len(points) - 1):
                segments.append([points[j], ])

        for segment in segments:
            if len(segment) == 10:
                print(char)
                sys.exit(1)

            if len(segment) == 2:
                paths.append(Line(
                    start=tuple_to_image(segment[0]),
                    end=tuple_to_image(segment[1])))

            elif len(segment) == 3:
                print('0', segment[0])
                print('1', segment[1])
                print('2', segment[2])

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[0]),
                    control=tuple_to_image(segment[1]),
                    end=tuple_to_image(segment[2])))

            elif len(segment) == 4:
                C = ((segment[1][0] + segment[2][0]) / 2.0,
                     (segment[1][1] + segment[2][1]) / 2.0)

                print('0', segment[0])
                print('1', segment[1])
                print('C', C)
                print('2', segment[2])
                print('3', segment[3])

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[0]),
                    control=tuple_to_image(segment[1]),
                    end=tuple_to_image(C)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(C),
                    control=tuple_to_image(segment[2]),
                    end=tuple_to_image(segment[3])))

            elif len(segment) == 5:
                C = ((segment[1][0] + segment[2][0]) / 2.0,
                     (segment[1][1] + segment[2][1]) / 2.0)

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[0]),
                    control=tuple_to_image(segment[1]),
                    end=tuple_to_image(C)))

                paths.append(CubicBezier(
                    start=tuple_to_image(C),
                    control1=tuple_to_image(segment[2]),
                    control2=tuple_to_image(segment[3]),
                    end=tuple_to_image(segment[4])))

            elif len(segment) == 6:
                C = ((segment[0][0] + segment[1][0]) / 2.0,
                     (segment[0][1] + segment[1][1]) / 2.0)

                C = ((segment[1][0] + segment[2][0]) / 2.0,
                     (segment[1][1] + segment[2][1]) / 2.0)

                D = ((segment[2][0] + segment[3][0]) / 2.0,
                     (segment[2][1] + segment[3][1]) / 2.0)

                E = ((segment[3][0] + segment[4][0]) / 2.0,
                     (segment[3][1] + segment[4][1]) / 2.0)

                print('0', segment[0])
                print('1', segment[1])
                print('C', C)
                print('2', segment[2])
                print('D', D)
                print('3', segment[3])
                print('E', E)
                print('4', segment[4])
                print('5', segment[5])

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[0]),
                    control=tuple_to_image(segment[1]),
                    end=tuple_to_image(C)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(C),
                    control=tuple_to_image(segment[2]),
                    end=tuple_to_image(D)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(D),
                    control=tuple_to_image(segment[3]),
                    end=tuple_to_image(E)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(E),
                    control=tuple_to_image(segment[4]),
                    end=tuple_to_image(segment[5])))

            elif len(segment) == 7:
                C = ((segment[0][0] + segment[1][0]) / 2.0,
                     (segment[0][1] + segment[1][1]) / 2.0)

                C = ((segment[1][0] + segment[2][0]) / 2.0,
                     (segment[1][1] + segment[2][1]) / 2.0)

                D = ((segment[2][0] + segment[3][0]) / 2.0,
                     (segment[2][1] + segment[3][1]) / 2.0)

                E = ((segment[3][0] + segment[4][0]) / 2.0,
                     (segment[3][1] + segment[4][1]) / 2.0)

                F = ((segment[4][0] + segment[5][0]) / 2.0,
                     (segment[4][1] + segment[5][1]) / 2.0)

                print('0', segment[0])
                print('1', segment[1])
                print('C', C)
                print('2', segment[2])
                print('D', D)
                print('3', segment[3])
                print('E', E)
                print('4', segment[4])
                print('F', F)
                print('5', segment[5])
                print('6', segment[6])

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[0]),
                    control=tuple_to_image(segment[1]),
                    end=tuple_to_image(C)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(C),
                    control=tuple_to_image(segment[2]),
                    end=tuple_to_image(D)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(D),
                    control=tuple_to_image(segment[3]),
                    end=tuple_to_image(E)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(E),
                    control=tuple_to_image(segment[4]),
                    end=tuple_to_image(F)))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(F),
                    control=tuple_to_image(segment[5]),
                    end=tuple_to_image(segment[6])))

            elif len(segment) == 8:
                print('0', segment[0])
                print('1', segment[1])
                print('2', segment[2])
                print('3', segment[3])
                print('4', segment[4])
                print('5', segment[5])
                print('6', segment[6])
                print('7', segment[7])

                paths.append(CubicBezier(
                    start=tuple_to_image(segment[0]),
                    control1=tuple_to_image(segment[1]),
                    control2=tuple_to_image(segment[2]),
                    end=tuple_to_image(segment[3])))

                paths.append(CubicBezier(
                    start=tuple_to_image(segment[4]),
                    control1=tuple_to_image(segment[5]),
                    control2=tuple_to_image(segment[6]),
                    end=tuple_to_image(segment[7])))

            elif len(segment) == 9:
                print('0', segment[0])
                print('1', segment[1])
                print('2', segment[2])
                print('3', segment[3])
                print('4', segment[4])
                print('5', segment[5])
                print('6', segment[6])
                print('7', segment[7])
                print('8', segment[8])

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[0]),
                    control=tuple_to_image(segment[1]),
                    end=tuple_to_image(segment[2])))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[2]),
                    control=tuple_to_image(segment[3]),
                    end=tuple_to_image(segment[4])))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[4]),
                    control=tuple_to_image(segment[5]),
                    end=tuple_to_image(segment[6])))

                paths.append(QuadraticBezier(
                    start=tuple_to_image(segment[6]),
                    control=tuple_to_image(segment[7]),
                    end=tuple_to_image(segment[8])))

            print('')
        start = end + 1

    path = Path(*paths)

    file_name = '{:x}'.format(ord(char))
    file_path = 'fonts/{}.svg'.format(file_name.upper())

    xmin, xmax, ymin, ymax = bounding_box(path)

    dx = xmax - xmin
    dy = ymax - ymin

    viewbox = '{} {} {} {}'.format(xmin, ymin, dx, dy)

    attr = {
        'width': '50%',
        'height': '50%',
        'viewBox': viewbox,
        'preserveAspectRatio': 'xMidYMid meet'
    }

    wsvg(paths=path, colors=['#016FB9'], fill='none',
         svg_attributes=attr, stroke_widths=[100], filename=file_path)

    break

Call the script with a path to a truetype font like so

./scriptname.py DejaVuSans.ttf
@mi6gan
Copy link

mi6gan commented Nov 26, 2023

Have a look at outline.decompose method

@mi6gan
Copy link

mi6gan commented Nov 26, 2023

My guess you followed this approach. Not recommend to use it at all, messy code and still can not render plenty of segments. I spent hours with it before realized that freetype can decompose a contour into simple move (M), line (L), conic (C) and cubic (Q) commands. You don't even need svgpathtools here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment