""" Based on code from Aaron Spike. See http://www.bobcookdev.com/inkscape/inkscape-dxf.html """ import collections import itertools import os import pkgutil import re from lxml import etree 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} 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.get('INKSCAPE_DXF_FLATNESS', 0.1)) 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').decode('ASCII')) def write_instruction(code, value): print(code, file=file) print(value, file=file) 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').decode('ASCII')) 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 ValueError( '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 ValueError( '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 ValueError( 'SVG document has no viewBox attribute. See ' 'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements')