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.