mirror of
https://github.com/Pathduck/gallery3.git
synced 2026-06-04 10:49:07 -04:00
- Added code to check/correct movie width, height, and mime in graphics::generate. As the comment says in the commit, this isn't ideal, but doing it in an upgrade script wouldn't be very ideal either.
544 lines
21 KiB
PHP
544 lines
21 KiB
PHP
<?php defined("SYSPATH") or die("No direct script access.");
|
|
/**
|
|
* Gallery - a web based photo album viewer and editor
|
|
* Copyright (C) 2000-2013 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;
|
|
private static $_rules_cache = array();
|
|
|
|
/**
|
|
* 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", "gallery_graphics::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 (<defining class>::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(<defining class>::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");
|
|
graphics::_update_item_dimensions($item);
|
|
$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. <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="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 <b>exec</b> function");
|
|
|
|
$toolkits->graphicsmagick->installed = false;
|
|
$toolkits->graphicsmagick->error = t("GraphicsMagick requires the <b>exec</b> 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 <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() {
|
|
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");
|
|
}
|
|
}
|