From c74efa59dc37b06cc1aeddd0ba736552a5b9f35e Mon Sep 17 00:00:00 2001 From: Michael Schwarz Date: Sat, 20 Dec 2014 22:22:04 +0100 Subject: Rewritten unit conversion methods of inkex.py to properly handle viewport settings. --- support/dxf_export/__main__.py | 8 ++- support/dxf_export/effect.py | 112 +++++++++++++++++++++++++++++++++++++---- support/dxf_export/inkex.py | 62 ++++++++++++++++++++--- support/lib/util.py | 7 ++- support/openscad/__main__.py | 8 ++- 5 files changed, 178 insertions(+), 19 deletions(-) diff --git a/support/dxf_export/__main__.py b/support/dxf_export/__main__.py index 676935e..c770a0e 100644 --- a/support/dxf_export/__main__.py +++ b/support/dxf_export/__main__.py @@ -76,4 +76,10 @@ def main(in_path, out_path): _export_dxf(temp_svg_path, out_path) -main(*sys.argv[1:]) +try: + main(*sys.argv[1:]) +except util.UserError as e: + print 'Error:', e + sys.exit(1) +except KeyboardInterrupt: + sys.exit(2) diff --git a/support/dxf_export/effect.py b/support/dxf_export/effect.py index 09699b0..206ce9d 100644 --- a/support/dxf_export/effect.py +++ b/support/dxf_export/effect.py @@ -2,17 +2,70 @@ Based on code from Aaron Spike. See http://www.bobcookdev.com/inkscape/inkscape-dxf.html """ -import pkgutil +import pkgutil, re from . import inkex, simpletransform, cubicsuperpath, cspsubdiv +def _get_unit_factors_map(): + # Fluctuates somewhat between Inkscape releases. + 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 DXFExportEffect(inkex.Effect): + _unit_factors = _get_unit_factors_map() + def __init__(self): inkex.Effect.__init__(self) + self._dxf_instructions = [] self._handle = 255 self._flatness = 0.1 + def _get_user_unit(self): + """ + Return the size in pixels of the unit used for measures without an explicit unit. + """ + + document_height = self._measure_to_pixels(self._get_document_height_attr()) + view_box_attr = self.document.getroot().get('viewBox') + + if view_box_attr: + _, _, _, view_box_height = map(float, view_box_attr.split()) + else: + view_box_height = document_height + + return document_height / view_box_height + + def _get_document_unit(self): + """ + Return the size in pixels that the user is working with in Inkscape. + """ + + inkscape_unit_attrs = self.document.getroot().xpath('./sodipodi:namedview/@inkscape:document-units', namespaces = inkex.NSS) + + if inkscape_unit_attrs: + unit = inkscape_unit_attrs[0] + else: + _, unit = self._parse_measure(self._get_document_height_attr()) + + return self._get_unit_factor(unit) + + def _get_document_height_attr(self): + return self.document.getroot().xpath('@height', namespaces = inkex.NSS)[0] + def _add_instruction(self, code, value): self._dxf_instructions.append((code, str(value))) @@ -40,24 +93,31 @@ class DXFExportEffect(inkex.Effect): e = sub[i + 1] self._add_dxf_line(layer, [s[1], e[1]]) - def _add_dxf_shape(self, node, document_transformation): + def _add_dxf_shape(self, node, document_transform, element_transform): layer = self._get_inkscape_layer(node) path = cubicsuperpath.parsePath(node.get('d')) - transformation = simpletransform.composeTransform( - document_transformation, - simpletransform.composeParents(node, [[1, 0, 0], [0, 1, 0]])) + transform = simpletransform.composeTransform( + document_transform, + simpletransform.composeParents(node, element_transform)) - simpletransform.applyTransformToPath(transformation, path) + simpletransform.applyTransformToPath(transform, path) self._add_dxf_path(layer, path) def effect(self): - height = self.unittouu(self.document.getroot().xpath('@height', namespaces = inkex.NSS)[0]) - document_transformation = [[1, 0, 0], [0, -1, height]] + user_unit = self._get_user_unit() + document_unit = self._get_document_unit() + height = self._measure_to_pixels(self._get_document_height_attr()) + + document_transform = simpletransform.composeTransform( + [[1 / document_unit, 0, 0], [0, 1 / document_unit, 0]], + [[1, 0, 0], [0, -1, height]]) + + element_transform = [[user_unit, 0, 0], [0, user_unit, 0]] for node in self.document.getroot().xpath('//svg:path', namespaces = inkex.NSS): - self._add_dxf_shape(node, document_transformation) + self._add_dxf_shape(node, document_transform, element_transform) def write(self, file): file.write(pkgutil.get_data(__name__, 'dxf_header.txt')) @@ -68,6 +128,30 @@ class DXFExportEffect(inkex.Effect): file.write(pkgutil.get_data(__name__, 'dxf_footer.txt')) + @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, default_unit_factor = None): + """ + Parse a string containing a measure and return it's value converted to pixels. If the measure has no unit, it will be assumed that the unit has the size of the specified number of pixels. + """ + + value, unit = cls._parse_measure(string) + + return value * cls._get_unit_factor(unit, default_unit_factor) + @classmethod def _get_inkscape_layer(cls, node): while node is not None: @@ -79,3 +163,13 @@ class DXFExportEffect(inkex.Effect): node = node.getparent() return '' + + @classmethod + def _get_unit_factor(cls, unit, default = None): + if unit is None: + if default is None: + default = 1 + + return default + else: + return cls._unit_factors[unit] diff --git a/support/dxf_export/inkex.py b/support/dxf_export/inkex.py index 0f88000..19e860b 100755 --- a/support/dxf_export/inkex.py +++ b/support/dxf_export/inkex.py @@ -101,6 +101,13 @@ def errormsg(msg): else: sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("UTF-8")) +def are_near_relative(a, b, eps): + if (a-b <= a*eps) and (a-b >= -a*eps): + return True + else: + return False + + # third party library try: from lxml import etree @@ -277,16 +284,57 @@ class Effect: retval = None return retval - def getDocumentUnit(self): - docunit = self.document.xpath('//sodipodi:namedview/@inkscape:document-units', namespaces=NSS) - if docunit: - return docunit[0] - else: - return 'px' - #a dictionary of unit to user unit conversion factors __uuconv = {'in':96.0, 'pt':1.33333333333, 'px':1.0, 'mm':3.77952755913, 'cm':37.7952755913, 'm':3779.52755913, 'km':3779527.55913, 'pc':16.0, 'yd':3456.0 , 'ft':1152.0} + + # Function returns the unit used for the values in SVG. + # For lack of an attribute in SVG that explicitly defines what units are used for SVG coordinates, + # try to calculate the unit from the SVG width and SVG viewbox. + # Defaults to 'px' units. + def getDocumentUnit(self): + svgunit = 'px' #default to pixels + + svgwidth = self.document.getroot().get('width') + viewboxstr = self.document.getroot().get('viewBox') + if viewboxstr: + unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) + param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + + p = param.match(svgwidth) + u = unitmatch.search(svgwidth) + + width = 100 #default + viewboxwidth = 100 #default + svgwidthunit = 'px' #default assume 'px' unit + if p: + width = float(p.string[p.start():p.end()]) + else: + errormsg(_("SVG Width not set correctly! Assuming width = 100")) + if u: + svgwidthunit = u.string[u.start():u.end()] + + viewboxnumbers = [] + for t in viewboxstr.split(): + try: + viewboxnumbers.append(float(t)) + except ValueError: + pass + if len(viewboxnumbers) == 4: #check for correct number of numbers + viewboxwidth = viewboxnumbers[2] + + svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth + + # try to find the svgunitfactor in the list of units known. If we don't find something, ... + eps = 0.01 #allow 1% error in factor + for key in self.__uuconv: + if are_near_relative(self.__uuconv[key], svgunitfactor, eps): + #found match! + svgunit = key; + + return svgunit + + def unittouu(self, string): '''Returns userunits given a string representation of units in another system''' unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) diff --git a/support/lib/util.py b/support/lib/util.py index bede240..8b65e58 100644 --- a/support/lib/util.py +++ b/support/lib/util.py @@ -1,6 +1,10 @@ import contextlib, subprocess, tempfile, shutil, re, os +class UserError(Exception): + pass + + @contextlib.contextmanager def TemporaryDirectory(): dir = tempfile.mkdtemp() @@ -15,7 +19,8 @@ def command(args): process = subprocess.Popen(args) process.wait() - assert not process.returncode + if process.returncode: + raise UserError('Command failed: {}'.format(' '.join(args))) def bash_escape_string(string): diff --git a/support/openscad/__main__.py b/support/openscad/__main__.py index 08b2ab6..0f319f8 100644 --- a/support/openscad/__main__.py +++ b/support/openscad/__main__.py @@ -34,4 +34,10 @@ def main(in_path, out_path, deps_path): _write_dependencies(deps_path, relpath(out_path), deps - ignored_files) -main(*sys.argv[1:]) +try: + main(*sys.argv[1:]) +except util.UserError as e: + print 'Error:', e + sys.exit(1) +except KeyboardInterrupt: + sys.exit(2) -- cgit