ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
ImageConversionTest.php
Go to the documentation of this file.
1 <?php
2 
19 namespace ILIAS\Filesystem\Util;
20 
29 
33 class ImageConversionTest extends TestCase
34 {
36 
37  protected const BY_WIDTH_FINAL = 256;
38  protected const BY_HEIGHT_FINAL = 756;
39  protected const W = 'width';
40  protected const H = 'height';
41  protected const IMAGE_JPEG = 'image/jpeg';
42  protected const IMAGE_PNG = 'image/png';
43  protected const IMAGE_WEBP = 'image/webp';
44  protected Images $images;
45 
46  protected function setUp(): void
47  {
48  $this->checkImagick();
49  $this->images = new Images(
50  true,
51  );
52  }
53 
54  public function testImageThumbnailActualImage(): void
55  {
56  $img = __DIR__ . '/img/robot.jpg';
57  $this->assertFileExists($img);
58  $getimagesize = getimagesize($img);
59  $original_width = $getimagesize[0]; // should be 600
60  $original_height = $getimagesize[1]; // should be 800
61  $this->assertEquals(600, $original_width);
62  $this->assertEquals(800, $original_height);
63 
64  // make tumbnail
65  $original_stream = Streams::ofResource(fopen($img, 'rb'));
66 
67  $thumbnail_converter = $this->images->thumbnail(
68  $original_stream,
69  100
70  );
71  $this->assertTrue($thumbnail_converter->isOK());
72  $this->assertNull($thumbnail_converter->getThrowableIfAny());
73  $converted_stream = $thumbnail_converter->getStream();
74 
75  $getimagesizefromstring = getimagesizefromstring((string) $converted_stream);
76 
77  $this->assertEquals(75, $getimagesizefromstring[0]); // width
78  $this->assertEquals(100, $getimagesizefromstring[1]); // height
79  }
80 
81  public function testImageSquareActualImage(): void
82  {
83  $img = __DIR__ . '/img/robot.jpg';
84  $this->assertFileExists($img);
85  $getimagesize = getimagesize($img);
86  $original_width = $getimagesize[0]; // should be 600
87  $original_height = $getimagesize[1]; // should be 800
88  $this->assertEquals(600, $original_width);
89  $this->assertEquals(800, $original_height);
90 
91  // make tumbnail
92  $original_stream = Streams::ofResource(fopen($img, 'rb'));
93 
94  $thumbnail_converter = $this->images->croppedSquare(
95  $original_stream,
96  200
97  );
98  $this->assertTrue($thumbnail_converter->isOK());
99  $this->assertNull($thumbnail_converter->getThrowableIfAny());
100 
101  $getimagesizefromstring = $this->getImageSizeFromStream($thumbnail_converter->getStream());
102 
103  $this->assertEquals(200, $getimagesizefromstring[self::W]);
104  $this->assertEquals(200, $getimagesizefromstring[self::H]);
105  }
106 
107  public static function getImageSizesByWidth(): array
108  {
109  return [
110  [400, 300, self::BY_WIDTH_FINAL, 192],
111  [300, 400, self::BY_WIDTH_FINAL, 341],
112  [543, 431, self::BY_WIDTH_FINAL, 203],
113  [200, 200, self::BY_WIDTH_FINAL, 256],
114  ];
115  }
116 
117  #[DataProvider('getImageSizesByWidth')]
118  public function testResizeToFitWidth(
119  int $width,
120  int $height,
121  int $final_width,
122  int $final_height
123  ): void {
124  $stream = $this->createTestImageStream($width, $height);
125  $dimensions = $this->getImageSizeFromStream($stream);
126  $this->assertEquals($width, $dimensions[self::W]);
127  $this->assertEquals($height, $dimensions[self::H]);
128 
129  // resize to fit width
130  $resized = $this->images->resizeByWidth($stream, self::BY_WIDTH_FINAL);
131  $this->assertTrue($resized->isOK());
132  $new_dimensions = $this->getImageSizeFromStream($resized->getStream());
133 
134  $this->assertEquals($final_width, $new_dimensions[self::W]);
135  $this->assertEquals($final_height, $new_dimensions[self::H]);
136 
137  // check aspect ratio
138  $this->assertEquals(
139  round($width > $height),
140  round($final_width > $final_height)
141  );
142  $this->assertEquals(
143  round($width / $height),
144  round($new_dimensions[self::W] / $new_dimensions[self::H])
145  );
146  $this->assertEquals(
147  $width > $height,
148  $width > $height
149  );
150  $this->assertEquals(
151  $width > $height,
152  $new_dimensions[self::W] > $new_dimensions[self::H]
153  );
154  }
155 
156  public static function getImageSizesByHeight(): array
157  {
158  return [
159  [400, 300, self::BY_HEIGHT_FINAL, 1008],
160  [300, 400, self::BY_HEIGHT_FINAL, 567],
161  [200, 200, self::BY_HEIGHT_FINAL, 756],
162  [248, 845, self::BY_HEIGHT_FINAL, 221],
163  ];
164  }
165 
166 
167  #[DataProvider('getImageSizesByHeight')]
168  public function testResizeToFitHeight(
169  int $width,
170  int $height,
171  int $final_height,
172  int $final_width
173  ): void {
174  $stream = $this->createTestImageStream($width, $height);
175  $dimensions = $this->getImageSizeFromStream($stream);
176  $this->assertEquals($width, $dimensions[self::W]);
177  $this->assertEquals($height, $dimensions[self::H]);
178 
179  // resize to fit
180  $resized = $this->images->resizeByHeight($stream, self::BY_HEIGHT_FINAL);
181  $this->assertTrue($resized->isOK());
182  $new_dimensions = $this->getImageSizeFromStream($resized->getStream());
183 
184  $this->assertEquals($final_width, $new_dimensions[self::W]);
185  $this->assertEquals($final_height, $new_dimensions[self::H]);
186 
187  // check aspect ratio
188  $this->assertEquals(
189  round($width > $height),
190  round($final_width > $final_height)
191  );
192  $this->assertEquals(
193  round($width / $height),
194  round($new_dimensions[self::W] / $new_dimensions[self::H])
195  );
196  $this->assertEquals(
197  $width > $height,
198  $width > $height
199  );
200  $this->assertEquals(
201  $width > $height,
202  $new_dimensions[self::W] > $new_dimensions[self::H]
203  );
204  }
205 
206  public static function getImageSizesByFixed(): array
207  {
208  return [
209  [1024, 768, 300, 100, true],
210  [1024, 768, 300, 100, false],
211  [1024, 768, 100, 300, true],
212  [1024, 768, 100, 300, false],
213  [400, 300, 500, 400, true],
214  [400, 300, 500, 400, false],
215  ];
216  }
217 
218  #[DataProvider('getImageSizesByFixed')]
219  public function testResizeByFixedSize(
220  int $width,
221  int $height,
222  int $final_width,
223  int $final_height,
224  bool $crop
225  ): void {
226  $stream = $this->createTestImageStream($width, $height);
227  $dimensions = $this->getImageSizeFromStream($stream);
228  $this->assertEquals($width, $dimensions[self::W]);
229  $this->assertEquals($height, $dimensions[self::H]);
230 
231  $by_fixed = $this->images->resizeToFixedSize($stream, $final_width, $final_height, $crop);
232  $this->assertTrue($by_fixed->isOK());
233  $new_dimensions = $this->getImageSizeFromStream($by_fixed->getStream());
234 
235  $this->assertEquals($final_width, $new_dimensions[self::W]);
236  $this->assertEquals($final_height, $new_dimensions[self::H]);
237  }
238 
239  public static function getImageOptions(): array
240  {
241  $options = new ImageOutputOptions();
242  return [
243  [$options, self::IMAGE_JPEG, 75],
244  [$options->withPngOutput()->withQuality(22), self::IMAGE_PNG, 0],
245  [$options->withJpgOutput()->withQuality(100), self::IMAGE_JPEG, 100],
246  [$options->withFormat('png')->withQuality(50), self::IMAGE_PNG, 0],
247  [$options->withFormat('jpg')->withQuality(87), self::IMAGE_JPEG, 87],
248  [$options->withQuality(5)->withJpgOutput(), self::IMAGE_JPEG, 5],
249  [$options->withQuality(10)->withJpgOutput(), self::IMAGE_JPEG, 10],
250  [$options->withQuality(35)->withJpgOutput(), self::IMAGE_JPEG, 35],
251  [$options->withQuality(0)->withWebPOutput(), self::IMAGE_WEBP, 0],
252  [$options->withQuality(100)->withWebPOutput(), self::IMAGE_WEBP, 100],
253  ];
254  }
255 
256 
257  #[DataProvider('getImageOptions')]
258  public function testImageOutputOptions(
259  ImageOutputOptions $options,
260  string $expected_mime_type,
261  int $expected_quality
262  ): void {
263  $resized = $this->images->resizeToFixedSize(
264  $this->createTestImageStream(10, 10),
265  5,
266  5,
267  true,
268  $options
269  );
270 
271  $this->assertEquals($expected_mime_type, $this->getImageTypeFromStream($resized->getStream()));
272  $this->assertEquals($expected_quality, $this->getImageQualityFromStream($resized->getStream()));
273  }
274 
275  public function testImageOutputOptionSanity(): void
276  {
277  $options = new ImageOutputOptions();
278 
279  // Defaults
280  $this->assertEquals('jpg', $options->getFormat());
281  $this->assertEquals(75, $options->getQuality());
282 
283  $png = $options->withPngOutput();
284  $this->assertEquals('png', $png->getFormat());
285  $this->assertEquals('jpg', $options->getFormat()); // original options should not change
286  $png_explicit = $options->withFormat('png');
287  $this->assertEquals('png', $png_explicit->getFormat());
288 
289  $jpg = $options->withJpgOutput();
290  $this->assertEquals('jpg', $jpg->getFormat());
291  $jpg_explicit = $options->withFormat('jpg');
292  $this->assertEquals('jpg', $jpg_explicit->getFormat());
293  $jpeg = $options->withFormat('jpeg');
294  $this->assertEquals('jpg', $jpeg->getFormat());
295 
296  // Quality
297  $low = $options->withQuality(5);
298  $this->assertEquals(5, $low->getQuality());
299  $this->assertEquals(75, $options->getQuality()); // original options should not change
300  }
301 
302  public static function getWrongFormats(): array
303  {
304  return [
305  ['gif'],
306  ['bmp'],
307  ['jpg2000'],
308  ];
309  }
310 
311  #[DataProvider('getWrongFormats')]
312  public function testWrongFormats(string $format): void
313  {
314  $options = new ImageOutputOptions();
315  $this->expectException(\InvalidArgumentException::class);
316  $wrong = $options->withFormat($format);
317  }
318 
319  public static function getWrongQualites(): array
320  {
321  return [
322  [-1],
323  [101],
324  [102],
325  ];
326  }
327 
328  #[DataProvider('getWrongQualites')]
329  public function testWrongQualities(int $quality): void
330  {
331  $options = new ImageOutputOptions();
332  $this->expectException(\InvalidArgumentException::class);
333  $wrong = $options->withQuality($quality);
334  }
335 
336  public function testFormatConvert(): void
337  {
338  $jpg = $this->createTestImageStream(10, 10);
339  $png = $this->images->convertToFormat(
340  $jpg,
341  'png'
342  );
343 
344  $this->assertEquals(self::IMAGE_PNG, $this->getImageTypeFromStream($png->getStream()));
345  $size = $this->getImageSizeFromStream($png->getStream());
346  $this->assertEquals(10, $size[self::W]);
347  $this->assertEquals(10, $size[self::H]);
348 
349  // With Dimensions
350  $jpg = $this->createTestImageStream(10, 10);
351  $png = $this->images->convertToFormat(
352  $jpg,
353  'png',
354  20,
355  20
356  );
357 
358  $this->assertEquals(self::IMAGE_PNG, $this->getImageTypeFromStream($png->getStream()));
359  $size = $this->getImageSizeFromStream($png->getStream());
360  $this->assertEquals(20, $size[self::W]);
361  $this->assertEquals(20, $size[self::H]);
362  }
363 
364  public function testFailed(): void
365  {
366  $false_stream = Streams::ofString('false');
367  $images = new Images(
368  false,
369  false
370  );
371 
372  $resized = $images->resizeToFixedSize(
373  $false_stream,
374  5,
375  5
376  );
377  $this->assertFalse($resized->isOK());
378  $this->assertInstanceOf(\Throwable::class, $resized->getThrowableIfAny());
379  }
380 
381  public static function getColors(): array
382  {
383  return [
384  [null],
385  ['#000000'],
386  ['#ff0000'],
387  ['#00ff00'],
388  ['#0000ff'],
389  ['#ffffff'],
390  ['#A3BF5A'],
391  ['#E9745A'],
392  ['#5A5AE9'],
393  ['#5AE9E9'],
394  ['#E95AE9'],
395  ['#E9E95A'],
396  ];
397  }
398 
399  private function colorDiff(string $hex_color_one, string $hex_color_two): int
400  {
401  preg_match('/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i', $hex_color_one, $rgb_one);
402  preg_match('/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i', $hex_color_two, $rgb_two);
403 
404  return abs(hexdec($rgb_one[1]) - hexdec($rgb_two[1]))
405  + abs(hexdec($rgb_one[2]) - hexdec($rgb_two[2]))
406  + abs(hexdec($rgb_one[3]) - hexdec($rgb_two[3]));
407  }
408 
409  #[DataProvider('getColors')]
410  public function testBackgroundColor(?string $color): void
411  {
412  $transparent_png = __DIR__ . '/img/transparent.png';
413  $this->assertFileExists($transparent_png);
414  $png = Streams::ofResource(fopen($transparent_png, 'rb'));
415 
416  $converter_options = (new ImageConversionOptions())
417  ->withThrowOnError(true)
418  ->withFixedDimensions(100, 100);
419 
420  if ($color !== null) {
421  $converter_options = $converter_options->withBackgroundColor($color);
422  } else {
423  $color = '#ffffff';
424  }
425 
426  $output_options = (new ImageOutputOptions())
427  ->withQuality(100)
428  ->withJpgOutput();
429 
430  $converter = new ImageConverter($converter_options, $output_options, $png);
431  $this->assertTrue($converter->isOK());
432  $converted_stream = $converter->getStream();
433  $gd_image = imagecreatefromstring((string) $converted_stream);
434  $colors = imagecolorsforindex($gd_image, imagecolorat($gd_image, 1, 1));
435 
436  $color_in_converted_picture = sprintf("#%02x%02x%02x", $colors['red'], $colors['green'], $colors['blue']);
437  $color_diff = $this->colorDiff($color, $color_in_converted_picture);
438 
439  $this->assertLessThan(3, $color_diff);
440  }
441 
442  public function testWriteImage(): void
443  {
444  $img = $this->createTestImageStream(10, 10);
445 
446  $output_path = __DIR__ . '/img/output.jpg';
447  $converter_options = (new ImageConversionOptions())
448  ->withThrowOnError(true)
449  ->withMakeTemporaryFiles(false)
450  ->withFixedDimensions(100, 100)
451  ->withOutputPath($output_path);
452 
453  $output_options = (new ImageOutputOptions())
454  ->withQuality(10)
455  ->withJpgOutput();
456 
457  $converter = new ImageConverter($converter_options, $output_options, $img);
458  $this->assertTrue($converter->isOK());
459 
460  $this->assertFileExists($output_path);
461  $stream = $converter->getStream();
462  $this->assertEquals($output_path, $stream->getMetadata('uri'));
463 
464  unlink($output_path);
465  }
466 
467  public function testHighDensityPixel(): void
468  {
469  $file = 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Jan_Vermeer_-_The_Art_of_Painting_-_Google_Art_Project.jpg';
470  $curl = curl_init();
471  curl_setopt($curl, CURLOPT_URL, $file);
472  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
473  curl_setopt($curl, CURLOPT_USERAGENT, 'PHPUnit/1.0');
474  $string = curl_exec($curl);
475  curl_close($curl);
476 
477  $img = Streams::ofString($string);
478  $this->assertInstanceOf(FileStream::class, $img);
479 
480  $converter_options = (new ImageConversionOptions())
481  ->withWidth(80)
482  ->withHeight(80)
483  ->withKeepAspectRatio(true)
484  ->withCrop(true)
485  ->withThrowOnError(true);
486 
487  $output_options = (new ImageOutputOptions())
488  ->withQuality(60)
489  ->withFormat(ImageOutputOptions::FORMAT_PNG);
490 
491  $converter = new ImageConverter($converter_options, $output_options, $img);
492  $this->assertTrue($converter->isOK());
493  }
494 
495 
496  protected function checkImagick(): void
497  {
498  if (!class_exists('Imagick')) {
499  $this->markTestSkipped('Imagick not installed');
500  }
501  }
502 
503  protected function getImageSizeFromStream(FileStream $stream): array
504  {
505  $getimagesizefromstring = getimagesizefromstring((string) $stream);
506  return [
507  self::W => (int) round($getimagesizefromstring[0]),
508  self::H => (int) round($getimagesizefromstring[1])
509  ];
510  }
511 
512  protected function getImageTypeFromStream(FileStream $stream): string
513  {
514  return finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $stream->read(255));
515  }
516 
517  protected function getImageQualityFromStream(FileStream $stream): int
518  {
519  $stream->rewind();
520  $img = new \Imagick();
521  $img->readImageBlob((string) $stream);
522 
523  return $img->getImageCompressionQuality();
524  }
525 
526  protected function createTestImageStream(int $width, int $height): FileStream
527  {
528  $img = new \Imagick();
529  $img->newImage($width, $height, new \ImagickPixel('black'));
530  $img->setImageFormat('jpg');
531 
532  $stream = Streams::ofString($img->getImageBlob());
533  $stream->rewind();
534  return $stream;
535  }
536 }
testResizeToFitWidth(int $width, int $height, int $final_width, int $final_height)
testResizeByFixedSize(int $width, int $height, int $final_width, int $final_height, bool $crop)
testImageOutputOptions(ImageOutputOptions $options, string $expected_mime_type, int $expected_quality)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
colorDiff(string $hex_color_one, string $hex_color_two)
testResizeToFitHeight(int $width, int $height, int $final_height, int $final_width)
The base interface for all filesystem streams.
Definition: FileStream.php:31