event photo wasserzeichen umgesetzt. Event admins können eigene einsetzen (als branding) falls das Paket es erlaubt. der Super Admin kann für die günstigen Pakete eigene Wasserzeichen erzwingen
This commit is contained in:
@@ -258,6 +258,7 @@ class EventResource extends Resource
|
||||
'create' => Pages\CreateEvent::route('/create'),
|
||||
'view' => Pages\ViewEvent::route('/{record}'),
|
||||
'edit' => Pages\EditEvent::route('/{record}/edit'),
|
||||
'watermark' => Pages\ManageWatermark::route('/{record}/watermark'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
141
app/Filament/Resources/EventResource/Pages/ManageWatermark.php
Normal file
141
app/Filament/Resources/EventResource/Pages/ManageWatermark.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EventResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EventResource;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ManageWatermark extends Page
|
||||
{
|
||||
protected static string $resource = EventResource::class;
|
||||
|
||||
protected string $view = 'filament.resources.event-resource.pages.manage-watermark';
|
||||
|
||||
public ?string $watermark_mode = 'base';
|
||||
public ?string $watermark_asset = null;
|
||||
public string $watermark_position = 'bottom-right';
|
||||
public float $watermark_opacity = 0.25;
|
||||
public float $watermark_scale = 0.2;
|
||||
public int $watermark_padding = 16;
|
||||
public bool $serve_originals = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$event = $this->record;
|
||||
$settings = $event->settings ?? [];
|
||||
$watermark = Arr::get($settings, 'watermark', []);
|
||||
|
||||
$this->watermark_mode = $watermark['mode'] ?? 'base';
|
||||
$this->watermark_asset = $watermark['asset'] ?? null;
|
||||
$this->watermark_position = $watermark['position'] ?? 'bottom-right';
|
||||
$this->watermark_opacity = (float) ($watermark['opacity'] ?? 0.25);
|
||||
$this->watermark_scale = (float) ($watermark['scale'] ?? 0.2);
|
||||
$this->watermark_padding = (int) ($watermark['padding'] ?? 16);
|
||||
$this->serve_originals = (bool) Arr::get($settings, 'watermark_serve_originals', false);
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form(
|
||||
$this->makeForm()
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make(__('filament-watermark.heading'))
|
||||
->schema([
|
||||
Forms\Components\Select::make('watermark_mode')
|
||||
->label(__('filament-watermark.mode.label'))
|
||||
->options([
|
||||
'base' => __('filament-watermark.mode.base'),
|
||||
'custom' => __('filament-watermark.mode.custom'),
|
||||
'off' => __('filament-watermark.mode.off'),
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\FileUpload::make('watermark_asset')
|
||||
->label(__('filament-watermark.asset'))
|
||||
->disk('public')
|
||||
->directory('branding')
|
||||
->preserveFilenames()
|
||||
->image()
|
||||
->visible(fn (callable $get) => $get('watermark_mode') === 'custom'),
|
||||
Forms\Components\Select::make('watermark_position')
|
||||
->label(__('filament-watermark.position'))
|
||||
->options([
|
||||
'top-left' => 'Top Left',
|
||||
'top-right' => 'Top Right',
|
||||
'bottom-left' => 'Bottom Left',
|
||||
'bottom-right' => 'Bottom Right',
|
||||
'center' => 'Center',
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('watermark_opacity')
|
||||
->label(__('filament-watermark.opacity'))
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(1)
|
||||
->step(0.05)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('watermark_scale')
|
||||
->label(__('filament-watermark.scale'))
|
||||
->numeric()
|
||||
->minValue(0.05)
|
||||
->maxValue(1)
|
||||
->step(0.05)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('watermark_padding')
|
||||
->label(__('filament-watermark.padding'))
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->required(),
|
||||
Forms\Components\Toggle::make('serve_originals')
|
||||
->label(__('filament-watermark.serve_originals'))
|
||||
->helperText('Nur Admin/Owner: falls aktiviert, werden Originale statt watermarked ausgeliefert.')
|
||||
->default(false),
|
||||
])
|
||||
->columns(2),
|
||||
])
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$event = $this->record;
|
||||
$package = $event->eventPackage?->package;
|
||||
|
||||
$brandingAllowed = $package?->branding_allowed === true;
|
||||
$policy = $package?->watermark_allowed === false ? 'none' : 'basic';
|
||||
|
||||
if (! $brandingAllowed && $this->watermark_mode === 'custom') {
|
||||
Notification::make()->title('Branding nicht erlaubt für dieses Paket')->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($policy === 'basic' && $this->watermark_mode === 'off') {
|
||||
Notification::make()->title('Kein Wasserzeichen ist in diesem Paket nicht erlaubt')->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $event->settings ?? [];
|
||||
$settings['watermark'] = [
|
||||
'mode' => $this->watermark_mode,
|
||||
'asset' => $this->watermark_asset,
|
||||
'position' => $this->watermark_position,
|
||||
'opacity' => (float) $this->watermark_opacity,
|
||||
'scale' => (float) $this->watermark_scale,
|
||||
'padding' => (int) $this->watermark_padding,
|
||||
];
|
||||
$settings['watermark_serve_originals'] = $this->serve_originals;
|
||||
|
||||
$event->forceFill(['settings' => $settings])->save();
|
||||
|
||||
Notification::make()
|
||||
->title(__('filament-watermark.saved'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
103
app/Filament/SuperAdmin/Pages/WatermarkSettingsPage.php
Normal file
103
app/Filament/SuperAdmin/Pages/WatermarkSettingsPage.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\SuperAdmin\Pages;
|
||||
|
||||
use App\Models\WatermarkSetting;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class WatermarkSettingsPage extends Page
|
||||
{
|
||||
protected static null|string|\BackedEnum $navigationIcon = 'heroicon-o-sparkles';
|
||||
|
||||
protected string $view = 'filament.super-admin.pages.watermark-settings-page';
|
||||
|
||||
protected static null|string|\UnitEnum $navigationGroup = 'Branding';
|
||||
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public ?string $asset = null;
|
||||
public string $position = 'bottom-right';
|
||||
public float $opacity = 0.25;
|
||||
public float $scale = 0.2;
|
||||
public int $padding = 16;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$settings = WatermarkSetting::query()->first();
|
||||
|
||||
if ($settings) {
|
||||
$this->asset = $settings->asset;
|
||||
$this->position = $settings->position;
|
||||
$this->opacity = (float) $settings->opacity;
|
||||
$this->scale = (float) $settings->scale;
|
||||
$this->padding = (int) $settings->padding;
|
||||
}
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Forms\Components\FileUpload::make('asset')
|
||||
->label('Basis-Wasserzeichen (PNG/SVG empfohlen)')
|
||||
->disk('public')
|
||||
->directory('branding')
|
||||
->preserveFilenames()
|
||||
->image()
|
||||
->required(),
|
||||
Forms\Components\Select::make('position')
|
||||
->label('Position')
|
||||
->options([
|
||||
'top-left' => 'Oben links',
|
||||
'top-right' => 'Oben rechts',
|
||||
'bottom-left' => 'Unten links',
|
||||
'bottom-right' => 'Unten rechts',
|
||||
'center' => 'Zentriert',
|
||||
])
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('opacity')
|
||||
->label('Opacity (0–1)')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(1)
|
||||
->step(0.05)
|
||||
->default(0.25)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('scale')
|
||||
->label('Skalierung (0–1, relativ zur Bildbreite)')
|
||||
->numeric()
|
||||
->minValue(0.05)
|
||||
->maxValue(1)
|
||||
->step(0.05)
|
||||
->default(0.2)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('padding')
|
||||
->label('Padding (px)')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(16)
|
||||
->required(),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$settings = WatermarkSetting::query()->firstOrNew([]);
|
||||
$settings->asset = $this->asset;
|
||||
$settings->position = $this->position;
|
||||
$settings->opacity = $this->opacity;
|
||||
$settings->scale = $this->scale;
|
||||
$settings->padding = $this->padding;
|
||||
$settings->save();
|
||||
|
||||
Notification::make()
|
||||
->title('Wasserzeichen aktualisiert')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ use App\Services\PushSubscriptionService;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
use App\Support\ApiError;
|
||||
use App\Support\ImageHelper;
|
||||
use App\Support\WatermarkConfigResolver;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -877,14 +878,32 @@ class EventPublicController extends BaseController
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
private function determineBrandingAllowed(Event $event): bool
|
||||
{
|
||||
return WatermarkConfigResolver::determineBrandingAllowed($event);
|
||||
}
|
||||
|
||||
private function determineWatermarkPolicy(Event $event): string
|
||||
{
|
||||
return WatermarkConfigResolver::determinePolicy($event);
|
||||
}
|
||||
|
||||
private function resolveWatermarkConfig(Event $event): array
|
||||
{
|
||||
return WatermarkConfigResolver::resolve($event);
|
||||
}
|
||||
|
||||
private function buildGalleryBranding(Event $event): array
|
||||
{
|
||||
$defaultPrimary = '#f43f5e';
|
||||
$defaultSecondary = '#fb7185';
|
||||
$defaultBackground = '#ffffff';
|
||||
|
||||
$eventBranding = Arr::get($event->settings, 'branding', []);
|
||||
$tenantBranding = Arr::get($event->tenant?->settings, 'branding', []);
|
||||
$event->loadMissing('eventPackage.package', 'tenant');
|
||||
$brandingAllowed = $this->determineBrandingAllowed($event);
|
||||
|
||||
$eventBranding = $brandingAllowed ? Arr::get($event->settings, 'branding', []) : [];
|
||||
$tenantBranding = $brandingAllowed ? Arr::get($event->tenant?->settings, 'branding', []) : [];
|
||||
|
||||
return [
|
||||
'primary_color' => Arr::get($eventBranding, 'primary_color')
|
||||
@@ -1239,6 +1258,7 @@ class EventPublicController extends BaseController
|
||||
$variantPreference = $variant === 'thumbnail'
|
||||
? ['thumbnail', 'original']
|
||||
: ['original'];
|
||||
$preferOriginals = (bool) ($event->settings['watermark_serve_originals'] ?? false);
|
||||
|
||||
return $this->streamGalleryPhoto($event, $photo, $variantPreference, 'inline');
|
||||
}
|
||||
@@ -1360,8 +1380,11 @@ class EventPublicController extends BaseController
|
||||
$branding = $this->buildGalleryBranding($event);
|
||||
$fontFamily = Arr::get($event->settings, 'branding.font_family')
|
||||
?? Arr::get($event->tenant?->settings, 'branding.font_family');
|
||||
$logoUrl = Arr::get($event->settings, 'branding.logo_url')
|
||||
?? Arr::get($event->tenant?->settings, 'branding.logo_url');
|
||||
$brandingAllowed = $this->determineBrandingAllowed($event);
|
||||
$logoUrl = $brandingAllowed
|
||||
? (Arr::get($event->settings, 'branding.logo_url')
|
||||
?? Arr::get($event->tenant?->settings, 'branding.logo_url'))
|
||||
: null;
|
||||
|
||||
if ($joinToken) {
|
||||
$this->joinTokenService->incrementUsage($joinToken);
|
||||
@@ -1430,6 +1453,9 @@ class EventPublicController extends BaseController
|
||||
|
||||
$package = $eventPackage->package;
|
||||
$summary = $this->packageLimitEvaluator->summarizeEventPackage($eventPackage);
|
||||
$watermarkPolicy = $this->determineWatermarkPolicy($event);
|
||||
$brandingAllowed = $this->determineBrandingAllowed($event);
|
||||
$watermark = $this->resolveWatermarkConfig($event);
|
||||
|
||||
return response()->json([
|
||||
'id' => $eventPackage->id,
|
||||
@@ -1441,18 +1467,23 @@ class EventPublicController extends BaseController
|
||||
'max_photos' => $package?->max_photos,
|
||||
'max_guests' => $package?->max_guests,
|
||||
'gallery_days' => $package?->gallery_days,
|
||||
'watermark_policy' => $watermarkPolicy,
|
||||
'branding_allowed' => $brandingAllowed,
|
||||
],
|
||||
'used_photos' => (int) $eventPackage->used_photos,
|
||||
'used_guests' => (int) $eventPackage->used_guests,
|
||||
'expires_at' => $eventPackage->gallery_expires_at?->toIso8601String(),
|
||||
'limits' => $summary,
|
||||
'watermark' => $watermark,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
private function streamGalleryPhoto(Event $event, Photo $record, array $variantPreference, string $disposition)
|
||||
{
|
||||
$preferOriginals = (bool) ($event->settings['watermark_serve_originals'] ?? false);
|
||||
|
||||
foreach ($variantPreference as $variant) {
|
||||
[$disk, $path, $mime] = $this->resolvePhotoVariant($record, $variant);
|
||||
[$disk, $path, $mime] = $this->resolvePhotoVariant($record, $variant, $preferOriginals);
|
||||
|
||||
if (! $path) {
|
||||
continue;
|
||||
@@ -1550,18 +1581,24 @@ class EventPublicController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
private function resolvePhotoVariant(Photo $record, string $variant): array
|
||||
private function resolvePhotoVariant(Photo $record, string $variant, bool $preferOriginals = false): array
|
||||
{
|
||||
if ($variant === 'thumbnail') {
|
||||
$asset = EventMediaAsset::where('photo_id', $record->id)->where('variant', 'thumbnail')->first();
|
||||
$watermarked = $preferOriginals
|
||||
? null
|
||||
: EventMediaAsset::where('photo_id', $record->id)->where('variant', 'watermarked_thumbnail')->first();
|
||||
$disk = $asset?->disk ?? $record->mediaAsset?->disk;
|
||||
$path = $asset?->path ?? ($record->thumbnail_path ?: $record->file_path);
|
||||
$mime = $asset?->mime_type ?? 'image/jpeg';
|
||||
$path = $watermarked?->path ?? $asset?->path ?? ($record->thumbnail_path ?: $record->file_path);
|
||||
$mime = $watermarked?->mime_type ?? $asset?->mime_type ?? 'image/jpeg';
|
||||
} else {
|
||||
$asset = $record->mediaAsset ?? EventMediaAsset::where('photo_id', $record->id)->where('variant', 'original')->first();
|
||||
$watermarked = $preferOriginals
|
||||
? null
|
||||
: EventMediaAsset::where('photo_id', $record->id)->where('variant', 'watermarked')->first();
|
||||
$asset = $record->mediaAsset ?? $watermarked ?? EventMediaAsset::where('photo_id', $record->id)->where('variant', 'original')->first();
|
||||
$disk = $asset?->disk ?? $record->mediaAsset?->disk;
|
||||
$path = $asset?->path ?? ($record->file_path ?? null);
|
||||
$mime = $asset?->mime_type ?? ($record->mime_type ?? 'image/jpeg');
|
||||
$path = $watermarked?->path ?? $asset?->path ?? ($record->file_path ?? null);
|
||||
$mime = $watermarked?->mime_type ?? $asset?->mime_type ?? ($record->mime_type ?? 'image/jpeg');
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -2363,7 +2400,7 @@ class EventPublicController extends BaseController
|
||||
$file = $validated['photo'];
|
||||
$disk = $this->eventStorageManager->getHotDiskForEvent($eventModel);
|
||||
$path = Storage::disk($disk)->putFile("events/{$eventId}/photos", $file);
|
||||
$url = $this->resolveDiskUrl($disk, $path);
|
||||
$watermarkConfig = WatermarkConfigResolver::resolve($eventModel);
|
||||
|
||||
// Generate thumbnail (JPEG) under photos/thumbs
|
||||
$baseName = pathinfo($path, PATHINFO_FILENAME);
|
||||
@@ -2371,7 +2408,27 @@ class EventPublicController extends BaseController
|
||||
$thumbPath = ImageHelper::makeThumbnailOnDisk($disk, $path, $thumbRel, 640, 82);
|
||||
$thumbUrl = $thumbPath
|
||||
? $this->resolveDiskUrl($disk, $thumbPath)
|
||||
: $url;
|
||||
: $this->resolveDiskUrl($disk, $path);
|
||||
|
||||
// Create watermarked copies (non-destructive).
|
||||
$watermarkedPath = $path;
|
||||
$watermarkedThumb = $thumbPath ?: $path;
|
||||
if ($watermarkConfig['type'] !== 'none' && ! empty($watermarkConfig['asset']) && ! ($watermarkConfig['serve_originals'] ?? false)) {
|
||||
$watermarkedPath = ImageHelper::copyWithWatermark($disk, $path, "events/{$eventId}/photos/watermarked/{$baseName}.{$file->getClientOriginalExtension()}", $watermarkConfig) ?? $path;
|
||||
if ($thumbPath) {
|
||||
$watermarkedThumb = ImageHelper::copyWithWatermark(
|
||||
$disk,
|
||||
$thumbPath,
|
||||
"events/{$eventId}/photos/watermarked/{$baseName}_thumb.jpg",
|
||||
$watermarkConfig
|
||||
) ?? $thumbPath;
|
||||
} else {
|
||||
$watermarkedThumb = $watermarkedPath;
|
||||
}
|
||||
}
|
||||
|
||||
$url = $this->resolveDiskUrl($disk, $watermarkedPath);
|
||||
$thumbUrl = $this->resolveDiskUrl($disk, $watermarkedThumb);
|
||||
|
||||
$photoId = DB::table('photos')->insertGetId([
|
||||
'event_id' => $eventId,
|
||||
@@ -2390,16 +2447,35 @@ class EventPublicController extends BaseController
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$storedPath = Storage::disk($disk)->path($path);
|
||||
$storedSize = Storage::disk($disk)->exists($path) ? Storage::disk($disk)->size($path) : $file->getSize();
|
||||
$asset = $this->eventStorageManager->recordAsset($eventModel, $disk, $path, [
|
||||
'variant' => 'original',
|
||||
'mime_type' => $file->getClientMimeType(),
|
||||
'size_bytes' => $file->getSize(),
|
||||
'checksum' => hash_file('sha256', $file->getRealPath()),
|
||||
'size_bytes' => $storedSize,
|
||||
'checksum' => file_exists($storedPath) ? hash_file('sha256', $storedPath) : hash_file('sha256', $file->getRealPath()),
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photoId,
|
||||
]);
|
||||
|
||||
$watermarkedAsset = null;
|
||||
if ($watermarkedPath !== $path) {
|
||||
$watermarkedAsset = $this->eventStorageManager->recordAsset($eventModel, $disk, $watermarkedPath, [
|
||||
'variant' => 'watermarked',
|
||||
'mime_type' => $file->getClientMimeType(),
|
||||
'size_bytes' => Storage::disk($disk)->exists($watermarkedPath)
|
||||
? Storage::disk($disk)->size($watermarkedPath)
|
||||
: null,
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photoId,
|
||||
'meta' => [
|
||||
'source_variant_id' => $asset->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($thumbPath) {
|
||||
$this->eventStorageManager->recordAsset($eventModel, $disk, $thumbPath, [
|
||||
'variant' => 'thumbnail',
|
||||
@@ -2415,6 +2491,21 @@ class EventPublicController extends BaseController
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($watermarkedThumb !== $thumbPath) {
|
||||
$this->eventStorageManager->recordAsset($eventModel, $disk, $watermarkedThumb, [
|
||||
'variant' => 'watermarked_thumbnail',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photoId,
|
||||
'size_bytes' => Storage::disk($disk)->exists($watermarkedThumb)
|
||||
? Storage::disk($disk)->size($watermarkedThumb)
|
||||
: null,
|
||||
'meta' => [
|
||||
'source_variant_id' => $watermarkedAsset?->id ?? $asset->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('photos')
|
||||
->where('id', $photoId)
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Services\Packages\PackageUsageTracker;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
use App\Support\ApiError;
|
||||
use App\Support\ImageHelper;
|
||||
use App\Support\WatermarkConfigResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
@@ -153,13 +154,38 @@ class PhotoController extends Controller
|
||||
$thumbnailPath = $thumbnailRelative;
|
||||
}
|
||||
|
||||
// Apply watermark policy (in-place) to original and thumbnail where applicable.
|
||||
$watermarkConfig = WatermarkConfigResolver::resolve($event);
|
||||
if ($watermarkConfig['type'] !== 'none' && ! empty($watermarkConfig['asset'])) {
|
||||
ImageHelper::applyWatermarkOnDisk($disk, $path, $watermarkConfig);
|
||||
if ($thumbnailRelative) {
|
||||
ImageHelper::applyWatermarkOnDisk($disk, $thumbnailPath, $watermarkConfig);
|
||||
}
|
||||
}
|
||||
|
||||
$watermarkConfig = WatermarkConfigResolver::resolve($event);
|
||||
$watermarkedPath = $path;
|
||||
$watermarkedThumb = $thumbnailPath;
|
||||
|
||||
if ($watermarkConfig['type'] !== 'none' && ! empty($watermarkConfig['asset']) && ! ($watermarkConfig['serve_originals'] ?? false)) {
|
||||
$watermarkedPath = ImageHelper::copyWithWatermark($disk, $path, "events/{$eventSlug}/watermarked/{$filename}", $watermarkConfig) ?? $path;
|
||||
if ($thumbnailRelative) {
|
||||
$watermarkedThumb = ImageHelper::copyWithWatermark(
|
||||
$disk,
|
||||
$thumbnailPath,
|
||||
"events/{$eventSlug}/watermarked/thumbnails/{$filename}",
|
||||
$watermarkConfig
|
||||
) ?? $thumbnailPath;
|
||||
}
|
||||
}
|
||||
|
||||
$photoAttributes = [
|
||||
'event_id' => $event->id,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'file_path' => $path,
|
||||
'thumbnail_path' => $thumbnailPath,
|
||||
'file_path' => $watermarkedPath,
|
||||
'thumbnail_path' => $watermarkedThumb,
|
||||
'width' => null, // Filled below
|
||||
'height' => null,
|
||||
'status' => 'pending', // Requires moderation
|
||||
@@ -179,17 +205,36 @@ class PhotoController extends Controller
|
||||
$photo = Photo::create($photoAttributes);
|
||||
|
||||
// Record primary asset metadata
|
||||
$checksum = hash_file('sha256', $file->getRealPath());
|
||||
$storedPath = Storage::disk($disk)->path($path);
|
||||
$checksum = file_exists($storedPath) ? hash_file('sha256', $storedPath) : hash_file('sha256', $file->getRealPath());
|
||||
$storedSize = Storage::disk($disk)->exists($path) ? Storage::disk($disk)->size($path) : $file->getSize();
|
||||
$asset = $this->eventStorageManager->recordAsset($event, $disk, $path, [
|
||||
'variant' => 'original',
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size_bytes' => $file->getSize(),
|
||||
'size_bytes' => $storedSize,
|
||||
'checksum' => $checksum,
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photo->id,
|
||||
]);
|
||||
|
||||
$watermarkedAsset = null;
|
||||
if ($watermarkedPath !== $path) {
|
||||
$watermarkedAsset = $this->eventStorageManager->recordAsset($event, $disk, $watermarkedPath, [
|
||||
'variant' => 'watermarked',
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size_bytes' => Storage::disk($disk)->exists($watermarkedPath)
|
||||
? Storage::disk($disk)->size($watermarkedPath)
|
||||
: null,
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photo->id,
|
||||
'meta' => [
|
||||
'source_variant_id' => $asset->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($thumbnailRelative) {
|
||||
$this->eventStorageManager->recordAsset($event, $disk, $thumbnailRelative, [
|
||||
'variant' => 'thumbnail',
|
||||
@@ -205,6 +250,21 @@ class PhotoController extends Controller
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($watermarkedThumb !== $thumbnailPath) {
|
||||
$this->eventStorageManager->recordAsset($event, $disk, $watermarkedThumb, [
|
||||
'variant' => 'watermarked_thumbnail',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photo->id,
|
||||
'size_bytes' => Storage::disk($disk)->exists($watermarkedThumb)
|
||||
? Storage::disk($disk)->size($watermarkedThumb)
|
||||
: null,
|
||||
'meta' => [
|
||||
'source_variant_id' => $watermarkedAsset?->id ?? $asset->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$photo->update(['media_asset_id' => $asset->id]);
|
||||
|
||||
|
||||
19
app/Models/WatermarkSetting.php
Normal file
19
app/Models/WatermarkSetting.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WatermarkSetting extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'asset',
|
||||
'position',
|
||||
'opacity',
|
||||
'scale',
|
||||
'padding',
|
||||
];
|
||||
}
|
||||
@@ -89,6 +89,10 @@ class SuperAdminPanelProvider extends PanelProvider
|
||||
LegalPageResource::class,
|
||||
InfrastructureActionLogResource::class,
|
||||
])
|
||||
->pages([
|
||||
Pages\Dashboard::class,
|
||||
\App\Filament\SuperAdmin\Pages\WatermarkSettingsPage::class,
|
||||
])
|
||||
->authGuard('web');
|
||||
|
||||
// SuperAdmin-Zugriff durch custom Middleware, globale Sichtbarkeit ohne Tenant-Isolation
|
||||
|
||||
@@ -140,6 +140,29 @@ class PhotoboothIngestService
|
||||
$thumbnailRelative = ImageHelper::makeThumbnailOnDisk($destinationDisk, $destinationPath, $thumbnailPath, 640, 82);
|
||||
$thumbnailToStore = $thumbnailRelative ?? $destinationPath;
|
||||
|
||||
// Create watermarked copies (non-destructive).
|
||||
$watermarkConfig = \App\Support\WatermarkConfigResolver::resolve($event);
|
||||
$watermarkedPath = $destinationPath;
|
||||
$watermarkedThumb = $thumbnailToStore;
|
||||
if ($watermarkConfig['type'] !== 'none' && ! empty($watermarkConfig['asset']) && ! ($watermarkConfig['serve_originals'] ?? false)) {
|
||||
$watermarkedPath = ImageHelper::copyWithWatermark(
|
||||
$destinationDisk,
|
||||
$destinationPath,
|
||||
"events/{$eventSlug}/watermarked/{$filename}",
|
||||
$watermarkConfig
|
||||
) ?? $destinationPath;
|
||||
if ($thumbnailRelative) {
|
||||
$watermarkedThumb = ImageHelper::copyWithWatermark(
|
||||
$destinationDisk,
|
||||
$thumbnailToStore,
|
||||
"events/{$eventSlug}/watermarked/thumbnails/{$filename}",
|
||||
$watermarkConfig
|
||||
) ?? $thumbnailToStore;
|
||||
} else {
|
||||
$watermarkedThumb = $watermarkedPath;
|
||||
}
|
||||
}
|
||||
|
||||
$size = Storage::disk($destinationDisk)->size($destinationPath);
|
||||
$mimeType = Storage::disk($destinationDisk)->mimeType($destinationPath) ?? 'image/jpeg';
|
||||
$originalName = basename($file);
|
||||
@@ -165,8 +188,8 @@ class PhotoboothIngestService
|
||||
'original_name' => $originalName,
|
||||
'mime_type' => $mimeType,
|
||||
'size' => $size,
|
||||
'file_path' => $destinationPath,
|
||||
'thumbnail_path' => $thumbnailToStore,
|
||||
'file_path' => $watermarkedPath,
|
||||
'thumbnail_path' => $watermarkedThumb,
|
||||
'status' => 'pending',
|
||||
'guest_name' => Photo::SOURCE_PHOTOBOOTH,
|
||||
'ingest_source' => Photo::SOURCE_PHOTOBOOTH,
|
||||
@@ -192,6 +215,23 @@ class PhotoboothIngestService
|
||||
'photo_id' => $photo->id,
|
||||
]);
|
||||
|
||||
$watermarkedAsset = null;
|
||||
if ($watermarkedPath !== $destinationPath) {
|
||||
$watermarkedAsset = $this->storageManager->recordAsset($event, $destinationDisk, $watermarkedPath, [
|
||||
'variant' => 'watermarked',
|
||||
'mime_type' => $mimeType,
|
||||
'size_bytes' => Storage::disk($destinationDisk)->exists($watermarkedPath)
|
||||
? Storage::disk($destinationDisk)->size($watermarkedPath)
|
||||
: null,
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photo->id,
|
||||
'meta' => [
|
||||
'source_variant_id' => $asset->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($thumbnailRelative) {
|
||||
$this->storageManager->recordAsset($event, $destinationDisk, $thumbnailRelative, [
|
||||
'variant' => 'thumbnail',
|
||||
@@ -201,6 +241,21 @@ class PhotoboothIngestService
|
||||
'photo_id' => $photo->id,
|
||||
]);
|
||||
}
|
||||
if ($watermarkedThumb !== $thumbnailToStore) {
|
||||
$this->storageManager->recordAsset($event, $destinationDisk, $watermarkedThumb, [
|
||||
'variant' => 'watermarked_thumbnail',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'status' => 'hot',
|
||||
'processed_at' => now(),
|
||||
'photo_id' => $photo->id,
|
||||
'size_bytes' => Storage::disk($destinationDisk)->exists($watermarkedThumb)
|
||||
? Storage::disk($destinationDisk)->size($watermarkedThumb)
|
||||
: null,
|
||||
'meta' => [
|
||||
'source_variant_id' => $watermarkedAsset?->id ?? $asset->id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$photo->update(['media_asset_id' => $asset->id]);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ trait PresentsPackages
|
||||
$packageArray = $package->toArray();
|
||||
$features = $packageArray['features'] ?? [];
|
||||
$features = $this->normaliseFeatures($features);
|
||||
$watermarkPolicy = $package->watermark_allowed === false ? 'none' : 'basic';
|
||||
|
||||
$locale = app()->getLocale();
|
||||
$name = $this->resolveTranslation($package->name_translations ?? null, $package->name ?? '', $locale);
|
||||
@@ -50,6 +51,7 @@ trait PresentsPackages
|
||||
'gallery_duration_label' => $galleryDuration,
|
||||
'events' => $package->type === 'endcustomer' ? 1 : ($package->max_events_per_year ?? null),
|
||||
'features' => $features,
|
||||
'watermark_policy' => $watermarkPolicy,
|
||||
'limits' => $package->limits,
|
||||
'max_photos' => $package->max_photos,
|
||||
'max_guests' => $package->max_guests,
|
||||
|
||||
@@ -65,5 +65,122 @@ class ImageHelper
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a watermark in-place on the given disk/path.
|
||||
* Expects $config with keys: asset (path), position, opacity (0..1), scale (0..1), padding (px).
|
||||
*/
|
||||
public static function applyWatermarkOnDisk(string $disk, string $path, array $config): bool
|
||||
{
|
||||
$fullSrc = Storage::disk($disk)->path($path);
|
||||
if (! file_exists($fullSrc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$assetPath = $config['asset'] ?? null;
|
||||
if (! $assetPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$assetFull = null;
|
||||
if (Storage::disk('public')->exists($assetPath)) {
|
||||
$assetFull = Storage::disk('public')->path($assetPath);
|
||||
} elseif (file_exists($assetPath)) {
|
||||
$assetFull = $assetPath;
|
||||
}
|
||||
|
||||
if (! $assetFull || ! file_exists($assetFull)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = @file_get_contents($fullSrc);
|
||||
$src = $data !== false ? @imagecreatefromstring($data) : null;
|
||||
if (! $src) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wmData = @file_get_contents($assetFull);
|
||||
$watermark = $wmData !== false ? @imagecreatefromstring($wmData) : null;
|
||||
if (! $watermark) {
|
||||
imagedestroy($src);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
imagesavealpha($src, true);
|
||||
imagesavealpha($watermark, true);
|
||||
|
||||
$srcW = imagesx($src);
|
||||
$srcH = imagesy($src);
|
||||
$wmW = imagesx($watermark);
|
||||
$wmH = imagesy($watermark);
|
||||
|
||||
if ($srcW <= 0 || $srcH <= 0 || $wmW <= 0 || $wmH <= 0) {
|
||||
imagedestroy($src);
|
||||
imagedestroy($watermark);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$scale = max(0.05, min(1.0, (float) ($config['scale'] ?? 0.2)));
|
||||
$targetW = max(1, (int) round($srcW * $scale));
|
||||
$targetH = max(1, (int) round($wmH * ($targetW / $wmW)));
|
||||
|
||||
$resized = imagecreatetruecolor($targetW, $targetH);
|
||||
imagealphablending($resized, false);
|
||||
imagesavealpha($resized, true);
|
||||
imagecopyresampled($resized, $watermark, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH);
|
||||
imagedestroy($watermark);
|
||||
|
||||
$padding = max(0, (int) ($config['padding'] ?? 0));
|
||||
$position = $config['position'] ?? 'bottom-right';
|
||||
$x = $padding;
|
||||
$y = $padding;
|
||||
|
||||
if ($position === 'top-right') {
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
} elseif ($position === 'bottom-left') {
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
} elseif ($position === 'bottom-right') {
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
} elseif ($position === 'center') {
|
||||
$x = (int) max(0, ($srcW - $targetW) / 2);
|
||||
$y = (int) max(0, ($srcH - $targetH) / 2);
|
||||
}
|
||||
|
||||
$opacity = max(0.0, min(1.0, (float) ($config['opacity'] ?? 0.25)));
|
||||
$mergeOpacity = (int) round($opacity * 100); // imagecopymerge uses 0-100
|
||||
|
||||
imagealphablending($src, true);
|
||||
imagecopymerge($src, $resized, $x, $y, 0, 0, $targetW, $targetH, $mergeOpacity);
|
||||
imagedestroy($resized);
|
||||
|
||||
// Overwrite original (respect mime: always JPEG for compatibility)
|
||||
@imagejpeg($src, $fullSrc, 90);
|
||||
imagedestroy($src);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a source to destination and apply watermark there.
|
||||
*/
|
||||
public static function copyWithWatermark(string $disk, string $sourcePath, string $destPath, array $config): ?string
|
||||
{
|
||||
if (! Storage::disk($disk)->exists($sourcePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Storage::disk($disk)->makeDirectory(dirname($destPath));
|
||||
Storage::disk($disk)->copy($sourcePath, $destPath);
|
||||
|
||||
$applied = self::applyWatermarkOnDisk($disk, $destPath, $config);
|
||||
|
||||
return $applied ? $destPath : null;
|
||||
}
|
||||
}
|
||||
|
||||
90
app/Support/WatermarkConfigResolver.php
Normal file
90
app/Support/WatermarkConfigResolver.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Event;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class WatermarkConfigResolver
|
||||
{
|
||||
public static function determineBrandingAllowed(Event $event): bool
|
||||
{
|
||||
$event->loadMissing('eventPackage.package');
|
||||
|
||||
return $event->eventPackage?->package?->branding_allowed === true;
|
||||
}
|
||||
|
||||
public static function determinePolicy(Event $event): string
|
||||
{
|
||||
$event->loadMissing('eventPackage.package');
|
||||
|
||||
return $event->eventPackage?->package?->watermark_allowed === false ? 'none' : 'basic';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{type:string, policy:string, asset?:string, position?:string, opacity?:float, scale?:float, padding?:int}
|
||||
*/
|
||||
public static function resolve(Event $event): array
|
||||
{
|
||||
$policy = self::determinePolicy($event);
|
||||
|
||||
if ($policy === 'none') {
|
||||
return [
|
||||
'type' => 'none',
|
||||
'policy' => $policy,
|
||||
];
|
||||
}
|
||||
|
||||
$baseSetting = WatermarkSetting::query()->first();
|
||||
$base = [
|
||||
'asset' => $baseSetting?->asset ?? config('watermark.base.asset', 'branding/fotospiel-watermark.png'),
|
||||
'position' => $baseSetting?->position ?? config('watermark.base.position', 'bottom-right'),
|
||||
'opacity' => $baseSetting?->opacity ?? config('watermark.base.opacity', 0.25),
|
||||
'scale' => $baseSetting?->scale ?? config('watermark.base.scale', 0.2),
|
||||
'padding' => $baseSetting?->padding ?? config('watermark.base.padding', 16),
|
||||
];
|
||||
|
||||
$event->loadMissing('eventPackage.package', 'tenant');
|
||||
$brandingAllowed = self::determineBrandingAllowed($event);
|
||||
$eventWatermark = Arr::get($event->settings, 'watermark', []);
|
||||
$tenantWatermark = Arr::get($event->tenant?->settings, 'watermark', []);
|
||||
$serveOriginals = (bool) Arr::get($event->settings, 'watermark_serve_originals', false);
|
||||
|
||||
$mode = $brandingAllowed
|
||||
? ($eventWatermark['mode'] ?? $tenantWatermark['mode'] ?? 'base')
|
||||
: 'base';
|
||||
|
||||
if ($mode === 'off' && $policy === 'basic') {
|
||||
$mode = 'base';
|
||||
}
|
||||
|
||||
if ($mode === 'off') {
|
||||
return [
|
||||
'type' => 'none',
|
||||
'policy' => $policy,
|
||||
];
|
||||
}
|
||||
|
||||
$source = $mode === 'custom' && $brandingAllowed ? ($eventWatermark ?: $tenantWatermark) : [];
|
||||
|
||||
$asset = $source['asset'] ?? $base['asset'] ?? null;
|
||||
$position = $source['position'] ?? $base['position'] ?? 'bottom-right';
|
||||
$opacity = (float) ($source['opacity'] ?? $base['opacity'] ?? 0.25);
|
||||
$scale = (float) ($source['scale'] ?? $base['scale'] ?? 0.2);
|
||||
$padding = (int) ($source['padding'] ?? $base['padding'] ?? 16);
|
||||
|
||||
$clamp = static fn (float $value, float $min, float $max) => max($min, min($max, $value));
|
||||
|
||||
return [
|
||||
'type' => $mode === 'custom' && $brandingAllowed ? 'custom' : 'base',
|
||||
'policy' => $policy,
|
||||
'asset' => $asset,
|
||||
'position' => $position,
|
||||
'opacity' => $clamp($opacity, 0.0, 1.0),
|
||||
'scale' => $clamp($scale, 0.05, 1.0),
|
||||
'padding' => max(0, $padding),
|
||||
'serve_originals' => $serveOriginals,
|
||||
];
|
||||
}
|
||||
}
|
||||
use App\Models\WatermarkSetting;
|
||||
Reference in New Issue
Block a user