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

Расширение для Inkscape. Дополнение.


В предыдущем посте я рассмотрел общий подход к созданию расширения для Inkscape на примере приложения C#, которое выполняет преобразование XSLT. Сейчас мне просто хотелось бы добавить пару примеров.

Расширение XSLT 2.0 с использованием Saxon


Приложение, запускающее преобразование XSLT, рассмотренное в предыдущем посте, может быть полезно в том случае, когда оно добавляет в преобразование ряд полезных для нашей задачи функций. Это могут быть специфические для работы с SVG функции (матричные операции, работа с путями, тригонометрия и т. д.) либо специфичные для решения конкретной задачи. В том виде, в котором это было реализовано там, данный пример мог быть полезен преимущественно для демонстрационных целей и, если уж в качестве языка расширения выбран XSLT, то лучше использовать его более продвинутую версию. В качестве примера покажу как можно выполнить преобразование с помощью XSLT-процессора Saxon. Процессор нужно установить в систему. Он существует в Java и .Net версии, нам для наших целей это без разницы, просто там могут быть нюансы в использовании. У меня установлена .Net-версия. Также там есть различные редакции. Редакция HE (Home Edition) бесплатна и ее нам тоже будет вполне достаточно.
Правила, по которым Saxon запускается из командной строки, несколько отличаются от тех, на которые рассчитан код на Python, написанный нами ранее. Поэтому в модуль apprunner.py мы дополним функцией, с помощью которой будем запускать преобразования на Saxon. Теперь этот модуль будет выглядеть так
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)

def saxon_xslt(xsltfile):
    saxonpath =
"C:\\Program Files\\Saxonica\\SaxonHE9.9N\\bin\\Transform.exe"
   
svgpath = sys.argv[-1]
    tmppath = svgpath +
".xml"
   
tmp = open(tmppath, "w+")
    tmp.write(open(svgpath,
"r").read())
    tmp.close()
    ids =
"ids=" + ",".join(map(lambda s: s[5:], filter(lambda s: s.startswith("--id="), sys.argv)))
    params = map(
lambda x: x[2:], filter(lambda s: "=" in s and not s.startswith("--id="), sys.argv[1:-1]))
    arglist = [saxonpath,
"-s:" + tmppath , "-xsl:" + xsltfile, ids] + list(params)
    subprocess.call(arglist, stdout = sys.stdout, stderr = sys.stderr, shell = True)
    os.remove(tmppath)


Здесь в функции saxon_xslt есть переменная saxonpath, ей я присвоил значение полного пути к Transform.exe установленного процессора. Если реальный путь другой (другая версия, редакция, выбрана ява, а не дотнет, или вообще выбран другой каталог установки), то реальный путь к этому файлу нужно указать именно здесь.
Создаем test-saxon.inx файл. Расширение будет  выполнять ту же функцию, что и прежде, поэтому мы просто переделаем немного файл, созданный ранее.
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
  <_name>Testing saxon transform</_name>
  <id>org.inkscape.effect.saxontest</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">testsaxon.py</command>
</script>
</inkscape-extension>

Файл testsaxon.py 
# -*- coding: utf-8 -*-
from apprunner import saxon_xslt
saxon_xslt( "C:\\Users\\username\\AppData\\Roaming\\inkscape\\extensions\\xslt\\saxon.xslt")

Здесь мы просто вызываем функцию и передаем ей полный путь к XSLT-файлу.
Ну и собственно само преобразование.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:fn="http://www.w3.org/2005/xpath-functions"
                xmlns:f="urn:this-document-functions"
> 
  <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:function name="f:containsId">
    <xsl:param name="id"/>
    <xsl:param name="ids" />
    <xsl:value-of select="fn:index-of(fn:tokenize($ids, ','), $id) != 0"/>
  </xsl:function>

  <xsl:template match="*[@id]">
    <xsl:choose>
      <xsl:when test="f:containsId(@id, $ids)">
        <xsl:copy>
          <xsl:for-each select="@*">
              <xsl:choose>
                <xsl:when test="local-name() = 'style'">
                  <xsl:variable name="filled" select="fn:replace(., 'fill\s*:[^;]+;', concat('fill:', $fill, ';'))"/>
                  <xsl:attribute name="style" select="fn:replace($filled, 'stroke\s*:[^;]+;', concat('stroke:', $stroke, ';'))"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:copy-of select="."/>
                </xsl:otherwise>
              </xsl:choose>
          </xsl:for-each>
          <xsl:apply-templates select="node()"/>
        </xsl:copy>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy>
          <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>


Здесь благодаря возможностям XSLT 2 мы функцию containsId создали прямо на месте, ну и стили установили также средствами поддерживаемых функций с использованием регулярных выражений.

Расширение на Node.js


XSLT – язык не очень популярный, достаточно многословный и имеет ряд ограничений, из-за которых его не всегда удобно использовать. В то же время SVG, являющийся веб-форматом, наверное многие могут захотеть обрабатывать с помощью JavaScript. В этом смысле  повествование было бы не полным, если бы я не привел пример с использованием Node.js.
Буду краток.
test-node.inx
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
  <_name>Testing node extension</_name>
  <id>org.inkscape.effect.nodetest</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">testnode.py</command>
</script>
</inkscape-extension>

testnode.py
 
# -*- coding: utf-8 -*-
from apprunner import run_ext
run_ext("node", "C:\\Users\\laet\\AppData\\Roaming\\inkscape\\extensions\\node\\testnode.js")

Само собой, подразумевается, что Node.js установлен в системе, а путь к testnode.js указан правильно. Папку node я создал в папке расширений Inkscape. В эту папку надо установить пакет
npm install cheerio
testnode.js
const cheerio = require("cheerio");
const fs = require("fs")
const process = require("process")
fs.readFile(process.argv[process.argv.length - 1], function (errdata)
{
    let doc = cheerio.load(data,{xmlMode:true});
    let ids = process.argv.filter(a => a.startsWith("--id="))
        .map(id => "#" + id.substring(5))
        .join(",");
    let fill = process.argv.find(a => a.startsWith("--fill=")).substring(7);
    let stroke = process.argv.find(a => a.startsWith("--stroke=")).substring(9);
    doc(ids).css("fill"fill).css("stroke"stroke);
    process.stdout.write(doc.html({ xmlMode: true }));    
})
Все три расширения делают одну и ту же работу: всем выделенным объектам добавляют заливку и обводку. Тестировались только простые объекты, то есть это были пути, прямоугольники и т.д., но не группы. Все это было сделано в демонстрационных целях, так что, если начать экспериментировать, то можно получить ошибки. Но все расширения работают, если все правильно указать, а также не следует забывать о том, что при установке нового расширения, доступно оно станет после перезапуска приложения.