#!/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!