Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xdg/IconTheme.py: 18%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Complete implementation of the XDG Icon Spec
3http://standards.freedesktop.org/icon-theme-spec/
4"""
6import os, time
7import re
9from xdg.IniFile import IniFile, is_ascii
10from xdg.BaseDirectory import xdg_data_dirs
11from xdg.Exceptions import NoThemeError, debug
13import xdg.Config
15class IconTheme(IniFile):
16 "Class to parse and validate IconThemes"
17 def __init__(self):
18 IniFile.__init__(self)
20 def __repr__(self):
21 return self.name
23 def parse(self, file):
24 IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"])
25 self.dir = os.path.dirname(file)
26 (nil, self.name) = os.path.split(self.dir)
28 def getDir(self):
29 return self.dir
31 # Standard Keys
32 def getName(self):
33 return self.get('Name', locale=True)
34 def getComment(self):
35 return self.get('Comment', locale=True)
36 def getInherits(self):
37 return self.get('Inherits', list=True)
38 def getDirectories(self):
39 return self.get('Directories', list=True)
40 def getScaledDirectories(self):
41 return self.get('ScaledDirectories', list=True)
42 def getHidden(self):
43 return self.get('Hidden', type="boolean")
44 def getExample(self):
45 return self.get('Example')
47 # Per Directory Keys
48 def getSize(self, directory):
49 return self.get('Size', type="integer", group=directory)
50 def getContext(self, directory):
51 return self.get('Context', group=directory)
52 def getType(self, directory):
53 value = self.get('Type', group=directory)
54 if value:
55 return value
56 else:
57 return "Threshold"
58 def getMaxSize(self, directory):
59 value = self.get('MaxSize', type="integer", group=directory)
60 if value or value == 0:
61 return value
62 else:
63 return self.getSize(directory)
64 def getMinSize(self, directory):
65 value = self.get('MinSize', type="integer", group=directory)
66 if value or value == 0:
67 return value
68 else:
69 return self.getSize(directory)
70 def getThreshold(self, directory):
71 value = self.get('Threshold', type="integer", group=directory)
72 if value or value == 0:
73 return value
74 else:
75 return 2
77 def getScale(self, directory):
78 value = self.get('Scale', type="integer", group=directory)
79 return value or 1
81 # validation stuff
82 def checkExtras(self):
83 # header
84 if self.defaultGroup == "KDE Icon Theme":
85 self.warnings.append('[KDE Icon Theme]-Header is deprecated')
87 # file extension
88 if self.fileExtension == ".theme":
89 pass
90 elif self.fileExtension == ".desktop":
91 self.warnings.append('.desktop fileExtension is deprecated')
92 else:
93 self.warnings.append('Unknown File extension')
95 # Check required keys
96 # Name
97 try:
98 self.name = self.content[self.defaultGroup]["Name"]
99 except KeyError:
100 self.errors.append("Key 'Name' is missing")
102 # Comment
103 try:
104 self.comment = self.content[self.defaultGroup]["Comment"]
105 except KeyError:
106 self.errors.append("Key 'Comment' is missing")
108 # Directories
109 try:
110 self.directories = self.content[self.defaultGroup]["Directories"]
111 except KeyError:
112 self.errors.append("Key 'Directories' is missing")
114 def checkGroup(self, group):
115 # check if group header is valid
116 if group == self.defaultGroup:
117 try:
118 self.name = self.content[group]["Name"]
119 except KeyError:
120 self.errors.append("Key 'Name' in Group '%s' is missing" % group)
121 try:
122 self.name = self.content[group]["Comment"]
123 except KeyError:
124 self.errors.append("Key 'Comment' in Group '%s' is missing" % group)
125 elif group in self.getDirectories():
126 try:
127 self.type = self.content[group]["Type"]
128 except KeyError:
129 self.type = "Threshold"
130 try:
131 self.name = self.content[group]["Size"]
132 except KeyError:
133 self.errors.append("Key 'Size' in Group '%s' is missing" % group)
134 elif not (re.match(r"^\[X-", group) and is_ascii(group)):
135 self.errors.append("Invalid Group name: %s" % group)
137 def checkKey(self, key, value, group):
138 # standard keys
139 if group == self.defaultGroup:
140 if re.match("^Name"+xdg.Locale.regex+"$", key):
141 pass
142 elif re.match("^Comment"+xdg.Locale.regex+"$", key):
143 pass
144 elif key == "Inherits":
145 self.checkValue(key, value, list=True)
146 elif key == "Directories":
147 self.checkValue(key, value, list=True)
148 elif key == "ScaledDirectories":
149 self.checkValue(key, value, list=True)
150 elif key == "Hidden":
151 self.checkValue(key, value, type="boolean")
152 elif key == "Example":
153 self.checkValue(key, value)
154 elif re.match("^X-[a-zA-Z0-9-]+", key):
155 pass
156 else:
157 self.errors.append("Invalid key: %s" % key)
158 elif group in self.getDirectories():
159 if key == "Size":
160 self.checkValue(key, value, type="integer")
161 elif key == "Context":
162 self.checkValue(key, value)
163 elif key == "Type":
164 self.checkValue(key, value)
165 if value not in ["Fixed", "Scalable", "Threshold"]:
166 self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value)
167 elif key == "MaxSize":
168 self.checkValue(key, value, type="integer")
169 if self.type != "Scalable":
170 self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type)
171 elif key == "MinSize":
172 self.checkValue(key, value, type="integer")
173 if self.type != "Scalable":
174 self.errors.append("Key 'MinSize' give, but Type is %s" % self.type)
175 elif key == "Threshold":
176 self.checkValue(key, value, type="integer")
177 if self.type != "Threshold":
178 self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
179 elif key == "Scale":
180 self.checkValue(key, value, type="integer")
181 elif re.match("^X-[a-zA-Z0-9-]+", key):
182 pass
183 else:
184 self.errors.append("Invalid key: %s" % key)
187class IconData(IniFile):
188 "Class to parse and validate IconData Files"
189 def __init__(self):
190 IniFile.__init__(self)
192 def __repr__(self):
193 displayname = self.getDisplayName()
194 if displayname:
195 return "<IconData: %s>" % displayname
196 else:
197 return "<IconData>"
199 def parse(self, file):
200 IniFile.parse(self, file, ["Icon Data"])
202 # Standard Keys
203 def getDisplayName(self):
204 """Retrieve the display name from the icon data, if one is specified."""
205 return self.get('DisplayName', locale=True)
206 def getEmbeddedTextRectangle(self):
207 """Retrieve the embedded text rectangle from the icon data as a list of
208 numbers (x0, y0, x1, y1), if it is specified."""
209 return self.get('EmbeddedTextRectangle', type="integer", list=True)
210 def getAttachPoints(self):
211 """Retrieve the anchor points for overlays & emblems from the icon data,
212 as a list of co-ordinate pairs, if they are specified."""
213 return self.get('AttachPoints', type="point", list=True)
215 # validation stuff
216 def checkExtras(self):
217 # file extension
218 if self.fileExtension != ".icon":
219 self.warnings.append('Unknown File extension')
221 def checkGroup(self, group):
222 # check if group header is valid
223 if not (group == self.defaultGroup \
224 or (re.match(r"^\[X-", group) and is_ascii(group))):
225 self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
227 def checkKey(self, key, value, group):
228 # standard keys
229 if re.match("^DisplayName"+xdg.Locale.regex+"$", key):
230 pass
231 elif key == "EmbeddedTextRectangle":
232 self.checkValue(key, value, type="integer", list=True)
233 elif key == "AttachPoints":
234 self.checkValue(key, value, type="point", list=True)
235 elif re.match("^X-[a-zA-Z0-9-]+", key):
236 pass
237 else:
238 self.errors.append("Invalid key: %s" % key)
242icondirs = []
243for basedir in xdg_data_dirs:
244 icondirs.append(os.path.join(basedir, "icons"))
245 icondirs.append(os.path.join(basedir, "pixmaps"))
246icondirs.append(os.path.expanduser("~/.icons"))
248# just cache variables, they give a 10x speed improvement
249themes = []
250theme_cache = {}
251dir_cache = {}
252icon_cache = {}
254def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]):
255 """Get the path to a specified icon.
257 size :
258 Icon size in pixels. Defaults to ``xdg.Config.icon_size``.
259 theme :
260 Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't
261 found in the specified theme, it will be looked up in the basic 'hicolor'
262 theme.
263 extensions :
264 List of preferred file extensions.
266 Example::
268 >>> getIconPath("inkscape", 32)
269 '/usr/share/icons/hicolor/32x32/apps/inkscape.png'
270 """
272 global themes
274 if size == None:
275 size = xdg.Config.icon_size
276 if theme == None:
277 theme = xdg.Config.icon_theme
279 # if we have an absolute path, just return it
280 if os.path.isabs(iconname):
281 return iconname
283 # check if it has an extension and strip it
284 if os.path.splitext(iconname)[1][1:] in extensions:
285 iconname = os.path.splitext(iconname)[0]
287 # parse theme files
288 if (themes == []) or (themes[0].name != theme):
289 themes = list(__get_themes(theme))
291 # more caching (icon looked up in the last 5 seconds?)
292 tmp = (iconname, size, theme, tuple(extensions))
293 try:
294 timestamp, icon = icon_cache[tmp]
295 except KeyError:
296 pass
297 else:
298 if (time.time() - timestamp) >= xdg.Config.cache_time:
299 del icon_cache[tmp]
300 else:
301 return icon
303 for thme in themes:
304 icon = LookupIcon(iconname, size, thme, extensions)
305 if icon:
306 icon_cache[tmp] = (time.time(), icon)
307 return icon
309 # cache stuff again (directories looked up in the last 5 seconds?)
310 for directory in icondirs:
311 if (directory not in dir_cache \
312 or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \
313 and dir_cache[directory][2] < os.path.getmtime(directory))) \
314 and os.path.isdir(directory):
315 dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory))
317 for dir, values in dir_cache.items():
318 for extension in extensions:
319 try:
320 if iconname + "." + extension in values[0]:
321 icon = os.path.join(dir, iconname + "." + extension)
322 icon_cache[tmp] = [time.time(), icon]
323 return icon
324 except UnicodeDecodeError as e:
325 if debug:
326 raise e
327 else:
328 pass
330 # we haven't found anything? "hicolor" is our fallback
331 if theme != "hicolor":
332 icon = getIconPath(iconname, size, "hicolor")
333 icon_cache[tmp] = [time.time(), icon]
334 return icon
336def getIconData(path):
337 """Retrieve the data from the .icon file corresponding to the given file. If
338 there is no .icon file, it returns None.
340 Example::
342 getIconData("/usr/share/icons/Tango/scalable/places/folder.svg")
343 """
344 if os.path.isfile(path):
345 icon_file = os.path.splitext(path)[0] + ".icon"
346 if os.path.isfile(icon_file):
347 data = IconData()
348 data.parse(icon_file)
349 return data
351def __get_themes(themename):
352 """Generator yielding IconTheme objects for a specified theme and any themes
353 from which it inherits.
354 """
355 for dir in icondirs:
356 theme_file = os.path.join(dir, themename, "index.theme")
357 if os.path.isfile(theme_file):
358 break
359 theme_file = os.path.join(dir, themename, "index.desktop")
360 if os.path.isfile(theme_file):
361 break
362 else:
363 if debug:
364 raise NoThemeError(themename)
365 return
367 theme = IconTheme()
368 theme.parse(theme_file)
369 yield theme
370 for subtheme in theme.getInherits():
371 for t in __get_themes(subtheme):
372 yield t
374def LookupIcon(iconname, size, theme, extensions):
375 # look for the cache
376 if theme.name not in theme_cache:
377 theme_cache[theme.name] = []
378 theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup
379 theme_cache[theme.name].append(0) # [1] mtime
380 theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]]
382 # cache stuff (directory lookuped up the in the last 5 seconds?)
383 if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time:
384 theme_cache[theme.name][0] = time.time()
385 for subdir in theme.getDirectories():
386 for directory in icondirs:
387 dir = os.path.join(directory,theme.name,subdir)
388 if (dir not in theme_cache[theme.name][2] \
389 or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \
390 and subdir != "" \
391 and os.path.isdir(dir):
392 theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)]
393 theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name))
395 for dir, values in theme_cache[theme.name][2].items():
396 if DirectoryMatchesSize(values[0], size, theme):
397 for extension in extensions:
398 if iconname + "." + extension in values[1]:
399 return os.path.join(dir, iconname + "." + extension)
401 minimal_size = 2**31
402 closest_filename = ""
403 for dir, values in theme_cache[theme.name][2].items():
404 distance = DirectorySizeDistance(values[0], size, theme)
405 if distance < minimal_size:
406 for extension in extensions:
407 if iconname + "." + extension in values[1]:
408 closest_filename = os.path.join(dir, iconname + "." + extension)
409 minimal_size = distance
411 return closest_filename
413def DirectoryMatchesSize(subdir, iconsize, theme):
414 Type = theme.getType(subdir)
415 Size = theme.getSize(subdir)
416 Threshold = theme.getThreshold(subdir)
417 MinSize = theme.getMinSize(subdir)
418 MaxSize = theme.getMaxSize(subdir)
419 if Type == "Fixed":
420 return Size == iconsize
421 elif Type == "Scaleable":
422 return MinSize <= iconsize <= MaxSize
423 elif Type == "Threshold":
424 return Size - Threshold <= iconsize <= Size + Threshold
426def DirectorySizeDistance(subdir, iconsize, theme):
427 Type = theme.getType(subdir)
428 Size = theme.getSize(subdir)
429 Threshold = theme.getThreshold(subdir)
430 MinSize = theme.getMinSize(subdir)
431 MaxSize = theme.getMaxSize(subdir)
432 if Type == "Fixed":
433 return abs(Size - iconsize)
434 elif Type == "Scalable":
435 if iconsize < MinSize:
436 return MinSize - iconsize
437 elif iconsize > MaxSize:
438 return MaxSize - iconsize
439 return 0
440 elif Type == "Threshold":
441 if iconsize < Size - Threshold:
442 return MinSize - iconsize
443 elif iconsize > Size + Threshold:
444 return iconsize - MaxSize
445 return 0