silverstripe-framework/model/Image.php
Fred Condo dbfd54ba50 BUGFIX: Get all method names to find cached images
Because some extensions add functions that generate cached images, deleteFormattedImages()
needs to find all the method names by passing true to allMethodNames().
2011-10-31 11:36:01 -07:00

485 lines
13 KiB
PHP

<?php
/**
* Represents an Image
*
* @package sapphire
* @subpackage filesystem
*/
class Image extends File {
const ORIENTATION_SQUARE = 0;
const ORIENTATION_PORTRAIT = 1;
const ORIENTATION_LANDSCAPE = 2;
static $casting = array(
'Tag' => 'HTMLText',
);
/**
* The width of an image thumbnail in a strip.
* @var int
*/
public static $strip_thumbnail_width = 50;
/**
* The height of an image thumbnail in a strip.
* @var int
*/
public static $strip_thumbnail_height = 50;
/**
* The width of an image thumbnail in the CMS.
* @var int
*/
public static $cms_thumbnail_width = 100;
/**
* The height of an image thumbnail in the CMS.
*/
public static $cms_thumbnail_height = 100;
/**
* The width of an image thumbnail in the Asset section.
*/
public static $asset_thumbnail_width = 100;
/**
* The height of an image thumbnail in the Asset section.
*/
public static $asset_thumbnail_height = 100;
/**
* The width of an image preview in the Asset section.
*/
public static $asset_preview_width = 400;
/**
* The height of an image preview in the Asset section.
*/
public static $asset_preview_height = 200;
/**
* Set up template methods to access the transformations generated by 'generate' methods.
*/
public function defineMethods() {
$methodNames = $this->allMethodNames();
foreach($methodNames as $methodName) {
if(substr($methodName,0,8) == 'generate') {
$this->addWrapperMethod(substr($methodName,8), 'getFormattedImage');
}
}
parent::defineMethods();
}
/**
* An image exists if it has a filename.
* Does not do any filesystem checks.
*
* @return boolean
*/
public function exists() {
if(isset($this->record["Filename"])) {
return true;
}
}
/**
* Return an XHTML img tag for this Image,
* or NULL if the image file doesn't exist on the filesystem.
*
* @return string
*/
function getTag() {
if(file_exists(Director::baseFolder() . '/' . $this->Filename)) {
$url = $this->getURL();
$title = ($this->Title) ? $this->Title : $this->Filename;
if($this->Title) {
$title = Convert::raw2att($this->Title);
} else {
if(preg_match("/([^\/]*)\.[a-zA-Z0-9]{1,6}$/", $title, $matches)) $title = Convert::raw2att($matches[1]);
}
return "<img src=\"$url\" alt=\"$title\" />";
}
}
/**
* Return an XHTML img tag for this Image.
*
* @return string
*/
function forTemplate() {
return $this->getTag();
}
/**
* File names are filtered through {@link FileNameFilter}, see class documentation
* on how to influence this behaviour.
*/
function loadUploadedImage($tmpFile) {
if(!is_array($tmpFile)) {
user_error("Image::loadUploadedImage() Not passed an array. Most likely, the form hasn't got the right enctype", E_USER_ERROR);
}
if(!$tmpFile['size']) {
return;
}
$class = $this->class;
// Create a folder
if(!file_exists(ASSETS_PATH)) {
mkdir(ASSETS_PATH, Filesystem::$folder_create_mask);
}
if(!file_exists(ASSETS_PATH . "/$class")) {
mkdir(ASSETS_PATH . "/$class", Filesystem::$folder_create_mask);
}
// Generate default filename
$nameFilter = Object::create('FileNameFilter');
$file = $nameFilter->filter($tmpFile['name']);
if(!$file) $file = "file.jpg";
$file = ASSETS_PATH . "/$class/$file";
while(file_exists(BASE_PATH . "/$file")) {
$i = $i ? ($i+1) : 2;
$oldFile = $file;
$file = ereg_replace('[0-9]*(\.[^.]+$)',$i . '\\1', $file);
if($oldFile == $file && $i > 2) user_error("Couldn't fix $file with $i", E_USER_ERROR);
}
if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], BASE_PATH . "/$file")) {
// Remove the old images
$this->deleteFormattedImages();
return true;
}
}
public function SetWidth($width) {
return $this->getWidth() == $width ? $this : $this->getFormattedImage('SetWidth', $width);
}
public function SetHeight($height) {
return $this->getHeight() == $height ? $this : $this->getFormattedImage('SetHeight', $height);
}
public function SetSize($width, $height) {
return (($this->getWidth() == $width) && ($this->getHeight() == $height)) ? $this : $this->getFormattedImage('SetSize', $width, $height);
}
public function SetRatioSize($width, $height) {
return $this->getFormattedImage('SetRatioSize', $width, $height);
}
public function generateSetRatioSize(GD $gd, $width, $height) {
return $gd->resizeRatio($width, $height);
}
/**
* Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
* @return GD
*/
public function generateSetWidth(GD $gd, $width) {
return $gd->resizeByWidth($width);
}
/**
* Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
* @return GD
*/
public function generateSetHeight(GD $gd, $height){
return $gd->resizeByHeight($height);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
* @return GD
*/
public function generateSetSize(GD $gd, $width, $height) {
return $gd->paddedResize($width, $height);
}
public function CMSThumbnail() {
return $this->getFormattedImage('CMSThumbnail');
}
/**
* Resize this image for the CMS. Use in templates with $CMSThumbnail.
* @return GD
*/
function generateCMSThumbnail(GD $gd) {
return $gd->paddedResize($this->stat('cms_thumbnail_width'),$this->stat('cms_thumbnail_height'));
}
/**
* Resize this image for preview in the Asset section. Use in templates with $AssetLibraryPreview.
* @return GD
*/
function generateAssetLibraryPreview(GD $gd) {
return $gd->paddedResize($this->stat('asset_preview_width'),$this->stat('asset_preview_height'));
}
/**
* Resize this image for thumbnail in the Asset section. Use in templates with $AssetLibraryThumbnail.
* @return GD
*/
function generateAssetLibraryThumbnail(GD $gd) {
return $gd->paddedResize($this->stat('asset_thumbnail_width'),$this->stat('asset_thumbnail_height'));
}
/**
* Resize this image for use as a thumbnail in a strip. Use in templates with $StripThumbnail.
* @return GD
*/
function generateStripThumbnail(GD $gd) {
return $gd->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height'));
}
function generatePaddedImage(GD $gd, $width, $height) {
return $gd->paddedResize($width, $height);
}
/**
* Return an image object representing the image in the given format.
* This image will be generated using generateFormattedImage().
* The generated image is cached, to flush the cache append ?flush=1 to your URL.
* @param string $format The name of the format.
* @param string $arg1 An argument to pass to the generate function.
* @param string $arg2 A second argument to pass to the generate function.
* @return Image_Cached
*/
function getFormattedImage($format, $arg1 = null, $arg2 = null) {
if($this->ID && $this->Filename && Director::fileExists($this->Filename)) {
$cacheFile = $this->cacheFilename($format, $arg1, $arg2);
if(!file_exists(Director::baseFolder()."/".$cacheFile) || isset($_GET['flush'])) {
$this->generateFormattedImage($format, $arg1, $arg2);
}
$cached = new Image_Cached($cacheFile);
// Pass through the title so the templates can use it
$cached->Title = $this->Title;
return $cached;
}
}
/**
* Return the filename for the cached image, given it's format name and arguments.
* @param string $format The format name.
* @param string $arg1 The first argument passed to the generate function.
* @param string $arg2 The second argument passed to the generate function.
* @return string
*/
function cacheFilename($format, $arg1 = null, $arg2 = null) {
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . "/";
$format = $format.$arg1.$arg2;
return $folder . "_resampled/$format-" . $this->Name;
}
/**
* Generate an image on the specified format. It will save the image
* at the location specified by cacheFilename(). The image will be generated
* using the specific 'generate' method for the specified format.
* @param string $format Name of the format to generate.
* @param string $arg1 Argument to pass to the generate method.
* @param string $arg2 A second argument to pass to the generate method.
*/
function generateFormattedImage($format, $arg1 = null, $arg2 = null) {
$cacheFile = $this->cacheFilename($format, $arg1, $arg2);
$gd = new GD(Director::baseFolder()."/" . $this->Filename);
if($gd->hasGD()){
$generateFunc = "generate$format";
if($this->hasMethod($generateFunc)){
$gd = $this->$generateFunc($gd, $arg1, $arg2);
if($gd){
$gd->writeTo(Director::baseFolder()."/" . $cacheFile);
}
} else {
user_error("Image::generateFormattedImage - Image $format function not found.",E_USER_WARNING);
}
}
}
/**
* Generate a resized copy of this image with the given width & height.
* Use in templates with $ResizedImage.
*/
function generateResizedImage($gd, $width, $height) {
if(is_numeric($gd) || !$gd){
user_error("Image::generateFormattedImage - generateResizedImage is being called by legacy code or gd is not set.",E_USER_WARNING);
}else{
return $gd->resize($width, $height);
}
}
/**
* Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
* Use in templates with $CroppedImage
*/
function generateCroppedImage($gd, $width, $height) {
return $gd->croppedResize($width, $height);
}
/**
* Remove all of the formatted cached images for this image.
*
* @return int The number of formatted images deleted
*/
public function deleteFormattedImages() {
if(!$this->Filename) return 0;
$numDeleted = 0;
$methodNames = $this->allMethodNames(true);
$cachedFiles = array();
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . '/';
$cacheDir = Director::getAbsFile($folder . '_resampled/');
if(is_dir($cacheDir)) {
if($handle = opendir($cacheDir)) {
while(($file = readdir($handle)) !== false) {
// ignore all entries starting with a dot
if(substr($file, 0, 1) != '.' && is_file($cacheDir . $file)) {
$cachedFiles[] = $file;
}
}
closedir($handle);
}
}
$generateFuncs = array();
foreach($methodNames as $methodName) {
if(substr($methodName, 0, 8) == 'generate') {
$format = substr($methodName, 8);
$generateFuncs[] = preg_quote($format);
}
}
// All generate functions may appear any number of times in the image cache name.
$generateFuncs = implode('|', $generateFuncs);
$pattern = "/^(({$generateFuncs})\d+\-)+" . preg_quote($this->Name) . "$/i";
foreach($cachedFiles as $cfile) {
if(preg_match($pattern, $cfile)) {
if(Director::fileExists($cacheDir . $cfile)) {
unlink($cacheDir . $cfile);
$numDeleted++;
}
}
}
return $numDeleted;
}
/**
* Get the dimensions of this Image.
* @param string $dim If this is equal to "string", return the dimensions in string form,
* if it is 0 return the height, if it is 1 return the width.
* @return string|int
*/
function getDimensions($dim = "string") {
if($this->getField('Filename')) {
$imagefile = Director::baseFolder() . '/' . $this->getField('Filename');
if(file_exists($imagefile)) {
$size = getimagesize($imagefile);
return ($dim === "string") ? "$size[0]x$size[1]" : $size[$dim];
} else {
return ($dim === "string") ? "file '$imagefile' not found" : null;
}
}
}
/**
* Get the width of this image.
* @return int
*/
function getWidth() {
return $this->getDimensions(0);
}
/**
* Get the height of this image.
* @return int
*/
function getHeight() {
return $this->getDimensions(1);
}
/**
* Get the orientation of this image.
* @return ORIENTATION_SQUARE | ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE
*/
function getOrientation() {
$width = $this->getWidth();
$height = $this->getHeight();
if($width > $height) {
return self::ORIENTATION_LANDSCAPE;
} elseif($height > $width) {
return self::ORIENTATION_PORTRAIT;
} else {
return self::ORIENTATION_SQUARE;
}
}
protected function onBeforeDelete() {
parent::onBeforeDelete();
$this->deleteFormattedImages();
}
}
/**
* A resized / processed {@link Image} object.
* When Image object are processed or resized, a suitable Image_Cached object is returned, pointing to the
* cached copy of the processed image.
*
* @package sapphire
* @subpackage filesystem
*/
class Image_Cached extends Image {
/**
* Create a new cached image.
* @param string $filename The filename of the image.
* @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods. Singletons
* don't have their defaults set.
*/
public function __construct($filename = null, $isSingleton = false) {
parent::__construct(array(), $isSingleton);
$this->ID = -1;
$this->Filename = $filename;
}
public function getRelativePath() {
return $this->getField('Filename');
}
/**
* Prevent creating new tables for the cached record
*
* @return false
*/
public function requireTable() {
return false;
}
/**
* Prevent writing the cached image to the database
*
* @throws Exception
*/
public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
throw new Exception("{$this->ClassName} can not be written back to the database.");
}
}