Spaces:
Runtime error
Runtime error
import torch | |
import xml.etree.ElementTree as etree | |
import numpy as np | |
import diffvg | |
import os | |
import pydiffvg | |
import svgpathtools | |
import svgpathtools.parser | |
import re | |
import warnings | |
import cssutils | |
import logging | |
import matplotlib.colors | |
cssutils.log.setLevel(logging.ERROR) | |
def remove_namespaces(s): | |
""" | |
{...} ... -> ... | |
""" | |
return re.sub('{.*}', '', s) | |
def parse_style(s, defs): | |
style_dict = {} | |
for e in s.split(';'): | |
key_value = e.split(':') | |
if len(key_value) == 2: | |
key = key_value[0].strip() | |
value = key_value[1].strip() | |
if key == 'fill' or key == 'stroke': | |
# Special case: convert colors into tensor in definitions so | |
# that different shapes can share the same color | |
value = parse_color(value, defs) | |
style_dict[key] = value | |
return style_dict | |
def parse_hex(s): | |
""" | |
Hex to tuple | |
""" | |
s = s.lstrip('#') | |
if len(s) == 3: | |
s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] | |
rgb = tuple(int(s[i:i+2], 16) for i in (0, 2, 4)) | |
# sRGB to RGB | |
# return torch.pow(torch.tensor([rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0]), 2.2) | |
return torch.pow(torch.tensor([rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0]), 1.0) | |
def parse_int(s): | |
""" | |
trim alphabets | |
""" | |
return int(float(''.join(i for i in s if (not i.isalpha())))) | |
def parse_color(s, defs): | |
if s is None: | |
return None | |
if isinstance(s, torch.Tensor): | |
return s | |
s = s.lstrip(' ') | |
color = torch.tensor([0.0, 0.0, 0.0, 1.0]) | |
if s[0] == '#': | |
color[:3] = parse_hex(s) | |
elif s[:3] == 'url': | |
# url(#id) | |
color = defs[s[4:-1].lstrip('#')] | |
elif s == 'none': | |
color = None | |
elif s[:4] == 'rgb(': | |
rgb = s[4:-1].split(',') | |
color = torch.tensor([int(rgb[0]) / 255.0, int(rgb[1]) / 255.0, int(rgb[2]) / 255.0, 1.0]) | |
elif s == 'none': | |
return None | |
else: | |
try : | |
rgba = matplotlib.colors.to_rgba(s) | |
color = torch.tensor(rgba) | |
except ValueError : | |
warnings.warn('Unknown color command ' + s) | |
return color | |
# https://github.com/mathandy/svgpathtools/blob/7ebc56a831357379ff22216bec07e2c12e8c5bc6/svgpathtools/parser.py | |
def _parse_transform_substr(transform_substr): | |
type_str, value_str = transform_substr.split('(') | |
value_str = value_str.replace(',', ' ') | |
values = list(map(float, filter(None, value_str.split(' ')))) | |
transform = np.identity(3) | |
if 'matrix' in type_str: | |
transform[0:2, 0:3] = np.array([values[0:6:2], values[1:6:2]]) | |
elif 'translate' in transform_substr: | |
transform[0, 2] = values[0] | |
if len(values) > 1: | |
transform[1, 2] = values[1] | |
elif 'scale' in transform_substr: | |
x_scale = values[0] | |
y_scale = values[1] if (len(values) > 1) else x_scale | |
transform[0, 0] = x_scale | |
transform[1, 1] = y_scale | |
elif 'rotate' in transform_substr: | |
angle = values[0] * np.pi / 180.0 | |
if len(values) == 3: | |
offset = values[1:3] | |
else: | |
offset = (0, 0) | |
tf_offset = np.identity(3) | |
tf_offset[0:2, 2:3] = np.array([[offset[0]], [offset[1]]]) | |
tf_rotate = np.identity(3) | |
tf_rotate[0:2, 0:2] = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) | |
tf_offset_neg = np.identity(3) | |
tf_offset_neg[0:2, 2:3] = np.array([[-offset[0]], [-offset[1]]]) | |
transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg) | |
elif 'skewX' in transform_substr: | |
transform[0, 1] = np.tan(values[0] * np.pi / 180.0) | |
elif 'skewY' in transform_substr: | |
transform[1, 0] = np.tan(values[0] * np.pi / 180.0) | |
else: | |
# Return an identity matrix if the type of transform is unknown, and warn the user | |
warnings.warn('Unknown SVG transform type: {0}'.format(type_str)) | |
return transform | |
def parse_transform(transform_str): | |
""" | |
Converts a valid SVG transformation string into a 3x3 matrix. | |
If the string is empty or null, this returns a 3x3 identity matrix | |
""" | |
if not transform_str: | |
return np.identity(3) | |
elif not isinstance(transform_str, str): | |
raise TypeError('Must provide a string to parse') | |
total_transform = np.identity(3) | |
transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty | |
for substr in transform_substrs: | |
total_transform = total_transform.dot(_parse_transform_substr(substr)) | |
return torch.from_numpy(total_transform).type(torch.float32) | |
def parse_linear_gradient(node, transform, defs): | |
begin = torch.tensor([0.0, 0.0]) | |
end = torch.tensor([0.0, 0.0]) | |
offsets = [] | |
stop_colors = [] | |
# Inherit from parent | |
for key in node.attrib: | |
if remove_namespaces(key) == 'href': | |
value = node.attrib[key] | |
parent = defs[value.lstrip('#')] | |
begin = parent.begin | |
end = parent.end | |
offsets = parent.offsets | |
stop_colors = parent.stop_colors | |
for attrib in node.attrib: | |
attrib = remove_namespaces(attrib) | |
if attrib == 'x1': | |
begin[0] = float(node.attrib['x1']) | |
elif attrib == 'y1': | |
begin[1] = float(node.attrib['y1']) | |
elif attrib == 'x2': | |
end[0] = float(node.attrib['x2']) | |
elif attrib == 'y2': | |
end[1] = float(node.attrib['y2']) | |
elif attrib == 'gradientTransform': | |
transform = transform @ parse_transform(node.attrib['gradientTransform']) | |
begin = transform @ torch.cat((begin, torch.ones([1]))) | |
begin = begin / begin[2] | |
begin = begin[:2] | |
end = transform @ torch.cat((end, torch.ones([1]))) | |
end = end / end[2] | |
end = end[:2] | |
for child in node: | |
tag = remove_namespaces(child.tag) | |
if tag == 'stop': | |
offset = float(child.attrib['offset']) | |
color = [0.0, 0.0, 0.0, 1.0] | |
if 'stop-color' in child.attrib: | |
c = parse_color(child.attrib['stop-color'], defs) | |
color[:3] = [c[0], c[1], c[2]] | |
if 'stop-opacity' in child.attrib: | |
color[3] = float(child.attrib['stop-opacity']) | |
if 'style' in child.attrib: | |
style = parse_style(child.attrib['style'], defs) | |
if 'stop-color' in style: | |
c = parse_color(style['stop-color'], defs) | |
color[:3] = [c[0], c[1], c[2]] | |
if 'stop-opacity' in style: | |
color[3] = float(style['stop-opacity']) | |
offsets.append(offset) | |
stop_colors.append(color) | |
if isinstance(offsets, list): | |
offsets = torch.tensor(offsets) | |
if isinstance(stop_colors, list): | |
stop_colors = torch.tensor(stop_colors) | |
return pydiffvg.LinearGradient(begin, end, offsets, stop_colors) | |
def parse_radial_gradient(node, transform, defs): | |
begin = torch.tensor([0.0, 0.0]) | |
end = torch.tensor([0.0, 0.0]) | |
center = torch.tensor([0.0, 0.0]) | |
radius = torch.tensor([0.0, 0.0]) | |
offsets = [] | |
stop_colors = [] | |
# Inherit from parent | |
for key in node.attrib: | |
if remove_namespaces(key) == 'href': | |
value = node.attrib[key] | |
parent = defs[value.lstrip('#')] | |
begin = parent.begin | |
end = parent.end | |
offsets = parent.offsets | |
stop_colors = parent.stop_colors | |
for attrib in node.attrib: | |
attrib = remove_namespaces(attrib) | |
if attrib == 'cx': | |
center[0] = float(node.attrib['cx']) | |
elif attrib == 'cy': | |
center[1] = float(node.attrib['cy']) | |
elif attrib == 'fx': | |
radius[0] = float(node.attrib['fx']) | |
elif attrib == 'fy': | |
radius[1] = float(node.attrib['fy']) | |
elif attrib == 'fr': | |
radius[0] = float(node.attrib['fr']) | |
radius[1] = float(node.attrib['fr']) | |
elif attrib == 'gradientTransform': | |
transform = transform @ parse_transform(node.attrib['gradientTransform']) | |
# TODO: this is incorrect | |
center = transform @ torch.cat((center, torch.ones([1]))) | |
center = center / center[2] | |
center = center[:2] | |
for child in node: | |
tag = remove_namespaces(child.tag) | |
if tag == 'stop': | |
offset = float(child.attrib['offset']) | |
color = [0.0, 0.0, 0.0, 1.0] | |
if 'stop-color' in child.attrib: | |
c = parse_color(child.attrib['stop-color'], defs) | |
color[:3] = [c[0], c[1], c[2]] | |
if 'stop-opacity' in child.attrib: | |
color[3] = float(child.attrib['stop-opacity']) | |
if 'style' in child.attrib: | |
style = parse_style(child.attrib['style'], defs) | |
if 'stop-color' in style: | |
c = parse_color(style['stop-color'], defs) | |
color[:3] = [c[0], c[1], c[2]] | |
if 'stop-opacity' in style: | |
color[3] = float(style['stop-opacity']) | |
offsets.append(offset) | |
stop_colors.append(color) | |
if isinstance(offsets, list): | |
offsets = torch.tensor(offsets) | |
if isinstance(stop_colors, list): | |
stop_colors = torch.tensor(stop_colors) | |
return pydiffvg.RadialGradient(begin, end, offsets, stop_colors) | |
def parse_stylesheet(node, transform, defs): | |
# collect CSS classes | |
sheet = cssutils.parseString(node.text) | |
for rule in sheet: | |
if hasattr(rule, 'selectorText') and hasattr(rule, 'style'): | |
name = rule.selectorText | |
if len(name) >= 2 and name[0] == '.': | |
defs[name[1:]] = parse_style(rule.style.getCssText(), defs) | |
return defs | |
def parse_defs(node, transform, defs): | |
for child in node: | |
tag = remove_namespaces(child.tag) | |
if tag == 'linearGradient': | |
if 'id' in child.attrib: | |
defs[child.attrib['id']] = parse_linear_gradient(child, transform, defs) | |
elif tag == 'radialGradient': | |
if 'id' in child.attrib: | |
defs[child.attrib['id']] = parse_radial_gradient(child, transform, defs) | |
elif tag == 'style': | |
defs = parse_stylesheet(child, transform, defs) | |
return defs | |
def parse_common_attrib(node, transform, fill_color, defs): | |
attribs = {} | |
if 'class' in node.attrib: | |
attribs.update(defs[node.attrib['class']]) | |
attribs.update(node.attrib) | |
name = '' | |
if 'id' in node.attrib: | |
name = node.attrib['id'] | |
stroke_color = None | |
stroke_width = torch.tensor(0.5) | |
use_even_odd_rule = False | |
new_transform = transform | |
if 'transform' in attribs: | |
new_transform = transform @ parse_transform(attribs['transform']) | |
if 'fill' in attribs: | |
fill_color = parse_color(attribs['fill'], defs) | |
fill_opacity = 1.0 | |
if 'fill-opacity' in attribs: | |
fill_opacity *= float(attribs['fill-opacity']) | |
if 'opacity' in attribs: | |
fill_opacity *= float(attribs['opacity']) | |
# Ignore opacity if the color is a gradient | |
if isinstance(fill_color, torch.Tensor): | |
fill_color[3] = fill_opacity | |
if 'fill-rule' in attribs: | |
if attribs['fill-rule'] == "evenodd": | |
use_even_odd_rule = True | |
elif attribs['fill-rule'] == "nonzero": | |
use_even_odd_rule = False | |
else: | |
warnings.warn('Unknown fill-rule: {}'.format(attribs['fill-rule'])) | |
if 'stroke' in attribs: | |
stroke_color = parse_color(attribs['stroke'], defs) | |
if 'stroke-width' in attribs: | |
stroke_width = attribs['stroke-width'] | |
if stroke_width[-2:] == 'px': | |
stroke_width = stroke_width[:-2] | |
stroke_width = torch.tensor(float(stroke_width) / 2.0) | |
if 'style' in attribs: | |
style = parse_style(attribs['style'], defs) | |
if 'fill' in style: | |
fill_color = parse_color(style['fill'], defs) | |
fill_opacity = 1.0 | |
if 'fill-opacity' in style: | |
fill_opacity *= float(style['fill-opacity']) | |
if 'opacity' in style: | |
fill_opacity *= float(style['opacity']) | |
if 'fill-rule' in style: | |
if style['fill-rule'] == "evenodd": | |
use_even_odd_rule = True | |
elif style['fill-rule'] == "nonzero": | |
use_even_odd_rule = False | |
else: | |
warnings.warn('Unknown fill-rule: {}'.format(style['fill-rule'])) | |
# Ignore opacity if the color is a gradient | |
if isinstance(fill_color, torch.Tensor): | |
fill_color[3] = fill_opacity | |
if 'stroke' in style: | |
if style['stroke'] != 'none': | |
stroke_color = parse_color(style['stroke'], defs) | |
# Ignore opacity if the color is a gradient | |
if isinstance(stroke_color, torch.Tensor): | |
if 'stroke-opacity' in style: | |
stroke_color[3] = float(style['stroke-opacity']) | |
if 'opacity' in style: | |
stroke_color[3] *= float(style['opacity']) | |
if 'stroke-width' in style: | |
stroke_width = style['stroke-width'] | |
if stroke_width[-2:] == 'px': | |
stroke_width = stroke_width[:-2] | |
stroke_width = torch.tensor(float(stroke_width) / 2.0) | |
if isinstance(fill_color, pydiffvg.LinearGradient): | |
fill_color.begin = new_transform @ torch.cat((fill_color.begin, torch.ones([1]))) | |
fill_color.begin = fill_color.begin / fill_color.begin[2] | |
fill_color.begin = fill_color.begin[:2] | |
fill_color.end = new_transform @ torch.cat((fill_color.end, torch.ones([1]))) | |
fill_color.end = fill_color.end / fill_color.end[2] | |
fill_color.end = fill_color.end[:2] | |
if isinstance(stroke_color, pydiffvg.LinearGradient): | |
stroke_color.begin = new_transform @ torch.cat((stroke_color.begin, torch.ones([1]))) | |
stroke_color.begin = stroke_color.begin / stroke_color.begin[2] | |
stroke_color.begin = stroke_color.begin[:2] | |
stroke_color.end = new_transform @ torch.cat((stroke_color.end, torch.ones([1]))) | |
stroke_color.end = stroke_color.end / stroke_color.end[2] | |
stroke_color.end = stroke_color.end[:2] | |
if 'filter' in style: | |
print('*** WARNING ***: Ignoring filter for path with id "{}"'.format(name)) | |
return new_transform, fill_color, stroke_color, stroke_width, use_even_odd_rule | |
def is_shape(tag): | |
return tag == 'path' or tag == 'polygon' or tag == 'line' or tag == 'circle' or tag == 'rect' | |
def parse_shape(node, transform, fill_color, shapes, shape_groups, defs): | |
tag = remove_namespaces(node.tag) | |
new_transform, new_fill_color, stroke_color, stroke_width, use_even_odd_rule = \ | |
parse_common_attrib(node, transform, fill_color, defs) | |
if tag == 'path': | |
d = node.attrib['d'] | |
name = '' | |
if 'id' in node.attrib: | |
name = node.attrib['id'] | |
force_closing = new_fill_color is not None | |
paths = pydiffvg.from_svg_path(d, new_transform, force_closing) | |
for idx, path in enumerate(paths): | |
assert(path.points.shape[1] == 2) | |
path.stroke_width = stroke_width | |
path.source_id = name | |
path.id = "{}-{}".format(name,idx) if len(paths)>1 else name | |
prev_shapes_size = len(shapes) | |
shapes = shapes + paths | |
shape_ids = torch.tensor(list(range(prev_shapes_size, len(shapes)))) | |
shape_groups.append(pydiffvg.ShapeGroup(\ | |
shape_ids = shape_ids, | |
fill_color = new_fill_color, | |
stroke_color = stroke_color, | |
use_even_odd_rule = use_even_odd_rule, | |
id = name)) | |
elif tag == 'polygon': | |
name = '' | |
if 'id' in node.attrib: | |
name = node.attrib['id'] | |
force_closing = new_fill_color is not None | |
pts = node.attrib['points'].strip() | |
pts = pts.split(' ') | |
# import ipdb; ipdb.set_trace() | |
pts = [[float(y) for y in re.split(',| ', x)] for x in pts if x] | |
pts = torch.tensor(pts, dtype=torch.float32).view(-1, 2) | |
polygon = pydiffvg.Polygon(pts, force_closing) | |
polygon.stroke_width = stroke_width | |
shape_ids = torch.tensor([len(shapes)]) | |
shapes.append(polygon) | |
shape_groups.append(pydiffvg.ShapeGroup(\ | |
shape_ids = shape_ids, | |
fill_color = new_fill_color, | |
stroke_color = stroke_color, | |
use_even_odd_rule = use_even_odd_rule, | |
shape_to_canvas = new_transform, | |
id = name)) | |
elif tag == 'line': | |
x1 = float(node.attrib['x1']) | |
y1 = float(node.attrib['y1']) | |
x2 = float(node.attrib['x2']) | |
y2 = float(node.attrib['y2']) | |
p1 = torch.tensor([x1, y1]) | |
p2 = torch.tensor([x2, y2]) | |
points = torch.stack((p1, p2)) | |
line = pydiffvg.Polygon(points, False) | |
line.stroke_width = stroke_width | |
shape_ids = torch.tensor([len(shapes)]) | |
shapes.append(line) | |
shape_groups.append(pydiffvg.ShapeGroup(\ | |
shape_ids = shape_ids, | |
fill_color = new_fill_color, | |
stroke_color = stroke_color, | |
use_even_odd_rule = use_even_odd_rule, | |
shape_to_canvas = new_transform)) | |
elif tag == 'circle': | |
radius = float(node.attrib['r']) | |
cx = float(node.attrib['cx']) | |
cy = float(node.attrib['cy']) | |
name = '' | |
if 'id' in node.attrib: | |
name = node.attrib['id'] | |
center = torch.tensor([cx, cy]) | |
circle = pydiffvg.Circle(radius = torch.tensor(radius), | |
center = center) | |
circle.stroke_width = stroke_width | |
shape_ids = torch.tensor([len(shapes)]) | |
shapes.append(circle) | |
shape_groups.append(pydiffvg.ShapeGroup(\ | |
shape_ids = shape_ids, | |
fill_color = new_fill_color, | |
stroke_color = stroke_color, | |
use_even_odd_rule = use_even_odd_rule, | |
shape_to_canvas = new_transform)) | |
elif tag == 'ellipse': | |
rx = float(node.attrib['rx']) | |
ry = float(node.attrib['ry']) | |
cx = float(node.attrib['cx']) | |
cy = float(node.attrib['cy']) | |
name = '' | |
if 'id' in node.attrib: | |
name = node.attrib['id'] | |
center = torch.tensor([cx, cy]) | |
circle = pydiffvg.Circle(radius = torch.tensor(radius), | |
center = center) | |
circle.stroke_width = stroke_width | |
shape_ids = torch.tensor([len(shapes)]) | |
shapes.append(circle) | |
shape_groups.append(pydiffvg.ShapeGroup(\ | |
shape_ids = shape_ids, | |
fill_color = new_fill_color, | |
stroke_color = stroke_color, | |
use_even_odd_rule = use_even_odd_rule, | |
shape_to_canvas = new_transform)) | |
elif tag == 'rect': | |
x = 0.0 | |
y = 0.0 | |
if x in node.attrib: | |
x = float(node.attrib['x']) | |
if y in node.attrib: | |
y = float(node.attrib['y']) | |
w = float(node.attrib['width']) | |
h = float(node.attrib['height']) | |
p_min = torch.tensor([x, y]) | |
p_max = torch.tensor([x + w, x + h]) | |
rect = pydiffvg.Rect(p_min = p_min, p_max = p_max) | |
rect.stroke_width = stroke_width | |
shape_ids = torch.tensor([len(shapes)]) | |
shapes.append(rect) | |
shape_groups.append(pydiffvg.ShapeGroup(\ | |
shape_ids = shape_ids, | |
fill_color = new_fill_color, | |
stroke_color = stroke_color, | |
use_even_odd_rule = use_even_odd_rule, | |
shape_to_canvas = new_transform)) | |
return shapes, shape_groups | |
def parse_group(node, transform, fill_color, shapes, shape_groups, defs): | |
if 'transform' in node.attrib: | |
transform = transform @ parse_transform(node.attrib['transform']) | |
if 'fill' in node.attrib: | |
fill_color = parse_color(node.attrib['fill'], defs) | |
for child in node: | |
tag = remove_namespaces(child.tag) | |
if is_shape(tag): | |
shapes, shape_groups = parse_shape(\ | |
child, transform, fill_color, shapes, shape_groups, defs) | |
elif tag == 'g': | |
shapes, shape_groups = parse_group(\ | |
child, transform, fill_color, shapes, shape_groups, defs) | |
return shapes, shape_groups | |
def parse_scene(node): | |
canvas_width = -1 | |
canvas_height = -1 | |
defs = {} | |
shapes = [] | |
shape_groups = [] | |
fill_color = torch.tensor([0.0, 0.0, 0.0, 1.0]) | |
transform = torch.eye(3) | |
if 'viewBox' in node.attrib: | |
view_box_array = node.attrib['viewBox'].split() | |
canvas_width = parse_int(view_box_array[2]) | |
canvas_height = parse_int(view_box_array[3]) | |
else: | |
if 'width' in node.attrib: | |
canvas_width = parse_int(node.attrib['width']) | |
else: | |
print('Warning: Can\'t find canvas width.') | |
if 'height' in node.attrib: | |
canvas_height = parse_int(node.attrib['height']) | |
else: | |
print('Warning: Can\'t find canvas height.') | |
for child in node: | |
tag = remove_namespaces(child.tag) | |
if tag == 'defs': | |
defs = parse_defs(child, transform, defs) | |
elif tag == 'style': | |
defs = parse_stylesheet(child, transform, defs) | |
elif tag == 'linearGradient': | |
if 'id' in child.attrib: | |
defs[child.attrib['id']] = parse_linear_gradient(child, transform, defs) | |
elif tag == 'radialGradient': | |
if 'id' in child.attrib: | |
defs[child.attrib['id']] = parse_radial_gradient(child, transform, defs) | |
elif is_shape(tag): | |
shapes, shape_groups = parse_shape(\ | |
child, transform, fill_color, shapes, shape_groups, defs) | |
elif tag == 'g': | |
shapes, shape_groups = parse_group(\ | |
child, transform, fill_color, shapes, shape_groups, defs) | |
return canvas_width, canvas_height, shapes, shape_groups | |
def svg_to_scene(filename): | |
""" | |
Load from a SVG file and convert to PyTorch tensors. | |
""" | |
tree = etree.parse(filename) | |
root = tree.getroot() | |
cwd = os.getcwd() | |
if (os.path.dirname(filename) != ''): | |
os.chdir(os.path.dirname(filename)) | |
ret = parse_scene(root) | |
os.chdir(cwd) | |
return ret | |