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