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
1 change: 1 addition & 0 deletions app/Actions/Album/PositionData.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function get(AbstractAlbum $album, bool $include_sub_albums = false): Pos
// variants per photo
$r->whereBetween('type', [SizeVariantType::SMALL2X, SizeVariantType::THUMB]);
},
'palette',
])
->whereNotNull('latitude')
->whereNotNull('longitude');
Expand Down
1 change: 1 addition & 0 deletions app/Actions/Albums/PositionData.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function do(): PositionDataResource
// variants per photo
$r->whereBetween('type', [SizeVariantType::SMALL2X, SizeVariantType::THUMB]);
},
'palette',
])
->whereNotNull('latitude')
->whereNotNull('longitude'),
Expand Down
2 changes: 2 additions & 0 deletions app/Actions/Photo/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ private function handleStandalone(InitDTO $init_dto): Photo
Standalone\EncodePlaceholder::class,
Standalone\ReplaceOriginalWithBackup::class,
Shared\UploadSizeVariantsToS3::class,
Shared\ExtractColourPalette::class,
];

return $this->executePipeOnDTO($pipes, $dto)->getPhoto();
Expand Down Expand Up @@ -270,6 +271,7 @@ private function handlePhotoLivePartner(InitDTO $init_dto): Photo
Standalone\EncodePlaceholder::class,
Standalone\ReplaceOriginalWithBackup::class,
Shared\UploadSizeVariantsToS3::class,
Shared\ExtractColourPalette::class,
];
$stand_alone_dto = $this->executePipeOnDTO($stand_alone_pipes, $stand_alone_dto);

Expand Down
16 changes: 16 additions & 0 deletions app/Actions/Photo/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Exceptions\ModelDBException;
use App\Image\FileDeleter;
use App\Models\Album;
use App\Models\Palette;
use App\Models\Photo;
use App\Models\SizeVariant;
use App\Models\Statistics;
Expand Down Expand Up @@ -320,6 +321,21 @@ private function deleteDBRecords(array $photo_ids, array $album_ids): void
})
->delete();
}
if (count($photo_ids) !== 0) {
Palette::query()
->whereIn('photo_id', $photo_ids)
->delete();
}
if (count($album_ids) !== 0) {
Palette::query()
->whereExists(function (BaseBuilder $query) use ($album_ids): void {
$query
->from('photos', 'p')
->whereColumn('p.id', '=', 'palettes.photo_id')
->whereIn('p.album_id', $album_ids);
})
->delete();
}
if (count($photo_ids) !== 0) {
Photo::query()->whereIn('id', $photo_ids)->delete();
}
Expand Down
41 changes: 41 additions & 0 deletions app/Actions/Photo/Pipes/Shared/ExtractColourPalette.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2025 LycheeOrg.
*/

namespace App\Actions\Photo\Pipes\Shared;

use App\Contracts\PhotoCreate\PhotoDTO;
use App\Contracts\PhotoCreate\PhotoPipe;
use App\Jobs\ExtractColoursJob;
use App\Models\Configs;

/**
* Extract the colour palette from the image.
*/
class ExtractColourPalette implements PhotoPipe
{
public function handle(PhotoDTO $state, \Closure $next): PhotoDTO
{
if (!Configs::getValueAsBool('enable_colour_extractions')) {
return $next($state);
}

// @codeCoverageIgnoreStart
// This is already tested directly in the ExtractColoursJobTest.
if (Configs::getValueAsBool('use_job_queues')) {
ExtractColoursJob::dispatch($state->getPhoto());

return $next($state);
}

$job = new ExtractColoursJob($state->getPhoto());
$job->handle();

return $next($state);
// @codeCoverageIgnoreEnd
}
}
2 changes: 1 addition & 1 deletion app/Actions/Search/PhotoSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function query(array $terms): Collection
public function sqlQuery(array $terms, ?Album $album = null): Builder
{
$query = $this->photoQueryPolicy->applySearchabilityFilter(
query: Photo::query()->with(['album', 'statistics', 'size_variants']),
query: Photo::query()->with(['album', 'statistics', 'size_variants', 'palette']),
origin: $album,
include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_search')
);
Expand Down
71 changes: 71 additions & 0 deletions app/Console/Commands/ImageProcessing/ExtractColourPalette.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2025 LycheeOrg.
*/

namespace App\Console\Commands\ImageProcessing;

use App\Exceptions\UnexpectedException;
use App\Jobs\ExtractColoursJob;
use App\Models\Photo;
use Illuminate\Console\Command;
use Safe\Exceptions\InfoException;
use function Safe\set_time_limit;

class ExtractColourPalette extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lychee:extract_colour_palette {limit=5 : number of photos to extract the colour palette for} {tm=600 : timeout time requirement}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Extract the colour palette if it has not been extracted yet';

/**
* Execute the console command.
*/
public function handle(): int
{
try {
$limit = (int) $this->argument('limit');
$timeout = (int) $this->argument('tm');

try {
set_time_limit($timeout);
} catch (InfoException) {
// Silently do nothing, if `set_time_limit` is denied.
}

$photos = Photo::with(['size_variants'])
->whereDoesntHave('palette')
->where('type', 'like', 'image/%')
->orderBy('id')
->lazyById($limit);

if (count($photos) === 0) {
$this->line('No photos require palette extraction.');

return 0;
}

foreach ($photos as $photo) {
$this->line(sprintf('Extracting Color Palette for %s [%s].', $photo->title, $photo->id));
ExtractColoursJob::dispatchSync($photo);
}

return 0;
} catch (\Throwable $e) {
throw new UnexpectedException($e);
}
}
}
24 changes: 24 additions & 0 deletions app/Contracts/Image/ColourPaletteExtractorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2025 LycheeOrg.
*/

