diff --git a/NEWS.md b/NEWS.md index 065d877..e5a987e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,14 @@ -# Reselase notes for 0.5.0 + +# Release notes for 0.6.0 + - this release is a complete rewrite! + - much cleaner code for better readability in a more 'pythonic' way + - logging is now handled via the logging module instead of plain print commands + - move GUI-code to separate python class with main window + - remove confusing command line options + - always create both demo files + - reduce memory consumption + +# Release notes for 0.5.0 - address comments from users Klartext and TobFleischi at https://de.industryarena.com/heidenhain/forum/gravieren-von-text-in-anderen-sprachen-ohne-cam--83908.html . These changes break compatibility for previous versions - add parameter for plunging feed rate - use active offset and add incremental changes @@ -8,31 +18,25 @@ - fixed Warning on implicit conversion of float to integer # Release notes for 0.4.1 - - update to freetype-py version 2.0 - freetype.dll included # Release notes for 0.4.0 - - added option to remove empty characters completely or reduce the file size by only adding them as labels # Release notes for 0.3.2 - - fixed error in path generation - fixed spelling errors in comments and documentation # Release notes for 0.3.2 - - code cleanup - option to remove empty characters from output # Release notes for 0.3.1 - - assign all values from the command line - setup-script for building stand alone windows binaries # Release notes for 0.3.0 - - First public release - converts font outline to NC code for TNC controls - support for 21 character ranges diff --git a/setup.py b/setup.py index 92455ce..649be0e 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import setuptools +from type2nc import __version__ with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="type2nc", - version="0.5.0", + version=__version__, author="drunsinn et al.", author_email="dr.unsinn@googlemail.com", description="convert truetype fonts to klartext nc-code", @@ -32,7 +33,4 @@ 'freetype-py>=1.1', 'argparse>=1.4.0'], scripts=['type2nc/type2nc.py'], - #data_files=[('', ['type2nc/demo_pgm_template.H', - # 'type2nc/pgm_foot_template.H', - # 'type2nc/pgm_head_template.H']),], ) diff --git a/setup_cxFreeze.py b/setup_cxFreeze.py deleted file mode 100644 index 6c14b8b..0000000 --- a/setup_cxFreeze.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import cx_Freeze -import os - -os.environ['TCL_LIBRARY'] = r'C:\Python36\tcl\tcl8.6' -os.environ['TK_LIBRARY'] = r'C:\Python36\tcl\tcl8.6' - -with open("README.md", "r") as fh: - long_description = fh.read() - -base = None - -executables = [cx_Freeze.Executable("type2nc/type2nc.py", base=base)] - -packages = ["idna", "os", "platform", "datetime", "string", "unicodedata", - "tkinter", "argparse", "numpy", "scipy", "freetype"] -options = { - 'build_exe': { - 'packages': packages, - }, -} - -cx_Freeze.setup( - name="type2nc", - author="drunsinn", - author_email="dr.unsinn@googlemail.com", - options=options, - version="0.5.0", - description="convert truetype fonts to klartext nc-code", - executables=executables -) diff --git a/type2nc/__init__.py b/type2nc/__init__.py index 3c50458..5ef5a2c 100644 --- a/type2nc/__init__.py +++ b/type2nc/__init__.py @@ -1,3 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- name = "type2nc" +__version__ = "0.6.0" diff --git a/type2nc/templates/demo_pgm_template_conventional.H b/type2nc/templates/demo_pgm_template_conventional.H index b791d09..a18ce33 100644 --- a/type2nc/templates/demo_pgm_template_conventional.H +++ b/type2nc/templates/demo_pgm_template_conventional.H @@ -5,12 +5,12 @@ BLK FORM 0.2 X+300 Y{0:+d} Z+0 TOOL CALL "GRAVIER" Z S5000 F200 ; QS10 = " ABC abc 123 !%& " -QS10 = QS10 || "(%0x0298 %0x203f %0x0298) " -QS10 = QS10 || "%0x041f%0x0440%0x0438%0x0432%0x0435%0x0442 " -QS10 = QS10 || "%0x043c%0x0438%0x0440 " -QS10 = QS10 || "%0x4f60%0x597d%0x4e16%0x754c " -QS10 = QS10 || "%0x0645%0x0631%0x062d%0x0628%0x0627 " -QS10 = QS10 || "%0x0628%0x0627%0x0644%0x0639%0x0627%0x0644%0x0645 " +;QS10 = QS10 || "(%0x0298 %0x203f %0x0298) " +;QS10 = QS10 || "%0x041f%0x0440%0x0438%0x0432%0x0435%0x0442 " +;QS10 = QS10 || "%0x043c%0x0438%0x0440 " +;QS10 = QS10 || "%0x4f60%0x597d%0x4e16%0x754c " +;QS10 = QS10 || "%0x0645%0x0631%0x062d%0x0628%0x0627 " +;QS10 = QS10 || "%0x0628%0x0627%0x0644%0x0639%0x0627%0x0644%0x0645 " ; ; QS500= QS10 ;GRAVIERTEXT diff --git a/type2nc/templates/demo_pgm_template_cycle.H b/type2nc/templates/demo_pgm_template_cycle.H index b23a709..55bf97f 100644 --- a/type2nc/templates/demo_pgm_template_cycle.H +++ b/type2nc/templates/demo_pgm_template_cycle.H @@ -5,12 +5,12 @@ BLK FORM 0.2 X+300 Y{0:+d} Z+0 TOOL CALL "GRAVIER" Z S5000 F200 ; QS10 = " ABC abc 123 !%& " -QS10 = QS10 || "(%0x0298 %0x203f %0x0298) " -QS10 = QS10 || "%0x041f%0x0440%0x0438%0x0432%0x0435%0x0442 " -QS10 = QS10 || "%0x043c%0x0438%0x0440 " -QS10 = QS10 || "%0x4f60%0x597d%0x4e16%0x754c " -QS10 = QS10 || "%0x0645%0x0631%0x062d%0x0628%0x0627 " -QS10 = QS10 || "%0x0628%0x0627%0x0644%0x0639%0x0627%0x0644%0x0645 " +;QS10 = QS10 || "(%0x0298 %0x203f %0x0298) " +;QS10 = QS10 || "%0x041f%0x0440%0x0438%0x0432%0x0435%0x0442 " +;QS10 = QS10 || "%0x043c%0x0438%0x0440 " +;QS10 = QS10 || "%0x4f60%0x597d%0x4e16%0x754c " +;QS10 = QS10 || "%0x0645%0x0631%0x062d%0x0628%0x0627 " +;QS10 = QS10 || "%0x0628%0x0627%0x0644%0x0639%0x0627%0x0644%0x0645 " ; ; Q1600 = 0 ;surface in Z diff --git a/type2nc/templates/pgm_head_template.H b/type2nc/templates/pgm_head_template.H index 8f2a263..4915883 100644 --- a/type2nc/templates/pgm_head_template.H +++ b/type2nc/templates/pgm_head_template.H @@ -1,7 +1,7 @@ ; ;Usage: set the following Q-Parameters to the desired values ;QS500 is the text -;Q200 is the sage distance +;Q200 is the safe distance for pre positioning ;Q203 is the Z-coordinate of the surface ;Q201 is the depth ;Q204 is the safe Z-coordinate for traversing between cuts diff --git a/type2nc/type2nc.py b/type2nc/type2nc.py index 4003eb8..7b5caf9 100644 --- a/type2nc/type2nc.py +++ b/type2nc/type2nc.py @@ -4,205 +4,180 @@ @author: drunsinn, TobFleischi, Klartext ''' -import os -import os.path as osp -import logging -# import platform import datetime -import string +import logging import argparse +import pathlib +import math +import string +import freetype import numpy as np from scipy.special import binom -import freetype as ft -logger = logging.getLogger(__name__) - -class Type2NC(object): - BASIC_LATIN = list(range(0x0020, 0x007E + 1)) - C1_CTRL_AND_LATIN1_SUPPLEMENT = list(range(0x0080, 0x00FF + 1)) - IPA_EEXTENTIONS = list(range(0x0250, 0x02AF + 1)) - GREEK_AND_COPTIC_CHARS = list(range(0x0370, 0x03FF + 1)) - CYRILLIC_CHARS = list(range(0x0400, 0x04FF + 1)) - CYRILLIC_SUPPLEMENT_CHARS = list(range(0x0500, 0x052F + 1)) - ARMENIAN_CHARS = list(range(0x0530, 0x058F + 1)) - HEBREW_CHARS = list(range(0x0590, 0x05FF + 1)) - ARABIC_CHARS = list(range(0x0600, 0x06FF + 1)) - SYRIAC_CHARS = list(range(0x0700, 0x074F + 1)) - ARABIC_SUPPLEMENT_CHARS = list(range(0x0750, 0x077F + 1)) - GENERAL_PUNCTUATION = list(range(0x2000, 0x206F + 1)) - ARROW_CHARS = list(range(0x2190, 0x21FF + 1)) - MATHEMATICAL_CHARS = list(range(0x2200, 0x22FF + 1)) - MISC_TECH_CHARS = list(range(0x2300, 0x23FF + 1)) - MISC_SYMBOLS = list(range(0x2600, 0x26FF + 1)) - DINGBATS = list(range(0x2700, 0x27BF + 1)) - CJK_UNIFIED_IDEOGRAPHS_PART = list(range(0x4E00, 0x9FFF + 1)) - - MODE_ALL = 1 - MODE_REDUCE = 2 - MODE_REMOVE = 3 - - def __init__(self, bezier_step_size, char_list, output_folder, size_pt=50, - size_dpi=100, target_height=10, output_mode=MODE_ALL): - self.__bezier_step_size = bezier_step_size - self.__char_list = char_list - self.__output_folder = output_folder - self.__char_size_pt = size_pt - self.__char_size_dpi = size_dpi - self.__char_target_height = target_height - self.__output_mode = output_mode - self.__nc_file_list = list() - self.__template_directory = osp.join(osp.dirname(osp.join(osp.realpath(__file__))), 'templates') - - def type2font(self, font_file_path): - face = ft.Face(font_file_path) - logger.info("File: {0:s}".format(font_file_path)) - logger.info("Font: {0:s}, Style: {1:s}".format( - face.family_name.decode("utf-8"), - face.style_name.decode("utf-8"))) - - face.set_char_size(height=self.__char_size_pt * self.__char_size_dpi) - - char_lines = [] - char_data_collection = list() - empty_char_list = list() - x_max = 0 - - for char in self.__char_list: - if face.get_char_index(char) == 0 and char != u"\u0020" and self.__output_mode is not Type2NC.MODE_ALL: - # only add empty characters if mode user wants it - empty_char_list.append(char) - else: - char_data = dict() - char_data['plain'] = str(char) - char_data['ord'] = ord(chr(char)) - char_data['info'] = self._get_char_info(face, char) - char_data['paths'] = self._get_paths_of_char(face, char) - char_data['text'] = self._get_char_name(char) - char_data_collection.append(char_data) - - # remember the highest x-value to calculate the scale factor - if char_data['info']['x_max'] > x_max: - x_max = char_data['info']['x_max'] - - scale_factor = self.__char_target_height / x_max - - for char_data in char_data_collection: - # create and add lines for non empty characters - char_lines.extend(self._create_char_label(char_data, scale_factor)) - - if self.__output_mode is Type2NC.MODE_REDUCE: - # create and add one label for empty characters - char_lines.extend(self._create_empty_label(empty_char_list, - scale_factor, - face)) - - nc_file_name = osp.basename(font_file_path).split('.')[0] + '.H' - nc_file_name = nc_file_name.replace(' ', '_') - self.__nc_file_list.append(nc_file_name) - output_file_path = osp.join(self.__output_folder, nc_file_name) - - output_lines = [] - output_lines.append('BEGIN PGM {0:s} MM'.format(nc_file_name.upper())) - output_lines.append(';') - output_lines.append('; Font PGM generated by type2nc') - output_lines.append('; {0:s} - {1:s}'.format( - face.family_name.decode("utf-8"), - face.style_name.decode("utf-8"))) - output_lines.append('; Generated: {:%Y-%m-%d %H:%M:%S}'.format( - datetime.datetime.today())) - output_lines.append('; Number of characters: {0:d}'.format( - len(self.__char_list))) - output_lines.append(';') - - with open(osp.join(self.__template_directory, 'pgm_head_template.H'), 'r') as template_file: - for line in template_file: - output_lines.append(line.rstrip('\n').rstrip('\r')) - - output_lines.append(';') - - output_lines.extend(char_lines) - - with open(osp.join(self.__template_directory, 'pgm_foot_template.H'), 'r') as template_file: - for line in template_file: - output_lines.append(line.rstrip('\n').rstrip('\r')) - - output_lines.append('END PGM {0:s} MM'.format(nc_file_name.upper())) - - output_fp = open(output_file_path, 'w') - for i, line in enumerate(output_lines): - output_fp.write('{0:d} {1:s}\n'.format(i, line)) - output_fp.close() - - file_size = osp.getsize(output_file_path) - if self.__output_mode is Type2NC.MODE_REDUCE or self.__output_mode is Type2NC.MODE_REMOVE: - logger.info("{0:d} of {1:d} selected characters were found empty".format( - len(empty_char_list), - len(self.__char_list))) - else: - logger.info("{0:d} characters were selected".format( - len(self.__char_list))) - logger.info("lines: {0:d}, file size: {1:d} bytes".format(len(output_lines), - file_size)) +class Point(): + def __init__(self, x, y): + self.x = float(x) + self.y = float(y) + + def __str__(self): + return "X:{:+0.4f} Y:{:+0.4f}".format(self.x, self.y) + + def scaled_str(self, scale_factor=1.0): + return "X{:+0.4f} Y{:+0.4f}".format(self.x * scale_factor, self.y * scale_factor) - def _create_empty_label(self, empty_chars, scale_factor, font_face): - lable_lines = list() - for char in empty_chars: - lable_lines.append('LBL "0x{0:04x}"'.format(ord(chr(char)))) - path = self._get_paths_of_char(font_face, empty_chars[1]) - info = self._get_char_info(font_face, empty_chars[1]) - - lable_lines.extend(self._generate_path_lines(path, scale_factor)) - lable_lines.append('QL20 = {0:+f} ; X-Advance'.format(info['x_advance'] * scale_factor)) - lable_lines.append('LBL 0') - return lable_lines - - def _create_char_label(self, char_data, scale_factor): - """Generate klartext code for character - - Keyword arguments: - char_data -- information on the character - scale_factor -- factor for scaling the x and y values - """ - char_lines = list() - # plain_char_name = char_data['plain'] - if char_data['text'] is not None: - char_lines.append('* - {0:s}'.format(char_data['text'])) - char_lines.append('LBL "{0:s}"'.format(char_data['text'])) - char_lines.append('* - Unicode Hex:0x{0:04x} : {1:s}'.format( - char_data['ord'], char_data['plain'])) - char_lines.append('LBL "0x{0:04x}"'.format(char_data['ord'])) - - char_lines.extend(self._generate_path_lines(char_data['paths'], - scale_factor)) - - if char_data['text'] is not None: - char_lines.append('LBL "{0:s}_X-Advance"'.format(char_data['text'])) - char_lines.append('LBL "0x{0:04x}_X-Advance"'.format(char_data['ord'])) - char_lines.append('QL20 = {0:+f} ; X-Advance'.format( - char_data['info']['x_advance'] * scale_factor)) - char_lines.append('LBL 0') - char_lines.append(';') - return char_lines +class Type2NC(object): - def _generate_path_lines(self, path_data, scale_factor): - path_lines = list() - for path in path_data: - path_lines.append('L X{0:+0.4f} Y{1:+0.4f} FMAX'.format( - path[0][0] * scale_factor, - path[0][1] * scale_factor)) - path_lines.append('L Z+QL15 F+Q206') - for point in path[1:]: - path_lines.append('L X{0:+0.4f} Y{1:+0.4f} F+Q207'.format( - point[0] * scale_factor, point[1] * scale_factor)) - if not (path[0][0] == point[0] and - path[0][1] == point[1]): - path_lines.append('L X{0:+0.4f} Y{1:+0.4f} F+Q207'.format( - path[0][0] * scale_factor, - path[0][1] * scale_factor)) - path_lines.append('L Z+Q204 F AUTO') + def __init__(self, output_folder, target_height, step_size, unicode_numbers=None): + self._log = logging.getLogger("Type2NC") + self.__output_folder = output_folder.resolve(strict=True) + self.__target_height = target_height + self.__step_size = step_size + self.__char_size_pt = 10 + self.__char_size_dpi = 140 + + self.__characters = list() + if unicode_numbers is None: + self.__characters.extend(list(range(0x0020, 0x007E + 1))) # BASIC_LATIN + self.__characters.extend(list(range(0x0080, 0x00FF + 1))) # C1_CTRL_AND_LATIN1_SUPPLEMENT + self.__characters.extend(list(range(0x2000, 0x206F + 1))) # GENERAL_PUNCTUATION + self.__characters.extend(list(range(0x0250, 0x02AF + 1))) # IPA_EEXTENTIONS + self.__characters.extend(list(range(0x0370, 0x03FF + 1))) # GREEK_AND_COPTIC_CHARS + self.__characters.extend(list(range(0x0400, 0x04FF + 1))) # CYRILLIC_CHARS + self.__characters.extend(list(range(0x0500, 0x052F + 1))) # CYRILLIC_SUPPLEMENT_CHARS + self.__characters.extend(list(range(0x0530, 0x058F + 1))) # ARMENIAN_CHARS + self.__characters.extend(list(range(0x0590, 0x05FF + 1))) # HEBREW_CHARS + self.__characters.extend(list(range(0x0600, 0x06FF + 1))) # ARABIC_CHARS + self.__characters.extend(list(range(0x0700, 0x074F + 1))) # SYRIAC_CHARS + self.__characters.extend(list(range(0x0750, 0x077F + 1))) # ARABIC_SUPPLEMENT_CHARS + self.__characters.extend(list(range(0x2190, 0x21FF + 1))) # ARROW_CHARS + self.__characters.extend(list(range(0x2200, 0x22FF + 1))) # MATHEMATICAL_CHARS + self.__characters.extend(list(range(0x2300, 0x23FF + 1))) # MISC_TECH_CHARS + self.__characters.extend(list(range(0x2600, 0x26FF + 1))) # MISC_SYMBOLS + self.__characters.extend(list(range(0x2700, 0x27BF + 1))) # DINGBATS + self.__characters.extend(list(range(0x4E00, 0x9FFF + 1))) # CJK_UNIFIED_IDEOGRAPHS_PART + else: + self.__characters.extend(unicode_numbers) + + self._log.debug("using folder '%s', step size '%f'", self.__output_folder, self.__step_size) + + def convert(self, font_file): + self._log.debug("running for font file '%s' for %d characters", font_file.resolve(strict=True), len(self.__characters)) + + font_face = freetype.Face(str(font_file.resolve(strict=True))) + self._log.info("Font: %s, Style: %s, Format: %s", font_face.family_name.decode("utf-8"), font_face.style_name.decode("utf-8"), font_face.get_format().decode("utf-8")) + #font_face.set_char_size(height=self.__char_size) + font_face.set_char_size(height=self.__char_size_pt * self.__char_size_dpi, hres=self.__char_size_dpi, vres=self.__char_size_dpi) + + self._log.info("Font BBox (min:max) X:(%d:%d) Y:(%d:%d)", font_face.bbox.xMax, font_face.bbox.xMin, font_face.bbox.yMax, font_face.bbox.yMin) + scale_factor = self.__target_height / (abs(font_face.bbox.yMax) + abs(font_face.bbox.yMin)) + self._log.info("set scaling factor to %f", scale_factor) - return path_lines + nc_file_name = font_file.name.replace(font_file.suffix, '.H') + nc_file_name = nc_file_name.replace(' ', '_') + nc_file_path = self.__output_folder.joinpath(nc_file_name) + + with open(nc_file_path, 'w') as ncfp: + ncfp.write("BEGIN PGM {0:s} MM\n".format(font_file.name.replace(font_file.suffix, '').upper())) + + ncfp.write(";\n; Font PGM generated by type2nc\n") + ncfp.write("; {0:s} - {1:s}\n".format(font_face.family_name.decode("utf-8"), font_face.style_name.decode("utf-8"))) + ncfp.write("; Generated: {:%Y-%m-%d %H:%M:%S}\n".format(datetime.datetime.today())) + ncfp.write("; Number of characters: {0:d}\n;\n".format(len(self.__characters))) + + with open(pathlib.Path.cwd().joinpath("templates", "pgm_head_template.H")) as template_file: + ncfp.writelines(template_file) + + for char_code in self.__characters: + char_index = font_face.get_char_index(char_code) + if char_index > 0: + self._log.debug("character '%s' available at index %d", chr(char_code), char_index) + + try: + font_face.load_char(chr(char_code)) + except freetype.ft_errors.FT_Exception as e: + self._log.error("character '%s' at index %d could not be loaded, skip. Error: %s", chr(char_code), char_index, e) + break + + c_glyph_slot = font_face.glyph + + contour_paths = list() + + self._log.debug("character '%s' advance X:%d Y:%d linear horizontal advance:%d", chr(char_code), c_glyph_slot.advance.x, c_glyph_slot.advance.y, c_glyph_slot.linearHoriAdvance) + + if c_glyph_slot.outline.n_points > 1: + self._log.debug("character '%s' has %d points in %d contour(s) with %d tags", chr(char_code), c_glyph_slot.outline.n_points, c_glyph_slot.outline.n_contours, len(c_glyph_slot.outline.tags)) + + start, end = 0, 0 + + for i in range(0, c_glyph_slot.outline.n_contours): + path_points = list() + contour_segments = list() + end = c_glyph_slot.outline.contours[i] + + # slice lists of points and tags according to length of contour + contour_tags = c_glyph_slot.outline.tags[start:end+1] + contour_tags.append(c_glyph_slot.outline.tags[0]) + contour_points = c_glyph_slot.outline.points[start:end+1] + contour_points.append(c_glyph_slot.outline.points[0]) + + # add first point in list to segment to start things of + contour_segments.append([contour_points[0], ]) + + # split the list of all the points in separate segments + # with the right amount for each contour type + for j in range(1, len(contour_points)): # skip first point as it is already in the list + contour_segments[-1].append(contour_points[j]) + if contour_tags[j] & (1 << 0) and j < (len(contour_points) - 1): + contour_segments.append([contour_points[j], ]) + + # finally, check each segment for the number of points it contains. + # if only two the segmet is a line so we can just add the end point to our list + # if there are more than two points in the segment we have to step along the bezier curve + for segment in contour_segments: + if len(segment) == 2: # line segment, add endpoint to list + path_points.append(Point(x=segment[0][0], y=segment[0][1])) + else: # bezier curve, split into segments an add them to list + num_points = int(1.0 / self.__step_size) + for t in np.linspace(0.0, 1.0, num_points, endpoint=True): + path_points.append(self._point_on_curve(segment, t)) + + # close path by addind the first point a second time + path_points.append(Point(x=contour_segments[0][0][0], y=contour_segments[0][0][1])) + + start = end + 1 + contour_paths.append(path_points) + self._log.debug("created %d points for character contour %d", len(path_points), i) + + self._log.debug("created %d paths for character", len(contour_paths)) + + else: + self._log.debug("character '%s' is empty, skipping", chr(char_code)) + + if c_glyph_slot.advance.x > 0: + x_advance = c_glyph_slot.advance.x + else: + self._log.debug("character '%s' has no advance x value, use glyph width %d", chr(char_code), c_glyph_slot.metrics.width) + x_advance = c_glyph_slot.metrics.width + + ncfp.writelines(self._creat_font_label(chr(char_code), contour_paths, x_advance, scale_factor)) + + else: + self._log.debug("character '%s' was selected but is not available, skipping", chr(char_code)) + + with open(pathlib.Path.cwd().joinpath("templates", "pgm_foot_template.H")) as template_file: + ncfp.writelines(template_file) + + ncfp.write("END PGM {0:s} MM".format(font_file.name.replace(font_file.suffix, '').upper())) + + self._log.info("finished writing %s", nc_file_name) + + def _build_char_string(self, range_list): + characters = list() + print(range_list) + for char_range in range_list: + characters.extend(char_range) + return characters def _point_on_curve(self, point_list, distance_factor): """Get x and y coordinate for point along a bezier curve relative to @@ -218,112 +193,42 @@ def _point_on_curve(self, point_list, distance_factor): c_t_x = 0 c_t_y = 0 for i, point in enumerate(point_list): - b_i_n = binom(n, i) * (distance_factor ** i) *\ - np.power((1 - distance_factor), (n - i)) + b_i_n = binom(n, i) * (distance_factor ** i) * np.power((1 - distance_factor), (n - i)) c_t_x += b_i_n * point[0] c_t_y += b_i_n * point[1] - return c_t_x, c_t_y + return Point(x=c_t_x, y=c_t_y) + + def _creat_font_label(self, char_str, contour_paths, x_advance, scale_factor): + self._log.debug("created label for char %s", char_str) + char_lines = list() + label_name = self._translate_label_name(char_str) + self._log.debug("writing label for character '%s': %d %s", char_str, ord(char_str), label_name) - def _get_char_info(self, font_face, char): - """returns information about the charkter as a dictionary + if char_str in string.ascii_letters + string.digits: + char_lines.append("* - {0:s}\n".format(char_str)) - Keyword arguments: - font_face -- freetype font face from the selected font file - char -- character which should be converted - """ - logger.debug('get information for char number %d', char) - font_face.load_char(char) - slot = font_face.glyph - outline = slot.outline - points = np.array(outline.points, dtype=[('x', float), ('y', float)]) - char_info = dict() - if len(slot.outline.points) > 0: - char_info['x_max'] = points['x'].max() - char_info['x_min'] = points['x'].min() - char_info['y_max'] = points['y'].max() - char_info['y_min'] = points['y'].min() - char_info['x_advance'] = slot.advance.x - char_info['y_advance'] = slot.advance.y + if label_name is not None: + char_lines.append("LBL \"{0:s}\"\n".format(label_name)) + char_lines.append("* - Unicode Hex:0x{0:04x} : {1:s}\n".format(ord(char_str), label_name)) else: - char_info['x_max'] = 0 - char_info['x_min'] = 0 - char_info['y_max'] = 0 - char_info['y_min'] = 0 - char_info['x_advance'] = slot.advance.x - char_info['y_advance'] = slot.advance.y - - return char_info + char_lines.append("* - Unicode Hex:0x{0:04x}\n".format(ord(char_str))) + char_lines.append("LBL \"0x{0:04x}\"\n".format(ord(char_str))) - def _get_paths_of_char(self, font_face, char): - """Get list of x and y coordinates the outline of a character - - Keyword arguments: - font_face -- freetype font face from the selected font file - char -- character which should be converted - """ - - font_face.load_char(char) - slot = font_face.glyph - outline = slot.outline - paths = [] - - if len(slot.outline.points) > 0: - points = np.array(outline.points, - dtype=[('x', float), ('y', float)]) - # x = points['x'] - start, end = 0, 0 - - # iterate over each contour - for i in range(len(outline.contours)): - end = outline.contours[i] # upper end of contour points - points = outline.points[start:end + 1] # contour points - - points.append(points[0]) - - # list of contour types - tags = outline.tags[start:end + 1] - tags.append(tags[0]) - - segments = [[points[0], ], ] - - path_points = [] - path_points.append(points[0]) - - # split the list of all the points in separate parts - # with the right amount for each contour type - for j in range(1, len(points)): - # skip first point as it is already in the list - segments[-1].append(points[j]) - if tags[j] & (1 << 0) and j < (len(points) - 1): - segments.append([points[j], ]) - - for segment in segments: - if len(segment) == 2: # line segment, add endpoint to list - path_points.append(segment[-1]) - else: # bezier curve, split into segments an add them to list - num_points = int(1.0 / self.__bezier_step_size) - for t in np.linspace( - 0.0, - 1.0, - num_points, - endpoint=True): - path_points.append( - self._point_on_curve(segment, t)) - - paths.append(path_points) - start = end + 1 - else: - pass - # char is empty - - return paths - - def _get_char_name(self, char): - """Get name of character for label call. + for path in contour_paths: + char_lines.append("L {0:s} FMAX\n".format(path[0].scaled_str(scale_factor))) + char_lines.append("L Z+QL15 F+Q206\n") + for point in path[1:]: + char_lines.append("L {0:s} F+Q207\n".format(point.scaled_str(scale_factor))) + char_lines.append("L Z+Q204 F+Q206\n") + char_lines.append("L Z+QL32 FMAX\n") + + if label_name is not None: char_lines.append("LBL \"{0:s}_X-Advance\"\n".format(label_name)) + char_lines.append("LBL \"0x{0:04x}_X-Advance\"\n".format(ord(char_str))) + char_lines.append("QL20 = {0:+f} ; X-Advance\n".format(x_advance * scale_factor)) + char_lines.append("LBL 0\n;\n") + return char_lines - Keyword arguments: - char -- character - """ + def _translate_label_name(self, char_str): label_map = { '!': 'exclamation_mark', '\'': 'apostrophe', @@ -357,169 +262,333 @@ def _get_char_name(self, char): 'Ü': 'UE', ' ': 'space' } - - char = chr(char) - - if char in label_map.keys(): - label_name = label_map[char] - elif char in string.ascii_letters + string.digits + string.punctuation: - label_name = char + if char_str in label_map.keys(): + label_name = label_map[char_str] + elif char_str in string.ascii_letters + string.digits + string.punctuation: + label_name = char_str else: label_name = None # could not identify character return label_name - - def generate_demo_file(self, use_cycle_def=False): - output_file_path = osp.join(self.__output_folder, "type2nc_demo.H") - - if use_cycle_def: - demo_template = osp.join(self.__template_directory, 'demo_pgm_template_cycle.H') - else: - demo_template = osp.join(self.__template_directory, 'demo_pgm_template_conventional.H') - - with open(demo_template, 'r') as template_file: - demo_file_content = template_file.read() - - part_y_max = 10 + len(self.__nc_file_list) * 20 + 10 - current_y = 10 - + + def generate_demo_files(self, font_file_list): + self._log.debug("create demo files") filler = "" - pgm_call_template = 'L X+5 Y+5 Z+100 R0 FMAX\n' - pgm_call_template += 'CALL PGM {1:s}\n;' - for file_path in self.__nc_file_list: - filler += pgm_call_template.format(current_y, file_path) - current_y += 20 - - output_fp = open(output_file_path, 'w') - output_fp.write(demo_file_content.format(part_y_max, filler)) - output_fp.close() + current_y = 5 + for font_file in font_file_list: + nc_file_name = font_file.name.replace(font_file.suffix, '.H') + nc_file_name = nc_file_name.replace(' ', '_') + + filler += "L X+5 Y+%d Z+100 R0 FMAX\n" % current_y + filler += "CALL PGM %s\n;\n" % nc_file_name + current_y += self.__target_height + 5 + filler = filler.rstrip("\n") + + block_heigth = current_y + 2 * self.__target_height + + with open(pathlib.Path.cwd().joinpath("templates", "demo_pgm_template_conventional.H")) as template_file: + with open(self.__output_folder.joinpath("type2nc_demo_conventional.H"), 'w') as output_file: + demo_file_content = template_file.read() + output_file.write(demo_file_content.format(block_heigth, filler)) + self._log.debug("demo file for conventional calling written successfully") + + with open(pathlib.Path.cwd().joinpath("templates", "demo_pgm_template_cycle.H")) as template_file: + with open(self.__output_folder.joinpath("type2nc_demo_cycle.H"), 'w') as output_file: + demo_file_content = template_file.read() + output_file.write(demo_file_content.format(block_heigth, filler)) + self._log.debug("demo file for cycle based calling written successfully") + + +class Type2NC_UI: + def __init__(self, root): + self._log = logging.getLogger("Type2NCUI") + self._log.debug("start building tk ui") + self._window_root = root + root.title("type2nc") + width=650 + height=260 + screenwidth = root.winfo_screenwidth() + screenheight = root.winfo_screenheight() + alignstr = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2) + self._window_root.geometry(alignstr) + self._window_root.resizable(width=False, height=False) + + ft = tkFont.Font(family='Times', size=12) + + current_y = 20 + delta_y = 40 + component_height = 30 + + self.btn_select_font = tk.Button(self._window_root) + self.btn_select_font["font"] = ft + self.btn_select_font["justify"] = "center" + self.btn_select_font["text"] = "Select Font File" + self.btn_select_font.place(x=20, y=current_y, width=160, height=component_height) + self.btn_select_font["command"] = self.btn_select_font_command + + self.lbl_font_filename = tk.Label(self._window_root) + self.lbl_font_filename["font"] = ft + self.lbl_font_filename["justify"] = "center" + self.lbl_font_filename["text"] = " " + self.lbl_font_filename.place(x=200, y=current_y, width=180, height=component_height) + current_y += delta_y + + self.btn_select_folder = tk.Button(self._window_root) + self.btn_select_folder["font"] = ft + self.btn_select_folder["justify"] = "center" + self.btn_select_folder["text"] = "Select Destination Folder" + self.btn_select_folder.place(x=20, y=current_y, width=160, height=component_height) + self.btn_select_folder["command"] = self.btn_select_folder_command + + self.lbl_output_path = tk.Label(self._window_root) + self.lbl_output_path["font"] = ft + self.lbl_output_path["justify"] = "center" + self.lbl_output_path["text"] = " " + self.lbl_output_path.place(x=200, y=current_y, width=180, height=component_height) + current_y += delta_y + + self.btn_select_step = tk.Button(self._window_root) + self.btn_select_step["font"] = ft + self.btn_select_step["justify"] = "center" + self.btn_select_step["text"] = "Select Step Size" + self.btn_select_step.place(x=20, y=current_y, width=160, height=component_height) + self.btn_select_step["command"] = self.btn_select_step_command + + self.lbl_step_size = tk.Label(self._window_root) + self.lbl_step_size["font"] = ft + self.lbl_step_size["justify"] = "center" + self.lbl_step_size["text"] = " " + self.lbl_step_size.place(x=200, y=current_y, width=180, height=component_height) + current_y += delta_y + + self.gen_demo_files = tk.IntVar() + self.chk_generate_demos = tk.Checkbutton(self._window_root) + self.chk_generate_demos["font"] = ft + self.chk_generate_demos["justify"] = "left" + self.chk_generate_demos["text"] = "Create Demo Files" + self.chk_generate_demos.place(x=20, y=current_y, width=150, height=component_height) + self.chk_generate_demos["onvalue"] = 1 + self.chk_generate_demos["offvalue"] = 0 + self.chk_generate_demos["variable"] = self.gen_demo_files + self.chk_generate_demos.select() + current_y += delta_y + + self.btn_generate_nc = tk.Button(self._window_root) + self.btn_generate_nc["font"] = ft + self.btn_generate_nc["justify"] = "center" + self.btn_generate_nc["text"] = "Generate" + self.btn_generate_nc.place(x=140, y=current_y, width=150, height=component_height) + self.btn_generate_nc["command"] = self.btn_generate_nc_command + current_y += delta_y + + self.pb_progress = ttk.Progressbar(self._window_root) + self.pb_progress["orient"] = tk.HORIZONTAL + self.pb_progress["length"] = 100 + self.pb_progress.place(x=115, y=current_y, width=200, height=component_height) + self.pb_progress["mode"] = 'determinate' + self.pb_progress['value'] = 0 + current_y = 20 + + self.select_basics = tk.IntVar() + self.chk_select_basic = tk.Checkbutton(self._window_root) + self.chk_select_basic["font"] = ft + self.chk_select_basic["justify"] = "left" + self.chk_select_basic["text"] = "ASCII 0x20-0x7E and Latin1 0x80-0xFF" + self.chk_select_basic.place(x=390, y=current_y, width=240, height=component_height) + self.chk_select_basic["onvalue"] = 1 + self.chk_select_basic["offvalue"] = 0 + self.chk_select_basic["variable"] = self.select_basics + self.chk_select_basic.select() + current_y += delta_y + + self.select_punctuation = tk.IntVar() + self.chk_select_punctuation = tk.Checkbutton(self._window_root) + self.chk_select_punctuation["font"] = ft + self.chk_select_punctuation["justify"] = "left" + self.chk_select_punctuation["text"] = "General Punctuation 0x2000-0x206F" + self.chk_select_punctuation.place(x=390, y=current_y, width=240, height=component_height) + self.chk_select_punctuation["onvalue"] = 1 + self.chk_select_punctuation["offvalue"] = 0 + self.chk_select_punctuation["variable"] = self.select_punctuation + self.chk_select_punctuation.select() + current_y += delta_y + + self.select_ipa = tk.IntVar() + self.chk_select_ipa = tk.Checkbutton(self._window_root) + self.chk_select_ipa["font"] = ft + self.chk_select_ipa["justify"] = "left" + self.chk_select_ipa["text"] = "IPA Extention 0x0250-0x02AF" + self.chk_select_ipa.place(x=390, y=current_y, width=240, height=component_height) + self.chk_select_ipa["onvalue"] = 1 + self.chk_select_ipa["offvalue"] = 0 + self.chk_select_ipa["variable"] = self.select_ipa + self.chk_select_ipa.select() + current_y += delta_y + + self.select_symbols= tk.IntVar() + self.chk_select_symbols = tk.Checkbutton(self._window_root) + self.chk_select_symbols["font"] = ft + self.chk_select_symbols["justify"] = "left" + self.chk_select_symbols["text"] = "Symbols 0x2190-0x23FF & 0x2600-0x27BF" + self.chk_select_symbols.place(x=390, y=current_y, width=240, height=component_height) + self.chk_select_symbols["onvalue"] = 1 + self.chk_select_symbols["offvalue"] = 0 + self.chk_select_symbols["variable"] = self.select_symbols + self.chk_select_symbols.select() + current_y += delta_y + + self.select_add_lang = tk.IntVar() + self.chk_select_lang = tk.Checkbutton(self._window_root) + self.chk_select_lang["font"] = ft + self.chk_select_lang["justify"] = "left" + self.chk_select_lang["text"] = "Lang 0x0370-0x077F & 0x4E00-0x9FFF" + self.chk_select_lang.place(x=390, y=current_y, width=240, height=component_height) + self.chk_select_lang["onvalue"] = 1 + self.chk_select_lang["offvalue"] = 0 + self.chk_select_lang["variable"] = self.select_add_lang + self.chk_select_lang.select() + current_y += delta_y + + self._log.debug("ui ready") + + self.selected_font_files = list() + self.output_path = None + self.step_size = None + + def btn_select_font_command(self): + self._log.debug("select font button pressed") + file_types = [('Font', '*.ttf *.tte *.ttc *.otf *.dfont *.pfb')] + font_file_list = tkfd.askopenfilename(parent=self._window_root, title="Select Font file", filetypes=file_types, multiple=1) + for path in font_file_list: + self.selected_font_files.append(pathlib.Path(path)) + + if len(self.selected_font_files) == 1: + self.lbl_font_filename["text"] = str(self.selected_font_files[0].name) + self._log.debug("No font file selected") + else: + self.lbl_font_filename["text"] = "%d files selected" % len(self.selected_font_files) + self._log.debug("Add %d fonts to list: %s", len(font_file_list), font_file_list) + + def btn_select_folder_command(self): + self._log.debug("select output folder button pressed") + selected_folder = tkfd.askdirectory(parent=self._window_root, title="Select destination folder") + self.output_path = pathlib.Path(selected_folder) + + if not self.output_path.is_dir(): + self.output_path = None + self.lbl_output_path["text"] = "No folder selected" + self._log.debug("No folder selected") + else: + self.lbl_output_path["text"] = self.output_path.name + self._log.debug("Output folder set to %s", self.output_path) + + def btn_select_step_command(self): + self._log.debug("select step size button pressed") + selected_step = tksd.askfloat(parent=self._window_root, title="Step Size", prompt="Step Size between 0.001 (very fine) and 0.2 (very coarse) for converting Splines", initialvalue=0.05, minvalue=0.001, maxvalue=0.2) + if selected_step is None: + self.step_size = None + self.lbl_step_size["text"] = "No step size selected" + self._log.debug("No step size selected") + else: + self.step_size = selected_step + self.lbl_step_size["text"] = "%f" % self.step_size + self._log.debug("step size set to %f", self.step_size) + + def btn_generate_nc_command(self): + self._log.debug("generate nc files button pressed") + if len(self.selected_font_files) == 0: + tkmb.showwarning(parent=self._window_root, title="Missing parameter", message="No font files selected") + self._log.debug("list of selected font files is empty!") + return + if self.output_path is None: + tkmb.showwarning(parent=self._window_root, title="Missing parameter", message="No output folder selected") + self._log.debug("no output path selected!") + return + if self.step_size is None: + tkmb.showwarning(parent=self._window_root, title="Missing parameter", message="No step size selected") + self._log.debug("no step size selected selected!") + return + + character_list = list() + if self.select_basics.get() > 0: + character_list.extend(list(range(0x0020, 0x007E + 1))) # BASIC_LATIN + character_list.extend(list(range(0x0080, 0x00FF + 1))) # C1_CTRL_AND_LATIN1_SUPPLEMENT + + if self.select_punctuation.get() > 0: + character_list.extend(list(range(0x2000, 0x206F + 1))) # GENERAL_PUNCTUATION + + if self.select_ipa.get() > 0: + character_list.extend(list(range(0x0250, 0x02AF + 1))) # IPA_EEXTENTIONS + + if self.select_symbols.get() > 0: + character_list.extend(list(range(0x2190, 0x21FF + 1))) # ARROW_CHARS + character_list.extend(list(range(0x2200, 0x22FF + 1))) # MATHEMATICAL_CHARS + character_list.extend(list(range(0x2300, 0x23FF + 1))) # MISC_TECH_CHARS + character_list.extend(list(range(0x2600, 0x26FF + 1))) # MISC_SYMBOLS + character_list.extend(list(range(0x2700, 0x27BF + 1))) # DINGBATS + + if self.select_add_lang.get() > 0: + character_list.extend(list(range(0x0370, 0x03FF + 1))) # GREEK_AND_COPTIC_CHARS + character_list.extend(list(range(0x0400, 0x04FF + 1))) # CYRILLIC_CHARS + character_list.extend(list(range(0x0500, 0x052F + 1))) # CYRILLIC_SUPPLEMENT_CHARS + character_list.extend(list(range(0x0530, 0x058F + 1))) # ARMENIAN_CHARS + character_list.extend(list(range(0x0590, 0x05FF + 1))) # HEBREW_CHARS + character_list.extend(list(range(0x0600, 0x06FF + 1))) # ARABIC_CHARS + character_list.extend(list(range(0x0700, 0x074F + 1))) # SYRIAC_CHARS + character_list.extend(list(range(0x0750, 0x077F + 1))) # ARABIC_SUPPLEMENT_CHARS + character_list.extend(list(range(0x4E00, 0x9FFF + 1))) # CJK_UNIFIED_IDEOGRAPHS_PART + + self.pb_progress['value'] = 0 + conv = Type2NC(output_folder=self.output_path, target_height=10, step_size=self.step_size, unicode_numbers=character_list) + self._log.debug("start processing %d font files", len(self.selected_font_files)) + for i, ff in enumerate(self.selected_font_files): + if ff.is_file(): + conv.convert(ff) + self.pb_progress['value'] = (i + 1) * (100 / len(self.selected_font_files)) + self.pb_progress.update() + + if self.gen_demo_files.get() > 0: + self._log.debug("generating demo files is enabled") + conv.generate_demo_files(self.selected_font_files) + + tkmb.showinfo(parent=self._window_root, title="Finished", message="Finished processing all font files") + + self.lbl_font_filename["text"] = " " + del self.selected_font_files[:] if __name__ == "__main__": - logging.basicConfig(encoding='utf-8', level=logging.DEBUG) - logger.info('test') - - parser = argparse.ArgumentParser( - description="Create Klartext NC code from font files") - parser.add_argument( - "-i", - "--input", - metavar="font input file", - nargs='+', - help="path of one or more font files") - parser.add_argument( - "-o", - "--out", - metavar="output folder", - required=False, - help="path to the output folder. If not set, use current directory.") - parser.add_argument( - "-s", - "--step_size", - metavar="step size", - type=float, - default=0.05, - required=False, - help="step size: between 0.001 (very fine) and 0.2 (very coarse)") - parser.add_argument( - "-r", - "--remove_empty", - action='store_true', - default=False, - help="if set, output won't contain labels for empty chars. overrides -c") - parser.add_argument( - "-c", - "--reduce_empty", - action='store_true', - default=False, - help="if set, output will contain labels for empty characters but no actual data") - parser.add_argument( - "-z", - "--use_cycle_def", - action='store_true', - default=False, - help="if set, demo output will use cycle 225 for definition of parameters") - - args = parser.parse_args() - - char_list = list() - char_list = Type2NC.BASIC_LATIN - # char_list += Type2NC.C1_CTRL_AND_LATIN1_SUPPLEMENT - # char_list += Type2NC.IPA_EEXTENTIONS - # char_list += Type2NC.GREEK_AND_COPTIC_CHARS - # char_list += Type2NC.CYRILLIC_CHARS - # char_list += Type2NC.CYRILLIC_SUPPLEMENT_CHARS - # char_list += Type2NC.ARMENIAN_CHARS - # char_list += Type2NC.HEBREW_CHARS - # char_list += Type2NC.ARABIC_CHARS - # char_list += Type2NC.SYRIAC_CHARS - # char_list += Type2NC.ARABIC_SUPPLEMENT_CHARS - # char_list += Type2NC.GENERAL_PUNCTUATION - # char_list += Type2NC.ARROW_CHARS - # char_list += Type2NC.MATHEMATICAL_CHARS - # char_list += Type2NC.MISC_TECH_CHARS - # char_list += Type2NC.MISC_SYMBOLS - # char_list += Type2NC.DINGBATS - # char_list += Type2NC.CJK_UNIFIED_IDEOGRAPHS_PART - - file_types = [('Font', '*.ttf *.tte *.ttc *.otf *.dfont *.pfb')] - - if args.input is None: - import tkinter as tk + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger('main') + logger.debug("startup") + + cmdl_parser = argparse.ArgumentParser(description="Create Klartext NC code from font files. If no options are given, start gui") + cmdl_parser.add_argument("-i", "--input", metavar="font input file", nargs='+', type=pathlib.Path, help="path of one or more font files") + cmdl_parser.add_argument("-o", "--output", metavar="output folder", type=pathlib.Path, help="path to the output folder where klartext files are generated") + cmdl_parser.add_argument("-s", "--step_size", metavar="step size", type=float, default=0.05, required=False, help="step size for converting curves to line segmenst: between 0.001 (very fine) and 0.2 (very coarse)") + cmdl_parser.add_argument("-d", "--create_demos", action="store_true", default=False, required=False, help="if set, demo output will use cycle 225 for definition of parameters") + arguments = cmdl_parser.parse_args() + + if arguments.input is not None: + if not arguments.output.is_dir(): + logger.error("the output path '%s' is not a folder", arguments.output) + + conv = Type2NC(output_folder=arguments.output, target_height=10, step_size=arguments.step_size) + for ff in arguments.input: + if ff.is_file(): + + conv.convert(ff) + + if arguments.create_demos: + conv.generate_demo_files(arguments.input) + else: import tkinter.filedialog as tkfd import tkinter.simpledialog as tksd import tkinter.messagebox as tkmb + import tkinter as tk + import tkinter.ttk as ttk + import tkinter.font as tkFont root = tk.Tk() - root.overrideredirect(1) - root.withdraw() - - font_file_list = tkfd.askopenfilename(parent=root, - title="Select Font file", - filetypes=file_types, - multiple=1) - if len(font_file_list) < 1: - exit(-1) - - output_folder = tkfd.askdirectory(parent=root, - title="Select destination folder") - if len(output_folder) < 1: - exit(-2) - - remove_empty = tkmb.askyesno("Remove empty", "Remove empty characters from output?") - - if not remove_empty: - reduce_empty = tkmb.askyesno("Reduce empty", "Reduce filesize by reducing empty characters?") - - step_size = tksd.askfloat("Step Size", - "Step Size between 0.001 (very fine) and 0.2 (very coarse) for converting Splines", - initialvalue=0.05, - minvalue=0.001, - maxvalue=0.2) - if step_size is None: - exit(-3) - - else: - font_file_list = args.input - step_size = args.step_size - if args.out is None: - output_folder = os.getcwd() - else: - output_folder = args.out - remove_empty = args.remove_empty - reduce_empty = args.reduce_empty - - if remove_empty: - mode_select = Type2NC.MODE_REMOVE - elif reduce_empty: - mode_select = Type2NC.MODE_REDUCE - else: - mode_select = Type2NC.MODE_ALL - - font_converter = Type2NC(bezier_step_size=step_size, - char_list=char_list, - output_folder=osp.abspath(output_folder), - output_mode=mode_select) - - for font_file in font_file_list: - font_converter.type2font(osp.abspath(font_file)) + app = Type2NC_UI(root) + root.mainloop() - font_converter.generate_demo_file(args.use_cycle_def) + logger.debug("finished") \ No newline at end of file