1: <?php
2: /**
3: * This class implements the Horde_Image:: API for ImageMagick.
4: *
5: * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
6: *
7: * See the enclosed file COPYING for license information (LGPL). If you
8: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
9: *
10: * @author Chuck Hagenbuch <chuck@horde.org>
11: * @author Mike Cochrane <mike@graftonhall.co.nz>
12: * @author Michael J. Rubinsky <mrubinsk@horde.org>
13: * @package Image
14: */
15: class Horde_Image_Im extends Horde_Image_Base
16: {
17: /**
18: * Capabilites of this driver.
19: *
20: * @var array
21: */
22: protected $_capabilities = array(
23: 'resize',
24: 'crop',
25: 'rotate',
26: 'grayscale',
27: 'flip',
28: 'mirror',
29: 'sepia',
30: 'canvas',
31: 'multipage',
32: 'pdf');
33:
34: /**
35: * Operations to be performed before the source filename is specified on the
36: * command line.
37: *
38: * @var array
39: */
40: protected $_operations = array();
41:
42: /**
43: * Operations to be added after the source filename is specified on the
44: * command line.
45: *
46: * @var array
47: */
48: protected $_postSrcOperations = array();
49:
50: /**
51: * An array of temporary filenames that need to be unlinked at the end of
52: * processing. Use addFileToClean() from client code (Effects) to add files
53: * to this array.
54: *
55: * @var array
56: */
57: protected $_toClean = array();
58:
59: /**
60: * Path to the convert binary
61: *
62: * @string
63: */
64: protected $_convert = '';
65:
66: /**
67: * Path to the identify binary
68: *
69: * @string
70: */
71: protected $_identify;
72:
73: /**
74: * Cache the number of image pages
75: *
76: * @var integer
77: */
78: private $_pages;
79:
80: /**
81: * Track current page for the iterator
82: *
83: * @var integer
84: */
85: private $_currentPage = 0;
86:
87: /**
88: * Constructor.
89: *
90: * @see Horde_Image_Base::_construct
91: */
92: public function __construct($params, $context = array())
93: {
94: parent::__construct($params, $context);
95:
96: if (empty($context['convert'])) {
97: throw new InvalidArgumentException('A path to the convert binary is required.');
98: }
99: $this->_convert = $context['convert'];
100:
101: if (!empty($context['identify'])) {
102: $this->_identify = $context['identify'];
103: }
104: if (!empty($params['filename'])) {
105: $this->loadFile($params['filename']);
106: } elseif (!empty($params['data'])) {
107: $this->loadString($params['data']);
108: } else {
109: $cmd = "-size {$this->_width}x{$this->_height} xc:{$this->_background} +profile \"*\" {$this->_type}:__FILEOUT__";
110: $this->executeConvertCmd($cmd);
111: }
112: }
113:
114: /**
115: * Publically visible raw method.
116: *
117: * @see self::_raw
118: */
119: public function raw($convert = false)
120: {
121: return $this->_raw($convert);
122: }
123:
124: /**
125: * Returns the raw data for this image.
126: *
127: * @param boolean $convert If true, the image data will be returned in the
128: * target format, even if no other image operations
129: * are present, otherwise, if no operations are
130: * present, the current raw data is returned
131: * unmodified.
132: *
133: * @return string The raw image data.
134: */
135: private function _raw($convert = false, $index = 0, $preserve_data = false)
136: {
137: if (empty($this->_data) ||
138: // If there are no operations, and we already have data, don't
139: // bother writing out files, just return the current data.
140: (!$convert &&
141: !count($this->_operations) &&
142: !count($this->_postSrcOperations))) {
143: return $this->_data;
144: }
145:
146: $tmpin = $this->toFile($this->_data);
147:
148: // Perform convert command if needed
149: if (count($this->_operations) || count($this->_postSrcOperations) || $convert) {
150: $tmpout = Horde_Util::getTempFile('img', false, $this->_tmpdir);
151: $command = $this->_convert . ' ' . implode(' ', $this->_operations)
152: . ' "' . $tmpin . '"\'[' . $index . ']\' '
153: . implode(' ', $this->_postSrcOperations)
154: . ' +profile "*" ' . $this->_type . ':"' . $tmpout . '" 2>&1';
155: $this->_logDebug(sprintf("convert command executed by Horde_Image_im::raw(): %s", $command));
156: exec($command, $output, $retval);
157: if ($retval) {
158: $error = sprintf("Error running command: %s", $command . "\n" . implode("\n", $output));
159: $this->_logErr($error);
160: throw new Horde_Image_Exception($error);
161: }
162:
163: /* Empty the operations queue */
164: $this->_operations = array();
165: $this->_postSrcOperations = array();
166:
167: /* Load the result */
168: $return = file_get_contents($tmpout);
169: if (!$preserve_data) {
170: $this->_data = $return;
171: }
172: }
173: @unlink($tmpin);
174: @unlink($tmpout);
175:
176: return $return;
177: }
178:
179: /**
180: * Reset the image data.
181: */
182: public function reset()
183: {
184: parent::reset();
185: $this->_operations = array();
186: $this->_postSrcOperations = array();
187: $this->clearGeometry();
188: }
189:
190: /**
191: * Resize the current image. This operation takes place immediately.
192: *
193: * @param integer $width The new width.
194: * @param integer $height The new height.
195: * @param boolean $ratio Maintain original aspect ratio.
196: * @param boolean $keepProfile Keep the image meta data.
197: */
198: public function resize($width, $height, $ratio = true, $keepProfile = false)
199: {
200: $resWidth = $width * 2;
201: $resHeight = $height * 2;
202: $this->_operations[] = "-size {$resWidth}x{$resHeight}";
203: if ($ratio) {
204: $this->_postSrcOperations[] = (($keepProfile) ? "-resize" : "-thumbnail") . " {$width}x{$height}";
205: } else {
206: $this->_postSrcOperations[] = (($keepProfile) ? "-resize" : "-thumbnail") . " {$width}x{$height}!";
207: }
208: // Reset the width and height instance variables since after resize
209: // we don't know the *exact* dimensions yet (especially if we maintained
210: // aspect ratio.
211: // Refresh the data
212: $this->raw();
213: $this->clearGeometry();
214: }
215:
216: /**
217: * Crop the current image.
218: *
219: * @param integer $x1 x for the top left corner
220: * @param integer $y1 y for the top left corner
221: * @param integer $x2 x for the bottom right corner of the cropped image.
222: * @param integer $y2 y for the bottom right corner of the cropped image.
223: */
224: public function crop($x1, $y1, $x2, $y2)
225: {
226: $line = ($x2 - $x1) . 'x' . ($y2 - $y1) . '+' . $x1 . '+' . $y1;
227: $this->_operations[] = '-crop ' . $line . ' +repage';
228:
229: // Reset width/height since these might change
230: $this->raw();
231: $this->clearGeometry();
232: }
233:
234: /**
235: * Rotate the current image. This is an atomic operation.
236: *
237: * @param integer $angle The angle to rotate the image by,
238: * in the clockwise direction.
239: * @param integer $background The background color to fill any triangles.
240: */
241: public function rotate($angle, $background = 'white')
242: {
243: $this->raw();
244: $this->_operations[] = "-background $background -rotate {$angle}";
245: $this->raw();
246:
247: // Reset width/height since these might have changed
248: $this->clearGeometry();
249: }
250:
251: /**
252: * Flip the current image.
253: */
254: public function flip()
255: {
256: $this->_operations[] = '-flip';
257: }
258:
259: /**
260: * Mirror the current image.
261: */
262: public function mirror()
263: {
264: $this->_operations[] = '-flop';
265: }
266:
267: /**
268: * Convert the current image to grayscale.
269: */
270: public function grayscale()
271: {
272: $this->_postSrcOperations[] = '-colorspace GRAY';
273: }
274:
275: /**
276: * Sepia filter.
277: *
278: * @param integer $threshold Extent of sepia effect.
279: */
280: public function sepia($threshold = 85)
281: {
282: $this->_operations[] = '-sepia-tone ' . $threshold . '%';
283: }
284:
285: /**
286: * Draws a text string on the image in a specified location, with
287: * the specified style information.
288: *
289: * @TODO: Need to differentiate between the stroke (border) and the fill color,
290: * but this is a BC break, since we were just not providing a border.
291: *
292: * @param string $text The text to draw.
293: * @param integer $x The left x coordinate of the start of the text string.
294: * @param integer $y The top y coordinate of the start of the text string.
295: * @param string $font The font identifier you want to use for the text.
296: * @param string $color The color that you want the text displayed in.
297: * @param integer $direction An integer that specifies the orientation of the text.
298: * @param string $fontsize Size of the font (small, medium, large, giant)
299: */
300: public function text($string, $x, $y, $font = '', $color = 'black', $direction = 0, $fontsize = 'small')
301: {
302: $string = addslashes('"' . $string . '"');
303: $fontsize = Horde_Image::getFontSize($fontsize);
304: $this->_postSrcOperations[] = "-fill $color " . (!empty($font) ? "-font $font" : '') . " -pointsize $fontsize -gravity northwest -draw \"text $x,$y $string\" -fill none";
305: }
306:
307: /**
308: * Draw a circle.
309: *
310: * @param integer $x The x coordinate of the centre.
311: * @param integer $y The y coordinate of the centre.
312: * @param integer $r The radius of the circle.
313: * @param string $color The line color of the circle.
314: * @param string $fill The color to fill the circle.
315: */
316: public function circle($x, $y, $r, $color, $fill = 'none')
317: {
318: $xMax = $x + $r;
319: $this->_postSrcOperations[] = "-stroke $color -fill $fill -draw \"circle $x,$y $xMax,$y\" -stroke none -fill none";
320: }
321:
322: /**
323: * Draw a polygon based on a set of vertices.
324: *
325: * @param array $vertices An array of x and y labeled arrays
326: * (eg. $vertices[0]['x'], $vertices[0]['y'], ...).
327: * @param string $color The color you want to draw the polygon with.
328: * @param string $fill The color to fill the polygon.
329: */
330: public function polygon($verts, $color, $fill = 'none')
331: {
332: $command = '';
333: foreach ($verts as $vert) {
334: $command .= sprintf(' %d,%d', $vert['x'], $vert['y']);
335: }
336: $this->_postSrcOperations[] = "-stroke $color -fill $fill -draw \"polygon $command\" -stroke none -fill none";
337: }
338:
339: /**
340: * Draw a rectangle.
341: *
342: * @param integer $x The left x-coordinate of the rectangle.
343: * @param integer $y The top y-coordinate of the rectangle.
344: * @param integer $width The width of the rectangle.
345: * @param integer $height The height of the rectangle.
346: * @param string $color The line color of the rectangle.
347: * @param string $fill The color to fill the rectangle.
348: */
349: public function rectangle($x, $y, $width, $height, $color, $fill = 'none')
350: {
351: $xMax = $x + $width;
352: $yMax = $y + $height;
353: $this->_postSrcOperations[] = "-stroke $color -fill $fill -draw \"rectangle $x,$y $xMax,$yMax\" -stroke none -fill none";
354: }
355:
356: /**
357: * Draw a rounded rectangle.
358: *
359: * @param integer $x The left x-coordinate of the rectangle.
360: * @param integer $y The top y-coordinate of the rectangle.
361: * @param integer $width The width of the rectangle.
362: * @param integer $height The height of the rectangle.
363: * @param integer $round The width of the corner rounding.
364: * @param string $color The line color of the rectangle.
365: * @param string $fill The color to fill the rounded rectangle with.
366: */
367: public function roundedRectangle($x, $y, $width, $height, $round, $color, $fill)
368: {
369: $x1 = $x + $width;
370: $y1 = $y + $height;
371: $this->_postSrcOperations[] = "-stroke $color -fill $fill -draw \"roundRectangle $x,$y $x1,$y1 $round,$round\" -stroke none -fill none";
372: }
373:
374: /**
375: * Draw a line.
376: *
377: * @param integer $x0 The x coordinate of the start.
378: * @param integer $y0 The y coordinate of the start.
379: * @param integer $x1 The x coordinate of the end.
380: * @param integer $y1 The y coordinate of the end.
381: * @param string $color The line color.
382: * @param string $width The width of the line.
383: */
384: public function line($x0, $y0, $x1, $y1, $color = 'black', $width = 1)
385: {
386: $this->_operations[] = "-stroke $color -strokewidth $width -draw \"line $x0,$y0 $x1,$y1\"";
387: }
388:
389: /**
390: * Draw a dashed line.
391: *
392: * @param integer $x0 The x co-ordinate of the start.
393: * @param integer $y0 The y co-ordinate of the start.
394: * @param integer $x1 The x co-ordinate of the end.
395: * @param integer $y1 The y co-ordinate of the end.
396: * @param string $color The line color.
397: * @param string $width The width of the line.
398: * @param integer $dash_length The length of a dash on the dashed line
399: * @param integer $dash_space The length of a space in the dashed line
400: */
401: public function dashedLine($x0, $y0, $x1, $y1, $color = 'black', $width = 1, $dash_length = 2, $dash_space = 2)
402: {
403: $this->_operations[] = "-stroke $color -strokewidth $width -draw \"line $x0,$y0 $x1,$y1\"";
404: }
405:
406: /**
407: * Draw a polyline (a non-closed, non-filled polygon) based on a
408: * set of vertices.
409: *
410: * @param array $vertices An array of x and y labeled arrays
411: * (eg. $vertices[0]['x'], $vertices[0]['y'], ...).
412: * @param string $color The color you want to draw the line with.
413: * @param string $width The width of the line.
414: */
415: public function polyline($verts, $color, $width = 1)
416: {
417: $command = '';
418: foreach ($verts as $vert) {
419: $command .= sprintf(' %d,%d', $vert['x'], $vert['y']);
420: }
421: $this->_operations[] = "-stroke $color -strokewidth $width -fill none -draw \"polyline $command\" -strokewidth 1 -stroke none -fill none";
422: }
423:
424: /**
425: * Draw an arc.
426: *
427: * @param integer $x The x coordinate of the centre.
428: * @param integer $y The y coordinate of the centre.
429: * @param integer $r The radius of the arc.
430: * @param integer $start The start angle of the arc.
431: * @param integer $end The end angle of the arc.
432: * @param string $color The line color of the arc.
433: * @param string $fill The fill color of the arc (defaults to none).
434: */
435: public function arc($x, $y, $r, $start, $end, $color = 'black', $fill = 'none')
436: {
437: // Split up arcs greater than 180 degrees into two pieces.
438: $this->_postSrcOperations[] = "-stroke $color -fill $fill";
439: $mid = round(($start + $end) / 2);
440: $x = round($x);
441: $y = round($y);
442: $r = round($r);
443: if ($mid > 90) {
444: $this->_postSrcOperations[] = "-draw \"ellipse $x,$y $r,$r $start,$mid\"";
445: $this->_postSrcOperations[] = "-draw \"ellipse $x,$y $r,$r $mid,$end\"";
446: } else {
447: $this->_postSrcOperations[] = "-draw \"ellipse $x,$y $r,$r $start,$end\"";
448: }
449:
450: // If filled, draw the outline.
451: if (!empty($fill)) {
452: list($x1, $y1) = Horde_Image::circlePoint($start, $r * 2);
453: list($x2, $y2) = Horde_Image::circlePoint($mid, $r * 2);
454: list($x3, $y3) = Horde_Image::circlePoint($end, $r * 2);
455:
456: // This seems to result in slightly better placement of
457: // pie slices.
458: $x++;
459: $y++;
460:
461: $verts = array(array('x' => $x + $x3, 'y' => $y + $y3),
462: array('x' => $x, 'y' => $y),
463: array('x' => $x + $x1, 'y' => $y + $y1));
464:
465: if ($mid > 90) {
466: $verts1 = array(array('x' => $x + $x2, 'y' => $y + $y2),
467: array('x' => $x, 'y' => $y),
468: array('x' => $x + $x1, 'y' => $y + $y1));
469: $verts2 = array(array('x' => $x + $x3, 'y' => $y + $y3),
470: array('x' => $x, 'y' => $y),
471: array('x' => $x + $x2, 'y' => $y + $y2));
472:
473: $this->polygon($verts1, $fill, $fill);
474: $this->polygon($verts2, $fill, $fill);
475: } else {
476: $this->polygon($verts, $fill, $fill);
477: }
478:
479: $this->polyline($verts, $color);
480:
481: $this->_postSrcOperations[] = '-stroke none -fill none';
482: }
483: }
484:
485: public function applyEffects()
486: {
487: $this->raw();
488: foreach ($this->_toClean as $tempfile) {
489: @unlink($tempfile);
490: }
491: }
492:
493: /**
494: * Method to execute a raw command directly in convert. Useful for executing
495: * more involved operations that may require multiple convert commands
496: * piped into each other for example. Really designed for use by im based
497: * Horde_Image_Effect objects..
498: *
499: * The input and output files are quoted and substituted for __FILEIN__ and
500: * __FILEOUT__ respectfully. In order to support piped convert commands, the
501: * path to the convert command is substitued for __CONVERT__ (but the
502: * initial convert command is added automatically).
503: *
504: * @param string $cmd The command string, with substitutable tokens
505: * @param array $values Any values that should be substituted for tokens.
506: *
507: * @return
508: */
509: public function executeConvertCmd($cmd, $values = array())
510: {
511: // First, get a temporary file for the input
512: if (strpos($cmd, '__FILEIN__') !== false) {
513: $tmpin = $this->toFile($this->_data);
514: } else {
515: $tmpin = '';
516: }
517:
518: // Now an output file
519: $tmpout = Horde_Util::getTempFile('img', false, $this->_tmpdir);
520:
521: // Substitue them in the cmd string
522: $cmd = str_replace(array('__FILEIN__', '__FILEOUT__', '__CONVERT__'),
523: array('"' . $tmpin . '"', '"' . $tmpout . '"', $this->_convert),
524: $cmd);
525:
526: //TODO: See what else needs to be replaced.
527: $cmd = $this->_convert . ' ' . $cmd . ' 2>&1';
528:
529: // Log it
530: $this->_logDebug(sprintf("convert command executed by Horde_Image_im::executeConvertCmd(): %s", $cmd));
531: exec($cmd, $output, $retval);
532: if ($retval) {
533: $this->_logErr(sprintf("Error running command: %s", $cmd . "\n" . implode("\n", $output)));
534: }
535: $this->_data = file_get_contents($tmpout);
536:
537: @unlink($tmpin);
538: @unlink($tmpout);
539: }
540:
541: /**
542: * Get the version of the convert command available. This needs to be
543: * publicly visable since it's used by various Effects.
544: *
545: * @return A version string suitable for using in version_compare()
546: */
547: public function getIMVersion()
548: {
549: static $version = null;
550: if (!is_array($version)) {
551: $commandline = $this->_convert . ' --version';
552: exec($commandline, $output, $retval);
553: if (preg_match('/([0-9])\.([0-9])\.([0-9])/', $output[0], $matches)) {
554: $version = $matches;
555: return $matches;
556: } else {
557: return false;
558: }
559: }
560:
561: return $version;
562: }
563:
564: public function addPostSrcOperation($operation)
565: {
566: $this->_postSrcOperations[] = $operation;
567: }
568:
569: public function addOperation($operation)
570: {
571: $this->_operations[] = $operation;
572: }
573:
574: public function addFileToClean($filename)
575: {
576: $this->_toClean[] = $filename;
577: }
578:
579: public function getConvertPath()
580: {
581: return $this->_convert;
582: }
583:
584: /**
585: * Reset the imagick iterator to the first image in the set.
586: *
587: * @return void
588: */
589: public function rewind()
590: {
591: $this->_logDebug('Horde_Image_Im#rewind');
592: $this->_currentPage = 0;
593: }
594:
595: /**
596: * Return the current image from the internal iterator.
597: *
598: * @return Horde_Image_Imagick
599: */
600: public function current()
601: {
602: $this->_logDebug('Horde_Image_Im#current');
603: return $this->getImageAtIndex($this->_currentPage);
604: }
605:
606: /**
607: * Get the index of the internal iterator.
608: *
609: * @return integer
610: */
611: public function key()
612: {
613: $this->_logDebug('Horde_Image_Im#key');
614: return $this->_currentPage;
615: }
616:
617: /**
618: * Advance the iterator
619: *
620: * @return Horde_Image_Im
621: */
622: public function next()
623: {
624: $this->_logDebug('Horde_Image_Im#next');
625: $this->_currentPage++;
626: if ($this->valid()) {
627: return $this->getImageAtIndex($this->_currentPage);
628: }
629: }
630:
631: /**
632: * Deterimines if the current iterator item is valid.
633: *
634: * @return boolean
635: */
636: public function valid()
637: {
638: return $this->_currentPage < $this->getImagePageCount();
639: }
640:
641: /**
642: * Request a specific image from the collection of images.
643: *
644: * @param integer $index The index to return
645: *
646: * @return Horde_Image_Base
647: */
648: public function getImageAtIndex($index)
649: {
650: $this->_logDebug('Horde_Image_Im#getImageAtIndex: ' . $index);
651: if ($index >= $this->getImagePageCount()) {
652: throw new Horde_Image_Exception('Image index out of bounds.');
653: }
654: $rawImage = $this->_raw(true, $index, true);
655: $image = new Horde_Image_Im(array('data' => $rawImage), $this->_context);
656:
657: return $image;
658: }
659:
660: /**
661: * Return the number of image pages available in the image object.
662: *
663: * @return integer
664: */
665: public function getImagePageCount()
666: {
667: if (is_null($this->_pages)) {
668: $pages = $this->_getImagePages();
669: $this->_pages = array_pop($pages);
670: }
671: $this->_logDebug('Horde_Image_Im#getImagePageCount: ' . $this->_pages);
672:
673: return $this->_pages;
674:
675: }
676:
677: private function _getImagePages()
678: {
679: $this->_logDebug('Horde_Image_Im#_getImagePages');
680: $filename = $this->toFile();
681: $cmd = $this->_identify . ' -format "%n" ' . $filename;
682: exec($cmd, $output, $retval);
683: if ($retval) {
684: $this->_logErr(sprintf("Error running command: %s", $cmd . "\n" . implode("\n", $output)));
685: }
686: unlink($filename);
687:
688: return $output;
689: }
690: }
691: