воскресенье, 31 мая 2020 г.

Пишем расширение для Inkscape на любом языке программирования


Что такое Inkscape, зачем нам его расширять и что значит «на любом языке»?


Начнем с того, что Inkscape – это редактор векторной графики с открытым исходным кодом. Я сам не занимаюсь графикой и мои потребности в этом направлении минимальны, просто иногда бывает надо что-то нарисовать, да еще чтобы этим рисунком можно было управлять. Естественно последнее обстоятельство (которое «управлять») заставляет делать выбор именно в пользу векторной графики, ну, а Inkscape, помимо того, что это редактор векторный и бесплатный, еще и ориентирован на SVG. То есть SVG является его основным форматом. Данный формат поддерживается современными браузерами, графика в этом формате может быть анимированной, интерактивной, управляемой скриптами и при этом имеет огромное количество возможностей. Редактор же при этом является полноценным профессиональным инструментом, позволяющим создавать векторную графику, ну наверно любой сложности.
Тем не менее, несмотря на мощный инструментарий программы, зачастую возникают задачи, требующие сделать что-то очень специфичное. Например, это может быть создание каких-то фигур, требующих математических расчетов, какого-то точного взаимного позиционирования элементов и тому подобных вещей. Естественно, для таких задач хотелось бы иметь возможность добавлять в программу собственный функционал. И тут надо сказать, что программа имеет достаточно мощную систему расширений, которая подразумевает возможность использования нескольких языков, но как быть, когда среди этих «нескольких» нет того, который нужен? Хотелось бы, наверно, иметь возможность писать расширения на том языке, который знаешь лучше или который подходит больше для решения конкретной задачи. Список поддерживаемых языков обычно бывает ограниченным, и вот вопросом о том, как в данном конкретном случае расширить этот список мы здесь и будем заниматься.

Система расширений Inkscape


Система расширений Inkscape описана здесь и далее по ссылкам. Вкратце все выглядит примерно следующим образом.
Существуют внутренние и внешние расширения.
Внутренние встраиваются в программу непосредственно и могут манипулировать объектами самой программы. К сожалению, пишутся они только на языке C++ и поэтому в контексте нашей темы они не интересны, хотя подозреваю, возможностей они имеют больше.
Внешние расширения могут писаться на языках: Python, Ruby, Perl, Bash и XSLT. Они бывают четырех типов: input, output, print и effect. Первые два типа – это импорт и экспорт соответственно, пока они нам не нужны, третий тип позволяет выводить данные на принтеры и прочие внешние устройства (тоже можно сказать разновидность экспорта). А вот effect-расширения – это как раз то, что нам и нужно. Этот тип расширений позволяет выполнять манипуляции над объектами прямо во время работы в программе, то есть создание, изменение объектов и тому подобные вещи. Вот этим мы сейчас и будем заниматься.
Здесь следует сказать, что среди перечисленных языков в нашем случае два отпадают сразу, а именно Bash и XSLT. Первый из-за того, что работает в Linux, а нам надо что-то более кроссплатформенное, а второй – может использоваться в input и output расширениях, а для effect расширений не поддерживается (это мы исправим). Из оставшихся трех языков лично я имел дело только с Python, но мое знакомство с ним не очень близкое. То есть написать что-то небольшое и несложное на нем я могу, а вот что-то посложнее и побольше – уже нежелательно. Но, как мы увидим дальше, этого мне вполне хватит для того, чтобы реализовать задумку.

Как работают внешние расширения


Именно механизм работы внешних расширений и натолкнул меня на мысль, так сказать, расширить этот самый механизм.
Общая схема создания расширения следующая:
  1. Открываем меню Правка > Параметры
  2. В открывшемся диалоге в ветке Система находим строку Пользовательские расширения, там указан каталог с пользовательскими расширениями и кнопка, с помощью которого его можно открыть.
  3. Собственные расширения следует размещать в этом каталоге.
  4. Расширения по сути состоит из двух файлов: файл .inx (Inkscape Extension Definition), являющийся XML-файлом и описывающий расширение; и собственно файл скрипта, на который в .inx файле будет ссылка.
