Newer
Older
Import / web / www.xiaofrog.com / gallery / modules / gd / classes / GdToolkit.class
<?php
/*
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2007 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.
 */

GalleryCoreApi::requireOnce('modules/core/classes/GalleryToolkit.class');
GalleryCoreApi::requireOnce('modules/gd/classes/GdToolkitHelper.class');

/**
 * A Gd version of GalleryToolkit
 * @package Gd
 * @subpackage Classes
 * @author Ernesto Baschny <ernst@baschny.de>
 * @version $Revision: 15513 $
 */
class GdToolkit extends GalleryToolkit {

    /**
     * Our private GdFunctionality instance
     * @var $_gdFunctionality
     * @access private
     */
    var $_gdFunctionality;

    /**
     * Does our GD library has a working imageCreateTrueColor?
     * @access private
     */
    var $_hasImageCreateTrueColor;

    /**
     * Return the current set gdFunctionality.
     * Default is GdFunctionality.class if no other is set.
     *
     * @return object implementation of GdFunctionality
     * @access private
     */
    function &_getGdFunctionality() {
	if (!isset($this->_gdFunctionality)) {
	    GalleryCoreApi::requireOnce('modules/gd/classes/GdFunctionality.class');
	    $gd =& new GdFunctionality();
	    $this->setGdFunctionality($gd);
	}
	return $this->_gdFunctionality;
    }

    /**
     * Set a different GdFunctionality object to be used by this Toolkit.
     * This is useful on our phpunit tests, which will make this Toolkit
     * use a pseudo-gd-implementation that simulates different PHP versions.
     * @return nothing
     */
    function setGdFunctionality(&$obj) {
	$this->_gdFunctionality =& $obj;
	unset($this->_hasImageCreateTrueColor);
    }

    /**
     * Do we have a working version of imageCreateTrueColor()?
     *
     * @return array object GalleryStatus
     *	       boolean true if this function should work
     * @access private
     */
    function _hasImageCreateTrueColor() {
	if (!isset($this->_hasImageCreateTrueColor)) {
	    $gd =& $this->_getGdFunctionality();
	    /* Remember the info for later calls */
	    list ($ret, $this->_hasImageCreateTrueColor) =
		GdToolkitHelper::hasImageCreateTrueColor($gd);
	    if ($ret) {
		return array($ret, null);
	    }
	}
	return array(null, $this->_hasImageCreateTrueColor);
    }

    /**
     * @see GalleryToolkit::getProperty
     */
    function getProperty($mimeType, $propertyName, $sourceFilename) {
	switch($propertyName) {
	case 'dimensions':
	    list ($ret, $width, $height) = $this->_getImageDimensions($mimeType, $sourceFilename);
	    if ($ret) {
		return array($ret, null);
	    }
	    $results = array((int)$width, (int)$height);
	    break;

	default:
	    return array(GalleryCoreApi::error(ERROR_UNIMPLEMENTED), null);
	}

	return array(null, $results);
    }

