From 3566dbc9438935d7cc6951a882a9d2ca148fb58d Mon Sep 17 00:00:00 2001 From: Michael Schwarz Date: Wed, 20 Sep 2017 20:33:29 +0200 Subject: PEP8 --- support/asymptote/__main__.py | 102 +++++---- support/inkscape/__main__.py | 108 ++++----- support/inkscape/effect.py | 520 ++++++++++++++++++++++-------------------- support/inkscape/inkscape.py | 226 +++++++++--------- support/lib/make.py | 2 +- support/lib/util.py | 197 ++++++++-------- support/openscad/__main__.py | 70 +++--- 7 files changed, 635 insertions(+), 590 deletions(-) (limited to 'support') diff --git a/support/asymptote/__main__.py b/support/asymptote/__main__.py index 705649a..77bbdc7 100644 --- a/support/asymptote/__main__.py +++ b/support/asymptote/__main__.py @@ -3,55 +3,61 @@ from lib import util, make def _asymptote(in_path, out_path, asymptote_dir, working_dir): - args = [os.environ['ASYMPTOTE'], '-vv', '-f', 'pdf', '-o', out_path, in_path] - - with util.command_context(args, set_env = { 'ASYMPTOTE_DIR': asymptote_dir }, working_dir = working_dir, use_stderr = True) as process: - def get_loaded_file(line): - if any(line.startswith(j) for j in ['Loading ', 'Including ']): - parts = line.rstrip('\n').split(' ') - - if len(parts) == 4: - _, _, from_, path = parts - - if from_ == 'from': - return path - - return None - - def iter_loaded_files(): - for i in process.stderr: - loaded_file = get_loaded_file(i) - - if loaded_file is not None: - yield loaded_file - elif not any(i.startswith(j) for j in ['cd ', 'Using configuration ']): - print >> sys.stderr, i, - - loaded_files = list(iter_loaded_files()) - - return loaded_files + args = [os.environ['ASYMPTOTE'], '-vv', '-f', 'pdf', '-o', out_path, in_path] + + with util.command_context(args, set_env={'ASYMPTOTE_DIR': asymptote_dir}, working_dir=working_dir, use_stderr=True) as process: + def get_loaded_file(line): + if any(line.startswith(j) for j in ['Loading ', 'Including ']): + parts = line.rstrip('\n').split(' ') + + if len(parts) == 4: + _, _, from_, path = parts + + if from_ == 'from': + return path + + return None + + def iter_loaded_files(): + for i in process.stderr: + loaded_file = get_loaded_file(i) + + if loaded_file is not None: + yield loaded_file + elif not any(i.startswith(j) for j in ['cd ', 'Using configuration ']): + print >> sys.stderr, i, + + loaded_files = list(iter_loaded_files()) + + return loaded_files @util.main def main(in_path, out_path): - try: - _, out_suffix = os.path.splitext(out_path) - - with util.TemporaryDirectory() as temp_dir: - absolute_in_path = os.path.abspath(in_path) - temp_out_path = os.path.join(temp_dir, 'out.pdf') - - # Asymptote creates A LOT of temp files (presumably when invoking LaTeX) and leaves some of them behind. Thus we run asymptote in a temporary directory. - loaded_files = _asymptote(absolute_in_path, 'out', os.path.dirname(absolute_in_path), temp_dir) - - if not os.path.exists(temp_out_path): - raise util.UserError('Asymptote did not generate a PDF file.', in_path) - - # All dependencies as paths relative to the project root. - dependencies = set(map(os.path.relpath, loaded_files)) - - # Write output files. - make.write_dependencies(out_path + '.d', out_path, dependencies - { in_path }) - shutil.copyfile(temp_out_path, out_path) - except util.UserError as e: - raise util.UserError('While processing {}: {}', in_path, e) + try: + _, out_suffix = os.path.splitext(out_path) + + with util.TemporaryDirectory() as temp_dir: + absolute_in_path = os.path.abspath(in_path) + temp_out_path = os.path.join(temp_dir, 'out.pdf') + + # Asymptote creates A LOT of temp files (presumably when invoking + # LaTeX) and leaves some of them behind. Thus we run asymptote + # in a temporary directory. + loaded_files = _asymptote( + absolute_in_path, + 'out', + os.path.dirname(absolute_in_path), + temp_dir) + + if not os.path.exists(temp_out_path): + raise util.UserError('Asymptote did not generate a PDF file.', in_path) + + # All dependencies as paths relative to the project root. + dependencies = set(map(os.path.relpath, loaded_files)) + + # Write output files. + make.write_dependencies(out_path + '.d', out_path, dependencies - {in_path}) + shutil.copyfile(temp_out_path, out_path) + except util.UserError as e: + raise util.UserError('While processing {}: {}', in_path, e) 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) diff --git a/support/lib/make.py b/support/lib/make.py index 3502ef6..c89b306 100644 --- a/support/lib/make.py +++ b/support/lib/make.py @@ -2,4 +2,4 @@ from . import util def write_dependencies(path, target, dependencies): - util.write_file(path, '{}: {}\n'.format(target, ' '.join(dependencies)).encode()) + util.write_file(path, '{}: {}\n'.format(target, ' '.join(dependencies)).encode()) diff --git a/support/lib/util.py b/support/lib/util.py index 8b38bc2..c00a5fe 100644 --- a/support/lib/util.py +++ b/support/lib/util.py @@ -1,117 +1,130 @@ -import sys, contextlib, subprocess, tempfile, shutil, re, os, inspect +import contextlib +import inspect +import os +import re +import shutil +import subprocess +import sys +import tempfile class UserError(Exception): - def __init__(self, message, *args): - super(UserError, self).__init__(message.format(*args)) + def __init__(self, message, *args): + super(UserError, self).__init__(message.format(*args)) def main(fn): - """Decorator for "main" functions. Decorates a function that should be called when the containing module is run as a script (e.g. via python -m ).""" - - frame = inspect.currentframe().f_back - - def wrapped_fn(*args, **kwargs): - try: - fn(*args, **kwargs) - except UserError as e: - print >> sys.stderr, 'Error:', e - sys.exit(1) - except KeyboardInterrupt: - sys.exit(2) - - if frame.f_globals['__name__'] == '__main__': - wrapped_fn(*sys.argv[1:]) - - # Allow the main function also to be called explicitly - return wrapped_fn + """ + Decorator for "main" functions. Decorates a function that should be + called when the containing module is run as a script (e.g. via python -m + ). + """ + frame = inspect.currentframe().f_back + + def wrapped_fn(*args, **kwargs): + try: + fn(*args, **kwargs) + except UserError as e: + print >> sys.stderr, 'Error:', e + sys.exit(1) + except KeyboardInterrupt: + sys.exit(2) + + if frame.f_globals['__name__'] == '__main__': + wrapped_fn(*sys.argv[1:]) + + # Allow the main function also to be called explicitly + return wrapped_fn def rename_atomic(source_path, target_path): - """ - Move the file at source_path to target_path. - - If both paths reside on the same device, os.rename() is used, otherwise the file is copied to a temporary name next to target_path and moved from there using os.rename(). - """ - - source_dir_stat = os.stat(os.path.dirname(source_path)) - target_dir_stat = os.stat(os.path.dirname(target_path)) - - if source_dir_stat.st_dev == target_dir_stat.st_dev: - os.rename(source_path, target_path) - else: - temp_path = target_path + '~' - - shutil.copyfile(source_path, temp_path) - os.rename(temp_path, target_path) + """ + Move the file at source_path to target_path. + + If both paths reside on the same device, os.rename() is used, otherwise + the file is copied to a temporary name next to target_path and moved from + there using os.rename(). + """ + source_dir_stat = os.stat(os.path.dirname(source_path)) + target_dir_stat = os.stat(os.path.dirname(target_path)) + + if source_dir_stat.st_dev == target_dir_stat.st_dev: + os.rename(source_path, target_path) + else: + temp_path = target_path + '~' + + shutil.copyfile(source_path, temp_path) + os.rename(temp_path, target_path) @contextlib.contextmanager def TemporaryDirectory(): - dir = tempfile.mkdtemp() - - try: - yield dir - finally: - shutil.rmtree(dir) + dir = tempfile.mkdtemp() + + try: + yield dir + finally: + shutil.rmtree(dir) @contextlib.contextmanager -def command_context(args, remove_env = [], set_env = { }, working_dir = None, use_stderr = False): - env = dict(os.environ) - - for i in remove_env: - del env[i] - - for k, v in set_env.items(): - env[k] = v - - if use_stderr: - stderr = subprocess.PIPE - else: - stderr = None - - try: - process = subprocess.Popen(args, env = env, cwd = working_dir, stderr = stderr) - except OSError as e: - raise UserError('Error running {}: {}', args[0], e) - - try: - yield process - except: - try: - process.kill() - except OSError: - # Ignore exceptions here so we don't mask the already-being-thrown exception. - pass - - raise - finally: - # Use communicate so that we won't deadlock if the process generates some unread output. - process.communicate() - - if process.returncode: - raise UserError('Command failed: {}', ' '.join(args)) - - -def command(args, remove_env = [], set_env = { }, working_dir = None): - with command_context(args, remove_env, set_env, working_dir): - pass +def command_context(args, remove_env=[], set_env={}, working_dir=None, use_stderr=False): + env = dict(os.environ) + + for i in remove_env: + del env[i] + + for k, v in set_env.items(): + env[k] = v + + if use_stderr: + stderr = subprocess.PIPE + else: + stderr = None + + try: + process = subprocess.Popen(args, env=env, cwd=working_dir, stderr=stderr) + except OSError as e: + raise UserError('Error running {}: {}', args[0], e) + + try: + yield process + except: + try: + process.kill() + except OSError: + # Ignore exceptions here so we don't mask the + # already-being-thrown exception. + pass + + raise + finally: + # Use communicate so that we won't deadlock if the process generates + # some unread output. + process.communicate() + + if process.returncode: + raise UserError('Command failed: {}', ' '.join(args)) + + +def command(args, remove_env=[], set_env={}, working_dir=None): + with command_context(args, remove_env, set_env, working_dir): + pass def bash_escape_string(string): - return "'{}'".format(re.sub("'", "'\"'\"'", string)) + return "'{}'".format(re.sub("'", "'\"'\"'", string)) def write_file(path, data): - temp_path = path + '~' - - with open(temp_path, 'wb') as file: - file.write(data) - - os.rename(temp_path, path) + temp_path = path + '~' + + with open(temp_path, 'wb') as file: + file.write(data) + + os.rename(temp_path, path) def read_file(path): - with open(path, 'rb') as file: - return file.read() + with open(path, 'rb') as file: + return file.read() diff --git a/support/openscad/__main__.py b/support/openscad/__main__.py index 7aeaa31..85f8b99 100644 --- a/support/openscad/__main__.py +++ b/support/openscad/__main__.py @@ -1,42 +1,46 @@ import os + from lib import util, make def _openscad(in_path, out_path, deps_path): - util.command([os.environ['OPENSCAD'], '-o', out_path, '-d', deps_path, in_path]) + util.command([os.environ['OPENSCAD'], '-o', out_path, '-d', deps_path, in_path]) @util.main def main(in_path, out_path): - cwd = os.getcwd() - - def relpath(path): - return os.path.relpath(path, cwd) - - with util.TemporaryDirectory() as temp_dir: - temp_deps_path = os.path.join(temp_dir, 'deps') - temp_mk_path = os.path.join(temp_dir, 'mk') - temp_files_path = os.path.join(temp_dir, 'files') - - _, out_ext = os.path.splitext(out_path) - - # OpenSCAD requires the output file name to end in .stl or .dxf. - temp_out_path = os.path.join(temp_dir, 'out' + out_ext) - - _openscad(in_path, temp_out_path, temp_deps_path) - - mk_content = '%:; echo "$@" >> {}'.format(util.bash_escape_string(temp_files_path)) - - # Use make to parse the dependency makefile written by OpenSCAD. - util.write_file(temp_mk_path, mk_content.encode()) - util.command(['make', '-s', '-B', '-f', temp_mk_path, '-f', temp_deps_path], remove_env = ['MAKELEVEL', 'MAKEFLAGS']) - - # All dependencies as paths relative to the project root. - deps = set(map(relpath, util.read_file(temp_files_path).decode().splitlines())) - - # Relative paths to all files that should not appear in the dependency makefile. - ignored_files = set(map(relpath, [in_path, temp_deps_path, temp_mk_path, temp_out_path])) - - # Write output files. - make.write_dependencies(out_path + '.d', out_path, deps - ignored_files) - util.rename_atomic(temp_out_path, out_path) + cwd = os.getcwd() + + def relpath(path): + return os.path.relpath(path, cwd) + + with util.TemporaryDirectory() as temp_dir: + temp_deps_path = os.path.join(temp_dir, 'deps') + temp_mk_path = os.path.join(temp_dir, 'mk') + temp_files_path = os.path.join(temp_dir, 'files') + + _, out_ext = os.path.splitext(out_path) + + # OpenSCAD requires the output file name to end in .stl or .dxf. + temp_out_path = os.path.join(temp_dir, 'out' + out_ext) + + _openscad(in_path, temp_out_path, temp_deps_path) + + mk_content = '%:; echo "$@" >> {}'.format(util.bash_escape_string(temp_files_path)) + + # Use make to parse the dependency makefile written by OpenSCAD. + util.write_file(temp_mk_path, mk_content.encode()) + util.command( + ['make', '-s', '-B', '-f', temp_mk_path, '-f', temp_deps_path], + remove_env=['MAKELEVEL', 'MAKEFLAGS']) + + # All dependencies as paths relative to the project root. + deps = set(map(relpath, util.read_file(temp_files_path).decode().splitlines())) + + # Relative paths to all files that should not appear in the + # dependency makefile. + ignored_files = set(map(relpath, [in_path, temp_deps_path, temp_mk_path, temp_out_path])) + + # Write output files. + make.write_dependencies(out_path + '.d', out_path, deps - ignored_files) + util.rename_atomic(temp_out_path, out_path) -- cgit