I’ve recently been working on a nodeapi-based image upload module for 4.7 and introduced it to the dev list. Walkah and Moshe suggested posting the code, so I am putting it here for review.

The image_nodeapi module currently has the following functionality:

- Settings page to determine which nodes should enable image uploads.
- Uses the image functions image_prepare, image_insert and image_update to do all the heavy lifting.
- Allows users to set a caption for the image.
- Default layout of a floated right image with the caption displayed underneath the image on pages.
- Passes the node object to the theme, allowing site admins to over-write presentation of images to fit site specific needs.
- Along these lines, admins could use a case statement in their theme file that would display a different layout for different node types, etc.

nodeapi_image.mysql

-- 
-- Table structure for table `nodeapi_image`
-- 

CREATE TABLE nodeapi_image (
  nid int(10) unsigned NOT NULL default '0',
  caption text,
  PRIMARY KEY  (nid)
) TYPE=MyISAM
/*!40100 DEFAULT CHARACTER SET utf8 */;

nodeapi_image.module

<?php
// $Id: $

/**
 * @file nodeapi_image.module
 */


/**
 * implementation of hook menu
 * 
 * This just adds the css file.
 */
function nodeapi_image_menu($may_cache){
  if (!$may_cache) {
    theme_add_style(drupal_get_path('module', 'nodeapi_image') .'/nodeapi_image.css');
  }
}	

/**
 * Implementation of hook_help().
 */
function nodeapi_image_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      // This description is shown in the listing at admin/modules.
      return t('Add a structured image to stories');
  }
}

/**
 * implementation of hook_form_alter()
 */
function nodeapi_image_form_alter($form_id, &$form) {
  if (!isset($form['type'])) {
    return;
  }
  // Make a copy of the type to shorten up the code
  $type =  $form['type']['#value'];
  $enabled = variable_get('nodeapi_image_'. $type, 0);
  switch ($form_id) {
  	
    // checkbox in the node's content type configuration page. 
    case $type .'_node_settings':
      if(function_exists('_image_check_settings')){
      	_image_check_settings();
        $form['workflow']['nodeapi_image_'. $type] = array(
          '#type' => 'radios',
          '#title' => t('Allow Images'),
          '#default_value' => $enabled,
          '#options' => array(0 => t('Disabled'), 1 => t('Enabled')),
          '#description' => t('Should this node allows users to upload an image?'),
        );
      } else {
        drupal_set_message(t('The image module is not installed. The nodeapi_image module will not function without it.'), 'error');
      }
      break;
    
    // if enabled adjust the form
    case $type .'_node_form':
      if ($enabled == 1 &&  function_exists('_image_check_settings')) {
        _image_check_settings();
        $node = $form['#node'];
        $form['#attributes'] = array("enctype" => "multipart/form-data");
  
        $sizes = _image_get_sizes();
        $form['images']['#tree'] = TRUE;
        $form['images']['_original'] = array('#type' => 'hidden', '#value' => $form['images']['_original']);
        foreach ($sizes as $size) {
          if ($node->new_file) {
            $form['images'][$size['label']] = array('#type' => 'hidden', '#value' => $node->images[$size['label']]);
          }
          else {
            $form['images'][$size['label']] = array('#type' => 'hidden', '#default_value' => $node->images[$size['label']]);
          }
        }

        $form['thumbnail']['#after_build'] = 'nodeapi_image_form_add_thumbnail';
        $form['image'] = array('#type' => 'file', '#title' => t('Image'), '#description' => t('Click "Browse..." to select an image to upload.'), '#weight' => 2.1);
        $form['nodeapi_image_caption'] = array('#type'=> 'textarea', '#title' => t('Caption'), '#default_value' =>  $node->nodeapi_image_caption, '#required' => FALSE, '#cols' => 60, '#rows' => 7, '#weight' => 2.2); 
      }
      break;
  }
}

/**
 * Moved this over directly from image so I could change the weight.
 */