    /**
     * @see GalleryToolkit::performOperation
     */
    function performOperation($mimeType, $operationName, $sourceFilename,
			      $destFilename, $parameters, $context=array()) {
	$gd =& $this->_getGdFunctionality();

	global $gallery;
	if ($gallery->getDebug()) {
	    $gallery->debug(sprintf('GdToolkit::performOperation(%s,%s,%s,%s,%s)',
				    $mimeType, $operationName, $sourceFilename,
				    $destFilename, join('|', $parameters)));
	}

	$outputMimeType = $mimeType;
	list ($ret, $sourceRes) = $this->_getImageResource($mimeType, $sourceFilename);
	if ($ret) {
	    return array($ret, null, null);
	}

	if (in_array($operationName, array('thumbnail', 'scale', 'resize'))) {
	    $usePercent = substr($parameters[0], -1) == '%'
		|| (isset($parameters[1]) && substr($parameters[1], -1) == '%');
	    if ($usePercent || $operationName != 'resize') {
		list ($ret, $width, $height) = $this->_getImageDimensionsForResource($sourceRes);
		if ($ret) {
		    $this->_free($sourceRes);
		    return array($ret, null, null);
		}
	    }
	    if ($usePercent) {
		/* Convert percentages to real image dimensions */
		if (substr($parameters[0], -1) == '%') {
		    $parameters[0] = (int)round($width * rtrim($parameters[0], '%') / 100);
		}
		if (isset($parameters[1]) && substr($parameters[1], -1) == '%') {
		    $parameters[1] = (int)round($height * rtrim($parameters[1], '%') / 100);
		}
	    }
	}

	switch($operationName) {
	case 'thumbnail':
	case 'scale':
	    /* $parameters[0]: target width, [1]: optional target height */
	    $targetHeight = empty($parameters[1]) ? $parameters[0] : $parameters[1];
	    /* Don't enlarge images for a thumbnail */
	    if ($operationName == 'thumbnail'
		    && $width <= $parameters[0] && $height <= $targetHeight) {
		break;
	    }

	    list ($destWidth, $destHeight) = GalleryUtilities::scaleDimensionsToFit(
					     $width, $height, $parameters[0], $targetHeight);
	    list ($ret, $destRes) = $this->_resizeImageResource($sourceRes,
								$destWidth, $destHeight);
	    $this->_free($sourceRes);
	    if ($ret) {
		return array($ret, null, null);
	    }
	    if (isset($context['width'])) {
		$context['width'] = $destWidth;
		$context['height'] = $destHeight;
	    }
	    break;

	case 'resize':
	    /*
	     * $parameters[0]: target width
	     * $parameters[1]: target height
	     */
	    list ($ret, $destRes) = $this->_resizeImageResource($sourceRes,
								$parameters[0], $parameters[1]);
	    $this->_free($sourceRes);
	    if ($ret) {
		return array($ret, null, null);
	    }
	    if (isset($context['width'])) {
		$context['width'] = $parameters[0];
		$context['height'] = $parameters[1];
	    }
	    break;

	case 'rotate':
	    /*
	     * PHP >= 4.3.0
	     * $parameters[0]: rotation degrees
	     */
	    $degrees = 0 - $parameters[0];
	    list ($ret, $destRes) = $gd->imageRotate($sourceRes, $degrees, 0);
	    $this->_free($sourceRes);
	    if ($ret) {
		return array($ret, null, null);
	    }
	    if (isset($context['width']) && ($degrees == 90 || $degrees == -90)) {
		$tmp = $context['width'];
		$context['width'] = $context['height'];
		$context['height'] = $tmp;
	    }
	    break;

	case 'crop':
	    /*
	     * $parameters[0]: left edge %
	     * $parameters[1]: top edge %
	     * $parameters[2]: width %
	     * $parameters[3]: height %
	     */

	    /* source dimensions are required to convert from percentages to pixels  */
	    list ($ret, $width, $height) = $this->_getImageDimensionsForResource($sourceRes);
	    if ($ret) {
		$this->_free($sourceRes);
		return array($ret, null, null);
	    }
	    $pixelX = round($parameters[0] / 100 * $width);
	    $pixelY = round($parameters[1] / 100 * $height);
	    $pixelWidth = round($parameters[2] / 100 * $width);
	    $pixelHeight = round($parameters[3] / 100 * $height);

	    list ($ret, $destRes) = $this->_getTrueColorImageRes($pixelWidth, $pixelHeight);
	    if ($ret) {
		$this->_free($sourceRes);
		return array($ret, null, null);
	    }
	    $ret = $gd->imageCopy($destRes, $sourceRes,
				  0, 0,				/* dst x,y */
				  $pixelX, $pixelY, $pixelWidth, $pixelHeight);
	    $this->_free($sourceRes);
	    if ($ret) {
		$this->_free($destRes);
		return array($ret, null, null);
	    }
	    if (isset($context['width'])) {
		$context['width'] = $pixelWidth;
		$context['height'] = $pixelHeight;
	    }
	    break;

	case 'convert-to-image/jpeg':
	    $destRes = $sourceRes;
	    $outputMimeType = 'image/jpeg';
	    break;

	case 'composite':
	    /*
	     * $parameters[0]: overlay path
	     * $parameters[1]: overlay mime type
	     * $parameters[2]: overlay width
	     * $parameters[3]: overlay height
	     * $parameters[6]: alignment type
	     * $parameters[4]: alignment x %
	     * $parameters[5]: alignment y %
	     */
	    $cmd = 'composite';
	    $compositeOverlayPath = $parameters[0];
	    $compositeOverlayMimeType = $parameters[1];
	    $compositeWidth = $parameters[2];
	    $compositeHeight = $parameters[3];
	    $compositeAlignmentType = $parameters[4];
	    $compositeAlignX = $parameters[5];
	    $compositeAlignY = $parameters[6];

	    list ($ret, $sourceWidth, $sourceHeight) =
		$this->_getImageDimensionsForResource($sourceRes);
	    if ($ret) {
		$this->_free($sourceRes);
		return array($ret, null, null);
	    }

	    switch ($compositeAlignmentType) {
	    case 'top-left':
		$compositeAlignX = 0;
		$compositeAlignY = 0;
		break;

	    case 'top':
		$compositeAlignX = 50;
		$compositeAlignY = 0;
		break;

	    case 'top-right':
		$compositeAlignX = 100;
		$compositeAlignY = 0;
		break;

	    case 'left':
		$compositeAlignX = 0;
		$compositeAlignY = 50;
		break;

	    case 'center':
		$compositeAlignX = 50;
		$compositeAlignY = 50;
		break;

	    case 'right':
		$compositeAlignX = 100;
		$compositeAlignY = 50;
		break;

	    case 'bottom-left':
		$compositeAlignX = 0;
		$compositeAlignY = 100;
		break;

	    case 'bottom':
		$compositeAlignX = 50;
		$compositeAlignY = 100;
		break;

	    case 'bottom-right':
		$compositeAlignX = 100;
		$compositeAlignY = 100;
		break;

	    case 'manual':
		/* Use the alignments we received */
		break;

	    default:
		$this->_free($sourceRes);
		return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
				    "Unknown composite alignment type: $compositeAlignmentType"),
			     null, null);
	    }

