# Exporting SVGs from Perspective

**URL:** <https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912>\
**Category:** Ignition\
**Tags:** ignition81, perspective\
**Created:** [November 1, 2022, 4:02am UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912 "2022-11-01T04:02:46Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![Douglas\_Lawrie](https://avatars.discourse-cdn.com/v4/letter/d/a87d85/32.png) [@Douglas\_Lawrie](https://forum.inductiveautomation.com/u/Douglas_Lawrie)\
**Post date:** [November 1, 2022, 4:02am UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/1 "2022-11-01T04:02:46Z")

</div>

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?

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [November 1, 2022, 10:35am UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/2 "2022-11-01T10:35:21Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![Sean\_Tesch](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/sean_tesch/32/40094_2.png) [@Sean\_Tesch](https://forum.inductiveautomation.com/u/Sean_Tesch)\
**Post date:** [November 2, 2022, 5:56pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/3 "2022-11-02T17:56:51Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [November 2, 2022, 10:02pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/4 "2022-11-02T22:02:47Z")

</div>

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

> [@System.tag.configure incorrectly overriding multiple UDT tag properties](https://forum.inductiveautomation.com/t/system-tag-configure-incorrectly-overriding-multiple-udt-tag-properties/40593/18):
>
> FWIW, I recently forgot about this and inadvertently added property overrides to a large number of UDTInstance tags confused So I created a script to remove the prop overrides on tags part of UDT instances that were not included in a list of property names. The script copies the tag json back into clipboard, so you can review it first before saving as a json and importing it back in. 
> 
> > **Main Functions**
> >
> > def removePropOverrides(tags, retainProps=None, tagpath='', insideUDTInstance=False): …

```auto
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)

```

---

<div class="post-metadata">

**Author:** ![Sean\_Tesch](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/sean_tesch/32/40094_2.png) [@Sean\_Tesch](https://forum.inductiveautomation.com/u/Sean_Tesch)\
**Post date:** [November 3, 2022, 6:45pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/5 "2022-11-03T18:45:48Z")

</div>

Thank you very much!

---

<div class="post-metadata">

**Author:** ![Ali\_Ali](https://avatars.discourse-cdn.com/v4/letter/a/bbe5ce/32.png) [@Ali\_Ali](https://forum.inductiveautomation.com/u/Ali_Ali)\
**Post date:** [October 26, 2023, 10:12am UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/6 "2023-10-26T10:12:06Z")

</div>

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:

```auto
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:

```auto
[
  {
    "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"
        }
      }
    }
  }
]

```

---

<div class="post-metadata">

**Author:** ![Ali\_Ali](https://avatars.discourse-cdn.com/v4/letter/a/bbe5ce/32.png) [@Ali\_Ali](https://forum.inductiveautomation.com/u/Ali_Ali)\
**Post date:** [October 26, 2023, 12:37pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/7 "2023-10-26T12:37:06Z")

</div>

> 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.

---

<div class="post-metadata">

**Author:** ![automationplc](https://avatars.discourse-cdn.com/v4/letter/a/4491bb/32.png) [@automationplc](https://forum.inductiveautomation.com/u/automationplc)\
**Post date:** [January 27, 2025, 1:25pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/8 "2025-01-27T13:25:19Z")

</div>

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.

```python
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)

```

---

<div class="post-metadata">

**Author:** ![brandon1](https://avatars.discourse-cdn.com/v4/letter/b/ea666f/32.png) [@brandon1](https://forum.inductiveautomation.com/u/brandon1)\
**Post date:** [January 27, 2025, 1:44pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/9 "2025-01-27T13:44:35Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![jlandwerlen](https://avatars.discourse-cdn.com/v4/letter/j/bc8723/32.png) [@jlandwerlen](https://forum.inductiveautomation.com/u/jlandwerlen)\
**Post date:** [January 27, 2025, 1:46pm UTC](https://forum.inductiveautomation.com/t/exporting-svgs-from-perspective/65912/10 "2025-01-27T13:46:57Z")

</div>

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