-
Notifications
You must be signed in to change notification settings - Fork 0
/
svg_parser.py
474 lines (359 loc) · 11.8 KB
/
svg_parser.py
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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
#!/usr/bin/env python3
# svg_parser.py
# A python program to generate points from and svg file
import bs4 as bs
from sys import argv
def get_d(filename):
"""
Gets the contents of the attribute d of the first path
it finds in the given svg file
"""
source = open(filename, "r")
soup = bs.BeautifulSoup(source, "xml")
# This doesnt have support for multiple d or paths
paths = soup.findAll("path")
ctrl_points = []
for path in paths:
ctrl_points.append(path.get("d"))
return ctrl_points
def list_d(ls):
"""
Turns d into a list
"""
# Processed items will go in here
new_ls = []
# Support for when the coordinates are divided by commas
# Z's are useless! (for our purpose)
ls = ls.replace(",", " ").replace("z", "").replace("Z", "")
items = list(ls)
# "M0" -> "M 0"
spaced_items = []
# Add a space after each letter
for item in items:
spaced_items.append(item)
if item.isalpha():
spaced_items.append(" ")
# Join and then split again to remove duplicate spaces
items = "".join(spaced_items).split()
for item in items:
# Leave string items as they are
if item.isalpha():
new_ls.append(item)
# Convert nums to actual nums
else:
coordinate = float(item.strip())
new_ls.append(coordinate)
return new_ls
def tuplify_d(ls):
"""
Convert a list given by sep_commands into using coordinates
in tuples instead of standalone
"""
# horiz = lambda x: ['l', [(x, 0)]]
# verti = lambda y: ['l', [(0, y)]]
new_ls = []
for item in ls:
new_item = [item[0]]
# There is probably a better way to do this but my brain power
# has gone into the imaginary plane. (It's not real)
if new_item[0].lower() == "h":
for pos in range(1, len(item)):
coord = (item[pos], 0)
new_item.append(coord)
elif new_item[0].lower() == "v":
for pos in range(1, len(item)):
coord = (0, item[pos])
new_item.append(coord)
else:
# Horizontal and vertical need special handling
# Go 2 by 2 to add the coords together
for pos in range(1, len(item), 2):
# Convert to tuple
coord = item[pos], item[pos + 1]
new_item.append(coord)
new_ls.append(new_item)
return new_ls
def sep_commands(ls):
"""
Creates 2D lists for each svg command
"""
new_ls = []
last = 0
i = 0
for i in range(len(ls)):
# When it encounters a string, it will add the previous
# section to the new list
if isinstance(ls[i], str):
to_append = ls[last:i]
# Only add if the list is not empty
if to_append:
new_ls.append(to_append)
last = i
# The last to_append doesnt usually get added at the end of the loop
# unless the last item is a char
# In case it does, the if should stop repeats hopefully
if ls[last:i] != new_ls[-1]:
new_ls.append(ls[last : i + 1])
return new_ls
def add_xy(coord1, coord2):
"""
Add two coordinates together
"""
return coord1[0] + coord2[0], coord1[1] + coord2[1]
def relative_to_absolute(ls):
"""
Taking a list generated from sep_commands(), convert
the values to absolute ones. These include the current point
as index 0
"""
# ls will look like
# [["m", [(0, 0)]], ["c", [(1, 1), (2, 2), (3, 3), (4, 4)]]
# ls[0][0] = "m"
# ls[0][1] = [(0, 0)]
# ls[0][1][0] = (0, 0)
new_ls = []
#
# # Set the current position to the one it was moved to
# if ls[0][0].lower() == "m":
# current = ls[0][1][0]
#
# # After getting this value, the move part is useless
# del ls[0]
current = (0, 0)
for i in range(0, len(ls)):
# M/m requires special handling
# If the move is absolute
if ls[i][0] == "M":
# Set coords for the point it was moved to
current = ls[i][1][0]
# Relative
elif ls[i][0] == "m":
current = add_xy(current, ls[i][1][0])
# Hh/Vv also requires special handling
# It will convert the command to a Lineto instead
elif ls[i][0].lower() in ["h", "v"]:
new_coords = [current]
# Horizontals (x, 0)
if ls[i][0] == "H":
# Yes, a 4D list. I couldnt think of anything else
# its accessing the x value of the horizontal line
# Replace the y value for the current one
coord = (ls[i][1][0][0], current[1])
elif ls[i][0] == "V":
# Replace x for current one
coord = (current[0], ls[i][1][0][1])
else:
# Replace the 0 value AND make specified absolute
coord = add_xy(ls[i][1][0], current)
# Verticals (0, y)
new_coords.append(coord)
new_ls.append(["l", new_coords])
# Set current to latest coordinate
current = new_ls[-1][1][-1]
else:
# Set the first value to
new_coords = [current]
# lower case are relatives. Upper are absolute
if ls[i][0].islower():
# Add the current value to each coordinate
for coord in ls[i][1]:
new_coords.append(add_xy(current, coord))
else:
# Item is already absolute, no changes needed
new_coords += ls[i][1]
# Add the new values, including the command char
# Command char's case doesnt matter now so it is turned to
# lower for ease of use
new_ls.append([ls[i][0].lower(), new_coords])
# Set current to latest coordinate
current = new_ls[-1][1][-1]
return new_ls
def separate_points(sep_ls):
"""
Taking a list generated from sep_commands(), it'll convert
the commands to separate continuous ones
"""
# NOTE: This can be used for h and v support
# Shortcuts
# horiz = lambda x: ['l', [(x, 0)]]
# verti = lambda y: ['l', [(0, y)]]
# The minimum amount of attributes each has
# coms is short for commands
coms = {
"m": 1, # Move
"l": 1, # Line to
"h": 1, # Horizontal
"v": 1, # Vertical
"c": 3, # Cubic bezier
"s": 2, # Smooth cubic bezier
"q": 2, # Quadratic bezier
"t": 1, # Smooth quadratic bezier
}
new_ls = []
for item in sep_ls:
# Z doesnt really matter!
if item[0].lower() == "z":
continue
com = item[0].lower()
# Start is one to avoid the command character
start = 1
# Get sections of length coms[com]
for _ in range(0, (len(item) - 1) // coms[com]):
to_append = item[start : start + coms[com]]
new_ls.append([item[0], to_append])
start += coms[com]
return new_ls
def implicit_lineto(ls):
"""
https://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
Convert extra sets of coordinates in moveto commands to
lineto commands
Multiple coords in line to commands are treated as implicit
lineto (l / L) commands, the usual upper and lower case
nonsense applies. m -> l, M -> L
"""
# The first item will be replaced by the new items
item = ls.pop(0)
new_items = []
# Get the inital one
new_items.append(item[:2])
# -- Add another item if there is more than one set of coords in
# the move commands
# If [0][0] is m and the len of [0] > 2,
# [m, (1,2), (4,5), (7,8)] -> [m, (1, 2)], [l, (4,5), (7,8)]
if item[0].lower() == "m" and len(item) > 2:
# Store the extra coordinates in a temporary var
temp_item = item[2:]
# Put appropiate line to command at the start
temp_item.insert(0, "L" if item[0].isupper() else "l")
new_items.append(temp_item)
# Add back the first items
new_ls = new_items + ls
return new_ls
# -- Equations -- #
def line(arg_points, t):
"""
Returns the point according to t
"""
# Wanted a useful docstring? Too bad!
coords = []
p0, p1 = arg_points
# for x and then y
# What's programming without a little mischief?
for i in [0, 1]:
# https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Linear_B%C3%A9zier_curves
num = p0[i] + t * (p1[i] - p0[i])
coords.append(num)
return tuple(coords)
def cubic_bezier(arg_points, t):
"""
Returns the point according to t
"""
coords = []
p0, p1, p2, p3 = arg_points
# for x and then y
for i in [0, 1]:
# Equation from
# https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
# TODO: Change these to pow()
num = (
(((1 - t) ** 3) * p0[i])
+ (3 * t * ((1 - t) ** 2) * p1[i])
+ (3 * (t**2) * (1 - t) * p2[i])
+ ((t**3) * p3[i])
)
coords.append(num)
return tuple(coords)
def quadratic_bezier(arg_points, t):
"""
Returns the point according to t
"""
coords = []
p0, p1, p2 = arg_points
# For x and then y
for i in [0, 1]:
# https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
num = (
(pow(1 - t, 2) * p0[i])
+ (2 * (1 - t) * t * p1[i])
+ (pow(t, 2) * p2[i])
)
coords.append(num)
return tuple(coords)
# --
def not_supported(*args):
"""
Sorry!!
"""
raise Exception(
"Sorry!! There is not support for this "
f"svg command yet!! ({args[0]}) Please create an "
"issue on github."
)
def main(filename, resolution):
# Get the content of d
all_d_points = get_d(filename)
abs_points = []
for d_markers in all_d_points:
# Hehe
d_list = list_d(d_markers)
# Group per command type
separated_list = sep_commands(d_list)
# Convert coordinates to tuples
tupled_list = tuplify_d(separated_list)
# Change multiple moveto coords to linetos
# See docstring for more info
processed_list = implicit_lineto(tupled_list)
# Convert shorthand commands to full ones
command_list = separate_points(processed_list)
# Convert relative values to absolute ones
absolute_list = relative_to_absolute(command_list)
abs_points.append(absolute_list)
all_points = []
for control_points in abs_points:
for control_point in control_points:
all_points.append(control_point)
# im tired
# If i want to support s or t, id have to make a function to
# convert them to c/q
# https://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands
# What equation to use for each
equations = {
# Line
"l": line,
"h": not_supported,
"v": not_supported,
# Cubic bezier
"c": cubic_bezier,
"s": not_supported,
# Quadratic bezier
"q": quadratic_bezier,
"t": not_supported,
}
# Start calculating values
points = []
increment = pow(resolution, -1)
for command in all_points:
# Get relevant equation
eq, args = command
equation = equations[eq]
# Calculate values
t = 0
while 0 <= t <= 1:
points.append(equation(args, t))
t += increment
return points
if __name__ == "__main__":
# A default value
res = 20
valid = True
try:
res = float(argv[2])
except IndexError:
valid = False
except ValueError:
valid = False
finally:
if not valid:
print("Using", res, "as resolution.")
print(main(argv[1], res))