GraphicsUtil.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\util;
12 
16 
17 if (!class_exists('PHPImageWorkshop\ImageWorkshop') ||
18  !class_exists('GifFrameExtractor\GifFrameExtractor') ||
19  !class_exists('GifCreator\GifCreator')) {
20  throw new \wcmf\lib\config\ConfigurationException(
21  'wcmf\lib\util\GraphicsUtil requires '.
22  'ImageWorkshop, GifFrameExtractor and GifCreator. If you are using composer, '.
23  'add sybio/image-workshop, sybio/gif-frame-extractor and sybio/gif-creator '.
24  'as dependency to your project');
25 }
26 
27 /**
28  * GraphicsUtil provides support for graphic manipulation.
29  *
30  * @note This class requires ImageWorkshop, GifFrameExtractor and GifCreator
31  *
32  * @author ingo herwig <ingo@wemove.com>
33  */
34 class GraphicsUtil {
35 
36  private $_errorMsg = '';
37 
38  /**
39  * Get last error message.
40  * @return The error string
41  */
42  public function getErrorMsg() {
43  return $this->_errorMsg;
44  }
45 
46  /**
47  * Check if a given file is an image.
48  * @param $imgname Name of the imagefile to check
49  * @return Boolean whether the file is an image
50  */
51  public function isImage($imgname) {
52  try {
53  ImageWorkshop::initFromPath($imgname);
54  return true;
55  } catch (\Exception $ex) {
56  return false;
57  }
58  }
59 
60  /**
61  * Check image dimensions.
62  * @param $imgname Name of the imagefile to check
63  * @param $width Width of the image, -1 means don't care
64  * @param $height Height of the image, -1 means don't care
65  * @param $exact Boolean whether the image should match the dimension exactly or might be smaller (default: _true_)
66  * @return Boolean whether the image meets the dimensions, error string provided by getErrorMsg()
67  */
68  public function isValidImageDimension($imgname, $width, $height, $exact=true) {
69  $widthOk = ($width == -1) || $this->isValidImageWidth($imgname, $width, $exact);
70  $heightOk = ($height == -1) || $this->isValidImageHeight($imgname, $height, $exact);
71  return ($widthOk && $heightOk);
72  }
73 
74  /**
75  * Check image width.
76  * @param $imgname Name of the imagefile to check
77  * @param $width Width of the image
78  * @param $exact Boolean whether the image width should match exactly or might be smaller (default: _true_)
79  * @return Boolean whether the image width meets the criteria, error string provided by getErrorMsg()
80  * @note This method returns true if the file does not exist.
81  */
82  public function isValidImageWidth($imgname, $width, $exact=true) {
83  try {
84  $image = ImageWorkshop::initFromPath($imgname);
85  } catch (\Exception $ex) {
86  return true;
87  }
88  $imgWitdh = $image->getWidth();
89  $dimOk = ($exact && $imgWitdh == $width) || (!$exact && $imgWitdh <= $width);
90  if (!$dimOk) {
91  $message = ObjectFactory::getInstance('message');
92  $constraint = $exact ? $message->getText("exactly") : $message->getText("smaller than");
93  $this->_errorMsg = $message->getText("Wrong image width. Image width must be %1% %2%px - actual image width is %3%px.",
94  array($constraint, $width, $imgWitdh));
95  $this->_errorMsg .= "\n";
96  }
97  return $dimOk;
98  }
99 
100  /**
101  * Check image height.
102  * @param $imgname Name of the imagefile to check
103  * @param $height Height of the image
104  * @param $exact Boolean whether the image height should match exactly or might be smaller (default: _true_)
105  * @return Boolean whether the image width meets the criteria, error string provided by getErrorMsg()
106  * @note This method returns true if the file does not exist.
107  */
108  public function isValidImageHeight($imgname, $height, $exact=true) {
109  try {
110  $image = ImageWorkshop::initFromPath($imgname);
111  } catch (\Exception $ex) {
112  return true;
113  }
114  $imgHeight = $image->getHeight();
115  $dimOk = ($exact && $imgHeight == $height) || (!$exact && $imgHeight <= $height);
116  if (!$dimOk) {
117  $message = ObjectFactory::getInstance('message');
118  $constraint = $exact ? $message->getText("exactly") : $message->getText("smaller than");
119  $this->_errorMsg .= $message->getText("Wrong image height. Image height must be %1% %2%px - actual image height is %3%px.",
120  array($constraint, $height, $imgHeight));
121  $this->_errorMsg .= "\n";
122  }
123  return $dimOk;
124  }
125 
126  /**
127  * Calculate image dimension to fit into a square, preserving the aspect ratio
128  * @param $srcName The source file name
129  * @param $maxDimension The maximum dimension the image should have (either width or height)
130  * @return Array with width and height value or null, on error, error string provided by getErrorMsg()
131  */
132  public function fitIntoSquare($srcName, $maxDimension) {
133  try {
134  $image = ImageWorkshop::initFromPath($srcName);
135  $sourceWidth = $image->getWidth();
136  $sourceHeight = $image->getHeight();
137  if ($sourceWidth < $sourceHeight) {
138  $height = $maxDimension;
139  $width = floor($sourceWidth*$height/$sourceHeight);
140  }
141  else {
142  $width = $maxDimension;
143  $height = floor($sourceHeight*$width/$sourceWidth);
144  }
145  // if image is actually smaller, leave small
146  if ($width > $sourceWidth && $height > $sourceHeigth) {
147  $width = $sourceWidth;
148  $height = $sourceHeight;
149  }
150  return array($width, $height);
151  } catch (\Exception $ex) {
152  $this->_errorMsg = $ex->getMessage();
153  return null;
154  }
155  }
156 
157  /**
158  * Create a thumbnail of an image file.
159  * @param $srcName The source file name
160  * @param $destName The destination file name
161  * @param $width The width of the thumbnail (maybe null)
162  * @param $height The height of the thumbnail (maybe null)
163  * @return Boolean whether the operation succeeded, error string provided by getErrorMsg()
164  * @note: supported image formats are GIF, JPG, PNG
165  * if only width or height are given the other dimension is calculated to preserve the aspect
166  */
167  public function createThumbnail($srcName, $destName, $width, $height) {
168  try {
169  $keepAspect = $width === null || $height === null;
170  $this->processImageFunction($srcName, $destName, "resizeInPixel", array($width, $height, $keepAspect));
171  return true;
172  } catch (\Exception $ex) {
173  $this->_errorMsg = $ex->getMessage();
174  return false;
175  }
176  }
177 
178  /**
179  * Crop an image to the given size starting from the middle of a given start point.
180  * @param $srcName The source file name
181  * @param $destName The destination file name
182  * @param $width The width of the cropped image (maybe null)
183  * @param $height The height of the cropped image (maybe null)
184  * @param $x The start point x coordinate (maybe null, default null)
185  * @param $y The start point y coordinate (maybe null, default null)
186  * @return Boolean whether the operation succeeded, error string provided by getErrorMsg()
187  * @note: supported image formats are GIF, JPG, PNG
188  * if only width or height are given the other dimension is taken from the original image
189  */
190  public function cropImage($srcName, $destName, $width, $height, $x=null, $y=null) {
191  try {
192  // calculate parameters based on image (or first frame of gif animation)
193  $image = ImageWorkshop::initFromPath($srcName);
194  list($width, $height) = $this->calculateSizeParams($width, $height, $image->getWidth(), $image->getHeight());
195  $x = ($x === null) ? $image->getWidth()/2 : $x;
196  $y = ($y === null) ? $image->getHeight()/2 : $y;
197 
198  $this->processImageFunction($srcName, $destName, "cropInPixel", array($width, $height, $x, $y, 'LT'));
199  return true;
200  } catch (\Exception $ex) {
201  $this->_errorMsg = $ex->getMessage();
202  return false;
203  }
204  }
205 
206  /**
207  * Create a black and white copy of an image.
208  * @param $srcName The source file name
209  * @param $destName The destination file name
210  */
211  public function createBlackWhiteImage($srcName, $destName) {
212  try {
213  $this->processImageFunction($srcName, $destName, "applyFilter", array(IMG_FILTER_GRAYSCALE));
214  return true;
215  } catch (\Exception $ex) {
216  $this->_errorMsg = $ex->getMessage();
217  return false;
218  }
219  }
220 
221  /**
222  * Process the given function on the given source image (supports animated gifs)
223  * and save the result in the given destination image.
224  * @param $srcName The source file name
225  * @param $destName The destination file name
226  * @param $function The name of the function
227  * @param $params The paremeters to be passed to the function
228  */
229  public function processImageFunction($srcName, $destName, $function, $params) {
230  if (GifFrameExtractor::isAnimatedGif($srcName)) {
231  // for animated gifs we need to process each frame
232  $gfe = new GifFrameExtractor();
233  $frames = $gfe->extract($srcName);
234  $retouchedFrames = array();
235  foreach ($frames as $frame) {
236  $frameLayer = ImageWorkshop::initFromResourceVar($frame['image']);
237  call_user_func_array(array($frameLayer, $function), $params);
238  $retouchedFrames[] = $frameLayer->getResult();
239  }
240  $gc = new GifCreator();
241  $gc->create($retouchedFrames, $gfe->getFrameDurations(), 0);
242  file_put_contents($destName, $gc->getGif());
243  }
244  else {
245  // all other images
246  $image = ImageWorkshop::initFromPath($srcName);
247  call_user_func_array(array($image, $function), $params);
248  $image->save(dirname($destName), basename($destName), true, null, 100);
249  }
250  chmod($destName, 0644);
251  }
252 
253  /**
254  * Render a text to an image. Using the default parameters the text will
255  * be rendered into a box that fits the text. If the width parameter is not null and the
256  * text exceeds the width, the text will be wrapped and the height parameter will be
257  * used as lineheight.
258  * Wrapping code is from http://de.php.net/manual/de/function.imagettfbbox.php#60673
259  * @param $text The text to render
260  * @param $fontfile The ttf font file to use
261  * @param $fontsize The font size to use (in pixels)
262  * @param $color The color to use for the text (as HEX value)
263  * @param $bgcolor The color to use for the background (as HEX value)
264  * @param $filename The name of the file to write to
265  * @param $width The width of the image (or null if it should fit the text) (default: _null_)
266  * @param $height The height of the image (or null if it should fit the text) (default: _null_)
267  * @param $x The x offset of the text (or null if it should be centered) (default: _null_)
268  * @param $y The y offset of the text (or null if the baseline should be the image border) (default: _null_)
269  * @param $angle The angle of the text (optional, default: _0_)
270  * @return Boolean whether the operation succeeded, error string provided by getErrorMsg()
271  */
272  public function renderText($text, $fontfile, $fontsize, $color, $bgcolor, $filename,
273  $width=null, $height=null, $x=null, $y=null, $angle=0) {
274  try {
275  // the destination lines array
276  $dstLines = array();
277  $lineheight = $height;
278 
279  // if a width is given, we wrap the text if necessary
280  if ($width != null) {
281  // remove windows line-breaks
282  $text = str_replace("\r", '', $text);
283  // split text into "lines"
284  $srcLines = split ("\n", $text);
285  foreach ($srcLines as $currentL) {
286  $line = '';
287  // split line into words
288  $wordsTmp = split(" ", $currentL);
289  // split at hyphens
290  $words = array();
291  foreach ($wordsTmp as $word) {
292  $wordParts = split(' ', str_replace(array('-', '/'), array('- ', '/ '), $word));
293  foreach ($wordParts as $wordPart) {
294  $words[] = $wordPart;
295  }
296  }
297  for ($i=0, $count=sizeof($words); $i<$count; $i++) {
298  $word = $words[$i];
299 
300  // get the length of this line, if the word is to be included
301  list($linewidth, $lineheight) = $this->getTextDimension($fontsize, $angle, $fontfile, $text);
302 
303  // check if it is too big if the word was added, if so, then move on
304  if ($linewidth > $width && !empty($line)) {
305  // add the line like it was without spaces
306  $dstLines[] = trim($line);
307  $line = '';
308  }
309  // add the trailing space only if the word does not end with a hyphen
310  // and it is not the last word
311  if (preg_match('/-$/', $word) || $i==sizeof($words)-1) {
312  $line .= $word;
313  }
314  else {
315  $line .= $word.' ';
316  }
317  }
318  // add the line when the line ends
319  $dstLines[] = trim($line);
320  }
321  // get the text dimensions
322  $textwidth = $width;
323  if ($height != null) {
324  $lineheight = $height;
325  }
326  $textheight = sizeof($dstLines)*$lineheight;
327  $height = $textheight;
328  }
329  else {
330  $dstLines[] = $text;
331  // get the text dimensions
332  list($textwidth, $textheight) = $this->getTextDimension($fontsize, $angle, $fontfile, $text);
333 
334  // add 5 pixels to the width.
335  // @todo make this a parameter
336  $textwidth += 5;
337 
338  // calculate offset and dimensions
339  list($width, $height, $x, $y) = $this->calculateTextParams($width, $height, $x, $y, $textwidth, $textheight);
340  }
341 
342  // create the image
343  $image = ImageWorkshop::initVirginLayer($width, $height, $bgcolor);
344 
345  // render the text onto the image
346  foreach ($dstLines as $nr => $line) {
347  // calculate offset and dimensions
348  list($width, $height, $x, $y) = $this->calculateTextParams($width, $height, $x, $y, $textwidth, $textheight);
349  // print the line
350  $image->write("$line", $fontfile, $fontsize, $color, $x, $y, $angle);
351  $y += $lineheight;
352  }
353 
354  // write the image
355  $image->save(dirname($filename), basename($filename), true, null, 100);
356  chmod($filename, 0644);
357  return true;
358  } catch (\Exception $ex) {
359  $this->_errorMsg = $ex->getMessage();
360  return false;
361  }
362  }
363 
364  /**
365  * Calculate the dimension of the given text
366  * @param $fontsize The font size (in pixels)
367  * @param $angle The angle of the characters (optional, default: _0_)
368  * @param $fontfile The font file
369  * @param $text The text
370  * @return An array with the width and height values
371  */
372  private function getTextDimension($fontsize, $angle, $fontfile, $text) {
373  list($x2, $y2, $x3, $y3, $x1, $y1, $x0, $y0) = imagettfbbox($fontsize, $angle, $fontfile, $text);
374  return array($x1-$x2, $y2-$y1);
375  }
376 
377  /**
378  * Calculate the offset of the text and the size of the image based on the given parameters
379  * @param $width The width of the image (or null if it should be the textwidth)
380  * @param $height The height of the image (or null if it should be the textheight)
381  * @param $x The x offset of the text (or null if it should be centered)
382  * @param $y The y offset of the text (or null if the baseline should be the image border)
383  * @param $textwidth The width of the text
384  * @param $textheight The height of the text
385  * @return An array with width, height, x, y values
386  */
387  private function calculateTextParams($width, $height, $x, $y, $textwidth, $textheight) {
388  // calculate dimensions
389  if ($width === null) {
390  $width = $textwidth;
391  }
392  if ($height === null) {
393  $height = $textheight;
394  }
395  // calculate offset
396  if ($x === null) {
397  $x = ($width-$textwidth)/2;
398  }
399  if ($y === null) {
400  $y = $height;
401  }
402  return array($width, $height, $x, $y);
403  }
404 
405  /**
406  * Calculate the size based on the image aspect, if only width or height
407  * are given.
408  * @param $width The requested width (maybe null)
409  * @param $height The requested height (maybe null)
410  * @param $imageWidth The image's width
411  * @param $imageHeight The image's height
412  * @return Array with width, height
413  */
414  private function calculateSizeParams($width, $height, $imageWidth, $imageHeight) {
415  if ($width == null) {
416  $width = $imageWidth/$imageHeight*$height;
417  }
418  elseif ($height == null) {
419  $height = $imageHeight/$imageWidth*$width;
420  }
421  return array(intval($width), intval($height));
422  }
423 }
424 ?>
isValidImageHeight($imgname, $height, $exact=true)
Check image height.
createBlackWhiteImage($srcName, $destName)
Create a black and white copy of an image.
createThumbnail($srcName, $destName, $width, $height)
Create a thumbnail of an image file.
Utility classes.
Definition: namespaces.php:97
fitIntoSquare($srcName, $maxDimension)
Calculate image dimension to fit into a square, preserving the aspect ratio.
cropImage($srcName, $destName, $width, $height, $x=null, $y=null)
Crop an image to the given size starting from the middle of a given start point.
static getInstance($name, $dynamicConfiguration=array())
isImage($imgname)
Check if a given file is an image.
isValidImageDimension($imgname, $width, $height, $exact=true)
Check image dimensions.
getErrorMsg()
Get last error message.
GraphicsUtil provides support for graphic manipulation.
processImageFunction($srcName, $destName, $function, $params)
Process the given function on the given source image (supports animated gifs) and save the result in ...
isValidImageWidth($imgname, $width, $exact=true)
Check image width.
renderText($text, $fontfile, $fontsize, $color, $bgcolor, $filename, $width=null, $height=null, $x=null, $y=null, $angle=0)
Render a text to an image.