File Coverage

blib/lib/FreeDesktop/Icons.pm
Criterion Covered Total %
statement 280 322 86.9
branch 107 150 71.3
condition 27 30 90.0
subroutine 24 31 77.4
pod 14 25 56.0
total 452 558 81.0


line stmt bran cond sub pod time code
1             package FreeDesktop::Icons;
2              
3             =head1 NAME
4              
5             FreeDesktop::Icons - Use icon libraries quick & easy
6              
7             =cut
8              
9              
10 2     2   234200 use strict;
  2         3  
  2         64  
11 2     2   8 use warnings;
  2         10  
  2         102  
12 2     2   7 use Carp;
  2         4  
  2         160  
13 2     2   11 use vars qw($VERSION);
  2         4  
  2         166  
14             $VERSION="0.04";
15 2     2   13 use Config;
  2         4  
  2         194  
16              
17             my $mswin = $Config{'osname'} eq 'MSWin32';
18              
19 2     2   15 use File::Basename;
  2         3  
  2         6387  
20              
21             my @extensions = (
22             '.jpg',
23             '.jpeg',
24             '.png',
25             '.gif',
26             '.xbm',
27             '.xpm',
28             '.svg',
29             );
30              
31             my %regsubs = (
32             1 => sub { return shift },
33             2 => sub {
34             my $test = shift;
35             $test =~ s/\-/_/g;
36             return $test;
37             },
38             3 => sub {
39             my $test = shift;
40             $test =~ s/\-//g;
41             return $test;
42             },
43             );
44              
45             my @defaulticonpath = ();
46             if ($mswin) {
47             push @defaulticonpath, $ENV{ALLUSERSPROFILE} . '\Icons'
48             } else {
49             my $local = $ENV{HOME} . '/.local/share/icons';
50             push @defaulticonpath, $local if -e $local;
51             my $xdgpath = $ENV{XDG_DATA_DIRS};
52             if (defined $xdgpath) {
53             my @xdgdirs = split /\:/, $xdgpath;
54             for (@xdgdirs) {
55             push @defaulticonpath, "$_/icons";
56             }
57             }
58             }
59              
60             =head1 SYNOPSIS
61              
62             my $iconlib = new FreeDeskTop::Icons;
63             $iconlib->theme('Oxygen');
64             $iconlib->size('16');
65             my $imagefile = $iconlib->get('edit-copy');
66              
67             =head1 DESCRIPTION
68              
69             This module gives access to icon libraries on your system. It more
70             ore less conforms to the Free Desktop specifications here:
71             L
72              
73             Furthermore it allows you to add your own icon folders through the B method.
74              
75             We have made provisions to make it work on Windows as well.
76              
77             The constructor takes a list of folders where it finds the icons
78             libraries. If you specify nothing, it will assign default values for:
79              
80             Windows: $ENV{ALLUSERSPROFILE} . '\Icons'. This package will not create
81             the folder if it does not exist. See also the README.md included in this distribution.
82              
83             Others: $ENV{HOME} . '/.local/share/icons', and the folder 'icons' in $ENV{XDG_DATA_DIRS}.
84              
85             =head1 METHODS
86              
87             =over 4
88              
89             =cut
90              
91             sub new {
92 2     2 0 313191 my $class = shift;
93 2         5 my $self = { };
94 2         4 bless $self, $class;
95              
96 2         11 $self->{CONTEXT} = undef;
97 2         5 $self->{ICONSIZE} = undef;
98 2         5 $self->{THEME} = undef;
99 2         5 $self->{THEMEPOOL} = {};
100 2         5 $self->{THEMES} = {};
101 2         7 $self->{RAWPATH} = [];
102 2         8 $self->{SIZE} = undef;
103              
104 2         5 my @iconpath = @_;
105 2 50       7 @iconpath = @defaulticonpath unless @iconpath;
106 2         12 $self->CollectThemes(@iconpath);
107              
108 2         11 return $self;
109             }
110              
111              
112             =item BI<($theme, >[ I<$name, $size> ] I<);>
113              
114             Returns a list of available contexts. If you set $name to undef if will return
115             all contexts of size $size. If you set $size to undef it will return all
116             contexts associated with icon $name. If you set $name and $size to undef it
117             will return all known contexts in the theme. out $size it returns a list
118             of all contexts found in $theme.
119              
120             =cut
121              
122             sub availableContexts {
123 32     32 1 37694 my ($self, $theme, $name, $size) = @_;
124 32         118 my $t = $self->getTheme($theme);
125 32         53 my %found = ();
126 32 100 100     206 if ((not defined $name) and (not defined $size)) {
    100 100        
    100 66        
127 4         14 my @names = keys %$t;
128 4         10 for (@names) {
129 32         34 my $si = $t->{$_};
130 32         44 my @sizes = keys %$si;
131 32         26 for (@sizes) {
132 32         31 my $ci = $si->{$_};
133 32         40 for (keys %$ci) {
134 32         99 $found{$_} = 1;
135             }
136             }
137             }
138             } elsif ((defined $name) and (not defined $size)) {
139 8         16 for (1 .. 3) {
140 24         38 my $sub = $regsubs{$_};
141 24         37 my $test = &$sub($name);
142 24 100       52 if (exists $t->{$test}) {
143 2         4 my $si = $t->{$test};
144 2         6 my @sizes = keys %$si;
145 2         5 for (@sizes) {
146 2         4 my $ci = $si->{$_};
147 2         4 for (keys %$ci) {
148 2         7 $found{$_} = 1;
149             }
150             }
151             }
152             }
153             } elsif ((not defined $name) and (defined $size)) {
154 8         43 my @names = keys %$t;
155 8         19 for (@names) {
156 64 100       227 if (exists $t->{$_}->{$size}) {
157 16         28 my $ci = $t->{$_}->{$size};
158 16         33 for (keys %$ci) {
159 16         36 $found{$_} = 1;
160             }
161             }
162             }
163             } else {
164 12         27 for (1 .. 3) {
165 36         63 my $sub = $regsubs{$_};
166 36         60 my $test = &$sub($name);
167 36 100       80 if (exists $t->{$test}) {
168 4         8 my $si = $t->{$test};
169 4 100       15 if (exists $si->{$size}) {
170 2         5 my $ci = $si->{$size};
171 2         10 %found = %$ci;
172             }
173             }
174             }
175             }
176 32         75 my $parent = $self->parentTheme($theme);
177 32 100       66 if (defined $parent) {
178 16         63 my @contexts = $self->availableContexts($parent, $name, $size);
179 16         29 for (@contexts) {
180 8         15 $found{$_} = 1
181             }
182             }
183 32         102 return sort keys %found
184             }
185              
186             =item BI<($theme, >[ I<$size, $context> ] I<);>
187              
188             Returns a list of available icons. If you set $size to undef the list will
189             contain names it found in all sizes. If you set $context to undef it will return
190             names it found in all contexts. If you leave out both then
191             you get a list of all available icons. Watch out, it might be pretty long.
192              
193             =cut
194              
195             sub availableIcons {
196 32     32 1 19325 my ($self, $theme, $size, $context) = @_;
197 32         81 my $t = $self->getTheme($theme);
198              
199 32         110 my @names = keys %$t;
200 32         58 my %matches = ();
201 32 100 100     203 if ((not defined $size) and (not defined $context)) {
    100 100        
    100 66        
202 4         24 %matches = %$t
203             } elsif ((defined $size) and (not defined $context)) {
204 8         17 for (@names) {
205 64 100       142 if (exists $t->{$_}->{$size}) { $matches{$_} = 1 }
  16         28  
206             }
207             } elsif ((not defined $size) and (defined $context)) {
208 8         17 for (@names) {
209 64         98 my $name = $_;
210 64         83 my $si = $t->{$name};
211 64         123 my @sizes = keys %$si;
212 64         94 for (@sizes) {
213 64 100       190 if (exists $t->{$name}->{$_}->{$context}) { $matches{$name} = 1 }
  16         44  
214             }
215             }
216             } else {
217 12         26 for (@names) {
218 96 100       188 if (exists $t->{$_}->{$size}) {
219 32         46 my $c = $t->{$_}->{$size};
220 32 100       65 if (exists $c->{$context}) {
221 8         16 $matches{$_} = 1
222             }
223             }
224             }
225             }
226 32         80 my $parent = $self->parentTheme($theme);
227 32 100       82 if (defined $parent) {
228 16         48 my @icons = $self->availableIcons($parent, $size, $context);
229 16         38 for (@icons) {
230 36         61 $matches{$_} = 1
231             }
232             }
233 32         176 return sort keys %matches
234             }
235              
236              
237             =item BI<($theme, >[ I<$name, $context> ] I<);>
238              
239             Returns a list of available contexts. If you leave out $size it returns a list
240             of all contexts found in $theme.
241              
242             =cut
243              
244             sub availableSizes {
245 32     32 1 19329 my ($self, $theme, $name, $context) = @_;
246 32         79 my $t = $self->getTheme($theme);
247 32 50       80 return () unless defined $t;
248              
249 32         55 my %found = ();
250 32 100 100     200 if ((not defined $name) and (not defined $context)) {
    100 100        
    100 66        
251 4         16 my @names = keys %$t;
252 4         9 for (@names) {
253 32         77 my $si = $t->{$_};
254 32         55 my @sizes = keys %$si;
255 32         38 for (@sizes) {
256 32         106 $found{$_} = 1
257             }
258             }
259             } elsif ((defined $name) and (not defined $context)) {
260 8         25 for (1 .. 3) {
261 24         49 my $sub = $regsubs{$_};
262 24         48 my $test = &$sub($name);
263 24 100       69 if (exists $t->{$test}) {
264 2         4 my $si = $t->{$test};
265 2         10 %found = %$si;
266             }
267             }
268             } elsif ((not defined $name) and (defined $context)) {
269 8         35 my @names = keys %$t;
270 8         18 for (@names) {
271 64         74 my $n = $_;
272 64         86 my $si = $t->{$n};
273 64         104 my @sizes = keys %$si;
274 64         87 for (@sizes) {
275 64 100       201 if (exists $t->{$n}->{$_}->{$context}) {
276 16         31 $found{$_} = 1
277             }
278             }
279             }
280             } else {
281 12         31 for (1 .. 3) {
282 36         71 my $sub = $regsubs{$_};
283 36         68 my $test = &$sub($name);
284 36 100       126 if (exists $t->{$test}) {
285 4         10 my $si = $t->{$test};
286 4         15 my @sizes = keys %$si;
287 4         9 for (@sizes) {
288 4 100       27 if (exists $t->{$test}->{$_}->{$context}) {
289 2         8 $found{$_} = 1
290             }
291             }
292             }
293             }
294             }
295 32         80 my $parent = $self->parentTheme($theme);
296 32 100       77 if (defined $parent) {
297 16         102 my @sizes = $self->availableSizes($parent, $name, $context);
298 16         37 for (@sizes) {
299 8         16 $found{$_} = 1
300             }
301             }
302 32         44 delete $found{'unknown'};
303 32         138 return sort keys %found
304             }
305              
306             =item B
307              
308             Returns a list of available themes it found while initiating the module.
309              
310             =cut
311              
312             sub availableThemes {
313 2     2 1 315 my $self = shift;
314 2         4 my $k = $self->{THEMES};
315 2         17 return sort keys %$k
316             }
317              
318             sub AvailableSizesCurrentTheme {
319 0     0 0 0 my $self = shift;
320 0         0 return $self->availableSizes($self->theme);
321             }
322              
323             sub CollectThemes {
324 2     2 0 10 my $self = shift;
325 2         9 my %themes = ();
326 2         7 for (@_) {
327 2         4 my $dir = $_;
328 2 50       151 if (opendir DIR, $dir) {
329 2         76 while (my $entry = readdir(DIR)) {
330 10         31 my $fullname = "$dir/$entry";
331 10 50       110 if (-d $fullname) {
332 10 100       171 if (-e "$fullname/index.theme") {
333 6         20 my $index = $self->LoadThemeFile($fullname);
334 6         13 my $main = delete $index->{general};
335 6         12 my $name = $main->{'Name'};
336 6 50       13 if (%$index) {
337 6         62 $themes{$name} = {
338             path => $fullname,
339             general => $main,
340             folders => $index,
341             }
342             }
343             }
344             }
345             }
346 2         20 closedir DIR;
347             }
348             }
349 2         9 $self->{THEMES} = \%themes
350             }
351              
352             =item BI<(?$context?)>
353              
354             Set and return the preferred context to search in.
355              
356             =cut
357              
358             sub context {
359 0     0 1 0 my $self = shift;
360 0 0       0 $self->{CONTEXT} = shift if @_;
361             return $self->{CONTEXT}
362 0         0 }
363              
364             sub CreateIndex {
365 4     4 0 8 my ($self, $tindex) = @_;
366 4         9 my %index = ();
367 4         9 my $base = $tindex->{path};
368 4         8 my $folders = $tindex->{folders};
369 4         16 foreach my $dir (keys %$folders) {
370 16         1518 my @raw = <"$base/$dir/*">;
371 16         51 foreach my $file (@raw) {
372 32 50       74 if ($self->IsImageFile($file)) {
373 32         1323 my ($name, $d, $e) = fileparse($file, @extensions);
374 32 50       94 unless (exists $index{$name}) {
375 32         88 $index{$name} = {}
376             }
377 32         68 my $size = $folders->{$dir}->{Size};
378 32 50       62 unless (defined $size) {
379 0         0 $size = 'unknown';
380             }
381 32 50       86 unless (exists $index{$name}->{$size}) {
382 32         55 $index{$name}->{$size} = {}
383             }
384 32         51 my $context = $folders->{$dir}->{Context};
385 32 50       45 unless (defined $context) {
386 0         0 $context = 'unknown';
387             }
388 32         113 $index{$name}->{$size}->{$context} = $file;
389             }
390             }
391             }
392 4         13 return \%index;
393             }
394              
395             sub FindImageC {
396 8     8 0 16 my ($self, $si, $context) = @_;
397 8 100       20 if (exists $si->{$context}) {
398 6         12 return $si->{$context}
399             } else {
400 2         11 my @contexts = sort keys %$si;
401 2 50       6 if (@contexts) {
402 2         6 return $si->{$contexts[0]};
403             }
404             }
405             return undef
406 0         0 }
407              
408             sub FindImageS {
409 8     8 0 20 my ($self, $nindex, $size, $context, $resize) = @_;
410 8 100       20 if (exists $nindex->{$size}) {
411 6         19 my $file = $self->FindImageC($nindex->{$size}, $context);
412 6 50       13 if (defined $file) { return $file }
  6         12  
413             } else {
414 2 50       9 if (defined $resize) {
415 2         6 $$resize = 1;
416 2         10 my @sizes = reverse sort keys %$nindex;
417 2         6 for (@sizes) {
418 2         3 my $si = $nindex->{$_};
419 2         7 my $file = $self->FindImageC($si, $context);
420 2 50       6 if (defined $file) { return $file }
  2         7  
421             }
422             }
423             }
424             return undef
425 0         0 }
426              
427             sub FindLibImage {
428 32     32 0 111 my ($self, $name, $size, $context, $resize, $theme) = @_;
429            
430 32 50       55 $size = $self->size unless (defined $size);
431 32 50       43 $context = $self->context unless (defined $context);
432 32 50       75 $context = 'unknown' unless defined $context;
433 32 100       66 $theme = $self->theme unless defined $theme;
434 32 50       58 unless (defined $size) {
435 0         0 warn "you must specify a size";
436             return undef
437 0         0 }
438 32 50       51 unless (defined $theme) {
439 0         0 warn "you must specify a theme";
440             return undef
441 0         0 }
442              
443 32         56 my $index = $self->getTheme($theme);
444 32         41 my $file;
445 32 100       78 $file = $self->FindImageS($index->{$name}, $size, $context, $resize,) if exists $index->{$name};
446 32 100       58 return $file if defined $file;
447              
448 24         103 my $parent = $self->parentTheme($theme);
449 24 100       78 $file = $self->FindLibImage($name, $size, $context, $resize, $parent) if defined $parent;
450 24 100       42 return $file if defined $file;
451              
452 22         37 return undef;
453             }
454              
455             sub FindRawImage {
456 21     21 0 41 my ($self, $name) = @_;
457 21         34 my $path = $self->{RAWPATH};
458 21         40 for (@$path) {
459 21         32 my $folder = $_;
460 21         853 opendir(DIR, $folder);
461 21         402 while (my $item = readdir(DIR)) {
462 63         98 my $full = "$folder/$item";
463 63 100 100     113 if ($self->IsImageFile($full) and ($item =~ /^$name/)) {
464 2         29 closedir(DIR);
465 2         9 return $full
466             }
467             }
468 19         256 closedir(DIR);
469             }
470             return undef
471 19         37 }
472              
473             =item BI<($name, ?$size?, ?$context?, ?\$resize?)>
474              
475             Returns the full filename of an image in the library. Finds the best suitable
476             version of the image in the library according to $size and $context. If you specify
477             \$resize B will attempt to return an icon of a different size if it cannot find
478             the requested size. If it eventually returns an image of another size, it sets $resize
479             to 1. This gives the opportunity to scale the image to the requested icon size.
480             All parameters except $name are optional.
481              
482             =cut
483              
484             sub get {
485 12     12 1 13448 my ($self, $name, $size, $context, $resize) = @_;
486              
487 12         39 for (1 .. 3) {
488 21         53 my $sub = $regsubs{$_};
489 21         49 my $test = &$sub($name);
490 21         57 my $img = $self->FindRawImage($test);
491 21 100       56 return $img if defined $img;
492 19         50 $img = $self->FindLibImage($test, $size, $context, $resize);
493 19 100       58 return $img if defined $img;
494             }
495 2         7 return undef;
496             }
497              
498             =item BI<($theme)>
499              
500             Returns a reference to the folders hash defined in the theme.index of $theme.
501              
502             =cut
503              
504             sub getFolders {
505 0     0 1 0 my ($self, $theme) = @_;
506 0 0       0 carp "undefined theme" unless defined $theme;
507 0         0 my $t = $self->{THEMES}->{$theme};
508 0 0       0 if (defined $t) {
509 0         0 return $t->{'folders'};
510             } else {
511 0         0 carp "theme '$theme' not found"
512             }
513             }
514              
515             =item BI<($theme, ?$key?)>
516              
517             Returns a reference to the folders hash defined in the theme.index of $theme.
518             If you specify $key it will return the value of that key in the hash.
519              
520             =cut
521              
522             sub getGeneral {
523 0     0 1 0 my ($self, $theme, $key) = @_;
524 0 0       0 carp "undefined theme" unless defined $theme;
525 0         0 my $t = $self->{THEMES}->{$theme};
526 0 0       0 if (defined $t) {
527 0 0       0 if (defined $key) {
528 0         0 return $t->{'general'}->{$key}
529             } else {
530 0         0 return $t->{'general'}
531             }
532             } else {
533 0         0 carp "theme '$theme' not found"
534             }
535             }
536              
537             =item BI<($theme)>
538              
539             Returns the full path of the theme folder of $theme.
540              
541             =cut
542              
543             sub getPath {
544 0     0 1 0 my ($self, $theme) = @_;
545 0 0       0 carp "undefined theme" unless defined $theme;
546 0         0 my $t = $self->{THEMES}->{$theme};
547 0 0       0 if (defined $t) {
548 0         0 return $t->{'path'}
549             } else {
550 0         0 carp "theme '$theme' not found"
551             }
552             }
553              
554             =item BI<($theme)>
555              
556             Returns the theme data hash of I<$theme>.
557             Returns undef if I<$theme> is not found.
558              
559             =cut
560              
561             sub getTheme {
562 128     128 1 213 my ($self, $name) = @_;
563 128         203 my $pool = $self->{THEMEPOOL};
564 128 100       278 if (exists $pool->{$name}) {
565 124         250 return $pool->{$name}
566             } else {
567 4         10 my $themindex = $self->{THEMES}->{$name};
568 4 50       11 if (defined $themindex) {
569 4         15 my $index = $self->CreateIndex($themindex);
570 4         11 $pool->{$name} = $index;
571 4         11 return $index
572             } else {
573             return undef
574 0         0 }
575             }
576             }
577              
578             sub IsImageFile {
579 95     95 0 150 my ($self, $file) = @_;
580 95 100       996 unless (-f $file) { return 0 } #It must be a file
  42         164  
581 53         3575 my ($d, $f, $e) = fileparse(lc($file), @extensions);
582 53 50       228 if ($e ne '') { return 1 }
  53         538  
583 0         0 return 0
584             }
585              
586             sub LoadThemeFile {
587 6     6 0 13 my ($self, $file) = @_;
588 6         11 $file = "$file/index.theme";
589 6 50       183 if (open(OFILE, "<", $file)) {
590 6         10 my %index = ();
591 6         6 my $section;
592 6         8 my %inf = ();
593 6         144 my $firstline = ;
594 6 50       35 unless ($firstline =~ /^\[.+\]$/) {
595 0         0 warn "Illegal file format $file";
596             } else {
597 6         19 while () {
598 134         126 my $line = $_;
599 134         131 chomp $line;
600 134 100       350 if ($line =~ /^\[([^\]]+)\]/) { #new section
    100          
601 24 100       35 if (defined $section) {
602 18         66 $index{$section} = { %inf }
603             } else {
604 6         21 $index{general} = { %inf }
605             }
606 24         35 $section = $1;
607 24         51 %inf = ();
608             } elsif ($line =~ s/^([^=]+)=//) { #new key
609 80         171 $inf{$1} = $line;
610             }
611             }
612 6 50       13 if (defined $section) {
613 6         42 $index{$section} = { %inf }
614             }
615 6         71 close OFILE;
616             }
617 6         22 return \%index;
618             } else {
619 0         0 warn "Cannot open theme index file: $file"
620             }
621             }
622              
623             sub parentTheme {
624 120     120 0 193 my ($self, $theme) = @_;
625 120         328 return $self->{THEMES}->{$theme}->{'general'}->{'Inherits'};
626             }
627              
628             =item BI<(?\@folders?)>
629              
630             Sets and returns a reference to a list of folders where raw icons can
631             be found.
632              
633             =cut
634              
635             sub rawpath {
636 2     2 1 23 my $self = shift;
637 2 50       9 $self->{RAWPATH} = shift if @_;
638             return $self->{RAWPATH}
639 2         6 }
640              
641             =item BI<(?$size?)>
642              
643             Sets and returns the preferred size to search for.
644              
645             =cut
646              
647             sub size {
648 0     0 1 0 my $self = shift;
649 0 0       0 $self->{SIZE} = shift if @_;
650             return $self->{SIZE}
651 0         0 }
652              
653             =item BI<(?$theme?)>
654              
655             returns a boolean.
656              
657             =cut
658              
659             sub themeExists {
660 0     0 1 0 my ($self, $theme) = @_;
661 0         0 return exists $self->{THEMES}->{$theme}
662             }
663              
664             =item BI<(?$theme?)>
665              
666             Sets and returns the theme to search in.
667              
668             =cut
669              
670             sub theme {
671 21     21 1 1288 my $self = shift;
672 21 100       49 $self->{THEME} = shift if @_;
673             return $self->{THEME}
674 21         55 }
675              
676             =back
677              
678             =head1 LICENSE
679              
680             Same as Perl.
681              
682             =head1 AUTHOR
683              
684             Hans Jeuken (hanje at cpan dot org)
685              
686             =head1 BUGS
687              
688             Unknown. If you find any, please contact the author.
689              
690             =cut
691              
692             1;
693              
694              
695              
696              
697              
698