Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ Deleting or not setting a config will use a built-in default list of values for
#### `occ config:app:set --value="64 256" previewgenerator squareSizes`
Cropped square previews which are mostly used in the list and tile views of the files app.

#### `occ config:app:set --value="256 4096" previewgenerator squareUncroppedSizes`
Will retain the aspect ratio and try to maximize **either** width **or** height.
#### `occ config:app:set --value="256 4096" previewgenerator fillWidthHeightSizes`
Will retain the aspect ratio and try to use the given size for the longer edge.

#### `occ config:app:set --value="256 4096" previewgenerator coverWidthHeightSizes`
Will retain the aspect ratio and try to use the given size for the shorter edge.

#### `occ config:app:set --value="64 256 1024" previewgenerator widthSizes`
Will retain the aspect ratio and use the specified width. The height will be scaled according to
Expand Down Expand Up @@ -115,7 +118,7 @@ This should include all previews requested by the files, photos and activity app

```
./occ config:app:set --value="64 256" previewgenerator squareSizes
./occ config:app:set --value="256 4096" previewgenerator squareUncroppedSizes
./occ config:app:set --value="256 4096" previewgenerator fillWidthHeightSizes
./occ config:app:set --value="" previewgenerator widthSizes
./occ config:app:set --value="" previewgenerator heightSizes
```
Expand All @@ -124,6 +127,14 @@ This will only generate:
* Cropped square previews of: 64x64 and 256x256
* Aspect ratio previews with a max width **or** max height of: 256 and 4096

### I'm using the Memories app

You can generate an additional preview for the timeline view of the Memories app.

```
./occ config:app:set --value="256" previewgenerator coverWidthHeightSizes
```

### I get "PHP Fatal error: Allowed memory size of X bytes exhausted"
You need to increase the memory allowance of PHP, by default it is 128 MB. You do that by changing the memory_limit in the php.ini file.

Expand Down
58 changes: 43 additions & 15 deletions lib/SizeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use OCA\PreviewGenerator\AppInfo\Application;
use OCP\IConfig;
use OCP\IPreview;

