From 2772e4880b109be461ee392a07a99258f7d88a2f Mon Sep 17 00:00:00 2001 From: Niklan Date: Sun, 29 Mar 2026 21:21:30 +0500 Subject: [PATCH 1/2] feat(image): replace static image styles with dynamic on-the-fly generation --- app/modules/blog/app_blog.info.yml | 1 + app/modules/blog/composer.json | 1 + app/modules/blog/src/Controller/RssFeed.php | 10 +- .../blog/src/Generator/BannerGenerator.php | 15 +- ...eprocessAppBlogLightboxResponsiveImage.php | 49 ---- app/modules/contract/src/Stub/Stub.php | 13 ++ app/modules/image/app_image.info.yml | 8 + app/modules/image/app_image.install | 24 ++ app/modules/image/app_image.routing.yml | 2 + app/modules/image/composer.json | 9 + .../image/src/AppImageServiceProvider.php | 34 +++ .../DynamicImageStyleController.php | 220 ++++++++++++++++++ .../DynamicImageStyle/DynamicImageStyle.php | 159 +++++++++++++ .../DynamicImageStyleBuilder.php | 80 +++++++ .../DynamicImageStylePathProcessor.php | 52 +++++ .../image/src/Routing/RouteProvider.php | 56 +++++ .../src/Twig/DynamicImageStyleExtension.php | 124 ++++++++++ .../main/src/AppMainServiceProvider.php | 2 + .../src/CompilerPass/StubsCompilerPass.php | 34 +++ .../src/AppPlatformServiceProvider.php | 5 - .../Markup/Twig/Extension/ImageDimensions.php | 62 ----- .../components/content/about/about.twig | 2 +- .../content/article-card/article-card.twig | 6 +- .../article-preview/article-preview.twig | 12 +- .../components/content/article/article.twig | 8 +- .../content/home-cards/home-cards.twig | 10 +- app/themes/laszlo/composer.json | 1 + app/themes/laszlo/laszlo.info.yml | 2 + .../Hook/Theme/PreprocessNodeBlogEntry.php | 3 +- ...p-blog-lightbox-responsive-image.html.twig | 20 +- composer.lock | 149 ++++++------ ...ntity_view_display.media.image.default.yml | 4 +- config/sync/core.extension.yml | 1 + config/sync/image.style.1000xy.yml | 21 -- config/sync/image.style.1200x1200.yml | 21 -- config/sync/image.style.1200xy.yml | 21 -- config/sync/image.style.150x200.yml | 21 -- config/sync/image.style.164x218.yml | 21 -- config/sync/image.style.200x200_contain.yml | 41 ---- config/sync/image.style.200x264.yml | 21 -- config/sync/image.style.300x400.yml | 21 -- config/sync/image.style.320x424.yml | 7 - config/sync/image.style.400x400.yml | 21 -- config/sync/image.style.400x528.yml | 21 -- config/sync/image.style.400xy.yml | 21 -- config/sync/image.style.600x600.yml | 21 -- config/sync/image.style.600x800.yml | 21 -- config/sync/image.style.600xy.yml | 21 -- config/sync/image.style.800x1064.yml | 21 -- config/sync/image.style.800x800.yml | 21 -- config/sync/image.style.800xy.yml | 21 -- config/sync/image.style.960xy.yml | 21 -- config/sync/image.style.about_photo_lg.yml | 21 -- config/sync/image.style.about_photo_md.yml | 21 -- config/sync/image.style.about_photo_sm.yml | 21 -- config/sync/image.style.about_photo_xl.yml | 21 -- config/sync/image.style.about_photo_xs.yml | 21 -- ....style.media_remote_video_thumbnail_lg.yml | 21 -- ....style.media_remote_video_thumbnail_md.yml | 21 -- ....style.media_remote_video_thumbnail_sm.yml | 21 -- ....style.media_remote_video_thumbnail_xs.yml | 21 -- ....style.node_blog_entry_teaser_promo_lg.yml | 21 -- ....style.node_blog_entry_teaser_promo_md.yml | 21 -- ....style.node_blog_entry_teaser_promo_sm.yml | 21 -- ....style.node_blog_entry_teaser_promo_xs.yml | 21 -- ...age.style.node_portfolio_full_photo_lg.yml | 21 -- ...age.style.node_portfolio_full_photo_md.yml | 21 -- ...age.style.node_portfolio_full_photo_sm.yml | 21 -- ...age.style.node_portfolio_full_photo_xs.yml | 21 -- .../image.style.paragraph_image_image_lg.yml | 21 -- .../image.style.paragraph_image_image_md.yml | 21 -- .../image.style.paragraph_image_image_sm.yml | 21 -- .../image.style.paragraph_image_image_xl.yml | 21 -- .../image.style.paragraph_image_image_xs.yml | 21 -- 74 files changed, 950 insertions(+), 1095 deletions(-) create mode 100644 app/modules/contract/src/Stub/Stub.php create mode 100644 app/modules/image/app_image.info.yml create mode 100644 app/modules/image/app_image.install create mode 100644 app/modules/image/app_image.routing.yml create mode 100644 app/modules/image/composer.json create mode 100644 app/modules/image/src/AppImageServiceProvider.php create mode 100644 app/modules/image/src/Controller/DynamicImageStyleController.php create mode 100644 app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php create mode 100644 app/modules/image/src/DynamicImageStyle/DynamicImageStyleBuilder.php create mode 100644 app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php create mode 100644 app/modules/image/src/Routing/RouteProvider.php create mode 100644 app/modules/image/src/Twig/DynamicImageStyleExtension.php create mode 100644 app/modules/main/src/CompilerPass/StubsCompilerPass.php delete mode 100644 app/modules/platform/src/Markup/Twig/Extension/ImageDimensions.php delete mode 100644 config/sync/image.style.1000xy.yml delete mode 100644 config/sync/image.style.1200x1200.yml delete mode 100644 config/sync/image.style.1200xy.yml delete mode 100644 config/sync/image.style.150x200.yml delete mode 100644 config/sync/image.style.164x218.yml delete mode 100644 config/sync/image.style.200x200_contain.yml delete mode 100644 config/sync/image.style.200x264.yml delete mode 100644 config/sync/image.style.300x400.yml delete mode 100644 config/sync/image.style.320x424.yml delete mode 100644 config/sync/image.style.400x400.yml delete mode 100644 config/sync/image.style.400x528.yml delete mode 100644 config/sync/image.style.400xy.yml delete mode 100644 config/sync/image.style.600x600.yml delete mode 100644 config/sync/image.style.600x800.yml delete mode 100644 config/sync/image.style.600xy.yml delete mode 100644 config/sync/image.style.800x1064.yml delete mode 100644 config/sync/image.style.800x800.yml delete mode 100644 config/sync/image.style.800xy.yml delete mode 100644 config/sync/image.style.960xy.yml delete mode 100644 config/sync/image.style.about_photo_lg.yml delete mode 100644 config/sync/image.style.about_photo_md.yml delete mode 100644 config/sync/image.style.about_photo_sm.yml delete mode 100644 config/sync/image.style.about_photo_xl.yml delete mode 100644 config/sync/image.style.about_photo_xs.yml delete mode 100644 config/sync/image.style.media_remote_video_thumbnail_lg.yml delete mode 100644 config/sync/image.style.media_remote_video_thumbnail_md.yml delete mode 100644 config/sync/image.style.media_remote_video_thumbnail_sm.yml delete mode 100644 config/sync/image.style.media_remote_video_thumbnail_xs.yml delete mode 100644 config/sync/image.style.node_blog_entry_teaser_promo_lg.yml delete mode 100644 config/sync/image.style.node_blog_entry_teaser_promo_md.yml delete mode 100644 config/sync/image.style.node_blog_entry_teaser_promo_sm.yml delete mode 100644 config/sync/image.style.node_blog_entry_teaser_promo_xs.yml delete mode 100644 config/sync/image.style.node_portfolio_full_photo_lg.yml delete mode 100644 config/sync/image.style.node_portfolio_full_photo_md.yml delete mode 100644 config/sync/image.style.node_portfolio_full_photo_sm.yml delete mode 100644 config/sync/image.style.node_portfolio_full_photo_xs.yml delete mode 100644 config/sync/image.style.paragraph_image_image_lg.yml delete mode 100644 config/sync/image.style.paragraph_image_image_md.yml delete mode 100644 config/sync/image.style.paragraph_image_image_sm.yml delete mode 100644 config/sync/image.style.paragraph_image_image_xl.yml delete mode 100644 config/sync/image.style.paragraph_image_image_xs.yml diff --git a/app/modules/blog/app_blog.info.yml b/app/modules/blog/app_blog.info.yml index b4305c8f1..5e0004dd9 100644 --- a/app/modules/blog/app_blog.info.yml +++ b/app/modules/blog/app_blog.info.yml @@ -4,6 +4,7 @@ core_version_requirement: ^11 package: App/Feature dependencies: - app_contract:app_contract + - app_image:app_image - app_media:app_media - app_platform:app_platform - drupal:image diff --git a/app/modules/blog/composer.json b/app/modules/blog/composer.json index e972f3f87..bff8da2ae 100644 --- a/app/modules/blog/composer.json +++ b/app/modules/blog/composer.json @@ -7,6 +7,7 @@ ], "require": { "app/contract": "^1.0", + "app/image": "*", "app/platform": "^1.0", "app-asset/hljs": "^11.9", "drupal/photoswipe": "^5.0", diff --git a/app/modules/blog/src/Controller/RssFeed.php b/app/modules/blog/src/Controller/RssFeed.php index d493a7685..461daf93b 100644 --- a/app/modules/blog/src/Controller/RssFeed.php +++ b/app/modules/blog/src/Controller/RssFeed.php @@ -6,13 +6,13 @@ use Drupal\app_blog\Node\ArticleBundle; use Drupal\app_contract\Utils\MediaHelper; +use Drupal\app_image\DynamicImageStyle\DynamicImageStyle; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Url; -use Drupal\image\ImageStyleInterface; use Drupal\node\NodeInterface; use Symfony\Component\HttpFoundation\Response; @@ -24,6 +24,7 @@ public function __construct( private EntityTypeManagerInterface $entityTypeManager, private LanguageManagerInterface $languageManager, private ConfigFactoryInterface $configFactory, + private DynamicImageStyle $dynamicImageStyle, ) {} public function __invoke(): CacheableResponse { @@ -156,9 +157,10 @@ private function addMediaContent(\DOMDocument $dom, \DOMElement $item, ArticleBu $file_uri = $file->getFileUri(); \assert(\is_string($file_uri)); - $image_style = $this->entityTypeManager->getStorage('image_style')->load('150x200'); - \assert($image_style instanceof ImageStyleInterface); - $url = $image_style->buildUrl($file_uri); + $url = $this + ->dynamicImageStyle + ->effect('image_scale_and_crop', ['width' => 150, 'height' => 200]) + ->buildUrl($file_uri); $media_content = $dom->createElement('media:content'); $media_content->setAttribute('url', $url); diff --git a/app/modules/blog/src/Generator/BannerGenerator.php b/app/modules/blog/src/Generator/BannerGenerator.php index 944ce20b9..ae8bcaa75 100644 --- a/app/modules/blog/src/Generator/BannerGenerator.php +++ b/app/modules/blog/src/Generator/BannerGenerator.php @@ -4,13 +4,12 @@ namespace Drupal\app_blog\Generator; +use Drupal\app_image\DynamicImageStyle\DynamicImageStyle; use Drupal\Component\Utility\Crypt; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Template\TwigEnvironment; -use Drupal\image\ImageStyleInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\String\UnicodeString; @@ -26,7 +25,7 @@ public function __construct( #[Autowire(service: 'file.mime_type.guesser')] private MimeTypeGuesserInterface $mimeTypeGuesser, private TwigEnvironment $twig, - private EntityTypeManagerInterface $entityTypeManager, + private DynamicImageStyle $dynamicImageStyle, ) {} public function generate(string $poster_path, string $title, int $created, int $comment_count): ?string { @@ -65,13 +64,9 @@ private function prepareBannerUri(string $poster_path, string $title, int $creat } private function renderSvg(string $poster_path, string $title, int $created, int $comment_count): string { - $image_style = $this->entityTypeManager->getStorage('image_style')->load('400x528'); - - if ($image_style instanceof ImageStyleInterface) { - $style_uri = $image_style->buildUri($poster_path); - $image_style->createDerivative($poster_path, $style_uri); - $poster_path = $style_uri; - } + $builder = $this->dynamicImageStyle->effect('image_scale_and_crop', ['width' => 400, 'height' => 528]); + $builder->createDerivative($poster_path); + $poster_path = $builder->buildUri($poster_path); $mime = $this->mimeTypeGuesser->guessMimeType($poster_path); $base64 = "data:$mime;base64," . \base64_encode((string) \file_get_contents($poster_path)); diff --git a/app/modules/blog/src/Hook/Theme/PreprocessAppBlogLightboxResponsiveImage.php b/app/modules/blog/src/Hook/Theme/PreprocessAppBlogLightboxResponsiveImage.php index 51a4fa152..cefbb705e 100644 --- a/app/modules/blog/src/Hook/Theme/PreprocessAppBlogLightboxResponsiveImage.php +++ b/app/modules/blog/src/Hook/Theme/PreprocessAppBlogLightboxResponsiveImage.php @@ -4,69 +4,20 @@ namespace Drupal\app_blog\Hook\Theme; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Hook\Attribute\Hook; -use Drupal\Core\Image\ImageFactory; -use Drupal\image\ImageStyleInterface; use Drupal\photoswipe\PhotoswipeAssetsManagerInterface; -use Drupal\responsive_image\ResponsiveImageStyleInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; #[Hook('preprocess_app_blog_lightbox_responsive_image')] final readonly class PreprocessAppBlogLightboxResponsiveImage { public function __construct( - private EntityTypeManagerInterface $entityTypeManager, - private ImageFactory $imageFactory, #[Autowire(service: 'photoswipe.assets_manager')] private PhotoswipeAssetsManagerInterface $photoswipeAssetsManager, ) {} public function __invoke(array &$variables): void { $this->photoswipeAssetsManager->attach($variables); - $this->prepareResponsiveImage($variables); - $this->prepareFullImageUrl($variables); - } - - private function prepareResponsiveImage(array &$variables): void { - \assert($variables['thumbnail_responsive_image_style_id']); - $storage = $this->entityTypeManager->getStorage('responsive_image_style'); - $style = $storage->load($variables['thumbnail_responsive_image_style_id']); - - if ($style instanceof ResponsiveImageStyleInterface) { - $variables['responsive_image'] = [ - '#type' => 'responsive_image', - '#responsive_image_style_id' => $variables['thumbnail_responsive_image_style_id'], - ]; - } - else { - $variables['responsive_image'] = [ - '#theme' => 'image', - ]; - } - - $variables['responsive_image']['#uri'] = $variables['uri']; - - if (\array_key_exists('alt', $variables)) { - $variables['responsive_image']['#attributes']['alt'] = $variables['alt']; - } - - if (!\array_key_exists('title', $variables)) { - return; - } - - $variables['responsive_image']['#attributes']['title'] = $variables['title']; - } - - private function prepareFullImageUrl(array &$variables): void { - \assert($variables['lightbox_image_style_id']); - $storage = $this->entityTypeManager->getStorage('image_style'); - $style = $storage->load($variables['lightbox_image_style_id']); - \assert($style instanceof ImageStyleInterface); - $variables['full_image_url'] = $style->buildUrl($variables['uri']); - // Prepare size for zoom aspect ratio. - $image = $this->imageFactory->get($style->buildUri($variables['uri'])); - $variables['size'] = "{$image->getWidth()}x{$image->getHeight()}"; } } diff --git a/app/modules/contract/src/Stub/Stub.php b/app/modules/contract/src/Stub/Stub.php new file mode 100644 index 000000000..9b519df6c --- /dev/null +++ b/app/modules/contract/src/Stub/Stub.php @@ -0,0 +1,13 @@ +set('app_image.dynamic_image_style_key', Crypt::randomBytesBase64()); +} + +/** + * Implements hook_uninstall(). + */ +function app_image_uninstall(): void { + \Drupal::state()->delete('app_image.dynamic_image_style_key'); +} diff --git a/app/modules/image/app_image.routing.yml b/app/modules/image/app_image.routing.yml new file mode 100644 index 000000000..88c1c3b45 --- /dev/null +++ b/app/modules/image/app_image.routing.yml @@ -0,0 +1,2 @@ +route_callbacks: + - Drupal\app_image\Routing\RouteProvider diff --git a/app/modules/image/composer.json b/app/modules/image/composer.json new file mode 100644 index 000000000..0c814e8ca --- /dev/null +++ b/app/modules/image/composer.json @@ -0,0 +1,9 @@ +{ + "name": "app/image", + "type": "drupal-custom-module", + "version": "1.0.0", + "require": { + "php": "^8.4", + "webmozart/assert": "^2.1" + } +} diff --git a/app/modules/image/src/AppImageServiceProvider.php b/app/modules/image/src/AppImageServiceProvider.php new file mode 100644 index 000000000..be40334f9 --- /dev/null +++ b/app/modules/image/src/AppImageServiceProvider.php @@ -0,0 +1,34 @@ + $container + ->autowire($class) + ->setPublic(TRUE) + ->setAutoconfigured(TRUE); + + $container->setParameter('app_image.skip_procedural_hook_scan', TRUE); + + $autowire(DynamicImageStyle::class); + $autowire(DynamicImageStyleController::class); + $autowire(DynamicImageStylePathProcessor::class); + $autowire(DynamicImageStyleExtension::class); + $autowire(RouteProvider::class); + } + +} diff --git a/app/modules/image/src/Controller/DynamicImageStyleController.php b/app/modules/image/src/Controller/DynamicImageStyleController.php new file mode 100644 index 000000000..50b1ee1d6 --- /dev/null +++ b/app/modules/image/src/Controller/DynamicImageStyleController.php @@ -0,0 +1,220 @@ +validateRequest($request); + + $is_public = StreamWrapperManager::getScheme($original_uri) !== 'private'; + $headers = $this->checkFileAccess($original_uri, $is_public); + + $derivative_uri = $this->dynamicImageStyle->buildUri($original_uri, $effects); + if (!\file_exists($derivative_uri)) { + $this->generateDerivative($original_uri, $effects, $compressed); + } + + return $this->deliverFile($derivative_uri, $headers, $is_public); + } + + /** + * Validates request parameters, token, and source file existence. + * + * @return array{string, list}>, string} + * A tuple of [original_uri, effects, compressed]. + */ + private function validateRequest(Request $request): array { + $compressed = $request->query->getString('effects'); + $itok = $request->query->getString('itok'); + if ($compressed === '' || $itok === '') { + throw new NotFoundHttpException(); + } + + $original_uri = $this->extractUri($request, $compressed); + if ($original_uri === NULL) { + throw new NotFoundHttpException(); + } + + if (!\hash_equals($this->dynamicImageStyle->generateToken($compressed, $original_uri), $itok)) { + throw new NotFoundHttpException(); + } + + if (!\file_exists($original_uri)) { + throw new NotFoundHttpException(); + } + + return [$original_uri, $this->tryDecompressEffects($compressed), $compressed]; + } + + /** + * @return list}> + */ + private function tryDecompressEffects(string $compressed): array { + try { + return $this->dynamicImageStyle->decompressEffects($compressed); + } + catch (\JsonException | \TypeError) { + throw new NotFoundHttpException(); + } + } + + /** + * Checks file download access for private files. + * + * @return array + * Headers from hook_file_download. + */ + private function checkFileAccess(string $uri, bool $is_public): array { + if ($is_public) { + return []; + } + + $headers = $this->moduleHandler->invokeAll('file_download', [$uri]); + if (\in_array(-1, $headers) || $headers === []) { + throw new AccessDeniedHttpException(); + } + + return $headers; + } + + /** + * @param list}> $effects + */ + private function generateDerivative(string $original_uri, array $effects, string $compressed): void { + $lock_name = 'dynamic_image_style:' . Crypt::hashBase64($compressed . ':' . $original_uri); + if (!$this->lock->acquire($lock_name)) { + throw new ServiceUnavailableHttpException( + retryAfter: 3, + message: 'Image generation in progress. Try again shortly.', + ); + } + + try { + $success = $this->dynamicImageStyle->createDerivative($original_uri, $effects); + } + finally { + $this->lock->release($lock_name); + } + + if (!$success) { + throw new NotFoundHttpException(); + } + } + + /** + * Extracts the original image URI from the request path. + * + * URL: /{base}/styles/dynamic/{hash}/{scheme}/{target}[.{converted_ext}] + * Result: {scheme}://{target} (without appended extension) + */ + private function extractUri(Request $request, string $compressed): ?string { + $path = $request->getPathInfo(); + $prefix = $this->detectPrefix($path); + if ($prefix === NULL) { + return NULL; + } + + // Parse: {hash}/{scheme}/{target}. + $parts = \explode('/', \substr($path, \strlen($prefix)), 3); + if (\count($parts) !== 3) { + return NULL; + } + [, $scheme, $target] = $parts; + + $target = $this->stripDerivativeExtension($target, $compressed); + + return $scheme . '://' . $target; + } + + /** + * Strips appended derivative extension (e.g., photo.jpg.webp → photo.jpg). + */ + private function stripDerivativeExtension(string $target, string $compressed): string { + try { + $effects = $this->dynamicImageStyle->decompressEffects($compressed); + $image_style = $this->dynamicImageStyle->createImageStyle($effects); + $current_extension = \pathinfo($target, \PATHINFO_EXTENSION); + $original_extension = \pathinfo(\pathinfo($target, \PATHINFO_FILENAME), \PATHINFO_EXTENSION); + + if ($original_extension !== '' && $image_style->getDerivativeExtension($original_extension) === $current_extension) { + return \substr($target, 0, -(\strlen($current_extension) + 1)); + } + } + catch (\JsonException | \TypeError) { + // Invalid effects — return target as-is, validation will fail later. + } + + return $target; + } + + private function detectPrefix(string $path): ?string { + $wrapper = $this->streamWrapperManager->getViaScheme('public'); + Assert::isInstanceOf($wrapper, LocalStream::class); + $public_prefix = '/' . $wrapper->getDirectoryPath() . '/styles/dynamic/'; + if (\str_starts_with($path, $public_prefix)) { + return $public_prefix; + } + + $private_prefix = '/system/files/styles/dynamic/'; + if (\str_starts_with($path, $private_prefix)) { + return $private_prefix; + } + + return NULL; + } + + private function deliverFile(string $derivative_uri, array $headers = [], bool $is_public = TRUE): BinaryFileResponse { + $image = $this->imageFactory->get($derivative_uri); + $uri = $this->streamWrapperManager->normalizeUri($derivative_uri); + + $headers += [ + 'Content-Type' => $image->getMimeType(), + 'Content-Length' => $image->getFileSize(), + ]; + + return new BinaryFileResponse( + file: $uri, + status: Response::HTTP_OK, + headers: $headers, + public: $is_public, + ); + } + +} diff --git a/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php b/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php new file mode 100644 index 000000000..cd1b35146 --- /dev/null +++ b/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php @@ -0,0 +1,159 @@ +effect('image_scale', ['width' => 300, 'height' => 200]) + * ->effect('image_convert', ['extension' => 'webp']) + * ->buildUrl('public://photos/sunset.jpg'); + * @endcode + */ +final readonly class DynamicImageStyle { + + public function __construct( + private EntityTypeManagerInterface $entityTypeManager, + private StreamWrapperManagerInterface $streamWrapperManager, + private StateInterface $state, + ) {} + + public function effect(string $id, array $data = []): DynamicImageStyleBuilder { + return new DynamicImageStyleBuilder($this)->effect($id, $data); + } + + /** + * @param list}> $effects + */ + public function buildUrl(string $uri, array $effects): string { + $scheme = StreamWrapperManager::getScheme($uri); + $target = StreamWrapperManager::getTarget($uri); + Assert::string($scheme); + Assert::string($target); + $compressed = $this->compressEffects($effects); + $itok = $this->generateToken($compressed, $uri); + $hash = Crypt::hashBase64($compressed); + + $image_style = $this->createImageStyle($effects); + $original_extension = \pathinfo($target, \PATHINFO_EXTENSION); + $derivative_extension = $image_style->getDerivativeExtension($original_extension); + + $path = $target; + if ($original_extension !== $derivative_extension) { + $path .= '.' . $derivative_extension; + } + + $base_path = $this->getBaseUrlPath($scheme); + + return '/' . $base_path . '/styles/dynamic/' . $hash . '/' . $scheme . '/' . $path + . '?effects=' . \urlencode($compressed) + . '&itok=' . $itok; + } + + /** + * @param list}> $effects + */ + public function buildUri(string $uri, array $effects): string { + $scheme = StreamWrapperManager::getScheme($uri); + $target = StreamWrapperManager::getTarget($uri); + Assert::string($scheme); + Assert::string($target); + + $compressed = $this->compressEffects($effects); + $hash = Crypt::hashBase64($compressed); + + $image_style = $this->createImageStyle($effects); + $original_extension = \pathinfo($target, \PATHINFO_EXTENSION); + $derivative_extension = $image_style->getDerivativeExtension($original_extension); + + $path = $target; + if ($original_extension !== $derivative_extension) { + $path .= '.' . $derivative_extension; + } + + return "$scheme://styles/dynamic/$hash/$scheme/$path"; + } + + /** + * @param list}> $effects + */ + public function createDerivative(string $uri, array $effects): bool { + $derivative_uri = $this->buildUri($uri, $effects); + $image_style = $this->createImageStyle($effects); + return $image_style->createDerivative($uri, $derivative_uri); + } + + /** + * @param list}> $effects + */ + public function compressEffects(array $effects): string { + $json = \json_encode($effects, flags: \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES); + return UrlHelper::compressQueryParameter($json); + } + + /** + * @return list}> + */ + public function decompressEffects(string $compressed): array { + $json = UrlHelper::uncompressQueryParameter($compressed); + /** @var list}> $effects */ + $effects = \json_decode($json, associative: TRUE, flags: \JSON_THROW_ON_ERROR); + return $effects; + } + + public function generateToken(string $compressed, string $uri): string { + $key = $this->state->get('app_image.dynamic_image_style_key', Settings::getHashSalt()); + return \substr(Crypt::hmacBase64($compressed . ':' . $uri, $key), 0, 8); + } + + /** + * @param list}> $effects + */ + public function createImageStyle(array $effects): ImageStyleInterface { + $storage = $this->entityTypeManager->getStorage('image_style'); + $image_style = $storage->create(['name' => 'dynamic']); + Assert::isInstanceOf($image_style, ImageStyleInterface::class); + + foreach ($effects as [$id, $data]) { + $image_style->addImageEffect(['id' => $id, 'data' => $data]); + } + + return $image_style; + } + + /** + * Returns the base URL path for the given stream wrapper scheme. + * + * Public files: sites/default/files (served by nginx directly after first + * generation). + * Private files: system/files (always served through PHP with access checks). + */ + private function getBaseUrlPath(string $scheme): string { + if ($scheme === 'private') { + return 'system/files'; + } + $wrapper = $this->streamWrapperManager->getViaScheme($scheme); + Assert::isInstanceOf($wrapper, LocalStream::class); + return $wrapper->getDirectoryPath(); + } + +} diff --git a/app/modules/image/src/DynamicImageStyle/DynamicImageStyleBuilder.php b/app/modules/image/src/DynamicImageStyle/DynamicImageStyleBuilder.php new file mode 100644 index 000000000..29d118a5c --- /dev/null +++ b/app/modules/image/src/DynamicImageStyle/DynamicImageStyleBuilder.php @@ -0,0 +1,80 @@ +effect('image_scale', ['width' => 300])->buildUrl($uri); + * + * // Twig — URI at the start: + * {{ uri|image_scale(300)|image_convert('avif') }} + * @endcode + */ +final readonly class DynamicImageStyleBuilder implements \Stringable { + + private const string DEFAULT_FORMAT = 'webp'; + + /** + * @param list}> $effects + */ + public function __construct( + private DynamicImageStyle $dynamicImageStyle, + private array $effects = [], + private ?string $uri = NULL, + ) {} + + #[\Override] + public function __toString(): string { + if ($this->uri === NULL) { + return ''; + } + return $this->dynamicImageStyle->buildUrl($this->uri, $this->resolveEffects()); + } + + public function effect(string $id, array $data = []): self { + return new self($this->dynamicImageStyle, [...$this->effects, [$id, $data]], $this->uri); + } + + public function buildUrl(string $uri): string { + return $this->dynamicImageStyle->buildUrl($uri, $this->resolveEffects()); + } + + public function buildUri(string $uri): string { + return $this->dynamicImageStyle->buildUri($uri, $this->resolveEffects()); + } + + public function createDerivative(string $uri): bool { + return $this->dynamicImageStyle->createDerivative($uri, $this->resolveEffects()); + } + + public function getUri(): ?string { + return $this->uri; + } + + /** + * @return list}> + */ + public function getEffects(): array { + return $this->effects; + } + + /** + * @return list}> + */ + private function resolveEffects(): array { + if (\array_any($this->effects, static fn (array $effect): bool => $effect[0] === 'image_convert')) { + return $this->effects; + } + return [...$this->effects, ['image_convert', ['extension' => self::DEFAULT_FORMAT]]]; + } + +} diff --git a/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php b/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php new file mode 100644 index 000000000..6943c33d0 --- /dev/null +++ b/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php @@ -0,0 +1,52 @@ + 301])] +final readonly class DynamicImageStylePathProcessor implements InboundPathProcessorInterface { + + public function __construct( + private StreamWrapperManagerInterface $streamWrapperManager, + ) {} + + public function processInbound($path, Request $request): string { + if (!$request->query->has('effects')) { + return $path; + } + + // Public files: /sites/default/files/styles/dynamic/... + $wrapper = $this->streamWrapperManager->getViaScheme('public'); + Assert::isInstanceOf($wrapper, LocalStream::class); + $directory_path = $wrapper->getDirectoryPath(); + $public_prefix = '/' . $directory_path . '/styles/dynamic/'; + if (\str_starts_with($path, $public_prefix)) { + return '/' . $directory_path . '/styles/dynamic'; + } + + // Private files: /system/files/styles/dynamic/... + // Rewrite to internal path that won't be caught by PathProcessorFiles. + $private_prefix = '/system/files/styles/dynamic/'; + if (\str_starts_with($path, $private_prefix)) { + return '/app-image/dynamic-private'; + } + + return $path; + } + +} diff --git a/app/modules/image/src/Routing/RouteProvider.php b/app/modules/image/src/Routing/RouteProvider.php new file mode 100644 index 000000000..d151634d9 --- /dev/null +++ b/app/modules/image/src/Routing/RouteProvider.php @@ -0,0 +1,56 @@ +streamWrapperManager->getViaScheme('public'); + Assert::isInstanceOf($wrapper, LocalStream::class); + $routes->add('app_image.dynamic_image_style', new Route( + path: '/' . $wrapper->getDirectoryPath() . '/styles/dynamic', + defaults: [ + '_controller' => DynamicImageStyleController::class, + '_disable_route_normalizer' => TRUE, + ], + requirements: [ + '_access' => 'TRUE', + ], + options: [ + 'no_cache' => TRUE, + ], + )); + + $routes->add('app_image.dynamic_image_style_private', new Route( + path: '/app-image/dynamic-private', + defaults: [ + '_controller' => DynamicImageStyleController::class, + '_disable_route_normalizer' => TRUE, + ], + requirements: [ + '_access' => 'TRUE', + ], + options: [ + 'no_cache' => TRUE, + ], + )); + + return $routes; + } + +} diff --git a/app/modules/image/src/Twig/DynamicImageStyleExtension.php b/app/modules/image/src/Twig/DynamicImageStyleExtension.php new file mode 100644 index 000000000..9cb525c6f --- /dev/null +++ b/app/modules/image/src/Twig/DynamicImageStyleExtension.php @@ -0,0 +1,124 @@ +imageScaleCrop(...)), + new TwigFilter('image_scale', $this->imageScale(...)), + new TwigFilter('image_convert', $this->imageConvert(...)), + new TwigFilter('image_set_canvas', $this->imageSetCanvas(...)), + new TwigFilter('dynamic_image_style', $this->dynamicImageStyle(...)), + new TwigFilter('dynamic_image_dimensions', $this->dynamicImageDimensions(...)), + new TwigFilter('image_dimensions', $this->imageDimensions(...)), + ]; + } + + public function imageScaleCrop(string|DynamicImageStyleBuilder $input, int $width, int $height): DynamicImageStyleBuilder { + return $this->ensureBuilder($input)->effect('image_scale_and_crop', ['width' => $width, 'height' => $height]); + } + + public function imageScale(string|DynamicImageStyleBuilder $input, ?int $width = NULL, ?int $height = NULL): DynamicImageStyleBuilder { + $data = \array_filter(['width' => $width, 'height' => $height], static fn ($v): bool => $v !== NULL); + return $this->ensureBuilder($input)->effect('image_scale', $data); + } + + public function imageConvert(string|DynamicImageStyleBuilder $input, string $extension): DynamicImageStyleBuilder { + return $this->ensureBuilder($input)->effect('image_convert', ['extension' => $extension]); + } + + public function imageSetCanvas(string|DynamicImageStyleBuilder $input, array $data): DynamicImageStyleBuilder { + $canvas_defaults = [ + 'canvas_size' => 'exact', + 'canvas_color' => '', + 'exact' => [ + 'width' => 0, + 'height' => 0, + 'placement' => 'center-center', + 'x_offset' => 0, + 'y_offset' => 0, + ], + ]; + $data = \array_replace_recursive($canvas_defaults, $data); + return $this->ensureBuilder($input)->effect('image_effects_set_canvas', $data); + } + + /** + * @param list}> $effects + */ + public function dynamicImageStyle(string|DynamicImageStyleBuilder $input, array $effects = []): DynamicImageStyleBuilder { + $builder = $this->ensureBuilder($input); + foreach ($effects as [$id, $data]) { + $builder = $builder->effect($id, $data); + } + return $builder; + } + + /** + * @return array{width: ?int, height: ?int} + */ + public function dynamicImageDimensions(DynamicImageStyleBuilder $builder): array { + $uri = $builder->getUri(); + if ($uri === NULL) { + return ['width' => NULL, 'height' => NULL]; + } + $image = $this->imageFactory->get($uri); + if (!$image->isValid()) { + return ['width' => NULL, 'height' => NULL]; + } + $dimensions = ['width' => $image->getWidth(), 'height' => $image->getHeight()]; + $image_style = $this->dynamicImageStyle->createImageStyle($builder->getEffects()); + $image_style->transformDimensions($dimensions, $uri); + // @phpstan-ignore return.type (transformDimensions passes by reference, losing shape info) + return $dimensions; + } + + /** + * @return array{width: ?int, height: ?int} + */ + public function imageDimensions(string $uri, ?string $style_name = NULL): array { + $dimensions = ['width' => NULL, 'height' => NULL]; + $image = $this->imageFactory->get($uri); + if (!$image->isValid()) { + return $dimensions; + } + $dimensions['width'] = $image->getWidth(); + $dimensions['height'] = $image->getHeight(); + if ($style_name) { + $style = $this->entityTypeManager->getStorage('image_style')->load($style_name); + if ($style instanceof ImageStyleInterface) { + $style->transformDimensions($dimensions, $uri); + } + } + // @phpstan-ignore return.type (transformDimensions passes by reference, losing shape info) + return $dimensions; + } + + private function ensureBuilder(string|DynamicImageStyleBuilder $input): DynamicImageStyleBuilder { + if ($input instanceof DynamicImageStyleBuilder) { + return $input; + } + return new DynamicImageStyleBuilder($this->dynamicImageStyle, uri: $input); + } + +} diff --git a/app/modules/main/src/AppMainServiceProvider.php b/app/modules/main/src/AppMainServiceProvider.php index 292192fef..77ebd17df 100644 --- a/app/modules/main/src/AppMainServiceProvider.php +++ b/app/modules/main/src/AppMainServiceProvider.php @@ -4,6 +4,7 @@ namespace Drupal\app_main; +use Drupal\app_main\CompilerPass\StubsCompilerPass; use Drupal\app_main\Llms\HomeLlmsSubscriber; use Drupal\app_main\Navigation\SiteMap\MainMenuSiteMap; use Drupal\app_main\Navigation\Toolbar\ContentEditingToolbarLinksBuilder; @@ -20,6 +21,7 @@ #[\Override] public function register(ContainerBuilder $container): void { + $container->addCompilerPass(new StubsCompilerPass(), priority: -100); $autowire = static fn (string $class) => $container ->autowire($class) ->setPublic(TRUE) diff --git a/app/modules/main/src/CompilerPass/StubsCompilerPass.php b/app/modules/main/src/CompilerPass/StubsCompilerPass.php new file mode 100644 index 000000000..a722b4880 --- /dev/null +++ b/app/modules/main/src/CompilerPass/StubsCompilerPass.php @@ -0,0 +1,34 @@ +stubService($container, DynamicImageStyle::class); + } + + /** + * @param class-string $class + */ + private function stubService(ContainerBuilder $container, string $class): void { + if ($container->hasDefinition($class) || $container->hasAlias($class)) { + return; + } + + if (!$container->hasDefinition(Stub::class)) { + $container->autowire(Stub::class, Stub::class); + } + + $container->setAlias($class, Stub::class); + } + +} diff --git a/app/modules/platform/src/AppPlatformServiceProvider.php b/app/modules/platform/src/AppPlatformServiceProvider.php index d5f83922f..fb8619da3 100644 --- a/app/modules/platform/src/AppPlatformServiceProvider.php +++ b/app/modules/platform/src/AppPlatformServiceProvider.php @@ -21,7 +21,6 @@ use Drupal\app_platform\Llms\LlmsRenderer; use Drupal\app_platform\Llms\Middleware\LlmsRequestLogger; use Drupal\app_platform\Llms\PathProcessor\LlmsFormatPathProcessor; -use Drupal\app_platform\Markup\Twig\Extension\ImageDimensions; use Drupal\app_platform\Pager\Controller\PagerAwareTitleResolver; use Drupal\app_platform\Pager\EventSubscriber\PagerRedirect; use Drupal\app_platform\Pager\PathProcessor\PagerPathProcessor; @@ -55,10 +54,6 @@ public function register(ContainerBuilder $container): void { $autowire(LanguageAwareSettingsRoutes::class) ->addTag('event_subscriber'); - // Markup. - $autowire(ImageDimensions::class) - ->addTag('twig.extension'); - // SiteMap. $autowire(SiteMapManager::class); $container->setAlias(SiteMapBuilder::class, SiteMapManager::class); diff --git a/app/modules/platform/src/Markup/Twig/Extension/ImageDimensions.php b/app/modules/platform/src/Markup/Twig/Extension/ImageDimensions.php deleted file mode 100644 index 853ce6abb..000000000 --- a/app/modules/platform/src/Markup/Twig/Extension/ImageDimensions.php +++ /dev/null @@ -1,62 +0,0 @@ -getImageDimensions(...)), - ]; - } - - public function getImageDimensions(string $uri, ?string $style_name = NULL): array { - $dimensions = [ - 'width' => NULL, - 'height' => NULL, - ]; - - $image = $this->imageFactory->get($uri); - - if (!$image->isValid()) { - return $dimensions; - } - - $dimensions['width'] = $image->getWidth(); - $dimensions['height'] = $image->getHeight(); - - if ($style_name) { - $this->processImageStyle($uri, $style_name, $dimensions); - } - - return $dimensions; - } - - private function processImageStyle(string $uri, string $style_name, array &$dimensions): void { - $style = $this - ->entityTypeManager - ->getStorage('image_style') - ->load($style_name); - - if (!$style instanceof ImageStyleInterface) { - return; - } - - $style->transformDimensions($dimensions, $uri); - } - -} diff --git a/app/themes/laszlo/components/content/about/about.twig b/app/themes/laszlo/components/content/about/about.twig index 97d031f9f..90933f248 100644 --- a/app/themes/laszlo/components/content/about/about.twig +++ b/app/themes/laszlo/components/content/about/about.twig @@ -14,7 +14,7 @@ attributes: create_attribute({ class: ['about__photo'], }), - src: photo_uri|image_style('200x200_contain'), + src: photo_uri|image_scale(200, 200)|image_set_canvas({exact: {width: 200, height: 200}}), width: 200, height: 200, alt: "Author's photo"|t, diff --git a/app/themes/laszlo/components/content/article-card/article-card.twig b/app/themes/laszlo/components/content/article-card/article-card.twig index 366b37dbe..6b30150fd 100644 --- a/app/themes/laszlo/components/content/article-card/article-card.twig +++ b/app/themes/laszlo/components/content/article-card/article-card.twig @@ -3,7 +3,7 @@ attributes: create_attribute({ class: ['article-card__poster'], }), - src: poster_uri|image_style('150x200'), + src: poster_uri|image_scale_crop(150, 200), width: 150, height: 200, loading: 'eager', @@ -12,8 +12,8 @@ '150px', ], srcset: { - '150w': poster_uri|image_style('150x200'), - '300w': poster_uri|image_style('300x400'), + '150w': poster_uri|image_scale_crop(150, 200), + '300w': poster_uri|image_scale_crop(300, 400), } } only %} diff --git a/app/themes/laszlo/components/content/article-preview/article-preview.twig b/app/themes/laszlo/components/content/article-preview/article-preview.twig index 31bdb9209..fdaf89cb3 100644 --- a/app/themes/laszlo/components/content/article-preview/article-preview.twig +++ b/app/themes/laszlo/components/content/article-preview/article-preview.twig @@ -3,7 +3,7 @@ attributes: create_attribute({ class: ['article-preview__poster'], }), - src: poster_uri|image_style('200x264'), + src: poster_uri|image_scale_crop(200, 264), width: 200, height: 264, alt: title, @@ -16,11 +16,11 @@ '182px', ], srcset: { - '200w': poster_uri|image_style('200x264'), - '300w': poster_uri|image_style('300x400'), - '400w': poster_uri|image_style('400x528'), - '600w': poster_uri|image_style('600x800'), - '800w': poster_uri|image_style('800x1064'), + '200w': poster_uri|image_scale_crop(200, 264), + '300w': poster_uri|image_scale_crop(300, 400), + '400w': poster_uri|image_scale_crop(400, 528), + '600w': poster_uri|image_scale_crop(600, 800), + '800w': poster_uri|image_scale_crop(800, 1064), } } only %} diff --git a/app/themes/laszlo/components/content/article/article.twig b/app/themes/laszlo/components/content/article/article.twig index 834dd2213..2444b4bf0 100644 --- a/app/themes/laszlo/components/content/article/article.twig +++ b/app/themes/laszlo/components/content/article/article.twig @@ -1,11 +1,11 @@
-
+
{% include 'laszlo:image' with { attributes: create_attribute({ class: ['article__poster'], }), - src: poster_uri|image_style('200x264'), + src: poster_uri|image_scale_crop(200, 264), width: 200, height: 264, alt: title, @@ -14,8 +14,8 @@ '200px', ], srcset: { - '200w': poster_uri|image_style('200x264'), - '400w': poster_uri|image_style('400x528'), + '200w': poster_uri|image_scale_crop(200, 264), + '400w': poster_uri|image_scale_crop(400, 528), } } only %} diff --git a/app/themes/laszlo/components/content/home-cards/home-cards.twig b/app/themes/laszlo/components/content/home-cards/home-cards.twig index e51cfddee..360b2ca45 100644 --- a/app/themes/laszlo/components/content/home-cards/home-cards.twig +++ b/app/themes/laszlo/components/content/home-cards/home-cards.twig @@ -16,7 +16,7 @@ attributes: create_attribute({ class: ['home-cards__background'], }), - src: background_uri|image_style('400x400'), + src: background_uri|image_scale_crop(400, 400), width: 400, height: 400, loading: 'eager', @@ -30,10 +30,10 @@ '374px', ], srcset: { - '400w': background_uri|image_style('400x400'), - '600w': background_uri|image_style('600x600'), - '800w': background_uri|image_style('800x800'), - '1200w': background_uri|image_style('1200x1200'), + '400w': background_uri|image_scale_crop(400, 400), + '600w': background_uri|image_scale_crop(600, 600), + '800w': background_uri|image_scale_crop(800, 800), + '1200w': background_uri|image_scale_crop(1200, 1200), } } only %} {% endif %} diff --git a/app/themes/laszlo/composer.json b/app/themes/laszlo/composer.json index b6f06e491..f094427a1 100644 --- a/app/themes/laszlo/composer.json +++ b/app/themes/laszlo/composer.json @@ -8,6 +8,7 @@ } }, "require": { + "app/image": "*", "app-asset/lora": "^35" } } diff --git a/app/themes/laszlo/laszlo.info.yml b/app/themes/laszlo/laszlo.info.yml index 542c98abb..3bf69d69d 100644 --- a/app/themes/laszlo/laszlo.info.yml +++ b/app/themes/laszlo/laszlo.info.yml @@ -7,6 +7,8 @@ description: >- type: theme base theme: false core_version_requirement: ^11 +dependencies: + - app_image:app_image regions: content: Content libraries: diff --git a/app/themes/laszlo/src/Hook/Theme/PreprocessNodeBlogEntry.php b/app/themes/laszlo/src/Hook/Theme/PreprocessNodeBlogEntry.php index b9e4a3f6f..58852df36 100644 --- a/app/themes/laszlo/src/Hook/Theme/PreprocessNodeBlogEntry.php +++ b/app/themes/laszlo/src/Hook/Theme/PreprocessNodeBlogEntry.php @@ -5,7 +5,6 @@ namespace Drupal\laszlo\Hook\Theme; use Drupal\app_blog\Node\ArticleBundle; -use Webmozart\Assert\Assert; final readonly class PreprocessNodeBlogEntry { @@ -45,7 +44,7 @@ private function addCompatibility(ArticleBundle $article, array &$variables): vo $compatibility = []; foreach ($article->get('field_compatibility') as $item) { $name = $item->get('name')->getValue(); - assert(\is_string($name)); + \assert(\is_string($name)); $compatibility[] = [ 'name' => $name, 'label' => self::SOFTWARE_LABELS[$name] ?? \ucfirst($name), diff --git a/app/themes/laszlo/templates/theme/app-blog-lightbox-responsive-image.html.twig b/app/themes/laszlo/templates/theme/app-blog-lightbox-responsive-image.html.twig index d1945e571..4fa0e27ae 100644 --- a/app/themes/laszlo/templates/theme/app-blog-lightbox-responsive-image.html.twig +++ b/app/themes/laszlo/templates/theme/app-blog-lightbox-responsive-image.html.twig @@ -1,16 +1,16 @@ -{% set dimensions = uri|image_dimensions('big_image') %} +{% set dimensions = uri|image_scale(2000)|dynamic_image_dimensions %} {% embed 'laszlo:lightbox' with { uri, - url: uri|image_style('big_image'), + url: uri|image_scale(2000), lightbox_width: dimensions.width, lightbox_height: dimensions.height, responsive_image, alt, } only %} {% block thumbnail %} - {% set dimensions = uri|image_dimensions('400xy') %} + {% set dimensions = uri|image_scale(400)|dynamic_image_dimensions %} {% include 'laszlo:image' with { - src: uri|image_style('400xy'), + src: uri|image_scale(400), width: dimensions.width, height: dimensions.height, alt, @@ -23,12 +23,12 @@ '944px', ], srcset: { - '400w': uri|image_style('400xy'), - '600w': uri|image_style('600xy'), - '800w': uri|image_style('800xy'), - '1000w': uri|image_style('1000xy'), - '1200w': uri|image_style('1200xy'), + '400w': uri|image_scale(400), + '600w': uri|image_scale(600), + '800w': uri|image_scale(800), + '1000w': uri|image_scale(1000), + '1200w': uri|image_scale(1200), } } only %} {% endblock %} -{% endembed %} \ No newline at end of file +{% endembed %} diff --git a/composer.lock b/composer.lock index e9eece497..b2703c92c 100644 --- a/composer.lock +++ b/composer.lock @@ -77,11 +77,12 @@ "dist": { "type": "path", "url": "app/modules/blog", - "reference": "64b3ca20021c767605d0787f979d99f36f961348" + "reference": "35080e9e7fbbd2cdbd45589c78da3777b70bae93" }, "require": { "app-asset/hljs": "^11.9", "app/contract": "^1.0", + "app/image": "*", "app/platform": "^1.0", "drupal/photoswipe": "^5.0", "ext-dom": "*", @@ -160,16 +161,34 @@ "relative": true } }, + { + "name": "app/image", + "version": "1.0.0", + "dist": { + "type": "path", + "url": "app/modules/image", + "reference": "741f80328940f8f794040e6e346cfbf8ea3ae378" + }, + "require": { + "php": "^8.4", + "webmozart/assert": "^2.1" + }, + "type": "drupal-custom-module", + "transport-options": { + "relative": true + } + }, { "name": "app/laszlo", "version": "1.0.0-dev", "dist": { "type": "path", "url": "app/themes/laszlo", - "reference": "853797f0a2c2244ea69e204d4f8c085e7518d272" + "reference": "94e1ee5c18168bf94c65301452c6c362a1b960ac" }, "require": { - "app-asset/lora": "^35" + "app-asset/lora": "^35", + "app/image": "*" }, "type": "drupal-custom-theme", "autoload": { @@ -9622,6 +9641,68 @@ "source": "https://github.com/VIPnytt/SitemapParser/tree/v1.3.1" }, "time": "2025-10-10T13:27:30+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.6" + }, + "time": "2026-02-27T10:28:38+00:00" } ], "packages-dev": [ @@ -16213,68 +16294,6 @@ "source": "https://github.com/webflo/drupal-finder/tree/1.3.1" }, "time": "2024-06-28T13:45:36+00:00" - }, - { - "name": "webmozart/assert", - "version": "2.1.6", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-filter": "*", - "php": "^8.2" - }, - "suggest": { - "ext-intl": "", - "ext-simplexml": "", - "ext-spl": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-feature/2-0": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - }, - { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.6" - }, - "time": "2026-02-27T10:28:38+00:00" } ], "aliases": [], diff --git a/config/sync/core.entity_view_display.media.image.default.yml b/config/sync/core.entity_view_display.media.image.default.yml index 575f5ddb5..38c57b3eb 100644 --- a/config/sync/core.entity_view_display.media.image.default.yml +++ b/config/sync/core.entity_view_display.media.image.default.yml @@ -4,7 +4,7 @@ status: true dependencies: config: - field.field.media.image.field_media_image - - image.style.paragraph_image_image_xs + - image.style.medium - media.type.image module: - image @@ -20,7 +20,7 @@ content: label: visually_hidden settings: image_link: file - image_style: paragraph_image_image_xs + image_style: medium image_loading: attribute: lazy third_party_settings: { } diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index e391616d1..350edb603 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -6,6 +6,7 @@ module: app_comment: 0 app_contract: 0 app_file: 0 + app_image: 0 app_main: 0 app_media: 0 app_platform: 0 diff --git a/config/sync/image.style.1000xy.yml b/config/sync/image.style.1000xy.yml deleted file mode 100644 index 5843968d1..000000000 --- a/config/sync/image.style.1000xy.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 12bfe456-8f5d-4263-8450-b2bf34045dc8 -langcode: ru -status: true -dependencies: { } -name: 1000xy -label: 1000×Y -effects: - 14064a25-1cfa-43d2-8e69-1dd3f7fd6054: - uuid: 14064a25-1cfa-43d2-8e69-1dd3f7fd6054 - id: image_convert - weight: 1 - data: - extension: webp - 1067b5f9-ece1-4397-94a6-90f91fee07d3: - uuid: 1067b5f9-ece1-4397-94a6-90f91fee07d3 - id: image_scale - weight: 2 - data: - width: 1000 - height: null - upscale: false diff --git a/config/sync/image.style.1200x1200.yml b/config/sync/image.style.1200x1200.yml deleted file mode 100644 index 678707096..000000000 --- a/config/sync/image.style.1200x1200.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: d643dae9-098b-4a22-b0e0-08b71328e0d3 -langcode: ru -status: true -dependencies: { } -name: 1200x1200 -label: 1200×1200 -effects: - 589c097c-8942-4d1c-886c-cb9510a70486: - uuid: 589c097c-8942-4d1c-886c-cb9510a70486 - id: image_convert - weight: 1 - data: - extension: webp - 5fa6456a-7c74-412f-9bf6-83ea47f057e4: - uuid: 5fa6456a-7c74-412f-9bf6-83ea47f057e4 - id: image_scale_and_crop - weight: 2 - data: - width: 1200 - height: 1200 - anchor: center-center diff --git a/config/sync/image.style.1200xy.yml b/config/sync/image.style.1200xy.yml deleted file mode 100644 index 1c7a8b343..000000000 --- a/config/sync/image.style.1200xy.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: ddbbee18-bb86-482f-bbef-04aa88db2568 -langcode: ru -status: true -dependencies: { } -name: 1200xy -label: 1200×Y -effects: - b3e3c8d1-b652-4bee-9b7d-cbbe432a1870: - uuid: b3e3c8d1-b652-4bee-9b7d-cbbe432a1870 - id: image_convert - weight: 1 - data: - extension: webp - cbeb1f88-0566-4486-afb4-5d3708ea577c: - uuid: cbeb1f88-0566-4486-afb4-5d3708ea577c - id: image_scale - weight: 2 - data: - width: 1200 - height: null - upscale: false diff --git a/config/sync/image.style.150x200.yml b/config/sync/image.style.150x200.yml deleted file mode 100644 index e03e5a433..000000000 --- a/config/sync/image.style.150x200.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 4d65b2d2-eb9b-4bbb-a4cc-907936a4388d -langcode: ru -status: true -dependencies: { } -name: 150x200 -label: 150×200 -effects: - c4829bc1-99a7-41bc-8f8a-68c26859367a: - uuid: c4829bc1-99a7-41bc-8f8a-68c26859367a - id: image_scale_and_crop - weight: -9 - data: - width: 150 - height: 200 - anchor: center-center - b7122425-9e60-40b0-b37c-bc17751169b6: - uuid: b7122425-9e60-40b0-b37c-bc17751169b6 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.164x218.yml b/config/sync/image.style.164x218.yml deleted file mode 100644 index 1c225c953..000000000 --- a/config/sync/image.style.164x218.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 0cdcc62a-b2db-4452-b59a-38f8ebcd4b09 -langcode: ru -status: true -dependencies: { } -name: 164x218 -label: 164×218 -effects: - 207e6a19-5f40-429d-b66c-bc12518a21ab: - uuid: 207e6a19-5f40-429d-b66c-bc12518a21ab - id: image_convert - weight: 1 - data: - extension: webp - 174d5c9a-fc5d-4a88-a0a1-fcc0df7b1e9c: - uuid: 174d5c9a-fc5d-4a88-a0a1-fcc0df7b1e9c - id: image_scale_and_crop - weight: 2 - data: - width: 164 - height: 218 - anchor: center-center diff --git a/config/sync/image.style.200x200_contain.yml b/config/sync/image.style.200x200_contain.yml deleted file mode 100644 index f21cd22c2..000000000 --- a/config/sync/image.style.200x200_contain.yml +++ /dev/null @@ -1,41 +0,0 @@ -uuid: b4bd4de3-34dc-42e7-9006-c90b45eae0eb -langcode: ru -status: true -dependencies: - module: - - image_effects -name: 200x200_contain -label: '200×200 (contain)' -effects: - 7fa9dfe5-9d49-430f-b809-bb7548d44015: - uuid: 7fa9dfe5-9d49-430f-b809-bb7548d44015 - id: image_scale - weight: -9 - data: - width: 200 - height: 200 - upscale: false - 37bd764b-2a23-40b8-940c-a51cddd1c001: - uuid: 37bd764b-2a23-40b8-940c-a51cddd1c001 - id: image_effects_set_canvas - weight: -8 - data: - canvas_size: exact - canvas_color: '' - exact: - width: '200' - height: '200' - placement: center-center - x_offset: 0 - y_offset: 0 - relative: - left: 0 - right: 0 - top: 0 - bottom: 0 - f08deb9f-f9a9-4bc1-bbde-368a2d6192ae: - uuid: f08deb9f-f9a9-4bc1-bbde-368a2d6192ae - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.200x264.yml b/config/sync/image.style.200x264.yml deleted file mode 100644 index 099152943..000000000 --- a/config/sync/image.style.200x264.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 5d91b15a-0ff1-4099-95ab-d973c0cd9e0e -langcode: ru -status: true -dependencies: { } -name: 200x264 -label: 200×264 -effects: - 500b3d36-e876-4f6d-8068-1c2c49ecc837: - uuid: 500b3d36-e876-4f6d-8068-1c2c49ecc837 - id: image_convert - weight: 1 - data: - extension: webp - e7cd4ece-2554-43eb-907b-f3935148fc03: - uuid: e7cd4ece-2554-43eb-907b-f3935148fc03 - id: image_scale_and_crop - weight: 2 - data: - width: 200 - height: 264 - anchor: center-center diff --git a/config/sync/image.style.300x400.yml b/config/sync/image.style.300x400.yml deleted file mode 100644 index c41d4ccf9..000000000 --- a/config/sync/image.style.300x400.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 58cd1fa6-2807-47bb-8032-ae0a92a48499 -langcode: ru -status: true -dependencies: { } -name: 300x400 -label: 300×400 -effects: - 1b903ab2-ca37-473f-b4a2-ae03970c4a9f: - uuid: 1b903ab2-ca37-473f-b4a2-ae03970c4a9f - id: image_convert - weight: 1 - data: - extension: webp - 933082a2-573a-41ad-a503-01c5aa7595c7: - uuid: 933082a2-573a-41ad-a503-01c5aa7595c7 - id: image_scale_and_crop - weight: 2 - data: - width: 300 - height: 400 - anchor: center-center diff --git a/config/sync/image.style.320x424.yml b/config/sync/image.style.320x424.yml deleted file mode 100644 index 185b19bde..000000000 --- a/config/sync/image.style.320x424.yml +++ /dev/null @@ -1,7 +0,0 @@ -uuid: 93166b81-90a2-4c5c-b267-54945b0fb9e7 -langcode: ru -status: true -dependencies: { } -name: 320x424 -label: 320×424 -effects: { } diff --git a/config/sync/image.style.400x400.yml b/config/sync/image.style.400x400.yml deleted file mode 100644 index 02f403deb..000000000 --- a/config/sync/image.style.400x400.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: f9f53f9c-0bc9-496c-9095-3469dd9fd5f4 -langcode: ru -status: true -dependencies: { } -name: 400x400 -label: 400×400 -effects: - 57421419-cf7c-4820-93b6-76ebdf3b8083: - uuid: 57421419-cf7c-4820-93b6-76ebdf3b8083 - id: image_convert - weight: 1 - data: - extension: webp - 23883509-a411-4646-8640-e02590e8fef1: - uuid: 23883509-a411-4646-8640-e02590e8fef1 - id: image_scale_and_crop - weight: 2 - data: - width: 400 - height: 400 - anchor: center-center diff --git a/config/sync/image.style.400x528.yml b/config/sync/image.style.400x528.yml deleted file mode 100644 index 95b72dcc8..000000000 --- a/config/sync/image.style.400x528.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: f3117df0-7f30-4bf5-a2dd-4c08e27f0d66 -langcode: ru -status: true -dependencies: { } -name: 400x528 -label: 400×528 -effects: - cdf34f2a-addc-4cf0-90e9-e28d6f0f8ea6: - uuid: cdf34f2a-addc-4cf0-90e9-e28d6f0f8ea6 - id: image_convert - weight: 1 - data: - extension: webp - 3d13417f-17ad-41d9-9054-e8a3eaa82301: - uuid: 3d13417f-17ad-41d9-9054-e8a3eaa82301 - id: image_scale_and_crop - weight: 2 - data: - width: 400 - height: 528 - anchor: center-center diff --git a/config/sync/image.style.400xy.yml b/config/sync/image.style.400xy.yml deleted file mode 100644 index 320765887..000000000 --- a/config/sync/image.style.400xy.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: d8e1cca5-1ebb-4412-88c3-8502fff61265 -langcode: ru -status: true -dependencies: { } -name: 400xy -label: 400×Y -effects: - cc10b044-fc85-4c21-8942-acac21f4b45d: - uuid: cc10b044-fc85-4c21-8942-acac21f4b45d - id: image_convert - weight: 1 - data: - extension: webp - e092d7b6-5098-42f4-9a26-ca3c61cc1730: - uuid: e092d7b6-5098-42f4-9a26-ca3c61cc1730 - id: image_scale - weight: 2 - data: - width: 400 - height: null - upscale: false diff --git a/config/sync/image.style.600x600.yml b/config/sync/image.style.600x600.yml deleted file mode 100644 index 07b31e2ee..000000000 --- a/config/sync/image.style.600x600.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 65289967-12e7-4759-a76d-cfa44c6fd0b6 -langcode: ru -status: true -dependencies: { } -name: 600x600 -label: 600×600 -effects: - 996d700a-95f2-4613-a093-2f4c8a9e3e2a: - uuid: 996d700a-95f2-4613-a093-2f4c8a9e3e2a - id: image_convert - weight: 1 - data: - extension: webp - b6ba2006-3ee0-47c4-b312-a56c4568ca28: - uuid: b6ba2006-3ee0-47c4-b312-a56c4568ca28 - id: image_scale_and_crop - weight: 2 - data: - width: 600 - height: 600 - anchor: center-center diff --git a/config/sync/image.style.600x800.yml b/config/sync/image.style.600x800.yml deleted file mode 100644 index d99fa7550..000000000 --- a/config/sync/image.style.600x800.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: b70059d5-4964-4aa5-b1dc-a42b4d48e341 -langcode: ru -status: true -dependencies: { } -name: 600x800 -label: 600×800 -effects: - a75abb7c-4e70-4469-b167-53350d1ed934: - uuid: a75abb7c-4e70-4469-b167-53350d1ed934 - id: image_convert - weight: 1 - data: - extension: webp - 491d87f0-0379-4644-b366-708d7e6f704d: - uuid: 491d87f0-0379-4644-b366-708d7e6f704d - id: image_scale_and_crop - weight: 2 - data: - width: 600 - height: 800 - anchor: center-center diff --git a/config/sync/image.style.600xy.yml b/config/sync/image.style.600xy.yml deleted file mode 100644 index 1ae1def34..000000000 --- a/config/sync/image.style.600xy.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 2c1a67fc-557e-4a2e-a553-2b92e490fa34 -langcode: ru -status: true -dependencies: { } -name: 600xy -label: 600×Y -effects: - 99998194-aa9c-4c67-9606-8dd456c26261: - uuid: 99998194-aa9c-4c67-9606-8dd456c26261 - id: image_convert - weight: 1 - data: - extension: webp - bce64a8e-79f4-4a6a-bfaf-32b2a9176aea: - uuid: bce64a8e-79f4-4a6a-bfaf-32b2a9176aea - id: image_scale - weight: 2 - data: - width: 600 - height: null - upscale: false diff --git a/config/sync/image.style.800x1064.yml b/config/sync/image.style.800x1064.yml deleted file mode 100644 index e2b3c2fce..000000000 --- a/config/sync/image.style.800x1064.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 779f618f-d39c-4639-b7ae-fb3e6d43ad77 -langcode: ru -status: true -dependencies: { } -name: 800x1064 -label: 800×1064 -effects: - c1ca151e-e14a-49ad-a815-98c8da8dd644: - uuid: c1ca151e-e14a-49ad-a815-98c8da8dd644 - id: image_convert - weight: 1 - data: - extension: webp - 756b902b-9f97-4a58-8da3-c825f22110ef: - uuid: 756b902b-9f97-4a58-8da3-c825f22110ef - id: image_scale_and_crop - weight: 2 - data: - width: 800 - height: 1064 - anchor: center-center diff --git a/config/sync/image.style.800x800.yml b/config/sync/image.style.800x800.yml deleted file mode 100644 index f541bc4fd..000000000 --- a/config/sync/image.style.800x800.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: fbbfef05-bf54-486d-985a-5d5a218435f7 -langcode: ru -status: true -dependencies: { } -name: 800x800 -label: 800×800 -effects: - c7efcc39-5b96-4b05-95d7-143de9b96877: - uuid: c7efcc39-5b96-4b05-95d7-143de9b96877 - id: image_convert - weight: 1 - data: - extension: webp - e23447bc-fa84-4564-8127-bb83661015f0: - uuid: e23447bc-fa84-4564-8127-bb83661015f0 - id: image_scale_and_crop - weight: 2 - data: - width: 800 - height: 800 - anchor: center-center diff --git a/config/sync/image.style.800xy.yml b/config/sync/image.style.800xy.yml deleted file mode 100644 index f1f63cc3e..000000000 --- a/config/sync/image.style.800xy.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: ba56a97c-6f00-44c2-9459-4b2c9200cec6 -langcode: ru -status: true -dependencies: { } -name: 800xy -label: 800×Y -effects: - 3acc99ab-209c-4512-88e9-06d238dac446: - uuid: 3acc99ab-209c-4512-88e9-06d238dac446 - id: image_convert - weight: 1 - data: - extension: webp - d47fb4a8-f028-4cf5-8100-5e7fbe70a299: - uuid: d47fb4a8-f028-4cf5-8100-5e7fbe70a299 - id: image_scale - weight: 2 - data: - width: 800 - height: null - upscale: false diff --git a/config/sync/image.style.960xy.yml b/config/sync/image.style.960xy.yml deleted file mode 100644 index 476ea5c27..000000000 --- a/config/sync/image.style.960xy.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: e2dc8433-13e1-49b5-9067-60f063789f55 -langcode: ru -status: true -dependencies: { } -name: 960xy -label: 960×Y -effects: - 959e6439-88b8-49d8-b4bf-7ff04aa3cdea: - uuid: 959e6439-88b8-49d8-b4bf-7ff04aa3cdea - id: image_scale - weight: -9 - data: - width: 960 - height: null - upscale: false - 3d934722-1d35-48ba-abb5-c7a36219811d: - uuid: 3d934722-1d35-48ba-abb5-c7a36219811d - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.about_photo_lg.yml b/config/sync/image.style.about_photo_lg.yml deleted file mode 100644 index 820122665..000000000 --- a/config/sync/image.style.about_photo_lg.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: da47b076-a3f2-4657-b3c2-195309ac2120 -langcode: ru -status: true -dependencies: { } -name: about_photo_lg -label: 'About photo: LG' -effects: - 8ac76f53-76f2-468a-a34a-e4858456b869: - uuid: 8ac76f53-76f2-468a-a34a-e4858456b869 - id: image_scale_and_crop - weight: -9 - data: - width: 180 - height: 180 - anchor: center-center - 5adb8214-ba3f-4157-b86a-9950d4bfa25e: - uuid: 5adb8214-ba3f-4157-b86a-9950d4bfa25e - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.about_photo_md.yml b/config/sync/image.style.about_photo_md.yml deleted file mode 100644 index 5259e3491..000000000 --- a/config/sync/image.style.about_photo_md.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: cc680476-8451-4fa7-885c-2377b8911f2d -langcode: ru -status: true -dependencies: { } -name: about_photo_md -label: 'About photo: MD' -effects: - 2070d838-2e5f-4e90-bea1-eb2e65201fda: - uuid: 2070d838-2e5f-4e90-bea1-eb2e65201fda - id: image_scale_and_crop - weight: -9 - data: - width: 237 - height: 237 - anchor: center-center - d784a58f-1d45-445d-b70c-19c4263a474e: - uuid: d784a58f-1d45-445d-b70c-19c4263a474e - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.about_photo_sm.yml b/config/sync/image.style.about_photo_sm.yml deleted file mode 100644 index 7aa0fc170..000000000 --- a/config/sync/image.style.about_photo_sm.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 4691362d-655c-474b-b324-8f6469a6aedc -langcode: ru -status: true -dependencies: { } -name: about_photo_sm -label: 'About photo: SM' -effects: - 9425c4f5-6734-4615-b540-9bc142479bd8: - uuid: 9425c4f5-6734-4615-b540-9bc142479bd8 - id: image_scale_and_crop - weight: -9 - data: - width: 170 - height: 170 - anchor: center-center - 9cbefbc7-ee4b-4c93-af03-75f3c182de34: - uuid: 9cbefbc7-ee4b-4c93-af03-75f3c182de34 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.about_photo_xl.yml b/config/sync/image.style.about_photo_xl.yml deleted file mode 100644 index 58b43497a..000000000 --- a/config/sync/image.style.about_photo_xl.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 4aadbea5-c477-4e3c-ae99-4502b3bdb228 -langcode: ru -status: true -dependencies: { } -name: about_photo_xl -label: 'About photo: XL' -effects: - 97593cb3-f178-4904-b5a0-57173da4c4a3: - uuid: 97593cb3-f178-4904-b5a0-57173da4c4a3 - id: image_scale_and_crop - weight: -9 - data: - width: 180 - height: 180 - anchor: center-center - 6a7b3f33-f776-4510-89a1-10d9554beb8d: - uuid: 6a7b3f33-f776-4510-89a1-10d9554beb8d - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.about_photo_xs.yml b/config/sync/image.style.about_photo_xs.yml deleted file mode 100644 index f10dd391d..000000000 --- a/config/sync/image.style.about_photo_xs.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 871070fb-e76c-4e34-93aa-ac40b5f4c369 -langcode: ru -status: true -dependencies: { } -name: about_photo_xs -label: 'About photo: XS' -effects: - 04530ec9-971f-4b05-a1af-357ca4124456: - uuid: 04530ec9-971f-4b05-a1af-357ca4124456 - id: image_scale_and_crop - weight: -9 - data: - width: 200 - height: 200 - anchor: center-center - 69af3033-9ea8-48b3-938a-b56016640a78: - uuid: 69af3033-9ea8-48b3-938a-b56016640a78 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.media_remote_video_thumbnail_lg.yml b/config/sync/image.style.media_remote_video_thumbnail_lg.yml deleted file mode 100644 index 2b8d04637..000000000 --- a/config/sync/image.style.media_remote_video_thumbnail_lg.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 552eceea-bcc3-4a5f-84f2-45f58c6b30a2 -langcode: ru -status: true -dependencies: { } -name: media_remote_video_thumbnail_lg -label: 'Media - Remote Video - Thumbnail: LG' -effects: - 835d78c0-5a9b-48fa-9a7c-8f69ee4c9315: - uuid: 835d78c0-5a9b-48fa-9a7c-8f69ee4c9315 - id: image_scale_and_crop - weight: -9 - data: - width: 608 - height: 342 - anchor: center-center - e087bebc-f9ea-43b6-bcb9-e1cdc33ec6a5: - uuid: e087bebc-f9ea-43b6-bcb9-e1cdc33ec6a5 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.media_remote_video_thumbnail_md.yml b/config/sync/image.style.media_remote_video_thumbnail_md.yml deleted file mode 100644 index 914493d8b..000000000 --- a/config/sync/image.style.media_remote_video_thumbnail_md.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 967468cd-58b8-449f-8b89-9f7d482eb9be -langcode: ru -status: true -dependencies: { } -name: media_remote_video_thumbnail_md -label: 'Media - Remote Video - Thumbnail: MD' -effects: - e3d34dfd-6be8-4580-912d-89f28b8aae96: - uuid: e3d34dfd-6be8-4580-912d-89f28b8aae96 - id: image_scale_and_crop - weight: -9 - data: - width: 736 - height: 414 - anchor: center-center - 266f645f-a578-45a0-801a-0a9b29829327: - uuid: 266f645f-a578-45a0-801a-0a9b29829327 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.media_remote_video_thumbnail_sm.yml b/config/sync/image.style.media_remote_video_thumbnail_sm.yml deleted file mode 100644 index f87549eb8..000000000 --- a/config/sync/image.style.media_remote_video_thumbnail_sm.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: c6dad27e-50a9-484b-b727-31e67828fbf8 -langcode: ru -status: true -dependencies: { } -name: media_remote_video_thumbnail_sm -label: 'Media - Remote Video - Thumbnail: SM' -effects: - dd548fc5-3d5b-44e8-9e55-46e4748156f2: - uuid: dd548fc5-3d5b-44e8-9e55-46e4748156f2 - id: image_scale_and_crop - weight: -9 - data: - width: 671 - height: 377 - anchor: center-center - 21365c47-0429-48d9-9b0c-8c30be31d71d: - uuid: 21365c47-0429-48d9-9b0c-8c30be31d71d - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.media_remote_video_thumbnail_xs.yml b/config/sync/image.style.media_remote_video_thumbnail_xs.yml deleted file mode 100644 index 84e809929..000000000 --- a/config/sync/image.style.media_remote_video_thumbnail_xs.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 899ffa61-f973-4a52-b5c1-268e34db599c -langcode: ru -status: true -dependencies: { } -name: media_remote_video_thumbnail_xs -label: 'Media - Remote Video - Thumbnail: XS' -effects: - 802568df-36bf-4726-9fe3-b842159d09ff: - uuid: 802568df-36bf-4726-9fe3-b842159d09ff - id: image_scale_and_crop - weight: -9 - data: - width: 543 - height: 305 - anchor: center-center - b0a5b960-d716-4b00-9326-289ed67ca04f: - uuid: b0a5b960-d716-4b00-9326-289ed67ca04f - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_blog_entry_teaser_promo_lg.yml b/config/sync/image.style.node_blog_entry_teaser_promo_lg.yml deleted file mode 100644 index 47b9c431a..000000000 --- a/config/sync/image.style.node_blog_entry_teaser_promo_lg.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 23e32d0d-c00b-4448-a35e-fc78873bd94b -langcode: ru -status: true -dependencies: { } -name: node_blog_entry_teaser_promo_lg -label: 'Node - Blog Entry - Teaser - Promo: LG' -effects: - 94035440-155d-4149-986e-56b044b08096: - uuid: 94035440-155d-4149-986e-56b044b08096 - id: image_scale_and_crop - weight: -9 - data: - width: 200 - height: 150 - anchor: center-center - 4ed642b3-58b1-4a30-96fc-3b8401cf17d3: - uuid: 4ed642b3-58b1-4a30-96fc-3b8401cf17d3 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_blog_entry_teaser_promo_md.yml b/config/sync/image.style.node_blog_entry_teaser_promo_md.yml deleted file mode 100644 index a1a89580d..000000000 --- a/config/sync/image.style.node_blog_entry_teaser_promo_md.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 6f6c37dd-649b-4ede-b0cc-a86c2498e0cd -langcode: ru -status: true -dependencies: { } -name: node_blog_entry_teaser_promo_md -label: 'Node - Blog Entry - Teaser - Promo: MD' -effects: - 1677f4a3-1775-436e-b8fe-0e4cc2678302: - uuid: 1677f4a3-1775-436e-b8fe-0e4cc2678302 - id: image_scale_and_crop - weight: -9 - data: - width: 200 - height: 200 - anchor: center-center - d0945fc5-4f70-4b92-b804-ddd7199d57d6: - uuid: d0945fc5-4f70-4b92-b804-ddd7199d57d6 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_blog_entry_teaser_promo_sm.yml b/config/sync/image.style.node_blog_entry_teaser_promo_sm.yml deleted file mode 100644 index 2e0a59bd0..000000000 --- a/config/sync/image.style.node_blog_entry_teaser_promo_sm.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 0f841381-f51c-4c91-9416-e3edd1998fb5 -langcode: ru -status: true -dependencies: { } -name: node_blog_entry_teaser_promo_sm -label: 'Node - Blog Entry - Teaser - Promo: SM' -effects: - e3a5e092-60e2-4daa-9170-79720d1f35da: - uuid: e3a5e092-60e2-4daa-9170-79720d1f35da - id: image_scale_and_crop - weight: -9 - data: - width: 140 - height: 140 - anchor: center-center - 45f5a155-e717-4a5c-82e9-7f84366458b4: - uuid: 45f5a155-e717-4a5c-82e9-7f84366458b4 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_blog_entry_teaser_promo_xs.yml b/config/sync/image.style.node_blog_entry_teaser_promo_xs.yml deleted file mode 100644 index 457e83893..000000000 --- a/config/sync/image.style.node_blog_entry_teaser_promo_xs.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 797d094b-208f-4b31-9c4c-6cb7b603fee3 -langcode: ru -status: true -dependencies: { } -name: node_blog_entry_teaser_promo_xs -label: 'Node - Blog Entry - Teaser - Promo: XS' -effects: - acb8379c-ad84-424e-90c7-e5268b916f34: - uuid: acb8379c-ad84-424e-90c7-e5268b916f34 - id: image_scale_and_crop - weight: -9 - data: - width: 543 - height: 250 - anchor: center-center - 7fe71fa2-b946-49a0-ad48-3c3543ca6b8a: - uuid: 7fe71fa2-b946-49a0-ad48-3c3543ca6b8a - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_portfolio_full_photo_lg.yml b/config/sync/image.style.node_portfolio_full_photo_lg.yml deleted file mode 100644 index 5ef3b736c..000000000 --- a/config/sync/image.style.node_portfolio_full_photo_lg.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: efd59b38-2603-49a3-ad7e-17e3c05315b0 -langcode: ru -status: true -dependencies: { } -name: node_portfolio_full_photo_lg -label: 'Node - Portfolio - Full - Photo: LG' -effects: - 17098d71-b0cf-452e-8f78-74e0552678d7: - uuid: 17098d71-b0cf-452e-8f78-74e0552678d7 - id: image_scale_and_crop - weight: -9 - data: - width: 224 - height: 224 - anchor: center-top - ea0145df-eaa5-44f1-872c-6f7daba9aa55: - uuid: ea0145df-eaa5-44f1-872c-6f7daba9aa55 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_portfolio_full_photo_md.yml b/config/sync/image.style.node_portfolio_full_photo_md.yml deleted file mode 100644 index afbdab247..000000000 --- a/config/sync/image.style.node_portfolio_full_photo_md.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 57130dc3-69a1-48b0-92e5-adcea729ca39 -langcode: ru -status: true -dependencies: { } -name: node_portfolio_full_photo_md -label: 'Node - Portfolio - Full - Photo: MD' -effects: - 4d861af7-de9c-4bf5-a3de-f5453f912579: - uuid: 4d861af7-de9c-4bf5-a3de-f5453f912579 - id: image_scale_and_crop - weight: -9 - data: - width: 287 - height: 287 - anchor: center-top - 53ee3f3c-2b23-4f73-af23-2ffc530df744: - uuid: 53ee3f3c-2b23-4f73-af23-2ffc530df744 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_portfolio_full_photo_sm.yml b/config/sync/image.style.node_portfolio_full_photo_sm.yml deleted file mode 100644 index 3d5300755..000000000 --- a/config/sync/image.style.node_portfolio_full_photo_sm.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 2f7da254-5546-4689-a364-2cbd0d661cd8 -langcode: ru -status: true -dependencies: { } -name: node_portfolio_full_photo_sm -label: 'Node - Portfolio - Full - Photo: SM' -effects: - c0beb1a7-50ca-47fa-b108-96590d88f61a: - uuid: c0beb1a7-50ca-47fa-b108-96590d88f61a - id: image_scale_and_crop - weight: -9 - data: - width: 328 - height: 328 - anchor: center-top - da0f12c8-4da2-4a88-a292-5926908ae0d5: - uuid: da0f12c8-4da2-4a88-a292-5926908ae0d5 - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.node_portfolio_full_photo_xs.yml b/config/sync/image.style.node_portfolio_full_photo_xs.yml deleted file mode 100644 index f78986ed7..000000000 --- a/config/sync/image.style.node_portfolio_full_photo_xs.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 2bdf5a14-a1d3-4404-87ae-906297a3fd96 -langcode: ru -status: true -dependencies: { } -name: node_portfolio_full_photo_xs -label: 'Node - Portfolio - Full - Photo: XS' -effects: - 1ad5b966-32e0-46b7-b390-674a4f62be86: - uuid: 1ad5b966-32e0-46b7-b390-674a4f62be86 - id: image_scale_and_crop - weight: -9 - data: - width: 263 - height: 263 - anchor: center-top - 27c18a72-0b7a-48bb-86e4-04ac738f799f: - uuid: 27c18a72-0b7a-48bb-86e4-04ac738f799f - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.paragraph_image_image_lg.yml b/config/sync/image.style.paragraph_image_image_lg.yml deleted file mode 100644 index 2bdf69fe3..000000000 --- a/config/sync/image.style.paragraph_image_image_lg.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 0449b15b-ec22-4aaf-8166-f257d275c3e4 -langcode: ru -status: true -dependencies: { } -name: paragraph_image_image_lg -label: 'Paragraph - Image - Image: LG' -effects: - 7ebdbb99-94ff-436b-b032-71dbded090a9: - uuid: 7ebdbb99-94ff-436b-b032-71dbded090a9 - id: image_scale - weight: -9 - data: - width: 704 - height: null - upscale: false - 3936416c-cd8c-49dd-a35a-e6eab2471edb: - uuid: 3936416c-cd8c-49dd-a35a-e6eab2471edb - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.paragraph_image_image_md.yml b/config/sync/image.style.paragraph_image_image_md.yml deleted file mode 100644 index b5c1f2933..000000000 --- a/config/sync/image.style.paragraph_image_image_md.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 965fec7a-ecd6-48d9-ac45-03028c62edb8 -langcode: ru -status: true -dependencies: { } -name: paragraph_image_image_md -label: 'Paragraph - Image - Image: MD' -effects: - 8f463c6b-13aa-4a7a-a29c-0350713483c9: - uuid: 8f463c6b-13aa-4a7a-a29c-0350713483c9 - id: image_scale - weight: -9 - data: - width: 895 - height: null - upscale: false - a9d3a098-5638-492e-8a37-f0b364d0af2b: - uuid: a9d3a098-5638-492e-8a37-f0b364d0af2b - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.paragraph_image_image_sm.yml b/config/sync/image.style.paragraph_image_image_sm.yml deleted file mode 100644 index 4d68455aa..000000000 --- a/config/sync/image.style.paragraph_image_image_sm.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: b50a4b17-5fad-4db0-90cc-7e8180b39d99 -langcode: ru -status: true -dependencies: { } -name: paragraph_image_image_sm -label: 'Paragraph - Image - Image: SM' -effects: - b2cdd0d4-4ae4-4987-bb0d-bf4f0573c101: - uuid: b2cdd0d4-4ae4-4987-bb0d-bf4f0573c101 - id: image_scale - weight: -9 - data: - width: 671 - height: null - upscale: false - db9e1002-3719-42d1-820d-6638b4a9c66c: - uuid: db9e1002-3719-42d1-820d-6638b4a9c66c - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.paragraph_image_image_xl.yml b/config/sync/image.style.paragraph_image_image_xl.yml deleted file mode 100644 index ac6b98d4d..000000000 --- a/config/sync/image.style.paragraph_image_image_xl.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 50b1e0b4-4f31-4d3c-af8c-b003845da42d -langcode: ru -status: true -dependencies: { } -name: paragraph_image_image_xl -label: 'Paragraph - Image - Image: XL' -effects: - d73a0a6d-1fb2-436f-9042-071818a50959: - uuid: d73a0a6d-1fb2-436f-9042-071818a50959 - id: image_scale - weight: -9 - data: - width: 704 - height: null - upscale: false - 50758588-2864-4889-95b3-4e94cbcbb88f: - uuid: 50758588-2864-4889-95b3-4e94cbcbb88f - id: image_convert - weight: -10 - data: - extension: webp diff --git a/config/sync/image.style.paragraph_image_image_xs.yml b/config/sync/image.style.paragraph_image_image_xs.yml deleted file mode 100644 index 6c4c41974..000000000 --- a/config/sync/image.style.paragraph_image_image_xs.yml +++ /dev/null @@ -1,21 +0,0 @@ -uuid: 188e24a3-387e-4dd1-8eb5-092a306d3afa -langcode: ru -status: true -dependencies: { } -name: paragraph_image_image_xs -label: 'Paragraph - Image - Image: XS' -effects: - e507b2bd-3b4e-4e1a-abe4-588b98d30a3b: - uuid: e507b2bd-3b4e-4e1a-abe4-588b98d30a3b - id: image_scale - weight: -9 - data: - width: 543 - height: null - upscale: false - 89ee6b0d-6aa4-4abe-9150-316e180e33f7: - uuid: 89ee6b0d-6aa4-4abe-9150-316e180e33f7 - id: image_convert - weight: -10 - data: - extension: webp From 5634554c0da83a9ff6d25d067c56485195f8f185 Mon Sep 17 00:00:00 2001 From: Niklan Date: Sun, 29 Mar 2026 22:08:38 +0500 Subject: [PATCH 2/2] feat(image): replace static image styles with dynamic on-the-fly generation --- .../tests/src/Kernel/Plugin/Filter/InternalLinkFilterTest.php | 1 + .../blog/tests/src/Kernel/Sync/ArticleSynchronizerTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/modules/blog/tests/src/Kernel/Plugin/Filter/InternalLinkFilterTest.php b/app/modules/blog/tests/src/Kernel/Plugin/Filter/InternalLinkFilterTest.php index 8b22d26f3..0ac810f91 100644 --- a/app/modules/blog/tests/src/Kernel/Plugin/Filter/InternalLinkFilterTest.php +++ b/app/modules/blog/tests/src/Kernel/Plugin/Filter/InternalLinkFilterTest.php @@ -21,6 +21,7 @@ final class InternalLinkFilterTest extends KernelTestBase { 'app_blog', 'app_contract', 'app_file', + 'app_image', 'app_media', 'app_platform', 'app_tag', diff --git a/app/modules/blog/tests/src/Kernel/Sync/ArticleSynchronizerTest.php b/app/modules/blog/tests/src/Kernel/Sync/ArticleSynchronizerTest.php index 51aab526f..594e9e7f3 100644 --- a/app/modules/blog/tests/src/Kernel/Sync/ArticleSynchronizerTest.php +++ b/app/modules/blog/tests/src/Kernel/Sync/ArticleSynchronizerTest.php @@ -39,6 +39,7 @@ final class ArticleSynchronizerTest extends KernelTestBase { 'app_blog', 'app_contract', 'app_file', + 'app_image', 'app_media', 'app_platform', 'app_tag',