f91526001efcea11858a187b0db1bea2bcdf6922
[pr0n] / perl / Sesse / pr0n / Common.pm
1 package Sesse::pr0n::Common;
2 use strict;
3 use warnings;
4
5 use Sesse::pr0n::Templates;
6 use Sesse::pr0n::Overload;
7
8 use Apache2::RequestRec (); # for $r->content_type
9 use Apache2::RequestIO ();  # for $r->print
10 use Apache2::Const -compile => ':common';
11 use Apache2::Log;
12 use ModPerl::Util;
13
14 use DBI;
15 use DBD::Pg;
16 use Image::Magick;
17 use POSIX;
18 use Digest::SHA1;
19 use MIME::Base64;
20 use MIME::Types;
21 use LWP::Simple;
22 # use Image::Info;
23 use Image::ExifTool;
24
25 BEGIN {
26         use Exporter ();
27         our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
28
29         $VERSION     = "v2.04";
30         @ISA         = qw(Exporter);
31         @EXPORT      = qw(&error &dberror);
32         %EXPORT_TAGS = qw();
33         @EXPORT_OK   = qw(&error &dberror);
34
35         our $dbh = DBI->connect("dbi:Pg:dbname=pr0n;host=127.0.0.1", "pr0n", "EsVdwImY")
36                 or die "Couldn't connect to PostgreSQL database: " . DBI->errstr;
37         our $mimetypes = new MIME::Types;
38         
39         Apache2::ServerUtil->server->log_error("Initializing pr0n $VERSION");
40 }
41 END {
42         our $dbh;
43         $dbh->disconnect;
44 }
45
46 our ($dbh, $mimetypes);
47
48 sub error {
49         my ($r,$err,$status,$title) = @_;
50
51         if (!defined($status) || !defined($title)) {
52                 $status = 500;
53                 $title = "Internal server error";
54         }
55         
56         $r->content_type('text/html; charset=utf-8');
57         $r->status($status);
58
59         header($r, $title);
60         $r->print("    <p>Error: $err</p>\n");
61         footer($r);
62
63         $r->log->error($err);
64
65         ModPerl::Util::exit();
66 }
67
68 sub dberror {
69         my ($r,$err) = @_;
70         error($r, "$err (DB error: " . $dbh->errstr . ")");
71 }
72
73 sub header {
74         my ($r,$title) = @_;
75
76         $r->content_type("text/html; charset=utf-8");
77
78         # Fetch quote if we're itk-bilder.samfundet.no
79         my $quote = "";
80         if ($r->get_server_name eq 'itk-bilder.samfundet.no') {
81                 $quote = LWP::Simple::get("http://itk.samfundet.no/include/quotes.cli.php");
82                 $quote = "Error: Could not fetch quotes." if (!defined($quote));
83         }
84         Sesse::pr0n::Templates::print_template($r, "header", { title => $title, quotes => $quote });
85 }
86
87 sub footer {
88         my ($r) = @_;
89         Sesse::pr0n::Templates::print_template($r, "footer",
90                 { version => $Sesse::pr0n::Common::VERSION });
91 }
92
93 sub scale_aspect {
94         my ($width, $height, $thumbxres, $thumbyres) = @_;
95
96         unless ($thumbxres >= $width &&
97                 $thumbyres >= $height) {
98                 my $sfh = $width / $thumbxres;
99                 my $sfv = $height / $thumbyres;
100                 if ($sfh > $sfv) {
101                         $width  /= $sfh;
102                         $height /= $sfh;
103                 } else {
104                         $width  /= $sfv;
105                         $height /= $sfv;
106                 }
107                 $width = POSIX::floor($width);
108                 $height = POSIX::floor($height);
109         }
110
111         return ($width, $height);
112 }
113
114 sub print_link {
115         my ($r, $title, $baseurl, $param, $defparam) = @_;
116         my $str = "<a href=\"$baseurl";
117         my $first = 1;
118
119         while (my ($key, $value) = each %$param) {
120                 next unless defined($value);
121                 next if (defined($defparam->{$key}) && $value == $defparam->{$key});
122         
123                 $str .= ($first) ? "?" : '&amp;';
124                 $str .= "$key=$value";
125                 $first = 0;
126         }
127         
128         $str .= "\">$title</a>";
129         $r->print($str);
130 }
131
132 sub get_dbh {
133         # Check that we are alive
134         if (!(defined($dbh) && $dbh->ping)) {
135                 # Try to reconnect
136                 Apache2::ServerUtil->server->log_error("Lost contact with PostgreSQL server, trying to reconnect...");
137                 unless ($dbh = DBI->connect("dbi:Pg:dbname=pr0n;host=127.0.0.1", "pr0n", "EsVdwImY")) {
138                         $dbh = undef;
139                         die "Couldn't connect to PostgreSQL database";
140                 }
141         }
142
143         return $dbh;
144 }
145
146 sub get_base {
147         my $r = shift;
148         return $r->dir_config('ImageBase');
149 }
150
151 sub get_disk_location {
152         my ($r, $id) = @_;
153         my $dir = POSIX::floor($id / 256);
154         return get_base($r) . "images/$dir/$id.jpg";
155 }
156
157 sub get_cache_location {
158         my ($r, $id, $width, $height, $infobox) = @_;
159         my $dir = POSIX::floor($id / 256);
160
161         if ($infobox) {
162                 return get_base($r) . "cache/$dir/$id-$width-$height.jpg";
163         } else {
164                 return get_base($r) . "cache/$dir/$id-$width-$height-nobox.jpg";
165         }
166 }
167
168 sub update_width_height {
169         my ($r, $id, $width, $height) = @_;
170
171         # Also find the date taken if appropriate (from the EXIF tag etc.)
172         my $info = Image::ExifTool::ImageInfo(get_disk_location($r, $id));
173         my $datetime = undef;
174
175         if (defined($info->{'DateTimeOriginal'})) {
176                 # Parse the date and time over to ISO format
177                 if ($info->{'DateTimeOriginal'} =~ /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/ && $1 > 1990) {
178                         $datetime = "$1-$2-$3 $4:$5:$6";
179                 }
180         }
181
182         $dbh->do('UPDATE images SET width=?, height=?, date=? WHERE id=?',
183                  undef, $width, $height, $datetime, $id)
184                 or die "Couldn't update width/height in SQL: $!";
185
186         # update the last_picture cache as well (this should of course be done
187         # via a trigger, but this is less complicated :-) )
188         $dbh->do('UPDATE events SET last_picture=(SELECT COALESCE(MAX(date),\'1970-01-01 00:00:00\') FROM images WHERE event=events.id) WHERE id=(SELECT event FROM images WHERE id=?)',
189                 undef, $id)
190                 or die "Couldn't update last_picture in SQL: $!";
191 }
192
193 sub check_access {
194         my $r = shift;
195
196         my $auth = $r->headers_in->{'authorization'};
197         if (!defined($auth) || $auth !~ m#^Basic ([a-zA-Z0-9+/]+=*)$#) {
198                 $r->content_type('text/plain; charset=utf-8');
199                 $r->status(401);
200                 $r->headers_out->{'www-authenticate'} = 'Basic realm="pr0n.sesse.net"';
201                 $r->print("Need authorization\n");
202                 return undef;
203         }
204         
205         #return qw(sesse Sesse);
206
207         my ($user, $pass) = split /:/, MIME::Base64::decode_base64($1);
208         # WinXP is stupid :-)
209         if ($user =~ /^.*\\(.*)$/) {
210                 $user = $1;
211         }
212
213         my $takenby;
214         if ($user =~ /^([a-zA-Z0-9^_-]+)\@([a-zA-Z0-9^_-]+)$/) {
215                 $user = $1;
216                 $takenby = $2;
217         } else {
218                 ($takenby = $user) =~ s/^([a-zA-Z])/uc($1)/e;
219         }
220         
221         my $oldpass = $pass;
222         $pass = Digest::SHA1::sha1_base64($pass);
223         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS auth FROM users WHERE username=? AND sha1password=? AND vhost=?',
224                 undef, $user, $pass, $r->get_server_name);
225         if ($ref->{'auth'} != 1) {
226                 $r->content_type('text/plain; charset=utf-8');
227                 warn "No user exists, only $auth";
228                 $r->status(401);
229                 $r->headers_out->{'www-authenticate'} = 'Basic realm="pr0n.sesse.net"';
230                 $r->print("Authorization failed");
231                 $r->log->warn("Authentication failed for $user/$takenby");
232                 return undef;
233         }
234
235         $r->log->info("Authentication succeeded for $user/$takenby");
236
237         return ($user, $takenby);
238 }
239         
240 sub stat_image {
241         my ($r, $event, $filename) = (@_);
242         my $ref = $dbh->selectrow_hashref(
243                 'SELECT id FROM images WHERE event=? AND filename=?',
244                 undef, $event, $filename);
245         if (!defined($ref)) {
246                 return (undef, undef, undef);
247         }
248         return stat_image_from_id($r, $ref->{'id'});
249 }
250
251 sub stat_image_from_id {
252         my ($r, $id) = @_;
253
254         my $fname = get_disk_location($r, $id);
255         my (undef, undef, undef, undef, undef, undef, undef, $size, undef, $mtime) = stat($fname)
256                 or return (undef, undef, undef);
257
258         return ($fname, $size, $mtime);
259 }
260
261 sub ensure_cached {
262         my ($r, $filename, $id, $dbwidth, $dbheight, $infobox, $xres, $yres, @otherres) = @_;
263
264         my $fname = get_disk_location($r, $id);
265         unless (defined($xres) && ($xres < $dbheight || $yres < $dbwidth || $dbwidth == -1 || $dbheight == -1)) {
266                 return ($fname, 0);
267         }
268
269         my $cachename = get_cache_location($r, $id, $xres, $yres, $infobox);
270         if (! -r $cachename or (-M $cachename > -M $fname)) {
271                 # If we are in overload mode (aka Slashdot mode), refuse to generate
272                 # new thumbnails.
273                 if (Sesse::pr0n::Overload::is_in_overload($r)) {
274                         $r->log->warn("In overload mode, not scaling $id to $xres x $yres");
275                         error($r, 'System is in overload mode, not doing any scaling');
276                 }
277         
278                 # Need to generate the cache; read in the image
279                 my $magick = new Image::Magick;
280                 my $info = Image::ExifTool::ImageInfo($fname);
281
282                 # NEF files aren't autodetected
283                 $fname = "NEF:$fname" if ($filename =~ /\.nef$/i);
284                 $r->log->warn("Generating $fname for $filename");
285                 
286                 my $err = $magick->Read($fname);
287                 if ($err) {
288                         $r->log->warn("$fname: $err");
289                         $err =~ /(\d+)/;
290                         if ($1 >= 400) {
291                                 undef $magick;
292                                 error($r, "$fname: $err");
293                         }
294                 }
295
296                 # If we use ->[0] unconditionally, text rendering (!) seems to crash
297                 my $img = (scalar @$magick > 1) ? $magick->[0] : $magick;
298
299                 my $width = $img->Get('columns');
300                 my $height = $img->Get('rows');
301
302                 # Update the SQL database if it doesn't contain the required info
303                 if ($dbwidth == -1 || $dbheight == -1) {
304                         $r->log->info("Updating width/height for $id: $width x $height");
305                         update_width_height($r, $id, $width, $height);
306                 }
307                         
308                 # We always want RGB JPEGs
309                 if ($img->Get('Colorspace') eq "CMYK") {
310                         $img->Set(colorspace=>'RGB');
311                 }
312
313                 while (defined($xres) && defined($yres)) {
314                         my ($nxres, $nyres) = (shift @otherres, shift @otherres);
315                         my $cachename = get_cache_location($r, $id, $xres, $yres, $infobox);
316                         
317                         my $cimg;
318                         if (defined($nxres) && defined($nyres)) {
319                                 # we have more resolutions to scale, so don't throw
320                                 # the image away
321                                 $cimg = $img->Clone();
322                         } else {
323                                 $cimg = $img;
324                         }
325                 
326                         my ($nwidth, $nheight) = scale_aspect($width, $height, $xres, $yres);
327
328                         # Use lanczos (sharper) for heavy scaling, mitchell (faster) otherwise
329                         my $filter = 'Mitchell';
330                         my $quality = 90;
331
332                         if ($width / $nwidth > 8.0 || $height / $nheight > 8.0) {
333                                 $filter = 'Lanczos';
334                                 $quality = 80;
335                         }
336
337                         $cimg->Resize(width=>$nwidth, height=>$nheight, filter=>$filter);
338
339                         if (($nwidth >= 800 || $nheight >= 600) && $infobox == 1) {
340                                 make_infobox($cimg, $info, $r);
341                         }
342
343                         # Strip EXIF tags etc.
344                         $cimg->Strip();
345
346                         $err = $cimg->write(filename=>$cachename, quality=>$quality);
347
348                         undef $cimg;
349
350                         ($xres, $yres) = ($nxres, $nyres);
351
352                         $r->log->info("New cache: $nwidth x $nheight for $id.jpg");
353                 }
354                 
355                 undef $magick;
356                 undef $img;
357                 if ($err) {
358                         $r->log->warn("$fname: $err");
359                         $err =~ /(\d+)/;
360                         if ($1 >= 400) {
361                                 @$magick = ();
362                                 error($r, "$fname: $err");
363                         }
364                 }
365         }
366         return ($cachename, 1);
367 }
368
369 sub get_mimetype_from_filename {
370         my $filename = shift;
371         my MIME::Type $type = $mimetypes->mimeTypeOf($filename);
372         $type = "image/jpeg" if (!defined($type));
373         return $type;
374 }
375
376 sub make_infobox {
377         my ($img, $info, $r) = @_;
378         
379         my @lines = ();
380         my @classic_fields = ();
381         
382         if (defined($info->{'DateTimeOriginal'}) &&
383             $info->{'DateTimeOriginal'} =~ /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/
384             && $1 >= 1990) {
385                 push @lines, "$1-$2-$3 $4:$5";
386         }
387
388         push @lines, $info->{'Model'} if (defined($info->{'Model'}));
389         
390         # classic fields
391         if (defined($info->{'FocalLength'}) && $info->{'FocalLength'} =~ /^(\d+)(?:\.\d+)?(?:mm)?$/) {
392                 push @classic_fields, ($1 . "mm");
393         } elsif (defined($info->{'FocalLength'}) && $info->{'FocalLength'} =~ /^(\d+)\/(\d+)$/) {
394                 push @classic_fields, (sprintf "%.1fmm", ($1/$2));
395         }
396         if (defined($info->{'ExposureTime'}) && $info->{'ExposureTime'} =~ /^(\d+)\/(\d+)$/) {
397                 my ($a, $b) = ($1, $2);
398                 my $gcd = gcd($a, $b);
399                 push @classic_fields, ($a/$gcd . "/" . $b/$gcd . "s");
400         }
401         if (defined($info->{'FNumber'}) && $info->{'FNumber'} =~ /^(\d+)\/(\d+)$/) {
402                 my $f = $1/$2;
403                 if ($f >= 10) {
404                         push @classic_fields, (sprintf "f/%.0f", $f);
405                 } else {
406                         push @classic_fields, (sprintf "f/%.1f", $f);
407                 }
408         } elsif (defined($info->{'FNumber'}) && $info->{'FNumber'} =~ /^(\d+)\.(\d+)$/) {
409                 my $f = $info->{'FNumber'};
410                 if ($f >= 10) {
411                         push @classic_fields, (sprintf "f/%.0f", $f);
412                 } else {
413                         push @classic_fields, (sprintf "f/%.1f", $f);
414                 }
415         }
416
417 #       Apache2::ServerUtil->server->log_error(join(':', keys %$info));
418
419         if (defined($info->{'NikonD1-ISOSetting'})) {
420                 push @classic_fields, $info->{'NikonD1-ISOSetting'}->[1] . " ISO";
421         } elsif (defined($info->{'ISOSetting'})) {
422                 push @classic_fields, $info->{'ISOSetting'} . " ISO";
423         }
424
425         push @classic_fields, $info->{'ExposureBiasValue'} . " EV" if (defined($info->{'ExposureBiasValue'}) && $info->{'ExposureBiasValue'} != 0);
426         
427         if (scalar @classic_fields > 0) {
428                 push @lines, join(', ', @classic_fields);
429         }
430
431         if (defined($info->{'Flash'})) {
432                 if ($info->{'Flash'} =~ /did not fire/ || $info->{'Flash'} =~ /No Flash/) {
433                         push @lines, "No flash";
434                 } elsif ($info->{'Flash'} =~ /fired/) {
435                         push @lines, "Flash";
436                 } else {
437                         push @lines, $info->{'Flash'};
438                 }
439         }
440
441         return if (scalar @lines == 0);
442
443         # OK, this sucks. Let's make something better :-)
444         @lines = ( join(" - ", @lines) );
445
446         # Find the required width
447         my $th = 14 * (scalar @lines) + 6;
448         my $tw = 1;
449
450         for my $line (@lines) {
451                 my $this_w = ($img->QueryFontMetrics(text=>$line, font=>'/usr/share/fonts/truetype/msttcorefonts/Arial.ttf', pointsize=>12))[4];
452                 $tw = $this_w if ($this_w >= $tw);
453         }
454
455         $tw += 6;
456
457         # Round up so we hit exact DCT blocks
458         $tw += 8 - ($tw % 8) unless ($tw % 8 == 0);
459         $th += 8 - ($th % 8) unless ($th % 8 == 0);
460         
461         return if ($tw > $img->Get('columns'));
462
463 #       my $x = $img->Get('columns') - 8 - $tw;
464 #       my $y = $img->Get('rows') - 8 - $th;
465         my $x = 0;
466         my $y = $img->Get('rows') - $th;
467         $tw = $img->Get('columns');
468
469         $x -= $x % 8;
470         $y -= $y % 8;
471
472         my $points = sprintf "%u,%u %u,%u", $x, $y, ($x+$tw-1), ($img->Get('rows') - 1);
473         my $lpoints = sprintf "%u,%u %u,%u", $x, $y, ($x+$tw-1), $y;
474 #       $img->Draw(primitive=>'rectangle', stroke=>'black', fill=>'white', points=>$points);
475         $img->Draw(primitive=>'rectangle', stroke=>'white', fill=>'white', points=>$points);
476         $img->Draw(primitive=>'line', stroke=>'black', points=>$lpoints);
477
478         my $i = -(scalar @lines - 1)/2.0;
479         my $xc = $x + $tw / 2 - $img->Get('columns')/2;
480         my $yc = ($y + $img->Get('rows'))/2 - $img->Get('rows')/2;
481         #my $yc = ($y + $img->Get('rows'))/4;
482         my $yi = $th / (scalar @lines);
483         
484         $lpoints = sprintf "%u,%u %u,%u", $x, $yc + $img->Get('rows')/2, ($x+$tw-1), $yc+$img->Get('rows')/2;
485
486         for my $line (@lines) {
487                 $img->Annotate(text=>$line, font=>'/usr/share/fonts/truetype/msttcorefonts/Arial.ttf', pointsize=>12, gravity=>'Center',
488                 # $img->Annotate(text=>$line, font=>'Helvetica', pointsize=>12, gravity=>'Center',
489                         x=>int($xc), y=>int($yc + $i * $yi));
490         
491                 $i = $i + 1;
492         }
493 }
494
495 sub gcd {
496         my ($a, $b) = @_;
497         return $a if ($b == 0);
498         return gcd($b, $a % $b);
499 }
500
501 1;
502
503