#!/usr/bin/env python 
'''
This file output script for Inkscape creates Laser Draw (LaserDRW) LYZ files.

File history:
0.1 Initial code (2/5/2017)
0.2 - Added support for rectangle, circle and ellipse (2/7/2017)
    - Added option to automatically convert text to paths
0.3 - Fixed x,y translation when view box is used in SVG file for scaling (2/10/2017)
0.4 - Changed limits in resolution to 100 dpi minimum and 1000 dpi maximum (if you get an out of memory error in LaserDRW try reducing the resolution)
0.5 - Removed some messages that were not needed
    - Fixed default resolution in inx files

Copyright (C) 2017 Scorch www.scorchworks.com
Derived from dxf_outlines.py by Aaron Spike and Alvin Penner

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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
'''
# standard library
import math
import tempfile, os, sys, shutil
from subprocess import Popen, PIPE
import zipfile
import re
# local library
import inkex
import simplestyle
import simpletransform
import cubicsuperpath
import cspsubdiv
from fablabchemnitz_lyz_library import LYZ_CLASS

try:
    inkex.localize()
except:
    pass

## Subprocess timout stuff ######
from threading import Timer
def run_external(cmd, timeout_sec):
  proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
  kill_proc = lambda p: p.kill()
  timer = Timer(timeout_sec, kill_proc, [proc])
  try:
    timer.start()
    stdout,stderr = proc.communicate()
  finally:
    timer.cancel()
##################################
    
