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

313 statements  

1""" 

2Complete implementation of the XDG Icon Spec 

3http://standards.freedesktop.org/icon-theme-spec/ 

4""" 

5 

6import os, time 

7import re 

8 

9from xdg.IniFile import IniFile, is_ascii 

10from xdg.BaseDirectory import xdg_data_dirs 

11from xdg.Exceptions import NoThemeError, debug 

12 

13import xdg.Config 

14 

15class IconTheme(IniFile): 

16 "Class to parse and validate IconThemes" 

17 def __init__(self): 

18 IniFile.__init__(self) 

19 

20 def __repr__(self): 

21 return self.name 

22 

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) 

27 

28 def getDir(self): 

29 return self.dir 

30 

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') 

46 

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 

76 

77 def getScale(self, directory): 

78 value = self.get('Scale', type="integer", group=directory) 

79 return value or 1 

80 

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') 

86 

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') 

94 

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") 

101 

102 # Comment 

103 try: 

104 self.comment = self.content[self.defaultGroup]["Comment"] 

105 except KeyError: 

106 self.errors.append("Key 'Comment' is missing") 

107 

108 # Directories 

109 try: 

110 self.directories = self.content[self.defaultGroup]["Directories"] 

111 except KeyError: 

112 self.errors.append("Key 'Directories' is missing") 

113 

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) 

136 

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) 

185 

186 

187class IconData(IniFile): 

188 "Class to parse and validate IconData Files" 

189 def __init__(self): 

190 IniFile.__init__(self) 

191 

192 def __repr__(self): 

193 displayname = self.getDisplayName() 

194 if displayname: 

195 return "<IconData: %s>" % displayname 

196 else: 

197 return "<IconData>" 

198 

199 def parse(self, file): 

200 IniFile.parse(self, file, ["Icon Data"]) 

201 

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) 

214 

215 # validation stuff 

216 def checkExtras(self): 

217 # file extension 

218 if self.fileExtension != ".icon": 

219 self.warnings.append('Unknown File extension') 

220 

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")) 

226 

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) 

239 

240 

241 

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")) 

247 

248# just cache variables, they give a 10x speed improvement 

249themes = [] 

250theme_cache = {} 

251dir_cache = {} 

252icon_cache = {} 

253 

254def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): 

255 """Get the path to a specified icon. 

256  

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. 

265  

266 Example:: 

267  

268 >>> getIconPath("inkscape", 32) 

269 '/usr/share/icons/hicolor/32x32/apps/inkscape.png' 

270 """ 

271 

272 global themes 

273 

274 if size == None: 

275 size = xdg.Config.icon_size 

276 if theme == None: 

277 theme = xdg.Config.icon_theme 

278 

279 # if we have an absolute path, just return it 

280 if os.path.isabs(iconname): 

281 return iconname 

282 

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] 

286 

287 # parse theme files 

288 if (themes == []) or (themes[0].name != theme): 

289 themes = list(__get_themes(theme)) 

290 

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 

302 

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 

308 

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)) 

316 

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 

329 

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 

335 

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. 

339  

340 Example:: 

341  

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 

350 

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 

366 

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 

373 

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]] 

381 

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)) 

394 

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) 

400 

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 

410 

411 return closest_filename 

412 

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 

425 

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