Exporting SVGs from Perspective

I have embedded an SVG in a Perspective view and edited its elements. Is there a way to export this back to a .svg file?

1 Like

Yep, but you'll have to do it yourself for now in script by generating the xml.. I can post my script when I'm back at my laptop, but it doesn't export more complex styles

3 Likes

If you still have the script, I'd also appreciate seeing that.

Sorry, time got away from me.
Script below.
obj is a copy of the "SVG" in the View, converted into a Py Object with json.loads / system.util.jsonDecode
The ATTR_LOOKUP is a conversion table (dictionary) of Ignition styles to its corresponding SVG style.

EDIT: Tested and working in the script console, although you'll need the clipboard functions. I've posted them elsewhere on the forum

import xml.etree.cElementTree as et
import json
import copy

def extract_svg_from_view(obj):
	#json_str = clipboard.getClipboard()
	#'[{         "type": "ia.shapes.svg",         "version": 0,         "props": {             "viewBox": "0 0 185 527.99",             "elements": [{                     "type": "path",                     "name": "rect933",                     "d": "m92.5 5c-43.75 0-87.5 30.69-87.5 92.07v425.92h175v-425.92c0-61.38-43.75-92.07-87.5-92.07z",                     "fill": {                         "paint": "#fff"                     },                     "stroke": {                         "paint": "#000",                         "linecap": "round",                         "width": "10"                     }                 }, {                     "type": "path",                     "name": "rect933-3",                     "d": "m92.5 5c-43.75 0-87.5 30.69-87.5 92.07v425.92h175v-425.92c0-61.38-43.75-92.07-87.5-92.07z",                     "fill": {                         "paint": "#fff"                     },                     "stroke": {                         "paint": "#000",                         "linecap": "round",                         "width": "10"                     }                 }, {                     "type": "group",                     "name": "g7299",                     "fill": {                         "paint": "#ff2a2a"                     },                     "stroke": {                         "paint": "#000",                         "linecap": "round",                         "width": "10"                     },                     "elements": [{                             "type": "circle",                             "name": "circle7295",                             "cx": "69.758",                             "cy": "157.43",                             "r": "64.685"                         }, {                             "type": "circle",                             "name": "path7213",                             "cx": "92.5",                             "cy": "263.99",                             "r": "64.685"                         }                     ]                 }             ]         },         "meta": {             "name": "TEST_Export"         },         "position": {             "basis": "20px"         },         "custom": {}     } ] '
	#svg_obj = json.loads(json_str)
	svg_obj = obj[0] # assume that the obj passed in is a copied "SVG" from a Perspective View which adds the SVG definition into a single item array. We want the item
	viewbox = svg_obj['props']['viewBox']
	elements = svg_obj['props']['elements']

	ATTR_LOOKUP = {}
	ATTR_LOOKUP['name'] = 'id'
	ATTR_LOOKUP['fill.paint'] = 'fill'
	ATTR_LOOKUP['fill.opacity'] = 'opacity'
	ATTR_LOOKUP['stroke.paint'] = 'stroke'
	ATTR_LOOKUP['style.stroke'] = 'stroke'
	ATTR_LOOKUP['stroke.linecap'] = 'stroke-linecap'
	ATTR_LOOKUP['stroke.width'] = 'stroke-width'
	ATTR_LOOKUP['style.strokeWidth'] = 'stroke-width'
	ATTR_LOOKUP['style.vector-effect'] = 'vector-effect'


	doc = et.Element('svg', viewBox=viewbox, version='1.1', xmlns='http://www.w3.org/2000/svg')

	not_included_SVG_elements = []
	def addElements(xmlobj, elements):
		for element in elements:
			if 'type' in element:
				attrs_dict = {}

				# collect the attributes for the current SVG element are store them in a dictionary
				for attr_name in element:
					# exclude any nested elements as they need to be added later. These are not added as attributes
					if attr_name != 'elements':
						attr = element[attr_name]

						# if the attribute is a dict in Ignition, then the dict will contain the actual attribute names
						if isinstance(attr, dict):
							for prop_name in attr:
								svg_prop_name = ATTR_LOOKUP.get('{}.{}'.format(attr_name, prop_name), 'UNSET')
								if svg_prop_name == 'UNSET':
									not_included_SVG_elements.append('{}.{}'.format(attr_name, prop_name))

								svg_prop_val = attr[prop_name]

								attrs_dict[svg_prop_name] = svg_prop_val
						else:
							attrs_dict[ATTR_LOOKUP.get(attr_name, attr_name)] = attr

				element_name = attrs_dict['type']
				if element_name == 'group':
					element_name = 'g'

				xmlobj_attr = et.SubElement(xmlobj, element_name, **attrs_dict)

				if 'elements' in element:
					attr_elements = copy.deepcopy(element['elements'])

					addElements(xmlobj_attr, attr_elements)

	addElements(doc, elements)
	#print ET.tostring(doc, encoding='utf8', method='xml')
	
	if not_included_SVG_elements != []:
		print "Copied to clipboard, but some SVG attributes are missing mapping. These have not been extracted {}".format(not_included_SVG_elements)
	
	return et.tostring(doc)	

