-
-
Save CatherineH/499a312a04582a00e7559ac0c8f133fa to your computer and use it in GitHub Desktop.
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") |
thanks @runxel !
Hi, great script, it works pretty well ! I still got issues: It renders bezier curves angularity and transform a lot the font :
Here is the rendered output :
<?xml version="1.0" ?> <svg baseProfile="full" height="600px" version="1.1" viewBox="-18.345500000000015 -149.8455 1424.691 1790.691" width="478px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs/> <path d="M 955.0,1137.0 L 846.0,1260.0 L 725.0,1322.0 L 589.0,1322.0 L 505.0,1322.0 L 436.0,1299.0 L 382.0,1254.0 L 328.0,1209.0 L 301.0,1149.0 L 301.0,1076.0 L 301.0,979.0 L 351.0,904.0 L 451.0,848.0 L 551.0,793.0 L 719.0,752.0 L 955.0,726.0 L 955.0,1137.0 M 967.0,1279.0 L 990.0,1424.0 L 1092.0,1491.0 L 1274.0,1478.0 L 1287.0,1322.0 L 1229.0,1324.0 L 1192.0,1314.0 L 1173.0,1294.0 L 1155.0,1273.0 L 1145.0,1236.0 L 1145.0,1180.0 L 1145.0,504.0 L 1148.0,168.0 L 980.0,0.0 L 642.0,0.0 L 554.0,0.0 L 461.0,17.0 L 365.0,50.0 L 269.0,82.0 L 187.0,122.0 L 119.0,169.0 L 162.0,348.0 L 226.0,301.0 L 300.0,261.0 L 385.0,228.0 L 470.0,195.0 L 549.0,179.0 L 623.0,179.0 L 844.0,179.0 L 955.0,290.0 L 955.0,514.0 L 955.0,596.0 L 386.0,629.0 L 101.0,796.0 L 101.0,1097.0 L 101.0,1210.0 L 143.0,1303.0 L 227.0,1377.0 L 311.0,1451.0 L 417.0,1487.0 L 546.0,1487.0 L 708.0,1487.0 L 845.0,1418.0 L 958.0,1279.0 L 967.0,1279.0" fill="none" stroke="#000000" stroke-width="1.491"/> </svg>
Do you have an idea why it gives so strange result ?
Thanks in advance
@Fqlox It looks like the font you're using is not free, so I can't reproduce this exactly. I've updated the script to print out when it encounters segments it can't render, can you run it on this font and tell me if it's unable to handle some of the curves due to the bezier curves having too many control points?
Thanks for your answer: I got outputs :
incompatible segment length: 6
incompatible segment length: 5
incompatible segment length: 6
incompatible segment length: 6
incompatible segment length: 5
incompatible segment length: 5
I tried to implement runxel's condition, it gives also some incompatible segments.
Here I tried with the syne font
and It got me that:
@runxel do you understand the algorithm in the paper you posted? The coefficients don't make sense to me and the original algorithm they're improving on is published in Chinese
I've been doing this wrong for a long time, the freetype-py library has a binding to FT_Outline_Decompose function that converts fonts to svg-compatible bezier curves, I'm going to try to get that working.
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 .
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.
@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?
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
... 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)
@p3t3r67x0 You would need a proper Bezier degree reduction algorithm for these cases, where the Bezier is of higher degrees.
And while any Bezier can always be of a higher degree ("degree elevation"), the inverse can't be said that easily. It would always be an aproximation.
The poor man's solution would be something like the following. However the generated curve is too flat, but at least the tangentiality is preserved.