Please note: This is an unpublished site and we are making changes - glitches still!!!

Image gallery

A simple flat image gallery is easy to build. Just let the user ftp any images to a directory. Then have PHP read the directory and present the images one way or another. Is this how we will do it? Absolutely not. We will take a much more flexible approach.

Images in the file system, information in the database

We will create a system where the user can upload an image to the file system and add some information that we store in a database table. That information may contain a short image title, a longer description, date and time of the image (when it was taken, for instance), width and height, copyright information, owner information, classification, which album it belongs to and so forth. But let's see what we are trying to achieve:

Gallery functionality

The gallery functionality

  • Mandatory album information one or several albums (family )
  • Optional classifictation: one or several (party, children, summer) (todo)
  • Optional keywords (dog, cat, goat)
  • Mandatory short descriptive title ("Children with the new dog")
  • Optional a longer description
  • Mandatory date information
  • Optional copyright text
  • Optional owner information
  • mandatory path to the image
  • mandatory path to the thumbnail
  • filtering by gallery or classification
  • sorting by date
  • searching by keywords
  • searching by free text search from the title, the keywords and the description

It seem that we need a decent model for the database tables. One table will not do, otherwise we are soon in deep trouble. What do you think about the following idea generated in 10 minutes:

Here we have the photo table in the middle. Tables image_classes and albums are collections tables and image_classes and image_albums are lists that join the tables together. This is how we can make an photo belong to several classes and several albums. 

This may be a too simple model for the task but it is quick and easy to build. The tables creation script is here.

Thumbnails, images and their naming

At some point we must decide how we name the images, what size they are and where they are stored. In this example the images have no size restrictions but thumbnail size has been pre-defined and stored to the configuration file. Now that we have a 600px wide working area we must scale the thumbnails accoringly. In the example gallery a width of 180px has been used which makes it possible to have three thumbnails on each row.

The naming has been kept simple. When an image is uploaded we get the Unix timestamp (seconds since the first day in 1970) which should be unique enough. Both the full-sized image and the thumbnail have the same name but the thumbnails are stored in another directory (once again defined in the config file).

If, for one reason or another, we need to change the thumbnail size we will create a small script that recreates all thumbnails resizing them.

Administering the gallery

This time we will create the administration part first.  We will use the good old trinity (browse, edit, update) spiced up with something extra. The DataBrowser part should be easy already:

<?php
require_once "lib/DataBrowser.class.php";
class gallery_list_module extends CmsModule {
    var $template = 'DataBrowser.tpl';
    var $gallery_table = "gallery";
    var $gallery_albums_table = "gallery_albums";
    var $gallery_classes = "gallery_classes";
    var $images_classes_table = "images_classes";
    var $images_albums_table = "images_albums";
    var $gallery_dir = "/gallery/";
    var $thumbnail_dir = "/gallery/tn/";
    var $gallery_html_dir = "/gallery/";
    var $thubnail_html_dir = "/gallery/tn/";
    var $maxwidth = 500;
 
    function init(){
        parent::init();
        $this->gallery_dir = $GLOBALS['cms_config']['gallery_dir'];
        // add other settings here - left out to save space
    }
 
