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")
@Fqlox
Copy link

Fqlox commented Dec 22, 2021

Thanks @CatherineH for looking thought finding a solution. I did a workaround using inkscape cli : inkscape text.svg --export-text-to-path --export-plain-svg=svg_path.svg , But I guess that a solution using freetype would have been way faster and better and can be great for someone looking to replicate this .

@CatherineH
Copy link
Author

FT_Outline_Decompose essentially does what this script does; it skips over higher order bezier curves. Inkscape also uses this function in their codebase, but only if the glyph is of type "outline", and all of these fonts report that they are type "bitmap". I'm not sure how Inkscape is able to convert non-glyph fonts. I need to understand the inkscape libnrtype library better.

@runxel
Copy link

runxel commented Dec 27, 2021

@CatherineH The link was just an example. Also I'm quite bad in understanding heavily math-ed papers :D

Does that mean that FT_Outline_Decompose isn't as helpful as you first thought?

@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