-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathandroidvector.py
More file actions
309 lines (250 loc) · 11.2 KB
/
androidvector.py
File metadata and controls
309 lines (250 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Exports the drawing as an XML appropriate for use as an Android VectorDrawable.
Copyright (C) 2017 Owen Tosh <owen@owentosh.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import sys
import lxml.etree as et
import inkex
import cubicsuperpath as csp
import simpletransform as st
import simplestyle as ss
inkex.localize()
def _ns(tag):
"""Add the Android namespace to a tag or attribute.
Required arguments:
tag -- string, tag or attribute
"""
return '{%s}%s' % ('http://schemas.android.com/apk/res/android', tag)
class AndroidVector(inkex.Effect):
"""Main effect class."""
def effect(self):
"""Convert the SVG to an Android Vector XML object."""
# get inkscape root element
svg = self.document.getroot()
# initialize android root element
vector = et.Element('vector')
# handle root attributes
vector.set(_ns('name'), svg.get('id', 'svgimage'))
width = svg.get('width')
height = svg.get('height')
if width is None:
inkex.errormsg(_('The document width attribute is missing.'))
return
if height is None:
inkex.errormsg(_('The document height attribute is missing.'))
return
# width and height are the only attributes using non-user units in the vector tag
vector.set(_ns('width'), str(self.uutounit(self.__unittouu(width), 'px')) + 'dp')
vector.set(_ns('height'), str(self.uutounit(self.__unittouu(height), 'px')) + 'dp')
view_box = svg.get('viewBox')
if view_box is None:
inkex.errormsg(_('The document viewBox attribute is missing.'))
return
view_box_split = view_box.split()
if len(view_box_split) != 4:
inkex.errormsg(_('The document viewBox attribute is not formatted correctly.'))
return
# determine scale factor
view_width = float(view_box_split[2])
view_height = float(view_box_split[3])
scale_fact = 1000.0/max(view_width, view_height)
# set scale (remove need for scientific notation)
svg.set('transform', 'scale(%f %f)' % (scale_fact, scale_fact))
vector.set(_ns('viewportWidth'), str(view_width * scale_fact))
vector.set(_ns('viewportHeight'), str(view_height * scale_fact))
# parse child elements
self.unique_id = 0
ancestors = [svg]
for el in svg:
tag = self._get_tag_name(el)
# ignore root's incompatible children
if tag == 'g' or tag == 'path':
subel = self._parse_child(el, ancestors)
if subel is not None:
vector.append(subel)
# save element tree
self.etree = et.ElementTree(vector)
def output(self):
"""Write element tree to file."""
self.etree.write(sys.stdout, pretty_print=True)
def __unittouu(self, param):
"""Wrap inkex.unittouu for compatibility.
Required arguments:
param -- string, to parse
"""
try:
return inkex.unittouu(param)
except AttributeError:
return self.unittouu(param)
def _get_tag_name(self, node):
"""Strip namespace from tag."""
if '}' in node.tag:
return node.tag.split('}', 1)[1]
else:
return node.tag
def _parse_child(self, src, ancestors):
"""Recursively parse through child elements.
Return an element (including all decendants).
Required arguments:
src -- Element, child element to parse
ancestors -- list of Element, list of parent elements up to root
"""
# check for compatible tag
tag = self._get_tag_name(src)
if tag == 'g':
el_tag = 'group'
elif tag == 'path':
el_tag = 'path'
else:
return None
# initialize android element
el = et.Element(el_tag)
# set name attribute
name = src.get('id')
if name is None:
name = el_tag + str(self.unique_id)
self.unique_id += 1
el.set(_ns('name'), name)
if tag == 'g':
# parse child elements
ancestors.append(src)
for child in src:
subel = self._parse_child(child, ancestors)
if subel is not None:
el.append(subel)
ancestors.pop()
else:
# path element
# get path data
d = src.get('d')
if d is None:
return None
# apply all transforms (including all ancestors - i.e. "flatten")
p = csp.parsePath(d)
ancestors.append(src)
for anc in reversed(ancestors):
if 'transform' in anc.attrib:
transform = anc.get('transform')
t = st.parseTransform(transform)
st.applyTransformToPath(t, p)
ancestors.pop()
# remove very small numbers (i.e. scientific notation)
for i in range(len(p)):
for j in range(len(p[i])):
for k in range(len(p[i][j])):
for l in range(len(p[i][j][k])):
if 'e-' in str(p[i][j][k][l]).lower():
p[i][j][k][l] = 0.0;
# save path data in vector element
el.set(_ns('pathData'), csp.formatPath(p))
# parse styles
if 'style' not in src.attrib:
# set some basic defaults
el.set(_ns('strokeColor'), '#000000')
el.set(_ns('strokeWidth'), '1')
el.set(_ns('fillColor'), '#FFFFFF')
else:
style = ss.parseStyle(src.get('style'))
# overall object opacity
# - not supported in android - merged with other opacities later
opacity = 1.0
if 'opacity' in style:
opacity = float(style['opacity'])
# fill styles
if 'fill' in style:
color = self._get_color(style['fill'])
if color is not None:
el.set(_ns('fillColor'), color)
if 'fill-opacity' in style or 'opacity' in style:
alpha = opacity
if 'fill-opacity' in style:
alpha *= float(style['fill-opacity'])
el.set(_ns('fillAlpha'), str(alpha))
if 'fill-rule' in style:
if style['fill-rule'] == 'evenodd':
el.set(_ns('fillType'), 'evenOdd')
elif style['fill-rule'] == 'nonzero':
el.set(_ns('fillType'), 'nonZero')
# stroke styles
if 'stroke' in style:
color = self._get_color(style['stroke'])
if color is not None:
el.set(_ns('strokeColor'), color)
if 'stroke-width' in style:
el.set(_ns('strokeWidth'), str(self.__unittouu(style['stroke-width'])))
if 'stroke-opacity' in style or 'opacity' in style:
alpha = opacity
if 'stroke-opacity' in style:
alpha *= float(style['stroke-opacity'])
el.set(_ns('strokeAlpha'), str(alpha))
if 'stroke-linecap' in style:
el.set(_ns('strokeLineCap'), style['stroke-linecap'])
if 'stroke-linejoin' in style:
el.set(_ns('strokeLineJoin'), style['stroke-linejoin'])
if 'stroke-miterlimit' in style:
el.set(_ns('strokeMiterLimit'), style['stroke-miterlimit'])
return el
def _get_color(self, color):
"""Retrieve and parse an inkscape color for use in android.
Return a string "#RRGGBB", or None if not understood. If inkscape color
is a gradient, attempt to use one of the gradient colors.
Required arguments:
color -- string, inkscape color (from "style" attribute)
"""
if color.startswith('#'):
# already in the correct format
return color
elif color == 'none':
# no color defined, transparent - we'll rely on the opacity attributes instead
return None
elif color.startswith('url(#') and color.endswith(')'):
# parse ID to check for a gradient
id = color[5:-1]
if id.startswith('linearGradient') or id.startswith('radialGradient'):
doc = self.document
# find defined gradient
prefix_terms = []
prefix_terms.append(inkex.addNS('defs', 'svg'))
prefix_terms.append('/')
prefix_terms.append('*[@id=\'')
prefix = ''.join(prefix_terms)
suffix = '\']'
gradient = doc.find(prefix + id + suffix)
# get link to the other gradient definition (with the colors defined)
link_attr = inkex.addNS('href', 'xlink')
if gradient is None or link_attr not in gradient.attrib:
return None
link_id = gradient.get(link_attr)
if link_id.startswith('#'):
link_id = link_id[1:]
# get stop element
suffix_terms = []
suffix_terms.append('\']/')
suffix_terms.append(inkex.addNS('stop', 'svg'))
suffix = ''.join(suffix_terms)
stop = doc.find(prefix + link_id + suffix)
# parse style attribute of stop element
if stop is None or 'style' not in stop.attrib:
return None
style = ss.parseStyle(stop.get('style'))
if 'stop-color' not in style:
return None
# recursively attempt to parse the found color
return self._get_color(style['stop-color'])
# color wasn't understood
return None
if __name__ == "__main__":
vector = AndroidVector()
vector.affect()