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