200, "height" => 200, "master" => Image::AUTO), 100); * * Specifies that "gallery" is adding a rule to resize thumbnails down to a max of 200px on * the longest side. The gallery module adds default rules at a priority of 100. You can set * higher and lower priorities to perform operations before or after this fires. * * @param string $module_name the module that added the rule * @param string $target the target for this operation ("thumb" or "resize") * @param string $operation the name of the operation (::method) * @param array $args arguments to the operation * @param integer $priority the priority for this rule (lower priorities are run first) */ static function add_rule($module_name, $target, $operation, $args, $priority) { $rule = ORM::factory("graphics_rule"); $rule->module_name = $module_name; $rule->target = $target; $rule->operation = $operation; $rule->priority = $priority; $rule->args = serialize($args); $rule->active = true; $rule->save(); graphics::mark_dirty($target == "thumb", $target == "resize"); } /** * Remove any matching graphics rules * @param string $module_name the module that added the rule * @param string $target the target for this operation ("thumb" or "resize") * @param string $operation the name of the operation(::method) */ static function remove_rule($module_name, $target, $operation) { db::build() ->delete("graphics_rules") ->where("module_name", "=", $module_name) ->where("target", "=", $target) ->where("operation", "=", $operation) ->execute(); graphics::mark_dirty($target == "thumb", $target == "resize"); } /** * Remove all rules for this module * @param string $module_name */ static function remove_rules($module_name) { $status = db::build() ->delete("graphics_rules") ->where("module_name", "=", $module_name) ->execute(); if (count($status)) { graphics::mark_dirty(true, true); } } /** * Activate the rules for this module, typically done when the module itself is deactivated. * Note that this does not mark images as dirty so that if you deactivate and reactivate a * module it won't cause all of your images to suddenly require a rebuild. */ static function activate_rules($module_name) { db::build() ->update("graphics_rules") ->set("active", true) ->where("module_name", "=", $module_name) ->execute(); } /** * Deactivate the rules for this module, typically done when the module itself is deactivated. * Note that this does not mark images as dirty so that if you deactivate and reactivate a * module it won't cause all of your images to suddenly require a rebuild. */ static function deactivate_rules($module_name) { db::build() ->update("graphics_rules") ->set("active", false) ->where("module_name", "=", $module_name) ->execute(); } /** * Rebuild the thumb and resize for the given item. * @param Item_Model $item */ static function generate($item) { if ($item->thumb_dirty) { $ops["thumb"] = $item->thumb_path(); } if ($item->resize_dirty && $item->is_photo()) { $ops["resize"] = $item->resize_path(); } if (empty($ops)) { $item->thumb_dirty = 0; $item->resize_dirty = 0; $item->save(); return; } try { foreach ($ops as $target => $output_file) { $working_file = $item->file_path(); // Delete anything that might already be there @unlink($output_file); switch ($item->type) { case "movie": // Run movie_extract_frame events, which can either: // - generate an output file, bypassing the ffmpeg-based movie::extract_frame // - add to the options sent to movie::extract_frame (e.g. change frame extract time, // add de-interlacing arguments to ffmpeg... see movie helper for more info) // Note that the args are similar to those of the events in gallery_graphics $movie_options_wrapper = new stdClass(); $movie_options_wrapper->movie_options = array(); module::event("movie_extract_frame", $working_file, $output_file, $movie_options_wrapper, $item); // If no output_file generated by events, run movie::extract_frame with movie_options clearstatcache(); if (@filesize($output_file) == 0) { try { movie::extract_frame($working_file, $output_file, $movie_options_wrapper->movie_options); // If we're here, we know ffmpeg is installed and the movie is valid. Because the // user may not always have had ffmpeg installed, the movie's width, height, and // mime type may need updating. Let's use this opportunity to make sure they're // correct. It's not optimal to do it at this low level, but it's not trivial to find // these cases quickly in an upgrade script. list ($width, $height, $mime_type) = movie::get_file_metadata($working_file); // Only set them if they need updating to avoid marking them as "changed" if (($item->width != $width) || ($item->height != $height) || ($item->mime_type != $mime_type)) { $item->width = $width; $item->height = $height; $item->mime_type = $mime_type; } } catch (Exception $e) { // Didn't work, likely because of MISSING_FFMPEG - use placeholder graphics::_replace_image_with_placeholder($item, $target); break; } } $working_file = $output_file; case "photo": // Run the graphics rules (for both movies and photos) foreach (self::_get_rules($target) as $rule) { $args = array($working_file, $output_file, unserialize($rule->args), $item); call_user_func_array($rule->operation, $args); $working_file = $output_file; } break; case "album": if (!$cover = $item->album_cover()) { // This album has no cover; copy its placeholder image. Because of an old bug, it's // possible that there's an album cover item id that points to an invalid item. In that // case, just null out the album cover item id. It's not optimal to do that at this low // level, but it's not trivial to find these cases quickly in an upgrade script and if we // don't do this, the album may be permanently marked as "needs rebuilding" // // ref: http://sourceforge.net/apps/trac/gallery/ticket/1172 // http://galleryproject.org/node/96926 if ($item->album_cover_item_id) { $item->album_cover_item_id = null; $item->save(); } graphics::_replace_image_with_placeholder($item, $target); break; } if ($cover->thumb_dirty) { graphics::generate($cover); } if (!$cover->thumb_dirty) { // Make the album cover from the cover item's thumb. Run gallery_graphics::resize with // null options and it will figure out if this is a direct copy or conversion to jpg. $working_file = $cover->thumb_path(); gallery_graphics::resize($working_file, $output_file, null, $item); } break; } } if (!empty($ops["thumb"])) { if (file_exists($item->thumb_path())) { $item->thumb_dirty = 0; } else { Kohana_Log::add("error", "Failed to rebuild thumb image: $item->title"); graphics::_replace_image_with_placeholder($item, "thumb"); } } if (!empty($ops["resize"])) { if (file_exists($item->resize_path())) { $item->resize_dirty = 0; } else { Kohana_Log::add("error", "Failed to rebuild resize image: $item->title"); graphics::_replace_image_with_placeholder($item, "resize"); } } graphics::_update_item_dimensions($item); $item->save(); } catch (Exception $e) { // Something went wrong rebuilding the image. Replace with the placeholder images, // leave it dirty and move on. Kohana_Log::add("error", "Caught exception rebuilding images: {$item->title}\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); if ($item->is_photo()) { graphics::_replace_image_with_placeholder($item, "resize"); } graphics::_replace_image_with_placeholder($item, "thumb"); try { graphics::_update_item_dimensions($item); } catch (Exception $e) { // Looks like get_file_metadata couldn't identify our placeholders. We should never get // here, but in the odd case we do, we need to do something. Let's put in hardcoded values. if ($item->is_photo()) { list ($item->resize_width, $item->resize_height) = array(200, 200); } list ($item->thumb_width, $item->thumb_height) = array(200, 200); } $item->save(); throw $e; } } private static function _update_item_dimensions($item) { if ($item->is_photo()) { list ($item->resize_width, $item->resize_height) = photo::get_file_metadata($item->resize_path()); } list ($item->thumb_width, $item->thumb_height) = photo::get_file_metadata($item->thumb_path()); } private static function _replace_image_with_placeholder($item, $target) { if ($item->is_album() && !$item->album_cover_item_id) { $input_path = MODPATH . "gallery/images/missing_album_cover.jpg"; } else if ($item->is_movie() || ($item->is_album() && $item->album_cover()->is_movie())) { $input_path = MODPATH . "gallery/images/missing_movie.jpg"; } else { $input_path = MODPATH . "gallery/images/missing_photo.jpg"; } if ($target == "thumb") { $output_path = $item->thumb_path(); $size = module::get_var("gallery", "thumb_size", 200); } else { $output_path = $item->resize_path(); $size = module::get_var("gallery", "resize_size", 640); } $options = array("width" => $size, "height" => $size, "master" => Image::AUTO); try { // Copy/convert/resize placeholder as needed. gallery_graphics::resize($input_path, $output_path, $options, null); } catch (Exception $e) { // Copy/convert/resize didn't work. Add to the log and copy the jpg version (which could have // a non-jpg extension). This is less than ideal, but it's better than putting nothing // there and causing theme views to act strangely because a file is missing. // @todo we should handle this better. Kohana_Log::add("error", "Caught exception converting placeholder for missing image: " . $item->title . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); copy($input_path, $output_path); } if (!file_exists($output_path)) { // Copy/convert/resize didn't throw an exception, but still didn't work - do the same as above. // @todo we should handle this better. Kohana_Log::add("error", "Failed to convert placeholder for missing image: $item->title"); copy($input_path, $output_path); } } private static function _get_rules($target) { if (empty(self::$_rules_cache[$target])) { $rules = array(); foreach (ORM::factory("graphics_rule") ->where("target", "=", $target) ->where("active", "=", true) ->order_by("priority", "asc") ->find_all() as $rule) { $rules[] = (object)$rule->as_array(); } self::$_rules_cache[$target] = $rules; } return self::$_rules_cache[$target]; } /** * Return a query result that locates all items with dirty images. * @return Database_Result Query result */ static function find_dirty_images_query() { return db::build() ->from("items") ->and_open() ->where("thumb_dirty", "=", 1) ->and_open() ->where("type", "<>", "album") ->or_where("album_cover_item_id", "IS NOT", null) ->close() ->or_open() ->where("resize_dirty", "=", 1) ->where("type", "=", "photo") ->close() ->close(); } /** * Mark thumbnails and resizes as dirty. They will have to be rebuilt. Optionally, only those of * a specified type and/or mime type can be marked (e.g. $type="movie" to rebuild movies only). */ static function mark_dirty($thumbs, $resizes, $type=null, $mime_type=null) { if ($thumbs || $resizes) { $db = db::build() ->update("items"); if ($type) { $db->where("type", "=", $type); } if ($mime_type) { $db->where("mime_type", "=", $mime_type); } if ($thumbs) { $db->set("thumb_dirty", 1); } if ($resizes) { $db->set("resize_dirty", 1); } $db->execute(); } $count = graphics::find_dirty_images_query()->count_records(); if ($count) { site_status::warning( t2("One of your photos is out of date. Click here to fix it", "%count of your photos are out of date. Click here to fix them", $count, array("attrs" => html::mark_clean(sprintf( 'href="%s" class="g-dialog-link"', url::site("admin/maintenance/start/gallery_task::rebuild_dirty_images?csrf=__CSRF__"))))), "graphics_dirty"); } } /** * Detect which graphics toolkits are available on this system. Return an array of key value * pairs where the key is one of gd, imagemagick, graphicsmagick and the value is information * about that toolkit. For GD we return the version string, and for ImageMagick and * GraphicsMagick we return the path to the directory containing the appropriate binaries. */ static function detect_toolkits() { $toolkits = new stdClass(); $toolkits->gd = new stdClass(); $toolkits->imagemagick = new stdClass(); $toolkits->graphicsmagick = new stdClass(); // GD is special, it doesn't use exec() $gd = function_exists("gd_info") ? gd_info() : array(); $toolkits->gd->name = "GD"; if (!isset($gd["GD Version"])) { $toolkits->gd->installed = false; $toolkits->gd->error = t("GD is not installed"); } else { $toolkits->gd->installed = true; $toolkits->gd->version = $gd["GD Version"]; $toolkits->gd->rotate = function_exists("imagerotate"); $toolkits->gd->sharpen = function_exists("imageconvolution"); $toolkits->gd->binary = ""; $toolkits->gd->dir = ""; if (!$toolkits->gd->rotate && !$toolkits->gd->sharpen) { $toolkits->gd->error = t("You have GD version %version, but it lacks image rotation and sharpening.", array("version" => $gd["GD Version"])); } else if (!$toolkits->gd->rotate) { $toolkits->gd->error = t("You have GD version %version, but it lacks image rotation.", array("version" => $gd["GD Version"])); } else if (!$toolkits->gd->sharpen) { $toolkits->gd->error = t("You have GD version %version, but it lacks image sharpening.", array("version" => $gd["GD Version"])); } } if (!function_exists("exec")) { $toolkits->imagemagick->installed = false; $toolkits->imagemagick->error = t("ImageMagick requires the exec function"); $toolkits->graphicsmagick->installed = false; $toolkits->graphicsmagick->error = t("GraphicsMagick requires the exec function"); } else { // ImageMagick & GraphicsMagick $magick_kits = array( "imagemagick" => array( "name" => "ImageMagick", "binary" => "convert", "version_arg" => "-version", "version_regex" => "/Version: \S+ (\S+)/"), "graphicsmagick" => array( "name" => "GraphicsMagick", "binary" => "gm", "version_arg" => "version", "version_regex" => "/\S+ (\S+)/")); // Loop through the kits foreach ($magick_kits as $index => $settings) { $path = system::find_binary( $settings["binary"], module::get_var("gallery", "graphics_toolkit_path")); $toolkits->$index->name = $settings["name"]; if ($path) { if (@is_file($path) && preg_match( $settings["version_regex"], shell_exec($path . " " . $settings["version_arg"]), $matches)) { $version = $matches[1]; $toolkits->$index->installed = true; $toolkits->$index->version = $version; $toolkits->$index->binary = $path; $toolkits->$index->dir = dirname($path); $toolkits->$index->rotate = true; $toolkits->$index->sharpen = true; } else { $toolkits->$index->installed = false; $toolkits->$index->error = t("%toolkit_name is installed, but PHP's open_basedir restriction prevents Gallery from using it.", array("toolkit_name" => $settings["name"])); } } else { $toolkits->$index->installed = false; $toolkits->$index->error = t("We could not locate %toolkit_name on your system.", array("toolkit_name" => $settings["name"])); } } } return $toolkits; } /** * This needs to be run once, after the initial install, to choose a graphics toolkit. */ static function choose_default_toolkit() { // Detect a graphics toolkit $toolkits = graphics::detect_toolkits(); foreach (array("imagemagick", "graphicsmagick", "gd") as $tk) { if ($toolkits->$tk->installed) { module::set_var("gallery", "graphics_toolkit", $tk); module::set_var("gallery", "graphics_toolkit_path", $toolkits->$tk->dir); break; } } if (!module::get_var("gallery", "graphics_toolkit")) { site_status::warning( t("Graphics toolkit missing! Please choose a toolkit", array("url" => html::mark_clean(url::site("admin/graphics")))), "missing_graphics_toolkit"); } } /** * Choose which driver the Kohana Image library uses. */ static function init_toolkit() { if (self::$init) { return; } switch(module::get_var("gallery", "graphics_toolkit")) { case "gd": Kohana_Config::instance()->set("image.driver", "GD"); break; case "imagemagick": Kohana_Config::instance()->set("image.driver", "ImageMagick"); Kohana_Config::instance()->set( "image.params.directory", module::get_var("gallery", "graphics_toolkit_path")); break; case "graphicsmagick": Kohana_Config::instance()->set("image.driver", "GraphicsMagick"); Kohana_Config::instance()->set( "image.params.directory", module::get_var("gallery", "graphics_toolkit_path")); break; } self::$init = 1; } /** * Verify that a specific graphics function is available with the active toolkit. * @param string $func (eg rotate, sharpen) * @return boolean */ static function can($func) { if (module::get_var("gallery", "graphics_toolkit") == "gd") { switch ($func) { case "rotate": return function_exists("imagerotate"); case "sharpen": return function_exists("imageconvolution"); } } return true; } /** * Return the max file size that this graphics toolkit can handle. */ static function max_filesize() { if (module::get_var("gallery", "graphics_toolkit") == "gd") { $memory_limit = trim(ini_get("memory_limit")); $memory_limit_bytes = num::convert_to_bytes($memory_limit); // GD expands images in memory and uses 4 bytes of RAM for every byte // in the file. $max_filesize = $memory_limit_bytes / 4; $max_filesize_human_readable = num::convert_to_human_readable($max_filesize); return array($max_filesize, $max_filesize_human_readable); } // Some arbitrarily large size return array(1000000000, "1G"); } }