ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
ImageConverter.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
25use ILIAS\Filesystem\Util\MemoryStreamToTempFileStream;
26
32{
33 use MemoryStreamToTempFileStream;
34
35 protected const STATUS_OK = 1;
36 protected const STATUS_FAILED = 2;
37 protected const STATUS_UNKNOWN = 4;
38
39 protected const RESOLUTION = 72;
40 protected const RESOLUTION_FACTOR = self::RESOLUTION / 72;
41
43 protected ?FileStream $output_stream = null;
44 protected ?\Throwable $throwable = null;
45 protected ?string $requested_background = null;
46 protected \Imagick $image;
47
48 public function __construct(
49 protected ImageConversionOptions $conversion_options,
50 protected ImageOutputOptions $output_options,
51 protected FileStream $stream
52 ) {
53 $this->image = new \Imagick();
54 $this->convert();
55 }
56
57 private function convert(): void
58 {
59 try {
60 $this->handleBackgroundColor();
61 $this->readInputStream();
63 $this->handleImageDimension();
64 $this->buildOutputStream();
65 } catch (\Throwable $t) {
66 $this->status = self::STATUS_FAILED;
67 $this->throwable = $t;
68 if ($this->conversion_options->throwOnError()) {
69 throw $t;
70 }
71 }
72 }
73
74
75 protected function handleImageDimension(): void
76 {
77 $requested_width = $this->conversion_options->getWidth();
78 $requested_height = $this->conversion_options->getHeight();
79 $original_image_width = $this->image->getImageWidth();
80 $original_image_height = $this->image->getImageHeight();
81
82 switch ($this->conversion_options->getDimensionMode()) {
83 default:
85 // no resizing
86 return;
88 if ($this->conversion_options->hasCrop()) {
89 $final_height = $requested_height;
90 $final_width = $requested_width;
91 } else {
92 // this is a special case, where we want to fit the image into the given dimensions and
93 // Imagick knows the thumbnail method for that
94 $this->doThumbnail(
95 $requested_width,
96 $requested_height
97 );
98 return;
99 }
100 break;
102 // by width and height
103 if ($requested_width > 0 && $requested_height > 0) {
104 $final_width = $requested_width;
105 $final_height = $requested_height;
106 } else {
107 throw new \InvalidArgumentException('Dimension Mode does not match the given width/height');
108 }
109 break;
111 // by height
112 if ($requested_width === null && $requested_height > 0) {
113 $ratio = $original_image_height / $requested_height;
114 $final_width = intval($original_image_width / $ratio);
115 $final_height = $requested_height;
116 $l = 1;
117 } else {
118 throw new \InvalidArgumentException('Dimension Mode does not match the given width/height');
119 }
120 break;
122 // by width
123 if ($requested_width > 0 && $requested_height === null) {
124 $ratio = $original_image_width / $requested_width;
125 $final_width = $requested_width;
126 $final_height = intval($original_image_height / $ratio);
127 } else {
128 throw new \InvalidArgumentException('Dimension Mode does not match the given width/height');
129 }
130 break;
132 // by none of them
133 if ($requested_width === null && $requested_height === null) {
134 $final_width = $original_image_width;
135 $final_height = $original_image_height;
136 } else {
137 throw new \InvalidArgumentException('Dimension Mode does not match the given width/height');
138 }
139 break;
140 }
141
142 if ($this->conversion_options->hasCrop()) {
143 $this->doCrop(
144 $final_width,
145 $final_height
146 );
147 } else {
148 $this->doResize(
149 $final_width,
150 $final_height
151 );
152 }
153 }
154
155
156 protected function buildOutputStream(): void
157 {
158 if ($this->conversion_options->getOutputPath() === null) {
159 $this->output_stream = Streams::ofString($this->image->getImageBlob());
160 } else {
161 $this->image->writeImage($this->conversion_options->getOutputPath());
162 $this->output_stream = Streams::ofResource(fopen($this->conversion_options->getOutputPath(), 'rb'));
163 }
164
165 $this->output_stream->rewind();
166
167 $this->status = self::STATUS_OK;
168 }
169
170 protected function handleFormatAndQuality(): void
171 {
172 $this->image->setImageResolution(
173 self::RESOLUTION,
174 self::RESOLUTION
175 );
176 // High density images do not support Resampling, cache to small. we deactivate this
177 /*$this->image->resampleImage(
178 self::RESOLUTION,
179 self::RESOLUTION,
180 \Imagick::FILTER_LANCZOS,
181 1
182 );*/
183 $quality = $this->output_options->getQuality();
184
185 // if $this->output_options->getFormat() is 'keep', we map it to the original format
186 if ($this->output_options->getFormat() === ImageOutputOptions::FORMAT_KEEP) {
187 try {
188 $this->output_options = $this->output_options->withFormat(strtolower($this->image->getImageFormat()));
189 } catch (\InvalidArgumentException) {
190 }
191 }
192
193 switch ($this->output_options->getFormat()) {
195 $this->image->setImageFormat('webp');
196 $this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
197 $this->image = $this->image->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
198 if ($quality === 0) {
199 $this->image->setOption('webp:lossless', 'false');
200 }
201 if ($quality === 100) {
202 $this->image->setOption('webp:lossless', 'true');
203 }
204 break;
206 $this->image->setImageFormat('jpeg');
207 $this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
208 $this->image = $this->image->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
209 $this->image->setImageCompression(\Imagick::COMPRESSION_JPEG);
210 $this->image->setImageCompressionQuality($quality);
211 break;
213 $png_compression_level = round($quality / 100 * 9, 0);
214 if ($this->requested_background !== null && $this->requested_background !== 'transparent') {
215 $this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
216 } else {
217 $this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_ACTIVATE);
218 }
219 $this->image->setImageFormat('png');
220 $this->image->setOption('png:compression-level', (string) $png_compression_level);
221 break;
222 }
223 $this->image->stripImage();
224 }
225
226 protected function handleBackgroundColor(): void
227 {
228 $this->requested_background = $this->conversion_options->getBackgroundColor();
229 if ($this->output_options->getFormat(
230 ) === ImageOutputOptions::FORMAT_JPG && $this->requested_background === null) {
231 $this->requested_background = '#FFFFFF';
232 }
233 if ($this->output_options->getFormat(
234 ) === ImageOutputOptions::FORMAT_PNG && $this->requested_background === null) {
235 $this->requested_background = 'transparent';
236 }
237 if ($this->requested_background !== null) {
238 $this->image->setBackgroundColor(new \ImagickPixel($this->requested_background));
239 }
240 }
241
242 protected function readInputStream(): void
243 {
244 if ($this->conversion_options->makeTemporaryFiles()) {
245 $this->stream = $this->maybeSafeToTempStream($this->stream);
246 }
247 $this->stream->rewind();
248 $this->image->readImageFile($this->stream->detach());
249 }
250
251
252 public function isOK(): bool
253 {
254 return $this->status === self::STATUS_OK;
255 }
256
257 public function getThrowableIfAny(): ?\Throwable
258 {
259 return $this->throwable;
260 }
261
262
263 public function getStream(): FileStream
264 {
266 }
267
268 private function factoredResolution(int $initial): int
269 {
270 return intval(round($initial * self::RESOLUTION_FACTOR, 0));
271 }
272
273 protected function doCrop(int $width, int $height): void
274 {
275 $this->image->setGravity(\Imagick::GRAVITY_CENTER);
276 $this->image->cropThumbnailImage(
277 $this->factoredResolution($width),
278 $this->factoredResolution($height)
279 );
280 }
281
282 protected function doResize(int $width, int $height): void
283 {
284 $this->image->resizeImage(
285 $this->factoredResolution($width),
286 $this->factoredResolution($height),
287 \Imagick::FILTER_LANCZOS,
288 1
289 );
290 }
291
292 protected function doThumbnail(int $width, int $height): void
293 {
294 $this->image->thumbnailImage(
295 $this->factoredResolution($width),
296 $this->factoredResolution($height),
297 true,
298 !$this->conversion_options->keepAspectRatio(),
299 );
300 }
301}
Stream factory which enables the user to create streams without the knowledge of the concrete class.
Definition: Streams.php:32
static ofString(string $string)
Creates a new stream with an initial value.
Definition: Streams.php:41
static ofResource($resource)
Wraps an already created resource with the stream abstraction.
Definition: Streams.php:64
__construct(protected ImageConversionOptions $conversion_options, protected ImageOutputOptions $output_options, protected FileStream $stream)
The base interface for all filesystem streams.
Definition: FileStream.php:32