silverstripe-framework/model/Image.php
Damian Mooyman d8e9af8af8 API New Database abstraction layer. Ticket #7429
Database abstraction broken up into controller, connector, query builder, and schema manager, each independently configurable via YAML / Injector
Creation of new DBQueryGenerator for database specific generation of SQL
Support for parameterised queries, move of code base to use these over escaped conditions
Refactor of SQLQuery into separate query classes for each of INSERT UPDATE DELETE and SELECT
Support for PDO
Installation process upgraded to use new ORM
SS_DatabaseException created to handle database errors, maintaining details of raw sql and parameter details for user code designed interested in that data.
Renamed DB static methods to conform correctly to naming conventions (e.g. DB::getConn -> DB::get_conn)
3.2 upgrade docs
Performance Optimisation and simplification of code to use more concise API
API Ability for database adapters to register extensions to ConfigureFromEnv.php
2014-07-09 18:04:05 +12:00

778 lines
22 KiB
PHP

<?php
/**
* Represents an Image
*
* @package framework
* @subpackage filesystem
*/
class Image extends File {
const ORIENTATION_SQUARE = 0;
const ORIENTATION_PORTRAIT = 1;
const ORIENTATION_LANDSCAPE = 2;
private static $backend = "GDBackend";
private static $casting = array(
'Tag' => 'HTMLText',
);
/**
* @config
* @var int The width of an image thumbnail in a strip.
*/
private static $strip_thumbnail_width = 50;
/**
* @config
* @var int The height of an image thumbnail in a strip.
*/
private static $strip_thumbnail_height = 50;
/**
* @config
* @var int The width of an image thumbnail in the CMS.
*/
private static $cms_thumbnail_width = 100;
/**
* @config
* @var int The height of an image thumbnail in the CMS.
*/
private static $cms_thumbnail_height = 100;
/**
* @config
* @var int The width of an image thumbnail in the Asset section.
*/
private static $asset_thumbnail_width = 100;
/**
* @config
* @var int The height of an image thumbnail in the Asset section.
*/
private static $asset_thumbnail_height = 100;
/**
* @config
* @var int The width of an image preview in the Asset section.
*/
private static $asset_preview_width = 400;
/**
* @config
* @var int The height of an image preview in the Asset section.
*/
private static $asset_preview_height = 200;
public static function set_backend($backend) {
self::$backend = $backend;
}
public static function get_backend() {
return self::$backend;
}
/**
* 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();
}
public function getCMSFields() {
$fields = parent::getCMSFields();
$urlLink = "<div class='field readonly'>";
$urlLink .= "<label class='left'>"._t('AssetTableField.URL','URL')."</label>";
$urlLink .= "<span class='readonly'><a href='{$this->Link()}'>{$this->RelativeLink()}</a></span>";
$urlLink .= "</div>";
//attach the addition file information for an image to the existing FieldGroup create in the parent class
$fileAttributes = $fields->fieldByName('Root.Main.FilePreview')->fieldByName('FilePreviewData');
$fileAttributes->push(new ReadonlyField("Dimensions", _t('AssetTableField.DIM','Dimensions') . ':'));
return $fields;
}
/**
* 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
*/
public 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
*/
public function forTemplate() {
return $this->getTag();
}
/**
* File names are filtered through {@link FileNameFilter}, see class documentation
* on how to influence this behaviour.
*
* @deprecated 3.2
*/
public function loadUploadedImage($tmpFile) {
Deprecation::notice('3.2', 'Use the Upload::loadIntoFile()');
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, Config::inst()->get('Filesystem', 'folder_create_mask'));
}
if(!file_exists(ASSETS_PATH . "/$class")) {
mkdir(ASSETS_PATH . "/$class", Config::inst()->get('Filesystem', 'folder_create_mask'));
}
// Generate default filename
$nameFilter = FileNameFilter::create();
$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 = preg_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;
}
}
/**
* Resize the image by preserving aspect ratio, keeping the image inside the
* $width and $height
*
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image
*/
public function SetRatioSize($width, $height) {
// Prevent divide by zero on missing/blank file
if(empty($this->width) || empty($this->height)) return null;
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->width;
$heightRatio = $height / $this->height;
if( $widthRatio < $heightRatio ) {
// Target is higher aspect ratio than image, so check width
if($this->isWidth($width)) return $this;
} else {
// Target is wider aspect ratio than image, so check height
if($this->isHeight($height)) return $this;
}
// Item must be regenerated
return $this->getFormattedImage('SetRatioSize', $width, $height);
}
/**
* Resize the image by preserving aspect ratio, keeping the image inside the
* $width and $height
*
* @param Image_Backend $backend
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image_Backend
*/
public function generateSetRatioSize(Image_Backend $backend, $width, $height) {
return $backend->resizeRatio($width, $height);
}
/**
* Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
*
* @param integer $width The width to set
* @return Image
*/
public function SetWidth($width) {
return $this->isWidth($width)
? $this
: $this->getFormattedImage('SetWidth', $width);
}
/**
* Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
*
* @param Image_Backend $backend
* @param int $width The width to set
* @return Image_Backend
*/
public function generateSetWidth(Image_Backend $backend, $width) {
return $backend->resizeByWidth($width);
}
/**
* Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
*
* @param integer $height The height to set
* @return Image
*/
public function SetHeight($height) {
return $this->isHeight($height)
? $this
: $this->getFormattedImage('SetHeight', $height);
}
/**
* Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
*
* @param Image_Backend $backend
* @param integer $height The height to set
* @return Image_Backend
*/
public function generateSetHeight(Image_Backend $backend, $height){
return $backend->resizeByHeight($height);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
* @see Image::PaddedImage()
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image
*/
public function SetSize($width, $height) {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('SetSize', $width, $height);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
*
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image_Backend
*/
public function generateSetSize(Image_Backend $backend, $width, $height) {
return $backend->paddedResize($width, $height);
}
public function CMSThumbnail() {
return $this->getFormattedImage('CMSThumbnail');
}
/**
* Resize this image for the CMS. Use in templates with $CMSThumbnail.
* @return Image_Backend
*/
public function generateCMSThumbnail(Image_Backend $backend) {
return $backend->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 Image_Backend
*/
public function generateAssetLibraryPreview(Image_Backend $backend) {
return $backend->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 Image_Backend
*/
public function generateAssetLibraryThumbnail(Image_Backend $backend) {
return $backend->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 Image_Backend
*/
public function generateStripThumbnail(Image_Backend $backend) {
return $backend->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height'));
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $PaddedImage.
* @see Image::SetSize()
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image
*/
public function PaddedImage($width, $height, $backgroundColor='FFFFFF') {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('PaddedImage', $width, $height, $backgroundColor);
}
/**
* Resize this Image by both width and height, using padded resize. Use in templates with $PaddedImage.
*
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image_Backend
*/
public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
return $backend->paddedResize($width, $height, $backgroundColor);
}
/**
* Determine if this image is of the specified size
*
* @param integer $width Width to check
* @param integer $height Height to check
* @return boolean
*/
public function isSize($width, $height) {
return $this->isWidth($width) && $this->isHeight($height);
}
/**
* Determine if this image is of the specified width
*
* @param integer $width Width to check
* @return boolean
*/
public function isWidth($width) {
return !empty($width) && $this->getWidth() == $width;
}
/**
* Determine if this image is of the specified width
*
* @param integer $height Height to check
* @return boolean
*/
public function isHeight($height) {
return !empty($height) && $this->getHeight() == $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.
*
* Just pass the correct number of parameters expected by the working function
*
* @param string $format The name of the format.
* @return Image_Cached
*/
public function getFormattedImage($format) {
$args = func_get_args();
if($this->ID && $this->Filename && Director::fileExists($this->Filename)) {
$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
if(!file_exists(Director::baseFolder()."/".$cacheFile) || isset($_GET['flush'])) {
call_user_func_array(array($this, "generateFormattedImage"), $args);
}
$cached = new Image_Cached($cacheFile);
// Pass through the title so the templates can use it
$cached->Title = $this->Title;
// Pass through the parent, to store cached images in correct folder.
$cached->ParentID = $this->ParentID;
return $cached;
}
}
/**
* Return the filename for the cached image, given it's format name and arguments.
* @param string $format The format name.
* @return string
* @throws InvalidArgumentException
*/
public function cacheFilename($format) {
$args = func_get_args();
array_shift($args);
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . "/";
$format = $format . base64_encode(json_encode($args, JSON_NUMERIC_CHECK));
$filename = $format . "-" . $this->Name;
$patterns = $this->getFilenamePatterns($this->Name);
if (!preg_match($patterns['FullPattern'], $filename)) {
throw new InvalidArgumentException('Filename ' . $filename
. ' that should be used to cache a resized image is invalid');
}
return $folder . "_resampled/" . $filename;
}
/**
* 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.
*/
public function generateFormattedImage($format) {
$args = func_get_args();
$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
$backend = Injector::inst()->createWithArgs(self::$backend, array(
Director::baseFolder()."/" . $this->Filename
));
if($backend->hasImageResource()) {
$generateFunc = "generate$format";
if($this->hasMethod($generateFunc)){
array_shift($args);
array_unshift($args, $backend);
$backend = call_user_func_array(array($this, $generateFunc), $args);
if($backend){
$backend->writeTo(Director::baseFolder()."/" . $cacheFile);
}
} else {
user_error("Image::generateFormattedImage - Image $format public function not found.",E_USER_WARNING);
}
}
}
/**
* Generate a resized copy of this image with the given width & height.
* Use in templates with $ResizedImage.
*
* @param integer $width Width to resize to
* @param integer $height Height to resize to
* @return Image
*/
public function ResizedImage($width, $height) {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('ResizedImage', $width, $height);
}
/**
* Generate a resized copy of this image with the given width & height.
* Use in templates with $ResizedImage.
*
* @param Image_Backend $backend
* @param integer $width Width to resize to
* @param integer $height Height to resize to
* @return Image_Backend
*/
public function generateResizedImage(Image_Backend $backend, $width, $height) {
if(!$backend){
user_error("Image::generateFormattedImage - generateResizedImage is being called by legacy code"
. " or Image::\$backend is not set.",E_USER_WARNING);
}else{
return $backend->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
*
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image
*/
public function CroppedImage($width, $height) {
return $this->isSize($width, $height)
? $this
: $this->getFormattedImage('CroppedImage', $width, $height);
}
/**
* Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
* Use in templates with $CroppedImage
*
* @param Image_Backend $backend
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image_Backend
*/
public function generateCroppedImage(Image_Backend $backend, $width, $height) {
return $backend->croppedResize($width, $height);
}
/**
* Generate patterns that will help to match filenames of cached images
* @param string $filename Filename of source image
* @return array
*/
private function getFilenamePatterns($filename) {
$methodNames = $this->allMethodNames(true);
$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);
$base64Match = "[a-zA-Z0-9\/\r\n+]*={0,2}";
return array(
'FullPattern' => "/^((?P<Generator>{$generateFuncs})(?P<Args>" . $base64Match . ")\-)+"
. preg_quote($filename) . "$/i",
'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64Match . ")\-/i"
);
}
/**
* Generate a list of images that were generated from this image
*/
private function getGeneratedImages() {
$generatedImages = array();
$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);
}
}
$pattern = $this->getFilenamePatterns($this->Name);
foreach($cachedFiles as $cfile) {
if(preg_match($pattern['FullPattern'], $cfile, $matches)) {
if(Director::fileExists($cacheDir . $cfile)) {
$subFilename = substr($cfile, 0, -1 * strlen($this->Name));
preg_match_all($pattern['GeneratorPattern'], $subFilename, $subMatches, PREG_SET_ORDER);
$generatorArray = array();
foreach ($subMatches as $singleMatch) {
$generatorArray[] = array('Generator' => $singleMatch['Generator'],
'Args' => json_decode(base64_decode($singleMatch['Args'])));
}
// Using array_reverse is important, as a cached image will
// have the generators settings in the filename in reversed
// order: the last generator given in the filename is the
// first that was used. Later resizements are prepended
$generatedImages[] = array ( 'FileName' => $cacheDir . $cfile,
'Generators' => array_reverse($generatorArray) );
}
}
}
return $generatedImages;
}
/**
* Regenerate all of the formatted cached images for this image.
*
* @return int The number of formatted images regenerated
*/
public function regenerateFormattedImages() {
if(!$this->Filename) return 0;
// Without this, not a single file would be written
// caused by a check in getFormattedImage()
$_GET['flush'] = 1;
$numGenerated = 0;
$generatedImages = $this->getGeneratedImages();
$doneList = array();
foreach($generatedImages as $singleImage) {
$cachedImage = $this;
if (in_array($singleImage['FileName'], $doneList) ) continue;
foreach($singleImage['Generators'] as $singleGenerator) {
$args = array_merge(array($singleGenerator['Generator']), $singleGenerator['Args']);
$cachedImage = call_user_func_array(array($cachedImage, "getFormattedImage"), $args);
}
$doneList[] = $singleImage['FileName'];
$numGenerated++;
}
return $numGenerated;
}
/**
* 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;
$generatedImages = $this->getGeneratedImages();
foreach($generatedImages as $singleImage) {
unlink($singleImage['FileName']);
$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
*/
public 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
*/
public function getWidth() {
return $this->getDimensions(0);
}
/**
* Get the height of this image.
* @return int
*/
public function getHeight() {
return $this->getDimensions(1);
}
/**
* Get the orientation of this image.
* @return ORIENTATION_SQUARE | ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE
*/
public 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;
}
}
public function onAfterUpload() {
$this->deleteFormattedImages();
parent::onAfterUpload();
}
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 framework
* @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.");
}
}