Spaces:
Runtime error
Runtime error
Download code/ttf.py from bkhmsi/Font-To-Sketch: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/spaces/bkhmsi/Font-To-Sketch/resolve/main/code/ttf.py
- Command line
-
hf download hf://spaces/bkhmsi/Font-To-Sketch/code/ttf.py
-
curl -L -o ttf.py https://huggingface.co/spaces/bkhmsi/Font-To-Sketch/resolve/main/code/ttf.py
14.8 kB
| 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") | |