Файл описания расширения .inx содержит информацию о расширении типа: название, описание, тип, файл скрипта, интерпретатор и т. д. Кроме того в нем содержится описание пользовательского интерфейса расширения. Когда расширение запускается, программа сначала выводит диалог, в котором можно установить параметры выполнения расширения и только после этого оно будет запущено. Вот описание этого диалога, набор параметров и их типы, все это можно описать в этом самом файле. Полная структура файла описана здесь, кроме того о параметрах подробнее здесь.
Теперь собственно о том, что происходит, когда параметры назначены и все эти данные отправляются на выполнение. Я уже упоминал выше, что для Inkscape SVG является «родным» форматом. Так вот перед запуском скрипта программа сохраняет текущее состояние документа в расширенном SVG формате во временный файл, а запуская скрипт она передает ему через аргументы командной строки следующие данные: идентификаторы выделенных объектов; параметры, заданные пользователем в диалоге, о котором выше писал; и последним аргументом – ни что иное как адрес того самого временного файла, в который программа сохранила текущий документ полностью. Задача расширения сводится к тому, чтобы прочитать этот самый документ, внести изменения в соответствии с полученными аргументами командной строки и вернуть программе документ со всеми изменениями через стандартный output (stdout) приложения. Это все.
И вот собственно из этого и возникла идея: «А что, если скрипт на Python будет просто получать данные, переадресовывать их другому приложению и пересылать полученный от него ответ программе?». Вот это ровно то, чем мы сейчас и займемся.

Создаем промежуточный скрипт на Python


В принципе задача, которую будет выполнять этот скрипт решается в несколько строк, так что можно было бы эти строки писать всякий раз при создании расширения, но все-таки упростим немного задачу и создадим отдельный модуль, чтобы в дальнейшем можно было просто импортировать этот модуль, вызвать функцию, передать ей адрес реального приложения и дальше работать уже с этим приложением.
import sys
import subprocess
import os

def run_ext(app, script = ""):
    tmppath = sys.argv[-1] + ".tmp"
    tmp = open(tmppath, "w+")
    tmp.write(open(sys.argv[-1], "r").read())
    tmp.close()
    subprocess.call([app, script] + sys.argv[1:-1] + [tmppath], stdout= sys.stdout, stderr=sys.stderr, shell=True)
    os.remove(tmppath)

Создаем в папке расширений файл apprunner.py с этим кодом и на этот модуль будем всегда ссылаться.
Далее процесс создания расширения будет сводиться к тому, чтобы создать описание расширения .inx, как это описано в документации, указать в нем в качестве скрипта файл с примерно таким содержимым
# -*- coding: utf-8 -*-

from apprunner import run_ext

run_ext("C:\\path\\to\\app.exe", "C:\\path\\to\\script.xxx")

Второй аргумент необязателен. Если первый аргумент – это скриптовый движок, или программа требует какого-то файла данных, то сам скрипт или файл данных можно указать вторым аргументом. Естественно, это будет работать в том случае, если движок подразумевает синтаксис командной строки, в котором адрес скрипта должен идти сразу после самой команды. Если там другая логика командной строки, то можно в модуль apprunner добавить ее поддержку.
Теперь, когда все предварительные приготовления закончены, можно перейти к созданию расширения для теста.

XSLT движок на C#


Итак, создадим собственное расширение. Выше я уже упоминал, что effect расширения не поддерживают XSLT, а между тем это достаточно удобный язык для преобразований XML документов, а SVG, я напомню, является XML. Таким образом я подумал, что можно написать на C# программу, которая будет выполнять XSLT преобразование и в результате мы и протестируем, что у нас получилось и добавим поддержку XSLT, тем более, что сделать это на C# совсем несложно.
Для начала создадим файл описания расширения. Чтобы долго не мудрить, я взял за основу код из документации, и внес в него несколько изменений. Так что id расширения не стал менять, но вполне понятно, что идентификатор должен быть уникальным для каждого расширения. Вот что у меня получилось
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
  <_name>Testing features</_name>
  <id>org.inkscape.effect.radiobuttontest</id>

  <param name="fill" type="string" _gui-text="Fill color"/>
  <param name="stroke" type="string" _gui-text="Stroke color"/>
  <effect>
    <object-type>all</object-type>
    <effects-menu>
      <submenu _name="Developer Examples"/>
    </effects-menu>
  </effect>
  <script>
<command reldir="extensions" interpreter="python">test.py</command>
</script>
</inkscape-extension>

Расширение, которое мы будем писать, будет выполнять очень простую работу, оно будет находить выделенные объекты и устанавливать для них обводку и заливку. Диалоговое окно, описанное в этом файле, содержит два параметра, как раз fill и stroke. Я указал тип параметра string, но можно было указать color, тогда там появился бы Color Picker, но мы просто будем писать код цвета, для наших целей этого достаточно. Тип объекта оставил all, хотя можно ограничить, например, path или что-то такое. submenu создает подменю в меню Расширения, если такого нет и добавляет наше расширение в это подменю. Файл test.py должен находиться в той же папке с расширениями и код его будет следующим
# -*- coding: utf-8 -*-