	    /* Convert from percentages to pixels */
	    $compositeAlignX = (int)($compositeAlignX / 100 * ($sourceWidth - $compositeWidth));
	    $compositeAlignY = (int)($compositeAlignY / 100 * ($sourceHeight - $compositeHeight));

	    /* Clip to our bounding box */
	    $compositeAlignX = min($compositeAlignX, $sourceWidth - $compositeWidth);
	    $compositeAlignX = max(0, $compositeAlignX);
	    $compositeAlignY = min($compositeAlignY, $sourceHeight - $compositeHeight);
	    $compositeAlignY = max(0, $compositeAlignY);

	    $dataDir = $gallery->getConfig('data.gallery.base');
	    list ($ret, $overlayRes) = $this->_getImageResource($compositeOverlayMimeType,
								$dataDir . $compositeOverlayPath);
	    if ($ret) {
		$this->_free($sourceRes);
		return array($ret, null, null);
	    }

	    $destRes = $sourceRes;
	    /* Don't use imageCopyMerge here, as it will lose alpha-transparency */
	    $ret = $gd->imageCopy($destRes, $overlayRes,
				       $compositeAlignX, $compositeAlignY,
				       0, 0,
				       $compositeWidth, $compositeHeight);
	    $this->_free($overlayRes);
	    if ($ret) {
		return array($ret, null, null);
	    }
	    break;

	case 'compress':
	    $targetSize = $parameters[0];
	    $fileSize = $gd->filesize($sourceFilename) >> 10; /* Size in KB */
	    if ($fileSize <= $targetSize) {
		break;
	    }
	    /* Use module quality parameter as initial guess */
	    list ($ret, $quality) =
		GalleryCoreApi::getPluginParameter('module', 'gd', 'jpegQuality');
	    if ($ret) {
		$this->_free($sourceRes);
		return array($ret, null, null);
	    }
	    $maxQuality = 100;
	    $minQuality = 5;
	    do {
		$ret = $this->_saveImageResourceToFile($sourceRes, $destFilename, $outputMimeType,
						       $quality);
		if ($ret) {
		    $this->_free($sourceRes);
		    return array($ret, null, null);
		}
		clearstatcache();
		$fileSize = $gd->filesize($destFilename) >> 10;
		if ($fileSize >= $targetSize) {
		    $maxQuality = $quality;
		}
		if ($fileSize <= $targetSize) {
		    $minQuality = $quality;
		}
		$quality = round(($minQuality + $maxQuality) / 2);
	    } while ($maxQuality - $minQuality > 2
		   && abs(($fileSize - $targetSize) / $targetSize) > 0.02);
	    $this->_free($sourceRes);
	    return array(null, $outputMimeType, $context);

