diff --git a/lib/elements/element.py b/lib/elements/element.py index 43ca50954..269cbdc23 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -58,60 +58,6 @@ def param(*args, **kwargs): class EmbroideryElement(object): def __init__(self, node): self.node = node - self._update_legacy_params() - - def _update_legacy_params(self): # noqa: C901 - # update legacy embroider_ attributes to namespaced attributes - legacy_attribs = False - for attrib in self.node.attrib: - if attrib.startswith('embroider_'): - self.replace_legacy_param(attrib) - legacy_attribs = True - - # convert legacy tie setting - legacy_tie = self.get_param('ties', None) - if legacy_tie == "True": - self.set_param('ties', 0) - elif legacy_tie == "False": - self.set_param('ties', 3) - - # convert legacy fill_method - legacy_fill_method = self.get_int_param('fill_method', None) - if legacy_fill_method == 0: - self.set_param('fill_method', 'auto_fill') - elif legacy_fill_method == 1: - self.set_param('fill_method', 'contour_fill') - elif legacy_fill_method == 2: - self.set_param('fill_method', 'guided_fill') - elif legacy_fill_method == 3: - self.set_param('fill_method', 'legacy_fill') - - # legacy satin method - if self.get_boolean_param('e_stitch', False) is True: - self.remove_param('e_stitch') - self.set_param('satin_method', 'e_stitch') - - # default setting for fill_underlay has changed - if legacy_attribs and not self.get_param('fill_underlay', ""): - self.set_param('fill_underlay', False) - - # convert legacy stroke_method - if self.get_style("stroke"): - # manual stitch - legacy_manual_stitch = self.get_boolean_param('manual_stitch', False) - if legacy_manual_stitch is True: - self.remove_param('manual_stitch') - self.set_param('stroke_method', 'manual_stitch') - # stroke_method - legacy_stroke_method = self.get_int_param('stroke_method', None) - if legacy_stroke_method == 0: - self.set_param('stroke_method', 'running_stitch') - elif legacy_stroke_method == 1: - self.set_param('stroke_method', 'ripple_stitch') - if (not self.get_param('stroke_method', None) and - self.get_param('satin_column', False) is False and - not self.node.style('stroke-dasharray')): - self.set_param('stroke_method', 'zigzag_stitch') @property def id(self): @@ -128,14 +74,6 @@ class EmbroideryElement(object): params.append(prop.fget.param) return params - def replace_legacy_param(self, param): - # remove "embroider_" prefix - new_param = param[10:] - if new_param in INKSTITCH_ATTRIBS: - value = self.node.get(param, "").strip() - self.set_param(param[10:], value) - del self.node.attrib[param] - @cache def get_param(self, param, default): value = self.node.get(INKSTITCH_ATTRIBS[param], "").strip() diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 7b3c6f1c9..e381e2c18 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -3,112 +3,33 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import json import os -import re -from collections.abc import MutableMapping - -from lxml import etree -from lxml.etree import Comment -from stringcase import snakecase import inkex +from lxml.etree import Comment +from stringcase import snakecase from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..elements.clone import is_clone from ..i18n import _ from ..marker import has_marker +from ..metadata import InkStitchMetadata from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_MASK_TAG) -from ..utils.settings import DEFAULT_METADATA, global_settings - -SVG_METADATA_TAG = inkex.addNS("metadata", "svg") +from ..update import update_inkstitch_document -def strip_namespace(tag): - """Remove xml namespace from a tag name. - - >>> {http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview - <<< namedview - """ - - match = re.match(r'^\{[^}]+\}(.+)$', tag) - - if match: - return match.group(1) - else: - return tag - - -class InkStitchMetadata(MutableMapping): - """Helper class to get and set inkstitch-specific metadata attributes. - - Operates on a document and acts like a dict. Setting an item adds or - updates a metadata element in the document. Getting an item retrieves - a metadata element's text contents or None if an element by that name - doesn't exist. - """ - - def __init__(self, document): - super().__init__() - self.document = document - self.metadata = document.metadata - - for setting in DEFAULT_METADATA: - if self[setting] is None: - self[setting] = global_settings[f'default_{setting}'] - - # Because this class inherints from MutableMapping, all we have to do is - # implement these five methods and we get a full dict-like interface. - - def __setitem__(self, name, value): - item = self._find_item(name) - item.text = json.dumps(value) - - def _find_item(self, name, create=True): - tag = inkex.addNS(name, "inkstitch") - item = self.metadata.find(tag) - if item is None and create: - item = etree.SubElement(self.metadata, tag) - - return item - - def __getitem__(self, name): - item = self._find_item(name) - - try: - return json.loads(item.text) - except (ValueError, TypeError): - return None - - def __delitem__(self, name): - item = self._find_item(name, create=False) - - if item is not None: - self.metadata.remove(item) - - def __iter__(self): - for child in self.metadata: - if child.prefix == "inkstitch": - yield strip_namespace(child.tag) - - def __len__(self): - i = 0 - for i, item in enumerate(self): - pass - - return i + 1 - - def __json__(self): - return dict(self) - - -class InkstitchExtension(inkex.Effect): +class InkstitchExtension(inkex.EffectExtension): """Base class for Inkstitch extensions. Not intended for direct use.""" + def load(self, *args, **kwargs): + document = super().load(*args, **kwargs) + update_inkstitch_document(document) + return document + @classmethod def name(cls): return snakecase(cls.__name__) diff --git a/lib/metadata.py b/lib/metadata.py new file mode 100644 index 000000000..837fbf008 --- /dev/null +++ b/lib/metadata.py @@ -0,0 +1,85 @@ +import json +import re +from collections.abc import MutableMapping + +import inkex +from lxml import etree + +from .utils.settings import DEFAULT_METADATA, global_settings + + +def strip_namespace(tag): + """Remove xml namespace from a tag name. + + >>> {http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview + <<< namedview + """ + + match = re.match(r'^\{[^}]+\}(.+)$', tag) + + if match: + return match.group(1) + else: + return tag + + +class InkStitchMetadata(MutableMapping): + """Helper class to get and set inkstitch-specific metadata attributes. + + Operates on a document and acts like a dict. Setting an item adds or + updates a metadata element in the document. Getting an item retrieves + a metadata element's text contents or None if an element by that name + doesn't exist. + """ + + def __init__(self, document): + super().__init__() + self.document = document + self.metadata = document.metadata + + for setting in DEFAULT_METADATA: + if self[setting] is None: + self[setting] = global_settings[f'default_{setting}'] + + # Because this class inherints from MutableMapping, all we have to do is + # implement these five methods and we get a full dict-like interface. + def __setitem__(self, name, value): + item = self._find_item(name) + item.text = json.dumps(value) + + def _find_item(self, name, create=True): + tag = inkex.addNS(name, "inkstitch") + item = self.metadata.find(tag) + if item is None and create: + item = etree.SubElement(self.metadata, tag) + + return item + + def __getitem__(self, name): + item = self._find_item(name) + + try: + return json.loads(item.text) + except (ValueError, TypeError): + return None + + def __delitem__(self, name): + item = self._find_item(name, create=False) + + if item is not None: + self.metadata.remove(item) + + def __iter__(self): + for child in self.metadata: + if child.prefix == "inkstitch": + yield strip_namespace(child.tag) + + def __len__(self): + i = 0 + for i, item in enumerate(self): + pass + + return i + 1 + + def __json__(self): + return dict(self) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index bbef6ebbd..8ce0c8a24 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -27,6 +27,7 @@ SVG_IMAGE_TAG = inkex.addNS('image', 'svg') SVG_CLIPPATH_TAG = inkex.addNS('clipPath', 'svg') SVG_MASK_TAG = inkex.addNS('mask', 'svg') +SVG_METADATA_TAG = inkex.addNS("metadata", "svg") INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') CONNECTION_START = inkex.addNS('connection-start', 'inkscape') diff --git a/lib/update.py b/lib/update.py new file mode 100644 index 000000000..cba6f671f --- /dev/null +++ b/lib/update.py @@ -0,0 +1,126 @@ +from inkex import errormsg + +from .i18n import _ +from .elements import EmbroideryElement +from .metadata import InkStitchMetadata +from .svg.tags import INKSTITCH_ATTRIBS + +INKSTITCH_SVG_VERSION = 1 + + +def update_inkstitch_document(svg): + document = svg.getroot() + # get the inkstitch svg version from the document + search_string = "//*[local-name()='inkstitch_svg_version']//text()" + file_version = document.findone(search_string) + try: + file_version = int(file_version) + except (TypeError, ValueError): + file_version = 0 + + if file_version == INKSTITCH_SVG_VERSION: + return + + if file_version > INKSTITCH_SVG_VERSION: + errormsg(_("This document was created with a newer Version of Ink/Stitch. " + "It is possible that not everything works as expected.\n\n" + "Please update your Ink/Stitch version: https://inkstitch.org/docs/install/")) + # they may not want to be bothered with this info everytime they call an inkstitch extension + # let's udowngrade the file version number + _update_inkstitch_svg_version(svg) + else: + # this document is either a new document or it is outdated + # if we cannot find any inkstitch attribute in the document, we assume that this is a new document which doesn't need to be updated + search_string = "//*[namespace-uri()='http://inkstitch.org/namespace' or " \ + "@*[namespace-uri()='http://inkstitch.org/namespace'] or " \ + "@*[starts-with(name(), 'embroider_')]]" + inkstitch_element = document.findone(search_string) + if inkstitch_element is None: + _update_inkstitch_svg_version(svg) + return + + # update elements + for element in document.iterdescendants(): + # We are just checking for params and update them. + # No need to check for specific stitch types at this point + update_legacy_params(EmbroideryElement(element), file_version, INKSTITCH_SVG_VERSION) + _update_inkstitch_svg_version(svg) + + +def _update_inkstitch_svg_version(svg): + # set inkstitch svg version + metadata = InkStitchMetadata(svg.getroot()) + metadata['inkstitch_svg_version'] = INKSTITCH_SVG_VERSION + + +def update_legacy_params(element, file_version, inkstitch_svg_version): + for version in range(file_version + 1, inkstitch_svg_version + 1): + _update_to(version, element) + + +def _update_to(version, element): + if version == 1: + _update_to_one(element) + + +def _update_to_one(element): # noqa: C901 + # update legacy embroider_ attributes to namespaced attributes + legacy_attribs = False + for attrib in element.node.attrib: + if attrib.startswith('embroider_'): + _replace_legacy_embroider_param(element, attrib) + legacy_attribs = True + + # convert legacy tie setting + legacy_tie = element.get_param('ties', None) + if legacy_tie == "True": + element.set_param('ties', 0) + elif legacy_tie == "False": + element.set_param('ties', 3) + + # convert legacy fill_method + legacy_fill_method = element.get_int_param('fill_method', None) + if legacy_fill_method == 0: + element.set_param('fill_method', 'auto_fill') + elif legacy_fill_method == 1: + element.set_param('fill_method', 'contour_fill') + elif legacy_fill_method == 2: + element.set_param('fill_method', 'guided_fill') + elif legacy_fill_method == 3: + element.set_param('fill_method', 'legacy_fill') + + # legacy satin method + if element.get_boolean_param('e_stitch', False) is True: + element.remove_param('e_stitch') + element.set_param('satin_method', 'e_stitch') + + # default setting for fill_underlay has changed + if legacy_attribs and not element.get_param('fill_underlay', ""): + element.set_param('fill_underlay', False) + + # convert legacy stroke_method + if element.get_style("stroke"): + # manual stitch + legacy_manual_stitch = element.get_boolean_param('manual_stitch', False) + if legacy_manual_stitch is True: + element.remove_param('manual_stitch') + element.set_param('stroke_method', 'manual_stitch') + # stroke_method + legacy_stroke_method = element.get_int_param('stroke_method', None) + if legacy_stroke_method == 0: + element.set_param('stroke_method', 'running_stitch') + elif legacy_stroke_method == 1: + element.set_param('stroke_method', 'ripple_stitch') + if (not element.get_param('stroke_method', None) and + element.get_param('satin_column', False) is False and + not element.node.style('stroke-dasharray')): + element.set_param('stroke_method', 'zigzag_stitch') + + +def _replace_legacy_embroider_param(element, param): + # remove "embroider_" prefix + new_param = param[10:] + if new_param in INKSTITCH_ATTRIBS: + value = element.node.get(param, "").strip() + element.set_param(param[10:], value) + del element.node.attrib[param]