2 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Icon checker: test that icon themes contain all needed icons
4 # Author: Martin Owens <doctormo@geek-2.com>
5 # Licensed under GPL version 2 or any later version, read the file "COPYING" for more information.
11 from collections
import defaultdict
13 THEME_PATH
= os
.path
.join('.', 'share', 'icons')
18 FALLBACK_THEME
= 'hicolor'
20 # These are hard coded as symbolic in the gtk source code
21 'list-add-symbolic.svg',
23 'list-remove-symbolic.svg',
25 'applications-graphics.svg',
26 'applications-graphics-symbolic.svg',
28 'edit-find-symbolic.svg',
30 'dialog-warning-symbolic.svg',
32 'edit-clear-symbolic.svg',
33 'view-refresh-symbolic.svg',
35 # Those are illustrations rather than icons
36 'feBlend-icon-symbolic.svg',
37 'feColorMatrix-icon-symbolic.svg',
38 'feComponentTransfer-icon-symbolic.svg',
39 'feComposite-icon-symbolic.svg',
40 'feConvolveMatrix-icon-symbolic.svg',
41 'feDiffuseLighting-icon-symbolic.svg',
42 'feDisplacementMap-icon-symbolic.svg',
43 'feFlood-icon-symbolic.svg',
44 'feGaussianBlur-icon-symbolic.svg',
45 'feImage-icon-symbolic.svg',
46 'feMerge-icon-symbolic.svg',
47 'feMorphology-icon-symbolic.svg',
48 'feOffset-icon-symbolic.svg',
49 'feSpecularLighting-icon-symbolic.svg',
50 'feTile-icon-symbolic.svg',
51 'feTurbulence-icon-symbolic.svg',
53 'feColorMatrix-icon.svg',
54 'feComponentTransfer-icon.svg',
55 'feComposite-icon.svg',
56 'feConvolveMatrix-icon.svg',
57 'feDiffuseLighting-icon.svg',
58 'feDisplacementMap-icon.svg',
60 'feGaussianBlur-icon.svg',
63 'feMorphology-icon.svg',
65 'feSpecularLighting-icon.svg',
67 'feTurbulence-icon.svg',
68 # Those are UI elements in form of icons; themes may define them, but they shouldn't have to
69 'resizing-handle-horizontal-symbolic.svg',
70 'resizing-handle-vertical-symbolic.svg',
77 ONLY_FOUND_IN
= range(5)
80 for name
in os
.listdir(THEME_PATH
):
81 filename
= os
.path
.join(THEME_PATH
, name
)
82 if name
in IGNORE_THEMES
or not os
.path
.isdir(filename
):
86 def theme_to_string(name
, kind
):
87 return f
"{name}-{kind}"
89 def find_errors_in(themes
):
93 data
= defaultdict(set)
98 for name
, path
in themes
:
99 for root
, dirs
, files
in os
.walk(path
):
101 root
= root
[len(path
)+1:]
104 (kind
, root
) = root
.split('/', 1)
105 if kind
not in ("symbolic", "scalable"):
106 continue # Not testing cursors, maybe later.
108 theme_name
= (name
, kind
)
109 if kind
== "symbolic":
110 all_symbolics
.add(name
)
113 if fname
in IGNORE_ICONS
:
115 if not fname
.endswith('.svg'):
118 if kind
== "symbolic":
119 if not fname
.endswith('-symbolic.svg'):
120 bad_symbolic
.append(os
.path
.join(orig
, fname
))
123 # Make filenames consistant for comparison
124 fname
= fname
.replace('-symbolic.svg', '.svg')
125 elif kind
== "scalable" and fname
.endswith('-symbolic.svg'):
126 bad_scalable
.append(os
.path
.join(orig
, fname
))
129 filename
= os
.path
.join(root
, fname
)
130 data
[filename
].add(theme_name
)
133 errors
.append((BAD_SYMBOLIC_NAME
, bad_symbolic
))
135 errors
.append((BAD_SCALABLE_NAME
, bad_scalable
))
137 only_found_in
= defaultdict(list)
138 missing_from
= defaultdict(list)
139 warn_missing_from
= defaultdict(list)
141 for filename
in sorted(data
):
142 datum
= data
[filename
]
144 symbolics
= set(name
for (name
, kind
) in datum
if kind
== 'symbolic')
145 scalables
= set(name
for (name
, kind
) in datum
if kind
== 'scalable')
147 # For every scalable, there must be a symbolic
148 diff
= scalables
- symbolics
151 missing_from
[f
"{name}-symbolic"].append(filename
)
154 # Icon present in all themes => no error
155 if symbolics
== all_symbolics
:
158 # Icon present in fallback theme but missing from some other theme => warning
159 if FALLBACK_THEME
in symbolics
:
160 for name
in all_symbolics
- symbolics
:
161 warn_missing_from
[name
].append(filename
)
164 # Icon present in some theme but not fallback => error
166 only_found_in
[theme_to_string(*list(datum
)[0])].append(filename
)
168 missing_from
[FALLBACK_THEME
].append(filename
)
171 errors
.append((ONLY_FOUND_IN
, only_found_in
))
173 errors
.append((MISSING_FROM
, missing_from
))
174 if warn_missing_from
:
175 warnings
.append((MISSING_FROM
, warn_missing_from
))
177 return errors
, warnings
179 if __name__
== '__main__':
180 errors
, warnings
= find_errors_in(icon_themes())
183 for error
, themes
in errors
:
184 if isinstance(themes
, list):
186 elif isinstance(themes
, dict):
187 count
+= sum([len(v
) for v
in themes
.values()])
188 sys
.stderr
.write(f
" == {count} errors found in icon themes! == \n\n")
189 for error
, themes
in errors
:
190 if error
is BAD_SCALABLE_NAME
:
191 sys
.stderr
.write(f
"Scalable themes should not have symbolic icons in them (They end with -symbolic.svg so won't be used):\n")
193 sys
.stderr
.write(f
" - {name}\n")
194 sys
.stderr
.write("\n")
195 elif error
is BAD_SYMBOLIC_NAME
:
196 sys
.stderr
.write(f
"Symbolic themes should only have symbolic icons in them (They don't end with -symbolic.svg so can't be used):\n")
198 sys
.stderr
.write(f
" - {name}\n")
199 sys
.stderr
.write("\n")
200 elif error
is MISSING_FROM
:
202 sys
.stderr
.write(f
"Icons missing from {theme}:\n")
203 for name
in themes
[theme
]:
204 sys
.stderr
.write(f
" - {name}\n")
205 sys
.stderr
.write("\n")
206 elif error
is ONLY_FOUND_IN
:
208 sys
.stderr
.write(f
"Icons only found in {theme}:\n")
209 for name
in themes
[theme
]:
210 sys
.stderr
.write(f
" + {name}\n")
211 sys
.stderr
.write("\n")
216 for warning
, themes
in warnings
:
217 count
+= sum([len(v
) for v
in themes
.values()])
218 sys
.stderr
.write(f
" == {count} warnings found in icon themes == \n\n")
219 for warning
, themes
in warnings
:
220 if warning
is MISSING_FROM
:
222 sys
.stderr
.write(f
"Icons missing from {theme}:\n")
223 for name
in themes
[theme
]:
224 sys
.stderr
.write(f
" - {name}\n")
225 sys
.stderr
.write("\n")