Badr AlKhamissi
starting space
913d3e3
from importlib import reload
import os
import numpy as np
import bezier
import freetype as ft
import pydiffvg
import torch
import save_svg
import vharfbuzz as hb
from svgpathtools import svgstr2paths
import xml.etree.ElementTree as ET
device = torch.device("cuda" if (
torch.cuda.is_available() and torch.cuda.device_count() > 0) else "cpu")
reload(bezier)
def fix_single_svg(svg_path, all_word=False):
target_h_letter = 360
target_canvas_width, target_canvas_height = 600, 600
canvas_width, canvas_height, shapes, shape_groups = pydiffvg.svg_to_scene(svg_path)
letter_h = canvas_height
letter_w = canvas_width
if all_word:
if letter_w > letter_h:
scale_canvas_w = target_h_letter / letter_w
hsize = int(letter_h * scale_canvas_w)
scale_canvas_h = hsize / letter_h
else:
scale_canvas_h = target_h_letter / letter_h
wsize = int(letter_w * scale_canvas_h)
scale_canvas_w = wsize / letter_w
else:
scale_canvas_h = target_h_letter / letter_h
wsize = int(letter_w * scale_canvas_h)
scale_canvas_w = wsize / letter_w
for num, p in enumerate(shapes):
p.points[:, 0] = p.points[:, 0] * scale_canvas_w
p.points[:, 1] = p.points[:, 1] * scale_canvas_h + target_h_letter
p.points[:, 1] = -p.points[:, 1]
# p.points[:, 0] = -p.points[:, 0]
w_min, w_max = min([torch.min(p.points[:, 0]) for p in shapes]), max([torch.max(p.points[:, 0]) for p in shapes])
h_min, h_max = min([torch.min(p.points[:, 1]) for p in shapes]), max([torch.max(p.points[:, 1]) for p in shapes])
for num, p in enumerate(shapes):
p.points[:, 0] = p.points[:, 0] + target_canvas_width/2 - int(w_min + (w_max - w_min) / 2)
p.points[:, 1] = p.points[:, 1] + target_canvas_height/2 - int(h_min + (h_max - h_min) / 2)
output_path = f"{svg_path[:-4]}_scaled.svg"
save_svg.save_svg(output_path, target_canvas_width, target_canvas_height, shapes, shape_groups)
def normalize_letter_size(dest_path, font, txt, chars):
fontname = os.path.splitext(os.path.basename(font))[0]
# for i, c in enumerate(chars):
# fname = f"{dest_path}/{fontname}_{c}.svg"
# fname = fname.replace(" ", "_")
# fix_single_svg(fname)
fname = f"{dest_path}/{fontname}_{txt}.svg"
fname = fname.replace(" ", "_")
fix_single_svg(fname, all_word=True)
def glyph_to_cubics(face, x=0, y=0):
''' Convert current font face glyph to cubic beziers'''
def linear_to_cubic(Q):
a, b = Q
return [a + (b - a) * t for t in np.linspace(0, 1, 4)]
def quadratic_to_cubic(Q):
return [Q[0],
Q[0] + (2 / 3) * (Q[1] - Q[0]),
Q[2] + (2 / 3) * (Q[1] - Q[2]),
Q[2]]
beziers = []
pt = lambda p: np.array([x + p.x, - p.y - y]) # Flipping here since freetype has y-up
last = lambda: beziers[-1][-1]
def move_to(a, beziers):
beziers.append([pt(a)])
def line_to(a, beziers):
Q = linear_to_cubic([last(), pt(a)])
beziers[-1] += Q[1:]
def conic_to(a, b, beziers):
Q = quadratic_to_cubic([last(), pt(a), pt(b)])
beziers[-1] += Q[1:]
def cubic_to(a, b, c, beziers):
beziers[-1] += [pt(a), pt(b), pt(c)]
face.glyph.outline.decompose(beziers, move_to=move_to, line_to=line_to, conic_to=conic_to, cubic_to=cubic_to)
beziers = [np.array(C).astype(float) for C in beziers]
return beziers
# def handle_ligature(glyph_infos, glyph_positions):
# combined_advance = sum(pos.x_advance for pos in glyph_positions)
# first_x_offset = glyph_positions[0].x_offset
# combined_advance = x_adv_1 + x_adv_2
# # Adjust the x_offset values based on the difference between the first glyph's x_offset and the combined_advance
# for pos in glyph_positions:
# pos.x_offset += combined_advance - pos.x_advance - first_x_offset
# # Render the ligature using the adjusted glyph positions
# render_glyphs(glyph_infos, glyph_positions)
def font_string_to_beziers(font, txt, size=30, spacing=1.0, merge=True, target_control=None):
''' Load a font and convert the outlines for a given string to cubic bezier curves,
if merge is True, simply return a list of all bezier curves,
otherwise return a list of lists with the bezier curves for each glyph'''
print(font)
vhb = hb.Vharfbuzz(font)
buf = vhb.shape(txt, {"features": {"kern": True, "liga": True}})
buf.guess_segment_properties()
glyph_infos = buf.glyph_infos
glyph_positions = buf.glyph_positions
glyph_count = {glyph_infos[i].cluster: 0 for i in range(len(glyph_infos))}
svg = vhb.buf_to_svg(buf)
paths, attributes = svgstr2paths(svg)
face = ft.Face(font)
face.set_char_size(64 * size)
pindex = -1
x, y = 0, 0
beziers, chars = [], []
for path_idx, path in enumerate(paths):
segment_vals = []
print("="*20 + str(path_idx) + "="*20)
for segment in path:
segment_type = segment.__class__.__name__
t_values = np.linspace(0, 1, 10)
points = [segment.point(t) for t in t_values]
for pt in points:
segment_vals += [[pt.real, -pt.imag]]
# points = [bezier.point(t) for t in t_values]
if segment_type == 'Line':
# Line segment
start = segment.start
end = segment.end
print(f"Line: ({start.real}, {start.imag}) to ({end.real}, {end.imag})")
elif segment_type == 'QuadraticBezier':
# Quadratic Bézier segment
start = segment.start
control = segment.control
end = segment.end
print(f"Quadratic Bézier: ({start.real}, {start.imag}) to ({end.real}, {end.imag}) with control point ({control.real}, {control.imag})")
elif segment_type == 'CubicBezier':
# Cubic Bézier segment
start = segment.start
control1 = segment.control1
control2 = segment.control2
end = segment.end
print(f"Cubic Bézier: ({start.real}, {start.imag}) to ({end.real}, {end.imag}) with control points ({control1.real}, {control1.imag}) and ({control2.real}, {control2.imag})")
else:
# Other segment types (Arc, Close)
print(f"Segment type: {segment_type}")
beziers += [[np.array(segment_vals)]]
beziers_2 = []
glyph_infos = glyph_infos[::-1]
glyph_positions = glyph_positions[::-1]
for i, (info, pos) in enumerate(zip(glyph_infos, glyph_positions)):
index = info.cluster
c = f"{txt[index]}_{glyph_count[index]}"
chars += [c]
glyph_count[index] += 1
glyph_index = info.codepoint
face.load_glyph(glyph_index, flags=ft.FT_LOAD_DEFAULT | ft.FT_LOAD_NO_BITMAP)
# face.load_char(c, ft.FT_LOAD_DEFAULT | ft.FT_LOAD_NO_BITMAP)
findex = -1
if i+1 < len(glyph_infos):
findex = glyph_infos[i+1].cluster
foffset = (glyph_positions[i+1].x_offset, glyph_positions[i+1].y_offset)
fadvance = (glyph_positions[i+1].x_advance, glyph_positions[i+1].y_advance)
# bez = glyph_to_cubics(face, x+pos.x_offset+pos.x_advance, y+pos.y_offset+pos.y_advance)
# if findex != index:
# x += pos.x_offset
# y += pos.y_offset
# else:
# x += pos.x_offset
# y += pos.y_offset
bez = glyph_to_cubics(face, x, y)
# Check number of control points if desired
if target_control is not None:
if c in target_control.keys():
nctrl = np.sum([len(C) for C in bez])
while nctrl < target_control[c]:
longest = np.max(
sum([[bezier.approx_arc_length(b) for b in bezier.chain_to_beziers(C)] for C in bez], []))
thresh = longest * 0.5
bez = [bezier.subdivide_bezier_chain(C, thresh) for C in bez]
nctrl = np.sum([len(C) for C in bez])
print(nctrl)
if merge:
beziers_2 += bez
else:
beziers_2.append(bez)
# kerning = face.get_kerning(index, findex)
# x += (slot.advance.x + kerning.x) * spacing
# previous = txt[index]
# print(f"C: {txt[index]}/{index} | X: {x+pos.x_offset}| Y: {y+pos.y_offset}")
print(f"C: {txt[index]}/{index} | X: {x}: {pos.x_advance}/{pos.x_offset} | Y: {y}: {pos.y_advance}/{pos.y_offset}")
# if findex != index:
x -= pos.x_advance
# y += pos.y_advance + pos.y_offset
pindex = index
return beziers_2, chars
def bezier_chain_to_commands(C, closed=True):
curves = bezier.chain_to_beziers(C)
cmds = 'M %f %f ' % (C[0][0], C[0][1])
n = len(curves)
for i, bez in enumerate(curves):
if i == n - 1 and closed:
cmds += 'C %f %f %f %f %f %fz ' % (*bez[1], *bez[2], *bez[3])
else:
cmds += 'C %f %f %f %f %f %f ' % (*bez[1], *bez[2], *bez[3])
return cmds
def count_cp(file_name, font_name):
canvas_width, canvas_height, shapes, shape_groups = pydiffvg.svg_to_scene(file_name)
p_counter = 0
for path in shapes:
p_counter += path.points.shape[0]
print(f"TOTAL CP: [{p_counter}]")
return p_counter
def write_letter_svg(c, header, fontname, beziers, subdivision_thresh, dest_path):
cmds = ''
svg = header
path = '<g><path d="'
for C in beziers:
if subdivision_thresh is not None:
print('subd')
C = bezier.subdivide_bezier_chain(C, subdivision_thresh)
cmds += bezier_chain_to_commands(C, True)
path += cmds + '"/>\n'
svg += path + '</g></svg>\n'
fname = f"{dest_path}/{fontname}_{c}.svg"
fname = fname.replace(" ", "_")
f = open(fname, 'w')
f.write(svg)
f.close()
return fname, path
def write_letter_svg_hb(vhb, c, dest_path, fontname):
buf = vhb.shape(c, {"features": {"kern": True, "liga": True}})
svg = vhb.buf_to_svg(buf)
fname = f"{dest_path}/{fontname}_{c}.svg"
fname = fname.replace(" ", "_")
f = open(fname, 'w')
f.write(svg)
f.close()
return fname
def font_string_to_svgs(dest_path, font, txt, size=30, spacing=1.0, target_control=None, subdivision_thresh=None):
fontname = os.path.splitext(os.path.basename(font))[0]
glyph_beziers, chars = font_string_to_beziers(font, txt, size, spacing, merge=False, target_control=target_control)
if not os.path.isdir(dest_path):
os.mkdir(dest_path)
# Compute boundig box
points = np.vstack(sum(glyph_beziers, []))
lt = np.min(points, axis=0)
rb = np.max(points, axis=0)
size = rb - lt
sizestr = 'width="%.1f" height="%.1f"' % (size[0], size[1])
boxstr = ' viewBox="%.1f %.1f %.1f %.1f"' % (lt[0], lt[1], size[0], size[1])
header = '''<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" '''
header += sizestr
header += boxstr
header += '>\n<defs/>\n'
svg_all = header
print(f"Len Glyph Bezier: {len(glyph_beziers)} | Chars: {len(chars)}")
for i, (c, beziers) in enumerate(zip(chars, glyph_beziers)):
print(f"==== {c} ====")
fname, path = write_letter_svg(c, header, fontname, beziers, subdivision_thresh, dest_path)
num_cp = count_cp(fname, fontname)
print(num_cp)
print(font, c)
# Add to global svg
svg_all += path + '</g>\n'
vhb = hb.Vharfbuzz(font)
buf = vhb.shape(txt, {"features": {"kern": True, "liga": True}})
svg = vhb.buf_to_svg(buf)
# Save global svg
svg_all += '</svg>\n'
fname = f"{dest_path}/{fontname}_{txt}.svg"
fname = fname.replace(" ", "_")
f = open(fname, 'w')
f.write(svg)
f.close()
return chars
def font_string_to_svgs_hb(dest_path, font, txt, size=30, spacing=1.0, target_control=None, subdivision_thresh=None):
fontname = os.path.splitext(os.path.basename(font))[0]
if not os.path.isdir(dest_path):
os.mkdir(dest_path)
vhb = hb.Vharfbuzz(font)
buf = vhb.shape(txt, {"features": {"kern": True, "liga": True}})
buf.guess_segment_properties()
buf = vhb.shape(txt, {"features": {"kern": True, "liga": True}})
svg = vhb.buf_to_svg(buf)
# Save global svg
fname = f"{dest_path}/{fontname}_{txt}.svg"
fname = fname.replace(" ", "_")
f = open(fname, 'w')
f.write(svg)
f.close()
return None
if __name__ == '__main__':
fonts = ["KaushanScript-Regular"]
level_of_cc = 1
if level_of_cc == 0:
target_cp = None
else:
target_cp = {"A": 120, "B": 120, "C": 100, "D": 100,
"E": 120, "F": 120, "G": 120, "H": 120,
"I": 35, "J": 80, "K": 100, "L": 80,
"M": 100, "N": 100, "O": 100, "P": 120,
"Q": 120, "R": 130, "S": 110, "T": 90,
"U": 100, "V": 100, "W": 100, "X": 130,
"Y": 120, "Z": 120,
"a": 120, "b": 120, "c": 100, "d": 100,
"e": 120, "f": 120, "g": 120, "h": 120,
"i": 35, "j": 80, "k": 100, "l": 80,
"m": 100, "n": 100, "o": 100, "p": 120,
"q": 120, "r": 130, "s": 110, "t": 90,
"u": 100, "v": 100, "w": 100, "x": 130,
"y": 120, "z": 120
}
target_cp = {k: v * level_of_cc for k, v in target_cp.items()}
for f in fonts:
print(f"======= {f} =======")
font_path = f"data/fonts/{f}.ttf"
output_path = f"data/init"
txt = "BUNNY"
subdivision_thresh = None
font_string_to_svgs(output_path, font_path, txt, target_control=target_cp,
subdivision_thresh=subdivision_thresh)
normalize_letter_size(output_path, font_path, txt)
print("DONE")