http://www.jwz.org/xscreensaver/xscreensaver-5.09.tar.gz
[xscreensaver] / driver / xscreensaver-getimage-file
1 #!/usr/bin/perl -w
2 # Copyright © 2001-2009 Jamie Zawinski <jwz@jwz.org>.
3 #
4 # Permission to use, copy, modify, distribute, and sell this software and its
5 # documentation for any purpose is hereby granted without fee, provided that
6 # the above copyright notice appear in all copies and that both that
7 # copyright notice and this permission notice appear in supporting
8 # documentation.  No representations are made about the suitability of this
9 # software for any purpose.  It is provided "as is" without express or 
10 # implied warranty.
11 #
12 # This program chooses a random file from under the given directory, and
13 # prints its name.  The file will be an image file whose dimensions are
14 # larger than a certain minimum size.
15 #
16 # The various xscreensaver hacks that manipulate images ("jigsaw", etc.) get
17 # the image to manipulate by running the "xscreensaver-getimage" program.
18 #
19 # Under X11, the "xscreensaver-getimage" program invokes this script,
20 # depending on the value of the "chooseRandomImages" and "imageDirectory"
21 # settings in the ~/.xscreensaver file (or .../app-defaults/XScreenSaver).
22 #
23 # Under Cocoa, this script lives inside the .saver bundle, and is invoked
24 # directly from utils/grabclient.c.
25 #
26 # Created: 12-Apr-01.
27
28 require 5;
29 #use diagnostics;       # Fails on some MacOS 10.5 systems
30 use strict;
31
32 use POSIX;
33 use Fcntl;
34
35 use Fcntl ':flock'; # import LOCK_* constants
36
37 use POSIX ':fcntl_h';                           # S_ISDIR was here in Perl 5.6
38 import Fcntl ':mode' unless defined &S_ISUID;   # but it is here in Perl 5.8
39         # but in Perl 5.10, both of these load, and cause errors!
40         # So we have to check for S_ISUID instead of S_ISDIR?  WTF?
41
42 use bytes;  # Larry can take Unicode and shove it up his ass sideways.
43             # Perl 5.8.0 causes us to start getting incomprehensible
44             # errors about UTF-8 all over the place without this.
45
46 my $progname = $0; $progname =~ s@.*/@@g;
47 my $version = q{ $Revision: 1.27 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/;
48
49 my $verbose = 0;
50
51 # Whether to use MacOS X's Spotlight to generate the list of files.
52 # When set to -1, uses Spotlight if "mdfind" exists.
53 #
54 # (In my experience, this isn't actually any faster, and might not find
55 # everything if your Spotlight index is out of date, which happens often.)
56 #
57 my $use_spotlight_p = 0;
58
59 # Whether to cache the results of the last run.
60 #
61 my $cache_p = 1;
62
63 # Regenerate the cache if it is older than this many seconds.
64 #
65 my $cache_max_age = 60 * 60 * 3;   # 3 hours
66
67
68 # This matches files that we are allowed to use as images (case-insensitive.)
69 # Anything not matching this is ignored.  This is so you can point your
70 # imageDirectory at directory trees that have things other than images in
71 # them, but it assumes that you gave your images sensible file extensions.
72 #
73 my @good_extensions = ('jpg', 'jpeg', 'pjpeg', 'pjpg', 'png', 'gif',
74                        'tif', 'tiff', 'xbm', 'xpm');
75 my $good_file_re = '\.(' . join("|", @good_extensions) . ')$';
76
77 # This matches file extensions that might occur in an image directory,
78 # and that are never used in the name of a subdirectory.  This is an
79 # optimization that prevents us from having to stat() those files to
80 # tell whether they are directories or not.  (It speeds things up a
81 # lot.  Don't give your directories stupid names.)
82 #
83 my @nondir_extensions = ('ai', 'bmp', 'bz2', 'cr2', 'crw', 'db',
84    'dmg', 'eps', 'gz', 'hqx', 'htm', 'html', 'icns', 'ilbm', 'mov',
85    'nef', 'pbm', 'pdf', 'pl', 'ppm', 'ps', 'psd', 'sea', 'sh', 'shtml',
86    'tar', 'tgz', 'thb', 'txt', 'xcf', 'xmp', 'Z', 'zip' );
87 my $nondir_re = '\.(' . join("|", @nondir_extensions) . ')$';
88
89
90 # JPEG, GIF, and PNG files that are are smaller than this are rejected:
91 # this is so that you can use an image directory that contains both big
92 # images and thumbnails, and have it only select the big versions.
93 #
94 my $min_image_width  = 255;
95 my $min_image_height = 255;
96
97 my @all_files = ();         # list of "good" files we've collected
98 my %seen_inodes;            # for breaking recursive symlink loops
99
100 # For diagnostic messages:
101 #
102 my $dir_count = 1;          # number of directories seen
103 my $stat_count = 0;         # number of files/dirs stat'ed
104 my $skip_count_unstat = 0;  # number of files skipped without stat'ing
105 my $skip_count_stat = 0;    # number of files skipped after stat
106
107 sub find_all_files {
108   my ($dir) = @_;
109
110   print STDERR "$progname:  + reading dir $dir/...\n" if ($verbose > 1);
111
112   local *DIR;
113   if (! opendir (DIR, $dir)) {
114     print STDERR "$progname: couldn't open $dir: $!\n" if ($verbose);
115     return;
116   }
117   my @files = readdir (DIR);
118   closedir (DIR);
119
120   my @dirs = ();
121
122   foreach my $file (@files) {
123     next if ($file =~ m/^\./);      # silently ignore dot files/dirs
124
125     if ($file =~ m/[~%\#]$/) {      # ignore backup files (and dirs...)
126       $skip_count_unstat++;
127       print STDERR "$progname:  - skip file  $file\n" if ($verbose > 1);
128     }
129
130     $file = "$dir/$file";
131
132     if ($file =~ m/$good_file_re/io) {
133       #
134       # Assume that files ending in .jpg exist and are not directories.
135       #
136       push @all_files, $file;
137       print STDERR "$progname:  - found file $file\n" if ($verbose > 1);
138
139     } elsif ($file =~ m/$nondir_re/io) {
140       #
141       # Assume that files ending in .html are not directories.
142       #
143       $skip_count_unstat++;
144       print STDERR "$progname: -- skip file  $file\n" if ($verbose > 1);
145
146     } else {
147       #
148       # Now we need to stat the file to see if it's a subdirectory.
149       #
150       # Note: we could use the trick of checking "nlinks" on the parent
151       # directory to see if this directory contains any subdirectories,
152       # but that would exclude any symlinks to directories.
153       #
154       my @st = stat($file);
155       my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
156           $atime,$mtime,$ctime,$blksize,$blocks) = @st;
157
158       $stat_count++;
159
160       if ($#st == -1) {
161         if ($verbose) {
162           my $ll = readlink $file;
163           if (defined ($ll)) {
164             print STDERR "$progname: + dangling symlink: $file -> $ll\n";
165           } else {
166             print STDERR "$progname: + unreadable: $file\n";
167           }
168         }
169         next;
170       }
171
172       next if ($seen_inodes{"$dev:$ino"}); # break symlink loops
173       $seen_inodes{"$dev:$ino"} = 1;
174
175       if (S_ISDIR($mode)) {
176         push @dirs, $file;
177         $dir_count++;
178         print STDERR "$progname:  + found dir  $file\n" if ($verbose > 1);
179
180       } else {
181         $skip_count_stat++;
182         print STDERR "$progname:  + skip file  $file\n" if ($verbose > 1);
183       }
184     }
185   }
186
187   foreach (@dirs) {
188     find_all_files ($_);
189   }
190 }
191
192
193 sub spotlight_all_files {
194   my ($dir) = @_;
195
196   my @terms = ();
197   # "public.image" matches all (indexed) images, including Photoshop, etc.
198 #  push @terms, "kMDItemContentTypeTree == 'public.image'";
199   foreach (@good_extensions) {
200
201     # kMDItemFSName hits the file system every time: much worse than "find".
202 #    push @terms, "kMDItemFSName == '*.$_'";
203
204     # kMDItemDisplayName matches against the name in the Spotlight index,
205     # but won't find files that (for whatever reason) didn't get indexed.
206     push @terms, "kMDItemDisplayName == '*.$_'";
207   }
208
209   $dir =~ s@([^-_/a-z\d.,])@\\$1@gsi;  # quote for sh
210   my $cmd = "mdfind -onlyin $dir \"" . join (' || ', @terms) . "\"";
211
212   print STDERR "$progname: executing: $cmd\n" if ($verbose > 1);
213   @all_files = split (/[\r\n]+/, `$cmd`);
214 }
215
216
217 # If we're using cacheing, read the cache file and return its contents,
218 # if any.  This also holds an exclusive lock on the cache file, which 
219 # has the additional benefit that if two copies of this program are
220 # running at once, one will wait for the other, instead of both of
221 # them spanking the same file system at the same time.
222 #
223 local *CACHE_FILE;
224 my $cache_file_name = undef;
225 my $read_cache_p = 0;
226
227 sub read_cache($) {
228   my ($dir) = @_;
229
230   return () unless ($cache_p);
231
232   my $dd = "$ENV{HOME}/Library/Caches";    # MacOS location
233   if (-d $dd) {
234     $cache_file_name = "$dd/org.jwz.xscreensaver.getimage.cache";
235   } elsif (-d "$ENV{HOME}/tmp") {
236     $cache_file_name = "$ENV{HOME}/tmp/.xscreensaver-getimage.cache";
237   } else {
238     $cache_file_name = "$ENV{HOME}/.xscreensaver-getimage.cache";
239   }
240
241   print STDERR "$progname: awaiting lock: $cache_file_name\n"
242     if ($verbose > 1);
243
244   my $file = $cache_file_name;
245   open (CACHE_FILE, "+>>$file") || error ("unable to write $file: $!");
246   flock (CACHE_FILE, LOCK_EX)   || error ("unable to lock $file: $!");
247   seek (CACHE_FILE, 0, 0)       || error ("unable to rewind $file: $!");
248
249   my $mtime = (stat(CACHE_FILE))[9];
250
251   if ($mtime + $cache_max_age < time) {
252     print STDERR "$progname: cache is too old\n" if ($verbose);
253     return ();
254   }
255
256   my $odir = <CACHE_FILE>;
257   $odir =~ s/[\r\n]+$//s if defined ($odir);
258   if (!defined ($odir) || ($dir ne $odir)) {
259     print STDERR "$progname: cache is for $odir, not $dir\n"
260       if ($verbose && $odir);
261     return ();
262   }
263
264   my @files = ();
265   while (<CACHE_FILE>) { 
266     s/[\r\n]+$//s;
267     push @files, "$odir/$_";
268   }
269
270   print STDERR "$progname: " . ($#files+1) . " files in cache\n"
271     if ($verbose);
272
273   $read_cache_p = 1;
274   return @files;
275 }
276
277
278 sub write_cache($) {
279   my ($dir) = @_;
280
281   return unless ($cache_p);
282
283   # If we read the cache, just close it without rewriting it.
284   # If we didn't read it, then write it now.
285
286   if (! $read_cache_p) {
287
288     truncate (CACHE_FILE, 0) ||
289       error ("unable to truncate $cache_file_name: $!");
290     seek (CACHE_FILE, 0, 0) ||
291       error ("unable to rewind $cache_file_name: $!");
292
293     if ($#all_files >= 0) {
294       print CACHE_FILE "$dir\n";
295       my $re = qr/$dir/;
296       foreach (@all_files) {
297         my $f = $_; # stupid Perl. do this to avoid modifying @all_files!
298         $f =~ s@^$re/@@so || die;
299         print CACHE_FILE "$f\n";
300       }
301     }
302
303     print STDERR "$progname: cached " . ($#all_files+1) . " files\n"
304       if ($verbose);
305   }
306
307   flock (CACHE_FILE, LOCK_UN) ||
308     error ("unable to unlock $cache_file_name: $!");
309   close (CACHE_FILE);
310 }
311
312
313 sub find_random_file($) {
314   my ($dir) = @_;
315
316   $dir =~ s@/+$@@g;
317
318   if ($use_spotlight_p == -1) {
319     $use_spotlight_p = 0;
320     if (-x '/usr/bin/mdfind') {
321       $use_spotlight_p = 1;
322     }
323   }
324
325   @all_files = read_cache ($dir);
326
327   if ($#all_files >= 0) {
328     # got it from the cache...
329
330   } elsif ($use_spotlight_p) {
331     print STDERR "$progname: spotlighting $dir...\n" if ($verbose);
332     spotlight_all_files ($dir);
333     print STDERR "$progname: found " . ($#all_files+1) .
334                  " file" . ($#all_files == 0 ? "" : "s") .
335                  " via Spotlight\n"
336       if ($verbose);
337   } else {
338     print STDERR "$progname: recursively reading $dir...\n" if ($verbose);
339     find_all_files ($dir);
340     print STDERR "$progname: " .
341                  "f=" . ($#all_files+1) . "; " .
342                  "d=$dir_count; " .
343                  "s=$stat_count; " .
344                  "skip=${skip_count_unstat}+$skip_count_stat=" .
345                   ($skip_count_unstat + $skip_count_stat) .
346                  ".\n"
347       if ($verbose);
348   }
349
350   write_cache ($dir);
351
352   @all_files = sort(@all_files);
353
354   if ($#all_files < 0) {
355     print STDERR "$progname: no files in $dir\n";
356     exit 1;
357   }
358
359   my $max_tries = 50;
360   for (my $i = 0; $i < $max_tries; $i++) {
361
362     my $n = int (rand ($#all_files + 1));
363     my $file = $all_files[$n];
364     if (large_enough_p ($file)) {
365       return $file;
366     }
367   }
368
369   print STDERR "$progname: no suitable images in $dir " .
370                "(after $max_tries tries)\n";
371   exit 1;
372 }
373
374
375 sub large_enough_p {
376   my ($file) = @_;
377
378   my ($w, $h) = image_file_size ($file);
379
380   if (!defined ($h)) {
381     print STDERR "$progname: $file: unable to determine image size\n"
382       if ($verbose);
383     # Assume that unknown files are of good sizes: this will happen if
384     # they matched $good_file_re, but we don't have code to parse them.
385     # (This will also happen if the file is junk...)
386     return 1;
387   }
388
389   if ($w < $min_image_width || $h < $min_image_height) {
390     print STDERR "$progname: $file: too small ($w x $h)\n" if ($verbose);
391     return 0;
392   }
393
394   print STDERR "$progname: $file: $w x $h\n" if ($verbose);
395   return 1;
396 }
397
398
399
400 # Given the raw body of a GIF document, returns the dimensions of the image.
401 #
402 sub gif_size {
403   my ($body) = @_;
404   my $type = substr($body, 0, 6);
405   my $s;
406   return () unless ($type =~ /GIF8[7,9]a/);
407   $s = substr ($body, 6, 10);
408   my ($a,$b,$c,$d) = unpack ("C"x4, $s);
409   return (($b<<8|$a), ($d<<8|$c));
410 }
411
412 # Given the raw body of a JPEG document, returns the dimensions of the image.
413 #
414 sub jpeg_size {
415   my ($body) = @_;
416   my $i = 0;
417   my $L = length($body);
418
419   my $c1 = substr($body, $i, 1); $i++;
420   my $c2 = substr($body, $i, 1); $i++;
421   return () unless (ord($c1) == 0xFF && ord($c2) == 0xD8);
422
423   my $ch = "0";
424   while (ord($ch) != 0xDA && $i < $L) {
425     # Find next marker, beginning with 0xFF.
426     while (ord($ch) != 0xFF) {
427       return () if (length($body) <= $i);
428       $ch = substr($body, $i, 1); $i++;
429     }
430     # markers can be padded with any number of 0xFF.
431     while (ord($ch) == 0xFF) {
432       return () if (length($body) <= $i);
433       $ch = substr($body, $i, 1); $i++;
434     }
435
436     # $ch contains the value of the marker.
437     my $marker = ord($ch);
438
439     if (($marker >= 0xC0) &&
440         ($marker <= 0xCF) &&
441         ($marker != 0xC4) &&
442         ($marker != 0xCC)) {  # it's a SOFn marker
443       $i += 3;
444       return () if (length($body) <= $i);
445       my $s = substr($body, $i, 4); $i += 4;
446       my ($a,$b,$c,$d) = unpack("C"x4, $s);
447       return (($c<<8|$d), ($a<<8|$b));
448
449     } else {
450       # We must skip variables, since FFs in variable names aren't
451       # valid JPEG markers.
452       return () if (length($body) <= $i);
453       my $s = substr($body, $i, 2); $i += 2;
454       my ($c1, $c2) = unpack ("C"x2, $s);
455       my $length = ($c1 << 8) | $c2;
456       return () if ($length < 2);
457       $i += $length-2;
458     }
459   }
460   return ();
461 }
462
463 # Given the raw body of a PNG document, returns the dimensions of the image.
464 #
465 sub png_size {
466   my ($body) = @_;
467   return () unless ($body =~ m/^\211PNG\r/s);
468   my ($bits) = ($body =~ m/^.{12}(.{12})/s);
469   return () unless defined ($bits);
470   return () unless ($bits =~ /^IHDR/);
471   my ($ign, $w, $h) = unpack("a4N2", $bits);
472   return ($w, $h);
473 }
474
475
476 # Given the raw body of a GIF, JPEG, or PNG document, returns the dimensions
477 # of the image.
478 #
479 sub image_size {
480   my ($body) = @_;
481   return () if (length($body) < 10);
482   my ($w, $h) = gif_size ($body);
483   if ($w && $h) { return ($w, $h); }
484   ($w, $h) = jpeg_size ($body);
485   if ($w && $h) { return ($w, $h); }
486   # #### TODO: need image parsers for TIFF, XPM, XBM.
487   return png_size ($body);
488 }
489
490 # Returns the dimensions of the image file.
491 #
492 sub image_file_size {
493   my ($file) = @_;
494   local *IN;
495   if (! open (IN, "<$file")) {
496     print STDERR "$progname: $file: $!\n" if ($verbose);
497     return undef;
498   }
499   binmode (IN);  # Larry can take Unicode and shove it up his ass sideways.
500   my $body = '';
501   sysread (IN, $body, 1024 * 50);   # The first 50k should be enough.
502   close IN;                         # (It's not for certain huge jpegs...
503   return image_size ($body);        # but we know they're huge!)
504 }
505
506
507 sub error($) {
508   my ($err) = @_;
509   print STDERR "$progname: $err\n";
510   exit 1;
511 }
512
513 sub usage {
514   print STDERR "usage: $progname [--verbose] directory\n" .
515   "       Prints the name of a randomly-selected image file.  The directory\n" .
516   "       is searched recursively.  Images smaller than " .
517          "${min_image_width}x${min_image_height} are excluded.\n";
518   exit 1;
519 }
520
521 sub main {
522   my $dir = undef;
523
524   while ($_ = $ARGV[0]) {
525     shift @ARGV;
526     if ($_ eq "--verbose") { $verbose++; }
527     elsif (m/^-v+$/) { $verbose += length($_)-1; }
528     elsif ($_ eq "--name") { }     # ignored, for compatibility
529     elsif ($_ eq "--spotlight")    { $use_spotlight_p = 1; }
530     elsif ($_ eq "--no-spotlight") { $use_spotlight_p = 0; }
531     elsif ($_ eq "--cache")        { $cache_p = 1; }
532     elsif ($_ eq "--no-cache")     { $cache_p = 0; }
533     elsif (m/^-./) { usage; }
534     elsif (!defined($dir)) { $dir = $_; }
535     else { usage; }
536   }
537
538   usage unless (defined($dir));
539
540   $dir =~ s@^~/@$ENV{HOME}/@s;     # allow literal "~/"
541
542   if (! -d $dir) {
543     print STDERR "$progname: $dir: not a directory\n";
544     usage;
545   }
546
547   my $file = find_random_file ($dir);
548   print STDOUT "$file\n";
549 }
550
551 main;
552 exit 0;