Rebuild WebDAV previews in Piwigo-compatible formats

This commit is contained in:
Terranom674
2026-08-18 23:30:22 +02:00
parent 450f9a0ac5
commit d18e7108dd

View File

@@ -6,6 +6,10 @@ if (PHP_SAPI !== 'cli')
exit(1); exit(1);
} }
const BRATONIEN_WEBDAV_PREVIEW_VERSION = 2;
const BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE = 4096;
const BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY = 88;
function fail_preview($message) function fail_preview($message)
{ {
throw new RuntimeException($message); throw new RuntimeException($message);
@@ -28,72 +32,161 @@ function fetch_remote_blob($url, $user, $password)
CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password, CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_FAILONERROR => false, CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.5.19', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.0',
)); ));
$body = curl_exec($ch); $body = curl_exec($ch);
$errno = curl_errno($ch); $errno = curl_errno($ch);
$error = curl_error($ch); $error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) fail_preview('WebDAV-Bild konnte nicht geladen werden: '.$error); if ($body === false || $errno !== 0) fail_preview('WebDAV-Bild konnte nicht geladen werden: '.$error);
if ($http < 200 || $http >= 300) fail_preview('WebDAV-Bild antwortete mit HTTP '.$http.'.'); if ($http < 200 || $http >= 300) fail_preview('WebDAV-Bild antwortete mit HTTP '.$http.'.');
if ($body === '') fail_preview('WebDAV-Bild ist leer.');
return (string)$body; return (string)$body;
} }
function write_preview($blob, $target) function preview_extension_for_entry(array $entry)
{
$content_type = strtolower(trim((string)($entry['content_type'] ?? '')));
$path = strtolower((string)($entry['webdav_path'] ?? ''));
if ($content_type === 'image/png' || preg_match('/\.png$/', $path))
{
return 'png';
}
return 'jpg';
}
function preview_target_for_entry($cache_dir, $key, array $entry)
{
return $cache_dir.'/'.$key.'.'.preview_extension_for_entry($entry);
}
function remove_other_preview_format($cache_dir, $key, $keep_target)
{
foreach (array('jpg', 'png', 'webp') as $ext)
{
$candidate = $cache_dir.'/'.$key.'.'.$ext;
if ($candidate !== $keep_target && is_file($candidate)) @unlink($candidate);
}
}
function write_preview_imagick($blob, $target, $extension)
{
$image = new Imagick();
try
{
$image->readImageBlob($blob);
if (method_exists($image, 'autoOrientImage')) @$image->autoOrientImage();
$image->setIteratorIndex(0);
$image->thumbnailImage(BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE, true, true);
$image->stripImage();
if ($extension === 'png')
{
$image->setImageFormat('png');
}
else
{
$image->setImageBackgroundColor('white');
if (method_exists($image, 'mergeImageLayers'))
{
$image = $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
}
$image->setImageFormat('jpeg');
$image->setImageCompression(Imagick::COMPRESSION_JPEG);
$image->setImageCompressionQuality(BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
$image->setInterlaceScheme(Imagick::INTERLACE_PLANE);
}
if (!$image->writeImage($target)) fail_preview('Imagick konnte das Preview nicht schreiben.');
}
finally
{
$image->clear();
$image->destroy();
}
}
function write_preview_gd($blob, $target, $extension)
{
$source = @imagecreatefromstring($blob);
if (!$source) fail_preview('GD konnte das Bild nicht dekodieren.');
try
{
$width = imagesx($source);
$height = imagesy($source);
if ($width < 1 || $height < 1) fail_preview('Bildabmessungen sind ungültig.');
$scale = min(1, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE / $width, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE / $height);
$target_width = max(1, (int)round($width * $scale));
$target_height = max(1, (int)round($height * $scale));
$preview = imagecreatetruecolor($target_width, $target_height);
if (!$preview) fail_preview('GD konnte die Preview-Arbeitsfläche nicht anlegen.');
try
{
if ($extension === 'png')
{
imagealphablending($preview, false);
imagesavealpha($preview, true);
$transparent = imagecolorallocatealpha($preview, 0, 0, 0, 127);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $transparent);
}
else
{
$white = imagecolorallocate($preview, 255, 255, 255);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $white);
}
if (!imagecopyresampled($preview, $source, 0, 0, 0, 0, $target_width, $target_height, $width, $height))
{
fail_preview('GD konnte das Preview nicht skalieren.');
}
$written = $extension === 'png'
? imagepng($preview, $target, 6)
: imagejpeg($preview, $target, BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
if (!$written) fail_preview('GD konnte das Preview nicht schreiben.');
}
finally
{
imagedestroy($preview);
}
}
finally
{
imagedestroy($source);
}
}
function write_preview($blob, $target, $extension)
{ {
$dir = dirname($target); $dir = dirname($target);
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.'); if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.');
if (class_exists('Imagick')) if (class_exists('Imagick'))
{ {
$image = new Imagick(); write_preview_imagick($blob, $target, $extension);
$image->readImageBlob($blob);
if (method_exists($image, 'autoOrientImage')) @$image->autoOrientImage();
$image->setIteratorIndex(0);
$image->thumbnailImage(1600, 1600, true, true);
$image->setImageFormat('webp');
$image->setImageCompressionQuality(85);
if (!$image->writeImage($target)) fail_preview('Imagick konnte das Preview nicht schreiben.');
$image->clear();
$image->destroy();
@chmod($target, 0644);
return;
} }
elseif (function_exists('imagecreatefromstring') && function_exists('imagejpeg') && function_exists('imagepng'))
if (function_exists('imagecreatefromstring') && function_exists('imagewebp'))
{ {
$source = @imagecreatefromstring($blob); write_preview_gd($blob, $target, $extension);
if (!$source) fail_preview('GD konnte das Bild nicht dekodieren.'); }
$width = imagesx($source); else
$height = imagesy($source); {
if ($width < 1 || $height < 1) fail_preview('Weder Imagick noch GD ist für die Preview-Erzeugung verfügbar.');
{
imagedestroy($source);
fail_preview('Bildabmessungen sind ungültig.');
}
$scale = min(1, 1600 / $width, 1600 / $height);
$target_width = max(1, (int)round($width * $scale));
$target_height = max(1, (int)round($height * $scale));
$preview = imagecreatetruecolor($target_width, $target_height);
imagealphablending($preview, false);
imagesavealpha($preview, true);
$transparent = imagecolorallocatealpha($preview, 0, 0, 0, 127);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $transparent);
imagecopyresampled($preview, $source, 0, 0, 0, 0, $target_width, $target_height, $width, $height);
if (!imagewebp($preview, $target, 85))
{
imagedestroy($preview);
imagedestroy($source);
fail_preview('GD konnte das Preview nicht schreiben.');
}
imagedestroy($preview);
imagedestroy($source);
@chmod($target, 0644);
return;
} }
fail_preview('Weder Imagick noch GD/WebP ist für die Preview-Erzeugung verfügbar.'); clearstatcache(true, $target);
$size = @getimagesize($target);
if (!is_array($size) || empty($size[0]) || empty($size[1]) || !is_file($target) || filesize($target) < 64)
{
@unlink($target);
fail_preview('Das erzeugte Preview ist ungültig.');
}
@chmod($target, 0644);
} }
try try
@@ -117,6 +210,7 @@ try
if (!is_dir($cache_dir) && !mkdir($cache_dir, 0755, true) && !is_dir($cache_dir)) fail_preview('Preview-Cache konnte nicht angelegt werden.'); if (!is_dir($cache_dir) && !mkdir($cache_dir, 0755, true) && !is_dir($cache_dir)) fail_preview('Preview-Cache konnte nicht angelegt werden.');
@chmod($cache_dir, 0755); @chmod($cache_dir, 0755);
$state_file = $cache_dir.'/state.json'; $state_file = $cache_dir.'/state.json';
$old_state = array(); $old_state = array();
if (is_readable($state_file)) if (is_readable($state_file))
@@ -135,12 +229,26 @@ try
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue; if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
$webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/'); $webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/');
if ($webdav_path === '') continue; if ($webdav_path === '') continue;
$etag = (string)($entry['etag'] ?? ''); $etag = (string)($entry['etag'] ?? '');
$key = sha1($webdav_path); $key = sha1($webdav_path);
$target = $cache_dir.'/'.$key.'.webp'; $target = preview_target_for_entry($cache_dir, $key, $entry);
$new_state[$key] = array('path'=>$webdav_path, 'etag'=>$etag); $extension = pathinfo($target, PATHINFO_EXTENSION);
$new_state[$key] = array(
'path' => $webdav_path,
'etag' => $etag,
'format' => $extension,
'version' => BRATONIEN_WEBDAV_PREVIEW_VERSION,
);
if (is_file($target) && isset($old_state[$key]) && (string)($old_state[$key]['etag'] ?? '') === $etag) $old = $old_state[$key] ?? null;
if (
is_file($target)
&& is_array($old)
&& (string)($old['etag'] ?? '') === $etag
&& (string)($old['format'] ?? '') === $extension
&& (int)($old['version'] ?? 0) === BRATONIEN_WEBDAV_PREVIEW_VERSION
)
{ {
$cached++; $cached++;
continue; continue;
@@ -150,7 +258,8 @@ try
{ {
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path); $url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path);
$blob = fetch_remote_blob($url, $user, $password); $blob = fetch_remote_blob($url, $user, $password);
write_preview($blob, $target); write_preview($blob, $target, $extension);
remove_other_preview_format($cache_dir, $key, $target);
$generated++; $generated++;
} }
catch (Throwable $e) catch (Throwable $e)
@@ -160,10 +269,12 @@ try
} }
} }
foreach (glob($cache_dir.'/*.webp') ?: array() as $file) foreach (glob($cache_dir.'/*') ?: array() as $file)
{ {
$key = basename($file, '.webp'); if (!is_file($file) || basename($file) === 'state.json') continue;
if (!isset($new_state[$key])) @unlink($file); $name = basename($file);
if (!preg_match('/^([a-f0-9]{40})\.(jpg|png|webp)$/', $name, $m)) continue;
if (!isset($new_state[$m[1]])) @unlink($file);
} }
file_put_contents($state_file, json_encode($new_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n", LOCK_EX); file_put_contents($state_file, json_encode($new_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n", LOCK_EX);