Source code for inkscape.inkext

#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
Inkscape extension boilerplate class.
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
from future_builtins import *

import os
import sys
import optparse
import math
import datetime
import gettext
import logging

from . import inksvg

_ = gettext.gettext
logger = logging.getLogger(__name__)

# Default name of debug output layer
_DEBUG_LAYER_NAME = 'inkext_debug'


def _check_inkbool(dummy_option, opt_str, value):
    """Convert a string boolean (ie 'True' or 'False') to Python boolean."""
    boolstr = str(value).upper()
    if boolstr in ('TRUE', 'T', 'YES', 'Y'):
        return True
    elif boolstr in ('FALSE', 'F', 'NO', 'N'):
        return False
    else:
        errstr = 'option %s: invalid inkbool value: %s' % (opt_str, value)
        raise optparse.OptionValueError(errstr)


def _check_degrees(dummy_option, opt_str, value):
    """Convert an angle specified in degrees to radians."""
    try:
        degree_angle = float(value)
        return math.radians(degree_angle)
    except:
        errstr = 'option %s: invalid degree value: %s' % (opt_str, value)
        raise optparse.OptionValueError(errstr)


def _check_percent(dummy_option, opt_str, value):
    """Convert a percentage specified as 0-100 to a float 0-1.0."""
    try:
        return float(value) / 100
    except:
        errstr = 'option %s: invalid percent value: %s' % (opt_str, value)
        raise optparse.OptionValueError(errstr)


