]> git.decadent.org.uk Git - memories.git/blob - Memories/Photo.pm
4279dc97c0eb4c622ec365390d97dd424938ae3e
[memories.git] / Memories / Photo.pm
1 package Memories::Photo;
2 use strict;
3 use Carp qw(cluck confess);
4 use base qw(Memories::DBI Maypole::Model::CDBI::Plain);
5 use Time::Piece;
6 use Image::Seek;
7 use constant PAGER_SYNTAX => "LimitXY";
8 __PACKAGE__->columns(Essential => qw(id title uploader uploaded x y rating rated hit_count));
9 __PACKAGE__->untaint_columns(printable => [qw/title/]);
10 __PACKAGE__->columns(TEMP => qw/exif_object/);
11 __PACKAGE__->set_sql(recent => q{
12 SELECT __ESSENTIAL__
13 FROM __TABLE__
14 ORDER BY uploaded DESC
15 LIMIT 4
16 });
17
18 __PACKAGE__->has_many(comments => "Memories::Comment");
19 __PACKAGE__->has_a(uploader => "Memories::User");
20 __PACKAGE__->has_a(uploaded => "Time::Piece",
21     inflate => sub { Time::Piece->strptime(shift, "%Y-%m-%d %H:%M:%S") },
22     deflate => 'datetime',
23 );
24
25 sub do_upload :Exported {
26     my ($self, $r) = @_;
27     my %upload = $r->upload("photo");
28
29     # XXX Stop anonymous uploads!
30     my $photo = $self->create({
31         uploader => $r->user,
32         uploaded => Time::Piece->new(),
33         title => $r->params->{title},
34         hit_count => 0,
35         rating => 0,
36         rated => 0, # Oh, the potential for divide by zero errors...
37     });
38
39     # Dump content
40     if (!open OUT, ">". $photo->path("file")) {
41         die "Can't write ".$photo->path("file")." because $!";
42     }
43     # XXX Check it's a JPEG, etc.
44     # XXX Unzip ZIP file
45     print OUT $upload{content};
46     close OUT;
47     my ($x, $y) = dim(image_info($photo->path));
48     $photo->x($x); $photo->y($y);
49
50     # Rotate?
51     $photo->unrotate(); 
52     if (!$photo->title){ 
53         $photo->title($photo->title_exif || $upload{filename});
54     }
55
56     $photo->make_thumb;
57     $photo->add_tags($r->{params}{tags});
58     $photo->add_to_imageseek_library;
59     Memories->zap_cache();
60
61     # Add system tags here
62     my $tag = "date:".$photo->shot->ymd;
63     $photo->add_to_system_tags({tag => Memories::SystemTag->find_or_create({name =>$tag}) });
64
65     # Set it up to go again
66     $r->objects([$photo]);
67     $r->template("view");
68     $r->message("Thanks for the upload! ".
69         ($r->{params}{tags} ? "" 
70         : "Don't forget to <a href=\"?".$r->config->uri_base."photo/view/".$photo->id."?active=tagedit\">tag your photo</a>"
71         )
72     ); 
73 }
74
75 sub view :Exported {
76     my ($self, $r) = @_;
77     my $photo = $r->{objects}[0];
78     $photo->hit_count($photo->hit_count()+1);
79     if ($r->{session}{last_search}) {
80         # This is slightly inefficient
81         my @search = split/,/, $r->{session}{last_search};
82         my $found = -1;
83         for my $i (0..$#search) {
84             next unless $photo->id == $search[$i];
85             $found = $i;
86         }
87         return unless $found > -1;
88         $r->{template_args}{next} = $self->retrieve($search[$found+1]) 
89             if $found+1 <= $#search;
90         $r->{template_args}{prev} = $self->retrieve($search[$found-1])
91             if $found-1 >= 0;
92     }
93 }
94 sub upload :Exported {}
95
96 use Class::DBI::Plugin::Pager;
97 use Class::DBI::Plugin::AbstractCount;
98
99 sub recent :Exported {
100     my ($self, $r) = @_;
101     my $page = $r->params->{page} || 1;
102     my $pager = $self->pager(
103         per_page => Memories->config->{photos_per_page}, 
104         page => $page,
105         syntax => PAGER_SYNTAX,
106         order_by => "uploaded desc"
107     );
108     $r->objects([$pager->retrieve_all ]);
109     $r->{template_args}{pager} = $pager;
110     $r->last_search;
111 }
112
113 sub add_comment :Exported {
114     my ($self, $r, $photo) = @_;
115     $r->template("view");
116     $r->objects->[0]->add_to_comments({
117         name => $r->params->{name},
118         content => $r->params->{content}
119     });
120 }
121
122 sub format { 
123     "jpg" # For now
124
125
126 use Cache::MemoryCache;
127 use Image::Info qw(dim image_info);
128 use Image::ExifTool;
129 my $cache = new Cache::MemoryCache( { 'namespace' => 'MemoriesInfo' });
130
131 sub add_to_imageseek_library {
132     my $self = shift;
133     Image::Seek::cleardb();
134     my $img = Image::Imlib2->load($self->path("file"));
135
136     Image::Seek::add_image($img, $self->id);
137     # Merge this new one into the main database; there is a bit of a
138     # race condition here. XXX
139     Image::Seek::loaddb(Memories->config->{image_seek});
140     Image::Seek::savedb(Memories->config->{image_seek});
141 }
142
143 sub recommended_tags {
144     my $self = shift;
145     my %tags = map { $_->name => $_ }
146                map { $_->tags } 
147                $self->find_similar(3);
148     values %tags;
149 }
150
151 sub find_similar {
152     my ($self, $count) = @_;
153     Image::Seek::cleardb();
154     Image::Seek::loaddb(Memories->config->{image_seek});
155     my @res = map {$_->[0] } Image::Seek::query_id($self->id, $count);
156     shift @res; # $self
157     map { $self->retrieve($_) } @res;
158 }
159
160 sub unrotate {
161     my $self = shift;
162     my $orient = $self->exif_info->{EXIF}->{Orientation};
163     return if $orient !~/Rotate (\d+)/i;
164     my $steps = $1/90;
165     my $img = Image::Imlib2->load($self->path("file"));
166     $img->image_orientate($steps);
167     $img->save($self->path("file"));
168 }
169
170 sub exif_info {
171     my $self = shift;
172     my $info = $self->exif_object;
173     return $info if $info;
174     # Get it from the Cache
175     if (!($info = $cache->get($self->id))) {
176         # Create it
177         $info = $self->_exif_info;
178         $cache->set($self->id, $info);
179     }
180     $self->exif_object($info);
181     $info;
182 }
183
184 my %banned_tags = map { $_ => 1 }
185     qw( CodedCharacterSet ApplicationRecordVersion );
186
187 sub _exif_info {
188     my $exifTool = new Image::ExifTool;
189     $exifTool->Options(Group0 => ['IPTC', 'EXIF', 'XMP', 'MakerNotes', 'Composite']);
190     my $info = $exifTool->ImageInfo(shift->path);
191     my $hash = {};
192     foreach my $tag ($exifTool->GetFoundTags('Group0')) {
193         next if $banned_tags{$tag};
194          my $group = $exifTool->GetGroup($tag);
195          my $val = $info->{$tag};
196          next if ref $val eq 'SCALAR';
197          next if $val =~ /^[0\s]*$/ or $val =~ /^nil$/;
198          $hash->{$group}->{$exifTool->GetDescription($tag)} = $val;
199     }
200     return $hash;
201 }
202
203 # XXX Cache this
204 sub dimensions { join "x", $_[0]->x, $_[0]->y }
205
206 sub is_bigger {
207     my ($self, $size) = @_;
208     return 1 if $size eq "full";
209     my ($w, $h) = ($self->x, $self->y);
210     my ($w2, $h2) = split /x/, $size;
211     return 1 if $w > $w2 or $h > $h2;
212     return 0;
213 }
214
215 sub sized_url { # Use this rather than ->path from TT
216     my ($self, $size) = @_;
217     my $url = Memories->config->{data_store_external};
218     my $resized = Memories->config->{sizes}->[$size];
219     if (!$resized) { cluck "Asked for crazy size $size"; return; }
220     if ($resized eq "full") { return $self->path("url") }
221     $self->scale($resized) 
222         unless -e $self->path( file => $resized );
223     return $self->path(url => $resized);
224 }
225
226 sub path { 
227     my ($self, $is_url, $scale) = @_;
228     my $path =
229         Memories->config->{$is_url eq "url" ? "data_store_external" : "data_store" };
230     if ($scale) { $path .= "$scale/" }
231     # Make dir if it doesn't exist, save trouble later
232     use File::Path;
233     if ($is_url ne "url") {mkpath($path);}
234     $path .= $self->id.".".$self->format;
235     return $path;
236 }
237
238 sub thumb_url { shift->path(url => Memories->config->{thumb_size}); }
239 sub make_thumb { shift->scale(Memories->config->{thumb_size}, 1); }
240
241 use Image::Imlib2;
242 sub scale {
243     my ($self, $scale, $swap) = @_;
244     my ($x, $y) = split /x/, $scale;
245     # Find out smaller axis
246     my ($cur_x, $cur_y) = ($self->x, $self->y);
247     if (!$swap) {
248         if ($cur_x < $cur_y) { $y = 0 } else { $x = 0 }
249     } else {
250         if ($cur_x > $cur_y) { $y = 0 } else { $x = 0 }
251     }
252     my $img = Image::Imlib2->load($self->path("file"));
253     unless ($img) {
254         cluck "Couldn't open image file ".$self->path("file");
255         return;
256     }
257     $img = $img->create_scaled_image($x, $y);
258     $img->image_set_format("jpeg");
259     my $file = $self->path( file => $scale );
260     $img->save($file);
261     if ($!) {
262         cluck "Couldn't write image file $file ($!)"; 
263         return;
264     }
265 }
266
267 use Text::Balanced qw(extract_multiple extract_quotelike);
268 sub edit_tags :Exported {
269     my ($self, $r) = @_;
270     my $photo = $r->objects->[0];
271     my %params = %{$r->params};
272     for (keys %params) { 
273         next unless /delete_(\d+)/;
274         my $tagging = Memories::Tagging->retrieve($1) or next;
275         next unless $tagging->photo->id == $photo->id;
276         $tagging->delete;
277     }
278     $photo->add_tags($params{newtags});
279     $r->template("view");
280 }
281
282 sub add_tags {
283     my ($photo, $tagstring) = @_;
284
285     for my $tag (map { s/^"|"$//g; $_} extract_multiple(lc $tagstring, [ \&extract_quotelike, qr/([^\s]+)/ ], undef,1)) {
286         $photo->add_to_tags({tag => Memories::Tag->find_or_create({name =>$tag}) })
287     }
288 }
289
290 # Work out some common properties from a set of potential photo metadata
291 # tags
292 sub _grovel_metadata {
293     my ($self, @tags) = @_;
294     my %md = map {%$_} values %{$self->exif_info};
295     for (@tags) {
296         if ($md{$_} and $md{$_} =~/[^ 0:]/) { return $md{$_} }
297     }
298     return;
299 }
300
301 sub shot {
302     my $self = shift;
303     my $dt = $self->_grovel_metadata(
304         'Shooting Date/Time',
305         'Date/Time Of Digitization',
306         'Date/Time Of Last Modification'
307     );
308     if (!$dt) { return $self->uploaded }
309     return Time::Piece->strptime($dt, "%Y:%m:%d %T") || $self->uploaded;
310 }
311
312 sub description {
313     shift->_grovel_metadata(
314         'Description', 'Image Description', 'Caption-Abstract'
315     );
316 }
317
318 sub title_exif { shift->_grovel_metadata( 'Headline', 'Title'); }
319 sub license { shift->_grovel_metadata( 'Rights Usage Terms', 'Usage Terms' ) }
320 sub copyright { shift->_grovel_metadata( 'Rights', 'Copyright', 'Copyright Notice') }
321 1;