[relative name => ['ai' => kind]]]. */ function raijuMediaMeta(bool $reload = false): array { static $cache = null; if ($cache === null || $reload) { $path = raijuMediaMetaPath(); $data = is_file($path) ? json_decode((string)file_get_contents($path), true) : []; $cache = is_array($data) ? $data : []; } return $cache; } /** Media type and name relative to the type folder for a public source, e.g. /assets/images/a/b.webp. */ function raijuMediaRefFromSrc(string $src): ?array { $path = rawurldecode((string)(parse_url($src, PHP_URL_PATH) ?? '')); foreach (['image' => 'assets/images/', 'video' => 'assets/videos/'] as $type => $marker) { $position = strpos($path, $marker); if ($position === false) continue; $name = substr($path, $position + strlen($marker)); if ($name === '' || str_contains($name, '..') || str_starts_with($name, '/')) return null; return ['type' => $type, 'name' => $name]; } return null; } /** Label kind of a media file: 'generated', 'modified' or ''. */ function raijuMediaAiLabelFor(string $type, string $name): string { $kind = raijuMediaMeta()[$type][$name]['ai'] ?? ''; return in_array($kind, RAIJU_AI_LABEL_KINDS, true) ? $kind : ''; } /** Label kind for a public image or video source. */ function raijuMediaAiLabel(string $src): string { $ref = raijuMediaRefFromSrc($src); return $ref ? raijuMediaAiLabelFor($ref['type'], $ref['name']) : ''; } /** Set ('generated', 'modified') or remove ('') the label of a media file. */ function raijuSetMediaAiLabel(string $type, string $name, string $kind): bool { $kind = in_array($kind, RAIJU_AI_LABEL_KINDS, true) ? $kind : ''; if ($kind === '' && !isset(raijuMediaMeta()[$type][$name])) return true; $ok = raijuJsonUpdate(raijuMediaMetaPath(), function (array &$data) use ($type, $name, $kind) { if ($kind !== '') { $data[$type][$name]['ai'] = $kind; return true; } unset($data[$type][$name]['ai']); raijuMediaMetaPrune($data, $type, $name); return true; }); raijuMediaMeta(true); return $ok; } /** * Source and licence of an imported image: media-meta[type][name]['credit']. * Keys: source, author, authorUrl, sourceUrl, license, attribution, importedAt, importedBy. */ function raijuSetMediaCredit(string $type, string $name, array $credit): bool { $clean = []; foreach (['source', 'author', 'authorUrl', 'sourceUrl', 'license', 'attribution', 'importedAt', 'importedBy'] as $key) { if (isset($credit[$key]) && is_string($credit[$key]) && $credit[$key] !== '') { $clean[$key] = mb_substr($credit[$key], 0, 300, 'UTF-8'); } } $ok = raijuJsonUpdate(raijuMediaMetaPath(), function (array &$data) use ($type, $name, $clean) { if ($clean) $data[$type][$name]['credit'] = $clean; else unset($data[$type][$name]['credit']); raijuMediaMetaPrune($data, $type, $name); return true; }); raijuMediaMeta(true); return $ok; } function raijuMediaCreditFor(string $type, string $name): array { $credit = raijuMediaMeta()[$type][$name]['credit'] ?? []; return is_array($credit) ? $credit : []; } /** Carry metadata along when a media file is renamed, moved, trashed or restored. */ function raijuMoveMediaMeta(string $fromType, string $fromName, string $toType, string $toName): bool { if (!isset(raijuMediaMeta()[$fromType][$fromName]) || ($fromType === $toType && $fromName === $toName)) return true; $ok = raijuJsonUpdate(raijuMediaMetaPath(), function (array &$data) use ($fromType, $fromName, $toType, $toName) { if (!isset($data[$fromType][$fromName])) return false; $data[$toType][$toName] = $data[$fromType][$fromName]; unset($data[$fromType][$fromName]); raijuMediaMetaPrune($data, $fromType, $fromName); return true; }); raijuMediaMeta(true); return $ok; } /** Forget the metadata of a deleted file, or of a whole namespace when $name is null. */ function raijuForgetMediaMeta(string $type, ?string $name = null): bool { $meta = raijuMediaMeta(); if ($name === null ? !isset($meta[$type]) : !isset($meta[$type][$name])) return true; $ok = raijuJsonUpdate(raijuMediaMetaPath(), function (array &$data) use ($type, $name) { if ($name === null) { unset($data[$type]); return true; } unset($data[$type][$name]); raijuMediaMetaPrune($data, $type, $name); return true; }); raijuMediaMeta(true); return $ok; } function raijuMediaMetaPrune(array &$data, string $type, string $name): void { if (isset($data[$type][$name]) && empty($data[$type][$name])) unset($data[$type][$name]); if (isset($data[$type]) && empty($data[$type])) unset($data[$type]); } /** * Best-effort detection from embedded provenance: the IPTC digital source type, * written to XMP or C2PA manifests by image generators and generative editing. */ function raijuDetectAiLabel(string $file): string { $size = @filesize($file); if (!$size || $size > 16 * 1024 * 1024) return ''; $bytes = (string)@file_get_contents($file); if (stripos($bytes, 'digitalsourcetype/compositeWithTrainedAlgorithmicMedia') !== false || stripos($bytes, 'digitalsourcetype/compositeSynthetic') !== false) { return 'modified'; } if (stripos($bytes, 'digitalsourcetype/trainedAlgorithmicMedia') !== false) { return 'generated'; } return ''; } /** Two-letter language of the rendered page. */ function raijuAiLabelLanguage(?string $lang = null): string { $lang = $lang ?? ($GLOBALS['currentLang'] ?? (defined('SITE_LANG_DEFAULT') ? SITE_LANG_DEFAULT : 'en')); return strtolower(substr((string)$lang, 0, 2)); } /** Accessible label text in the page language. */ function raijuAiLabelText(string $kind, ?string $lang = null): string { $texts = [ 'cs' => ['generated' => 'Vytvořeno pomocí AI', 'modified' => 'Upraveno pomocí AI'], 'de' => ['generated' => 'KI-generiert', 'modified' => 'KI-modifiziert'], 'en' => ['generated' => 'AI-generated', 'modified' => 'AI-modified'], 'es' => ['generated' => 'Generado con IA', 'modified' => 'Modificado con IA'], 'fr' => ['generated' => 'Généré par IA', 'modified' => 'Modifié par IA'], 'it' => ['generated' => 'Generato con IA', 'modified' => 'Modificato con IA'], 'pl' => ['generated' => 'Wygenerowano przez AI', 'modified' => 'Zmodyfikowano przez AI'], 'pt' => ['generated' => 'Gerado por IA', 'modified' => 'Modificado por IA'], 'tr' => ['generated' => 'Yapay zekâ ile oluşturuldu', 'modified' => 'Yapay zekâ ile değiştirildi'], ]; $lang = raijuAiLabelLanguage($lang); return ($texts[$lang] ?? $texts['en'])[$kind] ?? ''; } /** * Label artwork mode (settings.json → general.aiLabelArtwork): 'en' shows the English * artwork on every page (default), 'page' the German artwork on German pages. */ function raijuAiLabelArtworkMode(bool $reload = false): string { static $mode = null; if ($mode === null || $reload) { $path = defined('SETTINGS_PATH') ? SETTINGS_PATH : dirname(__DIR__) . '/content/settings.json'; $settings = is_file($path) ? json_decode((string)file_get_contents($path), true) : []; $mode = is_array($settings) && ($settings['general']['aiLabelArtwork'] ?? '') === 'page' ? 'page' : 'en'; } return $mode; } /** Label markup for an image source; empty when the file carries no AI label. */ function raijuAiLabelHtml(string $src, ?string $lang = null): string { $kind = raijuMediaAiLabel($src); return $kind === '' ? '' : raijuAiLabelMarkup($kind, $lang); } /** Label markup for a label kind: English artwork, or German on German pages if the site opts in. */ function raijuAiLabelMarkup(string $kind, ?string $lang = null): string { if (!in_array($kind, RAIJU_AI_LABEL_KINDS, true)) return ''; $lang = raijuAiLabelLanguage($lang); $artwork = (raijuAiLabelArtworkMode() === 'page' && $lang === 'de' ? 'de' : 'en') . '-' . $kind; // Width at the default height of 22px, from the artwork's aspect ratio $widths = ['de-generated' => 111, 'de-modified' => 122, 'en-generated' => 114, 'en-modified' => 102]; $GLOBALS['raijuAiLabelsRendered'] = true; $base = (string)($GLOBALS['basePath'] ?? ''); $text = htmlspecialchars(raijuAiLabelText($kind, $lang), ENT_QUOTES, 'UTF-8'); return '' . '' . $text . ''; } /** * Image markup followed by its label. With $frame, a labelled image and its label * share a positioned frame, so the label sits on the image even when text follows. */ function raijuAiLabelAttach(string $imageHtml, string $src, bool $frame = false): string { $label = raijuAiLabelHtml($src); if ($label === '') return $imageHtml; return $frame ? '' . $imageHtml . $label . '' : $imageHtml . $label; } /** Wrap already rendered image markup (image + label) in a label frame when it carries a label. */ function raijuAiLabelFrame(string $html): string { return str_contains($html, 'class="rj-ai-label ') ? '' . $html . '' : $html; } /** * Label data for the inline editor (window.RJ_AI_LABELS): labelled files and the label * markup per kind, so labels follow image swaps and media library changes right away. */ function raijuAiLabelEditorConfig(?string $lang = null): array { $files = []; foreach (['image', 'video'] as $type) { foreach (array_keys((array)(raijuMediaMeta()[$type] ?? [])) as $name) { $kind = raijuMediaAiLabelFor($type, (string)$name); if ($kind !== '') $files[$type][(string)$name] = $kind; } } // Templates only: they must not count as labels rendered on the page $rendered = $GLOBALS['raijuAiLabelsRendered'] ?? false; $markup = []; foreach (RAIJU_AI_LABEL_KINDS as $kind) { $markup[$kind] = raijuAiLabelMarkup($kind, $lang); } $GLOBALS['raijuAiLabelsRendered'] = $rendered; return ['files' => (object)array_map(fn($names) => (object)$names, $files), 'markup' => $markup]; } /** Whether a label was rendered on this page (loads js/ai-labels.js). */ function raijuAiLabelsRendered(): bool { return !empty($GLOBALS['raijuAiLabelsRendered']); }