Super Simple Photo Gallery

Our packing list:

DBIx::Class::InflateColumn::FS

This will be used to store our file pointer in the database, write the image to the file system, and later, give us a Path::Class::File object to manipulate our images later on.

Imager

The all and the everything for our image manipulation needs. If not installed already, make sure you install libjpeg so that JPEG files can be manipulated by Imager.

MIME::Types
File::Mimeinfo
DateTime
Catalyst::Controller::HTML::FormFu

Once you have installed these modules, move on to the next step.

Let's get started already!

Ok. So here comes the fun stuff. We're going to assume you know how to create a Catalyst application skeleton, controllers, and models at the very least. If not, you have some documentation to read first :-)

Create a controller in your application called Photos as follows:

    perl script/myapp_create.pl controller Photos

Next, you'll want to set up a table called Photos in your RDBMS of choice. Here, I've used MySQL, but you could use SQLite , Postresql or any other RDBMS supported by DBIx::Class. The necessary columns you will need are as follows:

photoid

type: INT size: 11 null: no other: PRIMARY KEY auto_increment

name

type: VARCHAR size: 255 null: no other:

mime

type: VARCHAR size: 255 null: no other:

mime

type: VARCHAR size: 255 null: no other:

uploaded

type: DATETIME size: null: no other: (If you want, include the InflateColumn::DateTime component to be able to manipulate this column as if it were a DateTime object)

path

type: TEXT size: null: no other:

caption

type: TEXT size: null: no other:

Now, create your model:

    ./script/myapp_create.pl model DB DBIC::Schema MyApp::Schema 'DBI:mysql:database=myapp' 'user' 'password'

So now you have your basic tables set up. Next, we need to generate our Schema files so DBIx::Class can interact with our database objects. I use this script to update/create my schema definition from the database. Copy/paste this script into a file called myapp_update_schema.pl (replace "myapp" with whatever you named this application) and stick it in the script/ directory of your application:

 #!/usr/bin/perl 




 use FindBin;
 use DBIx::Class::Schema::Loader qw| make_schema_at |;
 make_schema_at(
 	"MyApp::Schema",
 	{
            debug          => 1,
	    use_namespaces => 0,
	    components     => [qw/ InflateColumn::DateTime InflateColumn::FS PK::Auto/],
	    dump_directory => "$FindBin::Bin/../lib"
	    },
	    [ "dbi:mysql:myapp", "user", "password" ]
    );

