1#!/usr/local/bin/python3.8 2# coding=utf-8 3# 4# Copyright (C) 2005,2007,2008 Aaron Spike, aaron@ekips.org 5# Copyright (C) 2008,2010 Alvin Penner, penner@vaxxine.com 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20# 21""" 22This file output script for Inkscape creates a AutoCAD R14 DXF file. 23The spec can be found here: http://www.autodesk.com/techpubs/autocad/acadr14/dxf/index.htm. 24 25 File history: 26 - template dxf_outlines.dxf added Feb 2008 by Alvin Penner 27- ROBO-Master output option added Aug 2008 28- ROBO-Master multispline output added Sept 2008 29- LWPOLYLINE output modification added Dec 2008 30- toggle between LINE/LWPOLYLINE added Jan 2010 31- support for transform elements added July 2010 32- support for layers added July 2010 33- support for rectangle added Dec 2010 34""" 35 36from __future__ import print_function 37 38import inkex 39from inkex import colors, bezier, Transform, Group, Layer, Use, PathElement, \ 40 Rectangle, Line, Circle, Ellipse 41 42 43def get_matrix(u, i, j): 44 if j == i + 2: 45 return (u[i]-u[i-1])*(u[i]-u[i-1])/(u[i+2]-u[i-1])/(u[i+1]-u[i-1]) 46 elif j == i + 1: 47 return ((u[i]-u[i-1])*(u[i+2]-u[i])/(u[i+2]-u[i-1]) \ 48 + (u[i+1]-u[i])*(u[i]-u[i-2])/(u[i+1]-u[i-2]))/(u[i+1]-u[i-1]) 49 elif j == i: 50 return (u[i+1]-u[i])*(u[i+1]-u[i])/(u[i+1]-u[i-2])/(u[i+1]-u[i-1]) 51 else: 52 return 0 53 54def get_fit(u, csp, col): 55 return (1-u)**3*csp[0][col] + 3*(1-u)**2*u*csp[1][col] \ 56 + 3*(1-u)*u**2*csp[2][col] + u**3*csp[3][col] 57 58class DxfOutlines(inkex.OutputExtension): 59 def add_arguments(self, pars): 60 pars.add_argument("--tab") 61 pars.add_argument("-R", "--ROBO", type=inkex.Boolean, default=False) 62 pars.add_argument("-P", "--POLY", type=inkex.Boolean, default=False) 63 pars.add_argument("--units", default="72./96") # Points 64 pars.add_argument("--encoding", dest="char_encode", default="latin_1") 65 pars.add_argument("--layer_option", default="all") 66 pars.add_argument("--layer_name") 67 68 self.dxf = [] 69 self.handle = 255 # handle for DXF ENTITY 70 self.layers = ['0'] 71 self.layer = '0' # mandatory layer 72 self.layernames = [] 73 self.csp_old = [[0.0, 0.0]] * 4 # previous spline 74 self.d = [0.0] # knot vector 75 self.poly = [[0.0, 0.0]] # LWPOLYLINE data 76 77 def save(self, stream): 78 stream.write(b''.join(self.dxf)) 79 80 def dxf_add(self, str): 81 self.dxf.append(str.encode(self.options.char_encode)) 82 83 def dxf_line(self, csp): 84 """Draw a line in the DXF format""" 85 self.handle += 1 86 self.dxf_add(" 0\nLINE\n 5\n%x\n100\nAcDbEntity\n 8\n%s\n 62\n%d\n100\nAcDbLine\n" % (self.handle, self.layer, self.color)) 87 self.dxf_add(" 10\n%f\n 20\n%f\n 30\n0.0\n 11\n%f\n 21\n%f\n 31\n0.0\n" % (csp[0][0], csp[0][1], csp[1][0], csp[1][1])) 88 89 def LWPOLY_line(self, csp): 90 if (abs(csp[0][0] - self.poly[-1][0]) > .0001 91 or abs(csp[0][1] - self.poly[-1][1]) > .0001 92 or self.color_LWPOLY != self.color): # THIS LINE IS NEW 93 self.LWPOLY_output() # terminate current polyline 94 self.poly = [csp[0]] # initiallize new polyline 95 self.color_LWPOLY = self.color 96 self.layer_LWPOLY = self.layer 97 self.poly.append(csp[1]) 98 99 def LWPOLY_output(self): 100 if len(self.poly) == 1: 101 return 102 self.handle += 1 103 closed = 1 104 if (abs(self.poly[0][0] - self.poly[-1][0]) > .0001 105 or abs(self.poly[0][1] - self.poly[-1][1]) > .0001): 106 closed = 0 107 self.dxf_add(" 0\nLWPOLYLINE\n 5\n%x\n100\nAcDbEntity\n 8\n%s\n 62\n%d\n100\nAcDbPolyline\n 90\n%d\n 70\n%d\n" % (self.handle, self.layer_LWPOLY, self.color_LWPOLY, len(self.poly) - closed, closed)) 108 for i in range(len(self.poly) - closed): 109 self.dxf_add(" 10\n%f\n 20\n%f\n 30\n0.0\n" % (self.poly[i][0], self.poly[i][1])) 110 111 def dxf_spline(self, csp): 112 knots = 8 113 ctrls = 4 114 self.handle += 1 115 self.dxf_add(" 0\nSPLINE\n 5\n%x\n100\nAcDbEntity\n 8\n%s\n 62\n%d\n100\nAcDbSpline\n" % (self.handle, self.layer, self.color)) 116 self.dxf_add(" 70\n8\n 71\n3\n 72\n%d\n 73\n%d\n 74\n0\n" % (knots, ctrls)) 117 for i in range(2): 118 for j in range(4): 119 self.dxf_add(" 40\n%d\n" % i) 120 for i in csp: 121 self.dxf_add(" 10\n%f\n 20\n%f\n 30\n0.0\n" % (i[0], i[1])) 122 123 def ROBO_spline(self, csp): 124 """this spline has zero curvature at the endpoints, as in ROBO-Master""" 125 if (abs(csp[0][0] - self.csp_old[3][0]) > .0001 126 or abs(csp[0][1] - self.csp_old[3][1]) > .0001 127 or abs((csp[1][1] - csp[0][1]) * (self.csp_old[3][0] - self.csp_old[2][0]) - (csp[1][0] - csp[0][0]) * (self.csp_old[3][1] - self.csp_old[2][1])) > .001): 128 self.ROBO_output() # terminate current spline 129 self.xfit = [csp[0][0]] # initiallize new spline 130 self.yfit = [csp[0][1]] 131 self.d = [0.0] 132 self.color_ROBO = self.color 133 self.layer_ROBO = self.layer 134 self.xfit += 3 * [0.0] 135 self.yfit += 3 * [0.0] 136 self.d += 3 * [0.0] 137 for i in range(1, 4): 138 j = len(self.d) + i - 4 139 self.xfit[j] = get_fit(i / 3.0, csp, 0) 140 self.yfit[j] = get_fit(i / 3.0, csp, 1) 141 self.d[j] = self.d[j - 1] + bezier.pointdistance((self.xfit[j - 1], self.yfit[j - 1]), (self.xfit[j], self.yfit[j])) 142 self.csp_old = csp 143 144 def ROBO_output(self): 145 try: 146 import numpy 147 from numpy.linalg import solve 148 except ImportError: 149 inkex.errormsg("Failed to import the numpy or numpy.linalg modules. These modules are required by the ROBO option. Please install them and try again.") 150 return 151 152 if len(self.d) == 1: 153 return 154 fits = len(self.d) 155 ctrls = fits + 2 156 knots = ctrls + 4 157 self.xfit += 2 * [0.0] # pad with 2 endpoint constraints 158 self.yfit += 2 * [0.0] 159 self.d += 6 * [0.0] # pad with 3 duplicates at each end 160 self.d[fits + 2] = self.d[fits + 1] = self.d[fits] = self.d[fits - 1] 161 162 solmatrix = numpy.zeros((ctrls, ctrls), dtype=float) 163 for i in range(fits): 164 solmatrix[i, i] = get_matrix(self.d, i, i) 165 solmatrix[i, i + 1] = get_matrix(self.d, i, i + 1) 166 solmatrix[i, i + 2] = get_matrix(self.d, i, i + 2) 167 solmatrix[fits, 0] = self.d[2] / self.d[fits - 1] # curvature at start = 0 168 solmatrix[fits, 1] = -(self.d[1] + self.d[2]) / self.d[fits - 1] 169 solmatrix[fits, 2] = self.d[1] / self.d[fits - 1] 170 solmatrix[fits + 1, fits - 1] = (self.d[fits - 1] - self.d[fits - 2]) / self.d[fits - 1] # curvature at end = 0 171 solmatrix[fits + 1, fits] = (self.d[fits - 3] + self.d[fits - 2] - 2 * self.d[fits - 1]) / self.d[fits - 1] 172 solmatrix[fits + 1, fits + 1] = (self.d[fits - 1] - self.d[fits - 3]) / self.d[fits - 1] 173 xctrl = solve(solmatrix, self.xfit) 174 yctrl = solve(solmatrix, self.yfit) 175 self.handle += 1 176 self.dxf_add(" 0\nSPLINE\n 5\n%x\n100\nAcDbEntity\n 8\n%s\n 62\n%d\n100\nAcDbSpline\n" % (self.handle, self.layer_ROBO, self.color_ROBO)) 177 self.dxf_add(" 70\n0\n 71\n3\n 72\n%d\n 73\n%d\n 74\n%d\n" % (knots, ctrls, fits)) 178 for i in range(knots): 179 self.dxf_add(" 40\n%f\n" % self.d[i - 3]) 180 for i in range(ctrls): 181 self.dxf_add(" 10\n%f\n 20\n%f\n 30\n0.0\n" % (xctrl[i], yctrl[i])) 182 for i in range(fits): 183 self.dxf_add(" 11\n%f\n 21\n%f\n 31\n0.0\n" % (self.xfit[i], self.yfit[i])) 184 185 def process_shape(self, node, mat): 186 rgb = (0, 0, 0) 187 style = node.get('style') 188 if style: 189 style = dict(inkex.Style.parse_str(style)) 190 if 'stroke' in style: 191 if style['stroke'] and style['stroke'] != 'none' and style['stroke'][0:3] != 'url': 192 rgb = inkex.Color(style['stroke']).to_rgb() 193 hsl = colors.rgb_to_hsl(rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0) 194 self.color = 7 # default is black 195 if hsl[2]: 196 self.color = 1 + (int(6 * hsl[0] + 0.5) % 6) # use 6 hues 197 198 if not isinstance(node, (PathElement, Rectangle, Line, Circle, Ellipse)): 199 return 200 201 # Transforming /after/ superpath is more reliable than before 202 # because of some issues with arcs in transformations 203 for sub in node.path.to_superpath().transform(Transform(mat) * node.transform): 204 for i in range(len(sub) - 1): 205 s = sub[i] 206 e = sub[i + 1] 207 if s[1] == s[2] and e[0] == e[1]: 208 if self.options.POLY: 209 self.LWPOLY_line([s[1], e[1]]) 210 else: 211 self.dxf_line([s[1], e[1]]) 212 elif self.options.ROBO: 213 self.ROBO_spline([s[1], s[2], e[0], e[1]]) 214 else: 215 self.dxf_spline([s[1], s[2], e[0], e[1]]) 216 217 def process_clone(self, node): 218 """Process a clone node, looking for internal paths""" 219 trans = node.get('transform') 220 x = node.get('x') 221 y = node.get('y') 222 mat = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) 223 if trans: 224 mat *= Transform(trans) 225 if x: 226 mat *= Transform([[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]]) 227 if y: 228 mat *= Transform([[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]]) 229 # push transform 230 if trans or x or y: 231 self.groupmat.append(Transform(self.groupmat[-1]) * mat) 232 # get referenced node 233 refid = node.get('xlink:href') 234 refnode = self.svg.getElementById(refid[1:]) 235 if refnode is not None: 236 if isinstance(refnode, Group): 237 self.process_group(refnode) 238 elif isinstance(refnode, Use): 239 self.process_clone(refnode) 240 else: 241 self.process_shape(refnode, self.groupmat[-1]) 242 # pop transform 243 if trans or x or y: 244 self.groupmat.pop() 245 246 def process_group(self, group): 247 """Process group elements""" 248 if isinstance(group, Layer): 249 style = group.style 250 if style.get('display', '') == 'none' and self.options.layer_option and self.options.layer_option == 'visible': 251 return 252 layer = group.label 253 if self.options.layer_name and self.options.layer_option == 'name': 254 if not layer.lower() in self.options.layer_name: 255 return 256 257 layer = layer.replace(' ', '_') 258 if layer in self.layers: 259 self.layer = layer 260 trans = group.get('transform') 261 if trans: 262 self.groupmat.append(Transform(self.groupmat[-1]) * Transform(trans)) 263 for node in group: 264 if isinstance(node, Group): 265 self.process_group(node) 266 elif isinstance(node, Use): 267 self.process_clone(node) 268 else: 269 self.process_shape(node, self.groupmat[-1]) 270 if trans: 271 self.groupmat.pop() 272 273 def effect(self): 274 # Warn user if name match field is empty 275 if self.options.layer_option and self.options.layer_option == 'name' and not self.options.layer_name: 276 return inkex.errormsg("Error: Field 'Layer match name' must be filled when using 'By name match' option") 277 278 # Split user layer data into a list: "layerA,layerb,LAYERC" becomes ["layera", "layerb", "layerc"] 279 if self.options.layer_name: 280 self.options.layer_name = self.options.layer_name.lower().split(',') 281 282 # References: Minimum Requirements for Creating a DXF File of a 3D Model By Paul Bourke 283 # NURB Curves: A Guide for the Uninitiated By Philip J. Schneider 284 # The NURBS Book By Les Piegl and Wayne Tiller (Springer, 1995) 285 # self.dxf_add("999\nDXF created by Inkscape\n") # Some programs do not take comments in DXF files (KLayout 0.21.12 for example) 286 with open(self.get_resource('dxf14_header.txt'), 'r') as fhl: 287 self.dxf_add(fhl.read()) 288 for node in self.svg.xpath('//svg:g'): 289 if isinstance(node, Layer): 290 layer = node.label 291 self.layernames.append(layer.lower()) 292 if self.options.layer_name and self.options.layer_option and self.options.layer_option == 'name' and not layer.lower() in self.options.layer_name: 293 continue 294 layer = layer.replace(' ', '_') 295 if layer and layer not in self.layers: 296 self.layers.append(layer) 297 self.dxf_add(" 2\nLAYER\n 5\n2\n100\nAcDbSymbolTable\n 70\n%s\n" % len(self.layers)) 298 for i in range(len(self.layers)): 299 self.dxf_add(" 0\nLAYER\n 5\n%x\n100\nAcDbSymbolTableRecord\n100\nAcDbLayerTableRecord\n 2\n%s\n 70\n0\n 6\nCONTINUOUS\n" % (i + 80, self.layers[i])) 300 with open(self.get_resource('dxf14_style.txt'), 'r') as fhl: 301 self.dxf_add(fhl.read()) 302 303 scale = eval(self.options.units) 304 if not scale: 305 scale = 25.4 / 96 # if no scale is specified, assume inch as baseunit 306 scale /= self.svg.unittouu('1px') 307 h = self.svg.height 308 doc = self.document.getroot() 309 # process viewBox height attribute to correct page scaling 310 viewBox = doc.get('viewBox') 311 if viewBox: 312 viewBox2 = viewBox.split(',') 313 if len(viewBox2) < 4: 314 viewBox2 = viewBox.split(' ') 315 scale *= h / self.svg.unittouu(self.svg.add_unit(viewBox2[3])) 316 self.groupmat = [[[scale, 0.0, 0.0], [0.0, -scale, h * scale]]] 317 self.process_group(doc) 318 if self.options.ROBO: 319 self.ROBO_output() 320 if self.options.POLY: 321 self.LWPOLY_output() 322 with open(self.get_resource('dxf14_footer.txt'), 'r') as fhl: 323 self.dxf_add(fhl.read()) 324 # Warn user if layer data seems wrong 325 if self.options.layer_name and self.options.layer_option and self.options.layer_option == 'name': 326 for layer in self.options.layer_name: 327 if layer not in self.layernames: 328 inkex.errormsg("Warning: Layer '%s' not found!" % layer) 329 330 331if __name__ == '__main__': 332 DxfOutlines().run() 333