presseportale/app/Support/ImageCredit.php
Kevin Adametz 9f44546082 web/admin/pdf: gemeinsame Bildnachweis-Auflösung, Detailfenster am Artikelbild, Bestätigungsstand in Kunden- und Adminansicht, Wortwahl „automatisierte Prüfung" (Bildkennzeichnung Block D)
- App\Support\ImageCredit: Foto/Quelle-Zeile, KI-Hinweis mit Herkunft, Verantwortlich-Zeile nur mit Protokoll, sichere Links; PDF-Zeilen mit Bestätigungskennung
- Artikelseite: Overlay (eigenes i-Zeichen, Alpine ohne Plugin, Bottom-Sheet auf Mobil) mit Nachweis, Lizenz, Quelle, KI-Hinweis, Verantwortlich-Zeile, „Bild melden"
- Richtlinien 0.2: Prüfung als automatisiert beschrieben, Abschnitt „Bilder" (Decision-Update 3.5); Taglines, Suche, Newsrooms, Startseiten-CTA, Mails ohne „qualitätsgeprüft"
- Cache-Invalidierung bei Bildwechsel an veröffentlichten Meldungen; PHPStan-Baseline für das Resource bereinigt

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

159 lines
4.9 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(),
);
}
/**
* 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;
}
}