text: import json
  import copy
  import xml.etree.ElementTree as etree
  from xml.dom import minidom
  import warnings
  import torch
  import numpy as np
  import re
  import sys
  import pydiffvg
  import math
  from collections import namedtuple
  import cssutils
  
  class SvgOptimizationSettings:
  
      default_params = {
          "optimize_color": True,
          "color_lr": 2e-3,
          "optimize_alpha": False,
          "alpha_lr": 2e-3,
          "optimizer": "Adam",
          "transforms": {
              "optimize_transforms":True,
              "transform_mode":"rigid",
              "translation_mult":1e-3,
              "transform_lr":2e-3
          },
          "circles": {
              "optimize_center": True,
              "optimize_radius": True,
              "shape_lr": 2e-1
          },
          "paths": {
              "optimize_points": True,
              "shape_lr": 2e-1
          },
          "gradients": {
              "optimize_stops": True,
              "stop_lr": 2e-3,
              "optimize_color": True,
              "color_lr": 2e-3,
              "optimize_alpha": False,
              "alpha_lr": 2e-3,
              "optimize_location": True,
              "location_lr": 2e-1
          }
      }
  
      optims = {
          "Adam": torch.optim.Adam,
          "SGD": torch.optim.SGD,
          "ASGD": torch.optim.ASGD,
      }
  
      #region methods
      def __init__(self, f=None):
          self.store = {}
          if f is None:
              self.store["default"] = copy.deepcopy(SvgOptimizationSettings.default_params)
          else:
              self.store = json.load(f)
  
      # create default alias for root
      def default_name(self, dname):
          self.dname = dname
          if dname not in self.store:
              self.store[dname] = self.store["default"]
  
      def retrieve(self, node_id):
          if node_id not in self.store:
              return (self.store["default"], False)
          else:
              return (self.store[node_id], True)
  
      def reset_to_defaults(self, node_id):
          if node_id in self.store:
              del self.store[node_id]
  
          return self.store["default"]
  
      def undefault(self, node_id):
          if node_id not in self.store:
              self.store[node_id] = copy.deepcopy(self.store["default"])
  
          return self.store[node_id]
  
      def override_optimizer(self, optimizer):
          if optimizer is not None:
              for v in self.store.values():
                  v["optimizer"] = optimizer
  
      def global_override(self, path, value):
          for store in self.store.values():
              d = store
              for key in path[:-1]:
                  d = d[key]
  
              d[path[-1]] = value
  
      def save(self, file):
          self.store["default"] = self.store[self.dname]
          json.dump(self.store, file, indent="\t")
      #endregion
  
  class OptimizableSvg:
  
      class TransformTools:
          @staticmethod
          def parse_matrix(vals):
              assert(len(vals)==6)
              return np.array([[vals[0],vals[2],vals[4]],[vals[1], vals[3], vals[5]],[0,0,1]])
  
          @staticmethod
          def parse_translate(vals):
              assert(len(vals)>=1 and len(vals)<=2)
              mat=np.eye(3)
              mat[0,2]=vals[0]
              if len(vals)>1:
                  mat[1,2]=vals[1]
              return mat
  
          @staticmethod
          def parse_rotate(vals):
              assert (len(vals) == 1 or len(vals) == 3)
              mat = np.eye(3)
              rads=math.radians(vals[0])
              sint=math.sin(rads)
              cost=math.cos(rads)
              mat[0:2, 0:2] = np.array([[cost,-sint],[sint,cost]])
              if len(vals) > 1:
                  tr1=parse_translate(vals[1:3])
                  tr2=parse_translate([-vals[1],-vals[2]])
                  mat=tr1 @ mat @ tr2
              return mat
  
          @staticmethod
          def parse_scale(vals):
              assert (len(vals) >= 1 and len(vals) <= 2)
              d=np.array([vals[0], vals[1] if len(vals)>1 else vals[0],1])
              return np.diag(d)
  
          @staticmethod
          def parse_skewx(vals):
              assert(len(vals)==1)
              m=np.eye(3)
              m[0,1]=vals[0]
              return m
  
          @staticmethod
          def parse_skewy(vals):
              assert (len(vals) == 1)
              m = np.eye(3)
              m[1, 0] = vals[0]
              return m
  
          @staticmethod
          def transformPoints(pointsTensor, transform):
              assert(transform is not None)
              one=torch.ones((pointsTensor.shape[0],1),device=pointsTensor.device)
              homo_points = torch.cat([pointsTensor, one], dim=1)
              mult = transform.mm(homo_points.permute(1,0)).permute(1,0)
              tfpoints=mult[:, 0:2].contiguous()
              #print(torch.norm(mult[:,2]-one))
              assert(pointsTensor.shape == tfpoints.shape)
              return tfpoints
  
          @staticmethod
          def promote_numpy(M):
              ret = np.eye(3)
              ret[0:2, 0:2] = M
              return ret
  
          @staticmethod
          def recompose_numpy(Theta,ScaleXY,ShearX,TXY):
              cost=math.cos(Theta)
              sint=math.sin(Theta)
              Rot=np.array([[cost, -sint],[sint, cost]])
              Scale=np.diag(ScaleXY)
              Shear=np.eye(2)
              Shear[0,1]=ShearX
  
              Translate=np.eye(3)
              Translate[0:2,2]=TXY
              
              M=OptimizableSvg.TransformTools.promote_numpy(Rot @ Scale @ Shear) @ Translate
              return M
  
          @staticmethod
          def promote(m):
              M=torch.eye(3).to(m.device)
              M[0:2,0:2]=m
              return M
  
          @staticmethod
          def make_rot(Theta):
              sint=Theta.sin().squeeze()
              cost=Theta.cos().squeeze()
              #m=torch.tensor([[cost, -sint],[sint, cost]])
              Rot=torch.stack((torch.stack((cost,-sint)),torch.stack((sint,cost))))
              return Rot
  
          @staticmethod
          def make_scale(ScaleXY):
              if ScaleXY.squeeze().dim()==0:
                  ScaleXY=ScaleXY.squeeze()
                  #uniform scale
                  return torch.diag(torch.stack([ScaleXY,ScaleXY])).to(ScaleXY.device)
              else:
                  return torch.diag(ScaleXY).to(ScaleXY.device)
  
          @staticmethod
          def make_shear(ShearX):
              m=torch.eye(2).to(ShearX.device)
              m[0,1]=ShearX
              return m
  
          @staticmethod
          def make_translate(TXY):
              m=torch.eye(3).to(TXY.device)
              m[0:2,2]=TXY
              return m
  
          @staticmethod
          def recompose(Theta,ScaleXY,ShearX,TXY):
              Rot=OptimizableSvg.TransformTools.make_rot(Theta)
              Scale=OptimizableSvg.TransformTools.make_scale(ScaleXY)
              Shear=OptimizableSvg.TransformTools.make_shear(ShearX)
              Translate=OptimizableSvg.TransformTools.make_translate(TXY)
  
              return OptimizableSvg.TransformTools.promote(Rot.mm(Scale).mm(Shear)).mm(Translate)
  
          TransformDecomposition=namedtuple("TransformDecomposition","theta scale shear translate")
          TransformProperties=namedtuple("TransformProperties", "has_rotation has_scale has_mirror scale_uniform has_shear has_translation")
  
          @staticmethod
          def make_named(decomp):
              if not isinstance(decomp,OptimizableSvg.TransformTools.TransformDecomposition):
                  decomp=OptimizableSvg.TransformTools.TransformDecomposition(theta=decomp[0],scale=decomp[1],shear=decomp[2],translate=decomp[3])
              return decomp
  
          @staticmethod
          def analyze_transform(decomp):
              decomp=OptimizableSvg.TransformTools.make_named(decomp)
              epsilon=1e-3
              has_rotation=abs(decomp.theta)>epsilon
              has_scale=abs((abs(decomp.scale)-1)).max()>epsilon
              scale_len=decomp.scale.squeeze().ndim>0 if isinstance(decomp.scale,np.ndarray) else decomp.scale.squeeze().dim() > 0
              has_mirror=scale_len and decomp.scale[0]*decomp.scale[1] < 0
              scale_uniform=not scale_len or abs(abs(decomp.scale[0])-abs(decomp.scale[1]))<epsilon
              has_shear=abs(decomp.shear)>epsilon
              has_translate=max(abs(decomp.translate[0]),abs(decomp.translate[1]))>epsilon
  
              return OptimizableSvg.TransformTools.TransformProperties(has_rotation=has_rotation,has_scale=has_scale,has_mirror=has_mirror,scale_uniform=scale_uniform,has_shear=has_shear,has_translation=has_translate)
  
          @staticmethod
          def check_and_decomp(M):
              decomp=OptimizableSvg.TransformTools.decompose(M) if M is not None else OptimizableSvg.TransformTools.TransformDecomposition(theta=0,scale=(1,1),shear=0,translate=(0,0))
              props=OptimizableSvg.TransformTools.analyze_transform(decomp)
              return (decomp, props)
  
          @staticmethod
          def tf_to_string(M):
              tfstring = "matrix({} {} {} {} {} {})".format(M[0, 0], M[1, 0], M[0, 1], M[1, 1], M[0, 2], M[1, 2])
              return tfstring
  
          @staticmethod
          def decomp_to_string(decomp):
              decomp = OptimizableSvg.TransformTools.make_named(decomp)
              ret=""
              props=OptimizableSvg.TransformTools.analyze_transform(decomp)
              if props.has_rotation:
                  ret+="rotate({}) ".format(math.degrees(decomp.theta.item()))
              if props.has_scale:
                  if decomp.scale.dim()==0:
                      ret += "scale({}) ".format(decomp.scale.item())
                  else:
                      ret+="scale({} {}) ".format(decomp.scale[0], decomp.scale[1])
              if props.has_shear:
                  ret+="skewX({}) ".format(decomp.shear.item())
              if props.has_translation:
                  ret+="translate({} {}) ".format(decomp.translate[0],decomp.translate[1])
  
              return ret
  
          @staticmethod
          def decompose(M):
              m = M[0:2, 0:2]
              t0=M[0:2, 2]
              #get translation so that we can post-multiply with it
              TXY=np.linalg.solve(m,t0)
  
              T=np.eye(3)
              T[0:2,2]=TXY
  
              q, r = np.linalg.qr(m)
  
              ref = np.array([[1, 0], [0, np.sign(np.linalg.det(q))]])
  
              Rot = np.dot(q, ref)
  
              ref2 = np.array([[1, 0], [0, np.sign(np.linalg.det(r))]])
  
              r2 = np.dot(ref2, r)
  
              Ref = np.dot(ref, ref2)
              
              sc = np.diag(r2)
              Scale = np.diagflat(sc)
  
              Shear = np.eye(2)
              Shear[0, 1] = r2[0, 1] / sc[0]
              #the actual shear coefficient
              ShearX=r2[0, 1] / sc[0]
  
              if np.sum(sc) < 0:
                  # both scales are negative, flip this and add a 180 rotation
                  Rot = np.dot(Rot, -np.eye(2))
                  Scale = -Scale
  
              Theta = math.atan2(Rot[1, 0], Rot[0, 0])
              ScaleXY = np.array([Scale[0,0],Scale[1,1]*Ref[1,1]])
  
              return OptimizableSvg.TransformTools.TransformDecomposition(theta=Theta, scale=ScaleXY, shear=ShearX, translate=TXY)
  
      #region suboptimizers
  
      #optimizes color, but really any tensor that needs to stay between 0 and 1 per-entry
      class ColorOptimizer:
          def __init__(self,tensor,optim_type,lr):
              self.tensor=tensor
              self.optim=optim_type([tensor],lr=lr)
  
          def zero_grad(self):
              self.optim.zero_grad()
  
          def step(self):
              self.optim.step()
              self.tensor.data.clamp_(min=1e-4,max=1.)
  
      #optimizes gradient stop positions
      class StopOptimizer:
          def __init__(self,stops,optim_type,lr):
              self.stops=stops
              self.optim=optim_type([stops],lr=lr)
  
          def zero_grad(self):
              self.optim.zero_grad()
  
          def step(self):
              self.optim.step()
              self.stops.data.clamp_(min=0., max=1.)
              self.stops.data, _ = self.stops.sort()
              self.stops.data[0] = 0.
              self.stops.data[-1]=1.
  
      #optimizes gradient: stop, positions, colors+opacities, locations
      class GradientOptimizer:
          def __init__(self, begin, end, offsets, stops, optim_params):
              self.begin=begin.clone().detach() if begin is not None else None
              self.end=end.clone().detach() if end is not None else None
              self.offsets=offsets.clone().detach() if offsets is not None else None
              self.stop_colors=stops[:,0:3].clone().detach() if stops is not None else None
              self.stop_alphas=stops[:,3].clone().detach() if stops is not None else None
              self.optimizers=[]
  
              if optim_params["gradients"]["optimize_stops"] and self.offsets is not None:
                  self.offsets.requires_grad_(True)
                  self.optimizers.append(OptimizableSvg.StopOptimizer(self.offsets,SvgOptimizationSettings.optims[optim_params["optimizer"]],optim_params["gradients"]["stop_lr"]))
              if optim_params["gradients"]["optimize_color"] and self.stop_colors is not None:
                  self.stop_colors.requires_grad_(True)
                  self.optimizers.append(OptimizableSvg.ColorOptimizer(self.stop_colors,SvgOptimizationSettings.optims[optim_params["optimizer"]],optim_params["gradients"]["color_lr"]))
              if optim_params["gradients"]["optimize_alpha"] and self.stop_alphas is not None:
                  self.stop_alphas.requires_grad_(True)
                  self.optimizers.append(OptimizableSvg.ColorOptimizer(self.stop_alphas,SvgOptimizationSettings.optims[optim_params["optimizer"]],optim_params["gradients"]["alpha_lr"]))
              if optim_params["gradients"]["optimize_location"] and self.begin is not None and self.end is not None:
                  self.begin.requires_grad_(True)
                  self.end.requires_grad_(True)
                  self.optimizers.append(SvgOptimizationSettings.optims[optim_params["optimizer"]]([self.begin,self.end],lr=optim_params["gradients"]["location_lr"]))
  
  
          def get_vals(self):
              return self.begin, self.end, self.offsets, torch.cat((self.stop_colors,self.stop_alphas.unsqueeze(1)),1) if self.stop_colors is not None and self.stop_alphas is not None else None
  
          def zero_grad(self):
              for optim in self.optimizers:
                  optim.zero_grad()
  
          def step(self):
              for optim in self.optimizers:
                  optim.step()
  
      class TransformOptimizer:
          def __init__(self,transform,optim_params):
              self.transform=transform
              self.optimizes=optim_params["transforms"]["optimize_transforms"] and transform is not None
              self.params=copy.deepcopy(optim_params)
              self.transform_mode=optim_params["transforms"]["transform_mode"]
  
              if self.optimizes:
                  optimvars=[]
                  self.residual=None
                  lr=optim_params["transforms"]["transform_lr"]
                  tmult=optim_params["transforms"]["translation_mult"]
                  decomp,props=OptimizableSvg.TransformTools.check_and_decomp(transform.cpu().numpy())
                  if self.transform_mode=="move":
                      #only translation and rotation should be set
                      if props.has_scale or props.has_shear or props.has_mirror:
                          print("Warning: set to optimize move only, but input transform has residual scale or shear")
                          self.residual=self.transform.clone().detach().requires_grad_(False)
                          self.Theta=torch.tensor(0,dtype=torch.float32,requires_grad=True,device=transform.device)
                          self.translation=torch.tensor([0, 0],dtype=torch.float32,requires_grad=True,device=transform.device)
                      else:
                          self.residual=None
                          self.Theta=torch.tensor(decomp.theta,dtype=torch.float32,requires_grad=True,device=transform.device)
                          self.translation=torch.tensor(decomp.translate,dtype=torch.float32,requires_grad=True,device=transform.device)
                      optimvars+=[{'params':x,'lr':lr} for x in [self.Theta]]+[{'params':self.translation,'lr':lr*tmult}]
                  elif self.transform_mode=="rigid":
                      #only translation, rotation, and uniform scale should be set
                      if props.has_shear or props.has_mirror or not props.scale_uniform:
                          print("Warning: set to optimize rigid transform only, but input transform has residual shear, mirror or non-uniform scale")
                          self.residual = self.transform.clone().detach().requires_grad_(False)
                          self.Theta = torch.tensor(0, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.translation = torch.tensor([0, 0], dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.scale=torch.tensor(1, dtype=torch.float32, requires_grad=True,device=transform.device)
                      else:
                          self.residual = None
                          self.Theta = torch.tensor(decomp.theta, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.translation = torch.tensor(decomp.translate, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.scale = torch.tensor(decomp.scale[0], dtype=torch.float32, requires_grad=True,device=transform.device)
                      optimvars += [{'params':x,'lr':lr} for x in [self.Theta, self.scale]]+[{'params':self.translation,'lr':lr*tmult}]
                  elif self.transform_mode=="similarity":
                      if props.has_shear or not props.scale_uniform:
                          print("Warning: set to optimize rigid transform only, but input transform has residual shear or non-uniform scale")
                          self.residual = self.transform.clone().detach().requires_grad_(False)
                          self.Theta = torch.tensor(0, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.translation = torch.tensor([0, 0], dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.scale=torch.tensor(1, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.scale_sign=torch.tensor(1,dtype=torch.float32,requires_grad=False,device=transform.device)
                      else:
                          self.residual = None
                          self.Theta = torch.tensor(decomp.theta, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.translation = torch.tensor(decomp.translate, dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.scale = torch.tensor(decomp.scale[0], dtype=torch.float32, requires_grad=True,device=transform.device)
                          self.scale_sign = torch.tensor(np.sign(decomp.scale[0]*decomp.scale[1]), dtype=torch.float32, requires_grad=False,device=transform.device)
                      optimvars += [{'params':x,'lr':lr} for x in [self.Theta, self.scale]]+[{'params':self.translation,'lr':lr*tmult}]
                  elif self.transform_mode=="affine":
                      self.Theta = torch.tensor(decomp.theta, dtype=torch.float32, requires_grad=True,device=transform.device)
                      self.translation = torch.tensor(decomp.translate, dtype=torch.float32, requires_grad=True,device=transform.device)
                      self.scale = torch.tensor(decomp.scale, dtype=torch.float32, requires_grad=True,device=transform.device)
                      self.shear = torch.tensor(decomp.shear, dtype=torch.float32, requires_grad=True,device=transform.device)
                      optimvars += [{'params':x,'lr':lr} for x in [self.Theta, self.scale, self.shear]]+[{'params':self.translation,'lr':lr*tmult}]
                  else:
                      raise ValueError("Unrecognized transform mode '{}'".format(self.transform_mode))
                  self.optimizer=SvgOptimizationSettings.optims[optim_params["optimizer"]](optimvars)
  
          def get_transform(self):
              if not self.optimizes:
                  return self.transform
              else:
                  if self.transform_mode == "move":
                      composed=OptimizableSvg.TransformTools.recompose(self.Theta,torch.tensor([1.],device=self.Theta.device),torch.tensor(0.,device=self.Theta.device),self.translation)
                      return self.residual.mm(composed) if self.residual is not None else composed
                  elif self.transform_mode == "rigid":
                      composed = OptimizableSvg.TransformTools.recompose(self.Theta, self.scale, torch.tensor(0.,device=self.Theta.device),
                                                                         self.translation)
                      return self.residual.mm(composed) if self.residual is not None else composed
                  elif self.transform_mode == "similarity":
                      composed=OptimizableSvg.TransformTools.recompose(self.Theta, torch.cat((self.scale,self.scale*self.scale_sign)),torch.tensor(0.,device=self.Theta.device),self.translation)
                      return self.residual.mm(composed) if self.residual is not None else composed
                  elif self.transform_mode == "affine":
                      composed = OptimizableSvg.TransformTools.recompose(self.Theta, self.scale, self.shear, self.translation)
                      return composed
                  else:
                      raise ValueError("Unrecognized transform mode '{}'".format(self.transform_mode))
  
          def tfToString(self):
              if self.transform is None:
                  return None
              elif not self.optimizes:
                  return OptimizableSvg.TransformTools.tf_to_string(self.transform)
              else:
                  if self.transform_mode == "move":
                      str=OptimizableSvg.TransformTools.decomp_to_string((self.Theta,torch.tensor([1.]),torch.tensor(0.),self.translation))
                      return (OptimizableSvg.TransformTools.tf_to_string(self.residual) if self.residual is not None else "")+" "+str
                  elif self.transform_mode == "rigid":
                      str = OptimizableSvg.TransformTools.decomp_to_string((self.Theta, self.scale, torch.tensor(0.),
                                                                         self.translation))
                      return (OptimizableSvg.TransformTools.tf_to_string(self.residual) if self.residual is not None else "")+" "+str
                  elif self.transform_mode == "similarity":
                      str=OptimizableSvg.TransformTools.decomp_to_string((self.Theta, torch.cat((self.scale,self.scale*self.scale_sign)),torch.tensor(0.),self.translation))
                      return (OptimizableSvg.TransformTools.tf_to_string(self.residual) if self.residual is not None else "")+" "+str
                  elif self.transform_mode == "affine":
                      str = OptimizableSvg.TransformTools.decomp_to_string((self.Theta, self.scale, self.shear, self.translation))
                      return composed
  
          def zero_grad(self):
              if self.optimizes:
                  self.optimizer.zero_grad()
  
          def step(self):
              if self.optimizes:
                  self.optimizer.step()
  
      #endregion
  
      #region Nodes
      class SvgNode:
          def __init__(self,id,transform,appearance,settings):
              self.id=id
              self.children=[]
              self.optimizers=[]
              self.device = settings.device
              self.transform=torch.tensor(transform,dtype=torch.float32,device=self.device) if transform is not None else None
              self.transform_optim=OptimizableSvg.TransformOptimizer(self.transform,settings.retrieve(self.id)[0])
              self.optimizers.append(self.transform_optim)
              self.proc_appearance(appearance,settings.retrieve(self.id)[0])
  
          def tftostring(self):
              return self.transform_optim.tfToString()
  
          def appearanceToString(self):
              appstring=""
              for key,value in self.appearance.items():
                  if key in ["fill", "stroke"]:
                      #a paint-type value
                      if value[0] == "none":
                          appstring+="{}:none;".format(key)
                      elif value[0] == "solid":
                          appstring += "{}:{};".format(key,OptimizableSvg.rgb_to_string(value[1]))
                      elif value[0] == "url":
                          appstring += "{}:url(#{});".format(key,value[1].id)
                          #appstring += "{}:{};".format(key,"#ff00ff")
                  elif key in ["opacity", "fill-opacity", "stroke-opacity", "stroke-width", "fill-rule"]:
                      appstring+="{}:{};".format(key,value)
                  else:
                      raise ValueError("Don't know how to write appearance parameter '{}'".format(key))
              return appstring
  
  
          def write_xml_common_attrib(self,node,tfname="transform"):
              if self.transform is not None:
                  node.set(tfname,self.tftostring())
              if len(self.appearance)>0:
                  node.set('style',self.appearanceToString())
              if self.id is not None:
                  node.set('id',self.id)
  
  
          def proc_appearance(self,appearance,optim_params):
              self.appearance=appearance
              for key, value in appearance.items():
                  if key == "fill" or key == "stroke":
                      if optim_params["optimize_color"] and value[0]=="solid":
                          value[1].requires_grad_(True)
                          self.optimizers.append(OptimizableSvg.ColorOptimizer(value[1],SvgOptimizationSettings.optims[optim_params["optimizer"]],optim_params["color_lr"]))
                  elif key == "fill-opacity" or key == "stroke-opacity" or key == "opacity":
                      if optim_params["optimize_alpha"]:
                          value[1].requires_grad_(True)
                          self.optimizers.append(OptimizableSvg.ColorOptimizer(value[1], optim_params["optimizer"],
                                                                               optim_params["alpha_lr"]))
                  elif key == "fill-rule" or key == "stroke-width":
                      pass
                  else:
                      raise RuntimeError("Unrecognized appearance key '{}'".format(key))
  
          def prop_transform(self,intform):
              return intform.matmul(self.transform_optim.get_transform()) if self.transform is not None else intform
  
          def prop_appearance(self,inappearance):
              outappearance=copy.copy(inappearance)
              for key,value in self.appearance.items():
                  if key == "fill":
                      #gets replaced
                      outappearance[key]=value
                  elif key == "fill-opacity":
                      #gets multiplied
                      outappearance[key] = outappearance[key]*value
                  elif key == "fill-rule":
                      #gets replaced
                      outappearance[key] = value
                  elif key =="opacity":
                      # gets multiplied
                      outappearance[key] = outappearance[key]*value
                  elif key == "stroke":
                      # gets replaced
                      outappearance[key] = value
                  elif key == "stroke-opacity":
                      # gets multiplied
                      outappearance[key] = outappearance[key]*value
                  elif key =="stroke-width":
                      # gets replaced
                      outappearance[key] = value
                  else:
                      raise RuntimeError("Unrecognized appearance key '{}'".format(key))
              return outappearance
  
          def zero_grad(self):
              for optim in self.optimizers:
                  optim.zero_grad()
              for child in self.children:
                  child.zero_grad()
  
          def step(self):
              for optim in self.optimizers:
                  optim.step()
              for child in self.children:
                  child.step()
  
          def get_type(self):
              return "Generic node"
  
          def is_shape(self):
              return False
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              raise NotImplementedError("Abstract SvgNode cannot recurse")
  
      class GroupNode(SvgNode):
          def __init__(self, id, transform, appearance,settings):
              super().__init__(id, transform, appearance,settings)
  
          def get_type(self):
              return "Group node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              outtf=self.prop_transform(transform)
              outapp=self.prop_appearance(appearance)
              for child in self.children:
                  child.build_scene(shapes,shape_groups,outtf,outapp)
  
          def write_xml(self, parent):
              elm=etree.SubElement(parent,"g")
              self.write_xml_common_attrib(elm)
  
              for child in self.children:
                  child.write_xml(elm)
  
      class RootNode(SvgNode):
          def __init__(self, id, transform, appearance,settings):
              super().__init__(id, transform, appearance,settings)
  
          def write_xml(self,document):
              elm=etree.Element('svg')
              self.write_xml_common_attrib(elm)
              elm.set("version","2.0")
              elm.set("width",str(document.canvas[0]))
              elm.set("height", str(document.canvas[1]))
              elm.set("xmlns","http://www.w3.org/2000/svg")
              elm.set("xmlns:xlink","http://www.w3.org/1999/xlink")
              #write definitions before we write any children
              document.write_defs(elm)
  
              #write the children
              for child in self.children:
                  child.write_xml(elm)
  
              return elm
  
          def get_type(self):
              return "Root node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              outtf = self.prop_transform(transform).to(self.device)
              for child in self.children:
                  child.build_scene(shapes,shape_groups,outtf,appearance)
  
          @staticmethod
          def get_default_appearance(device):
              default_appearance = {"fill": ("solid", torch.tensor([0., 0., 0.],device=device)),
                                    "fill-opacity": torch.tensor([1.],device=device),
                                    "fill-rule": "nonzero",
                                    "opacity": torch.tensor([1.],device=device),
                                    "stroke": ("none", None),
                                    "stroke-opacity": torch.tensor([1.],device=device),
                                    "stroke-width": torch.tensor([0.],device=device)}
              return default_appearance
  
          @staticmethod
          def get_default_transform():
              return torch.eye(3)
  
  
  
      class ShapeNode(SvgNode):
          def __init__(self, id, transform, appearance,settings):
              super().__init__(id, transform, appearance,settings)
  
          def get_type(self):
              return "Generic shape node"
  
          def is_shape(self):
              return True
  
          def construct_paint(self,value,combined_opacity,transform):
              if value[0]   == "none":
                  return None
              elif value[0] == "solid":
                  return torch.cat([value[1],combined_opacity]).to(self.device)
              elif value[0] == "url":
                  #get the gradient object from this node
                  return value[1].getGrad(combined_opacity,transform)
              else:
                  raise ValueError("Unknown paint value type '{}'".format(value[0]))
  
          def make_shape_group(self,appearance,transform,num_shapes,num_subobjects):
              fill=self.construct_paint(appearance["fill"],appearance["opacity"]*appearance["fill-opacity"],transform)
              stroke=self.construct_paint(appearance["stroke"],appearance["opacity"]*appearance["stroke-opacity"],transform)
              sg = pydiffvg.ShapeGroup(shape_ids=torch.tensor(range(num_shapes, num_shapes + num_subobjects)),
                                       fill_color=fill,
                                       use_even_odd_rule=appearance["fill-rule"]=="evenodd",
                                       stroke_color=stroke,
                                       shape_to_canvas=transform,
                                       id=self.id)
              return sg
  
      class PathNode(ShapeNode):
          def __init__(self, id, transform, appearance,settings, paths):
              super().__init__(id, transform, appearance,settings)
              self.proc_paths(paths,settings.retrieve(self.id)[0])
  
          def proc_paths(self,paths,optim_params):
              self.paths=paths
              if optim_params["paths"]["optimize_points"]:
                  ptlist=[]
                  for path in paths:
                      ptlist.append(path.points.requires_grad_(True))
                  self.optimizers.append(SvgOptimizationSettings.optims[optim_params["optimizer"]](ptlist,lr=optim_params["paths"]["shape_lr"]))
  
          def get_type(self):
              return "Path node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              applytf=self.prop_transform(transform)
              applyapp = self.prop_appearance(appearance)
              sg=self.make_shape_group(applyapp,applytf,len(shapes),len(self.paths))
              for path in self.paths:
                  disp_path=pydiffvg.Path(path.num_control_points,path.points,path.is_closed,applyapp["stroke-width"],path.id)
                  shapes.append(disp_path)
              shape_groups.append(sg)
  
          def path_to_string(self,path):
              path_string = "M {},{} ".format(path.points[0][0].item(), path.points[0][1].item())
              idx = 1
              numpoints = path.points.shape[0]
              for type in path.num_control_points:
                  toproc = type + 1
                  if type == 0:
                      # add line
                      path_string += "L "
                  elif type == 1:
                      # add quadric
                      path_string += "Q "
                  elif type == 2:
                      # add cubic
                      path_string += "C "
                  while toproc > 0:
                      path_string += "{},{} ".format(path.points[idx % numpoints][0].item(),
                                                     path.points[idx % numpoints][1].item())
                      idx += 1
                      toproc -= 1
              if path.is_closed:
                  path_string += "Z "
  
              return path_string
  
          def paths_string(self):
              pstr=""
              for path in self.paths:
                  pstr+=self.path_to_string(path)
              return pstr
  
          def write_xml(self, parent):
              elm = etree.SubElement(parent, "path")
              self.write_xml_common_attrib(elm)
              elm.set("d",self.paths_string())
  
              for child in self.children:
                  child.write_xml(elm)
  
      class RectNode(ShapeNode):
          def __init__(self, id, transform, appearance,settings, rect):
              super().__init__(id, transform, appearance,settings)
              self.rect=torch.tensor(rect,dtype=torch.float,device=settings.device)
              optim_params=settings.retrieve(self.id)[0]
              #borrowing path settings for this
              if optim_params["paths"]["optimize_points"]:
                  self.optimizers.append(SvgOptimizationSettings.optims[optim_params["optimizer"]]([self.rect],lr=optim_params["paths"]["shape_lr"]))
  
          def get_type(self):
              return "Rect node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              applytf=self.prop_transform(transform)
              applyapp = self.prop_appearance(appearance)
              sg=self.make_shape_group(applyapp,applytf,len(shapes),1)
              shapes.append(pydiffvg.Rect(self.rect[0:2],self.rect[0:2]+self.rect[2:4],applyapp["stroke-width"],self.id))
              shape_groups.append(sg)
  
          def write_xml(self, parent):
              elm = etree.SubElement(parent, "rect")
              self.write_xml_common_attrib(elm)
              elm.set("x",str(self.rect[0]))
              elm.set("y", str(self.rect[1]))
              elm.set("width", str(self.rect[2]))
              elm.set("height", str(self.rect[3]))
  
              for child in self.children:
                  child.write_xml(elm)
  
      class CircleNode(ShapeNode):
          def __init__(self, id, transform, appearance,settings, rect):
              super().__init__(id, transform, appearance,settings)
              self.circle=torch.tensor(rect,dtype=torch.float,device=settings.device)
              optim_params=settings.retrieve(self.id)[0]
              #borrowing path settings for this
              if optim_params["paths"]["optimize_points"]:
                  self.optimizers.append(SvgOptimizationSettings.optims[optim_params["optimizer"]]([self.circle],lr=optim_params["paths"]["shape_lr"]))
  
          def get_type(self):
              return "Circle node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              applytf=self.prop_transform(transform)
              applyapp = self.prop_appearance(appearance)
              sg=self.make_shape_group(applyapp,applytf,len(shapes),1)
              shapes.append(pydiffvg.Circle(self.circle[2],self.circle[0:2],applyapp["stroke-width"],self.id))
              shape_groups.append(sg)
  
          def write_xml(self, parent):
              elm = etree.SubElement(parent, "circle")
              self.write_xml_common_attrib(elm)
              elm.set("cx",str(self.circle[0]))
              elm.set("cy", str(self.circle[1]))
              elm.set("r", str(self.circle[2]))
  
              for child in self.children:
                  child.write_xml(elm)
  
  
      class EllipseNode(ShapeNode):
          def __init__(self, id, transform, appearance,settings, ellipse):
              super().__init__(id, transform, appearance,settings)
              self.ellipse=torch.tensor(ellipse,dtype=torch.float,device=settings.device)
              optim_params=settings.retrieve(self.id)[0]
              #borrowing path settings for this
              if optim_params["paths"]["optimize_points"]:
                  self.optimizers.append(SvgOptimizationSettings.optims[optim_params["optimizer"]]([self.ellipse],lr=optim_params["paths"]["shape_lr"]))
  
          def get_type(self):
              return "Ellipse node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              applytf=self.prop_transform(transform)
              applyapp = self.prop_appearance(appearance)
              sg=self.make_shape_group(applyapp,applytf,len(shapes),1)
              shapes.append(pydiffvg.Ellipse(self.ellipse[2:4],self.ellipse[0:2],applyapp["stroke-width"],self.id))
              shape_groups.append(sg)
  
          def write_xml(self, parent):
              elm = etree.SubElement(parent, "ellipse")
              self.write_xml_common_attrib(elm)
              elm.set("cx", str(self.ellipse[0]))
              elm.set("cy", str(self.ellipse[1]))
              elm.set("rx", str(self.ellipse[2]))
              elm.set("ry", str(self.ellipse[3]))
  
              for child in self.children:
                  child.write_xml(elm)
  
      class PolygonNode(ShapeNode):
          def __init__(self, id, transform, appearance,settings, points):
              super().__init__(id, transform, appearance,settings)
              self.points=points
              optim_params=settings.retrieve(self.id)[0]
              #borrowing path settings for this
              if optim_params["paths"]["optimize_points"]:
                  self.optimizers.append(SvgOptimizationSettings.optims[optim_params["optimizer"]]([self.points],lr=optim_params["paths"]["shape_lr"]))
  
          def get_type(self):
              return "Polygon node"
  
          def build_scene(self,shapes,shape_groups,transform,appearance):
              applytf=self.prop_transform(transform)
              applyapp = self.prop_appearance(appearance)
              sg=self.make_shape_group(applyapp,applytf,len(shapes),1)
              shapes.append(pydiffvg.Polygon(self.points,True,applyapp["stroke-width"],self.id))
              shape_groups.append(sg)
  
          def point_string(self):
              ret=""
              for i in range(self.points.shape[0]):
                  pt=self.points[i,:]
                  #assert pt.shape == (1,2)
                  ret+= str(pt[0])+","+str(pt[1])+" "
              return ret
  
          def write_xml(self, parent):
              elm = etree.SubElement(parent, "polygon")
              self.write_xml_common_attrib(elm)
              elm.set("points",self.point_string())
  
              for child in self.children:
                  child.write_xml(elm)
  
      class GradientNode(SvgNode):
          def __init__(self, id, transform,settings,begin,end,offsets,stops,href):
              super().__init__(id, transform, {},settings)
              self.optim=OptimizableSvg.GradientOptimizer(begin, end, offsets, stops, settings.retrieve(id)[0])
              self.optimizers.append(self.optim)
              self.href=href
  
          def is_ref(self):
              return self.href is not None
  
          def get_type(self):
              return "Gradient node"
  
          def get_stops(self):
              _, _, offsets, stops=self.optim.get_vals()
              return offsets, stops
  
          def get_points(self):
              begin, end, _, _ =self.optim.get_vals()
              return begin, end
  
          def write_xml(self, parent):
              elm = etree.SubElement(parent, "linearGradient")
              self.write_xml_common_attrib(elm,tfname="gradientTransform")
  
              begin, end, offsets, stops = self.optim.get_vals()
  
              if self.href is None:
                  #we have stops
                  for idx, offset in enumerate(offsets):
                      stop=etree.SubElement(elm,"stop")
                      stop.set("offset",str(offset.item()))
                      stop.set("stop-color",OptimizableSvg.rgb_to_string(stops[idx,0:3]))
                      stop.set("stop-opacity",str(stops[idx,3].item()))
              else:
                  elm.set('xlink:href', "#{}".format(self.href.id))
  
              if begin is not None and end is not None:
                  #no stops
                  elm.set('x1', str(begin[0].item()))
                  elm.set('y1', str(begin[1].item()))
                  elm.set('x2', str(end[0].item()))
                  elm.set('y2', str(end[1].item()))
  
                  # magic value to make this work
                  elm.set("gradientUnits", "userSpaceOnUse")
  
              for child in self.children:
                  child.write_xml(elm)
  
          def getGrad(self,combined_opacity,transform):
              if self.is_ref():
                  offsets, stops=self.href.get_stops()
              else:
                  offsets, stops=self.get_stops()
  
              stops=stops.clone()
              stops[:,3]*=combined_opacity
  
              begin,end = self.get_points()
  
              applytf=self.prop_transform(transform)
              begin=OptimizableSvg.TransformTools.transformPoints(begin.unsqueeze(0),applytf).squeeze()
              end = OptimizableSvg.TransformTools.transformPoints(end.unsqueeze(0), applytf).squeeze()
  
              return pydiffvg.LinearGradient(begin, end, offsets, stops)
      #endregion
  
      def __init__(self, filename, settings=SvgOptimizationSettings(),optimize_background=False, verbose=False, device=torch.device("cpu")):
          self.settings=settings
          self.verbose=verbose
          self.device=device
          self.settings.device=device
  
          tree = etree.parse(filename)
          root = tree.getroot()
  
          #in case we need global optimization
          self.optimizers=[]
          self.background=torch.tensor([1.,1.,1.],dtype=torch.float32,requires_grad=optimize_background,device=self.device)
  
          if optimize_background:
              p=settings.retrieve("default")[0]
              self.optimizers.append(OptimizableSvg.ColorOptimizer(self.background,SvgOptimizationSettings.optims[p["optimizer"]],p["color_lr"]))
  
          self.defs={}
  
          self.depth=0
  
          self.dirty=True
          self.scene=None
  
          self.parseRoot(root)
  
      recognised_shapes=["path","circle","rect","ellipse","polygon"]
  
      #region core functionality
      def build_scene(self):
          if self.dirty:
              shape_groups=[]
              shapes=[]
              self.root.build_scene(shapes,shape_groups,OptimizableSvg.RootNode.get_default_transform().to(self.device),OptimizableSvg.RootNode.get_default_appearance(self.device))
              self.scene=(self.canvas[0],self.canvas[1],shapes,shape_groups)
              self.dirty=False
          return self.scene
  
      def zero_grad(self):
          self.root.zero_grad()
          for optim in self.optimizers:
              optim.zero_grad()
          for item in self.defs.values():
              if issubclass(item.__class__,OptimizableSvg.SvgNode):
                  item.zero_grad()
  
      def render(self,scale=None,seed=0):
          #render at native resolution
          scene = self.build_scene()
          scene_args = pydiffvg.RenderFunction.serialize_scene(*scene)
          render = pydiffvg.RenderFunction.apply
          out_size=(scene[0],scene[1]) if scale is None else (int(scene[0]*scale),int(scene[1]*scale))
          img = render(out_size[0],  # width
                       out_size[1],  # height
                       2,  # num_samples_x
                       2,  # num_samples_y
                       seed,  # seed
                       None, # background_image
                       *scene_args)
          return img
  
      def step(self):
          self.dirty=True
          self.root.step()
          for optim in self.optimizers:
              optim.step()
          for item in self.defs.values():
              if issubclass(item.__class__, OptimizableSvg.SvgNode):
                  item.step()
      #endregion
  
      #region reporting
  
      def offset_str(self,s):
          return ("\t"*self.depth)+s
  
      def reportSkippedAttribs(self, node, non_skipped=[]):
          skipped=set([k for k in node.attrib.keys() if not OptimizableSvg.is_namespace(k)])-set(non_skipped)
          if len(skipped)>0:
              tag=OptimizableSvg.remove_namespace(node.tag) if "id" not in node.attrib else "{}#{}".format(OptimizableSvg.remove_namespace(node.tag),node.attrib["id"])
              print(self.offset_str("Warning: Skipping the following attributes of node '{}': {}".format(tag,", ".join(["'{}'".format(atr) for atr in skipped]))))
  
      def reportSkippedChildren(self,node,skipped):
          skipped_names=["{}#{}".format(elm.tag,elm.attrib["id"]) if "id" in elm.attrib else elm.tag for elm in skipped]
          if len(skipped)>0:
              tag = OptimizableSvg.remove_namespace(node.tag) if "id" not in node.attrib else "{}#{}".format(OptimizableSvg.remove_namespace(node.tag),
                                                                                              node.attrib["id"])
              print(self.offset_str("Warning: Skipping the following children of node '{}': {}".format(tag,", ".join(["'{}'".format(name) for name in skipped_names]))))
  
      #endregion
  
      #region parsing
      @staticmethod
      def remove_namespace(s):
          """
              {...} ... -> ...
          """
          return re.sub('{.*}', '', s)
  
      @staticmethod
      def is_namespace(s):
          return re.match('{.*}', s) is not None
  
      @staticmethod
      def parseTransform(node):
          if "transform" not in node.attrib and "gradientTransform" not in node.attrib:
              return None
  
          tf_string=node.attrib["transform"] if "transform" in node.attrib else node.attrib["gradientTransform"]
          tforms=tf_string.split(")")[:-1]
          mat=np.eye(3)
          for tform in tforms:
              type = tform.split("(")[0]
              args = [float(val) for val in re.split("[, ]+",tform.split("(")[1])]
              if type == "matrix":
                  mat=mat @ OptimizableSvg.TransformTools.parse_matrix(args)
              elif type == "translate":
                  mat = mat @ OptimizableSvg.TransformTools.parse_translate(args)
              elif type == "rotate":
                  mat = mat @ OptimizableSvg.TransformTools.parse_rotate(args)
              elif type == "scale":
                  mat = mat @ OptimizableSvg.TransformTools.parse_scale(args)
              elif type == "skewX":
                  mat = mat @ OptimizableSvg.TransformTools.parse_skewx(args)
              elif type == "skewY":
                  mat = mat @ OptimizableSvg.TransformTools.parse_skewy(args)
              else:
                  raise ValueError("Unknown transform type '{}'".format(type))
          return mat
  
      #dictionary that defines what constant do we need to multiply different units to get the value in pixels
      #gleaned from the CSS definition
      unit_dict = {"px":1,
                   "mm":4,
                   "cm":40,
                   "in":25.4*4,
                   "pt":25.4*4/72,
                   "pc":25.4*4/6
                   }
  
      @staticmethod
      def parseLength(s):
          #length is a number followed possibly by a unit definition
          #we assume that default unit is the pixel (px) equal to 0.25mm
          #last two characters might be unit
          val=None
          for i in range(len(s)):
              try:
                  val=float(s[:len(s)-i])
                  unit=s[len(s)-i:]
                  break
              except ValueError:
                  continue
          if len(unit)>0 and unit not in OptimizableSvg.unit_dict:
              raise ValueError("Unknown or unsupported unit '{}' encountered while parsing".format(unit))
          if unit != "":
              val*=OptimizableSvg.unit_dict[unit]
          return val
  
      @staticmethod
      def parseOpacity(s):
          is_percent=s.endswith("%")
          s=s.rstrip("%")
          val=float(s)
          if is_percent:
              val=val/100
          return np.clip(val,0.,1.)
  
      @staticmethod
      def parse_color(s):
          """
              Hex to tuple
          """
          if s[0] != '#':
              raise ValueError("Color argument `{}` not supported".format(s))
          s = s.lstrip('#')
          if len(s)==6:
              rgb = tuple(int(s[i:i + 2], 16) for i in (0, 2, 4))
              return torch.tensor([rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0])
          elif len(s)==3:
              rgb = tuple((int(s[i:i + 1], 16)) for i in (0, 1, 2))
              return torch.tensor([rgb[0] / 15.0, rgb[1] / 15.0, rgb[2] / 15.0])
          else:
              raise ValueError("Color argument `{}` not supported".format(s))
          # sRGB to RGB
          # return torch.pow(torch.tensor([rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0]), 2.2)
  
  
      @staticmethod
      def rgb_to_string(val):
          byte_rgb=(val.clone().detach()*255).type(torch.int)
          byte_rgb.clamp_(min=0,max=255)
          s="#{:02x}{:02x}{:02x}".format(*byte_rgb)
          return s
  
      #parses a "paint" string for use in fill and stroke definitions
      @staticmethod
      def parsePaint(paintStr,defs,device):
          paintStr=paintStr.strip()
          if paintStr=="none":
              return ("none", None)
          elif paintStr[0]=="#":
              return ("solid",OptimizableSvg.parse_color(paintStr).to(device))
          elif paintStr.startswith("url"):
              url=paintStr.lstrip("url(").rstrip(")").strip("\'\"").lstrip("#")
              if url not in defs:
                  raise ValueError("Paint-type attribute referencing an unknown object with ID '#{}'".format(url))
              return ("url",defs[url])
          else:
              raise ValueError("Unrecognized paint string: '{}'".format(paintStr))
  
      appearance_keys=["fill","fill-opacity","fill-rule","opacity","stroke","stroke-opacity","stroke-width"]
  
      @staticmethod
      def parseAppearance(node, defs, device):
          ret={}
          parse_keys = OptimizableSvg.appearance_keys
          local_dict={key:value for key,value in node.attrib.items() if key in parse_keys}
          css_dict={}
          style_dict={}
          appearance_dict={}
          if "class" in node.attrib:
              cls=node.attrib["class"]
              if "."+cls in defs:
                  css_string=defs["."+cls]
                  css_dict={item.split(":")[0]:item.split(":")[1] for item in css_string.split(";") if len(item)>0 and item.split(":")[0] in parse_keys}
          if "style" in node.attrib:
              style_string=node.attrib["style"]
              style_dict={item.split(":")[0]:item.split(":")[1] for item in style_string.split(";") if len(item)>0 and item.split(":")[0] in parse_keys}
          appearance_dict.update(css_dict)
          appearance_dict.update(style_dict)
          appearance_dict.update(local_dict)
          for key,value in appearance_dict.items():
              if key=="fill":
                  ret[key]=OptimizableSvg.parsePaint(value,defs,device)
              elif key == "fill-opacity":
                  ret[key]=torch.tensor(OptimizableSvg.parseOpacity(value),device=device)
              elif key == "fill-rule":
                  ret[key]=value
              elif key == "opacity":
                  ret[key]=torch.tensor(OptimizableSvg.parseOpacity(value),device=device)
              elif key == "stroke":
                  ret[key]=OptimizableSvg.parsePaint(value,defs,device)
              elif key == "stroke-opacity":
                  ret[key]=torch.tensor(OptimizableSvg.parseOpacity(value),device=device)
              elif key == "stroke-width":
                  ret[key]=torch.tensor(OptimizableSvg.parseLength(value),device=device)
              else:
                  raise ValueError("Error while parsing appearance attributes: key '{}' should not be here".format(key))
  
          return ret
  
      def parseRoot(self,root):
          if self.verbose:
              print(self.offset_str("Parsing root"))
          self.depth += 1
  
          # get document canvas dimensions
          self.parseViewport(root)
          canvmax=np.max(self.canvas)
          self.settings.global_override(["transforms","translation_mult"],canvmax)
          id=root.attrib["id"] if "id" in root.attrib else None
  
          transform=OptimizableSvg.parseTransform(root)
          appearance=OptimizableSvg.parseAppearance(root,self.defs,self.device)
  
          version=root.attrib["version"] if "version" in root.attrib else "<unknown version>"
          if version != "2.0":
              print(self.offset_str("Warning: Version {} is not 2.0, strange things may happen".format(version)))
  
          self.root=OptimizableSvg.RootNode(id,transform,appearance,self.settings)
  
          if self.verbose:
              self.reportSkippedAttribs(root, ["width", "height", "id", "transform","version", "style"]+OptimizableSvg.appearance_keys)
  
          #go through the root children and parse them appropriately
          skipped=[]
          for child in root:
              if OptimizableSvg.remove_namespace(child.tag) in OptimizableSvg.recognised_shapes:
                  self.parseShape(child,self.root)
              elif OptimizableSvg.remove_namespace(child.tag) == "defs":
                  self.parseDefs(child)
              elif OptimizableSvg.remove_namespace(child.tag) == "style":
                  self.parseStyle(child)
              elif OptimizableSvg.remove_namespace(child.tag) == "g":
                  self.parseGroup(child,self.root)
              else:
                  skipped.append(child)
  
          if self.verbose:
              self.reportSkippedChildren(root,skipped)
  
          self.depth-=1
  
      def parseShape(self,shape,parent):
          tag=OptimizableSvg.remove_namespace(shape.tag)
          if self.verbose:
              print(self.offset_str("Parsing {}#{}".format(tag,shape.attrib["id"] if "id" in shape.attrib else "<No ID>")))
  
          self.depth+=1
          if tag == "path":
              self.parsePath(shape,parent)
          elif tag == "circle":
              self.parseCircle(shape,parent)
          elif tag == "rect":
              self.parseRect(shape,parent)
          elif tag == "ellipse":
              self.parseEllipse(shape,parent)
          elif tag == "polygon":
              self.parsePolygon(shape,parent)
          else:
              raise ValueError("Encountered unknown shape type '{}'".format(tag))
          self.depth -= 1
  
      def parsePath(self,shape,parent):
          path_string=shape.attrib['d']
          name = ''
          if 'id' in shape.attrib:
              name = shape.attrib['id']
          paths = pydiffvg.from_svg_path(path_string)
          for idx, path in enumerate(paths):
              path.stroke_width = torch.tensor([0.],device=self.device)
              path.num_control_points=path.num_control_points.to(self.device)
              path.points=path.points.to(self.device)
              path.source_id = name
              path.id = "{}-{}".format(name,idx) if len(paths)>1 else name
          transform = OptimizableSvg.parseTransform(shape)
          appearance = OptimizableSvg.parseAppearance(shape,self.defs,self.device)
          node=OptimizableSvg.PathNode(name,transform,appearance,self.settings,paths)
          parent.children.append(node)
  
          if self.verbose:
              self.reportSkippedAttribs(shape, ["id","d","transform","style"]+OptimizableSvg.appearance_keys)
              self.reportSkippedChildren(shape,list(shape))
  
      def parseEllipse(self, shape, parent):
          cx = float(shape.attrib["cx"]) if "cx" in shape.attrib else 0.
          cy = float(shape.attrib["cy"]) if "cy" in shape.attrib else 0.
          rx = float(shape.attrib["rx"])
          ry = float(shape.attrib["ry"])
          name = ''
          if 'id' in shape.attrib:
              name = shape.attrib['id']
          transform = OptimizableSvg.parseTransform(shape)
          appearance = OptimizableSvg.parseAppearance(shape, self.defs, self.device)
          node = OptimizableSvg.EllipseNode(name, transform, appearance, self.settings, (cx, cy, rx, ry))
          parent.children.append(node)
  
          if self.verbose:
              self.reportSkippedAttribs(shape, ["id", "x", "y", "r", "transform",
                                                "style"] + OptimizableSvg.appearance_keys)
              self.reportSkippedChildren(shape, list(shape))
  
      def parsePolygon(self, shape, parent):
          points_string = shape.attrib['points']
          name = ''
          points=[]
          for point_string in points_string.split(" "):
              if len(point_string) == 0:
                  continue
              coord_strings=point_string.split(",")
              assert len(coord_strings)==2
              points.append([float(coord_strings[0]),float(coord_strings[1])])
          points=torch.tensor(points,dtype=torch.float,device=self.device)
          if 'id' in shape.attrib:
              name = shape.attrib['id']
          transform = OptimizableSvg.parseTransform(shape)
          appearance = OptimizableSvg.parseAppearance(shape, self.defs, self.device)
          node = OptimizableSvg.PolygonNode(name, transform, appearance, self.settings, points)
          parent.children.append(node)
  
          if self.verbose:
              self.reportSkippedAttribs(shape, ["id", "points", "transform", "style"] + OptimizableSvg.appearance_keys)
              self.reportSkippedChildren(shape, list(shape))
  
      def parseCircle(self,shape,parent):
          cx = float(shape.attrib["cx"]) if "cx" in shape.attrib else 0.
          cy = float(shape.attrib["cy"]) if "cy" in shape.attrib else 0.
          r = float(shape.attrib["r"])
          name = ''
          if 'id' in shape.attrib:
              name = shape.attrib['id']
          transform = OptimizableSvg.parseTransform(shape)
          appearance = OptimizableSvg.parseAppearance(shape, self.defs, self.device)
          node = OptimizableSvg.CircleNode(name, transform, appearance, self.settings, (cx, cy, r))
          parent.children.append(node)
  
          if self.verbose:
              self.reportSkippedAttribs(shape, ["id", "x", "y", "r", "transform",
                                                "style"] + OptimizableSvg.appearance_keys)
              self.reportSkippedChildren(shape, list(shape))
  
      def parseRect(self,shape,parent):
          x =      float(shape.attrib["x"]) if "x" in shape.attrib else 0.
          y =      float(shape.attrib["y"]) if "y" in shape.attrib else 0.
          width =  float(shape.attrib["width"])
          height = float(shape.attrib["height"])
          name = ''
          if 'id' in shape.attrib:
              name = shape.attrib['id']
          transform = OptimizableSvg.parseTransform(shape)
          appearance = OptimizableSvg.parseAppearance(shape, self.defs, self.device)
          node = OptimizableSvg.RectNode(name, transform, appearance, self.settings, (x,y,width,height))
          parent.children.append(node)
  
          if self.verbose:
              self.reportSkippedAttribs(shape, ["id", "x", "y", "width", "height", "transform", "style"] + OptimizableSvg.appearance_keys)
              self.reportSkippedChildren(shape, list(shape))
  
      def parseGroup(self,group,parent):
          tag = OptimizableSvg.remove_namespace(group.tag)
          id = group.attrib["id"] if "id" in group.attrib else "<No ID>"
          if self.verbose:
              print(self.offset_str("Parsing {}#{}".format(tag, id)))
  
          self.depth+=1
  
          transform=self.parseTransform(group)
  
          #todo process more attributes
          appearance=OptimizableSvg.parseAppearance(group,self.defs,self.device)
          node=OptimizableSvg.GroupNode(id,transform,appearance,self.settings)
          parent.children.append(node)
  
          if self.verbose:
              self.reportSkippedAttribs(group,["id","transform","style"]+OptimizableSvg.appearance_keys)
  
          skipped_children=[]
          for child in group:
              if OptimizableSvg.remove_namespace(child.tag) in OptimizableSvg.recognised_shapes:
                  self.parseShape(child,node)
              elif OptimizableSvg.remove_namespace(child.tag) == "defs":
                  self.parseDefs(child)
              elif OptimizableSvg.remove_namespace(child.tag) == "style":
                  self.parseStyle(child)
              elif OptimizableSvg.remove_namespace(child.tag) == "g":
                  self.parseGroup(child,node)
              else:
                  skipped_children.append(child)
  
          if self.verbose:
              self.reportSkippedChildren(group,skipped_children)
  
          self.depth-=1
  
      def parseStyle(self,style_node):
          tag = OptimizableSvg.remove_namespace(style_node.tag)
          id = style_node.attrib["id"] if "id" in style_node.attrib else "<No ID>"
          if self.verbose:
              print(self.offset_str("Parsing {}#{}".format(tag, id)))
  
          if style_node.attrib["type"] != "text/css":
              raise ValueError("Only text/css style recognized, got {}".format(style_node.attrib["type"]))
  
          self.depth += 1
  
          # creating only a dummy node
          node = OptimizableSvg.SvgNode(id, None, {}, self.settings)
  
          if self.verbose:
              self.reportSkippedAttribs(def_node, ["id"])
  
          if len(style_node)>0:
              raise ValueError("Style node should not have children (has {})".format(len(style_node)))
  
          # collect CSS classes
          sheet = cssutils.parseString(style_node.text)
          for rule in sheet:
              if hasattr(rule, 'selectorText') and hasattr(rule, 'style'):
                  name = rule.selectorText
                  if len(name) >= 2 and name[0] == '.':
                      self.defs[name] = rule.style.getCssText().replace("\n","")
                  else:
                      raise ValueError("Unrecognized CSS selector {}".format(name))
              else:
                  raise ValueError("No style or selector text in CSS rule")
  
          if self.verbose:
              self.reportSkippedChildren(def_node, skipped_children)
  
          self.depth -= 1
  
      def parseDefs(self,def_node):
          #only linear gradients are currently supported
          tag = OptimizableSvg.remove_namespace(def_node.tag)
          id = def_node.attrib["id"] if "id" in def_node.attrib else "<No ID>"
          if self.verbose:
              print(self.offset_str("Parsing {}#{}".format(tag, id)))
  
          self.depth += 1
  
  
          # creating only a dummy node
          node = OptimizableSvg.SvgNode(id, None, {},self.settings)
  
          if self.verbose:
              self.reportSkippedAttribs(def_node, ["id"])
  
          skipped_children = []
          for child in def_node:
              if OptimizableSvg.remove_namespace(child.tag) == "linearGradient":
                  self.parseGradient(child,node)
              elif OptimizableSvg.remove_namespace(child.tag) in OptimizableSvg.recognised_shapes:
                  raise NotImplementedError("Definition/instantiation of shapes not supported")
              elif OptimizableSvg.remove_namespace(child.tag) == "defs":
                  raise NotImplementedError("Definition within definition not supported")
              elif OptimizableSvg.remove_namespace(child.tag) == "g":
                  raise NotImplementedError("Groups within definition not supported")
              else:
                  skipped_children.append(child)
  
              if len(node.children)>0:
                  #take this node out and enter it into defs
                  self.defs[node.children[0].id]=node.children[0]
                  node.children.pop()
  
  
          if self.verbose:
              self.reportSkippedChildren(def_node, skipped_children)
  
          self.depth -= 1
  
      def parseGradientStop(self,stop):
          param_dict={key:value for key,value in stop.attrib.items() if key in ["id","offset","stop-color","stop-opacity"]}
          style_dict={}
          if "style" in stop.attrib:
              style_dict={item.split(":")[0]:item.split(":")[1] for item in stop.attrib["style"].split(";") if len(item)>0}
          param_dict.update(style_dict)
  
          offset=OptimizableSvg.parseOpacity(param_dict["offset"])
          color=OptimizableSvg.parse_color(param_dict["stop-color"])
          opacity=OptimizableSvg.parseOpacity(param_dict["stop-opacity"]) if "stop-opacity" in param_dict else 1.
  
          return offset, color, opacity
  
      def parseGradient(self, gradient_node, parent):
          tag = OptimizableSvg.remove_namespace(gradient_node.tag)
          id = gradient_node.attrib["id"] if "id" in gradient_node.attrib else "<No ID>"
          if self.verbose:
              print(self.offset_str("Parsing {}#{}".format(tag, id)))
  
          self.depth += 1
          if "stop" not in [OptimizableSvg.remove_namespace(child.tag) for child in gradient_node]\
              and "href" not in [OptimizableSvg.remove_namespace(key) for key in gradient_node.attrib.keys()]:
              raise ValueError("Gradient {} has neither stops nor a href link to them".format(id))
  
          transform=self.parseTransform(gradient_node)
          begin=None
          end = None
          offsets=[]
          stops=[]
          href=None
  
          if "x1" in gradient_node.attrib or "y1" in gradient_node.attrib:
              begin=np.array([0.,0.])
              if "x1" in gradient_node.attrib:
                  begin[0] = float(gradient_node.attrib["x1"])
              if "y1" in gradient_node.attrib:
                  begin[1] = float(gradient_node.attrib["y1"])
              begin = torch.tensor(begin.transpose(),dtype=torch.float32)
  
          if "x2" in gradient_node.attrib or "y2" in gradient_node.attrib:
              end=np.array([0.,0.])
              if "x2" in gradient_node.attrib:
                  end[0] = float(gradient_node.attrib["x2"])
              if "y2" in gradient_node.attrib:
                  end[1] = float(gradient_node.attrib["y2"])
              end=torch.tensor(end.transpose(),dtype=torch.float32)
  
          stop_nodes=[node for node in list(gradient_node) if OptimizableSvg.remove_namespace(node.tag)=="stop"]
          if len(stop_nodes)>0:
              stop_nodes=sorted(stop_nodes,key=lambda n: float(n.attrib["offset"]))
  
              for stop in stop_nodes:
                  offset, color, opacity = self.parseGradientStop(stop)
                  offsets.append(offset)
                  stops.append(np.concatenate((color,np.array([opacity]))))
  
          hkey=next((value for key,value in gradient_node.attrib.items() if OptimizableSvg.remove_namespace(key)=="href"),None)
          if hkey is not None:
              href=self.defs[hkey.lstrip("#")]
  
          parent.children.append(OptimizableSvg.GradientNode(id,transform,self.settings,begin.to(self.device) if begin is not None else begin,end.to(self.device) if end is not None else end,torch.tensor(offsets,dtype=torch.float32,device=self.device) if len(offsets)>0 else None,torch.tensor(np.array(stops),dtype=torch.float32,device=self.device) if len(stops)>0 else None,href))
  
          self.depth -= 1
  
      def parseViewport(self, root):
          if "width" in root.attrib and "height" in root.attrib:
              self.canvas = np.array([int(math.ceil(float(root.attrib["width"]))), int(math.ceil(float(root.attrib["height"])))])
          elif "viewBox" in root.attrib:
              s=root.attrib["viewBox"].split(" ")
              w=s[2]
              h=s[3]
              self.canvas = np.array(
                  [int(math.ceil(float(w))), int(math.ceil(float(h)))])
          else:
              raise ValueError("Size information is missing from document definition")
      #endregion
  
      #region writing
      def write_xml(self):
          tree=self.root.write_xml(self)
          
          return minidom.parseString(etree.tostring(tree, 'utf-8')).toprettyxml(indent="  ")
  
      def write_defs(self,root):
          if len(self.defs)==0:
              return
  
          defnode = etree.SubElement(root, 'defs')
          stylenode = etree.SubElement(root,'style')
          stylenode.set('type','text/css')
          stylenode.text=""
  
          defcpy=copy.copy(self.defs)
          while len(defcpy)>0:
              torem=[]
              for key,value in defcpy.items():
                  if issubclass(value.__class__,OptimizableSvg.SvgNode):
                      if value.href is None or value.href not in defcpy:
                          value.write_xml(defnode)
                          torem.append(key)
                      else:
                          continue
                  else:
                      #this is a string, and hence a CSS attribute
                      stylenode.text+=key+" {"+value+"}\n"
                      torem.append(key)
  
              for key in torem:
                  del defcpy[key]
      #endregion
  
  
  