1 /**
2  * Lookup of icon themes and icons.
3  *
4  * Note: All found icons are just paths. They are not verified to be valid images.
5  *
6  * Authors:
7  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
8  * Copyright:
9  *  Roman Chistokhodov, 2015-2016
10  * License:
11  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
12  * See_Also:
13  *  $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification)
14  */
15 
16 module icontheme.lookup;
17 
18 import icontheme.file;
19 
20 package {
21     import std.file;
22     import std.path;
23     import std.range;
24     import std.traits;
25     import std.typecons;
26 }
27 
28 @trusted bool isDirNothrow(string dir) nothrow
29 {
30     bool ok;
31     collectException(dir.isDir(), ok);
32     return ok;
33 }
34 
35 @trusted bool isFileNothrow(string file) nothrow
36 {
37     bool ok;
38     collectException(file.isFile(), ok);
39     return ok;
40 }
41 
42 /**
43  * Default icon extensions. This array includes .png and .xpm.
44  * PNG is recommended format.
45  * XPM is kept for backward compatibility.
46  *
47  * Note: Icon Theme Specificiation also lists .svg as possible format,
48  * but it's less common to have SVG support for applications,
49  * hence this format is defined as optional by specificiation.
50  * If your application has proper support for SVG images,
51  * array should include it in the first place as the most preferred format
52  * because SVG images are scalable.
53  */
54 enum defaultIconExtensions = [".png", ".xpm"];
55 
56 /**
57  * Find all icon themes in searchIconDirs.
58  * Note:
59  *  You may want to skip icon themes duplicates if there're different versions of the index.theme file for the same theme.
60  * Returns:
61  *  Range of paths to index.theme files represented icon themes.
62  * Params:
63  *  searchIconDirs = base icon directories to search icon themes.
64  * See_Also: $(D icontheme.paths.baseIconDirs)
65  */
66 auto iconThemePaths(Range)(Range searchIconDirs)
67 if(is(ElementType!Range : string))
68 {
69     return searchIconDirs
70         .filter!(function(dir) {
71             bool ok;
72             collectException(dir.isDir, ok);
73             return ok;
74         }).map!(function(iconDir) {
75             return iconDir.dirEntries(SpanMode.shallow)
76                 .map!(p => buildPath(p, "index.theme")).cache()
77                 .filter!(isFileNothrow);
78         }).joiner;
79 }
80 
81 ///
82 version(iconthemeFileTest) unittest
83 {
84     auto paths = iconThemePaths(["test"]).array;
85     assert(paths.length == 3);
86     assert(paths.canFind(buildPath("test", "NewTango", "index.theme")));
87     assert(paths.canFind(buildPath("test", "Tango", "index.theme")));
88     assert(paths.canFind(buildPath("test", "hicolor", "index.theme")));
89 }
90 
91 /**
92  * Lookup index.theme files by theme name.
93  * Params:
94  *  themeName = theme name.
95  *  searchIconDirs = base icon directories to search icon themes.
96  * Returns:
97  *  Range of paths to index.theme file corresponding to the given theme.
98  * Note:
99  *  Usually you want to use the only first found file.
100  * See_Also: $(D icontheme.paths.baseIconDirs), $(D findIconTheme)
101  */
102 auto lookupIconTheme(Range)(string themeName, Range searchIconDirs)
103 if(is(ElementType!Range : string))
104 {
105     return searchIconDirs
106         .map!(dir => buildPath(dir, themeName, "index.theme")).cache()
107         .filter!(isFileNothrow);
108 }
109 
110 /**
111  * Find index.theme file by theme name.
112  * Returns:
113  *  Path to the first found index.theme file or null string if not found.
114  * Params:
115  *  themeName = Theme name.
116  *  searchIconDirs = Base icon directories to search icon themes.
117  * Returns:
118  *  Path to the first found index.theme file corresponding to the given theme.
119  * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIconTheme)
120  */
121 auto findIconTheme(Range)(string themeName, Range searchIconDirs)
122 {
123     auto paths = lookupIconTheme(themeName, searchIconDirs);
124     if (paths.empty) {
125         return null;
126     } else {
127         return paths.front;
128     }
129 }
130 
131 /**
132  * Find index.theme file for given theme and create instance of $(D icontheme.file.IconThemeFile). The first found file will be used.
133  * Returns: $(D icontheme.file.IconThemeFile) object read from the first found index.theme file corresponding to given theme or null if none were found.
134  * Params:
135  *  themeName = theme name.
136  *  searchIconDirs = base icon directories to search icon themes.
137  *  options = options for $(D icontheme.file.IconThemeFile) reading.
138  * Throws:
139  *  $(B ErrnoException) if file could not be opened.
140  *  $(B IniLikeException) if error occured while reading the file.
141  * See_Also: $(D findIconTheme), $(D icontheme.paths.baseIconDirs)
142  */
143 IconThemeFile openIconTheme(Range)(string themeName,
144                                          Range searchIconDirs,
145                                          IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init)
146 {
147     auto path = findIconTheme(themeName, searchIconDirs);
148     return path.empty ? null : new IconThemeFile(to!string(path), options);
149 }
150 
151 ///
152 version(iconthemeFileTest) unittest
153 {
154     auto tango = openIconTheme("Tango", ["test"]);
155     assert(tango);
156     assert(tango.displayName() == "Tango");
157 
158     auto hicolor = openIconTheme("hicolor", ["test"]);
159     assert(hicolor);
160     assert(hicolor.displayName() == "Hicolor");
161 
162     assert(openIconTheme("Nonexistent", ["test"]) is null);
163 }
164 
165 /**
166  * Result of icon lookup.
167  */
168 struct IconSearchResult(IconTheme) if (is(IconTheme : const(IconThemeFile)))
169 {
170     /**
171      * File path of found icon.
172      */
173     string filePath;
174     /**
175      * Subdirectory the found icon belongs to.
176      */
177     IconSubDir subdir;
178     /**
179      * $(D icontheme.file.IconThemeFile) the found icon belongs to.
180      */
181     IconTheme iconTheme;
182 }
183 
184 /**
185  * Lookup icon alternatives in icon themes. It uses icon theme cache wherever it's loaded. If searched icon is found in some icon theme all subsequent themes are ignored.
186  *
187  * This function may require many $(B stat) calls, so beware. Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) properties (e.g. by size or context) to decrease the number of searchable items and allocations. Loading $(D icontheme.cache.IconThemeCache) may also descrease the number of stats.
188  *
189  * Params:
190  *  iconName = Icon name.
191  *  iconThemes = Icon themes to search icon in.
192  *  searchIconDirs = Case icon directories.
193  *  extensions = Possible file extensions of needed icon file, in order of preference.
194  *  sink = Output range accepting $(D IconSearchResult)s.
195  *  reverse = Iterate over icon theme sub-directories in reverse way.
196  *      Usually directories with larger icon size are listed the last,
197  *      so this parameter may speed up the search when looking for the largest icon.
198  * Note: Specification says that extension must be ".png", ".xpm" or ".svg", though SVG is not required to be supported. Some icon themes also contain .svgz images.
199  * Example:
200 ----------
201 lookupIcon!(subdir => subdir.context == "Places" && subdir.size >= 32)(
202     "folder", iconThemes, baseIconDirs(), [".png", ".xpm"],
203     delegate void (IconSearchResult!IconThemeFile item) {
204         writefln("Icon file: %s. Context: %s. Size: %s. Theme: %s", item.filePath, item.subdir.context, item.subdir.size, item.iconTheme.displayName);
205     });
206 ----------
207  * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupFallbackIcon)
208  */
209 void lookupIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts, OutputRange)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, OutputRange sink, Flag!"reverse" reverse = No.reverse)
210 if (isInputRange!(IconThemes) && isForwardRange!(BaseDirs) && isForwardRange!(Exts) &&
211     is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) &&
212     is(ElementType!Exts : string) && isOutputRange!(OutputRange, IconSearchResult!(ElementType!IconThemes)))
213 {
214     bool onExtensions(string themeBaseDir, IconSubDir subdir, ElementType!IconThemes iconTheme)
215     {
216         string subdirPath = buildPath(themeBaseDir, subdir.name);
217         if (!subdirPath.isDirNothrow) {
218             return false;
219         }
220         bool found;
221         foreach(extension; extensions) {
222             string path = buildPath(subdirPath, iconName ~ extension);
223             if (path.isFileNothrow) {
224                 found = true;
225                 put(sink, IconSearchResult!(ElementType!IconThemes)(path, subdir, iconTheme));
226             }
227         }
228         return found;
229     }
230 
231     foreach(iconTheme; iconThemes) {
232         if (iconTheme is null || iconTheme.internalName().length == 0) {
233             continue;
234         }
235 
236         string[] themeBaseDirs = searchIconDirs.map!(dir => buildPath(dir, iconTheme.internalName())).filter!(isDirNothrow).array;
237 
238         bool found;
239 
240         auto bySubdir = choose(reverse, iconTheme.bySubdir().retro(), iconTheme.bySubdir());
241         foreach(subdir; bySubdir) {
242             if (!subdirFilter(subdir)) {
243                 continue;
244             }
245             foreach(themeBaseDir; themeBaseDirs) {
246                 if (iconTheme.cache !is null && themeBaseDir == iconTheme.cache.fileName.dirName) {
247                     if (iconTheme.cache.containsIcon(iconName, subdir.name)) {
248                         found = onExtensions(themeBaseDir, subdir, iconTheme) || found;
249                     }
250                 } else {
251                     found = onExtensions(themeBaseDir, subdir, iconTheme) || found;
252                 }
253             }
254         }
255         if (found) {
256             return;
257         }
258     }
259 }
260 
261 /**
262  * Iterate over all icons in icon themes.
263  * iconThemes is usually the range of the main theme and themes it inherits from.
264  * Note: Usually if some icon was found in icon theme, it should be ignored in all subsequent themes, including sizes not presented in former theme.
265  * Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) thus decreasing the number of searchable items and allocations.
266  * Returns: Range of $(D IconSearchResult).
267  * Params:
268  *  iconThemes = icon themes to search icon in.
269  *  searchIconDirs = base icon directories.
270  *  extensions = possible file extensions for icon files.
271  * Example:
272 -------------
273 foreach(item; lookupThemeIcons!(subdir => subdir.context == "MimeTypes" && subdir.size >= 32)(iconThemes, baseIconDirs(), [".png", ".xpm"]))
274 {
275     writefln("Icon file: %s. Context: %s. Size: %s", item.filePath, item.subdir.context, item.subdir.size);
276 }
277 -------------
278  * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D openBaseThemes)
279  */
280 
281 auto lookupThemeIcons(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions)
282 if (is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) && is (ElementType!Exts : string))
283 {
284     return iconThemes.filter!(iconTheme => iconTheme !is null).map!(
285         iconTheme => iconTheme.bySubdir().filter!(subdirFilter).map!(
286             subdir => searchIconDirs.map!(
287                 basePath => buildPath(basePath, iconTheme.internalName(), subdir.name)
288             ).filter!(isDirNothrow).map!(
289                 subdirPath => subdirPath.dirEntries(SpanMode.shallow).filter!(
290                     filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension)
291                 ).map!(filePath => IconSearchResult!(ElementType!IconThemes)(filePath, subdir, iconTheme))
292             ).joiner
293         ).joiner
294     ).joiner;
295 }
296 
297 /**
298  * Iterate over all icons out of icon themes.
299  * Returns: Range of found icon file paths.
300  * Params:
301  *  searchIconDirs = base icon directories.
302  *  extensions = possible file extensions for icon files.
303  * See_Also:
304  *  $(D lookupFallbackIcon), $(D icontheme.paths.baseIconDirs)
305  */
306 auto lookupFallbackIcons(BaseDirs, Exts)(BaseDirs searchIconDirs, Exts extensions)
307 if (isInputRange!(BaseDirs) && isForwardRange!(Exts) &&
308     is(ElementType!BaseDirs : string) && is(ElementType!Exts : string))
309 {
310     return searchIconDirs.filter!(isDirNothrow).map!(basePath => basePath.dirEntries(SpanMode.shallow).filter!(
311         filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension)
312     )).joiner;
313 }
314 
315 /**
316  * Lookup icon alternatives beyond the icon themes. May be used as fallback lookup, if lookupIcon returned empty range.
317  * Returns: The range of found icon file paths.
318  * Example:
319 ----------
320 auto result = lookupFallbackIcon("folder", baseIconDirs(), [".png", ".xpm"]);
321 ----------
322  * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D lookupFallbackIcons)
323  */
324 auto lookupFallbackIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions)
325 if (isInputRange!(BaseDirs) && isForwardRange!(Exts) &&
326     is(ElementType!BaseDirs : string) && is(ElementType!Exts : string))
327 {
328     return searchIconDirs.map!(basePath =>
329         extensions
330             .map!(extension => buildPath(basePath, iconName ~ extension)).cache()
331             .filter!(isFileNothrow)
332     ).joiner;
333 }
334 
335 /**
336  * Find fallback icon outside of icon themes. The first found is returned.
337  * See_Also: $(D lookupFallbackIcon), $(D icontheme.paths.baseIconDirs)
338  */
339 string findFallbackIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions)
340 {
341     auto r = lookupFallbackIcon(iconName, searchIconDirs, extensions);
342     if (r.empty) {
343         return null;
344     } else {
345         return r.front;
346     }
347 }
348 
349 ///
350 version(iconthemeFileTest) unittest
351 {
352     assert(findFallbackIcon("pidgin", ["test"], defaultIconExtensions) == buildPath("test", "pidgin.png"));
353     assert(findFallbackIcon("nonexistent", ["test"], defaultIconExtensions).empty);
354 }
355 
356 /**
357  * Find icon closest of the size. It uses icon theme cache wherever possible. The first perfect match is used.
358  * Params:
359  *  iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts).
360  *  size = Preferred icon size to get.
361  *  iconThemes = Range of $(D icontheme.file.IconThemeFile) objects.
362  *  searchIconDirs = Base icon directories.
363  *  extensions = Allowed file extensions.
364  *  allowFallback = Allow searching for non-themed fallback if could not find icon in themes (non-themed icon can be any size).
365  * Returns: Icon file path or empty string if not found.
366  * Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with closer size. Therefore the icon found in the more preferred theme always has presedence over icons from other themes.
367  * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findFallbackIcon), $(D iconSizeDistance)
368  */
369 string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback = Yes.allowFallbackIcon)
370 {
371     uint minDistance = uint.max;
372     string closest;
373 
374     lookupIcon!(delegate bool(const(IconSubDir) subdir) {
375         return minDistance != 0 && subdirFilter(subdir) && iconSizeDistance(subdir, size) <= minDistance;
376     })(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) {
377         auto path = t.filePath;
378         auto subdir = t.subdir;
379         auto theme = t.iconTheme;
380 
381         uint distance = iconSizeDistance(subdir, size);
382         if (distance < minDistance) {
383             minDistance = distance;
384             closest = path;
385         }
386     });
387 
388     if (closest.empty && allowFallback) {
389         return findFallbackIcon(iconName, searchIconDirs, extensions);
390     } else {
391         return closest;
392     }
393 }
394 
395 ///
396 version(iconthemeFileTest) unittest
397 {
398     auto baseDirs = ["test"];
399     auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)];
400 
401     string found;
402 
403     //exact match
404     found = findClosestIcon("folder", 32, iconThemes, baseDirs);
405     assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
406 
407     found = findClosestIcon("folder", 24, iconThemes, baseDirs);
408     assert(found == buildPath("test", "Tango", "24x24/devices", "folder.png"));
409 
410     found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 32, iconThemes, baseDirs);
411     assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
412 
413     found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 24, iconThemes, baseDirs);
414     assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
415 
416     found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("folder", 32, iconThemes, baseDirs);
417     assert(found.empty);
418 
419     //hicolor has exact match, but Tango is more preferred.
420     found = findClosestIcon("folder", 64, iconThemes, baseDirs);
421     assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
422 
423     //find xpm
424     found = findClosestIcon("folder", 32, iconThemes, baseDirs, [".xpm"]);
425     assert(found == buildPath("test", "Tango", "32x32/places", "folder.xpm"));
426 
427     //find big png, not exact match
428     found = findClosestIcon("folder", 200, iconThemes, baseDirs);
429     assert(found == buildPath("test", "Tango", "128x128/places", "folder.png"));
430 
431     //svg is closer
432     found = findClosestIcon("folder", 200, iconThemes, baseDirs, [".png", ".svg"]);
433     assert(found == buildPath("test", "Tango", "scalable/places", "folder.svg"));
434 
435     //lookup with fallback
436     found = findClosestIcon("pidgin", 96, iconThemes, baseDirs);
437     assert(found == buildPath("test", "pidgin.png"));
438 
439     //lookup without fallback
440     found = findClosestIcon("pidgin", 96, iconThemes, baseDirs, defaultIconExtensions, No.allowFallbackIcon);
441     assert(found.empty);
442 
443     found = findClosestIcon("text-plain", 48, iconThemes, baseDirs);
444     assert(found == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png"));
445 
446     found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("text-plain", 48, iconThemes, baseDirs);
447     assert(found == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png"));
448 
449     found = findClosestIcon!(subdir => subdir.context == "Actions")("text-plain", 48, iconThemes, baseDirs);
450     assert(found.empty);
451 }
452 
453 /**
454  * ditto, but with predefined extensions and fallback allowed.
455  * See_Also: $(D defaultIconExtensions)
456  */
457 string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs)
458 {
459     return findClosestIcon!subdirFilter(iconName, size, iconThemes, searchIconDirs, defaultIconExtensions);
460 }
461 
462 /**
463  * Find icon of the largest size. It uses icon theme cache wherever possible.
464  * Params:
465  *  iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts).
466  *  iconThemes = Range of $(D icontheme.file.IconThemeFile) objects.
467  *  searchIconDirs = Base icon directories.
468  *  extensions = Allowed file extensions.
469  *  allowFallback = Allow searching for non-themed fallback if could not find icon in themes.
470  * Returns: Icon file path or empty string if not found.
471  * Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with larger size. Therefore the icon found in the most preferred theme always has presedence over icons from other themes.
472  * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findFallbackIcon)
473  */
474 string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback = Yes.allowFallbackIcon)
475 {
476     uint max = 0;
477     string largest;
478 
479     lookupIcon!(delegate bool(const(IconSubDir) subdir) {
480         return subdirFilter(subdir) && subdir.size() >= max;
481     })(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) {
482         auto path = t.filePath;
483         auto subdir = t.subdir;
484         auto theme = t.iconTheme;
485 
486         if (subdir.size() > max) {
487             max = subdir.size();
488             largest = path;
489         }
490     }, Yes.reverse);
491 
492     if (largest.empty && allowFallback) {
493         return findFallbackIcon(iconName, searchIconDirs, extensions);
494     } else {
495         return largest;
496     }
497 }
498 
499 ///
500 version(iconthemeFileTest) unittest
501 {
502     auto baseDirs = ["test"];
503     auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)];
504 
505     string found;
506 
507     found = findLargestIcon("folder", iconThemes, baseDirs);
508     assert(found == buildPath("test", "Tango", "128x128/places", "folder.png"));
509 
510     found = findLargestIcon("desktop", iconThemes, baseDirs);
511     assert(found == buildPath("test", "Tango", "32x32/places", "desktop.png"));
512 
513     found = findLargestIcon("desktop", iconThemes, baseDirs, [".svg", ".png"]);
514     assert(found == buildPath("test", "Tango", "scalable/places", "desktop.svg"));
515 
516     //lookup with fallback
517     found = findLargestIcon("pidgin", iconThemes, baseDirs);
518     assert(found == buildPath("test", "pidgin.png"));
519 
520     //lookup without fallback
521     found = findLargestIcon("pidgin", iconThemes, baseDirs, defaultIconExtensions, No.allowFallbackIcon);
522     assert(found.empty);
523 }
524 
525 /**
526  * ditto, but with predefined extensions and fallback allowed.
527  * See_Also: $(D defaultIconExtensions)
528  */
529 string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs)
530 {
531     return findLargestIcon!subdirFilter(iconName, iconThemes, searchIconDirs, defaultIconExtensions);
532 }
533 
534 /**
535  * Distance between desired size and minimum or maximum size value supported by icon theme subdirectory.
536  */
537 @nogc @safe uint iconSizeDistance(in IconSubDir subdir, uint matchSize) nothrow pure
538 {
539     const uint size = subdir.size();
540     const uint minSize = subdir.minSize();
541     const uint maxSize = subdir.maxSize();
542     const uint threshold = subdir.threshold();
543 
544     final switch(subdir.type()) {
545         case IconSubDir.Type.Fixed:
546         {
547             if (size > matchSize) {
548                 return size - matchSize;
549             } else if (size < matchSize) {
550                 return matchSize - size;
551             } else {
552                 return 0;
553             }
554         }
555         case IconSubDir.Type.Scalable:
556         {
557             if (matchSize < minSize) {
558                 return minSize - matchSize;
559             } else if (matchSize > maxSize) {
560                 return matchSize - maxSize;
561             } else {
562                 return 0;
563             }
564         }
565         case IconSubDir.Type.Threshold:
566         {
567             if (matchSize < size - threshold) {
568                 return (size - threshold) - matchSize;
569             } else if (matchSize > size + threshold) {
570                 return matchSize - (size + threshold);
571             } else {
572                 return 0;
573             }
574         }
575     }
576 }
577 
578 ///
579 unittest
580 {
581     auto fixed = IconSubDir(32, IconSubDir.Type.Fixed);
582     assert(iconSizeDistance(fixed, fixed.size()) == 0);
583     assert(iconSizeDistance(fixed, 30) == 2);
584     assert(iconSizeDistance(fixed, 35) == 3);
585 
586     auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5);
587     assert(iconSizeDistance(threshold, threshold.size()) == 0);
588     assert(iconSizeDistance(threshold, threshold.size() - threshold.threshold()) == 0);
589     assert(iconSizeDistance(threshold, threshold.size() + threshold.threshold()) == 0);
590     assert(iconSizeDistance(threshold, 26) == 1);
591     assert(iconSizeDistance(threshold, 39) == 2);
592 
593     auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48);
594     assert(iconSizeDistance(scalable, scalable.size()) == 0);
595     assert(iconSizeDistance(scalable, scalable.minSize()) == 0);
596     assert(iconSizeDistance(scalable, scalable.maxSize()) == 0);
597     assert(iconSizeDistance(scalable, 20) == 4);
598     assert(iconSizeDistance(scalable, 50) == 2);
599 }
600 
601 /**
602  * Check if matchSize belongs to subdir's size range.
603  */
604 @nogc @safe bool matchIconSize(in IconSubDir subdir, uint matchSize) nothrow pure
605 {
606     const uint size = subdir.size();
607     const uint minSize = subdir.minSize();
608     const uint maxSize = subdir.maxSize();
609     const uint threshold = subdir.threshold();
610 
611     final switch(subdir.type()) {
612         case IconSubDir.Type.Fixed:
613             return size == matchSize;
614         case IconSubDir.Type.Threshold:
615             return matchSize <= (size + threshold) && matchSize >= (size - threshold);
616         case IconSubDir.Type.Scalable:
617             return matchSize >= minSize && matchSize <= maxSize;
618     }
619 }
620 
621 ///
622 unittest
623 {
624     auto fixed = IconSubDir(32, IconSubDir.Type.Fixed);
625     assert(matchIconSize(fixed, fixed.size()));
626     assert(!matchIconSize(fixed, fixed.size() - 2));
627 
628     auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5);
629     assert(matchIconSize(threshold, threshold.size() + threshold.threshold()));
630     assert(matchIconSize(threshold, threshold.size() - threshold.threshold()));
631     assert(!matchIconSize(threshold, threshold.size() + threshold.threshold() + 1));
632     assert(!matchIconSize(threshold, threshold.size() - threshold.threshold() - 1));
633 
634     auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48);
635     assert(matchIconSize(scalable, scalable.minSize()));
636     assert(matchIconSize(scalable, scalable.maxSize()));
637     assert(!matchIconSize(scalable, scalable.minSize() - 1));
638     assert(!matchIconSize(scalable, scalable.maxSize() + 1));
639 }
640 
641 /**
642  * Find icon closest to the given size among given alternatives.
643  * Params:
644  *  alternatives = range of $(D IconSearchResult)s, usually returned by $(D lookupIcon).
645  *  matchSize = desired size of icon.
646  */
647 string matchBestIcon(Range)(Range alternatives, uint matchSize)
648 {
649     uint minDistance = uint.max;
650     string closest;
651 
652     foreach(t; alternatives) {
653         auto path = t[0];
654         auto subdir = t[1];
655         uint distance = iconSizeDistance(subdir, matchSize);
656         if (distance < minDistance) {
657             minDistance = distance;
658             closest = path;
659         }
660         if (minDistance == 0) {
661             return closest;
662         }
663     }
664 
665     return closest;
666 }
667 
668 private void openBaseThemesHelper(Range)(ref IconThemeFile[] themes, IconThemeFile iconTheme,
669                                       Range searchIconDirs,
670                                       IconThemeFile.IconThemeReadOptions options)
671 {
672     foreach(name; iconTheme.inherits()) {
673         if (!themes.canFind!(function(theme, name) {
674             return theme.internalName == name;
675         })(name)) {
676             try {
677                 IconThemeFile f = openIconTheme(name, searchIconDirs, options);
678                 if (f) {
679                     themes ~= f;
680                     openBaseThemesHelper(themes, f, searchIconDirs, options);
681                 }
682             } catch(Exception e) {
683 
684             }
685         }
686     }
687 }
688 
689 /**
690  * Recursively find all themes the given theme is inherited from.
691  * Params:
692  *  iconTheme = Original icon theme to search for its base themes. Included as first element in resulting array.
693  *  searchIconDirs = Base icon directories to search icon themes.
694  *  fallbackThemeName = Name of fallback theme which is loaded the last. Not used if empty. It's NOT loaded twice if some theme in inheritance tree has it as base theme.
695  *  options = Options for $(D icontheme.file.IconThemeFile) reading.
696  * Returns:
697  *  Array of unique $(D icontheme.file.IconThemeFile) objects represented base themes.
698  */
699 IconThemeFile[] openBaseThemes(Range)(IconThemeFile iconTheme,
700                                       Range searchIconDirs,
701                                       string fallbackThemeName = "hicolor",
702                                       IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init)
703 if(isForwardRange!Range && is(ElementType!Range : string))
704 {
705     IconThemeFile[] themes;
706     openBaseThemesHelper(themes, iconTheme, searchIconDirs, options);
707 
708     if (fallbackThemeName.length) {
709         auto fallbackFound = themes.filter!(theme => theme !is null).find!(theme => theme.internalName == fallbackThemeName);
710         if (fallbackFound.empty) {
711             IconThemeFile fallbackTheme;
712             collectException(openIconTheme(fallbackThemeName, searchIconDirs, options), fallbackTheme);
713             if (fallbackTheme) {
714                 themes ~= fallbackTheme;
715             }
716         }
717     }
718 
719     return themes;
720 }
721 
722 ///
723 version(iconthemeFileTest) unittest
724 {
725     auto tango = openIconTheme("NewTango", ["test"]);
726     auto baseThemes = openBaseThemes(tango, ["test"]);
727 
728     assert(baseThemes.length == 2);
729     assert(baseThemes[0].internalName() == "Tango");
730     assert(baseThemes[1].internalName() == "hicolor");
731 
732     baseThemes = openBaseThemes(tango, ["test"], null);
733     assert(baseThemes.length == 1);
734     assert(baseThemes[0].internalName() == "Tango");
735 }