-
Notifications
You must be signed in to change notification settings - Fork 53
/
generate-24px-versions.py
executable file
·214 lines (173 loc) · 7.97 KB
/
generate-24px-versions.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
#!/usr/bin/env python3
"""
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
SPDX-FileCopyrightText: 2020 Niccolò Venerandi <niccolo@venerandi.com>
"""
import os
import re
import sys
from pathlib import Path
from lxml import etree
"""
This script generates 24px icons based on 22px icons
"""
# The BEGIN/END stuff is a Kate/KDevelop feature. Please don't remove it unless you have a good reason.
# BEGIN globals
# These are needed to prevent nonsense namespaces like ns0 from being
# added to otherwise perfectly fine svg elements and attributes
NAMESPACES = {
"svg": "http://www.w3.org/2000/svg",
"xlink": "http://www.w3.org/1999/xlink",
"inkscape": "http://www.inkscape.org/namespaces/inkscape",
"dc": "http://purl.org/dc/elements/1.1/",
"cc": "http://creativecommons.org/ns#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
}
for prefix, uri in NAMESPACES.items():
etree.register_namespace(prefix, uri)
# END globals
# BEGIN defs
def strip_split(s: str):
"""
Strip whitespace from the start and end, then split into a list of strings.
re.split() RegEx: match comma with [0,inf) whitespace characters after it OR [1,inf) whitespace characters.
"""
# Ignore warnings about invalid escape sequences, this works fine.
return re.split(r",\s*|\s+", s.strip())
def get_renderable_elements(root: etree.Element):
"""
Get renderable elements that are children of the root of the SVG.
See the Renderable Elements section of the SVG documentation on MDN web docs:
https://developer.mozilla.org/en-US/docs/Web/SVG/Element#SVG_elements_by_category
"""
return root.xpath(
"./svg:a | ./svg:circle | ./svg:ellipse | ./svg:foreignObject | ./svg:g "
+ "| ./svg:image | ./svg:line | ./svg:mesh | ./svg:path | ./svg:polygon "
+ "| ./svg:polyline | ./svg:rect | ./svg:switch | ./svg:svg | ./svg:symbol "
+ "| ./svg:text | ./svg:textPath | ./svg:tspan | ./svg:unknown | ./svg:use",
namespaces=NAMESPACES
)
def make_dir(input_dir, output_dir, path):
if not path.endswith('/22'):
return
folder24_destination = path.replace(input_dir, output_dir, 1).replace('/22', '/24')
# Make 24/
Path(folder24_destination).mkdir(parents=True, exist_ok=True)
# Make 24@2x/ and 24@3x/
for scale in (2, 3):
folder24_scaled_destination = folder24_destination.replace('/24', f'/24@{scale}x')
if os.path.islink(folder24_scaled_destination):
os.remove(folder24_scaled_destination)
os.symlink("24", folder24_scaled_destination, target_is_directory=True)
def make_file(input_dir, output_dir, path):
# only handle files
if os.path.islink(path):
return
# Filter out files
if not (path.endswith('.svg') and '/22/' in path):
return
file_destination = path.replace(input_dir, output_dir, 1).replace('/22/', '/24/')
# edit SVGs
etree.set_default_parser(etree.XMLParser(remove_blank_text=True))
tree = etree.parse(path)
root = tree.getroot()
viewBox_is_none = root.get('viewBox') is None
width_is_none = root.get('width') is None
height_is_none = root.get('height') is None
"""
NOTE:
- Using strip and split because the amount of whitespace and usage of commas can vary.
- Checking against real values because string values can have leading zeros.
- Replacing "px" with nothing so that values can be converted to real numbers and because px is the default unit type
- If another unit type is used in the <svg> element, this script will fail, but icons shouldn't use other unit types anyway
"""
# This is used to prevent SVGs with non-square or incorrect but valid viewBoxes from being converted to 24x24.
# If viewBox is None, but the SVG still has width and height, the SVG is still fine.
viewBox_matched_or_none = viewBox_is_none
if not viewBox_is_none:
viewBox_matched_or_none = (
list(map(float, strip_split(root.get('viewBox').strip('px'))))
== [0.0, 0.0, 22.0, 22.0]
)
# This is used to prevent SVGs that aren't square or are missing only height or only width from being converted to 24x24.
# If width and height are None, but the SVG still has a viewBox, the SVG is still fine.
width_height_matched_or_none = width_is_none and height_is_none
if not (width_is_none or height_is_none):
width_height_matched_or_none = (
float(root.get('width').strip('px').strip()) == 22.0 and
float(root.get('height').strip('px').strip()) == 22.0
)
if (width_height_matched_or_none and viewBox_matched_or_none
and not (viewBox_is_none and (width_is_none or height_is_none))):
# Resize to 24x24
root.set('viewBox', "0 0 24 24")
root.set('width', "24")
root.set('height', "24")
# Put content in a group that moves content down 1px, right 1px
group = etree.Element('g', attrib={'transform': "translate(1,1)"})
group.extend(get_renderable_elements(root))
root.append(group)
# print(file_destination)
tree.write(file_destination, method="xml", pretty_print=True, exclusive=True)
else:
skipped_message = " SKIPPED: "
if not viewBox_matched_or_none:
skipped_message += "not square or incorrect viewBox\nviewBox=\"" + root.get('viewBox') + "\""
elif not width_height_matched_or_none:
skipped_message += "not square or incorrect width and height\nwidth=\"" + root.get('width') + "height=\"" + root.get('height') + "\""
elif viewBox_is_none and (width_is_none or height_is_none):
skipped_message += "viewBox and width/height are missing"
else:
skipped_message += "You shouldn't be seeing this. Please fix " + os.path.basename(sys.argv[0])
print(path.lstrip(input_dir) + skipped_message)
def make_file_link(input_dir, output_dir, path):
# only handle links
if not os.path.islink(path):
return
# Filter out files
if not (path.endswith('.svg') and '/22/' in path):
return
file_destination = path.replace(input_dir, output_dir, 1).replace('/22/', '/24/')
# Regenerate symlinks or edit SVGs, don't create dead links for icons we failed to generate
symlink_source = os.readlink(path).replace('/22/', '/24/')
if not os.path.exists(symlink_source):
return
if os.path.islink(file_destination):
os.remove(file_destination)
if not os.path.exists(file_destination):
os.symlink(symlink_source, file_destination)
def main(input_dirs, output_dir):
# first pass: create dirs and files
for input_dir in input_dirs:
for dirpath, dirnames, filenames in os.walk(input_dir):
for d in dirnames:
make_dir(input_dir, output_dir, os.path.join(dirpath, d))
for f in filenames:
make_file(input_dir, output_dir, os.path.join(dirpath, f))
# second pass: create links, that avoids dead links
for input_dir in input_dirs:
for dirpath, dirnames, filenames in os.walk(input_dir):
for f in filenames:
make_file_link(input_dir, output_dir, os.path.join(dirpath, f))
# END defs
# I've structured the program like this in case I want to do multiprocessing later
if __name__ == '__main__':
argv_len = len(sys.argv)
if argv_len < 3:
print("missing arguments")
sys.exit(1)
input_dirs: list = []
for i in range(1, argv_len-1):
if Path(sys.argv[i]).is_dir():
input_dirs.append(sys.argv[i])
if len(input_dirs) < 1:
print("No valid input folders")
sys.exit(1)
output_dir: str = sys.argv[argv_len-1]
output_path = Path(output_dir)
if output_path.exists() and not output_path.is_dir():
print("Output is not a folder")
sys.exit(1)
sys.exit(main(input_dirs, output_dir))