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