]> git.decadent.org.uk Git - memories.git/blob - Memories/Photo.pm
Ajaxify the main tabs
[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     $r->{params}{tags} ||= join " ", map { qq{"$_"} } $photo->tags_exif;
58     $photo->add_tags($r->{params}{tags});
59     $photo->add_to_imageseek_library;
60     Memories->zap_cache();
61
62     # Add system tags here
63     my $tag = "date:".$photo->shot->ymd;
64     $photo->add_to_system_tags({tag => Memories::SystemTag->find_or_create({name =>$tag}) });
65
66     # Set it up to go again
67     $r->objects([$photo]);
68     $r->template("view");
69     $r->message("Thanks for the upload! ".
70         ($r->{params}{tags} ? "" 
71         : "Don't forget to <a href=\"?".$r->config->uri_base."photo/view/".$photo->id."?active=tagedit\">tag your photo</a>"
72         )
73     ); 
74 }
75
76 sub view :Exported {
77     my ($self, $r) = @_;
78     my $photo = $r->{objects}[0];
79     $photo->hit_count($photo->hit_count()+1);
80     if ($r->{session}{last_search}) {
81         # This is slightly inefficient
82         my @search = split/,/, $r->{session}{last_search};
83         my $found = -1;
84         for my $i (0..$#search) {
85             next unless $photo->id == $search[$i];
86             $found = $i;
87         }
88         return unless $found > -1;
89         $r->{template_args}{next} = $self->retrieve($search[$found+1]) 
90             if $found+1 <= $#search;
91         $r->{template_args}{prev} = $self->retrieve($search[$found-1])
92             if $found-1 >= 0;
93     }
94 }
95 sub upload :Exported {}
96 sub exif :Exported {}
97 sub comment :Exported {}
98 sub tagedit :Exported {}
99 sub similar :Exported {}
100
101 use Class::DBI::Plugin::Pager;
102 use Class::DBI::Plugin::AbstractCount;
103
104 sub recent :Exported {
105     my ($self, $r) = @_;
106     my $page = $r->params->{page} || 1;
107     my $pager = $self->pager(
108         per_page => Memories->config->{photos_per_page}, 
109         page => $page,
110         syntax => PAGER_SYNTAX,
111         order_by => "uploaded desc"
112     );
113     $r->objects([$pager->retrieve_all ]);
114     $r->{template_args}{pager} = $pager;
115     $r->last_search;
116 }
117
118 sub add_comment :Exported {
119     my ($self, $r, $photo) = @_;
120     $r->template("view");
121     $r->objects->[0]->add_to_comments({
122         name => $r->params->{name},
123         content => $r->params->{content}
124     });
125 }
126
127 sub format { 
128     "jpg" # For now
129
130
131 use Cache::MemoryCache;
132 use Image::Info qw(dim image_info);
133 use Image::ExifTool;
134 my $cache = new Cache::MemoryCache( { 'namespace' => 'MemoriesInfo' });
135
136 sub add_to_imageseek_library {
137     my $self = shift;
138     Image::Seek::cleardb();
139     my $img = Image::Imlib2->load($self->path("file"));
140
141     Image::Seek::add_image($img, $self->id);
142     # Merge this new one into the main database; there is a bit of a
143     # race condition here. XXX
144     Image::Seek::loaddb(Memories->config->{image_seek});
145     Image::Seek::savedb(Memories->config->{image_seek});
146 }
147
148 sub recommended_tags {
149     my $self = shift;
150     my %tags = map { $_->name => $_ }
151                map { $_->tags } 
152                $self->find_similar(3);
153     values %tags;
154 }
155
156 sub find_similar {
157     my ($self, $count) = @_;
158     Image::Seek::cleardb();
159     Image::Seek::loaddb(Memories->config->{image_seek});
160     my @res = map {$_->[0] } Image::Seek::query_id($self->id, $count);
161     shift @res; # $self
162     map { $self->retrieve($_) } @res;
163 }
164
165 sub unrotate {
166     my $self = shift;
167     my $orient = $self->exif_info->{EXIF}->{Orientation};
168     return if $orient !~/Rotate (\d+)/i;
169     my $steps = $1/90;
170     my $img = Image::Imlib2->load($self->path("file"));
171     $img->image_orientate($steps);
172     $img->save($self->path("file"));
173 }
174
175 sub exif_info {
176     my $self = shift;
177     my $info = $self->exif_object;
178     return $info if $info;
179     # Get it from the Cache
180     if (!($info = $cache->get($self->id))) {
181         # Create it
182         $info = $self->_exif_info;
183         $cache->set($self->id, $info);
184     }
185     $self->exif_object($info);
186     $info;
187 }
188
189 my %banned_tags = map { $_ => 1 }
190     qw( CodedCharacterSet ApplicationRecordVersion );
191
192 sub _exif_info {
193     my $exifTool = new Image::ExifTool;
194     $exifTool->Options(Group0 => ['IPTC', 'EXIF', 'XMP', 'MakerNotes', 'Composite']);
195     my $info = $exifTool->ImageInfo(shift->path);
196     my $hash = {};
197     foreach my $tag ($exifTool->GetFoundTags('Group0')) {
198         next if $banned_tags{$tag};
199          my $group = $exifTool->GetGroup($tag);
200          my $val = $info->{$tag};
201          next if ref $val eq 'SCALAR';
202          next if $val =~ /^[0\s]*$/ or $val =~ /^nil$/;
203          $hash->{$group}->{$exifTool->GetDescription($tag)} = $val;
204     }
205     return $hash;
206 }
207
208 # XXX Cache this
209 sub dimensions { join "x", $_[0]->x, $_[0]->y }
210
211 sub is_bigger {
212     my ($self, $size) = @_;
213     return 1 if $size eq "full";
214     my ($w, $h) = ($self->x, $self->y);
215     my ($w2, $h2) = split /x/, $size;
216     return 1 if $w > $w2 or $h > $h2;
217     return 0;
218 }
219
220 sub sized_url { # Use this rather than ->path from TT
221     my ($self, $size) = @_;
222     my $url = Memories->config->{data_store_external};
223     my $resized = Memories->config->{sizes}->[$size];
224     if (!$resized) { cluck "Asked for crazy size $size"; return; }
225     if ($resized eq "full") { return $self->path("url") }
226     $self->scale($resized) 
227         unless -e $self->path( file => $resized );
228     return $self->path(url => $resized);
229 }
230
231 sub path { 
232     my ($self, $is_url, $scale) = @_;
233     my $path =
234         Memories->config->{$is_url eq "url" ? "data_store_external" : "data_store" };
235     if ($scale) { $path .= "$scale/" }
236     # Make dir if it doesn't exist, save trouble later
237     use File::Path;
238     if ($is_url ne "url") {mkpath($path);}
239     $path .= $self->id.".".$self->format;
240     return $path;
241 }
242
243 sub thumb_url { shift->path(url => Memories->config->{thumb_size}); }
244 sub make_thumb { shift->scale(Memories->config->{thumb_size}, 1); }
245
246 use Image::Imlib2;
247 sub scale {
248     my ($self, $scale, $swap) = @_;
249     my ($x, $y) = split /x/, $scale;
250     # Find out smaller axis
251     my ($cur_x, $cur_y) = ($self->x, $self->y);
252     if (!$swap) {
253         if ($cur_x < $cur_y) { $y = 0 } else { $x = 0 }
254     } else {
255         if ($cur_x > $cur_y) { $y = 0 } else { $x = 0 }
256     }
257     my $img = Image::Imlib2->load($self->path("file"));
258     unless ($img) {
259         cluck "Couldn't open image file ".$self->path("file");
260         return;
261     }
262     $img = $img->create_scaled_image($x, $y);
263     $img->image_set_format("jpeg");
264     my $file = $self->path( file => $scale );
265     $img->save($file);
266     if ($!) {
267         cluck "Couldn't write image file $file ($!)"; 
268         return;
269     }
270 }
271
272 use Text::Balanced qw(extract_multiple extract_quotelike);
273 sub edit_tags :Exported {
274     my ($self, $r) = @_;
275     my $photo = $r->objects->[0];
276     my %params = %{$r->params};
277     for (keys %params) { 
278         next unless /delete_(\d+)/;
279         my $tagging = Memories::Tagging->retrieve($1) or next;
280         next unless $tagging->photo->id == $photo->id;
281         $tagging->delete;
282     }
283     $photo->add_tags($params{newtags});
284     $r->template("view");
285 }
286
287 sub add_tags {
288     my ($photo, $tagstring) = @_;
289
290     for my $tag (map { s/^"|"$//g; $_} extract_multiple(lc $tagstring, [ \&extract_quotelike, qr/([^\s]+)/ ], undef,1)) {
291         $photo->add_to_tags({tag => Memories::Tag->find_or_create({name =>$tag}) })
292     }
293 }
294
295 # Work out some common properties from a set of potential photo metadata
296 # tags
297 sub _grovel_metadata {
298     my ($self, @tags) = @_;
299     my %md = map {%$_} values %{$self->exif_info};
300     for (@tags) {
301         if ($md{$_} and $md{$_} =~/[^ 0:]/) { return $md{$_} }
302     }
303     return;
304 }
305
306 sub shot {
307     my $self = shift;
308     my $dt = $self->_grovel_metadata(
309         'Shooting Date/Time',
310         'Date/Time Of Digitization',
311         'Date/Time Of Last Modification'
312     );
313     if (!$dt) { return $self->uploaded }
314     return Time::Piece->strptime($dt, "%Y:%m:%d %T") || $self->uploaded;
315 }
316
317 sub description {
318     shift->_grovel_metadata(
319         'Description', 'Image Description', 'Caption-Abstract'
320     );
321 }
322
323 sub title_exif { shift->_grovel_metadata( 'Headline', 'Title'); }
324 sub license { shift->_grovel_metadata( 'Rights Usage Terms', 'Usage Terms' ) }
325 sub copyright { shift->_grovel_metadata( 'Rights', 'Copyright', 'Copyright Notice') }
326
327 # This one's slightly different since we want everything we can get...
328 sub tags_exif {
329     my $self = shift;
330     my %md = map {%$_} values %{$self->exif_info};
331     my %tags = 
332         map { s/\s+/-/g; lc $_ => 1  }
333         map { split /\s*,\s*/, $md{$_}}
334         grep {$md{$_} and $md{$_} =~/[^ 0:]/}
335         (qw(Keywords Subject City State Location Country Province-State), 
336         'Transmission Reference', 'Intellectual Genre', 
337         'Country-Primary Location Name'
338         );
339     return keys %tags;
340 }
341 1;