	default:
	    $this->_free($sourceRes);
	    return array(GalleryCoreApi::error(ERROR_UNSUPPORTED_OPERATION, __FILE__, __LINE__,
					       "$operationName $mimeType"), null, null);
	}

	if (isset($destRes)) {
	    $ret = $this->_saveImageResourceToFile($destRes, $destFilename, $outputMimeType);
	    $this->_free($destRes);
	    if ($ret) {
		return array($ret, null, null);
	    }
	} else {
	    /* Just copy the source to the destination */
	    if ($sourceFilename != $destFilename) {
		if (!$gd->copy($sourceFilename, $destFilename)) {
		    $this->_free($sourceRes);
		    return array(GalleryCoreApi::error(ERROR_PLATFORM_FAILURE), null, null);
		}
	    }
	}

	return array(null, $outputMimeType, $context);
    }

    /**
     * Return a GD image resource for the given filename so we can perform other operations on it.
     *
     * @param string $mimeType the mime-type of the image
     * @param string $filename the path to the file to read
     * @return array object GalleryStatus a status code
     *	       resource the GD resource
     * @access private
     */
    function _getImageResource($mimeType, $filename) {
	$gd =& $this->_getGdFunctionality();

	$res = null;
	switch ($mimeType) {
	case 'image/gif':
	    list ($ret, $res) = $gd->imageCreateFromGif($filename);
	    break;
	case 'image/jpeg':
	    list ($ret, $res) = $gd->imageCreateFromJpeg($filename);
	    break;
	case 'image/png':
	    list ($ret, $res) = $gd->imageCreateFromPng($filename);
	    break;
	case 'image/vnd.wap.wbmp':
	    list ($ret, $res) = $gd->imageCreateFromWbmp($filename);
	    break;
	case 'image/x-xpixmap':
	    list ($ret, $res) = $gd->imageCreateFromXpm($filename);
	    break;
	case 'image/x-xbitmap':
	    list ($ret, $res) = $gd->imageCreateFromXbm($filename);
	    break;
	}
	if ($ret) {
	    return array($ret, null);
	}
	if (!$res) {
	    return array(GalleryCoreApi::error(ERROR_TOOLKIT_FAILURE), null);
	}
	return array(null, $res);
    }

    /**
     * Create a truecolor GD resource with the specified sizes.
     *
     * On GD >= 2.0.1 there is a function to do this directly.  On earlier versions, we need to
     * create a temporary JPEG image and create the image from that.
     *
     * @return array object GalleryStatus
     *	       resource the GD resource
     * @access private
     */
    function _getTrueColorImageRes($width, $height) {
	$gd =& $this->_getGdFunctionality();
	list ($ret, $hasImageCreateTrueColor) = $this->_hasImageCreateTrueColor();
	if ($ret) {
	    return array($ret, null);
	}
	if ($hasImageCreateTrueColor) {
	    list ($ret, $res) = $gd->imageCreateTruecolor($width, $height);
	    if ($ret) {
		return array($ret, null);
	    }
	} else {
	    /* Do something else to get a truecolor GD resource */
	    global $gallery;
	    $tmpDir = $gallery->getConfig('data.gallery.tmp');
	    $tmpFile = $gd->tempnam($tmpDir, 'gd');
	    /* Create a temporary jpeg file ... */
	    list ($ret, $tmpRes) = $gd->imageCreate($width, $height);
	    if ($ret) {
		return array($ret, null);
	    }
	    $ret = $gd->imageJpeg($tmpRes, $tmpFile);
	    if ($ret) {
		return array($ret, null);
	    }
	    /* ... and load it back into a resource (now truecolor) */
	    list ($ret, $res) = $gd->imageCreateFromJpeg($tmpFile);
	    if ($ret) {
		return array($ret, null);
	    }
	    $gd->unlink($tmpFile);
	}
	if (!$res) {
	    return array(GalleryCoreApi::error(ERROR_TOOLKIT_FAILURE), null);
	}
	return array(null, $res);
    }

    /**
     * Save a GD resource to a file with a certain mime-type
     *
     * @param resource $res
     * @param string $filename the path to the file to read
     * @param string $mimeType the mime-type of the image
     * @param string $jpegQuality (optional)
     * @return object GalleryStatus a status code
     * @access private
     */
    function _saveImageResourceToFile($res, $filename, $mimeType, $jpegQuality=null) {
	$gd =& $this->_getGdFunctionality();
	switch ($mimeType) {
	case 'image/gif':
	    $ret = $gd->imageGif($res, $filename);
	    break;
	case 'image/jpeg':
	    if (!isset($jpegQuality)) {
		list ($ret, $jpegQuality) =
		    GalleryCoreApi::getPluginParameter('module', 'gd', 'jpegQuality');
		if ($ret) {
		    return $ret;
		}
	    }
	    $ret = $gd->imageJpeg($res, $filename, $jpegQuality);
	    break;
	case 'image/png':
	    $ret = $gd->imagePng($res, $filename);
	    break;
	case 'image/vnd.wap.wbmp':
	    $ret = $gd->imageWbmp($res, $filename);
	    break;
	case 'image/x-xpixmap':
	    $ret = $gd->imageXpm($res, $filename);
	    break;
	case 'image/x-xbitmap':
	    $ret = $gd->imageXbm($res, $filename);
	    break;
	}
	if ($ret) {
	    return $ret;
	}
	$gd->chmod($filename);
	return null;
    }

    /**
     * Resizes an open GD-resource to the specified size.
     *
     * @param resource $sourceRes the source GD-resource
     * @param int $destWidth the destination width
     * @param int $destHeight the destination height
     * @return array object GalleryStatus
     *	       resource the destination GD-resource
     * @access private
     */
    function _resizeImageResource($sourceRes, $destWidth, $destHeight) {
	$gd =& $this->_getGdFunctionality();

	list ($ret, $sourceWidth) = $gd->imageSX($sourceRes);
	if ($ret) {
	    return array($ret, null);
	}
	list ($ret, $sourceHeight) = $gd->imageSY($sourceRes);
	if ($ret) {
	    return array($ret, null);
	}

	list ($ret, $destRes) = $this->_getTrueColorImageRes($destWidth, $destHeight);
	if ($ret) {
	    return array($ret, null);
	}
	$result = false;
	$ret = $this->_imageCopyResampled($destRes, $sourceRes,
				 0, 0, 0, 0,    /* dst and src X,Y */
				 $destWidth, $destHeight,    /* dst W,H */
				 $sourceWidth, $sourceHeight /* src W,H */
				 );
	if ($ret) {
	    return array($ret, null);
	}
	return array(null, $destRes);
    }

    /**
     * @see GalleryGraphics::getImageDimensions
     * @access private
     */
    function _getImageDimensions($mimeType, $filename) {
	$gd =& $this->_getGdFunctionality();

	/*
	 * Try PHPs getimagesize first. If it runs afoul of open_basedir it'll return false and we
	 * can try using the GD functions
	 */
	$results = $gd->getImageSize($filename);
	if (($results != false) &&
	    (($results[0] > 1) && ($results[1] > 1))) {
	    return array(null, $results[0], $results[1]);
	}

	list ($ret, $res) = $this->_getImageResource($mimeType, $filename);
	if ($ret) {
	    return array(GalleryCoreApi::error(ERROR_TOOLKIT_FAILURE), 0, 0);
	}

	list ($ret, $width, $height) = $this->_getImageDimensionsForResource($res);
	$this->_free($res);
	if ($ret) {
	    return array($ret, null, null);
	}

	return array(null, $width, $height);
    }

    /**
     * Get the image dimentions for an already opened GD resource.
     *
     * @return array object GalleryStatus
     *	       int width
     *	       int height
     * @access private
     */
    function _getImageDimensionsForResource($res) {
	$gd =& $this->_getGdFunctionality();
	list ($ret, $x) = $gd->imageSx($res);
	if ($ret) {
	    return array($ret, null, null);
	}
	list ($ret, $y) = $gd->imageSy($res);
	if ($ret) {
	    return array($ret, null, null);
	}
	if ($x > 0 && $y > 0) {
	    return array(null, $x, $y);
	}
	return array(GalleryCoreApi::error(ERROR_TOOLKIT_FAILURE), null, null);
    }

    /**
     * Copy and resize part of an image with resampling or resizing.
     *
     * We will use GD's "resample" functionality and fallback to "resize"
     * if "resample" is not available.
     *
     * @return object GalleryStatus
     * @access private
     */
    function _imageCopyResampled($dstRes, $srcRes, $dstX, $dstY, $srcX, $srcY,
				 $dstW, $dstH, $srcW, $srcH) {
	$gd =& $this->_getGdFunctionality();
	if ($gd->functionExists('imageCopyResampled')) {
	    $ret = $gd->imageCopyResampled($dstRes, $srcRes, $dstX, $dstY, $srcX, $srcY,
					   $dstW, $dstH, $srcW, $srcH);
	} else {
	    /* Fallback */
	    $ret = $gd->imageCopyResized($dstRes, $srcRes, $dstX, $dstY, $srcX, $srcY,
					 $dstW, $dstH, $srcW, $srcH);
	}
	if ($ret) {
	    return $ret;
	}
	return null;
    }

    /**
     * @see GdFunctionality::imagedestroy
     */
    function _free($resource) {
	if ($resource) {
	    $gd =& $this->_getGdFunctionality();
	    $gd->imagedestroy($resource);
	}
    }
}
?>