53625101adb65c11db8a978b23dfb7ef1fbfccdf
[xscreensaver] / hacks / webcollage
1 #!/usr/bin/perl -w
2 #
3 # webcollage, Copyright (c) 1999-2002 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 #   default-n:  webcollage -root                                        \n\
19 #   default-n:  webcollage -root -filter 'vidwhacker -stdin -stdout'    \n\
20
21
22 # If you have the "driftnet" program installed, webcollage can display a
23 # collage of images sniffed off your local ethernet, instead of pulled out
24 # of search engines: in that way, your screensaver can display the images
25 # that your co-workers are downloading!
26 #
27 # Driftnet is available here: http://www.ex-parrot.com/~chris/driftnet/
28 # Use it like so:
29 #
30 #   default-n:  webcollage -root -driftnet                             \n\
31 #
32 # Driftnet is the Unix implementation of the MacOS "EtherPEG" program.
33
34
35 require 5;
36 use strict;
37
38 # We can't "use diagnostics" here, because that library malfunctions if
39 # you signal and catch alarms: it says "Uncaught exception from user code"
40 # and exits, even though I damned well AM catching it!
41 #use diagnostics;
42
43
44 use Socket;
45 require Time::Local;
46 require POSIX;
47 use Fcntl ':flock'; # import LOCK_* constants
48 use POSIX qw(strftime);
49
50 use bytes;  # Larry can take Unicode and shove it up his ass sideways.
51             # Perl 5.8.0 causes us to start getting incomprehensible
52             # errors about UTF-8 all over the place without this.
53
54
55 my $progname = $0; $progname =~ s@.*/@@g;
56 my $version = q{ $Revision: 1.96 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/;
57 my $copyright = "WebCollage $version, Copyright (c) 1999-2002" .
58     " Jamie Zawinski <jwz\@jwz.org>\n" .
59     "            http://www.jwz.org/xscreensaver/\n";
60
61
62
63 my @search_methods = (  40, "imagevista", \&pick_from_alta_vista_images,
64                         30, "altavista",  \&pick_from_alta_vista_text,
65                         19, "yahoorand",  \&pick_from_yahoo_random_link,
66                          9, "lycos",      \&pick_from_lycos_text,
67                          2, "yahoonews",  \&pick_from_yahoo_news_text,
68
69                      # Hotbot gives me "no matches" just about every time.
70                      # Then I try the same URL again, and it works.  I guess
71                      # it caches searches, and webcollage always busts its
72                      # cache and time out?  Or it just sucks.
73                      #   0, "hotbot",     \&pick_from_hotbot_text,
74
75                      # Google asked (nicely) for me to stop searching them.
76                      #   0, "googlenums", \&pick_from_google_image_numbers,
77                      #   0, "googleimgs", \&pick_from_google_images,
78
79                       );
80
81 # programs we can use to write to the root window (tried in ascending order.)
82 #
83 my @root_displayers = (
84   "chbg       -once -xscreensaver -max_size 100",
85   "xv         -root -quit -viewonly +noresetroot -quick24 -rmode 5" .
86   "           -rfg black -rbg black",
87   "xli        -quiet -onroot -center -border black",
88   "xloadimage -quiet -onroot -center -border black",
89
90 # this lame program wasn't built with vroot.h:
91 # "xsri       -scale -keep-aspect -center-horizontal -center-vertical",
92 );
93
94
95 # Some sites need cookies to work properly.   These are they.
96 #
97 my %cookies = (
98   "www.altavista.com"  =>  "AV_ALL=1",   # request uncensored searches
99   "web.altavista.com"  =>  "AV_ALL=1",
100
101                                          # log in as "cipherpunk"
102   "www.nytimes.com"    =>  'NYT-S=18cHMIlJOn2Y1bu5xvEG3Ufuk6E1oJ.' .
103                            'FMxWaQV0igaB5Yi/Q/guDnLeoL.pe7i1oakSb' .
104                            '/VqfdUdb2Uo27Vzt1jmPn3cpYRlTw9',
105 );
106
107
108 # If this is set, it's a helper program to use for pasting images together:
109 # this is a lot faster and more efficient than using PPM pipelines, which is
110 # what we do if this program doesn't exist.  (We check for "webcollage-helper"
111 # on $PATH at startup, and set this variable appropriately.)
112 #
113 my $webcollage_helper = undef;
114
115
116 # If we have the webcollage-helper program, then it will paste the images
117 # together with transparency!  0.0 is invisible, 1.0 is totally opaque.
118 #
119 my $opacity = 0.85;
120
121
122 # Some sites have  managed to poison the search engines.  These are they.
123 # (We auto-detect sites that have poisoned the search engines via excessive
124 # keywords or dictionary words,  but these are ones that slip through
125 # anyway.)
126 #
127 # This can contain full host names, or 2 or 3 component domains.
128 #
129 my %poisoners = (
130   "die.net"                 => 1,  # 'l33t h4ck3r d00dz.
131   "genforum.genealogy.com"  => 1,  # Cluttering altavista with human names.
132   "rootsweb.com"            => 1,  # Cluttering altavista with human names.
133   "akamai.net"              => 1,  # Lots of sites have their images on Akamai.
134                                    # But those are pretty much all banners.
135                                    # Since Akamai is super-expensive, let's
136                                    # go out on a limb and assume that all of
137                                    # their customers are rich-and-boring.
138   "bartleby.com"            => 1,  # Dictionary, cluttering altavista.
139   "encyclopedia.com"        => 1,  # Dictionary, cluttering altavista.
140   "onlinedictionary.datasegment.com" => 1,  # Dictionary, cluttering altavista.
141   "hotlinkpics.com"         => 1,  # Porn site that has poisoned imagevista
142                                    # (I don't see how they did it, though!)
143   "alwayshotels.com"        => 1,  # Poisoned Lycos pretty heavily.
144 );
145
146
147 # When verbosity is turned on, we warn about sites that we seem to be hitting
148 # a lot: usually this means some new poisoner has made it into the search
149 # engines.  But sometimes, the warning is just because that site has a lot
150 # of stuff on it.  So these are the sites that are immune to the "frequent
151 # site" diagnostic message.
152 #
153 my %warningless_sites = (
154   "home.earthlink.net"      => 1,  # Lots of home pages here.
155   "www.geocities.com"       => 1,
156   "www.angelfire.com"       => 1,
157   "members.aol.com"         => 1,
158
159   "yimg.com"                => 1,  # This is where dailynews.yahoo.com stores
160   "eimg.com"                => 1,  # its images, so pick_from_yahoo_news_text()
161                                    # hits this every time.
162
163   "driftnet"                => 1,  # builtin...
164 );
165
166
167 ##############################################################################
168 #
169 # Various global flags set by command line parameters, or computed
170 #
171 ##############################################################################
172
173
174 my $current_state = "???";      # for diagnostics
175 my $load_method;
176 my $last_search;
177 my $image_succeeded = -1;
178 my $suppress_audit = 0;
179
180 my $verbose_imgmap = 0;         # print out rectangles and URLs only (stdout)
181 my $verbose_warnings = 0;       # print out warnings when things go wrong
182 my $verbose_load = 0;           # diagnostics about loading of URLs
183 my $verbose_filter = 0;         # diagnostics about page selection/rejection
184 my $verbose_net = 0;            # diagnostics about network I/O
185 my $verbose_pbm = 0;            # diagnostics about PBM pipelines
186 my $verbose_http = 0;           # diagnostics about all HTTP activity
187 my $verbose_exec = 0;           # diagnostics about executing programs
188
189 my $report_performance_interval = 60 * 15;  # print some stats every 15 minutes
190
191 my $http_proxy = undef;
192 my $http_timeout = 30;
193 my $cvt_timeout = 10;
194
195 my $min_width = 50;
196 my $min_height = 50;
197 my $min_ratio = 1/5;
198
199 my $min_gif_area = (120 * 120);
200
201
202 my $no_output_p = 0;
203 my $urls_only_p = 0;
204
205 my @pids_to_kill = ();  # forked pids we should kill when we exit, if any.
206
207 my $driftnet_magic = 'driftnet';
208 my $driftnet_dir = undef;
209 my $default_driftnet_cmd = "driftnet -a -m 100";
210
211 my $wordlist;
212
213 my %rejected_urls;
214 my @tripwire_words = ("aberrate", "abode", "amorphous", "antioch",
215                       "arrhenius", "arteriole", "blanket", "brainchild",
216                       "burdensome", "carnival", "cherub", "chord", "clever",
217                       "dedicate", "dilogarithm", "dolan", "dryden",
218                       "eggplant");
219
220
221 ##############################################################################
222 #
223 # Retrieving URLs
224 #
225 ##############################################################################
226
227 # returns three values: the HTTP response line; the document headers;
228 # and the document body.
229 #
230 sub get_document_1 {
231   my ( $url, $referer, $timeout ) = @_;
232
233   if (!defined($timeout)) { $timeout = $http_timeout; }
234   if ($timeout > $http_timeout) { $timeout = $http_timeout; }
235
236   if ($timeout <= 0) {
237     LOG (($verbose_net || $verbose_load), "timed out for $url");
238     return ();
239   }
240
241   LOG ($verbose_net, "get_document_1 $url " . ($referer ? $referer : ""));
242
243   if (! ($url =~ m@^http://@i)) {
244     LOG ($verbose_net, "not an HTTP URL: $url");
245     return ();
246   }
247
248   my ($url_proto, $dummy, $serverstring, $path) = split(/\//, $url, 4);
249   $path = "" unless $path;
250
251   my ($them,$port) = split(/:/, $serverstring);
252   $port = 80 unless $port;
253
254   my $them2 = $them;
255   my $port2 = $port;
256   if ($http_proxy) {
257     $serverstring = $http_proxy if $http_proxy;
258     ($them2,$port2) = split(/:/, $serverstring);
259     $port2 = 80 unless $port2;
260   }
261
262   my ($remote, $iaddr, $paddr, $proto, $line);
263   $remote = $them2;
264   if ($port2 =~ /\D/) { $port2 = getservbyname($port2, 'tcp') }
265   if (!$port2) {
266     LOG (($verbose_net || $verbose_load), "unrecognised port in $url");
267     return ();
268   }
269   $iaddr   = inet_aton($remote);
270   if (!$iaddr) {
271     LOG (($verbose_net || $verbose_load), "host not found: $remote");
272     return ();
273   }
274   $paddr   = sockaddr_in($port2, $iaddr);
275
276
277   my $head = "";
278   my $body = "";
279
280   @_ =
281     eval {
282       local $SIG{ALRM} = sub {
283         LOG (($verbose_net || $verbose_load), "timed out ($timeout) for $url");
284         die "alarm\n";
285       };
286       alarm $timeout;
287
288       $proto   = getprotobyname('tcp');
289       if (!socket(S, PF_INET, SOCK_STREAM, $proto)) {
290         LOG (($verbose_net || $verbose_load), "socket: $!");
291         return ();
292       }
293       if (!connect(S, $paddr)) {
294         LOG (($verbose_net || $verbose_load), "connect($serverstring): $!");
295         return ();
296       }
297
298       select(S); $| = 1; select(STDOUT);
299
300       my $cookie = $cookies{$them};
301
302       my $user_agent = "$progname/$version";
303       if ($url =~ m@^http://www\.altavista\.com/@) {
304         # block this, you turkeys.
305         $user_agent = "Mozilla/4.76 [en] (X11; U; Linux 2.2.16-22 i686; Nav)";
306       }
307
308       my $hdrs = "GET " . ($http_proxy ? $url : "/$path") . " HTTP/1.0\r\n" .
309                  "Host: $them\r\n" .
310                  "User-Agent: $user_agent\r\n";
311       if ($referer) {
312         $hdrs .= "Referer: $referer\r\n";
313       }
314       if ($cookie) {
315         my @cc = split(/\r?\n/, $cookie);
316         $hdrs .= "Cookie: " . join('; ', @cc) . "\r\n";
317       }
318       $hdrs .= "\r\n";
319
320       foreach (split('\r?\n', $hdrs)) {
321         LOG ($verbose_http, "  ==> $_");
322       }
323       print S $hdrs;
324       my $http = <S> || "";
325
326       $_  = $http;
327       s/[\r\n]+$//s;
328       LOG ($verbose_http, "  <== $_");
329
330       while (<S>) {
331         $head .= $_;
332         s/[\r\n]+$//s;
333         last if m@^$@;
334         LOG ($verbose_http, "  <== $_");
335
336         if (m@^Set-cookie:\s*([^;\r\n]+)@i) {
337           set_cookie($them, $1)
338         }
339       }
340
341       my $lines = 0;
342       while (<S>) {
343         $body .= $_;
344         $lines++;
345       }
346
347       LOG ($verbose_http,
348            "  <== [ body ]: $lines lines, " . length($body) . " bytes");
349
350       close S;
351
352       if (!$http) {
353         LOG (($verbose_net || $verbose_load), "null response: $url");
354         return ();
355       }
356
357       return ( $http, $head, $body );
358     };
359   die if ($@ && $@ ne "alarm\n");       # propagate errors
360   if ($@) {
361     # timed out
362     $head = undef;
363     $body = undef;
364     $suppress_audit = 1;
365     return ();
366   } else {
367     # didn't
368     alarm 0;
369     return @_;
370   }
371 }
372
373
374 # returns two values: the document headers; and the document body.
375 # if the given URL did a redirect, returns the redirected-to document.
376 #
377 sub get_document {
378   my ( $url, $referer, $timeout ) = @_;
379   my $start = time;
380
381   if (defined($referer) && $referer eq $driftnet_magic) {
382     return get_driftnet_file ($url);
383   }
384
385   my $orig_url = $url;
386   my $loop_count = 0;
387   my $max_loop_count = 4;
388
389   do {
390     if (defined($timeout) && $timeout <= 0) {
391       LOG (($verbose_net || $verbose_load), "timed out for $url");
392       $suppress_audit = 1;
393       return ();
394     }
395
396     my ( $http, $head, $body ) = get_document_1 ($url, $referer, $timeout);
397
398     if (defined ($timeout)) {
399       my $now = time;
400       my $elapsed = $now - $start;
401       $timeout -= $elapsed;
402       $start = $now;
403     }
404
405     return () unless $http; # error message already printed
406
407     $http =~ s/[\r\n]+$//s;
408
409     if ( $http =~ m@^HTTP/[0-9.]+ 30[123]@ ) {
410       $_ = $head;
411       my ( $location ) = m@^location:[ \t]*(.*)$@im;
412       if ( $location ) {
413         $location =~ s/[\r\n]$//;
414
415         LOG ($verbose_net, "redirect from $url to $location");
416         $referer = $url;
417         $url = $location;
418
419         if ($url =~ m@^/@) {
420           $referer =~ m@^(http://[^/]+)@i;
421           $url = $1 . $url;
422         } elsif (! ($url =~ m@^[a-z]+:@i)) {
423           $_ = $referer;
424           s@[^/]+$@@g if m@^http://[^/]+/@i;
425           $_ .= "/" if m@^http://[^/]+$@i;
426           $url = $_ . $url;
427         }
428
429       } else {
430         LOG ($verbose_net, "no Location with \"$http\"");
431         return ( $url, $body );
432       }
433
434       if ($loop_count++ > $max_loop_count) {
435         LOG ($verbose_net,
436              "too many redirects ($max_loop_count) from $orig_url");
437         $body = undef;
438         return ();
439       }
440
441     } elsif ( $http =~ m@^HTTP/[0-9.]+ ([4-9][0-9][0-9].*)$@ ) {
442
443       LOG (($verbose_net || $verbose_load), "failed: $1 ($url)");
444
445       # http errors -- return nothing.
446       $body = undef;
447       return ();
448
449     } elsif (!$body) {
450
451       LOG (($verbose_net || $verbose_load), "document contains no data: $url");
452       return ();
453
454     } else {
455
456       # ok!
457       return ( $url, $body );
458     }
459
460   } while (1);
461 }
462
463 # If we already have a cookie defined for this site, and the site is trying
464 # to overwrite that very same cookie, let it do so.  This is because nytimes
465 # expires its cookies - it lets you upgrade to a new cookie without logging
466 # in again, but you have to present the old cookie to get the new cookie.
467 # So, by doing this, the built-in cypherpunks cookie will never go "stale".
468 #
469 sub set_cookie {
470   my ($host, $cookie) = @_;
471   my $oc = $cookies{$host};
472   return unless $oc;
473   $_ = $oc;
474   my ($oc_name, $oc_value) = m@^([^= \t\r\n]+)=(.*)$@;
475   $_ = $cookie;
476   my ($nc_name, $nc_value) = m@^([^= \t\r\n]+)=(.*)$@;
477
478   if ($oc_name eq $nc_name &&
479       $oc_value ne $nc_value) {
480     $cookies{$host} = $cookie;
481     LOG ($verbose_net, "overwrote ${host}'s $oc_name cookie");
482   }
483 }
484
485
486 ############################################################################
487 #
488 # Extracting image URLs from HTML
489 #
490 ############################################################################
491
492 # given a URL and the body text at that URL, selects and returns a random
493 # image from it.  returns () if no suitable images found.
494 #
495 sub pick_image_from_body {
496   my ( $url, $body ) = @_;
497
498   my $base = $url;
499   $_ = $url;
500
501   # if there's at least one slash after the host, take off the last
502   # pathname component
503   if ( m@^http://[^/]+/@io ) {
504     $base =~ s@[^/]+$@@go;
505   }
506
507   # if there are no slashes after the host at all, put one on the end.
508   if ( m@^http://[^/]+$@io ) {
509     $base .= "/";
510   }
511
512   $_ = $body;
513
514   # strip out newlines, compress whitespace
515   s/[\r\n\t ]+/ /go;
516
517   # nuke comments
518   s/<!--.*?-->//go;
519
520
521   # There are certain web sites that list huge numbers of dictionary
522   # words in their bodies or in their <META NAME=KEYWORDS> tags (surprise!
523   # Porn sites tend not to be reputable!)
524   #
525   # I do not want webcollage to filter on content: I want it to select
526   # randomly from the set of images on the web.  All the logic here for
527   # rejecting some images is really a set of heuristics for rejecting
528   # images that are not really images: for rejecting *text* that is in
529   # GIF/JPEG form.  I don't want text, I want pictures, and I want the
530   # content of the pictures to be randomly selected from among all the
531   # available content.
532   #
533   # So, filtering out "dirty" pictures by looking for "dirty" keywords
534   # would be wrong: dirty pictures exist, like it or not, so webcollage
535   # should be able to select them.
536   #
537   # However, picking a random URL is a hard thing to do.  The mechanism I'm
538   # using is to search for a selection of random words.  This is not
539   # perfect, but works ok most of the time.  The way it breaks down is when
540   # some URLs get precedence because their pages list *every word* as
541   # related -- those URLs come up more often than others.
542   #
543   # So, after we've retrieved a URL, if it has too many keywords, reject
544   # it.  We reject it not on the basis of what those keywords are, but on
545   # the basis that by having so many, the page has gotten an unfair
546   # advantage against our randomizer.
547   #
548   my $trip_count = 0;
549   foreach my $trip (@tripwire_words) {
550     $trip_count++ if m/$trip/i;
551   }
552
553   if ($trip_count >= $#tripwire_words - 2) {
554     LOG (($verbose_filter || $verbose_load),
555          "there is probably a dictionary in \"$url\": rejecting.");
556     $rejected_urls{$url} = -1;
557     $body = undef;
558     $_ = undef;
559     return ();
560   }
561
562
563   my @urls;
564   my %unique_urls;
565
566   foreach (split(/ *</)) {
567     if ( m/^meta /i ) {
568
569       # Likewise, reject any web pages that have a KEYWORDS meta tag
570       # that is too long.
571       #
572       if (m/name ?= ?\"?keywords\"?/i &&
573           m/content ?= ?\"([^\"]+)\"/) {
574         my $L = length($1);
575         if ($L > 1000) {
576           LOG (($verbose_filter || $verbose_load),
577                "excessive keywords ($L bytes) in $url: rejecting.");
578           $rejected_urls{$url} = $L;
579           $body = undef;
580           $_ = undef;
581           return ();
582         } else {
583           LOG ($verbose_filter, "  keywords ($L bytes) in $url (ok)");
584         }
585       }
586
587     } elsif ( m/^(img|a) .*(src|href) ?= ?\"? ?(.*?)[ >\"]/io ) {
588
589       my $was_inline = ( "$1" eq "a" || "$1" eq "A" );
590       my $link = $3;
591       my ( $width )  = m/width ?=[ \"]*(\d+)/oi;
592       my ( $height ) = m/height ?=[ \"]*(\d+)/oi;
593       $_ = $link;
594
595       if ( m@^/@o ) {
596         my $site;
597         ( $site = $base ) =~ s@^(http://[^/]*).*@$1@gio;
598         $_ = "$site$link";
599       } elsif ( ! m@^[^/:?]+:@ ) {
600         $_ = "$base$link";
601         s@/\./@/@g;
602         1 while (s@/[^/]+/\.\./@/@g);
603       }
604
605       # skip non-http
606       if ( ! m@^http://@io ) {
607         next;
608       }
609
610       # skip non-image
611       if ( ! m@[.](gif|jpg|jpeg|pjpg|pjpeg)$@io ) {
612         next;
613       }
614
615       # skip really short or really narrow images
616       if ( $width && $width < $min_width) {
617         if (!$height) { $height = "?"; }
618         LOG ($verbose_filter, "  skip narrow image $_ (${width}x$height)");
619         next;
620       }
621
622       if ( $height && $height < $min_height) {
623         if (!$width) { $width = "?"; }
624         LOG ($verbose_filter, "  skip short image $_ (${width}x$height)");
625         next;
626       }
627
628       # skip images with ratios that make them look like banners.
629       if ($min_ratio && $width && $height &&
630           ($width * $min_ratio ) > $height) {
631         if (!$height) { $height = "?"; }
632         LOG ($verbose_filter, "  skip bad ratio $_ (${width}x$height)");
633         next;
634       }
635
636       # skip GIFs with a small number of pixels -- those usually suck.
637       if ($width && $height &&
638           m/\.gif$/io &&
639           ($width * $height) < $min_gif_area) {
640         LOG ($verbose_filter, "  skip small GIF $_ (${width}x$height)");
641         next;
642       }
643       
644
645       my $url = $_;
646
647       if ($unique_urls{$url}) {
648         LOG ($verbose_filter, "  skip duplicate image $_");
649         next;
650       }
651
652       LOG ($verbose_filter,
653            "  image $url" .
654            ($width && $height ? " (${width}x${height})" : "") .
655            ($was_inline ? " (inline)" : ""));
656
657       $urls[++$#urls] = $url;
658       $unique_urls{$url}++;
659
660       # jpegs are preferable to gifs.
661       $_ = $url;
662       if ( ! m@[.]gif$@io ) {
663         $urls[++$#urls] = $url;
664       }
665
666       # pointers to images are preferable to inlined images.
667       if ( ! $was_inline ) {
668         $urls[++$#urls] = $url;
669         $urls[++$#urls] = $url;
670       }
671     }
672   }
673
674   my $fsp = ($body =~ m@<frameset@i);
675
676   $_ = undef;
677   $body = undef;
678
679   @urls = depoison (@urls);
680
681   if ( $#urls < 0 ) {
682     LOG ($verbose_load, "no images on $base" . ($fsp ? " (frameset)" : ""));
683     return ();
684   }
685
686   # pick a random element of the table
687   my $i = int(rand($#urls+1));
688   $url = $urls[$i];
689
690   LOG ($verbose_load, "picked image " .($i+1) . "/" . ($#urls+1) . ": $url");
691
692   return $url;
693 }
694
695
696 \f
697 ############################################################################
698 #
699 # Subroutines for getting pages and images out of search engines
700 #
701 ############################################################################
702
703
704 sub pick_dictionary {
705   my @dicts = ("/usr/dict/words",
706                "/usr/share/dict/words",
707                "/usr/share/lib/dict/words");
708   foreach my $f (@dicts) {
709     if (-f $f) {
710       $wordlist = $f;
711       last;
712     }
713   }
714   error ("$dicts[0] does not exist") unless defined($wordlist);
715 }
716
717 # returns a random word from the dictionary
718 #
719 sub random_word {
720     my $word = 0;
721     if (open (IN, "<$wordlist")) {
722         my $size = (stat(IN))[7];
723         my $pos = rand $size;
724         if (seek (IN, $pos, 0)) {
725             $word = <IN>;   # toss partial line
726             $word = <IN>;   # keep next line
727         }
728         if (!$word) {
729           seek( IN, 0, 0 );
730           $word = <IN>;
731         }
732         close (IN);
733     }
734
735     return 0 if (!$word);
736
737     $word =~ s/^[ \t\n\r]+//;
738     $word =~ s/[ \t\n\r]+$//;
739     $word =~ s/ys$/y/;
740     $word =~ s/ally$//;
741     $word =~ s/ly$//;
742     $word =~ s/ies$/y/;
743     $word =~ s/ally$/al/;
744     $word =~ s/izes$/ize/;
745     $word =~ tr/A-Z/a-z/;
746
747     if ( $word =~ s/[ \t\n\r]/\+/g ) {  # convert intra-word spaces to "+".
748       $word = "\%22$word\%22";          # And put quotes (%22) around it.
749     }
750
751     return $word;
752 }
753
754 sub random_words {
755   my ($or_p) = @_;
756   my $sep = ($or_p ? "%20OR%20" : "%20");
757   return (random_word . $sep .
758           random_word . $sep .
759           random_word . $sep .
760           random_word . $sep .
761           random_word);
762 }
763
764
765 sub url_quote {
766   my ($s) = @_;
767   $s =~ s|([^-a-zA-Z0-9.\@/_\r\n])|sprintf("%%%02X", ord($1))|ge;
768   return $s;
769 }
770
771 sub url_unquote {
772   my ($s) = @_;
773   $s =~ s/[+]/ /g;
774   $s =~ s/%([a-z0-9]{2})/chr(hex($1))/ige;
775   return $s;
776 }
777
778
779 # Loads the given URL (a search on some search engine) and returns:
780 # - the total number of hits the search engine claimed it had;
781 # - a list of URLs from the page that the search engine returned;
782 # Note that this list contains all kinds of internal search engine
783 # junk URLs too -- caller must prune them.
784 #
785 sub pick_from_search_engine {
786   my ( $timeout, $search_url, $words ) = @_;
787
788   $_ = $words;
789   s/%20/ /g;
790
791   print STDERR "\n\n" if ($verbose_load);
792
793   LOG ($verbose_load, "words: $_");
794   LOG ($verbose_load, "URL: $search_url");
795
796   $last_search = $search_url;   # for warnings
797
798   my $start = time;
799   my ( $base, $body ) = get_document ($search_url, undef, $timeout);
800   if (defined ($timeout)) {
801     $timeout -= (time - $start);
802     if ($timeout <= 0) {
803       $body = undef;
804       LOG (($verbose_net || $verbose_load),
805            "timed out (late) for $search_url");
806       $suppress_audit = 1;
807       return ();
808     }
809   }
810
811   return () if (! $body);
812
813
814   my @subpages;
815
816   my $search_count = "?";
817   if ($body =~ m@found (approximately |about )?(<B>)?(\d+)(</B>)? image@) {
818     $search_count = $3;
819   } elsif ($body =~ m@<NOBR>((\d{1,3})(,\d{3})*)&nbsp;@i) {
820     $search_count = $1;
821   } elsif ($body =~ m@found ((\d{1,3})(,\d{3})*|\d+) Web p@) {
822     $search_count = $1;
823   } elsif ($body =~ m@found about ((\d{1,3})(,\d{3})*|\d+) results@) {
824     $search_count = $1;
825   } elsif ($body =~ m@\b\d+ - \d+ of (\d+)\b@i) { # imagevista
826     $search_count = $1;
827   } elsif ($body =~ m@About ((\d{1,3})(,\d{3})*) images@i) { # imagevista
828     $search_count = $1;
829   } elsif ($body =~ m@We found ((\d{1,3})(,\d{3})*|\d+) results@i) { # *vista
830     $search_count = $1;
831   } elsif ($body =~ m@ of about <B>((\d{1,3})(,\d{3})*)<@i) { # googleimages
832     $search_count = $1;
833   } elsif ($body =~ m@<B>((\d{1,3})(,\d{3})*)</B> Web sites were found@i) {
834     $search_count = $1;    # lycos
835   } elsif ($body =~ m@WEB.*?RESULTS.*?\b((\d{1,3})(,\d{3})*)\b.*?Matches@i) {
836     $search_count = $1;                          # hotbot
837   } elsif ($body =~ m@no photos were found containing@i) { # imagevista
838     $search_count = "0";
839   } elsif ($body =~ m@found no document matching@i) { # altavista
840     $search_count = "0";
841   }
842   1 while ($search_count =~ s/^(\d+)(\d{3})/$1,$2/);
843
844 #  if ($search_count eq "?" || $search_count eq "0") {
845 #    local *OUT;
846 #    my $file = "/tmp/wc.html";
847 #    open(OUT, ">$file") || error ("writing $file: $!");
848 #    print OUT $body;
849 #    close OUT;
850 #    print STDERR  blurb() . "###### wrote $file\n";
851 #  }
852
853
854   my $length = length($body);
855   my $href_count = 0;
856
857   $_ = $body;
858
859   s/[\r\n\t ]+/ /g;
860
861
862   s/(<A )/\n$1/gi;
863   foreach (split(/\n/)) {
864     $href_count++;
865     my ($u) = m@<A\s.*\bHREF\s*=\s*([^>]+)>@i;
866     next unless $u;
867
868     if ($u =~ m/^\"([^\"]*)\"/) { $u = $1; }   # quoted string
869     elsif ($u =~ m/^([^\s]*)\s/) { $u = $1; }  # or token
870
871     if ( $rejected_urls{$u} ) {
872       LOG ($verbose_filter, "  pre-rejecting candidate: $u");
873       next;
874     }
875
876     LOG ($verbose_http, "    HREF: $u");
877
878     $subpages[++$#subpages] = $u;
879   }
880
881   if ( $#subpages < 0 ) {
882     LOG ($verbose_filter,
883          "found nothing on $base ($length bytes, $href_count links).");
884     return ();
885   }
886
887   LOG ($verbose_filter, "" . $#subpages+1 . " links on $search_url");
888
889   return ($search_count, @subpages);
890 }
891
892
893 sub depoison {
894   my (@urls) = @_;
895   my @urls2 = ();
896   foreach (@urls) {
897     my ($h) = m@^http://([^/: \t\r\n]+)@i;
898
899     next unless defined($h);
900
901     if ($poisoners{$h}) {
902       LOG (($verbose_filter), "  rejecting poisoner: $_");
903       next;
904     }
905     if ($h =~ m@([^.]+\.[^.]+\.[^.]+)$@ &&
906         $poisoners{$1}) {
907       LOG (($verbose_filter), "  rejecting poisoner: $_");
908       next;
909     }
910     if ($h =~ m@([^.]+\.[^.]+)$@ &&
911         $poisoners{$1}) {
912       LOG (($verbose_filter), "  rejecting poisoner: $_");
913       next;
914     }
915
916     push @urls2, $_;
917   }
918   return @urls2;
919 }
920
921
922 # given a list of URLs, picks one at random; loads it; and returns a
923 # random image from it.
924 # returns the url of the page loaded; the url of the image chosen;
925 # and a debugging description string.
926 #
927 sub pick_image_from_pages {
928   my ($base, $total_hit_count, $unfiltered_link_count, $timeout, @pages) = @_;
929
930   $total_hit_count = "?" unless defined($total_hit_count);
931
932   @pages = depoison (@pages);
933   LOG ($verbose_load,
934        "" . ($#pages+1) . " candidates of $unfiltered_link_count links" .
935        " ($total_hit_count total)");
936
937   return () if ($#pages < 0);
938
939   my $i = int(rand($#pages+1));
940   my $page = $pages[$i];
941
942   LOG ($verbose_load, "picked page $page");
943
944   $suppress_audit = 1;
945
946   my ( $base2, $body2 ) = get_document ($page, $base, $timeout);
947
948   if (!$base2 || !$body2) {
949     $body2 = undef;
950     return ();
951   }
952
953   my $img = pick_image_from_body ($base2, $body2);
954   $body2 = undef;
955
956   if ($img) {
957     return ($base2, $img);
958   } else {
959     return ();
960   }
961 }
962
963 \f
964 ############################################################################
965 #
966 # Pick images from random pages returned by the Yahoo Random Link
967 #
968 ############################################################################
969
970 # yahoorand
971 my $yahoo_random_link = "http://random.yahoo.com/bin/ryl";
972
973
974 # Picks a random page; picks a random image on that page;
975 # returns two URLs: the page containing the image, and the image.
976 # Returns () if nothing found this time.
977 #
978 sub pick_from_yahoo_random_link {
979   my ( $timeout ) = @_;
980
981   print STDERR "\n\n" if ($verbose_load);
982   LOG ($verbose_load, "URL: $yahoo_random_link");
983
984   $last_search = $yahoo_random_link;   # for warnings
985
986   $suppress_audit = 1;
987
988   my ( $base, $body ) = get_document ($yahoo_random_link, undef, $timeout);
989   if (!$base || !$body) {
990     $body = undef;
991     return;
992   }
993
994   LOG ($verbose_load, "redirected to: $base");
995
996   my $img = pick_image_from_body ($base, $body);
997   $body = undef;
998
999   if ($img) {
1000     return ($base, $img);
1001   } else {
1002     return ();
1003   }
1004 }
1005
1006 \f
1007 ############################################################################
1008 #
1009 # Pick images by feeding random words into Alta Vista Image Search
1010 #
1011 ############################################################################
1012
1013
1014 my $alta_vista_images_url = "http://www.altavista.com/cgi-bin/query" .
1015                             "?ipht=1" .       # photos
1016                             "&igrph=1" .      # graphics
1017                             "&iclr=1" .       # color
1018                             "&ibw=1" .        # b&w
1019                             "&micat=1" .      # no partner sites
1020                             "&imgset=1" .     # no partner sites
1021                             "&stype=simage" . # do image search
1022                             "&mmW=1" .        # unknown, but required
1023                             "&q=";
1024
1025 # imagevista
1026 sub pick_from_alta_vista_images {
1027   my ( $timeout ) = @_;
1028
1029   my $words = random_words(1);
1030   my $page = (int(rand(9)) + 1);
1031   my $search_url = $alta_vista_images_url . $words;
1032
1033   if ($page > 1) {
1034     $search_url .= "&pgno=" . $page;            # page number
1035     $search_url .= "&stq=" . (($page-1) * 12);  # first hit result on page
1036   }
1037
1038   my ($search_hit_count, @subpages) =
1039     pick_from_search_engine ($timeout, $search_url, $words);
1040
1041   my @candidates = ();
1042   foreach my $u (@subpages) {
1043
1044     # altavista is encoding their URLs now.
1045     next unless ($u =~ m@^/r.*\&r=([^&]+).*@);
1046     $u = url_unquote($1);
1047
1048     next unless ($u =~ m@^http://@i);    #  skip non-HTTP or relative URLs
1049     next if ($u =~ m@[/.]altavista\.com\b@i);     # skip altavista builtins
1050     next if ($u =~ m@[/.]doubleclick\.net\b@i);   # you cretins
1051     next if ($u =~ m@[/.]clicktomarket\.com\b@i); # more cretins
1052
1053     next if ($u =~ m@[/.]viewimages\.com\b@i);    # stacked deck
1054     next if ($u =~ m@[/.]gettyimages\.com\b@i);
1055
1056     LOG ($verbose_filter, "  candidate: $u");
1057     push @candidates, $u;
1058   }
1059
1060   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1061                                 $timeout, @candidates);
1062 }
1063
1064
1065 \f
1066 ############################################################################
1067 #
1068 # Pick images by feeding random words into Google Image Search.
1069 # By Charles Gales <gales@us.ibm.com>
1070 #
1071 ############################################################################
1072
1073
1074 my $google_images_url =     "http://images.google.com/images" .
1075                             "?site=images" .  # photos
1076                             "&btnG=Search" .  # graphics
1077                             "&safe=off" .     # no screening
1078                             "&imgsafe=off" .
1079                             "&q=";
1080
1081 # googleimgs
1082 sub pick_from_google_images {
1083   my ( $timeout ) = @_;
1084
1085   my $words = random_word;   # only one word for Google
1086   my $page = (int(rand(9)) + 1);
1087   my $num = 20;     # 20 images per page
1088   my $search_url = $google_images_url . $words;
1089
1090   if ($page > 1) {
1091     $search_url .= "&start=" . $page*$num;      # page number
1092     $search_url .= "&num="   . $num;            #images per page
1093   }
1094
1095   my ($search_hit_count, @subpages) =
1096     pick_from_search_engine ($timeout, $search_url, $words);
1097
1098   my @candidates = ();
1099   foreach my $u (@subpages) {
1100     next unless ($u =~ m@imgres\?imgurl@i);    #  All pics start with this
1101     next if ($u =~ m@[/.]google\.com\b@i);     # skip google builtins
1102
1103     if ($u =~ m@^/imgres\?imgurl=(.*?)\&imgrefurl=(.*?)\&@) {
1104       my $urlf = $2;
1105       LOG ($verbose_filter, "  candidate: $urlf");
1106       push @candidates, $urlf;
1107     }
1108   }
1109
1110   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1111                                 $timeout, @candidates);
1112 }
1113
1114
1115 \f
1116 ############################################################################
1117 #
1118 # Pick images by feeding random *numbers* into Google Image Search.
1119 # By jwz, suggested by from Ian O'Donnell.
1120 #
1121 ############################################################################
1122
1123
1124 # googlenums
1125 sub pick_from_google_image_numbers {
1126   my ( $timeout ) = @_;
1127
1128   my $max = 9999;
1129   my $number = int(rand($max));
1130
1131   $number = sprintf("%04d", $number)
1132     if (rand() < 0.3);
1133
1134   my $words = "$number";
1135   my $page = (int(rand(40)) + 1);
1136   my $num = 20;     # 20 images per page
1137   my $search_url = $google_images_url . $words;
1138
1139   if ($page > 1) {
1140     $search_url .= "&start=" . $page*$num;      # page number
1141     $search_url .= "&num="   . $num;            #images per page
1142   }
1143
1144   my ($search_hit_count, @subpages) =
1145     pick_from_search_engine ($timeout, $search_url, $words);
1146
1147   my @candidates = ();
1148   my %referers;
1149   foreach my $u (@subpages) {
1150     next unless ($u =~ m@imgres\?imgurl@i);    #  All pics start with this
1151     next if ($u =~ m@[/.]google\.com\b@i);     # skip google builtins
1152
1153     if ($u =~ m@^/imgres\?imgurl=(.*?)\&imgrefurl=(.*?)\&@) {
1154       my $ref = $2;
1155       my $img = "http://$1";
1156
1157       LOG ($verbose_filter, "  candidate: $ref");
1158       push @candidates, $img;
1159       $referers{$img} = $ref;
1160     }
1161   }
1162
1163   @candidates = depoison (@candidates);
1164   return () if ($#candidates < 0);
1165   my $i = int(rand($#candidates+1));
1166   my $img = $candidates[$i];
1167   my $ref = $referers{$img};
1168
1169   LOG ($verbose_load, "picked image " . ($i+1) . ": $img (on $ref)");
1170   return ($ref, $img);
1171 }
1172
1173
1174 \f
1175 ############################################################################
1176 #
1177 # Pick images by feeding random words into Alta Vista Text Search
1178 #
1179 ############################################################################
1180
1181
1182 my $alta_vista_url_1 = "http://www.altavista.com/cgi-bin/query?pg=q" .
1183                        "&text=yes&kl=XX&stype=stext&q=";
1184 my $alta_vista_url_2 = "http://www.altavista.com/sites/search/web?pg=q" .
1185                        "&kl=XX&search=Search&q=";
1186
1187 my $alta_vista_url = $alta_vista_url_2;
1188
1189 # altavista
1190 sub pick_from_alta_vista_text {
1191   my ( $timeout ) = @_;
1192
1193   my $words = random_words(1);
1194   my $page = (int(rand(9)) + 1);
1195   my $search_url = $alta_vista_url . $words;
1196
1197   if ($page > 1) {
1198     $search_url .= "&pgno=" . $page;
1199     $search_url .= "&stq=" . (($page-1) * 10);
1200   }
1201
1202   my ($search_hit_count, @subpages) =
1203     pick_from_search_engine ($timeout, $search_url, $words);
1204
1205   my @candidates = ();
1206   foreach my $u (@subpages) {
1207
1208     # Those altavista fuckers are playing really nasty redirection games
1209     # these days: the filter your clicks through their site, but use
1210     # onMouseOver to make it look like they're not!  Well, it makes it
1211     # easier for us to identify search results...
1212     #
1213     next unless ($u =~ m@^/r.*\&r=([^&]+).*@);
1214     $u = url_unquote($1);
1215
1216     LOG ($verbose_filter, "  candidate: $u");
1217     push @candidates, $u;
1218   }
1219
1220   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1221                                 $timeout, @candidates);
1222 }
1223
1224
1225 \f
1226 ############################################################################
1227 #
1228 # Pick images by feeding random words into Hotbot
1229 #
1230 ############################################################################
1231
1232 my $hotbot_search_url = "http://hotbot.lycos.com/" .
1233                         "?SM=SC" .
1234                         "&DV=0" .
1235                         "&LG=any" .
1236                         "&FVI=1" .
1237                         "&DC=100" .
1238                         "&DE=0" .
1239                         "&SQ=1" .
1240                         "&TR=13" .
1241                         "&AM1=MC" .
1242                         "&MT=";
1243
1244 sub pick_from_hotbot_text {
1245   my ( $timeout ) = @_;
1246
1247   my $words = random_words(0);
1248   my $search_url = $hotbot_search_url . $words;
1249
1250   my ($search_hit_count, @subpages) =
1251     pick_from_search_engine ($timeout, $search_url, $words);
1252
1253   my @candidates = ();
1254   foreach my $u (@subpages) {
1255
1256     # Hotbot plays redirection games too
1257     next unless ($u =~ m@^/director.asp\?target=([^&]+)@);
1258     $u = url_decode($1);
1259
1260     LOG ($verbose_filter, "  candidate: $u");
1261     push @candidates, $u;
1262   }
1263
1264   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1265                                 $timeout, @candidates);
1266 }
1267
1268
1269 \f
1270 ############################################################################
1271 #
1272 # Pick images by feeding random words into Lycos
1273 #
1274 ############################################################################
1275
1276 my $lycos_search_url = "http://lycospro.lycos.com/srchpro/" .
1277                        "?lpv=1" .
1278                        "&t=any" .
1279                        "&query=";
1280
1281 sub pick_from_lycos_text {
1282   my ( $timeout ) = @_;
1283
1284   my $words = random_words(0);
1285   my $start = int(rand(8)) * 10 + 1;
1286   my $search_url = $lycos_search_url . $words . "&start=$start";
1287
1288   my ($search_hit_count, @subpages) =
1289     pick_from_search_engine ($timeout, $search_url, $words);
1290
1291   my @candidates = ();
1292   foreach my $u (@subpages) {
1293
1294     # Lycos plays exact the same redirection game as hotbot.
1295     # Note that "id=0" is used for internal advertising links,
1296     # and 1+ are used for  search results.
1297     next unless ($u =~ m@^http://click.hotbot.com/director.asp
1298                          .*
1299                          [?&]id=[1-9]\d*
1300                          .*
1301                          \&target=([^&]+)
1302                          .*
1303                         @x);
1304     $u = url_decode($1);
1305
1306     LOG ($verbose_filter, "  candidate: $u");
1307     push @candidates, $u;
1308   }
1309
1310   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1311                                 $timeout, @candidates);
1312 }
1313
1314
1315 \f
1316 ############################################################################
1317 #
1318 # Pick images by feeding random words into news.yahoo.com
1319 #
1320 ############################################################################
1321
1322 my $yahoo_news_url = "http://search.news.yahoo.com/search/news_photos?" .
1323                      "&z=&n=100&o=o&2=&3=&p=";
1324
1325 # yahoonews
1326 sub pick_from_yahoo_news_text {
1327   my ( $timeout ) = @_;
1328
1329   my $words = random_words(1);
1330   my $search_url = $yahoo_news_url . $words;
1331
1332   my ($search_hit_count, @subpages) =
1333     pick_from_search_engine ($timeout, $search_url, $words);
1334
1335   my @candidates = ();
1336   foreach my $u (@subpages) {
1337     # only accept URLs on Yahoo's news site
1338     next unless ($u =~ m@^http://dailynews\.yahoo\.com/@i ||
1339                  $u =~ m@^http://story\.news\.yahoo\.com/@i);
1340
1341     LOG ($verbose_filter, "  candidate: $u");
1342     push @candidates, $u;
1343   }
1344
1345   return pick_image_from_pages ($search_url, $search_hit_count, $#subpages+1,
1346                                 $timeout, @candidates);
1347 }
1348
1349
1350
1351 \f
1352 ############################################################################
1353 #
1354 # Pick images by waiting for driftnet to populate a temp dir with files.
1355 # Requires driftnet version 0.1.5 or later.
1356 # (Driftnet is a program by Chris Lightfoot that sniffs your local ethernet
1357 # for images being downloaded by others.)
1358 # Driftnet/webcollage integration by jwz.
1359 #
1360 ############################################################################
1361
1362 # driftnet
1363 sub pick_from_driftnet {
1364   my ( $timeout ) = @_;
1365
1366   my $id = $driftnet_magic;
1367   my $dir = $driftnet_dir;
1368   my $start = time;
1369   my $now;
1370
1371   error ("\$driftnet_dir unset?") unless ($dir);
1372   $dir =~ s@/+$@@;
1373
1374   error ("$dir unreadable") unless (-d "$dir/.");
1375
1376   $timeout = $http_timeout unless ($timeout);
1377   $last_search = $id;
1378
1379   while ($now = time, $now < $start + $timeout) {
1380     local *DIR;
1381     opendir (DIR, $dir) || error ("$dir: $!");
1382     while (my $file = readdir(DIR)) {
1383       next if ($file =~ m/^\./);
1384       $file = "$dir/$file";
1385       closedir DIR;
1386       LOG ($verbose_load, "picked file $file ($id)");
1387       return ($id, $file);
1388     }
1389     closedir DIR;
1390   }
1391   LOG (($verbose_net || $verbose_load), "timed out for $id");
1392   return ();
1393 }
1394
1395
1396 sub get_driftnet_file {
1397   my ($file) = @_;
1398
1399   error ("\$driftnet_dir unset?") unless ($driftnet_dir);
1400
1401   my $id = $driftnet_magic;
1402   my $re = qr/$driftnet_dir/;
1403   error ("$id: $file not in $driftnet_dir?")
1404     unless ($file =~ m@^$re@o);
1405
1406   local *IN;
1407   open (IN, $file) || error ("$id: $file: $!");
1408   my $body = '';
1409   while (<IN>) { $body .= $_; }
1410   close IN;
1411   unlink ($file);
1412   return ($id, $body);
1413 }
1414
1415
1416 sub spawn_driftnet {
1417   my ($cmd) = @_;
1418
1419   # make a directory to use.
1420   while (1) {
1421     my $tmp = $ENV{TEMPDIR} || "/tmp";
1422     $driftnet_dir = sprintf ("$tmp/driftcollage-%08x", rand(0xffffffff));
1423     LOG ($verbose_exec, "mkdir $driftnet_dir");
1424     last if mkdir ($driftnet_dir, 0700);
1425   }
1426
1427   if (! ($cmd =~ m/\s/)) {
1428     # if the command didn't have any arguments in it, then it must be just
1429     # a pointer to the executable.  Append the default args to it.
1430     my $dargs = $default_driftnet_cmd;
1431     $dargs =~ s/^[^\s]+//;
1432     $cmd .= $dargs;
1433   }
1434
1435   # point the driftnet command at our newly-minted private directory.
1436   #
1437   $cmd .= " -d $driftnet_dir";
1438   $cmd .= ">/dev/null" unless ($verbose_exec);
1439
1440   my $pid = fork();
1441   if ($pid < 0) { error ("fork: $!\n"); }
1442   if ($pid) {
1443     # parent fork
1444     push @pids_to_kill, $pid;
1445     LOG ($verbose_exec, "forked for \"$cmd\"");
1446   } else {
1447     # child fork
1448     nontrapping_system ($cmd) || error ("exec: $!");
1449   }
1450
1451   # wait a bit, then make sure the process actually started up.
1452   #
1453   sleep (1);
1454   error ("pid $pid failed to start \"$cmd\"")
1455     unless (1 == kill (0, $pid));
1456 }
1457
1458 \f
1459 ############################################################################
1460 #
1461 # Pick a random image in a random way
1462 #
1463 ############################################################################
1464
1465
1466 # Picks a random image on a random page, and returns two URLs:
1467 # the page containing the image, and the image.
1468 # Returns () if nothing found this time.
1469 #
1470
1471 sub pick_image {
1472   my ( $timeout ) = @_;
1473
1474   $current_state = "select";
1475   $load_method = "none";
1476
1477   my $n = int(rand(100));
1478   my $fn = undef;
1479   my $total = 0;
1480   my @rest = @search_methods;
1481
1482   while (@rest) {
1483     my $pct  = shift @rest;
1484     my $name = shift @rest;
1485     my $tfn  = shift @rest;
1486     $total += $pct;
1487     if ($total > $n && !defined($fn)) {
1488       $fn = $tfn;
1489       $current_state = $name;
1490       $load_method = $current_state;
1491     }
1492   }
1493
1494   if ($total != 100) {
1495     error ("internal error: \@search_methods totals to $total%!");
1496   }
1497
1498   record_attempt ($current_state);
1499   return $fn->($timeout);
1500 }
1501
1502
1503 \f
1504 ############################################################################
1505 #
1506 # Statistics and logging
1507 #
1508 ############################################################################
1509
1510 sub timestr {
1511   return strftime ("%H:%M:%S: ", localtime);
1512 }
1513
1514 sub blurb {
1515   return "$progname: " . timestr() . "$current_state: ";
1516 }
1517
1518 sub error {
1519   my ($err) = @_;
1520   print STDERR blurb() . "$err\n";
1521   exit 1;
1522 }
1523
1524
1525 my $lastlog = "";
1526
1527 sub clearlog {
1528   $lastlog = "";
1529 }
1530
1531 sub showlog {
1532   my $head = "$progname: DEBUG: ";
1533   foreach (split (/\n/, $lastlog)) {
1534     print STDERR "$head$_\n";
1535   }
1536   $lastlog = "";
1537 }
1538
1539 sub LOG {
1540   my ($print, $msg) = @_;
1541   my $blurb = timestr() . "$current_state: ";
1542   $lastlog .= "$blurb$msg\n";
1543   print STDERR "$progname: $blurb$msg\n" if $print;
1544 }
1545
1546
1547 my %stats_attempts;
1548 my %stats_successes;
1549 my %stats_elapsed;
1550
1551 my $last_state = undef;
1552 sub record_attempt {
1553   my ($name) = @_;
1554
1555   if ($last_state) {
1556     record_failure($last_state) unless ($image_succeeded > 0);
1557   }
1558   $last_state = $name;
1559
1560   clearlog();
1561   report_performance();
1562
1563   start_timer($name);
1564   $image_succeeded = 0;
1565   $suppress_audit = 0;
1566 }
1567
1568 sub record_success {
1569   my ($name, $url, $base) = @_;
1570   if (defined($stats_successes{$name})) {
1571     $stats_successes{$name}++;
1572   } else {
1573     $stats_successes{$name} = 1;
1574   }
1575
1576   stop_timer ($name, 1);
1577   my $o = $current_state;
1578   $current_state = $name;
1579   save_recent_url ($url, $base);
1580   $current_state = $o;
1581   $image_succeeded = 1;
1582   clearlog();
1583 }
1584
1585
1586 sub record_failure {
1587   my ($name) = @_;
1588
1589   return if $image_succeeded;
1590
1591   stop_timer ($name, 0);
1592   if ($verbose_load && !$verbose_exec) {
1593
1594     if ($suppress_audit) {
1595       print STDERR "$progname: " . timestr() . "(audit log suppressed)\n";
1596       return;
1597     }
1598
1599     my $o = $current_state;
1600     $current_state = "DEBUG";
1601
1602     my $line =  "#" x 78;
1603     print STDERR "\n\n\n";
1604     print STDERR ("#" x 78) . "\n";
1605     print STDERR blurb() . "failed to get an image.  Full audit log:\n";
1606     print STDERR "\n";
1607     showlog();
1608     print STDERR ("-" x 78) . "\n";
1609     print STDERR "\n\n";
1610
1611     $current_state = $o;
1612   }
1613   $image_succeeded = 0;
1614 }
1615
1616
1617
1618 sub stats_of {
1619   my ($name) = @_;
1620   my $i = $stats_successes{$name};
1621   my $j = $stats_attempts{$name};
1622   $i = 0 unless $i;
1623   $j = 0 unless $j;
1624   return "" . ($j ? int($i * 100 / $j) : "0") . "%";
1625 }
1626
1627
1628 my $current_start_time = 0;
1629
1630 sub start_timer {
1631   my ($name) = @_;
1632   $current_start_time = time;
1633
1634   if (defined($stats_attempts{$name})) {
1635     $stats_attempts{$name}++;
1636   } else {
1637     $stats_attempts{$name} = 1;
1638   }
1639   if (!defined($stats_elapsed{$name})) {
1640     $stats_elapsed{$name} = 0;
1641   }
1642 }
1643
1644 sub stop_timer {
1645   my ($name, $success) = @_;
1646   $stats_elapsed{$name} += time - $current_start_time;
1647 }
1648
1649
1650 my $last_report_time = 0;
1651 sub report_performance {
1652
1653   return unless $verbose_warnings;
1654
1655   my $now = time;
1656   return unless ($now >= $last_report_time + $report_performance_interval);
1657   my $ot = $last_report_time;
1658   $last_report_time = $now;
1659
1660   return if ($ot == 0);
1661
1662   my $blurb = "$progname: " . timestr();
1663
1664   print STDERR "\n";
1665   print STDERR "${blurb}Current standings:\n";
1666
1667   foreach my $name (sort keys (%stats_attempts)) {
1668     my $try = $stats_attempts{$name};
1669     my $suc = $stats_successes{$name} || 0;
1670     my $pct = int($suc * 100 / $try);
1671     my $secs = $stats_elapsed{$name};
1672     my $secs_link = int($secs / $try);
1673     print STDERR sprintf ("$blurb   %-12s %4s (%d/%d);\t %2d secs/link\n",
1674                           "$name:", "$pct%", $suc, $try, $secs_link);
1675   }
1676 }
1677
1678
1679
1680 my $max_recent_images = 400;
1681 my $max_recent_sites  = 20;
1682 my @recent_images = ();
1683 my @recent_sites = ();
1684
1685 sub save_recent_url {
1686   my ($url, $base) = @_;
1687
1688   return unless ($verbose_warnings);
1689
1690   $_ = $url;
1691   my ($site) = m@^http://([^ \t\n\r/:]+)@;
1692
1693   if ($base eq $driftnet_magic) {
1694     $site = $driftnet_magic;
1695     @recent_images = ();
1696   }
1697
1698   my $done = 0;
1699   foreach (@recent_images) {
1700     if ($_ eq $url) {
1701       print STDERR blurb() . "WARNING: recently-duplicated image: $url" .
1702         " (on $base via $last_search)\n";
1703       $done = 1;
1704       last;
1705     }
1706   }
1707
1708   # suppress "duplicate site" warning via %warningless_sites.
1709   #
1710   if ($warningless_sites{$site}) {
1711     $done = 1;
1712   } elsif ($site =~ m@([^.]+\.[^.]+\.[^.]+)$@ &&
1713            $warningless_sites{$1}) {
1714     $done = 1;
1715   } elsif ($site =~ m@([^.]+\.[^.]+)$@ &&
1716            $warningless_sites{$1}) {
1717     $done = 1;
1718   }
1719
1720   if (!$done) {
1721     foreach (@recent_sites) {
1722       if ($_ eq $site) {
1723         print STDERR blurb() . "WARNING: recently-duplicated site: $site" .
1724         " ($url on $base via $last_search)\n";
1725         last;
1726       }
1727     }
1728   }
1729
1730   push @recent_images, $url;
1731   push @recent_sites,  $site;
1732   shift @recent_images if ($#recent_images >= $max_recent_images);
1733   shift @recent_sites  if ($#recent_sites  >= $max_recent_sites);
1734 }
1735
1736
1737 \f
1738 ##############################################################################
1739 #
1740 # other utilities
1741 #
1742 ##############################################################################
1743
1744 # Does %-decoding.
1745 #
1746 sub url_decode {
1747   ($_) = @_;
1748   tr/+/ /;
1749   s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
1750   return $_;
1751 }
1752
1753
1754 # Given the raw body of a GIF document, returns the dimensions of the image.
1755 #
1756 sub gif_size {
1757   my ($body) = @_;
1758   my $type = substr($body, 0, 6);
1759   my $s;
1760   return () unless ($type =~ /GIF8[7,9]a/);
1761   $s = substr ($body, 6, 10);
1762   my ($a,$b,$c,$d) = unpack ("C"x4, $s);
1763   return (($b<<8|$a), ($d<<8|$c));
1764 }
1765
1766 # Given the raw body of a JPEG document, returns the dimensions of the image.
1767 #
1768 sub jpeg_size {
1769   my ($body) = @_;
1770   my $i = 0;
1771   my $L = length($body);
1772
1773   my $c1 = substr($body, $i, 1); $i++;
1774   my $c2 = substr($body, $i, 1); $i++;
1775   return () unless (ord($c1) == 0xFF && ord($c2) == 0xD8);
1776
1777   my $ch = "0";
1778   while (ord($ch) != 0xDA && $i < $L) {
1779     # Find next marker, beginning with 0xFF.
1780     while (ord($ch) != 0xFF) {
1781       return () if (length($body) <= $i);
1782       $ch = substr($body, $i, 1); $i++;
1783     }
1784     # markers can be padded with any number of 0xFF.
1785     while (ord($ch) == 0xFF) {
1786       return () if (length($body) <= $i);
1787       $ch = substr($body, $i, 1); $i++;
1788     }
1789
1790     # $ch contains the value of the marker.
1791     my $marker = ord($ch);
1792
1793     if (($marker >= 0xC0) &&
1794         ($marker <= 0xCF) &&
1795         ($marker != 0xC4) &&
1796         ($marker != 0xCC)) {  # it's a SOFn marker
1797       $i += 3;
1798       return () if (length($body) <= $i);
1799       my $s = substr($body, $i, 4); $i += 4;
1800       my ($a,$b,$c,$d) = unpack("C"x4, $s);
1801       return (($c<<8|$d), ($a<<8|$b));
1802
1803     } else {
1804       # We must skip variables, since FFs in variable names aren't
1805       # valid JPEG markers.
1806       return () if (length($body) <= $i);
1807       my $s = substr($body, $i, 2); $i += 2;
1808       my ($c1, $c2) = unpack ("C"x2, $s);
1809       my $length = ($c1 << 8) | $c2;
1810       return () if ($length < 2);
1811       $i += $length-2;
1812     }
1813   }
1814   return ();
1815 }
1816
1817 # Given the raw body of a GIF or JPEG document, returns the dimensions of
1818 # the image.
1819 #
1820 sub image_size {
1821   my ($body) = @_;
1822   my ($w, $h) = gif_size ($body);
1823   if ($w && $h) { return ($w, $h); }
1824   return jpeg_size ($body);
1825 }
1826
1827
1828 # returns the full path of the named program, or undef.
1829 #
1830 sub which {
1831   my ($prog) = @_;
1832   foreach (split (/:/, $ENV{PATH})) {
1833     if (-x "$_/$prog") {
1834       return $prog;
1835     }
1836   }
1837   return undef;
1838 }
1839
1840
1841 # Like rand(), but chooses numbers with a bell curve distribution.
1842 sub bellrand {
1843   ($_) = @_;
1844   $_ = 1.0 unless defined($_);
1845   $_ /= 3.0;
1846   return (rand($_) + rand($_) + rand($_));
1847 }
1848
1849
1850 sub signal_cleanup {
1851   my ($sig) = @_;
1852   print STDERR blurb() . (defined($sig)
1853                           ? "caught signal $sig."
1854                           : "exiting.")
1855                        . "\n"
1856     if ($verbose_exec);
1857
1858   x_cleanup();
1859
1860   if (@pids_to_kill) {
1861     print STDERR blurb() . "killing: " . join(' ', @pids_to_kill) . "\n";
1862     kill ('TERM', @pids_to_kill);
1863   }
1864
1865   exit 1;
1866 }
1867
1868 ##############################################################################
1869 #
1870 # Generating a list of urls only
1871 #
1872 ##############################################################################
1873
1874 sub url_only_output {
1875   do {
1876     my ($base, $img) = pick_image;
1877     if ($img) {
1878       $base =~ s/ /%20/g;
1879       $img  =~ s/ /%20/g;
1880       print "$img $base\n";
1881     }
1882   } while (1);
1883 }
1884
1885 ##############################################################################
1886 #
1887 # Running as an xscreensaver module
1888 #
1889 ##############################################################################
1890
1891 my $image_ppm   = ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp") . "/webcollage." . $$;
1892 my $image_tmp1  = $image_ppm . "-1";
1893 my $image_tmp2  = $image_ppm . "-2";
1894
1895 my $filter_cmd = undef;
1896 my $post_filter_cmd = undef;
1897 my $background = undef;
1898
1899 my $img_width;            # size of the image being generated.
1900 my $img_height;
1901
1902 my $delay = 2;
1903
1904 sub x_cleanup {
1905   unlink $image_ppm, $image_tmp1, $image_tmp2;
1906 }
1907
1908
1909 # Like system, but prints status about exit codes, and kills this process
1910 # with whatever signal killed the sub-process, if any.
1911 #
1912 sub nontrapping_system {
1913   $! = 0;
1914
1915   $_ = join(" ", @_);
1916   s/\"[^\"]+\"/\"...\"/g;
1917
1918   LOG ($verbose_exec, "executing \"$_\"");
1919
1920   my $rc = system @_;
1921
1922   if ($rc == 0) {
1923     LOG ($verbose_exec, "subproc exited normally.");
1924   } elsif (($rc & 0xff) == 0) {
1925     $rc >>= 8;
1926     LOG ($verbose_exec, "subproc exited with status $rc.");
1927   } else {
1928     if ($rc & 0x80) {
1929       LOG ($verbose_exec, "subproc dumped core.");
1930       $rc &= ~0x80;
1931     }
1932     LOG ($verbose_exec, "subproc died with signal $rc.");
1933     # die that way ourselves.
1934     kill $rc, $$;
1935   }
1936
1937   return $rc;
1938 }
1939
1940
1941 # Given the URL of a GIF or JPEG image, and the body of that image, writes a
1942 # PPM to the given output file.  Returns the width/height of the image if
1943 # successful.
1944 #
1945 sub image_to_pnm {
1946   my ($url, $body, $output) = @_;
1947   my ($cmd, $cmd2, $w, $h);
1948
1949   if ((@_ = gif_size ($body))) {
1950     ($w, $h) = @_;
1951     $cmd = "giftopnm";
1952   } elsif ((@_ = jpeg_size ($body))) {
1953     ($w, $h) = @_;
1954     $cmd = "djpeg";
1955   } else {
1956     LOG (($verbose_pbm || $verbose_load),
1957          "not a GIF or JPG" .
1958          (($body =~ m@<(base|html|head|body|script|table|a href)>@i)
1959           ? " (looks like HTML)" : "") .
1960          ": $url");
1961     $suppress_audit = 1;
1962     return ();
1963   }
1964
1965   $cmd2 = "exec $cmd";        # yes, this really is necessary.  if we don't
1966                               # do this, the process doesn't die properly.
1967   if (!$verbose_pbm) {
1968     #
1969     # We get a "giftopnm: got a 'Application Extension' extension"
1970     # warning any time it's an animgif.
1971     #
1972     # Note that "giftopnm: EOF / read error on image data" is not
1973     # always a fatal error -- sometimes the image looks fine anyway.
1974     #
1975     $cmd2 .= " 2>/dev/null";
1976   }
1977
1978   # There exist corrupted GIF and JPEG files that can make giftopnm and
1979   # djpeg lose their minds and go into a loop.  So this gives those programs
1980   # a small timeout -- if they don't complete in time, kill them.
1981   #
1982   my $pid;
1983   @_ = eval {
1984     my $timed_out;
1985
1986     local $SIG{ALRM}  = sub {
1987       LOG ($verbose_pbm,
1988            "timed out ($cvt_timeout) for $cmd on \"$url\" in pid $pid");
1989       kill ('TERM', $pid) if ($pid);
1990       $timed_out = 1;
1991       $body = undef;
1992     };
1993
1994     if (($pid = open(PIPE, "| $cmd2 > $output"))) {
1995       $timed_out = 0;
1996       alarm $cvt_timeout;
1997       print PIPE $body;
1998       $body = undef;
1999       close PIPE;
2000
2001       LOG ($verbose_exec, "awaiting $pid");
2002       waitpid ($pid, 0);
2003       LOG ($verbose_exec, "$pid completed");
2004
2005       my $size = (stat($output))[7];
2006       $size = -1 unless defined($size);
2007       if ($size < 5) {
2008         LOG ($verbose_pbm, "$cmd on ${w}x$h \"$url\" failed ($size bytes)");
2009         return ();
2010       }
2011
2012       LOG ($verbose_pbm, "created ${w}x$h $output ($cmd)");
2013       return ($w, $h);
2014     } else {
2015       print STDERR blurb() . "$cmd failed: $!\n";
2016       return ();
2017     }
2018   };
2019   die if ($@ && $@ ne "alarm\n");       # propagate errors
2020   if ($@) {
2021     # timed out
2022     $body = undef;
2023     return ();
2024   } else {
2025     # didn't
2026     alarm 0;
2027     $body = undef;
2028     return @_;
2029   }
2030 }
2031
2032 sub pick_root_displayer {
2033   my @names = ();
2034
2035   foreach my $cmd (@root_displayers) {
2036     $_ = $cmd;
2037     my ($name) = m/^([^ ]+)/;
2038     push @names, "\"$name\"";
2039     LOG ($verbose_exec, "looking for $name...");
2040     foreach my $dir (split (/:/, $ENV{PATH})) {
2041       LOG ($verbose_exec, "  checking $dir/$name");
2042       return $cmd if (-x "$dir/$name");
2043     }
2044   }
2045
2046   $names[$#names] = "or " . $names[$#names];
2047   error "none of: " . join (", ", @names) . " were found on \$PATH.";
2048 }
2049
2050
2051 my $ppm_to_root_window_cmd = undef;
2052
2053
2054 sub x_or_pbm_output {
2055
2056   # Check for our helper program, to see whether we need to use PPM pipelines.
2057   #
2058   $_ = "webcollage-helper";
2059   if (defined ($webcollage_helper) || which ($_)) {
2060     $webcollage_helper = $_ unless (defined($webcollage_helper));
2061     LOG ($verbose_pbm, "found \"$webcollage_helper\"");
2062     $webcollage_helper .= " -v";
2063   } else {
2064     LOG (($verbose_pbm || $verbose_load), "no $_ program");
2065   }
2066
2067   # make sure the various programs we execute exist, right up front.
2068   #
2069   my @progs = ("ppmmake");  # always need this one
2070
2071   if (!defined($webcollage_helper)) {
2072     # Only need these others if we don't have the helper.
2073     @progs = (@progs, "giftopnm", "djpeg", "pnmpaste", "pnmscale", "pnmcut");
2074   }
2075
2076   foreach (@progs) {
2077     which ($_) || error "$_ not found on \$PATH.";
2078   }
2079
2080   # find a root-window displayer program.
2081   #
2082   $ppm_to_root_window_cmd = pick_root_displayer();
2083
2084   if (!$img_width || !$img_height) {
2085     $_ = "xdpyinfo";
2086     which ($_) || error "$_ not found on \$PATH.";
2087     $_ = `$_`;
2088     ($img_width, $img_height) = m/dimensions: *(\d+)x(\d+) /;
2089     if (!defined($img_height)) {
2090       error "xdpyinfo failed.";
2091     }
2092   }
2093
2094   my $bgcolor = "#000000";
2095   my $bgimage = undef;
2096
2097   if ($background) {
2098     if ($background =~ m/^\#[0-9a-f]+$/i) {
2099       $bgcolor = $background;
2100
2101     } elsif (-r $background) {
2102       $bgimage = $background;
2103
2104     } elsif (! $background =~ m@^[-a-z0-9 ]+$@i) {
2105       error "not a color or readable file: $background";
2106
2107     } else {
2108       # default to assuming it's a color
2109       $bgcolor = $background;
2110     }
2111   }
2112
2113   # Create the sold-colored base image.
2114   #
2115   $_ = "ppmmake '$bgcolor' $img_width $img_height";
2116   LOG ($verbose_pbm, "creating base image: $_");
2117   nontrapping_system "$_ > $image_ppm";
2118
2119   # Paste the default background image in the middle of it.
2120   #
2121   if ($bgimage) {
2122     my ($iw, $ih);
2123
2124     my $body = "";
2125     local *IMG;
2126     open(IMG, "<$bgimage") || error "couldn't open $bgimage: $!";
2127     my $cmd;
2128     while (<IMG>) { $body .= $_; }
2129     close (IMG);
2130
2131     if ((@_ = gif_size ($body))) {
2132       ($iw, $ih) = @_;
2133       $cmd = "giftopnm |";
2134
2135     } elsif ((@_ = jpeg_size ($body))) {
2136       ($iw, $ih) = @_;
2137       $cmd = "djpeg |";
2138
2139     } elsif ($body =~ m/^P\d\n(\d+) (\d+)\n/) {
2140       $iw = $1;
2141       $ih = $2;
2142       $cmd = "";
2143
2144     } else {
2145       error "$bgimage is not a GIF, JPEG, or PPM.";
2146     }
2147
2148     my $x = int (($img_width  - $iw) / 2);
2149     my $y = int (($img_height - $ih) / 2);
2150     LOG ($verbose_pbm,
2151          "pasting $bgimage (${iw}x$ih) into base image at $x,$y");
2152
2153     $cmd .= "pnmpaste - $x $y $image_ppm > $image_tmp1";
2154     open (IMG, "| $cmd") || error "running $cmd: $!";
2155     print IMG $body;
2156     $body = undef;
2157     close (IMG);
2158     LOG ($verbose_exec, "subproc exited normally.");
2159     rename ($image_tmp1, $image_ppm) ||
2160       error "renaming $image_tmp1 to $image_ppm: $!";
2161   }
2162
2163   clearlog();
2164
2165   while (1) {
2166     my ($base, $img) = pick_image();
2167     my $source = $current_state;
2168     $current_state = "loadimage";
2169     if ($img) {
2170       my ($headers, $body) = get_document ($img, $base);
2171       if ($body) {
2172         paste_image ($base, $img, $body, $source);
2173         $body = undef;
2174       }
2175     }
2176     $current_state = "idle";
2177     $load_method = "none";
2178
2179     unlink $image_tmp1, $image_tmp2;
2180     sleep $delay;
2181   }
2182 }
2183
2184 sub paste_image {
2185   my ($base, $img, $body, $source) = @_;
2186
2187   $current_state = "paste";
2188
2189   $suppress_audit = 0;
2190
2191   LOG ($verbose_pbm, "got $img (" . length($body) . ")");
2192
2193   my ($iw, $ih);
2194
2195   # If we are using the webcollage-helper, then we do not need to convert this
2196   # image to a PPM.  But, if we're using a filter command, we still must, since
2197   # that's what the filters expect (webcollage-helper can read PPMs, so that's
2198   # fine.)
2199   #
2200   if (defined ($webcollage_helper) &&
2201       !defined ($filter_cmd)) {
2202
2203     ($iw, $ih) = image_size ($body);
2204     if (!$iw || !$ih) {
2205       LOG (($verbose_pbm || $verbose_load),
2206            "not a GIF or JPG" .
2207            (($body =~ m@<(base|html|head|body|script|table|a href)>@i)
2208             ? " (looks like HTML)" : "") .
2209            ": $img");
2210       $suppress_audit = 1;
2211       $body = undef;
2212       return 0;
2213     }
2214
2215     local *OUT;
2216     open (OUT, ">$image_tmp1") || error ("writing $image_tmp1: $!");
2217     print OUT $body || error ("writing $image_tmp1: $!");
2218     close OUT || error ("writing $image_tmp1: $!");
2219
2220   } else {
2221     ($iw, $ih) = image_to_pnm ($img, $body, $image_tmp1);
2222     $body = undef;
2223     if (!$iw || !$ih) {
2224       LOG ($verbose_pbm, "unable to make PBM from $img");
2225       return 0;
2226     }
2227   }
2228
2229   record_success ($load_method, $img, $base);
2230
2231
2232   my $ow = $iw;  # used only for error messages
2233   my $oh = $ih;
2234
2235   # don't just tack this onto the front of the pipeline -- we want it to
2236   # be able to change the size of the input image.
2237   #
2238   if ($filter_cmd) {
2239     LOG ($verbose_pbm, "running $filter_cmd");
2240
2241     my $rc = nontrapping_system "($filter_cmd) < $image_tmp1 >$image_tmp2";
2242     if ($rc != 0) {
2243       LOG(($verbose_pbm || $verbose_load), "failed command: \"$filter_cmd\"");
2244       LOG(($verbose_pbm || $verbose_load), "failed URL: \"$img\" (${ow}x$oh)");
2245       return;
2246     }
2247     rename ($image_tmp2, $image_tmp1);
2248
2249     # re-get the width/height in case the filter resized it.
2250     local *IMG;
2251     open(IMG, "<$image_tmp1") || return 0;
2252     $_ = <IMG>;
2253     $_ = <IMG>;
2254     ($iw, $ih) = m/^(\d+) (\d+)$/;
2255     close (IMG);
2256     return 0 unless ($iw && $ih);
2257   }
2258
2259   my $target_w = $img_width;
2260   my $target_h = $img_height;
2261
2262   my $cmd = "";
2263   my $scale = 1.0;
2264
2265
2266   # Usually scale the image to fit on the screen -- but sometimes scale it
2267   # to fit on half or a quarter of the screen.  Note that we don't merely
2268   # scale it to fit, we instead cut it in half until it fits -- that should
2269   # give a wider distribution of sizes.
2270   #
2271   if (rand() < 0.3) { $target_w /= 2; $target_h /= 2; $scale /= 2; }
2272   if (rand() < 0.3) { $target_w /= 2; $target_h /= 2; $scale /= 2; }
2273
2274   if ($iw > $target_w || $ih > $target_h) {
2275     while ($iw > $target_w ||
2276            $ih > $target_h) {
2277       $iw = int($iw / 2);
2278       $ih = int($ih / 2);
2279     }
2280     if ($iw <= 10 || $ih <= 10) {
2281       LOG ($verbose_pbm, "scaling to ${iw}x$ih would have been bogus.");
2282       return 0;
2283     }
2284
2285     LOG ($verbose_pbm, "scaling to ${iw}x$ih");
2286
2287     $cmd .= " | pnmscale -xsize $iw -ysize $ih";
2288   }
2289
2290
2291   my $src = $image_tmp1;
2292
2293   my $crop_x = 0;     # the sub-rectangle of the image
2294   my $crop_y = 0;     # that we will actually paste.
2295   my $crop_w = $iw;
2296   my $crop_h = $ih;
2297
2298   # The chance that we will randomly crop out a section of an image starts
2299   # out fairly low, but goes up for images that are very large, or images
2300   # that have ratios that make them look like banners (we try to avoid
2301   # banner images entirely, but they slip through when the IMG tags didn't
2302   # have WIDTH and HEIGHT specified.)
2303   #
2304   my $crop_chance = 0.2;
2305   if ($iw > $img_width * 0.4 || $ih > $img_height * 0.4) {
2306     $crop_chance += 0.2;
2307   }
2308   if ($iw > $img_width * 0.7 || $ih > $img_height * 0.7) {
2309     $crop_chance += 0.2;
2310   }
2311   if ($min_ratio && ($iw * $min_ratio) > $ih) {
2312     $crop_chance += 0.7;
2313   }
2314
2315   if ($crop_chance > 0.1) {
2316     LOG ($verbose_pbm, "crop chance: $crop_chance");
2317   }
2318
2319   if (rand() < $crop_chance) {
2320
2321     my $ow = $crop_w;
2322     my $oh = $crop_h;
2323
2324     if ($crop_w > $min_width) {
2325       # if it's a banner, select the width linearly.
2326       # otherwise, select a bell.
2327       my $r = (($min_ratio && ($iw * $min_ratio) > $ih)
2328                ? rand()
2329                : bellrand());
2330       $crop_w = $min_width + int ($r * ($crop_w - $min_width));
2331       $crop_x = int (rand() * ($ow - $crop_w));
2332     }
2333     if ($crop_h > $min_height) {
2334       # height always selects as a bell.
2335       $crop_h = $min_height + int (bellrand() * ($crop_h - $min_height));
2336       $crop_y = int (rand() * ($oh - $crop_h));
2337     }
2338
2339     if ($crop_x != 0   || $crop_y != 0 ||
2340         $crop_w != $iw || $crop_h != $ih) {
2341       LOG ($verbose_pbm,
2342            "randomly cropping to ${crop_w}x$crop_h \@ $crop_x,$crop_y");
2343     }
2344   }
2345
2346   # Where the image should logically land -- this might be negative.
2347   #
2348   my $x = int((rand() * ($img_width  + $crop_w/2)) - $crop_w*3/4);
2349   my $y = int((rand() * ($img_height + $crop_h/2)) - $crop_h*3/4);
2350
2351   # if we have chosen to paste the image outside of the rectangle of the
2352   # screen, then we need to crop it.
2353   #
2354   if ($x < 0 ||
2355       $y < 0 ||
2356       $x + $crop_w > $img_width ||
2357       $y + $crop_h > $img_height) {
2358
2359     LOG ($verbose_pbm,
2360          "cropping for effective paste of ${crop_w}x$crop_h \@ $x,$y");
2361
2362     if ($x < 0) { $crop_x -= $x; $crop_w += $x; $x = 0; }
2363     if ($y < 0) { $crop_y -= $y; $crop_h += $y; $y = 0; }
2364
2365     if ($x + $crop_w >= $img_width)  { $crop_w = $img_width  - $x - 1; }
2366     if ($y + $crop_h >= $img_height) { $crop_h = $img_height - $y - 1; }
2367   }
2368
2369   # If any cropping needs to happen, add pnmcut.
2370   #
2371   if ($crop_x != 0   || $crop_y != 0 ||
2372         $crop_w != $iw || $crop_h != $ih) {
2373     $iw = $crop_w;
2374     $ih = $crop_h;
2375     $cmd .= " | pnmcut $crop_x $crop_y $iw $ih";
2376     LOG ($verbose_pbm, "cropping to ${crop_w}x$crop_h \@ $crop_x,$crop_y");
2377   }
2378
2379   LOG ($verbose_pbm, "pasting ${iw}x$ih \@ $x,$y in $image_ppm");
2380
2381   $cmd .= " | pnmpaste - $x $y $image_ppm";
2382
2383   $cmd =~ s@^ *\| *@@;
2384
2385   if (defined ($webcollage_helper)) {
2386     $cmd = "$webcollage_helper $image_tmp1 $image_ppm " .
2387                               "$scale $opacity " .
2388                               "$crop_x $crop_y $x $y " .
2389                               "$iw $ih";
2390     $_ = $cmd;
2391
2392   } else {
2393     # use a PPM pipeline
2394     $_ = "($cmd)";
2395     $_ .= " < $image_tmp1 > $image_tmp2";
2396   }
2397
2398   if ($verbose_pbm) {
2399     $_ = "($_) 2>&1 | sed s'/^/" . blurb() . "/'";
2400   } else {
2401     $_ .= " 2> /dev/null";
2402   }
2403
2404   my $rc = nontrapping_system ($_);
2405
2406   if (defined ($webcollage_helper) && -z $image_ppm) {
2407     LOG (1, "failed command: \"$cmd\"");
2408     print STDERR "\naudit log:\n\n\n";
2409     print STDERR ("#" x 78) . "\n";
2410     print STDERR blurb() . "$image_ppm has zero size\n";
2411     showlog();
2412     print STDERR "\n\n";
2413     exit (1);
2414   }
2415
2416   if ($rc != 0) {
2417     LOG (($verbose_pbm || $verbose_load), "failed command: \"$cmd\"");
2418     LOG (($verbose_pbm || $verbose_load), "failed URL: \"$img\" (${ow}x$oh)");
2419     return;
2420   }
2421
2422   if (!defined ($webcollage_helper)) {
2423     rename ($image_tmp2, $image_ppm) || return;
2424   }
2425
2426   my $target = "$image_ppm";
2427
2428   # don't just tack this onto the end of the pipeline -- we don't want it
2429   # to end up in $image_ppm, because we don't want the results to be
2430   # cumulative.
2431   #
2432   if ($post_filter_cmd) {
2433
2434     my $cmd;
2435
2436     $target = $image_tmp1;
2437     if (!defined ($webcollage_helper)) {
2438       $cmd = "($post_filter_cmd) < $image_ppm > $target";
2439     } else {
2440       # Blah, my scripts need the JPEG data, but some other folks need
2441       # the PPM data -- what to do?  Ignore the problem, that's what!
2442 #     $cmd = "djpeg < $image_ppm | ($post_filter_cmd) > $target";
2443       $cmd = "($post_filter_cmd) < $image_ppm > $target";
2444     }
2445
2446     $rc = nontrapping_system ($cmd);
2447     if ($rc != 0) {
2448       LOG ($verbose_pbm, "filter failed: \"$post_filter_cmd\"\n");
2449       return;
2450     }
2451   }
2452
2453   if (!$no_output_p) {
2454     my $tsize = (stat($target))[7];
2455     if ($tsize > 200) {
2456       $cmd = "$ppm_to_root_window_cmd $target";
2457
2458       # xv seems to hate being killed.  it tends to forget to clean
2459       # up after itself, and leaves windows around and colors allocated.
2460       # I had this same problem with vidwhacker, and I'm not entirely
2461       # sure what I did to fix it.  But, let's try this: launch xv
2462       # in the background, so that killing this process doesn't kill it.
2463       # it will die of its own accord soon enough.  So this means we
2464       # start pumping bits to the root window in parallel with starting
2465       # the next network retrieval, which is probably a better thing
2466       # to do anyway.
2467       #
2468       $cmd .= " &";
2469
2470       $rc = nontrapping_system ($cmd);
2471
2472       if ($rc != 0) {
2473         LOG (($verbose_pbm || $verbose_load), "display failed: \"$cmd\"");
2474         return;
2475       }
2476
2477     } else {
2478       LOG ($verbose_pbm, "$target size is $tsize");
2479     }
2480   }
2481
2482   $source .= "-" . stats_of($source);
2483   print STDOUT "image: ${iw}x${ih} @ $x,$y $base $source\n"
2484     if ($verbose_imgmap);
2485
2486   clearlog();
2487
2488   return 1;
2489 }
2490
2491
2492 sub init_signals {
2493
2494   $SIG{HUP}  = \&signal_cleanup;
2495   $SIG{INT}  = \&signal_cleanup;
2496   $SIG{QUIT} = \&signal_cleanup;
2497   $SIG{ABRT} = \&signal_cleanup;
2498   $SIG{KILL} = \&signal_cleanup;
2499   $SIG{TERM} = \&signal_cleanup;
2500
2501   # Need this so that if giftopnm dies, we don't die.
2502   $SIG{PIPE} = 'IGNORE';
2503 }
2504
2505 END { signal_cleanup(); }
2506
2507
2508 sub main {
2509   $| = 1;
2510   srand(time ^ $$);
2511
2512   my $verbose = 0;
2513   my $dict;
2514   my $driftnet_cmd = 0;
2515
2516   $current_state = "init";
2517   $load_method = "none";
2518
2519   my $root_p = 0;
2520
2521   # historical suckage: the environment variable name is lower case.
2522   $http_proxy = $ENV{http_proxy} || $ENV{HTTP_PROXY};
2523
2524   while ($_ = $ARGV[0]) {
2525     shift @ARGV;
2526     if ($_ eq "-display" ||
2527         $_ eq "-displ" ||
2528         $_ eq "-disp" ||
2529         $_ eq "-dis" ||
2530         $_ eq "-dpy" ||
2531         $_ eq "-d") {
2532       $ENV{DISPLAY} = shift @ARGV;
2533     } elsif ($_ eq "-root") {
2534       $root_p = 1;
2535     } elsif ($_ eq "-no-output") {
2536       $no_output_p = 1;
2537     } elsif ($_ eq "-urls-only") {
2538       $urls_only_p = 1;
2539       $no_output_p = 1;
2540     } elsif ($_ eq "-verbose") {
2541       $verbose++;
2542     } elsif (m/^-v+$/) {
2543       $verbose += length($_)-1;
2544     } elsif ($_ eq "-delay") {
2545       $delay = shift @ARGV;
2546     } elsif ($_ eq "-timeout") {
2547       $http_timeout = shift @ARGV;
2548     } elsif ($_ eq "-filter") {
2549       $filter_cmd = shift @ARGV;
2550     } elsif ($_ eq "-filter2") {
2551       $post_filter_cmd = shift @ARGV;
2552     } elsif ($_ eq "-background" || $_ eq "-bg") {
2553       $background = shift @ARGV;
2554     } elsif ($_ eq "-size") {
2555       $_ = shift @ARGV;
2556       if (m@^(\d+)x(\d+)$@) {
2557         $img_width = $1;
2558         $img_height = $2;
2559       } else {
2560         error "argument to \"-size\" must be of the form \"640x400\"";
2561       }
2562     } elsif ($_ eq "-proxy" || $_ eq "-http-proxy") {
2563       $http_proxy = shift @ARGV;
2564     } elsif ($_ eq "-dictionary" || $_ eq "-dict") {
2565       $dict = shift @ARGV;
2566     } elsif ($_ eq "-driftnet" || $_ eq "--driftnet") {
2567       @search_methods = ( 100, "driftnet", \&pick_from_driftnet );
2568       if (! ($ARGV[0] =~ m/^-/)) {
2569         $driftnet_cmd = shift @ARGV;
2570       } else {
2571         $driftnet_cmd = $default_driftnet_cmd;
2572       }
2573     } elsif ($_ eq "-debug" || $_ eq "--debug") {
2574       my $which = shift @ARGV;
2575       my @rest = @search_methods;
2576       my $ok = 0;
2577       while (@rest) {
2578         my $pct  = shift @rest;
2579         my $name = shift @rest;
2580         my $tfn  = shift @rest;
2581
2582         if ($name eq $which) {
2583           @search_methods = (100, $name, $tfn);
2584           $ok = 1;
2585           last;
2586         }
2587       }
2588       error "no such search method as \"$which\"" unless ($ok);
2589       LOG (1, "DEBUG: using only \"$which\"");
2590
2591     } else {
2592       print STDERR "$copyright\nusage: $progname " .
2593               "[-root] [-display dpy] [-verbose] [-debug which]\n" .
2594         "\t\t  [-timeout secs] [-delay secs] [-filter cmd] [-filter2 cmd]\n" .
2595         "\t\t  [-no-output] [-urls-only] [-background color] [-size WxH]\n" .
2596         "\t\t  [-dictionary dictionary-file] [-http-proxy host[:port]]\n" .
2597         "\t\t  [-driftnet [driftnet-program-and-args]]\n" .
2598         "\n";
2599       exit 1;
2600     }
2601   }
2602
2603   if ($http_proxy && $http_proxy eq "") {
2604     $http_proxy = undef;
2605   }
2606   if ($http_proxy && $http_proxy =~ m@^http://([^/]*)/?$@ ) {
2607     # historical suckage: allow "http://host:port" as well as "host:port".
2608     $http_proxy = $1;
2609   }
2610
2611   if (!$root_p && !$no_output_p) {
2612     print STDERR $copyright;
2613     error "the -root argument is mandatory (for now.)";
2614   }
2615
2616   if (!$no_output_p && !$ENV{DISPLAY}) {
2617     error "\$DISPLAY is not set.";
2618   }
2619
2620
2621   if ($verbose == 1) {
2622     $verbose_imgmap   = 1;
2623     $verbose_warnings = 1;
2624
2625   } elsif ($verbose == 2) {
2626     $verbose_imgmap   = 1;
2627     $verbose_warnings = 1;
2628     $verbose_load     = 1;
2629
2630   } elsif ($verbose == 3) {
2631     $verbose_imgmap   = 1;
2632     $verbose_warnings = 1;
2633     $verbose_load     = 1;
2634     $verbose_filter   = 1;
2635
2636   } elsif ($verbose == 4) {
2637     $verbose_imgmap   = 1;
2638     $verbose_warnings = 1;
2639     $verbose_load     = 1;
2640     $verbose_filter   = 1;
2641     $verbose_net      = 1;
2642
2643   } elsif ($verbose == 5) {
2644     $verbose_imgmap   = 1;
2645     $verbose_warnings = 1;
2646     $verbose_load     = 1;
2647     $verbose_filter   = 1;
2648     $verbose_net      = 1;
2649     $verbose_pbm      = 1;
2650
2651   } elsif ($verbose == 6) {
2652     $verbose_imgmap   = 1;
2653     $verbose_warnings = 1;
2654     $verbose_load     = 1;
2655     $verbose_filter   = 1;
2656     $verbose_net      = 1;
2657     $verbose_pbm      = 1;
2658     $verbose_http     = 1;
2659
2660   } elsif ($verbose >= 7) {
2661     $verbose_imgmap   = 1;
2662     $verbose_warnings = 1;
2663     $verbose_load     = 1;
2664     $verbose_filter   = 1;
2665     $verbose_net      = 1;
2666     $verbose_pbm      = 1;
2667     $verbose_http     = 1;
2668     $verbose_exec     = 1;
2669   }
2670
2671   if ($dict) {
2672     error ("$dict does not exist") unless (-f $dict);
2673     $wordlist = $dict;
2674   } else {
2675     pick_dictionary();
2676   }
2677
2678   init_signals();
2679
2680   spawn_driftnet ($driftnet_cmd) if ($driftnet_cmd);
2681
2682   if ($urls_only_p) {
2683     url_only_output;
2684   } else {
2685     x_or_pbm_output;
2686   }
2687 }
2688
2689 main;
2690 exit (0);