Files
server/usr/share/psa-pear/pear/php/Horde/Image/Imagick.php
2026-01-07 20:52:11 +01:00

881 lines
28 KiB
PHP

<?php
/**
* Copyright 2007-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 Michael J. Rubinsky <mrubinsk@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
* @package Image
*/
/**
* Imagick driver for the Horde_Image API.
*
* @author Michael J. Rubinsky <mrubinsk@horde.org>
* @category Horde
* @copyright 2007-2017 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
* @package Image
*
* @property-read Imagick $imagick The underlaying Imagick object.
*/
class Horde_Image_Imagick extends Horde_Image_Base
{
/**
* The underlaying Imagick object.
*
* @var Imagick
*/
protected $_imagick;
/**
* Flag for iterator, since calling nextImage on Imagick would result in a
* fatal error if there are no more images.
*
* @var boolean
*/
private $_noMoreImages = false;
/**
* Capabilites of this driver.
*
* @var string[]
*/
protected $_capabilities = array(
'canvas',
'circle',
'crop',
'dashedLine',
'flip',
'grayscale',
'line',
'mirror',
'multipage',
'pdf',
'polygon',
'polyline',
'rectangle',
'resize',
'rotate',
'roundedRectangle',
'sepia',
'text',
);
/**
* Constructor.
*
* @see Horde_Image_Base::_construct
*/
public function __construct($params, $context = array())
{
if (!Horde_Util::loadExtension('imagick')) {
throw new Horde_Image_Exception(
'Required PECL Imagick extension not found.'
);
}
parent::__construct($params, $context);
ini_set('imagick.locale_fix', 1);
$this->_imagick = new Imagick();
if (!empty($params['filename'])) {
$this->loadFile($params['filename']);
} elseif(!empty($params['data'])) {
$this->loadString($params['data']);
} else {
$this->_width = max(array($this->_width, 1));
$this->_height = max(array($this->_height, 1));
try {
$color = new ImagickPixel($this->_background);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->newImage(
$this->_width, $this->_height, $color
);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
}
try {
$this->_imagick->setImageFormat($this->_type);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
}
/**
* Loads the image data from a string.
*
* @param string $image_data The data to use for the image.
*
* @throws Horde_Image_Exception
*/
public function loadString($image_data)
{
parent::loadString($image_data);
$this->_imagick->clear();
try {
$this->_data->rewind();
$this->_imagick->readImageFile($this->_data->stream);
$this->_imagick->setImageFormat($this->_type);
$this->_imagick->setIteratorIndex(0);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$this->_data->close();
unset($this->_data);
}
/**
* 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();
try {
$this->_imagick->readImage($filename);
$this->_imagick->setImageFormat($this->_type);
$this->_imagick->setIteratorIndex(0);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
// // Parent function loads image data into $this->_data
// parent::loadFile($filename);
// $this->loadString($this->_data);
}
/**
* Sets the output image type.
*
* @param string $type An image type (png, jpg, etc.)
*
* @return string The previous image type.
*/
public function setType($type)
{
$old = parent::setType($type);
try {
$this->_imagick->setImageFormat($this->_type);
} catch (ImagickException $e) {
// Don't care about an empty wand here.
}
return $old;
}
/**
* Returns the raw data for this image.
*
* @param boolean $convert Ignored for Imagick driver.
* @param array $options Array of options:
* - stream: If true, return as a stream resource.
* DEFAULT: false.
*
* @return mixed The raw image data as a string or stream resource.
*/
public function raw($convert = false, $options = array())
{
try {
$this->_imagick->stripImage();
if (empty($options['stream'])) {
return $this->_imagick->getImageBlob();
}
$s = new Horde_Stream_Temp();
$s->add($this->_imagick->getImageBlob(), true);
return $s->stream;
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
}
/**
* Resets the image data to defaults.
*/
public function reset()
{
parent::reset();
$this->_imagick->clear();
$this->_noMoreImages = false;
}
/**
* 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()
{
if ($this->_height == 0 && $this->_width == 0) {
try {
$size = $this->_imagick->getImageGeometry();
} catch (ImagickException $e) {
return array('width' => 0, 'height' => 0);
}
$this->_height = $size['height'];
$this->_width = $size['width'];
}
return array('width' => $this->_width, 'height' => $this->_height);
}
/**
* Resizes the current image.
*
* @param integer $width The new width.
* @param integer $height The new height.
* @param boolean $ratio Maintain original aspect ratio.
* @param boolean $keepProfile Keep the image meta data.
*/
public function resize($width, $height, $ratio = true, $keepProfile = false)
{
try {
if ($keepProfile) {
$this->_imagick->resizeImage($width, $height, $ratio);
} else {
$this->_imagick->thumbnailImage($width, $height, $ratio);
}
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$this->clearGeometry();
}
/**
* Crops the current image.
*
* @param integer $x1 x for the top left corner.
* @param integer $y1 y for the top left corner.
* @param integer $x2 x for the bottom right corner.
* @param integer $y2 y for the bottom right corner.
*/
public function crop($x1, $y1, $x2, $y2)
{
try {
$result = $this->_imagick->cropImage($x2 - $x1, $y2 - $y1, $x1, $y1);
$this->_imagick->setImagePage(0, 0, 0, 0);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$this->clearGeometry();
}
/**
* Rotates the current image.
*
* @param integer $angle The angle to rotate the image by, in the
* clockwise direction.
* @param string $background The background color to fill any triangles.
*/
public function rotate($angle, $background = 'white')
{
try {
$this->_imagick->rotateImage($background, $angle);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$this->clearGeometry();
}
/**
* Flips the current image.
*/
public function flip()
{
try {
$this->_imagick->flipImage();
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
}
/**
* Mirrors the current image.
*/
public function mirror()
{
try {
$this->_imagick->flopImage();
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
}
/**
* Converts the current image to grayscale.
*/
public function grayscale()
{
try {
$this->_imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE);
} catch (ImageException $e) {
throw new Horde_Image_Exception($e);
}
}
/**
* Applies a sepia filter.
*
* @param integer $threshold Extent of sepia effect.
*/
public function sepia($threshold = 85)
{
try {
$this->_imagick->sepiaToneImage($threshold);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
}
/**
* Draws a text string on the image in a specified location, with the
* specified style information.
*
* @TODO: Need to differentiate between the stroke (border) and the fill
* color, but this is a BC break, since we were just not providing a
* border.
*
* @param string $text The text to draw.
* @param integer $x The left x coordinate of the start of the
* text string.
* @param integer $y The top y coordinate of the start of the text
* string.
* @param string $font The font identifier you want to use for the
* text.
* @param string $color The color that you want the text displayed in.
* @param integer $direction An integer that specifies the orientation of
* the text.
* @param string $fontsize Size of the font (small, medium, large, giant)
*/
public function text(
$string, $x, $y, $font = '', $color = 'black', $direction = 0,
$fontsize = 'small'
)
{
$fontsize = Horde_Image::getFontSize($fontsize);
try {
$pixel = new ImagickPixel($color);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$draw = new ImagickDraw();
$draw->setFillColor($pixel);
if (!empty($font)) {
$draw->setFont($font);
}
$draw->setFontSize($fontsize);
$draw->setGravity(Imagick::GRAVITY_NORTHWEST);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->annotateImage($draw, $x, $y, $direction, $string);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a circle.
*
* @param integer $x The x coordinate of the centre.
* @param integer $y The y coordinate of the centre.
* @param integer $r The radius of the circle.
* @param string $color The line color of the circle.
* @param string $fill The color to fill the circle.
*/
public function circle($x, $y, $r, $color, $fill = 'none')
{
try {
$draw = new ImagickDraw();
$draw->setFillColor(new ImagickPixel($fill));
$draw->setStrokeColor(new ImagickPixel($color));
$draw->circle($x, $y, $r + $x, $y);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a polygon based on a set of vertices.
*
* @param array $vertices An array of x and y labeled arrays
* (eg. $vertices[0]['x'], $vertices[0]['y'], ...).
* @param string $color The color you want to draw the polygon with.
* @param string $fill The color to fill the polygon.
*/
public function polygon($verts, $color, $fill = 'none')
{
try {
$draw = new ImagickDraw();
$draw->setFillColor(new ImagickPixel($fill));
$draw->setStrokeColor(new ImagickPixel($color));
$draw->polygon($verts);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a rectangle.
*
* @param integer $x The left x-coordinate of the rectangle.
* @param integer $y The top y-coordinate of the rectangle.
* @param integer $width The width of the rectangle.
* @param integer $height The height of the rectangle.
* @param string $color The line color of the rectangle.
* @param string $fill The color to fill the rectangle.
*/
public function rectangle($x, $y, $width, $height, $color, $fill = 'none')
{
try {
$draw = new ImagickDraw();
$draw->setStrokeColor(new ImagickPixel($color));
$draw->setFillColor(new ImagickPixel($fill));
$draw->rectangle($x, $y, $x + $width, $y + $height);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a rounded rectangle.
*
* @param integer $x The left x-coordinate of the rectangle.
* @param integer $y The top y-coordinate of the rectangle.
* @param integer $width The width of the rectangle.
* @param integer $height The height of the rectangle.
* @param integer $round The width of the corner rounding.
* @param string $color The line color of the rectangle.
* @param string $fill The color to fill the rounded rectangle with.
*/
public function roundedRectangle(
$x, $y, $width, $height, $round, $color, $fill
)
{
try {
$draw = new ImagickDraw();
$draw->setStrokeColor(new ImagickPixel($color));
$draw->setFillColor(new ImagickPixel($fill));
$draw->roundRectangle($x, $y, $x + $width, $y + $height, $round, $round);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a line.
*
* @param integer $x0 The x coordinate of the start.
* @param integer $y0 The y coordinate of the start.
* @param integer $x1 The x coordinate of the end.
* @param integer $y1 The y coordinate of the end.
* @param string $color The line color.
* @param string $width The width of the line.
*/
public function line($x0, $y0, $x1, $y1, $color = 'black', $width = 1)
{
try {
$draw = new ImagickDraw();
$draw->setStrokeColor(new ImagickPixel($color));
$draw->setStrokeWidth($width);
$draw->line($x0, $y0, $x1, $y1);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a dashed line.
*
* @param integer $x0 The x co-ordinate of the start.
* @param integer $y0 The y co-ordinate of the start.
* @param integer $x1 The x co-ordinate of the end.
* @param integer $y1 The y co-ordinate of the end.
* @param string $color The line color.
* @param string $width The width of the line.
* @param integer $dash_length The length of a dash on the dashed line.
* @param integer $dash_space The length of a space in the dashed line.
*/
public function dashedLine(
$x0, $y0, $x1, $y1, $color = 'black', $width = 1, $dash_length = 2,
$dash_space = 2
)
{
try {
$draw = new ImagickDraw();
$draw->setStrokeColor(new ImagickPixel($color));
$draw->setStrokeWidth($width);
$draw->setStrokeDashArray(array($dash_length, $dash_space));
$draw->line($x0, $y0, $x1, $y1);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImageException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws a polyline (a non-closed, non-filled polygon) based on a set of
* vertices.
*
* @param array $vertices An array of x and y labeled arrays
* (eg. $vertices[0]['x'], $vertices[0]['y'], ...).
* @param string $color The color you want to draw the line with.
* @param string $width The width of the line.
*/
public function polyline($verts, $color, $width = 1)
{
try {
$draw = new ImagickDraw();
$draw->setStrokeColor(new ImagickPixel($color));
$draw->setStrokeWidth($width);
$draw->setFillColor(new ImagickPixel('none'));
$draw->polyline($verts);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Draws an arc.
*
* @param integer $x The x coordinate of the centre.
* @param integer $y The y coordinate of the centre.
* @param integer $r The radius of the arc.
* @param integer $start The start angle of the arc.
* @param integer $end The end angle of the arc.
* @param string $color The line color of the arc.
* @param string $fill The fill color of the arc (defaults to none).
*/
public function arc(
$x, $y, $r, $start, $end, $color = 'black', $fill = 'none'
)
{
$points = Horde_Image::arcPoints($r, $start, $end);
$points['x1'] += $x;
$points['x2'] += $x;
$points['x3'] += $x;
$points['y1'] += $y;
$points['y2'] += $y;
$points['y3'] += $y;
try {
$draw = new ImagickDraw();
$draw->setStrokeColor(new ImagickPixel($color));
$draw->setFillColor(new ImagickPixel($fill));
$draw->arc($x - $r, $y - $r, $x + $r, $y + $r, $start, $end);
} catch (ImagickDrawException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
}
// If filled, draw the outline.
if (!empty($fill)) {
$mid = round(($start + $end) / 2);
list($x1, $y1) = Horde_Image::circlePoint($start, $r * 2);
list($x2, $y2) = Horde_Image::circlePoint($mid, $r * 2);
list($x3, $y3) = Horde_Image::circlePoint($end, $r * 2);
$verts = array(
array('x' => $x + round($x3), 'y' => $y + round($y3)),
array('x' => $x, 'y' => $y),
array('x' => $x + round($x1), 'y' => $y + round($y1))
);
if ($mid > 90) {
$verts1 = array(
array('x' => $x + round($x2), 'y' => $y + round($y2)),
array('x' => $x, 'y' => $y),
array('x' => $x + round($x1), 'y' => $y + round($y1))
);
$verts2 = array(
array('x' => $x + round($x3), 'y' => $y + round($y3)),
array('x' => $x, 'y' => $y),
array('x' => $x + round($x2), 'y' => $y + round($y2))
);
$this->polygon($verts1, $fill, $fill);
$this->polygon($verts2, $fill, $fill);
} else {
$this->polygon($verts, $fill, $fill);
}
$this->polyline($verts, $color);
}
try {
$this->_imagick->drawImage($draw);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$draw->destroy();
}
/**
* Applies any effects in the effect queue.
*/
public function applyEffects()
{
// noop for this driver.
}
/**
*/
public function __get($property)
{
switch ($property) {
case 'imagick':
return $this->_imagick;
}
}
/**
* Utility function to wrap Imagick::borderImage.
*
* Use when you don't want to replace all pixels in the clipping area with
* the border color i.e. you want to "frame" the existing image. Preserves
* transparency etc.
*
* @param Imagick &$image The Imagick object to border.
* @param string $color The border color.
* @param integer $width The image width including the border.
* @param integer $height The image height including the border.
*
* @todo Make non-static for H6.
*/
public static function frameImage(&$image, $color, $width, $height)
{
// Need to jump through these hoops in order to preserve any
// transparency.
try {
// @todo Use clone or $this->_cloneImagickObject().
$border = $image->clone();
$border->borderImage(new ImagickPixel($color), $width, $height);
$border->compositeImage($image, Imagick::COMPOSITE_COPY, $width, $height);
$image->clear();
$image->addImage($border);
} catch (ImagickPixelException $e) {
throw new Horde_Image_Exception($e);
} catch (ImagickException $e) {
throw new Horde_Image_Exception($e);
}
$border->destroy();
}
/**
* Resets the Imagick iterator to the first image in the set.
*/
public function rewind()
{
$this->_logDebug('Horde_Image_Imagick#rewind');
$this->_imagick->setFirstIterator();
$this->_noMoreImages = false;
}
/**
* Returns the current image from the internal iterator.
*
* @return Horde_Image_Imagick
*/
public function current()
{
$this->_logDebug('Horde_Image_Imagick#current');
$params = array('data' => $this->raw(false, array('stream' => true)));
$image = new Horde_Image_Imagick($params, $this->_context);
return $image;
}
/**
* Returns the index of the internal iterator.
*
* @return integer
*/
public function key()
{
$this->_logDebug('Horde_Image_Imagick#key: ' . $this->_imagick->getIteratorIndex());
return $this->_imagick->getIteratorIndex();
}
/**
* Advances the iterator.
*
* @return Horde_Image_Imagick
*/
public function next()
{
if ($this->_imagick->hasNextImage()) {
$this->_imagick->nextImage();
return $this->current();
} else {
$this->_noMoreImages = true;
return false;
}
}
/**
* Deterimines if the current iterator item is valid.
*
* @return boolean
*/
public function valid()
{
$this->_logDebug('Horde_Image_Imagick#valid:' . print_r(!$this->_noMoreImages, true));
return !$this->_noMoreImages;
}
/**
* Returns a specific image from the pages of images.
*
* @param integer $index The index to return.
* @param boolean $flatten If true, flatten the returned image using
* $bgColor as the background color. Useful
* for PDF files to remove transparency before
* rendering.
* @param string $bgColor The background color to use when flattening the
* image.
*
* @return Horde_Image_Imagick The requested image
*/
public function getImageAtIndex($index, $flatten = false, $bgColor = 'white')
{
if ($index > 0 && $index >= $this->getImagePageCount()) {
throw new Horde_Image_Exception('Image index out of bounds.');
}
$currentIndex = $this->_imagick->getIteratorIndex();
$this->_imagick->setIteratorIndex($index);
$image = $this->current();
$this->_imagick->setIteratorIndex($currentIndex);
if ($flatten) {
$image->flattenImage($bgColor);
}
return $image;
}
/**
* Flatten the provided image and remove transparency.
*
* @param string $bgColor The background color to use.
*/
public function flattenImage($bgColor = 'white')
{
$this->imagick->setImageBackgroundColor($bgColor);
if (defined('imagick::ALPHACHANNEL_REMOVE')) {
$this->imagick->setImageAlphaChannel(imagick::ALPHACHANNEL_REMOVE);
}
$this->loadString(
$this->imagick->mergeImageLayers(imagick::LAYERMETHOD_FLATTEN)->getImageBlob()
);
}
/**
* Returns the number of image pages available in the image object.
*
* @return integer The number of images.
*/
public function getImagePageCount()
{
$pages = $this->_imagick->getNumberImages();
$this->_logDebug('Horde_Image_Imagick#getImagePageCount: ' . $pages);
return $pages;
}
/**
* Wrapper around cloning the imagick resource object.
*
* @param Imagick $imagick A imagick resource object to clone. If empty
* will clone the imagick object associated with
* this Horde_Imagice_Imagick object.
*
* @todo Remove in H6 when we can increase version dependency of Imagick.
*
* @return Imagick
* @since 2.4.0
*/
public function cloneImagickObject($imagick = null)
{
if (version_compare(phpversion('imagick'), '3.1.0') >= 0) {
return empty($imagick)
? clone $this->_imagick
: clone $imagick;
}
return empty($imagick)
? $this->_imagick->clone()
: $imagick->clone();
}
}