'admin/settings/node_images',
'title' => t('Node images'),
'description' => t('Control how to upload node images.'),
'callback' => 'drupal_get_form',
'callback arguments' => array('node_images_admin_settings'),
'access' => $admin
);
$items[] = array(
'path' => 'node_images/js',
'callback' => '_node_images_js',
'access' => $access,
'type' => MENU_CALLBACK
);
} else {
if (arg(0) == 'node' && is_numeric(arg(1))) {
$node = node_load(arg(1));
if ($node->nid && variable_get('node_images_position_'.$node->type, 'hide') != 'hide') {
$items[] = array(
'path' => 'node/' . arg(1) . '/images',
'title' => t('Images'),
'callback' => '_node_images_edit_form',
'callback arguments' => array($node),
'access' => $access && node_access('update', $node),
'type' => MENU_LOCAL_TASK,
'weight' => 2
);
$items[] = array(
'path' => 'node/' . arg(1) . '/image_gallery',
'title' => t('Gallery'),
'callback' => '_node_images_gallery',
'callback arguments' => array($node),
'access' => node_access('view', $node),
'type' => MENU_CALLBACK
);
}
}
}
return $items;
}
/**
* implementation of hook_form_alter()
*/
function node_images_form_alter($form_id, &$form) {
$type = $form['#node_type']->type;
if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
// radio button in the node's content type configuration page
_node_images_check_settings();
$form['node_images'] = array(
'#type' => 'fieldset',
'#title' => t('Node images'),
'#collapsible' => TRUE,
);
$form['node_images']['node_images_position'] = array(
'#type' => 'radios',
'#title' => t('Position'),
'#default_value' => variable_get('node_images_position_'.$type, 'hide'),
'#options' => array('hide' => t('Do not show'), 'above' => t('Show above node body'), 'below' => t('Show below node body'),
'node_template' => t('Manually set in node template by variable $node->node_images')),
'#description' => t('The position of images in the node view.'),
);
$form['node_images']['node_images_gallery_link'] = array(
'#type' => 'radios',
'#title' => t('Link to image gallery'),
'#default_value' => variable_get('node_images_gallery_link_'.$type, TRUE),
'#options' => array(t('Do not show'), t('Show')),
'#description' => t('Choose whether to show or not the link to the image gallery.'),
);
$form['node_images']['node_images_teaser_images'] = array(
'#type' => 'textfield',
'#title' => t('Number of images in node teaser'),
'#default_value' => variable_get('node_images_teaser_images_'.$type, 2),
'#size' => 5,
'#maxlength' => 2,
'#description' => t('The maximum number of images to show in the node teaser. 0 will not show images. Leave blank to show all images.'),
);
$form['node_images']['node_images_body_images'] = array(
'#type' => 'textfield',
'#title' => t('Number of images in node body'),
'#size' => 5,
'#maxlength' => 2,
'#default_value' => variable_get('node_images_body_images_'.$type, NULL),
'#description' => t('The maximum number of images to show in the node body. 0 will not show images. Leave blank to show all images.'),
);
$form['node_images']['node_images_teaser_format'] = array(
'#type' => 'radios',
'#title' => t('Image format in node teaser'),
'#default_value' => variable_get('node_images_teaser_format_'.$type, 'thumbs'),
'#options' => array('thumbs' => t('Thumbnails'), 'fullsize' => t('Full size images')),
'#description' => t('Image format in node teaser.'),
);
$form['node_images']['node_images_body_format'] = array(
'#type' => 'radios',
'#title' => t('Image format in node body'),
'#default_value' => variable_get('node_images_body_format_'.$type, 'thumbs'),
'#options' => array('thumbs' => t('Thumbnails'), 'fullsize' => t('Full size images')),
'#description' => t('Image format in node body.'),
);
$form['submit']['#weight'] = 30;
$form['delete']['#weight'] = 31;
$form['reset']['#weight'] = 32;
}
}
/**
* Implementation of hook_nodeapi().
*/
function node_images_nodeapi(&$node, $op, $teaser, $page) {
// test if images are allowed for this node
if ($op == 'load' || $op == 'view') {
if (variable_get('node_images_position_'.$node->type, 'hide') == 'hide') return;
// fire an additional hook for the specific node type
// i.e. node images might be a pro feature, so only subscribed users can view them
// if the hook is not present in the node type module, images are loaded by default
$show = module_invoke($node->type, 'node_images', $op, $node);
if ($show === FALSE) return;
}
switch ($op) {
case 'load':
// load node images for the current node
$sql = db_query('SELECT * FROM {node_images} WHERE nid=%d AND status=1 ORDER BY weight', $node->nid);
while ($r = db_fetch_object($sql)) {
$node->node_images[$r->id] = $r;
}
break;
case 'insert':
if (user_access('create node images') && node_access('update', $node) &&
variable_get('node_images_position_'.$node->type, 'hide') != 'hide') {
drupal_set_message(t('Click the !images tab to add images to this node.',
array('!images' => l(t('images'), 'node/'.$node->nid.'/images'))));
}
break;
case 'delete':
// Delete image and thumbnail files
$sql = db_query('SELECT filepath, thumbpath FROM {node_images} WHERE nid=%d', $node->nid);
while ($r = db_fetch_object($sql)) {
file_delete($r->filepath);
file_delete($r->thumbpath);
}
// Delete all images associated with the node
db_query('DELETE FROM {node_images} WHERE nid=%d', $node->nid);
break;
case 'view':
if (empty($node->node_images)) return;
// search for a themed view for the current node type
$nodetype_function = 'theme_'.$node->type.'_node_images_view';
if (function_exists($nodetype_function)) {
$output = $nodetype_function($node, $teaser, $page);
}
else {
// use the default theme function
$output = theme('node_images_view', $node, $teaser, $page);
}
$node->node_images = $output;
$output = '
'.$output.'
';
$position = variable_get('node_images_position_'.$node->type, 'hide');
switch ($position) {
case 'node_template':
case 'hide':
break;
case 'above':
$node->content['node_images'] = array('#value' => $output, '#weight' => -1);
break;
default:
$node->content['node_images'] = array('#value' => $output, '#weight' => 1);
break;
}
return $node;
}
}
/**
* Implementation of hook_link().
*/
function node_images_link($type, $node = null, $teaser = false) {
$links = array();
if ($type == 'node' && $node->nid && variable_get('node_images_position_'.$node->type, 'hide') != 'hide') {
if (node_access('update', $node) && user_access('create node images')) {
$links['node_images_edit'] = array(
'title' => t('Edit node images'),
'href' => "node/$node->nid/images",
);
}
if (count($node->node_images) && variable_get('node_images_gallery_link_'.$node->type, TRUE)) {
$links['node_images_gallery'] = array(
'title' => t('Open the image gallery'),
'href' => "node/$node->nid/image_gallery",
);
}
}
return $links;
}
/**
* Implementation of hook_user().
*/
function node_images_user($type, &$edit, &$user, $category = NULL) {
switch ($type) {
case 'delete':
// Set uid=0 for images uploaded by the deleted user
db_query('UPDATE {node_images} SET uid=0 WHERE uid=%d', $user->uid);
}
}
/**
* Implementation of hook_file_download().
* Find out if an image is accessible when download method is set to private
*/
function node_images_file_download($file) {
$path = file_create_path($file);
$result = db_query("SELECT * FROM {node_images} WHERE filepath='%s' OR thumbpath='%s'", $path, $path);
if ($file = db_fetch_object($result)) {
$node = node_load($file->nid);
if (node_access('view', $node)) {
if ($path == $file->thumbpath) {
// update header info if thumb is requested
$name = mime_header_encode(basename($file->thumbpath));
$size = $file->thumbsize;
}
else {
$name = mime_header_encode($file->filename);
$size = $file->filesize;
}
$type = mime_header_encode($file->filemime);
return array(
'Content-Type: '. $type .'; name='. $name,
'Content-Length: '. $size,
'Cache-Control: private'
);
}
else {
return -1;
}
}
}
/************************************************************
* Node edit functions
************************************************************/
/**
* Display the page to edit and upload node images.
*
* @param $node
* the current node
* @return
* the node images page.
*/
function _node_images_edit_form($node) {
global $user;
$output = '
';
// Generate image list
$sql = db_query('SELECT * FROM {node_images} WHERE nid=%d ORDER BY weight', $node->nid);
if (db_num_rows($sql)>0) $output .= drupal_get_form('_node_images_list', $node, $sql);
// test if this node can accept new images
// i.e. there might be limits to the number of uploaded images for certain node types or user roles
// if the hook is not present in the node type module, upload is enabled by default
$upload = module_invoke($node->type, 'node_images', 'upload', $node);
if ($upload !== FALSE) {
$output .= drupal_get_form('_node_images_edit_form_upload', $node);
}
$output .= '
';
drupal_set_title(check_plain($node->title));
return $output;
}
function _node_images_edit_form_upload($node) {
$path = variable_get('node_images_path', 'files');
_node_images_check_directory(NULL, $path);
$extensions = variable_get('node_images_extensions', 'jpg jpeg gif png');
drupal_add_js('misc/progress.js');
drupal_add_js('misc/upload.js');
// Generate upload form
$form['new'] = array(
'#type'=>'fieldset',
'#title'=>t('Upload a new image'),
);
$form['new']['description'] = array(
'#type' => 'textfield',
'#title' => t('Description'),
'#size' => 50,
'#maxlength' => 255,
'#default_value' => $edit['new']['description'],
'#description' => t('Enter a description for the new image, max 255 chars.'),
'#prefix' => '
';
return $output;
}
/************************************************************
* Helper functions - admin settings
************************************************************/
/**
* Menu callback; admin settings page.
*/
function node_images_admin_settings() {
_node_images_check_settings();
$form['node_images_path'] = array(
'#type' => 'textfield',
'#title' => t('Node images path'),
'#default_value' => variable_get('node_images_path', 'files'),
'#maxlength' => 255,
'#description' => t('A file system path where the node images will be stored. This directory has to exist and be writable by Drupal. You can use the following variables: %uid, %username'),
'#after_build' => array('_node_images_check_directory'),
);
$form['node_images_thumb_resolution'] = array(
'#type' => 'textfield',
'#title' => t('Resolution for thumbnails'),
'#default_value' => variable_get('node_images_thumb_resolution', '100x100'),
'#size' => 15,
'#maxlength' => 7,
'#description' => t('The thumbnail size expressed as WIDTHxHEIGHT (e.g. 100x75).'),
);
$form['node_images_thumb_sizemethod'] = array(
'#type' => 'select',
'#title' => t('Thumbnail Generation Method'),
'#default_value' => variable_get('node_images_thumb_sizemethod', 'fitLongestSide'),
'#description' => t('This determines the way node_images calculates the size and aspect ratio of thumbnails. Fit to width or Fit to height scale the thumbnail proportionally until it is the width or height specified above, maintaining the aspect ratio of the original and disregarding the other thumbnail dimension. Useful if all thumbnails must be a certain width but height can vary, or all must be a certain height but width can vary. Fit to longest side is the TYPICAL, DEFAULT BEHAVIOR -- it scales the whole thing down so that whichever side is longer fits into the thumbnail "box." Original aspect ratio is maintained. Fit to shortest side is similar but matches whichever side is SHORTER to its corresponding thumbnail dimension. With this, there can be some spillover so the whole thing might not fit in the thumbnail "box" dimensions. Aspect ratio maintained. Crop and fit exactly is the gem; it fits the thumbnail to be EXACTLY the size specified above, cropping the longer side (centering the image in the crop rectangle).'),
'#options' => array(
'fitToWidth' => t('Fit to width'),
'fitToHeight' => t('Fit to height'),
'fitLongestSide' => t('Fit to longest side'),
'fitShortestSide' => t('Fit to shortest side'),
'fitExactSize' => t('Crop and fit exactly')
),
);
$form['node_images_display'] = array(
'#type' => 'select',
'#title' => t('Image Display'),
'#default_value' => variable_get('node_images_display', 'default'),
'#description' => t('This sets the display type for Node Images when the thumbnail is clicked on. If the \'lightbox\' or \'thickbox\' modules are enabled, then these can be selected here as the default output.'),
'#options' => _node_images_get_display_options()
);
$form['node_image_gallery_node_display_pos'] = array(
'#type' => 'select',
'#title' => t('Node display position on Image Gallery page'),
'#default_value' => variable_get('node_image_gallery_node_display_pos', 'bottom'),
'#description' => t('This sets the node body display position on the \'Image Gallery\' page'),
'#options' => array(
'top' => t('Top'),
'bottom' => t('Bottom'),
'none' => t('None')
),
);
$form['node_images_extensions'] = array(
'#type' => 'textfield',
'#title' => t('Default permitted image extensions'),
'#default_value' => variable_get('node_images_extensions', 'jpg jpeg gif png'),
'#maxlength' => 255,
'#description' => t('Default image extensions that users can upload. Separate extensions with a space and do not include the leading dot.'),
);
$form['node_images_max_images'] = array(
'#type' => 'textfield',
'#title' => t('Maximum number of images'),
'#default_value' => variable_get('node_images_max_images', 4),
'#size' => 6,
'#maxlength' => 2,
'#description' => t('The maximum number of images a user can upload for each node.'),
);
$form['node_images_md5name'] = array(
'#type' => 'checkbox',
'#title'=> t('MD5 filenames'),
'#description' => t('Override uploaded filenames with the MD5 hash of the file.'),
'#default_value' => variable_get('node_images_md5name', FALSE),
);
return system_settings_form($form);
}
/**
* Checks if the upload.module is enabled, and the existence of an image toolkit.
*
* @param $form_element
* The form element containing the name of the directory to check.
*/
function _node_images_check_settings() {
// Make sure upload module is enabled
if (!module_exists('upload')) {
drupal_set_message(t('node_images.module require upload.module to be enabled.'), 'error');
}
// Make sure we've got a working toolkit
if (!image_get_toolkit()) {
drupal_set_message(t('Make sure you have a working image toolkit installed and enabled, for more information see: %settings', array('%settings' => l(t('the settings page'), 'admin/settings/image-toolkit'))), 'error');
}
}
/************************************************************
* Helper functions - other
************************************************************/
/**
* Checks the existence of the destination directory.
* The directory can be specified either in $form_element (when called from the admin settings)
* or in $path (when called from the node images edit page).
*
* @param $form_element
* The form element containing the name of the directory to check.
*/
function _node_images_check_directory($form_element = NULL, $path = '') {
global $user;
$variables = array('%uid' => $user->uid, '%username' => $user->name);
if ($form_element) {
$path = strtr($form_element['#value'], $variables);
_node_images_mkdir_recursive($path, FILE_CREATE_DIRECTORY, $form_element['#parents'][0]);
return $form_element;
}
$path = strtr($path, $variables);
_node_images_mkdir_recursive($path, FILE_CREATE_DIRECTORY, $form_element['#parents'][0]);
}
function _node_images_mkdir_recursive($path, $mode, $form_item) {
$folders = explode("/", $path);
$dirs = array();
foreach($folders as $folder) {
$dirs[] = $folder;
if (!file_check_directory(implode("/", $dirs), $mode, $form_item)) {
mkdir(implode("/", $dirs));
}
}
}
/**
* Return the destination directory.
*/
function _node_images_get_directory() {
global $user;
$path = variable_get('node_images_path', 'files');
return strtr($path, array('%uid' => $user->uid, '%username' => $user->name));
}
/**
* A universal thumbnail generator function. Provides several possible methods for calculating and/or cropping
* the actual thumbnail, and returns an array of properties you can then use to invoke whatever image toolkit
* you wish to do the actual image manipulation and disk work.
*
* @param $originalWidth
* The width in pixels of the original image
* @param $originalHeight
* The height in pixels of the original image
* @param $suggestedWidth
* The width in pixels that you or your program's admin panel have specified for the thumbnail width
* @param $suggestedHeight
* The height in pixels that you or your program's admin panel have specified for the thumbnail width
* @param $resizeMethod
* A string. The magic value used to decide how to generate the thumbnail. It is suggested that in
* the code which calls this function, you run a test of your own design to determine which value to
* pass. For example, you could have an administration screen somewhere which allows a user to determine
* it, your code reads the configuration value, and passes the appropriate flag to this function.
* Must be one of the following five values:
*
* 'fitToWidth': Scales the thumbnail proportionally until it is the suggested width.
* 'fitToHeight': Scales the thumbnail proportionally until it is the suggested height.
* 'fitLongestSide': Picks the LONGEST dimension of the image, then scales the thumbnail proportionally
* to match suggested size. Result: the image will fit inside the suggested dimensions
* every time, but often will not fill up the whole box except in the one direction.
* 'fitShortestSide':Picks the SHORTEST dimension of the image, then scales the thumbnail proportionally
* to match suggested size. Result: the longer dimension may "stick out" past its suggested size.
* 'fitExactSize': Performs fitShortest, then centers the image and crops off the extra so thumbnail fits
* EXACTLY in the suggested box dimensions, every single time.
* @return
* This function returns an array containing:
* 'scaledX': The newly-calculated width of the thumbnail
* 'scaledY': The newly-calculated height of the thumbnail
* 'offsetX': If cropping is required, this will be the amount from the left to offset the image
* 'offsetY': If cropping is required, this will be the amount from the top to offset the image
* 'croppedX': The width of the thumbnail after cropping
* 'croppedY': The height of the thumbnail after cropping
*
*/
function universal_thumbnail_calculator( $originalWidth, $originalHeight, $suggestedWidth, $suggestedHeight, $resizeMethod = 'fitLongestSide' ) {
$ratioX = $suggestedWidth/$originalWidth;
$ratioY = $suggestedHeight/$originalHeight;
$crop = FALSE;
if ($resizeMethod == 'fitExactSize') {
$resizeMethod = 'fitShortestSide';
$crop = TRUE;
}
if ($resizeMethod == 'fitLongestSide') {
$resizeMethod = ($originalWidth > $originalHeight) ? 'fitToWidth' : 'fitToHeight';
}
if ($resizeMethod == 'fitShortestSide') {
$resizeMethod = ($originalWidth < $originalHeight) ? 'fitToWidth' : 'fitToHeight';
}
switch ($resizeMethod) {
case 'fitToWidth':
$thumbnailValues = array( 'scaledX' => $suggestedWidth, 'scaledY' => round($originalHeight*$ratioX) );
break;
case 'fitToHeight':
$thumbnailValues = array( 'scaledX' => round($originalWidth*$ratioY), 'scaledY' => $suggestedHeight );
break;
}
if ($crop) {
// Calculate the offset (where to begin 'drawing' the crop rectangle) as half the pixel overflow:
$thumbnailValues['offsetX'] = round($thumbnailValues['scaledX'] - $suggestedWidth)/2;
$thumbnailValues['offsetY'] = round($thumbnailValues['scaledY'] - $suggestedHeight)/2;
// Pass along the "newly cropped" dimensions to be used by later code:
$thumbnailValues['croppedX'] = $suggestedWidth;
$thumbnailValues['croppedY'] = $suggestedHeight;
}
return $thumbnailValues;
}
/**
* Create the thumbnail for the uploaded image.
*/
function _node_images_create_thumbnail($path, $suffix='_tn') {
$size = variable_get('node_images_thumb_resolution', '100x100');
list($width, $height) = explode('x', $size);
$dest_path = preg_replace('!(\.[^/.]+?)?$!', "$suffix\\1", $path, 1);
$resizeThumbMethod = variable_get('node_images_thumb_sizemethod', 'fitLongestSide');
if ($size = getimagesize($path)) {
$currentWidth = $size[0];
$currentHeight = $size[1];
$thumbnailDimensions = universal_thumbnail_calculator( $currentWidth, $currentHeight, $width, $height, $resizeThumbMethod);
image_resize($path, $dest_path, $thumbnailDimensions['scaledX'], $thumbnailDimensions['scaledY']);
if ((isset($thumbnailDimensions['offsetX']) && ($thumbnailDimensions['offsetX']>0)) || (isset($thumbnailDimensions['offsetY']) && ($thumbnailDimensions['offsetY']>0))) {
image_crop($dest_path, $dest_path, $thumbnailDimensions['offsetX'], $thumbnailDimensions['offsetY'], $thumbnailDimensions['croppedX'], $thumbnailDimensions['croppedY']);
}
$info = image_get_info($dest_path);
$thumb = new stdClass();
$thumb->filename = basename($dest_path);
$thumb->filepath = $dest_path;
$thumb->filesize = $info['file_size'];
$thumb->filemime = $info['mime_type'];
return $thumb;
}
return NULL;
}
/**
* Determine how much disk space is occupied by a user's uploaded files.
*
* @param $uid
* The integer user id of a user.
* @return
* The amount of disk space used by the user in bytes.
*/
function _node_images_space_used($uid) {
return db_result(db_query('SELECT SUM(filesize+thumbsize) FROM {node_images} WHERE uid=%d', $uid));
}
/**
* Menu-callback for JavaScript-based uploads.
*/
function _node_images_js() {
$edit = $_POST;
if ($nid = $edit['nid']) {
$node = node_load($nid);
// upload image
_node_images_upload($edit, $node);
// generate node images edit page
$form = _node_images_edit_form($node);
} else {
drupal_set_message(t('Unable to attach images to the current node.'));
}
$output = theme('status_messages');
if ($form) $output .= $form;
// We send the updated file attachments form.
print drupal_to_js(array('status' => TRUE, 'data' => $output));
exit();
}
/************************************************************
* Views API hooks
************************************************************/
/**
* Implementation of hook_views_default_tables().
*/
function node_images_views_tables() {
require_once './'. drupal_get_path('module', 'node_images') .'/node_images.views.inc';
return _node_images_views_tables();
}
/**
* Implementation of hook_views_pre_query().
*/
function node_images_views_pre_query() {
require_once './'. drupal_get_path('module', 'node_images') .'/node_images.views.inc';
}
/******************************************************
* Display extension settings - integration of lightbox & thickbox
*****************************************************/
/**
* return an array of display options dependent upon modules that are enabled
*
* the 'key' part of the array should contain the module name
* the 'value' part of the array is the display name in the settings.
* any 'key' defined here should also form the name of an implemented associated function theme_node_images_thumb_[key](&$node, &$image, &$parameters) which contains the appropriate js or whatever to trigger the display module.
*
* add as necessary and write the appropriate theme_node_images_thumb_[key](&$node, &$image, &$parameters)
*/
function _node_images_get_display_options() {
$ret = array('default' => t('Node Images (default)'));
if (module_exists('lightbox2')) {
$ret += array('lightbox2' => t('Lightbox'));
}
if (module_exists('thickbox')) {
$ret += array('thickbox' => t('Thickbox'));
}
return $ret;
}