diff --git a/runtime/lib/precache-webdav-previews.php b/runtime/lib/precache-webdav-previews.php index 078207d..aed8fbf 100644 --- a/runtime/lib/precache-webdav-previews.php +++ b/runtime/lib/precache-webdav-previews.php @@ -6,6 +6,10 @@ if (PHP_SAPI !== 'cli') 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) { throw new RuntimeException($message); @@ -28,72 +32,161 @@ function fetch_remote_blob($url, $user, $password) CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => $user.':'.$password, 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); $errno = curl_errno($ch); $error = curl_error($ch); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + 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 ($body === '') fail_preview('WebDAV-Bild ist leer.'); 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); if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.'); if (class_exists('Imagick')) { - $image = new Imagick(); - $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; + write_preview_imagick($blob, $target, $extension); } - - if (function_exists('imagecreatefromstring') && function_exists('imagewebp')) + elseif (function_exists('imagecreatefromstring') && function_exists('imagejpeg') && function_exists('imagepng')) { - $source = @imagecreatefromstring($blob); - if (!$source) fail_preview('GD konnte das Bild nicht dekodieren.'); - $width = imagesx($source); - $height = imagesy($source); - if ($width < 1 || $height < 1) - { - 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; + write_preview_gd($blob, $target, $extension); + } + else + { + fail_preview('Weder Imagick noch GD ist für die Preview-Erzeugung verfügbar.'); } - 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 @@ -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.'); @chmod($cache_dir, 0755); + $state_file = $cache_dir.'/state.json'; $old_state = array(); if (is_readable($state_file)) @@ -135,12 +229,26 @@ try if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue; $webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/'); if ($webdav_path === '') continue; + $etag = (string)($entry['etag'] ?? ''); $key = sha1($webdav_path); - $target = $cache_dir.'/'.$key.'.webp'; - $new_state[$key] = array('path'=>$webdav_path, 'etag'=>$etag); + $target = preview_target_for_entry($cache_dir, $key, $entry); + $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++; continue; @@ -150,7 +258,8 @@ try { $url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path); $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++; } 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 (!isset($new_state[$key])) @unlink($file); + if (!is_file($file) || basename($file) === 'state.json') continue; + $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);