function nodeapi_image_form_add_thumbnail($form_id, $edit) {
  if ($edit['images']['thumbnail']) {
    $node = (object)($edit);
    $form = array('#type' => 'item', '#title' => t('Thumbnail'), '#value' => image_display($node, 'thumbnail'), '#weight' => 2);
  }
  return $form;
}

/**
 * Implementation of hook_nodeapi().
 */
function nodeapi_image_nodeapi(&$node, $op, $teaser, $page) {
  if(variable_get('nodeapi_image_'. $node->type, 0) == 0){
  	return;
  }
  switch ($op) {
  	// Make sure the caption is under 2000 characters
  	// TODO: Consider another approach for limiting the caption?
    case 'validate':
      if (isset($node->nodeapi_image_caption)) {
        $caption_count = strlen(trim($node->nodeapi_image_caption));
        if ($caption_count > 2000) {
          form_set_error('nodeapi_image_caption', t('The caption field must be under 2000 characters long. The current description is %length characters.', array('%length' => $caption_count)));
	    }
      }
      break;
      
    // Insert the caption then prepare the images and insert
    case 'insert':
      db_query("INSERT INTO {nodeapi_image} (nid, nodeapi_image_caption) VALUES (%d, '%s')", $node->nid, $node->nodeapi_image_caption);
      if(function_exists('image_prepare') && function_exists('image_insert')){
          image_prepare($node, 'image');
          if($node->images['_original'] != ''){
            image_insert($node);
          }
      }
      
      break;
      
    // Delete any old captions and insert the new one.
    // Prepare the image, then update
    case 'update':
      db_query('DELETE FROM {nodeapi_image} WHERE nid = %d', $node->nid); 
      db_query("INSERT INTO {nodeapi_image} (nid, nodeapi_image_caption) VALUES (%d, '%s')", $node->nid, $node->nodeapi_image_caption);
      if(function_exists('image_prepare') && function_exists('image_update')){
        image_prepare($node, 'image');
      	//print_r($node, FALSE);
      	if($node->images[_original] != ''){
          image_update($node);
      	}
      }
      
      break; 
    
    // Remove the captions, file definitions and the files for this node
    case 'delete':
      db_query('DELETE FROM {nodeapi_image} WHERE nid = %d', $node->nid);
      foreach ($node->images as $label => $image) {
        file_delete(file_create_path($image));
        db_query("DELETE FROM {files} WHERE filename='%s' AND nid=%d", $label, $node->nid);
      }
      break;
      
    // Get the images and pack them directly in the node reference
    // then get the caption and return that into the node object
    case 'load':
      $result = db_query("SELECT filename, filepath FROM {files} WHERE nid=%d", $node->nid);
      image_load($node);
      $object = db_fetch_object(db_query('SELECT nodeapi_image_caption FROM {nodeapi_image} WHERE nid = %d', $node->nid));
      return array('nodeapi_image_caption' => $object->nodeapi_image_caption);
      
    // Pass the body and teaser objects to the theme again to add the images
    case 'view':
      if(function_exists('image_display')){
        $node->body = theme('nodeapi_image_body', $node);
        if($teaser){
          $node->teaser = theme('nodeapi_image_teaser', $node);
        }
      }
  }
}

/**
 * Theme the teaser.
 * 
 * Override this in template.php to include a case statement if you want different node types to appear differently.
 * If you have additional image sizes you defined in image.module, you can use them by theming this function as well.
 */
function theme_nodeapi_image_teaser($node){
  $request = ($_GET['size']) ? $_GET['size'] : 'thumbnail';
  $request = check_plain($request);
  $info = image_get_info(file_create_path($node->images[$request]));
  $output = '';
  $output .= '<div style="width: '. $info['width'] .'px" class="nodeapi_image_teaser">';
  $output .= l(image_display($node, $request), "node/$node->nid", array(), NULL, NULL, FALSE, TRUE);
  $output .= '</div>'."\n";
  $output .= $node->teaser;
  return $output;
}

/**
 * Theme the body
 * 
 * Override this in template.php to include a case statement if you want different node types to appear differently.
 * If you have additional image sizes you defined in image.module, you can use them by theming this function as well.
 */