now run this script:

    perl script/myapp_update_schema.pl                                     
    MyApp::Schema::Photos->load_components(
      "InflateColumn::DateTime",
      "InflateColumn::FS",
    ...
    MyApp::Schema::Photos->set_primary_key("photoid");
    MyApp::Schema::Photos->add_unique_constraint("photos_pkey", ["photoid"]);
    Dumping manual schema for MyApp::Schema to directory /tmp/MyApp/script/../lib ...
    Schema dump completed.

If there are no errors, proceed!

Adding the InflateColumn::FS magic

Open up lib/Schema/Photos.pm. Since we used ::Loader, there will be a good portion of the file that you're not supposed to manually edit. This is marked by these lines:

    # Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-12-01 02:57:02
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A/YrsvGDAWKpDaGlJ3Dwqg

You can safely modify anything below this though. Add these lines to the file:

 use MyApp;
 __PACKAGE__->add_columns(
 "path",
     {
	data_type      => 'TEXT',
        is_fs_column   => 1,
        fs_column_path => MyApp->path_to( 'root', 'static', 'photos' ) . ""
     }
);

This tells DBIx::Class that the column "path" is now a Path::Class::File object and we can call Path::Class::File methods on it upon retrieval.

Great! Now let's edit our controller.

The Photos Controller

Edit lib/Controller/Photos.pm to match this file:

    package MyApp::Controller::Photos;

    use strict;
    use warnings;
    use parent 'Catalyst::Controller::HTML::FormFu';
    use DateTime;
    use Imager;
    use MIME::Types;
    use File::MimeInfo ();
    use MyApp;

    __PACKAGE__->mk_accessors(qw(thumbnail_size));

    =head1 NAME

     MyApp::Controller::Photos - Catalyst Controller

    =head1 DESCRIPTION

    Catalyst Controller.

    =head1 METHODS

    =cut

    =head2 index 

      display the photos

    =cut

    sub index : Path : Args(0) {
	    my ( $self, $c ) = @_;

	    my @photos = $c->model('DB::Photos')->all;

	    $c->stash->{photos}   = \@photos;
    	$c->stash->{template} = "photos/index.tt2";

    }

    =head2 get_photos

      set up photo stash

    =cut

    sub get_photos : Chained('/') PathPart('photo') CaptureArgs(1) {
	    my ( $self, $c, $photoid ) = @_;

	    my $photo = $c->model('DB::Photos')->find($photoid);

    	if ( $photo == undef ) {

		    $c->stash->{error_msg} = "No such photo.";

	    }
	    else {

    		$c->stash->{photo} = $photo;

    	}

    }

    =head2 add_photo

      Add a photo to the database

    =cut

    sub add_photo : Path('/photo/add') FormConfig('photos/add.yml') {
	    my ( $self, $c ) = @_;

	    $c->stash->{template} = "photos/add.tt2";
	    my $form = $c->stash->{form};
	    my $mime = MIME::Types->new;

        ## comment out this block if you're not using the Authorization plugin
	    if ( $c->can('check_user_roles') && !$c->check_user_roles('admin') ) {

		    $c->flash->{error_msg} =
		      "You don't have the proper permissions to add photos here";
		    $c->res->redirect( $c->uri_for('/photos') );

	    } 

	    if ( $form->submitted_and_valid ) {

		    my $photo = $c->model('DB::Photos')->create(
		    	{
	    			name     => $form->param('photo_name'),
				    path     => $c->req->upload('photo')->fh,
				    caption  => $form->param('caption'),
				    uploaded => DateTime->now,
				    mime => $mime->mimeTypeOf( $c->req->upload('photo')->basename ),
			    }
		    );

		    $c->stash->{status_msg} = "Successfully uploaded!";
	    	$c->stash->{photo}      = $photo;
    		$c->detach;

    	}

    }

    =head2 generate_thumbnail

      this method generates a thumbnail of a 
      given image

    =cut

    sub generate_thumbnail : Chained('get_photos') PathPart('thumbnail') Args(0) {
	    my ( $self, $c ) = @_;

	


	    my $photo = $c->stash->{photo};
	    my $size  = $self->thumbnail_size;

	    my $mimeinfo = File::MimeInfo->new;

	    my $data = $photo->path->open('r') or die "Error: $!";
	    my $img = Imager->new;
	    $img->read( fh => $data ) or die $img->errstr;
	    my $scaled = $img->scale( xheight => $size );
	    my $out;
	    $scaled->write(
		    type => $mimeinfo->extensions( $photo->mime ),
		    data => \$out
	      )
	      or die $scaled->errstr;
	    $c->res->content_type( $photo->mime );
	    $c->res->content_length( -s $out );
	    $c->res->header( "Content-Disposition" => "inline; filename="
		      . $mimeinfo->extensions( $photo->mime ) );

	    binmode $out;
	    $c->res->body($out);

    }

    =head2 view_image

      hackish method to view
      an image full-size

    =cut

    sub view_image : Chained('get_photos') PathPart('generate') Args(0) {
	    my ( $self, $c ) = @_;

	    my $photo = $c->stash->{photo};

	    my $mimeinfo = File::MimeInfo->new;

	    my $data = $photo->path->open('r') or die "Error: $!";
	    my $img = Imager->new;
	    $img->read( fh => $data ) or die $img->errstr;

	    my $out;
	    $img->write(
		    type => $mimeinfo->extensions( $photo->mime ),
		    data => \$out
	      )
	      or die $img->errstr;
	    $c->res->content_type( $photo->mime );
	    $c->res->content_length( -s $out );
	    $c->res->header( "Content-Disposition" => "inline; filename="
    		  . $mimeinfo->extensions( $photo->mime ) );

	    binmode $out;
	    $c->res->body($out);

    }

    =head2 view_photo

      view an individual photo

    =cut

    sub view_photo : Chained("get_photos") PathPart('view') Args(0) {
	    my ( $self, $c ) = @_;

	    my $photo = $c->stash->{photo};

	    $c->stash->{template} = "photos/view.tt2";

    }

    =head2 delete_photo

      delete a photo or photos

    =cut

    sub delete_photo : Chained("get_photos") PathPart('delete') Args(0) {
    	my ( $self, $c ) = @_;

    	my $photo = $c->stash->{photo};
	    $c->stash->{template} = 'photos/delete.tt2';

	    if ( $c->check_user_roles("admin") ) {

		    if ( $c->req->param('delete') eq 'yes' ) {

			    $photo->delete;
		    	$c->stash->{status_msg} = "Photo " . $photo->photoid . " deleted!";
	    		$c->detach;

    		}

	    }
	    else {

		    $c->flash->{error_msg} =
		      "You don't have proper permissions to delete images.";
	    	$c->res->redirect("/");

    	}

    }

    1;

The view:

root/photos/add.tt2
    <h2>Upload your photos</h2>
    [% UNLESS form.submitted_and_valid %]
     [% form %]

    [% ELSE %]

    <p>Success!</p>
    <p>Here's your image: <img src="[% c.uri_for("/photo/$photo.photoid/thumbnail") %]" />

    [% END %]

root/photos/delete.tt2
    [% UNLESS c.req.param('delete') %]
    <h2>Deleting [% photo.name %]</h2>
    <div id="photo">
    <img src="[% c.uri_for("/photo/$photo.photoid/generate") %]" alt="[% photo.name %]" />
    </div>
    <p>ARE YOU SURE YOU WANT TO DELETE THIS PHOTO?</p>
    <p><a href="[% c.uri_for("/photo/$photo.photoid/delete", { delete=> 'yes' } ) %]">yes</a> I do</p>
    <p><a href="[% c.uri_for("/photos") %]">no</a> I don't</p>
    <div>&laquo;<a href="[% c.uri_for("/photos") %]">back</a>
    [% ELSE %]
    <p>Deleted!</p>
    [% END %]

root/photos/index.tt2
    [% USE table (photos, rows=3) %]
    <h2>Puppy Photos</h2>

    <div id="puppy-list">
    <table>
    [% FOREACH column = table.cols %]
    <tr>
    [% FOREACH photo = column %]
    <td>
      [% IF photo %]
      <a href="[% c.uri_for("/photo/$photo.photoid/view") %]">
         <img src="[% c.uri_for("/photo/$photo.photoid/thumbnail")%]" />
      </a>
      [% ELSE %]
      <img src="[% c.uri_for("/static/photos/blank.gif")%]" width="150" height="100"/>
      [% END %]
    </td>
    [% END %]
    </tr>
    [% END %]
    <table>
    </div>

root/photos/view.tt2
    <h2>[% photo.name %]</h2>
    <div id="photo">
    <img src="[% c.uri_for("/photo/$photo.photoid/generate") %]" alt="[% photo.name %]" />
    </div>
    <p>[% photo.caption %]</p>
    <div>&laquo;<a href="[% c.uri_for("/photos") %]">back</a>

FormFu

And last but not least, the required HTML::FormFu files:

root/forms/photos/add.yml
     ---
     indicator: submit

    elements:
      - type: Text
        name: photo_name
        label: Name this photo
        container_tag: div

      - type: File
        name: photo
        label: Your photo
        container_tag: div

      - type: Textarea
        name: caption
        label: Photo caption
        container_tag: div

      - type: Submit
        name: submit
        value: Upload!

    constraints:
      SingleValue

    filters:
      HTMLScrubber

Voila! There you have it. Go nuts.

AUTHOR

Devin Austin

devin.austin@gmail.com

http://www.codedright.net

LICENSE

This library is free software, you can redistribute it and/or modify it under the same terms as Perl itself.