mourningdove/cgi-bin/DW/Media/Photo.pm

208 lines
7.4 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# DW::Media::Photo
#
# Special module for photos for the DW media system.
#
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2010 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#
package DW::Media::Photo;
use strict;
use Carp qw/ croak confess /;
use Image::Magick;
use Image::ExifTool qw/ :Public /;
use DW::BlobStore;
use DW::Media::Base;
use base 'DW::Media::Base';
sub new_from_row {
my ( $class, %opts ) = @_;
$opts{versions} ||= {};
# We might be given an optional width and height parameters, which aren't
# part of our basic object.
my ( $width, $height ) = ( delete $opts{width}, delete $opts{height} );
my $self = bless \%opts, $class;
# Save the URL width/height, since we'll need that for later.
$self->{url_width} = $width;
$self->{url_height} = $height;
# Now pull out width and height for the default version.
foreach my $vid ( keys %{ $self->{versions} } ) {
if ( $vid == $self->id ) {
$self->{width} = $self->{versions}->{$vid}->{width};
$self->{height} = $self->{versions}->{$vid}->{height};
# save the original values of these for reference in case we resize later
$self->{orig_width} = $self->{width};
$self->{orig_height} = $self->{height};
$self->{orig_filesize} = $self->{filesize};
last;
}
}
# Now, if given a width and height, select for it.
$self->_select_version( width => $width, height => $height )
if defined $width && defined $height;
return $self;
}
# Called with the file extension (one of our well known file types) and a
# reference to the image data, which is updated if necessary.
sub preprocess {
my ( $class, $ext, $dataref ) = @_;
# For now, we only care about jpegs since they need to be reoriented.
return unless $ext eq 'jpg';
# Extract EXIF orientation data to calculate our operations.
my $timage = Image::Magick->new()
or croak 'Failed to instantiate Image::Magick object.';
$timage->BlobToImage($$dataref);
$timage->AutoOrient();
$$dataref = $timage->ImageToBlob;
# The orientation should now be reset to 1 to prevent browser rotating.
my $exif = Image::ExifTool->new;
$exif->SetNewValue( Orientation => 1, Type => 'Raw' );
$exif->WriteInfo($dataref);
}
sub _resize {
my ( $self, %opts ) = @_;
my ( $want_width, $want_height ) =
( delete $opts{width}, delete $opts{height} );
return unless defined $want_width && defined $want_height;
# Do not allow resizing of scaled images.
croak 'Attempted to resize already resized image.'
if $self->{mediaid} != $self->{versionid};
# Allocate new version ID.
my $versionid = LJ::alloc_user_counter( $self->u, 'A' )
or croak 'Failed to allocate version id for media resize.';
# Scale the sizes.
my ( $width, $height ) = ( $self->{width}, $self->{height} );
my ( $horiz_ratio, $vert_ratio ) = ( $want_width / $width, $want_height / $height );
my $ratio = $horiz_ratio < $vert_ratio ? $horiz_ratio : $vert_ratio;
( $width, $height ) = ( int( $width * $ratio + 0.5 ), int( $height * $ratio + 0.5 ) );
# Load the image data, then scale it.
my ( $username, $mediaid ) = ( $self->u->user, $self->{mediaid} );
my $dataref = DW::BlobStore->retrieve( media => $self->mogkey )
or croak "Failed to load image file $mediaid for $username.";
my $timage = Image::Magick->new()
or croak 'Failed to instantiate Image::Magick object.';
$timage->BlobToImage($$dataref);
$timage->Scale( width => $width, height => $height );
my $blob = $timage->ImageToBlob;
# Fix up this object's internal representation.
$self->{versionid} = $versionid;
$self->{width} = $timage->Get('width');
$self->{height} = $timage->Get('height');
$self->{filesize} = length $blob;
# Now save to file storage first, before adding it to the database.
DW::BlobStore->store( media => $self->mogkey, \$blob )
or croak 'Unable to save resized file to storage.';
# Insert into the database, then we're done.
my $u = $self->u;
$u->do(
q{INSERT INTO media_versions (userid, mediaid, versionid, height, width, filesize)
VALUES (?, ?, ?, ?, ?, ?)},
undef, $self->{userid}, $self->{mediaid}, $versionid, $self->{height},
$self->{width}, $self->{filesize}
);
croak $u->errstr if $u->err;
return $self;
}
# Requires both width and height.
sub _select_version {
my ( $self, %opts ) = @_;
my ( $want_width, $want_height ) =
( delete $opts{width}, delete $opts{height} );
return unless defined $want_width && defined $want_height;
# Ensure no extra options (mostly, makes sure this code gets updated if
# someone wants to add extra stuff).
croak 'Extra options to _select_version.' if %opts;
my ( $width, $height ) = ( $self->{width}, $self->{height} );
croak 'Image has no internal width/height!' # Should never fire...
unless defined $width && $width > 0 && defined $height && $height > 0;
# If we want larger than we are (and this is the original), accept it.
return if $want_width >= $width && $want_height >= $height;
# We have a simple algorithm: we look at our existing versions and try to
# find one that has an edge match where the other side is within bounds. If
# that is true, we trust it and return it.
foreach my $vid ( keys %{ $self->{versions} } ) {
my ( $ver_width, $ver_height ) =
( $self->{versions}->{$vid}->{width}, $self->{versions}->{$vid}->{height} );
if ( ( $ver_width == $want_width && $ver_height <= $want_height )
|| ( $ver_height == $want_height && $ver_width <= $want_width ) )
{
$self->{versionid} = $vid;
$self->{$_} = $self->{versions}->{$vid}->{$_} foreach qw/ width height filesize /;
return;
}
}
# No version found... so now we want to kick off a Gearman job to do the
# resize for us. FIXME: This is inline for now.
croak 'Failed to resize.'
unless $self->_resize( width => $want_width, height => $want_height );
# The _resize call also updates our internal data, so this image is now
# the resized image.
}
# this adds on to the base method by also deleting any associated thumbnails
sub delete {
my $self = $_[0];
my $deleted = $self->SUPER::delete; # this deletes the original as before
return 0 unless $deleted; # was already deleted
# at this point the image has just been deleted - look for thumbnails
my $u = $self->u or croak 'Sorry, unable to load the user.';
my @mv = $u->selectrow_array(
"SELECT versionid FROM media_versions WHERE userid=? AND mediaid=?" . " AND versionid != ?",
undef, $u->id, $self->versionid, $self->versionid
);
return $deleted unless @mv;
foreach my $id (@mv) {
# create a fake object to get the mogkey
my $fakeobj = bless { userid => $u->id, versionid => $id }, 'DW::Media::Photo';
# we aren't concerned whether the file existed or not,
# and the associated media row is already in a deleted state
DW::BlobStore->delete( media => $fakeobj->mogkey );
}
return 1; # done
}
1;