Contact

FUxCon 2013


Pictures

CakePHP

Our CakePHP implementation treats pictures and derived formats a bit like stepchildren. To keep the implementation small, pictures are not represented by model classes but are rather uploaded into the file system directly. Also, we only support JPEG as upload format.

The bad thing about this simplistic approach is that in order to check if a project has an uploaded picture, we have to check the file system, a potentially slow operation. Also, we save all pictures in a single directory, a thing you wouldn’t want to do if you have more than a few thousand pictures because then file system access will become even slower.

Controllers don’t do anything special to handle pictures.

The views use a special helper in file app/View/Helper/ThumbnailHelper.php to create derived images in the fly. These are then saved in the file system. Through a proper rewrite configuration, the web server delivers these cached derivatives directly, without going through the process of re-creating the derivatives again.

The helper uses special directories to save the derivatives:

CakePHP thumbnails

The same helper is used in the edit forms to show an image thumbnail of an already existing picture:

<div class="pull-right thumbnail">
	<?php
  echo $this->Thumbnail->render('project/' . 1 . '.jpg', array(
    'width' => 160, 
    'height' => 160, 
    'resizeOption' => 'auto',
  )); 
  ?>
</div>
<?php
echo $this->Form->input('picture', array(
  'type' => 'file',
  'id' => 'picture-field'
));
?>

The handling of file uploads is done in the Project model class in the before- and afterSave callbacks:

<?php
/**
 * Before saving the project, check an uploaded picture
 */
public function beforeSave($options = array()) {
  if (!isset($this->data[$this->alias]['picture'])) {
    return TRUE;
  }
  $file = $this->data[$this->alias]['picture'];
  if ($file['error'] === UPLOAD_ERR_NO_FILE) {
    return TRUE;
  }
  if ($file['error'] !== UPLOAD_ERR_OK) {
    return FALSE;
  }
  if (strpos($file['type'], 'image/jpeg') !== 0) {
    return FALSE;
  }
  return TRUE;
}

/**
 * After saving the project, save an uploaded picture
 */
public function afterSave($created) {
  if (!isset($this->data[$this->alias]['picture'])) {
    return;
  }
  $file = $this->data[$this->alias]['picture'];

  if ($file['error'] === UPLOAD_ERR_OK) {
    if (!move_uploaded_file($file['tmp_name'], IMAGES . 'project' . DS . $this->id . '.jpg')) {
      throw new Exception("Failed to move file: " 
        . posix_strerror(posix_get_last_error()));
    }
  }
}

A picture upload is checked before saving a project and actually stored after saving when we have the project ID.

Django

In our Django implementation, pictures and derived picture formats are handled by the django-imagekit extension. There would have been some alternatives.

After installing with pip and registering in INSTALLED_APPS in the usual way, we can add special fields to our project model in file projects/models.py:

class Project(models.Model):
  photo = models.ImageField(upload_to='project', blank=True, null=True)
  
  # derived images: https://github.com/matthewwithanm/django-imagekit#readme
  photo_thumbnail = ImageSpecField(source='photo',
    processors=[ResizeToFill(100, 100)],
    format='JPEG',
    options={'quality': 60}
  )
  photo_medium = ImageSpecField(source='photo',
    processors=[ResizeToFill(200, 200)],
    format='JPEG',
    options={'quality': 60}
  )
  photo_big = ImageSpecField(source='photo',
    processors=[ResizeToFill(380, 380)],
    format='JPEG',
    options={'quality': 60}
  )

  def admin_thumbnail(self):
    if self.photo:
      return '<img src="%s">' % self.photo_thumbnail.url
  admin_thumbnail.allow_tags = True
  

Derived images are special fields that are filled during project save. These fields are not stored in the database. Only the path to the original photo is stored in a column in the database.

In addition to the fields, we also add a method that provides the admin interface with a thumbnail for the project list view.

Once these fields are added to the model, they can be accessed from templates like any other image field. Here is an excerpt from our project detail page in file templates/projects/detail.html:

{% if project.photo %}
<p class="thumbnail picture-content">
  <img src="{{ project.photo_big.url }}">
</p>
{% endif %}

Drupal

Pictures are a built-in field type in Drupal. They can be added to a content type in the admin interface:

Drupal picture field

There are quite some settings which can be adjusted for image fields. Here are some that we tweaked:

Drupal picture field

Aside from the image field, the derived picture styles must be configured:

Drupal image styles

Once these are configured, We still have to determine which derivative to show on project detail pages and in the project lists. The medium derivative is chosen for project detail pages:

Drupal picture detail style

The thumbnail size for the project list is chosen in the configuration of the list:

Drupal picture list style

Derived images can now be used in templates. Here is an excerpt from file sites/all/themes/fuxcon2013/node–project.tpl.php:

<div class="thumbnail picture-content">
  <?php echo render($content['field_picture']); ?>
</div>

The image style is chosen by Drupal according to context: medium for project detail pages, thumbnail for project lists.

Symfony

Our picture handling in Symfony mimics the way we have used in our CakePHP implementation. There, we have already mentioned that this approach would need some refinements if used on a production site.

Our model has some barebones methods for getting and setting pictures and for moving them to their proper place after update:

<?php
namespace FUxCon2013\ProjectsBundle\Entity;

class Project
{
  
  private function getPicturePath()
  { return __DIR__ . '/../../../../web/images/project/' . $this->getId() . '.jpg'; }

  public function getPicture()
  {
      if (empty($this->picture)) {
          return null;
      }
      return new \Symfony\Component\HttpFoundation\File\File(
          $this->getPicturePath(), /*checkPath*/false
      );
  }

  public function setPicture($picture)
  {
      $mimeType = $picture->getMimeType();
      if (!in_array($mimeType, array('image/jpeg'))) {
          throw Exception("You may only have images of type JPEG");
      }
      $this->picture = $picture;
  }

  public function processPicture()
  {
      if (! ($this->picture instanceof UploadedFile) ) {
          return false;
      }

      $file = $this->getPicturePath();

      $this->picture->move(
          pathinfo($file, PATHINFO_DIRNAME),
          pathinfo($file, PATHINFO_BASENAME)
      );
      return true;
  }
}

in Symfony, the construct corresponding to a view helper is an extension to the Twig template language. For our thumbnails, we have implemented such an extension. This provides us with a new function that we can then use in templates. Here is an excerpt from src/FUxCon2013/ProjectsBundle/Resources/views/Project/index.html.twig:

<a class="thumbnail" href="{{ path('project_show', { 'id': project.id } ) }}">
    <img src="{{ thumbnail([ '/images/project/', project.id, '.jpg' ] | join, '200x200') }}">
</a>

The function definition lives in file src/FUxCon2013/ProjectsBundle/Twig/Extension/FUxCon2013Extension.php. We have already shown it when introducing views in Symfony.

This is the code we use for creating a new project and saving the image:

<?php
namespace FUxCon2013\ProjectsBundle\Controller;

class ProjectController
{
  /**
   * Creates a new Project entity.
   *
   * @Route("/", name="project_create")
   * @Method("POST")
   * @Template("FUxCon2013ProjectsBundle:Project:new.html.twig")
   */
  public function createAction(Request $request)
  {
      $project = new Project();
      $form = $this->createForm(new ProjectType(), $project);
      $form->bind($request);

      if ($form->isValid()) {
          $em = $this->getDoctrine()->getManager();
          $em->persist($project);
          $em->flush();

          // Picture processed after saving. We need the project ID
          if ($project->getPicture() && $project->processPicture()) {
              $this->flash('The project has been saved', 'success');
          }
      }
      // some more processing and error handling
  }
}


comments powered by Disqus