function theme_nodeapi_image_body($node){
  $request = ($_GET['size']) ? $_GET['size'] : 'thumbnail';
  $request = check_plain($request);
  $info = image_get_info(file_create_path($node->images[$request]));
  $output = '';
  if($request == 'thumbnail'){
    $output .= '<div style="width: '. $info['width'] .'px" class="nodeapi_image">'. l(image_display($node, $request), "node/$node->nid", NULL, 'size='. urlencode('preview'), NULL, FALSE, TRUE)."\n";
    if($node->nodeapi_image_caption){
      $output .= '  <div style="width: '. ($info['width']-10) .'px" class="nodeapi_image_caption">'. check_markup($node->nodeapi_image_caption, $node->format) .'</div>'."\n";
    }
    $output .= '</div>'."\n";
    $output .= $node->body;
  } else {
    $output .= '<div class="nodeapi_image_backtonode">'. l(t('Back to %l', array('%l' => $node->title)), "node/$node->nid") .'</div>'."\n";
    $output .= '<div class="nodeapi_image_preview">'. image_display($node, $request) .'</div>'."\n";
    $output .= '<div class="nodeapi_image_caption_preview">'. check_markup($node->nodeapi_image_caption, $node->format) .'</div>'."\n";
  }
  return $output;
}


?>

nodeapi_image.css

.nodeapi_image {
  float: right;
  margin-left: 1em;
}
.nodeapi_image_teaser {
  float: right;
  margin-left: 1em;
}
.nodeapi_image_preview {
  margin-bottom: 1em;
}
.nodeapi_image_caption {
  margin-left: 5px;
}
.node {
  clear: both;
}

Enjoy.

Comments

coupet’s picture

Excellent contribs!

Upload more than one image to a node ?

Also why not use image_delete image function?

feature request
----------------------
user uploads into a subdir that is a filsystem-safe version of the user's name. (ie, if user "joe cool" uploads file "test.txt", it ends up in "files/joe_cool/test.txt"). see node:14790

Apache is bandwidth limited, PHP is CPU limited, and MySQL is memory limited.

seanbfuller’s picture

Good point on the image_delete function. I'll take a look at using that instead.

Multiple images seems like it might be more complicated than it seems. Looking in to it.

--------------------
Sean B. Fuller
www.seanbfuller.com

--------------------
Sean B. Fuller
www.seanbfuller.com

coupet’s picture

Creating an array of form elements
http://drupal.org/node/53038

Another example might be a mass-categorization page for lots of posts.

suggest define quantity (select option 1-10) in settings.

Apache is bandwidth limited, PHP is CPU limited, and MySQL is memory limited.

eaton’s picture

The issue is not configuration so much as the fact that image.module is designed to deal with a single image per node, not multiple ones. Adding multi-image support would force he module to re-implement large chunks of image's save/display code.
--
Jeff Eaton | I heart Drupal.

--
Eaton — Partner at Autogram

coupet’s picture

How can we implement this feature?

Limit of one image per node seems restrictive!

Adding a gallery that include multiple images maybe a better approach. Node relationship could be useful. Also, file handling mechanisms might be issues.

Apache is bandwidth limited, PHP is CPU limited, and MySQL is memory limited.

seanbfuller’s picture

This has been discussed on the dev list. From what I can tell there is interest in creating the kind of node to node relationship you are talking about. Unfortunately, it's pretty much out of scope for this little module. Jeff is correct when he says that this code is based on image.module's one-file-to-one-node relationship scheme. (Browse your files database table to see how the relationship currently works.)

--------------------
Sean B. Fuller
www.seanbfuller.com

--------------------
Sean B. Fuller
www.seanbfuller.com

respinos’s picture

In any use-case I can imagine, you'd quickly move from wanting multiple images per node to wanting to make a second gallery with some of those images. That suggests (to me) images-as-node, and a "mini-gallery/scrapbook" node that links to and weights image nodes, and renders them as a gallery.

coupet’s picture

A new approach might be required to handle files, and solution should benefit different type of files including but not limited to audio, image, video, pdf, etc.

Apache is bandwidth limited, PHP is CPU limited, and MySQL is memory limited.

