Files
gallery3/modules/gallery/helpers/graphics.php
Bharat Mediratta b9293755c0 Deal with the aftermath of adding sharpen() calls. Since GD does not
support it, this causes crashes as soon as you try to use it, which
breaks a bunch of our tests.  Also, give the user some idea that
sharpen() is missing in the UI.  Fixes #689.
2009-09-02 11:57:20 -07:00

508 lines
18 KiB
PHP

<?php defined("SYSPATH") or die("No direct script access.");
/**
* Gallery - a web based photo album viewer and editor
* Copyright (C) 2000-2009 Bharat Mediratta
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
*/
class graphics_Core {
private static $init;
/**
* Add a new graphics rule.
*
* Rules are applied to targets (thumbnails and resizes) in priority order. Rules are functions
* in the graphics class. So for example, the following rule:
*
* graphics::add_rule("gallery", "thumb", "resize",
* array("width" => 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
* @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();
self::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
*/
static function remove_rule($module_name, $target, $operation) {
ORM::factory("graphics_rule")
->where("module_name", $module_name)
->where("target", $target)
->where("operation", $operation)
->delete_all();
self::mark_dirty($target == "thumb", $target == "resize");
}
/**
* Remove all rules for this module
* @param string $module_name
*/
static function remove_rules($module_name) {
$status = Database::instance()->delete("graphics_rules", array("module_name" => $module_name));
if (count($status)) {
self::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) {
Database::instance()
->update("graphics_rules",array("active" => true), array("module_name" => $module_name));
}
/**
* 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) {
Database::instance()
->update("graphics_rules",array("active" => false), array("module_name" => $module_name));
}
/**
* Rebuild the thumb and resize for the given item.
* @param Item_Model $item
* @return true on successful generation
*/
static function generate($item) {
if ($item->is_album()) {
if (!$cover = $item->album_cover()) {
return false;
}
$input_file = $cover->file_path();
$input_item = $cover;
} else {
$input_file = $item->file_path();
$input_item = $item;
}
if ($item->thumb_dirty) {
$ops["thumb"] = $item->thumb_path();
}
if ($item->resize_dirty && !$item->is_album() && !$item->is_movie()) {
$ops["resize"] = $item->resize_path();
}
if (empty($ops)) {
$item->thumb_dirty = 0;
$item->resize_dirty = 0;
$item->save();
return true;
}
try {
foreach ($ops as $target => $output_file) {
if ($input_item->is_movie()) {
// Convert the movie to a JPG first
$output_file = preg_replace("/...$/", "jpg", $output_file);
try {
movie::extract_frame($input_file, $output_file);
} catch (Exception $e) {
// Assuming this is MISSING_FFMPEG for now
copy(MODPATH . "gallery/images/missing_movie.png", $output_file);
}
$working_file = $output_file;
} else {
$working_file = $input_file;
}
foreach (ORM::factory("graphics_rule")
->where("target", $target)
->where("active", true)
->orderby("priority", "asc")
->find_all() as $rule) {
$args = array($working_file, $output_file, unserialize($rule->args));
call_user_func_array(array("graphics", $rule->operation), $args);
$working_file = $output_file;
}
}
if (!empty($ops["thumb"])) {
$dims = getimagesize($item->thumb_path());
$item->thumb_width = $dims[0];
$item->thumb_height = $dims[1];
$item->thumb_dirty = 0;
}
if (!empty($ops["resize"])) {
$dims = getimagesize($item->resize_path());
$item->resize_width = $dims[0];
$item->resize_height = $dims[1];
$item->resize_dirty = 0;
}
$item->save();
} catch (Exception $e) {
// Something went wrong rebuilding the image. Leave it dirty and move on.
// @todo we should handle this better.
Kohana::log("error", "Caught exception rebuilding image: {$item->title}\n" .
$e->getMessage() . "\n" . $e->getTraceAsString());
return false;
}
return true;
}
/**
* Resize an image. Valid options are width, height and master. Master is one of the Image
* master dimension constants.
*
* @param string $input_file
* @param string $output_file
* @param array $options
*/
static function resize($input_file, $output_file, $options) {
if (!self::$init) {
self::init_toolkit();
}
module::event("graphics_resize", $input_file, $output_file, $options);
if (@filesize($input_file) == 0) {
throw new Exception("@todo EMPTY_INPUT_FILE");
}
$dims = getimagesize($input_file);
if (max($dims[0], $dims[1]) < min($options["width"], $options["height"])) {
// Image would get upscaled; do nothing
copy($input_file, $output_file);
} else {
$image = Image::factory($input_file)
->resize($options["width"], $options["height"], $options["master"])
->quality(module::get_var("gallery", "image_quality"));
if (graphics::can("sharpen")) {
$image->sharpen(module::get_var("gallery", "image_sharpen"));
}
$image->save($output_file);
}
module::event("graphics_resize_completed", $input_file, $output_file, $options);
}
/**
* Rotate an image. Valid options are degrees
*
* @param string $input_file
* @param string $output_file
* @param array $options
*/
static function rotate($input_file, $output_file, $options) {
if (!self::$init) {
self::init_toolkit();
}
module::event("graphics_rotate", $input_file, $output_file, $options);
Image::factory($input_file)
->quality(module::get_var("gallery", "image_quality"))
->rotate($options["degrees"])
->save($output_file);
module::event("graphics_rotate_completed", $input_file, $output_file, $options);
}
/**
* Overlay an image on top of the input file.
*
* Valid options are: file, mime_type, position, transparency_percent, padding
*
* Valid positions: northwest, north, northeast,
* west, center, east,
* southwest, south, southeast
*
* padding is in pixels
*
* @param string $input_file
* @param string $output_file
* @param array $options
*/
static function composite($input_file, $output_file, $options) {
if (!self::$init) {
self::init_toolkit();
}
module::event("graphics_composite", $input_file, $output_file, $options);
list ($width, $height) = getimagesize($input_file);
list ($w_width, $w_height) = getimagesize($options["file"]);
$pad = isset($options["padding"]) ? $options["padding"] : 10;
$top = $pad;
$left = $pad;
$y_center = max($height / 2 - $w_height / 2, $pad);
$x_center = max($width / 2 - $w_width / 2, $pad);
$bottom = max($height - $w_height - $pad, $pad);
$right = max($width - $w_width - $pad, $pad);
switch ($options["position"]) {
case "northwest": $x = $left; $y = $top; break;
case "north": $x = $x_center; $y = $top; break;
case "northeast": $x = $right; $y = $top; break;
case "west": $x = $left; $y = $y_center; break;
case "center": $x = $x_center; $y = $y_center; break;
case "east": $x = $right; $y = $y_center; break;
case "southwest": $x = $left; $y = $bottom; break;
case "south": $x = $x_center; $y = $bottom; break;
case "southeast": $x = $right; $y = $bottom; break;
}
Image::factory($input_file)
->composite($options["file"], $x, $y, $options["transparency"])
->quality(module::get_var("gallery", "image_quality"))
->save($output_file);
module::event("graphics_composite_completed", $input_file, $output_file, $options);
}
/**
* Return a query result that locates all items with dirty images.
* @return Database_Result Query result
*/
static function find_dirty_images_query() {
return Database::instance()->query(
"SELECT `id` FROM {items} " .
"WHERE ((`thumb_dirty` = 1 AND (`type` <> 'album' OR `album_cover_item_id` IS NOT NULL))" .
" OR (`resize_dirty` = 1 AND `type` = 'photo')) " .
" AND `id` != 1");
}
/**
* Mark thumbnails and resizes as dirty. They will have to be rebuilt.
*/
static function mark_dirty($thumbs, $resizes) {
if ($thumbs || $resizes) {
$db = Database::instance();
$fields = array();
if ($thumbs) {
$fields["thumb_dirty"] = 1;
}
if ($resizes) {
$fields["resize_dirty"] = 1;
}
$db->update("items", $fields, true);
}
$count = self::find_dirty_images_query()->count();
if ($count) {
site_status::warning(
t2("One of your photos is out of date. <a %attrs>Click here to fix it</a>",
"%count of your photos are out of date. <a %attrs>Click here to fix them</a>",
$count,
array("attrs" => html::mark_clean(sprintf(
'href="%s" class="gDialogLink"',
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();
// 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 <b>exec</b> function");
$toolkits->graphicsmagick->installed = false;
$toolkits->graphicsmagick->error = t("GraphicsMagick requires the <b>exec</b> function");
} else {
putenv("PATH=" . getenv("PATH") . ":/usr/local/bin:/opt/local/bin:/opt/bin");
// @todo: consider refactoring the two segments below into a loop since they are so
// similar.
// ImageMagick
$path = exec("which convert");
$toolkits->imagemagick->name = "ImageMagick";
if ($path) {
if (@is_file($path)) {
preg_match('/Version: \S+ (\S+)/', `convert -v`, $matches);
$version = $matches[1];
$toolkits->imagemagick->installed = true;
$toolkits->imagemagick->version = $version;
$toolkits->imagemagick->binary = $path;
$toolkits->imagemagick->dir = dirname($path);
$toolkits->imagemagick->rotate = true;
$toolkits->imagemagick->sharpen = true;
} else {
$toolkits->imagemagick->installed = false;
$toolkits->imagemagick->error =
t("ImageMagick is installed, but PHP's open_basedir restriction " .
"prevents Gallery from using it.");
}
} else {
$toolkits->imagemagick->installed = false;
$toolkits->imagemagick->error = t("We could not locate ImageMagick on your system.");
}
// GraphicsMagick
$path = exec("which gm");
$toolkits->graphicsmagick->name = "GraphicsMagick";
if ($path) {
if (@is_file($path)) {
preg_match('/\S+ (\S+)/', `gm version`, $matches);
$version = $matches[1];
$toolkits->graphicsmagick->installed = true;
$toolkits->graphicsmagick->version = $version;
$toolkits->graphicsmagick->binary = $path;
$toolkits->graphicsmagick->dir = dirname($path);
$toolkits->graphicsmagick->rotate = true;
$toolkits->graphicsmagick->sharpen = true;
} else {
$toolkits->graphicsmagick->installed = false;
$toolkits->graphicsmagick->error =
t("GraphicsMagick is installed, but PHP's open_basedir restriction " .
"prevents Gallery from using it.");
}
} else {
$toolkits->graphicsmagick->installed = false;
$toolkits->graphicsmagick->error = t("We could not locate GraphicsMagick on your system.");
}
}
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 <a href=\"%url\">choose a toolkit</a>",
array("url" => html::mark_clean(url::site("admin/graphics")))),
"missing_graphics_toolkit");
}
}
/**
* Choose which driver the Kohana Image library uses.
*/
static function init_toolkit() {
switch(module::get_var("gallery", "graphics_toolkit")) {
case "gd":
Kohana::config_set("image.driver", "GD");
break;
case "imagemagick":
Kohana::config_set("image.driver", "ImageMagick");
Kohana::config_set(
"image.params.directory", module::get_var("gallery", "graphics_toolkit_path"));
break;
case "graphicsmagick":
Kohana::config_set("image.driver", "GraphicsMagick");
Kohana::config_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;
}
}