NEW: Enable multiple image manipulation back-ends on the Image class

This commit is contained in:
Justin Martin 2012-10-24 15:28:39 -07:00
parent d8c920285c
commit d24b586830
7 changed files with 573 additions and 86 deletions

View File

@ -4,7 +4,7 @@
* @package framework
* @subpackage filesystem
*/
class GD extends Object {
class GDBackend extends Object implements Image_Backend {
protected $gd, $width, $height;
protected $quality;
@ -28,29 +28,48 @@ class GD extends Object {
// We use getimagesize instead of extension checking, because sometimes extensions are wrong.
list($width, $height, $type, $attr) = getimagesize($filename);
switch($type) {
case 1: if(function_exists('imagecreatefromgif')) $this->setGD(imagecreatefromgif($filename)); break;
case 2: if(function_exists('imagecreatefromjpeg')) $this->setGD(imagecreatefromjpeg($filename)); break;
case 3: if(function_exists('imagecreatefrompng')) {
$img = imagecreatefrompng($filename);
imagesavealpha($img, true); // save alphablending setting (important)
$this->setGD($img);
case 1:
if(function_exists('imagecreatefromgif'))
$this->setImageResource(imagecreatefromgif($filename));
break;
case 2:
if(function_exists('imagecreatefromjpeg'))
$this->setImageResource(imagecreatefromjpeg($filename));
break;
case 3:
if(function_exists('imagecreatefrompng')) {
$img = imagecreatefrompng($filename);
imagesavealpha($img, true); // save alphablending setting (important)
$this->setImageResource($img);
}
break;
}
}
}
$this->quality = self::$default_quality;
parent::__construct();
}
public function setImageResource($resource) {
$this->gd = $resource;
$this->width = imagesx($resource);
$this->height = imagesy($resource);
}
public function setGD($gd) {
$this->gd = $gd;
$this->width = imagesx($gd);
$this->height = imagesy($gd);
Deprecation::notice('3.1', 'Use GD::setImageResource instead',
Deprecation::SCOPE_CLASS);
return $this->setImageResource($gd);
}
public function getImageResource() {
return $this->gd;
}
public function getGD() {
return $this->gd;
Deprecation::notice('3.1', 'GD::getImageResource instead',
Deprecation::SCOPE_CLASS);
return $this->getImageResource();
}
/**
@ -106,7 +125,7 @@ class GD extends Object {
imagecopyresampled($newGD, $this->gd, 0,0, $srcX, $srcY, $width, $height, $srcWidth, $srcHeight);
}
$output = clone $this;
$output->setGD($newGD);
$output->setImageResource($newGD);
return $output;
}
@ -121,10 +140,21 @@ class GD extends Object {
return $gd;
}
public function hasGD() {
/**
* hasImageResource
*
* @return boolean
*/
public function hasImageResource() {
return $this->gd ? true : false;
}
public function hasGD() {
Deprecation::notice('3.1', 'GD::hasImageResource instead',
Deprecation::SCOPE_CLASS);
return $this->hasImageResource();
}
/**
* Resize an image, skewing it as necessary.
@ -153,7 +183,7 @@ class GD extends Object {
imagecopyresampled($newGD, $this->gd, 0,0, 0, 0, $width, $height, $this->width, $this->height);
$output = clone $this;
$output->setGD($newGD);
$output->setImageResource($newGD);
return $output;
}
@ -175,7 +205,7 @@ class GD extends Object {
$newGD = $this->rotatePixelByPixel($angle);
}
$output = clone $this;
$output->setGD($newGD);
$output->setImageResource($newGD);
return $output;
}
@ -237,7 +267,7 @@ class GD extends Object {
imagecopyresampled($newGD, $this->gd, 0, 0, $left, $top, $width, $height, $width, $height);
$output = clone $this;
$output->setGD($newGD);
$output->setImageResource($newGD);
return $output;
}
@ -356,7 +386,7 @@ class GD extends Object {
$destWidth, $destHeight, $this->width, $this->height);
}
$output = clone $this;
$output->setGD($newGD);
$output->setImageResource($newGD);
return $output;
}
@ -388,7 +418,7 @@ class GD extends Object {
}
$output = clone $this;
$output->setGD($newGD);
$output->setImageResource($newGD);
return $output;
}
@ -429,4 +459,4 @@ class GD extends Object {
}
}
class_alias("GDBackend", "GD");

View File

@ -0,0 +1,266 @@
<?php
class ImagickBackend extends Imagick implements Image_Backend {
protected static $default_quality = 75;
/**
* __construct
*
* @param string $filename = null
* @return void
*/
public function __construct($filename = null) {
if(is_string($filename)) {
parent::__construct($filename);
} else {
self::setImageCompressionQuality(self::$default_quality);
}
}
/**
* writeTo
*
* @param string $path
* @return void
*/
public function writeTo($path) {
Filesystem::makeFolder(dirname($path));
if(is_dir(dirname($path)))
self::writeImage($path);
}
/**
* set_default_quality
*
* @static
* @param int $quality
* @return void
*/
public static function set_default_quality($quality) {
self::$default_quality = $quality;
}
/**
* setQuality
*
* @param int $quality
* @return void
*/
public function setQuality($quality) {
self::setImageCompressionQuality($quality);
}
/**
* setImageResource
*
* Set the backend-specific resource handling the manipulations. Replaces Image::setGD()
*
* @param mixed $resource
* @return void
*/
public function setImageResource($resource) {
trigger_error("Imagick::setImageResource is not supported", E_USER_ERROR);
}
/**
* getImageResource
*
* Get the backend-specific resource handling the manipulations. Replaces Image::getGD()
*
* @return mixed
*/
public function getImageResource() {
return $this;
}
/**
* hasImageResource
*
* @return boolean
*/
public function hasImageResource() {
return true; // $this is the resource, necessarily
}
/**
* resize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function resize($width, $height) {
if(!$this->valid()) return;
$width = round($width);
$height = round($height);
$geometry = $this->getImageGeometry();
// Check that a resize is actually necessary.
if ($width == $geometry["width"] && $height == $geometry["height"]) {
return $this;
}
if(!$width && !$height) user_error("No dimensions given", E_USER_ERROR);
if(!$width) user_error("Width not given", E_USER_ERROR);
if(!$height) user_error("Height not given", E_USER_ERROR);
$new = clone $this;
$new->resizeImage($width, $height, self::FILTER_LANCZOS, 1);
return $new;
}
/**
* resizeRatio
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false) {
if(!$this->valid()) return;
$geometry = $this->getImageGeometry();
$widthRatio = $maxWidth / $geometry["width"];
$heightRatio = $maxHeight / $geometry["height"];
if( $widthRatio < $heightRatio )
return $useAsMinimum ? $this->resizeByHeight( $maxHeight ) : $this->resizeByWidth( $maxWidth );
else
return $useAsMinimum ? $this->resizeByWidth( $maxWidth ) : $this->resizeByHeight( $maxHeight );
}
/**
* resizeByWidth
*
* @param int $width
* @return Image_Backend
*/
public function resizeByWidth($width) {
if(!$this->valid()) return;
$geometry = $this->getImageGeometry();
$heightScale = $width / $geometry["width"];
return $this->resize( $width, $heightScale * $geometry["height"] );
}
/**
* resizeByHeight
*
* @param int $height
* @return Image_Backend
*/
public function resizeByHeight($height) {
if(!$this->valid()) return;
$geometry = $this->getImageGeometry();
$scale = $height / $geometry["height"];
return $this->resize( $scale * $geometry["width"], $height );
}
/**
* paddedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF") {
if(!$this->valid()) return;
$width = round($width);
$height = round($height);
$geometry = $this->getImageGeometry();
// Check that a resize is actually necessary.
if ($width == $geometry["width"] && $height == $geometry["height"]) {
return $this;
}
$new = clone $this;
$new->setBackgroundColor($backgroundColor);
$destAR = $width / $height;
if ($geometry["width"] > 0 && $geometry["height"] > 0) {
// We can't divide by zero theres something wrong.
$srcAR = $geometry["width"] / $geometry["height"];
// Destination narrower than the source
if($destAR > $srcAR) {
$destY = 0;
$destHeight = $height;
$destWidth = round( $height * $srcAR );
$destX = round( ($width - $destWidth) / 2 );
// Destination shorter than the source
} else {
$destX = 0;
$destWidth = $width;
$destHeight = round( $width / $srcAR );
$destY = round( ($height - $destHeight) / 2 );
}
$new->extentImage($width, $height, $destX, $destY);
}
return $new;
}
/**
* croppedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function croppedResize($width, $height) {
if(!$this->valid()) return;
$width = round($width);
$height = round($height);
$geometry = $this->getImageGeometry();
// Check that a resize is actually necessary.
if ($width == $geometry["width"] && $height == $geometry["height"]) {
return $this;
}
$new = clone $this;
$new->setBackgroundColor($backgroundColor);
$destAR = $width / $height;
if ($geometry["width"] > 0 && $geometry["height"] > 0) {
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if($destAR < $srcAR) {
$srcY = 0;
$srcHeight = $this->height;
$srcWidth = round( $this->height * $destAR );
$srcX = round( ($this->width - $srcWidth) / 2 );
// Destination shorter than the source
} else {
$srcX = 0;
$srcWidth = $this->width;
$srcHeight = round( $this->width / $destAR );
$srcY = round( ($this->height - $srcHeight) / 2 );
}
$new->extentImage($width, $height, $destX, $destY);
}
return $new;
}
}

View File

@ -12,6 +12,8 @@ class Image extends File {
const ORIENTATION_PORTRAIT = 1;
const ORIENTATION_LANDSCAPE = 2;
static $backend = "GDBackend";
static $casting = array(
'Tag' => 'HTMLText',
);
@ -59,6 +61,14 @@ class Image extends File {
*/
public 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.
*/
@ -195,32 +205,32 @@ class Image extends File {
return $this->getFormattedImage('SetRatioSize', $width, $height);
}
public function generateSetRatioSize(GD $gd, $width, $height) {
return $gd->resizeRatio($width, $height);
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.
* @return GD
* @return Image_Backend
*/
public function generateSetWidth(GD $gd, $width) {
return $gd->resizeByWidth($width);
public function generateSetWidth(Image_Backend $backend, $width) {
return $backend->resizeByWidth($width);
}
/**
* Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
* @return GD
* @return Image_Backend
*/
public function generateSetHeight(GD $gd, $height){
return $gd->resizeByHeight($height);
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.
* @return GD
* @return Image_Backend
*/
public function generateSetSize(GD $gd, $width, $height) {
return $gd->paddedResize($width, $height);
public function generateSetSize(Image_Backend $backend, $width, $height) {
return $backend->paddedResize($width, $height);
}
public function CMSThumbnail() {
@ -229,38 +239,38 @@ class Image extends File {
/**
* Resize this image for the CMS. Use in templates with $CMSThumbnail.
* @return GD
* @return Image_Backend
*/
public function generateCMSThumbnail(GD $gd) {
return $gd->paddedResize($this->stat('cms_thumbnail_width'),$this->stat('cms_thumbnail_height'));
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 GD
* @return Image_Backend
*/
public function generateAssetLibraryPreview(GD $gd) {
return $gd->paddedResize($this->stat('asset_preview_width'),$this->stat('asset_preview_height'));
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 GD
* @return Image_Backend
*/
public function generateAssetLibraryThumbnail(GD $gd) {
return $gd->paddedResize($this->stat('asset_thumbnail_width'),$this->stat('asset_thumbnail_height'));
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 GD
* @return Image_Backend
*/
public function generateStripThumbnail(GD $gd) {
return $gd->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height'));
public function generateStripThumbnail(Image_Backend $backend) {
return $backend->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height'));
}
public function generatePaddedImage(GD $gd, $width, $height) {
return $gd->paddedResize($width, $height);
public function generatePaddedImage(Image_Backend $backend, $width, $height) {
return $backend->paddedResize($width, $height);
}
/**
@ -312,16 +322,18 @@ class Image extends File {
*/
public function generateFormattedImage($format, $arg1 = null, $arg2 = null) {
$cacheFile = $this->cacheFilename($format, $arg1, $arg2);
$gd = new GD(Director::baseFolder()."/" . $this->Filename);
if($gd->hasGD()){
$backend = Injector::inst()->createWithArgs(self::$backend, array(
Director::baseFolder()."/" . $this->Filename
));
if($backend->hasImageResource()) {
$generateFunc = "generate$format";
if($this->hasMethod($generateFunc)){
$gd = $this->$generateFunc($gd, $arg1, $arg2);
if($gd){
$gd->writeTo(Director::baseFolder()."/" . $cacheFile);
$backend = $this->$generateFunc($backend, $arg1, $arg2);
if($backend){
$backend->writeTo(Director::baseFolder()."/" . $cacheFile);
}
} else {
@ -334,12 +346,12 @@ class Image extends File {
* Generate a resized copy of this image with the given width & height.
* Use in templates with $ResizedImage.
*/
public function generateResizedImage($gd, $width, $height) {
if(is_numeric($gd) || !$gd){
public function generateResizedImage(Image_Backend $backend, $width, $height) {
if(!$backend){
user_error("Image::generateFormattedImage - generateResizedImage is being called by legacy code"
. " or gd is not set.",E_USER_WARNING);
. " or Image::\$backend is not set.",E_USER_WARNING);
}else{
return $gd->resize($width, $height);
return $backend->resize($width, $height);
}
}
@ -347,8 +359,8 @@ class Image extends File {
* Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
* Use in templates with $CroppedImage
*/
public function generateCroppedImage($gd, $width, $height) {
return $gd->croppedResize($width, $height);
public function generateCroppedImage(Image_Backend $backend, $width, $height) {
return $backend->croppedResize($width, $height);
}
/**

122
model/Image_Backend.php Normal file
View File

@ -0,0 +1,122 @@
<?php
/**
* Image_Backend
*
* A backend for manipulation of images via the Image class
*
* @package framework
* @subpackage filesystem
*/
interface Image_Backend {
/**
* __construct
*
* @param string $filename = null
* @return void
*/
public function __construct($filename = null);
/**
* writeTo
*
* @param string $path
* @return void
*/
public function writeTo($path);
/**
* set_default_quality
*
* @static
* @param int $quality
* @return void
*/
public static function set_default_quality($quality);
/**
* setQuality
*
* @param int $quality
* @return void
*/
public function setQuality($quality);
/**
* setImageResource
*
* Set the backend-specific resource handling the manipulations. Replaces Image::setGD()
*
* @param mixed $resource
* @return void
*/
public function setImageResource($resource);
/**
* getImageResource
*
* Get the backend-specific resource handling the manipulations. Replaces Image::getGD()
*
* @return mixed
*/
public function getImageResource();
/**
* hasImageResource
*
* @return boolean
*/
public function hasImageResource();
/**
* resize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function resize($width, $height);
/**
* resizeRatio
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false);
/**
* resizeByWidth
*
* @param int $width
* @return Image_Backend
*/
public function resizeByWidth($width);
/**
* resizeByHeight
*
* @param int $height
* @return Image_Backend
*/
public function resizeByHeight($height);
/**
* paddedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF");
/**
* croppedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function croppedResize($width, $height);
}

View File

@ -0,0 +1,28 @@
<?php
class GDImageTest extends ImageTest {
public function setUp() {
if(!extension_loaded("gd")) {
$this->markTestSkipped("The GD extension is required");
$this->skipTest = true;
parent::setUp();
return;
}
parent::setUp();
Image::set_backend("GDBackend");
// Create a test files for each of the fixture references
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
$image = imagecreatetruecolor(300,300);
imagepng($image, BASE_PATH."/{$file->Filename}");
imagedestroy($image);
$file->write();
}
}
}

View File

@ -7,10 +7,20 @@
class ImageTest extends SapphireTest {
static $fixture_file = 'ImageTest.yml';
protected $origBackend;
public function setUp() {
if(get_class($this) == "ImageTest")
$this->skipTest = true;
parent::setUp();
if($this->skipTest)
return;
$this->origBackend = Image::get_backend();
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
// Create a test folders for each of the fixture references
@ -21,18 +31,28 @@ class ImageTest extends SapphireTest {
if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename");
}
// Create a test files for each of the fixture references
}
public function tearDown() {
Image::set_backend($this->origBackend);
/* Remove the test files that we've created */
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
$image = imagecreatetruecolor(300,300);
imagepng($image, BASE_PATH."/$file->Filename");
imagedestroy($image);
$file->write();
if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename");
}
/* Remove the test folders that we've crated */
$folderIDs = $this->allFixtureIDs('Folder');
foreach($folderIDs as $folderID) {
$folder = DataObject::get_by_id('Folder', $folderID);
if($folder && file_exists(BASE_PATH."/$folder->Filename")) {
Filesystem::removeFolder(BASE_PATH."/$folder->Filename");
}
}
parent::tearDown();
}
public function testGetTagWithTitle() {
@ -61,26 +81,6 @@ class ImageTest extends SapphireTest {
$this->assertEquals($expected, $actual);
}
public function tearDown() {
/* Remove the test files that we've created */
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename");
}
/* Remove the test folders that we've crated */
$folderIDs = $this->allFixtureIDs('Folder');
foreach($folderIDs as $folderID) {
$folder = DataObject::get_by_id('Folder', $folderID);
if($folder && file_exists(BASE_PATH."/$folder->Filename")) {
Filesystem::removeFolder(BASE_PATH."/$folder->Filename");
}
}
parent::tearDown();
}
public function testMultipleGenerateManipulationCalls() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle');

View File

@ -0,0 +1,29 @@
<?php
class ImagickImageTest extends ImageTest {
public function setUp() {
if(!extension_loaded("imagick")) {
$this->markTestSkipped("The Imagick extension is not available.");
$this->skipTest = true;
parent::setUp();
return;
}
parent::setUp();
Image::set_backend("ImagickBackend");
// Create a test files for each of the fixture references
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
$image = new Imagick();
$image->newImage(300,300, new ImagickPixel("white"));
$image->setImageFormat("png");
$image->writeImage(BASE_PATH."/{$file->Filename}");
$file->write();
}
}
}