zirafa’s picture

Hi,

I've been working on abstracting out functionality in the playlist relationship API to make ordered lists of any node type. I know there has been some talk of a generalized relationship API, this is NOT it. :) But it will allow for making playlists/feeds of audio, video, image, or could potentially be used to make some weird gallery type thing like described above. I'm working on it in my sandbox if anyone is interested.

Basically it provides:
1) a table
2) some functions to get in and out of the table
3) a theme_sort function to sort the items in a playlist

Here is the thread: http://drupal.org/node/58493

coupet’s picture

Module dependency checker
http://drupal.org/node/54463

Apache is bandwidth limited, PHP is CPU limited, and MySQL is memory limited.

seanbfuller’s picture

In my last email with him, Walkah said that this functionality will be incorporated into image.module in some manner.

--------------------
Sean B. Fuller
www.seanbfuller.com

--------------------
Sean B. Fuller
www.seanbfuller.com

simon rawson’s picture

This is excellent functionality. The sooner this goes into contribs the better!

Just one small bug in that your table definition creates a column named "caption" while your code refers to a column called "nodeapi_image_caption".

Don't know which you want to keep. I think "caption" is neater, personally.

seanbfuller’s picture

After working with this code for a bit, I realized that the theme functions are not passing $node->teaser and $node->body through check_markup().

So in the theme_nodeapi_image_teaser() function change:

    $output .= $node->teaser;
Should be:
    $output .= check_markup($node->teaser, $node->format);

So in the theme_nodeapi_image_body() function change:

    $output .= $node->body;
Should be:
    $output .= check_markup($node->body, $node->format);

Also, one point of funcionality that is not included in the above code is a way to delete images from a node. This would involve a checkbox in formalter and looking to see if it is checked on when the nodapi update case is called.

I'll try to post code for that in the next few days.

--------------------
Sean B. Fuller
www.seanbfuller.com

--------------------
Sean B. Fuller
www.seanbfuller.com

simon rawson’s picture

Are you sure that's right about needing the extra check_markup. I find that using the "filtered html" filter with this causes

tags to be removed (since they are not allowed tags).

Surely only the caption and image code being introduced needs to be checked, not the body/teaser again?

Simon

seanbfuller’s picture

I think you're right, actually. The issue came from some bad html sneaking through. After looking into this over the weekend I realized that the code I was testing against was an old version of stoy.module. As far as I can tell, story.module had a hook_view, but was not doing anything to filter the body or teaser. As a result, some content was getting through. (I did a full walkthrough of the node process and posted it here: http://drupal.org/node/60152 )

The newer 4.7rc3 plugs this hole and based on my current understanding, you are right: I should not be trying to filter the body or the teaser at this level.

Thanks for the catch. All others, please disregard the above comment.

--------------------
Sean B. Fuller
www.seanbfuller.com
www.tractiv.com

--------------------
Sean B. Fuller
www.seanbfuller.com

walkah’s picture

Hey guys, I've added an image.module contrib "image_attach" module ... which is loosely based on this. The big difference is that it creates separate image nodes - which will make it much easier to support multiple nodes.

The module is in image/contrib/image_attach ... currently in CVS HEAD only.

Feedback is welcome.

Cheers
--
James Walker :: Bryght Guy

--
James Walker :: http://walkah.net/

willdashwood’s picture

...multiple images per node or multiple nodes referencing one image? I'm trying any way I can find to be able to attach multiple images to my cck created node and really struggling.

Also, does anyone know how I get the full size image to display when viewing the node? I get thumbnail for the teaser and for the full page view.

coupet’s picture

Slideshow Module
http://drupal.org/node/59469

This module provides basic slideshow capabilities. It transforms images attached to nodes to a slideshow that is enhanced with JavaScript. If the user does not have JavaScript enabled, it degrades to a "regular" slideshow where the "next" button points to the next image and a whole new page is loaded.

----
Darly

MattKelly’s picture

Eventually, I plan on adding to this.
I'd like to be able to have more than one image upload in a form and to be able to control where each file is saved.