2 Complete implementation of the XDG Icon Spec Version 0.8
3 http://standards.freedesktop.org/icon-theme-spec/
8 from xdg.IniFile import *
9 from xdg.BaseDirectory import *
10 from xdg.Exceptions import *
14 class IconTheme(IniFile):
15 "Class to parse and validate IconThemes"
17 IniFile.__init__(self)
22 def parse(self, file):
23 IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"])
24 self.dir = os.path.dirname(file)
25 (nil, self.name) = os.path.split(self.dir)
32 return self.get('Name', locale=True)
34 return self.get('Comment', locale=True)
35 def getInherits(self):
36 return self.get('Inherits', list=True)
37 def getDirectories(self):
38 return self.get('Directories', list=True)
40 return self.get('Hidden', type="boolean")
42 return self.get('Example')
45 def getSize(self, directory):
46 return self.get('Size', type="integer", group=directory)
47 def getContext(self, directory):
48 return self.get('Context', group=directory)
49 def getType(self, directory):
50 value = self.get('Type', group=directory)
55 def getMaxSize(self, directory):
56 value = self.get('MaxSize', type="integer", group=directory)
57 if value or value == 0:
60 return self.getSize(directory)
61 def getMinSize(self, directory):
62 value = self.get('MinSize', type="integer", group=directory)
63 if value or value == 0:
66 return self.getSize(directory)
67 def getThreshold(self, directory):
68 value = self.get('Threshold', type="integer", group=directory)
69 if value or value == 0:
75 def checkExtras(self):
77 if self.defaultGroup == "KDE Icon Theme":
78 self.warnings.append('[KDE Icon Theme]-Header is deprecated')
81 if self.fileExtension == ".theme":
83 elif self.fileExtension == ".desktop":
84 self.warnings.append('.desktop fileExtension is deprecated')
86 self.warnings.append('Unknown File extension')
91 self.name = self.content[self.defaultGroup]["Name"]
93 self.errors.append("Key 'Name' is missing")
97 self.comment = self.content[self.defaultGroup]["Comment"]
99 self.errors.append("Key 'Comment' is missing")
103 self.directories = self.content[self.defaultGroup]["Directories"]
105 self.errors.append("Key 'Directories' is missing")
107 def checkGroup(self, group):
108 # check if group header is valid
109 if group == self.defaultGroup:
111 elif group in self.getDirectories():
113 self.type = self.content[group]["Type"]
115 self.type = "Threshold"
117 self.name = self.content[group]["Name"]
119 self.errors.append("Key 'Name' in Group '%s' is missing" % group)
120 elif not (re.match("^\[X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group):
121 self.errors.append("Invalid Group name: %s" % group)
123 def checkKey(self, key, value, group):
125 if group == self.defaultGroup:
126 if re.match("^Name"+xdg.Locale.regex+"$", key):
128 elif re.match("^Comment"+xdg.Locale.regex+"$", key):
130 elif key == "Inherits":
131 self.checkValue(key, value, list=True)
132 elif key == "Directories":
133 self.checkValue(key, value, list=True)
134 elif key == "Hidden":
135 self.checkValue(key, value, type="boolean")
136 elif key == "Example":
137 self.checkValue(key, value)
138 elif re.match("^X-[a-zA-Z0-9-]+", key):
141 self.errors.append("Invalid key: %s" % key)
142 elif group in self.getDirectories():
144 self.checkValue(key, value, type="integer")
145 elif key == "Context":
146 self.checkValue(key, value)
148 self.checkValue(key, value)
149 if value not in ["Fixed", "Scalable", "Threshold"]:
150 self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value)
151 elif key == "MaxSize":
152 self.checkValue(key, value, type="integer")
153 if self.type != "Scalable":
154 self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type)
155 elif key == "MinSize":
156 self.checkValue(key, value, type="integer")
157 if self.type != "Scalable":
158 self.errors.append("Key 'MinSize' give, but Type is %s" % self.type)
159 elif key == "Threshold":
160 self.checkValue(key, value, type="integer")
161 if self.type != "Threshold":
162 self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
163 elif re.match("^X-[a-zA-Z0-9-]+", key):
166 self.errors.append("Invalid key: %s" % key)
169 class IconData(IniFile):
170 "Class to parse and validate IconData Files"
172 IniFile.__init__(self)
175 return self.getDisplayName()
177 def parse(self, file):
178 IniFile.parse(self, file, ["Icon Data"])
181 def getDisplayName(self):
182 return self.get('DisplayName', locale=True)
183 def getEmbeddedTextRectangle(self):
184 return self.get('EmbeddedTextRectangle', list=True)
185 def getAttachPoints(self):
186 return self.get('AttachPoints', type="point", list=True)
189 def checkExtras(self):
191 if self.fileExtension != ".icon":
192 self.warnings.append('Unknown File extension')
194 def checkGroup(self, group):
195 # check if group header is valid
196 if not (group == self.defaultGroup \
197 or (re.match("^\[X-", group) and group.encode("ascii", 'ignore') == group)):
198 self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
200 def checkKey(self, key, value, group):
202 if re.match("^DisplayName"+xdg.Locale.regex+"$", key):
204 elif key == "EmbeddedTextRectangle":
205 self.checkValue(key, value, type="integer", list=True)
206 elif key == "AttachPoints":
207 self.checkValue(key, value, type="point", list=True)
208 elif re.match("^X-[a-zA-Z0-9-]+", key):
211 self.errors.append("Invalid key: %s" % key)
216 for basedir in xdg_data_dirs:
217 icondirs.append(os.path.join(basedir, "icons"))
218 icondirs.append(os.path.join(basedir, "pixmaps"))
219 icondirs.append(os.path.expanduser("~/.icons"))
221 # just cache variables, they give a 10x speed improvement
227 def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]):
231 size = xdg.Config.icon_size
233 theme = xdg.Config.icon_theme
235 # if we have an absolute path, just return it
236 if os.path.isabs(iconname):
239 # check if it has an extension and strip it
240 if os.path.splitext(iconname)[1][1:] in extensions:
241 iconname = os.path.splitext(iconname)[0]
245 if themes[0].name != theme:
251 # more caching (icon looked up in the last 5 seconds?)
252 tmp = "".join([iconname, str(size), theme, "".join(extensions)])
253 if eache.has_key(tmp):
254 if int(time.time() - eache[tmp][0]) >= xdg.Config.cache_time:
260 icon = LookupIcon(iconname, size, thme, extensions)
262 eache[tmp] = [time.time(), icon]
265 # cache stuff again (directories lookuped up in the last 5 seconds?)
266 for directory in icondirs:
267 if (not dache.has_key(directory) \
268 or (int(time.time() - dache[directory][1]) >= xdg.Config.cache_time \
269 and dache[directory][2] < os.path.getmtime(directory))) \
270 and os.path.isdir(directory):
271 dache[directory] = [os.listdir(directory), time.time(), os.path.getmtime(directory)]
273 for dir, values in dache.items():
274 for extension in extensions:
276 if iconname + "." + extension in values[0]:
277 icon = os.path.join(dir, iconname + "." + extension)
278 eache[tmp] = [time.time(), icon]
280 except UnicodeDecodeError, e:
286 # we haven't found anything? "hicolor" is our fallback
287 if theme != "hicolor":
288 icon = getIconPath(iconname, size, "hicolor")
289 eache[tmp] = [time.time(), icon]
292 def getIconData(path):
293 if os.path.isfile(path):
294 dirname = os.path.dirname(path)
295 basename = os.path.basename(path)
296 if os.path.isfile(os.path.join(dirname, basename + ".icon")):
298 data.parse(os.path.join(dirname, basename + ".icon"))
301 def __addTheme(theme):
303 if os.path.isfile(os.path.join(dir, theme, "index.theme")):
304 __parseTheme(os.path.join(dir,theme, "index.theme"))
306 elif os.path.isfile(os.path.join(dir, theme, "index.desktop")):
307 __parseTheme(os.path.join(dir,theme, "index.desktop"))
311 raise NoThemeError(theme)
313 def __parseTheme(file):
317 for subtheme in theme.getInherits():
320 def LookupIcon(iconname, size, theme, extensions):
322 if not cache.has_key(theme.name):
323 cache[theme.name] = []
324 cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup
325 cache[theme.name].append(0) # [1] mtime
326 cache[theme.name].append(dict()) # [2] dir: [subdir, [items]]
328 # cache stuff (directory lookuped up the in the last 5 seconds?)
329 if int(time.time() - cache[theme.name][0]) >= xdg.Config.cache_time:
330 cache[theme.name][0] = time.time()
331 for subdir in theme.getDirectories():
332 for directory in icondirs:
333 dir = os.path.join(directory,theme.name,subdir)
334 if (not cache[theme.name][2].has_key(dir) \
335 or cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \
337 and os.path.isdir(dir):
338 cache[theme.name][2][dir] = [subdir, os.listdir(dir)]
339 cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name))
341 for dir, values in cache[theme.name][2].items():
342 if DirectoryMatchesSize(values[0], size, theme):
343 for extension in extensions:
344 if iconname + "." + extension in values[1]:
345 return os.path.join(dir, iconname + "." + extension)
347 minimal_size = sys.maxint
348 closest_filename = ""
349 for dir, values in cache[theme.name][2].items():
350 distance = DirectorySizeDistance(values[0], size, theme)
351 if distance < minimal_size:
352 for extension in extensions:
353 if iconname + "." + extension in values[1]:
354 closest_filename = os.path.join(dir, iconname + "." + extension)
355 minimal_size = distance
357 return closest_filename
359 def DirectoryMatchesSize(subdir, iconsize, theme):
360 Type = theme.getType(subdir)
361 Size = theme.getSize(subdir)
362 Threshold = theme.getThreshold(subdir)
363 MinSize = theme.getMinSize(subdir)
364 MaxSize = theme.getMaxSize(subdir)
366 return Size == iconsize
367 elif Type == "Scaleable":
368 return MinSize <= iconsize <= MaxSize
369 elif Type == "Threshold":
370 return Size - Threshold <= iconsize <= Size + Threshold
372 def DirectorySizeDistance(subdir, iconsize, theme):
373 Type = theme.getType(subdir)
374 Size = theme.getSize(subdir)
375 Threshold = theme.getThreshold(subdir)
376 MinSize = theme.getMinSize(subdir)
377 MaxSize = theme.getMaxSize(subdir)
379 return abs(Size - iconsize)
380 elif Type == "Scalable":
381 if iconsize < MinSize:
382 return MinSize - iconsize
383 elif iconsize > MaxSize:
384 return MaxSize - iconsize
386 elif Type == "Threshold":
387 if iconsize < Size - Threshold:
388 return MinSize - iconsize
389 elif iconsize > Size + Threshold:
390 return iconsize - MaxSize