namespace App\Contracts\Image;

use App\Image\Files\FlysystemFile;

/**
* Interface ColourPaletteExtractorInterface.
*/
interface ColourPaletteExtractorInterface
{
/**
* Given a file (hopefully an image), this method extracts the dominant colours as an array of hex strings.
*
* @return string[]
*/
public function extract(FlysystemFile $file): array;
}
2 changes: 1 addition & 1 deletion app/Factories/AlbumFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function findBaseAlbumOrFail(string $album_id, bool $with_relations = tru
$tag_album_query = TagAlbum::query();

if ($with_relations) {
$album_query->with(['access_permissions', 'photos', 'children', 'children.owner', 'photos.size_variants', 'photos.statistics']);
$album_query->with(['access_permissions', 'photos', 'children', 'children.owner', 'photos.size_variants', 'photos.statistics', 'photos.palette']);
$tag_album_query->with(['photos']);
}

Expand Down
62 changes: 62 additions & 0 deletions app/Http/Controllers/Admin/Maintenance/MissingPalettes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2025 LycheeOrg.
*/

namespace App\Http\Controllers\Admin\Maintenance;

use App\Http\Requests\Maintenance\MaintenanceRequest;
use App\Jobs\ExtractColoursJob;
use App\Models\Configs;
use App\Models\Photo;
use Illuminate\Routing\Controller;
use LycheeVerify\Verify;

/**
* Handles missing palettes for photos.
*/
class MissingPalettes extends Controller
{
/**
* Count photos without a palette.
*
* @return int
*/
public function check(MaintenanceRequest $request): int
{
if (!resolve(Verify::class)->check() || !Configs::getValueAsBool('enable_colour_extractions')) {
return 0;
}

return Photo::query()
->where('type', 'like', 'image/%')
->whereDoesntHave('palette')
->count();
}

/**
* Generate missing palettes for photos in chunks.
*
* @return void
*/
public function do(MaintenanceRequest $request): void
{
if (!resolve(Verify::class)->check() || !Configs::getValueAsBool('enable_colour_extractions')) {
return;
}

$limit = Configs::getValueAsInt('maintenance_processing_limit');
$photos = Photo::with(['size_variants'])
->whereDoesntHave('palette')
->where('type', 'like', 'image/%')
->orderBy('id')
->lazyById($limit);

foreach ($photos as $photo) {
ExtractColoursJob::dispatchSync($photo);
}
}
}
4 changes: 2 additions & 2 deletions app/Http/Controllers/Gallery/FrameController.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ private function loadPhoto(AbstractAlbum|null $album, int $retries = 5): ?Photo
// default query
if ($album === null) {
$query = $this->photo_query_policy->applySearchabilityFilter(
query: Photo::query()->with(['album', 'size_variants']),
query: Photo::query()->with(['album', 'size_variants', 'palette']),
origin: null,
include_nsfw: !Configs::getValueAsBool('hide_nsfw_in_frame')
);
} else {
$query = $album->photos()->with(['album', 'size_variants']);
$query = $album->photos()->with(['album', 'size_variants', 'palette']);
}

/** @var ?Photo $photo */
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Middleware/ConfigIntegrity.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class ConfigIntegrity
'live_metrics_enabled',
'live_metrics_access',
'live_metrics_max_time',
'enable_colour_extractions',
'colour_extraction_driver',
];

/**
Expand Down
41 changes: 41 additions & 0 deletions app/Http/Resources/Models/ColourPaletteResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2025 LycheeOrg.
*/

namespace App\Http\Resources\Models;

use App\Models\Palette;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript()]
class ColourPaletteResource extends Data
{
public string $colour_1;
public string $colour_2;
public string $colour_3;
public string $colour_4;
public string $colour_5;

public function __construct(Palette $palette)
{
$this->colour_1 = Palette::toHex($palette->colour_1);
$this->colour_2 = Palette::toHex($palette->colour_2);
$this->colour_3 = Palette::toHex($palette->colour_3);
$this->colour_4 = Palette::toHex($palette->colour_4);
$this->colour_5 = Palette::toHex($palette->colour_5);
}

public static function fromModel(?Palette $p): ?ColourPaletteResource
{
if ($p === null) {
return null;
}

return new self($p);
}
}
2 changes: 2 additions & 0 deletions app/Http/Resources/Models/PhotoResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class PhotoResource extends Data
public PreformattedPhotoData $preformatted;
public PreComputedPhotoData $precomputed;
public ?TimelineData $timeline = null;
public ?ColourPaletteResource $palette = null;

private Carbon $timeline_data_carbon;

Expand Down Expand Up @@ -102,6 +103,7 @@ public function __construct(Photo $photo)
$this->previous_photo_id = null;
$this->preformatted = new PreformattedPhotoData($photo, $this->size_variants->original);
$this->precomputed = new PreComputedPhotoData($photo);
$this->palette = ColourPaletteResource::fromModel($photo->palette);

$this->timeline_data_carbon = $photo->taken_at ?? $photo->created_at;

Expand Down
Loading