presseportale/app/Support/ImageCredit.php
Kevin Adametz 075d0d931f legal/web: Zustimmungsgate je Dokumentversion (aus bis AGB 0.7), OG-Variante mit eingebranntem Bildnachweis, Nachweis in Teasern (Bildkennzeichnung Blöcke G und H)
- legal_document_acceptances (insert-only), LegalAcceptanceGate, Pflicht-Schalter im Einreichungs-Modal über den gemeinsamen Trait; API: GET/POST /api/v1/legal/acceptances, submit antwortet 428 mit den geforderten Fassungen; LEGAL_ACCEPTANCE_GATE
- ImageService: og-Variante (1200×630) mit halbtransparentem Balken und Inter SemiBold, regenerateOgVariant() für Korrekturen und Bestand; Fokuspunkt/Letterbox getestet
- Uploader brennt den Nachweis beim Upload ein, ImageCorrectionService baut die OG-Variante nach einer Korrektur neu; Artikelseite nutzt og für og:image
- Teaser: Nachweis in alt/title (Feed, Hero, Kategorie, Newsroom), Unterschrift am Hero-Bild; PressRelease::cardImageCredit()

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:12:17 +00:00

179 lines
5.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Support;
use App\Enums\AiDeclarationSource;
use App\Models\Company;
use App\Models\ImageRightsConfirmation;
use App\Models\PressReleaseImage;
use Illuminate\Support\Str;
/**
* Eine Auflösung des Bildnachweises für alle Ausgaben (Bildkennzeichnung
* D.1): Artikelseite, Kunden- und Adminansicht, Beleg-PDF, Overlay.
*
* Regeln (Decision-Update 3.4):
* - Nachweiszeile: „Foto: [copyright]", sonst „Foto: [author]", sonst im
* Bestand „Quelle: [Firma]".
* - KI-Zusatz aus der Portalkennzeichnung, Bestand-Fallback `is_ai_generated`.
* - Verantwortlich-Zeile „Bild und Angaben: [Firma], eingereicht am [Datum]"
* NUR mit Bestätigungsprotokoll — der Altbestand hat keins.
* - Links nur mit http/https.
*/
final readonly class ImageCredit
{
private function __construct(
public ?string $credit,
public string $creditLabel,
public ?string $aiLabel,
public ?string $aiSourceLabel,
public ?string $licenseUrl,
public ?string $sourceUrl,
public ?string $responsibleLine,
public ?ImageRightsConfirmation $confirmation,
public bool $isLegacy,
) {}
public static function for(PressReleaseImage $image, ?Company $company = null): self
{
$company ??= $image->pressRelease?->company;
$companyName = $company?->name;
$credit = self::clean($image->copyright) ?? self::clean($image->author);
$creditLabel = __('web.detail.photo');
if ($credit === null && $image->isLegacyRules() && filled($companyName)) {
$credit = $companyName;
$creditLabel = __('web.detail.source');
}
$aiLabel = $image->ai_declaration?->publicLabel() ?? ($image->is_ai_generated ? __('KI-generiert') : null);
$confirmation = $image->relationLoaded('latestRightsConfirmation')
? $image->latestRightsConfirmation
: $image->latestRightsConfirmation()->first();
$responsible = null;
if ($confirmation !== null && filled($companyName)) {
$responsible = __('web.detail.image_responsible', [
'company' => $companyName,
'date' => $confirmation->confirmed_at->locale('de')->isoFormat('D. MMMM YYYY'),
]);
}
return new self(
credit: $credit,
creditLabel: $creditLabel,
aiLabel: $aiLabel,
aiSourceLabel: $aiLabel !== null ? $image->ai_declaration_source?->publicLabel() ?? ($image->isLegacyRules() ? null : AiDeclarationSource::Customer->publicLabel()) : null,
licenseUrl: self::safeUrl($image->license_url),
sourceUrl: self::safeUrl($image->source_url),
responsibleLine: $responsible,
confirmation: $confirmation,
isLegacy: $image->isLegacyRules(),
);
}
/**
* Zeile aus Einzelteilen — für den eingebrannten OG-Balken beim Upload,
* bevor es ein Modell gibt (H.1).
*/
public static function compose(?string $credit, ?string $aiLabel, ?string $label = null): ?string
{
$credit = self::clean($credit);
$parts = [];
if ($credit !== null) {
$parts[] = ($label ?? __('web.detail.photo')).': '.$credit;
}
if ($aiLabel !== null && $aiLabel !== '') {
$parts[] = $aiLabel;
}
return $parts === [] ? null : implode(' · ', $parts);
}
/**
* Sichtbare Zeile unter dem Bild, z. B. „Foto: Musterfirma · KI-generiert".
*/
public function line(): ?string
{
if ($this->credit === null && $this->aiLabel === null) {
return null;
}
$parts = [];
if ($this->credit !== null) {
$parts[] = $this->creditLabel.': '.$this->credit;
}
if ($this->aiLabel !== null) {
$parts[] = $this->aiLabel;
}
return implode(' · ', $parts);
}
public function hasDetails(): bool
{
return $this->credit !== null || $this->aiLabel !== null || $this->responsibleLine !== null;
}
/**
* Zeilen für das Beleg-PDF (D.4).
*
* @return list<string>
*/
public function pdfLines(): array
{
$lines = [];
if ($this->credit !== null) {
$lines[] = 'Bildnachweis: '.$this->credit;
}
if ($this->licenseUrl !== null) {
$lines[] = 'Lizenz: '.$this->licenseUrl;
}
if ($this->sourceUrl !== null) {
$lines[] = 'Quelle: '.$this->sourceUrl;
}
if ($this->aiLabel !== null) {
$lines[] = 'KI-Kennzeichnung: '.$this->aiLabel.($this->aiSourceLabel !== null ? ' ('.$this->aiSourceLabel.')' : '');
}
if ($this->confirmation !== null) {
$lines[] = sprintf(
'Rechte bestätigt am %s (%s, AGB %s)',
$this->confirmation->confirmed_at->format('d.m.Y H:i'),
$this->confirmation->legal_text_key,
$this->confirmation->agb_version ?? '–',
);
}
return $lines;
}
private static function clean(?string $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private static function safeUrl(?string $url): ?string
{
$url = trim((string) $url);
if ($url === '' || ! Str::startsWith(strtolower($url), ['http://', 'https://'])) {
return null;
}
return filter_var($url, FILTER_VALIDATE_URL) !== false ? $url : null;
}
}