kanta's spike

Simple Inkscape Scriptingをコマンドラインで実行するときに、スクリプトにdata.txtのような追加の引数を渡したい。1

% simple_inkscape_scripting.py --py-source my_script.py input.svg data.txt

現状のソースの解析結果

  1. Simple Inkscape Scriptingは、SimpleInkscapeScriptingクラスのadd_arguments(self, pars)2で、コマンドライン引数を定義している。

    class SimpleInkscapeScripting(inkex.EffectExtension):
         # ..略..
         def add_arguments(self, pars):
             'Process program parameters passed in from the UI.'
             pars.add_argument('--tab', dest='tab',
                             help='The selected UI tab when OK was pressed')
             pars.add_argument('--program', type=str,
                             help='Python code to execute')
             pars.add_argument('--py-source', type=str,
                             help='Python source file to execute')
         # ..略..
    
  2. inkex.EffectExtensionの親クラスであるInkscapeExtension 3SvgInputMixin 4 のコンストラクタで標準のコマンドライン引数を定義し、さらに、add_arguments(self, pars)を呼び出し、子クラス固有のコマンドライン引数を定義している。

    class InkscapeExtension:
        # ..略..
        def __init__(self):
                # ..略..
                self.arg_parser = ArgumentParser(description=self.__doc__)
    
                self.arg_parser.add_argument(
                    "input_file",
                    nargs="?",
                    metavar="INPUT_FILE",
                    type=filename_arg,
                    help="Filename of the input file (default is stdin)",
                    default=None,
    
                )
    
                self.arg_parser.add_argument(
                    "--output",
                    type=str,
                    default=None,
                    help="Optional output filename for saving the result (default is stdout).",
                )
    
                self.add_arguments(self.arg_parser)
                # ..略..
    
    class SvgInputMixin(_Base):
         # ..略..
         def __init__(self):
            # ..略..
            self.arg_parser.add_argument(
                "--id",
                action="append",
                type=str,
                dest="ids",
                default=[],
                help="id attribute of object to manipulate",
            )
    
            self.arg_parser.add_argument(
                "--selected-nodes",
                action="append",
                type=str,
                dest="selected_nodes",
                default=[],
                help="id:subpath:position of selected nodes, if any",
            )
        # ..略..
    
  3. そのため、ターミナルからsimple_inkscape_scripting.py-hオプションを指定して実行した結果は以下となる。

    % DYLD_LIBRARY_PATH=/Applications/Inkscape.app/Contents/Resources/lib PYTHONPATH=/Applications/Inkscape.app/Contents/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages:/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions /Applications/Inkscape.app/Contents/Resources/bin/python3 ~/Library/Application\ Support/org.inkscape.Inkscape/config/inkscape/extensions/SimpInkScr/simpinkscr/simple_inkscape_scripting.py -h
    usage: simple_inkscape_scripting.py [-h] [--output OUTPUT] [--tab TAB]
                                        [--program PROGRAM]
                                        [--py-source PY_SOURCE] [--id IDS]
                                        [--selected-nodes SELECTED_NODES]
                                        [INPUT_FILE]
    
    Help the user create Inkscape objects with a simple API.
    
    positional arguments:
    INPUT_FILE            Filename of the input file (default is stdin)
    
    options:
    -h, --help            show this help message and exit
    --output OUTPUT       Optional output filename for saving the result
                            (default is stdout).
    --tab TAB             The selected UI tab when OK was pressed
    --program PROGRAM     Python code to execute
    --py-source PY_SOURCE
                            Python source file to execute
    --id IDS              id attribute of object to manipulate
    --selected-nodes SELECTED_NODES
                            id:subpath:position of selected nodes, if any
    

解決策

SimpleInkscapeScriptingクラスのadd_arguments(self, pars)に、以下のコマンドライン引数をrest_args定義し5、追加の引数を保持させる。6

そして、effect(self)内で、保持した引数をスクリプトのグローバル変数rest_argsとして定義すれば、Simple Inkscape Scriptingに指定したスクリプトで利用できる。7