from apprunner import run_ext

run_ext("C:\\Users\\username\\AppData\\Roaming\\inkscape\\extensions\\XsltRunner.exe", "C:\\Users\\username\\AppData\\Roaming\\inkscape\\extensions\\transform.xslt")
Здесь первый аргумент, как уже говорилось, будет адресом нашего приложения на C#, а второй – адресом XSLT преобразования, которое и будет выполнять основную работу.

Приложение на C#


Создаем консольное приложение. Задача приложения состоит в том, чтобы прочитать аргументы командной строки, найти файл преобразования, файл SVG вычислить аргументы, которые нужно передать в преобразование, выполнить преобразование и отправить результат в выходной поток приложения. Кроме того, было бы совсем не лишним добавить в XSLT несколько дополнительных функций, которые позволили бы решать некоторые специфические задачи. Например: мы передадим преобразованию в качестве параметров идентификаторы выделенных элементов. Это будет один параметр, нам надо будет идентификаторы как-то разделить и неплохо было бы иметь функцию, которая сможет определить, присутствует ли конкретный идентификатор в этом списке. Другой пример – работа с CSS. В XSLT нет таких функций, так что тоже было бы неплохо что-то такое иметь. Для нашей задачи этого достаточно, но вообще, было бы неплохо иметь функции для работы с матрицами, путями, тригонометрией и т.п. Все это тоже можно и желательно добавить.
Собственно, класс, реализующий описанный выше набор выглядит так
    class Helper
    {
        private Dictionary<string, string> GetCssProps(string att)
        {
            return att.Split(';', StringSplitOptions.RemoveEmptyEntries).ToDictionary(s => s.Split(':')[0].Trim(), s => s.Split(':')[1].Trim());
        }
        public string CssSet(string att, string prop, string value)
        {
            var props = GetCssProps(att);
            props[prop] = value;
            return string.Join("; ", props.Keys.Select(k => k + ": " + props[k]));
        }

        public string CssGet(string att, string prop)
        {
            return GetCssProps(att)[prop];
        }

        public bool containsId(string id, string ids)
        {
            return ids.Split(',').Contains(id);
        }
    }

Здесь метод containsId получает id элемента и строку ids, содержащую id выделенных элементов, возвращает значение, указывающее, есть ли элемент в списке.
CssGet получает значение атрибута style и имя CSS свойства и возвращает значение.
CssSet получает текущее значение атрибута style, имя свойства, которое нужно изменить и новое значение, а возвращает строку нового значения всего атрибута style.
Все это мы будем использовать в преобразовании.

Теперь в Program.cs создаем код преобразования
        static void Transform(string source, string xslt, Dictionary<string, string> args)
        {

            var trans = new XslCompiledTransform();
            var settings = new XsltSettings(true, true);
            trans.Load(xslt, settings, new XmlUrlResolver());
            var svg = new XmlDocument();
            svg.Load(source);

            var arglist = new XsltArgumentList();
            foreach (var key in args.Keys)
            {
                arglist.AddParam(key, "", args[key]);
            }
            arglist.AddExtensionObject("urn:inkscape-svg-transform:helper", new Helper());
            trans.Transform(svg, arglist, Console.OpenStandardOutput());


        }
Метод получает строку SVG первым аргументом, строку XSLT вторым аргументом и словарь с параметрами, которые нужно передать преобразованию. Помимо аргументов в преобразование передаем экземпляр класса Helper, для того, чтобы его методы были доступны в преобразовании и назначаем для него пространство имен. Результат преобразования отправляем в стандартный аутпут.
Ну и метод Main

        static void Main(string[] args)
        {
            File.WriteAllText(@"C:\Users\diadiavova\Desktop\qwerty.txt", string.Join(" ", args));

            var a = args.Skip(1);
            var ids = string.Join(",", a.TakeWhile(s => s.StartsWith("--id=")).Select(s => s.Substring(5)));
            var pars = a.SkipWhile(s => s.StartsWith("--id="));
            var dict = pars.Take(pars.Count() - 1).ToDictionary(s => s.Split('=')[0].Substring(2), s => s.Split('=')[1]);
            dict.Add("ids", ids);
            var path = args.Last();
            Transform(path, args[0], dict);
        }

Здесь первой строкой аргументы записываются в файл на рабочем столе. Это делалось для того, чтобы иметь точное представление о том, что получает программа и как это следует обрабатывать и в окончательном коде эта строка не нужна. Дальнейший код просто разбирает эти аргументы и передает нужные данные методу Transform. В принципе, что касается приложения на C#, то это все. Основная логика расширения будет реализована в XSLT, а на C# мы по сути написали движок.

