543 lines
15 KiB
PHP
543 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2002-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file LICENSE for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Michael J. Rubinsky <mrubinsk@horde.org>
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
|
|
* @package Image
|
|
*/
|
|
|
|
/**
|
|
* This class defines the Horde_Image API, and also provides some utility
|
|
* functions, such as generating highlights of a color.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Michael J. Rubinsky <mrubinsk@horde.org>
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @category Horde
|
|
* @copyright 2002-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
|
|
* @package Image
|
|
*/
|
|
abstract class Horde_Image_Base extends EmptyIterator
|
|
{
|
|
/**
|
|
* Background color.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_background = 'white';
|
|
|
|
/**
|
|
* Capabilites of this driver.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_capabilities = array();
|
|
|
|
/**
|
|
* The current image data.
|
|
*
|
|
* @var Horde_Stream
|
|
*/
|
|
protected $_data;
|
|
|
|
/**
|
|
* Logger.
|
|
*/
|
|
protected $_logger;
|
|
|
|
/**
|
|
* The current width of the image data.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_width = 0;
|
|
|
|
/**
|
|
* The current height of the image data.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_height = 0;
|
|
|
|
/**
|
|
* A directory for temporary files.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_tmpdir;
|
|
|
|
/**
|
|
* Array containing available Effects
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_loadedEffects = array();
|
|
|
|
/**
|
|
* What kind of images should ImageMagick generate? Defaults to 'png'.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_type = 'png';
|
|
|
|
/**
|
|
* Cache the context
|
|
*
|
|
* @param array
|
|
*/
|
|
protected $_context;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $params The image object parameters. Values include:
|
|
* - background: (string) The background color.
|
|
* DEFAULT: white.
|
|
* - data: (string) The image binary data.
|
|
* - height: (integer) The desired image height.
|
|
* - type: (string) The output image type (png, jpeg
|
|
* etc.). DEFAULT: png.
|
|
* - width: (integer) The desired image width.
|
|
* @param array $context The object context - configuration, injected
|
|
* objects:
|
|
* - logger: (Horde_Log_Logger) A logger.
|
|
* - tmpdir: [REQUIRED] (string) Temporary directory.
|
|
*
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
protected function __construct($params, $context = array())
|
|
{
|
|
$this->_params = $params;
|
|
$this->_context = $context;
|
|
|
|
if (empty($context['tmpdir'])) {
|
|
throw new InvalidArgumentException(
|
|
'A path to a temporary directory is required.'
|
|
);
|
|
}
|
|
$this->_tmpdir = $context['tmpdir'];
|
|
|
|
$this->_logger = !empty($context['logger'])
|
|
? $context['logger']
|
|
: new Horde_Support_Stub();
|
|
|
|
if (isset($params['width'])) {
|
|
$this->_width = (integer)$params['width'];
|
|
}
|
|
if (isset($params['height'])) {
|
|
$this->_height = (integer)$params['height'];
|
|
}
|
|
if (!empty($params['type'])) {
|
|
// We only want the extension, not the full mimetype.
|
|
if (strpos($params['type'], 'image/') !== false) {
|
|
$params['type'] = substr($params['type'], 6);
|
|
}
|
|
$this->_type = $params['type'];
|
|
}
|
|
if (!empty($params['background'])) {
|
|
$this->_background = $params['background'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Catch-all method so that we don't error out when calling an unsupported
|
|
* manipulation method.
|
|
*/
|
|
public function __call($method, $args)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Returns the capabilities.
|
|
*
|
|
* @return array A list of backend capabilities.
|
|
*/
|
|
public function getCapabilities()
|
|
{
|
|
return $this->_capabilities;
|
|
}
|
|
|
|
/**
|
|
* Checks the existence of a particular capability.
|
|
*
|
|
* @param string $capability The capability to check for.
|
|
*
|
|
* @return boolean True if the backend has this capability.
|
|
*/
|
|
public function hasCapability($capability)
|
|
{
|
|
return in_array($capability, $this->_capabilities);
|
|
}
|
|
|
|
/**
|
|
* Sends HTTP headers for the image.
|
|
*/
|
|
public function headers()
|
|
{
|
|
header('Content-type: ' . $this->getContentType());
|
|
}
|
|
|
|
/**
|
|
* Returns the MIME type for this image.
|
|
*
|
|
* @return string The MIME type for this image.
|
|
*/
|
|
public function getContentType()
|
|
{
|
|
return 'image/' . $this->_type;
|
|
}
|
|
|
|
/**
|
|
* Returns the image type.
|
|
*
|
|
* @return string The type of this image (png, jpg, etc.).
|
|
*/
|
|
public function getType()
|
|
{
|
|
return $this->_type;
|
|
}
|
|
|
|
/**
|
|
* Sets the output image type.
|
|
*
|
|
* @param string $type An image type (png, jpg, etc.)
|
|
*
|
|
* @return string The previous image type.
|
|
*/
|
|
public function setType($type)
|
|
{
|
|
// We only want the extension, not the full mimetype.
|
|
if (strpos($type, 'image/') !== false) {
|
|
$type = substr($type, 6);
|
|
}
|
|
$old = $this->_type;
|
|
$this->_type = $type;
|
|
|
|
return $old;
|
|
}
|
|
|
|
/**
|
|
* Draws a shaped point at the specified (x,y) point.
|
|
*
|
|
* Useful for scatter diagrams, debug points, etc. Draws squares, circles,
|
|
* diamonds, and triangles.
|
|
*
|
|
* @param integer $x The x coordinate of the point to brush.
|
|
* @param integer $y The y coordinate of the point to brush.
|
|
* @param string $color The color to brush the point with.
|
|
* @param string $shape What brush to use? Defaults to a square.
|
|
*/
|
|
public function brush($x, $y, $color = 'black', $shape = 'square')
|
|
{
|
|
switch ($shape) {
|
|
case 'triangle':
|
|
$verts[0] = array('x' => $x + 3, 'y' => $y + 3);
|
|
$verts[1] = array('x' => $x, 'y' => $y - 3);
|
|
$verts[2] = array('x' => $x - 3, 'y' => $y + 3);
|
|
$this->polygon($verts, $color, $color);
|
|
break;
|
|
|
|
case 'circle':
|
|
$this->circle($x, $y, 3, $color, $color);
|
|
break;
|
|
|
|
case 'diamond':
|
|
$verts[0] = array('x' => $x - 3, 'y' => $y);
|
|
$verts[1] = array('x' => $x, 'y' => $y + 3);
|
|
$verts[2] = array('x' => $x + 3, 'y' => $y);
|
|
$verts[3] = array('x' => $x, 'y' => $y - 3);
|
|
$this->polygon($verts, $color, $color);
|
|
break;
|
|
|
|
case 'square':
|
|
default:
|
|
$this->rectangle($x - 2, $y - 2, 4, 4, $color, $color);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the image data to defaults.
|
|
*/
|
|
public function reset()
|
|
{
|
|
if ($this->_data) {
|
|
$this->_data->close();
|
|
}
|
|
$this->_data = null;
|
|
$this->_width = null;
|
|
$this->_height = null;
|
|
$this->_background = 'white';
|
|
}
|
|
|
|
/**
|
|
* Returns the height and width of the current image data.
|
|
*
|
|
* @return array An hash with 'width' containing the width,
|
|
* 'height' containing the height of the image.
|
|
*/
|
|
public function getDimensions()
|
|
{
|
|
// Check if we know it already
|
|
if ($this->_width == 0 && $this->_height == 0) {
|
|
$tmp = $this->toFile();
|
|
$details = @getimagesize($tmp);
|
|
list($this->_width, $this->_height) = $details;
|
|
unlink($tmp);
|
|
}
|
|
|
|
return array('width' => $this->_width, 'height' => $this->_height);
|
|
}
|
|
|
|
/**
|
|
* Loads the image data from a string.
|
|
*
|
|
* @param mixed $image_data The data to use for the image as a string,
|
|
* Horde_Stream, or stream resource.
|
|
*/
|
|
public function loadString($image_data)
|
|
{
|
|
$this->reset();
|
|
$this->_data = new Horde_Stream_Temp();
|
|
$this->_data->add($image_data, true);
|
|
}
|
|
|
|
/**
|
|
* Loads the image data from a file.
|
|
*
|
|
* @param string $filename The full path and filename to the file to load
|
|
* the image data from.
|
|
*
|
|
* @throws Horde_Image_Exception
|
|
*/
|
|
public function loadFile($filename)
|
|
{
|
|
$this->reset();
|
|
if (!file_exists($filename)) {
|
|
throw new Horde_Image_Exception(
|
|
sprintf("The image file, %s, does not exist.", $filename)
|
|
);
|
|
}
|
|
$fp = fopen($filename, 'r');
|
|
$this->_data = new Horde_Stream_Temp();
|
|
$this->_data->add($fp, true);
|
|
|
|
if (!$this->_data->length()) {
|
|
throw new Horde_Image_Exception(
|
|
sprintf("Could not load the image file %s", $filename)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves image data to file.
|
|
*
|
|
* If $data is false-ish, saves current image data after performing pending
|
|
* operations on the data. If $data contains raw image data, saves that
|
|
* data to file without regard for the current image data.
|
|
*
|
|
* @param mixed String or stream resource of binary image data.
|
|
*
|
|
* @return string Path to temporary file.
|
|
* @throws Horde_Image_Exception
|
|
*/
|
|
public function toFile($data = null)
|
|
{
|
|
if (empty($data)) {
|
|
if ($data = $this->raw(false, array('stream' => true))) {
|
|
return $this->toFile($data);
|
|
}
|
|
throw new Horde_Image_Exception('Unable to copy to file.');
|
|
}
|
|
|
|
$tmp = Horde_Util::getTempFile('img', false, $this->_tmpdir);
|
|
$fp = fopen($tmp, 'wb');
|
|
|
|
if (is_resource($data)) {
|
|
rewind($data);
|
|
while (!feof($data)) {
|
|
fwrite($fp, fread($data, 8192));
|
|
}
|
|
} elseif ($data) {
|
|
fwrite($fp, $data);
|
|
}
|
|
|
|
fclose($fp);
|
|
return $tmp;
|
|
}
|
|
|
|
/**
|
|
* Displays the current image.
|
|
*/
|
|
public function display()
|
|
{
|
|
$this->headers();
|
|
$data = $this->raw(true, array('stream' => true));
|
|
$output = fopen('php://output', 'w');
|
|
while (!feof($data)) {
|
|
fwrite($output, fread($data, 8192));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the raw data for this image.
|
|
*
|
|
* @param boolean $convert If true, the image data will be returned in the
|
|
* target format, independently from any image
|
|
* operations.
|
|
* @param array $options Array of options:
|
|
* - stream: If true, return as a stream resource.
|
|
* DEFAULT: false.
|
|
*
|
|
* @return string The raw image data.
|
|
*/
|
|
public function raw($convert = false, $options = array())
|
|
{
|
|
if (empty($options['stream'])) {
|
|
return $this->_data->__toString();
|
|
}
|
|
|
|
return $this->_data->stream;
|
|
}
|
|
|
|
/**
|
|
* Attempts to apply requested effect to this image.
|
|
*
|
|
* @param string $type The type of effect to apply.
|
|
* @param array $params Any parameters for the effect.
|
|
*/
|
|
public function addEffect($type, $params)
|
|
{
|
|
$class = str_replace('Horde_Image_', '', get_class($this));
|
|
$params['logger'] = $this->_logger;
|
|
$effect = Horde_Image_Effect::factory($type, $class, $params);
|
|
$effect->setImageObject($this);
|
|
$effect->apply();
|
|
}
|
|
|
|
/**
|
|
* Returns a list of available effects for this driver.
|
|
*/
|
|
public function getLoadedEffects()
|
|
{
|
|
if (!count($this->_loadedEffects)) {
|
|
$class = str_replace('Horde_Image_', '', get_class($this));
|
|
$this->_loadedEffects = array();
|
|
// First, load the driver-agnostic Effects.
|
|
$path = __DIR__ . '/Effect/';
|
|
if (is_dir($path)) {
|
|
if ($handle = opendir($path)) {
|
|
while (($file = readdir($handle)) !== false) {
|
|
if (substr($file, -4, 4) == '.php') {
|
|
$this->_loadedEffects[] = substr(
|
|
$file, 0, strlen($file) - 4
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Driver specific effects.
|
|
$path = $path . $class;
|
|
if (is_dir($path)) {
|
|
if ($handle = opendir($path)) {
|
|
while (($file = readdir($handle)) !== false) {
|
|
if (substr($file, -4, 4) == '.php') {
|
|
$this->_loadedEffects[] = substr(
|
|
$file, 0, strlen($file) - 4
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->_loadedEffects;
|
|
}
|
|
|
|
/**
|
|
* Applies any effects in the effect queue.
|
|
*/
|
|
public function applyEffects()
|
|
{
|
|
$this->raw();
|
|
}
|
|
|
|
/**
|
|
* Returns the current temporary directory.
|
|
*
|
|
* @return string The current temporary directory.
|
|
*/
|
|
public function getTmpDir()
|
|
{
|
|
return $this->_tmpdir;
|
|
}
|
|
|
|
/**
|
|
* Utility function to zero out cached geometry information.
|
|
*
|
|
* Shouldn't really be called from client code, but is needed since effects
|
|
* may need to clear these.
|
|
*/
|
|
public function clearGeometry()
|
|
{
|
|
$this->_height = 0;
|
|
$this->_width = 0;
|
|
}
|
|
|
|
/**
|
|
* Logs a message at debug level.
|
|
*
|
|
* @param string $message The log message.
|
|
*/
|
|
protected function _logDebug($message)
|
|
{
|
|
$this->_logger->debug($message);
|
|
}
|
|
|
|
/**
|
|
* Logs a message at error level.
|
|
*
|
|
* @param string $message The log message.
|
|
*/
|
|
protected function _logErr($message)
|
|
{
|
|
$this->_logger->err($message);
|
|
}
|
|
|
|
/**
|
|
* Returns a specific image from the pages of images.
|
|
*
|
|
* @param integer $index The index to return.
|
|
*
|
|
* @return Horde_Image_Base The requested image
|
|
*/
|
|
public function getImageAtIndex($index)
|
|
{
|
|
if ($index > 0) {
|
|
throw new Horde_Image_Exception('Image index out of bounds.');
|
|
}
|
|
$class = get_class($this);
|
|
return new $class(array('data' => $this->raw()), $this->_context);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of image pages available in the image object.
|
|
*
|
|
* @return integer The number of images.
|
|
*/
|
|
public function getImagePageCount()
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
}
|