#!/usr/bin/env python3 # coding=utf-8 # # Copyright (C) 2008-2009 Alvin Penner, penner@vaxxine.com # 2009, Christian Mayer, inkscape@christianmayer.de # 2020, MartinOwens, doctormo@geek-2.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # """ Input a DXF file >= (AutoCAD Release 13 == AC1012) """ import os import re import sys import math from collections import defaultdict from urllib.parse import quote from lxml import etree import inkex from inkex.localization import inkex_gettext as _ global defs global block # 2021.6 global layer global svg global scale global xmin global ymin global height global style_font3 global style_direction COLORS = [ "PAD", "#FF0000", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", "#FF00FF", "#000000", "#808080", "#C0C0C0", "#FF0000", "#FF7F7F", "#CC0000", "#CC6666", "#990000", "#994C4C", "#7F0000", "#7F3F3F", "#4C0000", "#4C2626", "#FF3F00", "#FF9F7F", "#CC3300", "#CC7F66", "#992600", "#995F4C", "#7F1F00", "#7F4F3F", "#4C1300", "#4C2F26", "#FF7F00", "#FFBF7F", "#CC6600", "#CC9966", "#994C00", "#99724C", "#7F3F00", "#7F5F3F", "#4C2600", "#4C3926", "#FFBF00", "#FFDF7F", "#CC9900", "#CCB266", "#997200", "#99854C", "#7F5F00", "#7F6F3F", "#4C3900", "#4C4226", "#FFFF00", "#FFFF7F", "#CCCC00", "#CCCC66", "#989800", "#98984C", "#7F7F00", "#7F7F3F", "#4C4C00", "#4C4C26", "#BFFF00", "#DFFF7F", "#99CC00", "#B2CC66", "#729800", "#85984C", "#5F7F00", "#6F7F3F", "#394C00", "#424C26", "#7FFF00", "#BFFF7F", "#66CC00", "#99CC66", "#4C9800", "#72984C", "#3F7F00", "#5F7F3F", "#264C00", "#394C26", "#3FFF00", "#9FFF7F", "#33CC00", "#7FCC66", "#269800", "#5F984C", "#1F7F00", "#4F7F3F", "#134C00", "#2F4C26", "#00FF00", "#7FFF7F", "#00CC00", "#66CC66", "#009800", "#4C984C", "#007F00", "#3F7F3F", "#004C00", "#264C26", "#00FF3F", "#7FFF9F", "#00CC33", "#66CC7F", "#009826", "#4C985F", "#007F1F", "#3F7F4F", "#004C13", "#264C2F", "#00FF7F", "#7FFFBF", "#00CC66", "#66CC99", "#00984C", "#4C9872", "#007F3F", "#3F7F5F", "#004C26", "#264C39", "#00FFBF", "#7FFFDF", "#00CC99", "#66CCB2", "#009872", "#4C9885", "#007F5F", "#3F7F6F", "#004C39", "#264C42", "#00FFFF", "#7FFFFF", "#00CCCC", "#66CCCC", "#009898", "#4C9898", "#007F7F", "#3F7F7F", "#004C4C", "#264C4C", "#00BFFF", "#7FDFFF", "#0099CC", "#66B2CC", "#007298", "#4C8598", "#005F7F", "#3F6F7F", "#00394C", "#26424C", "#007FFF", "#7FBFFF", "#0066CC", "#6699CC", "#004C98", "#4C7298", "#003F7F", "#3F5F7F", "#00264C", "#26394C", "#003FFF", "#7F9FFF", "#0033CC", "#667FCC", "#002698", "#4C5F98", "#001F7F", "#3F4F7F", "#00134C", "#262F4C", "#0000FF", "#7F7FFF", "#0000CC", "#6666CC", "#000098", "#4C4C98", "#00007F", "#3F3F7F", "#00004C", "#26264C", "#3F00FF", "#9F7FFF", "#3300CC", "#7F66CC", "#260098", "#5F4C98", "#1F007F", "#4F3F7F", "#13004C", "#2F264C", "#7F00FF", "#BF7FFF", "#6600CC", "#9966CC", "#4C0098", "#724C98", "#3F007F", "#5F3F7F", "#26004C", "#39264C", "#BF00FF", "#DF7FFF", "#9900CC", "#B266CC", "#720098", "#854C98", "#5F007F", "#6F3F7F", "#39004C", "#42264C", "#FF00FF", "#FF7FFF", "#CC00CC", "#CC66CC", "#980098", "#984C98", "#7F007F", "#7F3F7F", "#4C004C", "#4C264C", "#FF00BF", "#FF7FDF", "#CC0099", "#CC66B2", "#980072", "#984C85", "#7F005F", "#7F3F6F", "#4C0039", "#4C2642", "#FF007F", "#FF7FBF", "#CC0066", "#CC6699", "#98004C", "#984C72", "#7F003F", "#7F3F5F", "#4C0026", "#4C2639", "#FF003F", "#FF7F9F", "#CC0033", "#CC667F", "#980026", "#984C5F", "#7F001F", "#7F3F4F", "#4C0013", "#4C262F", "#333333", "#5B5B5B", "#848484", "#ADADAD", "#D6D6D6", "#FFFFFF", ] def get_rgbcolor(dxfcolor, parent_color="#000000"): """Returns hex color code corresponding to a color value dxfcolor -- dxf code to convert to hex color code 0 (BYBLOCK) and 256 (BYLAYER) use parent_color No more differentiation is currently done Negative values are ignored (specification allows layer to be hidden here) Negative values also use parent_color parent_color -- hex color code from parent layer. Use default color '#000000' if parent layer color undefined. """ rgbcolor = None if dxfcolor in range(1, len(COLORS)): rgbcolor = COLORS[dxfcolor] if not rgbcolor: rgbcolor = parent_color return rgbcolor class ValueConstruct(defaultdict): """Store values from the DXF and provide them as named attributes""" values = { "1": ("text", "default"), "2": ("tag", "block_name"), "3": ("mtext",), "6": ("line_type",), "7": ("text_style",), "8": ("layer_name",), "10": ("x1",), "11": ("x2",), "13": ("x3",), "14": ("x4",), "20": ("y1",), "21": ("y2",), "23": ("y3",), "24": ("y4",), "40": ("scale", "knots", "radius", "width_ratio"), "41": ("ellipse_a1", "insert_scale_x", "mtext_width"), "42": ("ellipse_a2", "bulge", "insert_scale_y"), "50": ("angle",), "51": ("angle2",), "62": ("color",), "70": ("fill", "flags"), "71": ("attach_pos",), "72": ("edge_type",), "73": ("sweep",), # ccw "92": ("path_type",), "93": ("num_edges",), "230": ("extrude",), "370": ("line_weight",), } attrs = dict([(name, a) for a, b in values.items() for name in b]) def __init__(self): super().__init__(list) @classmethod def is_valid(cls, key): return key in cls.values def __getattr__(self, attr): is_list = attr.endswith("_list") key = attr[:-5] if is_list else attr if key in self.attrs: ret = self[self.attrs[key]] if not attr.endswith("_list"): return ret[0] return ret if attr.startswith("has_"): key = attr[4:] if key in self.attrs: return self.attrs[key] in self raise AttributeError(f"Can't find dxf attribute '{key}' {attr}") def __setattr__(self, attr, value): if not attr in self.attrs: raise AttributeError(f"Can't set bad dxf attribute '{attr}'") if not isinstance(value, list): value = [value] self[self.attrs[attr]] = value def adjust_coords(self, xmin, ymin, scale, extrude, height): """Adjust the x,y coordinates to fit on the page""" for xgrp in set(["10", "11", "13", "14"]) & set(self): # scale/reflect x values for i in range(len(self[xgrp])): self[xgrp][i] = scale * (extrude * self[xgrp][i] - xmin) for ygrp in set(["20", "21", "23", "24"]) & set(self): # scale y values for i in range(len(self[ygrp])): self[ygrp][i] = height - scale * (self[ygrp][i] - ymin) export_viewport = False export_endsec = False def re_hex2unichar(m): # return unichr(int(m.group(1), 16)) return chr(int(m.group(1), 16)) def formatStyle(style): return str(inkex.Style(style)) def export_text(vals): # mandatory group codes : (11, 12, 72, 73) (fit_x, fit_y, horizon, vertical) # TODO: position to display at by (x2,y2) according to 72(horizon),73(vertical) # groupcode 72:0(left),1(center),2(right),3(both side),4(middle),5(fit) # grouocode 73:0(standard),1(floor),2(center),3(ceiling) vals["71"].append(1) # attach=pos left in mtext vals["70"].append(1) # text: flags=1 return export_mtext(vals) def export_mtext(vals): # mandatory group codes : (1 or 3, 10, 20) (text, x, y) # TODO: text-format: \Font; \W; \Q; \L..\l etc if (vals.has_text or vals.has_mtext) and vals.has_x1 and vals.has_y1: x = vals.x1 y = vals.y1 # optional group codes : (21, 40, 50) (direction, text height mm, text angle) # optional group codes : 2: char style is defined at TABLES Section size = 12 # default fontsize in px if vals.has_scale: size = scale * textscale * vals.scale dx = dy = 0 if not vals.has_flags: # as mtext, putting in the box dy = size anchor = "start" if vals.has_attach_pos: if vals.attach_pos in (2, 5, 8): anchor = "middle" elif vals.attach_pos in (3, 6, 9): anchor = "end" if vals.attach_pos in (4, 5, 6): dy = size / 2 # if vals.has_text_style and vals.text_style in style_font3 : # attribs = {'x': '%f' % x, 'y': '%f' % y, 'style': 'font-size: %.3fpx; fill: %s; font-family: %s' \ # % (size, color, style_font3[vals.text_style])} # else : # attribs = {'x': '%f' % x, 'y': '%f' % y, 'style': 'font-size: %.3fpx; fill: %s; font-family: %s' % (size, color, options.font)} attribs = { "x": "%f" % x, "y": "%f" % y, "style": "font-size: %.3fpx; fill: %s; font-family: %s; text-anchor: %s" % (size, color, options.font, anchor), } angle = 0 # default angle in degrees bVertical = False if vals.has_angle: # TEXT only if vals.angle != 0: angle = vals.angle # attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)}) elif vals.has_y2 and vals.has_x2: # MTEXT # recover original data # (x,y)=(scale*(x-xmin), height-scale*(y-ymin) orgx = vals.x2 / scale + xmin orgy = -(vals.y2 - height) / scale + ymin unit = math.sqrt(orgy * orgy + orgx * orgx) if (unit < 1.01) and (unit > 0.99): ang1 = math.atan2(orgy, orgx) angle = 180 * ang1 / math.pi # attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)}) if vals.has_text_style and vals.text_style in style_direction: if style_direction[vals.text_style] & 4: # angle = -90 # attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)}) bVertical = True angle = 0 dx = size attribs = { "x": "%f" % x, "y": "%f" % y, "style": "font-size: %.3fpx; fill: %s; font-family: %s; text-anchor: %s; writing-mode: tb" % (size, color, options.font, anchor), } if angle != 0: attribs.update({"transform": "rotate (%f %f %f)" % (-angle, x, y)}) node = layer.add(inkex.TextElement(**attribs)) node.set("sodipodi:linespacing", "125%") text = "" if vals.has_mtext: text = "".join(vals.mtext_list) if vals.has_text: text += vals.text # folding long text if vals.has_mtext_width and vals.has_scale: if vals.mtext_width > 10.0 and vals.scale > 1.0: charsperline = vals.mtext_width * 2 / vals.scale # or 1.6? nochars = int(charsperline) if len(text) > charsperline: # plain text only no \P,{} better divide by space if (text.find(r"\P") < 0) and (text.find(r"{") < 0): pos = 0 while len(text) > pos: text = text[:pos] + r"\P" + text[pos:] pos += nochars + 2 text = mtext_normalize(text) lines = 0 found = text.find(r"\P") # new line while found > -1: tspan = node.add(inkex.Tspan()) if bVertical: tspan.set("y", "%f" % y) if lines > 0: tspan.set("dx", "%f" % size) else: tspan.set("sodipodi:role", "line") if lines > 0: tspan.set("x", x + dx) else: tspan.set("dx", "%f" % dx) tspan.set("dy", "%f" % dy) # tspan.text = text[:found] text1 = text[:found] mtext_separate(node, tspan, text1) text = text[(found + 2) :] found = text.find(r"\P") lines += 1 tspan = node.add(inkex.Tspan()) if bVertical: tspan.set("y", "%f" % y) if lines > 0: tspan.set("dx", "%f" % dx) else: tspan.set("sodipodi:role", "line") if lines > 0: tspan.set("x", x + dx) else: tspan.set("dx", "%f" % dx) tspan.set("dy", "%f" % dy) # tspan.text = text text1 = text mtext_separate(node, tspan, text1) def mtext_normalize(text): # { \P } -> { }\P{ } found = text.find(r"\P") while found > -1: nest = False posL = text.rfind(r"{", 0, found) posR = text.rfind(r"}", 0, found) if posL > -1: if posR == -1: nest = True else: if posL > posR: nest = True if nest: # paste }\P{ control = "" if text[posL + 1] == "\\": posC = text.find(r";", posL) if posC != -1 and (posC - posL) < 20: control = text[posL + 1 : posC + 1] text = text[:found] + r"}\P{" + control + text[found + 2 :] found = text.find(r"\P", found + 2) return text def mtext_separate(node, tspan, text): # sparate aaa{bbb}(ccc) -> aaa,bbb.ccc tspanAdd = True found = text.find(r"{") while found > -1: if found == 0: found1 = text.find(r"}") if found1 < 1: break text1 = text[:found1] # tspan text1 = text1[found + 1 :] if tspanAdd == False: tspan = node.add(inkex.Tspan()) mtext_ctrl(tspan, text1) # tspan.text = text1 +'+1' tspanAdd = False text = text[found1 + 1 :] found = text.find(r"{") else: text1 = text[:found] # tspan if tspanAdd == False: tspan = node.add(inkex.Tspan()) mtext_ctrl(tspan, text1) # tspan.text = text1 +'+2' tspanAdd = False text = text[found:] found = 0 if len(text) > 0: text1 = text if tspanAdd == False: tspan = node.add(inkex.Tspan()) mtext_ctrl(tspan, text1) # tspan.text = text1 +'+3' tspanAdd = False def mtext_ctrl(tspan, phrase): if len(phrase) == 0: return if phrase[0] != "\\": tspan.text = phrase return # if you'll add the function, you should remove the auto re.sub at setting group code:1 if phrase[1].upper() in ("C", "H", "T", "Q", "W", "A"): # get the value found = phrase.find(r";") if found > 2: cvalue = phrase[:found] cvalue = cvalue[2:] try: value = float(cvalue) except ValueError: done = False else: done = True if phrase[1].upper() == "C": i = int(value) color = get_rgbcolor(i) tspan.set("style", "stroke: %s" % color) elif phrase[1].upper() == "H": value *= scale tspan.set("style", "font-size: %.3fpx;" % value) elif phrase[1].upper() == "T": tspan.set("style", "letter-spacing: %f;" % value) elif phrase[1].upper() == "A": if value == 0: tspan.set("dominant-baseline", "text-bottom") elif value == 1: tspan.set("dominant-baseline", "central") elif value == 2: tspan.set("dominant-baseline", "text-top") tspan.text = phrase[found + 1 :] else: tspan.text = phrase else: if phrase[1].upper() == "F": # get the value font-name & style & cut from text 2022.March # \FArial|b0|i0|c0|p0; b:bold,i:italic,c:charset,ppitch found = phrase.find(r";") if found > 2: cvalue = phrase[:found] tspan.text = phrase[found + 1 :] else: tspan.text = phrase def export_point(vals, w): # mandatory group codes : (10, 20) (x, y) if vals.has_x1 and vals.has_y1: if vals["70"]: inkex.errormsg("$PDMODE is ignored. A point is displayed as normal.") if options.gcodetoolspoints: generate_gcodetools_point(vals.x1, vals.y1) else: generate_ellipse(vals.x1, vals.y1, w / 2, 0.0, 1.0, 0.0, 0.0) def export_line(vals): """Draw a straight line from the dxf""" # mandatory group codes : (10, 11, 20, 21) (x1, x2, y1, y2) if vals.has_x1 and vals.has_x2 and vals.has_y1 and vals.has_y2: path = inkex.PathElement() path.style = style path.path = "M %f,%f %f,%f" % (vals.x1, vals.y1, vals.x2, vals.y2) layer.add(path) def export_solid(vals): # arrows of dimension # mandatory group codes : (10, 11, 12, 20, 21, 22) (x1, x2, x3, y1, y2, y3) # TODO: 4th point if ( vals.has_x1 and vals.has_x2 and vals.has_x3 and vals.has_y1 and vals.has_y2 and vals.has_y3 ): path = inkex.PathElement() path.style = style path.path = "M %f,%f %f,%f %f,%f z" % ( vals.x1, vals.y1, vals.x2, vals.y2, vals.x3, vals.y3, ) layer.add(path) def export_spline(vals): # see : http://www.mactech.com/articles/develop/issue_25/schneider.html # mandatory group codes : (10, 20, 40, 70) (x[], y[], knots[], flags) if ( vals.has_flags and vals.has_knots and vals.x1_list and len(vals.x1_list) == len(vals.y1_list) ): knots = vals.knots_list ctrls = len(vals.x1_list) if ctrls > 3 and len(knots) == ctrls + 4: # cubic if ctrls > 4: for i in range(len(knots) - 5, 3, -1): if knots[i] != knots[i - 1] and knots[i] != knots[i + 1]: a0 = (knots[i] - knots[i - 2]) / (knots[i + 1] - knots[i - 2]) a1 = (knots[i] - knots[i - 1]) / (knots[i + 2] - knots[i - 1]) vals.x1_list.insert( i - 1, (1.0 - a1) * vals.x1_list[i - 2] + a1 * vals.x1_list[i - 1], ) vals.y1_list.insert( i - 1, (1.0 - a1) * vals.y1_list[i - 2] + a1 * vals.y1_list[i - 1], ) vals.x1_list[i - 2] = (1.0 - a0) * vals.x1_list[ i - 3 ] + a0 * vals.x1_list[i - 2] vals.y1_list[i - 2] = (1.0 - a0) * vals.y1_list[ i - 3 ] + a0 * vals.y1_list[i - 2] knots.insert(i, knots[i]) for i in range(len(knots) - 6, 3, -2): if ( knots[i] != knots[i + 2] and knots[i - 1] != knots[i + 1] and knots[i - 2] != knots[i] ): a1 = (knots[i] - knots[i - 1]) / (knots[i + 2] - knots[i - 1]) vals.x1_list.insert( i - 1, (1.0 - a1) * vals.x1_list[i - 2] + a1 * vals.x1_list[i - 1], ) vals.y1_list.insert( i - 1, (1.0 - a1) * vals.y1_list[i - 2] + a1 * vals.y1_list[i - 1], ) ctrls = len(vals.x1_list) path = "M %f,%f" % (vals.x1, vals.y1) for i in range(0, (ctrls - 1) // 3): path += " C %f,%f %f,%f %f,%f" % ( vals.x1_list[3 * i + 1], vals.y1_list[3 * i + 1], vals.x1_list[3 * i + 2], vals.y1_list[3 * i + 2], vals.x1_list[3 * i + 3], vals.y1_list[3 * i + 3], ) if vals.flags & 1: # closed path path += " z" attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) if ctrls == 3 and len(knots) == 6: # quadratic path = "M %f,%f Q %f,%f %f,%f" % ( vals.x1, vals.y1, vals.x1_list[1], vals.y1_list[1], vals.x1_list[2], vals.y1_list[2], ) attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) if ctrls == 5 and len(knots) == 8: # spliced quadratic path = "M %f,%f Q %f,%f %f,%f Q %f,%f %f,%f" % ( vals.x1, vals.y1, vals.x1_list[1], vals.y1_list[1], vals.x1_list[2], vals.y1_list[2], vals.x1_list[3], vals.y1_list[3], vals.x1_list[4], vals.y1_list[4], ) attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) def export_circle(vals): # mandatory group codes : (10, 20, 40) (x, y, radius) if vals.has_x1 and vals.has_y1 and vals.has_radius: generate_ellipse(vals.x1, vals.y1, scale * vals.radius, 0.0, 1.0, 0.0, 0.0) def export_arc(vals): # mandatory group codes : (10, 20, 40, 50, 51) (x, y, radius, angle1, angle2) if ( vals.has_x1 and vals.has_y1 and vals.has_radius and vals.has_angle and vals.has_angle2 ): generate_ellipse( vals.x1, vals.y1, scale * vals.radius, 0.0, 1.0, vals.angle * math.pi / 180.0, vals.angle2 * math.pi / 180.0, ) def export_ellipse(vals): # mandatory group codes : (10, 11, 20, 21, 40, 41, 42) (xc, xm, yc, ym, width ratio, angle1, angle2) if ( vals.has_x1 and vals.has_x2 and vals.has_y1 and vals.has_y2 and vals.has_width_ratio and vals.has_ellipse_a1 and vals.has_ellipse_a2 ): # generate_ellipse(vals.x1, vals.y1, scale*vals.x2, scale*vals.y2, vals.width_ratio, vals.ellipse_a1, vals.ellipse_a2) # vals are through adjust_coords : recover proper value # (x,y)=(scale*x-xmin, height-scale*y-ymin) x2 = vals.x2 + xmin y2 = -vals.y2 + ymin + height generate_ellipse( vals.x1, vals.y1, x2, y2, vals.width_ratio, vals.ellipse_a1, vals.ellipse_a2 ) def export_leader(vals): # mandatory group codes : (10, 20) (x, y) if vals.has_x1 and vals.has_y1: if len(vals.x1_list) > 1 and len(vals.y1_list) == len(vals.x1_list): path = "M %f,%f" % (vals.x1, vals.y1) for i in range(1, len(vals.x1_list)): path += " %f,%f" % (vals.x1_list[i], vals.y1_list[i]) attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) def export_polyline(vals): return export_lwpolyline(vals) def export_lwpolyline(vals): # mandatory group codes : (10, 20, 70) (x, y, flags) if vals.has_x1 and vals.has_y1 and vals.has_flags: if len(vals.x1_list) > 1 and len(vals.y1_list) == len(vals.x1_list): # optional group codes : (42) (bulge) iseqs = 0 ibulge = 0 if vals.flags & 1: # closed path seqs.append("20") vals.x1_list.append(vals.x1) vals.y1_list.append(vals.y1) while seqs[iseqs] != "20": iseqs += 1 path = "M %f,%f" % (vals.x1, vals.y1) xold = vals.x1 yold = vals.y1 for i in range(1, len(vals.x1_list)): bulge = 0 iseqs += 1 while seqs[iseqs] != "20": if seqs[iseqs] == "42": bulge = vals.bulge_list[ibulge] ibulge += 1 iseqs += 1 if bulge: sweep = 0 # sweep CCW if bulge < 0: sweep = 1 # sweep CW bulge = -bulge large = 0 # large-arc-flag if bulge > 1: large = 1 r = math.sqrt( (vals.x1_list[i] - xold) ** 2 + (vals.y1_list[i] - yold) ** 2 ) r = 0.25 * r * (bulge + 1.0 / bulge) path += " A %f,%f 0.0 %d %d %f,%f" % ( r, r, large, sweep, vals.x1_list[i], vals.y1_list[i], ) else: path += " L %f,%f" % (vals.x1_list[i], vals.y1_list[i]) xold = vals.x1_list[i] yold = vals.y1_list[i] if vals.flags & 1: # closed path path += " z" attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) def export_hatch(vals): # mandatory group codes : (10, 20, 70, 72, 92, 93) (x, y, fill, Edge Type, Path Type, Number of edges) # TODO: Hatching Pattern if ( vals.has_x1 and vals.has_y1 and vals.has_fill and vals.has_edge_type and vals.has_path_type and vals.has_num_edges ): if len(vals.x1_list) > 1 and len(vals.y1_list) == len(vals.x1_list): # optional group codes : (11, 21, 40, 50, 51, 73) (x, y, r, angle1, angle2, CCW) i10 = 1 # count start points i11 = 0 # count line end points i40 = 0 # count circles i72 = 0 # count edge type flags path = "" for i in range(0, len(vals.num_edges_list)): xc = vals.x1_list[i10] yc = vals.y1_list[i10] if vals.edge_type_list[i72] == 2: # arc rm = scale * vals.radius_list[i40] a1 = vals.angle_list[i40] path += "M %f,%f " % ( xc + rm * math.cos(a1 * math.pi / 180.0), yc + rm * math.sin(a1 * math.pi / 180.0), ) else: a1 = 0 path += "M %f,%f " % (xc, yc) for j in range(0, vals.num_edges_list[i]): if vals.path_type_list[i] & 2: # polyline if j > 0: path += "L %f,%f " % (vals.x1_list[i10], vals.y1_list[i10]) if j == vals.path_type_list[i] - 1: i72 += 1 elif vals.edge_type_list[i72] == 2: # arc xc = vals.x1_list[i10] yc = vals.y1_list[i10] rm = scale * vals.radius_list[i40] a2 = vals.angle2_list[i40] diff = (a2 - a1 + 360) % 360 sweep = 1 - vals.sweep_list[i40] # sweep CCW large = 0 # large-arc-flag if diff: path += "A %f,%f 0.0 %d %d %f,%f " % ( rm, rm, large, sweep, xc + rm * math.cos(a2 * math.pi / 180.0), yc + rm * math.sin(a2 * math.pi / 180.0), ) else: path += "A %f,%f 0.0 %d %d %f,%f " % ( rm, rm, large, sweep, xc + rm * math.cos((a1 + 180.0) * math.pi / 180.0), yc + rm * math.sin((a1 + 180.0) * math.pi / 180.0), ) path += "A %f,%f 0.0 %d %d %f,%f " % ( rm, rm, large, sweep, xc + rm * math.cos(a1 * math.pi / 180.0), yc + rm * math.sin(a1 * math.pi / 180.0), ) i40 += 1 i72 += 1 elif vals.edge_type_list[i72] == 1: # line path += "L %f,%f " % (vals.x2_list[i11], vals.y2_list[i11]) i11 += 1 i72 += 1 i10 += 1 path += "z " if vals.has_fill: style = formatStyle({"fill": "%s" % color}) else: style = formatStyle({"fill": "url(#Hatch)", "fill-opacity": "1.0"}) attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) def export_dimension(vals): # mandatory group codes : (10, 11, 13, 14, 20, 21, 23, 24) (x1..4, y1..4) # block_name: dimension definition for 10mm if vals.has_x1 and vals.has_x2 and vals.has_y1 and vals.has_y2: if vals.has_block_name: attribs = { inkex.addNS("href", "xlink"): "#%s" % (vals.block_name) } # not use quote because name *D2 etc. changed to %2AD2 tform = "translate(0 0)" # if vals.has_angle : # tform += ' rotate(%f,%f,%f)' % (vals.angle,vals.x4,vals.y4) attribs.update({"transform": tform}) etree.SubElement(layer, "use", attribs) else: # TODO: improve logic when INSERT in BLOCK dx = abs(vals.x1 - vals.x3) dy = abs(vals.y1 - vals.y3) if (vals.x1 == vals.x4) and dx > 0.00001: d = dx / scale dy = 0 path = "M %f,%f %f,%f" % (vals.x1, vals.y1, vals.x3, vals.y1) elif (vals.y1 == vals.y4) and dy > 0.00001: d = dy / scale dx = 0 path = "M %f,%f %f,%f" % (vals.x1, vals.y1, vals.x1, vals.y3) else: return attribs = { "d": path, "style": style + "; marker-start: url(#DistanceX); marker-end: url(#DistanceX); stroke-width: 0.25px", } etree.SubElement(layer, "path", attribs) x = vals.x2 y = vals.y2 size = 12 # default fontsize in px if vals.has_mtext: if vals.mtext in DIMTXT: size = scale * textscale * DIMTXT[vals.mtext] if size < 2: size = 2 attribs = { "x": "%f" % x, "y": "%f" % y, "style": "font-size: %.3fpx; fill: %s; font-family: %s; text-anchor: middle; text-align: center" % (size, color, options.font), } if dx == 0: attribs.update({"transform": "rotate (%f %f %f)" % (-90, x, y)}) node = etree.SubElement(layer, "text", attribs) tspan = node.add(inkex.Tspan()) tspan.set("sodipodi:role", "line") tspan.text = str(float("%.2f" % d)) def export_insert(vals): # mandatory group codes : (2, 10, 20) (block name, x, y) # TODO: repeat by row and column # (times,interval)= row(70,44), column(71,45) if vals.has_block_name and vals.has_x1 and vals.has_y1: # vals are through adjust_coords except block # block (x,y)=(0,0) : (scale*x-xmin, height-scale*y-ymin) # translate(move x units,move y units) # 2021.6 translate..ok scale..x rotate X # as scale, the line is wider ->same width -> you should fix global height cx = scale * xmin # transorm-origin: cy = scale * ymin + height # center of rotation x = vals.x1 + scale * xmin y = vals.y1 - scale * ymin - height ixscale = iyscale = 1 if vals.has_insert_scale_y: ixscale = vals.insert_scale_x if vals.has_insert_scale_y: iyscale = vals.insert_scale_y x += cx * (iyscale - 1) y -= cy * (iyscale - 1) elem = layer.add(inkex.Use()) elem.set( inkex.addNS("href", "xlink"), "#" + quote(vals.block_name.replace(" ", "_").encode("utf-8")), ) # add style stroke-width=1px for reducing thick line fwide = abs(0.5 / ixscale) # better to use w/ixscale elem.style["stroke-width"] = "%.3fpx" % fwide elem.transform.add_translate(x, y) if vals.has_insert_scale_x and vals.has_insert_scale_y: elem.transform.add_scale(ixscale, iyscale) if vals.has_angle: rotated_angle = vals.angle if ixscale * iyscale > 0: rotated_angle = 360 - rotated_angle elem.transform.add_rotate(rotated_angle, -cx, cy) def export_block(vals): # mandatory group codes : (2) (block name) if vals.has_block_name: global block block = etree.SubElement( defs, "symbol", {"id": vals.block_name.replace(" ", "_")} ) def export_endblk(vals): global block block = defs # initiallize with dummy def export_attdef(vals): # mandatory group codes : (1, 2) (default, tag) if vals.has_default and vals.has_tag: vals.text_list.append(vals.tag) export_mtext(vals) def generate_ellipse(xc, yc, xm, ym, w, a1, a2): rm = math.sqrt(xm * xm + ym * ym) a = math.atan2(ym, xm) # x-axis-rotation diff = (a2 - a1 + 2 * math.pi) % (2 * math.pi) if abs(diff) > 0.0000001 and abs(diff - 2 * math.pi) > 0.0000001: # open arc large = 0 # large-arc-flag if diff > math.pi: large = 1 xt = rm * math.cos(a1) yt = w * rm * math.sin(a1) x1 = xt * math.cos(a) - yt * math.sin(a) y1 = xt * math.sin(a) + yt * math.cos(a) xt = rm * math.cos(a2) yt = w * rm * math.sin(a2) x2 = xt * math.cos(a) - yt * math.sin(a) y2 = xt * math.sin(a) + yt * math.cos(a) path = "M %f,%f A %f,%f %f %d 0 %f,%f" % ( xc + x1, yc - y1, rm, w * rm, -180.0 * a / math.pi, large, xc + x2, yc - y2, ) else: # closed arc path = "M %f,%f A %f,%f %f 0, 0 %f,%f A %f,%f %f 0, 0 %f,%f z" % ( xc + xm, yc - ym, rm, w * rm, -180.0 * a / math.pi, xc - xm, yc + ym, rm, w * rm, -180.0 * a / math.pi, xc + xm, yc - ym, ) attribs = {"d": path, "style": style} etree.SubElement(layer, "path", attribs) def generate_gcodetools_point(xc, yc): elem = layer.add(inkex.PathElement()) elem.style = "stroke:none;fill:#ff0000" elem.set("inkscape:dxfpoint", "1") elem.path = ( "m %s,%s 2.9375,-6.34375 0.8125,1.90625 6.84375,-6.84375 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.8125 z" % (xc, yc) ) # define DXF Entities and specify which Group Codes to monitor class DxfInput(inkex.InputExtension): def add_arguments(self, pars): pars.add_argument("--tab", default="options") pars.add_argument("--scalemethod", default="manual") pars.add_argument("--scale", default="1.0") pars.add_argument("--textscale", default="1.0") pars.add_argument("--xmin", default="0.0") pars.add_argument("--ymin", default="0.0") pars.add_argument("--gcodetoolspoints", default=False, type=inkex.Boolean) pars.add_argument("--encoding", dest="input_encode", default="latin_1") pars.add_argument("--font", default="Arial") def load(self, stream): return stream def effect(self): global options global defs global entity global seqs global style global layer global scale global textscale global color global extrude global xmin global ymin global height global DIMTXT global block global svg global style_font3 global style_direction global be_extrude options = self.options doc = self.get_template(width=210 * 96 / 25.4, height=297 * 96 / 25.4) svg = doc.getroot() defs = svg.defs marker = etree.SubElement( defs, "marker", { "id": "DistanceX", "orient": "auto", "refX": "0.0", "refY": "0.0", "style": "overflow:visible", }, ) etree.SubElement( marker, "path", { "d": "M 3,-3 L -3,3 M 0,-5 L 0,5", "style": "stroke:#000000; stroke-width:0.5", }, ) pattern = etree.SubElement( defs, "pattern", { "id": "Hatch", "patternUnits": "userSpaceOnUse", "width": "8", "height": "8", "x": "0", "y": "0", }, ) etree.SubElement( pattern, "path", { "d": "M8 4 l-4,4", "stroke": "#000000", "stroke-width": "0.25", "linecap": "square", }, ) etree.SubElement( pattern, "path", { "d": "M6 2 l-4,4", "stroke": "#000000", "stroke-width": "0.25", "linecap": "square", }, ) etree.SubElement( pattern, "path", { "d": "M4 0 l-4,4", "stroke": "#000000", "stroke-width": "0.25", "linecap": "square", }, ) def _get_line(): return self.document.readline().strip().decode(options.input_encode) def get_line(): return _get_line(), _get_line() def get_group(group): line = get_line() if line[0] == group: return float(line[1]) return 0.0 xmax = xmin = ymin = 0.0 ltscale = 1.0 # $LTSCALE:global scale of line-style height = 297.0 * 96.0 / 25.4 # default A4 height in pixels measurement = 0 # default inches flag = 0 # (0, 1, 2, 3, 4) = (none, LAYER, LTYPE, DIMTXT, STYLE) layer_colors = {} # store colors by layer layer_nodes = {} # store nodes by layer linetypes = {} # store linetypes by name DIMTXT = {} # store DIMENSION text sizes # style_name = {} # style name style_font3 = {} # style font 1byte style_font4 = {} # style font 2byte style_direction = {} # style display direction be_extrude = False line = get_line() if line[0] == "AutoCAD Binary DXF": inkex.errormsg( _( "Inkscape cannot read binary DXF files. \n" "Please convert to ASCII format first." ) + str(len(line[0])) + " " + str(len(line[1])) ) self.document = doc return inENTITIES = False style_name = "*" layername = None linename = None stylename = None style_name = None errno = 0 pdmode_err = False while line[0] and line[1] != "BLOCKS": line = get_line() if line[1] == "ENTITIES": # no BLOCK SECTION inENTITIES = True break if line[1] == "$PDMODE" and not pdmode_err: inkex.errormsg("$PDMODE is ignored. A point is displayed as normal.") pdmode_err = True if options.scalemethod == "file": if line[1] == "$MEASUREMENT": measurement = get_group("70") elif options.scalemethod == "auto": if line[1] == "$EXTMIN": xmin = get_group("10") ymin = get_group("20") if line[1] == "$EXTMAX": xmax = get_group("10") if line[1] == "$LTSCALE": ltscale = get_group("40") if flag == 1 and line[0] == "2": layername = line[1] layer_nodes[layername] = svg.add(inkex.Layer.new(layername)) if flag == 2 and line[0] == "2": linename = line[1] linetypes[linename] = [] if flag == 3 and line[0] == "2": stylename = line[1] if flag == 4 and line[0] == "2": style_name = line[1] style_font3[style_name] = [] style_font4[style_name] = [] style_direction[style_name] = [] if line[0] == "2" and line[1] == "LAYER": flag = 1 if line[0] == "2" and line[1] == "LTYPE": flag = 2 if line[0] == "2" and line[1] == "DIMSTYLE": flag = 3 if line[0] == "2" and line[1] == "STYLE": flag = 4 if flag == 1 and line[0] == "62": if layername is None: errno = 1 break layer_colors[layername] = int(line[1]) if flag == 2 and line[0] == "49": if linename is None: errno = 2 break linetypes[linename].append(float(line[1])) if flag == 3 and line[0] == "140": if stylename is None: errno = 3 break DIMTXT[stylename] = float(line[1]) if flag == 4 and line[0] == "3": if style_name is None: errno = 4 break style_font3[style_name].append(line[1]) if flag == 4 and line[0] == "4": if style_name is None: errno = 4 break style_font4[style_name].append(line[1]) if flag == 4 and line[0] == "70": # not no of STYLE if style_name != "*": style_direction[style_name] = int(line[1]) if line[0] == "0" and line[1] == "ENDTAB": flag = 0 if errno != 0: if errno == 1: errMsg = "LAYER" elif errno == 2: errMsg = "LTYPE" elif errno == 3: errMsg = "DIMSTYLE" else: # errno == 4 errMsg = "STYLE" inkex.errormsg( "Import stopped. DXF is incorrect.\ngroup code 2 (" + errMsg + ") is missing" ) self.document = doc return if options.scalemethod == "file": scale = 25.4 # default inches if measurement == 1.0: scale = 1.0 # use mm elif options.scalemethod == "auto": scale = 1.0 if xmax > xmin: scale = 210.0 / (xmax - xmin) # scale to A4 width else: scale = float(options.scale) # manual scale factor xmin = float(options.xmin) ymin = float(options.ymin) svg.desc = "%s - scale = %f, origin = (%f, %f), method = %s" % ( os.path.basename(options.input_file), scale, xmin, ymin, options.scalemethod, ) scale *= 96.0 / 25.4 # convert from mm to pixels textscale = float(options.textscale) if "0" not in layer_nodes: layer_nodes["0"] = svg.add(inkex.Layer.new("0")) layer_colors["0"] = 7 for linename in linetypes.keys(): # scale the dashed lines linetype = "" for length in linetypes[linename]: if length == 0: # test for dot linetype += " 0.5," else: linetype += "%.4f," % math.fabs(length * scale * ltscale) if linetype == "": linetypes[linename] = "stroke-linecap: round" else: linetypes[linename] = "stroke-dasharray:" + linetype entity = "" block = defs # initiallize with dummy while line[0] and (line[1] != "ENDSEC" or not inENTITIES): line = get_line() if line[1] == "ENTITIES": inENTITIES = True if entity and vals.is_valid(line[0]): seqs.append(line[0]) # list of group codes if line[0] in ("1", "2", "3", "6", "7", "8"): # text value # TODO: if add funs of export_mtext, delete the line val = line[1].replace(r"\~", " ") # val = re.sub(r"\\A.*;", "", val) # val = re.sub(r'\\H.*;', '', val) val = re.sub(r"\^I", "", val) val = re.sub(r"{\\L", "", val) # val = re.sub(r'}', '', val) {\\C; '}' in mtext val = re.sub(r"\\S.*;", "", val) val = re.sub(r"\\W.*;", "", val) val = val val = re.sub(r"\\U\+([0-9A-Fa-f]{4})", re_hex2unichar, val) elif line[0] in ("62", "70", "92", "93"): val = int(line[1]) else: # unscaled float value val = float(line[1]) vals[line[0]].append(val) elif has_export(line[1]): if has_export(entity): if block != defs: # in a BLOCK layer = block elif vals.has_layer_name: # use Common Layer Name if not vals.layer_name: vals.layer_name = "0" # use default name if vals.layer_name not in layer_nodes: # attribs = {inkex.addNS('groupmode','inkscape') : # 'layer', inkex.addNS('label','inkscape') : '%s' % vals.layer_name} # layer_nodes[vals.layer_name] = etree.SubElement(doc.getroot(), 'g', attribs) layer_nodes[vals.layer_name] = svg.add( inkex.Layer.new(vals.layer_name) ) layer = layer_nodes[vals.layer_name] color = "#000000" # default color if vals.has_layer_name: if vals.layer_name in layer_colors: color = get_rgbcolor(layer_colors[vals.layer_name], color) if vals.has_color: # Common Color Number color = get_rgbcolor(vals.color, color) style = formatStyle({"stroke": "%s" % color, "fill": "none"}) w = 0.5 # default lineweight for POINT if vals.has_line_weight: # Common Lineweight if vals.line_weight > 0: w = 96.0 / 25.4 * vals.line_weight / 100.0 w *= scale # real wide : line_weight /144 inch if w < 0.5: w = 0.5 if ( block == defs ): # not in a BLOCK for INSERT except stroke-width 2021.july style = formatStyle( { "stroke": "%s" % color, "fill": "none", "stroke-width": "%.3f" % w, } ) if vals.has_line_type: # Common Linetype if vals.line_type in linetypes: style += ";" + linetypes[vals.line_type] extrude = 1.0 if vals.has_extrude: if (entity != "LINE") and (entity != "POINT"): extrude = float(vals.extrude) if extrude < 1.0: be_extrude = True vals.adjust_coords(xmin, ymin, scale, extrude, height) if extrude == -1.0: # reflect angles if vals.has_angle and vals.has_angle2: vals.angle2, vals.angle = ( 180.0 - vals.angle, 180.0 - vals.angle2, ) exporter = get_export(entity) if exporter: if entity == "POINT": exporter(vals, w) else: exporter(vals) if line[1] == "POLYLINE": inVertexs = False entity = "LWPOLYLINE" vals = ValueConstruct() seqs = [] flag70 = 0 # default closed-line or not val8 = "0" # default layer name val10 = 0 # x val20 = 0 # y val42 = 0 # bulge valid = True while line[0] and (line[1] != "SEQEND"): line = get_line() if line[1] == "VERTEX": if inVertexs == True: if valid: seqs.append("10") vals["10"].append(val10) seqs.append("20") vals["20"].append(val20) seqs.append("42") vals["42"].append(val42) val42 = 0 inVertexs = True valid = True if inVertexs == False: if line[0] == "6": # 6:line style seqs.append(line[0]) vals[line[0]].append(line[1]) if line[0] == "8": # 8:layer val8 = line[1] if line[0] == "70": # flag flag70 = int(line[1]) else: if line[0] == "70": if int(line[1]) == 16: # control point valid = False if line[0] in ("10", "20", "42"): # vertexs val = float(line[1]) if line[0] == "10": val10 = val elif line[0] == "20": val20 = val else: val42 = val if valid: seqs.append("8") # layer_name vals["8"].append(val8) seqs.append("10") vals["10"].append(val10) seqs.append("20") vals["20"].append(val20) seqs.append("42") # bulge vals["42"].append(val42) seqs.append("70") # closed line? vals["70"].append(flag70) continue entity = line[1] vals = ValueConstruct() seqs = [] # for debug # tree = etree.ElementTree(svg) # tree.write('c:\Python\svgCH2.xml') if be_extrude: inkex.errormsg( _( "An object that has the extrude parameter set was detected. " "The imported figure may be displayed incorrectly." ) ) self.document = doc def get_export(opt): return globals().get("export_" + opt.lower(), None) def has_export(opt): return get_export(opt) is not None if __name__ == "__main__": DxfInput().run()