a218de166db1d717aca52a10052732eed271f38a
[xscreensaver] / local / bin / webcollage
1 #!/usr/local/bin/perl5 -w
2 #
3 # webcollage, for xscreensaver, Copyright (c) 1999 Jamie Zawinski <jwz@jwz.org>
4 #
5 # Permission to use, copy, modify, distribute, and sell this software and its
6 # documentation for any purpose is hereby granted without fee, provided that
7 # the above copyright notice appear in all copies and that both that
8 # copyright notice and this permission notice appear in supporting
9 # documentation.  No representations are made about the suitability of this
10 # software for any purpose.  It is provided "as is" without express or 
11 # implied warranty.
12 #
13 #
14 # This program decorate the screen with random images from the web.
15
16
17 use Socket;
18
19 my $progname = "$0";
20 my $version = "1.0";
21
22 $progname =~ s@^.*/([^/]+)$@$1@;
23
24 my $random_redirector = "http://random.yahoo.com/bin/ryl";
25 my $image_randomizer_a = "http://image.altavista.com/";
26 my $image_randomizer = $image_randomizer_a . "cgi-bin/avncgi" .
27                        "?do=3&verb=no&oshape=n&oorder=" .
28                        "&ophoto=1&oart=1&ocolor=1&obw=1" .
29                        "&stype=simage&oprem=0&query=";
30
31 my $http_timeout = 30;
32 my $ppm_to_root_window_cmd = "xv -root -rmode 5 -viewonly" .
33                              " +noresetroot %%PPM%% -quit";
34 my $filter_cmd = undef;
35 my $post_filter_cmd = undef;
36 my $background = undef;
37 my $no_output_p = 0;
38 my $delay = 0;
39
40 my $wordlist = "/usr/dict/words";
41
42 if (!-r $wordlist) {
43     $wordlist = "/usr/share/lib/dict/words";    # irix
44 }
45
46
47 my $min_width = 50;
48 my $min_height = 50;
49
50 my $DEBUG = 0;
51
52
53 # returns three values: the HTTP response line; the document headers;
54 # and the document body.
55 #
56 sub get_document_1 {
57     my ( $url ) = @_;
58
59     if ( $DEBUG > 2 ) {
60         print STDERR "get_document_1 $url\n";
61     }
62
63     my($dummy, $dummy, $serverstring, $path) = split(/\//, $url, 4);
64     my($them,$port) = split(/:/, $serverstring);
65     $port = 80 unless $port;
66     my $size="";
67
68     my ($remote, $iaddr, $paddr, $proto, $line);
69     $remote = $them;
70     if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') }
71     return unless $port;
72     $iaddr   = inet_aton($remote)               || return;
73     $paddr   = sockaddr_in($port, $iaddr);
74
75     @_ =
76     eval {
77         local $SIG{ALRM}  = sub {
78             if ($DEBUG > 0) {
79                 print STDERR "timed out for $url\n";
80             }
81             die "alarm\n" };
82         alarm $http_timeout;
83
84         $proto   = getprotobyname('tcp');
85         socket(S, PF_INET, SOCK_STREAM, $proto)  || return;
86         connect(S, $paddr)    || return;
87
88         select(S); $| = 1; select(STDOUT);
89
90         print S ("GET /$path HTTP/1.0\n" .
91                  "Host: $them\n" .
92                  "User-Agent: $progname/$version\n" .
93                  "\n");
94
95         my $http = <S>;
96
97         my $head = "";
98         my $body = "";
99         while (<S>) {
100             $head .= $_;
101             last if m@^[\r\n]@;
102         }
103         while (<S>) {
104             $body .= $_;
105         }
106
107         close S;
108
109         return ( $http, $head, $body );
110     };
111     die if ($@ && $@ ne "alarm\n");       # propagate errors
112     if ($@) {
113         # timed out
114         return undef;
115     } else {
116         # didn't
117         alarm 0;
118         return @_;
119     }
120 }
121
122
123 # returns two values: the document headers; and the document body.
124 # if the given URL did a redirect, returns the redirected-to document.
125 #
126 sub get_document {
127     my ( $url ) = @_;
128
129     do {
130         my ( $http, $head, $body ) = get_document_1 $url;
131
132         return undef if ( ! $body );
133
134         if ( $http =~ m@HTTP/[0-9.]+ 30[23]@ ) {
135             $_ = $head;
136             my ( $location ) = m@^location:[ \t]*(.*)$@im;
137             if ( $location ) {
138
139                 if ( $DEBUG > 2 ) {
140                     print STDERR "redirect from $url to $location\n";
141                 }
142                 $url = $location;
143             } else {
144                 return ( $url, $body );
145             }
146
147         } elsif ( $http =~ m@HTTP/[0-9.]+ [4-9][0-9][0-9]@ ) {
148             # http errors -- return nothing.
149             return undef;
150
151         } else {
152
153             return ( $url, $body );
154         }
155
156     } while (1);
157 }
158
159
160 # given a URL and the body text at that URL, selects and returns a random
161 # image from it.  returns undef if no suitable images found.
162 #
163 sub pick_image_from_body {
164     my ( $base, $body ) = @_;
165
166     $_ = $base;
167
168     # if there's at least one slash after the host, take off the last
169     # pathname component
170     if ( m@^http://[^/]+/@io ) {
171         ( $base = $base ) =~ s@[^/]+$@@go;
172     }
173
174     # if there are no slashes after the host at all, put one on the end.
175     if ( m@^http://[^/]+$@io ) {
176         $base .= "/";
177     }
178
179     if ( $DEBUG > 2 ) {
180         print STDERR "base is $base\n";
181     }
182
183
184     $_ = $body;
185
186     # strip out newlines, compress whitespace
187     s/[\r\n\t ]+/ /go;
188
189     # nuke comments
190     s/<!--.*?-->//go;
191
192     my @urls;
193     my %unique_urls;
194
195     foreach (split(/ *</)) {
196         if ( m/^(img|a) .*(src|href) ?= ?\"? ?(.*?)[ >\"]/io ) {
197
198             my $was_inline = ( "$1" eq "a" || "$1" eq "A" );
199             my $link = $3;
200             my ( $width )  = m/width ?= ?([0-9]+)/oi;
201             my ( $height ) = m/height ?= ?([0-9]+)/oi;
202             $_ = $link;
203
204             if ( m@^/@o ) {
205                 my $site;
206                 ( $site = $base ) =~ s@^(http://[^/]*).*@$1@gio;
207                 $_ = "$site$link";
208             } elsif ( ! m/:/ ) {
209                 $_ = "$base$link";
210                 s@/\./@/@;
211                 while (s@/\.\./@/@g) {
212                 }
213             }
214
215             # skip non-http
216             if ( ! m@^http://@io ) {
217                 next;
218             }
219
220             # skip non-image
221             if ( ! m@[.](gif|jpg|jpeg|pjpg|pjpeg)@io ) {
222                 next;
223             }
224
225             # skip GIF!
226 #           if ( m@[.](gif)@io ) {
227 ##              if ( $DEBUG > 2 ) { print "skip GIF $_\n"; }
228 #               next;
229 #           }
230
231             # skip really short or really narrow images
232             if ( $width && $width < $min_width) {
233                 if ( $DEBUG > 2 ) {
234                     print STDERR "skip narrow image $_ ($width x $height)\n";
235                 }
236                 next;
237             }
238
239             if ( $height && $height < $min_height) {
240                 if ( $DEBUG > 2 ) {
241                     print STDERR "skip short image $_ ($width x $height)\n";
242                 }
243                 next;
244             }
245
246             my $url = $_;
247
248             if ( $unique_urls{$url} ) {
249                 if ( $DEBUG > 2 ) { print STDERR "skip duplicate image $_\n"; }
250                 next;
251             }
252
253             if ( $DEBUG > 2 ) {
254                 print STDERR "got $url" . 
255                     ($width && $height ? " (${width}x${height})" : "") .
256                     ($was_inline ? " (inline)" : "") . "\n";
257             }
258
259             $urls[++$#urls] = $url;
260             $unique_urls{$url}++;
261
262             # pointers to images are preferable to inlined images
263             if ( ! $was_inline ) {
264                 $urls[++$#urls] = $url;
265             }
266         }
267     }
268
269     if ( $#urls == 0 ) {
270         if ( $DEBUG > 2 ) {
271             print STDERR "no images on $base\n";
272         }
273         return undef;
274     }
275
276     return undef if ( $#urls < 1 );
277
278     # pick a random element of the table
279     my $i = ((rand() * 99999) % $#urls);
280
281     # if the page has several images on it, prefer the later ones most of
282     # the time.
283     my $fudge = 4;
284     if ($#urls > ($fudge * 2) && $i <= $fudge && ((rand() < 0.9))) {
285         if ( $DEBUG > 2 ) {
286             print STDERR "skipping first $fudge of $#urls images.\n";
287         }
288         $i += ($fudge - $i);
289     }
290
291     my $url = $urls[$i];
292
293     if ( $DEBUG > 2 ) {
294         print STDERR "picked $url\n";
295     }
296
297     return $url;
298 }
299
300
301 # Using the URL-randomizer, picks a random image on a random page, and
302 # returns two URLs: the page containing the image, and the image.
303 # Returns undef if nothing found this time.
304 #
305 sub pick_from_url_randomizer {
306
307     if ( $DEBUG > 2 ) {
308         print STDERR "\n\npicking from $random_redirector...\n\n";
309     }
310
311     my ( $base, $body ) = get_document $random_redirector;
312
313     return if (!$base || !$body);
314     my $img = pick_image_from_body ($base, $body);
315
316     if ($img) {
317         return ($base, $img);
318     } else {
319         return undef;
320     }
321 }
322
323
324 sub random_word {
325     
326     my $word = 0;
327     if (open (IN, "<$wordlist")) {
328         my $size = (stat(IN))[7];
329         my $pos = rand $size;
330         if (seek (IN, $pos, 0)) {
331             $word = <IN>;   # toss partial line
332             $word = <IN>;   # keep next line
333         }
334         close (IN);
335     }
336
337     return 0 if (!$word);
338
339     $word =~ s/^[ \t\n\r]+//;
340     $word =~ s/[ \t\n\r]+$//;
341     $word =~ s/ly$//;
342     $word =~ s/ies$/y/;
343     $word =~ s/ally$/al/;
344
345     return $word;
346 }
347
348
349
350 # Using the image-randomizer, picks a random image on a random page, and
351 # returns two URLs: the page containing the image, and the image.
352 # Returns undef if nothing found this time.
353 #
354 sub pick_from_image_randomizer {
355
356     my $words = random_word;
357     $words .= "%20" . random_word;
358     $words .= "%20" . random_word;
359
360     my $search_url = $image_randomizer . $words;
361
362     if ( $DEBUG > 2 ) {
363         print STDERR "\n\npicking from $search_url\n";
364     }
365
366     my ( $base, $body ) = get_document $search_url;
367
368     return if (! $body);
369
370
371     my @subpages;
372     my $skipped = 0;
373
374     $_ = $body;
375     s/(<A )/\n$1/gi;
376     foreach (split(/\n/)) {
377
378         if ( m@<A HREF=([^>]+)><IMG SRC=http://image\.altavista\.com@i ) {
379
380             my $u = $1;
381             if (m/^"(.*)"$/) { $u = $1; }
382
383             if (m@\.corbis\.com/@) {
384                 $skipped = 1;
385                 if ( $DEBUG > 2 ) {
386                     print STDERR "skipping corbis URL: $_\n";
387                 }
388                 next;
389             } elsif ( $DEBUG > 2 ) {
390                 print STDERR "sub-page: $1\n";
391             }
392
393             $subpages[++$#subpages] = $u;
394         }
395     }
396
397     if ( $#subpages <= 0 ) {
398         if (!$skipped) {
399             print STDERR "Found nothing on $base\n";
400         }
401         return undef;
402     }
403
404     # pick a random element of the table
405     my $i = ((rand() * 99999) % $#subpages);
406     my $subpage = $subpages[$i];
407
408     if ( $DEBUG > 2 ) {
409         print STDERR "picked page $subpage\n";
410     }
411
412
413
414     my ( $base2, $body2 ) = get_document $subpage;
415
416     return undef if (!$base2 || !body2);
417
418     my $img = pick_image_from_body ($base2, $body2);
419
420     if ($img) {
421         return ($base2, $img);
422     } else {
423         return undef;
424     }
425 }
426
427
428 # Picks a random image on a random page, and returns two URLs:
429 # the page containing the image, and the image. 
430 # Returns undef if nothing found this time.
431 # Uses the url-randomizer 1 time in 5, else the image randomizer.
432 #
433 sub pick_image {
434     if (int(rand 5) == 0) {
435         return pick_from_url_randomizer;
436     } else {
437         return pick_from_image_randomizer;
438     }
439 }
440
441
442 # returns the full path of the named program, or undef.
443 #
444 sub which {
445     my ($prog) = @_;
446     foreach (split (/:/, $ENV{PATH})) {
447         if (-x "$_/$prog") {
448             return $prog;
449         }
450     }
451     return undef;
452 }
453
454
455
456 #################################
457 #
458 # running as a CGI
459 #
460 #################################
461
462
463 sub do_html_output {
464
465     $| = 1;
466
467     if ( $progname =~ m/nph-/o ) {
468         print "HTTP/1.0 200 OK\n";
469         print "Content-type: text/html\n";
470         print "\n";
471     }
472
473     print "<TITLE>random images</TITLE>\n";
474     print "<BODY BGCOLOR=\"#FFFFFF\" TEXT=\"#000000\"";
475     print "  LINK=\"#0000EE\" VLINK=\"#551A8B\" ALINK=\"#FF0000\">\n";
476     print "<H1 ALIGN=CENTER>random images</H1><P>\n";
477     print "<P><BLOCKQUOTE><BLOCKQUOTE>\n";
478     print "These images have been selected randomly from the web,\n";
479     print "by using both <A HREF=\"$random_redirector\">\n";
480     print "$random_redirector</A> and <A HREF=\"$image_randomizer_a\">\n";
481     print "$image_randomizer_a</A> as a source of URLs from which\n";
482     print "images are extracted.\n";
483     print "<P>\n";
484     print "Note: if you leave this running\n";
485     print "long enough, your browser will undoubtedly run out of memory\n";
486     print "and crash...\n";
487     print "</BLOCKQUOTE></BLOCKQUOTE><P><HR><P ALIGN=CENTER>\n";
488
489     do {
490         my ($base, $img) = pick_image;
491         if ($img) {
492             if ($DEBUG > 0) {
493                 print STDERR "$img\n";
494             }
495             print "<A HREF=\"$base\">";
496             print "<IMG ALIGN=MIDDLE BORDER=0 SRC=\"$img\"></A>\n";
497         }
498
499         sleep $delay;
500
501     } while (1);
502 }
503
504
505 #################################
506 #
507 # running as an xscreensaver mode
508 #
509 #################################
510
511
512 my $image = ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp") . "/webcollage." . $$;
513 my $tmp   = $image . "-1";
514 my $tmp2  = $image . "-2";
515 my $tmp3  = $image . "-3";
516
517 sub x_cleanup {
518     if ($DEBUG > 0) { print STDERR "caught signal\n"; }
519     unlink $image, $tmp, $tmp2, $tmp3;
520     exit 1;
521 }
522
523 my $screen_width = undef;
524 my $screen_height = undef;
525
526 sub do_x_output {
527
528     my $win_cmd = $ppm_to_root_window_cmd;
529     $win_cmd =~ s/^([^ \t\r\n]+).*$/$1/;
530
531     # make sure the various programs we execute exist, right up front.
532     foreach ("ppmmake", "giftopnm", "djpeg", "pnmpaste", $win_cmd) {
533         which ($_) || die "$progname: $_ not found on \$PATH.\n";
534     }
535
536     $SIG{HUP}  = \&x_cleanup;
537     $SIG{INT}  = \&x_cleanup;
538     $SIG{QUIT} = \&x_cleanup;
539     $SIG{ABRT} = \&x_cleanup;
540     $SIG{KILL} = \&x_cleanup;
541     $SIG{TERM} = \&x_cleanup;
542
543     # Need this so that if giftopnm dies, we don't die.
544     $SIG{PIPE} = 'IGNORE';
545
546     if (!$screen_width || !$screen_height) {
547         $_ = `xdpyinfo`;
548         ($screen_width, $screen_height) = m/dimensions: *([0-9]+)x([0-9]+) /;
549     }
550
551     my $bgcolor = "#000000";
552     my $bgimage = undef;
553
554     if ($background) {
555         if ($background =~ m/^\#[0-9a-f]+$/i) {
556             $bgcolor = $background;
557         } elsif (-r $background) {
558             $bgimage = $background;
559             
560         } elsif (! $background =~ m@^[-a-z0-9 ]+$@i) {
561             print STDERR "not a color or readable file: $background\n";
562             exit 1;
563         } else {
564             # default to assuming it's a color
565             $bgcolor = $background;
566         }
567     }
568
569     # Create the sold-colored base image.
570     #
571     $_ = "ppmmake '$bgcolor' $screen_width $screen_height";
572     if ($DEBUG > 1) {
573         print STDERR "creating base image: $_\n";
574     }
575     system "$_ > $image";
576
577     # Paste the default background image in the middle of it.
578     #
579     if ($bgimage) {
580         my ($iw, $ih);
581         if (open(IMG, "<$bgimage")) {
582             $_ = <IMG>;
583             $_ = <IMG>;
584             ($iw, $ih) = m/^([0-9]+) ([0-9]+)$/;
585             close (IMG);
586         }
587         my $x = int (($screen_width - $iw) / 2);
588         my $y = int (($screen_height - $ih) / 2);
589         if ($DEBUG > 1) {
590             print STDERR "pasting $bgimage into base image at $x, $y\n";
591         }
592         system "pnmpaste $bgimage $x $y $image > $tmp2 && mv $tmp2 $image";
593     }
594
595
596     do {
597         my ($base, $img) = pick_image;
598
599         my ($headers, $body);
600         if ($img) {
601             ($headers, $body) = get_document ($img);
602         }
603
604         if ($body) {
605
606             if ($DEBUG > 0) {
607                 print STDERR "got $img (" . length($body) . ")\n";
608             }
609
610             my $cmd;
611             if ($img =~ m/\.gif/i) {
612                 $cmd = "giftopnm";
613             } else {
614                 $cmd = "djpeg";
615             }
616
617             if ($DEBUG == 0) {
618                 $cmd .= " 2>/dev/null";
619             }
620
621             if (open(PIPE, "| $cmd > $tmp")) {
622                 print PIPE $body;
623                 close PIPE;
624
625                 if ($DEBUG > 1) {
626                     print STDERR "created $tmp ($cmd)\n";
627                 }
628             }
629
630             if (-s $tmp) {
631
632                 if ($filter_cmd) {
633                     if ($DEBUG > 1) {
634                         print STDERR "running $filter_cmd\n";
635                     }
636                     system "($filter_cmd) < $tmp > $tmp3 && mv $tmp3 $tmp";
637                 }
638
639                 my ($iw, $ih);
640                 if (open(IMG, "<$tmp")) {
641                     $_ = <IMG>;
642                     $_ = <IMG>;
643                     ($iw, $ih) = m/^([0-9]+) ([0-9]+)$/;
644                     close (IMG);
645                 }
646
647                 if ($iw && $ih) {
648
649                     if ($DEBUG > 1) {
650                         print STDERR "image size is $iw x $ih\n";
651                     }
652
653                     if ($iw > $screen_width || $ih > $screen_height) {
654                         while ($iw > $screen_width ||
655                                $ih > $screen_height) {
656                             $iw = int($iw / 2);
657                             $ih = int($ih / 2);
658                         }
659                         if ($DEBUG > 1) {
660                             print STDERR "scaling to $iw x $ih\n";
661                         }
662                         system "pnmscale -xysize $iw $ih $tmp > $tmp2" .
663                                " 2>/dev/null && mv $tmp2 $tmp";
664                     }
665
666                     my $x = int (rand() * ($screen_width - $iw));
667                     my $y = int (rand() * ($screen_height - $ih));
668
669                     if ($DEBUG > 1) {
670                         print STDERR "pasting at $x, $y in $image\n";
671                     }
672
673                     system "pnmpaste $tmp $x $y $image > $tmp2 " .
674                            "&& mv $tmp2 $image";
675
676
677                     my $target = $image;
678                     if ($post_filter_cmd) {
679                         if ($DEBUG > 1) {
680                             print STDERR "running $post_filter_cmd\n";
681                         }
682                         system "($post_filter_cmd) < $image > $tmp3";
683                         $target = $tmp3;
684                     }
685
686                     if (!$no_output_p) {
687
688                         my $tsize = (stat($target))[7];
689                         if ($tsize > 200) {
690                             $_ = $ppm_to_root_window_cmd;
691                             s/%%PPM%%/$target/;
692
693                             if ($DEBUG > 1) {
694                                 print STDERR "running $_\n";
695                             }
696                             system $_;
697
698                         } elsif ($DEBUG > 1) {
699                             print STDERR "$target size is $tsize\n";
700                         }
701                     }
702                 }
703             }
704             unlink $tmp, $tmp2, $tmp3;
705         }
706
707         sleep $delay;
708
709     } while (1);
710 }
711
712
713 #################################
714 #
715 # decide how to run
716 #
717 #################################
718
719 sub main {
720     srand(time ^ $$);
721
722     my $usage ="WebCollage, Copyright (c) 1999" .
723         " Jamie Zawinski <jwz\@jwz.org>\n" .
724         "            http://www.jwz.org/xscreensaver/\n";
725
726     if ( $progname =~ m/\.cgi$/i ) {
727         $#ARGV == -1 || die "$usage\nusage: $progname (no arguments)\n";
728         do_html_output;
729
730     } else {
731         my $root_p = 0;
732
733         while ($_ = $ARGV[0]) {
734             shift @ARGV;
735             if ($_ eq "-display" ||
736                 $_ eq "-displ" ||
737                 $_ eq "-disp" ||
738                 $_ eq "-dis" ||
739                 $_ eq "-dpy" ||
740                 $_ eq "-d") {
741                 $ENV{DISPLAY} = shift @ARGV;
742             } elsif ($_ eq "-root") {
743                 $root_p = 1;
744             } elsif ($_ eq "-no-output") {
745                 $no_output_p = 1;
746             } elsif ($_ eq "-verbose") {
747                 $DEBUG++;
748             } elsif (m/^-v+$/) {
749                 $DEBUG += length($_)-1;
750             } elsif ($_ eq "-delay") {
751                 $delay = shift @ARGV;
752             } elsif ($_ eq "-timeout") {
753                 $http_timeout = shift @ARGV;
754             } elsif ($_ eq "-filter") {
755                 $filter_cmd = shift @ARGV;
756             } elsif ($_ eq "-filter2") {
757                 $post_filter_cmd = shift @ARGV;
758             } elsif ($_ eq "-background" || $_ eq "-bg") {
759                 $background = shift @ARGV;
760             } elsif ($_ eq "-size") {
761                 $_ = shift @ARGV;
762                 if (m@^([0-9]+)x([0-9]+)$@) {
763                     $screen_width = $1;
764                     $screen_height = $2;
765                 } else {
766                     die "$progname: argument to \"-size\" must be" .
767                         " of the form \"640x400\"\n";
768                 }
769             } else {
770                die "$usage\nusage: $progname [-root]" .
771                    " [-display dpy] [-root] [-verbose] [-timeout secs]\n" .
772                    "\t\t  [-delay secs] [-filter cmd] [-filter2 cmd]\n";
773             }
774         }
775         if (!$root_p && !$no_output_p) {
776             die "$progname: the -root argument is manditory (for now.)\n";
777         }
778
779         if (!$no_output_p && !$ENV{DISPLAY}) {
780             die "$progname: \$DISPLAY is not set.\n";
781         }
782
783         do_x_output;
784     }
785 }
786
787 main;
788 exit 0;