diff options
Diffstat (limited to 'support/inkscape')
-rw-r--r-- | support/inkscape/__main__.py | 108 | ||||
-rw-r--r-- | support/inkscape/effect.py | 520 | ||||
-rw-r--r-- | support/inkscape/inkscape.py | 226 |
3 files changed, 438 insertions, 416 deletions
diff --git a/support/inkscape/__main__.py b/support/inkscape/__main__.py index 78ffb55..29ac745 100644 --- a/support/inkscape/__main__.py +++ b/support/inkscape/__main__.py @@ -4,61 +4,61 @@ from . import effect, inkscape def _unfuck_svg_document(temp_svg_path): - """ - Unfucks an SVG document so is can be processed by the better_dxf_export plugin (or what's left of it). - """ - - command_line = inkscape.InkscapeCommandLine(temp_svg_path) - layers = command_line.layers - - command_line.apply_to_document('LayerUnlockAll', 'LayerShowAll') - - layer_copies = [] - - for i in layers: - layer_copy = command_line.duplicate_layer(i) - layer_copies.append(layer_copy) - - command_line.apply_to_layer_content(layer_copy, 'ObjectToPath') - command_line.apply_to_layer_content(layer_copy, 'SelectionUnGroup') - - if not i.use_paths: - command_line.apply_to_layer_content(layer_copy, 'StrokeToPath') - command_line.apply_to_layer_content(layer_copy, 'SelectionUnion') - - for original, copy in zip(layers, layer_copies): - command_line.clear_layer(original) - command_line.move_content(copy, original) - command_line.delete_layer(copy) - - command_line.apply_to_document('FileSave', 'FileClose', 'FileQuit') - - command_line.run() + """ + Unfucks an SVG document so is can be processed by the better_dxf_export + plugin (or what's left of it). + """ + command_line = inkscape.InkscapeCommandLine(temp_svg_path) + layers = command_line.layers + + command_line.apply_to_document('LayerUnlockAll', 'LayerShowAll') + + layer_copies = [] + + for i in layers: + layer_copy = command_line.duplicate_layer(i) + layer_copies.append(layer_copy) + + command_line.apply_to_layer_content(layer_copy, 'ObjectToPath') + command_line.apply_to_layer_content(layer_copy, 'SelectionUnGroup') + + if not i.use_paths: + command_line.apply_to_layer_content(layer_copy, 'StrokeToPath') + command_line.apply_to_layer_content(layer_copy, 'SelectionUnion') + + for original, copy in zip(layers, layer_copies): + command_line.clear_layer(original) + command_line.move_content(copy, original) + command_line.delete_layer(copy) + + command_line.apply_to_document('FileSave', 'FileClose', 'FileQuit') + + command_line.run() @util.main def main(in_path, out_path): - try: - _, out_suffix = os.path.splitext(out_path) - - effect.ExportEffect.check_document_units(in_path) - - with util.TemporaryDirectory() as temp_dir: - temp_svg_path = os.path.join(temp_dir, os.path.basename(in_path)) - - shutil.copyfile(in_path, temp_svg_path) - - _unfuck_svg_document(temp_svg_path) - - export_effect = effect.ExportEffect() - export_effect.affect(args = [temp_svg_path], output = False) - - with open(out_path, 'w') as file: - if out_suffix == '.dxf': - export_effect.write_dxf(file) - elif out_suffix == '.asy': - export_effect.write_asy(file) - else: - raise Exception('Unknown file type: {}'.format(out_suffix)) - except util.UserError as e: - raise util.UserError('While processing {}: {}', in_path, e) + try: + _, out_suffix = os.path.splitext(out_path) + + effect.ExportEffect.check_document_units(in_path) + + with util.TemporaryDirectory() as temp_dir: + temp_svg_path = os.path.join(temp_dir, os.path.basename(in_path)) + + shutil.copyfile(in_path, temp_svg_path) + + _unfuck_svg_document(temp_svg_path) + + export_effect = effect.ExportEffect() + export_effect.affect(args=[temp_svg_path], output=False) + + with open(out_path, 'w') as file: + if out_suffix == '.dxf': + export_effect.write_dxf(file) + elif out_suffix == '.asy': + export_effect.write_asy(file) + else: + raise Exception('Unknown file type: {}'.format(out_suffix)) + except util.UserError as e: + raise util.UserError('While processing {}: {}', in_path, e) diff --git a/support/inkscape/effect.py b/support/inkscape/effect.py index 6acabfb..313032c 100644 --- a/support/inkscape/effect.py +++ b/support/inkscape/effect.py @@ -1,264 +1,280 @@ """ -Based on code from Aaron Spike. See http://www.bobcookdev.com/inkscape/inkscape-dxf.html +Based on code from Aaron Spike. See +http://www.bobcookdev.com/inkscape/inkscape-dxf.html """ -import pkgutil, os, re, collections, itertools +import collections +import itertools +import os +import pkgutil +import re from lxml import etree + from lib import util from . import inkex, simpletransform, cubicsuperpath, cspsubdiv, inkscape def _get_unit_factors_map(): - # Fluctuates somewhat between Inkscape releases _and_ between SVG version. - pixels_per_inch = 96. - pixels_per_mm = pixels_per_inch / 25.4 - - return { - 'px': 1.0, - 'mm': pixels_per_mm, - 'cm': pixels_per_mm * 10, - 'm' : pixels_per_mm * 1e3, - 'km': pixels_per_mm * 1e6, - 'pt': pixels_per_inch / 72, - 'pc': pixels_per_inch / 6, - 'in': pixels_per_inch, - 'ft': pixels_per_inch * 12, - 'yd': pixels_per_inch * 36 } + # Fluctuates somewhat between Inkscape releases _and_ between SVG version. + pixels_per_inch = 96. + pixels_per_mm = pixels_per_inch / 25.4 + + return { + 'px': 1.0, + 'mm': pixels_per_mm, + 'cm': pixels_per_mm * 10, + 'm': pixels_per_mm * 1e3, + 'km': pixels_per_mm * 1e6, + 'pt': pixels_per_inch / 72, + 'pc': pixels_per_inch / 6, + 'in': pixels_per_inch, + 'ft': pixels_per_inch * 12, + 'yd': pixels_per_inch * 36} class ExportEffect(inkex.Effect): - _unit_factors = _get_unit_factors_map() - _asymptote_all_paths_name = 'all' - - def __init__(self): - inkex.Effect.__init__(self) - - self._flatness = float(os.environ['DXF_FLATNESS']) - - self._layers = None - self._paths = None - - def _get_document_scale(self): - """ - Return scaling factor applied to the document because of a viewBox setting. This currently ignores any setting of a preserveAspectRatio attribute (like Inkscape). - """ - - document_height = self._get_height() - view_box = self._get_view_box() - - if view_box is None or document_height is None: - return 1 - else: - _, _, _, view_box_height = view_box - - return document_height / view_box_height - - def _get_document_height(self): - """ - Get the height of the document in pixels in the document coordinate system as it is interpreted by Inkscape. - """ - - view_box = self._get_view_box() - document_height = self._get_height() - - if view_box is not None: - _, _, _, view_box_height = view_box - - return view_box_height - elif document_height is not None: - return document_height - else: - return 0 - - def _get_height(self): - height_attr = self.document.getroot().get('height') - - if height_attr is None: - return None - else: - return self._measure_to_pixels(height_attr) - - def _get_view_box(self): - view_box_attr = self.document.getroot().get('viewBox') - - if view_box_attr is None: - return None - else: - return [float(i) for i in view_box_attr.split()] - - def _get_shape_paths(self, node, transform): - shape = cubicsuperpath.parsePath(node.get('d')) - - transform = simpletransform.composeTransform( - transform, - simpletransform.composeParents(node, [[1, 0, 0], [0, 1, 0]])) - - simpletransform.applyTransformToPath(transform, shape) - - def iter_paths(): - for path in shape: - cspsubdiv.subdiv(path, self._flatness) - - # path contains two control point coordinates and the actual coordinates per point. - yield [i for _, i, _ in path] - - return list(iter_paths()) - - def effect(self): - document_height = self._get_document_height() - document_scale = self._get_document_scale() - - transform = simpletransform.composeTransform( - [[document_scale, 0, 0], [0, document_scale, 0]], - [[1, 0, 0], [0, -1, document_height]]) - - layers = inkscape.get_inkscape_layers(self.svg_file) - layers_by_inkscape_name = { i.inkscape_name: i for i in layers } - - def iter_paths(): - for node in self.document.getroot().xpath('//svg:path', namespaces = inkex.NSS): - layer = layers_by_inkscape_name.get(self._get_inkscape_layer_name(node)) - - for path in self._get_shape_paths(node, transform): - yield layer, path - - self._layers = layers - self._paths = list(iter_paths()) - - def write_dxf(self, file): - # Scales pixels to millimeters. This is the predominant unit in CAD. - unit_factor = self._unit_factors['mm'] - - layer_indices = { l: i for i, l in enumerate(self._layers) } - - file.write(pkgutil.get_data(__name__, 'dxf_header.txt')) - - def write_instruction(code, value): - print >> file, code - print >> file, value - - handle_iter = itertools.count(256) - - for layer, path in self._paths: - for (x1, y1), (x2, y2) in zip(path, path[1:]): - write_instruction(0, 'LINE') - - if layer is not None: - write_instruction(8, layer.export_name) - write_instruction(62, layer_indices.get(layer, 0)) - - write_instruction(5, '{:x}'.format(next(handle_iter))) - write_instruction(100, 'AcDbEntity') - write_instruction(100, 'AcDbLine') - write_instruction(10, repr(x1 / unit_factor)) - write_instruction(20, repr(y1 / unit_factor)) - write_instruction(30, 0.0) - write_instruction(11, repr(x2 / unit_factor)) - write_instruction(21, repr(y2 / unit_factor)) - write_instruction(31, 0.0) - - file.write(pkgutil.get_data(__name__, 'dxf_footer.txt')) - - def write_asy(self, file): - def write_line(format, *args): - print >> file, format.format(*args) + ';' - - # Scales pixels to points. Asymptote uses PostScript points (1 / 72 inch) by default. - unit_factor = self._unit_factors['pt'] - - paths_by_layer = collections.defaultdict(list) - variable_names = [] - - for layer, path in self._paths: - paths_by_layer[layer].append(path) - - for layer in self._layers + [None]: - paths = paths_by_layer[layer] - variable_name = self._asymptote_identifier_from_layer(layer) - write_line('path[] {}', variable_name) - - variable_names.append(variable_name) - - for path in paths: - point_strs = ['({}, {})'.format(x / unit_factor, y / unit_factor) for x, y in path] - - # Hack. We should determine this from whether Z or z was used to close the path in the SVG document. - if path[0] == path[-1]: - point_strs[-1] = 'cycle' - - write_line('{}.push({})', variable_name, ' -- '.join(point_strs)) - - if self._asymptote_all_paths_name not in variable_names: - write_line('path[] {}', self._asymptote_all_paths_name) - - for i in variable_names: - write_line('{}.append({})', self._asymptote_all_paths_name, i) - - @classmethod - def _parse_measure(cls, string): - value_match = re.match(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)', string) - unit_match = re.search('(%s)$' % '|'.join(cls._unit_factors.keys()), string) - - value = float(string[value_match.start():value_match.end()]) - - if unit_match: - unit = string[unit_match.start():unit_match.end()] - else: - unit = None - - return value, unit - - @classmethod - def _measure_to_pixels(cls, string): - """ - Parse a string containing a measure and return it's value converted to pixels. - """ - - value, unit = cls._parse_measure(string) - - return value * cls._get_unit_factor(unit) - - @classmethod - def _get_inkscape_layer_name(cls, node): - while node is not None: - layer = node.get(inkex.addNS('label', 'inkscape')) - - if layer is not None: - return layer - - node = node.getparent() - - return None - - @classmethod - def _get_unit_factor(cls, unit): - if unit is None: - return 1 - else: - return cls._unit_factors[unit] - - @classmethod - def _asymptote_identifier_from_layer(cls, layer): - if layer is None: - return '_' - else: - return re.sub('[^a-zA-Z0-9]', '_', layer.export_name) - - @classmethod - def check_document_units(cls, path): - with open(path, 'r') as file: - p = etree.XMLParser(huge_tree = True) - document = etree.parse(file, parser = p) - - height_attr = document.getroot().get('height') - - if height_attr is None: - raise util.UserError('SVG document has no height attribute. See https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') - - _, height_unit = cls._parse_measure(height_attr) - - if height_unit is None or height_unit == 'px': - raise util.UserError('Height of SVG document is not an absolute measure. See https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') - - if document.getroot().get('viewBox') is None: - raise util.UserError('SVG document has no viewBox attribute. See https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') + _unit_factors = _get_unit_factors_map() + _asymptote_all_paths_name = 'all' + + def __init__(self): + inkex.Effect.__init__(self) + + self._flatness = float(os.environ['DXF_FLATNESS']) + + self._layers = None + self._paths = None + + def _get_document_scale(self): + """ + Return scaling factor applied to the document because of a viewBox + setting. This currently ignores any setting of a preserveAspectRatio + attribute (like Inkscape). + """ + document_height = self._get_height() + view_box = self._get_view_box() + + if view_box is None or document_height is None: + return 1 + else: + _, _, _, view_box_height = view_box + + return document_height / view_box_height + + def _get_document_height(self): + """ + Get the height of the document in pixels in the document coordinate + system as it is interpreted by Inkscape. + """ + view_box = self._get_view_box() + document_height = self._get_height() + + if view_box is not None: + _, _, _, view_box_height = view_box + + return view_box_height + elif document_height is not None: + return document_height + else: + return 0 + + def _get_height(self): + height_attr = self.document.getroot().get('height') + + if height_attr is None: + return None + else: + return self._measure_to_pixels(height_attr) + + def _get_view_box(self): + view_box_attr = self.document.getroot().get('viewBox') + + if view_box_attr is None: + return None + else: + return [float(i) for i in view_box_attr.split()] + + def _get_shape_paths(self, node, transform): + shape = cubicsuperpath.parsePath(node.get('d')) + + transform = simpletransform.composeTransform( + transform, + simpletransform.composeParents(node, [[1, 0, 0], [0, 1, 0]])) + + simpletransform.applyTransformToPath(transform, shape) + + def iter_paths(): + for path in shape: + cspsubdiv.subdiv(path, self._flatness) + + # path contains two control point coordinates and the actual + # coordinates per point. + yield [i for _, i, _ in path] + + return list(iter_paths()) + + def effect(self): + document_height = self._get_document_height() + document_scale = self._get_document_scale() + + transform = simpletransform.composeTransform( + [[document_scale, 0, 0], [0, document_scale, 0]], + [[1, 0, 0], [0, -1, document_height]]) + + layers = inkscape.get_inkscape_layers(self.svg_file) + layers_by_inkscape_name = {i.inkscape_name: i for i in layers} + + def iter_paths(): + for node in self.document.getroot().xpath('//svg:path', namespaces=inkex.NSS): + layer = layers_by_inkscape_name.get(self._get_inkscape_layer_name(node)) + + for path in self._get_shape_paths(node, transform): + yield layer, path + + self._layers = layers + self._paths = list(iter_paths()) + + def write_dxf(self, file): + # Scales pixels to millimeters. This is the predominant unit in CAD. + unit_factor = self._unit_factors['mm'] + + layer_indices = {l: i for i, l in enumerate(self._layers)} + + file.write(pkgutil.get_data(__name__, 'dxf_header.txt')) + + def write_instruction(code, value): + print >> file, code + print >> file, value + + handle_iter = itertools.count(256) + + for layer, path in self._paths: + for (x1, y1), (x2, y2) in zip(path, path[1:]): + write_instruction(0, 'LINE') + + if layer is not None: + write_instruction(8, layer.export_name) + write_instruction(62, layer_indices.get(layer, 0)) + + write_instruction(5, '{:x}'.format(next(handle_iter))) + write_instruction(100, 'AcDbEntity') + write_instruction(100, 'AcDbLine') + write_instruction(10, repr(x1 / unit_factor)) + write_instruction(20, repr(y1 / unit_factor)) + write_instruction(30, 0.0) + write_instruction(11, repr(x2 / unit_factor)) + write_instruction(21, repr(y2 / unit_factor)) + write_instruction(31, 0.0) + + file.write(pkgutil.get_data(__name__, 'dxf_footer.txt')) + + def write_asy(self, file): + def write_line(format, *args): + print >> file, format.format(*args) + ';' + + # Scales pixels to points. Asymptote uses PostScript points (1 / 72 + # inch) by default. + unit_factor = self._unit_factors['pt'] + + paths_by_layer = collections.defaultdict(list) + variable_names = [] + + for layer, path in self._paths: + paths_by_layer[layer].append(path) + + for layer in self._layers + [None]: + paths = paths_by_layer[layer] + variable_name = self._asymptote_identifier_from_layer(layer) + write_line('path[] {}', variable_name) + + variable_names.append(variable_name) + + for path in paths: + point_strs = ['({}, {})'.format(x / unit_factor, y / unit_factor) for x, y in path] + + # Hack. We should determine this from whether Z or z was used + # to close the path in the SVG document. + if path[0] == path[-1]: + point_strs[-1] = 'cycle' + + write_line('{}.push({})', variable_name, ' -- '.join(point_strs)) + + if self._asymptote_all_paths_name not in variable_names: + write_line('path[] {}', self._asymptote_all_paths_name) + + for i in variable_names: + write_line('{}.append({})', self._asymptote_all_paths_name, i) + + @classmethod + def _parse_measure(cls, string): + value_match = re.match(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)', string) + unit_match = re.search('(%s)$' % '|'.join(cls._unit_factors.keys()), string) + + value = float(string[value_match.start():value_match.end()]) + + if unit_match: + unit = string[unit_match.start():unit_match.end()] + else: + unit = None + + return value, unit + + @classmethod + def _measure_to_pixels(cls, string): + """ + Parse a string containing a measure and return it's value converted + to pixels. + """ + value, unit = cls._parse_measure(string) + + return value * cls._get_unit_factor(unit) + + @classmethod + def _get_inkscape_layer_name(cls, node): + while node is not None: + layer = node.get(inkex.addNS('label', 'inkscape')) + + if layer is not None: + return layer + + node = node.getparent() + + return None + + @classmethod + def _get_unit_factor(cls, unit): + if unit is None: + return 1 + else: + return cls._unit_factors[unit] + + @classmethod + def _asymptote_identifier_from_layer(cls, layer): + if layer is None: + return '_' + else: + return re.sub('[^a-zA-Z0-9]', '_', layer.export_name) + + @classmethod + def check_document_units(cls, path): + with open(path, 'r') as file: + p = etree.XMLParser(huge_tree = True) + document = etree.parse(file, parser = p) + + height_attr = document.getroot().get('height') + + if height_attr is None: + raise util.UserError( + 'SVG document has no height attribute. See ' + 'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') + + _, height_unit = cls._parse_measure(height_attr) + + if height_unit is None or height_unit == 'px': + raise util.UserError( + 'Height of SVG document is not an absolute measure. See ' + 'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') + + if document.getroot().get('viewBox') is None: + raise util.UserError( + 'SVG document has no viewBox attribute. See ' + 'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') diff --git a/support/inkscape/inkscape.py b/support/inkscape/inkscape.py index 21634d2..dee1542 100644 --- a/support/inkscape/inkscape.py +++ b/support/inkscape/inkscape.py @@ -1,125 +1,131 @@ import os import xml.etree.ElementTree as etree + from lib import util def get_inkscape_layers(svg_path): - document = etree.parse(svg_path) - - def iter_layers(): - nodes = document.findall( - '{http://www.w3.org/2000/svg}g[@{http://www.inkscape.org/namespaces/inkscape}groupmode="layer"]') - - for i in nodes: - inkscape_name = i.get('{http://www.inkscape.org/namespaces/inkscape}label').strip() - - if inkscape_name.endswith(']'): - export_name, args = inkscape_name[:-1].rsplit('[', 1) - - export_name = export_name.strip() - args = args.strip() - - use_paths = 'p' in args - else: - use_paths = False - export_name = inkscape_name - - yield Layer(inkscape_name, export_name, use_paths) - - return list(iter_layers()) + document = etree.parse(svg_path) + + def iter_layers(): + nodes = document.findall( + '{http://www.w3.org/2000/svg}g[@{http://www.inkscape.org/namespaces/inkscape}groupmode="layer"]') + + for i in nodes: + inkscape_name = i.get('{http://www.inkscape.org/namespaces/inkscape}label').strip() + + if inkscape_name.endswith(']'): + export_name, args = inkscape_name[:-1].rsplit('[', 1) + + export_name = export_name.strip() + args = args.strip() + + use_paths = 'p' in args + else: + use_paths = False + export_name = inkscape_name + + yield Layer(inkscape_name, export_name, use_paths) + + return list(iter_layers()) def _inkscape(svg_path, verbs): - def iter_args(): - yield os.environ['INKSCAPE'] - - for i in verbs: - yield '--verb' - yield i - - yield svg_path - - util.command(list(iter_args())) + def iter_args(): + yield os.environ['INKSCAPE'] + + for i in verbs: + yield '--verb' + yield i + + yield svg_path + + util.command(list(iter_args())) class Layer(object): - def __init__(self, inkscape_name, export_name, use_paths): - self.inkscape_name = inkscape_name - self.export_name = export_name - self.use_paths = use_paths + def __init__(self, inkscape_name, export_name, use_paths): + self.inkscape_name = inkscape_name + self.export_name = export_name + self.use_paths = use_paths class InkscapeCommandLine(object): - def __init__(self, path): - self._path = path - self._layers = get_inkscape_layers(path) - self._current_layer_index = None - self._verbs = [] - - def apply_to_document(self, *verb): - self._verbs.extend(verb) - - def apply_to_layer(self, layer, *verb): - self._go_to_layer(layer) - self.apply_to_document(*verb) - - def select_all_in_layer(self, layer): - self.apply_to_layer(layer, 'EditSelectAll') - - def apply_to_layer_content(self, layer, *verbs): - self.select_all_in_layer(layer) - self.apply_to_document(*verbs) - - def _go_to_layer(self, layer, with_selection = False): - if self._current_layer_index is None: - # Initialize to a known state. We cannot assume that any layer is selected and thus we need as many LayerPrev as we have layers. - self._current_layer_index = len(self._layers) - self._go_to_layer(self._layers[0]) - - target_index = self._layers.index(layer) - - if self._current_layer_index < target_index: - for _ in range(target_index - self._current_layer_index): - self.apply_to_document('LayerMoveToNext' if with_selection else 'LayerNext') - elif self._current_layer_index > target_index: - for _ in range(self._current_layer_index - target_index): - self.apply_to_document('LayerMoveToPrev' if with_selection else 'LayerPrev') - else: - return - - if with_selection: - # When using LayerMoveToNext and LayerMoveToPrev, inkscape does not reliably select the next/previous layer. - self._current_layer_index = None - else: - self._current_layer_index = target_index - - def duplicate_layer(self, layer): - self.apply_to_layer(layer, 'LayerDuplicate') - - # Inkscape 0.91 places a duplicated layer above (after) the selected one and selects the new layer. - new_layer = Layer(layer.inkscape_name + ' copy', layer.export_name, layer.use_paths) - self._current_layer_index += 1 - self._layers.insert(self._current_layer_index, new_layer) - - return new_layer - - def delete_layer(self, layer): - self.apply_to_layer(layer, 'LayerDelete') - - # Inkscape 0.91 selects the layer above (after) the deleted layer. - del self._layers[self._current_layer_index] - - def clear_layer(self, layer): - self.select_all_in_layer(layer) - self.apply_to_document('EditDelete') - - def move_content(self, source_layer, target_layer): - self.select_all_in_layer(source_layer) - self._go_to_layer(target_layer, True) - - def run(self): - _inkscape(self._path, self._verbs) - - @property - def layers(self): - return list(self._layers) + def __init__(self, path): + self._path = path + self._layers = get_inkscape_layers(path) + self._current_layer_index = None + self._verbs = [] + + def apply_to_document(self, *verb): + self._verbs.extend(verb) + + def apply_to_layer(self, layer, *verb): + self._go_to_layer(layer) + self.apply_to_document(*verb) + + def select_all_in_layer(self, layer): + self.apply_to_layer(layer, 'EditSelectAll') + + def apply_to_layer_content(self, layer, *verbs): + self.select_all_in_layer(layer) + self.apply_to_document(*verbs) + + def _go_to_layer(self, layer, with_selection=False): + if self._current_layer_index is None: + # Initialize to a known state. We cannot assume that any layer is + # selected and thus we need as many LayerPrev as we have layers. + self._current_layer_index = len(self._layers) + self._go_to_layer(self._layers[0]) + + target_index = self._layers.index(layer) + + if self._current_layer_index < target_index: + for _ in range(target_index - self._current_layer_index): + self.apply_to_document( + 'LayerMoveToNext' if with_selection else 'LayerNext') + elif self._current_layer_index > target_index: + for _ in range(self._current_layer_index - target_index): + self.apply_to_document( + 'LayerMoveToPrev' if with_selection else 'LayerPrev') + else: + return + + if with_selection: + # When using LayerMoveToNext and LayerMoveToPrev, inkscape does + # not reliably select the next/previous layer. + self._current_layer_index = None + else: + self._current_layer_index = target_index + + def duplicate_layer(self, layer): + self.apply_to_layer(layer, 'LayerDuplicate') + + # Inkscape 0.91 places a duplicated layer above (after) the selected + # one and selects the new layer. + new_layer = Layer(layer.inkscape_name + ' copy', layer.export_name, layer.use_paths) + self._current_layer_index += 1 + self._layers.insert(self._current_layer_index, new_layer) + + return new_layer + + def delete_layer(self, layer): + self.apply_to_layer(layer, 'LayerDelete') + + # Inkscape 0.91 selects the layer above (after) the deleted layer. + del self._layers[self._current_layer_index] + + def clear_layer(self, layer): + self.select_all_in_layer(layer) + self.apply_to_document('EditDelete') + + def move_content(self, source_layer, target_layer): + self.select_all_in_layer(source_layer) + self._go_to_layer(target_layer, True) + + def run(self): + _inkscape(self._path, self._verbs) + + @property + def layers(self): + return list(self._layers) |