289 lines
17 KiB
Python
289 lines
17 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
"""
|
||
|
Extension for InkScape 1.0
|
||
|
|
||
|
Import any DWG or DXF file using ODA File Converter, sk1 UniConverter, ezdxf and more tools.
|
||
|
|
||
|
Author: Mario Voigt / FabLab Chemnitz
|
||
|
Mail: mario.voigt@stadtfabrikanten.org
|
||
|
Date: 23.08.2020
|
||
|
Last patch: 23.08.2020
|
||
|
License: GNU GPL v3
|
||
|
|
||
|
Module licenses
|
||
|
- ezdxf (https://github.com/mozman/ezdxf) - MIT License
|
||
|
- node.js (https://raw.githubusercontent.com/nodejs/node/master/LICENSE) - MIT License
|
||
|
- https://github.com/bjnortier/dxf - MIT License
|
||
|
- ODA File Converter - not bundled (due to restrictions by vendor)
|
||
|
- sk1 UniConverter (https://github.com/sk1project/uniconvertor) - AGPL v3.0 - not bundled
|
||
|
"""
|
||
|
|
||
|
import inkex
|
||
|
import sys
|
||
|
import os
|
||
|
import re
|
||
|
import subprocess, tempfile
|
||
|
from lxml import etree
|
||
|
from subprocess import Popen, PIPE
|
||
|
import shutil
|
||
|
from pathlib import Path
|
||
|
|
||
|
#ezdxf related imports
|
||
|
import matplotlib.pyplot as plt
|
||
|
import ezdxf
|
||
|
from ezdxf.addons.drawing import RenderContext, Frontend
|
||
|
from ezdxf.addons.drawing.matplotlib_backend import MatplotlibBackend
|
||
|
from ezdxf.addons import Importer
|
||
|
|
||
|
class DXFDWGImport(inkex.Effect):
|
||
|
def __init__(self):
|
||
|
inkex.Effect.__init__(self)
|
||
|
self.arg_parser.add_argument("--odafileconverter", default=r"C:\Program Files\ODA\ODAFileConverter_title 21.6.0\ODAFileConverter.exe", help="Full path to 'ODAFileConverter.exe'")
|
||
|
self.arg_parser.add_argument("--odahidewindow", type=inkex.Boolean, default=True, help="Hide ODA GUI window")
|
||
|
self.arg_parser.add_argument("--outputformat", default="ACAD2018_DXF", help="ODA AutoCAD Output version")
|
||
|
self.arg_parser.add_argument("--sk1uniconverter", default=r"C:\Program Files (x86)\sK1 Project\UniConvertor-1.1.6\uniconvertor.cmd", help="Full path to 'uniconvertor.cmd'")
|
||
|
self.arg_parser.add_argument("--opendironerror", type=inkex.Boolean, default=True, help="Open containing output directory on conversion errors")
|
||
|
self.arg_parser.add_argument("--skip_dxf_to_dxf", type=inkex.Boolean, default=False, help="Skip conversion from DXF to DXF")
|
||
|
self.arg_parser.add_argument("--audit_repair", type=inkex.Boolean, default=True, help="Perform audit / autorepair")
|
||
|
self.arg_parser.add_argument("--dxf_to_svg_parser", default="bjnortier", help="Choose a DXF to SVG parser")
|
||
|
self.arg_parser.add_argument("--resizetoimport", type=inkex.Boolean, default=True, help="Resize the canvas to the imported drawing's bounding box")
|
||
|
self.arg_parser.add_argument("--THREE_DFACE", type=inkex.Boolean, default=True) #3DFACE
|
||
|
self.arg_parser.add_argument("--ARC", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--BLOCK", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--CIRCLE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--ELLIPSE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--LINE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--LWPOLYLINE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--POINT", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--POLYLINE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--POP_TRAFO", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--SEQEND", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--SOLID", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--SPLINE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--TABLE", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--VERTEX", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--VIEWPORT", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--inputfile")
|
||
|
self.arg_parser.add_argument("--extraborder", type=float, default=0.0)
|
||
|
self.arg_parser.add_argument("--extraborder_units")
|
||
|
self.arg_parser.add_argument("--ezdxf_output_version", default="SAME")
|
||
|
self.arg_parser.add_argument("--ezdxf_preprocessing", type=inkex.Boolean, default=True)
|
||
|
self.arg_parser.add_argument("--allentities", type=inkex.Boolean, default=True)
|
||
|
|
||
|
def effect(self):
|
||
|
#get input file and copy it to some new temporary directory
|
||
|
inputfile = self.options.inputfile
|
||
|
|
||
|
temp_input_dir = os.path.join(tempfile.gettempdir(),"inkscape-oda-convert-input")
|
||
|
|
||
|
#remove the input directory before doing new job
|
||
|
shutil.rmtree(temp_input_dir, ignore_errors=True)
|
||
|
if not os.path.exists(temp_input_dir):
|
||
|
os.mkdir(temp_input_dir) #recreate blank dir
|
||
|
|
||
|
shutil.copy2(inputfile, os.path.join(temp_input_dir, Path(inputfile).name)) # complete target filename given
|
||
|
|
||
|
#Prepapre output conversion
|
||
|
outputfilebase = os.path.splitext(os.path.splitext(os.path.basename(inputfile))[0])[0]
|
||
|
inputfile_ending = os.path.splitext(os.path.splitext(os.path.basename(inputfile))[1])[0]
|
||
|
temp_output_dir = os.path.join(tempfile.gettempdir(),"inkscape-oda-convert-output")
|
||
|
|
||
|
#remove the output directory before doing new job
|
||
|
shutil.rmtree(temp_output_dir, ignore_errors=True)
|
||
|
if not os.path.exists(temp_output_dir):
|
||
|
os.mkdir(temp_output_dir)
|
||
|
|
||
|
autocad_version = self.options.outputformat.split("_")[0]
|
||
|
autocad_format = self.options.outputformat.split("_")[1]
|
||
|
if self.options.audit_repair: #overwrite string bool with int value
|
||
|
self.options.audit_repair = "1"
|
||
|
else:
|
||
|
self.options.audit_repair = "0"
|
||
|
entityspace = []
|
||
|
if self.options.allentities or self.options.THREE_DFACE: entityspace.append("3DFACE")
|
||
|
if self.options.allentities or self.options.ARC: entityspace.append("ARC")
|
||
|
if self.options.allentities or self.options.BLOCK: entityspace.append("BLOCK")
|
||
|
if self.options.allentities or self.options.CIRCLE: entityspace.append("CIRCLE")
|
||
|
if self.options.allentities or self.options.ELLIPSE: entityspace.append("ELLIPSE")
|
||
|
if self.options.allentities or self.options.LINE: entityspace.append("LINE")
|
||
|
if self.options.allentities or self.options.LWPOLYLINE: entityspace.append("LWPOLYLINE")
|
||
|
if self.options.allentities or self.options.POINT: entityspace.append("POINT")
|
||
|
if self.options.allentities or self.options.POLYLINE: entityspace.append("POLYLINE")
|
||
|
if self.options.allentities or self.options.POP_TRAFO: entityspace.append("POP_TRAFO")
|
||
|
if self.options.allentities or self.options.SEQEND: entityspace.append("SEQEND")
|
||
|
if self.options.allentities or self.options.SOLID: entityspace.append("SOLID")
|
||
|
if self.options.allentities or self.options.SPLINE: entityspace.append("SPLINE")
|
||
|
if self.options.allentities or self.options.TABLE: entityspace.append("TABLE")
|
||
|
if self.options.allentities or self.options.VERTEX: entityspace.append("VERTEX")
|
||
|
if self.options.allentities or self.options.VIEWPORT: entityspace.append("VIEWPORT")
|
||
|
|
||
|
#ODA to ezdxf mapping
|
||
|
oda_ezdxf_mapping = []
|
||
|
oda_ezdxf_mapping.append(["ACAD9","R12","AC1004"]) #this mapping is not supported directly. so we use the lowest possible which is R12
|
||
|
oda_ezdxf_mapping.append(["ACAD10","R12","AC1006"]) #this mapping is not supported directly. so we use the lowest possible which is R12
|
||
|
oda_ezdxf_mapping.append(["ACAD12","R12","AC1009"])
|
||
|
oda_ezdxf_mapping.append(["ACAD13","R2000","AC1012"]) #R13 was overwritten by R2000 which points to AC1015 instead of AC1014 (see documentation)
|
||
|
oda_ezdxf_mapping.append(["ACAD14","R2000","AC1014"]) #R14 was overwritten by R2000 which points to AC1015 instead of AC1014 (see documentation)
|
||
|
oda_ezdxf_mapping.append(["ACAD2000","R2000","AC1015"])
|
||
|
oda_ezdxf_mapping.append(["ACAD2004","R2004","AC1018"])
|
||
|
oda_ezdxf_mapping.append(["ACAD2007","R2007","AC1021"])
|
||
|
oda_ezdxf_mapping.append(["ACAD2010","R2010","AC1024"])
|
||
|
oda_ezdxf_mapping.append(["ACAD2013","R2013","AC1027"])
|
||
|
oda_ezdxf_mapping.append(["ACAD2018","R2018","AC1032"])
|
||
|
|
||
|
ezdxf_autocad_format = None
|
||
|
for oe in oda_ezdxf_mapping:
|
||
|
if oe[0] == autocad_version:
|
||
|
ezdxf_autocad_format = oe[1]
|
||
|
break
|
||
|
if ezdxf_autocad_format is None:
|
||
|
inkex.errormsg("ezdxf conversion format version unknown")
|
||
|
|
||
|
if self.options.skip_dxf_to_dxf == False or inputfile_ending == ".dwg":
|
||
|
# Build and run ODA File Converter command // "cmd.exe /c start \"\" /MIN /WAIT"
|
||
|
oda_cmd = [self.options.odafileconverter, temp_input_dir, temp_output_dir, autocad_version, autocad_format, "0", self.options.audit_repair]
|
||
|
if self.options.odahidewindow:
|
||
|
info = subprocess.STARTUPINFO() #hide the ODA File Converter window because it is annoying
|
||
|
info.dwFlags = 1
|
||
|
info.wShowWindow = 0
|
||
|
proc = subprocess.Popen(oda_cmd, startupinfo=info, shell=False, stdout=PIPE, stderr=PIPE)
|
||
|
else: proc = subprocess.Popen(oda_cmd, shell=False, stdout=PIPE, stderr=PIPE)
|
||
|
stdout, stderr = proc.communicate()
|
||
|
if proc.returncode != 0:
|
||
|
inkex.errormsg("ODA File Converter failed: %d %s %s" % (proc.returncode, stdout, stderr))
|
||
|
if self.options.skip_dxf_to_dxf: #if true we need to move the file to simulate "processed"
|
||
|
shutil.move(os.path.join(temp_input_dir, Path(inputfile).name), os.path.join(temp_output_dir, Path(inputfile).name))
|
||
|
|
||
|
# Prepare files
|
||
|
dxf_file = os.path.join(temp_output_dir, outputfilebase + ".dxf")
|
||
|
svg_file = os.path.join(temp_output_dir, outputfilebase + ".svg")
|
||
|
|
||
|
# Preprocessing DXF to DXF (entity filter)
|
||
|
if self.options.ezdxf_preprocessing:
|
||
|
#uniconverter does not handle all entities. we parse the file to exlude stuff which lets uniconverter fail
|
||
|
dxf = ezdxf.readfile(dxf_file)
|
||
|
modelspace = dxf.modelspace()
|
||
|
allowed_entities = []
|
||
|
# supported entities by UniConverter- impossible: MTEXT TEXT INSERT and a lot of others
|
||
|
query_string = str(entityspace)[1:-1].replace("'","").replace(",","")
|
||
|
for e in modelspace.query(query_string):
|
||
|
allowed_entities.append(e)
|
||
|
#inkex.utils.debug(ezdxf_autocad_format)
|
||
|
#inkex.utils.debug(self.options.ezdxf_output_version)
|
||
|
if self.options.ezdxf_output_version == "SAME":
|
||
|
doc = ezdxf.new(ezdxf_autocad_format)
|
||
|
else:
|
||
|
doc = ezdxf.new(self.options.ezdxf_output_version) #use the string values from inx file. Required to match the values from ezdxf library. See Python reference
|
||
|
msp = doc.modelspace()
|
||
|
for e in allowed_entities:
|
||
|
msp.add_foreign_entity(e)
|
||
|
doc.saveas(dxf_file)
|
||
|
|
||
|
# make SVG from DXF
|
||
|
if self.options.dxf_to_svg_parser == "uniconverter":
|
||
|
uniconverter_cmd = [self.options.sk1uniconverter, dxf_file, svg_file]
|
||
|
#inkex.utils.debug(uniconverter_cmd)
|
||
|
proc = subprocess.Popen(uniconverter_cmd, shell=False, stdout=PIPE, stderr=PIPE)
|
||
|
stdout, stderr = proc.communicate()
|
||
|
if proc.returncode != 0:
|
||
|
inkex.errormsg("UniConverter failed: %d %s %s" % (proc.returncode, stdout, stderr))
|
||
|
if self.options.opendironerror:
|
||
|
subprocess.Popen(["explorer",temp_output_dir],close_fds=True)
|
||
|
|
||
|
elif self.options.dxf_to_svg_parser == "bjnortier":
|
||
|
bjnortier_cmd = ["node", os.path.join("node_modules","dxf","lib","cli.js"), dxf_file, svg_file]
|
||
|
#inkex.utils.debug(bjnortier_cmd)
|
||
|
proc = subprocess.Popen(bjnortier_cmd, shell=False, stdout=PIPE, stderr=PIPE)
|
||
|
stdout, stderr = proc.communicate()
|
||
|
if proc.returncode != 0:
|
||
|
inkex.errormsg("node.js DXF to SVG conversion failed: %d %s %s" % (proc.returncode, stdout, stderr))
|
||
|
if self.options.opendironerror:
|
||
|
subprocess.Popen(["explorer",temp_output_dir],close_fds=True)
|
||
|
elif self.options.dxf_to_svg_parser == "ezdxf":
|
||
|
doc = ezdxf.readfile(dxf_file)
|
||
|
#doc.header['$DIMSCALE'] = 0.2 does not apply to the plot :-(
|
||
|
#inkex.utils.debug(doc.header['$DIMSCALE'])
|
||
|
#inkex.utils.debug(doc.header['$MEASUREMENT'])
|
||
|
auditor = doc.audit() #audit & repair DXF document before rendering
|
||
|
# The auditor.errors attribute stores severe errors, which *may* raise exceptions when rendering.
|
||
|
if len(auditor.errors) == 0:
|
||
|
fig = plt.figure()
|
||
|
ax = plt.axes([0., 0., 1., 1.], frameon=False, xticks=[], yticks=[])
|
||
|
#ax.patches = []
|
||
|
plt.axis('off')
|
||
|
plt.margins(0, 0)
|
||
|
plt.gca().xaxis.set_major_locator(plt.NullLocator())
|
||
|
plt.gca().yaxis.set_major_locator(plt.NullLocator())
|
||
|
plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)
|
||
|
out = MatplotlibBackend(fig.add_axes(ax))
|
||
|
Frontend(RenderContext(doc), out).draw_layout(doc.modelspace(), finalize=True)
|
||
|
#plt.show()
|
||
|
#fig.savefig(os.path.join(temp_output_dir, outputfilebase + ".png"), dpi=300)
|
||
|
fig.savefig(svg_file) #see https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.savefig.html
|
||
|
else:
|
||
|
inkex.utils.debug("undefined parser")
|
||
|
exit(1)
|
||
|
|
||
|
# Write the generated SVG into canvas
|
||
|
stream = open(svg_file, 'r')
|
||
|
p = etree.XMLParser(huge_tree=True)
|
||
|
doc = etree.parse(stream, parser=etree.XMLParser(huge_tree=True)).getroot()
|
||
|
stream.close()
|
||
|
|
||
|
#newGroup = self.document.getroot().add(inkex.Group())
|
||
|
doc.set('id', self.svg.get_unique_id('dxf_dwg_import'))
|
||
|
self.document.getroot().append(doc)
|
||
|
|
||
|
#get children of the doc and move them one group above - we don't do this for bjnortier tool because this has different structure which we don't want to disturb
|
||
|
if self.options.dxf_to_svg_parser == "uniconverter":
|
||
|
elements = []
|
||
|
emptyGroup = None
|
||
|
for firstGroup in doc.getchildren():
|
||
|
emptyGroup = firstGroup
|
||
|
for element in firstGroup.getchildren():
|
||
|
elements.append(element)
|
||
|
#break #only one cycle - could be bad idea or not
|
||
|
for element in elements:
|
||
|
doc.set('id', self.svg.get_unique_id('dxf_dwg_import'))
|
||
|
doc.insert(doc.index(firstGroup), element)
|
||
|
|
||
|
if emptyGroup is not None:
|
||
|
emptyGroup.getparent().remove(emptyGroup)
|
||
|
|
||
|
#empty the following vals because they destroy the size aspects of the import
|
||
|
if self.options.dxf_to_svg_parser == "bjnortier":
|
||
|
doc.set('width','')
|
||
|
doc.set('height','')
|
||
|
doc.set('viewBox','')
|
||
|
doc.getchildren()[0].set('transform','')
|
||
|
|
||
|
#adjust viewport and width/height to have the import at the center of the canvas - unstable at the moment.
|
||
|
if self.options.resizetoimport:
|
||
|
elements = []
|
||
|
for child in doc.getchildren():
|
||
|
#if child.tag == inkex.addNS('g','svg'):
|
||
|
elements.append(child)
|
||
|
|
||
|
#build some of bounding boxes and ignore errors for faulty elements (sum function often fails for that usecase!)
|
||
|
bbox = None
|
||
|
try:
|
||
|
bbox = elements[0].bounding_box() #init with the first bounding box of the tree (and hope that it is not a faulty one)
|
||
|
except:
|
||
|
pass
|
||
|
count = 0
|
||
|
for element in elements:
|
||
|
if count == 0: continue #skip the first
|
||
|
try:
|
||
|
bbox.add(element.bounding_box())
|
||
|
except:
|
||
|
pass
|
||
|
count += 1 #some stupid counter
|
||
|
if bbox is not None:
|
||
|
root = self.svg.getElement('//svg:svg');
|
||
|
offset = self.svg.unittouu(str(self.options.extraborder) + self.options.extraborder_units)
|
||
|
root.set('viewBox', '%f %f %f %f' % (bbox.left - offset, bbox.top - offset, bbox.width + 2 * offset, bbox.height + 2 * offset))
|
||
|
root.set('width', bbox.width + 2 * offset)
|
||
|
root.set('height', bbox.height + 2 * offset)
|
||
|
|
||
|
DXFDWGImport().run()
|