2020-07-30 01:16:18 +02:00
#!/usr/bin/env python3
'''
This extension strips everything which is not selected from
the current svg , saves it and
calls VisiCut on it .
Copyright ( C ) 2012 Thomas Oster , thomas . oster @rwth - aachen . de
Copyright ( C ) 2014 - 2018 Max Gaukler , development @maxgaukler.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
'''
import sys
import os
import re
import subprocess
from subprocess import Popen
import traceback
import tempfile
import unicodedata
import codecs
DEVNULL = open ( os . devnull , ' w ' )
def get_single_instance_port ( ) :
"""
get the single instance port used by VisiCut .
CAUTION : This code must behave exactly the same as ApplicationInstanceManager . getSingleInstancePort ( ) in Visicut .
"""
port = 6543
if ( sys . platform == " linux " ) :
d = os . environ . get ( " DISPLAY " )
if ( d != None ) :
d = d . split ( ' : ' ) [ 1 ] . split ( ' . ' ) [ 0 ]
port + = int ( d )
if ( sys . platform == " win32 " ) :
d = os . environ . get ( " SESSIONNAME " )
if d == None :
# no Terminal Services installed
pass
else :
# get session ID
try :
CREATE_NO_WINDOW = 0x08000000
id = subprocess . check_output ( " powershell -Command (get-process -pid $pid).sessionid " , creationflags = CREATE_NO_WINDOW )
id = int ( id . strip ( ) )
port + = 2 + id
except :
sys . stderr . write ( " Warning: Cannot determine session ID. please report this. \n " )
return port
# if Visicut or Inkscape cannot be found, change these lines here to VISICUTDIR="C:/Programs/Visicut" or wherever you installed it.
# please use forward slashes (/), not backslashes (\).
#
# example:
# VISICUTDIR="C:/Program Files (x86)/VisiCut/"
# INKSCAPEDIR="C:/Program Files (x86)/Inkscape/"
VISICUTDIR = " "
INKSCAPEDIR = " "
# wether to add (true) or replace (false) current visicut's content
IMPORT = True
# Store the IDs of selected Elements
elements = [ ]
arguments = [ ]
for arg in sys . argv [ 1 : ] :
if arg [ 0 ] == " - " :
if len ( arg ) > = 5 and arg [ 0 : 5 ] == " --id= " :
elements + = [ arg [ 5 : ] ]
elif len ( arg ) > = 13 and arg [ 0 : 13 ] == " --visicutbin= " :
# unused
pass
# VISICUTBIN=arg[13:]
elif len ( arg ) > = 9 and arg [ 0 : 9 ] == " --import= " :
IMPORT = " true " in arg [ 9 : ]
else :
arguments + = [ arg ]
else :
filename = arg
if IMPORT :
# do not replace old file
arguments + = [ " --add " ]
# find executable in the PATH
def which ( program , extraPaths = [ ] ) :
pathlist = extraPaths + os . environ [ " PATH " ] . split ( os . pathsep ) + [ " " ]
if " nt " in os . name : # Windows
if not program . lower ( ) . endswith ( " .exe " ) :
program + = " .exe "
programfiles = os . environ . get ( " ProgramFiles " , " C: \\ Program Files \\ " )
programfiles86 = os . environ . get ( " ProgramFiles(x86) " , " C: \\ Program Files (x86) \\ " )
# also look in %ProgramFiles%/yourProgram/yourProgram.exe
pathlist + = [ programfiles + " \\ " + program + " \\ " , programfiles86 + " \\ " + program + " \\ " ]
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
raise Exception ( " Cannot find executable {0} in PATH= {1} . \n \n "
" Please report this bug on https://github.com/t-oster/VisiCut/issues \n \n "
" For a quick fix: Set VISICUTDIR and INKSCAPEDIR in "
" {2} "
. format ( repr ( program ) , repr ( pathlist ) , os . path . realpath ( __file__ ) ) )
def inkscape_version ( ) :
""" determine if Inkscape is version 0 or 1 """
version = subprocess . check_output ( [ INKSCAPEBIN , " --version " ] , stderr = DEVNULL ) . decode ( ' ASCII ' , ' ignore ' )
assert version . startswith ( " Inkscape " )
if version . startswith ( " Inkscape 0 " ) :
return 0
else :
return 1
# 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 ) :
version = inkscape_version ( )
# create temporary file for opening with inkscape.
# delete this file later so that it will disappear from the "recently opened" list.
tmpfile = tempfile . NamedTemporaryFile ( delete = False , prefix = ' temp-visicut- ' , suffix = ' .svg ' )
tmpfile . close ( )
tmpfile = tmpfile . name
import shutil
shutil . copyfile ( src , tmpfile )
if version == 0 :
# inkscape 0.92 long-term-support release. Will be in Linux distributions until 2025 or so
# Selection commands: select items, invert selection, delete
selection = [ ]
for el in elements :
selection + = [ " --select= " + el ]
if len ( elements ) > 0 :
# selection += ["--verb=FitCanvasToSelection"] # TODO add a user configuration option whether to keep the page size (and by this the position relative to the page)
selection + = [ " --verb=EditInvertInAllLayers " , " --verb=EditDelete " ]
hidegui = [ " --without-gui " ]
# currently this only works with gui because of a bug in inkscape: https://bugs.launchpad.net/inkscape/+bug/843260
hidegui = [ ]
command = [ INKSCAPEBIN ] + hidegui + [ tmpfile , " --verb=UnlockAllInAllLayers " , " --verb=UnhideAllInAllLayers " ] + selection + [ " --verb=EditSelectAllInAllLayers " , " --verb=EditUnlinkClone " , " --verb=ObjectToPath " , " --verb=FileSave " , " --verb=FileQuit " ]
else :
# Inkscape 1.0, to be released ca 2020
# inkscape --select=... --verbs=...
# (see inkscape --help, inkscape --verb-list)
command = [ INKSCAPEBIN , tmpfile , " --batch-process " ]
verbs = [ " ObjectToPath " , " UnlockAllInAllLayers " ]
if elements : # something is selected
# --select=object1,object2,object3,...
command + = [ " --select= " + " , " . join ( elements ) ]
else :
verbs + = [ " EditSelectAllInAllLayers " ]
verbs + = [ " UnhideAllInAllLayers " , " EditInvertInAllLayers " , " EditDelete " , " EditSelectAllInAllLayers " , " EditUnlinkClone " , " ObjectToPath " , " FileSave " ]
# --verb=action1;action2;...
command + = [ " --verb= " + " ; " . join ( verbs ) ]
DEBUG = False
if DEBUG :
# Inkscape sometimes silently ignores wrong verbs, so we need to double-check that everything's right
for verb in verbs :
verb_list = [ line . split ( " : " ) [ 0 ] for line in subprocess . check_output ( [ INKSCAPEBIN , " --verb-list " ] , stderr = DEVNULL ) . split ( " \n " ) ]
if verb not in verb_list :
sys . stderr . write ( " Inkscape does not have the verb ' {} ' . Please report this as a VisiCut bug. " . format ( verb ) )
inkscape_output = " (not yet run) "
try :
#sys.stderr.write(" ".join(command))
# run inkscape, buffer output
inkscape = subprocess . Popen ( command , stdout = subprocess . PIPE , stderr = subprocess . STDOUT )
inkscape_output = inkscape . communicate ( ) [ 0 ]
if inkscape . returncode != 0 :
sys . stderr . write ( " Error: cleaning the document with inkscape failed. Something might still be shown in visicut, but it could be incorrect. \n Inkscape ' s output was: \n " + inkscape_output )
except :
sys . stderr . write ( " Error: cleaning the document with inkscape failed. Something might still be shown in visicut, but it could be incorrect. Exception information: \n " + str ( sys . exc_info ( ) [ 0 ] ) + " Inkscape ' s output was: \n " + inkscape_output )
# move output to the intended destination filename
os . rename ( tmpfile , dest )
"""
Get document name ( original filename ) from Inkscape SVG
Inkscape saves the file to a random temporary name .
However , the original filename is stored inside the SVG .
"""
def get_original_filename ( filename ) :
docname = None
# parse SVG for docname tag
with codecs . open ( filename , " r " , encoding = ' utf-8 ' ) as f :
for line in f :
if ' sodipodi:docname= " ' in line :
matches = re . search ( ' sodipodi:docname= " (.*).svg " ' , line )
if not matches :
break
try :
docname = matches . group ( 1 )
except IndexError :
# something is wrong with this line
break
# unescape XML string
docname = docname . replace ( ' < ' , ' < ' )
docname = docname . replace ( ' > ' , ' > ' )
docname = docname . replace ( ' " ' , ' " ' )
docname = docname . replace ( ' & ' , ' & ' )
# normalize accented characters (äöü -> aou)
docname = unicodedata . normalize ( ' NFKD ' , docname ) . encode ( ' ASCII ' , ' ignore ' ) . decode ( ' ASCII ' )
break
if not docname :
# failed to read filename from SVG, return original one
docname = os . path . basename ( filename )
if str . endswith ( docname , " .svg " ) :
docname = docname [ : - 4 ]
if str . startswith ( docname , " ink_ext_ " ) :
# inkscape temporary file, the name is useless
docname = " new "
# sanitize the filename:
# filter out special characters (@/\& ...)
docname = " " . join ( x for x in docname if ( x . isalnum ( ) or x in " ._- " ) )
docname = docname + " .svg "
return docname
# find executable paths
import platform
if platform . system ( ) == ' Darwin ' :
VISICUTBIN = which ( " VisiCut.MacOS " , [ VISICUTDIR ] )
elif " nt " in os . name : # Windows
VISICUTBIN = which ( " VisiCut.exe " , [ VISICUTDIR ] )
else :
VISICUTBIN = which ( " VisiCut.Linux " , [ VISICUTDIR , " /usr/share/visicut " ] )
INKSCAPEBIN = which ( " inkscape " , [ INKSCAPEDIR ] )
tmpdir = tempfile . mkdtemp ( prefix = ' temp-visicut- ' )
dest_filename = os . path . join ( tmpdir , get_original_filename ( filename ) )
# remove all non-selected elements and convert inkscape-specific elements (text-to-path etc.)
stripSVG_inkscape ( src = filename , dest = dest_filename , elements = elements )
# Try to connect to running VisiCut instance
# Note: this step may be omitted, as VisiCut will do the same.
# However, doing it here saves 2-3 seconds of waiting time on Windows.
try :
import socket
s = socket . socket ( )
s . connect ( ( " localhost " , get_single_instance_port ( ) ) )
if IMPORT :
s . send ( " @ " + dest_filename + " \n " )
else :
s . send ( dest_filename + " \n " )
s . close ( )
sys . exit ( 0 )
except SystemExit as e :
sys . exit ( e )
except :
pass
# Try to start own VisiCut instance
try :
2021-05-24 20:45:14 +02:00
daemonize = True
2020-07-30 01:16:18 +02:00
creationflags = 0
close_fds = False
if os . name == " nt " :
DETACHED_PROCESS = 8 # start as "daemon"
creationflags = DETACHED_PROCESS
close_fds = True
else :
try :
import daemonize
daemonize . createDaemon ( )
except :
2021-05-24 20:45:14 +02:00
daemonize = False
2020-07-30 01:16:18 +02:00
cmd = [ VISICUTBIN ] + arguments + [ dest_filename ]
2021-05-24 20:45:14 +02:00
if daemonize is True :
Popen ( cmd , creationflags = creationflags , close_fds = close_fds )
else :
Popen ( cmd , start_new_session = True , stdout = subprocess . DEVNULL , stderr = subprocess . DEVNULL , creationflags = creationflags , close_fds = close_fds )
2020-07-30 01:16:18 +02:00
except :
sys . stderr . write ( " Can not start VisiCut ( " + str ( sys . exc_info ( ) [ 0 ] ) + " ). Please start manually or change the VISICUTDIR variable in the Inkscape-Extension script \n " )
# TODO (complicated, probably WONTFIX): cleanup temporary directories -- this is really difficult because we need to make sure that visicut no longer needs the file, even for reloading!