1#!/usr/local/bin/python3.8
2'''
3Copyright (C) 2017-2021 Scorch www.scorchworks.com
4Derived from dxf_outlines.py by Aaron Spike and Alvin Penner
5
6This program is free software; you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation; either version 2 of the License, or
9(at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19'''
20
21# standard library
22import math
23import tempfile, os, sys, shutil
24
25import zipfile
26import re
27# local library
28import inkex
29import simplestyle
30import simpletransform
31import cubicsuperpath
32import cspsubdiv
33import traceback
34
35from PIL import Image
36Image.MAX_IMAGE_PIXELS = None
37
38from lxml import etree
39
40try:
41    inkex.localize()
42except:
43    pass
44
45#### Subprocess timout stuff ######
46from subprocess import Popen, PIPE
47from threading import Timer
48def run_external(cmd, timeout_sec):
49    stdout=None
50    stderr=None
51    FLAG=[True]
52    try:
53        proc = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE, stdin=PIPE, startupinfo=None)
54    except Exception as e:
55        raise Exception("\n%s\n\nExecutable Path:\n%s" %(e,cmd[0]))
56    if timeout_sec > 0:
57        kill_proc = lambda p: kill_sub_process(p,timeout_sec, FLAG)
58
59        timer = Timer(timeout_sec, kill_proc, [proc])
60        try:
61            timer.start()
62            stdout,stderr = proc.communicate()
63        finally:
64            timer.cancel()
65        if not FLAG[0]:
66            raise Exception("\nInkscape sub-process terminated after %d seconds." %(timeout_sec))
67        return (stdout,stderr)
68
69def kill_sub_process(p,timeout_sec, FLAG):
70    FLAG[0]=False
71    p.kill()
72
73##################################
74class SVG_TEXT_EXCEPTION(Exception):
75    def __init__(self, value):
76        self.value = value
77    def __str__(self):
78        return repr(self.value)
79
80class SVG_ENCODING_EXCEPTION(Exception):
81    def __init__(self, value):
82        self.value = value
83    def __str__(self):
84        return repr(self.value)
85
86class SVG_PXPI_EXCEPTION(Exception):
87    def __init__(self, value):
88        self.value = value
89    def __str__(self):
90        return repr(self.value)
91
92class CSS_value_class():
93    def __init__(self,type_name,value):
94        type_name_list = type_name.split('.')
95        try:
96            self.object_type = type_name_list[0]
97        except:
98            self.object_type = ""
99
100        try:
101            self.value_name  = type_name_list[1]
102        except:
103            self.value_name  = ""
104        self.data_string = value
105
106
107class CSS_values_class():
108    def __init__(self):
109        self.CSS_value_list = []
110
111    def add(self,type_name,value):
112        self.CSS_value_list.append(CSS_value_class(type_name,value))
113
114    def get_css_value(self,tag_type,class_val):
115        value = ""
116        for entry in self.CSS_value_list:
117            if entry.object_type == "":
118                if entry.value_name  == class_val:
119                    value = value + entry.data_string
120            if entry.object_type == tag_type:
121                if entry.value_name  == class_val:
122                    value = entry.data_string
123                    break
124        return value
125
126
127class SVG_READER(inkex.Effect):
128    def __init__(self):
129        inkex.Effect.__init__(self)
130        self.flatness = 0.01
131        self.image_dpi = 1000
132        self.inkscape_exe_list = []
133        self.inkscape_exe_list.append("C:\\Program Files\\Inkscape\\bin\\inkscape.exe")
134        self.inkscape_exe_list.append("C:\\Program Files (x86)\\Inkscape\\bin\\inkscape.exe")
135        self.inkscape_exe_list.append("C:\\Program Files\\Inkscape\\inkscape.exe")
136        self.inkscape_exe_list.append("C:\\Program Files (x86)\\Inkscape\\inkscape.exe")
137        self.inkscape_exe_list.append("/usr/bin/inkscape")
138        self.inkscape_exe_list.append("/usr/local/bin/inkscape")
139        self.inkscape_exe_list.append("/Applications/Inkscape.app/Contents/Resources/bin/inkscape")
140        self.inkscape_exe_list.append("/Applications/Inkscape.app/Contents/MacOS/Inkscape")
141        self.inkscape_exe = None
142        self.lines =[]
143        self.Cut_Type = {}
144        self.Xsize=40
145        self.Ysize=40
146        self.raster   = True
147        self.SVG_dpi = None
148
149        self.SVG_inkscape_version = None
150        self.SVG_size_mm = None
151        self.SVG_size_px = None
152        self.SVG_ViewBox = None
153
154        self.raster_PIL = None
155        self.cut_lines = []
156        self.eng_lines = []
157        self.id_cnt = 0
158
159        self.png_area = "--export-area-page"
160        self.timout = 180 #timeout time for external calls to Inkscape in seconds
161
162        self.layers = ['0']
163        self.layer = '0'
164        self.layernames = []
165        self.txt2paths = False
166        self.CSS_values = CSS_values_class()
167
168    def parse_svg(self,filename):
169        try:
170            self.parse(filename)
171            #self.parse(filename, encoding='utf-8')
172        except Exception as e:
173            exception_msg = "%s" %(e)
174            if exception_msg.find("encoding"):
175                self.parse(filename,encoding="ISO-8859-1")
176            else:
177                raise Exception(e)
178
179    def set_inkscape_path(self,PATH):
180        if PATH!=None:
181            self.inkscape_exe_list.insert(0,PATH)
182        for location in self.inkscape_exe_list:
183            if ( os.path.isfile( location ) ):
184                self.inkscape_exe=location
185                break
186
187    def colmod(self,r,g,b,path_id):
188        changed=False
189        k40_action = 'raster'
190        delta = 10
191        # Check if the color is Red (or close to it)
192        if (r >= 255-delta) and (g <= delta) and (b <= delta):
193            k40_action = "cut"
194            self.Cut_Type[path_id]=k40_action
195            (r,g,b) = (255,255,255)
196            changed=True
197        # Check if the color is Blue (or close to it)
198        elif (r <= delta) and (g <= delta) and (b >= 255-delta):
199            k40_action = "engrave"
200            self.Cut_Type[path_id]=k40_action
201            (r,g,b) = (255,255,255)
202            changed=True
203        else:
204            k40_action = "raster"
205            self.Cut_Type[path_id]=k40_action
206            changed=False
207        color_out = '#%02x%02x%02x' %(r,g,b)
208        return (color_out, changed, k40_action)
209
210
211    def process_shape(self, node, mat, group_stroke = None):
212        #################################
213        ### Determine the shape type  ###
214        #################################
215        try:
216            i = node.tag.find('}')
217            if i >= 0:
218                tag_type = node.tag[i+1:]
219        except:
220            tag_type=""
221
222        ##############################################
223        ### Set a unique identifier for each shape ###
224        ##############################################
225        self.id_cnt=self.id_cnt+1
226        path_id = "ID%d"%(self.id_cnt)
227        sw_flag = False
228        changed = False
229        #######################################
230        ### Handle references to CSS data   ###
231        #######################################
232        class_val = node.get('class')
233        if class_val:
234            css_data = ""
235            for cv in class_val.split(' '):
236                if css_data!="":
237                    css_data = self.CSS_values.get_css_value(tag_type,cv)+";"+css_data
238                else:
239                    css_data = self.CSS_values.get_css_value(tag_type,cv)
240
241            # Remove the reference to the CSS data
242            del node.attrib['class']
243
244            # Check if a style entry already exists. If it does
245            # append the the existing style data to the CSS data
246            # otherwise create a new style entry.
247            if node.get('style'):
248                if css_data!="":
249                    css_data = css_data + ";" + node.get('style')
250                    node.set('style', css_data)
251            else:
252                node.set('style', css_data)
253
254        style   = node.get('style')
255        self.Cut_Type[path_id]="raster" # Set default type to raster
256
257        text_message_warning = "SVG File with Color Coded Text Outlines Found: (i.e. Blue: engrave/ Red: cut)"
258        line1 = "SVG File with color coded text outlines found (i.e. Blue: engrave/ Red: cut)."
259        line2 = "Automatic conversion to paths failed: Try upgrading to Inkscape .90 or later"
260        line3 = "To convert manually in Inkscape: select the text then select \"Path\"-\"Object to Path\" in the menu bar."
261        text_message_fatal  = "%s\n\n%s\n\n%s" %(line1,line2,line3)
262
263        ##############################################
264        ### Handle 'style' data outside of style   ###
265        ##############################################
266        stroke_outside = node.get('stroke')
267        if not stroke_outside:
268            stroke_outside = group_stroke
269        if stroke_outside:
270            stroke_width_outside = node.get('stroke-width')
271
272            col = stroke_outside
273            col= col.strip()
274            if simplestyle.isColor(col):
275                c=simplestyle.parseColor(col)
276                (new_val,changed,k40_action)=self.colmod(c[0],c[1],c[2],path_id)
277            else:
278                new_val = col
279            if changed:
280                node.set('stroke',new_val)
281                node.set('stroke-width',"0.0")
282                node.set('k40_action', k40_action)
283                sw_flag = True
284
285            if sw_flag == True:
286                if node.tag == inkex.addNS('text','svg') or node.tag == inkex.addNS('flowRoot','svg'):
287                    if (self.txt2paths==False):
288                        raise SVG_TEXT_EXCEPTION(text_message_warning)
289                    else:
290                        raise Exception(text_message_fatal)
291
292        ###################################################
293        ### Handle 'k40_action' data outside of style   ###
294        ###################################################
295        if node.get('k40_action'):
296            k40_action = node.get('k40_action')
297            changed=True
298            self.Cut_Type[path_id]=k40_action
299
300        ##############################################
301        ### Handle 'style' data                    ###
302        ##############################################
303        if style:
304            declarations = style.split(';')
305            i_sw = -1
306
307            sw_prop = 'stroke-width'
308            for i,decl in enumerate(declarations):
309                parts = decl.split(':', 2)
310                if len(parts) == 2:
311                    (prop, col) = parts
312                    prop = prop.strip().lower()
313
314                    if prop == 'display' and col == "none":
315                        # display is 'none' return without processing group
316                        return
317
318                    if prop == 'k40_action':
319                        changed = True
320                        self.Cut_Type[path_id]=col
321
322                    #if prop in color_props:
323                    if prop == sw_prop:
324                        i_sw = i
325                    if prop == 'stroke':
326                        col= col.strip()
327                        if simplestyle.isColor(col):
328                            c=simplestyle.parseColor(col)
329                            (new_val,changed,k40_action)=self.colmod(c[0],c[1],c[2],path_id)
330                        else:
331                            new_val = col
332                        if changed:
333                            declarations[i] = prop + ':' + new_val
334                            declarations.append('k40_action' + ':' + k40_action)
335                            sw_flag = True
336            if sw_flag == True:
337                if node.tag == inkex.addNS('text','svg') or node.tag == inkex.addNS('flowRoot','svg'):
338                    if (self.txt2paths==False):
339                        raise SVG_TEXT_EXCEPTION(text_message_warning)
340                    else:
341                        raise Exception(text_message_fatal)
342
343                if i_sw != -1:
344                    declarations[i_sw] = sw_prop + ':' + "0.0"
345                else:
346                    declarations.append(sw_prop + ':' + "0.0")
347            node.set('style', ';'.join(declarations))
348        ##############################################
349
350        #####################################################
351        ### If vector data was found save the path data   ###
352        #####################################################
353        if changed:
354            if node.get('display')=='none':
355                return
356            if node.tag == inkex.addNS('path','svg'):
357                d = node.get('d')
358                if not d:
359                    return
360                p = cubicsuperpath.parsePath(d)
361            elif node.tag == inkex.addNS('rect','svg'):
362                x = 0.0
363                y = 0.0
364                if node.get('x'):
365                    x=float(node.get('x'))
366                if node.get('y'):
367                    y=float(node.get('y'))
368
369                width = float(node.get('width'))
370                height = float(node.get('height'))
371                rx = 0.0
372                ry = 0.0
373                if node.get('rx'):
374                    rx=float(node.get('rx'))
375                if node.get('ry'):
376                    ry=float(node.get('ry'))
377
378                if max(rx,ry) > 0.0:
379                    if rx==0.0 or ry==0.0:
380                        rx = max(rx,ry)
381                        ry = rx
382                    Rxmax = abs(width)/2.0
383                    Rymax = abs(height)/2.0
384                    rx = min(rx,Rxmax)
385                    ry = min(ry,Rymax)
386                    L1 = "M %f,%f %f,%f "      %(x+rx       , y          , x+width-rx , y          )
387                    C1 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x+width    , y+ry       )
388                    L2 = "M %f,%f %f,%f "      %(x+width    , y+ry       , x+width    , y+height-ry)
389                    C2 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x+width-rx , y+height   )
390                    L3 = "M %f,%f %f,%f "      %(x+width-rx , y+height   , x+rx       , y+height   )
391                    C3 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x          , y+height-ry)
392                    L4 = "M %f,%f %f,%f "      %(x          , y+height-ry, x          , y+ry       )
393                    C4 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x+rx       , y          )
394                    d =  L1 + C1 + L2 + C2 + L3 + C3 + L4 + C4
395                else:
396                    d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y,  x+width,y+height, x,y+height)
397                p = cubicsuperpath.parsePath(d)
398
399            elif node.tag == inkex.addNS('circle','svg'):
400                cx = 0.0
401                cy = 0.0
402                if node.get('cx'):
403                    cx=float(node.get('cx'))
404                if node.get('cy'):
405                    cy=float(node.get('cy'))
406                if node.get('r'):
407                    r  = float(node.get('r'))
408                    d  = "M %f,%f A   %f,%f 0 0 1 %f,%f   %f,%f 0 0 1 %f,%f   %f,%f 0 0 1 %f,%f   %f,%f 0 0 1 %f,%f Z" %(cx+r,cy, r,r,cx,cy+r,  r,r,cx-r,cy,  r,r,cx,cy-r, r,r,cx+r,cy)
409                else: #if there is no radius assume it is a path
410                    if node.get('d'):
411                        d = node.get('d')
412                        p = cubicsuperpath.parsePath(d)
413                    else:
414                        raise Exception("Radius of SVG circle is not defined.")
415                p = cubicsuperpath.parsePath(d)
416
417            elif node.tag == inkex.addNS('ellipse','svg'):
418                cx = 0.0
419                cy = 0.0
420                if node.get('cx'):
421                    cx=float(node.get('cx'))
422                if node.get('cy'):
423                    cy=float(node.get('cy'))
424                if node.get('r'):
425                    r = float(node.get('r'))
426                    rx = r
427                    ry = r
428                if node.get('rx'):
429                    rx = float(node.get('rx'))
430                if node.get('ry'):
431                    ry = float(node.get('ry'))
432
433                d  = "M %f,%f A   %f,%f 0 0 1 %f,%f   %f,%f 0 0 1 %f,%f   %f,%f 0 0 1 %f,%f   %f,%f 0 0 1 %f,%f Z" %(cx+rx,cy, rx,ry,cx,cy+ry,  rx,ry,cx-rx,cy,  rx,ry,cx,cy-ry, rx,ry,cx+rx,cy)
434                p = cubicsuperpath.parsePath(d)
435
436            elif (node.tag == inkex.addNS('polygon','svg')) or (node.tag == inkex.addNS('polyline','svg')):
437                points = node.get('points')
438                if not points:
439                    return
440
441                points = points.replace(',', ' ')
442                while points.find('  ') > -1:
443                    points = points.replace('  ', ' ')
444
445                points = points.strip().split(" ")
446                d = "M "
447                for i in range(0,len(points),2):
448                    x = float(points[i])
449                    y = float(points[i+1])
450                    d = d + "%f,%f " %(x,y)
451
452                #Close the loop if it is a ploygon
453                if node.tag == inkex.addNS('polygon','svg'):
454                    d = d + "Z"
455                p = cubicsuperpath.parsePath(d)
456
457            elif node.tag == inkex.addNS('line','svg'):
458                x1 = float(node.get('x1'))
459                y1 = float(node.get('y1'))
460                x2 = float(node.get('x2'))
461                y2 = float(node.get('y2'))
462                d = "M "
463                d = "M %f,%f %f,%f" %(x1,y1,x2,y2)
464                p = cubicsuperpath.parsePath(d)
465            else:
466                #print("something was ignored")
467                #print(node.tag)
468                return
469
470            trans = node.get('transform')
471            if trans:
472                mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
473            simpletransform.applyTransformToPath(mat, p)
474
475            ##########################################
476            ## Break Curves down into small lines  ###
477            ##########################################
478            f = self.flatness
479            is_flat = 0
480            while is_flat < 1:
481                try:
482                    cspsubdiv.cspsubdiv(p, f)
483                    is_flat = 1
484                except IndexError:
485                    break
486                except:
487                    f += 0.1
488                    if f>2 :
489                      break
490                      #something has gone very wrong.
491            ##########################################
492            rgb=(0,0,0)
493            for sub in p:
494                for i in range(len(sub)-1):
495                    x1 = sub[i][1][0]
496                    y1 = sub[i][1][1]
497                    x2 = sub[i+1][1][0]
498                    y2 = sub[i+1][1][1]
499                    self.lines.append([x1,y1,x2,y2,rgb,path_id])
500        #####################################################
501        ### End of saving the vector path data            ###
502        #####################################################
503
504
505    def process_clone(self, node):
506        trans = node.get('transform')
507        x = node.get('x')
508        y = node.get('y')
509        mat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
510        if trans:
511            mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
512        if x:
513            mat = simpletransform.composeTransform(mat, [[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]])
514        if y:
515            mat = simpletransform.composeTransform(mat, [[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]])
516        # push transform
517        if trans or x or y:
518            self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], mat))
519        # get referenced node
520        refid = node.get(inkex.addNS('href','xlink'))
521        #print(refid,node.get('id'),node.get('layer'))
522        refnode = self.getElementById(refid[1:])
523        if refnode is not None:
524            if refnode.tag == inkex.addNS('g','svg') or refnode.tag == inkex.addNS('switch','svg'):
525                self.process_group(refnode)
526            elif refnode.tag == inkex.addNS('use', 'svg'):
527                #print(refnode,'1')
528                self.process_clone(refnode)
529            else:
530                self.process_shape(refnode, self.groupmat[-1])
531        # pop transform
532        if trans or x or y:
533            self.groupmat.pop()
534
535    def process_group(self, group):
536        ##############################################
537        ### Get color set at group level
538        stroke_group = group.get('stroke')
539        if group.get('display')=='none':
540            return
541        ##############################################
542        ### Handle 'style' data
543        style = group.get('style')
544        if style:
545            declarations = style.split(';')
546            for i,decl in enumerate(declarations):
547                parts = decl.split(':', 2)
548                if len(parts) == 2:
549                    (prop, val) = parts
550                    prop = prop.strip().lower()
551                    if prop == 'stroke':
552                        stroke_group = val.strip()
553                    if prop == 'display' and val == "none":
554                        #group display is 'none' return without processing group
555                        return
556        ##############################################
557        if group.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
558            style = group.get('style')
559            if style:
560                style = simplestyle.parseStyle(style)
561                if 'display' in style:
562                    if style['display'] == 'none':
563                        #layer display is 'none' return without processing layer
564                        return
565            layer = group.get(inkex.addNS('label', 'inkscape'))
566
567            layer = layer.replace(' ', '_')
568            if layer in self.layers:
569                self.layer = layer
570        trans = group.get('transform')
571        if trans:
572            self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans)))
573        for node in group:
574            if node.tag == inkex.addNS('g','svg') or  node.tag == inkex.addNS('switch','svg'):
575                self.process_group(node)
576            elif node.tag == inkex.addNS('use', 'svg'):
577                #print(node.get('id'),'2',node.get('href'))
578                self.process_clone(node)
579
580            elif node.tag == inkex.addNS('style', 'svg'):
581                if node.get('type')=="text/css":
582                    self.parse_css(node.text)
583
584            elif node.tag == inkex.addNS('defs', 'svg'):
585                for sub in node:
586                    if sub.tag == inkex.addNS('style','svg'):
587                        self.parse_css(sub.text)
588            else:
589                self.process_shape(node, self.groupmat[-1], group_stroke = stroke_group)
590        if trans:
591            self.groupmat.pop()
592
593    def parse_css(self,css_string):
594        if css_string == None:
595            return
596        name_list=[]
597        value_list=[]
598        name=""
599        value=""
600        i=0
601        while i < len(css_string):
602            c=css_string[i]
603            if c==",":
604                i=i+1
605                name_list.append(name)
606                value_list.append(value)
607                name=""
608                value=""
609                continue
610            if c=="{":
611                i=i+1
612                while i < len(css_string):
613                    c=css_string[i]
614                    i=i+1
615                    if c=="}":
616                        break
617                    else:
618                        value = value+c
619
620                if len(value_list)>0:
621                    len_value_list = len(value_list)
622                    k=-1
623                    while abs(k) <= len_value_list and value_list[k]=="":
624                        value_list[k]=value
625                        k=k-1
626                name_list.append(name)
627                value_list.append(value)
628                name=""
629                value=""
630                continue
631            name=name+c
632            i=i+1
633        for i in range(len(name_list)):
634            name_list[i]=" ".join(name_list[i].split())
635            self.CSS_values.add(name_list[i],value_list[i])
636
637
638    def unit2mm(self, string): #,dpi=None):
639        if string==None:
640            return None
641        # Returns mm given a string representation of units in another system
642        # a dictionary of unit to user unit conversion factors
643        uuconv = {'in': 25.4,
644                  'pt': 25.4/72.0,
645                  'mm': 1.0,
646                  'cm': 10.0,
647                  'm' : 1000.0,
648                  'km': 1000.0*1000.0,
649                  'pc': 25.4/6.0,
650                  'yd': 25.4*12*3,
651                  'ft': 25.4*12}
652
653        unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
654        param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
655
656        string = string.replace(' ','')
657        p = param.match(string)
658        u = unit.search(string)
659
660        if p:
661            retval = float(p.string[p.start():p.end()])
662        else:
663            return None
664        if u:
665            retunit = u.string[u.start():u.end()]
666        else:
667            return None
668        try:
669            return retval * uuconv[retunit]
670        except KeyError:
671            return None
672
673    def unit2px(self, string):
674        if string==None:
675            return None
676        string = string.replace(' ','')
677        string = string.replace('px','')
678        try:
679            retval = float(string)
680        except:
681            retval = None
682        return retval
683
684
685    def Make_PNG(self):
686        #create OS temp folder
687        tmp_dir = tempfile.mkdtemp()
688
689        if self.inkscape_exe != None:
690            try:
691                svg_temp_file = os.path.join(tmp_dir, "k40w_temp.svg")
692                png_temp_file = os.path.join(tmp_dir, "k40w_image.png")
693                dpi = "%d" %(self.image_dpi)
694                self.document.write(svg_temp_file)
695                #self.document.write("svg_temp_file.svg", encoding='utf-8')
696
697                # Check Version of Inkscape
698                cmd = [ self.inkscape_exe, "-V"]
699                (stdout,stderr)=run_external(cmd, self.timout)
700                if stdout.find(b'Inkscape 1.')==-1:
701                    cmd = [ self.inkscape_exe, self.png_area, "--export-dpi", dpi, \
702                            "--export-background","rgb(255, 255, 255)","--export-background-opacity", \
703                            "255" ,"--export-png", png_temp_file, svg_temp_file ]
704                else:
705                    cmd = [ self.inkscape_exe, self.png_area, "--export-dpi", dpi, \
706                            "--export-background","rgb(255, 255, 255)","--export-background-opacity", \
707                            "255" ,"--export-type=png", "--export-filename=%s" %(png_temp_file), svg_temp_file ]
708
709                run_external(cmd, self.timout)
710                self.raster_PIL = Image.open(png_temp_file)
711                self.raster_PIL = self.raster_PIL.convert("L")
712            except Exception as e:
713                try:
714                    shutil.rmtree(tmp_dir)
715                except:
716                    pass
717                error_text = "%s" %(e)
718                raise Exception("Inkscape Execution Failed (while making raster data).\n%s" %(error_text))
719        else:
720            raise Exception("Inkscape Not found.")
721        try:
722            shutil.rmtree(tmp_dir)
723        except:
724            raise Exception("Temp dir failed to delete:\n%s" %(tmp_dir) )
725
726
727##    def open_cdr_file(self,filename):
728##        #create OS temp folder
729##        svg_temp_file=filename
730##        tmp_dir = tempfile.mkdtemp()
731##        if self.inkscape_exe != None:
732##            try:
733##                #svg_temp_file = os.path.join(tmp_dir, "k40w_temp.svg")
734##                txt2path_file = os.path.join(tmp_dir, "txt2path.svg")
735##                #self.document.write(svg_temp_file)
736##                cmd = [ self.inkscape_exe, "--export-text-to-path","--export-plain-svg",txt2path_file, svg_temp_file ]
737##                run_external(cmd, self.timout)
738##                self.parse_svg(txt2path_file)
739##            except Exception as e:
740##                raise Exception("Inkscape Execution Failed (while converting text to paths).\n\n"+str(e))
741##        else:
742##            raise Exception("Inkscape Not found.")
743##        try:
744##            shutil.rmtree(tmp_dir)
745##        except:
746##            raise Exception("Temp dir failed to delete:\n%s" %(tmp_dir) )
747
748    def convert_text2paths(self):
749        #create OS temp folder
750        tmp_dir = tempfile.mkdtemp()
751        if self.inkscape_exe != None:
752            try:
753                svg_temp_file = os.path.join(tmp_dir, "k40w_temp.svg")
754                txt2path_file = os.path.join(tmp_dir, "txt2path.svg")
755                self.document.write(svg_temp_file)
756
757                # Check Version of Inkscape
758                cmd = [ self.inkscape_exe, "-V"]
759                (stdout,stderr)=run_external(cmd, self.timout)
760                if stdout.find(b'Inkscape 1.')==-1:
761                    cmd = [ self.inkscape_exe, "--export-text-to-path","--export-plain-svg", \
762                            txt2path_file, svg_temp_file,  ]
763                else:
764                    cmd = [ self.inkscape_exe, "--export-text-to-path","--export-plain-svg", \
765                            "--export-filename=%s" %(txt2path_file), svg_temp_file,  ]
766
767                (stdout,stderr)=run_external(cmd, self.timout)
768                self.parse_svg(txt2path_file)
769            except Exception as e:
770                raise Exception("Inkscape Execution Failed (while converting text to paths).\n\n"+str(e))
771        else:
772            raise Exception("Inkscape Not found.")
773        try:
774            shutil.rmtree(tmp_dir)
775        except:
776            raise Exception("Temp dir failed to delete:\n%s" %(tmp_dir) )
777
778
779    def set_size(self,pxpi,viewbox):
780        width_mm = viewbox[2]/pxpi*25.4
781        height_mm = viewbox[3]/pxpi*25.4
782        self.document.getroot().set('width', '%fmm' %(width_mm))
783        self.document.getroot().set('height','%fmm' %(height_mm))
784        self.document.getroot().set('viewBox', '%f %f %f %f' %(viewbox[0],viewbox[1],viewbox[2],viewbox[3]))
785
786
787    def make_paths(self, txt2paths=False ):
788        self.txt2paths = txt2paths
789        msg               = ""
790
791        if (self.txt2paths):
792            self.convert_text2paths()
793
794        #################
795        ## GET VIEWBOX ##
796        #################
797        view_box_array = self.document.getroot().xpath('@viewBox', namespaces=inkex.NSS) #[0]
798        if view_box_array == []:
799            view_box_str = None
800        else:
801            view_box_str=view_box_array[0]
802        #################
803        ##  GET SIZE   ##
804        #################
805        h_array = self.document.getroot().xpath('@height', namespaces=inkex.NSS)
806        w_array = self.document.getroot().xpath('@width' , namespaces=inkex.NSS)
807        if h_array == []:
808            h_string = None
809        else:
810            h_string = h_array[0]
811        if w_array == []:
812            w_string = None
813        else:
814            w_string = w_array[0]
815        #################
816        w_mm = self.unit2mm(w_string)
817        h_mm = self.unit2mm(h_string)
818        w_px = self.unit2px(w_string)
819        h_px = self.unit2px(h_string)
820        self.SVG_Size = [w_mm, h_mm, w_px, h_px]
821
822        if view_box_str!=None:
823            view_box_list = view_box_str.split(' ')
824            DXpix= float(view_box_list[0])
825            DYpix= float(view_box_list[1])
826            Wpix = float(view_box_list[2])
827            Hpix = float(view_box_list[3])
828            self.SVG_ViewBox = [DXpix, DYpix, Wpix, Hpix]
829        else:
830            SVG_ViewBox = None
831
832        if h_mm==None or w_mm==None or self.SVG_ViewBox==None:
833            line1  = "Cannot determine SVG size. Viewbox missing or Units not set."
834            raise SVG_PXPI_EXCEPTION("%s" %(line1))
835
836        scale_h = h_mm/Hpix
837        scale_w = w_mm/Wpix
838        Dx = DXpix * scale_w
839        Dy = DYpix * scale_h
840
841        if abs(1.0-scale_h/scale_w) > .01:
842            line1 ="SVG Files with different scales in X and Y are not supported.\n"
843            line2 ="In Inkscape (v0.92): 'File'-'Document Properties'"
844            line3 ="on the 'Page' tab adjust 'Scale x:' in the 'Scale' section"
845            raise Exception("%s\n%s\n%s" %(line1,line2,line3))
846
847        for node in self.document.getroot().xpath('//svg:g', namespaces=inkex.NSS):
848            if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
849                layer = node.get(inkex.addNS('label', 'inkscape'))
850                self.layernames.append(layer.lower())
851                layer = layer.replace(' ', '_')
852                if layer and not layer in self.layers:
853                    self.layers.append(layer)
854
855        self.groupmat = [[[scale_w,    0.0,  0.0-Dx],
856                          [0.0  , -scale_h, h_mm+Dy]]]
857
858        self.process_group(self.document.getroot())
859
860
861        #################################################
862        xmin= 0.0
863        xmax=  w_mm
864        ymin= -h_mm
865        ymax= 0.0
866        self.Make_PNG()
867
868        self.Xsize=xmax-xmin
869        self.Ysize=ymax-ymin
870        Xcorner=xmin
871        Ycorner=ymax
872        for ii in range(len(self.lines)):
873            self.lines[ii][0] = self.lines[ii][0]-Xcorner
874            self.lines[ii][1] = self.lines[ii][1]-Ycorner
875            self.lines[ii][2] = self.lines[ii][2]-Xcorner
876            self.lines[ii][3] = self.lines[ii][3]-Ycorner
877
878        self.cut_lines = []
879        self.eng_lines = []
880        for line in self.lines:
881            ID=line[5]
882            if (self.Cut_Type[ID]=="engrave"):
883                self.eng_lines.append([line[0],line[1],line[2],line[3]])
884            elif (self.Cut_Type[ID]=="cut"):
885                self.cut_lines.append([line[0],line[1],line[2],line[3]])
886            else:
887                pass
888
889if __name__ == '__main__':
890    svg_reader =  SVG_READER()
891    #svg_reader.parse("test.svg")
892    #svg_reader.make_paths()
893    tests=["100 mm ",".1 m ","4 in ","100 px ", "100  "]
894    for line in tests:
895        print(svg_reader.unit2mm(line),svg_reader.unit2px(line))
896