1608 lines
68 KiB
Python
1608 lines
68 KiB
Python
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
|
|
|
|
|