| 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, |
| } |
|
|
| |
| 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) |
|
|
| |
| 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") |
| |
|
|
| 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() |
| |
| 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() |
| |
| 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() |
| |
| 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] |
| |
| 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] |
| |
| ShearX=r2[0, 1] / sc[0] |
|
|
| if np.sum(sc) < 0: |
| |
| 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) |
|
|
| |
|
|
| |
| 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.) |
|
|
| |
| 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. |
|
|
| |
| 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": |
| |
| 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": |
| |
| 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() |
|
|
| |
|
|
| |
| 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"]: |
| |
| 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) |
| |
| 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": |
| |
| outappearance[key]=value |
| elif key == "fill-opacity": |
| |
| outappearance[key] = outappearance[key]*value |
| elif key == "fill-rule": |
| |
| outappearance[key] = value |
| elif key =="opacity": |
| |
| outappearance[key] = outappearance[key]*value |
| elif key == "stroke": |
| |
| outappearance[key] = value |
| elif key == "stroke-opacity": |
| |
| outappearance[key] = outappearance[key]*value |
| elif key =="stroke-width": |
| |
| 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") |
| |
| document.write_defs(elm) |
|
|
| |
| 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": |
| |
| 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: |
| |
| path_string += "L " |
| elif type == 1: |
| |
| path_string += "Q " |
| elif type == 2: |
| |
| 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] |
| |
| 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] |
| |
| 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] |
| |
| 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] |
| |
| 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,:] |
| |
| 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: |
| |
| 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: |
| |
| 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())) |
|
|
| |
| 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) |
| |
|
|
| 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() |
|
|
| |
| 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"] |
|
|
| |
| 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): |
| |
| 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], |
| out_size[1], |
| 2, |
| 2, |
| seed, |
| None, |
| *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() |
| |
|
|
| |
|
|
| 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])))) |
|
|
| |
|
|
| |
| @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 |
|
|
| |
| |
| 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): |
| |
| |
| |
| 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)) |
| |
| |
|
|
|
|
| @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 |
|
|
| |
| @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 |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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))) |
|
|
| |
| 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): |
| |
| 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 |
|
|
|
|
| |
| 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: |
| |
| 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") |
| |
|
|
| |
| 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: |
| |
| stylenode.text+=key+" {"+value+"}\n" |
| torem.append(key) |
|
|
| for key in torem: |
| del defcpy[key] |
| |
|
|
|
|
|
|