Completed
Push — master ( b9480f...47c078 )
by Christoph
20:35 queued 14s
created
lib/private/Preview/Generator.php 1 patch
Indentation   +574 added lines, -574 removed lines patch added patch discarded remove patch
@@ -25,592 +25,592 @@
 block discarded – undo
25 25
 use Psr\Log\LoggerInterface;
26 26
 
27 27
 class Generator {
28
-	public const SEMAPHORE_ID_ALL = 0x0a11;
29
-	public const SEMAPHORE_ID_NEW = 0x07ea;
30
-
31
-	public function __construct(
32
-		private IConfig $config,
33
-		private IPreview $previewManager,
34
-		private IAppData $appData,
35
-		private GeneratorHelper $helper,
36
-		private IEventDispatcher $eventDispatcher,
37
-		private LoggerInterface $logger,
38
-	) {
39
-	}
40
-
41
-	/**
42
-	 * Returns a preview of a file
43
-	 *
44
-	 * The cache is searched first and if nothing usable was found then a preview is
45
-	 * generated by one of the providers
46
-	 *
47
-	 * @return ISimpleFile
48
-	 * @throws NotFoundException
49
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
50
-	 */
51
-	public function getPreview(
52
-		File $file,
53
-		int $width = -1,
54
-		int $height = -1,
55
-		bool $crop = false,
56
-		string $mode = IPreview::MODE_FILL,
57
-		?string $mimeType = null,
58
-		bool $cacheResult = true,
59
-	): ISimpleFile {
60
-		$specification = [
61
-			'width' => $width,
62
-			'height' => $height,
63
-			'crop' => $crop,
64
-			'mode' => $mode,
65
-		];
66
-
67
-		$this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent(
68
-			$file,
69
-			$width,
70
-			$height,
71
-			$crop,
72
-			$mode,
73
-			$mimeType,
74
-		));
75
-
76
-		$this->logger->debug('Requesting preview for {path} with width={width}, height={height}, crop={crop}, mode={mode}, mimeType={mimeType}', [
77
-			'path' => $file->getPath(),
78
-			'width' => $width,
79
-			'height' => $height,
80
-			'crop' => $crop,
81
-			'mode' => $mode,
82
-			'mimeType' => $mimeType,
83
-		]);
84
-
85
-
86
-		// since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
87
-		return $this->generatePreviews($file, [$specification], $mimeType, $cacheResult);
88
-	}
89
-
90
-	/**
91
-	 * Generates previews of a file
92
-	 *
93
-	 * @throws NotFoundException
94
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
95
-	 */
96
-	public function generatePreviews(File $file, array $specifications, ?string $mimeType = null, bool $cacheResult = true): ISimpleFile {
97
-		//Make sure that we can read the file
98
-		if (!$file->isReadable()) {
99
-			$this->logger->warning('Cannot read file: {path}, skipping preview generation.', ['path' => $file->getPath()]);
100
-			throw new NotFoundException('Cannot read file');
101
-		}
102
-
103
-		if ($mimeType === null) {
104
-			$mimeType = $file->getMimeType();
105
-		}
106
-
107
-		$previewFolder = $this->getPreviewFolder($file);
108
-		// List every existing preview first instead of trying to find them one by one
109
-		$previewFiles = $previewFolder->getDirectoryListing();
110
-
111
-		$previewVersion = '';
112
-		if ($file instanceof IVersionedPreviewFile) {
113
-			$previewVersion = $file->getPreviewVersion() . '-';
114
-		}
115
-
116
-		// Get the max preview and infer the max preview sizes from that
117
-		$maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion);
118
-		$maxPreviewImage = null; // only load the image when we need it
119
-		if ($maxPreview->getSize() === 0) {
120
-			$maxPreview->delete();
121
-			$this->logger->error('Max preview generated for file {path} has size 0, deleting and throwing exception.', ['path' => $file->getPath()]);
122
-			throw new NotFoundException('Max preview size 0, invalid!');
123
-		}
124
-
125
-		[$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
126
-
127
-		if ($maxWidth <= 0 || $maxHeight <= 0) {
128
-			throw new NotFoundException('The maximum preview sizes are zero or less pixels');
129
-		}
130
-
131
-		$preview = null;
132
-
133
-		foreach ($specifications as $specification) {
134
-			$width = $specification['width'] ?? -1;
135
-			$height = $specification['height'] ?? -1;
136
-			$crop = $specification['crop'] ?? false;
137
-			$mode = $specification['mode'] ?? IPreview::MODE_FILL;
138
-
139
-			// If both width and height are -1 we just want the max preview
140
-			if ($width === -1 && $height === -1) {
141
-				$width = $maxWidth;
142
-				$height = $maxHeight;
143
-			}
144
-
145
-			// Calculate the preview size
146
-			[$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
147
-
148
-			// No need to generate a preview that is just the max preview
149
-			if ($width === $maxWidth && $height === $maxHeight) {
150
-				// ensure correct return value if this was the last one
151
-				$preview = $maxPreview;
152
-				continue;
153
-			}
154
-
155
-			// Try to get a cached preview. Else generate (and store) one
156
-			try {
157
-				try {
158
-					$preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
159
-				} catch (NotFoundException $e) {
160
-					if (!$this->previewManager->isMimeSupported($mimeType)) {
161
-						throw new NotFoundException();
162
-					}
163
-
164
-					if ($maxPreviewImage === null) {
165
-						$maxPreviewImage = $this->helper->getImage($maxPreview);
166
-					}
167
-
168
-					$this->logger->debug('Cached preview not found for file {path}, generating a new preview.', ['path' => $file->getPath()]);
169
-					$preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion, $cacheResult);
170
-					// New file, augment our array
171
-					$previewFiles[] = $preview;
172
-				}
173
-			} catch (\InvalidArgumentException $e) {
174
-				throw new NotFoundException('', 0, $e);
175
-			}
176
-
177
-			if ($preview->getSize() === 0) {
178
-				$preview->delete();
179
-				throw new NotFoundException('Cached preview size 0, invalid!');
180
-			}
181
-		}
182
-		assert($preview !== null);
183
-
184
-		// Free memory being used by the embedded image resource.  Without this the image is kept in memory indefinitely.
185
-		// Garbage Collection does NOT free this memory.  We have to do it ourselves.
186
-		if ($maxPreviewImage instanceof \OCP\Image) {
187
-			$maxPreviewImage->destroy();
188
-		}
189
-
190
-		return $preview;
191
-	}
192
-
193
-	/**
194
-	 * Acquire a semaphore of the specified id and concurrency, blocking if necessary.
195
-	 * Return an identifier of the semaphore on success, which can be used to release it via
196
-	 * {@see Generator::unguardWithSemaphore()}.
197
-	 *
198
-	 * @param int $semId
199
-	 * @param int $concurrency
200
-	 * @return false|\SysvSemaphore the semaphore on success or false on failure
201
-	 */
202
-	public static function guardWithSemaphore(int $semId, int $concurrency) {
203
-		if (!extension_loaded('sysvsem')) {
204
-			return false;
205
-		}
206
-		$sem = sem_get($semId, $concurrency);
207
-		if ($sem === false) {
208
-			return false;
209
-		}
210
-		if (!sem_acquire($sem)) {
211
-			return false;
212
-		}
213
-		return $sem;
214
-	}
215
-
216
-	/**
217
-	 * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}.
218
-	 *
219
-	 * @param false|\SysvSemaphore $semId the semaphore identifier returned by guardWithSemaphore
220
-	 * @return bool
221
-	 */
222
-	public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool {
223
-		if ($semId === false || !($semId instanceof \SysvSemaphore)) {
224
-			return false;
225
-		}
226
-		return sem_release($semId);
227
-	}
228
-
229
-	/**
230
-	 * Get the number of concurrent threads supported by the host.
231
-	 *
232
-	 * @return int number of concurrent threads, or 0 if it cannot be determined
233
-	 */
234
-	public static function getHardwareConcurrency(): int {
235
-		static $width;
236
-
237
-		if (!isset($width)) {
238
-			if (function_exists('ini_get')) {
239
-				$openBasedir = ini_get('open_basedir');
240
-				if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) {
241
-					$width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0;
242
-				} else {
243
-					$width = 0;
244
-				}
245
-			} else {
246
-				$width = 0;
247
-			}
248
-		}
249
-		return $width;
250
-	}
251
-
252
-	/**
253
-	 * Get number of concurrent preview generations from system config
254
-	 *
255
-	 * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`,
256
-	 * are available. If not set, the default values are determined with the hardware concurrency
257
-	 * of the host. In case the hardware concurrency cannot be determined, or the user sets an
258
-	 * invalid value, fallback values are:
259
-	 * For new images whose previews do not exist and need to be generated, 4;
260
-	 * For all preview generation requests, 8.
261
-	 * Value of `preview_concurrency_all` should be greater than or equal to that of
262
-	 * `preview_concurrency_new`, otherwise, the latter is returned.
263
-	 *
264
-	 * @param string $type either `preview_concurrency_new` or `preview_concurrency_all`
265
-	 * @return int number of concurrent preview generations, or -1 if $type is invalid
266
-	 */
267
-	public function getNumConcurrentPreviews(string $type): int {
268
-		static $cached = [];
269
-		if (array_key_exists($type, $cached)) {
270
-			return $cached[$type];
271
-		}
272
-
273
-		$hardwareConcurrency = self::getHardwareConcurrency();
274
-		switch ($type) {
275
-			case 'preview_concurrency_all':
276
-				$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
277
-				$concurrency_all = $this->config->getSystemValueInt($type, $fallback);
278
-				$concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new');
279
-				$cached[$type] = max($concurrency_all, $concurrency_new);
280
-				break;
281
-			case 'preview_concurrency_new':
282
-				$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
283
-				$cached[$type] = $this->config->getSystemValueInt($type, $fallback);
284
-				break;
285
-			default:
286
-				return -1;
287
-		}
288
-		return $cached[$type];
289
-	}
290
-
291
-	/**
292
-	 * @param ISimpleFolder $previewFolder
293
-	 * @param ISimpleFile[] $previewFiles
294
-	 * @param File $file
295
-	 * @param string $mimeType
296
-	 * @param string $prefix
297
-	 * @return ISimpleFile
298
-	 * @throws NotFoundException
299
-	 */
300
-	private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) {
301
-		// We don't know the max preview size, so we can't use getCachedPreview.
302
-		// It might have been generated with a higher resolution than the current value.
303
-		foreach ($previewFiles as $node) {
304
-			$name = $node->getName();
305
-			if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) {
306
-				return $node;
307
-			}
308
-		}
309
-
310
-		$maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
311
-		$maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
312
-
313
-		return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix);
314
-	}
315
-
316
-	private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) {
317
-		$previewProviders = $this->previewManager->getProviders();
318
-		foreach ($previewProviders as $supportedMimeType => $providers) {
319
-			// Filter out providers that does not support this mime
320
-			if (!preg_match($supportedMimeType, $mimeType)) {
321
-				continue;
322
-			}
323
-
324
-			foreach ($providers as $providerClosure) {
325
-				$provider = $this->helper->getProvider($providerClosure);
326
-				if (!($provider instanceof IProviderV2)) {
327
-					continue;
328
-				}
329
-
330
-				if (!$provider->isAvailable($file)) {
331
-					continue;
332
-				}
333
-
334
-				$previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
335
-				$sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
336
-				try {
337
-					$this->logger->debug('Calling preview provider for {mimeType} with width={width}, height={height}', [
338
-						'mimeType' => $mimeType,
339
-						'width' => $width,
340
-						'height' => $height,
341
-					]);
342
-					$preview = $this->helper->getThumbnail($provider, $file, $width, $height);
343
-				} finally {
344
-					self::unguardWithSemaphore($sem);
345
-				}
346
-
347
-				if (!($preview instanceof IImage)) {
348
-					continue;
349
-				}
350
-
351
-				$path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix);
352
-				try {
353
-					if ($preview instanceof IStreamImage) {
354
-						return $previewFolder->newFile($path, $preview->resource());
355
-					} else {
356
-						return $previewFolder->newFile($path, $preview->data());
357
-					}
358
-				} catch (NotPermittedException $e) {
359
-					throw new NotFoundException();
360
-				}
361
-
362
-				return $file;
363
-			}
364
-		}
365
-
366
-		throw new NotFoundException('No provider successfully handled the preview generation');
367
-	}
368
-
369
-	/**
370
-	 * @param ISimpleFile $file
371
-	 * @param string $prefix
372
-	 * @return int[]
373
-	 */
374
-	private function getPreviewSize(ISimpleFile $file, string $prefix = '') {
375
-		$size = explode('-', substr($file->getName(), strlen($prefix)));
376
-		return [(int)$size[0], (int)$size[1]];
377
-	}
378
-
379
-	/**
380
-	 * @param int $width
381
-	 * @param int $height
382
-	 * @param bool $crop
383
-	 * @param bool $max
384
-	 * @param string $mimeType
385
-	 * @param string $prefix
386
-	 * @return string
387
-	 */
388
-	private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) {
389
-		$path = $prefix . (string)$width . '-' . (string)$height;
390
-		if ($crop) {
391
-			$path .= '-crop';
392
-		}
393
-		if ($max) {
394
-			$path .= '-max';
395
-		}
396
-
397
-		$ext = $this->getExtension($mimeType);
398
-		$path .= '.' . $ext;
399
-		return $path;
400
-	}
401
-
402
-
403
-	/**
404
-	 * @param int $width
405
-	 * @param int $height
406
-	 * @param bool $crop
407
-	 * @param string $mode
408
-	 * @param int $maxWidth
409
-	 * @param int $maxHeight
410
-	 * @return int[]
411
-	 */
412
-	private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
413
-		/*
28
+    public const SEMAPHORE_ID_ALL = 0x0a11;
29
+    public const SEMAPHORE_ID_NEW = 0x07ea;
30
+
31
+    public function __construct(
32
+        private IConfig $config,
33
+        private IPreview $previewManager,
34
+        private IAppData $appData,
35
+        private GeneratorHelper $helper,
36
+        private IEventDispatcher $eventDispatcher,
37
+        private LoggerInterface $logger,
38
+    ) {
39
+    }
40
+
41
+    /**
42
+     * Returns a preview of a file
43
+     *
44
+     * The cache is searched first and if nothing usable was found then a preview is
45
+     * generated by one of the providers
46
+     *
47
+     * @return ISimpleFile
48
+     * @throws NotFoundException
49
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
50
+     */
51
+    public function getPreview(
52
+        File $file,
53
+        int $width = -1,
54
+        int $height = -1,
55
+        bool $crop = false,
56
+        string $mode = IPreview::MODE_FILL,
57
+        ?string $mimeType = null,
58
+        bool $cacheResult = true,
59
+    ): ISimpleFile {
60
+        $specification = [
61
+            'width' => $width,
62
+            'height' => $height,
63
+            'crop' => $crop,
64
+            'mode' => $mode,
65
+        ];
66
+
67
+        $this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent(
68
+            $file,
69
+            $width,
70
+            $height,
71
+            $crop,
72
+            $mode,
73
+            $mimeType,
74
+        ));
75
+
76
+        $this->logger->debug('Requesting preview for {path} with width={width}, height={height}, crop={crop}, mode={mode}, mimeType={mimeType}', [
77
+            'path' => $file->getPath(),
78
+            'width' => $width,
79
+            'height' => $height,
80
+            'crop' => $crop,
81
+            'mode' => $mode,
82
+            'mimeType' => $mimeType,
83
+        ]);
84
+
85
+
86
+        // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
87
+        return $this->generatePreviews($file, [$specification], $mimeType, $cacheResult);
88
+    }
89
+
90
+    /**
91
+     * Generates previews of a file
92
+     *
93
+     * @throws NotFoundException
94
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
95
+     */
96
+    public function generatePreviews(File $file, array $specifications, ?string $mimeType = null, bool $cacheResult = true): ISimpleFile {
97
+        //Make sure that we can read the file
98
+        if (!$file->isReadable()) {
99
+            $this->logger->warning('Cannot read file: {path}, skipping preview generation.', ['path' => $file->getPath()]);
100
+            throw new NotFoundException('Cannot read file');
101
+        }
102
+
103
+        if ($mimeType === null) {
104
+            $mimeType = $file->getMimeType();
105
+        }
106
+
107
+        $previewFolder = $this->getPreviewFolder($file);
108
+        // List every existing preview first instead of trying to find them one by one
109
+        $previewFiles = $previewFolder->getDirectoryListing();
110
+
111
+        $previewVersion = '';
112
+        if ($file instanceof IVersionedPreviewFile) {
113
+            $previewVersion = $file->getPreviewVersion() . '-';
114
+        }
115
+
116
+        // Get the max preview and infer the max preview sizes from that
117
+        $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion);
118
+        $maxPreviewImage = null; // only load the image when we need it
119
+        if ($maxPreview->getSize() === 0) {
120
+            $maxPreview->delete();
121
+            $this->logger->error('Max preview generated for file {path} has size 0, deleting and throwing exception.', ['path' => $file->getPath()]);
122
+            throw new NotFoundException('Max preview size 0, invalid!');
123
+        }
124
+
125
+        [$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
126
+
127
+        if ($maxWidth <= 0 || $maxHeight <= 0) {
128
+            throw new NotFoundException('The maximum preview sizes are zero or less pixels');
129
+        }
130
+
131
+        $preview = null;
132
+
133
+        foreach ($specifications as $specification) {
134
+            $width = $specification['width'] ?? -1;
135
+            $height = $specification['height'] ?? -1;
136
+            $crop = $specification['crop'] ?? false;
137
+            $mode = $specification['mode'] ?? IPreview::MODE_FILL;
138
+
139
+            // If both width and height are -1 we just want the max preview
140
+            if ($width === -1 && $height === -1) {
141
+                $width = $maxWidth;
142
+                $height = $maxHeight;
143
+            }
144
+
145
+            // Calculate the preview size
146
+            [$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
147
+
148
+            // No need to generate a preview that is just the max preview
149
+            if ($width === $maxWidth && $height === $maxHeight) {
150
+                // ensure correct return value if this was the last one
151
+                $preview = $maxPreview;
152
+                continue;
153
+            }
154
+
155
+            // Try to get a cached preview. Else generate (and store) one
156
+            try {
157
+                try {
158
+                    $preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
159
+                } catch (NotFoundException $e) {
160
+                    if (!$this->previewManager->isMimeSupported($mimeType)) {
161
+                        throw new NotFoundException();
162
+                    }
163
+
164
+                    if ($maxPreviewImage === null) {
165
+                        $maxPreviewImage = $this->helper->getImage($maxPreview);
166
+                    }
167
+
168
+                    $this->logger->debug('Cached preview not found for file {path}, generating a new preview.', ['path' => $file->getPath()]);
169
+                    $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion, $cacheResult);
170
+                    // New file, augment our array
171
+                    $previewFiles[] = $preview;
172
+                }
173
+            } catch (\InvalidArgumentException $e) {
174
+                throw new NotFoundException('', 0, $e);
175
+            }
176
+
177
+            if ($preview->getSize() === 0) {
178
+                $preview->delete();
179
+                throw new NotFoundException('Cached preview size 0, invalid!');
180
+            }
181
+        }
182
+        assert($preview !== null);
183
+
184
+        // Free memory being used by the embedded image resource.  Without this the image is kept in memory indefinitely.
185
+        // Garbage Collection does NOT free this memory.  We have to do it ourselves.
186
+        if ($maxPreviewImage instanceof \OCP\Image) {
187
+            $maxPreviewImage->destroy();
188
+        }
189
+
190
+        return $preview;
191
+    }
192
+
193
+    /**
194
+     * Acquire a semaphore of the specified id and concurrency, blocking if necessary.
195
+     * Return an identifier of the semaphore on success, which can be used to release it via
196
+     * {@see Generator::unguardWithSemaphore()}.
197
+     *
198
+     * @param int $semId
199
+     * @param int $concurrency
200
+     * @return false|\SysvSemaphore the semaphore on success or false on failure
201
+     */
202
+    public static function guardWithSemaphore(int $semId, int $concurrency) {
203
+        if (!extension_loaded('sysvsem')) {
204
+            return false;
205
+        }
206
+        $sem = sem_get($semId, $concurrency);
207
+        if ($sem === false) {
208
+            return false;
209
+        }
210
+        if (!sem_acquire($sem)) {
211
+            return false;
212
+        }
213
+        return $sem;
214
+    }
215
+
216
+    /**
217
+     * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}.
218
+     *
219
+     * @param false|\SysvSemaphore $semId the semaphore identifier returned by guardWithSemaphore
220
+     * @return bool
221
+     */
222
+    public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool {
223
+        if ($semId === false || !($semId instanceof \SysvSemaphore)) {
224
+            return false;
225
+        }
226
+        return sem_release($semId);
227
+    }
228
+
229
+    /**
230
+     * Get the number of concurrent threads supported by the host.
231
+     *
232
+     * @return int number of concurrent threads, or 0 if it cannot be determined
233
+     */
234
+    public static function getHardwareConcurrency(): int {
235
+        static $width;
236
+
237
+        if (!isset($width)) {
238
+            if (function_exists('ini_get')) {
239
+                $openBasedir = ini_get('open_basedir');
240
+                if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) {
241
+                    $width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0;
242
+                } else {
243
+                    $width = 0;
244
+                }
245
+            } else {
246
+                $width = 0;
247
+            }
248
+        }
249
+        return $width;
250
+    }
251
+
252
+    /**
253
+     * Get number of concurrent preview generations from system config
254
+     *
255
+     * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`,
256
+     * are available. If not set, the default values are determined with the hardware concurrency
257
+     * of the host. In case the hardware concurrency cannot be determined, or the user sets an
258
+     * invalid value, fallback values are:
259
+     * For new images whose previews do not exist and need to be generated, 4;
260
+     * For all preview generation requests, 8.
261
+     * Value of `preview_concurrency_all` should be greater than or equal to that of
262
+     * `preview_concurrency_new`, otherwise, the latter is returned.
263
+     *
264
+     * @param string $type either `preview_concurrency_new` or `preview_concurrency_all`
265
+     * @return int number of concurrent preview generations, or -1 if $type is invalid
266
+     */
267
+    public function getNumConcurrentPreviews(string $type): int {
268
+        static $cached = [];
269
+        if (array_key_exists($type, $cached)) {
270
+            return $cached[$type];
271
+        }
272
+
273
+        $hardwareConcurrency = self::getHardwareConcurrency();
274
+        switch ($type) {
275
+            case 'preview_concurrency_all':
276
+                $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
277
+                $concurrency_all = $this->config->getSystemValueInt($type, $fallback);
278
+                $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new');
279
+                $cached[$type] = max($concurrency_all, $concurrency_new);
280
+                break;
281
+            case 'preview_concurrency_new':
282
+                $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
283
+                $cached[$type] = $this->config->getSystemValueInt($type, $fallback);
284
+                break;
285
+            default:
286
+                return -1;
287
+        }
288
+        return $cached[$type];
289
+    }
290
+
291
+    /**
292
+     * @param ISimpleFolder $previewFolder
293
+     * @param ISimpleFile[] $previewFiles
294
+     * @param File $file
295
+     * @param string $mimeType
296
+     * @param string $prefix
297
+     * @return ISimpleFile
298
+     * @throws NotFoundException
299
+     */
300
+    private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) {
301
+        // We don't know the max preview size, so we can't use getCachedPreview.
302
+        // It might have been generated with a higher resolution than the current value.
303
+        foreach ($previewFiles as $node) {
304
+            $name = $node->getName();
305
+            if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) {
306
+                return $node;
307
+            }
308
+        }
309
+
310
+        $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
311
+        $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
312
+
313
+        return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix);
314
+    }
315
+
316
+    private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) {
317
+        $previewProviders = $this->previewManager->getProviders();
318
+        foreach ($previewProviders as $supportedMimeType => $providers) {
319
+            // Filter out providers that does not support this mime
320
+            if (!preg_match($supportedMimeType, $mimeType)) {
321
+                continue;
322
+            }
323
+
324
+            foreach ($providers as $providerClosure) {
325
+                $provider = $this->helper->getProvider($providerClosure);
326
+                if (!($provider instanceof IProviderV2)) {
327
+                    continue;
328
+                }
329
+
330
+                if (!$provider->isAvailable($file)) {
331
+                    continue;
332
+                }
333
+
334
+                $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
335
+                $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
336
+                try {
337
+                    $this->logger->debug('Calling preview provider for {mimeType} with width={width}, height={height}', [
338
+                        'mimeType' => $mimeType,
339
+                        'width' => $width,
340
+                        'height' => $height,
341
+                    ]);
342
+                    $preview = $this->helper->getThumbnail($provider, $file, $width, $height);
343
+                } finally {
344
+                    self::unguardWithSemaphore($sem);
345
+                }
346
+
347
+                if (!($preview instanceof IImage)) {
348
+                    continue;
349
+                }
350
+
351
+                $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix);
352
+                try {
353
+                    if ($preview instanceof IStreamImage) {
354
+                        return $previewFolder->newFile($path, $preview->resource());
355
+                    } else {
356
+                        return $previewFolder->newFile($path, $preview->data());
357
+                    }
358
+                } catch (NotPermittedException $e) {
359
+                    throw new NotFoundException();
360
+                }
361
+
362
+                return $file;
363
+            }
364
+        }
365
+
366
+        throw new NotFoundException('No provider successfully handled the preview generation');
367
+    }
368
+
369
+    /**
370
+     * @param ISimpleFile $file
371
+     * @param string $prefix
372
+     * @return int[]
373
+     */
374
+    private function getPreviewSize(ISimpleFile $file, string $prefix = '') {
375
+        $size = explode('-', substr($file->getName(), strlen($prefix)));
376
+        return [(int)$size[0], (int)$size[1]];
377
+    }
378
+
379
+    /**
380
+     * @param int $width
381
+     * @param int $height
382
+     * @param bool $crop
383
+     * @param bool $max
384
+     * @param string $mimeType
385
+     * @param string $prefix
386
+     * @return string
387
+     */
388
+    private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) {
389
+        $path = $prefix . (string)$width . '-' . (string)$height;
390
+        if ($crop) {
391
+            $path .= '-crop';
392
+        }
393
+        if ($max) {
394
+            $path .= '-max';
395
+        }
396
+
397
+        $ext = $this->getExtension($mimeType);
398
+        $path .= '.' . $ext;
399
+        return $path;
400
+    }
401
+
402
+
403
+    /**
404
+     * @param int $width
405
+     * @param int $height
406
+     * @param bool $crop
407
+     * @param string $mode
408
+     * @param int $maxWidth
409
+     * @param int $maxHeight
410
+     * @return int[]
411
+     */
412
+    private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
413
+        /*
414 414
 		 * If we are not cropping we have to make sure the requested image
415 415
 		 * respects the aspect ratio of the original.
416 416
 		 */