class MyEffect(inkex.Effect):
    def __init__(self):
        inkex.Effect.__init__(self)
        self.flatness = 0.01
        self.lines =[]
        self.Cut_Type = {}
        self.Xsize=40
        self.Ysize=40
        self.margin = 2
        self.PNG_DATA = None
        self.png_area = "--export-area-page"
        self.timout = 60 #timeout time for external calls to Inkscape in seconds 

        self.OptionParser.add_option("--area_select" , action="store", type="string"  , dest="area_select"  , default="page_area" )
        self.OptionParser.add_option("--cut_select"  , action="store", type="string"  , dest="cut_select"   , default="zip"       )
        self.OptionParser.add_option("--resolution"  , action="store", type="int"     , dest="resolution"   , default=1000        )
        self.OptionParser.add_option("--margin"      , action="store", type="float"   , dest="margin"       , default=2.00        )
        self.OptionParser.add_option("--inkscape_dpi", action="store", type="float"   , dest="inkscape_dpi" , default=96.0        )
        self.OptionParser.add_option("--txt2paths"   , action="store", type="inkbool" , dest="txt2paths"    , default=False       )
              
        self.layers = ['0']
        self.layer = '0'
        self.layernames = []
      
      
    def stream_binary_data(self,filename):
        # Change the format for STDOUT to binary to support
        # writing the binary output file through STDOUT
        if os.name == 'nt': #if sys.platform == "win32":
            try:
                import msvcrt
                #msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)  
                msvcrt.setmode(1, os.O_BINARY)
            except:
                pass
        # Open the temporary file for reading
        out = open(filename,'rb')
        # Send the contents of the temp file to STDOUT
        sys.stdout.write(out.read())
        out.close()
        
    
    def output(self):
        #create OS temp folder
        self.tmp_dir = tempfile.mkdtemp()
        
        if (self.cut_select=="image"  ):
            LYZ=LYZ_CLASS()
            LYZ.setup_new_header()
            LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
            LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
            LYZ.set_margin(self.margin)
            image_file = os.path.join(self.tmp_dir, "image.lyz")
            LYZ.write_file(image_file)
        
        if (self.cut_select=="all"    ) or (self.cut_select=="zip" ):
            LYZ=LYZ_CLASS()
            LYZ.setup_new_header()
            LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
            for line in self.lines:
                ID=line[7]
                if (self.Cut_Type[ID]=="cut") or (self.Cut_Type[ID]=="engrave"):
                    LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
            LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
            LYZ.set_margin(self.margin)
            all_file = os.path.join(self.tmp_dir, "all.lyz")
            LYZ.write_file(all_file)
        
        if (self.cut_select=="raster" ) or (self.cut_select=="zip" ):
            LYZ=LYZ_CLASS()
            LYZ.setup_new_header()
            LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
            LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
            LYZ.set_margin(self.margin)
            raster_file = os.path.join(self.tmp_dir, "01_raster_engrave.lyz")
            LYZ.write_file(raster_file)
        
        if (self.cut_select=="vector_red" ) or (self.cut_select=="zip" ):
            LYZ=LYZ_CLASS()
            LYZ.setup_new_header()
            for line in self.lines:
                ID=line[7]
                if (self.Cut_Type[ID]=="cut"):
                    LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
            LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
            LYZ.set_margin(self.margin)
            cut_file = os.path.join(self.tmp_dir, "03_vector_cut.lyz")
            LYZ.write_file(cut_file)
        
        if (self.cut_select=="vector_blue" ) or (self.cut_select=="zip" ):
            LYZ=LYZ_CLASS()
            LYZ.setup_new_header()
            for line in self.lines:
                ID=line[7]
                if (self.Cut_Type[ID]=="engrave"):
                    LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
            LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
            LYZ.set_margin(self.margin)
            engrave_file = os.path.join(self.tmp_dir, "02_vector_engrave.lyz")
            LYZ.write_file(engrave_file)
        
        if (self.cut_select=="image"  ):
            self.stream_binary_data(image_file)
        
        if (self.cut_select=="all"    ):
            self.stream_binary_data(all_file)
        
        if (self.cut_select=="raster" ):
            self.stream_binary_data(raster_file)
        
        if (self.cut_select=="vector_red" ):
            self.stream_binary_data(cut_file)
        
        if (self.cut_select=="vector_blue"):
            self.stream_binary_data(engrave_file)
            
        if (self.cut_select=="zip" ):
            # Add image LYZ file? Encode zip file names?
            zip_file = os.path.join(self.tmp_dir, "lyz_files.zip")
            z = zipfile.ZipFile(zip_file, 'w')
            z.write(all_file    , os.path.basename(all_file)    )
            z.write(raster_file , os.path.basename(raster_file) )
            z.write(cut_file    , os.path.basename(cut_file)    )
            z.write(engrave_file, os.path.basename(engrave_file))
            z.write(sys.argv[-1], "design.svg"                  )
            z.close()
            self.stream_binary_data(zip_file)

        #Delete the temp folder and file
        shutil.rmtree(self.tmp_dir)
        
    def dxf_line(self,csp,pen_width=0.025,color=None,path_id="",layer="none"):
        x1 =  csp[0][0]
        y1 =  csp[0][1]
        x2 =  csp[1][0]
        y2 =  csp[1][1]
        self.lines.append([x1,-y1,x2,-y2,layer,pen_width,color,path_id])

    def colmod(self,r,g,b,path_id):
        if (r,g,b) ==(255,0,0):
            self.Cut_Type[path_id]="cut"
            (r,g,b) = (255,255,255)
        elif (r,g,b)==(0,0,255):
            self.Cut_Type[path_id]="engrave"
            (r,g,b) = (255,255,255)
        else:
            self.Cut_Type[path_id]="raster"
            (r,g,b) = (0,0,0)
    
        return '%02x%02x%02x' % (r,g,b)
        
    def process_shape(self, node, mat):
        rgb = (0,0,0)
        path_id = node.get('id') 
        style   = node.get('style')
        self.Cut_Type[path_id]="raster" # Set default type to raster
        
        color_props_fill = ('fill', 'stop-color', 'flood-color', 'lighting-color')
        color_props_stroke = ('stroke',)
        color_props = color_props_fill + color_props_stroke
        
        #####################################################
        ## The following is ripped off from Coloreffect.py ##
        #####################################################
        if style:
            declarations = style.split(';')
            for i,decl in enumerate(declarations):
                parts = decl.split(':', 2)
                if len(parts) == 2:
                    (prop, col) = parts
                    prop = prop.strip().lower()
                    #if prop in color_props:
                    if prop == 'stroke':
                        col= col.strip()
                        if simplestyle.isColor(col):
                            c=simplestyle.parseColor(col)
                            new_val='#'+self.colmod(c[0],c[1],c[2],path_id)
                        else:
                            new_val = col
                        if new_val != col:
                            declarations[i] = prop + ':' + new_val
            node.set('style', ';'.join(declarations))

        #####################################################
        if node.tag == inkex.addNS('path','svg'):
            d = node.get('d')
            if not d:
                return
            p = cubicsuperpath.parsePath(d)
        elif node.tag == inkex.addNS('rect','svg'):
            x = float(node.get('x'))
            y = float(node.get('y'))
            width = float(node.get('width'))
            height = float(node.get('height'))
            #d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y,  x+width,y+height, x,y+height) 
            #p = cubicsuperpath.parsePath(d)
            rx = 0.0
            ry = 0.0
            if node.get('rx'):
                rx=float(node.get('rx'))
            if node.get('ry'):
                ry=float(node.get('ry'))
                
            if max(rx,ry) > 0.0:
                if rx==0.0 or ry==0.0:
                    rx = max(rx,ry)
                    ry = rx
                #msg = "rx = %f ry = %f " %(rx,ry)
                #inkex.errormsg(_(msg))
                L1 = "M %f,%f %f,%f "      %(x+rx       , y          , x+width-rx , y          )
                C1 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x+width    , y+ry       )
                L2 = "M %f,%f %f,%f "      %(x+width    , y+ry       , x+width    , y+height-ry)
                C2 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x+width-rx , y+height   )
                L3 = "M %f,%f %f,%f "      %(x+width-rx , y+height   , x+rx       , y+height   )
                C3 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x          , y+height-ry)
                L4 = "M %f,%f %f,%f "      %(x          , y+height-ry, x          , y+ry       )
                C4 = "A %f,%f 0 0 1 %f,%f" %(rx         , ry         , x+rx       , y          )
                d =  L1 + C1 + L2 + C2 + L3 + C3 + L4 + C4    
            else:
                d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y,  x+width,y+height, x,y+height) 
            p = cubicsuperpath.parsePath(d)
            
        elif node.tag == inkex.addNS('circle','svg'):
            cx = float(node.get('cx') )
            cy = float(node.get('cy'))
            r  = float(node.get('r'))
            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)
            p = cubicsuperpath.parsePath(d)
        
        elif node.tag == inkex.addNS('ellipse','svg'):
            cx = float(node.get('cx')) 
            cy = float(node.get('cy'))
            rx = float(node.get('rx'))
            ry = float(node.get('ry'))
            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)
            p = cubicsuperpath.parsePath(d) 
        else:
            return
        trans = node.get('transform')
        if trans:
            mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
        simpletransform.applyTransformToPath(mat, p)
        
        ###################################################
        ## Break Curves down into small lines
        ###################################################
        f = self.flatness
        is_flat = 0
        while is_flat < 1:
            try:
                cspsubdiv.cspsubdiv(p, f)
                is_flat = 1
            except IndexError:
                break
            except:
                f += 0.1
                if f>2 :
                  break
                  #something has gone very wrong.
        ###################################################
        for sub in p:
            for i in range(len(sub)-1):
                s = sub[i]
                e = sub[i+1]
                self.dxf_line([s[1],e[1]],0.025,rgb,path_id)
                
                    
    def process_clone(self, node):
        trans = node.get('transform')
        x = node.get('x')
        y = node.get('y')
        mat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
        if trans:
            mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
        if x:
            mat = simpletransform.composeTransform(mat, [[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]])
        if y:
            mat = simpletransform.composeTransform(mat, [[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]])
        # push transform
        if trans or x or y:
            self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], mat))
        # get referenced node
        refid = node.get(inkex.addNS('href','xlink'))
        refnode = self.getElementById(refid[1:])
        if refnode is not None:
            if refnode.tag == inkex.addNS('g','svg'):
                self.process_group(refnode)
            elif refnode.tag == inkex.addNS('use', 'svg'):
                self.process_clone(refnode)
            else:
                self.process_shape(refnode, self.groupmat[-1])
        # pop transform
        if trans or x or y:
            self.groupmat.pop()

    def process_group(self, group):
        if group.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
            style = group.get('style')
            if style:
                style = simplestyle.parseStyle(style)
                if style.has_key('display'):
                    if style['display'] == 'none' and self.options.layer_option and self.options.layer_option=='visible':
                        return
            layer = group.get(inkex.addNS('label', 'inkscape'))
              
            layer = layer.replace(' ', '_')
            if layer in self.layers:
                self.layer = layer
        trans = group.get('transform')
        if trans:
            self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans)))
        for node in group:
            if node.tag == inkex.addNS('g','svg'):
                self.process_group(node)
            elif node.tag == inkex.addNS('use', 'svg'):
                self.process_clone(node)
            else:
                self.process_shape(node, self.groupmat[-1])
        if trans:
            self.groupmat.pop()
            
            
    def Make_PNG(self):
        #create OS temp folder
        tmp_dir = tempfile.mkdtemp()
        svg_temp_file = os.path.join(tmp_dir, "LYZimage.svg")
        png_temp_file = os.path.join(tmp_dir, "LYZpngdata.png")
        
        dpi = "%d" %(self.options.resolution)
        if (self.cut_select=="raster") or (self.cut_select=="all") or (self.cut_select=="zip"):            
            self.document.write(svg_temp_file)            
            cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
            
            #p = Popen(cmd, stdout=PIPE, stderr=PIPE)
            #stdout, stderr = p.communicate()
            run_external(cmd, self.timout)
        else:    
            shutil.copyfile(sys.argv[-1], svg_temp_file)
            cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
            #p = Popen(cmd, stdout=PIPE, stderr=PIPE)
            #stdout, stderr = p.communicate()
            run_external(cmd, self.timout)
        try:
            with open(png_temp_file, 'rb') as f:
                self.PNG_DATA = f.read()
        except:
            inkex.errormsg(_("PNG generation timed out.\nTry saving again.\n\n"))
            
        #Delete the temp folder and any files
        shutil.rmtree(tmp_dir)   
    
    def unit2mm(self, string):
        # Returns mm given a string representation of units in another system
        # a dictionary of unit to user unit conversion factors
        uuconv = {'in': 25.4,
                  'pt': 25.4/72.0,
                  'px': 25.4/self.inkscape_dpi,
                  'mm': 1.0,
                  'cm': 10.0,
                  'm' : 1000.0,
                  'km': 1000.0*1000.0,
                  'pc': 25.4/6.0,
                  'yd': 25.4*12*3,
                  'ft': 25.4*12}
  
        unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
        param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
 
        p = param.match(string)
        u = unit.search(string)
        if p:
            retval = float(p.string[p.start():p.end()])
        else:
            inkex.errormsg(_("Size was not determined returning zero value"))
            retval = 0.0
        if u:
            retunit = u.string[u.start():u.end()]
        else:
            inkex.errormsg(_("units not understood assuming px  at %d dpi" %(self.inkscape_dpi)))
            retunit = 'px'
            
        try:
            return retval * uuconv[retunit]
        except KeyError:
            return retval
    
    def effect(self):
        
        msg = ""
        #area_select = self.options.area_select # "page_area", "object_area"
        area_select       = "page_area"
        self.cut_select   = self.options.cut_select   # "vector_red", "vector_blue", "raster", "all", "image", "Zip"
        self.margin       = self.options.margin       # float value
        self.inkscape_dpi = self.options.inkscape_dpi # float value
        self.txt2paths    = self.options.txt2paths    # boolean Value
        
        
        if (self.txt2paths):
            #create OS temp folder
            tmp_dir = tempfile.mkdtemp()
            txt2path_file = os.path.join(tmp_dir, "txt2path.svg")
            cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg",txt2path_file, sys.argv[-1] ]
            run_external(cmd, self.timout)
            self.document.parse(txt2path_file)
            #Delete the temp folder and any files
            shutil.rmtree(tmp_dir)
            
        try:
            h_uu = self.unittouu(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
            w_uu = self.unittouu(self.document.getroot().xpath('@width' , namespaces=inkex.NSS)[0])
        except:
            h_uu = inkex.unittouu(self.document.getroot().xpath('@height',namespaces=inkex.NSS)[0])
            w_uu = inkex.unittouu(self.document.getroot().xpath('@width',namespaces=inkex.NSS)[0])
        
        h_mm = self.unit2mm(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
        w_mm = self.unit2mm(self.document.getroot().xpath('@width', namespaces=inkex.NSS)[0])
        
        try:
            view_box_str = self.document.getroot().xpath('@viewBox', namespaces=inkex.NSS)[0]
            view_box_list = view_box_str.split(' ')
            Wpix = float(view_box_list[2])
            Hpix = float(view_box_list[3])
            scale = h_mm/Hpix
            Dx = float(view_box_list[0]) / ( float(view_box_list[2])/w_mm )
            Dy = float(view_box_list[1]) / ( float(view_box_list[3])/h_mm )
        except:
            #inkex.errormsg(_("Using Default Inkscape Scale"))
            scale = 25.4/self.inkscape_dpi
            Dx = 0
            Dy = 0
        
        for node in self.document.getroot().xpath('//svg:g', namespaces=inkex.NSS):
            if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
                layer = node.get(inkex.addNS('label', 'inkscape'))
                self.layernames.append(layer.lower())
                # 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:
                    # continue
                layer = layer.replace(' ', '_')
                if layer and not layer in self.layers:
                    self.layers.append(layer)

        #self.groupmat = [[[scale, 0.0, 0.0], [0.0, -scale, h_mm]]]
        self.groupmat = [[[scale,    0.0,  0.0-Dx],
                          [0.0  , -scale, h_mm+Dy]]]
        #doc = self.document.getroot()
        self.process_group(self.document.getroot())
        #################################################
        
        # msg = msg + self.getDocumentUnit() + "\n"
        # msg = msg + "scale  = %f\n" %(scale)
        msg = msg + "Height(mm)= %f\n" %(h_mm)
        msg = msg + "Width (mm)= %f\n" %(w_mm)
        # msg = msg + "h_uu   = %f\n" %(h_uu)
        # msg = msg + "w_uu   = %f\n" %(w_uu)

        #inkex.errormsg(_(msg))
        
        if (area_select=="object_area"):
            self.png_area = "--export-area-drawing" 
            xmin= self.lines[0][0]+0.0
            xmax= self.lines[0][0]+0.0
            ymin= self.lines[0][1]+0.0
            ymax= self.lines[0][1]+0.0
            for line in self.lines:
                x1= line[0]
                y1= line[1]
                x2= line[2]
                y2= line[3]
                xmin = min(min(xmin,x1),x2)
                ymin = min(min(ymin,y1),y2)
                xmax = max(max(xmax,x1),x2)
                ymax = max(max(ymax,y1),y2)
        else:
            self.png_area = "--export-area-page"
            xmin= 0.0
            xmax=  w_mm 
            ymin= -h_mm 
            ymax= 0.0
            
        self.Xsize=xmax-xmin
        self.Ysize=ymax-ymin
        Xcenter=(xmax+xmin)/2.0
        Ycenter=(ymax+ymin)/2.0
        for ii in range(len(self.lines)):
            self.lines[ii][0] = self.lines[ii][0]-Xcenter
            self.lines[ii][1] = self.lines[ii][1]-Ycenter
            self.lines[ii][2] = self.lines[ii][2]-Xcenter
            self.lines[ii][3] = self.lines[ii][3]-Ycenter
        
        if (self.cut_select=="raster") or \
           (self.cut_select=="all"   ) or \
           (self.cut_select=="image" ) or \
           (self.cut_select=="zip"   ):
            self.Make_PNG()
        
if __name__ == '__main__':
    e = MyEffect()
    e.affect()
