diff --git a/system/src/Grav/Common/Media/Factories/LocalMediaFactory.php b/system/src/Grav/Common/Media/Factories/LocalMediaFactory.php index 417836d21..b567f2b8f 100644 --- a/system/src/Grav/Common/Media/Factories/LocalMediaFactory.php +++ b/system/src/Grav/Common/Media/Factories/LocalMediaFactory.php @@ -12,6 +12,8 @@ namespace Grav\Common\Media\Factories; use Grav\Common\Media\Interfaces\MediaCollectionInterface; use Grav\Common\Media\Interfaces\MediaFactoryInterface; use Grav\Common\Page\Media; +use Grav\Common\Utils; +use RuntimeException; /** * @@ -38,4 +40,40 @@ class LocalMediaFactory implements MediaFactoryInterface return new Media($path, $order, $load); } + + /** + * @param string $type + * @param string $path + * @return string + */ + public function readFile(string $type, string $path): string + { + $filepath = GRAV_WEBROOT . '/' . $path; + + error_clear_last(); + $contents = @file_get_contents($filepath); + if (false === $contents) { + throw new RuntimeException('Reading media file failed: ' . (error_get_last()['message'] ?? sprintf('Cannot read %s', Utils::basename($filepath)))); + } + + return $contents; + } + + /** + * @param string $type + * @param string $path + * @return resource + */ + public function readStream(string $type, string $path) + { + $filepath = GRAV_WEBROOT . '/' . $path; + + error_clear_last(); + $contents = @fopen($filepath, 'rb'); + if (false === $contents) { + throw new RuntimeException('Reading media file failed: ' . (error_get_last()['message'] ?? sprintf('Cannot open %s', Utils::basename($filepath)))); + } + + return $contents; + } } diff --git a/system/src/Grav/Common/Media/Factories/MediaFactory.php b/system/src/Grav/Common/Media/Factories/MediaFactory.php index 21e449d1b..dbd5eda30 100644 --- a/system/src/Grav/Common/Media/Factories/MediaFactory.php +++ b/system/src/Grav/Common/Media/Factories/MediaFactory.php @@ -13,6 +13,7 @@ use Grav\Common\Grav; use Grav\Common\Media\Events\MediaEventSubscriber; use Grav\Common\Media\Interfaces\MediaCollectionInterface; use Grav\Common\Media\Interfaces\MediaFactoryInterface; +use RuntimeException; use Symfony\Component\EventDispatcher\EventDispatcher; /** @@ -96,4 +97,48 @@ final class MediaFactory implements MediaFactoryInterface return null; } + + /** + * @param string $uri + * @return string + */ + public function readFile(string $uri): string + { + if (preg_match('{^(?:media-([^:]+)://)?(.*)$}', $uri, $matches)) { + $type = str_replace('-', '_', $matches[1]); + $filepath = $matches[2]; + } else { + $type = 'local'; + $filepath = $uri; + } + + $factory = $this->collectionTypes[$type] ?? null; + if ($factory) { + return $factory->readFile($type, $filepath); + } + + throw new RuntimeException(sprintf('Reading media file failed: type %s does not exist', $type), 500); + } + + /** + * @param string $uri + * @return resource + */ + public function readStream(string $uri) + { + if (preg_match('{^(?:media-([^:]+)://)?(.*)$}', $uri, $matches)) { + $type = str_replace('-', '_', $matches[1]); + $filepath = $matches[2]; + } else { + $type = 'local'; + $filepath = $uri; + } + + $factory = $this->collectionTypes[$type] ?? null; + if ($factory) { + return $factory->readStream($type, $filepath); + } + + throw new RuntimeException(sprintf('Reading media file failed: type %s does not exist', $type), 500); + } } diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php index 947ad0377..40f03433a 100644 --- a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php +++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php @@ -18,6 +18,8 @@ use Grav\Framework\File\Formatter\JsonFormatter; use Grav\Framework\File\JsonFile; use Grav\Framework\Image\Adapter\GdAdapter; use Grav\Framework\Image\Image; +use Grav\Framework\Psr7\Response; +use Psr\Http\Message\ResponseInterface; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RuntimeException; use function func_num_args; @@ -58,6 +60,70 @@ trait ImageMediaTrait /** @var string */ protected $sizes = '100vw'; + /** + * @param string $path + * @return array|null + */ + protected static function createImageFromCache(string $path): ?array + { + $filepath = GRAV_WEBROOT . $path; + $cachepath = "{$filepath}.json"; + $file = static::getCacheMetaFile($cachepath); + if (!$file->exists()) { + return null; + } + + $data = $file->load(); + $format = $data['extra']['format'] ?? 'jpg'; + $mime = $data['extra']['mime'] ?? ''; + $quality = $data['extra']['quality'] ?? 80; + $mediaUri = $data['extra']['media-uri'] ?? null; + + $mediaFactory = Grav::instance()['media_factory']; + + $fileData = $mediaFactory->readFile($mediaUri); + $adapter = GdAdapter::createFromString($fileData); + + $image = Image::createFromArray($data); + + $image->setAdapter($adapter); + $filepath = $image->save($filepath, $format, $quality); + $image->freeAdapter(); + + $time = filemtime($filepath); + $size = filesize($filepath); + + return [$filepath, $mime, $time, $size]; + } + + /** + * @param string $path + * @return ResponseInterface + */ + public static function createImageResponseFromCache(string $path): ResponseInterface + { + [$filepath, $mime, $time, $size] = static::createImageFromCache($path); + if (is_file($filepath)) { + $code = 200; + } else { + $code = 404; + // TODO: customizable 404 image? + $filepath = GRAV_WEBROOT . '/system/images/media/thumb-jpg.png'; + $mime = 'image/png'; + $time = filemtime($filepath); + $size = filesize($filepath); + } + + $body = fopen($filepath, 'rb'); + $headers = [ + 'Content-Type' => $mime, + 'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT', + 'ETag' => sprintf('%x-%x', $size, $time) + ]; + + return new Response($code, $headers, $body); + } + /** * Also unset the image on destruct. */ @@ -599,9 +665,12 @@ trait ImageMediaTrait $format = $extension; } + $image = $this->image; $image->extra['format'] = $format; $image->extra['quality'] = $quality; + $image->extra['media-uri'] = $this->getMedia()->getMediaUri($this->filename); + $image->extra['mime'] = $this->mime; $data = $image->jsonSerialize(); $hash = $data['hash']; @@ -618,14 +687,15 @@ trait ImageMediaTrait $imageFile = '/' . $locator->getResource($imageFile, false); $cacheFile = $locator->getResource($cacheFile, true); - $file = $this->getCacheMetaFile($cacheFile); + $file = static::getCacheMetaFile($cacheFile); if (!$file->exists()) { $file->save($data); } else { $file->touch(); } - return $this->generateCacheImage(GRAV_WEBROOT . $imageFile); + return GRAV_WEBROOT . $imageFile; + //return $this->generateCacheImage(GRAV_WEBROOT . $imageFile); } /** @@ -663,7 +733,7 @@ trait ImageMediaTrait * @param string $filepath * @return JsonFile */ - protected function getCacheMetaFile(string $filepath): JsonFile + protected static function getCacheMetaFile(string $filepath): JsonFile { $formatter = new JsonFormatter(['encode_options' => JSON_PRETTY_PRINT]); diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index 99c47c112..1d3dd4f36 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -112,6 +112,18 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac */ abstract public function getUrl(string $filename): string; + /** + * @param string $filename + * @return string + */ + public function getMediaUri(string $filename): string + { + $schema = 'media-' . str_replace('_', '-', $this->getName()); + $path = $this->getRealPath($filename); + + return "{$schema}://{$path}"; + } + /** * @return bool */ diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index 6b31450d3..1d69c81ad 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -13,7 +13,6 @@ use Grav\Common\Config\Config; use Grav\Common\Data\Blueprint; use Grav\Common\Media\Interfaces\ImageManipulateInterface; use Grav\Common\Media\Interfaces\ImageMediaInterface; -use Grav\Common\Media\Interfaces\MediaCollectionInterface; use Grav\Common\Media\Interfaces\MediaLinkInterface; use Grav\Common\Media\Traits\ImageLoadingTrait; use Grav\Common\Media\Traits\ImageMediaTrait; diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php index 04e06edf3..3fec3db14 100644 --- a/system/src/Grav/Common/Processors/InitializeProcessor.php +++ b/system/src/Grav/Common/Processors/InitializeProcessor.php @@ -13,6 +13,7 @@ use Grav\Common\Config\Config; use Grav\Common\Debugger; use Grav\Common\Errors\Errors; use Grav\Common\Grav; +use Grav\Common\Page\Medium\ImageMedium; use Grav\Common\Page\Pages; use Grav\Common\Plugins; use Grav\Common\Session; @@ -30,6 +31,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use function defined; +use function dirname; use function in_array; /** @@ -98,6 +100,14 @@ class InitializeProcessor extends ProcessorBase // Load plugins. $this->initializePlugins(); + // Image handling can return response right away. + $response = $this->handleImageRequest($request); + if ($response) { + $this->stopTimer('_init'); + + return $response; + } + // Load pages. $this->initializePages($config); @@ -331,6 +341,31 @@ class InitializeProcessor extends ProcessorBase return null; } + /** + * @param ServerRequestInterface $request + * @return ResponseInterface|null + */ + protected function handleImageRequest(ServerRequestInterface $request): ?ResponseInterface + { + // Handle clockwork API calls. + $uri = $request->getUri(); + $server = $request->getServerParams(); + $basePath = str_replace('\\', '/', dirname(parse_url($server['SCRIPT_NAME'], PHP_URL_PATH))); + if ($basePath === '/') { + $basePath = ''; + } + $imagePath = $basePath . '/images/'; + $path = $uri->getPath(); + + if (str_starts_with($path, $imagePath)) { + $path = mb_substr($path, mb_strlen($basePath)); + + return ImageMedium::createImageResponseFromCache($path); + } + + return null; + } + /** * @param Config $config */