417
-		if (!$crop) {
418
-			$ratio = $maxHeight / $maxWidth;
417
+        if (!$crop) {
418
+            $ratio = $maxHeight / $maxWidth;
419 419
 
420
-			if ($width === -1) {
421
-				$width = $height / $ratio;
422
-			}
423
-			if ($height === -1) {
424
-				$height = $width * $ratio;
425
-			}
420
+            if ($width === -1) {
421
+                $width = $height / $ratio;
422
+            }
423
+            if ($height === -1) {
424
+                $height = $width * $ratio;
425
+            }
426 426
 
427
-			$ratioH = $height / $maxHeight;
428
-			$ratioW = $width / $maxWidth;
427
+            $ratioH = $height / $maxHeight;
428
+            $ratioW = $width / $maxWidth;
429 429
 
430
-			/*
430
+            /*
431 431
 			 * Fill means that the $height and $width are the max
432 432
 			 * Cover means min.
433 433
 			 */
434
-			if ($mode === IPreview::MODE_FILL) {
435
-				if ($ratioH > $ratioW) {
436
-					$height = $width * $ratio;
437
-				} else {
438
-					$width = $height / $ratio;
439
-				}
440
-			} elseif ($mode === IPreview::MODE_COVER) {
441
-				if ($ratioH > $ratioW) {
442
-					$width = $height / $ratio;
443
-				} else {
444
-					$height = $width * $ratio;
445
-				}
446
-			}
447
-		}
448
-
449
-		if ($height !== $maxHeight && $width !== $maxWidth) {
450
-			/*
434
+            if ($mode === IPreview::MODE_FILL) {
435
+                if ($ratioH > $ratioW) {
436
+                    $height = $width * $ratio;
437
+                } else {
438
+                    $width = $height / $ratio;
439
+                }
440
+            } elseif ($mode === IPreview::MODE_COVER) {
441
+                if ($ratioH > $ratioW) {
442
+                    $width = $height / $ratio;
443
+                } else {
444
+                    $height = $width * $ratio;
445
+                }
446
+            }
447
+        }
448
+
449
+        if ($height !== $maxHeight && $width !== $maxWidth) {
450
+            /*
451 451
 			 * Scale to the nearest power of four
452 452
 			 */
453
-			$pow4height = 4 ** ceil(log($height) / log(4));
454
-			$pow4width = 4 ** ceil(log($width) / log(4));
455
-
456
-			// Minimum size is 64
457
-			$pow4height = max($pow4height, 64);
458
-			$pow4width = max($pow4width, 64);
459
-
460
-			$ratioH = $height / $pow4height;
461
-			$ratioW = $width / $pow4width;
462
-
463
-			if ($ratioH < $ratioW) {
464
-				$width = $pow4width;
465
-				$height /= $ratioW;
466
-			} else {
467
-				$height = $pow4height;
468
-				$width /= $ratioH;
469
-			}
470
-		}
471
-
472
-		/*
453
+            $pow4height = 4 ** ceil(log($height) / log(4));
454
+            $pow4width = 4 ** ceil(log($width) / log(4));
455
+
456
+            // Minimum size is 64
457
+            $pow4height = max($pow4height, 64);
458
+            $pow4width = max($pow4width, 64);
459
+
460
+            $ratioH = $height / $pow4height;
461
+            $ratioW = $width / $pow4width;
462
+
463
+            if ($ratioH < $ratioW) {
464
+                $width = $pow4width;
465
+                $height /= $ratioW;
466
+            } else {
467
+                $height = $pow4height;
468
+                $width /= $ratioH;
469
+            }
470
+        }
471
+
472
+        /*
473 473
 		 * Make sure the requested height and width fall within the max
474 474
 		 * of the preview.
475 475
 		 */
476
-		if ($height > $maxHeight) {
477
-			$ratio = $height / $maxHeight;
478
-			$height = $maxHeight;
479
-			$width /= $ratio;
480
-		}
481
-		if ($width > $maxWidth) {
482
-			$ratio = $width / $maxWidth;
483
-			$width = $maxWidth;
484
-			$height /= $ratio;
485
-		}
486
-
487
-		return [(int)round($width), (int)round($height)];
488
-	}
489
-
490
-	/**
491
-	 * @throws NotFoundException
492
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
493
-	 */
494
-	private function generatePreview(
495
-		ISimpleFolder $previewFolder,
496
-		IImage $maxPreview,
497
-		int $width,
498
-		int $height,
499
-		bool $crop,
500
-		int $maxWidth,
501
-		int $maxHeight,
502
-		string $prefix,
503
-		bool $cacheResult,
504
-	): ISimpleFile {
505
-		$preview = $maxPreview;
506
-		if (!$preview->valid()) {
507
-			throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
508
-		}
509
-
510
-		$previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
511
-		$sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
512
-		try {
513
-			if ($crop) {
514
-				if ($height !== $preview->height() && $width !== $preview->width()) {
515
-					//Resize
516
-					$widthR = $preview->width() / $width;
517
-					$heightR = $preview->height() / $height;
518
-
519
-					if ($widthR > $heightR) {
520
-						$scaleH = $height;
521
-						$scaleW = $maxWidth / $heightR;
522
-					} else {
523
-						$scaleH = $maxHeight / $widthR;
524
-						$scaleW = $width;
525
-					}
526
-					$preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
527
-				}
528
-				$cropX = (int)floor(abs($width - $preview->width()) * 0.5);
529
-				$cropY = (int)floor(abs($height - $preview->height()) * 0.5);
530
-				$preview = $preview->cropCopy($cropX, $cropY, $width, $height);
531
-			} else {
532
-				$preview = $maxPreview->resizeCopy(max($width, $height));
533
-			}
534
-		} finally {
535
-			self::unguardWithSemaphore($sem);
536
-		}
537
-
538
-
539
-		$path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix);
540
-		try {
541
-			if ($cacheResult) {
542
-				return $previewFolder->newFile($path, $preview->data());
543
-			} else {
544
-				return new InMemoryFile($path, $preview->data());
545
-			}
546
-		} catch (NotPermittedException $e) {
547
-			throw new NotFoundException();
548
-		}
549
-		return $file;
550
-	}
551
-
552
-	/**
553
-	 * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing()
554
-	 * @param int $width
555
-	 * @param int $height
556
-	 * @param bool $crop
557
-	 * @param string $mimeType
558
-	 * @param string $prefix
559
-	 * @return ISimpleFile
560
-	 *
561
-	 * @throws NotFoundException
562
-	 */
563
-	private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) {
564
-		$path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix);
565
-		foreach ($files as $file) {
566
-			if ($file->getName() === $path) {
567
-				$this->logger->debug('Found cached preview: {path}', ['path' => $path]);
568
-				return $file;
569
-			}
570
-		}
571
-		throw new NotFoundException();
572
-	}
573
-
574
-	/**
575
-	 * Get the specific preview folder for this file
576
-	 *
577
-	 * @param File $file
578
-	 * @return ISimpleFolder
579
-	 *
580
-	 * @throws InvalidPathException
581
-	 * @throws NotFoundException
582
-	 * @throws NotPermittedException
583
-	 */
584
-	private function getPreviewFolder(File $file) {
585
-		// Obtain file id outside of try catch block to prevent the creation of an existing folder
586
-		$fileId = (string)$file->getId();
587
-
588
-		try {
589
-			$folder = $this->appData->getFolder($fileId);
590
-		} catch (NotFoundException $e) {
591
-			$folder = $this->appData->newFolder($fileId);
592
-		}
593
-
594
-		return $folder;
595
-	}
596
-
597
-	/**
598
-	 * @param string $mimeType
599
-	 * @return null|string
600
-	 * @throws \InvalidArgumentException
601
-	 */
602
-	private function getExtension($mimeType) {
603
-		switch ($mimeType) {
604
-			case 'image/png':
605
-				return 'png';
606
-			case 'image/jpeg':
607
-				return 'jpg';
608
-			case 'image/webp':
609
-				return 'webp';
610
-			case 'image/gif':
611
-				return 'gif';
612
-			default:
613
-				throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
614
-		}
615
-	}
476
+        if ($height > $maxHeight) {
477
+            $ratio = $height / $maxHeight;
478
+            $height = $maxHeight;
479
+            $width /= $ratio;
480
+        }
481
+        if ($width > $maxWidth) {
482
+            $ratio = $width / $maxWidth;
483
+            $width = $maxWidth;
484
+            $height /= $ratio;
485
+        }
486
+
487
+        return [(int)round($width), (int)round($height)];
488
+    }
489
+
490
+    /**
491
+     * @throws NotFoundException
492
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
493
+     */
494
+    private function generatePreview(
495
+        ISimpleFolder $previewFolder,
496
+        IImage $maxPreview,
497
+        int $width,
498
+        int $height,
499
+        bool $crop,
500
+        int $maxWidth,
501
+        int $maxHeight,
502
+        string $prefix,
503
+        bool $cacheResult,
504
+    ): ISimpleFile {
505
+        $preview = $maxPreview;
506
+        if (!$preview->valid()) {
507
+            throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
508
+        }
509
+
510
+        $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
511
+        $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
512
+        try {
513
+            if ($crop) {
514
+                if ($height !== $preview->height() && $width !== $preview->width()) {
515
+                    //Resize
516
+                    $widthR = $preview->width() / $width;
517
+                    $heightR = $preview->height() / $height;
518
+
519
+                    if ($widthR > $heightR) {
520
+                        $scaleH = $height;
521
+                        $scaleW = $maxWidth / $heightR;
522
+                    } else {
523
+                        $scaleH = $maxHeight / $widthR;
524
+                        $scaleW = $width;
525
+                    }
526
+                    $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
527
+                }
528
+                $cropX = (int)floor(abs($width - $preview->width()) * 0.5);
529
+                $cropY = (int)floor(abs($height - $preview->height()) * 0.5);
530
+                $preview = $preview->cropCopy($cropX, $cropY, $width, $height);
531
+            } else {
532
+                $preview = $maxPreview->resizeCopy(max($width, $height));
533
+            }
534
+        } finally {
535
+            self::unguardWithSemaphore($sem);
536
+        }
537
+
538
+
539
+        $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix);
540
+        try {
541
+            if ($cacheResult) {
542
+                return $previewFolder->newFile($path, $preview->data());
543
+            } else {
544
+                return new InMemoryFile($path, $preview->data());
545
+            }
546
+        } catch (NotPermittedException $e) {
547
+            throw new NotFoundException();
548
+        }
549
+        return $file;
550
+    }
551
+
552
+    /**
553
+     * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing()
554
+     * @param int $width
555
+     * @param int $height
556
+     * @param bool $crop
557
+     * @param string $mimeType
558
+     * @param string $prefix
559
+     * @return ISimpleFile
560
+     *
561
+     * @throws NotFoundException
562
+     */
563
+    private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) {
564
+        $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix);
565
+        foreach ($files as $file) {
566
+            if ($file->getName() === $path) {
567
+                $this->logger->debug('Found cached preview: {path}', ['path' => $path]);
568
+                return $file;
569
+            }
570
+        }
571
+        throw new NotFoundException();
572
+    }
573
+
574
+    /**
575
+     * Get the specific preview folder for this file
576
+     *
577
+     * @param File $file
578
+     * @return ISimpleFolder
579
+     *
580
+     * @throws InvalidPathException
581
+     * @throws NotFoundException
582
+     * @throws NotPermittedException
583
+     */
584
+    private function getPreviewFolder(File $file) {
585
+        // Obtain file id outside of try catch block to prevent the creation of an existing folder
586
+        $fileId = (string)$file->getId();
587
+
588
+        try {
589
+            $folder = $this->appData->getFolder($fileId);
590
+        } catch (NotFoundException $e) {
591
+            $folder = $this->appData->newFolder($fileId);
592
+        }
593
+
594
+        return $folder;
595
+    }
596
+
597
+    /**
598
+     * @param string $mimeType
599
+     * @return null|string
600
+     * @throws \InvalidArgumentException
601
+     */
602
+    private function getExtension($mimeType) {
603
+        switch ($mimeType) {
604
+            case 'image/png':
605
+                return 'png';
606
+            case 'image/jpeg':
607
+                return 'jpg';
608
+            case 'image/webp':
609
+                return 'webp';
610
+            case 'image/gif':
611
+                return 'gif';
612
+            default:
613
+                throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
614
+        }
615
+    }
616 616
 }
Please login to merge, or discard this patch.