package LJ; use strict; # # name: LJ::load_userpics # des: Loads a bunch of userpics at once. # args: dbarg?, upics, idlist # des-upics: hashref to load pictures into, keys being the picids. # des-idlist: [$u, $picid] or [[$u, $picid], [$u, $picid], +] objects # also supports deprecated old method, of an array ref of picids. # sub load_userpics { &nodb; my ($upics, $idlist) = @_; return undef unless ref $idlist eq 'ARRAY' && $idlist->[0]; # deal with the old calling convention, just an array ref of picids eg. [7, 4, 6, 2] if (! ref $idlist->[0] && $idlist->[0]) { # assume we have an old style caller my $in = join(',', map { $_+0 } @$idlist); my $dbr = LJ::get_db_reader(); my $sth = $dbr->prepare("SELECT userid, picid, width, height " . "FROM userpic WHERE picid IN ($in)"); $sth->execute; while ($_ = $sth->fetchrow_hashref) { my $id = $_->{'picid'}; undef $_->{'picid'}; $upics->{$id} = $_; } return; } # $idlist needs to be an arrayref of arrayrefs, # HOWEVER, there's a special case where it can be # an arrayref of 2 items: $u (which is really an arrayref) # as well due to 'fields' and picid which is an integer. # # [$u, $picid] needs to map to [[$u, $picid]] while allowing # [[$u1, $picid1], [$u2, $picid2], [etc...]] to work. if (scalar @$idlist == 2 && ! ref $idlist->[1]) { $idlist = [ $idlist ]; } my @load_list; foreach my $row (@{$idlist}) { my ($u, $id) = @$row; next unless ref $u; if ($LJ::CACHE_USERPIC{$id}) { $upics->{$id} = $LJ::CACHE_USERPIC{$id}; } elsif ($id+0) { push @load_list, [$u, $id+0]; } } return unless @load_list; if (@LJ::MEMCACHE_SERVERS) { my @mem_keys = map { [$_->[1],"userpic.$_->[1]"] } @load_list; my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; while (my ($k, $v) = each %$mem) { next unless $v && $k =~ /(\d+)/; my $id = $1; $upics->{$id} = LJ::MemCache::array_to_hash("userpic", $v); } @load_list = grep { ! $upics->{$_->[1]} } @load_list; return unless @load_list; } my %db_load; my @load_list_d6; foreach my $row (@load_list) { # ignore users on clusterid 0 next unless $row->[0]->{clusterid}; if ($row->[0]->{'dversion'} > 6) { push @{$db_load{$row->[0]->{'clusterid'}}}, $row; } else { push @load_list_d6, $row; } } foreach my $cid (keys %db_load) { my $dbcr = LJ::get_cluster_def_reader($cid); unless ($dbcr) { print STDERR "Error: LJ::load_userpics unable to get handle; cid = $cid\n"; next; } my (@bindings, @data); foreach my $row (@{$db_load{$cid}}) { push @bindings, "(userid=? AND picid=?)"; push @data, ($row->[0]->{userid}, $row->[1]); } next unless @data && @bindings; my $sth = $dbcr->prepare("SELECT userid, picid, width, height, fmt, state, ". " UNIX_TIMESTAMP(picdate) AS 'picdate', location, flags ". "FROM userpic2 WHERE " . join(' OR ', @bindings)); $sth->execute(@data); while (my $ur = $sth->fetchrow_hashref) { my $id = delete $ur->{'picid'}; $upics->{$id} = $ur; # force into numeric context so they'll be smaller in memcache: foreach my $k (qw(userid width height flags picdate)) { $ur->{$k} += 0; } $ur->{location} = uc(substr($ur->{location}, 0, 1)); $LJ::CACHE_USERPIC{$id} = $ur; LJ::MemCache::set([$id,"userpic.$id"], LJ::MemCache::hash_to_array("userpic", $ur)); } } # following path is only for old style d6 userpics... don't load any if we don't # have any to load return unless @load_list_d6; my $dbr = LJ::get_db_writer(); my $picid_in = join(',', map { $_->[1] } @load_list_d6); my $sth = $dbr->prepare("SELECT userid, picid, width, height, contenttype, state, ". " UNIX_TIMESTAMP(picdate) AS 'picdate' ". "FROM userpic WHERE picid IN ($picid_in)"); $sth->execute; while (my $ur = $sth->fetchrow_hashref) { my $id = delete $ur->{'picid'}; $upics->{$id} = $ur; # force into numeric context so they'll be smaller in memcache: foreach my $k (qw(userid width height picdate)) { $ur->{$k} += 0; } $ur->{location} = "?"; $ur->{flags} = undef; $ur->{fmt} = { 'image/gif' => 'G', 'image/jpeg' => 'J', 'image/png' => 'P', }->{delete $ur->{contenttype}}; $LJ::CACHE_USERPIC{$id} = $ur; LJ::MemCache::set([$id,"userpic.$id"], LJ::MemCache::hash_to_array("userpic", $ur)); } } # # name: LJ::expunge_userpic # des: Expunges a userpic so that the system will no longer deliver this userpic. If # your site has off-site caching or something similar, you can also define a hook # "expunge_userpic" which will be called with a picid and userid when a pic is # expunged. # args: u, picid # des-picid: ID of the picture to expunge. # des-u: User object # returns: undef on error, or the userid of the picture owner on success. # sub expunge_userpic { # take in a picid and expunge it from the system so that it can no longer be used my ($u, $picid) = @_; $picid += 0; return undef unless $picid && ref $u; # get the pic information my $state; if ($u->{'dversion'} > 6) { my $dbcm = LJ::get_cluster_master($u); return undef unless $dbcm && $u->writer; $state = $dbcm->selectrow_array('SELECT state FROM userpic2 WHERE userid = ? AND picid = ?', undef, $u->{'userid'}, $picid); return undef unless $state; # invalid pic return $u->{'userid'} if $state eq 'X'; # already expunged # else now mark it $u->do("UPDATE userpic2 SET state='X' WHERE userid = ? AND picid = ?", undef, $u->{'userid'}, $picid); return LJ::error($dbcm) if $dbcm->err; $u->do("DELETE FROM userpicmap2 WHERE userid = ? AND picid = ?", undef, $u->{'userid'}, $picid); } else { my $dbr = LJ::get_db_reader(); return undef unless $dbr; $state = $dbr->selectrow_array('SELECT state FROM userpic WHERE picid = ?', undef, $picid); return undef unless $state; # invalid pic return $u->{'userid'} if $state eq 'X'; # already expunged # else now mark it my $dbh = LJ::get_db_writer(); return undef unless $dbh; $dbh->do("UPDATE userpic SET state='X' WHERE picid = ?", undef, $picid); return LJ::error($dbh) if $dbh->err; $dbh->do("DELETE FROM userpicmap WHERE userid = ? AND picid = ?", undef, $u->{'userid'}, $picid); } # now clear the user's memcache picture info LJ::Userpic->delete_cache($u); # call the hook and get out of here my @rval = LJ::run_hooks('expunge_userpic', $picid, $u->{'userid'}); return ($u->{'userid'}, map {$_->[0]} grep {$_ && @$_ && $_->[0]} @rval); } # # name: LJ::activate_userpics # des: des: Wrapper around [func[LJ::User::activate_userpics]] for compatibility. # args: uuserid # returns: undef on failure 1 on success # sub activate_userpics { my $u = shift; return undef unless LJ::isu($u); # if a userid was given, get a real $u object $u = LJ::load_userid($u, "force") unless isu($u); # should have a $u object now return undef unless isu($u); return $u->activate_userpics; } # # name: LJ::get_userpic_info # des: Given a user, gets their userpic information. # args: uuid, opts? # des-uuid: userid, or user object. # des-opts: Optional; hash of options, 'load_comments'. # returns: hash of userpicture information; # for efficiency, we store the userpic structures # in memcache in a packed format. # info: memory format: # [ # version number of format, # userid, # "packed string", which expands to an array of {width=>..., ...} # "packed string", which expands to { 'kw1' => id, 'kw2' => id, ...} # ] # sub get_userpic_info { my ($uuid, $opts) = @_; return undef unless $uuid; my $userid = LJ::want_userid($uuid); my $u = LJ::want_user($uuid); # This should almost always be in memory already return undef unless $u && $u->{clusterid}; # in the cache, cool, well unless it doesn't have comments or urls # and we need them if (my $cachedata = $LJ::CACHE_USERPIC_INFO{$userid}) { my $good = 1; if ($u->{'dversion'} > 6) { $good = 0 if $opts->{'load_comments'} && ! $cachedata->{'_has_comments'}; $good = 0 if $opts->{'load_urls'} && ! $cachedata->{'_has_urls'}; } return $cachedata if $good; } my $VERSION_PICINFO = 3; my $memkey = [$u->{'userid'},"upicinf:$u->{'userid'}"]; my ($info, $minfo); if ($minfo = LJ::MemCache::get($memkey)) { # the pre-versioned memcache data was a two-element hash. # since then, we use an array and include a version number. if (ref $minfo eq 'HASH' || $minfo->[0] != $VERSION_PICINFO) { # old data in the cache. delete. LJ::MemCache::delete($memkey); } else { my (undef, $picstr, $kwstr) = @$minfo; $info = { 'pic' => {}, 'kw' => {}, }; while (length $picstr >= 7) { my $pic = { userid => $u->{'userid'} }; ($pic->{picid}, $pic->{width}, $pic->{height}, $pic->{state}) = unpack "NCCA", substr($picstr, 0, 7, ''); $info->{pic}->{$pic->{picid}} = $pic; } my ($pos, $nulpos); $pos = $nulpos = 0; while (($nulpos = index($kwstr, "\0", $pos)) > 0) { my $kw = substr($kwstr, $pos, $nulpos-$pos); my $id = unpack("N", substr($kwstr, $nulpos+1, 4)); $pos = $nulpos + 5; # skip NUL + 4 bytes. $info->{kw}->{$kw} = $info->{pic}->{$id} if $info; } } if ($u->{'dversion'} > 6) { # Load picture comments if ($opts->{'load_comments'}) { my $commemkey = [$u->{'userid'}, "upiccom:$u->{'userid'}"]; my $comminfo = LJ::MemCache::get($commemkey); if ($comminfo) { my ($pos, $nulpos); $pos = $nulpos = 0; while (($nulpos = index($comminfo, "\0", $pos)) > 0) { my $comment = substr($comminfo, $pos, $nulpos-$pos); my $id = unpack("N", substr($comminfo, $nulpos+1, 4)); $pos = $nulpos + 5; # skip NUL + 4 bytes. $info->{'pic'}->{$id}->{'comment'} = $comment; $info->{'comment'}->{$id} = $comment; } $info->{'_has_comments'} = 1; } else { # Requested to load comments, but they aren't in memcache # so force a db load undef $info; } } # Load picture urls if ($opts->{'load_urls'} && $info) { my $urlmemkey = [$u->{'userid'}, "upicurl:$u->{'userid'}"]; my $urlinfo = LJ::MemCache::get($urlmemkey); if ($urlinfo) { my ($pos, $nulpos); $pos = $nulpos = 0; while (($nulpos = index($urlinfo, "\0", $pos)) > 0) { my $url = substr($urlinfo, $pos, $nulpos-$pos); my $id = unpack("N", substr($urlinfo, $nulpos+1, 4)); $pos = $nulpos + 5; # skip NUL + 4 bytes. $info->{'pic'}->{$id}->{'url'} = $url; } $info->{'_has_urls'} = 1; } else { # Requested to load urls, but they aren't in memcache # so force a db load undef $info; } } } } my %minfocom; # need this in this scope my %minfourl; unless ($info) { $info = { 'pic' => {}, 'kw' => {}, }; my ($picstr, $kwstr); my $sth; my $dbcr = LJ::get_cluster_def_reader($u); my $db = @LJ::MEMCACHE_SERVERS ? LJ::get_db_writer() : LJ::get_db_reader(); return undef unless $dbcr && $db; if ($u->{'dversion'} > 6) { $sth = $dbcr->prepare("SELECT picid, width, height, state, userid, comment, url ". "FROM userpic2 WHERE userid=?"); } else { $sth = $db->prepare("SELECT picid, width, height, state, userid ". "FROM userpic WHERE userid=?"); } $sth->execute($u->{'userid'}); my @pics; while (my $pic = $sth->fetchrow_hashref) { next if $pic->{state} eq 'X'; # no expunged pics in list push @pics, $pic; $info->{'pic'}->{$pic->{'picid'}} = $pic; $minfocom{int($pic->{picid})} = $pic->{comment} if $u->{'dversion'} > 6 && $opts->{'load_comments'} && $pic->{'comment'}; $minfourl{int($pic->{'picid'})} = $pic->{'url'} if $u->{'dversion'} > 6 && $opts->{'load_urls'} && $pic->{'url'}; } $picstr = join('', map { pack("NCCA", $_->{picid}, $_->{width}, $_->{height}, $_->{state}) } @pics); if ($u->{'dversion'} > 6) { $sth = $dbcr->prepare("SELECT k.keyword, m.picid FROM userpicmap2 m, userkeywords k ". "WHERE k.userid=? AND m.kwid=k.kwid AND m.userid=k.userid"); } else { $sth = $db->prepare("SELECT k.keyword, m.picid FROM userpicmap m, keywords k ". "WHERE m.userid=? AND m.kwid=k.kwid"); } $sth->execute($u->{'userid'}); my %minfokw; while (my ($kw, $id) = $sth->fetchrow_array) { next unless $info->{'pic'}->{$id}; next if $kw =~ /[\n\r\0]/; # used to be a bug that allowed these to get in. $info->{'kw'}->{$kw} = $info->{'pic'}->{$id}; $minfokw{$kw} = int($id); } $kwstr = join('', map { pack("Z*N", $_, $minfokw{$_}) } keys %minfokw); $memkey = [$u->{'userid'},"upicinf:$u->{'userid'}"]; $minfo = [ $VERSION_PICINFO, $picstr, $kwstr ]; LJ::MemCache::set($memkey, $minfo); if ($u->{'dversion'} > 6) { if ($opts->{'load_comments'}) { $info->{'comment'} = \%minfocom; my $commentstr = join('', map { pack("Z*N", $minfocom{$_}, $_) } keys %minfocom); my $memkey = [$u->{'userid'}, "upiccom:$u->{'userid'}"]; LJ::MemCache::set($memkey, $commentstr); $info->{'_has_comments'} = 1; } if ($opts->{'load_urls'}) { my $urlstr = join('', map { pack("Z*N", $minfourl{$_}, $_) } keys %minfourl); my $memkey = [$u->{'userid'}, "upicurl:$u->{'userid'}"]; LJ::MemCache::set($memkey, $urlstr); $info->{'_has_urls'} = 1; } } } $LJ::CACHE_USERPIC_INFO{$u->{'userid'}} = $info; return $info; } # # name: LJ::get_pic_from_keyword # des: Given a userid and keyword, returns the pic row hashref. # args: u, keyword # des-keyword: The keyword of the userpic to fetch. # returns: hashref of pic row found # sub get_pic_from_keyword { my ($u, $kw) = @_; my $info = LJ::get_userpic_info($u) or return undef; if (my $pic = $info->{'kw'}{$kw}) { return $pic; } # the lame "pic#2343" thing when they didn't assign a keyword if ($kw =~ /^pic\#(\d+)$/) { my $picid = $1; if (my $pic = $info->{'pic'}{$picid}) { return $pic; } } return undef; } sub get_picid_from_keyword { my ($u, $kw, $default) = @_; $default ||= (ref $u ? $u->{'defaultpicid'} : 0); return $default unless $kw; my $info = LJ::get_userpic_info($u); return $default unless $info; my $pr = $info->{'kw'}{$kw}; # normal keyword return $pr->{picid} if $pr->{'picid'}; # the lame "pic#2343" thing when they didn't assign a keyword if ($kw =~ /^pic\#(\d+)$/) { my $picid = $1; return $picid if $info->{'pic'}{$picid}; } return $default; } # this will return a user's userpicfactory image stored in mogile scaled down. # if only $size is passed, will return image scaled so the largest dimension will # not be greater than $size. If $x1, $y1... are set then it will return the image # scaled so the largest dimension will not be greater than 100 # all parameters are optional, default size is 640. # # if maxfilesize option is passed, get_upf_scaled will decrease the image quality # until it reaches maxfilesize, in kilobytes. (only applies to the 100x100 userpic) # # returns [imageref, mime, width, height] on success, undef on failure. # # note: this will always keep the image's original aspect ratio and not distort it. sub get_upf_scaled { my @args = @_; my $gc = LJ::gearman_client(); # no gearman, do this in-process return LJ::_get_upf_scaled(@args) unless $gc; # invoke gearman my $u = LJ::get_remote() or die "No remote user"; unshift @args, "userid" => $u->id; my $result; my $arg = Storable::nfreeze(\@args); my $task = Gearman::Task->new('lj_upf_resize', \$arg, { uniq => '-', on_complete => sub { my $res = shift; return unless $res; $result = Storable::thaw($$res); } }); my $ts = $gc->new_task_set(); $ts->add_task($task); $ts->wait(timeout => 30); # 30 sec timeout; # job failed ... error reporting? die "Could not resize image down\n" unless $result; return $result; } # actual method sub _get_upf_scaled { my %opts = @_; my $size = delete $opts{size} || 640; my $x1 = delete $opts{x1}; my $y1 = delete $opts{y1}; my $x2 = delete $opts{x2}; my $y2 = delete $opts{y2}; my $border = delete $opts{border} || 0; my $maxfilesize = delete $opts{maxfilesize} || 38; my $u = LJ::want_user(delete $opts{userid} || delete $opts{u}) || LJ::get_remote(); my $mogkey = delete $opts{mogkey}; my $downsize_only = delete $opts{downsize_only}; croak "No userid or remote" unless $u || $mogkey; $maxfilesize *= 1024; croak "Invalid parameters to get_upf_scaled\n" if scalar keys %opts; my $mode = ($x1 || $y1 || $x2 || $y2) ? "crop" : "scale"; eval "use Image::Magick (); 1;" or return undef; eval "use Image::Size (); 1;" or return undef; $mogkey ||= 'upf:' . $u->{userid}; my $dataref = LJ::mogclient()->get_file_data($mogkey) or return undef; # original width/height my ($ow, $oh) = Image::Size::imgsize($dataref); return undef unless $ow && $oh; # converts an ImageMagick object to the form returned to our callers my $imageParams = sub { my $im = shift; my $blob = $im->ImageToBlob; return [\$blob, $im->Get('MIME'), $im->Get('width'), $im->Get('height')]; }; # compute new width and height while keeping aspect ratio my $getSizedCoords = sub { my $newsize = shift; my $fromw = $ow; my $fromh = $oh; my $img = shift; if ($img) { $fromw = $img->Get('width'); $fromh = $img->Get('height'); } return (int($newsize * $fromw/$fromh), $newsize) if $fromh > $fromw; return ($newsize, int($newsize * $fromh/$fromw)); }; # get the "medium sized" width/height. this is the size which # the user selects from my ($medw, $medh) = $getSizedCoords->($size); return undef unless $medw && $medh; # simple scaling mode if ($mode eq "scale") { my $image = Image::Magick->new(size => "${medw}x${medh}") or return undef; $image->BlobToImage($$dataref); unless ($downsize_only && ($medw > $ow || $medh > $oh)) { $image->Resize(width => $medw, height => $medh); } return $imageParams->($image); } # else, we're in 100x100 cropping mode # scale user coordinates up from the medium pixelspace to full pixelspace $x1 *= ($ow/$medw); $x2 *= ($ow/$medw); $y1 *= ($oh/$medh); $y2 *= ($oh/$medh); # cropping dimensions from the full pixelspace my $tw = $x2 - $x1; my $th = $y2 - $y1; # but if their selected region in full pixelspace is 800x800 or something # ridiculous, no point decoding the JPEG to its full size... we can # decode to a smaller size so we get 100px when we crop my $min_dim = $tw < $th ? $tw : $th; my ($decodew, $decodeh) = ($ow, $oh); my $wanted_size = 100; if ($min_dim > $wanted_size) { # then let's not decode the full JPEG down from its huge size my $de_scale = $wanted_size / $min_dim; $decodew = int($de_scale * $decodew); $decodeh = int($de_scale * $decodeh); $_ *= $de_scale foreach ($x1, $x2, $y1, $y2); } $_ = int($_) foreach ($x1, $x2, $y1, $y2, $tw, $th); # make the pristine (uncompressed) 100x100 image my $timage = Image::Magick->new(size => "${decodew}x${decodeh}") or return undef; $timage->BlobToImage($$dataref); $timage->Scale(width => $decodew, height => $decodeh); my $w = ($x2 - $x1); my $h = ($y2 - $y1); my $foo = $timage->Mogrify(crop => "${w}x${h}+$x1+$y1"); my $targetSize = $border ? 98 : 100; my ($nw, $nh) = $getSizedCoords->($targetSize, $timage); $timage->Scale(width => $nw, height => $nh); # add border if desired $timage->Border(geometry => "1x1", color => 'black') if $border; foreach my $qual (qw(100 90 85 75)) { # work off a copy of the image so we aren't recompressing it my $piccopy = $timage->Clone(); $piccopy->Set('quality' => $qual); my $ret = $imageParams->($piccopy); return $ret if length(${ $ret->[0] }) < $maxfilesize; } return undef; } 1;