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