http://www.tienza.es/crux/src/www.jwz.org/xscreensaver/xscreensaver-5.06.tar.gz
[xscreensaver] / hacks / webcollage
1 #!/usr/bin/perl -w
2 #
3 # webcollage, Copyright (c) 1999-2005 by Jamie Zawinski <jwz@jwz.org>
4 # This program decorates the screen with random images from the web.
5 # One satisfied customer described it as "a nonstop pop culture brainbath."
6 #
7 # Permission to use, copy, modify, distribute, and sell this software and its
8 # documentation for any purpose is hereby granted without fee, provided that
9 # the above copyright notice appear in all copies and that both that
10 # copyright notice and this permission notice appear in supporting
11 # documentation.  No representations are made about the suitability of this
12 # software for any purpose.  It is provided "as is" without express or
13 # implied warranty.
14
15
16 # To run this as a display mode with xscreensaver, add this to `programs':
17 #
18 #     webcollage -root
19 #     webcollage -root -filter 'vidwhacker -stdin -stdout'
20 #
21 #
22 # You can see this in action at http://www.jwz.org/webcollage/ --
23 # it auto-reloads about once a minute.  To make a page similar to
24 # that on your own system, do this:
25 #
26 #     webcollage -size '800x600' -imagemap $HOME/www/webcollage/index
27 #
28 #
29 # If you have the "driftnet" program installed, webcollage can display a
30 # collage of images sniffed off your local ethernet, instead of pulled out
31 # of search engines: in that way, your screensaver can display the images
32 # that your co-workers are downloading!
33 #
34 # Driftnet is available here: http://www.ex-parrot.com/~chris/driftnet/
35 # Use it like so:
36 #
37 #     webcollage -root -driftnet
38 #
39 # Driftnet is the Unix implementation of the MacOS "EtherPEG" program.
40
41
42 require 5;
43 use strict;
44
45 # We can't "use diagnostics" here, because that library malfunctions if
46 # you signal and catch alarms: it says "Uncaught exception from user code"
47 # and exits, even though I damned well AM catching it!
48 #use diagnostics;
49
50
51 use Socket;
52 require Time::Local;
53 require POSIX;
54 use Fcntl ':flock'; # import LOCK_* constants
55 use POSIX qw(strftime);
56
57 use bytes;  # Larry can take Unicode and shove it up his ass sideways.
58             # Perl 5.8.0 causes us to start getting incomprehensible
59             # errors about UTF-8 all over the place without this.
60
61
62 my $progname = $0; $progname =~ s@.*/@@g;
63 my $version = q{ $Revision: 1.142 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/;
64 my $copyright = "WebCollage $version, Copyright (c) 1999-2005" .
65     " Jamie Zawinski <jwz\@jwz.org>\n" .
66     "            http://www.jwz.org/webcollage/\n";
67
68
69
70 my @search_methods = ( 20, "googlephotos",  \&pick_from_google_image_photos,
71                        11, "googleimgs",    \&pick_from_google_images,
72                        11, "googlenums",    \&pick_from_google_image_numbers,
73                        20, "altavista",     \&pick_from_alta_vista_random_link,
74                        13, "flickr_recent", \&pick_from_flickr_recent,
75                        10, "flickr_random", \&pick_from_flickr_random,
76                         9, "livejournal",   \&pick_from_livejournal_images,
77                         6, "yahoorand",     \&pick_from_yahoo_random_link,
78
79                      # This is a cute way to search for a certain webcams.
80                      # Not included in default methods, since these images
81                      # aren't terribly interesting by themselves.
82                      # See also "SurveillanceSaver".
83                      #
84                         0, "securitycam",   \&pick_from_security_camera,
85
86                      # In Apr 2002, Google asked me to stop searching them.
87                      # I asked them to add a "random link" url.  They said
88                      # "that would be easy, we'll think about it" and then
89                      # never wrote back.  Booo Google!  Booooo!  So, screw
90                      # those turkeys, I've turned Google searching back on.
91                      # I'm sure they can take it.  (Jan 2005.)
92
93                      # Jan 2005: Yahoo fucked up their search form so that
94                      # it's no longer possible to do "or" searches on news
95                      # images, so we rarely get any hits there any more.
96                      # 
97                      #  0, "yahoonews",   \&pick_from_yahoo_news_text,
98
99                      # Dec 2004: the ircimages guy's server can't take the
100                      # heat, so he started banning the webcollage user agent.
101                      # I tried to convince him to add a lighter-weight page to
102                      # support webcollage better, but he doesn't care.
103                      #
104                      #  0, "ircimages", \&pick_from_ircimages,
105
106                      # Dec 2002: Alta Vista has a new "random link" URL now.
107                      # They added it specifically to better support webcollage!
108                      # That was super cool of them.  This is how we used to do
109                      # it, before:
110                      #
111                      #  0, "avimages", \&pick_from_alta_vista_images,
112                      #  0, "avtext",   \&pick_from_alta_vista_text,
113
114                      # This broke in 2004.  Eh, Lycos sucks anyway.
115                      #
116                      #   0, "lycos",      \&pick_from_lycos_text,
117
118                      # This broke in 2003, I think.  I suspect Hotbot is
119                      # actually the same search engine data as Lycos.
120                      #
121                      #  0, "hotbot",     \&pick_from_hotbot_text,
122                       );
123
124 # programs we can use to write to the root window (tried in ascending order.)
125 #
126 my @root_displayers = (
127   "xscreensaver-getimage -root -file",
128   "chbg       -once -xscreensaver -max_size 100",
129   "xv         -root -quit -viewonly +noresetroot -quick24 -rmode 5" .
130   "           -rfg black -rbg black",
131   "xli        -quiet -onroot -center -border black",
132   "xloadimage -quiet -onroot -center -border black",
133
134 # this lame program wasn't built with vroot.h:
135 # "xsri       -scale -keep-aspect -center-horizontal -center-vertical",
136 );
137
138
139 # Some sites need cookies to work properly.   These are they.
140 #
141 my %cookies = (
142   "www.altavista.com"  =>  "AV_ALL=1",   # request uncensored searches
143   "web.altavista.com"  =>  "AV_ALL=1",
144
145                                          # log in as "cipherpunk"
146   "www.nytimes.com"    =>  'NYT-S=18cHMIlJOn2Y1bu5xvEG3Ufuk6E1oJ.' .
147                            'FMxWaQV0igaB5Yi/Q/guDnLeoL.pe7i1oakSb' .
148                            '/VqfdUdb2Uo27Vzt1jmPn3cpYRlTw9',
149
150   "ircimages.com"      =>  'disclaimer=1',
151 );
152
153
154 # If this is set, it's a helper program to use for pasting images together:
155 # this is a lot faster and more efficient than using PPM pipelines, which is
156 # what we do if this program doesn't exist.  (We check for "webcollage-helper"
157 # on $PATH at startup, and set this variable appropriately.)
158 #
159 my $webcollage_helper = undef;
160
161
162 # If we have the webcollage-helper program, then it will paste the images
163 # together with transparency!  0.0 is invisible, 1.0 is totally opaque.
164 #
165 my $opacity = 0.85;
166
167
168 # Some sites have  managed to poison the search engines.  These are they.
169 # (We auto-detect sites that have poisoned the search engines via excessive
170 # keywords or dictionary words,  but these are ones that slip through
171 # anyway.)
172 #
173 # This can contain full host names, or 2 or 3 component domains.
174 #
175 my %poisoners = (
176   "die.net"                 => 1,  # 'l33t h4ck3r d00dz.
177   "genforum.genealogy.com"  => 1,  # Cluttering avtext with human names.
178   "rootsweb.com"            => 1,  # Cluttering avtext with human names.
179   "akamai.net"              => 1,  # Lots of sites have their images on Akamai.
180   "akamaitech.net"          => 1,  # But those are pretty much all banners.
181                                    # Since Akamai is super-expensive, let's
182                                    # go out on a limb and assume that all of
183                                    # their customers are rich-and-boring.
184   "bartleby.com"            => 1,  # Dictionary, cluttering avtext.
185   "encyclopedia.com"        => 1,  # Dictionary, cluttering avtext.
186   "onlinedictionary.datasegment.com" => 1,  # Dictionary, cluttering avtext.
187   "hotlinkpics.com"         => 1,  # Porn site that has poisoned avimages
188                                    # (I don't see how they did it, though!)
189   "alwayshotels.com"        => 1,  # Poisoned Lycos pretty heavily.
190   "nextag.com"              => 1,  # Poisoned Alta Vista real good.
191   "ghettodriveby.com"       => 1,  # Poisoned Google Images.
192   "crosswordsolver.org"     => 1,  # Poisoned Google Images.
193   "xona.com"                => 1,  # Poisoned Google Images.
194   "freepatentsonline.com"   => 1,  # Poisoned Google Images.
195   "herbdatanz.com"          => 1,  # Poisoned Google Images.
196 );
197
198
199 # When verbosity is turned on, we warn about sites that we seem to be hitting
200 # a lot: usually this means some new poisoner has made it into the search
201 # engines.  But sometimes, the warning is just because that site has a lot
202 # of stuff on it.  So these are the sites that are immune to the "frequent
203 # site" diagnostic message.
204 #
205 my %warningless_sites = (
206   "home.earthlink.net"      => 1,  # Lots of home pages here.
207   "www.geocities.com"       => 1,
208   "www.angelfire.com"       => 1,
209   "members.aol.com"         => 1,
210   "img.photobucket.com"     => 1,
211   "pics.livejournal.com"    => 1,
212   "tinypic.com"             => 1,
213   "flickr.com"              => 1,
214   "pbase.com"               => 1,
215   "blogger.com"             => 1,
216   "multiply.com"            => 1,
217   "wikimedia.org"           => 1,
218
219   "yimg.com"                => 1,  # This is where dailynews.yahoo.com stores
220   "eimg.com"                => 1,  # its images, so pick_from_yahoo_news_text()
221                                    # hits this every time.
222
223   "images.quizfarm.com"     => 1,  # damn those LJ quizzes...
224   "images.quizilla.com"     => 1,
225   "images.quizdiva.net"     => 1,
226
227   "driftnet"                => 1,  # builtin...
228   "local-directory"         => 1,  # builtin...
229 );
230
231
232 # For decoding HTML-encoded character entities to URLs.
233 #
234 my %entity_table = (
235    "apos"   => '\'',
236    "quot"   => '"', "amp"    => '&', "lt"     => '<', "gt"     => '>',
237    "nbsp"   => ' ', "iexcl"  => '¡', "cent"   => '¢', "pound"  => '£',
238    "curren" => '¤', "yen"    => '¥', "brvbar" => '¦', "sect"   => '§',
239    "uml"    => '¨', "copy"   => '©', "ordf"   => 'ª', "laquo"  => '«',
240    "not"    => '¬', "shy"    => '­', "reg"    => '®', "macr"   => '¯',
241    "deg"    => '°', "plusmn" => '±', "sup2"   => '²', "sup3"   => '³',
242    "acute"  => '´', "micro"  => 'µ', "para"   => '¶', "middot" => '·',
243    "cedil"  => '¸', "sup1"   => '¹', "ordm"   => 'º', "raquo"  => '»',
244    "frac14" => '¼', "frac12" => '½', "frac34" => '¾', "iquest" => '¿',
245    "Agrave" => 'À', "Aacute" => 'Á', "Acirc"  => 'Â', "Atilde" => 'Ã',
246    "Auml"   => 'Ä', "Aring"  => 'Å', "AElig"  => 'Æ', "Ccedil" => 'Ç',
247    "Egrave" => 'È', "Eacute" => 'É', "Ecirc"  => 'Ê', "Euml"   => 'Ë',
248    "Igrave" => 'Ì', "Iacute" => 'Í', "Icirc"  => 'Î', "Iuml"   => 'Ï',
249    "ETH"    => 'Ð', "Ntilde" => 'Ñ', "Ograve" => 'Ò', "Oacute" => 'Ó',
250    "Ocirc"  => 'Ô', "Otilde" => 'Õ', "Ouml"   => 'Ö', "times"  => '×',
251    "Oslash" => 'Ø', "Ugrave" => 'Ù', "Uacute" => 'Ú', "Ucirc"  => 'Û',
252    "Uuml"   => 'Ü', "Yacute" => 'Ý', "THORN"  => 'Þ', "szlig"  => 'ß',
253    "agrave" => 'à', "aacute" => 'á', "acirc"  => 'â', "atilde" => 'ã',
254    "auml"   => 'ä', "aring"  => 'å', "aelig"  => 'æ', "ccedil" => 'ç',
255    "egrave" => 'è', "eacute" => 'é', "ecirc"  => 'ê', "euml"   => 'ë',
256    "igrave" => 'ì', "iacute" => 'í', "icirc"  => 'î', "iuml"   => 'ï',
257    "eth"    => 'ð', "ntilde" => 'ñ', "ograve" => 'ò', "oacute" => 'ó',
258    "ocirc"  => 'ô', "otilde" => 'õ', "ouml"   => 'ö', "divide" => '÷',
259    "oslash" => 'ø', "ugrave" => 'ù', "uacute" => 'ú', "ucirc"  => 'û',
260    "uuml"   => 'ü', "yacute" => 'ý', "thorn"  => 'þ', "yuml"   => 'ÿ',
261    "ndash"  => '-', "mdash"  => "--"
262 );
263
264
265 ##############################################################################
266 #
267 # Various global flags set by command line parameters, or computed
268 #
269 ##############################################################################
270
271
272 my $current_state = "???";      # for diagnostics
273 my $load_method;
274 my $last_search;
275 my $image_succeeded = -1;
276 my $suppress_audit = 0;
277
278 my $verbose_imgmap = 0;         # print out rectangles and URLs only (stdout)
279 my $verbose_warnings = 0;       # print out warnings when things go wrong
280 my $verbose_load = 0;           # diagnostics about loading of URLs
281 my $verbose_filter = 0;         # diagnostics about page selection/rejection
282 my $verbose_net = 0;            # diagnostics about network I/O
283 my $verbose_pbm = 0;            # diagnostics about PBM pipelines
284 my $verbose_http = 0;           # diagnostics about all HTTP activity
285 my $verbose_exec = 0;           # diagnostics about executing programs
286
287 my $report_performance_interval = 60 * 15;  # print some stats every 15 minutes
288
289 my $http_proxy = undef;
290 my $http_timeout = 20;
291 my $cvt_timeout = 10;
292
293 my $min_width = 50;
294 my $min_height = 50;
295 my $min_ratio = 1/5;
296
297 my $min_gif_area = (120 * 120);
298
299
300 my $no_output_p = 0;
301 my $urls_only_p = 0;
302 my $cocoa_p = 0;
303 my $imagemap_base = undef;
304
305 my @pids_to_kill = ();  # forked pids we should kill when we exit, if any.
306
307 my $driftnet_magic = 'driftnet';
308 my $driftnet_dir = undef;
309 my $default_driftnet_cmd = "driftnet -a -m 100";
310
311 my $local_magic = 'local-directory';
312 my $local_dir = undef;
313
314 my $wordlist;
315
316 my %rejected_urls;
317 my @tripwire_words = ("aberrate", "abode", "amorphous", "antioch",
318                       "arrhenius", "arteriole", "blanket", "brainchild",
319                       "burdensome", "carnival", "cherub", "chord", "clever",
320                       "dedicate", "dilogarithm", "dolan", "dryden",
321                       "eggplant");
322
323
324 ##############################################################################
325 #
326 # Retrieving URLs
327 #
328 ##############################################################################
329
330 # returns three values: the HTTP response line; the document headers;
331 # and the document body.
332 #
333 sub get_document_1($$$) {
334   my ($url, $referer, $timeout) = @_;
335
336   if (!defined($timeout)) { $timeout = $http_timeout; }
337   if ($timeout > $http_timeout) { $timeout = $http_timeout; }
338
339   if ($timeout <= 0) {
340     LOG (($verbose_net || $verbose_load), "timed out for $url");
341     return ();
342   }
343
344   LOG ($verbose_net, "get_document_1 $url " . ($referer ? $referer : ""));
345
346   if (! ($url =~ m@^http://@i)) {
347     LOG ($verbose_net, "not an HTTP URL: $url");
348     return ();
349   }
350
351   my ($url_proto, $dummy, $serverstring, $path) = split(/\//, $url, 4);
352   $path = "" unless $path;
353
354   if (!$url_proto || !$serverstring) {
355     LOG (($verbose_net || $verbose_load), "unparsable URL: $url");
356     return ();
357   }
358
359   my ($them,$port) = split(/:/, $serverstring);
360   $port = 80 unless $port;
361
362   my $them2 = $them;
363   my $port2 = $port;
364   if ($http_proxy) {
365     $serverstring = $http_proxy if $http_proxy;
366     $serverstring =~ s@^[a-z]+://@@;
367     ($them2,$port2) = split(/:/, $serverstring);
368     $port2 = 80 unless $port2;
369   }
370
371   my ($remote, $iaddr, $paddr, $proto, $line);
372   $remote = $them2;
373   if ($port2 =~ /\D/) { $port2 = getservbyname($port2, 'tcp') }
374   if (!$port2) {
375     LOG (($verbose_net || $verbose_load), "unrecognised port in $url");
376     return ();
377   }
378   $iaddr   = inet_aton($remote);
379   if (!$iaddr) {
380     LOG (($verbose_net || $verbose_load), "host not found: $remote");
381     return ();
382   }
383   $paddr   = sockaddr_in($port2, $iaddr);
384
385
386   my $head = "";
387   my $body = "";
388
389   @_ =
390     eval {
391       local $SIG{ALRM} = sub {
392         LOG (($verbose_net || $verbose_load), "timed out ($timeout) for $url");
393         die "alarm\n";
394       };
395       alarm $timeout;
396
397       $proto   = getprotobyname('tcp');
398       if (!socket(S, PF_INET, SOCK_STREAM, $proto)) {
399         LOG (($verbose_net || $verbose_load), "socket: $!");
400         return ();
401       }
402       if (!connect(S, $paddr)) {
403         LOG (($verbose_net || $verbose_load), "connect($serverstring): $!");
404         return ();
405       }
406
407       select(S); $| = 1; select(STDOUT);
408
409       my $cookie = $cookies{$them};
410
411       my $user_agent = "$progname/$version";
412
413       if ($url =~ m@^http://www\.altavista\.com/@ ||
414           $url =~ m@^http://random\.yahoo\.com/@ ||
415           $url =~ m@^http://images\.google\.com/@ ||
416           $url =~ m@^http://www\.google\.com/@) {
417         # block this, you turkeys.
418         $user_agent = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7)" .
419           " Gecko/20070914 Firefox/2.0.0.7";
420
421         # 28-Jun-2007: Google Images now emits the entire page in JS if
422         # you claim to be Gecko.  They also still block "webcollage".
423         # They serve non-JS for unrecognised agents, so let's try this...
424         $user_agent = "NoJavascriptPlease/1.0"
425           if ($url =~ m@^http://[a-z]+\.google\.com/@);
426       }
427
428       my $hdrs = "GET " . ($http_proxy ? $url : "/$path") . " HTTP/1.0\r\n" .
429                  "Host: $them\r\n" .
430                  "User-Agent: $user_agent\r\n";
431       if ($referer) {
432         $hdrs .= "Referer: $referer\r\n";
433       }
434       if ($cookie) {
435         my @cc = split(/\r?\n/, $cookie);
436         $hdrs .= "Cookie: " . join('; ', @cc) . "\r\n";
437       }
438       $hdrs .= "\r\n";
439
440       foreach (split('\r?\n', $hdrs)) {
441         LOG ($verbose_http, "  ==> $_");
442       }
443       print S $hdrs;
444       my $http = <S> || "";
445
446       # Kludge: the Yahoo Random Link is now returning as its first
447       # line "Status: 301" instead of "HTTP/1.0 301 Found".  Fix it...
448       #
449       $http =~ s@^Status:\s+(\d+)\b@HTTP/1.0 $1@i;
450
451       $_  = $http;
452       s/[\r\n]+$//s;
453       LOG ($verbose_http, "  <== $_");
454
455       while (<S>) {
456         $head .= $_;
457         s/[\r\n]+$//s;
458         last if m@^$@;
459         LOG ($verbose_http, "  <== $_");
460
461         if (m@^Set-cookie:\s*([^;\r\n]+)@i) {
462           set_cookie($them, $1)
463         }
464       }
465
466       my $lines = 0;
467       while (<S>) {
468         $body .= $_;
469         $lines++;
470       }
471
472       LOG ($verbose_http,
473            "  <== [ body ]: $lines lines, " . length($body) . " bytes");
474
475       close S;
476
477       if (!$http) {
478         LOG (($verbose_net || $verbose_load), "null response: $url");
479         return ();
480       }
481
482       $SIG{ALRM} = 'DEFAULT';  # seem to be suffering a race?
483       return ( $http, $head, $body );
484     };
485   die if ($@ && $@ ne "alarm\n");       # propagate errors
486
487   if ($@ && $@ ne "alarm\n") {
488     print STDERR blurb() . "DIE " . join(" ", $@) . "\n";
489     die;
490   }
491
492   if ($@) {
493     # timed out
494     $head = undef;
495     $body = undef;
496     $suppress_audit = 1;
497     return ();
498   } else {
499     # didn't
500     alarm 0;
501     return @_;
502   }
503 }
504
505
506 # returns two values: the document headers; and the document body.
507 # if the given URL did a redirect, returns the redirected-to document.
508 #
509 sub get_document($$;$) {
510   my ($url, $referer, $timeout) = @_;
511   my $start = time;
512
513   if (defined($referer) && $referer eq $driftnet_magic) {
514     return get_driftnet_file ($url);
515   }
516
517   if (defined($referer) && $referer eq $local_magic) {
518     return get_local_file ($url);
519   }
520
521   my $orig_url = $url;
522   my $loop_count = 0;
523   my $max_loop_count = 4;
524
525   do {
526     if (defined($timeout) && $timeout <= 0) {
527       LOG (($verbose_net || $verbose_load), "timed out for $url");
528       $suppress_audit = 1;
529       return ();
530     }
531
532     my ( $http, $head, $body ) = get_document_1 ($url, $referer, $timeout);
533
534     if (defined ($timeout)) {
535       my $now = time;
536       my $elapsed = $now - $start;
537       $timeout -= $elapsed;
538       $start = $now;
539     }
540
541     return () unless $http; # error message already printed
542
543     $http =~ s/[\r\n]+$//s;
544
545     if ( $http =~ m@^HTTP/[0-9.]+ 30[123]@ ) {
546       $_ = $head;
547
548       my ( $location ) = m@^location:[ \t]*(.*)$@im;
549       if ( $location ) {
550         $location =~ s/[\r\n]$//;
551
552         LOG ($verbose_net, "redirect from $url to $location");
553         $referer = $url;
554         $url = $location;
555
556         if ($url =~ m@^/@) {
557           $referer =~ m@^(http://[^/]+)@i;
558           $url = $1 . $url;
559         } elsif (! ($url =~ m@^[a-z]+:@i)) {
560           $_ = $referer;
561           s@[^/]+$@@g if m@^http://[^/]+/@i;
562           $_ .= "/" if m@^http://[^/]+$@i;
563           $url = $_ . $url;
564         }
565
566       } else {
567         LOG ($verbose_net, "no Location with \"$http\"");
568         return ( $url, $body );
569       }
570
571       if ($loop_count++ > $max_loop_count) {
572         LOG ($verbose_net,
573              "too many redirects ($max_loop_count) from $orig_url");
574         $body = undef;
575         return ();
576       }
577
578     } elsif ( $http =~ m@^HTTP/[0-9.]+ ([4-9][0-9][0-9].*)$@ ) {
579
580       LOG (($verbose_net || $verbose_load), "failed: $1 ($url)");
581
582       # http errors -- return nothing.
583       $body = undef;
584       return ();
585
586     } elsif (!$body) {
587
588       LOG (($verbose_net || $verbose_load), "document contains no data: $url");
589       return ();
590
591     } else {
592
593       # ok!
594       return ( $url, $body );
595     }
596
597   } while (1);
598 }
599
600 # If we already have a cookie defined for this site, and the site is trying
601 # to overwrite that very same cookie, let it do so.  This is because nytimes
602 # expires its cookies - it lets you upgrade to a new cookie without logging
603 # in again, but you have to present the old cookie to get the new cookie.
604 # So, by doing this, the built-in cypherpunks cookie will never go "stale".
605 #
606 sub set_cookie($$) {
607   my ($host, $cookie) = @_;
608   my $oc = $cookies{$host};
609   return unless $oc;
610   $_ = $oc;
611   my ($oc_name, $oc_value) = m@^([^= \t\r\n]+)=(.*)$@;
612   $_ = $cookie;
613   my ($nc_name, $nc_value) = m@^([^= \t\r\n]+)=(.*)$@;
614
615   if ($oc_name eq $nc_name &&
616       $oc_value ne $nc_value) {
617     $cookies{$host} = $cookie;
618     LOG ($verbose_net, "overwrote ${host}'s $oc_name cookie");
619   }
620 }
621
622
623 ############################################################################
624 #
625 # Extracting image URLs from HTML
626 #
627 ############################################################################
628
629 # given a URL and the body text at that URL, selects and returns a random
630 # image from it.  returns () if no suitable images found.
631 #
632 sub pick_image_from_body($$) {
633   my ($url, $body) = @_;
634
635   my $base = $url;
636   $_ = $url;
637
638   # if there's at least one slash after the host, take off the last
639   # pathname component
640   if ( m@^http://[^/]+/@io ) {
641     $base =~ s@[^/]+$@@go;
642   }
643
644   # if there are no slashes after the host at all, put one on the end.
645   if ( m@^http://[^/]+$@io ) {
646     $base .= "/";
647   }
648
649   $_ = $body;
650
651   # strip out newlines, compress whitespace
652   s/[\r\n\t ]+/ /go;
653
654   # nuke comments
655   s/<!--.*?-->//go;
656
657
658   # There are certain web sites that list huge numbers of dictionary
659   # words in their bodies or in their <META NAME=KEYWORDS> tags (surprise!
660   # Porn sites tend not to be reputable!)
661   #
662   # I do not want webcollage to filter on content: I want it to select
663   # randomly from the set of images on the web.  All the logic here for
664   # rejecting some images is really a set of heuristics for rejecting
665   # images that are not really images: for rejecting *text* that is in
666   # GIF/JPEG/PNG form.  I don't want text, I want pictures, and I want
667   # the content of the pictures to be randomly selected from among all
668   # the available content.
669   #
670   # So, filtering out "dirty" pictures by looking for "dirty" keywords
671   # would be wrong: dirty pictures exist, like it or not, so webcollage
672   # should be able to select them.
673   #
674   # However, picking a random URL is a hard thing to do.  The mechanism I'm
675   # using is to search for a selection of random words.  This is not
676   # perfect, but works ok most of the time.  The way it breaks down is when
677   # some URLs get precedence because their pages list *every word* as
678   # related -- those URLs come up more often than others.
679   #
680   # So, after we've retrieved a URL, if it has too many keywords, reject
681   # it.  We reject it not on the basis of what those keywords are, but on
682   # the basis that by having so many, the page has gotten an unfair
683   # advantage against our randomizer.
684   #
685   my $trip_count = 0;
686   foreach my $trip (@tripwire_words) {
687     $trip_count++ if m/$trip/i;
688   }
689
690   if ($trip_count >= $#tripwire_words - 2) {
691     LOG (($verbose_filter || $verbose_load),
692          "there is probably a dictionary in \"$url\": rejecting.");
693     $rejected_urls{$url} = -1;
694     $body = undef;
695     $_ = undef;
696     return ();
697   }
698
699
700   my @urls;
701   my %unique_urls;
702
703   foreach (split(/ *</)) {
704     if ( m/^meta /i ) {
705
706       # Likewise, reject any web pages that have a KEYWORDS meta tag
707       # that is too long.
708       #
709       if (m/name ?= ?\"?keywords\"?/i &&
710           m/content ?= ?\"([^\"]+)\"/) {
711         my $L = length($1);
712         if ($L > 1000) {
713           LOG (($verbose_filter || $verbose_load),
714                "excessive keywords ($L bytes) in $url: rejecting.");
715           $rejected_urls{$url} = $L;
716           $body = undef;
717           $_ = undef;
718           return ();
719         } else {
720           LOG ($verbose_filter, "  keywords ($L bytes) in $url (ok)");
721         }
722       }
723
724     } elsif ( m/^(img|a) .*(src|href) ?= ?\"? ?(.*?)[ >\"]/io ) {
725
726       my $was_inline = (! ( "$1" eq "a" || "$1" eq "A" ));
727       my $link = $3;
728       my ( $width )  = m/width ?=[ \"]*(\d+)/oi;
729       my ( $height ) = m/height ?=[ \"]*(\d+)/oi;
730       $_ = $link;
731
732       if ( m@^/@o ) {
733         my $site;
734         ( $site = $base ) =~ s@^(http://[^/]*).*@$1@gio;
735         $_ = "$site$link";
736       } elsif ( ! m@^[^/:?]+:@ ) {
737         $_ = "$base$link";
738         s@/\./@/@g;
739         1 while (s@/[^/]+/\.\./@/@g);
740       }
741
742       # skip non-http
743       if ( ! m@^http://@io ) {
744         next;
745       }
746
747       # skip non-image
748       if ( ! m@[.](gif|jpg|jpeg|pjpg|pjpeg|png)$@io ) {
749         next;
750       }
751
752       # skip really short or really narrow images
753       if ( $width && $width < $min_width) {
754         if (!$height) { $height = "?"; }
755         LOG ($verbose_filter, "  skip narrow image $_ (${width}x$height)");
756         next;
757       }
758
759       if ( $height && $height < $min_height) {
760         if (!$width) { $width = "?"; }
761         LOG ($verbose_filter, "  skip short image $_ (${width}x$height)");
762         next;
763       }
764
765       # skip images with ratios that make them look like banners.
766       if ($min_ratio && $width && $height &&
767           ($width * $min_ratio ) > $height) {
768         if (!$height) { $height = "?"; }
769         LOG ($verbose_filter, "  skip bad ratio $_ (${width}x$height)");
770         next;
771       }
772
773       # skip GIFs with a small number of pixels -- those usually suck.
774       if ($width && $height &&
775           m/\.gif$/io &&
776           ($width * $height) < $min_gif_area) {
777         LOG ($verbose_filter, "  skip small GIF $_ (${width}x$height)");
778         next;
779       }
780       
781       # skip images with a URL that indicates a Yahoo thumbnail.
782       if (m@\.yimg\.com/.*/t/@) {
783         if (!$width)  { $width  = "?"; }
784         if (!$height) { $height = "?"; }
785         LOG ($verbose_filter, "  skip yahoo thumb $_ (${width}x$height)");
786         next;
787       }
788
789       my $url = $_;
790
791       if ($unique_urls{$url}) {
792         LOG ($verbose_filter, "  skip duplicate image $_");
793         next;
794       }
795
796       LOG ($verbose_filter,
797            "  image $url" .
798            ($width && $height ? " (${width}x${height})" : "") .
799            ($was_inline ? " (inline)" : ""));
800
801       $urls[++$#urls] = $url;
802       $unique_urls{$url}++;
803
804       # JPEGs are preferable to GIFs and PNGs.
805       $_ = $url;
806       if ( ! m@[.](gif|png)$@io ) {
807         $urls[++$#urls] = $url;
808       }
809
810       # pointers to images are preferable to inlined images.
811       if ( ! $was_inline ) {
812         $urls[++$#urls] = $url;
813         $urls[++$#urls] = $url;
814       }
815     }
816   }
817
818   my $fsp = ($body =~ m@<frameset@i);
819
820   $_ = undef;
821   $body = undef;
822
823   @urls = depoison (@urls);
824
825   if ( $#urls < 0 ) {
826     LOG ($verbose_load, "no images on $base" . ($fsp ? " (frameset)" : ""));
827     return ();
828   }
829
830   # pick a random element of the table
831   my $i = int(rand($#urls+1));
832   $url = $urls[$i];
833
834   LOG ($verbose_load, "picked image " .($i+1) . "/" . ($#urls+1) . ": $url");
835
836   return $url;
837 }
838
839 # Given a URL and the RSS feed from that URL, pick a random image from
840 # the feed.  This is a lot simpler than extracting images out of a page:
841 # we already know we have reasonable images, so we just pick one.
842 # Returns: the real URL of the page (preferably not the RSS version),
843 # and the image.
844
845 sub pick_image_from_rss($$) {
846   my ( $url, $body ) = @_;
847   my @suitable = ($body =~ m/<enclosure url="(.*?)"/g);
848
849   my ($base) = ($body =~ m@<link>([^<>]+)</link>@i);
850   $base = $url unless $base;
851
852   # pick a random element of the table
853   if (@suitable) {
854     my $i = int(rand(scalar @suitable));
855     my $url = $suitable[$i];
856     LOG ($verbose_load, "picked image " .($i+1) . "/" . 
857                         ($#suitable+1) . ": $url");
858     return ($base, $url);
859   }
860   return;
861 }
862
863 \f
864 ############################################################################
865 #
866 # Subroutines for getting pages and images out of search engines
867 #
868 ############################################################################
869
870
871 sub pick_dictionary() {
872   my @dicts = ("/usr/dict/words",
873                "/usr/share/dict/words",
874                "/usr/share/lib/dict/words",
875                "/usr/share/dict/cracklib-small",
876                "/usr/share/dict/cracklib-words"
877                );
878   foreach my $f (@dicts) {
879     if (-f $f) {
880       $wordlist = $f;
881       last;
882     }
883   }
884   error ("$dicts[0] does not exist") unless defined($wordlist);
885 }
886
887 # returns a random word from the dictionary
888 #
889 sub random_word() {
890
891   local *IN;
892   if (! open (IN, "<$wordlist")) {
893     return undef;
894   }
895
896   my $size = (stat(IN))[7];
897   my $word = undef;
898   my $count = 0;
899
900   while (1) {
901     error ("looping ($count) while reading $wordlist")
902       if (++$count > 100);
903
904     my $pos = int (rand ($size));
905     if (seek (IN, $pos, 0)) {
906       $word = <IN>;   # toss partial line
907       $word = <IN>;   # keep next line
908     }
909
910     next unless ($word);
911     next if ($word =~ m/^[-\']/);
912
913     $word = lc($word);
914     $word =~ s/^.*-//s;
915     $word =~ s/^[^a-z]+//s;
916     $word =~ s/[^a-z]+$//s;
917     $word =~ s/\'s$//s;
918     $word =~ s/ys$/y/s;
919     $word =~ s/ally$//s;
920     $word =~ s/ly$//s;
921     $word =~ s/ies$/y/s;
922     $word =~ s/ally$/al/s;
923     $word =~ s/izes$/ize/s;
924     $word =~ s/esses$/ess/s;
925     $word =~ s/(.{5})ing$/$1/s;
926
927     next if (length ($word) > 14);
928     last if ($word);
929   }
930
931   close (IN);
932
933   if ( $word =~ s/\s/\+/gs ) {  # convert intra-word spaces to "+".
934     $word = "\%22$word\%22";    # And put quotes (%22) around it.
935   }
936
937   return $word;
938 }
939
940
941 sub random_words($) {
942   my ($or_p) = @_;
943   my $sep = ($or_p ? "%20OR%20" : "%20");
944   return (random_word . $sep .
945           random_word . $sep .
946           random_word . $sep .
947           random_word . $sep .
948           random_word);
949 }
950
951
952 sub url_quote($) {
953   my ($s) = @_;
954   $s =~ s|([^-a-zA-Z0-9.\@/_\r\n])|sprintf("%%%02X", ord($1))|ge;
955   return $s;
956 }
957
958 sub url_unquote($) {
959   my ($s) = @_;
960   $s =~ s/[+]/ /g;
961   $s =~ s/%([a-z0-9]{2})/chr(hex($1))/ige;
962   return $s;
963 }
964
965 sub html_quote($) {
966   my ($s) = @_;
967   $s =~ s/&/&amp;/gi;
968   $s =~ s/</&lt;/gi;
969   $s =~ s/>/&gt;/gi;
970   $s =~ s/\"/&quot;/gi;
971   return $s;
972 }
973
974 sub html_unquote($) {
975   my ($s) = @_;
976   $s =~ s/(&([a-z]+);)/{ $entity_table{$2} || $1; }/gexi;  # e.g., &apos;
977   $s =~ s/(&\#(\d+);)/{ chr($2) }/gexi;                    # e.g., &#39;
978   return $s;
979 }
980
981
982 # Loads the given URL (a search on some search engine) and returns:
983 # - the total number of hits the search engine claimed it had;
984 # - a list of URLs from the page that the search engine returned;
985 # Note that this list contains all kinds of internal search engine
986 # junk URLs too -- caller must prune them.
987 #
988 sub pick_from_search_engine($$$) {
989   my ( $timeout, $search_url, $words ) = @_;
990
991   $_ = $words;
992   s/%20/ /g;
993
994   print STDERR "\n\n" if ($verbose_load);
995
996   LOG ($verbose_load, "words: $_");
997   LOG ($verbose_load, "URL: $search_url");
998
999   $last_search = $search_url;   # for warnings
1000
1001   my $start = time;
1002   my ( $base, $body ) = get_document ($search_url, undef, $timeout);
1003   if (defined ($timeout)) {
1004     $timeout -= (time - $start);
1005     if ($timeout <= 0) {
1006       $body = undef;
1007       LOG (($verbose_net || $verbose_load),
1008            "timed out (late) for $search_url");
1009       $suppress_audit = 1;
1010       return ();
1011     }
1012   }
1013
1014   return () if (! $body);
1015
1016
1017   my @subpages;
1018
1019   my $search_count = "?";
1020   if ($body =~ m@found (approximately |about )?(<B>)?(\d+)(</B>)? image@) {
1021     $search_count = $3;
1022   } elsif ($body =~ m@<NOBR>((\d{1,3})(,\d{3})*)&nbsp;@i) {
1023     $search_count = $1;
1024   } elsif ($body =~ m@found ((\d{1,3})(,\d{3})*|\d+) Web p@) {
1025     $search_count = $1;
1026   } elsif ($body =~ m@found about ((\d{1,3})(,\d{3})*|\d+) results@) {
1027     $search_count = $1;
1028   } elsif ($body =~ m@\b\d+ - \d+ of (\d+)\b@i) { # avimages
1029     $search_count = $1;
1030   } elsif ($body =~ m@About ((\d{1,3})(,\d{3})*) images@i) { # avimages
1031     $search_count = $1;
1032   } elsif ($body =~ m@We found ((\d{1,3})(,\d{3})*|\d+) results@i) { # *vista
1033     $search_count = $1;
1034   } elsif ($body =~ m@ of about <B>((\d{1,3})(,\d{3})*)<@i) { # googleimages
1035     $search_count = $1;
1036   } elsif ($body =~ m@<B>((\d{1,3})(,\d{3})*)</B> Web sites were found@i) {
1037     $search_count = $1;    # lycos
1038   } elsif ($body =~ m@WEB.*?RESULTS.*?\b((\d{1,3})(,\d{3})*)\b.*?Matches@i) {
1039     $search_count = $1;                          # hotbot
1040   } elsif ($body =~ m@no photos were found containing@i) { # avimages
1041     $search_count = "0";
1042   } elsif ($body =~ m@found no document matching@i) { # avtext
1043     $search_count = "0";
1044   }
1045   1 while ($search_count =~ s/^(\d+)(\d{3})/$1,$2/);
1046
1047 #  if ($search_count eq "?" || $search_count eq "0") {
1048 #    local *OUT;
1049 #    my $file = "/tmp/wc.html";
1050 #    open(OUT, ">$file") || error ("writing $file: $!");
1051 #    print OUT $body;
1052 #    close OUT;
1053 #    print STDERR  blurb() . "###### wrote $file\n";
1054 #  }
1055
1056
1057   my $length = length($body);
1058   my $href_count = 0;
1059
1060   $_ = $body;
1061
1062   s/[\r\n\t ]+/ /g;
1063
1064
1065   s/(<A )/\n$1/gi;
1066   foreach (split(/\n/)) {
1067     $href_count++;
1068     my ($u) = m@<A\s.*\bHREF\s*=\s*([^>]+)>@i;
1069     next unless $u;
1070
1071     if ($u =~ m/^\"([^\"]*)\"/) { $u = $1; }   # quoted string
1072     elsif ($u =~ m/^([^\s]*)\s/) { $u = $1; }  # or token
1073
1074     if ( $rejected_urls{$u} ) {
1075       LOG ($verbose_filter, "  pre-rejecting candidate: $u");
1076       next;
1077     }
1078
1079     LOG ($verbose_http, "    HREF: $u");
1080
1081     $subpages[++$#subpages] = $u;
1082   }
1083
1084   if ( $#subpages < 0 ) {
1085     LOG ($verbose_filter,
1086          "found nothing on $base ($length bytes, $href_count links).");
1087     return ();
1088   }
1089
1090   LOG ($verbose_filter, "" . $#subpages+1 . " links on $search_url");
1091
1092   return ($search_count, @subpages);
1093 }
1094
1095
1096 sub depoison(@) {
1097   my (@urls) = @_;
1098   my @urls2 = ();
1099   foreach (@urls) {
1100     my ($h) = m@^http://([^/: \t\r\n]+)@i;
1101
1102     next unless defined($h);
1103
1104     if ($poisoners{$h}) {
1105       LOG (($verbose_filter), "  rejecting poisoner: $_");
1106       next;
1107     }
1108     if ($h =~ m@([^.]+\.[^.]+\.[^.]+)$@ &&
1109         $poisoners{$1}) {
1110       LOG (($verbose_filter), "  rejecting poisoner: $_");
1111       next;
1112     }
1113     if ($h =~ m@([^.]+\.[^.]+)$@ &&
1114         $poisoners{$1}) {
1115       LOG (($verbose_filter), "  rejecting poisoner: $_");
1116       next;
1117     }
1118
1119     push @urls2, $_;
1120   }
1121   return @urls2;
1122 }
1123
1124
1125 # given a list of URLs, picks one at random; loads it; and returns a
1126 # random image from it.
1127 # returns the url of the page loaded; the url of the image chosen.
1128 #
1129 sub pick_image_from_pages($$$$@) {
1130   my ($base, $total_hit_count, $unfiltered_link_count, $timeout, @pages) = @_;
1131
1132   $total_hit_count = "?" unless defined($total_hit_count);
1133
1134   @pages = depoison (@pages);
1135   LOG ($verbose_load,
1136        "" . ($#pages+1) . " candidates of $unfiltered_link_count links" .
1137        " ($total_hit_count total)");
1138
1139   return () if ($#pages < 0);
1140
1141   my $i = int(rand($#pages+1));
1142   my $page = $pages[$i];
1143
1144   LOG ($verbose_load, "picked page $page");
1145
1146   $suppress_audit = 1;
1147
1148   my ( $base2, $body2 ) = get_document ($page, $base, $timeout);
1149
1150   if (!$base2 || !$body2) {
1151     $body2 = undef;
1152     return ();
1153   }
1154
1155   my $img = pick_image_from_body ($base2, $body2);
1156   $body2 = undef;
1157
1158   if ($img) {
1159     return ($base2, $img);
1160   } else {
1161     return ();
1162   }
1163 }
1164
1165 \f
1166 ############################################################################
1167 #
1168 # Pick images from random pages returned by the Yahoo Random Link
1169 #
1170 ############################################################################
1171
1172 # yahoorand
1173 my $yahoo_random_link = "http://random.yahoo.com/fast/ryl";
1174
1175
1176 # Picks a random page; picks a random image on that page;
1177 # returns two URLs: the page containing the image, and the image.
1178 # Returns () if nothing found this time.
1179 #
1180 sub pick_from_yahoo_random_link($) {
1181   my ($timeout) = @_;
1182
1183   print STDERR "\n\n" if ($verbose_load);
1184   LOG ($verbose_load, "URL: $yahoo_random_link");
1185
1186   $last_search = $yahoo_random_link;   # for warnings
1187
1188   $suppress_audit = 1;
1189
1190   my ( $base, $body ) = get_document ($yahoo_random_link, undef, $timeout);
1191   if (!$base || !$body) {
1192     $body = undef;
1193     return;
1194   }
1195
1196   LOG ($verbose_load, "redirected to: $base");
1197
1198   my $img = pick_image_from_body ($base, $body);
1199   $body = undef;
1200
1201   if ($img) {
1202     return ($base, $img);
1203   } else {
1204     return ();
1205   }
1206 }
1207
1208 \f
1209 ############################################################################
1210 #
1211 # Pick images from random pages returned by the Alta Vista Random Link
1212 # Note: this seems to have gotten a *lot* less random lately (2007).
1213 #
1214 ############################################################################
1215
1216 # altavista
1217 my $alta_vista_random_link = "http://www.altavista.com/image/randomlink";
1218
1219
1220 # Picks a random page; picks a random image on that page;
1221 # returns two URLs: the page containing the image, and the image.
1222 # Returns () if nothing found this time.
1223 #
1224 sub pick_from_alta_vista_random_link($) {
1225   my ($timeout) = @_;
1226
1227   print STDERR "\n\n" if ($verbose_load);
1228   LOG ($verbose_load, "URL: $alta_vista_random_link");
1229
1230   $last_search = $alta_vista_random_link;   # for warnings
1231
1232   $suppress_audit = 1;
1233
1234   my ( $base, $body ) = get_document ($alta_vista_random_link,
1235                                       undef, $timeout);
1236   if (!$base || !$body) {
1237     $body = undef;
1238     return;
1239   }
1240
1241   LOG ($verbose_load, "redirected to: $base");
1242
1243   my $img = pick_image_from_body ($base, $body);
1244   $body = undef;
1245
1246   if ($img) {
1247     return ($base, $img);
1248   } else {
1249     return ();
1250   }
1251 }
1252
1253 \f
1254 ############################################################################
1255 #
1256 # Pick images by feeding random words into Alta Vista Image Search
1257 #
1258 ############################################################################
1259
1260
1261 my $alta_vista_images_url = "http://www.altavista.com/image/results" .
1262                             "?ipht=1" .       # photos
1263                             "&igrph=1" .      # graphics
1264                             "&iclr=1" .       # color
1265                             "&ibw=1" .        # b&w
1266                             "&micat=1" .      # no partner sites
1267                             "&sc=on" .        # "site collapse"
1268                             "&q=";
1269
1270 # avimages
1271 sub pick_from_alta_vista_images($) {
1272   my ($timeout) = @_;
1273
1274   my $words = random_word();
1275   my $page = (int(rand(9)) + 1);
1276   my $search_url = $alta_vista_images_url . $words;
1277
1278   if ($page > 1) {
1279     $search_url .= "&pgno=" . $page;            # page number
1280     $search_url .= "&stq=" . (($page-1) * 12);  # first hit result on page
1281   }
1282
1283   my ($search_hit_count, @subpages) =
1284     pick_from_search_engine ($timeout, $search_url, $words);
1285
1286   my @candidates = ();
1287   foreach my $u (@subpages) {
1288
1289     # avimages is encoding their URLs now.
1290     next unless ($u =~ s/^.*\*\*(http%3a.*$)/$1/gsi);
1291     $u = url_unquote($u);
1292
1293     next unless ($u =~ m@^http://@i);    #  skip non-HTTP or relative URLs
1294     next if ($u =~ m@[/.]altavista\.com\b@i);     # skip altavista builtins
1295     next if ($u =~ m@[/.]yahoo\.com\b@i);         # yahoo and av in cahoots?
1296     next if ($u =~ m@[/.]doubleclick\.net\b@i);   # you cretins
1297     next if ($u =~ m@[/.]clicktomarket\.com\b@i); # more cretins
1298
1299     next if ($u =~ m@[/.]viewimages\.com\b@i);    # stacked deck
1300     next if ($u =~ m@[/.]gettyimages\.com\b@i);
1301
1302     LOG ($verbose_filter, "  candidate: $u");
1303     push @candidates, $u;
1304   }
1305
1306   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1307                                 $timeout, @candidates);
1308 }
1309
1310
1311 \f
1312 ############################################################################
1313 #
1314 # Pick images from Aptix security cameras
1315 # Cribbed liberally from google image search code.
1316 # By Jason Sullivan <jasonsul@us.ibm.com>
1317 #
1318 ############################################################################
1319
1320 my $aptix_images_url = ("http://www.google.com/search" .
1321                         "?q=inurl:%22jpg/image.jpg%3Fr%3D%22");
1322
1323 # securitycam
1324 sub pick_from_security_camera($) {
1325   my ($timeout) = @_;
1326
1327   my $page = (int(rand(9)) + 1);
1328   my $num = 20;                                 # 20 images per page
1329   my $search_url = $aptix_images_url;
1330
1331   if ($page > 1) {
1332     $search_url .= "&start=" . $page*$num;      # page number
1333     $search_url .= "&num="   . $num;            #images per page
1334   }
1335
1336   my ($search_hit_count, @subpages) =
1337     pick_from_search_engine ($timeout, $search_url, '');
1338
1339   my @candidates = ();
1340   my %referers;
1341   foreach my $u (@subpages) {
1342     next if ($u =~ m@[/.]google\.com\b@i);        # skip google builtins (most links)
1343     next unless ($u =~ m@jpg/image.jpg\?r=@i);    #  All pics contain this
1344
1345     LOG ($verbose_filter, "  candidate: $u");
1346     push @candidates, $u;
1347     $referers{$u} = $u;
1348     }
1349
1350   @candidates = depoison (@candidates);
1351   return () if ($#candidates < 0);
1352   my $i = int(rand($#candidates+1));
1353   my $img = $candidates[$i];
1354   my $ref = $referers{$img};
1355
1356   LOG ($verbose_load, "picked image " . ($i+1) . ": $img (on $ref)");
1357   return ($ref, $img);
1358 }
1359
1360 \f
1361 ############################################################################
1362 #
1363 # Pick images by feeding random words into Google Image Search.
1364 # By Charles Gales <gales@us.ibm.com>
1365 #
1366 ############################################################################
1367
1368
1369 my $google_images_url =     "http://images.google.com/images" .
1370                             "?site=images" .  # photos
1371                             "&btnG=Search" .  # graphics
1372                             "&safe=off" .     # no screening
1373                             "&imgsafe=off" .
1374                             "&q=";
1375
1376 # googleimgs
1377 sub pick_from_google_images($;$$) {
1378   my ($timeout, $words, $max_page) = @_;
1379
1380   if (!defined($words)) {
1381     $words = random_word;   # only one word for Google
1382   }
1383
1384   my $page = (int(rand(9)) + 1);
1385   my $num = 20;     # 20 images per page
1386   my $search_url = $google_images_url . $words;
1387
1388   if ($page > 1) {
1389     $search_url .= "&start=" . $page*$num;      # page number
1390     $search_url .= "&num="   . $num;            #images per page
1391   }
1392
1393   my ($search_hit_count, @subpages) =
1394     pick_from_search_engine ($timeout, $search_url, $words);
1395
1396   my @candidates = ();
1397   my %referers;
1398   foreach my $u (@subpages) {
1399     next unless ($u =~ m@imgres\?imgurl@i);    #  All pics start with this
1400     next if ($u =~ m@[/.]google\.com\b@i);     # skip google builtins
1401
1402     if ($u =~ m@^/imgres\?imgurl=(.*?)\&imgrefurl=(.*?)\&@) {
1403       my $ref = $2;
1404       my $img = $1;
1405       $img = "http://$img" unless ($img =~ m/^http:/i);
1406
1407       LOG ($verbose_filter, "  candidate: $ref");
1408       push @candidates, $img;
1409       $referers{$img} = $ref;
1410     }
1411   }
1412
1413   @candidates = depoison (@candidates);
1414   return () if ($#candidates < 0);
1415   my $i = int(rand($#candidates+1));
1416   my $img = $candidates[$i];
1417   my $ref = $referers{$img};
1418
1419   LOG ($verbose_load, "picked image " . ($i+1) . ": $img (on $ref)");
1420   return ($ref, $img);
1421 }
1422
1423
1424 \f
1425 ############################################################################
1426 #
1427 # Pick images by feeding random numbers into Google Image Search.
1428 # By jwz, suggested by Ian O'Donnell.
1429 #
1430 ############################################################################
1431
1432
1433 # googlenums
1434 sub pick_from_google_image_numbers($) {
1435   my ($timeout) = @_;
1436
1437   my $max = 9999;
1438   my $number = int(rand($max));
1439
1440   $number = sprintf("%04d", $number)
1441     if (rand() < 0.3);
1442
1443   pick_from_google_images ($timeout, "$number");
1444 }
1445
1446
1447 \f
1448 ############################################################################
1449 #
1450 # Pick images by feeding random digital camera file names into 
1451 # Google Image Search.
1452 # By jwz, inspired by the excellent Random Personal Picture Finder
1453 # at http://www.diddly.com/random/
1454 #
1455 ############################################################################
1456
1457 my @photomakers = (
1458   #
1459   # Common digital camera file name formats, as described at
1460   # http://www.diddly.com/random/about.html
1461   #
1462   sub { sprintf ("dcp%05d.jpg",  int(rand(4000))); },   # Kodak
1463   sub { sprintf ("dsc%05d.jpg",  int(rand(4000))); },   # Nikon
1464   sub { sprintf ("dscn%04d.jpg", int(rand(4000))); },   # Nikon
1465   sub { sprintf ("mvc-%03d.jpg", int(rand(999)));  },   # Sony Mavica
1466   sub { sprintf ("mvc%05d.jpg",  int(rand(9999))); },   # Sony Mavica
1467   sub { sprintf ("P101%04d.jpg", int(rand(9999))); },   # Olympus w/ date=101
1468   sub { sprintf ("P%x%02d%04d.jpg",                     # Olympus
1469                  int(rand(0xC)), int(rand(30))+1,
1470                  rand(9999)); },
1471   sub { sprintf ("IMG_%03d.jpg",  int(rand(999))); },   # ?
1472   sub { sprintf ("IMAG%04d.jpg",  int(rand(9999))); },  # RCA and Samsung
1473   sub { my $n = int(rand(9999));                        # Canon
1474           sprintf ("1%02d-%04d.jpg", int($n/100), $n); },
1475   sub { my $n = int(rand(9999));                        # Canon
1476           sprintf ("1%02d-%04d_IMG.jpg",
1477                    int($n/100), $n); },
1478   sub { sprintf ("IMG_%04d.jpg", int(rand(9999))); },   # Canon
1479   sub { sprintf ("dscf%04d.jpg", int(rand(9999))); },   # Fuji Finepix
1480   sub { sprintf ("pdrm%04d.jpg", int(rand(9999))); },   # Toshiba PDR
1481   sub { sprintf ("IM%06d.jpg", int(rand(9999))); },     # HP Photosmart
1482   sub { sprintf ("EX%06d.jpg", int(rand(9999))); },     # HP Photosmart
1483 #  sub { my $n = int(rand(3));                          # Kodak DC-40,50,120
1484 #        sprintf ("DC%04d%s.jpg", int(rand(9999)),
1485 #                 $n == 0 ? 'S' : $n == 1 ? 'M' : 'L'); },
1486   sub { sprintf ("pict%04d.jpg", int(rand(9999))); },   # Minolta Dimage
1487   sub { sprintf ("P%07d.jpg", int(rand(9999))); },      # Kodak DC290
1488 #  sub { sprintf ("%02d%02d%04d.jpg",                   # Casio QV3000, QV4000
1489 #                 int(rand(12))+1, int(rand(31))+1,
1490 #                 int(rand(999))); },
1491 #  sub { sprintf ("%02d%x%02d%04d.jpg",                 # Casio QV7000
1492 #                 int(rand(6)), # year
1493 #                 int(rand(12))+1, int(rand(31))+1,
1494 #                 int(rand(999))); },
1495   sub { sprintf ("IMGP%04d.jpg", int(rand(9999))); },   # Pentax Optio S
1496   sub { sprintf ("PANA%04d.jpg", int(rand(9999))); },   # Panasonic vid still
1497   sub { sprintf ("HPIM%04d.jpg", int(rand(9999))); },   # HP Photosmart
1498   sub { sprintf ("PCDV%04d.jpg", int(rand(9999))); },   # ?
1499  );
1500
1501
1502 # googlephotos
1503 sub pick_from_google_image_photos($) {
1504   my ($timeout) = @_;
1505
1506   my $i = int(rand($#photomakers + 1));
1507   my $fn = $photomakers[$i];
1508   my $file = &$fn;
1509   my $words .= $file . "%20filetype:jpg";
1510
1511   pick_from_google_images ($timeout, $words);
1512 }
1513
1514
1515 \f
1516 ############################################################################
1517 #
1518 # Pick images by feeding random words into Alta Vista Text Search
1519 #
1520 ############################################################################
1521
1522
1523 my $alta_vista_url = "http://www.altavista.com/web/results" .
1524                      "?pg=aq" .
1525                      "&aqmode=s" .
1526                      "&filetype=html" .
1527                      "&sc=on" .        # "site collapse"
1528                      "&nbq=50" .
1529                      "&aqo=";
1530
1531 # avtext
1532 sub pick_from_alta_vista_text($) {
1533   my ($timeout) = @_;
1534
1535   my $words = random_words(0);
1536   my $page = (int(rand(9)) + 1);
1537   my $search_url = $alta_vista_url . $words;
1538
1539   if ($page > 1) {
1540     $search_url .= "&pgno=" . $page;
1541     $search_url .= "&stq=" . (($page-1) * 10);
1542   }
1543
1544   my ($search_hit_count, @subpages) =
1545     pick_from_search_engine ($timeout, $search_url, $words);
1546
1547   my @candidates = ();
1548   foreach my $u (@subpages) {
1549
1550     # Those altavista fuckers are playing really nasty redirection games
1551     # these days: the filter your clicks through their site, but use
1552     # onMouseOver to make it look like they're not!  Well, it makes it
1553     # easier for us to identify search results...
1554     #
1555     next unless ($u =~ s/^.*\*\*(http%3a.*$)/$1/gsi);
1556     $u = url_unquote($u);
1557
1558     next unless ($u =~ m@^http://@i);    #  skip non-HTTP or relative URLs
1559     next if ($u =~ m@[/.]altavista\.com\b@i);     # skip altavista builtins
1560     next if ($u =~ m@[/.]yahoo\.com\b@i);         # yahoo and av in cahoots?
1561
1562     LOG ($verbose_filter, "  candidate: $u");
1563     push @candidates, $u;
1564   }
1565
1566   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1567                                 $timeout, @candidates);
1568 }
1569
1570
1571 \f
1572 ############################################################################
1573 #
1574 # Pick images by feeding random words into Hotbot
1575 #
1576 ############################################################################
1577
1578 my $hotbot_search_url =("http://hotbot.lycos.com/default.asp" .
1579                         "?ca=w" .
1580                         "&descriptiontype=0" .
1581                         "&imagetoggle=1" .
1582                         "&matchmode=any" .
1583                         "&nummod=2" .
1584                         "&recordcount=50" .
1585                         "&sitegroup=1" .
1586                         "&stem=1" .
1587                         "&cobrand=undefined" .
1588                         "&query=");
1589
1590 sub pick_from_hotbot_text($) {
1591   my ($timeout) = @_;
1592
1593   $last_search = $hotbot_search_url;   # for warnings
1594
1595   # lycos seems to always give us back dictionaries and word lists if
1596   # we search for more than one word...
1597   #
1598   my $words = random_word();
1599
1600   my $start = int(rand(8)) * 10 + 1;
1601   my $search_url = $hotbot_search_url . $words . "&first=$start&page=more";
1602
1603   my ($search_hit_count, @subpages) =
1604     pick_from_search_engine ($timeout, $search_url, $words);
1605
1606   my @candidates = ();
1607   foreach my $u (@subpages) {
1608
1609     # Hotbot plays redirection games too
1610     # (not any more?)
1611 #    next unless ($u =~ m@/director.asp\?.*\btarget=([^&]+)@);
1612 #    $u = url_decode($1);
1613
1614     next unless ($u =~ m@^http://@i);    #  skip non-HTTP or relative URLs
1615     next if ($u =~ m@[/.]hotbot\.com\b@i);     # skip hotbot builtins
1616     next if ($u =~ m@[/.]lycos\.com\b@i);      # skip hotbot builtins
1617     next if ($u =~ m@[/.]inktomi\.com\b@i);    # skip hotbot builtins
1618
1619     LOG ($verbose_filter, "  candidate: $u");
1620     push @candidates, $u;
1621   }
1622
1623   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1624                                 $timeout, @candidates);
1625 }
1626
1627
1628 \f
1629 ############################################################################
1630 #
1631 # Pick images by feeding random words into Lycos
1632 #
1633 ############################################################################
1634
1635 my $lycos_search_url = "http://search.lycos.com/default.asp" .
1636                        "?lpv=1" .
1637                        "&loc=searchhp" .
1638                        "&tab=web" .
1639                        "&query=";
1640
1641 sub pick_from_lycos_text($) {
1642   my ($timeout) = @_;
1643
1644   $last_search = $lycos_search_url;   # for warnings
1645
1646   # lycos seems to always give us back dictionaries and word lists if
1647   # we search for more than one word...
1648   #
1649   my $words = random_word();
1650
1651   my $start = int(rand(8)) * 10 + 1;
1652   my $search_url = $lycos_search_url . $words . "&first=$start&page=more";
1653
1654   my ($search_hit_count, @subpages) =
1655     pick_from_search_engine ($timeout, $search_url, $words);
1656
1657   my @candidates = ();
1658   foreach my $u (@subpages) {
1659
1660     # Lycos plays redirection games.
1661     # (not any more?)
1662 #    next unless ($u =~ m@^http://click.lycos.com/director.asp
1663 #                         .*
1664 #                         \btarget=([^&]+)
1665 #                         .*
1666 #                        @x);
1667 #    $u = url_decode($1);
1668
1669     next unless ($u =~ m@^http://@i);    #  skip non-HTTP or relative URLs
1670     next if ($u =~ m@[/.]hotbot\.com\b@i);     # skip lycos builtins
1671     next if ($u =~ m@[/.]lycos\.com\b@i);      # skip lycos builtins
1672     next if ($u =~ m@[/.]terralycos\.com\b@i); # skip lycos builtins
1673     next if ($u =~ m@[/.]inktomi\.com\b@i);    # skip lycos builtins
1674
1675
1676     LOG ($verbose_filter, "  candidate: $u");
1677     push @candidates, $u;
1678   }
1679
1680   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1681                                 $timeout, @candidates);
1682 }
1683
1684
1685 \f
1686 ############################################################################
1687 #
1688 # Pick images by feeding random words into news.yahoo.com
1689 #
1690 ############################################################################
1691
1692 my $yahoo_news_url = "http://news.search.yahoo.com/search/news" .
1693                      "?c=news_photos" .
1694                      "&p=";
1695
1696 # yahoonews
1697 sub pick_from_yahoo_news_text($) {
1698   my ($timeout) = @_;
1699
1700   $last_search = $yahoo_news_url;   # for warnings
1701
1702   my $words = random_word();
1703   my $search_url = $yahoo_news_url . $words;
1704
1705   my ($search_hit_count, @subpages) =
1706     pick_from_search_engine ($timeout, $search_url, $words);
1707
1708   my @candidates = ();
1709   foreach my $u (@subpages) {
1710
1711     # de-redirectize the URLs
1712     $u =~ s@^http://rds\.yahoo\.com/.*-http%3A@http:@s;
1713
1714     # only accept URLs on Yahoo's news site
1715     next unless ($u =~ m@^http://dailynews\.yahoo\.com/@i ||
1716                  $u =~ m@^http://story\.news\.yahoo\.com/@i);
1717     next unless ($u =~ m@&u=/@);
1718
1719     LOG ($verbose_filter, "  candidate: $u");
1720     push @candidates, $u;
1721   }
1722
1723   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1724                                 $timeout, @candidates);
1725 }
1726
1727
1728 \f
1729 ############################################################################
1730 #
1731 # Pick images from LiveJournal's list of recently-posted images.
1732 #
1733 ############################################################################
1734
1735 my $livejournal_img_url = "http://www.livejournal.com/stats/latest-img.bml";
1736
1737 # With most of our image sources, we get a random page and then select
1738 # from the images on it.  However, in the case of LiveJournal, the page
1739 # of images tends to update slowly; so we'll remember the last N entries
1740 # on it and randomly select from those, to get a wider variety each time.
1741
1742 my $lj_cache_size = 1000;
1743 my @lj_cache = (); # fifo, for ordering by age
1744 my %lj_cache = (); # hash, for detecting dups
1745
1746 # livejournal
1747 sub pick_from_livejournal_images($) {
1748   my ($timeout) = @_;
1749
1750   $last_search = $livejournal_img_url;   # for warnings
1751
1752   my ( $base, $body ) = get_document ($livejournal_img_url, undef, $timeout);
1753   return () unless $body;
1754
1755   $body =~ s/\n/ /gs;
1756   $body =~ s/(<recent-image)\b/\n$1/gsi;
1757
1758   foreach (split (/\n/, $body)) {
1759     next unless (m/^<recent-image\b/);
1760     next unless (m/\bIMG=[\'\"]([^\'\"]+)[\'\"]/si);
1761     my $img = html_unquote ($1);
1762
1763     next if ($lj_cache{$img}); # already have it
1764
1765     next unless (m/\bURL=[\'\"]([^\'\"]+)[\'\"]/si);
1766     my $page = html_unquote ($1);
1767     my @pair = ($img, $page);
1768     LOG ($verbose_filter, "  candidate: $img");
1769     push @lj_cache, \@pair;
1770     $lj_cache{$img} = \@pair;
1771   }
1772
1773   return () if ($#lj_cache == -1);
1774
1775   my $n = $#lj_cache+1;
1776   my $i = int(rand($n));
1777   my ($img, $page) = @{$lj_cache[$i]};
1778
1779   # delete this one from @lj_cache and from %lj_cache.
1780   #
1781   @lj_cache = ( @lj_cache[0 .. $i-1],
1782                 @lj_cache[$i+1 .. $#lj_cache] );
1783   delete $lj_cache{$img};
1784
1785   # Keep the size of the cache under the limit by nuking older entries
1786   #
1787   while ($#lj_cache >= $lj_cache_size) {
1788     my $pairP = shift @lj_cache;
1789     my $img = $pairP->[0];
1790     delete $lj_cache{$img};
1791   }
1792
1793   LOG ($verbose_load, "picked image " .($i+1) . "/$n: $img");
1794
1795   return ($page, $img);
1796 }
1797
1798 \f
1799 ############################################################################
1800 #
1801 # Pick images from ircimages.com (images that have been in the /topic of
1802 # various IRC channels.)
1803 #
1804 ############################################################################
1805
1806 my $ircimages_url = "http://ircimages.com/";
1807
1808 # ircimages
1809 sub pick_from_ircimages($) {
1810   my ($timeout) = @_;
1811
1812   $last_search = $ircimages_url;   # for warnings
1813
1814   my $n = int(rand(2900));
1815   my $search_url = $ircimages_url . "page-$n";
1816
1817   my ( $base, $body ) = get_document ($search_url, undef, $timeout);
1818   return () unless $body;
1819
1820   my @candidates = ();
1821
1822   $body =~ s/\n/ /gs;
1823   $body =~ s/(<A)\b/\n$1/gsi;
1824
1825   foreach (split (/\n/, $body)) {
1826
1827     my ($u) = m@<A\s.*\bHREF\s*=\s*([^>]+)>@i;
1828     next unless $u;
1829
1830     if ($u =~ m/^\"([^\"]*)\"/) { $u = $1; }   # quoted string
1831     elsif ($u =~ m/^([^\s]*)\s/) { $u = $1; }  # or token
1832
1833     next unless ($u =~ m/^http:/i);
1834     next if ($u =~ m@^http://(searchirc\.com\|ircimages\.com)@i);
1835     next unless ($u =~ m@[.](gif|jpg|jpeg|pjpg|pjpeg|png)$@i);
1836
1837     LOG ($verbose_http, "    HREF: $u");
1838     push @candidates, $u;
1839   }
1840
1841   LOG ($verbose_filter, "" . $#candidates+1 . " links on $search_url");
1842
1843   return () if ($#candidates == -1);
1844
1845   my $i = int(rand($#candidates+1));
1846   my $img = $candidates[$i];
1847
1848   LOG ($verbose_load, "picked image " .($i+1) . "/" . ($#candidates+1) .
1849        ": $img");
1850
1851   $search_url = $img;  # hmm...
1852   return ($search_url, $img);
1853 }
1854
1855 \f
1856 ############################################################################
1857 #
1858 # Pick images from Flickr's page of recently-posted photos.
1859 #
1860 ############################################################################
1861
1862 my $flickr_img_url = "http://www.flickr.com/photos/";
1863
1864 # Like LiveJournal, the Flickr page of images tends to update slowly,
1865 # so remember the last N entries on it and randomly select from those.
1866
1867 # I know that Flickr has an API (http://www.flickr.com/services/api/)
1868 # but it was easy enough to scrape the HTML, so I didn't bother exploring.
1869
1870 my $flickr_cache_size = 1000;
1871 my @flickr_cache = (); # fifo, for ordering by age
1872 my %flickr_cache = (); # hash, for detecting dups
1873
1874
1875 # flickr_recent
1876 sub pick_from_flickr_recent($) {
1877   my ($timeout) = @_;
1878
1879   my $start = 16 * int(rand(100));
1880
1881   $last_search = $flickr_img_url;   # for warnings
1882   $last_search .= "?start=$start" if ($start > 0);
1883
1884   my ( $base, $body ) = get_document ($last_search, undef, $timeout);
1885   return () unless $body;
1886
1887   $body =~ s/[\r\n]/ /gs;
1888   $body =~ s/(<a)\b/\n$1/gsi;
1889
1890   my $count = 0;
1891   my $count2 = 0;
1892   foreach (split (/\n/, $body)) {
1893     my ($page, $thumb) = m@<A \s [^<>]* \b HREF=\"([^<>\"]+)\" [^<>]* > \s*
1894                            <IMG \s [^<>]* \b SRC=\"([^<>\"]+)\" @xsi;
1895     next unless defined ($thumb);
1896     $page = html_unquote ($page);
1897     $thumb = html_unquote ($thumb);
1898
1899     next unless ($thumb =~ m@^http://farm\d*\.static\.flickr\.com/@);
1900
1901     my $base = "http://www.flickr.com/";
1902     $page  =~ s@^/@$base@;
1903     $thumb =~ s@^/@$base@;
1904
1905     my $img = $thumb;
1906     $img =~ s/_[a-z](\.[a-z\d]+)$/$1/si;  # take off "thumb" suffix
1907
1908     $count++;
1909     next if ($flickr_cache{$img}); # already have it
1910
1911     my @pair = ($img, $page, $start);
1912     LOG ($verbose_filter, "  candidate: $img");
1913     push @flickr_cache, \@pair;
1914     $flickr_cache{$img} = \@pair;
1915     $count2++;
1916   }
1917
1918   return () if ($#flickr_cache == -1);
1919
1920   my $n = $#flickr_cache+1;
1921   my $i = int(rand($n));
1922   my ($img, $page) = @{$flickr_cache[$i]};
1923
1924   # delete this one from @flickr_cache and from %flickr_cache.
1925   #
1926   @flickr_cache = ( @flickr_cache[0 .. $i-1],
1927                     @flickr_cache[$i+1 .. $#flickr_cache] );
1928   delete $flickr_cache{$img};
1929
1930   # Keep the size of the cache under the limit by nuking older entries
1931   #
1932   while ($#flickr_cache >= $flickr_cache_size) {
1933     my $pairP = shift @flickr_cache;
1934     my $img = $pairP->[0];
1935     delete $flickr_cache{$img};
1936   }
1937
1938   LOG ($verbose_load, "picked image " .($i+1) . "/$n: $img");
1939
1940   return ($page, $img);
1941 }
1942
1943 \f
1944 ############################################################################
1945 #
1946 # Pick images from a random RSS feed on Flickr.
1947 #
1948 ############################################################################
1949
1950 my $flickr_rss_base = ("http://www.flickr.com/services/feeds/photos_public.gne" .
1951                        "?format=rss_200_enc&tags=");
1952
1953 # Picks a random RSS feed; picks a random image from that feed;
1954 # returns 2 URLs: the page containing the image, and the image.
1955 # Mostly by Joe Mcmahon <mcmahon@yahoo-inc.com>
1956 #
1957 # flickr_random
1958 sub pick_from_flickr_random($) {
1959   my $timeout = shift;
1960
1961   my $rss = $flickr_rss_base . random_word();
1962   $last_search = $rss;
1963
1964   print STDERR "\n\n" if ($verbose_load);
1965   LOG ($verbose_load, "URL: $last_search");
1966
1967   $suppress_audit = 1;
1968
1969   my ( $base, $body ) = get_document ($last_search, undef, $timeout);
1970   if (!$base || !$body) {
1971     $body = undef;
1972     return;
1973   }
1974
1975   my $img;
1976   ($base, $img) = pick_image_from_rss ($base, $body);
1977   $body = undef;
1978   return () unless defined ($img);
1979
1980   LOG ($verbose_load, "redirected to: $base");
1981   return ($base, $img);
1982 }
1983
1984 \f
1985 ############################################################################
1986 #
1987 # Pick images by waiting for driftnet to populate a temp dir with files.
1988 # Requires driftnet version 0.1.5 or later.
1989 # (Driftnet is a program by Chris Lightfoot that sniffs your local ethernet
1990 # for images being downloaded by others.)
1991 # Driftnet/webcollage integration by jwz.
1992 #
1993 ############################################################################
1994
1995 # driftnet
1996 sub pick_from_driftnet($) {
1997   my ($timeout) = @_;
1998
1999   my $id = $driftnet_magic;
2000   my $dir = $driftnet_dir;
2001   my $start = time;
2002   my $now;
2003
2004   error ("\$driftnet_dir unset?") unless ($dir);
2005   $dir =~ s@/+$@@;
2006
2007   error ("$dir unreadable") unless (-d "$dir/.");
2008
2009   $timeout = $http_timeout unless ($timeout);
2010   $last_search = $id;
2011
2012   while ($now = time, $now < $start + $timeout) {
2013     local *DIR;
2014     opendir (DIR, $dir) || error ("$dir: $!");
2015     while (my $file = readdir(DIR)) {
2016       next if ($file =~ m/^\./);
2017       $file = "$dir/$file";
2018       closedir DIR;
2019       LOG ($verbose_load, "picked file $file ($id)");
2020       return ($id, $file);
2021     }
2022     closedir DIR;
2023   }
2024   LOG (($verbose_net || $verbose_load), "timed out for $id");
2025   return ();
2026 }
2027
2028
2029 sub get_driftnet_file($) {
2030   my ($file) = @_;
2031
2032   error ("\$driftnet_dir unset?") unless ($driftnet_dir);
2033
2034   my $id = $driftnet_magic;
2035   my $re = qr/$driftnet_dir/;
2036   error ("$id: $file not in $driftnet_dir?")
2037     unless ($file =~ m@^$re@o);
2038
2039   local *IN;
2040   open (IN, $file) || error ("$id: $file: $!");
2041   my $body = '';
2042   while (<IN>) { $body .= $_; }
2043   close IN || error ("$id: $file: $!");
2044   unlink ($file) || error ("$id: $file: rm: $!");
2045   return ($id, $body);
2046 }
2047
2048
2049 sub spawn_driftnet($) {
2050   my ($cmd) = @_;
2051
2052   # make a directory to use.
2053   while (1) {
2054     my $tmp = $ENV{TEMPDIR} || "/tmp";
2055     $driftnet_dir = sprintf ("$tmp/driftcollage-%08x", rand(0xffffffff));
2056     LOG ($verbose_exec, "mkdir $driftnet_dir");
2057     last if mkdir ($driftnet_dir, 0700);
2058   }
2059
2060   if (! ($cmd =~ m/\s/)) {
2061     # if the command didn't have any arguments in it, then it must be just
2062     # a pointer to the executable.  Append the default args to it.
2063     my $dargs = $default_driftnet_cmd;
2064     $dargs =~ s/^[^\s]+//;
2065     $cmd .= $dargs;
2066   }
2067
2068   # point the driftnet command at our newly-minted private directory.
2069   #
2070   $cmd .= " -d $driftnet_dir";
2071   $cmd .= ">/dev/null" unless ($verbose_exec);
2072
2073   my $pid = fork();
2074   if ($pid < 0) { error ("fork: $!\n"); }
2075   if ($pid) {
2076     # parent fork
2077     push @pids_to_kill, $pid;
2078     LOG ($verbose_exec, "forked for \"$cmd\"");
2079   } else {
2080     # child fork
2081     nontrapping_system ($cmd) || error ("exec: $!");
2082   }
2083
2084   # wait a bit, then make sure the process actually started up.
2085   #
2086   sleep (1);
2087   error ("pid $pid failed to start \"$cmd\"")
2088     unless (1 == kill (0, $pid));
2089 }
2090
2091 # local-directory
2092 sub pick_from_local_dir {
2093   my ( $timeout ) = @_;
2094
2095   my $id = $local_magic;
2096   $last_search = $id;
2097
2098   my $dir = $local_dir;
2099   error ("\$local_dir unset?") unless ($dir);
2100   $dir =~ s@/+$@@;
2101
2102   error ("$dir unreadable") unless (-d "$dir/.");
2103
2104   my $v = ($verbose_exec ? "-v" : "");
2105   my $pick = `xscreensaver-getimage-file $v "$dir"`;
2106
2107   LOG ($verbose_load, "picked file $pick ($id)");
2108   return ($id, $pick);
2109 }
2110
2111
2112 sub get_local_file {
2113   my ($file) = @_;
2114
2115   error ("\$local_dir unset?") unless ($local_dir);
2116
2117   my $id = $local_magic;
2118   my $re = qr/$local_dir/;
2119   error ("$id: $file not in $local_dir?")
2120     unless ($file =~ m@^$re@o);
2121
2122   local *IN;
2123   open (IN, $file) || error ("$id: $file: $!");
2124   my $body = '';
2125   while (<IN>) { $body .= $_; }
2126   close IN || error ("$id: $file: $!");
2127   return ($id, $body);
2128 }
2129
2130
2131 \f
2132 ############################################################################
2133 #
2134 # Pick a random image in a random way
2135 #
2136 ############################################################################
2137
2138
2139 # Picks a random image on a random page, and returns two URLs:
2140 # the page containing the image, and the image.
2141 # Returns () if nothing found this time.
2142 #
2143
2144 sub pick_image(;$) {
2145   my ($timeout) = @_;
2146
2147   $current_state = "select";
2148   $load_method = "none";
2149
2150   my $n = int(rand(100));
2151   my $fn = undef;
2152   my $total = 0;
2153   my @rest = @search_methods;
2154
2155   while (@rest) {
2156     my $pct  = shift @rest;
2157     my $name = shift @rest;
2158     my $tfn  = shift @rest;
2159     $total += $pct;
2160     if ($total > $n && !defined($fn)) {
2161       $fn = $tfn;
2162       $current_state = $name;
2163       $load_method = $current_state;
2164     }
2165   }
2166
2167   if ($total != 100) {
2168     error ("internal error: \@search_methods totals to $total%!");
2169   }
2170
2171   record_attempt ($current_state);
2172   return $fn->($timeout);
2173 }
2174
2175
2176 \f
2177 ############################################################################
2178 #
2179 # Statistics and logging
2180 #
2181 ############################################################################
2182
2183 sub timestr() {
2184   return strftime ("%H:%M:%S: ", localtime);
2185 }
2186
2187 sub blurb() {
2188   return "$progname: " . timestr() . "$current_state: ";
2189 }
2190
2191 sub error($) {
2192   my ($err) = @_;
2193   print STDERR blurb() . "$err\n";
2194   exit 1;
2195 }
2196
2197 sub stacktrace() {
2198   my $i = 1;
2199   print STDERR "$progname: stack trace:\n";
2200   while (1) {
2201     my ($package, $filename, $line, $subroutine) = caller($i++);
2202     last unless defined($package);
2203     $filename =~ s@^.*/@@;
2204     print STDERR "  $filename#$line, $subroutine\n";
2205   }
2206 }
2207
2208
2209 my $lastlog = "";
2210
2211 sub clearlog() {
2212   $lastlog = "";
2213 }
2214
2215 sub showlog() {
2216   my $head = "$progname: DEBUG: ";
2217   foreach (split (/\n/, $lastlog)) {
2218     print STDERR "$head$_\n";
2219   }
2220   $lastlog = "";
2221 }
2222
2223 sub LOG($$) {
2224   my ($print, $msg) = @_;
2225   my $blurb = timestr() . "$current_state: ";
2226   $lastlog .= "$blurb$msg\n";
2227   print STDERR "$progname: $blurb$msg\n" if $print;
2228 }
2229
2230
2231 my %stats_attempts;
2232 my %stats_successes;
2233 my %stats_elapsed;
2234
2235 my $last_state = undef;
2236 sub record_attempt($) {
2237   my ($name) = @_;
2238
2239   if ($last_state) {
2240     record_failure($last_state) unless ($image_succeeded > 0);
2241   }
2242   $last_state = $name;
2243
2244   clearlog();
2245   report_performance();
2246
2247   start_timer($name);
2248   $image_succeeded = 0;
2249   $suppress_audit = 0;
2250 }
2251
2252 sub record_success($$$) {
2253   my ($name, $url, $base) = @_;
2254   if (defined($stats_successes{$name})) {
2255     $stats_successes{$name}++;
2256   } else {
2257     $stats_successes{$name} = 1;
2258   }
2259
2260   stop_timer ($name, 1);
2261   my $o = $current_state;
2262   $current_state = $name;
2263   save_recent_url ($url, $base);
2264   $current_state = $o;
2265   $image_succeeded = 1;
2266   clearlog();
2267 }
2268
2269
2270 sub record_failure($) {
2271   my ($name) = @_;
2272
2273   return if $image_succeeded;
2274
2275   stop_timer ($name, 0);
2276   if ($verbose_load && !$verbose_exec) {
2277
2278     if ($suppress_audit) {
2279       print STDERR "$progname: " . timestr() . "(audit log suppressed)\n";
2280       return;
2281     }
2282
2283     my $o = $current_state;
2284     $current_state = "DEBUG";
2285
2286     my $line =  "#" x 78;
2287     print STDERR "\n\n\n";
2288     print STDERR ("#" x 78) . "\n";
2289     print STDERR blurb() . "failed to get an image.  Full audit log:\n";
2290     print STDERR "\n";
2291     showlog();
2292     print STDERR ("-" x 78) . "\n";
2293     print STDERR "\n\n";
2294
2295     $current_state = $o;
2296   }
2297   $image_succeeded = 0;
2298 }
2299
2300
2301
2302 sub stats_of($) {
2303   my ($name) = @_;
2304   my $i = $stats_successes{$name};
2305   my $j = $stats_attempts{$name};
2306   $i = 0 unless $i;
2307   $j = 0 unless $j;
2308   return "" . ($j ? int($i * 100 / $j) : "0") . "%";
2309 }
2310
2311
2312 my $current_start_time = 0;
2313
2314 sub start_timer($) {
2315   my ($name) = @_;
2316   $current_start_time = time;
2317
2318   if (defined($stats_attempts{$name})) {
2319     $stats_attempts{$name}++;
2320   } else {
2321     $stats_attempts{$name} = 1;
2322   }
2323   if (!defined($stats_elapsed{$name})) {
2324     $stats_elapsed{$name} = 0;
2325   }
2326 }
2327
2328 sub stop_timer($$) {
2329   my ($name, $success) = @_;
2330   $stats_elapsed{$name} += time - $current_start_time;
2331 }
2332
2333
2334 my $last_report_time = 0;
2335 sub report_performance() {
2336
2337   return unless $verbose_warnings;
2338
2339   my $now = time;
2340   return unless ($now >= $last_report_time + $report_performance_interval);
2341   my $ot = $last_report_time;
2342   $last_report_time = $now;
2343
2344   return if ($ot == 0);
2345
2346   my $blurb = "$progname: " . timestr();
2347
2348   print STDERR "\n";
2349   print STDERR "${blurb}Current standings:\n";
2350
2351   foreach my $name (sort keys (%stats_attempts)) {
2352     my $try = $stats_attempts{$name};
2353     my $suc = $stats_successes{$name} || 0;
2354     my $pct = int($suc * 100 / $try);
2355     my $secs = $stats_elapsed{$name};
2356     my $secs_link = $secs / $try;
2357     print STDERR sprintf ("$blurb %-14s %4s (%d/%d);" .
2358                           "       \t %.1f secs/link\n",
2359                           "$name:", "$pct%", $suc, $try, $secs_link);
2360   }
2361 }
2362
2363
2364
2365 my $max_recent_images = 400;
2366 my $max_recent_sites  = 20;
2367 my @recent_images = ();
2368 my @recent_sites = ();
2369
2370 sub save_recent_url($$) {
2371   my ($url, $base) = @_;
2372
2373   return unless ($verbose_warnings);
2374
2375   $_ = $url;
2376   my ($site) = m@^http://([^ \t\n\r/:]+)@;
2377   return unless defined ($site);
2378
2379   if ($base eq $driftnet_magic || $base eq $local_magic) {
2380     $site = $base;
2381     @recent_images = ();
2382   }
2383
2384   my $done = 0;
2385   foreach (@recent_images) {
2386     if ($_ eq $url) {
2387       print STDERR blurb() . "WARNING: recently-duplicated image: $url" .
2388         " (on $base via $last_search)\n";
2389       $done = 1;
2390       last;
2391     }
2392   }
2393
2394   # suppress "duplicate site" warning via %warningless_sites.
2395   #
2396   if ($warningless_sites{$site}) {
2397     $done = 1;
2398   } elsif ($site =~ m@([^.]+\.[^.]+\.[^.]+)$@ &&
2399            $warningless_sites{$1}) {
2400     $done = 1;
2401   } elsif ($site =~ m@([^.]+\.[^.]+)$@ &&
2402            $warningless_sites{$1}) {
2403     $done = 1;
2404   }
2405
2406   if (!$done) {
2407     foreach (@recent_sites) {
2408       if ($_ eq $site) {
2409         print STDERR blurb() . "WARNING: recently-duplicated site: $site" .
2410         " ($url on $base via $last_search)\n";
2411         last;
2412       }
2413     }
2414   }
2415
2416   push @recent_images, $url;
2417   push @recent_sites,  $site;
2418   shift @recent_images if ($#recent_images >= $max_recent_images);
2419   shift @recent_sites  if ($#recent_sites  >= $max_recent_sites);
2420 }
2421
2422
2423 \f
2424 ##############################################################################
2425 #
2426 # other utilities
2427 #
2428 ##############################################################################
2429
2430 # Does %-decoding.
2431 #
2432 sub url_decode($) {
2433   ($_) = @_;
2434   tr/+/ /;
2435   s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
2436   return $_;
2437 }
2438
2439
2440 # Given the raw body of a GIF document, returns the dimensions of the image.
2441 #
2442 sub gif_size($) {
2443   my ($body) = @_;
2444   my $type = substr($body, 0, 6);
2445   my $s;
2446   return () unless ($type =~ /GIF8[7,9]a/);
2447   $s = substr ($body, 6, 10);
2448   my ($a,$b,$c,$d) = unpack ("C"x4, $s);
2449   return () unless defined ($d);
2450   return (($b<<8|$a), ($d<<8|$c));
2451 }
2452
2453 # Given the raw body of a JPEG document, returns the dimensions of the image.
2454 #
2455 sub jpeg_size($) {
2456   my ($body) = @_;
2457   my $i = 0;
2458   my $L = length($body);
2459
2460   my $c1 = substr($body, $i, 1); $i++;
2461   my $c2 = substr($body, $i, 1); $i++;
2462   return () unless (ord($c1) == 0xFF && ord($c2) == 0xD8);
2463
2464   my $ch = "0";
2465   while (ord($ch) != 0xDA && $i < $L) {
2466     # Find next marker, beginning with 0xFF.
2467     while (ord($ch) != 0xFF) {
2468       return () if (length($body) <= $i);
2469       $ch = substr($body, $i, 1); $i++;
2470     }
2471     # markers can be padded with any number of 0xFF.
2472     while (ord($ch) == 0xFF) {
2473       return () if (length($body) <= $i);
2474       $ch = substr($body, $i, 1); $i++;
2475     }
2476
2477     # $ch contains the value of the marker.
2478     my $marker = ord($ch);
2479
2480     if (($marker >= 0xC0) &&
2481         ($marker <= 0xCF) &&
2482         ($marker != 0xC4) &&
2483         ($marker != 0xCC)) {  # it's a SOFn marker
2484       $i += 3;
2485       return () if (length($body) <= $i);
2486       my $s = substr($body, $i, 4); $i += 4;
2487       my ($a,$b,$c,$d) = unpack("C"x4, $s);
2488       return (($c<<8|$d), ($a<<8|$b));
2489
2490     } else {
2491       # We must skip variables, since FFs in variable names aren't
2492       # valid JPEG markers.
2493       return () if (length($body) <= $i);
2494       my $s = substr($body, $i, 2); $i += 2;
2495       my ($c1, $c2) = unpack ("C"x2, $s);
2496       my $length = ($c1 << 8) | $c2;
2497       return () if ($length < 2);
2498       $i += $length-2;
2499     }
2500   }
2501   return ();
2502 }
2503
2504 # Given the raw body of a PNG document, returns the dimensions of the image.
2505 #
2506 sub png_size($) {
2507   my ($body) = @_;
2508   return () unless ($body =~ m/^\211PNG\r/);
2509   my ($bits) = ($body =~ m/^.{12}(.{12})/s);
2510   return () unless defined ($bits);
2511   return () unless ($bits =~ /^IHDR/);
2512   my ($ign, $w, $h) = unpack("a4N2", $bits);
2513   return ($w, $h);
2514 }
2515
2516
2517 # Given the raw body of a GIF, JPEG, or PNG document, returns the dimensions
2518 # of the image.
2519 #
2520 sub image_size($) {
2521   my ($body) = @_;
2522   my ($w, $h) = gif_size ($body);
2523   if ($w && $h) { return ($w, $h); }
2524   ($w, $h) = jpeg_size ($body);
2525   if ($w && $h) { return ($w, $h); }
2526   return png_size ($body);
2527 }
2528
2529
2530 # returns the full path of the named program, or undef.
2531 #
2532 sub which($) {
2533   my ($prog) = @_;
2534   foreach (split (/:/, $ENV{PATH})) {
2535     if (-x "$_/$prog") {
2536       return $prog;
2537     }
2538   }
2539   return undef;
2540 }
2541
2542
2543 # Like rand(), but chooses numbers with a bell curve distribution.
2544 sub bellrand(;$) {
2545   ($_) = @_;
2546   $_ = 1.0 unless defined($_);
2547   $_ /= 3.0;
2548   return (rand($_) + rand($_) + rand($_));
2549 }
2550
2551
2552 sub exit_cleanup() {
2553   x_cleanup();
2554   print STDERR "$progname: exiting\n" if ($verbose_warnings);
2555   if (@pids_to_kill) {
2556     print STDERR blurb() . "killing: " . join(' ', @pids_to_kill) . "\n";
2557     kill ('TERM', @pids_to_kill);
2558   }
2559 }
2560
2561 sub signal_cleanup($) {
2562   my ($sig) = @_;
2563   print STDERR blurb() . (defined($sig)
2564                           ? "caught signal $sig."
2565                           : "exiting.")
2566                        . "\n"
2567     if ($verbose_exec || $verbose_warnings);
2568   exit 1;
2569 }
2570
2571
2572
2573 ##############################################################################
2574 #
2575 # Generating a list of urls only
2576 #
2577 ##############################################################################
2578
2579 sub url_only_output() {
2580   do {
2581     my ($base, $img) = pick_image;
2582     if ($img) {
2583       $base =~ s/ /%20/g;
2584       $img  =~ s/ /%20/g;
2585       print "$img $base\n";
2586     }
2587   } while (1);
2588 }
2589
2590 ##############################################################################
2591 #
2592 # Running as an xscreensaver module, or as a web page imagemap
2593 #
2594 ##############################################################################
2595
2596 my $image_ppm   = sprintf ("%s/webcollage-%08x",
2597                            ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp"),
2598                            rand(0xFFFFFFFF));
2599 my $image_tmp1  = sprintf ("%s/webcollage-1-%08x",
2600                            ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp"),
2601                            rand(0xFFFFFFFF));
2602 my $image_tmp2  = sprintf ("%s/webcollage-2-%08x",
2603                            ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp"),
2604                            rand(0xFFFFFFFF));
2605
2606 my $filter_cmd = undef;
2607 my $post_filter_cmd = undef;
2608 my $background = undef;
2609
2610 my @imagemap_areas = ();
2611 my $imagemap_html_tmp = undef;
2612 my $imagemap_jpg_tmp = undef;
2613
2614
2615 my $img_width;            # size of the image being generated.
2616 my $img_height;
2617
2618 my $delay = 2;
2619
2620 sub x_cleanup() {
2621   unlink $image_ppm, $image_tmp1, $image_tmp2;
2622   unlink $imagemap_html_tmp, $imagemap_jpg_tmp
2623     if (defined ($imagemap_html_tmp));
2624 }
2625
2626
2627 # Like system, but prints status about exit codes, and kills this process
2628 # with whatever signal killed the sub-process, if any.
2629 #
2630 sub nontrapping_system(@) {
2631   $! = 0;
2632
2633   $_ = join(" ", @_);
2634   s/\"[^\"]+\"/\"...\"/g;
2635
2636   LOG ($verbose_exec, "executing \"$_\"");
2637
2638   my $rc = system @_;
2639
2640   if ($rc == 0) {
2641     LOG ($verbose_exec, "subproc exited normally.");
2642   } elsif (($rc & 0xff) == 0) {
2643     $rc >>= 8;
2644     LOG ($verbose_exec, "subproc exited with status $rc.");
2645   } else {
2646     if ($rc & 0x80) {
2647       LOG ($verbose_exec, "subproc dumped core.");
2648       $rc &= ~0x80;
2649     }
2650     LOG ($verbose_exec, "subproc died with signal $rc.");
2651     # die that way ourselves.
2652     kill $rc, $$;
2653   }
2654
2655   return $rc;
2656 }
2657
2658
2659 # Given the URL of a GIF, JPEG, or PNG image, and the body of that image,
2660 # writes a PPM to the given output file.  Returns the width/height of the
2661 # image if successful.
2662 #
2663 sub image_to_pnm($$$) {
2664   my ($url, $body, $output) = @_;
2665   my ($cmd, $cmd2, $w, $h);
2666
2667   if ((@_ = gif_size ($body))) {
2668     ($w, $h) = @_;
2669     $cmd = "giftopnm";
2670   } elsif ((@_ = jpeg_size ($body))) {
2671     ($w, $h) = @_;
2672     $cmd = "djpeg";
2673   } elsif ((@_ = png_size ($body))) {
2674     ($w, $h) = @_;
2675     $cmd = "pngtopnm";
2676   } else {
2677     LOG (($verbose_pbm || $verbose_load),
2678          "not a GIF, JPG, or PNG" .
2679          (($body =~ m@<(base|html|head|body|script|table|a href)\b@i)
2680           ? " (looks like HTML)" : "") .
2681          ": $url");
2682     $suppress_audit = 1;
2683     return ();
2684   }
2685
2686   $cmd2 = "exec $cmd";        # yes, this really is necessary.  if we don't
2687                               # do this, the process doesn't die properly.
2688   if (!$verbose_pbm) {
2689     #
2690     # We get a "giftopnm: got a 'Application Extension' extension"
2691     # warning any time it's an animgif.
2692     #
2693     # Note that "giftopnm: EOF / read error on image data" is not
2694     # always a fatal error -- sometimes the image looks fine anyway.
2695     #
2696     $cmd2 .= " 2>/dev/null";
2697   }
2698
2699   # There exist corrupted GIF and JPEG files that can make giftopnm and
2700   # djpeg lose their minds and go into a loop.  So this gives those programs
2701   # a small timeout -- if they don't complete in time, kill them.
2702   #
2703   my $pid;
2704   @_ = eval {
2705     my $timed_out;
2706
2707     local $SIG{ALRM}  = sub {
2708       LOG ($verbose_pbm,
2709            "timed out ($cvt_timeout) for $cmd on \"$url\" in pid $pid");
2710       kill ('TERM', $pid) if ($pid);
2711       $timed_out = 1;
2712       $body = undef;
2713     };
2714
2715     if (($pid = open(PIPE, "| $cmd2 > $output"))) {
2716       $timed_out = 0;
2717       alarm $cvt_timeout;
2718       print PIPE $body;
2719       $body = undef;
2720       close PIPE;
2721
2722       LOG ($verbose_exec, "awaiting $pid");
2723       waitpid ($pid, 0);
2724       LOG ($verbose_exec, "$pid completed");
2725
2726       my $size = (stat($output))[7];
2727       $size = -1 unless defined($size);
2728       if ($size < 5) {
2729         LOG ($verbose_pbm, "$cmd on ${w}x$h \"$url\" failed ($size bytes)");
2730         return ();
2731       }
2732
2733       LOG ($verbose_pbm, "created ${w}x$h $output ($cmd)");
2734       return ($w, $h);
2735     } else {
2736       print STDERR blurb() . "$cmd failed: $!\n";
2737       return ();
2738     }
2739   };
2740   die if ($@ && $@ ne "alarm\n");       # propagate errors
2741   if ($@) {
2742     # timed out
2743     $body = undef;
2744     return ();
2745   } else {
2746     # didn't
2747     alarm 0;
2748     $body = undef;
2749     return @_;
2750   }
2751 }
2752
2753
2754 # Same as the "ppmmake" command: creates a solid-colored PPM.
2755 # Does not understand the rgb.txt color names except "black" and "white".
2756 #
2757 sub ppmmake($$$$) {
2758   my ($outfile, $bgcolor, $w, $h) = @_;
2759
2760   my ($r, $g, $b);
2761   if ($bgcolor =~ m/^\#?([\dA-F][\dA-F])([\dA-F][\dA-F])([\dA-F][\dA-F])$/i ||
2762       $bgcolor =~ m/^\#?([\dA-F])([\dA-F])([\dA-F])$/i) {
2763     ($r, $g, $b) = (hex($1), hex($2), hex($3));
2764   } elsif ($bgcolor =~ m/^black$/i) {
2765     ($r, $g, $b) = (0, 0, 0);
2766   } elsif ($bgcolor =~ m/^white$/i) {
2767     ($r, $g, $b) = (0xFF, 0xFF, 0xFF);
2768   } else {
2769     error ("unparsable color name: $bgcolor");
2770   }
2771
2772   my $pixel = pack('CCC', $r, $g, $b);
2773   my $bits = "P6\n$w $h\n255\n" . ($pixel x ($w * $h));
2774
2775   local *OUT;
2776   open (OUT, ">$outfile") || error ("$outfile: $!");
2777   print OUT $bits;
2778   close OUT;
2779 }
2780
2781
2782 sub pick_root_displayer() {
2783   my @names = ();
2784
2785   if ($cocoa_p) {
2786     # see "xscreensaver/hacks/webcollage-cocoa.m"
2787     return "echo COCOA LOAD ";
2788   }
2789
2790   foreach my $cmd (@root_displayers) {
2791     $_ = $cmd;
2792     my ($name) = m/^([^ ]+)/;
2793     push @names, "\"$name\"";
2794     LOG ($verbose_exec, "looking for $name...");
2795     foreach my $dir (split (/:/, $ENV{PATH})) {
2796       LOG ($verbose_exec, "  checking $dir/$name");
2797       return $cmd if (-x "$dir/$name");
2798     }
2799   }
2800
2801   $names[$#names] = "or " . $names[$#names];
2802   error "none of: " . join (", ", @names) . " were found on \$PATH.";
2803 }
2804
2805
2806 my $ppm_to_root_window_cmd = undef;
2807
2808
2809 sub x_or_pbm_output($) {
2810   my ($window_id) = @_;
2811
2812   # Check for our helper program, to see whether we need to use PPM pipelines.
2813   #
2814   $_ = "webcollage-helper";
2815   if (defined ($webcollage_helper) || which ($_)) {
2816     $webcollage_helper = $_ unless (defined($webcollage_helper));
2817     LOG ($verbose_pbm, "found \"$webcollage_helper\"");
2818     $webcollage_helper .= " -v";
2819   } else {
2820     LOG (($verbose_pbm || $verbose_load), "no $_ program");
2821   }
2822
2823   if ($cocoa_p && !defined ($webcollage_helper)) {
2824     error ("webcollage-helper not found in Cocoa-mode!");
2825   }
2826
2827
2828   # make sure the various programs we execute exist, right up front.
2829   #
2830   my @progs = ();
2831
2832   if (!defined($webcollage_helper)) {
2833     # Only need these others if we don't have the helper.
2834     @progs = (@progs,
2835               "giftopnm", "pngtopnm", "djpeg",
2836               "pnmpaste", "pnmscale", "pnmcut");
2837   }
2838
2839   foreach (@progs) {
2840     which ($_) || error "$_ not found on \$PATH.";
2841   }
2842
2843   # find a root-window displayer program.
2844   #
2845   if (!$no_output_p) {
2846     $ppm_to_root_window_cmd = pick_root_displayer();
2847   }
2848
2849   if (defined ($window_id)) {
2850     error ("-window-id only works if xscreensaver-getimage is installed")
2851       unless ($ppm_to_root_window_cmd =~ m/^xscreensaver-getimage\b/);
2852
2853     error ("unparsable window id: $window_id")
2854       unless ($window_id =~ m/^\d+$|^0x[\da-f]+$/i);
2855     $ppm_to_root_window_cmd =~ s/--?root\b/$window_id/ ||
2856       error ("unable to munge displayer: $ppm_to_root_window_cmd");
2857   }
2858
2859   if (!$img_width || !$img_height) {
2860
2861     if (!defined ($window_id) &&
2862         defined ($ENV{XSCREENSAVER_WINDOW})) {
2863       $window_id = $ENV{XSCREENSAVER_WINDOW};
2864     }
2865
2866     if (!defined ($window_id)) {
2867       $_ = "xdpyinfo";
2868       which ($_) || error "$_ not found on \$PATH.";
2869       $_ = `$_`;
2870       ($img_width, $img_height) = m/dimensions: *(\d+)x(\d+) /;
2871       if (!defined($img_height)) {
2872         error "xdpyinfo failed.";
2873       }
2874     } else {  # we have a window id
2875       $_ = "xwininfo";
2876       which ($_) || error "$_ not found on \$PATH.";
2877       $_ .= " -id $window_id";
2878       $_ = `$_`;
2879       ($img_width, $img_height) = m/^\s*Width:\s*(\d+)\n\s*Height:\s*(\d+)\n/m;
2880
2881       if (!defined($img_height)) {
2882         error "xwininfo failed.";
2883       }
2884     }
2885   }
2886
2887   my $bgcolor = "#000000";
2888   my $bgimage = undef;
2889
2890   if ($background) {
2891     if ($background =~ m/^\#[0-9a-f]+$/i) {
2892       $bgcolor = $background;
2893
2894     } elsif (-r $background) {
2895       $bgimage = $background;
2896
2897     } elsif (! $background =~ m@^[-a-z0-9 ]+$@i) {
2898       error "not a color or readable file: $background";
2899
2900     } else {
2901       # default to assuming it's a color
2902       $bgcolor = $background;
2903     }
2904   }
2905
2906   # Create the sold-colored base image.
2907   #
2908   LOG ($verbose_pbm, "creating base image: ${img_width}x${img_height}");
2909   $_ = ppmmake ($image_ppm, $bgcolor, $img_width, $img_height);
2910
2911   # Paste the default background image in the middle of it.
2912   #
2913   if ($bgimage) {
2914     my ($iw, $ih);
2915
2916     my $body = "";
2917     local *IMG;
2918     open(IMG, "<$bgimage") || error "couldn't open $bgimage: $!";
2919     my $cmd;
2920     while (<IMG>) { $body .= $_; }
2921     close (IMG);
2922
2923     if ((@_ = gif_size ($body))) {
2924       ($iw, $ih) = @_;
2925       $cmd = "giftopnm |";
2926
2927     } elsif ((@_ = jpeg_size ($body))) {
2928       ($iw, $ih) = @_;
2929       $cmd = "djpeg |";
2930
2931     } elsif ((@_ = png_size ($body))) {
2932       ($iw, $ih) = @_;
2933       $cmd = "pngtopnm |";
2934
2935     } elsif ($body =~ m/^P\d\n(\d+) (\d+)\n/) {
2936       $iw = $1;
2937       $ih = $2;
2938       $cmd = "";
2939
2940     } else {
2941       error "$bgimage is not a GIF, JPEG, PNG, or PPM.";
2942     }
2943
2944     my $x = int (($img_width  - $iw) / 2);
2945     my $y = int (($img_height - $ih) / 2);
2946     LOG ($verbose_pbm,
2947          "pasting $bgimage (${iw}x$ih) into base image at $x,$y");
2948
2949     $cmd .= "pnmpaste - $x $y $image_ppm > $image_tmp1";
2950     open (IMG, "| $cmd") || error "running $cmd: $!";
2951     print IMG $body;
2952     $body = undef;
2953     close (IMG);
2954     LOG ($verbose_exec, "subproc exited normally.");
2955     rename ($image_tmp1, $image_ppm) ||
2956       error "renaming $image_tmp1 to $image_ppm: $!";
2957   }
2958
2959   clearlog();
2960
2961   while (1) {
2962     my ($base, $img) = pick_image();
2963     my $source = $current_state;
2964     $current_state = "loadimage";
2965     if ($img) {
2966       my ($headers, $body) = get_document ($img, $base);
2967       if ($body) {
2968         paste_image ($base, $img, $body, $source);
2969         $body = undef;
2970       }
2971     }
2972     $current_state = "idle";
2973     $load_method = "none";
2974
2975     unlink $image_tmp1, $image_tmp2;
2976     sleep $delay;
2977   }
2978 }
2979
2980 sub paste_image($$$$) {
2981   my ($base, $img, $body, $source) = @_;
2982
2983   $current_state = "paste";
2984
2985   $suppress_audit = 0;
2986
2987   LOG ($verbose_pbm, "got $img (" . length($body) . ")");
2988
2989   my ($iw, $ih);
2990
2991   # If we are using the webcollage-helper, then we do not need to convert this
2992   # image to a PPM.  But, if we're using a filter command, we still must, since
2993   # that's what the filters expect (webcollage-helper can read PPMs, so that's
2994   # fine.)
2995   #
2996   if (defined ($webcollage_helper) &&
2997       !defined ($filter_cmd)) {
2998
2999     ($iw, $ih) = image_size ($body);
3000     if (!$iw || !$ih) {
3001       LOG (($verbose_pbm || $verbose_load),
3002            "not a GIF, JPG, or PNG" .
3003            (($body =~ m@<(base|html|head|body|script|table|a href)>@i)
3004             ? " (looks like HTML)" : "") .
3005            ": $img");
3006       $suppress_audit = 1;
3007       $body = undef;
3008       return 0;
3009     }
3010
3011     local *OUT;
3012     open (OUT, ">$image_tmp1") || error ("writing $image_tmp1: $!");
3013     print OUT $body || error ("writing $image_tmp1: $!");
3014     close OUT || error ("writing $image_tmp1: $!");
3015
3016   } else {
3017     ($iw, $ih) = image_to_pnm ($img, $body, $image_tmp1);
3018     $body = undef;
3019     if (!$iw || !$ih) {
3020       LOG ($verbose_pbm, "unable to make PBM from $img");
3021       return 0;
3022     }
3023   }
3024
3025   record_success ($load_method, $img, $base);
3026
3027
3028   my $ow = $iw;  # used only for error messages
3029   my $oh = $ih;
3030
3031   # don't just tack this onto the front of the pipeline -- we want it to
3032   # be able to change the size of the input image.
3033   #
3034   if ($filter_cmd) {
3035     LOG ($verbose_pbm, "running $filter_cmd");
3036
3037     my $rc = nontrapping_system "($filter_cmd) < $image_tmp1 >$image_tmp2";
3038     if ($rc != 0) {
3039       LOG(($verbose_pbm || $verbose_load), "failed command: \"$filter_cmd\"");
3040       LOG(($verbose_pbm || $verbose_load), "failed URL: \"$img\" (${ow}x$oh)");
3041       return;
3042     }
3043     rename ($image_tmp2, $image_tmp1);
3044
3045     # re-get the width/height in case the filter resized it.
3046     local *IMG;
3047     open(IMG, "<$image_tmp1") || return 0;
3048     $_ = <IMG>;
3049     $_ = <IMG>;
3050     ($iw, $ih) = m/^(\d+) (\d+)$/;
3051     close (IMG);
3052     return 0 unless ($iw && $ih);
3053   }
3054
3055   my $target_w = $img_width;   # max rectangle into which the image must fit
3056   my $target_h = $img_height;
3057
3058   my $cmd = "";
3059   my $scale = 1.0;
3060
3061
3062   # Usually scale the image to fit on the screen -- but sometimes scale it
3063   # to fit on half or a quarter of the screen.  (We do this by reducing the
3064   # size of the target rectangle.)  Note that the image is not merely scaled
3065   # to fit; we instead cut the image in half repeatedly until it fits in the
3066   # target rectangle -- that gives a wider distribution of sizes.
3067   #
3068   if (rand() < 0.3) { $target_w /= 2; $target_h /= 2; } # reduce target rect
3069   if (rand() < 0.3) { $target_w /= 2; $target_h /= 2; }
3070
3071   if ($iw > $target_w || $ih > $target_h) {
3072     while ($iw > $target_w ||
3073            $ih > $target_h) {
3074       $iw = int($iw / 2);
3075       $ih = int($ih / 2);
3076       $scale /= 2;
3077     }
3078     if ($iw <= 10 || $ih <= 10) {
3079       LOG ($verbose_pbm, "scaling to ${iw}x$ih would have been bogus.");
3080       return 0;
3081     }
3082
3083     LOG ($verbose_pbm, "scaling to ${iw}x$ih ($scale)");
3084
3085     $cmd .= " | pnmscale -xsize $iw -ysize $ih";
3086   }
3087
3088
3089   my $src = $image_tmp1;
3090
3091   my $crop_x = 0;     # the sub-rectangle of the image
3092   my $crop_y = 0;     # that we will actually paste.
3093   my $crop_w = $iw;
3094   my $crop_h = $ih;
3095
3096   # The chance that we will randomly crop out a section of an image starts
3097   # out fairly low, but goes up for images that are very large, or images
3098   # that have ratios that make them look like banners (we try to avoid
3099   # banner images entirely, but they slip through when the IMG tags didn't
3100   # have WIDTH and HEIGHT specified.)
3101   #
3102   my $crop_chance = 0.2;
3103   if ($iw > $img_width * 0.4 || $ih > $img_height * 0.4) {
3104     $crop_chance += 0.2;
3105   }
3106   if ($iw > $img_width * 0.7 || $ih > $img_height * 0.7) {
3107     $crop_chance += 0.2;
3108   }
3109   if ($min_ratio && ($iw * $min_ratio) > $ih) {
3110     $crop_chance += 0.7;
3111   }
3112
3113   if ($crop_chance > 0.1) {
3114     LOG ($verbose_pbm, "crop chance: $crop_chance");
3115   }
3116
3117   if (rand() < $crop_chance) {
3118
3119     my $ow = $crop_w;
3120     my $oh = $crop_h;
3121
3122     if ($crop_w > $min_width) {
3123       # if it's a banner, select the width linearly.
3124       # otherwise, select a bell.
3125       my $r = (($min_ratio && ($iw * $min_ratio) > $ih)
3126                ? rand()
3127                : bellrand());
3128       $crop_w = $min_width + int ($r * ($crop_w - $min_width));
3129       $crop_x = int (rand() * ($ow - $crop_w));
3130     }
3131     if ($crop_h > $min_height) {
3132       # height always selects as a bell.
3133       $crop_h = $min_height + int (bellrand() * ($crop_h - $min_height));
3134       $crop_y = int (rand() * ($oh - $crop_h));
3135     }
3136
3137     if ($crop_x != 0   || $crop_y != 0 ||
3138         $crop_w != $iw || $crop_h != $ih) {
3139       LOG ($verbose_pbm,
3140            "randomly cropping to ${crop_w}x$crop_h \@ $crop_x,$crop_y");
3141     }
3142   }
3143
3144   # Where the image should logically land -- this might be negative.
3145   #
3146   my $x = int((rand() * ($img_width  + $crop_w/2)) - $crop_w*3/4);
3147   my $y = int((rand() * ($img_height + $crop_h/2)) - $crop_h*3/4);
3148
3149   # if we have chosen to paste the image outside of the rectangle of the
3150   # screen, then we need to crop it.
3151   #
3152   if ($x < 0 ||
3153       $y < 0 ||
3154       $x + $crop_w > $img_width ||
3155       $y + $crop_h > $img_height) {
3156
3157     LOG ($verbose_pbm,
3158          "cropping for effective paste of ${crop_w}x$crop_h \@ $x,$y");
3159
3160     if ($x < 0) { $crop_x -= $x; $crop_w += $x; $x = 0; }
3161     if ($y < 0) { $crop_y -= $y; $crop_h += $y; $y = 0; }
3162
3163     if ($x + $crop_w >= $img_width)  { $crop_w = $img_width  - $x - 1; }
3164     if ($y + $crop_h >= $img_height) { $crop_h = $img_height - $y - 1; }
3165   }
3166
3167   # If any cropping needs to happen, add pnmcut.
3168   #
3169   if ($crop_x != 0   || $crop_y != 0 ||
3170       $crop_w != $iw || $crop_h != $ih) {
3171     $iw = $crop_w;
3172     $ih = $crop_h;
3173     $cmd .= " | pnmcut $crop_x $crop_y $iw $ih";
3174     LOG ($verbose_pbm, "cropping to ${crop_w}x$crop_h \@ $crop_x,$crop_y");
3175   }
3176
3177   LOG ($verbose_pbm, "pasting ${iw}x$ih \@ $x,$y in $image_ppm");
3178
3179   $cmd .= " | pnmpaste - $x $y $image_ppm";
3180
3181   $cmd =~ s@^ *\| *@@;
3182
3183   if (defined ($webcollage_helper)) {
3184     $cmd = "$webcollage_helper $image_tmp1 $image_ppm " .
3185                               "$scale $opacity " .
3186                               "$crop_x $crop_y $x $y " .
3187                               "$iw $ih";
3188     $_ = $cmd;
3189
3190   } else {
3191     # use a PPM pipeline
3192     $_ = "($cmd)";
3193     $_ .= " < $image_tmp1 > $image_tmp2";
3194   }
3195
3196   if ($verbose_pbm) {
3197     $_ = "($_) 2>&1 | sed s'/^/" . blurb() . "/'";
3198   } else {
3199     $_ .= " 2> /dev/null";
3200   }
3201
3202   my $rc = nontrapping_system ($_);
3203
3204   if (defined ($webcollage_helper) && -z $image_ppm) {
3205     LOG (1, "failed command: \"$cmd\"");
3206     print STDERR "\naudit log:\n\n\n";
3207     print STDERR ("#" x 78) . "\n";
3208     print STDERR blurb() . "$image_ppm has zero size\n";
3209     showlog();
3210     print STDERR "\n\n";
3211     exit (1);
3212   }
3213
3214   if ($rc != 0) {
3215     LOG (($verbose_pbm || $verbose_load), "failed command: \"$cmd\"");
3216     LOG (($verbose_pbm || $verbose_load), "failed URL: \"$img\" (${ow}x$oh)");
3217     return;
3218   }
3219
3220   if (!defined ($webcollage_helper)) {
3221     rename ($image_tmp2, $image_ppm) || return;
3222   }
3223
3224   my $target = "$image_ppm";
3225
3226   # don't just tack this onto the end of the pipeline -- we don't want it
3227   # to end up in $image_ppm, because we don't want the results to be
3228   # cumulative.
3229   #
3230   if ($post_filter_cmd) {
3231
3232     my $cmd;
3233
3234     $target = $image_tmp1;
3235     if (!defined ($webcollage_helper)) {
3236       $cmd = "($post_filter_cmd) < $image_ppm > $target";
3237     } else {
3238       # Blah, my scripts need the JPEG data, but some other folks need
3239       # the PPM data -- what to do?  Ignore the problem, that's what!
3240 #     $cmd = "djpeg < $image_ppm | ($post_filter_cmd) > $target";
3241       $cmd = "($post_filter_cmd) < $image_ppm > $target";
3242     }
3243
3244     $rc = nontrapping_system ($cmd);
3245     if ($rc != 0) {
3246       LOG ($verbose_pbm, "filter failed: \"$post_filter_cmd\"\n");
3247       return;
3248     }
3249   }
3250
3251   if (!$no_output_p) {
3252     my $tsize = (stat($target))[7];
3253     if ($tsize > 200) {
3254       $cmd = "$ppm_to_root_window_cmd $target";
3255
3256       # xv seems to hate being killed.  it tends to forget to clean
3257       # up after itself, and leaves windows around and colors allocated.
3258       # I had this same problem with vidwhacker, and I'm not entirely
3259       # sure what I did to fix it.  But, let's try this: launch xv
3260       # in the background, so that killing this process doesn't kill it.
3261       # it will die of its own accord soon enough.  So this means we
3262       # start pumping bits to the root window in parallel with starting
3263       # the next network retrieval, which is probably a better thing
3264       # to do anyway.
3265       #
3266       $cmd .= " &";
3267
3268       $rc = nontrapping_system ($cmd);
3269
3270       if ($rc != 0) {
3271         LOG (($verbose_pbm || $verbose_load), "display failed: \"$cmd\"");
3272         return;
3273       }
3274
3275     } else {
3276       LOG ($verbose_pbm, "$target size is $tsize");
3277     }
3278   }
3279
3280   $source .= "-" . stats_of($source);
3281   print STDOUT "image: ${iw}x${ih} @ $x,$y $base $source\n"
3282     if ($verbose_imgmap);
3283   if ($imagemap_base) {
3284     update_imagemap ($base, $x, $y, $iw, $ih,
3285                      $image_ppm, $img_width, $img_height);
3286   }
3287
3288   clearlog();
3289
3290   return 1;
3291 }
3292
3293
3294 sub update_imagemap($$$$$$$$) {
3295   my ($url, $x, $y, $w, $h, $image_ppm, $image_width, $image_height) = @_;
3296
3297   $current_state = "imagemap";
3298
3299   my $max_areas = 200;
3300
3301   $url = html_quote ($url);
3302   my $x2 = $x + $w;
3303   my $y2 = $y + $h;
3304   my $area = "<AREA SHAPE=RECT COORDS=\"$x,$y,$x2,$y2\" HREF=\"$url\">";
3305   unshift @imagemap_areas, $area;       # put one on the front
3306   if ($#imagemap_areas >= $max_areas) {
3307     pop @imagemap_areas;                # take one off the back.
3308   }
3309
3310   LOG ($verbose_pbm, "area: $x,$y,$x2,$y2 (${w}x$h)");
3311
3312   my $map_name = $imagemap_base;
3313   $map_name =~ s@^.*/@@;
3314   $map_name = 'collage' if ($map_name eq '');
3315
3316   my $imagemap_html = $imagemap_base . ".html";
3317   my $imagemap_jpg  = $imagemap_base . ".jpg";
3318
3319   if (!defined ($imagemap_html_tmp)) {
3320     $imagemap_html_tmp = $imagemap_html . sprintf (".%08x", rand(0xffffffff));
3321     $imagemap_jpg_tmp  = $imagemap_jpg  . sprintf (".%08x", rand(0xffffffff));
3322   }
3323
3324   # Read the imagemap html file (if any) to get a template.
3325   #
3326   my $template_html = '';
3327   {
3328     local *IN;
3329     if (open (IN, "<$imagemap_html")) {
3330       while (<IN>) { $template_html .= $_; }
3331       close IN;
3332       LOG ($verbose_pbm, "read template $imagemap_html");
3333     }
3334
3335     if ($template_html =~ m/^\s*$/s) {
3336       $template_html = ("<MAP NAME=\"$map_name\"></MAP>\n" .
3337                         "<IMG SRC=\"$imagemap_base.jpg\"" .
3338                         " USEMAP=\"$map_name\">\n");
3339       LOG ($verbose_pbm, "created dummy template");
3340     }
3341   }
3342
3343   # Write the jpg to a tmp file
3344   #
3345   {
3346     my $cmd;
3347     if (defined ($webcollage_helper)) {
3348       $cmd = "cp -p $image_ppm $imagemap_jpg_tmp";
3349     } else {
3350       $cmd = "cjpeg < $image_ppm > $imagemap_jpg_tmp";
3351     }
3352     my $rc = nontrapping_system ($cmd);
3353     if ($rc != 0) {
3354       error ("imagemap jpeg failed: \"$cmd\"\n");
3355     }
3356   }
3357
3358   # Write the html to a tmp file
3359   #
3360   {
3361     my $body = $template_html;
3362     my $areas = join ("\n\t", @imagemap_areas);
3363     my $map = ("<MAP NAME=\"$map_name\">\n\t$areas\n</MAP>");
3364     my $img = ("<IMG SRC=\"$imagemap_base.jpg\" " .
3365                "BORDER=0 " .
3366                "WIDTH=$image_width HEIGHT=$image_height " .
3367                "USEMAP=\"#$map_name\">");
3368     $body =~ s@(<MAP\s+NAME=\"[^\"]*\"\s*>).*?(</MAP>)@$map@is;
3369     $body =~ s@<IMG\b[^<>]*\bUSEMAP\b[^<>]*>@$img@is;
3370
3371     # if there are magic webcollage spans in the html, update those too.
3372     #
3373     {
3374       my @st = stat ($imagemap_jpg_tmp);
3375       my $date = strftime("%d-%b-%Y %l:%M:%S %p %Z", localtime($st[9]));
3376       my $size = int(($st[7] / 1024) + 0.5) . "K";
3377       $body =~ s@(<SPAN\s+CLASS=\"webcollage_date\">).*?(</SPAN>)@$1$date$2@si;
3378       $body =~ s@(<SPAN\s+CLASS=\"webcollage_size\">).*?(</SPAN>)@$1$size$2@si;
3379     }
3380
3381     local *OUT;
3382     open (OUT, ">$imagemap_html_tmp") || error ("$imagemap_html_tmp: $!");
3383     print OUT $body                   || error ("$imagemap_html_tmp: $!");
3384     close OUT                         || error ("$imagemap_html_tmp: $!");
3385     LOG ($verbose_pbm, "wrote $imagemap_html_tmp");
3386   }
3387
3388   # Rename the two tmp files to the real files
3389   #
3390   rename ($imagemap_html_tmp, $imagemap_html) ||
3391     error "renaming $imagemap_html_tmp to $imagemap_html";
3392   LOG ($verbose_pbm, "wrote $imagemap_html");
3393   rename ($imagemap_jpg_tmp,  $imagemap_jpg) ||
3394     error "renaming $imagemap_jpg_tmp to $imagemap_jpg";
3395   LOG ($verbose_pbm, "wrote $imagemap_jpg");
3396 }
3397
3398
3399 sub init_signals() {
3400
3401   $SIG{HUP}  = \&signal_cleanup;
3402   $SIG{INT}  = \&signal_cleanup;
3403   $SIG{QUIT} = \&signal_cleanup;
3404   $SIG{ABRT} = \&signal_cleanup;
3405   $SIG{KILL} = \&signal_cleanup;
3406   $SIG{TERM} = \&signal_cleanup;
3407
3408   # Need this so that if giftopnm dies, we don't die.
3409   $SIG{PIPE} = 'IGNORE';
3410 }
3411
3412 END { exit_cleanup(); }
3413
3414
3415 sub main() {
3416   $| = 1;
3417   srand(time ^ $$);
3418
3419   my $verbose = 0;
3420   my $dict;
3421   my $driftnet_cmd = 0;
3422
3423   $current_state = "init";
3424   $load_method = "none";
3425
3426   my $root_p = 0;
3427   my $window_id = undef;
3428
3429   # historical suckage: the environment variable name is lower case.
3430   $http_proxy = $ENV{http_proxy} || $ENV{HTTP_PROXY};
3431
3432   while ($_ = $ARGV[0]) {
3433     shift @ARGV;
3434     if ($_ eq "-display" ||
3435         $_ eq "-displ" ||
3436         $_ eq "-disp" ||
3437         $_ eq "-dis" ||
3438         $_ eq "-dpy" ||
3439         $_ eq "-d") {
3440       $ENV{DISPLAY} = shift @ARGV;
3441     } elsif ($_ eq "-root") {
3442       $root_p = 1;
3443     } elsif ($_ eq "-window-id" || $_ eq "--window-id") {
3444       $window_id = shift @ARGV;
3445       $root_p = 1;
3446     } elsif ($_ eq "-no-output") {
3447       $no_output_p = 1;
3448     } elsif ($_ eq "-urls-only") {
3449       $urls_only_p = 1;
3450       $no_output_p = 1;
3451     } elsif ($_ eq "-cocoa") {
3452       $cocoa_p = 1;
3453     } elsif ($_ eq "-imagemap") {
3454       $imagemap_base = shift @ARGV;
3455       $no_output_p = 1;
3456     } elsif ($_ eq "-verbose") {
3457       $verbose++;
3458     } elsif (m/^-v+$/) {
3459       $verbose += length($_)-1;
3460     } elsif ($_ eq "-delay") {
3461       $delay = shift @ARGV;
3462     } elsif ($_ eq "-timeout") {
3463       $http_timeout = shift @ARGV;
3464     } elsif ($_ eq "-filter") {
3465       $filter_cmd = shift @ARGV;
3466     } elsif ($_ eq "-filter2") {
3467       $post_filter_cmd = shift @ARGV;
3468     } elsif ($_ eq "-background" || $_ eq "-bg") {
3469       $background = shift @ARGV;
3470     } elsif ($_ eq "-size") {
3471       $_ = shift @ARGV;
3472       if (m@^(\d+)x(\d+)$@) {
3473         $img_width = $1;
3474         $img_height = $2;
3475       } else {
3476         error "argument to \"-size\" must be of the form \"640x400\"";
3477       }
3478     } elsif ($_ eq "-proxy" || $_ eq "-http-proxy") {
3479       $http_proxy = shift @ARGV;
3480     } elsif ($_ eq "-dictionary" || $_ eq "-dict") {
3481       $dict = shift @ARGV;
3482     } elsif ($_ eq "-opacity") {
3483       $opacity = shift @ARGV;
3484       error ("opacity must be between 0.0 and 1.0")
3485         if ($opacity <= 0 || $opacity > 1);
3486     } elsif ($_ eq "-driftnet" || $_ eq "--driftnet") {
3487       @search_methods = ( 100, "driftnet", \&pick_from_driftnet );
3488       if (! ($ARGV[0] =~ m/^-/)) {
3489         $driftnet_cmd = shift @ARGV;
3490       } else {
3491         $driftnet_cmd = $default_driftnet_cmd;
3492       }
3493     } elsif ($_ eq "-directory" || $_ eq "--directory") {
3494       @search_methods = ( 100, "local", \&pick_from_local_dir );
3495       if (! ($ARGV[0] =~ m/^-/)) {
3496         $local_dir = shift @ARGV;
3497       } else {
3498         error ("local directory path must be set")
3499       }
3500     } elsif ($_ eq "-debug" || $_ eq "--debug") {
3501       my $which = shift @ARGV;
3502       my @rest = @search_methods;
3503       my $ok = 0;
3504       while (@rest) {
3505         my $pct  = shift @rest;
3506         my $name = shift @rest;
3507         my $tfn  = shift @rest;
3508
3509         if ($name eq $which) {
3510           @search_methods = (100, $name, $tfn);
3511           $ok = 1;
3512           last;
3513         }
3514       }
3515       error "no such search method as \"$which\"" unless ($ok);
3516       LOG (1, "DEBUG: using only \"$which\"");
3517
3518     } else {
3519       print STDERR "$copyright\nusage: $progname " .
3520               "[-root] [-display dpy] [-verbose] [-debug which]\n" .
3521         "\t\t  [-timeout secs] [-delay secs] [-size WxH]\n" .
3522         "\t\t  [-no-output] [-urls-only] [-imagemap filename]\n" .
3523         "\t\t  [-filter cmd] [-filter2 cmd] [-background color]\n" .
3524         "\t\t  [-dictionary dictionary-file] [-http-proxy host[:port]]\n" .
3525         "\t\t  [-driftnet [driftnet-program-and-args]]\n" .
3526         "\t\t  [-directory local-image-directory]\n" .
3527         "\n";
3528       exit 1;
3529     }
3530   }
3531
3532   if ($http_proxy && $http_proxy eq "") {
3533     $http_proxy = undef;
3534   }
3535   if ($http_proxy && $http_proxy =~ m@^http://([^/]*)/?$@ ) {
3536     # historical suckage: allow "http://host:port" as well as "host:port".
3537     $http_proxy = $1;
3538   }
3539
3540   if (!$root_p && !$no_output_p && !$cocoa_p) {
3541     print STDERR $copyright;
3542     error "the -root argument is mandatory (for now.)";
3543   }
3544
3545   if (!$no_output_p && !$cocoa_p && !$ENV{DISPLAY}) {
3546     error "\$DISPLAY is not set.";
3547   }
3548
3549
3550   if ($verbose == 1) {
3551     $verbose_imgmap   = 1;
3552     $verbose_warnings = 1;
3553
3554   } elsif ($verbose == 2) {
3555     $verbose_imgmap   = 1;
3556     $verbose_warnings = 1;
3557     $verbose_load     = 1;
3558
3559   } elsif ($verbose == 3) {
3560     $verbose_imgmap   = 1;
3561     $verbose_warnings = 1;
3562     $verbose_load     = 1;
3563     $verbose_filter   = 1;
3564
3565   } elsif ($verbose == 4) {
3566     $verbose_imgmap   = 1;
3567     $verbose_warnings = 1;
3568     $verbose_load     = 1;
3569     $verbose_filter   = 1;
3570     $verbose_net      = 1;
3571
3572   } elsif ($verbose == 5) {
3573     $verbose_imgmap   = 1;
3574     $verbose_warnings = 1;
3575     $verbose_load     = 1;
3576     $verbose_filter   = 1;
3577     $verbose_net      = 1;
3578     $verbose_pbm      = 1;
3579
3580   } elsif ($verbose == 6) {
3581     $verbose_imgmap   = 1;
3582     $verbose_warnings = 1;
3583     $verbose_load     = 1;
3584     $verbose_filter   = 1;
3585     $verbose_net      = 1;
3586     $verbose_pbm      = 1;
3587     $verbose_http     = 1;
3588
3589   } elsif ($verbose >= 7) {
3590     $verbose_imgmap   = 1;
3591     $verbose_warnings = 1;
3592     $verbose_load     = 1;
3593     $verbose_filter   = 1;
3594     $verbose_net      = 1;
3595     $verbose_pbm      = 1;
3596     $verbose_http     = 1;
3597     $verbose_exec     = 1;
3598   }
3599
3600   if ($dict) {
3601     error ("$dict does not exist") unless (-f $dict);
3602     $wordlist = $dict;
3603   } else {
3604     pick_dictionary();
3605   }
3606
3607   if ($imagemap_base && !($img_width && $img_height)) {
3608     error ("-size WxH is required with -imagemap");
3609   }
3610
3611   if (defined ($local_dir)) {
3612     $_ = "xscreensaver-getimage-file";
3613     which ($_) || error "$_ not found on \$PATH.";
3614   }
3615
3616   init_signals();
3617
3618   spawn_driftnet ($driftnet_cmd) if ($driftnet_cmd);
3619
3620   if ($urls_only_p) {
3621     url_only_output ();
3622   } else {
3623     x_or_pbm_output ($window_id);
3624   }
3625 }
3626
3627 main();
3628 exit (0);