[docs]class ExtOption(optparse.Option): """Subclass of optparse.Option that adds additional type checkers for handling Inkscape-specific types. This should be used in lieu of optparse.Option for Inkscape extensions. """ # TODO: switch to argparse... _EXT_TYPES = ('inkbool', 'docunits', 'degrees',) _EXT_TYPE_CHECKER = {'inkbool': _check_inkbool, 'degrees': _check_degrees, 'percent': _check_percent, 'docunits': optparse.Option.TYPE_CHECKER['float']} optparse.Option.TYPES = optparse.Option.TYPES + _EXT_TYPES optparse.Option.TYPE_CHECKER.update(_EXT_TYPE_CHECKER)
# TYPES = optparse.Option.TYPES + ('inkbool', 'docunits', 'degrees',) # TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER) # TYPE_CHECKER['inkbool'] = _check_inkbool # TYPE_CHECKER['degrees'] = _check_degrees # # This is just a placeholder since the checker needs a # # document unit type to convert GUI values to user units. # TYPE_CHECKER['docunits'] = TYPE_CHECKER['float']
[docs]class InkscapeExtension(object): """Base class for Inkscape extensions. This does not depend on Inkscape being installed and can be invoked as a stand-alone application. If an input document is not specified a new blank SVG document will be created. This replaces inkex.Effect which ships with Inkscape. See Also: inkex.Effect """ # Built-in default extension options. These are commonly used... _DEFAULT_OPTIONS = ( # This option is used by Inkscape to pass the ids of selected # SVG elements ExtOption('--id', action='append', dest='ids', default=[], help=_('id attribute of selected objects.')), # This option is used by Inkscape to pass a list of selected # Inkscape path nodes. # Each list element is a string of the form # '<path-id>:<subpath-index>:<node-index>' ExtOption('--selected-nodes', action='append', default=[], help=_('Selected nodes')), # Used by Inkscape extension dialog to keep track of current tab ExtOption('--active-tab',), ExtOption('--output-file', '-o', help=_('Output file.')), ExtOption('--doc-width', type='float', default=500, help=_('Document width')), ExtOption('--doc-height', type='float', default=500, help=_('Document height')), ExtOption('--doc-units', default='px', help=_('Document units (in, mm, px, etc)')), ExtOption('--create-debug-layer', type='inkbool', default=False, help=_('Create debug layer')), ExtOption('--log-create', type='inkbool', default=False, help='Create log file'), ExtOption('--log-level', default='DEBUG', help=_('Log level')), ExtOption('--log-filename', default='~/inkext.log', help=_('Full pathname of log file')), ) def __init__(self): """""" #: Parsed command line option values available to the extension self.options = None #: SVG context for this extension self.svg = None #: Debug SVG context if a debug layer has been created self.debug_svg = None # List of selected SVG elements self._selected_elements = [] # List of selected Inkscape path nodes self._selected_nodes = []
[docs] def main(self, optionspec=None, flip_debug_layer=False, debug_layer_name=_DEBUG_LAYER_NAME): """Main entry point for the extension. Args: optionspec: An optional list of :class:`optarg.Option` objects. flip_debug_layer: Flip the Y axis of the debug layer. This is useful if the GUI coordinate origin is at the bottom left. Default is False. """ # Parse command line options self.options, args = self._process_options(sys.argv[1:], optionspec) if args: # Parse the SVG document from a file. # This may contain a document unit type # so this needs to be done before the options of # type 'docunits' can be converted to user units. self.svg = inksvg.InkscapeSVGContext.parse(args[0]) # Convert 'docunits' type options to user units. self._post_process_options(self.options, self.svg.get_document_units()) else: # Convert 'docunits' type options to user units. # Width and height will be needed to create the new SVG document. self._post_process_options(self.options, self.options.doc_units) # Create a new blank SVG document context document = inksvg.create_inkscape_document( self.options.doc_width, self.options.doc_height, doc_units=self.options.doc_units) self.svg = inksvg.InkscapeSVGContext(document) # Create debug log file if specified. # The log file name is derived from a command line option # so this needs to be done after option parsing. if getattr(self.options, 'log_create', False): self.create_log(getattr(self.options, 'log_filename'), getattr(self.options, 'log_level')) # Create debug layer and context if specified if getattr(self.options, 'create_debug_layer', False): self.debug_svg = inksvg.InkscapeSVGContext(self.svg.document) debug_layer = self.debug_svg.create_layer(debug_layer_name, flipy=flip_debug_layer) self.debug_svg.current_parent = debug_layer # Create a list of selected elements. # Inkscape passes a list of element node ids via the '--ids' # command line option. if getattr(self.options, 'ids', False): for node_id in self.options.ids: node = self.svg.get_node_by_id(node_id) self._selected_elements.append(node) # Create a list of selected Inkscape path nodes if any. # TODO: # logger.debug('nodes: ' + str(self.options.selected_nodes)) # for opt_str in self.options.docunit_options: # value = self.options.docunit_options[opt_str] # uvalue = getattr(self.options, opt_str) # Run the extension self.run() # Write the output. Default is stdout. self.svg.write(filename=getattr(self.options, 'output_file', None))
[docs] def run(self): """Extensions override this method to do the actual work.""" pass
[docs] def get_elements(self, selected_only=False): """Get selected document elements. Tries to get selected elements first. If nothing is selected and `selected_only` is False then <strike>either the currently selected layer or</strike> the document root is returned. The elements may or may not be visible. Args: selected_only: Get selected elements only. Default is False. Returns: A (possibly empty) iterable collection of elements. """ elements = self._selected_elements if not elements and not selected_only: elements = self.svg.docroot # elements = self.svg.get_selected_layer() # if (elements is None or not len(elements) # or not self.svg.node_is_visible(elements)): # elements = self.svg.docroot return elements
[docs] def errormsg(self, msg): """Intended for end-user-visible error messages. Inkscape displays stderr output with an error dialog. """ sys.stderr.write((unicode(msg) + "\n").encode("UTF-8"))
[docs] def create_log(self, log_path=None, log_level='DEBUG'): """Create a log file for debug output. Args: log_path: Path to log file. If None or empty the log path name will be the command line invocation name (argv[0]) with a '.log' suffix in the user's home directory. log_level: Log level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. Default is 'DEBUG'. """ if log_path is None or not log_path: log_dir = os.path.expanduser('~') log_path = os.path.join(log_dir, sys.argv[0] + '.log') log_path = os.path.expanduser(log_path) logging.basicConfig(filename=os.path.abspath(log_path), filemode='w', level=log_level.upper()) logger = logging.getLogger(__name__) logger.info('Log started %s, level=%s', datetime.datetime.now(), logging.getLevelName(logger.getEffectiveLevel()))
def _process_options(self, argv, optionspec): """Set up option spec and parse command line options. """ docunit_options = {} def _check_docunits(option, dummy_opt_str, value): # Docunits will be converted to user units later # once an SVG context is created. docunit_options[option.dest] = float(value) return value ExtOption.TYPE_CHECKER['docunits'] = _check_docunits option_parser = optparse.OptionParser( usage='usage: %prog [options] [file]', option_list=self._DEFAULT_OPTIONS, option_class=ExtOption) if optionspec is not None: for option in optionspec: option_parser.add_option(option) options, args = option_parser.parse_args(argv) options.docunit_options = docunit_options return (options, args) def _post_process_options(self, options, doc_units): """ Options values that are of type 'docunits' will be converted to SVG user units. """ # This needs to be done after the SVG document is parsed # so that the document unit can be determined. # If it's a new document then the unit type is hopefully # specified as a command line option. If not, a default # will be used. if doc_units is None or not doc_units: doc_units = 'px' for opt_str in options.docunit_options: value = options.docunit_options[opt_str] uu_value = self.svg.unit2uu(value, from_unit=doc_units) setattr(options, opt_str, uu_value)