Elgg  Version master
ImageService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
14 
21 class ImageService {
22 
23  use Loggable;
24 
25  const JPEG_QUALITY = 75;
26  const WEBP_QUALITY = 75;
27 
31  protected $imagine;
32 
36  protected $config;
37 
41  protected $mimetype;
42 
49  public function __construct(Config $config, MimeTypeService $mimetype) {
50 
51  switch ($config->image_processor) {
52  case 'imagick':
53  if (extension_loaded('imagick')) {
54  $this->imagine = new \Imagine\Imagick\Imagine();
55  break;
56  }
57 
58  // fallback to GD if Imagick is not loaded
59  default:
60  // default use GD
61  $this->imagine = new \Imagine\Gd\Imagine();
62  break;
63  }
64 
65  $this->config = $config;
66  $this->mimetype = $mimetype;
67  }
68 
97  public function resize(string $source, string $destination = null, array $params = []): bool {
98 
99  $destination = $destination ?? $source;
100 
101  try {
102  $resize_params = $this->normalizeResizeParameters($source, $params);
103 
104  $image = $this->imagine->open($source);
105 
106  $max_width = (int) elgg_extract('w', $resize_params);
107  $max_height = (int) elgg_extract('h', $resize_params);
108 
109  $x1 = (int) elgg_extract('x1', $resize_params, 0);
110  $y1 = (int) elgg_extract('y1', $resize_params, 0);
111  $x2 = (int) elgg_extract('x2', $resize_params, 0);
112  $y2 = (int) elgg_extract('y2', $resize_params, 0);
113 
114  if ($x2 > $x1 && $y2 > $y1) {
115  $crop_start = new Point($x1, $y1);
116  $crop_size = new Box($x2 - $x1, $y2 - $y1);
117  $image->crop($crop_start, $crop_size);
118  }
119 
120  $target_size = new Box($max_width, $max_height);
121  $image->resize($target_size);
122 
123  // create new canvas with a background (default: white)
124  $background_color = elgg_extract('background_color', $params, 'ffffff');
125  $thumbnail = $this->imagine->create($image->getSize(), $image->palette()->color($background_color));
126  $thumbnail->paste($image, new Point(0, 0));
127 
128  if (pathinfo($destination, PATHINFO_EXTENSION) === 'webp') {
129  $options = [
130  'webp_quality' => elgg_extract('webp_quality', $params, self::WEBP_QUALITY),
131  ];
132  } else {
133  $options = [
134  'format' => $this->getFileFormat($source, $params),
135  'jpeg_quality' => elgg_extract('jpeg_quality', $params, self::JPEG_QUALITY),
136  ];
137  }
138 
139  $thumbnail->save($destination, $options);
140 
141  unset($image);
142  unset($thumbnail);
143  } catch (\Exception $ex) {
144  $this->getLogger()->error($ex);
145 
146  return false;
147  }
148 
149  return true;
150  }
151 
159  public function fixOrientation($filename) {
160  try {
161  $image = $this->imagine->open($filename);
162  $metadata = $image->metadata();
163  if (!isset($metadata['ifd0.Orientation'])) {
164  // no need to perform an orientation fix
165  return true;
166  }
167 
168  $autorotate = new Autorotate();
169  $autorotate->apply($image)->save($filename);
170 
171  $image->strip()->save($filename);
172 
173  return true;
174  } catch (\Exception $ex) {
175  $this->getLogger()->notice($ex);
176  }
177 
178  return false;
179  }
180 
196  public function normalizeResizeParameters(string $source, array $params = []): array {
197 
198  $image = $this->imagine->open($source);
199 
200  $width = $image->getSize()->getWidth();
201  $height = $image->getSize()->getHeight();
202 
203  $max_width = (int) elgg_extract('w', $params, 100, false);
204  $max_height = (int) elgg_extract('h', $params, 100, false);
205  if (!$max_height || !$max_width) {
206  throw new InvalidArgumentException('Resize width and height parameters are required');
207  }
208 
209  $square = elgg_extract('square', $params, false);
210  $upscale = elgg_extract('upscale', $params, false);
211 
212  $x1 = (int) elgg_extract('x1', $params, 0);
213  $y1 = (int) elgg_extract('y1', $params, 0);
214  $x2 = (int) elgg_extract('x2', $params, 0);
215  $y2 = (int) elgg_extract('y2', $params, 0);
216 
217  $cropping_mode = $x1 || $y1 || $x2 || $y2;
218 
219  if ($cropping_mode) {
220  $crop_width = $x2 - $x1;
221  $crop_height = $y2 - $y1;
222  if ($crop_width <= 0 || $crop_height <= 0 || $crop_width > $width || $crop_height > $height) {
223  throw new RangeException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for image cropping");
224  }
225  } else {
226  // everything selected if no crop parameters
227  $crop_width = $width;
228  $crop_height = $height;
229  }
230 
231  // determine cropping offsets
232  if ($square) {
233  // asking for a square image back
234 
235  // size of the new square image
236  $max_width = min($max_width, $max_height);
237  $max_height = $max_width;
238 
239  // find largest square that fits within the selected region
240  $crop_width = min($crop_width, $crop_height);
241  $crop_height = $crop_width;
242 
243  if (!$cropping_mode) {
244  // place square region in the center
245  $x1 = floor(($width - $crop_width) / 2);
246  $y1 = floor(($height - $crop_height) / 2);
247  }
248  } else {
249  // maintain aspect ratio of original image/crop
250  if ($crop_height / $max_height > $crop_width / $max_width) {
251  $max_width = floor($max_height * $crop_width / $crop_height);
252  } else {
253  $max_height = floor($max_width * $crop_height / $crop_width);
254  }
255  }
256 
257  if (!$upscale && ($crop_height < $max_height || $crop_width < $max_width)) {
258  // we cannot upscale and selected area is too small so we decrease size of returned image
259  $max_height = $crop_height;
260  $max_width = $crop_width;
261  }
262 
263  return [
264  'w' => $max_width,
265  'h' => $max_height,
266  'x1' => $x1,
267  'y1' => $y1,
268  'x2' => $x1 + $crop_width,
269  'y2' => $y1 + $crop_height,
270  'square' => $square,
271  'upscale' => $upscale,
272  ];
273  }
274 
284  protected function getFileFormat($filename, $params) {
285 
286  $accepted_formats = [
287  'image/jpeg' => 'jpeg',
288  'image/pjpeg' => 'jpeg',
289  'image/png' => 'png',
290  'image/x-png' => 'png',
291  'image/gif' => 'gif',
292  'image/vnd.wap.wbmp' => 'wbmp',
293  'image/x‑xbitmap' => 'xbm',
294  'image/x‑xbm' => 'xbm',
295  ];
296 
297  // was a valid output format supplied
298  $format = elgg_extract('format', $params);
299  if (in_array($format, $accepted_formats)) {
300  return $format;
301  }
302 
303  try {
304  return elgg_extract($this->mimetype->getMimeType($filename), $accepted_formats);
305  } catch (InvalidArgumentException $e) {
306  $this->getLogger()->warning($e);
307  }
308  }
309 
315  public function hasWebPSupport(): bool {
316  if ($this->config->webp_enabled === false) {
317  return false;
318  }
319 
320  if ($this->imagine instanceof \Imagine\Imagick\Imagine) {
321  return !empty(\Imagick::queryformats('WEBP*'));
322  } elseif ($this->imagine instanceof \Imagine\Gd\Imagine) {
323  return (bool) elgg_extract('WebP Support', gd_info(), false);
324  }
325 
326  return false;
327  }
328 }
$format
Definition: date.php:37
__construct(Config $config, MimeTypeService $mimetype)
Constructor.
Exception thrown to indicate range errors during program execution.
normalizeResizeParameters(string $source, array $params=[])
Calculate the parameters for resizing an image.
$source
Exception thrown if an argument is not of the expected type.
$params
Saves global plugin settings.
Definition: save.php:13
if($item instanceof\ElggEntity) elseif($item instanceof\ElggRiverItem) elseif($item instanceof\ElggRelationship) elseif(is_callable([$item, 'getType']))
Definition: item.php:48
$options
Elgg admin footer.
Definition: footer.php:6
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
fixOrientation($filename)
If needed the image will be rotated based on orientation information.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
resize(string $source, string $destination=null, array $params=[])
Crop and resize an image.
$image
Definition: image_block.php:25
Image manipulation service.
getFileFormat($filename, $params)
Determine the image file format, this is needed for correct resizing.
getLogger()
Returns logger.
Definition: Loggable.php:37
$metadata
Output annotation metadata.
Definition: metadata.php:9
hasWebPSupport()
Checks if imagine has WebP support.
Public service related to MIME type detection.