class SimpleInkscapeScripting(inkex.EffectExtension):
        # ..略..
        def add_arguments(self, pars):
            # ..略..
            pars.add_argument('rest_args', nargs="*",
                              help='Rest Arguments for passing Python code')
        # ..略..

        def effect(self):
            # ..略..
            sis_globals['rest_args'] = self.options.rest_args
            # ..略..

この修正により、追加の引数は、グローバル変数rest_argsとして利用可能となる。

% DYLD_LIBRARY_PATH=/Applications/Inkscape.app/Contents/Resources/lib PYTHONPATH=/Applications/Inkscape.app/Contents/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages:/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions /Applications/Inkscape.app/Contents/Resources/bin/python3 ~/Library/Application\ Support/org.inkscape.Inkscape/config/inkscape/extensions/SimpInkScr/simpinkscr/simple_inkscape_scripting.py --program "print('rest_args: ', rest_args)"  --output output.svg input.svg 1 2 3
rest_args:  ['1', '2', '3']

ただし、-a--bのように-で始まる引数をスクリプトに渡す場合は、位置引数として解釈させる必要があるため、ダミー引数--を必ず挿入する必要がある。8

% DYLD_LIBRARY_PATH=/Applications/Inkscape.app/Contents/Resources/lib PYTHONPATH=/Applications/Inkscape.app/Contents/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages:/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions /Applications/Inkscape.app/Contents/Resources/bin/python3 ~/Library/Application\ Support/org.inkscape.Inkscape/config/inkscape/extensions/SimpInkScr/simpinkscr/simple_inkscape_scripting.py --program "print('rest_args: ', rest_args)"  --output output.svg input.svg -- -a --b 1 2 3
rest_args:  ['-a', '--b', '1', '2', '3']

課題

ただし、この修正は、1つ問題がある。 標準の位置引数INPUT_FILEを省略した場合、以下のようにrest_argsの1番目の引数がINPUT_FILEと解釈される。

% DYLD_LIBRARY_PATH=/Applications/Inkscape.app/Contents/Resources/lib PYTHONPATH=/Applications/Inkscape.app/Contents/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages:/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions /Applications/Inkscape.app/Contents/Resources/bin/python3 ~/Library/Application\ Support/org.inkscape.Inkscape/config/inkscape/extensions/SimpInkScr/simpinkscr/simple_inkscape_scripting.py --program "print('rest_args: ', rest_args)"  --output output.svg a b c
usage: simple_inkscape_scripting.py [-h] [--output OUTPUT] [--tab TAB]
                                    [--program PROGRAM]
                                    [--py-source PY_SOURCE] [--id IDS]
                                    [--selected-nodes SELECTED_NODES]
                                    [INPUT_FILE] [rest_args ...]
simple_inkscape_scripting.py: error: argument INPUT_FILE: File not found: a

Inkscapeの拡張機能では、INPUT_FILEを省略した場合、標準入力からSVGファイルのデータを取得する。 この標準入力の取得とrest_argsを共存させるために、catコマンドの-引数のように、明示的に標準入力を指定できるように変更する必要がある。9

そのため、InkscapeExtensionで定義されたINPUT_FILE引数を、argparseのトリッキーな方法10で、以下のように変更する。11 12

class SimpleInkscapeScripting(inkex.EffectExtension):
    # ..略..
    def filename_arg(self, name):
        """Existing file to read or option used in script arguments"""
        if name == "-":
            return None  # filename is set to None to read stdin
        return inkex.utils.filename_arg(name)

    def reconfigure_input_file_argument(self, pars):
        target_action = None
        for action in pars._actions:
            if 'input_file' == action.dest:
                target_action = action
                break

        target_action.container._remove_action(target_action)
        pars.add_argument(
            "input_file",
            nargs="?",
            metavar="INPUT_FILE",
            type=self.filename_arg,
            help="Filename of the input file (default is stdin). Filename can be `-` for stdin",
            default=None,
        )

    def add_arguments(self, pars):
        # ..略..
        self.reconfigure_input_file_argument(pars)
        # ..略..

ただし、この方法は、argparse_actions_remove_actionなど非公開のAPIを利用しているため、Pull Requestしても採用されそうにないかも。

参考

作成日: 2023/01/17