From 61712f0d7b543d26fb3d02b1b08879adf1144399 Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Fri, 28 Feb 2025 19:21:35 +0200 Subject: [PATCH 01/14] Issue #3509597 by vasike: Image Style Action plugin for image files - first attempt. --- .../config/schema/image.action.schema.yml | 8 + .../Plugin/Action/FileImageStyleAction.php | 195 ++++++++++++++++++ .../Action/FileImageStyleActionTest.php | 127 ++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 core/modules/image/config/schema/image.action.schema.yml create mode 100644 core/modules/image/src/Plugin/Action/FileImageStyleAction.php create mode 100644 core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php diff --git a/core/modules/image/config/schema/image.action.schema.yml b/core/modules/image/config/schema/image.action.schema.yml new file mode 100644 index 000000000000..ba013bd67cfd --- /dev/null +++ b/core/modules/image/config/schema/image.action.schema.yml @@ -0,0 +1,8 @@ + +action.configuration.file_image_style_action: + type: mapping + label: 'Configuration for "File Image Style" action' + mapping: + image_style: + type: string + label: 'The image style to be applied to an image file entity.' diff --git a/core/modules/image/src/Plugin/Action/FileImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileImageStyleAction.php new file mode 100644 index 000000000000..e28c29c237a1 --- /dev/null +++ b/core/modules/image/src/Plugin/Action/FileImageStyleAction.php @@ -0,0 +1,195 @@ +<?php + +namespace Drupal\image\Plugin\Action; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\file\FileInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\File\FileExists; +use Drupal\Core\StringTranslation\ByteSizeMarkup; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\image\ImageStyleStorageInterface; +use Psr\Log\LoggerInterface; + +/** + * Action to replace an image file with one processed by an image style. + */ +#[Action( + id: 'file_image_style_action', + action_label: new TranslatableMarkup('Replace image file with the provided image style'), + type: 'file' +)] + class FileImageStyleAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface { + + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + protected FileSystemInterface $fileSystem, + protected ImageStyleStorageInterface $imageStyleStorage, + protected LoggerInterface $logger, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('file_system'), + $container->get('entity_type.manager')->getStorage('image_style'), + $container->get('logger.factory')->get('image'), + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'image_style' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $styles = $this->imageStyleStorage->loadMultiple(); + $options = []; + foreach ($styles as $style) { + $options[$style->id()] = $style->label(); + } + + $form['image_style'] = [ + '#type' => 'select', + '#title' => $this->t('Image style'), + '#description' => $this->t('Select the image style to apply to the original image.'), + '#options' => $options, + '#default_value' => $this->configuration['image_style'], + '#required' => TRUE, + ]; + + $form['warning'] = [ + '#type' => 'markup', + '#markup' => '<div class="messages messages--warning">' . $this->t('Warning: This action will permanently replace the original image files with the styled versions. This cannot be undone.') . '</div>', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { + $this->configuration['image_style'] = $form_state->getValue('image_style'); + } + + /** + * {@inheritdoc} + */ + public function execute($file = NULL): void { + + // Get the image style. + $style_id = $this->configuration['image_style']; + /** @var \Drupal\image\Entity\ImageStyle $style */ + $style = $this->imageStyleStorage->load($style_id); + if (!$style) { + $this->logger->error('Image style %style not found.', ['%style' => $style_id]); + return; + } + + $source_uri = $file->getFileUri(); + + // Check if style extension is different from the original file extension, + // and if so, change the file name and uri. + $file_name = $file->getFilename(); + $original_extension = pathinfo($file_name, PATHINFO_EXTENSION); + $styled_extenstion = $style->getDerivativeExtension($original_extension); + $styled_image_uri = $source_uri; + $extension_changed = FALSE; + if ($styled_extenstion !== $original_extension) { + $file_name = str_replace('.' . $original_extension, '.' . $styled_extenstion, $file_name); + $styled_image_uri = str_replace('.' . $original_extension, '.' . $styled_extenstion, $source_uri); + $extension_changed = TRUE; + } + + // Create a temporary file to store the styled image. + $directory = $this->fileSystem->dirname($source_uri); + $destination = $this->fileSystem->createFilename('temp_' . $file_name, $directory); + + try { + // Generate the styled image. + $style->createDerivative($source_uri, $destination); + + // Get the file stats before replacement. + $original_size = filesize($source_uri); + + // Replace the original file with the styled version. + $this->fileSystem->copy($destination, $styled_image_uri, FileExists::Replace); + + // Update the file metadata. + $new_size = filesize($source_uri); + if ($extension_changed) { + $file->setFileUri($styled_image_uri); + $file->setFilename($file_name); + $this->fileSystem->delete($source_uri); + } + $file->setSize($new_size); + $file->save(); + + // Clean up the temporary file. + $this->fileSystem->delete($destination); + + $this->logger->info('Replaced image %file with style %style. Original size: %old_size, new size: %new_size.', [ + '%file' => $file->getFilename(), + '%style' => $style_id, + '%old_size' => ByteSizeMarkup::create($original_size), + '%new_size' => ByteSizeMarkup::create($new_size), + ]); + } + catch (\Exception $e) { + $this->logger->get('image_style_action')->error('Failed to replace image %file with style %style: @error', [ + '%file' => $file->getFilename(), + '%style' => $style_id, + '@error' => $e->getMessage(), + ]); + + // Clean up the temporary file if it exists. + if (file_exists($destination)) { + $this->fileSystem->delete($destination); + } + } + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool|AccessResultInterface { + if (!($object instanceof FileInterface)) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + + // Only process image files. + $mime_type = $object->getMimeType(); + if (strpos($mime_type, 'image/') !== 0) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + + $access = $object->access('create', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + return $return_as_object ? $access : $access->isAllowed(); + } + + } diff --git a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php new file mode 100644 index 000000000000..d3513222bfcc --- /dev/null +++ b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php @@ -0,0 +1,127 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\image\Kernel\Plugin\Action; + +use Drupal\Core\Image\ImageFactory; +use Drupal\file\Entity\File; +use Drupal\image\Entity\ImageStyle; +use Drupal\KernelTests\KernelTestBase; +use Drupal\system\Entity\Action; +use Drupal\Tests\TestFileCreationTrait; + +/** + * Tests Content Entity Translate action. + * + * @covers \Drupal\image\Plugin\Action\FileImageStyleAction + * + * @group action + * @group image + */ +class FileImageStyleActionTest extends KernelTestBase { + + use TestFileCreationTrait { + getTestFiles as drupalGetTestFiles; + } + + /** + * The image factory service. + */ + protected ImageFactory $imageFactory; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'file', + 'image', + 'system', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig(['system']); + $this->installEntitySchema('file'); + $this->installEntitySchema('user'); + $this->installSchema('file', ['file_usage']); + + $this->imageFactory = $this->container->get('image.factory'); + + $original_style = ImageStyle::create([ + 'name' => 'original_style', + 'label' => 'Original style', + ]); + + // Add an image effect. + $convert_effect = [ + 'id' => 'image_convert', + 'data' => [ + 'extension' => 'webp', + ], + 'weight' => 0, + ]; + $original_style->addImageEffect($convert_effect); + $resize_effect = [ + 'id' => 'image_scale_and_crop', + 'data' => [ + 'anchor' => 'center-center', + 'width' => 1, + 'height' => 1, + ], + 'weight' => 1, + ]; + $original_style->addImageEffect($resize_effect); + $original_style->save(); + } + + /** + * Tests moving a randomly generated image. + */ + public function testFileImageStyleAction(): void { + // Create a file for testing. + $file_original = File::create((array) current($this->drupalGetTestFiles('image'))); + $file_original->save(); + $original_image = $this->imageFactory->get($file_original->getFileUri()); + + // Create an action with unexisting image style. + $action = Action::create([ + 'id' => 'file_image_style_action', + 'label' => 'Optimize image', + 'plugin' => 'file_image_style_action', + 'configuration' => [ + 'image_style' => 'invalid_style', + ], + ]); + $action->save(); + + // Pick a file for testing. + $action->execute([$file_original]); + $file_not_styled = File::load($file_original->id()); + $not_styled_image = $this->imageFactory->get($file_not_styled->getFileUri()); + $this->assertEquals($original_image->getFileSize(), $not_styled_image->getFileSize()); + $this->assertEquals($original_image->getMimeType(), $not_styled_image->getMimeType()); + $this->assertEquals($original_image->getHeight(), $not_styled_image->getHeight()); + + // Correct action image style configuration. + $action->set('configuration', ['image_style' => 'original_style']); + $action->save(); + + $action->execute([$file_original]); + + // Test that the original file has been replaced with the styled one. + $file_styled = File::load($file_original->id()); + $styled_image = $this->imageFactory->get($file_styled->getFileUri()); + $this->assertNotEquals($original_image->getFileSize(), $styled_image->getFileSize()); + $this->assertNotEquals($original_image->getMimeType(), $styled_image->getMimeType()); + $this->assertNotEquals($original_image->getHeight(), $styled_image->getHeight()); + $this->assertFalse(file_exists($original_image->getSource())); + $this->assertTrue(file_exists($styled_image->getSource())); + } + +} -- GitLab From fc63efff8a17fe956ba53290243eb8dc268c2de1 Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Fri, 28 Feb 2025 19:34:25 +0200 Subject: [PATCH 02/14] Cspell fixes 1. --- .../image/src/Plugin/Action/FileImageStyleAction.php | 8 ++++---- .../src/Kernel/Plugin/Action/FileImageStyleActionTest.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileImageStyleAction.php index e28c29c237a1..5b0c8af6489e 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStyleAction.php +++ b/core/modules/image/src/Plugin/Action/FileImageStyleAction.php @@ -116,12 +116,12 @@ public function execute($file = NULL): void { // and if so, change the file name and uri. $file_name = $file->getFilename(); $original_extension = pathinfo($file_name, PATHINFO_EXTENSION); - $styled_extenstion = $style->getDerivativeExtension($original_extension); + $styled_extension = $style->getDerivativeExtension($original_extension); $styled_image_uri = $source_uri; $extension_changed = FALSE; - if ($styled_extenstion !== $original_extension) { - $file_name = str_replace('.' . $original_extension, '.' . $styled_extenstion, $file_name); - $styled_image_uri = str_replace('.' . $original_extension, '.' . $styled_extenstion, $source_uri); + if ($styled_extension !== $original_extension) { + $file_name = str_replace('.' . $original_extension, '.' . $styled_extension, $file_name); + $styled_image_uri = str_replace('.' . $original_extension, '.' . $styled_extension, $source_uri); $extension_changed = TRUE; } diff --git a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php index d3513222bfcc..578c51f745dc 100644 --- a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php +++ b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php @@ -81,7 +81,7 @@ protected function setUp(): void { } /** - * Tests moving a randomly generated image. + * Test File Image Style Action. */ public function testFileImageStyleAction(): void { // Create a file for testing. @@ -89,7 +89,7 @@ public function testFileImageStyleAction(): void { $file_original->save(); $original_image = $this->imageFactory->get($file_original->getFileUri()); - // Create an action with unexisting image style. + // Create an action with a not existing image style. $action = Action::create([ 'id' => 'file_image_style_action', 'label' => 'Optimize image', -- GitLab From a2ebe8f8f6b927812354b31bbe816b0f45b7fbbb Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Sat, 1 Mar 2025 13:28:42 +0200 Subject: [PATCH 03/14] Complete File Image Styles Action with Derivatives Generation for provided Image Styles. --- .../config/schema/image.action.schema.yml | 14 +- .../Action/FileImageStyleActionBase.php | 65 ++++++++ .../Action/FileImageStylesGenerateAction.php | 106 ++++++++++++ .../Action/FileOriginalImageStyleAction.php | 156 ++++++++++++++++++ .../Action/FileImageStyleActionTest.php | 77 +++++++-- 5 files changed, 399 insertions(+), 19 deletions(-) create mode 100644 core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php create mode 100644 core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php create mode 100644 core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php diff --git a/core/modules/image/config/schema/image.action.schema.yml b/core/modules/image/config/schema/image.action.schema.yml index ba013bd67cfd..a80161e839cf 100644 --- a/core/modules/image/config/schema/image.action.schema.yml +++ b/core/modules/image/config/schema/image.action.schema.yml @@ -1,7 +1,17 @@ +action.configuration.file_image_styles_generate_action: + type: mapping + label: 'Configuration for "File Image Styles" action' + mapping: + image_styles: + type: sequence + label: 'The image styles to generate image derivatives for.' + sequence: + type: string + label: 'Image style' -action.configuration.file_image_style_action: +action.configuration.file_original_image_style_action: type: mapping - label: 'Configuration for "File Image Style" action' + label: 'Configuration for "File Oroginal Image Style" action' mapping: image_style: type: string diff --git a/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php b/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php new file mode 100644 index 000000000000..d02601949784 --- /dev/null +++ b/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\image\Plugin\Action; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Action\ConfigurableActionBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\file\FileInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\image\ImageStyleStorageInterface; +use Psr\Log\LoggerInterface; + +/** + * Base class for entity-based actions. + */ +abstract class FileImageStyleActionBase extends ConfigurableActionBase implements ContainerFactoryPluginInterface { + + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + protected FileSystemInterface $fileSystem, + protected ImageStyleStorageInterface $imageStyleStorage, + protected LoggerInterface $logger, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('file_system'), + $container->get('entity_type.manager')->getStorage('image_style'), + $container->get('logger.factory')->get('image'), + ); + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool|AccessResultInterface { + if (!($object instanceof FileInterface)) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + + // Only process image files. + $mime_type = $object->getMimeType(); + if (strpos($mime_type, 'image/') !== 0) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + + $access = $object->access('create', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + return $return_as_object ? $access : $access->isAllowed(); + } + +} diff --git a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php new file mode 100644 index 000000000000..9c88cdc4e3f6 --- /dev/null +++ b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php @@ -0,0 +1,106 @@ +<?php + +namespace Drupal\image\Plugin\Action; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\ByteSizeMarkup; +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Action to generate an image file derivatives for the given image styles. + */ +#[Action( + id: 'file_image_styles_generate_action', + action_label: new TranslatableMarkup('Generate image derivatives for the provided image styles'), + type: 'file' +)] + class FileImageStylesGenerateAction extends FileImageStyleActionBase { + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'image_styles' => [], + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $styles = $this->imageStyleStorage->loadMultiple(); + $options = []; + foreach ($styles as $style) { + $options[$style->id()] = $style->label(); + } + + $form['image_styles'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Image styles'), + '#description' => $this->t('Select the image style to apply to the original image.'), + '#options' => $options, + '#default_value' => $this->configuration['image_styles'], + '#required' => TRUE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { + $this->configuration['image_styles'] = $form_state->getValue('image_styles'); + } + + /** + * {@inheritdoc} + */ + public function execute($file = NULL): void { + $style_ids = array_filter($this->configuration['image_styles']); + /** @var \Drupal\image\Entity\ImageStyle[] $styles */ + $styles = $this->imageStyleStorage->loadMultiple($style_ids); + + $original_uri = $file->getFileUri(); + $original_size = filesize($original_uri); + foreach ($styles as $style) { + // Set up derivative file information. + $derivative_uri = $style->buildUri($original_uri); + // Create derivative if necessary. + if (!file_exists($derivative_uri)) { + $style->createDerivative($original_uri, $derivative_uri); + $new_size = filesize($original_uri); + $this->logger->info('New image derivative %file_uri with style %style was generated. Original size: %old_size, new size: %new_size.', [ + '%file' => $derivative_uri, + '%style' => $style->id(), + '%old_size' => ByteSizeMarkup::create($original_size), + '%new_size' => ByteSizeMarkup::create($new_size), + ]); + + } + } + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool|AccessResultInterface { + // Make sure the image styles are set and they are real. + $style_ids = array_filter($this->configuration['image_styles']); + if (empty($style_ids)) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + $styles = $this->imageStyleStorage->loadMultiple($style_ids); + if (empty($styles)) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + + return parent::access($object, $account, $return_as_object); + } + + } diff --git a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php new file mode 100644 index 000000000000..76a86eb9d542 --- /dev/null +++ b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php @@ -0,0 +1,156 @@ +<?php + +namespace Drupal\image\Plugin\Action; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\File\FileExists; +use Drupal\Core\StringTranslation\ByteSizeMarkup; +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Action to replace an image file with one processed by an image style. + */ +#[Action( + id: 'file_original_image_style_action', + action_label: new TranslatableMarkup('Replace image file with the provided image style'), + type: 'file' +)] + class FileOriginalImageStyleAction extends FileImageStyleActionBase implements ContainerFactoryPluginInterface { + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'image_style' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $styles = $this->imageStyleStorage->loadMultiple(); + $options = []; + foreach ($styles as $style) { + $options[$style->id()] = $style->label(); + } + + $form['image_style'] = [ + '#type' => 'select', + '#title' => $this->t('Original image style'), + '#description' => $this->t('Select the image style to apply to the original image.'), + '#options' => $options, + '#default_value' => $this->configuration['image_style'], + '#required' => TRUE, + ]; + + $form['warning'] = [ + '#type' => 'markup', + '#markup' => '<div class="messages messages--warning">' . $this->t('Warning: This action will permanently replace the original image files with the styled versions. This cannot be undone.') . '</div>', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { + $this->configuration['image_style'] = $form_state->getValue('image_style'); + } + + /** + * {@inheritdoc} + */ + public function execute($file = NULL): void { + // Get the image style. + $style_id = $this->configuration['image_style']; + /** @var \Drupal\image\Entity\ImageStyle $style */ + $style = $this->imageStyleStorage->load($style_id); + if (!$style) { + $this->logger->error('Image style %style not found.', ['%style' => $style_id]); + return; + } + + $source_uri = $file->getFileUri(); + + // Check if style extension is different from the original file extension, + // and if so, change the file name and uri. + $file_name = $file->getFilename(); + $original_extension = pathinfo($file_name, PATHINFO_EXTENSION); + $styled_extension = $style->getDerivativeExtension($original_extension); + $styled_image_uri = $source_uri; + $extension_changed = FALSE; + if ($styled_extension !== $original_extension) { + $file_name = str_replace('.' . $original_extension, '.' . $styled_extension, $file_name); + $styled_image_uri = str_replace('.' . $original_extension, '.' . $styled_extension, $source_uri); + $extension_changed = TRUE; + } + + // Create a temporary file to store the styled image. + $directory = $this->fileSystem->dirname($source_uri); + $destination = $this->fileSystem->createFilename('temp_' . $file_name, $directory); + + try { + // Generate the styled image. + $style->createDerivative($source_uri, $destination); + + // Get the file stats before replacement. + $original_size = filesize($source_uri); + + // Replace the original file with the styled version. + $this->fileSystem->copy($destination, $styled_image_uri, FileExists::Replace); + + // Update the file metadata. + $new_size = filesize($source_uri); + if ($extension_changed) { + $file->setFileUri($styled_image_uri); + $file->setFilename($file_name); + $this->fileSystem->delete($source_uri); + } + $file->setSize($new_size); + $file->save(); + + // Clean up the temporary file. + $this->fileSystem->delete($destination); + + $this->logger->info('Replaced image %file with style %style. Original size: %old_size, new size: %new_size.', [ + '%file' => $file->getFilename(), + '%style' => $style_id, + '%old_size' => ByteSizeMarkup::create($original_size), + '%new_size' => ByteSizeMarkup::create($new_size), + ]); + } + catch (\Exception $e) { + $this->logger->error('Failed to replace image %file with style %style: @error', [ + '%file' => $file->getFilename(), + '%style' => $style_id, + '@error' => $e->getMessage(), + ]); + + // Clean up the temporary file if it exists. + if (file_exists($destination)) { + $this->fileSystem->delete($destination); + } + } + } + + /** + * {@inheritdoc} + */ + public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool|AccessResultInterface { + // Make sure the image styles are set. + if (empty($this->configuration['image_style'])) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } + + return parent::access($object, $account, $return_as_object); + } + + } diff --git a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php index 578c51f745dc..d8310681060b 100644 --- a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php +++ b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php @@ -6,15 +6,17 @@ use Drupal\Core\Image\ImageFactory; use Drupal\file\Entity\File; +use Drupal\file\FileInterface; use Drupal\image\Entity\ImageStyle; use Drupal\KernelTests\KernelTestBase; use Drupal\system\Entity\Action; use Drupal\Tests\TestFileCreationTrait; /** - * Tests Content Entity Translate action. + * Tests File Image Styles actions. * - * @covers \Drupal\image\Plugin\Action\FileImageStyleAction + * @covers \Drupal\image\Plugin\Action\FileImageStylesGenerateAction + * @covers \Drupal\image\Plugin\Action\FileOriginalImageStyleAction * * @group action * @group image @@ -30,6 +32,16 @@ class FileImageStyleActionTest extends KernelTestBase { */ protected ImageFactory $imageFactory; + /** + * An image file path for uploading. + */ + protected FileInterface $image; + + /** + * An image style. + */ + protected ImageStyle $imageStyle; + /** * {@inheritdoc} */ @@ -50,10 +62,11 @@ protected function setUp(): void { $this->installEntitySchema('file'); $this->installEntitySchema('user'); $this->installSchema('file', ['file_usage']); + $this->installEntitySchema('image_style'); $this->imageFactory = $this->container->get('image.factory'); - $original_style = ImageStyle::create([ + $this->imageStyle = ImageStyle::create([ 'name' => 'original_style', 'label' => 'Original style', ]); @@ -66,7 +79,7 @@ protected function setUp(): void { ], 'weight' => 0, ]; - $original_style->addImageEffect($convert_effect); + $this->imageStyle->addImageEffect($convert_effect); $resize_effect = [ 'id' => 'image_scale_and_crop', 'data' => [ @@ -76,24 +89,53 @@ protected function setUp(): void { ], 'weight' => 1, ]; - $original_style->addImageEffect($resize_effect); - $original_style->save(); + $this->imageStyle->addImageEffect($resize_effect); + $this->imageStyle->save(); + + $image_files = $this->drupalGetTestFiles('image'); + $this->image = File::create((array) current($image_files)); + $this->image->save(); + } + + /** + * Test File Image Styles Generate Action. + */ + public function testFileImageStylesGenerateAction(): void { + // Make sure the derivative does not exist, initially. + $derivative_uri = $this->imageStyle->buildUri($this->image->getFileUri()); + $this->assertFalse(file_exists($derivative_uri)); + + // Create the File Image Styles Generate Action. + $action = Action::create([ + 'id' => 'file_image_styles_generate_action', + 'label' => 'Optimize image', + 'plugin' => 'file_image_styles_generate_action', + 'configuration' => [ + 'image_styles' => ['original_style', 'invalid_style'], + ], + ]); + $action->save(); + $action->execute([$this->image]); + // Make sure the derivative images was generated and the image style effects + // were applied. + $this->assertTrue(file_exists($derivative_uri)); + $derivative_image = $this->imageFactory->get($derivative_uri); + $this->assertEquals('image/webp', $derivative_image->getMimeType()); + $this->assertEquals($derivative_image->getHeight(), 1); + } /** - * Test File Image Style Action. + * Test File Original Image Style Action. */ public function testFileImageStyleAction(): void { - // Create a file for testing. - $file_original = File::create((array) current($this->drupalGetTestFiles('image'))); - $file_original->save(); - $original_image = $this->imageFactory->get($file_original->getFileUri()); + $original_image = $this->imageFactory->get($this->image->getFileUri()); // Create an action with a not existing image style. $action = Action::create([ - 'id' => 'file_image_style_action', + 'id' => 'file_original_image_style_action', 'label' => 'Optimize image', - 'plugin' => 'file_image_style_action', + 'plugin' => 'file_original_image_style_action', 'configuration' => [ 'image_style' => 'invalid_style', ], @@ -101,8 +143,8 @@ public function testFileImageStyleAction(): void { $action->save(); // Pick a file for testing. - $action->execute([$file_original]); - $file_not_styled = File::load($file_original->id()); + $action->execute([$this->image]); + $file_not_styled = File::load($this->image->id()); $not_styled_image = $this->imageFactory->get($file_not_styled->getFileUri()); $this->assertEquals($original_image->getFileSize(), $not_styled_image->getFileSize()); $this->assertEquals($original_image->getMimeType(), $not_styled_image->getMimeType()); @@ -112,16 +154,17 @@ public function testFileImageStyleAction(): void { $action->set('configuration', ['image_style' => 'original_style']); $action->save(); - $action->execute([$file_original]); + $action->execute([$this->image]); // Test that the original file has been replaced with the styled one. - $file_styled = File::load($file_original->id()); + $file_styled = File::load($this->image->id()); $styled_image = $this->imageFactory->get($file_styled->getFileUri()); $this->assertNotEquals($original_image->getFileSize(), $styled_image->getFileSize()); $this->assertNotEquals($original_image->getMimeType(), $styled_image->getMimeType()); $this->assertNotEquals($original_image->getHeight(), $styled_image->getHeight()); $this->assertFalse(file_exists($original_image->getSource())); $this->assertTrue(file_exists($styled_image->getSource())); + $this->assertEquals($styled_image->getHeight(), 1); } } -- GitLab From 390ace5c9505a85fc9cd80c668e696ed7579de77 Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Sat, 1 Mar 2025 13:29:25 +0200 Subject: [PATCH 04/14] Remove/renamed old file. --- .../Plugin/Action/FileImageStyleAction.php | 195 ------------------ 1 file changed, 195 deletions(-) delete mode 100644 core/modules/image/src/Plugin/Action/FileImageStyleAction.php diff --git a/core/modules/image/src/Plugin/Action/FileImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileImageStyleAction.php deleted file mode 100644 index 5b0c8af6489e..000000000000 --- a/core/modules/image/src/Plugin/Action/FileImageStyleAction.php +++ /dev/null @@ -1,195 +0,0 @@ -<?php - -namespace Drupal\image\Plugin\Action; - -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Access\AccessResultInterface; -use Drupal\Core\Action\Attribute\Action; -use Drupal\Core\Action\ConfigurableActionBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\file\FileInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\File\FileExists; -use Drupal\Core\StringTranslation\ByteSizeMarkup; -use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\image\ImageStyleStorageInterface; -use Psr\Log\LoggerInterface; - -/** - * Action to replace an image file with one processed by an image style. - */ -#[Action( - id: 'file_image_style_action', - action_label: new TranslatableMarkup('Replace image file with the provided image style'), - type: 'file' -)] - class FileImageStyleAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface { - - public function __construct( - array $configuration, - $plugin_id, - $plugin_definition, - protected FileSystemInterface $fileSystem, - protected ImageStyleStorageInterface $imageStyleStorage, - protected LoggerInterface $logger, - ) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('file_system'), - $container->get('entity_type.manager')->getStorage('image_style'), - $container->get('logger.factory')->get('image'), - ); - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return [ - 'image_style' => '', - ]; - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $styles = $this->imageStyleStorage->loadMultiple(); - $options = []; - foreach ($styles as $style) { - $options[$style->id()] = $style->label(); - } - - $form['image_style'] = [ - '#type' => 'select', - '#title' => $this->t('Image style'), - '#description' => $this->t('Select the image style to apply to the original image.'), - '#options' => $options, - '#default_value' => $this->configuration['image_style'], - '#required' => TRUE, - ]; - - $form['warning'] = [ - '#type' => 'markup', - '#markup' => '<div class="messages messages--warning">' . $this->t('Warning: This action will permanently replace the original image files with the styled versions. This cannot be undone.') . '</div>', - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { - $this->configuration['image_style'] = $form_state->getValue('image_style'); - } - - /** - * {@inheritdoc} - */ - public function execute($file = NULL): void { - - // Get the image style. - $style_id = $this->configuration['image_style']; - /** @var \Drupal\image\Entity\ImageStyle $style */ - $style = $this->imageStyleStorage->load($style_id); - if (!$style) { - $this->logger->error('Image style %style not found.', ['%style' => $style_id]); - return; - } - - $source_uri = $file->getFileUri(); - - // Check if style extension is different from the original file extension, - // and if so, change the file name and uri. - $file_name = $file->getFilename(); - $original_extension = pathinfo($file_name, PATHINFO_EXTENSION); - $styled_extension = $style->getDerivativeExtension($original_extension); - $styled_image_uri = $source_uri; - $extension_changed = FALSE; - if ($styled_extension !== $original_extension) { - $file_name = str_replace('.' . $original_extension, '.' . $styled_extension, $file_name); - $styled_image_uri = str_replace('.' . $original_extension, '.' . $styled_extension, $source_uri); - $extension_changed = TRUE; - } - - // Create a temporary file to store the styled image. - $directory = $this->fileSystem->dirname($source_uri); - $destination = $this->fileSystem->createFilename('temp_' . $file_name, $directory); - - try { - // Generate the styled image. - $style->createDerivative($source_uri, $destination); - - // Get the file stats before replacement. - $original_size = filesize($source_uri); - - // Replace the original file with the styled version. - $this->fileSystem->copy($destination, $styled_image_uri, FileExists::Replace); - - // Update the file metadata. - $new_size = filesize($source_uri); - if ($extension_changed) { - $file->setFileUri($styled_image_uri); - $file->setFilename($file_name); - $this->fileSystem->delete($source_uri); - } - $file->setSize($new_size); - $file->save(); - - // Clean up the temporary file. - $this->fileSystem->delete($destination); - - $this->logger->info('Replaced image %file with style %style. Original size: %old_size, new size: %new_size.', [ - '%file' => $file->getFilename(), - '%style' => $style_id, - '%old_size' => ByteSizeMarkup::create($original_size), - '%new_size' => ByteSizeMarkup::create($new_size), - ]); - } - catch (\Exception $e) { - $this->logger->get('image_style_action')->error('Failed to replace image %file with style %style: @error', [ - '%file' => $file->getFilename(), - '%style' => $style_id, - '@error' => $e->getMessage(), - ]); - - // Clean up the temporary file if it exists. - if (file_exists($destination)) { - $this->fileSystem->delete($destination); - } - } - } - - /** - * {@inheritdoc} - */ - public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool|AccessResultInterface { - if (!($object instanceof FileInterface)) { - return $return_as_object ? AccessResult::forbidden() : FALSE; - } - - // Only process image files. - $mime_type = $object->getMimeType(); - if (strpos($mime_type, 'image/') !== 0) { - return $return_as_object ? AccessResult::forbidden() : FALSE; - } - - $access = $object->access('create', $account, TRUE) - ->andIf($object->access('update', $account, TRUE)); - return $return_as_object ? $access : $access->isAllowed(); - } - - } -- GitLab From 0545dbeb78a42be97caf9f30ae0fabe14772de16 Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Sat, 1 Mar 2025 13:35:02 +0200 Subject: [PATCH 05/14] Cspell fixes 2. --- core/modules/image/config/schema/image.action.schema.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/modules/image/config/schema/image.action.schema.yml b/core/modules/image/config/schema/image.action.schema.yml index a80161e839cf..15206bd36403 100644 --- a/core/modules/image/config/schema/image.action.schema.yml +++ b/core/modules/image/config/schema/image.action.schema.yml @@ -1,6 +1,6 @@ action.configuration.file_image_styles_generate_action: type: mapping - label: 'Configuration for "File Image Styles" action' + label: 'Configuration for "File Image Styles Generate" action' mapping: image_styles: type: sequence @@ -11,7 +11,7 @@ action.configuration.file_image_styles_generate_action: action.configuration.file_original_image_style_action: type: mapping - label: 'Configuration for "File Oroginal Image Style" action' + label: 'Configuration for "File Original Image Style" action' mapping: image_style: type: string -- GitLab From 10e7ca392cf6fc10c0d96af6bc0e5b821b7a50ba Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Sat, 1 Mar 2025 19:26:31 +0200 Subject: [PATCH 06/14] Add Regenerate image derivatives option. --- .../config/schema/image.action.schema.yml | 3 +++ .../Action/FileImageStylesGenerateAction.php | 13 ++++++++-- .../Action/FileImageStyleActionTest.php | 25 ++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/core/modules/image/config/schema/image.action.schema.yml b/core/modules/image/config/schema/image.action.schema.yml index 15206bd36403..605fb1981e07 100644 --- a/core/modules/image/config/schema/image.action.schema.yml +++ b/core/modules/image/config/schema/image.action.schema.yml @@ -8,6 +8,9 @@ action.configuration.file_image_styles_generate_action: sequence: type: string label: 'Image style' + regenerate: + type: boolean + label: 'Regenerate image derivatives' action.configuration.file_original_image_style_action: type: mapping diff --git a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php index 9c88cdc4e3f6..8fe2e7d51f91 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php +++ b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php @@ -26,6 +26,7 @@ class FileImageStylesGenerateAction extends FileImageStyleActionBase { public function defaultConfiguration() { return [ 'image_styles' => [], + 'regenerate' => FALSE, ]; } @@ -42,12 +43,19 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta $form['image_styles'] = [ '#type' => 'checkboxes', '#title' => $this->t('Image styles'), - '#description' => $this->t('Select the image style to apply to the original image.'), + '#description' => $this->t('Select the image styles to generate derivatives for.'), '#options' => $options, '#default_value' => $this->configuration['image_styles'], '#required' => TRUE, ]; + $form['regenerate'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Regenerate'), + '#description' => $this->t('Force regenerate the derivatives, even they already exists. Usually this is needed when the styles effects were changed.'), + '#default_value' => $this->configuration['regenerate'], + ]; + return $form; } @@ -56,6 +64,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { $this->configuration['image_styles'] = $form_state->getValue('image_styles'); + $this->configuration['regenerate'] = $form_state->getValue('regenerate'); } /** @@ -72,7 +81,7 @@ public function execute($file = NULL): void { // Set up derivative file information. $derivative_uri = $style->buildUri($original_uri); // Create derivative if necessary. - if (!file_exists($derivative_uri)) { + if (!file_exists($derivative_uri) || $this->configuration['regenerate']) { $style->createDerivative($original_uri, $derivative_uri); $new_size = filesize($original_uri); $this->logger->info('New image derivative %file_uri with style %style was generated. Original size: %old_size, new size: %new_size.', [ diff --git a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php index d8310681060b..0a31993eac2c 100644 --- a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php +++ b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php @@ -42,6 +42,11 @@ class FileImageStyleActionTest extends KernelTestBase { */ protected ImageStyle $imageStyle; + /** + * The ID of the resize effect. + */ + protected string $resizeEffectId; + /** * {@inheritdoc} */ @@ -89,7 +94,7 @@ protected function setUp(): void { ], 'weight' => 1, ]; - $this->imageStyle->addImageEffect($resize_effect); + $this->resizeEffectId = $this->imageStyle->addImageEffect($resize_effect); $this->imageStyle->save(); $image_files = $this->drupalGetTestFiles('image'); @@ -123,6 +128,24 @@ public function testFileImageStylesGenerateAction(): void { $this->assertEquals('image/webp', $derivative_image->getMimeType()); $this->assertEquals($derivative_image->getHeight(), 1); + // Update the image style effects. + $resize_effect = $this->imageStyle->getEffect($this->resizeEffectId); + $this->imageStyle->deleteImageEffect($resize_effect); + $resize_effect_config = $resize_effect->getConfiguration(); + $resize_effect_config['data']['width'] = 2; + $resize_effect_config['data']['height'] = 2; + $this->imageStyle->addImageEffect($resize_effect_config); + $this->imageStyle->save(); + + // Regenerate the derivative image. + $action->set('configuration', [ + 'image_styles' => ['original_style'], + 'regenerate' => TRUE, + ]); + $action->save(); + $action->execute([$this->image]); + $derivative_image = $this->imageFactory->get($derivative_uri); + $this->assertEquals($derivative_image->getHeight(), 2); } /** -- GitLab From ec343f1b2ec7a0735cc593b42ed1e6f923e73cb5 Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Sun, 2 Mar 2025 16:17:48 +0200 Subject: [PATCH 07/14] Some code clean-up. --- .../Action/FileImageStyleActionBase.php | 4 +- .../Action/FileOriginalImageStyleAction.php | 45 +++++++------------ .../Action/FileImageStyleActionTest.php | 5 +-- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php b/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php index d02601949784..00049ddbf677 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php +++ b/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php @@ -57,8 +57,8 @@ public function access($object, ?AccountInterface $account = NULL, $return_as_ob return $return_as_object ? AccessResult::forbidden() : FALSE; } - $access = $object->access('create', $account, TRUE) - ->andIf($object->access('update', $account, TRUE)); + $access = $object->access('update', $account, TRUE) + ->andIf($object->access('delete', $account, TRUE)); return $return_as_object ? $access : $access->isAllowed(); } diff --git a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php index 76a86eb9d542..b849abbbad71 100644 --- a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php +++ b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php @@ -8,7 +8,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\File\FileExists; use Drupal\Core\StringTranslation\ByteSizeMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -78,48 +77,37 @@ public function execute($file = NULL): void { return; } - $source_uri = $file->getFileUri(); - // Check if style extension is different from the original file extension, // and if so, change the file name and uri. + $original_uri = $file->getFileUri(); $file_name = $file->getFilename(); $original_extension = pathinfo($file_name, PATHINFO_EXTENSION); - $styled_extension = $style->getDerivativeExtension($original_extension); - $styled_image_uri = $source_uri; + $derivative_extension = $style->getDerivativeExtension($original_extension); + $derivative_uri = $original_uri; $extension_changed = FALSE; - if ($styled_extension !== $original_extension) { - $file_name = str_replace('.' . $original_extension, '.' . $styled_extension, $file_name); - $styled_image_uri = str_replace('.' . $original_extension, '.' . $styled_extension, $source_uri); + if ($derivative_extension !== $original_extension) { + $file_name = str_replace('.' . $original_extension, '.' . $derivative_extension, $file_name); + $derivative_uri = str_replace('.' . $original_extension, '.' . $derivative_extension, $original_uri); $extension_changed = TRUE; } - // Create a temporary file to store the styled image. - $directory = $this->fileSystem->dirname($source_uri); - $destination = $this->fileSystem->createFilename('temp_' . $file_name, $directory); - try { // Generate the styled image. - $style->createDerivative($source_uri, $destination); + $style->createDerivative($original_uri, $derivative_uri); // Get the file stats before replacement. - $original_size = filesize($source_uri); - - // Replace the original file with the styled version. - $this->fileSystem->copy($destination, $styled_image_uri, FileExists::Replace); + $original_size = filesize($original_uri); // Update the file metadata. - $new_size = filesize($source_uri); + $new_size = filesize($derivative_uri); if ($extension_changed) { - $file->setFileUri($styled_image_uri); + $file->setFileUri($derivative_uri); $file->setFilename($file_name); - $this->fileSystem->delete($source_uri); + $this->fileSystem->delete($original_uri); } $file->setSize($new_size); $file->save(); - // Clean up the temporary file. - $this->fileSystem->delete($destination); - $this->logger->info('Replaced image %file with style %style. Original size: %old_size, new size: %new_size.', [ '%file' => $file->getFilename(), '%style' => $style_id, @@ -133,11 +121,6 @@ public function execute($file = NULL): void { '%style' => $style_id, '@error' => $e->getMessage(), ]); - - // Clean up the temporary file if it exists. - if (file_exists($destination)) { - $this->fileSystem->delete($destination); - } } } @@ -145,10 +128,14 @@ public function execute($file = NULL): void { * {@inheritdoc} */ public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool|AccessResultInterface { - // Make sure the image styles are set. + // Make sure the action image style is set and real. if (empty($this->configuration['image_style'])) { return $return_as_object ? AccessResult::forbidden() : FALSE; } + $style = $this->imageStyleStorage->load($this->configuration['image_style']); + if (!$style) { + return $return_as_object ? AccessResult::forbidden() : FALSE; + } return parent::access($object, $account, $return_as_object); } diff --git a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php index 0a31993eac2c..f1e8cfb4c5b8 100644 --- a/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php +++ b/core/modules/image/tests/src/Kernel/Plugin/Action/FileImageStyleActionTest.php @@ -154,7 +154,7 @@ public function testFileImageStylesGenerateAction(): void { public function testFileImageStyleAction(): void { $original_image = $this->imageFactory->get($this->image->getFileUri()); - // Create an action with a not existing image style. + // Create an action with a non existing image style. $action = Action::create([ 'id' => 'file_original_image_style_action', 'label' => 'Optimize image', @@ -165,7 +165,7 @@ public function testFileImageStyleAction(): void { ]); $action->save(); - // Pick a file for testing. + // Check that the action does not execute. $action->execute([$this->image]); $file_not_styled = File::load($this->image->id()); $not_styled_image = $this->imageFactory->get($file_not_styled->getFileUri()); @@ -176,7 +176,6 @@ public function testFileImageStyleAction(): void { // Correct action image style configuration. $action->set('configuration', ['image_style' => 'original_style']); $action->save(); - $action->execute([$this->image]); // Test that the original file has been replaced with the styled one. -- GitLab From cf916914bdd08ccb8c04dc2dcc0f33fecf0b120c Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Mon, 3 Mar 2025 13:14:28 +0200 Subject: [PATCH 08/14] Some code clean-up 2. --- .../image/src/Plugin/Action/FileImageStylesGenerateAction.php | 4 ++-- .../image/src/Plugin/Action/FileOriginalImageStyleAction.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php index 8fe2e7d51f91..7d9937106ef7 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php +++ b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php @@ -18,7 +18,7 @@ action_label: new TranslatableMarkup('Generate image derivatives for the provided image styles'), type: 'file' )] - class FileImageStylesGenerateAction extends FileImageStyleActionBase { +class FileImageStylesGenerateAction extends FileImageStyleActionBase { /** * {@inheritdoc} @@ -112,4 +112,4 @@ public function access($object, ?AccountInterface $account = NULL, $return_as_ob return parent::access($object, $account, $return_as_object); } - } +} diff --git a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php index b849abbbad71..19e025712c3c 100644 --- a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php +++ b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php @@ -19,7 +19,7 @@ action_label: new TranslatableMarkup('Replace image file with the provided image style'), type: 'file' )] - class FileOriginalImageStyleAction extends FileImageStyleActionBase implements ContainerFactoryPluginInterface { +class FileOriginalImageStyleAction extends FileImageStyleActionBase implements ContainerFactoryPluginInterface { /** * {@inheritdoc} @@ -140,4 +140,4 @@ public function access($object, ?AccountInterface $account = NULL, $return_as_ob return parent::access($object, $account, $return_as_object); } - } +} -- GitLab From 67ed94529d54d74e99262c4a76fa32d003486a1b Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Mon, 3 Mar 2025 13:27:50 +0200 Subject: [PATCH 09/14] Some code clean-up - fix 2. --- .../image/src/Plugin/Action/FileImageStylesGenerateAction.php | 2 +- .../image/src/Plugin/Action/FileOriginalImageStyleAction.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php index 7d9937106ef7..817d621413f0 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php +++ b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php @@ -15,7 +15,7 @@ */ #[Action( id: 'file_image_styles_generate_action', - action_label: new TranslatableMarkup('Generate image derivatives for the provided image styles'), + label: new TranslatableMarkup('Generate image derivatives for the provided image styles'), type: 'file' )] class FileImageStylesGenerateAction extends FileImageStyleActionBase { diff --git a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php index 19e025712c3c..d00e1fa5e9db 100644 --- a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php +++ b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php @@ -16,7 +16,7 @@ */ #[Action( id: 'file_original_image_style_action', - action_label: new TranslatableMarkup('Replace image file with the provided image style'), + label: new TranslatableMarkup('Replace image file with the provided image style'), type: 'file' )] class FileOriginalImageStyleAction extends FileImageStyleActionBase implements ContainerFactoryPluginInterface { -- GitLab From 209af49c280c9c386c234238ce4be442f0e429cd Mon Sep 17 00:00:00 2001 From: Tavi Toporjinschi <43430-vasike@users.noreply.drupalcode.org> Date: Tue, 22 Apr 2025 09:56:23 +0000 Subject: [PATCH 10/14] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Stephen Mustgrave <38930-smustgrave@users.noreply.drupalcode.org> --- .../image/src/Plugin/Action/FileImageStylesGenerateAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php index 817d621413f0..32b0ff289019 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php +++ b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php @@ -23,7 +23,7 @@ class FileImageStylesGenerateAction extends FileImageStyleActionBase { /** * {@inheritdoc} */ - public function defaultConfiguration() { + public function defaultConfiguration(): array { return [ 'image_styles' => [], 'regenerate' => FALSE, -- GitLab From bc374fbf1ffb3b6ba0aaff99b96054abe94f8ee2 Mon Sep 17 00:00:00 2001 From: Tavi Toporjinschi <43430-vasike@users.noreply.drupalcode.org> Date: Tue, 22 Apr 2025 09:56:33 +0000 Subject: [PATCH 11/14] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Stephen Mustgrave <38930-smustgrave@users.noreply.drupalcode.org> --- .../image/src/Plugin/Action/FileImageStylesGenerateAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php index 32b0ff289019..5b1fd6fbbb78 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php +++ b/core/modules/image/src/Plugin/Action/FileImageStylesGenerateAction.php @@ -33,7 +33,7 @@ public function defaultConfiguration(): array { /** * {@inheritdoc} */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { $styles = $this->imageStyleStorage->loadMultiple(); $options = []; foreach ($styles as $style) { -- GitLab From ed83362a7ea821e0299999c9ec045627f3bd5c66 Mon Sep 17 00:00:00 2001 From: Tavi Toporjinschi <43430-vasike@users.noreply.drupalcode.org> Date: Tue, 22 Apr 2025 09:56:40 +0000 Subject: [PATCH 12/14] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Stephen Mustgrave <38930-smustgrave@users.noreply.drupalcode.org> --- .../image/src/Plugin/Action/FileOriginalImageStyleAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php index d00e1fa5e9db..127e81f1941e 100644 --- a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php +++ b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php @@ -24,7 +24,7 @@ class FileOriginalImageStyleAction extends FileImageStyleActionBase implements C /** * {@inheritdoc} */ - public function defaultConfiguration() { + public function defaultConfiguration(): array { return [ 'image_style' => '', ]; -- GitLab From 751023b23d04414de4b0ae7a1571753f843e8631 Mon Sep 17 00:00:00 2001 From: Tavi Toporjinschi <43430-vasike@users.noreply.drupalcode.org> Date: Tue, 22 Apr 2025 09:56:52 +0000 Subject: [PATCH 13/14] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Stephen Mustgrave <38930-smustgrave@users.noreply.drupalcode.org> --- .../image/src/Plugin/Action/FileOriginalImageStyleAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php index 127e81f1941e..118ca796e309 100644 --- a/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php +++ b/core/modules/image/src/Plugin/Action/FileOriginalImageStyleAction.php @@ -33,7 +33,7 @@ public function defaultConfiguration(): array { /** * {@inheritdoc} */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { $styles = $this->imageStyleStorage->loadMultiple(); $options = []; foreach ($styles as $style) { -- GitLab From 222bab53d44c3b97631cb80f5c240ae0c1683636 Mon Sep 17 00:00:00 2001 From: tavi toporjinschi <vasike@gmail.com> Date: Tue, 22 Apr 2025 13:41:40 +0300 Subject: [PATCH 14/14] MR Review updates 1. --- .../image/src/Plugin/Action/FileImageStyleActionBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php b/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php index 00049ddbf677..d1e583109c9f 100644 --- a/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php +++ b/core/modules/image/src/Plugin/Action/FileImageStyleActionBase.php @@ -14,7 +14,7 @@ use Psr\Log\LoggerInterface; /** - * Base class for entity-based actions. + * Base class for file image styles actions. */ abstract class FileImageStyleActionBase extends ConfigurableActionBase implements ContainerFactoryPluginInterface { -- GitLab