From c74efa59dc37b06cc1aeddd0ba736552a5b9f35e Mon Sep 17 00:00:00 2001
From: Michael Schwarz <michi.schwarz@gmail.com>
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(-)

(limited to 'support')

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