From 003ee406a5bc5d31f5f62183fc5d67b4140a39f9 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 18 Dec 2018 20:16:54 -0500 Subject: [PATCH 01/24] add trim checkbox --- lib/extensions/lettering.py | 17 +++++++++++------ lib/lettering/font.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index b6d67c0b5..fd9c7628b 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -38,6 +38,10 @@ class LetteringFrame(wx.Frame): self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) + self.trim_checkbox = wx.CheckBox(self, label=_("Add trims")) + self.trim_checkbox.SetValue(bool(self.settings.trim)) + self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) + # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) @@ -86,15 +90,16 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() + def update_lettering(self): + font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) + font = Font(font_path) + self.group[:] = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + def generate_patches(self, abort_early=None): patches = [] - font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) - font = Font(font_path) - try: - lines = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth) - self.group[:] = lines + self.update_lettering() elements = nodes_to_elements(self.group.iterdescendants(SVG_PATH_TAG)) for element in elements: @@ -131,7 +136,7 @@ class LetteringFrame(wx.Frame): def apply(self, event): self.preview.disable() - self.generate_patches() + self.update_lettering() self.save_settings() self.close() diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 9d0389a0a..05465c8c4 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -81,7 +81,7 @@ class Font(object): kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) - def render_text(self, text, variant=None, back_and_forth=True): + def render_text(self, text, variant=None, back_and_forth=True, trim=False): if variant is None: variant = self.default_variant From aea7b846a2841b587cffe4a31622a79584868caa Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 7 Jan 2019 19:55:05 -0500 Subject: [PATCH 02/24] correct for viewbox --- lib/extensions/lettering.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index fd9c7628b..60b4e1f30 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -12,6 +12,7 @@ from ..elements import nodes_to_elements from ..gui import PresetsPanel, SimulatorPreview from ..i18n import _ from ..lettering import Font +from ..svg import get_correction_transform from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL, INKSTITCH_LETTERING from ..utils import get_bundled_dir, DotDict from .commands import CommandsExtension @@ -215,7 +216,8 @@ class Lettering(CommandsExtension): else: self.ensure_current_layer() return inkex.etree.SubElement(self.current_layer, SVG_GROUP_TAG, { - INKSCAPE_LABEL: _("Ink/Stitch Lettering") + INKSCAPE_LABEL: _("Ink/Stitch Lettering"), + "transform": get_correction_transform(self.current_layer, child=True) }) def effect(self): From 3611e2340997b917cc89e7d405b3c7d9bc86aab5 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 17 Jan 2019 19:26:56 -0500 Subject: [PATCH 03/24] fix two font issues --- fonts/small_font/←.svg | 44 +++++++++++++++++++++--------------------- fonts/small_font/→.svg | 12 ++++++------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/fonts/small_font/←.svg b/fonts/small_font/←.svg index 1875cce32..dd635bccd 100644 --- a/fonts/small_font/←.svg +++ b/fonts/small_font/←.svg @@ -25,10 +25,10 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="22.4" - inkscape:cx="7.7366328" + inkscape:cx="-1.7053315" inkscape:cy="19.401085" inkscape:document-units="px" - inkscape:current-layer="g20430" + inkscape:current-layer="g21438" showgrid="false" units="px" inkscape:window-width="1366" @@ -422,18 +422,18 @@ style="display:none" id="g21438"> + sodipodi:nodetypes="cccc" /> + sodipodi:nodetypes="cccc" + embroider_center_walk_underlay_stitch_length_mm="1.2" /> + id="path7425" /> + id="path37800" /> Date: Fri, 22 Feb 2019 22:07:15 -0500 Subject: [PATCH 04/24] refactor add_commands() out into commands module --- lib/commands.py | 136 +++++++++++++++++- lib/extensions/auto_satin.py | 4 +- lib/extensions/base.py | 11 +- lib/extensions/commands.py | 118 --------------- lib/extensions/layer_commands.py | 4 +- lib/extensions/object_commands.py | 9 +- lib/svg/__init__.py | 7 +- .../{realistic_rendering.py => rendering.py} | 129 ++++++++++++++++- lib/svg/svg.py | 127 ++-------------- 9 files changed, 282 insertions(+), 263 deletions(-) rename lib/svg/{realistic_rendering.py => rendering.py} (51%) diff --git a/lib/commands.py b/lib/commands.py index 3c7397088..ddee83269 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -1,12 +1,18 @@ +from copy import deepcopy +import os +from random import random import sys -import inkex + import cubicsuperpath +import inkex import simpletransform -from .svg import apply_transforms, get_node_transform -from .svg.tags import SVG_USE_TAG, SVG_SYMBOL_TAG, CONNECTION_START, CONNECTION_END, XLINK_HREF -from .utils import cache, Point from .i18n import _, N_ +from .svg import apply_transforms, get_node_transform, get_correction_transform, get_document, generate_unique_id +from .svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG, SVG_USE_TAG, SVG_SYMBOL_TAG, \ + CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE, XLINK_HREF, INKSCAPE_LABEL +from .utils import cache, get_bundled_dir, Point + COMMANDS = { # L10N command attached to an object @@ -228,3 +234,125 @@ def _standalone_commands(svg): def is_command(node): return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib + + +@cache +def symbols_path(): + return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg") + + +@cache +def symbols_svg(): + with open(symbols_path()) as symbols_file: + return inkex.etree.parse(symbols_file) + + +@cache +def symbol_defs(): + return get_defs(symbols_svg()) + + +@cache +def get_defs(document): + return document.find(SVG_DEFS_TAG) + + +def ensure_symbol(document, command): + """Make sure the command's symbol definition exists in the tag.""" + + path = "./*[@id='inkstitch_%s']" % command + defs = get_defs(document) + if defs.find(path) is None: + defs.append(deepcopy(symbol_defs().find(path))) + + +def add_group(document, node, command): + return inkex.etree.SubElement( + node.getparent(), + SVG_GROUP_TAG, + { + "id": generate_unique_id(document, "group"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + "transform": get_correction_transform(node) + }) + + +def add_connector(document, symbol, element): + # I'd like it if I could position the connector endpoint nicely but inkscape just + # moves it to the element's center immediately after the extension runs. + start_pos = (symbol.get('x'), symbol.get('y')) + end_pos = element.shape.centroid + + path = inkex.etree.Element(SVG_PATH_TAG, + { + "id": generate_unique_id(document, "connector"), + "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), + "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", + CONNECTION_START: "#%s" % symbol.get('id'), + CONNECTION_END: "#%s" % element.node.get('id'), + CONNECTOR_TYPE: "polyline", + + # l10n: the name of the line that connects a command to the object it applies to + INKSCAPE_LABEL: _("connector") + }) + + symbol.getparent().insert(0, path) + + +def add_symbol(document, group, command, pos): + return inkex.etree.SubElement(group, SVG_USE_TAG, + { + "id": generate_unique_id(document, "use"), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(pos.x), + "y": str(pos.y), + + # l10n: the name of a command symbol (example: scissors icon for trim command) + INKSCAPE_LABEL: _("command marker"), + }) + + +def get_command_pos(element, index, total): + # Put command symbols 30 pixels out from the shape, spaced evenly around it. + + # get a line running 30 pixels out from the shape + outline = element.shape.buffer(30).exterior + + # pick this item's spot arond the outline and perturb it a bit to avoid + # stacking up commands if they run the extension multiple times + position = index / float(total) + position += random() * 0.1 + + return outline.interpolate(position, normalized=True) + + +def remove_legacy_param(element, command): + if command == "trim" or command == "stop": + # If they had the old "TRIM after" or "STOP after" attributes set, + # automatically delete them. THe new commands will do the same + # thing. + # + # If we didn't delete these here, then things would get confusing. + # If the user were to delete a "trim" symbol added by this extension + # but the "embroider_trim_after" attribute is still set, then the + # trim would keep happening. + + attribute = "embroider_%s_after" % command + + if attribute in element.node.attrib: + del element.node.attrib[attribute] + + +def add_commands(element, commands): + document = get_document(element.node) + + for i, command in enumerate(commands): + ensure_symbol(document, command) + remove_legacy_param(element, command) + + group = add_group(document, element.node, command) + pos = get_command_pos(element, i, len(commands)) + symbol = add_symbol(document, group, command, pos) + add_connector(document, symbol, element) diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index f846ac6b9..90d8fe33c 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -2,6 +2,7 @@ import sys import inkex +from ..commands import add_commands from ..elements import SatinColumn from ..i18n import _ from ..stitches.auto_satin import auto_satin @@ -97,6 +98,5 @@ class AutoSatin(CommandsExtension): def add_trims(self, new_elements, trim_indices): if self.options.trim and trim_indices: - self.ensure_symbol("trim") for i in trim_indices: - self.add_commands(new_elements[i], ["trim"]) + add_commands(new_elements[i], ["trim"]) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 986735410..8d45f790f 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -9,6 +9,7 @@ from stringcase import snakecase from ..commands import layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..i18n import _ +from ..svg import generate_unique_id from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS @@ -194,15 +195,7 @@ class InkstitchExtension(inkex.Effect): def uniqueId(self, prefix, make_new_id=True): """Override inkex.Effect.uniqueId with a nicer naming scheme.""" - i = 1 - while True: - new_id = "%s%d" % (prefix, i) - if new_id not in self.doc_ids: - break - i += 1 - self.doc_ids[new_id] = 1 - - return new_id + return generate_unique_id(self.document, prefix) def parse(self): """Override inkex.Effect.parse to add Ink/Stitch xml namespace""" diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 07b450e1a..86e291fd5 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -1,16 +1,4 @@ -import os -import inkex -from copy import deepcopy -from random import random - - from .base import InkstitchExtension -from ..utils import get_bundled_dir, cache -from ..commands import get_command_description -from ..i18n import _ -from ..svg.tags import SVG_DEFS_TAG, SVG_PATH_TAG, CONNECTION_START, CONNECTION_END, \ - CONNECTOR_TYPE, INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_USE_TAG, XLINK_HREF -from ..svg import get_correction_transform class CommandsExtension(InkstitchExtension): @@ -20,109 +8,3 @@ class CommandsExtension(InkstitchExtension): InkstitchExtension.__init__(self, *args, **kwargs) for command in self.COMMANDS: self.OptionParser.add_option("--%s" % command, type="inkbool") - - @property - def symbols_path(self): - return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg") - - @property - @cache - def symbols_svg(self): - with open(self.symbols_path) as symbols_file: - return inkex.etree.parse(symbols_file) - - @property - @cache - def symbol_defs(self): - return self.symbols_svg.find(SVG_DEFS_TAG) - - @property - @cache - def defs(self): - return self.document.find(SVG_DEFS_TAG) - - def ensure_symbol(self, command): - path = "./*[@id='inkstitch_%s']" % command - if self.defs.find(path) is None: - self.defs.append(deepcopy(self.symbol_defs.find(path))) - - def add_connector(self, symbol, element): - # I'd like it if I could position the connector endpoint nicely but inkscape just - # moves it to the element's center immediately after the extension runs. - start_pos = (symbol.get('x'), symbol.get('y')) - end_pos = element.shape.centroid - - path = inkex.etree.Element(SVG_PATH_TAG, - { - "id": self.uniqueId("connector"), - "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), - "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", - CONNECTION_START: "#%s" % symbol.get('id'), - CONNECTION_END: "#%s" % element.node.get('id'), - CONNECTOR_TYPE: "polyline", - - # l10n: the name of the line that connects a command to the object it applies to - INKSCAPE_LABEL: _("connector") - } - ) - - symbol.getparent().insert(0, path) - - def get_command_pos(self, element, index, total): - # Put command symbols 30 pixels out from the shape, spaced evenly around it. - - # get a line running 30 pixels out from the shape - outline = element.shape.buffer(30).exterior - - # pick this item's spot arond the outline and perturb it a bit to avoid - # stacking up commands if they run the extension multiple times - position = index / float(total) - position += random() * 0.1 - - return outline.interpolate(position, normalized=True) - - def remove_legacy_param(self, element, command): - if command == "trim" or command == "stop": - # If they had the old "TRIM after" or "STOP after" attributes set, - # automatically delete them. THe new commands will do the same - # thing. - # - # If we didn't delete these here, then things would get confusing. - # If the user were to delete a "trim" symbol added by this extension - # but the "embroider_trim_after" attribute is still set, then the - # trim would keep happening. - - attribute = "embroider_%s_after" % command - - if attribute in element.node.attrib: - del element.node.attrib[attribute] - - def add_commands(self, element, commands): - for i, command in enumerate(commands): - self.remove_legacy_param(element, command) - - pos = self.get_command_pos(element, i, len(commands)) - - group = inkex.etree.SubElement(element.node.getparent(), SVG_GROUP_TAG, - { - "id": self.uniqueId("group"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - "transform": get_correction_transform(element.node) - } - ) - - symbol = inkex.etree.SubElement(group, SVG_USE_TAG, - { - "id": self.uniqueId("use"), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(pos.x), - "y": str(pos.y), - - # l10n: the name of a command symbol (example: scissors icon for trim command) - INKSCAPE_LABEL: _("command marker"), - } - ) - - self.add_connector(symbol, element) diff --git a/lib/extensions/layer_commands.py b/lib/extensions/layer_commands.py index 3a746fcfb..c124ec95c 100644 --- a/lib/extensions/layer_commands.py +++ b/lib/extensions/layer_commands.py @@ -1,6 +1,6 @@ import inkex -from ..commands import LAYER_COMMANDS, get_command_description +from ..commands import LAYER_COMMANDS, get_command_description, ensure_symbol from ..i18n import _ from ..svg import get_correction_transform from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF @@ -21,7 +21,7 @@ class LayerCommands(CommandsExtension): correction_transform = get_correction_transform(self.current_layer, child=True) for i, command in enumerate(commands): - self.ensure_symbol(command) + ensure_symbol(command) inkex.etree.SubElement(self.current_layer, SVG_USE_TAG, { diff --git a/lib/extensions/object_commands.py b/lib/extensions/object_commands.py index 47fb361df..d33ab2ba6 100644 --- a/lib/extensions/object_commands.py +++ b/lib/extensions/object_commands.py @@ -1,8 +1,8 @@ import inkex -from .commands import CommandsExtension -from ..commands import OBJECT_COMMANDS +from ..commands import OBJECT_COMMANDS, add_commands from ..i18n import _ +from .commands import CommandsExtension class ObjectCommands(CommandsExtension): @@ -24,14 +24,11 @@ class ObjectCommands(CommandsExtension): inkex.errormsg(_("Please choose one or more commands to attach.")) return - for command in commands: - self.ensure_symbol(command) - # Each object (node) in the SVG may correspond to multiple Elements of different # types (e.g. stroke + fill). We only want to process each one once. seen_nodes = set() for element in self.elements: if element.node not in seen_nodes: - self.add_commands(element, commands) + add_commands(element, commands) seen_nodes.add(element.node) diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py index df76c0d25..0b4a6ee4f 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,4 +1,5 @@ -from .svg import color_block_to_point_lists, render_stitch_plan -from .units import * -from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp from .guides import get_guides +from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp +from .rendering import color_block_to_point_lists, render_stitch_plan +from .svg import get_document, generate_unique_id +from .units import * \ No newline at end of file diff --git a/lib/svg/realistic_rendering.py b/lib/svg/rendering.py similarity index 51% rename from lib/svg/realistic_rendering.py rename to lib/svg/rendering.py index 73da3a09c..41ed53d7f 100644 --- a/lib/svg/realistic_rendering.py +++ b/lib/svg/rendering.py @@ -1,8 +1,17 @@ -import simplepath import math -from .units import PIXELS_PER_MM +import inkex +import simplepath +import simplestyle +import simpletransform + +from ..i18n import _ from ..utils import Point +from ..utils import cache +from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG +from .units import PIXELS_PER_MM +from .units import get_viewbox_transform + # The stitch vector path looks like this: # _______ @@ -10,7 +19,6 @@ from ..utils import Point # # It's 0.32mm high, which is the approximate thickness of common machine # embroidery threads. - # 1.216 pixels = 0.32mm stitch_height = 1.216 @@ -128,3 +136,118 @@ def realistic_stitch(start, end): simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) return simplepath.formatPath(path) + + +def color_block_to_point_lists(color_block): + point_lists = [[]] + + for stitch in color_block: + if stitch.trim: + if point_lists[-1]: + point_lists.append([]) + continue + + if not stitch.jump and not stitch.color_change: + point_lists[-1].append(stitch.as_tuple()) + + # filter out empty point lists + point_lists = [p for p in point_lists if p] + + return point_lists + + +@cache +def get_correction_transform(svg): + transform = get_viewbox_transform(svg) + + # we need to correct for the viewbox + transform = simpletransform.invertTransform(transform) + transform = simpletransform.formatTransform(transform) + + return transform + + +def color_block_to_realistic_stitches(color_block, svg): + paths = [] + + for point_list in color_block_to_point_lists(color_block): + if not point_list: + continue + + color = color_block.color.visible_on_white.darker.to_hex_str() + start = point_list[0] + for point in point_list[1:]: + paths.append(inkex.etree.Element( + SVG_PATH_TAG, + {'style': simplestyle.formatStyle( + { + 'fill': color, + 'stroke': 'none', + 'filter': 'url(#realistic-stitch-filter)' + }), + 'd': realistic_stitch(start, point), + 'transform': get_correction_transform(svg) + })) + start = point + + return paths + + +def color_block_to_paths(color_block, svg): + paths = [] + # We could emit just a single path with one subpath per point list, but + # emitting multiple paths makes it easier for the user to manipulate them. + for point_list in color_block_to_point_lists(color_block): + color = color_block.color.visible_on_white.to_hex_str() + paths.append(inkex.etree.Element( + SVG_PATH_TAG, + {'style': simplestyle.formatStyle( + {'stroke': color, + 'stroke-width': "0.4", + 'fill': 'none'}), + 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), + 'transform': get_correction_transform(svg), + 'embroider_manual_stitch': 'true', + 'embroider_trim_after': 'true', + })) + + # no need to trim at the end of a thread color + if paths: + paths[-1].attrib.pop('embroider_trim_after') + + return paths + + +def render_stitch_plan(svg, stitch_plan, realistic=False): + layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + if layer is None: + layer = inkex.etree.Element(SVG_GROUP_TAG, + {'id': '__inkstitch_stitch_plan__', + INKSCAPE_LABEL: _('Stitch Plan'), + INKSCAPE_GROUPMODE: 'layer'}) + else: + # delete old stitch plan + del layer[:] + + # make sure the layer is visible + layer.set('style', 'display:inline') + + for i, color_block in enumerate(stitch_plan): + group = inkex.etree.SubElement(layer, + SVG_GROUP_TAG, + {'id': '__color_block_%d__' % i, + INKSCAPE_LABEL: "color block %d" % (i + 1)}) + if realistic: + group.extend(color_block_to_realistic_stitches(color_block, svg)) + else: + group.extend(color_block_to_paths(color_block, svg)) + + svg.append(layer) + + if realistic: + defs = svg.find(SVG_DEFS_TAG) + + if defs is None: + defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG) + + defs.append(inkex.etree.fromstring(realistic_filter)) diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 3fceebfba..0ec43f751 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,124 +1,19 @@ -import inkex -import simplestyle -import simpletransform - -from ..i18n import _ from ..utils import cache -from .realistic_rendering import realistic_stitch, realistic_filter -from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG -from .units import get_viewbox_transform - - -def color_block_to_point_lists(color_block): - point_lists = [[]] - - for stitch in color_block: - if stitch.trim: - if point_lists[-1]: - point_lists.append([]) - continue - - if not stitch.jump and not stitch.color_change: - point_lists[-1].append(stitch.as_tuple()) - - # filter out empty point lists - point_lists = [p for p in point_lists if p] - - return point_lists @cache -def get_correction_transform(svg): - transform = get_viewbox_transform(svg) - - # we need to correct for the viewbox - transform = simpletransform.invertTransform(transform) - transform = simpletransform.formatTransform(transform) - - return transform +def get_document(node): + return node.getroottree().getroot() -def color_block_to_realistic_stitches(color_block, svg): - paths = [] +def generate_unique_id(document, prefix="path"): + doc_ids = {node.get('id') for node in document.iterdescendants() if 'id' in node.attrib} - for point_list in color_block_to_point_lists(color_block): - if not point_list: - continue + i = 1 + while True: + new_id = "%s%d" % (prefix, i) + if new_id not in doc_ids: + break + i += 1 - color = color_block.color.visible_on_white.darker.to_hex_str() - start = point_list[0] - for point in point_list[1:]: - paths.append(inkex.etree.Element( - SVG_PATH_TAG, - {'style': simplestyle.formatStyle( - { - 'fill': color, - 'stroke': 'none', - 'filter': 'url(#realistic-stitch-filter)' - }), - 'd': realistic_stitch(start, point), - 'transform': get_correction_transform(svg) - })) - start = point - - return paths - - -def color_block_to_paths(color_block, svg): - paths = [] - # We could emit just a single path with one subpath per point list, but - # emitting multiple paths makes it easier for the user to manipulate them. - for point_list in color_block_to_point_lists(color_block): - color = color_block.color.visible_on_white.to_hex_str() - paths.append(inkex.etree.Element( - SVG_PATH_TAG, - {'style': simplestyle.formatStyle( - {'stroke': color, - 'stroke-width': "0.4", - 'fill': 'none'}), - 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), - 'transform': get_correction_transform(svg), - 'embroider_manual_stitch': 'true', - 'embroider_trim_after': 'true', - })) - - # no need to trim at the end of a thread color - if paths: - paths[-1].attrib.pop('embroider_trim_after') - - return paths - - -def render_stitch_plan(svg, stitch_plan, realistic=False): - layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") - if layer is None: - layer = inkex.etree.Element(SVG_GROUP_TAG, - {'id': '__inkstitch_stitch_plan__', - INKSCAPE_LABEL: _('Stitch Plan'), - INKSCAPE_GROUPMODE: 'layer'}) - else: - # delete old stitch plan - del layer[:] - - # make sure the layer is visible - layer.set('style', 'display:inline') - - for i, color_block in enumerate(stitch_plan): - group = inkex.etree.SubElement(layer, - SVG_GROUP_TAG, - {'id': '__color_block_%d__' % i, - INKSCAPE_LABEL: "color block %d" % (i + 1)}) - if realistic: - group.extend(color_block_to_realistic_stitches(color_block, svg)) - else: - group.extend(color_block_to_paths(color_block, svg)) - - svg.append(layer) - - if realistic: - defs = svg.find(SVG_DEFS_TAG) - - if defs is None: - defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG) - - defs.append(inkex.etree.fromstring(realistic_filter)) + return new_id From 53a9bd6b31ca3a1f50d41f228e0b598a7d9da8ea Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 25 Feb 2019 19:49:38 -0500 Subject: [PATCH 05/24] add trims in stitches.auto_satin --- lib/extensions/auto_satin.py | 52 ++-------------- lib/stitches/auto_satin.py | 115 ++++++++++++++++++++++++++++++++--- lib/svg/svg.py | 5 +- 3 files changed, 116 insertions(+), 56 deletions(-) diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index 90d8fe33c..12588d1e5 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -38,22 +38,6 @@ class AutoSatin(CommandsExtension): if command is not None: return command.target_point - def effect(self): - if not self.check_selection(): - return - - if self.options.preserve_order: - # when preservering order, auto_satin() takes care of putting the - # newly-created elements into the existing group nodes in the SVG - # DOM - new_elements, trim_indices = self.auto_satin() - else: - group = self.create_group() - new_elements, trim_indices = self.auto_satin() - self.add_elements(group, new_elements) - - self.add_trims(new_elements, trim_indices) - def check_selection(self): if not self.get_elements(): return @@ -65,38 +49,10 @@ class AutoSatin(CommandsExtension): return True - def create_group(self): - first = self.elements[0].node - parent = first.getparent() - insert_index = parent.index(first) - group = inkex.etree.Element(SVG_GROUP_TAG, { - "transform": get_correction_transform(parent, child=True) - }) - parent.insert(insert_index, group) + def effect(self): + if not self.check_selection(): + return - return group - - def auto_satin(self): starting_point = self.get_starting_point() ending_point = self.get_ending_point() - return auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point) - - def add_elements(self, group, new_elements): - for i, element in enumerate(new_elements): - if isinstance(element, SatinColumn): - element.node.set("id", self.uniqueId("autosatin")) - - # L10N Label for a satin column created by Auto-Route Satin Columns extension - element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % (i + 1)) - else: - element.node.set("id", self.uniqueId("autosatinrun")) - - # L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns extension - element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % (i + 1)) - - group.append(element.node) - - def add_trims(self, new_elements, trim_indices): - if self.options.trim and trim_indices: - for i in trim_indices: - add_commands(new_elements[i], ["trim"]) + auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point, self.options.trim) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index e204a4456..aea26427b 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -9,9 +9,11 @@ import simplestyle import networkx as nx +from ..commands import add_commands from ..elements import Stroke, SatinColumn -from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform -from ..svg.tags import SVG_PATH_TAG +from ..i18n import _ +from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform, generate_unique_id +from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL from ..utils import Point as InkstitchPoint, cut, cache @@ -258,7 +260,7 @@ class RunningStitch(object): return RunningStitch(new_path, self.original_element) -def auto_satin(elements, preserve_order=False, starting_point=None, ending_point=None): +def auto_satin(elements, preserve_order=False, starting_point=None, ending_point=None, trim=False): """Find an optimal order to stitch a list of SatinColumns. Add running stitch and jump stitches as necessary to construct a stitch @@ -294,14 +296,20 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point If preserve_order is True, then the elements and any newly-created elements will be in the same position in the SVG DOM. If preserve_order is False, then - the elements will be removed from the SVG DOM and it's up to the caller to - decide where to put the returned SVG path nodes. + the elements will be removed from their current position in SVG DOM and added + to a newly-created group node. - Returns: a list of SVG path nodes making up the selected stitch order. + If trim is True, then Trim commands will be added to avoid jump stitches. + + Returns: a list of Element instances making up the stitching order chosen. Jumps between objects are implied if they are not right next to each other. """ + # save these for create_new_group() call below + parent = elements[0].node.getparent() + index = parent.index(elements[0].node) + graph = build_graph(elements, preserve_order) add_jumps(graph, elements, preserve_order) starting_node, ending_node = get_starting_and_ending_nodes( @@ -310,12 +318,21 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point operations = path_to_operations(graph, path) operations = collapse_sequential_segments(operations) new_elements, trims, original_parents = operations_to_elements_and_trims(operations, preserve_order) + remove_original_elements(elements) if preserve_order: preserve_original_groups(new_elements, original_parents) + else: + group = create_new_group(parent, index) + add_elements_to_group(new_elements, group) - return new_elements, trims + name_elements(new_elements, preserve_order) + + if trim: + new_elements = add_trims(new_elements, trims) + + return new_elements def build_graph(elements, preserve_order=False): @@ -628,3 +645,87 @@ def preserve_original_groups(elements, original_parent_nodes): if parent is not None: parent.append(element.node) element.node.set('transform', get_correction_transform(parent, child=True)) + + +def create_new_group(parent, insert_index): + group = inkex.etree.Element(SVG_GROUP_TAG, { + INKSCAPE_LABEL: _("Auto-Satin"), + "transform": get_correction_transform(parent, child=True) + }) + parent.insert(insert_index, group) + + return group + + +def add_elements_to_group(elements, group): + for element in elements: + group.append(element.node) + + +def name_elements(new_elements, preserve_order): + """Give the newly-created SVG objects useful names. + + Objects will be named like this: + + * AutoSatin 1 + * AutoSatin 2 + * AutoSatin Running Stitch 3 + * AutoSatin 4 + * AutoSatin Running Stitch 5 + ... + + Objects are numbered starting with 1. Satins are named "AutoSatin #", and + running stitches are named "AutoSatin Running Stitch #". + + If preserve_order is true and the element already has an INKSCAPE_LABEL, + we'll leave it alone. That way users can see which original satin the new + satin(s) came from. + + SVG element IDs are also set. Since these need to be unique across the + document, the numbers will likely not match up with the numbers in the + name we set. + """ + + index = 1 + for element in new_elements: + if isinstance(element, SatinColumn): + element.node.set("id", generate_unique_id(element.node, "autosatin")) + else: + element.node.set("id", generate_unique_id(element.node, "autosatinrun")) + + if not (preserve_order and INKSCAPE_LABEL in element.node.attrib): + if isinstance(element, SatinColumn): + # L10N Label for a satin column created by Auto-Route Satin Columns and Lettering extensions + element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % index) + else: + # L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns amd Lettering extensions + element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % index) + + index += 1 + + +def add_trims(elements, trim_indices): + """Add trim commands on the specified elements. + + If any running stitches immediately follow a trim, they are eliminated. + When we're trimming, there's no need to try to reduce the jump length, + so the running stitch would be a waste of time (and thread). + """ + + trim_indices = set(trim_indices) + new_elements = [] + just_trimmed = False + for i, element in enumerate(elements): + if just_trimmed and isinstance(element, Stroke): + element.node.getparent().remove(element.node) + continue + + if i in trim_indices: + add_commands(element, ["trim"]) + just_trimmed = True + else: + just_trimmed = False + + new_elements.append(element) + + return new_elements diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 0ec43f751..3715def8b 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,3 +1,5 @@ +from inkex import etree + from ..utils import cache @@ -6,7 +8,8 @@ def get_document(node): return node.getroottree().getroot() -def generate_unique_id(document, prefix="path"): +def generate_unique_id(document_or_element, prefix="path"): + document = get_document(document_or_element) doc_ids = {node.get('id') for node in document.iterdescendants() if 'id' in node.attrib} i = 1 From 602f201cb6236a7cb4a041b84e761aaedc358ab0 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 6 Mar 2019 20:32:51 -0500 Subject: [PATCH 06/24] implement trim option for lettering --- lib/commands.py | 7 ++++++- lib/extensions/auto_satin.py | 4 ---- lib/extensions/lettering.py | 10 ++++++---- lib/lettering/font.py | 19 +++++++++---------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index ddee83269..53b9e77f2 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -254,7 +254,12 @@ def symbol_defs(): @cache def get_defs(document): - return document.find(SVG_DEFS_TAG) + defs = document.find(SVG_DEFS_TAG) + + if defs is None: + defs = inkex.etree.SubElement(document, SVG_DEFS_TAG) + + return defs def ensure_symbol(document, command): diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index 12588d1e5..b7cee83bc 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -2,12 +2,8 @@ import sys import inkex -from ..commands import add_commands -from ..elements import SatinColumn from ..i18n import _ from ..stitches.auto_satin import auto_satin -from ..svg import get_correction_transform -from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_LABEL from .commands import CommandsExtension diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 60b4e1f30..3718b5ab7 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -37,11 +37,11 @@ class LetteringFrame(wx.Frame): self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) - self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) + self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) self.trim_checkbox = wx.CheckBox(self, label=_("Add trims")) self.trim_checkbox.SetValue(bool(self.settings.trim)) - self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) + self.trim_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) @@ -94,7 +94,8 @@ class LetteringFrame(wx.Frame): def update_lettering(self): font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) font = Font(font_path) - self.group[:] = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + del self.group[:] + font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) def generate_patches(self, abort_early=None): patches = [] @@ -155,7 +156,8 @@ class LetteringFrame(wx.Frame): outer_sizer = wx.BoxSizer(wx.VERTICAL) options_sizer = wx.StaticBoxSizer(self.options_box, wx.VERTICAL) - options_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 10) + options_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5) + options_sizer.Add(self.trim_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) text_editor_sizer = wx.StaticBoxSizer(self.text_editor_box, wx.VERTICAL) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 05465c8c4..28807cd69 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -81,7 +81,9 @@ class Font(object): kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) - def render_text(self, text, variant=None, back_and_forth=True, trim=False): + def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): + """Render text into an SVG group element.""" + if variant is None: variant = self.default_variant @@ -90,9 +92,6 @@ class Font(object): else: glyph_sets = [self.get_variant(variant)] * 2 - line_group = inkex.etree.Element(SVG_GROUP_TAG, { - INKSCAPE_LABEL: _("Ink/Stitch Text") - }) position = Point(0, 0) for i, line in enumerate(text.splitlines()): glyph_set = glyph_sets[i % 2] @@ -101,15 +100,15 @@ class Font(object): letter_group = self._render_line(line, position, glyph_set) if glyph_set.variant == FontVariant.RIGHT_TO_LEFT: letter_group[:] = reversed(letter_group) - line_group.append(letter_group) + destination_group.append(letter_group) position.x = 0 position.y += self.leading - if self.auto_satin and len(line_group) > 0: - self._apply_auto_satin(line_group) + if self.auto_satin and len(destination_group) > 0: + self._apply_auto_satin(destination_group, trim) - return line_group + return destination_group def get_variant(self, variant): return self.variants.get(variant, self.variants[self.default_variant]) @@ -174,7 +173,7 @@ class Font(object): return node - def _apply_auto_satin(self, group): + def _apply_auto_satin(self, group, trim): """Apply Auto-Satin to an SVG XML node tree with an svg:g at its root. The group's contents will be replaced with the results of the auto- @@ -182,4 +181,4 @@ class Font(object): """ elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG)) - auto_satin(elements, preserve_order=True) + auto_satin(elements, preserve_order=True, trim=trim) From a14ed903cf6043e4138d7d3d59a5cac9750cb191 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 19:59:28 -0500 Subject: [PATCH 07/24] auto satin should trim at the end too --- lib/stitches/auto_satin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index aea26427b..7c09b0231 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -728,4 +728,8 @@ def add_trims(elements, trim_indices): new_elements.append(element) + # trim at the end, too + if i not in trim_indices: + add_commands(element, ["trim"]) + return new_elements From fb3c8186d275afa18c8146a453654afaf879ed34 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 20:06:36 -0500 Subject: [PATCH 08/24] lower trim threshold to 0.75mm --- lib/stitches/auto_satin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index 7c09b0231..75b131761 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -613,7 +613,7 @@ def operations_to_elements_and_trims(operations, preserve_order): elements.append(operation.to_element()) original_parent_nodes.append(operation.original_node.getparent()) elif isinstance(operation, (JumpStitch)): - if elements and operation.length > PIXELS_PER_MM: + if elements and operation.length > 0.75 * PIXELS_PER_MM: trims.append(len(elements) - 1) return elements, list(set(trims)), original_parent_nodes From d1c001857d1e389950b3f346c1a0413d82899ca4 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 20:50:48 -0500 Subject: [PATCH 09/24] get root properly --- lib/svg/svg.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 3715def8b..464a2a18a 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -9,7 +9,11 @@ def get_document(node): def generate_unique_id(document_or_element, prefix="path"): - document = get_document(document_or_element) + if isinstance(document_or_element, etree._ElementTree): + document = document_or_element.getroot() + else: + document = get_document(document_or_element) + doc_ids = {node.get('id') for node in document.iterdescendants() if 'id' in node.attrib} i = 1 From 13b6c67644acaeff04918e8489e943b4a8795863 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 20:51:23 -0500 Subject: [PATCH 10/24] less haphazard positioning for commands --- lib/commands.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index 53b9e77f2..8e35d7ee0 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -5,6 +5,7 @@ import sys import cubicsuperpath import inkex +from shapely import geometry as shgeo import simpletransform from .i18n import _, N_ @@ -325,10 +326,15 @@ def get_command_pos(element, index, total): # get a line running 30 pixels out from the shape outline = element.shape.buffer(30).exterior - # pick this item's spot arond the outline and perturb it a bit to avoid - # stacking up commands if they run the extension multiple times + # find the top center point on the outline and start there + top_center = shgeo.Point(outline.centroid.x, outline.bounds[1]) + start_position = outline.project(top_center, normalized=True) + + # pick this item's spot around the outline and perturb it a bit to avoid + # stacking up commands if they add commands multiple times position = index / float(total) - position += random() * 0.1 + position += random() * 0.05 + position += start_position return outline.interpolate(position, normalized=True) From 55505369496c0986e54fe5722e7e8ddce0a9294e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 25 Mar 2019 19:40:37 -0400 Subject: [PATCH 11/24] implement font chooser --- lib/extensions/lettering.py | 69 +++++++++++++++++++++++++++++++------ lib/lettering/__init__.py | 2 +- lib/lettering/font.py | 12 +++++++ 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 3718b5ab7..a33277df9 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -5,20 +5,23 @@ import json import os import sys +import appdirs import inkex import wx from ..elements import nodes_to_elements -from ..gui import PresetsPanel, SimulatorPreview +from ..gui import PresetsPanel, SimulatorPreview, info_dialog from ..i18n import _ -from ..lettering import Font +from ..lettering import Font, FontError from ..svg import get_correction_transform from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL, INKSTITCH_LETTERING -from ..utils import get_bundled_dir, DotDict +from ..utils import get_bundled_dir, DotDict, cache from .commands import CommandsExtension class LetteringFrame(wx.Frame): + DEFAULT_FONT = "small_font" + def __init__(self, *args, **kwargs): # begin wxGlade: MyFrame.__init__ self.group = kwargs.pop('group') @@ -46,8 +49,9 @@ class LetteringFrame(wx.Frame): # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) - self.font_chooser = wx.ComboBox(self, wx.ID_ANY) + self.font_chooser = wx.ComboBox(self, wx.ID_ANY, style=wx.CB_READONLY) self.update_font_list() + self.set_initial_font(self.settings.font) self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP, value=self.settings.text) self.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) @@ -73,7 +77,7 @@ class LetteringFrame(wx.Frame): self.settings = DotDict({ "text": u"", "back_and_forth": True, - "font": "small_font" + "font": None }) def save_settings(self): @@ -87,13 +91,61 @@ class LetteringFrame(wx.Frame): # https://bugs.launchpad.net/inkscape/+bug/1804346 self.group.set(INKSTITCH_LETTERING, b64encode(json.dumps(self.settings))) + def update_font_list(self): + font_paths = { + get_bundled_dir("fonts"), + os.path.expanduser("~/.inkstitch/fonts"), + os.path.join(appdirs.user_config_dir('inkstitch'), 'fonts'), + } + + self.fonts = {} + self.fonts_by_id = {} + + for font_path in font_paths: + try: + font_dirs = os.listdir(font_path) + except OSError: + continue + + try: + for font_dir in font_dirs: + font = Font(os.path.join(font_path, font_dir)) + self.fonts[font.name] = font + self.fonts_by_id[font.id] = font + except FontError: + pass + + self.font_chooser.SetItems(sorted(self.fonts)) + + if len(self.fonts) == 0: + info_dialog(self, _("Unable to find any fonts! Please try reinstalling Ink/Stitch.")) + self.cancel() + + def set_initial_font(self, font_id): + if font_id is not None: + if font_id not in self.fonts_by_id: + info_dialog(self, _( + '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) + + try: + self.font_chooser.SetValue(self.fonts_by_id[font_id].name) + except KeyError: + self.font_chooser.SetValue(self.default_font.name) + + @property + @cache + def default_font(self): + try: + return self.fonts[self.DEFAULT_FONT] + except KeyError: + return self.fonts.values()[0] + def on_change(self, attribute, event): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() def update_lettering(self): - font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) - font = Font(font_path) + font = self.fonts_by_id.get(self.settings.font, self.default_font) del self.group[:] font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) @@ -120,9 +172,6 @@ class LetteringFrame(wx.Frame): return patches - def update_font_list(self): - pass - def get_preset_data(self): # called by self.presets_panel preset = {} diff --git a/lib/lettering/__init__.py b/lib/lettering/__init__.py index c62012236..5d20d6837 100644 --- a/lib/lettering/__init__.py +++ b/lib/lettering/__init__.py @@ -1 +1 @@ -from font import Font \ No newline at end of file +from font import Font, FontError \ No newline at end of file diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 28807cd69..6f749d289 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -7,6 +7,7 @@ import os import inkex from ..elements import nodes_to_elements +from ..exceptions import InkstitchException from ..i18n import _ from ..stitches.auto_satin import auto_satin from ..svg import PIXELS_PER_MM @@ -15,6 +16,10 @@ from ..utils import Point from .font_variant import FontVariant +class FontError(InkstitchException): + pass + + def font_metadata(name, default=None, multiplier=None): def getter(self): value = self.metadata.get(name, default) @@ -47,6 +52,9 @@ class Font(object): self._load_license() self._load_variants() + if self.variants.get(self.default_variant) is None: + raise FontError("font not found or has no default variant") + def _load_metadata(self): try: with open(os.path.join(self.path, "font.json")) as metadata_file: @@ -81,6 +89,10 @@ class Font(object): kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) + @property + def id(self): + return os.path.basename(self.path) + def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): """Render text into an SVG group element.""" From a9cf553066c3fd5b907593751bb00f77f32ce86a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 2 Apr 2019 22:36:54 -0400 Subject: [PATCH 12/24] add font description to font selector dropdown --- lib/extensions/lettering.py | 31 +++++++++---- lib/gui/__init__.py | 1 + lib/gui/subtitle_combo_box.py | 85 +++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 lib/gui/subtitle_combo_box.py diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index a33277df9..74d036cf5 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -10,7 +10,7 @@ import inkex import wx from ..elements import nodes_to_elements -from ..gui import PresetsPanel, SimulatorPreview, info_dialog +from ..gui import PresetsPanel, SimulatorPreview, info_dialog, SubtitleComboBox from ..i18n import _ from ..lettering import Font, FontError from ..svg import get_correction_transform @@ -49,8 +49,10 @@ class LetteringFrame(wx.Frame): # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) - self.font_chooser = wx.ComboBox(self, wx.ID_ANY, style=wx.CB_READONLY) self.update_font_list() + self.font_chooser = SubtitleComboBox(self, wx.ID_ANY, choices=self.get_font_names(), + subtitles=self.get_font_descriptions(), style=wx.CB_READONLY) + self.font_chooser.Bind(wx.EVT_COMBOBOX, self.update_preview) self.set_initial_font(self.settings.font) self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP, value=self.settings.text) @@ -115,12 +117,19 @@ class LetteringFrame(wx.Frame): except FontError: pass - self.font_chooser.SetItems(sorted(self.fonts)) - if len(self.fonts) == 0: info_dialog(self, _("Unable to find any fonts! Please try reinstalling Ink/Stitch.")) self.cancel() + def get_font_names(self): + font_names = [font.name for font in self.fonts.itervalues()] + font_names.sort() + + return font_names + + def get_font_descriptions(self): + return {font.name: font.description for font in self.fonts.itervalues()} + def set_initial_font(self, font_id): if font_id is not None: if font_id not in self.fonts_by_id: @@ -128,9 +137,9 @@ class LetteringFrame(wx.Frame): '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) try: - self.font_chooser.SetValue(self.fonts_by_id[font_id].name) + self.font_chooser.SetValueByUser(self.fonts_by_id[font_id].name) except KeyError: - self.font_chooser.SetValue(self.default_font.name) + self.font_chooser.SetValueByUser(self.default_font.name) @property @cache @@ -144,8 +153,11 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() + def update_preview(self, event=None): + self.preview.update() + def update_lettering(self): - font = self.fonts_by_id.get(self.settings.font, self.default_font) + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) del self.group[:] font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) @@ -209,8 +221,11 @@ class LetteringFrame(wx.Frame): options_sizer.Add(self.trim_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + font_chooser_sizer = wx.BoxSizer(wx.VERTICAL) + font_chooser_sizer.Add(self.font_chooser, 0, wx.ALL | wx.EXPAND, 10) + text_editor_sizer = wx.StaticBoxSizer(self.text_editor_box, wx.VERTICAL) - text_editor_sizer.Add(self.font_chooser, 0, wx.ALL, 10) + text_editor_sizer.Add(font_chooser_sizer, 0, wx.RIGHT | wx.EXPAND, 100) text_editor_sizer.Add(self.text_editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) outer_sizer.Add(text_editor_sizer, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py index 060c3d938..51890cf94 100644 --- a/lib/gui/__init__.py +++ b/lib/gui/__init__.py @@ -1,3 +1,4 @@ from dialogs import info_dialog, confirm_dialog from presets import PresetsPanel from simulator import EmbroiderySimulator, SimulatorPreview, show_simulator +from subtitle_combo_box import SubtitleComboBox diff --git a/lib/gui/subtitle_combo_box.py b/lib/gui/subtitle_combo_box.py new file mode 100644 index 000000000..64c42153d --- /dev/null +++ b/lib/gui/subtitle_combo_box.py @@ -0,0 +1,85 @@ +import wx +import wx.adv +from wx.lib.wordwrap import wordwrap + + +class SubtitleComboBox(wx.adv.OwnerDrawnComboBox): + TITLE_FONT_SIZE = 12 + SUBTITLE_FONT_SIZE = 10 + + # I'd love to make this 12 too, but if I do it seems to get drawn as 10 + # initially no matter what I do. + CONTROL_FONT_SIZE = 12 + + MARGIN = 5 + + def __init__(self, *args, **kwargs): + self.titles = kwargs.get('choices', []) + subtitles = kwargs.pop('subtitles', {}) + self.subtitles = [subtitles.get(title, '') for title in self.titles] + wx.adv.OwnerDrawnComboBox.__init__(self, *args, **kwargs) + + self.control_font = wx.Font(pointSize=self.CONTROL_FONT_SIZE, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL) + self.title_font = wx.Font(pointSize=self.TITLE_FONT_SIZE, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL) + self.subtitle_font = wx.Font(pointSize=self.SUBTITLE_FONT_SIZE, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL) + + def OnMeasureItemWidth(self, item): + # This _should_ allow us to set the width of the combobox to match the + # width of the widest title. In reality, this method is never called + # and I can't figure out why. We just use self.GetSize().GetWidth() + # instead and rely on the parent window to size us appropriately. Ugh. + + title = self.titles[item] + + # technique from https://stackoverflow.com/a/23529463/4249120 + dc = wx.ScreenDC() + dc.SetFont(self.title_font) + + return dc.GetTextExtent(title).GetWidth() + 2 * self.MARGIN + + def OnMeasureItem(self, item): + title = self.titles[item] + subtitle = self.subtitles[item] + + dc = wx.ScreenDC() + dc.SetFont(self.subtitle_font) + wrapped = wordwrap(subtitle, self.GetSize().GetWidth(), dc) + subtitle_height = dc.GetTextExtent(wrapped).GetHeight() + + dc = wx.ScreenDC() + dc.SetFont(self.title_font) + title_height = dc.GetTextExtent(title).GetHeight() + + return subtitle_height + title_height + 3 * self.MARGIN + + def OnDrawBackground(self, dc, rect, item, flags): + if flags & wx.adv.ODCB_PAINTING_SELECTED: + # let the parent class draw the selected item so we don't + # hae to figure out the highlight color + wx.adv.OwnerDrawnComboBox.OnDrawBackground(self, dc, rect, item, flags) + else: + # alternate white and grey for the dropdown items, and draw the + # combo box itself as white + if flags & wx.adv.ODCB_PAINTING_CONTROL or item % 2 == 0: + background_color = wx.Colour(255, 255, 255) + else: + background_color = wx.Colour(240, 240, 240) + + dc.SetBrush(wx.Brush(background_color)) + dc.SetPen(wx.Pen(background_color)) + dc.DrawRectangle(rect) + + def OnDrawItem(self, dc, rect, item, flags): + if flags & wx.adv.ODCB_PAINTING_CONTROL: + # painting the selected item in the box + dc.SetFont(self.control_font) + dc.DrawText(self.titles[item], rect.x + self.MARGIN, rect.y + self.MARGIN) + else: + # painting the items in the popup + dc.SetFont(self.title_font) + title_height = dc.GetCharHeight() + dc.DrawText(self.titles[item], rect.x + self.MARGIN, rect.y + self.MARGIN) + + dc.SetFont(self.subtitle_font) + subtitle = wordwrap(self.subtitles[item], self.GetSize().GetWidth(), dc) + dc.DrawText(subtitle, rect.x + self.MARGIN, rect.y + title_height + self.MARGIN * 2) From a6a86973dd54d623394fdbfc61e6e4c0ca263cc0 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 2 Apr 2019 23:07:38 -0400 Subject: [PATCH 13/24] add localization for font names and descriptions --- Makefile | 1 + bin/inkstitch-fonts-gettext | 19 +++++++++++++++++++ lib/i18n.py | 28 ++++++++++++++++++++++++++-- lib/lettering/font.py | 25 ++++++++++++++++++++++--- 4 files changed, 68 insertions(+), 5 deletions(-) create mode 100755 bin/inkstitch-fonts-gettext diff --git a/Makefile b/Makefile index ed86a36ce..5635547e2 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ inx: locales messages.po: rm -f messages.po bin/pyembroidery-gettext > pyembroidery-format-descriptions.py + bin/inkstitch-fonts-gettext > inkstitch-fonts-metadata.py pybabel extract -o messages.po -F babel.conf --add-location=full --add-comments=l10n,L10n,L10N --sort-by-file --strip-comments -k N_ . rm pyembroidery-format-descriptions.py diff --git a/bin/inkstitch-fonts-gettext b/bin/inkstitch-fonts-gettext new file mode 100755 index 000000000..8a3aa7289 --- /dev/null +++ b/bin/inkstitch-fonts-gettext @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import os +import json + +# generate fake python code containing the names and descriptions of all built- +# in fonts as gettext calls so that pybabel will extract them into messages.po + +fonts_dir = os.path.join(os.path.dirname(__file__), "..", "fonts") + +for font in os.listdir(fonts_dir): + with open(os.path.join(fonts_dir, font, "font.json")) as font_json: + font_metadata = json.load(font_json) + + print "# L10N name of font in fonts/%s" % font + print "_(%s)" % repr(font_metadata.get("name", "")) + + print "# L10N description of font in fonts/%s" % font + print "_(%s)" % repr(font_metadata.get("description", "")) \ No newline at end of file diff --git a/lib/i18n.py b/lib/i18n.py index 98f63ec17..f57bbf9c7 100644 --- a/lib/i18n.py +++ b/lib/i18n.py @@ -1,7 +1,9 @@ -import sys +import gettext import os from os.path import dirname, realpath -import gettext +import sys + +from .utils import cache _ = translation = None locale_dir = None @@ -33,5 +35,27 @@ def localize(languages=None): _ = translation.ugettext +@cache +def get_languages(): + """return a list of languages configured by the user + + I really wish gettext provided this as a function. Instead, we've duplicated + its code below. + """ + + languages = [] + + for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): + val = os.environ.get(envar) + if val: + languages = val.split(':') + break + + if 'C' not in languages: + languages.append('C') + + return languages + + _set_locale_dir() localize() diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 6f749d289..46e2648dd 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -8,7 +8,7 @@ import inkex from ..elements import nodes_to_elements from ..exceptions import InkstitchException -from ..i18n import _ +from ..i18n import _, get_languages from ..stitches.auto_satin import auto_satin from ..svg import PIXELS_PER_MM from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG, INKSCAPE_LABEL @@ -32,6 +32,25 @@ def font_metadata(name, default=None, multiplier=None): return property(getter) +def localized_font_metadata(name, default=None): + def getter(self): + # If the font contains a localized version of the attribute, use it. + for language in get_languages(): + attr = "%s_%s" % (name, language) + if attr in self.metadata: + return self.metadata.get(attr) + + if name in self.metadata: + # This may be a font packaged with Ink/Stitch, in which case the + # text will have been sent to CrowdIn for community translation. + # Try to fetch the translated version. + return _(self.metadata.get(name)) + else: + return default + + return property(getter) + + class Font(object): """Represents a font with multiple variants. @@ -79,8 +98,8 @@ class Font(object): # we'll deal with missing variants when we apply lettering pass - name = font_metadata('name', '') - description = font_metadata('description', '') + name = localized_font_metadata('name', '') + description = localized_font_metadata('description', '') default_variant = font_metadata('default_variant', FontVariant.LEFT_TO_RIGHT) default_glyph = font_metadata('defalt_glyph', u"�") letter_spacing = font_metadata('letter_spacing', 1.5, multiplier=PIXELS_PER_MM) From 6c5e57d39c9a72a9a56acc3b7ffe93e67f38ecb1 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 2 Apr 2019 23:39:54 -0400 Subject: [PATCH 14/24] implement lettering presets --- lib/extensions/lettering.py | 36 +++++++++++++++++++++++------------- lib/gui/presets.py | 11 ++++++----- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 74d036cf5..9193a723e 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -33,17 +33,13 @@ class LetteringFrame(wx.Frame): self.preview = SimulatorPreview(self, target_duration=1) self.presets_panel = PresetsPanel(self) - self.load_settings() - # options self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) - self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) self.trim_checkbox = wx.CheckBox(self, label=_("Add trims")) - self.trim_checkbox.SetValue(bool(self.settings.trim)) self.trim_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) # text editor @@ -53,10 +49,9 @@ class LetteringFrame(wx.Frame): self.font_chooser = SubtitleComboBox(self, wx.ID_ANY, choices=self.get_font_names(), subtitles=self.get_font_descriptions(), style=wx.CB_READONLY) self.font_chooser.Bind(wx.EVT_COMBOBOX, self.update_preview) - self.set_initial_font(self.settings.font) - self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP, value=self.settings.text) - self.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) + self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) + self.text_editor.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) @@ -66,9 +61,12 @@ class LetteringFrame(wx.Frame): self.apply_button.Bind(wx.EVT_BUTTON, self.apply) self.__do_layout() - # end wxGlade + + self.load_settings() + self.apply_settings() def load_settings(self): + """Load the settings saved into the SVG group element""" try: if INKSTITCH_LETTERING in self.group.attrib: self.settings = DotDict(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) @@ -82,7 +80,16 @@ class LetteringFrame(wx.Frame): "font": None }) + def apply_settings(self): + """Make the settings in self.settings visible in the UI.""" + self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) + self.trim_checkbox.SetValue(bool(self.settings.trim)) + self.set_initial_font(self.settings.font) + self.text_editor.SetValue(self.settings.text) + def save_settings(self): + """Save the settings into the SVG group element.""" + # We base64 encode the string before storing it in an XML attribute. # In theory, lxml should properly html-encode the string, using HTML # entities like as necessary. However, we've found that Inkscape @@ -186,12 +193,15 @@ class LetteringFrame(wx.Frame): def get_preset_data(self): # called by self.presets_panel - preset = {} - return preset + settings = dict(self.settings) + del settings["text"] + return settings - def apply_preset_data(self): - # called by self.presets_panel - return + def apply_preset_data(self, preset_data): + settings = DotDict(preset_data) + settings["text"] = self.settings.text + self.settings = settings + self.apply_settings() def get_preset_suite_name(self): # called by self.presets_panel diff --git a/lib/gui/presets.py b/lib/gui/presets.py index 5337d8792..bd0b17878 100644 --- a/lib/gui/presets.py +++ b/lib/gui/presets.py @@ -63,11 +63,12 @@ class PresetsPanel(wx.Panel): self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset) presets_sizer = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL) - presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + self.preset_chooser.SetMinSize((200, -1)) + presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.BOTTOM | wx.EXPAND, 10) + presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) + presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) + presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) + presets_sizer.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT | wx.RIGHT, 10) self.SetSizerAndFit(presets_sizer) self.Layout() From 98e59f255039911a4c1cc009325f7b30839cafdd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 10 Apr 2019 20:23:11 -0400 Subject: [PATCH 15/24] add support for scaling text --- fonts/small_font/font.json | 4 +++- lib/extensions/lettering.py | 32 ++++++++++++++++++++++++++------ lib/lettering/font.py | 2 ++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/fonts/small_font/font.json b/fonts/small_font/font.json index a89925e74..7732a7a04 100644 --- a/fonts/small_font/font.json +++ b/fonts/small_font/font.json @@ -1,11 +1,13 @@ { "name": "Ink/Stitch Small Font", - "description": "A font suited for small characters. The capital em is 0.2 inches wide.", + "description": "A font suited for small characters. The capital em is 0.2 inches wide at 100% scale. Can be scaled up to 300%.", "leading": 8, "letter_spacing": 1.5, "word_spacing": 4.5, "auto_satin": true, "default_glyph": "�", + "min_scale": 1.0, + "max_scale": 3.0, "kerning_pairs": { "wo": -0.3 } diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 9193a723e..428bb0ca9 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -48,7 +48,10 @@ class LetteringFrame(wx.Frame): self.update_font_list() self.font_chooser = SubtitleComboBox(self, wx.ID_ANY, choices=self.get_font_names(), subtitles=self.get_font_descriptions(), style=wx.CB_READONLY) - self.font_chooser.Bind(wx.EVT_COMBOBOX, self.update_preview) + self.font_chooser.Bind(wx.EVT_COMBOBOX, self.on_font_changed) + + self.scale_spinner = wx.SpinCtrl(self, wx.ID_ANY, min=100, max=100, initial=100) + self.scale_spinner.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("scale", event)) self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) self.text_editor.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) @@ -77,7 +80,8 @@ class LetteringFrame(wx.Frame): self.settings = DotDict({ "text": u"", "back_and_forth": True, - "font": None + "font": None, + "scale": 100 }) def apply_settings(self): @@ -86,6 +90,7 @@ class LetteringFrame(wx.Frame): self.trim_checkbox.SetValue(bool(self.settings.trim)) self.set_initial_font(self.settings.font) self.text_editor.SetValue(self.settings.text) + self.scale_spinner.SetValue(self.settings.scale) def save_settings(self): """Save the settings into the SVG group element.""" @@ -148,6 +153,8 @@ class LetteringFrame(wx.Frame): except KeyError: self.font_chooser.SetValueByUser(self.default_font.name) + self.on_font_changed() + @property @cache def default_font(self): @@ -160,14 +167,24 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() + def on_font_changed(self, event=None): + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) + self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) + self.update_preview() + def update_preview(self, event=None): self.preview.update() def update_lettering(self): - font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) del self.group[:] + self.group.attrib.pop('transform', None) + + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + if self.settings.scale != 100: + self.group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) + def generate_patches(self, abort_early=None): patches = [] @@ -231,11 +248,14 @@ class LetteringFrame(wx.Frame): options_sizer.Add(self.trim_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - font_chooser_sizer = wx.BoxSizer(wx.VERTICAL) - font_chooser_sizer.Add(self.font_chooser, 0, wx.ALL | wx.EXPAND, 10) + font_sizer = wx.BoxSizer(wx.HORIZONTAL) + font_sizer.Add(self.font_chooser, 1, wx.EXPAND, 0) + font_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Scale"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 20) + font_sizer.Add(self.scale_spinner, 0, wx.LEFT, 10) + font_sizer.Add(wx.StaticText(self, wx.ID_ANY, "%"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 3) text_editor_sizer = wx.StaticBoxSizer(self.text_editor_box, wx.VERTICAL) - text_editor_sizer.Add(font_chooser_sizer, 0, wx.RIGHT | wx.EXPAND, 100) + text_editor_sizer.Add(font_sizer, 0, wx.ALL | wx.EXPAND, 10) text_editor_sizer.Add(self.text_editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) outer_sizer.Add(text_editor_sizer, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 46e2648dd..883821ec9 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -107,6 +107,8 @@ class Font(object): word_spacing = font_metadata('word_spacing', 3, multiplier=PIXELS_PER_MM) kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) + min_scale = font_metadata('min_scale', 1.0) + max_scale = font_metadata('max_scale', 1.0) @property def id(self): From 313cd44483bf216c123e19dfb3dd294eb57a9c3d Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 15 Apr 2019 20:26:30 -0400 Subject: [PATCH 16/24] don't overwrite user's positioning of text when re-editing --- lib/extensions/lettering.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 428bb0ca9..e6b828a8e 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -177,13 +177,22 @@ class LetteringFrame(wx.Frame): def update_lettering(self): del self.group[:] - self.group.attrib.pop('transform', None) + + if self.settings.scale == 100: + destination_group = self.group + else: + destination_group = inkex.etree.SubElement(self.group, SVG_GROUP_TAG, { + # L10N The user has chosen to scale the text by some percentage + # (50%, 200%, etc). If you need to use the percentage symbol, + # make sure to double it (%%). + INKSCAPE_LABEL: _("Text scale %s%%") % self.settings.scale + }) font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) - font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) if self.settings.scale != 100: - self.group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) + destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) def generate_patches(self, abort_early=None): patches = [] From 30d80ab41bfe19b5bd9d71a903c5e796266a849b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 16 Apr 2019 21:01:25 -0400 Subject: [PATCH 17/24] add scale bar to simulator for comparison --- lib/gui/simulator.py | 55 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index e0d78983c..c07a7af3b 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -356,14 +356,51 @@ class DrawingPanel(wx.Panel): self.last_frame_duration = time.time() - start if last_stitch: - x = last_stitch[0] - y = last_stitch[1] - x, y = transform.TransformPoint(float(x), float(y)) - canvas.SetTransform(canvas.CreateMatrix()) - crosshair_radius = 10 - canvas.SetPen(self.black_pen) - canvas.DrawLines(((x - crosshair_radius, y), (x + crosshair_radius, y))) - canvas.DrawLines(((x, y - crosshair_radius), (x, y + crosshair_radius))) + self.draw_crosshair(last_stitch[0], last_stitch[1], canvas, transform) + + self.draw_scale(canvas) + + def draw_crosshair(self, x, y, canvas, transform): + x, y = transform.TransformPoint(float(x), float(y)) + canvas.SetTransform(canvas.CreateMatrix()) + crosshair_radius = 10 + canvas.SetPen(self.black_pen) + canvas.DrawLines(((x - crosshair_radius, y), (x + crosshair_radius, y))) + canvas.DrawLines(((x, y - crosshair_radius), (x, y + crosshair_radius))) + + def draw_scale(self, canvas): + canvas_width, canvas_height = self.GetClientSize() + + one_mm = PIXELS_PER_MM * self.zoom + scale_width = one_mm + max_width = min(canvas_width * 0.5, 300) + + while scale_width > max_width: + scale_width /= 2.0 + + while scale_width < 50: + scale_width += one_mm + + scale_width_mm = scale_width / self.zoom / PIXELS_PER_MM + + # The scale bar looks like this: + # + # | | + # |_____|_____| + + scale_lower_left_x = 20 + scale_lower_left_y = canvas_height - 20 + + canvas.DrawLines(((scale_lower_left_x, scale_lower_left_y - 6), + (scale_lower_left_x, scale_lower_left_y), + (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y), + (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y - 3), + (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y), + (scale_lower_left_x + scale_width, scale_lower_left_y), + (scale_lower_left_x + scale_width, scale_lower_left_y - 5))) + + canvas.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL), wx.Colour((0, 0, 0))) + canvas.DrawText("%s mm" % scale_width_mm, scale_lower_left_x, scale_lower_left_y + 5) def clear(self): dc = wx.ClientDC(self) @@ -520,7 +557,7 @@ class DrawingPanel(wx.Panel): # If we just change the zoom, the design will appear to move on the # screen. We have to adjust the pan to compensate. We want to keep # the part of the design under the mouse pointer in the same spot - # after we zoom, so that we appar to be zooming centered on the + # after we zoom, so that we appear to be zooming centered on the # mouse pointer. # This will create a matrix that takes a point in the design and From e0c12fbbaa4e05f116a3436617260c6e2d1d2781 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 18 Apr 2019 11:34:38 -0400 Subject: [PATCH 18/24] add medium font --- fonts/medium_font/LICENSE | 94 ++++++++ fonts/medium_font/README_en.md | 24 ++ fonts/medium_font/font.json | 14 ++ fonts/medium_font/←.svg | 392 ++++++++++++++++++++++++++++++++ fonts/medium_font/↑.svg | 389 ++++++++++++++++++++++++++++++++ fonts/medium_font/→.svg | 393 +++++++++++++++++++++++++++++++++ fonts/medium_font/↓.svg | 389 ++++++++++++++++++++++++++++++++ 7 files changed, 1695 insertions(+) create mode 100644 fonts/medium_font/LICENSE create mode 100644 fonts/medium_font/README_en.md create mode 100644 fonts/medium_font/font.json create mode 100644 fonts/medium_font/←.svg create mode 100644 fonts/medium_font/↑.svg create mode 100644 fonts/medium_font/→.svg create mode 100644 fonts/medium_font/↓.svg diff --git a/fonts/medium_font/LICENSE b/fonts/medium_font/LICENSE new file mode 100644 index 000000000..10ea73a81 --- /dev/null +++ b/fonts/medium_font/LICENSE @@ -0,0 +1,94 @@ +Copyright (c) 2017, Lex Neva. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + diff --git a/fonts/medium_font/README_en.md b/fonts/medium_font/README_en.md new file mode 100644 index 000000000..129d3fb86 --- /dev/null +++ b/fonts/medium_font/README_en.md @@ -0,0 +1,24 @@ +Small Font +========== + +This is a small font for use in machine embroidery. It includes capital and lowercase letters, numbers, and the following punctuation: `< > = - + , : ( ) @ . _ ' " “ ”` + +Currently one size is available, 0.2 inches, as measured by the width of the capital letter "M". + +You may use this font for free in any embroidery project, including in designs that you intend to sell or sew out and sell. There is no limitation on the number of designs or items you may sell. For full details, check the LICENSE file in the top directory. + +I've taken pains to make this font sew out nicely even though it's tiny. Every character is properly underlayed using a center-walk stitch. Even still, your choice of fabric, needle, and stabilizer will definitely affect your end results! Make sure you test it out before sewing onto a garment or anything else that you really care about. + +If you can see areas for improvement, I'd like to know. Just open a github issue in this repo and we can talk through your ideas. + +File Structure +-------------- + +This font is designed for Ink/Stitch and uses its standard font folder layout. The folder contains four files: + +* `→.svg` contains glyphs designed for words laid out horizontally and stitched left-to-right +* `←.svg` contains glyphs designed for words laid out horizontally and stitched right-to-left +* `↓.svg` contains glyphs designed for words laid out vertically and stitched top-to-bottom +* `↑.svg` contains glyphs designed for words laid out vertically and stitched bottom-to-top + +The files are named as they are so as to (hopefully) be recognizable to native speakers of multiple languages. diff --git a/fonts/medium_font/font.json b/fonts/medium_font/font.json new file mode 100644 index 000000000..63eba7184 --- /dev/null +++ b/fonts/medium_font/font.json @@ -0,0 +1,14 @@ +{ + "name": "Ink/Stitch Medium Font", + "description": "A basic font suited for medium-sized characters. The capital em is 0.6 inches wide at 100% scale. Can be scaled down to 75% or up to 150%. Every satin has contour underlay.", + "leading": 24, + "letter_spacing": 4.5, + "word_spacing": 13.5, + "auto_satin": true, + "default_glyph": "�", + "min_scale": 0.75, + "max_scale": 2.0, + "kerning_pairs": { + "wo": -0.3 + } +} diff --git a/fonts/medium_font/←.svg b/fonts/medium_font/←.svg new file mode 100644 index 000000000..3a2c793fb --- /dev/null +++ b/fonts/medium_font/←.svg @@ -0,0 +1,392 @@ + + + + + + + + + + + Satin Column cut point + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/medium_font/↑.svg b/fonts/medium_font/↑.svg new file mode 100644 index 000000000..30c6c02ad --- /dev/null +++ b/fonts/medium_font/↑.svg @@ -0,0 +1,389 @@ + + + + + + + + + + + Satin Column cut point + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/medium_font/→.svg b/fonts/medium_font/→.svg new file mode 100644 index 000000000..38fe99a05 --- /dev/null +++ b/fonts/medium_font/→.svg @@ -0,0 +1,393 @@ + + + + + + + + + + + Satin Column cut point + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/medium_font/↓.svg b/fonts/medium_font/↓.svg new file mode 100644 index 000000000..30ba1887b --- /dev/null +++ b/fonts/medium_font/↓.svg @@ -0,0 +1,389 @@ + + + + + + + + + + + Satin Column cut point + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From fee43e0941b11c709d7ae23fcd4bee4cd89d2a55 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 18 Apr 2019 11:35:29 -0400 Subject: [PATCH 19/24] fix parameter management --- lib/extensions/lettering.py | 18 ++++++++++-------- lib/utils/dotdict.py | 6 ++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index e6b828a8e..174354923 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -70,12 +70,6 @@ class LetteringFrame(wx.Frame): def load_settings(self): """Load the settings saved into the SVG group element""" - try: - if INKSTITCH_LETTERING in self.group.attrib: - self.settings = DotDict(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) - return - except (TypeError, ValueError): - pass self.settings = DotDict({ "text": u"", @@ -84,9 +78,16 @@ class LetteringFrame(wx.Frame): "scale": 100 }) + try: + if INKSTITCH_LETTERING in self.group.attrib: + self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + return + except (TypeError, ValueError): + pass + def apply_settings(self): """Make the settings in self.settings visible in the UI.""" - self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) + self.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) self.trim_checkbox.SetValue(bool(self.settings.trim)) self.set_initial_font(self.settings.font) self.text_editor.SetValue(self.settings.text) @@ -143,7 +144,7 @@ class LetteringFrame(wx.Frame): return {font.name: font.description for font in self.fonts.itervalues()} def set_initial_font(self, font_id): - if font_id is not None: + if font_id: if font_id not in self.fonts_by_id: info_dialog(self, _( '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) @@ -169,6 +170,7 @@ class LetteringFrame(wx.Frame): def on_font_changed(self, event=None): font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) + self.settings.font = font.id self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) self.update_preview() diff --git a/lib/utils/dotdict.py b/lib/utils/dotdict.py index 1ab3a4fec..76f23697a 100644 --- a/lib/utils/dotdict.py +++ b/lib/utils/dotdict.py @@ -6,7 +6,13 @@ class DotDict(dict): def __init__(self, *args, **kwargs): super(DotDict, self).__init__(*args, **kwargs) + self._dotdictify() + def update(self, *args, **kwargs): + super(DotDict, self).update(*args, **kwargs) + self.dotdictify() + + def _dotdictify(self): for k, v in self.iteritems(): if isinstance(v, dict): self[k] = DotDict(v) From b2d11ce956d14b08addb7d938e59aaa1aa8f42d4 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 18 Apr 2019 11:36:51 -0400 Subject: [PATCH 20/24] fix medium kerning --- fonts/medium_font/font.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fonts/medium_font/font.json b/fonts/medium_font/font.json index 63eba7184..df760bdeb 100644 --- a/fonts/medium_font/font.json +++ b/fonts/medium_font/font.json @@ -9,6 +9,6 @@ "min_scale": 0.75, "max_scale": 2.0, "kerning_pairs": { - "wo": -0.3 + "wo": -0.9 } } From b307b8e8247678a4bf128ded80a9bfd7b9b54c81 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 20 Apr 2019 22:01:58 -0400 Subject: [PATCH 21/24] fix style --- lib/extensions/lettering.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 174354923..a2a729b5d 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -146,8 +146,9 @@ class LetteringFrame(wx.Frame): def set_initial_font(self, font_id): if font_id: if font_id not in self.fonts_by_id: - info_dialog(self, _( - '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) + message = '''This text was created using the font "%s", but Ink/Stitch can't find that font. ''' \ + '''A default font will be substituted.''' + info_dialog(self, _(message) % font_id) try: self.font_chooser.SetValueByUser(self.fonts_by_id[font_id].name) From 013b4c2739edaa4336132fbc87eb131b38c7eb54 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 30 Apr 2019 20:15:58 -0400 Subject: [PATCH 22/24] speed up startup by lazy-loading glyphs --- lib/lettering/font.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 883821ec9..13a2b78c2 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -67,36 +67,39 @@ class Font(object): def __init__(self, font_path): self.path = font_path + self.metadata = {} + self.license = None + self.variants = {} + self._load_metadata() self._load_license() - self._load_variants() - - if self.variants.get(self.default_variant) is None: - raise FontError("font not found or has no default variant") def _load_metadata(self): try: with open(os.path.join(self.path, "font.json")) as metadata_file: self.metadata = json.load(metadata_file) except IOError: - self.metadata = {} + pass def _load_license(self): try: with open(os.path.join(self.path, "LICENSE")) as license_file: self.license = license_file.read() except IOError: - self.license = None + pass def _load_variants(self): - self.variants = {} + if not self.variants: + for variant in FontVariant.VARIANT_TYPES: + try: + self.variants[variant] = FontVariant(self.path, variant, self.default_glyph) + except IOError: + # we'll deal with missing variants when we apply lettering + pass - for variant in FontVariant.VARIANT_TYPES: - try: - self.variants[variant] = FontVariant(self.path, variant, self.default_glyph) - except IOError: - # we'll deal with missing variants when we apply lettering - pass + def _check_variants(self): + if self.variants.get(self.default_variant) is None: + raise FontError("font not found or has no default variant") name = localized_font_metadata('name', '') description = localized_font_metadata('description', '') @@ -116,6 +119,7 @@ class Font(object): def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): """Render text into an SVG group element.""" + self._load_variants() if variant is None: variant = self.default_variant From 62f5fbf3970a3dbaf2b240a9f7c0917f171729cf Mon Sep 17 00:00:00 2001 From: jameskolme <44113605+jameskolme@users.noreply.github.com> Date: Thu, 4 Apr 2019 14:14:53 +0300 Subject: [PATCH 23/24] add James's TT_Masters font --- fonts/tt_masters/README_en.md | 13 + fonts/tt_masters/font.json | 16 + fonts/tt_masters/←.svg | 2948 +++++++++++++++++++++++++++++++++ fonts/tt_masters/↑.svg | 2948 +++++++++++++++++++++++++++++++++ fonts/tt_masters/→.svg | 2948 +++++++++++++++++++++++++++++++++ fonts/tt_masters/↓.svg | 2948 +++++++++++++++++++++++++++++++++ 6 files changed, 11821 insertions(+) create mode 100644 fonts/tt_masters/README_en.md create mode 100644 fonts/tt_masters/font.json create mode 100644 fonts/tt_masters/←.svg create mode 100644 fonts/tt_masters/↑.svg create mode 100644 fonts/tt_masters/→.svg create mode 100644 fonts/tt_masters/↓.svg diff --git a/fonts/tt_masters/README_en.md b/fonts/tt_masters/README_en.md new file mode 100644 index 000000000..1941e46f6 --- /dev/null +++ b/fonts/tt_masters/README_en.md @@ -0,0 +1,13 @@ +Font origin and licence for TT_Masters + +www.free-fonts-download.com/Script/tt-masters-font +Font's name: TT Masters Font +Font's license: Public domain / GPL / OFL +Zip File size: 532 kb +Designed by: Jovanny Lemonad +Designer web: http://typetype.ru/ + +Font is traced and converted to suit Inkstitch embroidery lettering format. + + + diff --git a/fonts/tt_masters/font.json b/fonts/tt_masters/font.json new file mode 100644 index 000000000..341aae54d --- /dev/null +++ b/fonts/tt_masters/font.json @@ -0,0 +1,16 @@ +{ + "name": "TT Masters", + "description": "A font suited for heavy typing :)", + "leading": 22, + "letter_spacing": 1.5, + "word_spacing": 7, + "auto_satin": true, + "default_glyph": "?", + "min_scale": 1.0, + "max_scale": 3.0, + "kerning_pairs": { + "wo": -0.3, + "vo": -0.3 + + } +} diff --git a/fonts/tt_masters/←.svg b/fonts/tt_masters/←.svg new file mode 100644 index 000000000..16dd7a403 --- /dev/null +++ b/fonts/tt_masters/←.svg @@ -0,0 +1,2948 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/tt_masters/↑.svg b/fonts/tt_masters/↑.svg new file mode 100644 index 000000000..16dd7a403 --- /dev/null +++ b/fonts/tt_masters/↑.svg @@ -0,0 +1,2948 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/tt_masters/→.svg b/fonts/tt_masters/→.svg new file mode 100644 index 000000000..16dd7a403 --- /dev/null +++ b/fonts/tt_masters/→.svg @@ -0,0 +1,2948 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/tt_masters/↓.svg b/fonts/tt_masters/↓.svg new file mode 100644 index 000000000..16dd7a403 --- /dev/null +++ b/fonts/tt_masters/↓.svg @@ -0,0 +1,2948 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c51ae9ccb7052e4015b4a3dd3e2817d8625e3ab0 Mon Sep 17 00:00:00 2001 From: jameskolme <44113605+jameskolme@users.noreply.github.com> Date: Thu, 4 Apr 2019 14:14:53 +0300 Subject: [PATCH 24/24] add James's TT_Directors font --- fonts/tt_directors/README_en.md | 13 + fonts/tt_directors/font.json | 14 + fonts/tt_directors/←.svg | 2306 +++++++++++++++++++++++++++++++ fonts/tt_directors/↑.svg | 2306 +++++++++++++++++++++++++++++++ fonts/tt_directors/→.svg | 2306 +++++++++++++++++++++++++++++++ fonts/tt_directors/↓.svg | 2306 +++++++++++++++++++++++++++++++ 6 files changed, 9251 insertions(+) create mode 100644 fonts/tt_directors/README_en.md create mode 100644 fonts/tt_directors/font.json create mode 100644 fonts/tt_directors/←.svg create mode 100644 fonts/tt_directors/↑.svg create mode 100644 fonts/tt_directors/→.svg create mode 100644 fonts/tt_directors/↓.svg diff --git a/fonts/tt_directors/README_en.md b/fonts/tt_directors/README_en.md new file mode 100644 index 000000000..489dc465a --- /dev/null +++ b/fonts/tt_directors/README_en.md @@ -0,0 +1,13 @@ +Font origin and licence for TT_Masters + +www.free-fonts-download.com/Script/tt-directors-font +Font's name: TT Directors Font +Font's license: Public domain / GPL / OFL +Zip File size: 532 kb +Designed by: Jovanny Lemonad +Designer web: http://typetype.ru/ + +Font is traced and converted to suit Inkstitch embroidery lettering format. + + + diff --git a/fonts/tt_directors/font.json b/fonts/tt_directors/font.json new file mode 100644 index 000000000..a819c2ccf --- /dev/null +++ b/fonts/tt_directors/font.json @@ -0,0 +1,14 @@ +{ + "name": "TT Directors", + "description": "A font suited for directing", + "leading": 20, + "letter_spacing": 1.0, + "word_spacing": 4.5, + "auto_satin": true, + "default_glyph": "?", + "min_scale": 1.0, + "max_scale": 5.0, + "kerning_pairs": { + "wo": -0.3 + } +} diff --git a/fonts/tt_directors/←.svg b/fonts/tt_directors/←.svg new file mode 100644 index 000000000..d944f1964 --- /dev/null +++ b/fonts/tt_directors/←.svg @@ -0,0 +1,2306 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/tt_directors/↑.svg b/fonts/tt_directors/↑.svg new file mode 100644 index 000000000..d944f1964 --- /dev/null +++ b/fonts/tt_directors/↑.svg @@ -0,0 +1,2306 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/tt_directors/→.svg b/fonts/tt_directors/→.svg new file mode 100644 index 000000000..d944f1964 --- /dev/null +++ b/fonts/tt_directors/→.svg @@ -0,0 +1,2306 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/tt_directors/↓.svg b/fonts/tt_directors/↓.svg new file mode 100644 index 000000000..d944f1964 --- /dev/null +++ b/fonts/tt_directors/↓.svg @@ -0,0 +1,2306 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +