2020-07-30 01:16:18 +02:00
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
'''
Roland CutStudio export script
Copyright ( C ) 2014 - 2020 Max Gaukler < development @maxgaukler.de >
skeleton based on Visicut Inkscape Plugin :
Copyright ( C ) 2012 Thomas Oster , thomas . oster @rwth - aachen . de
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
'''
2021-07-15 14:19:12 +02:00
'''
KNOWN BUGS :
WONTFIX : clipping of paths doesnt work
WONTFIX : if there is any object with opacity != 100 % , inkscape exports some objects as bitmaps . They will disappear in CutStudio ! The same problem also occurs if the alpha value of stroke or fill color is not 100 % .
WONTFIX : filters ( e . g . blur ) are not supported
'''
2020-07-30 01:16:18 +02:00
import sys
import os
import subprocess
2021-07-15 14:19:12 +02:00
from subprocess import Popen
from functools import reduce
from pathlib import Path
2020-07-30 01:16:18 +02:00
import shutil
import numpy
2020-09-07 23:55:59 +02:00
import atexit
import filecmp
2020-07-30 01:16:18 +02:00
import tempfile
2021-07-15 14:19:12 +02:00
import warnings
import inkex . command
from inkex . command import inkscape
2020-07-30 01:16:18 +02:00
DEVNULL = open ( os . devnull , ' w ' )
2020-09-07 23:55:59 +02:00
atexit . register ( DEVNULL . close )
2020-07-30 01:16:18 +02:00
2020-09-07 23:55:59 +02:00
def message ( s ) :
sys . stderr . write ( s + " \n " )
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
def debug ( s ) :
2020-09-07 23:55:59 +02:00
message ( s )
2020-07-30 01:16:18 +02:00
# copied from https://github.com/t-oster/VisiCut/blob/0abe785a30d5d5085dd3b5953b38239b1ff83358/tools/inkscape_extension/visicut_export.py
2020-08-30 11:18:33 +02:00
def which ( program , raiseError , extraPaths = [ ] , subdir = None ) :
2020-07-30 01:16:18 +02:00
"""
find program in the $ PATH environment variable and in $ extraPaths .
If $ subdir is given , also look in the given subdirectory of each $ PATH entry .
"""
pathlist = os . environ [ " PATH " ] . split ( os . pathsep )
if " nt " in os . name :
pathlist . append ( os . environ . get ( " ProgramFiles " , " C: \ Program Files \\ " ) )
pathlist . append ( os . environ . get ( " ProgramFiles(x86) " , " C: \ Program Files (x86) \\ " ) )
pathlist . append ( " C: \ Program Files \\ " ) # needed for 64bit inkscape on 64bit Win7 machines
pathlist . append ( os . path . dirname ( os . path . dirname ( os . getcwd ( ) ) ) ) # portable application in the current directory
2020-08-31 21:25:41 +02:00
pathlist + = extraPaths
2020-07-30 01:16:18 +02:00
if subdir :
pathlist = [ os . path . join ( p , subdir ) for p in pathlist ] + pathlist
def is_exe ( fpath ) :
return os . path . isfile ( fpath ) and ( os . access ( fpath , os . X_OK ) or fpath . endswith ( " .exe " ) )
for path in pathlist :
exe_file = os . path . join ( path , program )
if is_exe ( exe_file ) :
return exe_file
2020-08-30 11:18:33 +02:00
if raiseError :
raise Exception ( " Cannot find " + str ( program ) + " in any of these paths: " + str ( pathlist ) + " . Either the program is not installed, PATH is not set correctly, or this is a bug. " )
else :
return None
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
# copied from https://github.com/t-oster/VisiCut/blob/0abe785a30d5d5085dd3b5953b38239b1ff83358/tools/inkscape_extension/visicut_export.py
# Strip SVG to only contain selected elements, convert objects to paths, unlink clones
# Inkscape version: takes care of special cases where the selected objects depend on non-selected ones.
# Examples are linked clones, flowtext limited to a shape and linked flowtext boxes (overflow into the next box).
#
# Inkscape is called with certain "verbs" (gui actions) to do the required cleanup
# The idea is similar to http://bazaar.launchpad.net/~nikitakit/inkscape/svg2sif/view/head:/share/extensions/synfig_prepare.py#L181 , but more primitive - there is no need for more complicated preprocessing here
def stripSVG_inkscape ( src , dest , elements ) :
# create temporary file for opening with inkscape.
# delete this file later so that it will disappear from the "recently opened" list.
2021-07-15 14:19:12 +02:00
tmpfile = tempfile . NamedTemporaryFile ( delete = False , prefix = ' temp-cutstudio- ' , suffix = ' .svg ' )
2020-07-30 01:16:18 +02:00
tmpfile . close ( )
tmpfile = tmpfile . name
shutil . copyfile ( src , tmpfile )
2021-07-15 14:19:12 +02:00
actions_list = [ ]
actions_list . append ( " ObjectToPath " )
actions_list . append ( " UnlockAllInAllLayers " )
2020-07-30 01:16:18 +02:00
2021-07-15 14:19:12 +02:00
if elements : # something is selected
actions_list . append ( " select: {} " . format ( " , " . join ( elements ) ) )
2020-07-30 01:16:18 +02:00
else :
2021-07-15 14:19:12 +02:00
actions_list . append ( " EditSelectAllInAllLayers " )
actions_list . append ( " UnhideAllInAllLayers " )
actions_list . append ( " EditInvertInAllLayers " )
actions_list . append ( " EditDelete " )
actions_list . append ( " EditSelectAllInAllLayers " )
actions_list . append ( " EditUnlinkClone " )
actions_list . append ( " ObjectToPath " )
actions_list . append ( " FileSave " )
actions = " ; " . join ( actions_list )
cli_output = inkscape ( tmpfile , " --batch-process " , actions = actions ) #process recent file
if len ( cli_output ) > 0 :
self . msg ( " Inkscape returned the following output when trying to run the file export; the file export may still have worked: " )
self . msg ( cli_output )
2020-07-30 01:16:18 +02:00
# move output to the intended destination filename
os . rename ( tmpfile , dest )
# header
# for debugging purposes you can open the resulting EPS file in Inkscape,
# select all, ungroup multiple times
# --> now you can view the exported lines in inkscape
prefix = """
% ! PS - Adobe - 3.0 EPSF - 3.0
% % LanguageLevel : 2
% % BoundingBox - 10000 - 10000 10000 10000
% % EndComments
% % BeginSetup
% % EndSetup
% % BeginProlog
% This code ( until EndProlog ) is from an inkscape - exported EPS , copyright unknown , see cairo - library
save
50 dict begin
/ q { gsave } bind def
/ Q { grestore } bind def
/ cm { 6 array astore concat } bind def
/ w { setlinewidth } bind def
/ J { setlinecap } bind def
/ j { setlinejoin } bind def
/ M { setmiterlimit } bind def
/ d { setdash } bind def
/ m { moveto } bind def
/ l { lineto } bind def
/ c { curveto } bind def
/ h { closepath } bind def
/ re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
0 exch rlineto 0 rlineto closepath } bind def
/ S { stroke } bind def
/ f { fill } bind def
/ f * { eofill } bind def
/ n { newpath } bind def
/ W { clip } bind def
/ W * { eoclip } bind def
/ BT { } bind def
/ ET { } bind def
/ pdfmark where { pop globaldict / ? pdfmark / exec load put }
{ globaldict begin / ? pdfmark / pop load def / pdfmark
/ cleartomark load def end } ifelse
/ BDC { mark 3 1 roll / BDC pdfmark } bind def
/ EMC { mark / EMC pdfmark } bind def
/ cairo_store_point { / cairo_point_y exch def / cairo_point_x exch def } def
/ Tj { show currentpoint cairo_store_point } bind def
/ TJ {
{
dup
type / stringtype eq
{ show } { - 0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
} forall
currentpoint cairo_store_point
} bind def
/ cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
/ Tf { pop / cairo_font exch def / cairo_font_matrix where
{ pop cairo_selectfont } if } bind def
/ Td { matrix translate cairo_font_matrix matrix concatmatrix dup
/ cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
/ cairo_font where { pop cairo_selectfont } if } bind def
/ Tm { 2 copy 8 2 roll 6 array astore / cairo_font_matrix exch def
cairo_store_point / cairo_font where { pop cairo_selectfont } if } bind def
/ g { setgray } bind def
/ rg { setrgbcolor } bind def
/ d1 { setcachedevice } bind def
% % EndProlog
% % Page : 1 1
% % BeginPageSetup
% % PageBoundingBox : - 10000 - 10000 10000 10000
% % EndPageSetup
% This is a severely crippled fucked - up pseudo - postscript for importing in Roland CutStudio
% Do not even try to open it with something else
% FIXME opening with inkscape currently does not show any objects , although it worked some time in the past
% Inkscape header , not used by cutstudio
% Start
q - 10000 - 10000 10000 10000 rectclip q
0 g
0.286645 w
0 J
0 j
[ ] 0.0 d
4 M q
% Cutstudio Start
"""
postfix = """
% Cutstudio End
% this is necessary for CutStudio so that the last line isnt skipped :
0 0 m
% Inkscape footer
S Q
Q Q
showpage
% % Trailer
end restore
% % EOF
"""
2021-06-02 23:30:37 +02:00
def OpenInRolandCutStudio ( src , dest , mirror = False ) :
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
def outputFromStack ( stack , n , transformCoordinates = True ) :
arr = stack [ - ( n + 1 ) : - 1 ]
if transformCoordinates :
arrTransformed = [ ]
for i in range ( n / / 2 ) :
arrTransformed + = transform ( arr [ 2 * i ] , arr [ 2 * i + 1 ] )
return output ( arrTransformed + [ stack [ - 1 ] ] )
else :
return output ( arr + [ stack [ - 1 ] ] )
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
def transform ( x , y ) :
#debug("trafo from: {} {}".format(x, y))
2020-09-07 23:55:59 +02:00
p = numpy . array ( [ [ float ( x ) , float ( y ) , 1 ] ] ) . transpose ( )
multiply = lambda a , b : numpy . matmul ( a , b )
2020-07-30 01:16:18 +02:00
# concatenate transformations by multiplying: new = transformation x previousTransformtaion
m = reduce ( multiply , scalingStack [ : : - 1 ] )
m = m . transpose ( )
#debug("with {}".format(m))
2020-09-07 23:55:59 +02:00
pnew = numpy . matmul ( m , p )
2020-07-30 01:16:18 +02:00
x = float ( pnew [ 0 ] )
y = float ( pnew [ 1 ] )
#debug("to: {} {}".format(x, y))
return [ x , y ]
2021-07-15 14:19:12 +02:00
2020-09-07 23:55:59 +02:00
def toString ( v ) :
"""
like str ( ) , but gives the exact same output for floats across python2 and python3
"""
if isinstance ( v , ( type ( float ( ) ) , type ( int ( ) ) ) ) :
return repr ( v )
else :
return str ( v )
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
def outputMoveto ( x , y ) :
[ xx , yy ] = transform ( x , y )
2020-09-07 23:55:59 +02:00
return output ( [ toString ( xx ) , toString ( yy ) , " m " ] )
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
def outputLineto ( x , y ) :
[ xx , yy ] = transform ( x , y )
2020-09-07 23:55:59 +02:00
return output ( [ toString ( xx ) , toString ( yy ) , " l " ] )
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
def output ( array ) :
2020-09-07 23:55:59 +02:00
array = list ( map ( toString , array ) )
2020-07-30 01:16:18 +02:00
output = " " . join ( array )
#debug("OUTPUT: "+output)
return output + " \n "
2021-07-15 14:19:12 +02:00
2020-07-30 01:16:18 +02:00
stack = [ ]
2020-09-07 23:55:59 +02:00
scalingStack = [ numpy . identity ( 3 ) ]
2020-07-30 01:16:18 +02:00
if mirror :
2020-09-07 23:55:59 +02:00
scalingStack . append ( numpy . diag ( [ - 1 , 1 , 1 ] ) )
2020-07-30 01:16:18 +02:00
lastMoveCoordinates = None
outputStr = prefix
inputFile = open ( src )
outputFile = open ( dest , " w " )
for line in inputFile . readlines ( ) :
line = line . strip ( )
if line . startswith ( " % " ) :
# comment line
continue
if line . endswith ( " re W n " ) :
continue # ignore clipping rectangle
#debug(line)
for item in line . split ( " " ) :
item = item . strip ( )
if item == " " :
continue
#debug("INPUT: " + item.__repr__())
stack . append ( item )
if item == " h " : # close path
assert lastMoveCoordinates , " closed path before first moveto "
outputStr + = outputLineto ( float ( lastMoveCoordinates [ 0 ] ) , float ( lastMoveCoordinates [ 1 ] ) )
elif item == " c " : # bezier curveto
outputStr + = outputFromStack ( stack , 6 )
stack = [ ]
elif item == " re " : # rectangle
x = float ( stack [ - 5 ] )
y = float ( stack [ - 4 ] )
dx = float ( stack [ - 3 ] )
dy = float ( stack [ - 2 ] )
outputStr + = outputMoveto ( x , y )
outputStr + = outputLineto ( x + dx , y )
outputStr + = outputLineto ( x + dx , y + dy )
outputStr + = outputLineto ( x , y + dy )
outputStr + = outputLineto ( x , y )
elif item == " cm " : # matrix transformation
2020-09-07 23:55:59 +02:00
newTrafo = numpy . array ( [ [ float ( stack [ - 7 ] ) , float ( stack [ - 6 ] ) , 0 ] , [ float ( stack [ - 5 ] ) , float ( stack [ - 4 ] ) , 0 ] , [ float ( stack [ - 3 ] ) , float ( stack [ - 2 ] ) , 1 ] ] )
2020-07-30 01:16:18 +02:00
#debug("applying trafo "+str(newTrafo))
2020-09-07 23:55:59 +02:00
scalingStack [ - 1 ] = numpy . matmul ( scalingStack [ - 1 ] , newTrafo )
2020-07-30 01:16:18 +02:00
elif item == " q " : # save graphics state to stack
2020-09-07 23:55:59 +02:00
scalingStack . append ( numpy . identity ( 3 ) )
2020-07-30 01:16:18 +02:00
elif item == " Q " : # pop graphics state from stack
scalingStack . pop ( )
elif item in [ " m " , " l " ] :
if item == " m " : # moveto
lastMoveCoordinates = stack [ - 3 : - 1 ]
elif item == " l " : # lineto
pass
outputStr + = outputFromStack ( stack , 2 )
stack = [ ]
else :
pass # do nothing
outputStr + = postfix
outputFile . write ( outputStr )
outputFile . close ( )
2020-08-30 11:18:33 +02:00
inputFile . close ( )
2020-07-30 01:16:18 +02:00
selectedElements = [ ]
for arg in sys . argv [ 1 : ] :
if arg [ 0 ] == " - " :
if len ( arg ) > = 5 and arg [ 0 : 5 ] == " --id= " :
selectedElements + = [ arg [ 5 : ] ]
else :
filename = arg
2020-09-07 23:55:59 +02:00
if " --selftest " in sys . argv :
filename = " ./test-input.svg "
2020-07-30 01:16:18 +02:00
if len ( selectedElements ) == 0 :
shutil . copyfile ( filename , filename + " .filtered.svg " )
else :
# only take selected elements
stripSVG_inkscape ( src = filename , dest = filename + " .filtered.svg " , elements = selectedElements )
2021-07-15 14:19:12 +02:00
actions_list = [ ]
actions_list . append ( " export-text-to-path " )
actions_list . append ( " export-ignore-filters " )
actions_list . append ( " export-area-drawing " )
actions_list . append ( " export-filename: {} " . format ( filename + " .inkscape.eps " ) )
actions_list . append ( " export-do " )
actions = " ; " . join ( actions_list )
cli_output = inkscape ( filename + " .filtered.svg " , actions = actions ) #process recent file
if len ( cli_output ) > 0 :
self . msg ( " Inkscape returned the following output when trying to run the file export; the file export may still have worked: " )
self . msg ( cli_output )
2020-07-30 01:16:18 +02:00
inkscape_eps_file = filename + " .inkscape.eps "
assert os . path . exists ( inkscape_eps_file ) , ' EPS conversion failed: command did not create result file: ' + ' " ' + ' " " ' . join ( cmd ) + ' " '
2020-09-07 23:55:59 +02:00
if " --selftest " in sys . argv :
# used for unit-testing: fixed location of output file
destination = " ./test-output-actual.cutstudio.eps "
else :
# normally
destination = filename + " .cutstudio.eps "
2021-07-11 00:13:45 +02:00
OpenInRolandCutStudio ( inkscape_eps_file , destination , mirror = ( " --mirror=true " in sys . argv ) )
2020-09-07 23:55:59 +02:00
if " --selftest " in sys . argv :
# unittest: compare with known reference output
TEST_REFERENCE_FILE = " ./test-output-reference.cutstudio.eps "
assert filecmp . cmp ( destination , TEST_REFERENCE_FILE ) , " Test output changed. Please compare " + destination + " and " + TEST_REFERENCE_FILE
print ( " Selftest successful :-) " )
sys . exit ( 0 )
2020-07-30 01:16:18 +02:00
if os . name == " nt " :
DETACHED_PROCESS = 8 # start as "daemon"
2021-07-15 14:19:12 +02:00
warnings . simplefilter ( ' ignore ' , ResourceWarning ) #suppress "enable tracemalloc to get the object allocation traceback"
proc = Popen ( [ which ( " CutStudio \ CutStudio.exe " , True ) , " /import " , destination ] , creationflags = DETACHED_PROCESS , close_fds = True )
#warnings.simplefilter("default", ResourceWarning)
2020-08-30 11:18:33 +02:00
else : #check if we have access to "wine"
2020-09-07 23:55:59 +02:00
CUTSTUDIO_C_DRIVE = str ( Path . home ( ) ) + " /.wine/drive_c/ "
CUTSTUDIO_PATH_LINUX_WINE = CUTSTUDIO_C_DRIVE + " Program Files (x86)/CutStudio/CutStudio.exe "
CUTSTUDIO_COMMANDLINE = [ " wine " , CUTSTUDIO_PATH_LINUX_WINE , " /import " , r ' C: \ cutstudio.eps ' ]
try :
if not which ( " wine " , False ) :
raise Exception ( " Cannot find ' wine ' " )
if not os . path . exists ( CUTSTUDIO_PATH_LINUX_WINE ) :
raise Exception ( " Cannot find CutStudio in " + CUTSTUDIO_PATH_LINUX_WINE )
shutil . copyfile ( destination , CUTSTUDIO_C_DRIVE + " cutstudio.eps " )
subprocess . check_call ( CUTSTUDIO_COMMANDLINE )
except Exception as exc :
message ( " Could not open CutStudio. \n Instead, your file was saved to: \n " + destination + " \n " + \
" Please open that with CutStudio manually. \n \n " + \
" Tip: On Linux, you can use ' wine ' to install CutStudio 3.10. Then, the file will be directly opened with CutStudio. \n " + \
2021-07-15 14:19:12 +02:00
" Diagnostic information: \n " + str ( exc ) )