json = shared.util.clipboard.readText()
obj = system.util.jsonDecode(json)
svg = extract_svg_from_view(obj)
shared.util.clipboard.writeText(svg)
3 Likes

Thank you very much!

1 Like

Hello @nminchin. First I would like to thank you for effort.
I know it's been a while, but I just tried your script and got this error below:

Copied to clipboard, but some SVG attributes are missing mapping. These have not been extracted ['stroke.miterlimit', 'stroke.dasharray', 'stroke.opacity', 'stroke.miterlimit', 'stroke.dasharray', 'stroke.opacity', 'stroke.miterlimit', 'stroke.dasharray', 'stroke.opacity', 'stroke.miterlimit', 'stroke.dasharray', 'stroke.opacity']
Traceback (most recent call last):
  File "<input>", line 3, in <module>
  File "<module:SVGtoView>", line 69, in extract_svg_from_view
  File "C:\Users\ali.elboraay\.ignition\cache\gwlocalhost_8088\C1\pylib\xml\etree\ElementTree.py", line 1128, in tostring
    ElementTree(element).write(file, encoding, method=method)
  File "C:\Users\ali.elboraay\.ignition\cache\gwlocalhost_8088\C1\pylib\xml\etree\ElementTree.py", line 821, in write
    serialize(write, self._root, encoding, qnames, namespaces)
  File "C:\Users\ali.elboraay\.ignition\cache\gwlocalhost_8088\C1\pylib\xml\etree\ElementTree.py", line 941, in _serialize_xml
    _serialize_xml(write, e, encoding, qnames, None)
  File "C:\Users\ali.elboraay\.ignition\cache\gwlocalhost_8088\C1\pylib\xml\etree\ElementTree.py", line 934, in _serialize_xml
    v = _escape_attrib(v, encoding)
  File "C:\Users\ali.elboraay\.ignition\cache\gwlocalhost_8088\C1\pylib\xml\etree\ElementTree.py", line 1094, in _escape_attrib
    _raise_serialization_error(text)
  File "C:\Users\ali.elboraay\.ignition\cache\gwlocalhost_8088\C1\pylib\xml\etree\ElementTree.py", line 1053, in _raise_serialization_error
    raise TypeError(
TypeError: cannot serialize 1 (type int)

and the svg I copied is:

[
  {
    "type": "ia.shapes.svg",
    "version": 0,
    "props": {
      "viewBox": "0 0 15.87501 30.956209",
      "elements": [
        {
          "d": "M 1.6955795,-5.5e-5 C 0.7562625,-5.5e-5 0,0.756209 0,1.695527 v 16.592928 l 10.095088,5.125915 5.627073,-4.269909 V 1.695527 c 0,-0.939318 -0.756262,-1.695582 -1.695582,-1.695582 z M 0,18.768089 v 10.471828 c 0,0.939312 0.7562625,1.69557 1.6955795,1.69557 H 14.026579 c 0.93932,0 1.695582,-0.756258 1.695582,-1.69557 V 19.68647 l -5.563431,4.220177 z",
          "fill": {
            "opacity": 1
          },
          "name": "driver_body",
          "stroke": {
            "dasharray": "none",
            "linecap": "round",
            "miterlimit": "4",
            "opacity": "1",
            "paint": "",
            "width": 0
          },
          "style": {},
          "type": "path"
        },
        {
          "elements": [
            {
              "d": "m 5.2853724,1.536714 h 7.2516916 c 0.842235,0 1.52028,0.598107 1.52028,1.341048 v 10.325979 c 0,0.786693 -4.31169,2.462549 -5.16104,2.462549 -0.842235,0 -5.1312101,-1.719608 -5.1312101,-2.462549 V 2.877762 c 0,-0.742941 0.6780437,-1.341048 1.5202785,-1.341048 z",
              "fill": {
                "opacity": "1",
                "paint": "#f2f2f2"
              },
              "name": "panel",
              "stroke": {
                "dasharray": "none",
                "linecap": "round",
                "miterlimit": "4",
                "opacity": "1",
                "paint": "none",
                "width": "0.529"
              },
              "style": {},
              "type": "path"
            },
            {
              "d": "m 31.664062,37.642578 v 3.533203 h 4.00586 v -3.533203 z m -6.355468,4.953125 v 3.53125 H 29.3125 v -3.53125 z m 12.814453,0 v 3.53125 h 4.003906 v -3.53125 z m -6.458985,4.9375 v 3.533203 h 4.00586 v -3.533203 z",
              "fill": {
                "opacity": "1",
                "paint": "#b3b3b3"
              },
              "name": "rect881",
              "stroke": {
                "dasharray": "none",
                "linecap": "round",
                "miterlimit": "4",
                "opacity": "1",
                "paint": "none",
                "width": "2"
              },
              "style": {},
              "transform": "matrix(0.2645835,0,0,0.2645835,0,-3.025e-5)",
              "type": "path"
            },
            {
              "d": "m 5.4915642,2.7496701 h 6.9566448 c 0.489159,0 0.882959,0.4246184 0.882959,0.9520602 v 4.4054356 c 0,0.5274417 -0.3938,0.9520601 -0.882959,0.9520601 H 5.4915642 C 5.0024053,9.059226 4.608606,8.6346076 4.608606,8.1071659 V 3.7017303 c 0,-0.5274418 0.3937993,-0.9520602 0.8829582,-0.9520602 z",
              "fill": {
                "opacity": "1",
                "paint": "#333333"
              },
              "name": "rect889",
              "stroke": {
                "dasharray": "none",
                "linecap": "round",
                "miterlimit": "4",
                "opacity": "1",
                "paint": "none",
                "width": "0.529167"
              },
              "style": {},
              "type": "path"
            }
          ],
          "name": "g860",
          "transform": "translate(0,0.21612603)",
          "type": "group"
        }
      ],
      "style": {
        "cursor": "pointer"
      }
    },
    "meta": {
      "name": "VFD"
    },
    "position": {
      "x": -1,
      "height": 95,
      "width": 50
    },
    "custom": {},
    "propConfig": {
      "props.elements[0].fill.paint": {
        "binding": {
          "config": {
            "fallbackDelay": 2.5,
            "mode": "indirect",
            "references": {
              "tagPath": "{view.params.tagPath}"
            },
            "tagPath": "{tagPath}/Run"
          },
          "overlayOptOut": true,
          "transforms": [
            {
              "expression": "coalesce(if({value}, \"var(--color-en)\", \"var(--color-de)\") , \"var(--color-de)\")",
              "type": "expression"
            }
          ],
          "type": "tag"
        }
      }
    },
    "events": {
      "dom": {
        "onClick": {
          "config": {
            "script": "\tPID.OpenPopup(self, event, \"Lib/SLD/MD/VFD/popup\", 350, 650, self.view.params.tagPath, \"righDock\", \"Lib/SLD/MD/VFD/popup\")"
          },
          "scope": "G",
          "type": "script"
        }
      }
    }
  }
]

TypeError: cannot serialize 1 (type int)

I got the error. Somehow Ignition css property become int or float instead of string.
e.g.

"opacity": 1

so either I delete the value and leave the property empty, or I copy it from another normal property

"width": 0

here I just added "px" after "0"
and so on in each property that throw error.

Merged code from nminchin. To use this script, copy this script into the script console, copy the drawing object in the designer, update the destination file path and then execute in the script console.

import xml.etree.cElementTree as et
import json
import copy
from java.awt.datatransfer import StringSelection
from java.awt.datatransfer import Clipboard
from java.awt import Toolkit 

def setup():
	global toolkit, clipboard
	
	toolkit = Toolkit.getDefaultToolkit()
	clipboard = toolkit.getSystemClipboard()	

def writeText(text):
	setup()
	clipboard.setContents(StringSelection(text), None)
	
def readText():
	setup()
	from java.awt.datatransfer import DataFlavor
	contents = clipboard.getContents(None)
	return contents.getTransferData(DataFlavor.stringFlavor)


def extract_svg_from_view(obj):
	#json_str = clipboard.getClipboard()
	#'[{         "type": "ia.shapes.svg",         "version": 0,         "props": {             "viewBox": "0 0 185 527.99",             "elements": [{                     "type": "path",                     "name": "rect933",                     "d": "m92.5 5c-43.75 0-87.5 30.69-87.5 92.07v425.92h175v-425.92c0-61.38-43.75-92.07-87.5-92.07z",                     "fill": {                         "paint": "#fff"                     },                     "stroke": {                         "paint": "#000",                         "linecap": "round",                         "width": "10"                     }                 }, {                     "type": "path",                     "name": "rect933-3",                     "d": "m92.5 5c-43.75 0-87.5 30.69-87.5 92.07v425.92h175v-425.92c0-61.38-43.75-92.07-87.5-92.07z",                     "fill": {                         "paint": "#fff"                     },                     "stroke": {                         "paint": "#000",                         "linecap": "round",                         "width": "10"                     }                 }, {                     "type": "group",                     "name": "g7299",                     "fill": {                         "paint": "#ff2a2a"                     },                     "stroke": {                         "paint": "#000",                         "linecap": "round",                         "width": "10"                     },                     "elements": [{                             "type": "circle",                             "name": "circle7295",                             "cx": "69.758",                             "cy": "157.43",                             "r": "64.685"                         }, {                             "type": "circle",                             "name": "path7213",                             "cx": "92.5",                             "cy": "263.99",                             "r": "64.685"                         }                     ]                 }             ]         },         "meta": {             "name": "TEST_Export"         },         "position": {             "basis": "20px"         },         "custom": {}     } ] '
	#svg_obj = json.loads(json_str)
	svg_obj = obj[0] # assume that the obj passed in is a copied "SVG" from a Perspective View which adds the SVG definition into a single item array. We want the item
	viewbox = svg_obj['props']['viewBox']
	elements = svg_obj['props']['elements']
	name = svg_obj['meta']['name']
	ATTR_LOOKUP = {}
	ATTR_LOOKUP['name'] = 'id'
	ATTR_LOOKUP['fill.paint'] = 'fill'
	ATTR_LOOKUP['fill.opacity'] = 'opacity'
	ATTR_LOOKUP['stroke.paint'] = 'stroke'
	ATTR_LOOKUP['style.stroke'] = 'stroke'
	ATTR_LOOKUP['stroke.linecap'] = 'stroke-linecap'
	ATTR_LOOKUP['stroke.width'] = 'stroke-width'
	ATTR_LOOKUP['style.strokeWidth'] = 'stroke-width'
	ATTR_LOOKUP['style.vector-effect'] = 'vector-effect'


	doc = et.Element('svg', viewBox=viewbox, version='1.1', xmlns='http://www.w3.org/2000/svg')

	not_included_SVG_elements = []
	def addElements(xmlobj, elements):
		for element in elements:
			if 'type' in element:
				attrs_dict = {}

				# collect the attributes for the current SVG element are store them in a dictionary
				for attr_name in element:
					# exclude any nested elements as they need to be added later. These are not added as attributes
					if attr_name != 'elements':
						attr = element[attr_name]

						# if the attribute is a dict in Ignition, then the dict will contain the actual attribute names
						if isinstance(attr, dict):
							for prop_name in attr:
								svg_prop_name = ATTR_LOOKUP.get('{}.{}'.format(attr_name, prop_name), 'UNSET')
								if svg_prop_name == 'UNSET':
									not_included_SVG_elements.append('{}.{}'.format(attr_name, prop_name))

								svg_prop_val = attr[prop_name]

								attrs_dict[svg_prop_name] = svg_prop_val
						else:
							attrs_dict[ATTR_LOOKUP.get(attr_name, attr_name)] = attr

				element_name = attrs_dict['type']
				if element_name == 'group':
					element_name = 'g'

				xmlobj_attr = et.SubElement(xmlobj, element_name, **attrs_dict)

				if 'elements' in element:
					attr_elements = copy.deepcopy(element['elements'])

					addElements(xmlobj_attr, attr_elements)

	addElements(doc, elements)
	#print ET.tostring(doc, encoding='utf8', method='xml')
	
	if not_included_SVG_elements != []:
		print "Copied to clipboard, but some SVG attributes are missing mapping. These have not been extracted {}".format(not_included_SVG_elements)
	
	return et.tostring(doc), name

# Update file path
path = "C:\Testing\\"

json = readText()
obj = system.util.jsonDecode(json)
svg, name = extract_svg_from_view(obj)

with open(path + name + ".svg", "w") as file:
    file.write(svg)
    print("Saved: " + name)

I tried using that code and it ran without an error but did not write the file into my document folder. Any idea why?

I haven't tried it, but you should be able to get a SVG using inspect in the browser.