diff --git a/handwrite/__init__.py b/handwrite/__init__.py index a4dd30f..73d239b 100644 --- a/handwrite/__init__.py +++ b/handwrite/__init__.py @@ -1,4 +1,4 @@ -from handwrite.sheettopng import SHEETtoPNG -from handwrite.pngtosvg import PNGtoSVG +from handwrite.sheettobmp import SHEETtoBMP +from handwrite.bmptosvg import BMPtoSVG from handwrite.svgtottf import SVGtoTTF from handwrite.cli import converters diff --git a/handwrite/bmptosvg.py b/handwrite/bmptosvg.py new file mode 100644 index 0000000..5370fbf --- /dev/null +++ b/handwrite/bmptosvg.py @@ -0,0 +1,59 @@ +# from PIL import Image, ImageChops +import os +import shutil +import subprocess + + +class PotraceNotFound(Exception): + pass + + +class BMPtoSVG: + """Converter class to convert character BMPs to BMPs and SVGs.""" + + def convert(self, directory): + """Call converters on each .bmp in the provider directory. + + Walk through the custom directory containing all .bmp files + from sheettobmp and convert them to svg. + """ + path = os.walk(directory) + for root, dirs, files in path: + for f in files: + if f.endswith(".bmp"): + # self.trim(root + "/" + f) + self.bmpToSvg(root + "/" + f) + + def bmpToSvg(self, path): + """Convert .bmp image to .svg using potrace. + + Converts the passed .bmp file to .svg using the potrace + (http://potrace.sourceforge.net/). Each .bmp is passed as + a parameter to potrace which is called as a subprocess. + + Parameters + ---------- + path : str + Path to the bmp file to be converted. + + Raises + ------ + PotraceNotFound + Raised if potrace not found in path by shutil.which() + """ + if shutil.which("potrace") is None: + raise PotraceNotFound("Potrace is either not installed or not in path") + else: + subprocess.run(["potrace", path, "-b", "svg", "-o", path[0:-4] + ".svg"]) + + # def trim(self, im_path): + # im = Image.open(im_path) + # bg = Image.new(im.mode, im.size, im.getpixel((0, 0))) + # diff = ImageChops.difference(im, bg) + # bbox = list(diff.getbbox()) + # bbox[0] -= 1 + # bbox[1] -= 1 + # bbox[2] += 1 + # bbox[3] += 1 + # cropped_im = im.crop(bbox) + # cropped_im.save(im_path) diff --git a/handwrite/cli.py b/handwrite/cli.py index 7870cb3..c2f35e7 100644 --- a/handwrite/cli.py +++ b/handwrite/cli.py @@ -1,58 +1,58 @@ -import os -import shutil -import argparse -import tempfile - -from handwrite import SHEETtoPNG -from handwrite import PNGtoSVG -from handwrite import SVGtoTTF - - -def run(sheet, output_directory, characters_dir, config, metadata): - SHEETtoPNG().convert(sheet, characters_dir, config) - PNGtoSVG().convert(directory=characters_dir) - SVGtoTTF().convert(characters_dir, output_directory, config, metadata) - - -def converters(sheet, output_directory, directory=None, config=None, metadata=None): - if not directory: - directory = tempfile.mkdtemp() - isTempdir = True - else: - isTempdir = False - - if config is None: - config = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "default.json" - ) - if os.path.isdir(config): - raise IsADirectoryError("Config parameter should not be a directory.") - - if os.path.isdir(sheet): - raise IsADirectoryError("Sheet parameter should not be a directory.") - else: - run(sheet, output_directory, directory, config, metadata) - - if isTempdir: - shutil.rmtree(directory) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("input_path", help="Path to sample sheet") - parser.add_argument("output_directory", help="Directory Path to save font output") - parser.add_argument( - "--directory", - help="Generate additional files to this path (Temp by default)", - default=None, - ) - parser.add_argument("--config", help="Use custom configuration file", default=None) - parser.add_argument("--filename", help="Font File name", default=None) - parser.add_argument("--family", help="Font Family name", default=None) - parser.add_argument("--style", help="Font Style name", default=None) - - args = parser.parse_args() - metadata = {"filename": args.filename, "family": args.family, "style": args.style} - converters( - args.input_path, args.output_directory, args.directory, args.config, metadata - ) +import argparse +import os +import shutil +import tempfile + +from bmptosvg import BMPtoSVG +from sheettobmp import SHEETtoBMP +from svgtottf import SVGtoTTF + + +def run(sheet, output_directory, characters_dir, config, metadata): + SHEETtoBMP().convert(sheet, characters_dir, config) + BMPtoSVG().convert(directory=characters_dir) + SVGtoTTF().convert(characters_dir, output_directory, config, metadata) + + +def converters(sheet, output_directory, directory=None, config=None, metadata=None): + if not directory: + directory = tempfile.mkdtemp() + isTempdir = True + else: + isTempdir = False + + if config is None: + config = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "default.json" + ) + if os.path.isdir(config): + raise IsADirectoryError("Config parameter should not be a directory.") + + if os.path.isdir(sheet): + raise IsADirectoryError("Sheet parameter should not be a directory.") + else: + run(sheet, output_directory, directory, config, metadata) + + if isTempdir: + shutil.rmtree(directory) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("input_path", help="Path to sample sheet") + parser.add_argument("output_directory", help="Directory Path to save font output") + parser.add_argument( + "--directory", + help="Generate additional files to this path (Temp by default)", + default=None, + ) + parser.add_argument("--config", help="Use custom configuration file", default=None) + parser.add_argument("--filename", help="Font File name", default=None) + parser.add_argument("--family", help="Font Family name", default=None) + parser.add_argument("--style", help="Font Style name", default=None) + + args = parser.parse_args() + metadata = {"filename": args.filename, "family": args.family, "style": args.style} + converters( + args.input_path, args.output_directory, args.directory, args.config, metadata + ) diff --git a/handwrite/default.json b/handwrite/default.json index 51719f8..8eec171 100644 --- a/handwrite/default.json +++ b/handwrite/default.json @@ -1,346 +1,953 @@ -{ - "threshold_value": 200, - "props": { - "ascent": 800, - "descent": 200, - "em": 1000, - "encoding": "UnicodeFull", - "lang": "English (US)", - "filename": "MyFont", - "style": "Regular" - }, - "sfnt_names": { - "Copyright": "Copyright (c) 2021 by Nobody", - "Family": "MyFont", - "SubFamily": "Regular", - "UniqueID": "MyFont 2021-02-04", - "Fullname": "MyFont Regular", - "Version": "Version 1.0", - "PostScriptName": "MyFont-Regular" - }, - "glyphs": [ - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - 97, - 98, - 99, - 100, - 101, - 102, - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117, - 118, - 119, - 120, - 121, - 122, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 46, - 44, - 59, - 58, - 33, - 63, - 34, - 39, - 45, - 43, - 61, - 47, - 37, - 38, - 40, - 41, - 91, - 93 - ], - "typography_parameters": { - "bearing_table": { - "Default": [60, 60], - "A": [60, -50], - "a": [30, 40], - "B": [60, 0], - "C": [60, -30], - "c": [null, 40], - "b": [null, 40], - "D": [null, 10], - "d": [30, -20], - "e": [30, 40], - "E": [70, 10], - "F": [70, 0], - "f": [0, -20], - "G": [60, 30], - "g": [20, 60], - "h": [40, 40], - "I": [80, 50], - "i": [null, 60], - "J": [40, 30], - "j": [-70, 40], - "k": [40, 20], - "K": [80, 0], - "H": [null, 10], - "L": [80, 10], - "l": [null, 0], - "M": [60, 30], - "m": [40, null], - "N": [70, 10], - "n": [30, 40], - "O": [70, 10], - "o": [40, 40], - "P": [70, 0], - "p": [null, 40], - "Q": [70, 10], - "q": [20, 30], - "R": [70, -10], - "r": [null, 40], - "S": [60, 60], - "s": [20, 40], - "T": [null, -10], - "t": [-10, 20], - "U": [70, 20], - "u": [40, 40], - "V": [null, -10], - "v": [20, 20], - "W": [70, 20], - "w": [40, 40], - "X": [null, -10], - "x": [10, 20], - "y": [20, 30], - "Y": [40, 0], - "Z": [null, -10], - "z": [10, 20], - "1": [-10, 30], - "2": [-10, 30], - "3": [10, 40], - "4": [30, 30], - "5": [30, 40], - "6": [20, 20], - "7": [30, 20], - "8": [30, 20], - "9": [30, 30], - "0": [50, 40], - ".": [null, 10], - ",": [null, 10], - ";": [null, 10], - ":": [null, 20], - "!": [null, 20], - "?": [null, 30], - "\"": [null, 20], - "'": [null, 10], - "-": [null, 20], - "+": [null, 20], - "=": [null, 20], - "/": [null, 20], - "%": [40, 40], - "&": [40, 40], - "(": [10, 10], - ")": [10, 10], - "[": [10, 10], - "]": [10, 10] - }, - "kerning_table": { - "autokern": true, - "seperation": 0, - "rows": [ - null, - "f-+=/?", - "t", - "i", - "r", - "k", - "l.,;:!\"'()[]", - "v", - "bop%&", - "nm", - "a", - "W", - "T", - "F", - "P", - "g", - "qdhyj", - "cesuwxz", - "V", - "A", - "Y", - "MNHI", - "OQDU", - "J", - "C", - "E", - "L", - "P", - "KR", - "G", - "BSXZ" - ], - "cols": [ - null, - "oacedgqw%&", - "ft-+=/?", - "xvz", - "hbli.,;:!\"'()[]", - "j", - "mnpru", - "k", - "y", - "s", - "T", - "F", - "Zero" - ], - "table": [ - [ - [0, 0, 0, 0, 0, 0, 0, null, null, 0, 0, null, 0], - [0, -30, -61, -20, null, 0, null, null, null, 0, -150, null, -70], - [0, -50, -41, -20, null, 0, 0, null, null, 0, -150, null, -10], - [ - null, - null, - -40, - null, - null, - null, - null, - null, - null, - null, - -150, - null, - null - ], - [0, -32, -40, null, null, 0, null, null, null, 0, -170, null, 29], - [0, -10, -50, null, null, 0, null, null, null, -48, -150, null, -79], - [0, -10, -20, null, 0, 0, 0, null, null, 0, -110, null, -20], - [0, -40, -35, -15, null, 0, 0, null, null, 0, -170, null, 30], - [0, null, -40, null, 0, 0, 0, null, null, 0, -170, null, 43], - [ - null, - null, - -30, - null, - null, - null, - null, - null, - null, - null, - -170, - null, - null - ], - [0, -23, -30, null, 0, 0, 0, null, null, 0, -170, null, 7], - [0, -40, -30, -10, null, 0, 0, null, null, 0, null, null, null], - [0, -150, -120, -120, -30, -40, -130, null, -100, -80, 0, null, null], - [0, -90, -90, -70, -30, 0, -70, null, -50, -80, -40, null, null], - [0, -100, -70, -50, null, 0, -70, null, -30, -80, -20, null, null], - [ - null, - null, - null, - null, - null, - 40, - null, - null, - null, - null, - -120, - null, - null - ], - [null, null, null, null, 30, 30, 30, 30, 30, null, -100, null, null], - [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - -120, - null, - null - ], - [null, -70, 30, 30, null, -80, -20, null, -40, -40, -10, null, null], - [null, 30, 60, 30, 30, null, 20, 40, 20, -80, -120, 20, 20], - [null, 20, 60, 30, 30, null, 20, 20, 40, 20, -10, null, null], - [null, 20, 10, 40, 30, null, 10, 20, 20, null, null, null, null], - [null, null, 50, 40, 30, -20, 30, 20, 30, null, -70, null, null], - [null, null, 40, 20, 20, -20, 10, 10, 30, null, -30, null, null], - [null, 10, 40, 10, 30, null, 30, 30, 20, null, -30, null, null], - [null, -10, 50, null, 10, -20, 10, null, 20, null, null, null, null], - [ - null, - -10, - -10, - null, - null, - -30, - null, - null, - 20, - null, - -90, - null, - null - ], - [null, -50, 30, 20, 20, null, null, 20, 20, null, -30, null, null], - [null, 20, 20, 20, 10, null, 20, 20, 20, null, -60, null, null], - [null, 20, 40, 30, 30, null, 20, 20, 20, null, -100, 10, null], - [null, 20, 40, 30, 30, null, 20, 20, 20, 20, -20, 10, null] - ] - ] - } - }, - "# vim: set et sw=2 ts=2 sts=2:": false -} +{ + "threshold_value": 200, + "props": { + "ascent": 800, + "descent": 200, + "em": 1000, + "encoding": "UnicodeFull", + "lang": "English (US)", + "filename": "MyFont", + "style": "Regular" + }, + "sfnt_names": { + "Copyright": "Copyright (c) 2021 by Nobody", + "Family": "MyFont", + "SubFamily": "Regular", + "UniqueID": "MyFont 2021-02-04", + "Fullname": "MyFont Regular", + "Version": "Version 1.0", + "PostScriptName": "MyFont-Regular" + }, + "glyphs": [ + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 46, + 44, + 59, + 58, + 33, + 63, + 34, + 39, + 45, + 43, + 61, + 47, + 37, + 38, + 40, + 41, + 91, + 93 + ], + "typography_parameters": { + "bearing_table": { + "Default": [ + 60, + 60 + ], + "A": [ + 60, + -50 + ], + "a": [ + 30, + 40 + ], + "B": [ + 60, + 0 + ], + "C": [ + 60, + -30 + ], + "c": [ + null, + 40 + ], + "b": [ + null, + 40 + ], + "D": [ + null, + 10 + ], + "d": [ + 30, + -20 + ], + "e": [ + 30, + 40 + ], + "E": [ + 70, + 10 + ], + "F": [ + 70, + 0 + ], + "f": [ + 0, + -20 + ], + "G": [ + 60, + 30 + ], + "g": [ + 20, + 60 + ], + "h": [ + 40, + 40 + ], + "I": [ + 80, + 50 + ], + "i": [ + null, + 60 + ], + "J": [ + 40, + 30 + ], + "j": [ + -70, + 40 + ], + "k": [ + 40, + 20 + ], + "K": [ + 80, + 0 + ], + "H": [ + null, + 10 + ], + "L": [ + 80, + 10 + ], + "l": [ + null, + 0 + ], + "M": [ + 60, + 30 + ], + "m": [ + 40, + null + ], + "N": [ + 70, + 10 + ], + "n": [ + 30, + 40 + ], + "O": [ + 70, + 10 + ], + "o": [ + 40, + 40 + ], + "P": [ + 70, + 0 + ], + "p": [ + null, + 40 + ], + "Q": [ + 70, + 10 + ], + "q": [ + 20, + 30 + ], + "R": [ + 70, + -10 + ], + "r": [ + null, + 40 + ], + "S": [ + 60, + 60 + ], + "s": [ + 20, + 40 + ], + "T": [ + null, + -10 + ], + "t": [ + -10, + 20 + ], + "U": [ + 70, + 20 + ], + "u": [ + 40, + 40 + ], + "V": [ + null, + -10 + ], + "v": [ + 20, + 20 + ], + "W": [ + 70, + 20 + ], + "w": [ + 40, + 40 + ], + "X": [ + null, + -10 + ], + "x": [ + 10, + 20 + ], + "y": [ + 20, + 30 + ], + "Y": [ + 40, + 0 + ], + "Z": [ + null, + -10 + ], + "z": [ + 10, + 20 + ], + "1": [ + -10, + 30 + ], + "2": [ + -10, + 30 + ], + "3": [ + 10, + 40 + ], + "4": [ + 30, + 30 + ], + "5": [ + 30, + 40 + ], + "6": [ + 20, + 20 + ], + "7": [ + 30, + 20 + ], + "8": [ + 30, + 20 + ], + "9": [ + 30, + 30 + ], + "0": [ + 50, + 40 + ], + ".": [ + null, + 10 + ], + ",": [ + null, + 10 + ], + ";": [ + null, + 10 + ], + ":": [ + null, + 20 + ], + "!": [ + null, + 20 + ], + "?": [ + null, + 30 + ], + "\"": [ + null, + 20 + ], + "'": [ + null, + 10 + ], + "-": [ + null, + 20 + ], + "+": [ + null, + 20 + ], + "=": [ + null, + 20 + ], + "/": [ + null, + 20 + ], + "%": [ + 40, + 40 + ], + "&": [ + 40, + 40 + ], + "(": [ + 10, + 10 + ], + ")": [ + 10, + 10 + ], + "[": [ + 10, + 10 + ], + "]": [ + 10, + 10 + ] + }, + "kerning_table": { + "autokern": true, + "seperation": 0, + "rows": [ + null, + "f-+=/?", + "t", + "i", + "r", + "k", + "l.,;:!\"'()[]", + "v", + "bop%&", + "nm", + "a", + "W", + "T", + "F", + "P", + "g", + "qdhyj", + "cesuwxz", + "V", + "A", + "Y", + "MNHI", + "OQDU", + "J", + "C", + "E", + "L", + "P", + "KR", + "G", + "BSXZ" + ], + "cols": [ + null, + "oacedgqw%&", + "ft-+=/?", + "xvz", + "hbli.,;:!\"'()[]", + "j", + "mnpru", + "k", + "y", + "s", + "T", + "F", + "Zero" + ], + "table": [ + [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0 + ], + [ + 0, + -30, + -61, + -20, + null, + 0, + null, + null, + null, + 0, + -150, + null, + -70 + ], + [ + 0, + -50, + -41, + -20, + null, + 0, + 0, + null, + null, + 0, + -150, + null, + -10 + ], + [ + null, + null, + -40, + null, + null, + null, + null, + null, + null, + null, + -150, + null, + null + ], + [ + 0, + -32, + -40, + null, + null, + 0, + null, + null, + null, + 0, + -170, + null, + 29 + ], + [ + 0, + -10, + -50, + null, + null, + 0, + null, + null, + null, + -48, + -150, + null, + -79 + ], + [ + 0, + -10, + -20, + null, + 0, + 0, + 0, + null, + null, + 0, + -110, + null, + -20 + ], + [ + 0, + -40, + -35, + -15, + null, + 0, + 0, + null, + null, + 0, + -170, + null, + 30 + ], + [ + 0, + null, + -40, + null, + 0, + 0, + 0, + null, + null, + 0, + -170, + null, + 43 + ], + [ + null, + null, + -30, + null, + null, + null, + null, + null, + null, + null, + -170, + null, + null + ], + [ + 0, + -23, + -30, + null, + 0, + 0, + 0, + null, + null, + 0, + -170, + null, + 7 + ], + [ + 0, + -40, + -30, + -10, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null + ], + [ + 0, + -150, + -120, + -120, + -30, + -40, + -130, + null, + -100, + -80, + 0, + null, + null + ], + [ + 0, + -90, + -90, + -70, + -30, + 0, + -70, + null, + -50, + -80, + -40, + null, + null + ], + [ + 0, + -100, + -70, + -50, + null, + 0, + -70, + null, + -30, + -80, + -20, + null, + null + ], + [ + null, + null, + null, + null, + null, + 40, + null, + null, + null, + null, + -120, + null, + null + ], + [ + null, + null, + null, + null, + 30, + 30, + 30, + 30, + 30, + null, + -100, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + -120, + null, + null + ], + [ + null, + -70, + 30, + 30, + null, + -80, + -20, + null, + -40, + -40, + -10, + null, + null + ], + [ + null, + 30, + 60, + 30, + 30, + null, + 20, + 40, + 20, + -80, + -120, + 20, + 20 + ], + [ + null, + 20, + 60, + 30, + 30, + null, + 20, + 20, + 40, + 20, + -10, + null, + null + ], + [ + null, + 20, + 10, + 40, + 30, + null, + 10, + 20, + 20, + null, + null, + null, + null + ], + [ + null, + null, + 50, + 40, + 30, + -20, + 30, + 20, + 30, + null, + -70, + null, + null + ], + [ + null, + null, + 40, + 20, + 20, + -20, + 10, + 10, + 30, + null, + -30, + null, + null + ], + [ + null, + 10, + 40, + 10, + 30, + null, + 30, + 30, + 20, + null, + -30, + null, + null + ], + [ + null, + -10, + 50, + null, + 10, + -20, + 10, + null, + 20, + null, + null, + null, + null + ], + [ + null, + -10, + -10, + null, + null, + -30, + null, + null, + 20, + null, + -90, + null, + null + ], + [ + null, + -50, + 30, + 20, + 20, + null, + null, + 20, + 20, + null, + -30, + null, + null + ], + [ + null, + 20, + 20, + 20, + 10, + null, + 20, + 20, + 20, + null, + -60, + null, + null + ], + [ + null, + 20, + 40, + 30, + 30, + null, + 20, + 20, + 20, + null, + -100, + 10, + null + ], + [ + null, + 20, + 40, + 30, + 30, + null, + 20, + 20, + 20, + 20, + -20, + 10, + null + ] + ] + ] + } + }, + "# vim: set et sw=2 ts=2 sts=2:": false +} \ No newline at end of file diff --git a/handwrite/pngtosvg.py b/handwrite/pngtosvg.py deleted file mode 100644 index 7293e06..0000000 --- a/handwrite/pngtosvg.py +++ /dev/null @@ -1,90 +0,0 @@ -from PIL import Image, ImageChops -import os -import shutil -import subprocess - - -class PotraceNotFound(Exception): - pass - - -class PNGtoSVG: - """Converter class to convert character PNGs to BMPs and SVGs.""" - - def convert(self, directory): - """Call converters on each .png in the provider directory. - - Walk through the custom directory containing all .png files - from sheettopng and convert them to png -> bmp -> svg. - """ - path = os.walk(directory) - for root, dirs, files in path: - for f in files: - if f.endswith(".png"): - self.pngToBmp(root + "/" + f) - # self.trim(root + "/" + f[0:-4] + ".bmp") - self.bmpToSvg(root + "/" + f[0:-4] + ".bmp") - - def bmpToSvg(self, path): - """Convert .bmp image to .svg using potrace. - - Converts the passed .bmp file to .svg using the potrace - (http://potrace.sourceforge.net/). Each .bmp is passed as - a parameter to potrace which is called as a subprocess. - - Parameters - ---------- - path : str - Path to the bmp file to be converted. - - Raises - ------ - PotraceNotFound - Raised if potrace not found in path by shutil.which() - """ - if shutil.which("potrace") is None: - raise PotraceNotFound("Potrace is either not installed or not in path") - else: - subprocess.run(["potrace", path, "-b", "svg", "-o", path[0:-4] + ".svg"]) - - def pngToBmp(self, path): - """Convert .bmp image to .svg using potrace. - - Converts the passed .bmp file to .svg using the potrace - (http://potrace.sourceforge.net/). Each .bmp is passed as - a parameter to potrace which is called as a subprocess. - - Parameters - ---------- - path : str - Path to the bmp file to be converted. - - Raises - ------ - PotraceNotFound - Raised if potrace not found in path by shutil.which() - """ - img = Image.open(path).convert("RGBA").resize((100, 100)) - - # Threshold image to convert each pixel to either black or white - threshold = 200 - data = [] - for pix in list(img.getdata()): - if pix[0] >= threshold and pix[1] >= threshold and pix[3] >= threshold: - data.append((255, 255, 255, 0)) - else: - data.append((0, 0, 0, 1)) - img.putdata(data) - img.save(path[0:-4] + ".bmp") - - def trim(self, im_path): - im = Image.open(im_path) - bg = Image.new(im.mode, im.size, im.getpixel((0, 0))) - diff = ImageChops.difference(im, bg) - bbox = list(diff.getbbox()) - bbox[0] -= 1 - bbox[1] -= 1 - bbox[2] += 1 - bbox[3] += 1 - cropped_im = im.crop(bbox) - cropped_im.save(im_path) diff --git a/handwrite/sheettopng.py b/handwrite/sheettobmp.py similarity index 80% rename from handwrite/sheettopng.py rename to handwrite/sheettobmp.py index b38af6e..3921e6c 100644 --- a/handwrite/sheettopng.py +++ b/handwrite/sheettobmp.py @@ -1,161 +1,178 @@ -import os -import itertools -import json - -import cv2 - -# Seq: A-Z, a-z, 0-9, SPECIAL_CHARS -ALL_CHARS = list( - itertools.chain( - range(65, 91), - range(97, 123), - range(48, 58), - [ord(i) for i in ".,;:!?\"'-+=/%&()[]"], - ) -) - - -class SHEETtoPNG: - """Converter class to convert input sample sheet to character PNGs.""" - - def convert(self, sheet, characters_dir, config, cols=8, rows=10): - """Convert a sheet of sample writing input to a custom directory structure of PNGs. - - Detect all characters in the sheet as a separate contours and convert each to - a PNG image in a temp/user provided directory. - - Parameters - ---------- - sheet : str - Path to the sheet file to be converted. - characters_dir : str - Path to directory to save characters in. - config: str - Path to config file. - cols : int, default=8 - Number of columns of expected contours. Defaults to 8 based on the default sample. - rows : int, default=10 - Number of rows of expected contours. Defaults to 10 based on the default sample. - """ - with open(config) as f: - threshold_value = json.load(f).get("threshold_value", 200) - if os.path.isdir(sheet): - raise IsADirectoryError("Sheet parameter should not be a directory.") - characters = self.detect_characters( - sheet, threshold_value, cols=cols, rows=rows - ) - self.save_images( - characters, - characters_dir, - ) - - def detect_characters(self, sheet_image, threshold_value, cols=8, rows=10): - """Detect contours on the input image and filter them to get only characters. - - Uses opencv to threshold the image for better contour detection. After finding all - contours, they are filtered based on area, cropped and then sorted sequentially based - on coordinates. Finally returs the cols*rows top candidates for being the character - containing contours. - - Parameters - ---------- - sheet_image : str - Path to the sheet file to be converted. - threshold_value : int - Value to adjust thresholding of the image for better contour detection. - cols : int, default=8 - Number of columns of expected contours. Defaults to 8 based on the default sample. - rows : int, default=10 - Number of rows of expected contours. Defaults to 10 based on the default sample. - - Returns - ------- - sorted_characters : list of list - Final rows*cols contours in form of list of list arranged as: - sorted_characters[x][y] denotes contour at x, y position in the input grid. - """ - # TODO Raise errors and suggest where the problem might be - - # Read the image and convert to grayscale - image = cv2.imread(sheet_image) - gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - - # Threshold and filter the image for better contour detection - _, thresh = cv2.threshold(gray, threshold_value, 255, 1) - close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) - close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel, iterations=2) - - # Search for contours. - contours, h = cv2.findContours( - close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE - ) - - # Filter contours based on number of sides and then reverse sort by area. - contours = sorted( - filter( - lambda cnt: len( - cv2.approxPolyDP(cnt, 0.01 * cv2.arcLength(cnt, True), True) - ) - == 4, - contours, - ), - key=cv2.contourArea, - reverse=True, - ) - - # Calculate the bounding of the first contour and approximate the height - # and width for final cropping. - x, y, w, h = cv2.boundingRect(contours[0]) - space_h, space_w = 7 * h // 16, 7 * w // 16 - - # Since amongst all the contours, the expected case is that the 4 sided contours - # containing the characters should have the maximum area, so we loop through the first - # rows*colums contours and add them to final list after cropping. - characters = [] - for i in range(rows * cols): - x, y, w, h = cv2.boundingRect(contours[i]) - cx, cy = x + w // 2, y + h // 2 - - roi = image[cy - space_h : cy + space_h, cx - space_w : cx + space_w] - characters.append([roi, cx, cy]) - - # Now we have the characters but since they are all mixed up we need to position them. - # Sort characters based on 'y' coordinate and group them by number of rows at a time. Then - # sort each group based on the 'x' coordinate. - characters.sort(key=lambda x: x[2]) - sorted_characters = [] - for k in range(rows): - sorted_characters.extend( - sorted(characters[cols * k : cols * (k + 1)], key=lambda x: x[1]) - ) - - return sorted_characters - - def save_images(self, characters, characters_dir): - """Create directory for each character and save as PNG. - - Creates directory and PNG file for each image as following: - - characters_dir/ord(character)/ord(character).png (SINGLE SHEET INPUT) - characters_dir/sheet_filename/ord(character)/ord(character).png (MULTIPLE SHEETS INPUT) - - Parameters - ---------- - characters : list of list - Sorted list of character images each inner list representing a row of images. - characters_dir : str - Path to directory to save characters in. - """ - os.makedirs(characters_dir, exist_ok=True) - - # Create directory for each character and save the png for the characters - # Structure (single sheet): UserProvidedDir/ord(character)/ord(character).png - # Structure (multiple sheets): UserProvidedDir/sheet_filename/ord(character)/ord(character).png - for k, images in enumerate(characters): - character = os.path.join(characters_dir, str(ALL_CHARS[k])) - if not os.path.exists(character): - os.mkdir(character) - cv2.imwrite( - os.path.join(character, str(ALL_CHARS[k]) + ".png"), - images[0], - ) +import itertools +import json +import os + +import cv2 +from PIL import Image + +# Seq: A-Z, a-z, 0-9, SPECIAL_CHARS +ALL_CHARS = list( + itertools.chain( + range(65, 91), + range(97, 123), + range(48, 58), + [ord(i) for i in ".,;:!?\"'-+=/%&()[]"], + ) +) + + +class SHEETtoBMP: + """Converter class to convert input sample sheet to character BMPs.""" + + def convert(self, sheet, characters_dir, config, cols=8, rows=10): + """Convert a sheet of sample writing input to a custom directory structure of BMPs. + + Detect all characters in the sheet as a separate contours and convert each to + a BMP image in a temp/user provided directory. + + Parameters + ---------- + sheet : str + Path to the sheet file to be converted. + characters_dir : str + Path to directory to save characters in. + config: str + Path to config file. + cols : int, default=8 + Number of columns of expected contours. Defaults to 8 based on the default sample. + rows : int, default=10 + Number of rows of expected contours. Defaults to 10 based on the default sample. + """ + with open(config) as f: + threshold_value = json.load(f).get("threshold_value", 200) + if os.path.isdir(sheet): + raise IsADirectoryError("Sheet parameter should not be a directory.") + characters = self.detect_characters( + sheet, threshold_value, cols=cols, rows=rows + ) + self.save_images( + characters, + characters_dir, + ) + + def detect_characters(self, sheet_image, threshold_value, cols=8, rows=10): + """Detect contours on the input image and filter them to get only characters. + + Uses opencv to threshold the image for better contour detection. After finding all + contours, they are filtered based on area, cropped and then sorted sequentially based + on coordinates. Finally returs the cols*rows top candidates for being the character + containing contours. + + Parameters + ---------- + sheet_image : str + Path to the sheet file to be converted. + threshold_value : int + Value to adjust thresholding of the image for better contour detection. + cols : int, default=8 + Number of columns of expected contours. Defaults to 8 based on the default sample. + rows : int, default=10 + Number of rows of expected contours. Defaults to 10 based on the default sample. + + Returns + ------- + sorted_characters : list of list + Final rows*cols contours in form of list of list arranged as: + sorted_characters[x][y] denotes contour at x, y position in the input grid. + """ + # TODO Raise errors and suggest where the problem might be + + # Read the image and convert to grayscale + image = cv2.imread(sheet_image) + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + + # Threshold and filter the image for better contour detection + _, thresh = cv2.threshold(gray, threshold_value, 255, 1) + close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel, iterations=2) + + # Search for contours. + contours, h = cv2.findContours( + close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + + # Filter contours based on number of sides and then reverse sort by area. + contours = sorted( + filter( + lambda cnt: len( + cv2.approxPolyDP(cnt, 0.01 * cv2.arcLength(cnt, True), True) + ) + == 4, + contours, + ), + key=cv2.contourArea, + reverse=True, + ) + + # Calculate the bounding of the first contour and approximate the height + # and width for final cropping. + x, y, w, h = cv2.boundingRect(contours[0]) + space_h, space_w = 7 * h // 16, 7 * w // 16 + + # Since amongst all the contours, the expected case is that the 4 sided contours + # containing the characters should have the maximum area, so we loop through the first + # rows*colums contours and add them to final list after cropping. + characters = [] + for i in range(rows * cols): + x, y, w, h = cv2.boundingRect(contours[i]) + cx, cy = x + w // 2, y + h // 2 + + roi = image[cy - space_h : cy + space_h, cx - space_w : cx + space_w] + characters.append([roi, cx, cy]) + + # Now we have the characters but since they are all mixed up we need to position them. + # Sort characters based on 'y' coordinate and group them by number of rows at a time. Then + # sort each group based on the 'x' coordinate. + characters.sort(key=lambda x: x[2]) + sorted_characters = [] + for k in range(rows): + sorted_characters.extend( + sorted(characters[cols * k : cols * (k + 1)], key=lambda x: x[1]) + ) + + return sorted_characters + + def save_images(self, characters, characters_dir): + """Create directory for each character and save as BMP. + + Creates directory and BMP file for each image as following: + + characters_dir/ord(character)/ord(character).bmp (SINGLE SHEET INPUT) + characters_dir/sheet_filename/ord(character)/ord(character).bmp (MULTIPLE SHEETS INPUT) + + Parameters + ---------- + characters : list of list + Sorted list of character images each inner list representing a row of images. + characters_dir : str + Path to directory to save characters in. + """ + os.makedirs(characters_dir, exist_ok=True) + + # Create directory for each character and save the bmp for the characters + # Structure (single sheet): UserProvidedDir/ord(character)/ord(character).bmp + # Structure (multiple sheets): UserProvidedDir/sheet_filename/ord(character)/ord(character).bmp + for k, images in enumerate(characters): + character = os.path.join(characters_dir, str(ALL_CHARS[k])) + if not os.path.exists(character): + os.mkdir(character) + # Convert the image to BMP and apply thresholding and trimming + self.save_as_bmp( + images[0], os.path.join(character, str(ALL_CHARS[k]) + ".bmp") + ) + + def save_as_bmp(self, image, path): + img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGBA)).resize( + (100, 100) + ) + + # Threshold image to convert each pixel to either black or white + threshold = 200 + data = [] + for pix in list(img.getdata()): + if pix[0] >= threshold and pix[1] >= threshold and pix[3] >= threshold: + data.append((255, 255, 255, 0)) + else: + data.append((0, 0, 0, 1)) + img.putdata(data) + img.save(path) diff --git a/handwrite/svgtottf.py b/handwrite/svgtottf.py index febae0d..2f4f212 100644 --- a/handwrite/svgtottf.py +++ b/handwrite/svgtottf.py @@ -1,212 +1,209 @@ -import sys -import os -import json -import uuid - - -class SVGtoTTF: - def convert(self, directory, outdir, config, metadata=None): - """Convert a directory with SVG images to TrueType Font. - - Calls a subprocess to the run this script with Fontforge Python - environment. - - Parameters - ---------- - directory : str - Path to directory with SVGs to be converted. - outdir : str - Path to output directory. - config : str - Path to config file. - metadata : dict - Dictionary containing the metadata (filename, family or style) - """ - import subprocess - import platform - - subprocess.run( - ( - ["ffpython"] - if platform.system() == "Windows" - else ["fontforge", "-script"] - ) - + [ - os.path.abspath(__file__), - config, - directory, - outdir, - json.dumps(metadata), - ] - ) - - def set_properties(self): - """Set metadata of the font from config.""" - props = self.config["props"] - lang = props.get("lang", "English (US)") - fontname = self.metadata.get("filename", None) or props.get( - "filename", "Example" - ) - family = self.metadata.get("family", None) or fontname - style = self.metadata.get("style", None) or props.get("style", "Regular") - - self.font.familyname = fontname - self.font.fontname = fontname + "-" + style - self.font.fullname = fontname + " " + style - self.font.encoding = props.get("encoding", "UnicodeFull") - - for k, v in props.items(): - if hasattr(self.font, k): - if isinstance(v, list): - v = tuple(v) - setattr(self.font, k, v) - - if self.config.get("sfnt_names", None): - self.config["sfnt_names"]["Family"] = family - self.config["sfnt_names"]["Fullname"] = family + " " + style - self.config["sfnt_names"]["PostScriptName"] = family + "-" + style - self.config["sfnt_names"]["SubFamily"] = style - - self.config["sfnt_names"]["UniqueID"] = family + " " + str(uuid.uuid4()) - - for k, v in self.config.get("sfnt_names", {}).items(): - self.font.appendSFNTName(str(lang), str(k), str(v)) - - def add_glyphs(self, directory): - """Read and add SVG images as glyphs to the font. - - Walks through the provided directory and uses each ord(character).svg file - as glyph for the character. Then using the provided config, set the font - parameters and export TTF file to outdir. - - Parameters - ---------- - directory : str - Path to directory with SVGs to be converted. - """ - space = self.font.createMappedChar(ord(" ")) - space.width = 500 - - for k in self.config["glyphs"]: - # Create character glyph - g = self.font.createMappedChar(k) - self.unicode_mapping.setdefault(k, g.glyphname) - # Get outlines - src = "{}/{}.svg".format(k, k) - src = directory + os.sep + src - g.importOutlines(src, ("removeoverlap", "correctdir")) - g.removeOverlap() - - def set_bearings(self, bearings): - """Add left and right bearing from config - - Parameters - ---------- - bearings : dict - Map from character: [left bearing, right bearing] - """ - default = bearings.get("Default", [60, 60]) - - for k, v in bearings.items(): - if v[0] is None: - v[0] = default[0] - if v[1] is None: - v[1] = default[1] - - if k != "Default": - glyph_name = self.unicode_mapping[ord(str(k))] - self.font[glyph_name].left_side_bearing = v[0] - self.font[glyph_name].right_side_bearing = v[1] - - def set_kerning(self, table): - """Set kerning values in the font. - - Parameters - ---------- - table : dict - Config dictionary with kerning values/autokern bool. - """ - rows = table["rows"] - rows = [list(i) if i != None else None for i in rows] - cols = table["cols"] - cols = [list(i) if i != None else None for i in cols] - - self.font.addLookup("kern", "gpos_pair", 0, [["kern", [["latn", ["dflt"]]]]]) - - if table.get("autokern", True): - self.font.addKerningClass( - "kern", "kern-1", table.get("seperation", 0), rows, cols, True - ) - else: - kerning_table = table.get("table", False) - if not kerning_table: - raise ValueError("Kerning offsets not found in the config file.") - flatten_list = ( - lambda y: [x for a in y for x in flatten_list(a)] - if type(y) is list - else [y] - ) - offsets = [0 if x is None else x for x in flatten_list(kerning_table)] - self.font.addKerningClass("kern", "kern-1", rows, cols, offsets) - - def generate_font_file(self, filename, outdir, config_file): - """Output TTF file. - - Additionally checks for multiple outputs and duplicates. - - Parameters - ---------- - filename : str - Output filename. - outdir : str - Path to output directory. - config_file : str - Path to config file. - """ - if filename is None: - raise NameError("filename not found in config file.") - - outfile = str( - outdir - + os.sep - + (filename + ".ttf" if not filename.endswith(".ttf") else filename) - ) - - while os.path.exists(outfile): - outfile = os.path.splitext(outfile)[0] + " (1).ttf" - - sys.stderr.write("\nGenerating %s...\n" % outfile) - self.font.generate(outfile) - - def convert_main(self, config_file, directory, outdir, metadata): - try: - self.font = fontforge.font() - except: - import fontforge - - with open(config_file) as f: - self.config = json.load(f) - self.metadata = json.loads(metadata) or {} - - self.font = fontforge.font() - self.unicode_mapping = {} - self.set_properties() - self.add_glyphs(directory) - - # bearing table - self.set_bearings(self.config["typography_parameters"].get("bearing_table", {})) - - # kerning table - self.set_kerning(self.config["typography_parameters"].get("kerning_table", {})) - - # Generate font and save as a .ttf file - filename = self.metadata.get("filename", None) or self.config["props"].get( - "filename", None - ) - self.generate_font_file(str(filename), outdir, config_file) - - -if __name__ == "__main__": - if len(sys.argv) != 5: - raise ValueError("Incorrect call to SVGtoTTF") - SVGtoTTF().convert_main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) +import itertools +import json +import os +import sys +import uuid + + +class SVGtoTTF: + def convert(self, directory, outdir, config, metadata=None): + """Convert a directory with SVG images to TrueType Font. + + Calls a subprocess to the run this script with Fontforge Python + environment. + + Parameters + ---------- + directory : str + Path to directory with SVGs to be converted. + outdir : str + Path to output directory. + config : str + Path to config file. + metadata : dict + Dictionary containing the metadata (filename, family or style) + """ + import platform + import subprocess + + subprocess.run( + ( + ["ffpython"] + if platform.system() == "Windows" + else ["fontforge", "-script"] + ) + + [ + os.path.abspath(__file__), + config, + directory, + outdir, + json.dumps(metadata), + ] + ) + + def set_properties(self): + """Set metadata of the font from config.""" + props = self.config["props"] + lang = props.get("lang", "English (US)") + fontname = self.metadata.get("filename", None) or props.get( + "filename", "Example" + ) + family = self.metadata.get("family", None) or fontname + style = self.metadata.get("style", None) or props.get("style", "Regular") + + self.font.familyname = fontname + self.font.fontname = fontname + "-" + style + self.font.fullname = fontname + " " + style + self.font.encoding = props.get("encoding", "UnicodeFull") + + for k, v in props.items(): + if hasattr(self.font, k): + if isinstance(v, list): + v = tuple(v) + setattr(self.font, k, v) + + if self.config.get("sfnt_names", None): + self.config["sfnt_names"]["Family"] = family + self.config["sfnt_names"]["Fullname"] = family + " " + style + self.config["sfnt_names"]["PostScriptName"] = family + "-" + style + self.config["sfnt_names"]["SubFamily"] = style + + self.config["sfnt_names"]["UniqueID"] = family + " " + str(uuid.uuid4()) + + for k, v in self.config.get("sfnt_names", {}).items(): + self.font.appendSFNTName(str(lang), str(k), str(v)) + + def add_glyphs(self, directory): + """Read and add SVG images as glyphs to the font. + + Walks through the provided directory and uses each ord(character).svg file + as glyph for the character. Then using the provided config, set the font + parameters and export TTF file to outdir. + + Parameters + ---------- + directory : str + Path to directory with SVGs to be converted. + """ + space = self.font.createMappedChar(ord(" ")) + space.width = 500 + + for k in self.config["glyphs"]: + # Create character glyph + g = self.font.createMappedChar(k) + self.unicode_mapping.setdefault(k, g.glyphname) + # Get outlines + src = "{}/{}.svg".format(k, k) + src = directory + os.sep + src + g.importOutlines(src, ("removeoverlap", "correctdir")) + g.removeOverlap() + + def set_bearings(self, bearings): + """Add left and right bearing from config + + Parameters + ---------- + bearings : dict + Map from character: [left bearing, right bearing] + """ + default = bearings.get("Default", [60, 60]) + + for k, v in bearings.items(): + if v[0] is None: + v[0] = default[0] + if v[1] is None: + v[1] = default[1] + + if k != "Default": + glyph_name = self.unicode_mapping[ord(str(k))] + self.font[glyph_name].left_side_bearing = v[0] + self.font[glyph_name].right_side_bearing = v[1] + + def set_kerning(self, table): + """Set kerning values in the font. + + Parameters + ---------- + table : dict + Config dictionary with kerning values/autokern bool. + """ + rows = table["rows"] + rows = [list(i) if i is not None else None for i in rows] + cols = table["cols"] + cols = [list(i) if i is not None else None for i in cols] + + self.font.addLookup("kern", "gpos_pair", 0, [["kern", [["latn", ["dflt"]]]]]) + + if table.get("autokern", True): + self.font.addKerningClass( + "kern", "kern-1", table.get("seperation", 0), rows, cols, True + ) + else: + kerning_table = table.get("table", False) + if not kerning_table: + raise ValueError("Kerning offsets not found in the config file.") + flattened_kerning_table = list(itertools.chain.from_iterable(kerning_table)) + offsets = [0 if x is None else x for x in flattened_kerning_table] + self.font.addKerningClass("kern", "kern-1", rows, cols, offsets) + + def generate_font_file(self, filename, outdir, config_file): + """Output TTF file. + + Additionally checks for multiple outputs and duplicates. + + Parameters + ---------- + filename : str + Output filename. + outdir : str + Path to output directory. + config_file : str + Path to config file. + """ + if filename is None: + raise NameError("filename not found in config file.") + + outfile = str( + outdir + + os.sep + + (filename + ".ttf" if not filename.endswith(".ttf") else filename) + ) + + while os.path.exists(outfile): + outfile = os.path.splitext(outfile)[0] + " (1).ttf" + + sys.stderr.write("\nGenerating %s...\n" % outfile) + self.font.generate(outfile) + + def convert_main(self, config_file, directory, outdir, metadata): + try: + self.font = fontforge.font() + except: + import fontforge + + with open(config_file) as f: + self.config = json.load(f) + self.metadata = json.loads(metadata) or {} + + self.font = fontforge.font() + self.unicode_mapping = {} + self.set_properties() + self.add_glyphs(directory) + + # bearing table + self.set_bearings(self.config["typography_parameters"].get("bearing_table", {})) + + # kerning table + self.set_kerning(self.config["typography_parameters"].get("kerning_table", {})) + + # Generate font and save as a .ttf file + filename = self.metadata.get("filename", None) or self.config["props"].get( + "filename", None + ) + self.generate_font_file(str(filename), outdir, config_file) + + +if __name__ == "__main__": + if len(sys.argv) != 5: + raise ValueError("Incorrect call to SVGtoTTF") + SVGtoTTF().convert_main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])