Об XSLT


К XSLT есть пара требований. Поскольку данный формат выполняет преобразование всего документа, нам нужно позаботиться о том, чтобы документ в основном был скопирован в точности, а изменениям подверглись только те части, которые должны быть изменены. В XSLT должны быть объявлены все параметры, которые будут ему переданы. Кроме того, если мы хотим использовать функционал класса Helper, то надо в документе объявить пространство имен, с которым мы его связали. Также не следует забывать и о том, что Inkscape использует расширенный SVG и расширения связаны с определенными пространствами имен, так что, если мы хотим работать с этими расширениями, то их пространства имен тоже надо будет объявить в документе.
Собственно, вот что у меня получилось
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:cc="http://creativecommons.org/ns#"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    xmlns:hlp="urn:inkscape-svg-transform:helper"
                >
  <xsl:output method="xml" indent="yes"/>

  <xsl:param name="ids"/>
  <xsl:param name="fill"/>
  <xsl:param name="stroke"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="*[@id]">
    <xsl:choose>
      <xsl:when test="hlp:containsId(@id, $ids)">
        <xsl:variable name="att" select="@style"/>
        <xsl:copy>
          <xsl:for-each select="@*[name()!='style']">
            <xsl:copy-of select="."/>
          </xsl:for-each>
          <xsl:attribute name="style">
            <xsl:variable name="filled" select="hlp:CssSet($att, 'fill', $fill)"/>
            <xsl:value-of select="hlp:CssSet($filled, 'stroke', $stroke)"/>
          </xsl:attribute>
        </xsl:copy>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy>
          <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>

После установки всех файлов на свои места и указания этих мест в коде, расширение будет доступно после перезапуска Inkscape, если приложение запущено.

Несколько слов об Inkscape SVG


Уже упоминал выше, что в Inkscape довольно специфический SVG. То есть SVG там вроде как нормальный, но при этом он имеет несколько дополнительных пространств имен, добавляющих нестандартные атрибуты и элементы в документ. В общем и целом, на отображение документа в браузере это никак не влияет, поскольку движки SVG все лишнее и непонятное для них просто игнорируют (в пределах разумного, разумеется), однако для нашей задачи некоторое понимание этих «приращений» может оказаться не только полезным, но и необходимым.
Все, что я нашел по поводу этих расширений формата в документации – это вот эта страница. Там есть вначале ссылка на страницу, где, по всей видимости, раньше была еще какая-то информация, но сейчас такой страницы не существует, так что и говорить не о чем. Но и из этой страницы (той, которая пока еще есть) мы узнаем важные вещи. Например то, что если данные в этих дополнительных атрибутах будут расходиться с данными основных атрибутов SVG, то Inkscape отдаст предпочтение своим атрибутам, а атрибуты SVG будут приведены в соответствие с ними.
Что такого важного содержится в этих атрибутах? Возьмем для примера нестандартные фигуры, то есть те, которых нет в SVG, но которые поддерживаются Inkscape. Например, многоугольники и звезды. Понятно, что эти объекты будут порождать элементы path, в которых будут задаваться просто координаты точек, по которым будут строиться фигуры. Но это в конечном итоге, а в процессе редактирования программе нужно как-то с этими объектами работать, то есть программа должна воспринимать их не как просто пути, а как звезду, у которой есть координаты центра, большой и малый радиусы, количество лучей и тому подобные вещи. Просто для того, чтобы этим можно было как-то управлять. Когда в интерфейсе программы меняются какие-то из этих характеристик, параметры пути пересчитываются. Таким образом, если мы в своем расширении хотим звезде добавить лучей, то ну нужно вычислять где будут находиться точки в новой звезде, а вместо этого надо изменить значение соответствующего атрибута. Поэтому тут возникает вопрос о том, где брать информацию о назначении атрибутов. Назначение некоторых можно понять по названию, другие надо изучать. В программе есть редактор XML, с помощью которого можно кое-что понять. Если выделен тот или иной объект, можно подвигать различные манипуляторы и посмотреть, как при этом меняются атрибуты. Кое что все равно остается непонятным, например у тех же звезд есть пара манипуляторов, которые соответствуют атрибутам  sodipodi:arg1 и sodipodi:arg2, что они делают визуально можно наблюдать, но смысл их значений лично я не уловил, так что воспользоваться этим будет сложно. Но в общем и целом разобраться в основных моментах, как мне кажется, большого труда не составит. 


 

Комментариев нет :

Отправить комментарий