summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/image
diff options
context:
space:
mode:
Diffstat (limited to 'core/modules/image')
-rw-r--r--core/modules/image/config/install/image.style.large.yml2
-rw-r--r--core/modules/image/config/install/image.style.medium.yml2
-rw-r--r--core/modules/image/config/install/image.style.thumbnail.yml2
-rw-r--r--core/modules/image/config/install/image.style.wide.yml2
-rw-r--r--core/modules/image/config/schema/image.schema.yml4
-rw-r--r--core/modules/image/src/Hook/ImageRequirements.php3
-rw-r--r--core/modules/image/src/ImageEffectBase.php12
-rw-r--r--core/modules/image/src/Plugin/Field/FieldType/ImageItem.php4
-rw-r--r--core/modules/image/src/Plugin/ImageEffect/AvifImageEffect.php82
-rw-r--r--core/modules/image/tests/src/Kernel/ImageEffectsTest.php25
-rw-r--r--core/modules/image/tests/src/Kernel/ImageItemTest.php42
-rw-r--r--core/modules/image/tests/src/Unit/ImageStyleTest.php8
12 files changed, 168 insertions, 20 deletions
diff --git a/core/modules/image/config/install/image.style.large.yml b/core/modules/image/config/install/image.style.large.yml
index e0b8394552e3..1e327eea8e57 100644
--- a/core/modules/image/config/install/image.style.large.yml
+++ b/core/modules/image/config/install/image.style.large.yml
@@ -14,7 +14,7 @@ effects:
upscale: false
6e8fe467-84c1-4ef0-a73b-7eccf1cc20e8:
uuid: 6e8fe467-84c1-4ef0-a73b-7eccf1cc20e8
- id: image_convert
+ id: image_convert_avif
weight: 2
data:
extension: webp
diff --git a/core/modules/image/config/install/image.style.medium.yml b/core/modules/image/config/install/image.style.medium.yml
index f096610c6593..d7ea09a67894 100644
--- a/core/modules/image/config/install/image.style.medium.yml
+++ b/core/modules/image/config/install/image.style.medium.yml
@@ -14,7 +14,7 @@ effects:
upscale: false
c410ed2f-aa30-4d9c-a224-d2865d9188cd:
uuid: c410ed2f-aa30-4d9c-a224-d2865d9188cd
- id: image_convert
+ id: image_convert_avif
weight: 2
data:
extension: webp
diff --git a/core/modules/image/config/install/image.style.thumbnail.yml b/core/modules/image/config/install/image.style.thumbnail.yml
index c03c60e00e27..c2d7a4e5042b 100644
--- a/core/modules/image/config/install/image.style.thumbnail.yml
+++ b/core/modules/image/config/install/image.style.thumbnail.yml
@@ -14,7 +14,7 @@ effects:
upscale: false
c4eb9942-2c9e-4a81-949f-6161a44b6559:
uuid: c4eb9942-2c9e-4a81-949f-6161a44b6559
- id: image_convert
+ id: image_convert_avif
weight: 2
data:
extension: webp
diff --git a/core/modules/image/config/install/image.style.wide.yml b/core/modules/image/config/install/image.style.wide.yml
index 8573ae26346f..b62e05f3e386 100644
--- a/core/modules/image/config/install/image.style.wide.yml
+++ b/core/modules/image/config/install/image.style.wide.yml
@@ -14,7 +14,7 @@ effects:
upscale: false
294c5f76-42a4-43ce-82c2-81c2f4723da0:
uuid: 294c5f76-42a4-43ce-82c2-81c2f4723da0
- id: image_convert
+ id: image_convert_avif
weight: 2
data:
extension: webp
diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml
index f805caa378ca..68edccf507ab 100644
--- a/core/modules/image/config/schema/image.schema.yml
+++ b/core/modules/image/config/schema/image.schema.yml
@@ -52,6 +52,10 @@ image.effect.image_convert:
Choice:
callback: 'Drupal\Core\ImageToolkit\ImageToolkitManager::getAllValidExtensions'
+image.effect.image_convert_avif:
+ type: image.effect.image_convert
+ label: 'Convert to AVIF'
+
image.effect.image_resize:
type: image_size
label: 'Image resize'
diff --git a/core/modules/image/src/Hook/ImageRequirements.php b/core/modules/image/src/Hook/ImageRequirements.php
index cf631bfe3754..e1018cf539be 100644
--- a/core/modules/image/src/Hook/ImageRequirements.php
+++ b/core/modules/image/src/Hook/ImageRequirements.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\image\Hook;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\ImageToolkit\ImageToolkitManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -46,7 +47,7 @@ class ImageRequirements {
'title' => $this->t('Image toolkit'),
'value' => $this->t('None'),
'description' => $this->t("No image toolkit is configured on the site. Check PHP installed extensions or add a contributed toolkit that doesn't require a PHP extension. Make sure that at least one valid image toolkit is installed."),
- 'severity' => REQUIREMENT_ERROR,
+ 'severity' => RequirementSeverity::Error,
],
];
}
diff --git a/core/modules/image/src/ImageEffectBase.php b/core/modules/image/src/ImageEffectBase.php
index 58be370c1e6e..745976133be7 100644
--- a/core/modules/image/src/ImageEffectBase.php
+++ b/core/modules/image/src/ImageEffectBase.php
@@ -3,7 +3,7 @@
namespace Drupal\image;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurablePluginBase;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @see \Drupal\image\ImageEffectManager
* @see plugin_api
*/
-abstract class ImageEffectBase extends PluginBase implements ImageEffectInterface, ContainerFactoryPluginInterface {
+abstract class ImageEffectBase extends ConfigurablePluginBase implements ImageEffectInterface, ContainerFactoryPluginInterface {
/**
* The image effect ID.
@@ -46,7 +46,6 @@ abstract class ImageEffectBase extends PluginBase implements ImageEffectInterfac
public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
- $this->setConfiguration($configuration);
$this->logger = $logger;
}
@@ -154,13 +153,6 @@ abstract class ImageEffectBase extends PluginBase implements ImageEffectInterfac
/**
* {@inheritdoc}
*/
- public function defaultConfiguration() {
- return [];
- }
-
- /**
- * {@inheritdoc}
- */
public function calculateDependencies() {
return [];
}
diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
index 7ed6b0d3371c..72937d4e79a3 100644
--- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
+++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
@@ -389,7 +389,9 @@ class ImageItem extends FileItem {
$image->setFileName($file_system->basename($path));
$destination_dir = static::doGetUploadLocation($settings);
$file_system->prepareDirectory($destination_dir, FileSystemInterface::CREATE_DIRECTORY);
- $destination = $destination_dir . '/' . basename($path);
+ // Ensure directory ends with a slash.
+ $destination_dir .= str_ends_with($destination_dir, '/') ? '' : '/';
+ $destination = $destination_dir . basename($path);
$file = \Drupal::service('file.repository')->move($image, $destination);
$images[$extension][$min_resolution][$max_resolution][$file->id()] = $file;
}
diff --git a/core/modules/image/src/Plugin/ImageEffect/AvifImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/AvifImageEffect.php
new file mode 100644
index 000000000000..595743eece7a
--- /dev/null
+++ b/core/modules/image/src/Plugin/ImageEffect/AvifImageEffect.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image\Plugin\ImageEffect;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Image\ImageInterface;
+use Drupal\Core\ImageToolkit\ImageToolkitManager;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\image\Attribute\ImageEffect;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Converts an image resource to AVIF, with fallback.
+ */
+#[ImageEffect(
+ id: "image_convert_avif",
+ label: new TranslatableMarkup("Convert to AVIF"),
+ description: new TranslatableMarkup("Converts an image to AVIF, with a fallback if AVIF is not supported."),
+)]
+class AvifImageEffect extends ConvertImageEffect {
+
+ /**
+ * The image toolkit manager.
+ *
+ * @var \Drupal\Core\ImageToolkit\ImageToolkitManager
+ */
+ protected ImageToolkitManager $imageToolkitManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
+ $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+ $instance->imageToolkitManager = $container->get(ImageToolkitManager::class);
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyEffect(ImageInterface $image) {
+ // If avif is not supported fallback to the parent.
+ if (!$this->isAvifSupported()) {
+ return parent::applyEffect($image);
+ }
+
+ if (!$image->convert('avif')) {
+ $this->logger->error('Image convert failed using the %toolkit toolkit on %path (%mimetype)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType()]);
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeExtension($extension) {
+ return $this->isAvifSupported() ? 'avif' : $this->configuration['extension'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+ unset($form['extension']['#options']['avif']);
+ $form['extension']['#title'] = $this->t('Fallback format');
+ $form['extension']['#description'] = $this->t('Format to use if AVIF is not available.');
+ return $form;
+ }
+
+ /**
+ * Is AVIF supported by the image toolkit.
+ */
+ protected function isAvifSupported(): bool {
+ return in_array('avif', $this->imageToolkitManager->getDefaultToolkit()->getSupportedExtensions());
+ }
+
+}
diff --git a/core/modules/image/tests/src/Kernel/ImageEffectsTest.php b/core/modules/image/tests/src/Kernel/ImageEffectsTest.php
index 1e5c75339225..54130e7818b6 100644
--- a/core/modules/image/tests/src/Kernel/ImageEffectsTest.php
+++ b/core/modules/image/tests/src/Kernel/ImageEffectsTest.php
@@ -120,6 +120,31 @@ class ImageEffectsTest extends KernelTestBase {
}
/**
+ * Tests the 'image_convert_avif' effect when avif is supported.
+ */
+ public function testConvertAvifEffect(): void {
+ $this->container->get('keyvalue')->get('image_test')->set('avif_enabled', TRUE);
+ $this->assertImageEffect(['convert'], 'image_convert_avif', [
+ 'extension' => 'webp',
+ ]);
+
+ $calls = $this->imageTestGetAllCalls();
+ $this->assertEquals('avif', $calls['convert'][0][0]);
+ }
+
+ /**
+ * Tests the 'image_convert_avif' effect with webp fallback.
+ */
+ public function testConvertAvifEffectFallback(): void {
+ $this->assertImageEffect(['convert'], 'image_convert_avif', [
+ 'extension' => 'webp',
+ ]);
+
+ $calls = $this->imageTestGetAllCalls();
+ $this->assertEquals('webp', $calls['convert'][0][0]);
+ }
+
+ /**
* Tests the 'image_scale_and_crop' effect.
*/
public function testScaleAndCropEffect(): void {
diff --git a/core/modules/image/tests/src/Kernel/ImageItemTest.php b/core/modules/image/tests/src/Kernel/ImageItemTest.php
index 0598b25e9db8..55f2503686d4 100644
--- a/core/modules/image/tests/src/Kernel/ImageItemTest.php
+++ b/core/modules/image/tests/src/Kernel/ImageItemTest.php
@@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
@@ -19,6 +20,7 @@ use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\Tests\field\Kernel\FieldKernelTestBase;
+use Drupal\image\Plugin\Field\FieldType\ImageItem;
use Drupal\user\Entity\Role;
/**
@@ -208,6 +210,14 @@ class ImageItemTest extends FieldKernelTestBase {
}
/**
+ * Tests image URIs for empty and custom directories.
+ */
+ public function testImageUriDirectories(): void {
+ $this->validateImageUriForDirectory('', 'public://');
+ $this->validateImageUriForDirectory('custom_directory/subdir', 'public://custom_directory/subdir/');
+ }
+
+ /**
* Tests display_default.
*/
public function testDisplayDefaultValue(): void {
@@ -225,4 +235,36 @@ class ImageItemTest extends FieldKernelTestBase {
self::assertEquals(1, $form_state->getValue(['image_test', 0, 'display']));
}
+ /**
+ * Validates the image file URI generated for a given file directory.
+ *
+ * @param string $file_directory
+ * The file directory to test (e.g., empty or 'custom_directory/subdir').
+ * @param string $expected_start
+ * The expected starting string of the file URI (e.g., 'public://').
+ */
+ private function validateImageUriForDirectory(string $file_directory, string $expected_start): void {
+ // Mock the field definition with the specified file directory.
+ $definition = $this->createMock(FieldDefinitionInterface::class);
+ $definition->expects($this->any())
+ ->method('getSettings')
+ ->willReturn([
+ 'file_extensions' => 'jpg',
+ 'file_directory' => $file_directory,
+ 'uri_scheme' => 'public',
+ ]);
+ // Generate sample value and check the URI format.
+ $value = ImageItem::generateSampleValue($definition);
+ $this->assertNotEmpty($value);
+
+ // Load the file entity and get its URI.
+ $fid = $value['target_id'];
+ $file = File::load($fid);
+ $fileUri = $file->getFileUri();
+
+ // Verify the file URI starts with the expected protocol and structure.
+ $this->assertStringStartsWith($expected_start, $fileUri);
+ $this->assertMatchesRegularExpression('#^' . preg_quote($expected_start, '#') . '[^/]+#', $fileUri);
+ }
+
}
diff --git a/core/modules/image/tests/src/Unit/ImageStyleTest.php b/core/modules/image/tests/src/Unit/ImageStyleTest.php
index d247642b3cd4..8b877753fefd 100644
--- a/core/modules/image/tests/src/Unit/ImageStyleTest.php
+++ b/core/modules/image/tests/src/Unit/ImageStyleTest.php
@@ -174,8 +174,8 @@ class ImageStyleTest extends UnitTestCase {
// Assert the extension has been added to the URI before creating the token.
$this->assertEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg'));
- $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
- $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
+ $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
+ $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
// Image style that doesn't change the extension.
$image_effect_id = $this->randomMachineName();
@@ -195,8 +195,8 @@ class ImageStyleTest extends UnitTestCase {
->willReturn($hash_salt);
// Assert no extension has been added to the uri before creating the token.
$this->assertNotEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg'));
- $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
- $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':' . 'public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
+ $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
+ $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
}
/**