    /**
* Resize the image to fit the browse screen
*/
function cb_image($path,$obj){ $fullpath = $obj->gallery_dir .$path; $imgs = getimagesize($fullpath); $w = $imgs[0]; $dimstring = ""; // resize only if bigger than we wish if ($w > $obj->maxwidth){} { $h = $imgs[1]; $proportion = $w/$h; $newwidth = $obj->maxwidth; $newheight= $newwidth / $proportion; $dimstring ="widh='$newwidth' height='$newheight'"; } $htmlpath = $obj->gallery_html_dir.$path; // return an image tag with the size information $imagestr = "<img src='$htmlpath' alt='' $dimstring />"; return $imagestr; }   function fetch(){ $dataBrowser = new DataBrowser($this); // add gallery information later if needed $condition = ""; $query = "SELECT id,imagedatetime,title, keywords, imagepath " . "FROM $this->gallery_table $condition"; $dataBrowser->setQuery($query); $dataBrowser->setTitle("Gallery list"); $dataBrowser->addColumn("id",10,"text",0); $dataBrowser->addColumn("Date","40","text",1); $dataBrowser->addColumn("Title","200","text",1); $dataBrowser->addColumn("Keywords","100","text",1); $dataBrowser->addColumn("Image","100","text",2,'cb_image',$this); // quickie: create std edit/update/delete with // 'act' set to 'gallery_edit_image' $dataBrowser->createStandardLinks('gallery_edit_image'); if (isset($_GET['id'])){ $dataBrowser->setActiveRecord($_GET['id']); } $dataBrowser->assignSmarty(); // add hidden fields needed for links $dataBrowser->addHiddenFields("act"); return $dataBrowser->fetchTemplate("DataBrowser.tpl"); } } ?>

Okay. Here you can see that adding standard edit/delete/insert links has been created a shortcut. Nice, eh? No we need to create the edit form which is quite a good deal trickier.

Gallery upload and edit

The 'insert' part of the edit form must have a file upload field. It is in the classes ready-made, no problem. Another, bigger probem is that we would like to see two select fields like this:

 

This is absolutely something that does not belong to a normal HTML form and needs quite a bit of tinkering. How about adding just a few lines of code to achieve the functionality? Yes, the database handling classes support it now.

The module code

<?php
require_once "UpdateFormObj.class.php";
class gallery_edit_image_module extends CmsModule{
    var $template      = 'UpdateFormObj.tpl';
    var $gallery_table = "gallery";
    var $gallery_albums_table = "gallery_albums";
    var $gallery_classes      = "gallery_classes";
    var $images_classes_table = "images_classes";
    var $images_albums_table  = "images_albums";
    var $gallery_path   = "/gallery/";
    var $thumbnail_path = "/gallery/tn/";
 
    function init(){
        parent::init();
        // save the referring page at init
        saveReferer();
    }
 
    function fetch(){
        $form = new UpdateForm();
        $editact = $_GET['editact'];
        // first get data for form if needed (edit & delete)
        if ($editact=='edit' || $editact=='delete'){
          $id =$_GET['id'];
          $query = "SELECT title,description,keywords,copyright," .
                   "owner,imagedatetime,imagepath " .
                   "FROM $this->gallery_table " .
                   "WHERE id=$id";
          $result = mysql_query($query) or die(mysql_error().":".$query);
          $obj = mysql_fetch_object($result);
          // use the double dollar hack to get variables quickly
          foreach ($obj as $key=>$value)
              ${$key}=$value;
          if ($editact=='delete'){
            $form->setReadOnly();
            $form->setTitle("Delete existing image");
            $form->setSubmitText("Delete image");
            $form->setAction(
"index.php?act=gallery_update_image&saveact=delete"
); } else { $form->setTitle("Edit image information"); $form->setSubmitText("Save changes"); $form->setAction("index.php?act=gallery_update_image&saveact=update"); } } else { $form->setTitle("Add new image"); $form->setSubmitText("Add image"); $form->setAction("index.php?act=gallery_update_image&saveact=insert"); $title = ''; $description = ''; $keywords = ''; $copyright = ''; $owner= ''; $imagedatetime = date("%Y-%m-d"); } // -------------- now build the form ------------------------------- $form->addSubTitle("Mandatory fields"); $form->addField("imagedatetime",$imagedatetime,"Date","Date"); $form->addField("title",$title,"Title","Text"); $form->addHiddenField("id",$id); //------------------------ add preview when it is available -------- if ($editact=='edit' || $editact=='delete'){ // show thumbnail sho that the user knows what he is doing $form->addField("preview"
, "<img src='$this->thumbnail_path$imagepath' />", "Preview","plaintext"); } //--------------now the tricky part with the two list boxes --------- $form->addSubTitle("Double click item in 'Available albums' to " . "add and in 'Selected albums to remove"); // create the field and get it's ID because we will need it $albumFieldId = $form->addField('','','Albums','doubleselect'); $src_query = "SELECT id,name FROM gallery_albums ORDER BY id"; $src_array = $form->getSelectOptionArray($src_query); // if in edit we will possibly have a list of albums ----------------- if ($editact=="edit" || $editact=="delete"){ $dst_query ="SELECT album_id,name " . "FROM images_albums, gallery_albums " . "WHERE photo_id=$id " . "AND gallery_albums.id=images_albums.album_id"; $dst_array = $form->getSelectOptionArray($dst_query); } else { // insert - always show empty destination box $dst_array = array(); } // now we have the arrays, lets have the class build the boxes $form->setDoubleSelectionArrays($albumFieldId,$src_array,
$dst_array
,'value'); // set the name for the arrays in the form, note the "[]" $form->setDoubleSelectionNames($albumFieldId,'available_albums[]',
'selected_albums[]'
); // and finally the titles $form->setDoubleSelectionTitles($albumFieldId,'Available albums',
'Selected albums'
);   if ($editact=='insert'){ // upload field for insertion only $form->addField("image","","Upload image","File"); } // and the rest of the fields here $form->addSubTitle("The rest is optional, fill in if necessary"); $form->addField("description",$description,"Description","Textarea"); $form->addField("keywords",$keywords,"Keywords","Text"); $form->addField("copyright",$copyright,"Copyright","Text"); $form->addField("owner",$owner,"Owner","Text"); $form->setSmarty(new Smarty()); return $form->fetchTemplate($this->template); } } ?>

This module is horrendously long - more than 100 lines of code with comments. As you can quite a bit of the lines is taken by the "double select box". Imagine how much code you would have needed to add without the class, though.

A couple of things are worth mentioning.  The getSelectionOptionArray() function builds a simple array that contains the key/value pairs to be used in the select boxes. These are later checked and rebuilt by setDoubleSelectionArrays() that removes duplicates (the already selected items)  from the two boxes. Note that the selection names end in brackets [] which is mandatory because that is how the receiving script gets the result as an array.

That is a bit tricky but a lot easier than doing the whole thing by hand.

The database update

The update routine has quite a few things to perform:

  • Handle image upload, possible resizing and moving to the correct directory
  • Thumbnail creation
  • Updating the images-to-albums table
  • Updating the database
  • Removing images when deleting
  • Recovering from all imaginable errors and leaving a clean state
  • Note: We will restrict the image types to jpg only

We will leave resizing the actual image to a later date but will build the rest. And here we go (sorry about highlighting, the tool I used crashed )

<?php
require_once "DBUpdate.class.php";
class gallery_update_image_module extends CmsModule{
    var $gallery_dir = "";
    var $thumbnail_dir = "";
    var $no_gallery_dir_alert = "No Gallery Dir defined ";
    var $no_thumbnail_dir_alert = "No Thumbnail Dir defined ";
    var $thumbnail_width = "200";
    var $gallery_table = 'gallery';
    function init(){
        parent::init();
        // TODO: add more checks here
        $this->gallery_dir     = $GLOBALS['cms_config']['gallery_dir'];
        $this->thumbnail_dir   = $GLOBALS['cms_config']['thumbnail_dir'];
        $this->thumbnail_width = $GLOBALS['cms_config']['thumbnail_width'];
        if ($this->gallery_dir ==""){
            $ref = getReferer();
            redirect($ref."_alert=$this->no_gallery_dir_alert");
        }
        if ($this->thumbnail_dir ==""){
            $ref = getReferer();
            redirect($ref."_alert=$this->no_thumbnail_dir_alert");
        }
    }
    /**
* Something goes wrong, let's go back
*/
function fail($message){ $ref = getReferer(); redirect("$ref&amp;_alert=$message!"); } // successful redirect function success($message){ $ref = getReferer($message=''); if ($message != "") redirect("$ref&amp;_alert=$message!"); else redirect($ref); } /**
* Create thumbnail. Always return true, die when something fails
*/
function create_thumbnail($filename){ $fullimagepath = $this->gallery_dir.$filename; if (!file_exists($fullimagepath)) die ("image: $fullimagepath not found"); $img = imagecreatefromjpeg($fullimagepath); $orgwidth = imagesx($img); $orgheight= imagesy($img); $proportion = $orgwidth / $orgheight; $thumbnail_height = $this->thumbnail_width / $proportion; $tn = imagecreatetruecolor($this->thumbnail_width,$thumbnail_height); imagecopyresampled($tn,$img,0,0,0,0, $this->thumbnail_width,$thumbnail_height, $orgwidth, $orgheight); $thumbnailpath = $this->thumbnail_dir.$filename; $result = imagejpeg($tn,$thumbnailpath); if (!$result){ return false; @unlink ($this->gallery_dir.$filename); die("error creating: ".$thumbnailpath); //redirect("$ref&;_alert=Could not create thumbnail ($)'"); } return true; }   /**
* Handle the upload. We assume that the image field name is "image"
*/
function handleUpload(){ $upload_dir = $this->gallery_dir; $upload_name = basename($_FILES['image']['name']); if ($upload_name==''){ $this->fail("No file uploaded"); } $extension = strtolower(substr($upload_name,-3)); if ($extension != 'jpg') { $this->fail('Illegal file type ($extension), only jpg accepted'); } // create file name from Unix time - good enough for us $filename = time(). "." .$extension; $full_filename = $upload_dir."/".$filename; //die("full_filename=$full_filename"); if (move_uploaded_file($_FILES['image']['tmp_name'],$full_filename)){ // ok, create thumbnail if (!$this->create_thumbnail($filename)){ @unlink($full_filename); return false; } // worked, return true return $filename; } else { $this->fail("Upload failed"); } }   /**
*
*/
function addAlbumReferences($id){ if (isset($_POST['selected_albums'])){ // add all selected albums foreach($_POST['selected_albums'] as $album){ mysql_query("INSERT INTO images_albums(album_id,photo_id) " . "VALUES($album,$id)"); if (mysql_error()) echo mysql_error(); } } }   /**
* Handle the whole image update/insert process.
*
* Quite a few things can go wrong here and a lot of
* error handling is needed.
*/
function fetch(){ $ref = getreferer(); $dbu = new DBUpdate(); $dbu->setTableName($this->gallery_table); $textfields= explode(',',"title,description,keywords,copyright,owner"); // batch add all text fields foreach ($textfields as $field) $dbu->addField($field,mysql_real_escape_string($_POST[$field]),'text');   $dbu->addField("imagedatetime",$_POST['imagedatetime'],"date");   $saveact = $_POST['saveact']; if ($saveact=='insert'){ // upload first $filename = $this->handleUpload(); if (!$filename){ // we got false, something wrong $this->fail("Upload failed"); } $dbu->addField('imagepath',$filename); $dbu->setSaveAct('insert'); $result = $dbu->commitQuery(); if (!$result){ // something went wrong, delete images immediately unlink($this->thumbnail_dir.$filename); unlink($this->gallery_dir.$filename); $this->fail("Error inserting image to database"); } else{ $id = $dbu->getInsertID(); $this->addAlbumReferences($id); redirect("$ref&amp;Image added successfully"); }   } else if ($saveact=='update') { $id = $_POST['id']; $dbu->setSaveAct('update'); $dbu->setCondition("id=$id"); $result = $dbu->commitQuery(); // delete all album_image references @mysql_query("DELETE FROM images_albums WHERE photo_id=$id"); // and add the current ones $this->addAlbumReferences($id); if (!$result){ die(mysql_error(). " : ".$dbu->query); $this->fail("Error updating image"); exit; } else{ $this->success("Image updated successfully"); exit; } } else if ($saveact=='delete') { $id = $_POST['id']; // delete all album references @mysql_query("DELETE FROM image_albums WHERE photo_id=$id"); // find image, remove image file and thumbnail file $filename =getSQLvalue("SELECT imagepath FROM $this->gallery_table ". "WHERE id=$id "); unlink ($this->thumbnail_dir.$filename); unlink ($this->gallery_dir.$filename); // delete record $dbu->setSaveAct('delete'); $dbu->setCondition("id=$id"); $result = $dbu->commitQuery(); if (!$result){ die($dbu->getError()."|".$dbu->query); $this->fail("Error removing image"); exit; } else{ $this->success("Image removedsuccessfully"); exit; }   } } } ?>

Recreating and resizing thumbnails

This simple script walks through the image gallery. Please notice that we are using hard-coded default paths in the example to save screen space. You must add a few lines in the init() function to get the correct paths set in the configuration file or change the default paths if they do not match your directory settings.

<?php
class gallery_create_thumbnails_module extends CmsModule{
    // defaults, please add correct code in the init()
    var $gallery_path = "../gallery/";
    var $thumbnail_path= "../gallery/tn/";
    var $max_width  = 200;
    var $max_height  = 150;
 
    function init(){
        //if ($this->vars['gallery'])
        parent::init();
        if ($GLOBALS['cms_config']['gallery_thumbnail_width'] >0)
            $this->max_width =
                 $GLOBALS['cms_config']['gallery_thumbnail_width'];
        // fetch paths from config, please add code here
    }
 
    function fetch(){
        // let's use a directory based approach this time
        $handle = opendir($this->gallery_path);
        $counter = 0;
        while (false !== ($file = readdir($handle))) {
            if (strpos(strtolower($file),'.jpg')=== false)
                      continue;
            $counter++;
               // get src image and calculate proportion
            $src = imagecreatefromjpeg($this->gallery_path.$file);
            $width = imagesx($src);
            $height= imagesy($src);
            $proportion = $width / $height;
            if ($width >= $height){
                $tn_width = $this->max_width;
                $tn_height= $tn_width / $proportion;
            } else {
                $tn_height = $this->max_height;
                $tn_width  = $tn_height*$proportion;
            }
            // create empty destination image resource
            $dst = imagecreatetruecolor($tn_width,$tn_height);
            // resize to the destination resource
            imagecopyresampled($dst,$src, 0,0, 0,0,
                               $tn_width,$tn_height,
                               $width,$height);
            // create the image file
            imagejpeg($dst,$this->thumbnail_path."/".$file);
       }
       return "<pre>Ok, we created $counter thumbnails.</pre>";
    }
}
?>

Things to do

There is quite a lot to do. If we need the classification, for instance, we must make a few changes. We could also create a small script that checks whether an image exists in the gallery table and deletes it if it is an orphan. There are more things that we might need in the future.