class SizeHelper {
public function __construct(
Expand All @@ -28,7 +29,8 @@ public function generateSpecifications(): array {

$sizes = [
'square' => [],
'squareUncropped' => [],
'fillWidthHeight' => [],
'coverWidthHeight' => [],
'height' => [],
'width' => [],
];
Expand All @@ -39,7 +41,7 @@ public function generateSpecifications(): array {
$s = 64;
while ($s <= $maxW || $s <= $maxH) {
$sizes['square'][] = $s;
$sizes['squareUncropped'][] = $s;
$sizes['fillWidthHeight'][] = $s;
$s *= 4;
}

Expand All @@ -60,8 +62,8 @@ public function generateSpecifications(): array {
* Note that only powers of 4 matter but if users supply different
* stuff it is their own fault and we just ignore it
*/
$getCustomSizes = function (IConfig $config, $key) {
$raw = $config->getAppValue(Application::APP_ID, $key, null);
$getCustomSizes = function ($key) {
$raw = $this->config->getAppValue(Application::APP_ID, $key, null);
if ($raw === null) {
return null;
}
Expand All @@ -81,19 +83,28 @@ public function generateSpecifications(): array {
return $values;
};

$squares = $getCustomSizes($this->config, 'squareSizes');
$squaresUncropped = $getCustomSizes($this->config, 'squareUncroppedSizes');
$widths = $getCustomSizes($this->config, 'widthSizes');
$heights = $getCustomSizes($this->config, 'heightSizes');
$squares = $getCustomSizes('squareSizes');
$fillWidthHeight = $getCustomSizes('fillWidthHeightSizes')
?? $getCustomSizes('squareUncroppedSizes');
$coverWidthHeight = $getCustomSizes('coverWidthHeightSizes');
$widths = $getCustomSizes('widthSizes');
$heights = $getCustomSizes('heightSizes');

if ($squares !== null) {
$sizes['square'] = array_intersect($sizes['square'], $squares);
}

if ($squaresUncropped !== null) {
$sizes['squareUncropped'] = array_intersect(
$sizes['squareUncropped'],
$squaresUncropped,
if ($fillWidthHeight !== null) {
$sizes['fillWidthHeight'] = array_intersect(
$sizes['fillWidthHeight'],
$fillWidthHeight,
);
}

if ($coverWidthHeight !== null) {
$sizes['coverWidthHeight'] = array_filter(
$coverWidthHeight,
static fn ($size) => $size <= $maxW && $size <= $maxH && self::isPowerOfTwo($size),
);
}

Expand All @@ -117,9 +128,22 @@ private function mergeSpecifications(array $sizes): array {
array_map(static function ($squareSize) {
return ['width' => $squareSize, 'height' => $squareSize, 'crop' => true];
}, $sizes['square']),
array_map(static function ($squareSize) {
return ['width' => $squareSize, 'height' => $squareSize, 'crop' => false];
}, $sizes['squareUncropped']),
array_map(static function ($size) {
return [
'width' => $size,
'height' => $size,
'crop' => false,
'mode' => IPreview::MODE_COVER,
];
}, $sizes['coverWidthHeight']),
array_map(static function ($size) {
return [
'width' => $size,
'height' => $size,
'crop' => false,
'mode' => IPreview::MODE_FILL,
];
}, $sizes['fillWidthHeight']),
array_map(static function ($heightSize) {
return ['width' => -1, 'height' => $heightSize, 'crop' => false];
}, $sizes['height']),
Expand All @@ -128,4 +152,8 @@ private function mergeSpecifications(array $sizes): array {
}, $sizes['width'])
);
}

private static function isPowerOfTwo(int $n): bool {
return ($n & ($n - 1)) === 0;
}
}
114 changes: 114 additions & 0 deletions tests/SizeHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\PreviewGenerator\Tests;

use OCA\PreviewGenerator\SizeHelper;
use OCP\IConfig;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class SizeHelperTest extends TestCase {
private SizeHelper $sizeHelper;

private IConfig|MockObject $config;

public function setUp(): void {
parent::setUp();

$this->config = $this->createMock(IConfig::class);

$this->sizeHelper = new SizeHelper($this->config);
}

private function mockMaxDimensions(int $maxW = 4096, int $maxH = 4096): void {
$this->config->method('getSystemValue')
->willReturnMap([
['preview_max_x', 4096, $maxW],
['preview_max_y', 4096, $maxH],
]);
}

public static function provideGenerateSpecificationsData(): array {
return [
// Fallback and precedence for squareUncroppedSizes
[
[
'squareSizes' => '',
'squareUncroppedSizes' => '256',
'fillWidthHeightSizes' => null,
'coverWidthHeightSizes' => '',
'widthSizes' => '',
'heightSizes' => '',
],
[['width' => 256, 'height' => 256, 'crop' => false, 'mode' => 'fill']],
],
[
[
'squareSizes' => '',
'squareUncroppedSizes' => '256',
'fillWidthHeightSizes' => '',
'coverWidthHeightSizes' => '',
'widthSizes' => '',
'heightSizes' => '',
],
[],
],
[
[
'squareSizes' => '',
'squareUncroppedSizes' => '',
'fillWidthHeightSizes' => '256',
'coverWidthHeightSizes' => '',
'widthSizes' => '',
'heightSizes' => '',
],
[['width' => 256, 'height' => 256, 'crop' => false, 'mode' => 'fill']],
],
[
[
'squareSizes' => '',
'squareUncroppedSizes' => null,
'fillWidthHeightSizes' => '256',
'coverWidthHeightSizes' => '',
'widthSizes' => '',
'heightSizes' => '',
],
[['width' => 256, 'height' => 256, 'crop' => false, 'mode' => 'fill']],
],
// No default value for coverWidthHeightSizes
[
[
'squareSizes' => '',
'squareUncroppedSizes' => '',
'fillWidthHeightSizes' => '',
'coverWidthHeightSizes' => null,
'widthSizes' => '',
'heightSizes' => '',
],
[],
],
];
}

/** @dataProvider provideGenerateSpecificationsData */
public function testGenerateSpecifications(array $config, array $expectedSpecs): void {
$this->mockMaxDimensions();

$this->config->method('getAppValue')
->willReturnCallback(function (string $appName, string $key, ?string $default) use ($config) {
$this->assertEquals($appName, 'previewgenerator');
$this->assertNull($default);
return $config[$key] ?? $default;
});

$actualSpecs = $this->sizeHelper->generateSpecifications();
$this->assertEqualsCanonicalizing($expectedSpecs, $actualSpecs);
}
}
Loading