Skip to content

Instantly share code, notes, and snippets.

@CatherineH
Last active December 5, 2023 13:41
Show Gist options
  • Save CatherineH/499a312a04582a00e7559ac0c8f133fa to your computer and use it in GitHub Desktop.
Save CatherineH/499a312a04582a00e7559ac0c8f133fa to your computer and use it in GitHub Desktop.
Convert a text character to an SVG path.
from svgpathtools import wsvg, Line, QuadraticBezier, Path
from freetype import Face
def tuple_to_imag(t):
return t[0] + t[1] * 1j
face = Face('./Vera.ttf')
face.set_char_size(48 * 64)
face.load_char('a')
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]
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 tags[j] and j < (len(points) - 1):
segments.append([points[j], ])
for segment in segments:
if len(segment) == 2:
paths.append(Line(start=tuple_to_imag(segment[0]),
end=tuple_to_imag(segment[1])))
elif len(segment) == 3:
paths.append(QuadraticBezier(start=tuple_to_imag(segment[0]),
control=tuple_to_imag(segment[1]),
end=tuple_to_imag(segment[2])))
elif len(segment) == 4:
C = ((segment[1][0] + segment[2][0]) / 2.0,
(segment[1][1] + segment[2][1]) / 2.0)
paths.append(QuadraticBezier(start=tuple_to_imag(segment[0]),
control=tuple_to_imag(segment[1]),
end=tuple_to_imag(C)))
paths.append(QuadraticBezier(start=tuple_to_imag(C),
control=tuple_to_imag(segment[2]),
end=tuple_to_imag(segment[3])))
else:
print(f"incompatible segment length: {len(segment)}")
start = end + 1
path = Path(*paths)
wsvg(path, filename="text.svg")
@xmarduel
Copy link

xmarduel commented Jan 8, 2022

as test with char 'B' and
face = Face('C:\Windows\Fonts\arial.ttf')
there are some segments of length 5 and 6

this seems to be Ok (as TrueType infact has only quads):

...
elif len(segment) == 5:
C12 = segment[1]
C23 = segment[2]
C34 = segment[3]

            P1 = segment[0]
            P2 = ((segment[1][0] + segment[2][0]) / 2.0,
                 (segment[1][1] + segment[2][1]) / 2.0)
            P3 = ((segment[2][0] + segment[3][0]) / 2.0,
                 (segment[2][1] + segment[3][1]) / 2.0)
            P4 = segment[4]

            paths.append(QuadraticBezier(start=tuple_to_imag(P1),
                                         control=tuple_to_imag(C12),
                                         end=tuple_to_imag(P2)))
            paths.append(QuadraticBezier(start=tuple_to_imag(P2),
                                         control=tuple_to_imag(C23),
                                         end=tuple_to_imag(P3)))
            paths.append(QuadraticBezier(start=tuple_to_imag(P3),
                                         control=tuple_to_imag(C34),
                                         end=tuple_to_imag(P4)))

        elif len(segment) == 6:
            C12 = segment[1]
            C23 = segment[2]
            C34 = segment[3]
            C45 = segment[4]

            P1 = segment[0]
            P2 = ((segment[1][0] + segment[2][0]) / 2.0,
                 (segment[1][1] + segment[2][1]) / 2.0)
            P3 = ((segment[2][0] + segment[3][0]) / 2.0,
                 (segment[2][1] + segment[3][1]) / 2.0)
            P4 = ((segment[3][0] + segment[4][0]) / 2.0,
                 (segment[3][1] + segment[4][1]) / 2.0)
            P5 = segment[5]

            paths.append(QuadraticBezier(start=tuple_to_imag(P1),
                                         control=tuple_to_imag(C12),
                                         end=tuple_to_imag(P2)))
            paths.append(QuadraticBezier(start=tuple_to_imag(P2),
                                         control=tuple_to_imag(C23),
                                         end=tuple_to_imag(P3)))
            paths.append(QuadraticBezier(start=tuple_to_imag(P3),
                                         control=tuple_to_imag(C34),
                                         end=tuple_to_imag(P4)))
            paths.append(QuadraticBezier(start=tuple_to_imag(P4),
                                         control=tuple_to_imag(C45),
                                         end=tuple_to_imag(P5)))

By the way:

def move_to(a, ctx):
ctx.append("M {},{}".format(a.x, a.y))

def line_to(a, ctx):
ctx.append("L {},{}".format(a.x, a.y))

def conic_to(a, b, ctx):
ctx.append("Q {},{} {},{}".format(a.x, a.y, b.x, b.y))

def cubic_to(a, b, c, ctx):
ctx.append("C {},{} {},{} {},{}".format(a.x, a.y, b.x, b.y, c.x, c.y))

ctx = []
outline.decompose(ctx, move_to=move_to, line_to=line_to, conic_to=conic_to, cubic_to=cubic_to)

works, but gives svg "y" in the opposite direction

https://github.com/rougier/freetype-py/releases

https://github.com/rougier/freetype-py/blob/master/examples/glyph-vector-decompose.py

Has someone here understood the face.set_char_size() and how to get a 'reasonable' svg font size ?
how to get the right scaling back to for example to style="font-size=11" , when working with svg ?

Thanks in advance,
XM

@xmarduel
Copy link

xmarduel commented Jan 8, 2022

... and with ctx:

svg = """
<svg xmlns="http://www.w3.org/2000/svg"
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"

<path
  transform="scale(0.00338) scale(10)"
  style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
  d="{}"
/>
""".format(" ".join(ctx))

print(svg)

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