370 lines
14 KiB
Python
370 lines
14 KiB
Python
|
#!/usr/bin/env python2
|
||
|
# -*- coding: utf-8 -*-
|
||
|
'''
|
||
|
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')
|
||
|
|
||
|
SINGLEINSTANCEPORT = 6543
|
||
|
|
||
|
# if on linux, add display variable to singleinstanceport
|
||
|
if (sys.platform == "linux"):
|
||
|
d = os.environ.get("DISPLAY")
|
||
|
if (d != None):
|
||
|
d = d.split(':')[1].split('.')[0]
|
||
|
SINGLEINSTANCEPORT += int(d)
|
||
|
|
||
|
# if on Windows with Terminal Services, choose a singleinstanceport unique for each session ID.
|
||
|
# note: we cannot use SESSIONNAME here because it can change when disconnecting and reconnecting a session!
|
||
|
# (think of SESSIONNAME like a display that can be connected to different session IDs)
|
||
|
if (sys.platform == "win32"):
|
||
|
d = os.environ.get("SESSIONNAME")
|
||
|
if d == None:
|
||
|
# no Terminal Services installed
|
||
|
pass
|
||
|
else:
|
||
|
# get ID by parsing output of `query session`:
|
||
|
# the relevant line looks like:
|
||
|
# >rdp-tcp#0 Fablab 12 Aktiv rdpwd
|
||
|
# where "12" is the ID.
|
||
|
|
||
|
def querySession():
|
||
|
query = Popen(["query", "session"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||
|
return query.communicate()[0]
|
||
|
try:
|
||
|
query_output = None
|
||
|
query_output = querySession()
|
||
|
except WindowsError:
|
||
|
# as if this is not easy enough, we have to invoke some black magic to make this work with 32bit python on 64bit windows
|
||
|
# (query.exe lives in Windows/System32 folder, but access to this is redirected to the Syswow64 folder for 32bit applications, where no query.exe exists!)
|
||
|
# https://mail.python.org/pipermail/python-win32/2009-June/009263.html
|
||
|
import ctypes
|
||
|
k32 = ctypes.windll.kernel32
|
||
|
wow64 = ctypes.c_long(0)
|
||
|
# disable system32 redirection
|
||
|
try:
|
||
|
k32.Wow64DisableWow64FsRedirection(ctypes.byref(wow64))
|
||
|
# do what we want
|
||
|
try:
|
||
|
query_output = querySession()
|
||
|
except WindowsError:
|
||
|
# in some cases query doesn't exist on windows 7 if terminal services isn't installed
|
||
|
pass
|
||
|
finally:
|
||
|
# re-enable system32 redirection
|
||
|
k32.Wow64EnableWow64FsRedirection(wow64)
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
|
||
|
if query_output is not None:
|
||
|
id = None
|
||
|
for line in query_output.splitlines():
|
||
|
if line.startswith(">"): # current session
|
||
|
numbers = re.findall("[0-9]+", line)
|
||
|
id = int(numbers[-1]) # ID is the last number on the line
|
||
|
break
|
||
|
assert id, "could not parse TS session ID"
|
||
|
assert 0 < id < 1000
|
||
|
SINGLEINSTANCEPORT += 2 + id
|
||
|
|
||
|
# 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 = []
|
||
|
|
||
|
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 = arg[9:]
|
||
|
else:
|
||
|
arguments += [arg]
|
||
|
else:
|
||
|
filename = arg
|
||
|
|
||
|
# 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(str(program), str(pathlist), os.path.realpath(__file__)))
|
||
|
|
||
|
# def removeAllButThem(element, elements):
|
||
|
# if element.get('id') in elements:
|
||
|
# return True
|
||
|
# else:
|
||
|
# keepSubtree = False
|
||
|
# for e in element:
|
||
|
# if not removeAllButThem(e, elements):
|
||
|
# element.remove(e)
|
||
|
# else:
|
||
|
# keepSubtree = True
|
||
|
# return keepSubtree
|
||
|
#
|
||
|
# Strip SVG to only contain selected elements
|
||
|
# LXML version
|
||
|
# def stripSVG_lxml(src,dest,elements):
|
||
|
# try:
|
||
|
# from lxml import etree
|
||
|
# tree = etree.parse(src)
|
||
|
# if len(elements) > 0:
|
||
|
# removeAllButThem(tree.getroot(), elements)
|
||
|
# tree.write(dest)
|
||
|
# except:
|
||
|
# sys.stderr.write("Python-LXML not installed. Can only send complete SVG\n")
|
||
|
# import shutil
|
||
|
# shutil.copyfile(src, dest)
|
||
|
|
||
|
|
||
|
|
||
|
def inkscape_version():
|
||
|
"""determine if Inkscape is version 0 or 1"""
|
||
|
version = subprocess.check_output([INKSCAPEBIN, "--version"], stderr=DEVNULL)
|
||
|
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.\nInkscape'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')
|
||
|
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
|
||
|
try:
|
||
|
import socket
|
||
|
s = socket.socket()
|
||
|
s.connect(("localhost", SINGLEINSTANCEPORT))
|
||
|
if (IMPORT == "true" or IMPORT == true or IMPORT == "\"true\""):
|
||
|
s.send("@" + dest_filename + "\n")
|
||
|
else:
|
||
|
s.send(dest_filename + "\n")
|
||
|
s.close()
|
||
|
sys.exit(0)
|
||
|
except SystemExit, e:
|
||
|
sys.exit(e)
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
# Try to start own VisiCut instance
|
||
|
try:
|
||
|
arguments = ["--singleinstanceport", str(SINGLEINSTANCEPORT)]
|
||
|
|
||
|
creationflags = 0
|
||
|
close_fds = False
|
||
|
if os.name == "nt":
|
||
|
DETACHED_PROCESS = 8 # start as "daemon"
|
||
|
creationflags = DETACHED_PROCESS
|
||
|
close_fds = True
|
||
|
else:
|
||
|
try:
|
||
|
import fablabchemnitz_daemonize
|
||
|
daemonize.createDaemon()
|
||
|
except:
|
||
|
sys.stderr.write("Could not daemonize. Sorry, but Inkscape was blocked until VisiCut is closed")
|
||
|
cmd = [VISICUTBIN] + arguments + [dest_filename]
|
||
|
Popen(cmd, creationflags=creationflags, close_fds=close_fds)
|
||
|
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!
|