Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,57 @@
<?php
namespace Drupal\image\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an image effect annotation object.
*
* Plugin Namespace: Plugin\ImageEffect
*
* For a working example, see
* \Drupal\image\Plugin\ImageEffect\ResizeImageEffect
*
* @see hook_image_effect_info_alter()
* @see \Drupal\image\ConfigurableImageEffectInterface
* @see \Drupal\image\ConfigurableImageEffectBase
* @see \Drupal\image\ImageEffectInterface
* @see \Drupal\image\ImageEffectBase
* @see \Drupal\image\ImageEffectManager
* @see \Drupal\Core\ImageToolkit\Annotation\ImageToolkitOperation
* @see plugin_api
*
* @Annotation
*/
class ImageEffect extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the image effect.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* A brief description of the image effect.
*
* This property is optional and it does not need to be declared.
*
* This will be shown when adding or configuring this image effect.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description = '';
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\image\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines an ImageEffect attribute for plugin discovery.
*
* Plugin Namespace: Plugin\ImageEffect
*
* For a working example, see
* \Drupal\image\Plugin\ImageEffect\ResizeImageEffect
*
* @see hook_image_effect_info_alter()
* @see \Drupal\image\ConfigurableImageEffectInterface
* @see \Drupal\image\ConfigurableImageEffectBase
* @see \Drupal\image\ImageEffectInterface
* @see \Drupal\image\ImageEffectBase
* @see \Drupal\image\ImageEffectManager
* @see \Drupal\Core\ImageToolkit\Attribute\ImageToolkitOperation
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ImageEffect extends Plugin {
/**
* Constructs an ImageEffect attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $label
* The human-readable name of the image effect.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
* (optional) A brief description of the image effect. This will be shown
* when adding or configuring this image effect.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $label,
public readonly ?TranslatableMarkup $description = NULL,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\image;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a base class for configurable image effects.
*
* @see \Drupal\image\Annotation\ImageEffect
* @see \Drupal\image\ConfigurableImageEffectInterface
* @see \Drupal\image\ImageEffectInterface
* @see \Drupal\image\ImageEffectBase
* @see \Drupal\image\ImageEffectManager
* @see plugin_api
*/
abstract class ConfigurableImageEffectBase extends ImageEffectBase implements ConfigurableImageEffectInterface {
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Drupal\image;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Defines the interface for configurable image effects.
*
* @see \Drupal\image\Annotation\ImageEffect
* @see \Drupal\image\ConfigurableImageEffectBase
* @see \Drupal\image\ImageEffectInterface
* @see \Drupal\image\ImageEffectBase
* @see \Drupal\image\ImageEffectManager
* @see plugin_api
*/
interface ConfigurableImageEffectInterface extends ImageEffectInterface, PluginFormInterface {
}

View File

@ -0,0 +1,315 @@
<?php
namespace Drupal\image\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\system\FileDownloadController;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* Defines a controller to serve image styles.
*/
class ImageStyleDownloadController extends FileDownloadController {
/**
* The lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* The image factory.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* File system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructs an ImageStyleDownloadController object.
*
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
* The stream wrapper manager.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The system service.
*/
public function __construct(LockBackendInterface $lock, ImageFactory $image_factory, StreamWrapperManagerInterface $stream_wrapper_manager, FileSystemInterface $file_system) {
parent::__construct($stream_wrapper_manager);
$this->lock = $lock;
$this->imageFactory = $image_factory;
$this->logger = $this->getLogger('image');
$this->fileSystem = $file_system;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('lock'),
$container->get('image.factory'),
$container->get('stream_wrapper_manager'),
$container->get('file_system')
);
}
/**
* Generates a derivative, given a style and image path.
*
* After generating an image, transfer it to the requesting agent.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param string $scheme
* The file scheme, defaults to 'private'.
* @param \Drupal\image\ImageStyleInterface $image_style
* The image style to deliver.
* @param string $required_derivative_scheme
* The required scheme for the derivative image.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
* The transferred file as response or some error response.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the file request is invalid.
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown when the user does not have access to the file.
* @throws \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException
* Thrown when the file is still being generated.
*/
public function deliver(Request $request, $scheme, ImageStyleInterface $image_style, string $required_derivative_scheme) {
$target = $request->query->get('file');
$image_uri = $scheme . '://' . $target;
$image_uri = $this->streamWrapperManager->normalizeUri($image_uri);
$sample_image_uri = $scheme . '://' . $this->config('image.settings')->get('preview_image');
if ($this->streamWrapperManager->isValidScheme($scheme)) {
$normalized_target = $this->streamWrapperManager->getTarget($image_uri);
if ($normalized_target !== FALSE) {
if (!in_array($scheme, Settings::get('file_sa_core_2023_005_schemes', []))) {
$parts = explode('/', $normalized_target);
if (array_intersect($parts, ['.', '..'])) {
throw new NotFoundHttpException();
}
}
}
}
// Check that the style is defined and the scheme is valid.
$valid = !empty($image_style) && $this->streamWrapperManager->isValidScheme($scheme);
// Also validate the derivative token. Sites which require image
// derivatives to be generated without a token can set the
// 'image.settings:allow_insecure_derivatives' configuration to TRUE to
// bypass this check, but this will increase the site's vulnerability
// to denial-of-service attacks. To prevent this variable from leaving the
// site vulnerable to the most serious attacks, a token is always required
// when a derivative of a style is requested.
// The $target variable for a derivative of a style has
// styles/<style_name>/... as structure, so we check if the $target variable
// starts with styles/.
$token = $request->query->get(IMAGE_DERIVATIVE_TOKEN, '');
$token_is_valid = hash_equals($image_style->getPathToken($image_uri), $token)
|| hash_equals($image_style->getPathToken($scheme . '://' . $target), $token);
if (!$this->config('image.settings')->get('allow_insecure_derivatives') || str_starts_with(ltrim($target, '\/'), 'styles/')) {
$valid = $valid && $token_is_valid;
}
if (!$valid) {
// Return a 404 (Page Not Found) rather than a 403 (Access Denied) as the
// image token is for DDoS protection rather than access checking. 404s
// are more likely to be cached (e.g. at a proxy) which enhances
// protection from DDoS.
throw new NotFoundHttpException();
}
$derivative_uri = $image_style->buildUri($image_uri);
$derivative_scheme = $this->streamWrapperManager->getScheme($derivative_uri);
if ($required_derivative_scheme !== $derivative_scheme) {
throw new AccessDeniedHttpException("The scheme for this image doesn't match the scheme for the original image");
}
if ($token_is_valid) {
$is_public = ($scheme !== 'private');
}
else {
$core_schemes = ['public', 'private', 'temporary'];
$additional_public_schemes = array_diff(Settings::get('file_additional_public_schemes', []), $core_schemes);
$public_schemes = array_merge(['public'], $additional_public_schemes);
$is_public = in_array($derivative_scheme, $public_schemes, TRUE);
}
$headers = [];
// Don't try to generate file if source is missing.
if ($image_uri !== $sample_image_uri && !$this->sourceImageExists($image_uri, $token_is_valid)) {
// If the image style converted the extension, it has been added to the
// original file, resulting in filenames like image.png.jpeg. So to find
// the actual source image, we remove the extension and check if that
// image exists.
$converted_image_uri = static::getUriWithoutConvertedExtension($image_uri);
if ($converted_image_uri !== $image_uri &&
$this->sourceImageExists($converted_image_uri, $token_is_valid)) {
// The converted file does exist, use it as the source.
$image_uri = $converted_image_uri;
}
else {
$this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', ['%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri]);
return new Response($this->t('Error generating image, missing source file.'), 404);
}
}
// If not using a public scheme, let other modules provide headers and
// control access to the file.
if (!$is_public) {
$headers = $this->moduleHandler()->invokeAll('file_download', [$image_uri]);
if (in_array(-1, $headers) || empty($headers)) {
throw new AccessDeniedHttpException();
}
}
// If it is default sample.png, ignore scheme.
// This value swap must be done after hook_file_download is called since
// the hooks are expecting a URI, not a file path.
if ($image_uri === $sample_image_uri) {
$image_uri = $target;
}
// Don't start generating the image if the derivative already exists or if
// generation is in progress in another thread.
if (!file_exists($derivative_uri)) {
$lock_name = 'image_style_deliver:' . $image_style->id() . ':' . Crypt::hashBase64($image_uri);
$lock_acquired = $this->lock->acquire($lock_name);
if (!$lock_acquired) {
// Tell client to retry again in 3 seconds. Currently no browsers are
// known to support Retry-After.
throw new ServiceUnavailableHttpException(3, 'Image generation in progress. Try again shortly.');
}
}
// Try to generate the image, unless another thread just did it while we
// were acquiring the lock.
$success = file_exists($derivative_uri) || $image_style->createDerivative($image_uri, $derivative_uri);
if (!empty($lock_acquired)) {
$this->lock->release($lock_name);
}
if ($success) {
$image = $this->imageFactory->get($derivative_uri);
$uri = $image->getSource();
$headers += [
'Content-Type' => $image->getMimeType(),
'Content-Length' => $image->getFileSize(),
];
// \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond()
// sets response as not cacheable if the Cache-Control header is not
// already modified. When $is_public is TRUE, the following sets the
// Cache-Control header to "public".
return new BinaryFileResponse($uri, 200, $headers, $is_public);
}
else {
$this->logger->notice('Unable to generate the derived image located at %path.', ['%path' => $derivative_uri]);
return new Response($this->t('Error generating image.'), 500);
}
}
/**
* Checks whether the provided source image exists.
*
* @param string $image_uri
* The URI for the source image.
* @param bool $token_is_valid
* Whether a valid image token was supplied.
*
* @return bool
* Whether the source image exists.
*/
private function sourceImageExists(string $image_uri, bool $token_is_valid): bool {
$exists = file_exists($image_uri);
// If the file doesn't exist, we can stop here.
if (!$exists) {
return FALSE;
}
if ($token_is_valid) {
return TRUE;
}
if (StreamWrapperManager::getScheme($image_uri) !== 'public') {
return TRUE;
}
$image_path = $this->fileSystem->realpath($image_uri);
$private_path = Settings::get('file_private_path');
if ($private_path) {
$private_path = realpath($private_path);
if ($private_path && str_starts_with($image_path, $private_path)) {
return FALSE;
}
}
return TRUE;
}
/**
* Get the file URI without the extension from any conversion image style.
*
* If the image style converted the image, then an extension has been added
* to the original file, resulting in filenames like image.png.jpeg.
*
* @param string $uri
* The file URI.
*
* @return string
* The file URI without the extension from any conversion image style.
*/
public static function getUriWithoutConvertedExtension(string $uri): string {
$original_uri = $uri;
$path_info = pathinfo(StreamWrapperManager::getTarget($uri));
// Only convert the URI when the filename still has an extension.
if (!empty($path_info['filename']) && pathinfo($path_info['filename'], PATHINFO_EXTENSION)) {
$original_uri = StreamWrapperManager::getScheme($uri) . '://';
if (!empty($path_info['dirname']) && $path_info['dirname'] !== '.') {
$original_uri .= $path_info['dirname'] . DIRECTORY_SEPARATOR;
}
$original_uri .= $path_info['filename'];
}
return $original_uri;
}
}

View File

@ -0,0 +1,548 @@
<?php
namespace Drupal\image\Entity;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Action\Attribute\ActionMethod;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Routing\RequestHelper;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\image\Form\ImageStyleAddForm;
use Drupal\image\Form\ImageStyleDeleteForm;
use Drupal\image\Form\ImageStyleEditForm;
use Drupal\image\Form\ImageStyleFlushForm;
use Drupal\image\ImageEffectPluginCollection;
use Drupal\image\ImageEffectInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\image\ImageStyleListBuilder;
use Drupal\image\ImageStyleStorage;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
/**
* Defines an image style configuration entity.
*/
#[ConfigEntityType(
id: 'image_style',
label: new TranslatableMarkup('Image style'),
label_collection: new TranslatableMarkup('Image styles'),
label_singular: new TranslatableMarkup('image style'),
label_plural: new TranslatableMarkup('image styles'),
config_prefix: 'style',
entity_keys: [
'id' => 'name',
'label' => 'label',
],
handlers: [
'form' => [
'add' => ImageStyleAddForm::class,
'edit' => ImageStyleEditForm::class,
'delete' => ImageStyleDeleteForm::class,
'flush' => ImageStyleFlushForm::class,
],
'list_builder' => ImageStyleListBuilder::class,
'storage' => ImageStyleStorage::class,
],
links: [
'flush-form' => '/admin/config/media/image-styles/manage/{image_style}/flush',
'edit-form' => '/admin/config/media/image-styles/manage/{image_style}',
'delete-form' => '/admin/config/media/image-styles/manage/{image_style}/delete',
'collection' => '/admin/config/media/image-styles',
],
admin_permission: 'administer image styles',
label_count: [
'singular' => '@count image style',
'plural' => '@count image styles',
],
config_export: [
'name',
'label',
'effects',
],
)]
class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, EntityWithPluginCollectionInterface {
/**
* The name of the image style.
*
* @var string
*/
protected $name;
/**
* The image style label.
*
* @var string
*/
protected $label;
/**
* The array of image effects for this image style.
*
* @var array
*/
protected $effects = [];
/**
* Holds the collection of image effects that are used by this image style.
*
* @var \Drupal\image\ImageEffectPluginCollection
*/
protected $effectsCollection;
/**
* {@inheritdoc}
*/
public function id() {
return $this->name;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($update) {
if ($this->getOriginal() && ($this->id() !== $this->getOriginal()->id())) {
// The old image style name needs flushing after a rename.
$this->getOriginal()->flush();
// Update field settings if necessary.
if (!$this->isSyncing()) {
static::replaceImageStyle($this);
}
}
else {
// Flush image style when updating without changing the name.
$this->flush();
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
/** @var \Drupal\image\ImageStyleInterface[] $entities */
foreach ($entities as $style) {
// Flush cached media for the deleted style.
$style->flush();
// Clear the replacement ID, if one has been previously stored.
/** @var \Drupal\image\ImageStyleStorageInterface $storage */
$storage->clearReplacementId($style->id());
}
}
/**
* Update field settings if the image style name is changed.
*
* @param \Drupal\image\ImageStyleInterface $style
* The image style.
*/
protected static function replaceImageStyle(ImageStyleInterface $style) {
if ($style->id() != $style->getOriginalId()) {
// Loop through all entity displays looking for formatters / widgets using
// the image style.
foreach (EntityViewDisplay::loadMultiple() as $display) {
foreach ($display->getComponents() as $name => $options) {
if (isset($options['type']) && $options['type'] == 'image' && $options['settings']['image_style'] == $style->getOriginalId()) {
$options['settings']['image_style'] = $style->id();
$display->setComponent($name, $options)
->save();
}
}
}
foreach (EntityFormDisplay::loadMultiple() as $display) {
foreach ($display->getComponents() as $name => $options) {
if (isset($options['type']) && $options['type'] == 'image_image' && $options['settings']['preview_image_style'] == $style->getOriginalId()) {
$options['settings']['preview_image_style'] = $style->id();
$display->setComponent($name, $options)
->save();
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function buildUri($uri) {
$source_scheme = $scheme = StreamWrapperManager::getScheme($uri);
$default_scheme = $this->fileDefaultScheme();
if ($source_scheme) {
$path = StreamWrapperManager::getTarget($uri);
// The scheme of derivative image files only needs to be computed for
// source files not stored in the default scheme.
if ($source_scheme != $default_scheme) {
$class = $this->getStreamWrapperManager()->getClass($source_scheme);
$is_writable = NULL;
if ($class) {
$is_writable = $class::getType() & StreamWrapperInterface::WRITE;
}
// Compute the derivative URI scheme. Derivatives created from writable
// source stream wrappers will inherit the scheme. Derivatives created
// from read-only stream wrappers will fall-back to the default scheme.
$scheme = $is_writable ? $source_scheme : $default_scheme;
}
}
else {
$path = $uri;
$source_scheme = $scheme = $default_scheme;
}
return "$scheme://styles/{$this->id()}/$source_scheme/{$this->addExtension($path)}";
}
/**
* {@inheritdoc}
*/
public function buildUrl($path, $clean_urls = NULL) {
/** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
$uri = $stream_wrapper_manager->normalizeUri($this->buildUri($path));
// The token query is added even if the
// 'image.settings:allow_insecure_derivatives' configuration is TRUE, so
// that the emitted links remain valid if it is changed back to the default
// FALSE. However, sites which need to prevent the token query from being
// emitted at all can additionally set the
// 'image.settings:suppress_itok_output' configuration to TRUE to achieve
// that (if both are set, the security token will neither be emitted in the
// image derivative URL nor checked for in
// \Drupal\image\ImageStyleInterface::deliver()).
$token_query = [];
if (!\Drupal::config('image.settings')->get('suppress_itok_output')) {
// The passed $path variable can be either a relative path or a full URI.
if (!$stream_wrapper_manager::getScheme($path)) {
$path = \Drupal::config('system.file')->get('default_scheme') . '://' . $path;
}
$original_uri = $stream_wrapper_manager->normalizeUri($path);
$token_query = [IMAGE_DERIVATIVE_TOKEN => $this->getPathToken($original_uri)];
}
if ($clean_urls === NULL) {
// Assume clean URLs unless the request tells us otherwise.
$clean_urls = TRUE;
try {
$request = \Drupal::request();
$clean_urls = RequestHelper::isCleanUrl($request);
}
catch (ServiceNotFoundException) {
}
}
// If not using clean URLs, the image derivative callback is only available
// with the script path. If the file does not exist, use Url::fromUri() to
// ensure that it is included. Once the file exists it's fine to fall back
// to the actual file path, this avoids bootstrapping PHP once the files are
// built.
if ($clean_urls === FALSE && $stream_wrapper_manager::getScheme($uri) == 'public' && !file_exists($uri)) {
$directory_path = $stream_wrapper_manager->getViaUri($uri)->getDirectoryPath();
return Url::fromUri('base:' . $directory_path . '/' . $stream_wrapper_manager::getTarget($uri), ['absolute' => TRUE, 'query' => $token_query])->toString();
}
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
$file_url = $file_url_generator->generateAbsoluteString($uri);
// Append the query string with the token, if necessary.
if ($token_query) {
$file_url .= (str_contains($file_url, '?') ? '&' : '?') . UrlHelper::buildQuery($token_query);
}
return $file_url;
}
/**
* {@inheritdoc}
*/
public function flush($path = NULL) {
// A specific image path has been provided. Flush only that derivative.
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
if (isset($path)) {
$derivative_uri = $this->buildUri($path);
if (file_exists($derivative_uri)) {
try {
$file_system->delete($derivative_uri);
}
catch (FileException) {
// Ignore failed deletes.
}
}
}
else {
// Delete the style directory in each registered wrapper.
$wrappers = $this->getStreamWrapperManager()->getWrappers(StreamWrapperInterface::WRITE_VISIBLE);
foreach ($wrappers as $wrapper => $wrapper_data) {
if (file_exists($directory = $wrapper . '://styles/' . $this->id())) {
try {
$file_system->deleteRecursive($directory);
}
catch (FileException) {
// Ignore failed deletes.
}
}
}
}
// Let other modules update as necessary on flush.
$module_handler = \Drupal::moduleHandler();
$module_handler->invokeAll('image_style_flush', [$this, $path]);
// Clear caches when the complete image style is flushed,
// so that field formatters may be added for this style.
if (!isset($path)) {
\Drupal::service('theme.registry')->reset();
Cache::invalidateTags($this->getCacheTagsToInvalidate());
}
return $this;
}
/**
* {@inheritdoc}
*/
public function createDerivative($original_uri, $derivative_uri) {
// If the source file doesn't exist, return FALSE without creating folders.
$image = $this->getImageFactory()->get($original_uri);
if (!$image->isValid()) {
return FALSE;
}
// Get the folder for the final location of this style.
$directory = \Drupal::service('file_system')->dirname($derivative_uri);
// Build the destination folder tree if it doesn't already exist.
if (!\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
\Drupal::logger('image')->error('Failed to create style directory: %directory', ['%directory' => $directory]);
return FALSE;
}
foreach ($this->getEffects() as $effect) {
$effect->applyEffect($image);
}
if (!$image->save($derivative_uri)) {
if (file_exists($derivative_uri)) {
\Drupal::logger('image')->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', ['%destination' => $derivative_uri]);
}
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function transformDimensions(array &$dimensions, $uri) {
foreach ($this->getEffects() as $effect) {
$effect->transformDimensions($dimensions, $uri);
}
}
/**
* {@inheritdoc}
*/
public function getDerivativeExtension($extension) {
foreach ($this->getEffects() as $effect) {
$extension = $effect->getDerivativeExtension($extension);
}
return $extension;
}
/**
* {@inheritdoc}
*/
public function getPathToken($uri) {
// Return the first 8 characters.
return substr(Crypt::hmacBase64($this->id() . ':' . $this->addExtension($uri), $this->getPrivateKey() . $this->getHashSalt()), 0, 8);
}
/**
* {@inheritdoc}
*/
public function deleteImageEffect(ImageEffectInterface $effect) {
$this->getEffects()->removeInstanceId($effect->getUuid());
$this->save();
return $this;
}
/**
* {@inheritdoc}
*/
public function supportsUri($uri) {
// Only support the URI if its extension is supported by the current image
// toolkit.
return in_array(
mb_strtolower(pathinfo($uri, PATHINFO_EXTENSION)),
$this->getImageFactory()->getSupportedExtensions()
);
}
/**
* {@inheritdoc}
*/
public function getEffect($effect) {
return $this->getEffects()->get($effect);
}
/**
* {@inheritdoc}
*/
public function getEffects() {
if (!$this->effectsCollection) {
$this->effectsCollection = new ImageEffectPluginCollection($this->getImageEffectPluginManager(), $this->effects);
$this->effectsCollection->sort();
}
return $this->effectsCollection;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return ['effects' => $this->getEffects()];
}
/**
* {@inheritdoc}
*/
#[ActionMethod(adminLabel: new TranslatableMarkup('Add an image effect'))]
public function addImageEffect(array $configuration) {
$configuration['uuid'] = $this->uuidGenerator()->generate();
$this->getEffects()->addInstanceId($configuration['uuid'], $configuration);
return $configuration['uuid'];
}
/**
* {@inheritdoc}
*/
public function getReplacementID() {
/** @var \Drupal\image\ImageStyleStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
return $storage->getReplacementId($this->id());
}
/**
* {@inheritdoc}
*/
public function getName() {
return $this->get('name');
}
/**
* {@inheritdoc}
*/
public function setName($name) {
$this->set('name', $name);
return $this;
}
/**
* Returns the image effect plugin manager.
*
* @return \Drupal\Component\Plugin\PluginManagerInterface
* The image effect plugin manager.
*/
protected function getImageEffectPluginManager() {
return \Drupal::service('plugin.manager.image.effect');
}
/**
* Returns the image factory.
*
* @return \Drupal\Core\Image\ImageFactory
* The image factory.
*/
protected function getImageFactory() {
return \Drupal::service('image.factory');
}
/**
* Gets the Drupal private key.
*
* @return string
* The Drupal private key.
*/
protected function getPrivateKey() {
return \Drupal::service('private_key')->get();
}
/**
* Gets a salt useful for hardening against SQL injection.
*
* @return string
* A salt based on information in settings.php, not in the database.
*
* @throws \RuntimeException
*/
protected function getHashSalt() {
return Settings::getHashSalt();
}
/**
* Adds an extension to a path.
*
* If this image style changes the extension of the derivative, this method
* adds the new extension to the given path. This way we avoid filename
* clashes while still allowing us to find the source image.
*
* @param string $path
* The path to add the extension to.
*
* @return string
* The given path if this image style doesn't change its extension, or the
* path with the added extension if it does.
*/
protected function addExtension($path) {
$original_extension = pathinfo($path, PATHINFO_EXTENSION);
$extension = $this->getDerivativeExtension($original_extension);
if ($original_extension !== $extension) {
$path .= '.' . $extension;
}
return $path;
}
/**
* Provides a wrapper to allow unit testing.
*
* Gets the default file stream implementation.
*
* @return string
* 'public', 'private' or any other file scheme defined as the default.
*/
protected function fileDefaultScheme() {
return \Drupal::config('system.file')->get('default_scheme');
}
/**
* Gets the stream wrapper manager service.
*
* @return \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
* The stream wrapper manager service
*
* @todo Properly inject this service in Drupal 9.0.x.
*/
protected function getStreamWrapperManager() {
return \Drupal::service('stream_wrapper_manager');
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\image\ImageEffectManager;
use Drupal\image\ImageStyleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an add form for image effects.
*
* @internal
*/
class ImageEffectAddForm extends ImageEffectFormBase {
/**
* The image effect manager.
*
* @var \Drupal\image\ImageEffectManager
*/
protected $effectManager;
/**
* Constructs a new ImageEffectAddForm.
*
* @param \Drupal\image\ImageEffectManager $effect_manager
* The image effect manager.
*/
public function __construct(ImageEffectManager $effect_manager) {
$this->effectManager = $effect_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.image.effect')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
$form = parent::buildForm($form, $form_state, $image_style, $image_effect);
$form['#title'] = $this->t('Add %label effect to style %style', [
'%label' => $this->imageEffect->label(),
'%style' => $image_style->label(),
]);
$form['actions']['submit']['#value'] = $this->t('Add effect');
return $form;
}
/**
* {@inheritdoc}
*/
protected function prepareImageEffect($image_effect) {
$image_effect = $this->effectManager->createInstance($image_effect);
// Set the initial weight so this effect comes last.
$image_effect->setWeight(count($this->imageStyle->getEffects()));
return $image_effect;
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\image\ImageStyleInterface;
/**
* Form for deleting an image effect.
*
* @internal
*/
class ImageEffectDeleteForm extends ConfirmFormBase {
/**
* The image style containing the image effect to be deleted.
*
* @var \Drupal\image\ImageStyleInterface
*/
protected $imageStyle;
/**
* The image effect to be deleted.
*
* @var \Drupal\image\ImageEffectInterface
*/
protected $imageEffect;
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the @effect effect from the %style style?', ['%style' => $this->imageStyle->label(), '@effect' => $this->imageEffect->label()]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->imageStyle->toUrl('edit-form');
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'image_effect_delete_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
$this->imageStyle = $image_style;
$this->imageEffect = $this->imageStyle->getEffect($image_effect);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->imageStyle->deleteImageEffect($this->imageEffect);
$this->messenger()->addStatus($this->t('The image effect %name has been deleted.', ['%name' => $this->imageEffect->label()]));
$form_state->setRedirectUrl($this->imageStyle->toUrl('edit-form'));
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\image\ImageStyleInterface;
/**
* Provides an edit form for image effects.
*
* @internal
*/
class ImageEffectEditForm extends ImageEffectFormBase {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
$form = parent::buildForm($form, $form_state, $image_style, $image_effect);
$form['#title'] = $this->t('Edit %label effect on style %style', [
'%label' => $this->imageEffect->label(),
'%style' => $image_style->label(),
]);
$form['actions']['submit']['#value'] = $this->t('Update effect');
return $form;
}
/**
* {@inheritdoc}
*/
protected function prepareImageEffect($image_effect) {
return $this->imageStyle->getEffect($image_effect);
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\image\ConfigurableImageEffectInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides a base form for image effects.
*/
abstract class ImageEffectFormBase extends FormBase {
/**
* The image style.
*
* @var \Drupal\image\ImageStyleInterface
*/
protected $imageStyle;
/**
* The image effect.
*
* @var \Drupal\image\ImageEffectInterface|\Drupal\image\ConfigurableImageEffectInterface
*/
protected $imageEffect;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'image_effect_form';
}
/**
* {@inheritdoc}
*
* @param array $form
* A nested array form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\image\ImageStyleInterface $image_style
* The image style.
* @param string $image_effect
* The image effect ID.
*
* @return array
* The form structure.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function buildForm(array $form, FormStateInterface $form_state, ?ImageStyleInterface $image_style = NULL, $image_effect = NULL) {
$this->imageStyle = $image_style;
try {
$this->imageEffect = $this->prepareImageEffect($image_effect);
}
catch (PluginNotFoundException) {
throw new NotFoundHttpException("Invalid effect id: '$image_effect'.");
}
$request = $this->getRequest();
if (!($this->imageEffect instanceof ConfigurableImageEffectInterface)) {
throw new NotFoundHttpException();
}
$form['#attached']['library'][] = 'image/admin';
$form['uuid'] = [
'#type' => 'value',
'#value' => $this->imageEffect->getUuid(),
];
$form['id'] = [
'#type' => 'value',
'#value' => $this->imageEffect->getPluginId(),
];
$form['data'] = [];
$subform_state = SubformState::createForSubform($form['data'], $form, $form_state);
$form['data'] = $this->imageEffect->buildConfigurationForm($form['data'], $subform_state);
$form['data']['#tree'] = TRUE;
// Check the URL for a weight, then the image effect, otherwise use default.
$form['weight'] = [
'#type' => 'hidden',
'#value' => $request->query->has('weight') ? (int) $request->query->get('weight') : $this->imageEffect->getWeight(),
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#button_type' => 'primary',
];
$form['actions']['cancel'] = [
'#type' => 'link',
'#title' => $this->t('Cancel'),
'#url' => $this->imageStyle->toUrl('edit-form'),
'#attributes' => ['class' => ['button']],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// The image effect configuration is stored in the 'data' key in the form,
// pass that through for validation.
$this->imageEffect->validateConfigurationForm($form['data'], SubformState::createForSubform($form['data'], $form, $form_state));
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->cleanValues();
// The image effect configuration is stored in the 'data' key in the form,
// pass that through for submission.
$this->imageEffect->submitConfigurationForm($form['data'], SubformState::createForSubform($form['data'], $form, $form_state));
$this->imageEffect->setWeight($form_state->getValue('weight'));
if ($uuid = $this->imageEffect->getUuid()) {
$this->imageStyle->getEffect($uuid)->setConfiguration($this->imageEffect->getConfiguration());
}
else {
$this->imageStyle->addImageEffect($this->imageEffect->getConfiguration());
}
$this->imageStyle->save();
$this->messenger()->addStatus($this->t('The image effect was successfully applied.'));
$form_state->setRedirectUrl($this->imageStyle->toUrl('edit-form'));
}
/**
* Converts an image effect ID into an object.
*
* @param string $image_effect
* The image effect ID.
*
* @return \Drupal\image\ImageEffectInterface
* The image effect object.
*/
abstract protected function prepareImageEffect($image_effect);
}

View File

@ -0,0 +1,32 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Controller for image style addition forms.
*
* @internal
*/
class ImageStyleAddForm extends ImageStyleFormBase {
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->messenger()->addStatus($this->t('Style %name was created.', ['%name' => $this->entity->label()]));
}
/**
* {@inheritdoc}
*/
public function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Create new style');
return $actions;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Creates a form to delete an image style.
*
* @internal
*/
class ImageStyleDeleteForm extends EntityDeleteForm {
/**
* Replacement options.
*
* @var array
*/
protected $replacementOptions;
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Optionally select a style before deleting %style', ['%style' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if (count($this->getReplacementOptions()) > 1) {
return $this->t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted. If no replacement style is selected, the dependent configurations might need manual reconfiguration.');
}
return $this->t('All images that have been generated for this style will be permanently deleted. The dependent configurations might need manual reconfiguration.');
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$replacement_styles = $this->getReplacementOptions();
// If there are non-empty options in the list, allow the user to optionally
// pick up a replacement.
if (count($replacement_styles) > 1) {
$form['replacement'] = [
'#type' => 'select',
'#title' => $this->t('Replacement style'),
'#options' => $replacement_styles,
'#empty_option' => $this->t('- No replacement -'),
'#weight' => -5,
];
}
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Save a selected replacement in the image style storage. It will be used
// later, in the same request, when resolving dependencies.
if ($replacement = $form_state->getValue('replacement')) {
/** @var \Drupal\image\ImageStyleStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($this->entity->getEntityTypeId());
$storage->setReplacementId($this->entity->id(), $replacement);
}
parent::submitForm($form, $form_state);
}
/**
* Returns a list of image style replacement options.
*
* @return array
* An option list suitable for the form select '#options'.
*/
protected function getReplacementOptions() {
if (!isset($this->replacementOptions)) {
$this->replacementOptions = array_diff_key(image_style_options(), [$this->getEntity()->id() => '']);
}
return $this->replacementOptions;
}
}

View File

@ -0,0 +1,276 @@
<?php
namespace Drupal\image\Form;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\image\ConfigurableImageEffectInterface;
use Drupal\image\ImageEffectManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller for image style edit form.
*
* @internal
*/
class ImageStyleEditForm extends ImageStyleFormBase {
/**
* The image effect manager service.
*
* @var \Drupal\image\ImageEffectManager
*/
protected $imageEffectManager;
/**
* Constructs an ImageStyleEditForm object.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
* The storage.
* @param \Drupal\image\ImageEffectManager $image_effect_manager
* The image effect manager service.
*/
public function __construct(EntityStorageInterface $image_style_storage, ImageEffectManager $image_effect_manager) {
parent::__construct($image_style_storage);
$this->imageEffectManager = $image_effect_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('image_style'),
$container->get('plugin.manager.image.effect')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$user_input = $form_state->getUserInput();
$form['#title'] = $this->t('Edit style %name', ['%name' => $this->entity->label()]);
$form['#tree'] = TRUE;
$form['#attached']['library'][] = 'image/admin';
// Show the thumbnail preview.
$preview_arguments = ['#theme' => 'image_style_preview', '#style' => $this->entity];
$form['preview'] = [
'#type' => 'item',
'#title' => $this->t('Preview (Click for actual images)'),
'#markup' => \Drupal::service('renderer')->render($preview_arguments),
// Render preview above parent elements.
'#weight' => -5,
];
// Build the list of existing image effects for this image style.
$form['effects'] = [
'#type' => 'table',
'#header' => [
$this->t('Effect'),
$this->t('Weight'),
$this->t('Operations'),
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'image-effect-order-weight',
],
],
'#attributes' => [
'id' => 'image-style-effects',
],
'#empty' => $this->t('There are currently no effects in this style. Add one by selecting an option below.'),
// Render effects below parent elements.
'#weight' => 5,
];
foreach ($this->entity->getEffects() as $effect) {
$key = $effect->getUuid();
$form['effects'][$key]['#attributes']['class'][] = 'draggable';
$form['effects'][$key]['#weight'] = isset($user_input['effects']) ? $user_input['effects'][$key]['weight'] : NULL;
$form['effects'][$key]['effect'] = [
'#tree' => FALSE,
'data' => [
'label' => [
'#plain_text' => $effect->label(),
],
],
];
$summary = $effect->getSummary();
if (!empty($summary)) {
$summary['#prefix'] = ' ';
$form['effects'][$key]['effect']['data']['summary'] = $summary;
}
$form['effects'][$key]['weight'] = [
'#type' => 'weight',
'#title' => $this->t('Weight for @title', ['@title' => $effect->label()]),
'#title_display' => 'invisible',
'#default_value' => $effect->getWeight(),
'#attributes' => [
'class' => ['image-effect-order-weight'],
],
];
$links = [];
$is_configurable = $effect instanceof ConfigurableImageEffectInterface;
if ($is_configurable) {
$links['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('image.effect_edit_form', [
'image_style' => $this->entity->id(),
'image_effect' => $key,
]),
];
}
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('image.effect_delete', [
'image_style' => $this->entity->id(),
'image_effect' => $key,
]),
];
$form['effects'][$key]['operations'] = [
'#type' => 'operations',
'#links' => $links,
];
}
// Build the new image effect addition form and add it to the effect list.
$new_effect_options = [];
$effects = $this->imageEffectManager->getDefinitions();
uasort($effects, function ($a, $b) {
return Unicode::strcasecmp($a['label'], $b['label']);
});
foreach ($effects as $effect => $definition) {
$new_effect_options[$effect] = $definition['label'];
}
$form['effects']['new'] = [
'#tree' => FALSE,
'#weight' => $user_input['weight'] ?? NULL,
'#attributes' => ['class' => ['draggable']],
];
$form['effects']['new']['effect'] = [
'data' => [
'new' => [
'#type' => 'select',
'#title' => $this->t('Effect'),
'#title_display' => 'invisible',
'#options' => $new_effect_options,
'#empty_option' => $this->t('Select a new effect'),
],
[
'add' => [
'#type' => 'submit',
'#value' => $this->t('Add'),
'#validate' => ['::effectValidate'],
'#submit' => ['::submitForm', '::effectSave'],
],
],
],
'#prefix' => '<div class="image-style-new">',
'#suffix' => '</div>',
];
$form['effects']['new']['weight'] = [
'#type' => 'weight',
'#title' => $this->t('Weight for new effect'),
'#title_display' => 'invisible',
'#default_value' => count($this->entity->getEffects()) + 1,
'#attributes' => ['class' => ['image-effect-order-weight']],
];
$form['effects']['new']['operations'] = [
'data' => [],
];
return parent::form($form, $form_state);
}
/**
* Validate handler for image effect.
*/
public function effectValidate($form, FormStateInterface $form_state) {
if (!$form_state->getValue('new')) {
$form_state->setErrorByName('new', $this->t('Select an effect to add.'));
}
}
/**
* Submit handler for image effect.
*/
public function effectSave($form, FormStateInterface $form_state) {
$this->save($form, $form_state);
// Check if this field has any configuration options.
$effect = $this->imageEffectManager->getDefinition($form_state->getValue('new'));
// Load the configuration form for this option.
if (is_subclass_of($effect['class'], '\Drupal\image\ConfigurableImageEffectInterface')) {
$form_state->setRedirect(
'image.effect_add_form',
[
'image_style' => $this->entity->id(),
'image_effect' => $form_state->getValue('new'),
],
['query' => ['weight' => $form_state->getValue('weight')]]
);
$form_state->setIgnoreDestination();
}
// If there's no form, immediately add the image effect.
else {
$effect = [
'id' => $effect['id'],
'data' => [],
'weight' => $form_state->getValue('weight'),
];
$effect_id = $this->entity->addImageEffect($effect);
$this->entity->save();
if (!empty($effect_id)) {
$this->messenger()->addStatus($this->t('The image effect was successfully applied.'));
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Update image effect weights.
if (!$form_state->isValueEmpty('effects')) {
$this->updateEffectWeights($form_state->getValue('effects'));
}
parent::submitForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
parent::save($form, $form_state);
$this->messenger()->addStatus($this->t('Changes to the style have been saved.'));
}
/**
* Updates image effect weights.
*
* @param array $effects
* Associative array with effects having effect uuid as keys and array
* with effect data as values.
*/
protected function updateEffectWeights(array $effects) {
foreach ($effects as $uuid => $effect_data) {
if ($this->entity->getEffects()->has($uuid)) {
$this->entity->getEffect($uuid)->setWeight($effect_data['weight']);
}
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for image style flush.
*
* @internal
*/
class ImageStyleFlushForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to apply the updated %name image effect to all images?', ['%name' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('This operation does not change the original images but the copies created for this style will be recreated.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Flush');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl('collection');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->flush();
$this->messenger()->addStatus($this->t('The image style %name has been flushed.', ['%name' => $this->entity->label()]));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace Drupal\image\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form for image style add and edit forms.
*/
abstract class ImageStyleFormBase extends EntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\image\ImageStyleInterface
*/
protected $entity;
/**
* The image style entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $imageStyleStorage;
/**
* Constructs a base class for image style add and edit forms.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
* The image style entity storage.
*/
public function __construct(EntityStorageInterface $image_style_storage) {
$this->imageStyleStorage = $image_style_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('image_style')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Image style name'),
'#default_value' => $this->entity->label(),
'#required' => TRUE,
];
$form['name'] = [
'#type' => 'machine_name',
'#machine_name' => [
'exists' => [$this->imageStyleStorage, 'load'],
],
'#default_value' => $this->entity->id(),
'#required' => TRUE,
];
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
parent::save($form, $form_state);
$form_state->setRedirectUrl($this->entity->toUrl('edit-form'));
}
}

View File

@ -0,0 +1,386 @@
<?php
namespace Drupal\image\Hook;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field\FieldConfigInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\file\FileInterface;
use Drupal\image\Controller\ImageStyleDownloadController;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for image.
*/
class ImageHooks {
use StringTranslationTrait;
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.image':
$field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
$output = '';
$output .= '<h2>' . $this->t('About') . '</h2>';
$output .= '<p>' . $this->t('The Image module allows you to create fields that contain image files and to configure <a href=":image_styles">Image styles</a> that can be used to manipulate the display of images. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for terminology and general information on entities, fields, and how to create and manage fields. For more information, see the <a href=":image_documentation">online documentation for the Image module</a>.', [
':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(),
':field' => Url::fromRoute('help.page', [
'name' => 'field',
])->toString(),
':field_ui' => $field_ui_url,
':image_documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/image-module/working-with-images',
]) . '</p>';
$output .= '<h2>' . $this->t('Uses') . '</h2>';
$output .= '<dt>' . $this->t('Defining image styles') . '</dt>';
$output .= '<dd>' . $this->t('The concept of image styles is that you can upload a single image but display it in several ways; each display variation, or <em>image style</em>, is the result of applying one or more <em>effects</em> to the original image. As an example, you might upload a high-resolution image with a 4:3 aspect ratio, and display it scaled down, square cropped, or black-and-white (or any combination of these effects). The Image module provides a way to do this efficiently: you configure an image style with the desired effects on the <a href=":image">Image styles page</a>, and the first time a particular image is requested in that style, the effects are applied. The resulting image is saved, and the next time that same style is requested, the saved image is retrieved without the need to recalculate the effects. Drupal core provides several effects that you can use to define styles; others may be provided by contributed modules.', [
':image' => Url::fromRoute('entity.image_style.collection')->toString(),
]);
$output .= '<dt>' . $this->t('Naming image styles') . '</dt>';
$output .= '<dd>' . $this->t('When you define an image style, you will need to choose a displayed name and a machine name. The displayed name is shown in administrative pages, and the machine name is used to generate the URL for accessing an image processed in that style. There are two common approaches to naming image styles: either based on the effects being applied (for example, <em>Square 85x85</em>), or based on where you plan to use it (for example, <em>Profile picture</em>).') . '</dd>';
$output .= '<dt>' . $this->t('Configuring image fields') . '</dt>';
$output .= '<dd>' . $this->t('A few of the settings for image fields are defined once when you create the field and cannot be changed later; these include the choice of public or private file storage and the number of images that can be stored in the field. The rest of the settings can be edited later; these settings include the field label, help text, allowed file extensions, image dimensions restrictions, and the subdirectory in the public or private file storage where the images will be stored. The editable settings can also have different values for different entity sub-types; for instance, if your image field is used on both Page and Article content types, you can store the files in a different subdirectory for the two content types.') . '</dd>';
$output .= '<dd>' . $this->t('For accessibility and search engine optimization, all images that convey meaning on websites should have alternate text. Drupal also allows entry of title text for images, but it can lead to confusion for screen reader users and its use is not recommended. Image fields can be configured so that alternate and title text fields are enabled or disabled; if enabled, the fields can be set to be required. The recommended setting is to enable and require alternate text and disable title text.') . '</dd>';
$output .= '<dd>' . $this->t('When you create an image field, you will need to choose whether the uploaded images will be stored in the public or private file directory defined in your settings.php file and shown on the <a href=":file-system">File system page</a>. This choice cannot be changed later. You can also configure your field to store files in a subdirectory of the public or private directory; this setting can be changed later and can be different for each entity sub-type using the field. For more information on file storage, see the <a href=":system-help">System module help page</a>.', [
':file-system' => Url::fromRoute('system.file_system_settings')->toString(),
':system-help' => Url::fromRoute('help.page', [
'name' => 'system',
])->toString(),
]) . '</dd>';
$output .= '<dd>' . $this->t('The maximum file size that can be uploaded is limited by PHP settings of the server, but you can restrict it further by configuring a <em>Maximum upload size</em> in the field settings (this setting can be changed later). The maximum file size, either from PHP server settings or field configuration, is automatically displayed to users in the help text of the image field.') . '</dd>';
$output .= '<dd>' . $this->t('You can also configure a minimum and/or maximum dimensions for uploaded images. Images that are too small will be rejected. Images that are to large will be resized. During the resizing the <a href="http://wikipedia.org/wiki/Exchangeable_image_file_format">EXIF data</a> in the image will be lost.') . '</dd>';
$output .= '<dd>' . $this->t('You can also configure a default image that will be used if no image is uploaded in an image field. This default can be defined for all instances of the field in the field storage settings when you create a field, and the setting can be overridden for each entity sub-type that uses the field.') . '</dd>';
$output .= '<dt>' . $this->t('Configuring displays and form displays') . '</dt>';
$output .= '<dd>' . $this->t('On the <em>Manage display</em> page, you can choose the image formatter, which determines the image style used to display the image in each display mode and whether or not to display the image as a link. On the <em>Manage form display</em> page, you can configure the image upload widget, including setting the preview image style shown on the entity edit form.') . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.image_style.collection':
return '<p>' . $this->t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '</p>';
case 'image.effect_add_form':
$effect = \Drupal::service('plugin.manager.image.effect')->getDefinition($route_match->getParameter('image_effect'));
return isset($effect['description']) ? '<p>' . $effect['description'] . '</p>' : NULL;
case 'image.effect_edit_form':
$effect = $route_match->getParameter('image_style')->getEffect($route_match->getParameter('image_effect'));
$effect_definition = $effect->getPluginDefinition();
return isset($effect_definition['description']) ? '<p>' . $effect_definition['description'] . '</p>' : NULL;
}
return NULL;
}
/**
* Implements hook_theme().
*/
#[Hook('theme')]
public function theme() : array {
return [
// Theme functions in image.module.
'image_style' => [
// HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft
// allows the alt attribute to be omitted in some cases. Therefore,
// default the alt attribute to an empty string, but allow code using
// '#theme' => 'image_style' to pass explicit NULL for it to be omitted.
// Usually, neither omission nor an empty string satisfies accessibility
// requirements, so it is strongly encouraged for code using '#theme' =>
// 'image_style' to pass a meaningful value for the alt variable.
// - https://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
// - https://www.w3.org/TR/xhtml1/dtds.html
// - http://dev.w3.org/html5/spec/Overview.html#alt
// The title attribute is optional in all cases, so it is omitted by
// default.
'variables' => [
'style_name' => NULL,
'uri' => NULL,
'width' => NULL,
'height' => NULL,
'alt' => '',
'title' => NULL,
'attributes' => [],
],
],
// Theme functions in image.admin.inc.
'image_style_preview' => [
'variables' => [
'style' => NULL,
],
'file' => 'image.admin.inc',
],
'image_anchor' => [
'render element' => 'element',
'file' => 'image.admin.inc',
],
'image_resize_summary' => [
'variables' => [
'data' => NULL,
'effect' => [],
],
],
'image_scale_summary' => [
'variables' => [
'data' => NULL,
'effect' => [],
],
],
'image_crop_summary' => [
'variables' => [
'data' => NULL,
'effect' => [],
],
],
'image_scale_and_crop_summary' => [
'variables' => [
'data' => NULL,
'effect' => [],
],
],
'image_rotate_summary' => [
'variables' => [
'data' => NULL,
'effect' => [],
],
],
// Theme functions in image.field.inc.
'image_widget' => [
'render element' => 'element',
'file' => 'image.field.inc',
],
'image_formatter' => [
'variables' => [
'item' => NULL,
'item_attributes' => NULL,
'url' => NULL,
'image_style' => NULL,
],
'file' => 'image.field.inc',
],
];
}
/**
* Implements hook_file_download().
*
* Control the access to files underneath the styles directory.
*/
#[Hook('file_download')]
public function fileDownload($uri): array|int|null {
$path = StreamWrapperManager::getTarget($uri);
// Private file access for image style derivatives.
if (str_starts_with($path, 'styles/')) {
$args = explode('/', $path);
// Discard "styles", style name, and scheme from the path
$args = array_slice($args, 3);
// Then the remaining parts are the path to the image.
$original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
// Check that the file exists and is an image.
$image = \Drupal::service('image.factory')->get($uri);
if ($image->isValid()) {
// If the image style converted the extension, it has been added to the
// original file, resulting in filenames like image.png.jpeg. So to find
// the actual source image, we remove the extension and check if that
// image exists.
if (!file_exists($original_uri)) {
$converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
// The converted file does exist, use it as the source.
$original_uri = $converted_original_uri;
}
}
// Check the permissions of the original to grant access to this image.
$headers = \Drupal::moduleHandler()->invokeAll('file_download', [$original_uri]);
// Confirm there's at least one module granting access and none denying
// access.
if (!empty($headers) && !in_array(-1, $headers)) {
return [
// Send headers describing the image's size, and MIME-type.
'Content-Type' => $image->getMimeType(),
'Content-Length' => $image->getFileSize(),
];
}
}
return -1;
}
// If it is the sample image we need to grant access.
$samplePath = \Drupal::config('image.settings')->get('preview_image');
if ($path === $samplePath) {
$image = \Drupal::service('image.factory')->get($samplePath);
return [
// Send headers describing the image's size, and MIME-type.
'Content-Type' => $image->getMimeType(),
'Content-Length' => $image->getFileSize(),
];
}
return NULL;
}
/**
* Implements hook_file_move().
*/
#[Hook('file_move')]
public function fileMove(FileInterface $file, FileInterface $source): void {
// Delete any image derivatives at the original image path.
image_path_flush($source->getFileUri());
}
/**
* Implements hook_ENTITY_TYPE_predelete() for file entities.
*/
#[Hook('file_predelete')]
public function filePredelete(FileInterface $file): void {
// Delete any image derivatives of this image.
image_path_flush($file->getFileUri());
}
/**
* Implements hook_entity_presave().
*
* Transforms default image of image field from array into single value at
* save.
*/
#[Hook('entity_presave')]
public function entityPresave(EntityInterface $entity): void {
// Get the default image settings, return if not saving an image field
// storage or image field entity.
$default_image = [];
if (($entity instanceof FieldStorageConfigInterface || $entity instanceof FieldConfigInterface) && $entity->getType() == 'image') {
$default_image = $entity->getSetting('default_image');
}
else {
return;
}
if ($entity->isSyncing()) {
return;
}
$uuid = $default_image['uuid'];
if ($uuid) {
$original_uuid = $entity->getOriginal()?->getSetting('default_image')['uuid'];
if ($uuid != $original_uuid) {
$file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid);
if ($file) {
$image = \Drupal::service('image.factory')->get($file->getFileUri());
$default_image['width'] = $image->getWidth();
$default_image['height'] = $image->getHeight();
}
else {
$default_image['uuid'] = NULL;
}
}
}
// Both FieldStorageConfigInterface and FieldConfigInterface have a
// setSetting() method.
$entity->setSetting('default_image', $default_image);
}
/**
* Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
*/
#[Hook('field_storage_config_update')]
public function fieldStorageConfigUpdate(FieldStorageConfigInterface $field_storage): void {
if ($field_storage->getType() != 'image') {
// Only act on image fields.
return;
}
$prior_field_storage = $field_storage->getOriginal();
// The value of a managed_file element can be an array if #extended == TRUE.
$uuid_new = $field_storage->getSetting('default_image')['uuid'];
$uuid_old = $prior_field_storage->getSetting('default_image')['uuid'];
$file_new = $uuid_new ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_new) : FALSE;
if ($uuid_new != $uuid_old) {
// Is there a new file?
if ($file_new) {
$file_new->setPermanent();
$file_new->save();
\Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field_storage->uuid());
}
// Is there an old file?
if ($uuid_old && ($file_old = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_old))) {
\Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field_storage->uuid());
}
}
// If the upload destination changed, then move the file.
if ($file_new && StreamWrapperManager::getScheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme')) {
$directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
\Drupal::service('file.repository')->move($file_new, $directory . $file_new->getFilename());
}
}
/**
* Implements hook_ENTITY_TYPE_update() for 'field_config'.
*/
#[Hook('field_config_update')]
public function fieldConfigUpdate(FieldConfigInterface $field): void {
$field_storage = $field->getFieldStorageDefinition();
if ($field_storage->getType() != 'image') {
// Only act on image fields.
return;
}
$prior_instance = $field->getOriginal();
$uuid_new = $field->getSetting('default_image')['uuid'];
$uuid_old = $prior_instance->getSetting('default_image')['uuid'];
// If the old and new files do not match, update the default accordingly.
$file_new = $uuid_new ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_new) : FALSE;
if ($uuid_new != $uuid_old) {
// Save the new file, if present.
if ($file_new) {
$file_new->setPermanent();
$file_new->save();
\Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field->uuid());
}
// Delete the old file, if present.
if ($uuid_old && ($file_old = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_old))) {
\Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field->uuid());
}
}
// If the upload destination changed, then move the file.
if ($file_new && StreamWrapperManager::getScheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme')) {
$directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
\Drupal::service('file.repository')->move($file_new, $directory . $file_new->getFilename());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
*/
#[Hook('field_storage_config_delete')]
public function fieldStorageConfigDelete(FieldStorageConfigInterface $field): void {
if ($field->getType() != 'image') {
// Only act on image fields.
return;
}
// The value of a managed_file element can be an array if #extended == TRUE.
$uuid = $field->getSetting('default_image')['uuid'];
if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
\Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'field_config'.
*/
#[Hook('field_config_delete')]
public function fieldConfigDelete(FieldConfigInterface $field): void {
$field_storage = $field->getFieldStorageDefinition();
if ($field_storage->getType() != 'image') {
// Only act on image fields.
return;
}
// The value of a managed_file element can be an array if #extended == TRUE.
$uuid = $field->getSetting('default_image')['uuid'];
// Remove the default image when the instance is deleted.
if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
\Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
}
}
}

View File

@ -0,0 +1,58 @@
<?php
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;
/**
* Requirements for the Image module.
*/
class ImageRequirements {
use StringTranslationTrait;
public function __construct(
protected readonly ImageToolkitManager $imageToolkitManager,
) {}
/**
* Implements hook_runtime_requirements().
*/
#[Hook('runtime_requirements')]
public function runtime(): array {
$toolkit = $this->imageToolkitManager->getDefaultToolkit();
if ($toolkit) {
$plugin_definition = $toolkit->getPluginDefinition();
$requirements = [
'image.toolkit' => [
'title' => $this->t('Image toolkit'),
'value' => $toolkit->getPluginId(),
'description' => $plugin_definition['title'],
],
];
foreach ($toolkit->getRequirements() as $key => $requirement) {
$namespaced_key = 'image.toolkit.' . $toolkit->getPluginId() . '.' . $key;
$requirements[$namespaced_key] = $requirement;
}
}
else {
$requirements = [
'image.toolkit' => [
'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' => RequirementSeverity::Error,
],
];
}
return $requirements;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Drupal\image\Hook;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field\FieldStorageConfigInterface;
/**
* Hook implementations for image.
*/
class ImageViewsHooks {
use StringTranslationTrait;
public function __construct(
protected readonly EntityFieldManagerInterface $entityFieldManager,
) {}
/**
* Implements hook_field_views_data().
*
* Views integration for image fields. Adds an image relationship to the
* default field data.
*
* @see FieldViewsDataProvider::defaultFieldImplementation()
*/
#[Hook('field_views_data')]
public function fieldViewsData(FieldStorageConfigInterface $field_storage): array {
$data = \Drupal::service('views.field_data_provider')->defaultFieldImplementation($field_storage);
foreach ($data as $table_name => $table_data) {
// Add the relationship only on the target_id field.
$data[$table_name][$field_storage->getName() . '_target_id']['relationship'] = [
'id' => 'standard',
'base' => 'file_managed',
'entity type' => 'file',
'base field' => 'fid',
'label' => $this->t('image from @field_name', [
'@field_name' => $field_storage->getName(),
]),
];
}
return $data;
}
/**
* Implements hook_field_views_data_views_data_alter().
*
* Views integration to provide reverse relationships on image fields.
*/
#[Hook('field_views_data_views_data_alter')]
public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInterface $field_storage): void {
$entity_type_id = $field_storage->getTargetEntityTypeId();
$field_name = $field_storage->getName();
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
$pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
[$label] = $this->entityFieldManager->getFieldLabels($entity_type_id, $field_name);
$data['file_managed'][$pseudo_field_name]['relationship'] = [
'title' => $this->t('@entity using @field', [
'@entity' => $entity_type->getLabel(),
'@field' => $label,
]),
'label' => $this->t('@field_name', [
'@field_name' => $field_name,
]),
'help' => $this->t('Relate each @entity with a @field set to the image.', [
'@entity' => $entity_type->getLabel(),
'@field' => $label,
]),
'group' => $entity_type->getLabel(),
'id' => 'entity_reverse',
'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
'entity_type' => $entity_type_id,
'base field' => $entity_type->getKey('id'),
'field_name' => $field_name,
'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
'field field' => $field_name . '_target_id',
'join_extra' => [
0 => [
'field' => 'deleted',
'value' => 0,
'numeric' => TRUE,
],
],
];
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Drupal\image;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for image effects.
*
* @see \Drupal\image\Annotation\ImageEffect
* @see \Drupal\image\ImageEffectInterface
* @see \Drupal\image\ConfigurableImageEffectInterface
* @see \Drupal\image\ConfigurableImageEffectBase
* @see \Drupal\image\ImageEffectManager
* @see plugin_api
*/
abstract class ImageEffectBase extends PluginBase implements ImageEffectInterface, ContainerFactoryPluginInterface {
/**
* The image effect ID.
*
* @var string
*/
protected $uuid;
/**
* The weight of the image effect.
*
* @var int|string
*/
protected $weight = '';
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->setConfiguration($configuration);
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('logger.factory')->get('image')
);
}
/**
* {@inheritdoc}
*/
public function transformDimensions(array &$dimensions, $uri) {
// Most image effects will not change the dimensions. This base
// implementation represents this behavior. Override this method if your
// image effect does change the dimensions.
}
/**
* {@inheritdoc}
*/
public function getDerivativeExtension($extension) {
// Most image effects will not change the extension. This base
// implementation represents this behavior. Override this method if your
// image effect does change the extension.
return $extension;
}
/**
* {@inheritdoc}
*/
public function getSummary() {
return [
'#markup' => '',
'#effect' => [
'id' => $this->pluginDefinition['id'],
'label' => $this->label(),
'description' => $this->pluginDefinition['description'],
],
];
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function getUuid() {
return $this->uuid;
}
/**
* {@inheritdoc}
*/
public function setWeight($weight) {
$this->weight = $weight;
return $this;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->weight;
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return [
'uuid' => $this->getUuid(),
'id' => $this->getPluginId(),
'weight' => $this->getWeight(),
'data' => $this->configuration,
];
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$configuration += [
'data' => [],
'uuid' => '',
'weight' => '',
];
$this->configuration = $configuration['data'] + $this->defaultConfiguration();
$this->uuid = $configuration['uuid'];
$this->weight = $configuration['weight'];
return $this;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace Drupal\image;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Image\ImageInterface;
/**
* Defines the interface for image effects.
*
* @see \Drupal\image\Annotation\ImageEffect
* @see \Drupal\image\ImageEffectBase
* @see \Drupal\image\ConfigurableImageEffectInterface
* @see \Drupal\image\ConfigurableImageEffectBase
* @see \Drupal\image\ImageEffectManager
* @see plugin_api
*/
interface ImageEffectInterface extends PluginInspectionInterface, ConfigurableInterface, DependentPluginInterface {
/**
* Applies an image effect to the image object.
*
* @param \Drupal\Core\Image\ImageInterface $image
* An image file object.
*
* @return bool
* TRUE on success. FALSE if unable to perform the image effect on the
* image.
*/
public function applyEffect(ImageInterface $image);
/**
* Determines the dimensions of the styled image.
*
* @param array &$dimensions
* Dimensions to be modified - an array with the following keys:
* - width: the width in pixels, or NULL if unknown
* - height: the height in pixels, or NULL if unknown
* When either of the dimensions are NULL, the corresponding HTML attribute
* will be omitted when an image style using this image effect is used.
* @param string $uri
* Original image file URI. It is passed in to allow an effect to
* optionally use this information to retrieve additional image metadata
* to determine dimensions of the styled image.
* ImageEffectInterface::transformDimensions key objective is to calculate
* styled image dimensions without performing actual image operations, so
* be aware that performing IO on the URI may lead to decrease in
* performance.
*/
public function transformDimensions(array &$dimensions, $uri);
/**
* Returns the extension of the derivative after applying this image effect.
*
* @param string $extension
* The file extension the derivative has before applying.
*
* @return string
* The file extension after applying.
*/
public function getDerivativeExtension($extension);
/**
* Returns a render array summarizing the configuration of the image effect.
*
* @return array
* A render array.
*/
public function getSummary();
/**
* Returns the image effect label.
*
* @return string
* The image effect label.
*/
public function label();
/**
* Returns the unique ID representing the image effect.
*
* @return string
* The image effect ID.
*/
public function getUuid();
/**
* Returns the weight of the image effect.
*
* @return int|string
* Either the integer weight of the image effect, or an empty string.
*/
public function getWeight();
/**
* Sets the weight for this image effect.
*
* @param int $weight
* The weight for this image effect.
*
* @return $this
*/
public function setWeight($weight);
}

View File

@ -0,0 +1,41 @@
<?php
namespace Drupal\image;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\image\Attribute\ImageEffect;
/**
* Manages image effect plugins.
*
* @see hook_image_effect_info_alter()
* @see \Drupal\image\Annotation\ImageEffect
* @see \Drupal\image\ConfigurableImageEffectInterface
* @see \Drupal\image\ConfigurableImageEffectBase
* @see \Drupal\image\ImageEffectInterface
* @see \Drupal\image\ImageEffectBase
* @see plugin_api
*/
class ImageEffectManager extends DefaultPluginManager {
/**
* Constructs a new ImageEffectManager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/ImageEffect', $namespaces, $module_handler, ImageEffectInterface::class, ImageEffect::class, 'Drupal\image\Annotation\ImageEffect');
$this->alterInfo('image_effect_info');
$this->setCacheBackend($cache_backend, 'image_effect_plugins');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Drupal\image;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
/**
* A collection of image effects.
*/
class ImageEffectPluginCollection extends DefaultLazyPluginCollection {
/**
* {@inheritdoc}
*
* @return \Drupal\image\ImageEffectInterface
* The image effect plugin.
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* {@inheritdoc}
*/
public function sortHelper($aID, $bID) {
return $this->get($aID)->getWeight() <=> $this->get($bID)->getWeight();
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace Drupal\image;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining an image style entity.
*/
interface ImageStyleInterface extends ConfigEntityInterface {
/**
* Returns the image style.
*
* @return string
* The name of the image style.
*/
public function getName();
/**
* Sets the name of the image style.
*
* @param string $name
* The name of the image style.
*
* @return $this
* The class instance this method is called on.
*/
public function setName($name);
/**
* Returns the URI of this image when using this style.
*
* The path returned by this function may not exist. The default generation
* method only creates images when they are requested by a user's browser.
* Modules may implement this method to decide where to place derivatives.
*
* @param string $uri
* The URI or path to the original image.
*
* @return string
* The URI to the image derivative for this style.
*/
public function buildUri($uri);
/**
* Returns the URL of this image derivative for an original image path or URI.
*
* @param string $path
* The path or URI to the original image.
* @param mixed $clean_urls
* (optional) Whether clean URLs are in use.
*
* @return string
* The absolute URL where a style image can be downloaded, suitable for use
* in an <img> tag. Requesting the URL will cause the image to be created.
*
* @see \Drupal\image\Controller\ImageStyleDownloadController::deliver()
* @see \Drupal\Core\File\FileUrlGeneratorInterface::transformRelative()
*/
public function buildUrl($path, $clean_urls = NULL);
/**
* Generates a token to protect an image style derivative.
*
* This prevents unauthorized generation of an image style derivative,
* which can be costly both in CPU time and disk space.
*
* @param string $uri
* The URI of the original image of this style.
*
* @return string
* An eight-character token which can be used to protect image style
* derivatives against denial-of-service attacks.
*/
public function getPathToken($uri);
/**
* Flushes cached media for this style.
*
* @param string $path
* (optional) The original image path or URI. If it's supplied, only this
* image derivative will be flushed.
*
* @return $this
*/
public function flush($path = NULL);
/**
* Creates a new image derivative based on this image style.
*
* Generates an image derivative applying all image effects and saving the
* resulting image.
*
* @param string $original_uri
* Original image file URI.
* @param string $derivative_uri
* Derivative image file URI.
*
* @return bool
* TRUE if an image derivative was generated, or FALSE if the image
* derivative could not be generated.
*/
public function createDerivative($original_uri, $derivative_uri);
/**
* Determines the dimensions of this image style.
*
* Stores the dimensions of this image style into $dimensions associative
* array. Implementations have to provide at least values to next keys:
* - width: Integer with the derivative image width.
* - height: Integer with the derivative image height.
*
* @param array $dimensions
* Associative array passed by reference. Implementations have to store the
* resulting width and height, in pixels.
* @param string $uri
* Original image file URI. It is passed in to allow effects to
* optionally use this information to retrieve additional image metadata
* to determine dimensions of the styled image.
* ImageStyleInterface::transformDimensions key objective is to calculate
* styled image dimensions without performing actual image operations, so
* be aware that performing IO on the URI may lead to decrease in
* performance.
*
* @see ImageEffectInterface::transformDimensions
*/
public function transformDimensions(array &$dimensions, $uri);
/**
* Determines the extension of the derivative without generating it.
*
* @param string $extension
* The file extension of the original image.
*
* @return string
* The extension the derivative image will have, given the extension of the
* original.
*/
public function getDerivativeExtension($extension);
/**
* Returns a specific image effect.
*
* @param string $effect
* The image effect ID.
*
* @return \Drupal\image\ImageEffectInterface
* The image effect object.
*/
public function getEffect($effect);
/**
* Returns the image effects for this style.
*
* @return \Drupal\image\ImageEffectPluginCollection|\Drupal\image\ImageEffectInterface[]
* The image effect plugin collection.
*/
public function getEffects();
/**
* Saves an image effect for this style.
*
* @param array $configuration
* An array of image effect configuration.
*
* @return string
* The image effect ID.
*/
public function addImageEffect(array $configuration);
/**
* Deletes an image effect from this style.
*
* @param \Drupal\image\ImageEffectInterface $effect
* The image effect object.
*
* @return $this
*/
public function deleteImageEffect(ImageEffectInterface $effect);
/**
* Determines if this style can be applied to a given image.
*
* @param string $uri
* The URI of the image.
*
* @return bool
* TRUE if the image is supported, FALSE otherwise.
*/
public function supportsUri($uri);
}

View File

@ -0,0 +1,58 @@
<?php
namespace Drupal\image;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
/**
* Defines a class to build a listing of image style entities.
*
* @see \Drupal\image\Entity\ImageStyle
*/
class ImageStyleListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Style name');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$flush = [
'title' => $this->t('Flush'),
'weight' => 200,
'url' => $entity->toUrl('flush-form'),
];
return parent::getDefaultOperations($entity) + [
'flush' => $flush,
];
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$build['table']['#empty'] = $this->t('There are currently no styles. <a href=":url">Add a new one</a>.', [
':url' => Url::fromRoute('image.style_add')->toString(),
]);
return $build;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Drupal\image;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
/**
* Storage controller class for "image style" configuration entities.
*/
class ImageStyleStorage extends ConfigEntityStorage implements ImageStyleStorageInterface {
/**
* Image style replacement memory storage.
*
* This value is not stored in the backend. It's used during the deletion of
* an image style to save the replacement image style in the same request. The
* value is used later, when resolving dependencies.
*
* @var string[]
*
* @see \Drupal\image\Form\ImageStyleDeleteForm::submitForm()
*/
protected $replacement = [];
/**
* {@inheritdoc}
*/
public function setReplacementId($name, $replacement) {
$this->replacement[$name] = $replacement;
}
/**
* {@inheritdoc}
*/
public function getReplacementId($name) {
return $this->replacement[$name] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function clearReplacementId($name) {
unset($this->replacement[$name]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Drupal\image;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
/**
* Interface for storage controller for "image style" configuration entities.
*/
interface ImageStyleStorageInterface extends ConfigEntityStorageInterface {
/**
* Stores a replacement ID for an image style being deleted.
*
* The method stores a replacement style to be used by the configuration
* dependency system when an image style is deleted. The replacement style is
* replacing the deleted style in other configuration entities that are
* depending on the image style being deleted.
*
* @param string $name
* The ID of the image style to be deleted.
* @param string $replacement
* The ID of the image style used as replacement.
*/
public function setReplacementId($name, $replacement);
/**
* Retrieves the replacement ID of a deleted image style.
*
* The method is retrieving the value stored by ::setReplacementId().
*
* @param string $name
* The ID of the image style to be replaced.
*
* @return string|null
* The ID of the image style used as replacement, if there's any, or NULL.
*
* @see \Drupal\image\ImageStyleStorageInterface::setReplacementId()
*/
public function getReplacementId($name);
/**
* Clears a replacement ID from the storage.
*
* The method clears the value previously stored with ::setReplacementId().
*
* @param string $name
* The ID of the image style to be replaced.
*
* @see \Drupal\image\ImageStyleStorageInterface::setReplacementId()
*/
public function clearReplacementId($name);
}

View File

@ -0,0 +1,78 @@
<?php
namespace Drupal\image\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a path processor to rewrite image styles URLs.
*
* As the route system does not allow arbitrary amount of parameters convert
* the file path to a query parameter on the request.
*
* This processor handles two different cases:
* - public image styles: In order to allow the webserver to serve these files
* directly, the route is registered under the same path as the image style so
* it took over the first generation. Therefore the path processor converts
* the file path to a query parameter.
* - private image styles: In contrast to public image styles, private
* derivatives are already using system/files/styles. Similar to public image
* styles, it also converts the file path to a query parameter.
*/
class PathProcessorImageStyles implements InboundPathProcessorInterface {
/**
* The stream wrapper manager service.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* Constructs a new PathProcessorImageStyles object.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
* The stream wrapper manager service.
*/
public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager) {
$this->streamWrapperManager = $stream_wrapper_manager;
}
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
$directory_path = $this->streamWrapperManager->getViaScheme('public')->getDirectoryPath();
if (str_starts_with($path, '/' . $directory_path . '/styles/')) {
$path_prefix = '/' . $directory_path . '/styles/';
}
// Check if the string '/system/files/styles/' exists inside the path,
// that means we have a case of private file's image style.
elseif (str_contains($path, '/system/files/styles/')) {
$path_prefix = '/system/files/styles/';
$path = substr($path, strpos($path, $path_prefix), strlen($path));
}
else {
return $path;
}
// Strip out path prefix.
$rest = preg_replace('|^' . preg_quote($path_prefix, '|') . '|', '', $path);
// Get the image style, scheme and path.
if (substr_count($rest, '/') >= 2) {
[$image_style, $scheme, $file] = explode('/', $rest, 3);
// Set the file as query parameter.
$request->query->set('file', $file);
return $path_prefix . $image_style . '/' . $scheme;
}
else {
return $path;
}
}
}

View File

@ -0,0 +1,312 @@
<?php
namespace Drupal\image\Plugin\Field\FieldFormatter;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Link;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\image\Entity\ImageStyle;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Cache\Cache;
/**
* Plugin implementation of the 'image' formatter.
*/
#[FieldFormatter(
id: 'image',
label: new TranslatableMarkup('Image'),
field_types: [
'image',
],
)]
class ImageFormatter extends ImageFormatterBase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The image style entity storage.
*
* @var \Drupal\image\ImageStyleStorageInterface
*/
protected $imageStyleStorage;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* Constructs an ImageFormatter object.
*
* @param string $plugin_id
* The plugin ID for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
* The image style storage.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
* The file URL generator.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $image_style_storage, FileUrlGeneratorInterface $file_url_generator) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->currentUser = $current_user;
$this->imageStyleStorage = $image_style_storage;
$this->fileUrlGenerator = $file_url_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('current_user'),
$container->get('entity_type.manager')->getStorage('image_style'),
$container->get('file_url_generator')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'image_style' => '',
'image_link' => '',
'image_loading' => [
'attribute' => 'lazy',
],
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$image_styles = image_style_options(FALSE);
$description_link = Link::fromTextAndUrl(
$this->t('Configure Image Styles'),
Url::fromRoute('entity.image_style.collection')
);
$element['image_style'] = [
'#title' => $this->t('Image style'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_style'),
'#empty_option' => $this->t('None (original image)'),
'#options' => $image_styles,
'#description' => $description_link->toRenderable() + [
'#access' => $this->currentUser->hasPermission('administer image styles'),
],
];
$link_types = [
'content' => $this->t('Content'),
'file' => $this->t('File'),
];
$element['image_link'] = [
'#title' => $this->t('Link image to'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_link'),
'#empty_option' => $this->t('Nothing'),
'#options' => $link_types,
];
$image_loading = $this->getSetting('image_loading');
$element['image_loading'] = [
'#type' => 'details',
'#title' => $this->t('Image loading'),
'#weight' => 10,
'#description' => $this->t('Lazy render images with native image loading attribute (<em>loading="lazy"</em>). This improves performance by allowing browsers to lazily load images.'),
];
$loading_attribute_options = [
'lazy' => $this->t('Lazy (<em>loading="lazy"</em>)'),
'eager' => $this->t('Eager (<em>loading="eager"</em>)'),
];
$element['image_loading']['attribute'] = [
'#title' => $this->t('Image loading attribute'),
'#type' => 'radios',
'#default_value' => $image_loading['attribute'],
'#options' => $loading_attribute_options,
'#description' => $this->t('Select the loading attribute for images. <a href=":link">Learn more about the loading attribute for images.</a>', [
':link' => 'https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-loading-attributes',
]),
];
$element['image_loading']['attribute']['lazy']['#description'] = $this->t('Delays loading the image until that section of the page is visible in the browser. When in doubt, lazy loading is recommended.');
$element['image_loading']['attribute']['eager']['#description'] = $this->t('Force browsers to download an image as soon as possible. This is the browser default for legacy reasons. Only use this option when the image is always expected to render.');
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$image_styles = image_style_options(FALSE);
// Unset possible 'No defined styles' option.
unset($image_styles['']);
// Styles could be lost because of enabled/disabled modules that defines
// their styles in code.
$image_style_setting = $this->getSetting('image_style');
if (isset($image_styles[$image_style_setting])) {
$summary[] = $this->t('Image style: @style', ['@style' => $image_styles[$image_style_setting]]);
}
else {
$summary[] = $this->t('Original image');
}
$link_types = [
'content' => $this->t('Linked to content'),
'file' => $this->t('Linked to file'),
];
// Display this setting only if image is linked.
$image_link_setting = $this->getSetting('image_link');
if (isset($link_types[$image_link_setting])) {
$summary[] = $link_types[$image_link_setting];
}
$image_loading = $this->getSetting('image_loading');
$summary[] = $this->t('Image loading: @attribute', [
'@attribute' => $image_loading['attribute'],
]);
return array_merge($summary, parent::settingsSummary());
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$files = $this->getEntitiesToView($items, $langcode);
// Early opt-out if the field is empty.
if (empty($files)) {
return $elements;
}
$url = NULL;
$image_link_setting = $this->getSetting('image_link');
// Check if the formatter involves a link.
if ($image_link_setting == 'content') {
$entity = $items->getEntity();
if (!$entity->isNew()) {
$url = $entity->toUrl();
}
}
elseif ($image_link_setting == 'file') {
$link_file = TRUE;
}
$image_style_setting = $this->getSetting('image_style');
// Collect cache tags to be added for each item in the field.
$base_cache_tags = [];
if (!empty($image_style_setting)) {
$image_style = $this->imageStyleStorage->load($image_style_setting);
$base_cache_tags = $image_style->getCacheTags();
}
foreach ($files as $delta => $file) {
if (isset($link_file)) {
$image_uri = $file->getFileUri();
$url = $this->fileUrlGenerator->generate($image_uri);
}
$cache_tags = Cache::mergeTags($base_cache_tags, $file->getCacheTags());
// Extract field item attributes for the theme function, and unset them
// from the $item so that the field template does not re-render them.
$item = $file->_referringItem;
$item_attributes = $item->_attributes;
unset($item->_attributes);
$image_loading_settings = $this->getSetting('image_loading');
$item_attributes['loading'] = $image_loading_settings['attribute'];
$elements[$delta] = [
'#theme' => 'image_formatter',
'#item' => $item,
'#item_attributes' => $item_attributes,
'#image_style' => $image_style_setting,
'#url' => $url,
'#cache' => [
'tags' => $cache_tags,
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$style_id = $this->getSetting('image_style');
/** @var \Drupal\image\ImageStyleInterface $style */
if ($style_id && $style = ImageStyle::load($style_id)) {
// If this formatter uses a valid image style to display the image, add
// the image style configuration entity as dependency of this formatter.
$dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName();
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
$style_id = $this->getSetting('image_style');
/** @var \Drupal\image\ImageStyleInterface $style */
if ($style_id && $style = ImageStyle::load($style_id)) {
if (!empty($dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) {
$replacement_id = $this->imageStyleStorage->getReplacementId($style_id);
// If a valid replacement has been provided in the storage, replace the
// image style with the replacement and signal that the formatter plugin
// settings were updated.
if ($replacement_id && ImageStyle::load($replacement_id)) {
$this->setSetting('image_style', $replacement_id);
$changed = TRUE;
}
}
}
return $changed;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Drupal\image\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
/**
* Base class for image file formatters.
*/
abstract class ImageFormatterBase extends FileFormatterBase {
/**
* {@inheritdoc}
*/
protected function getEntitiesToView(EntityReferenceFieldItemListInterface $items, $langcode) {
// Add the default image if needed.
if ($items->isEmpty()) {
$default_image = $this->getFieldSetting('default_image');
// If we are dealing with a configurable field, look in both
// instance-level and field-level settings.
if (empty($default_image['uuid']) && $this->fieldDefinition instanceof FieldConfigInterface) {
$default_image = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('default_image');
}
if (!empty($default_image['uuid']) && $file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $default_image['uuid'])) {
// Clone the FieldItemList into a runtime-only object for the formatter,
// so that the fallback image can be rendered without affecting the
// field values in the entity being rendered.
$items = clone $items;
$items->setValue([
'target_id' => $file->id(),
'alt' => $default_image['alt'],
'title' => $default_image['title'],
'width' => $default_image['width'],
'height' => $default_image['height'],
'entity' => $file,
'_loaded' => TRUE,
'_is_default' => TRUE,
]);
if ($file->_referringItem) {
// If the file entity is already being referenced by another field
// item, clone it so that _referringItem is set to the correct item
// in each instance.
$file = clone $file;
$items[0]->entity = $file;
}
$file->_referringItem = $items[0];
}
}
return parent::getEntitiesToView($items, $langcode);
}
}

View File

@ -0,0 +1,179 @@
<?php
namespace Drupal\image\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'image_url' formatter.
*/
#[FieldFormatter(
id: 'image_url',
label: new TranslatableMarkup('URL to image'),
field_types: [
'image',
],
)]
class ImageUrlFormatter extends ImageFormatterBase {
/**
* The image style entity storage.
*
* @var \Drupal\image\ImageStyleStorageInterface
*/
protected $imageStyleStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs an ImageFormatter object.
*
* @param string $plugin_id
* The plugin ID for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
* The image style storage.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityStorageInterface $image_style_storage, AccountInterface $current_user) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->imageStyleStorage = $image_style_storage;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('entity_type.manager')->getStorage('image_style'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'image_style' => '',
];
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
unset($element['image_link'], $element['image_loading']);
$image_styles = image_style_options(FALSE);
$description_link = Link::fromTextAndUrl(
$this->t('Configure Image Styles'),
Url::fromRoute('entity.image_style.collection')
);
$element['image_style'] = [
'#title' => $this->t('Image style'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_style'),
'#empty_option' => $this->t('None (original image)'),
'#options' => $image_styles,
'#description' => $description_link->toRenderable() + [
'#access' => $this->currentUser->hasPermission('administer image styles'),
],
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$image_styles = image_style_options(FALSE);
// Unset possible 'No defined styles' option.
unset($image_styles['']);
// Styles could be lost because of enabled/disabled modules that defines
// their styles in code.
$image_style_setting = $this->getSetting('image_style');
if (isset($image_styles[$image_style_setting])) {
$summary[] = $this->t('Image style: @style', ['@style' => $image_styles[$image_style_setting]]);
}
else {
$summary[] = $this->t('Original image');
}
return array_merge($summary, parent::settingsSummary());
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
/** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items */
if (empty($images = $this->getEntitiesToView($items, $langcode))) {
// Early opt-out if the field is empty.
return $elements;
}
/** @var \Drupal\image\ImageStyleInterface $image_style */
$image_style = $this->imageStyleStorage->load($this->getSetting('image_style'));
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
/** @var \Drupal\file\FileInterface[] $images */
foreach ($images as $delta => $image) {
$image_uri = $image->getFileUri();
$url = $image_style ? $file_url_generator->transformRelative($image_style->buildUrl($image_uri)) : $file_url_generator->generateString($image_uri);
// Add cacheability metadata from the image and image style.
$cacheability = CacheableMetadata::createFromObject($image);
if ($image_style) {
$cacheability->addCacheableDependency(CacheableMetadata::createFromObject($image_style));
}
$elements[$delta] = ['#markup' => $url];
$cacheability->applyTo($elements[$delta]);
}
return $elements;
}
}

View File

@ -0,0 +1,532 @@
<?php
namespace Drupal\image\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\file\Entity\File;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Drupal\file\Plugin\Field\FieldType\FileItem;
/**
* Plugin implementation of the 'image' field type.
*/
#[FieldType(
id: "image",
label: new TranslatableMarkup("Image"),
description: [
new TranslatableMarkup("For uploading images"),
new TranslatableMarkup("Allows a user to upload an image with configurable extensions, image dimensions, upload size"),
new TranslatableMarkup(
"Can be configured with options such as allowed file extensions, maximum upload size and image dimensions minimums/maximums"
),
],
category: "file_upload",
default_widget: "image_image",
default_formatter: "image",
list_class: FileFieldItemList::class,
constraints: ["ReferenceAccess" => [], "FileValidation" => []],
column_groups: [
"file" => [
"label" => new TranslatableMarkup("File"),
"columns" => [
"target_id",
"width",
"height",
],
"require_all_groups_for_translation" => TRUE,
],
"alt" => [
"label" => new TranslatableMarkup("Alt"),
"translatable" => TRUE,
],
"title" => [
"label" => new TranslatableMarkup("Title"),
"translatable" => TRUE,
],
]
)]
class ImageItem extends FileItem {
use LoggerChannelTrait;
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'default_image' => [
'uuid' => NULL,
'alt' => '',
'title' => '',
'width' => NULL,
'height' => NULL,
],
'display_default' => TRUE,
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
$settings = [
'file_extensions' => 'png gif jpg jpeg webp',
'alt_field' => 1,
'alt_field_required' => 1,
'title_field' => 0,
'title_field_required' => 0,
'max_resolution' => '',
'min_resolution' => '',
'default_image' => [
'uuid' => NULL,
'alt' => '',
'title' => '',
'width' => NULL,
'height' => NULL,
],
] + parent::defaultFieldSettings();
unset($settings['description_field']);
return $settings;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'target_id' => [
'description' => 'The ID of the file entity.',
'type' => 'int',
'unsigned' => TRUE,
],
'alt' => [
'description' => "Alternative image text, for the image's 'alt' attribute.",
'type' => 'varchar',
'length' => 512,
],
'title' => [
'description' => "Image title text, for the image's 'title' attribute.",
'type' => 'varchar',
'length' => 1024,
],
'width' => [
'description' => 'The width of the image in pixels.',
'type' => 'int',
'unsigned' => TRUE,
],
'height' => [
'description' => 'The height of the image in pixels.',
'type' => 'int',
'unsigned' => TRUE,
],
],
'indexes' => [
'target_id' => ['target_id'],
],
'foreign keys' => [
'target_id' => [
'table' => 'file_managed',
'columns' => ['target_id' => 'fid'],
],
],
];
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
unset($properties['display']);
unset($properties['description']);
$properties['alt'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Alternative text'))
->setDescription(new TranslatableMarkup("Alternative image text, for the image's 'alt' attribute."));
$properties['title'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Title'))
->setDescription(new TranslatableMarkup("Image title text, for the image's 'title' attribute."));
$properties['width'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Width'))
->setDescription(new TranslatableMarkup('The width of the image in pixels.'));
$properties['height'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('Height'))
->setDescription(new TranslatableMarkup('The height of the image in pixels.'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function storageSettingsSummary(FieldStorageDefinitionInterface $storage_definition): array {
// Bypass the parent setting summary as it produces redundant information.
return [];
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = [];
// We need the field-level 'default_image' setting, and $this->getSettings()
// will only provide the instance-level one, so we need to explicitly fetch
// the field.
$settings = $this->getFieldDefinition()->getFieldStorageDefinition()->getSettings();
$scheme_options = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE);
$element['uri_scheme'] = [
'#type' => 'radios',
'#title' => $this->t('Upload destination'),
'#options' => $scheme_options,
'#default_value' => $settings['uri_scheme'],
'#description' => $this->t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
];
// Add default_image element.
static::defaultImageForm($element, $settings);
$element['default_image']['#description'] = $this->t('If no image is uploaded, this image will be shown on display.');
return $element;
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
// Get base form from FileItem.
$element = parent::fieldSettingsForm($form, $form_state);
$settings = $this->getSettings();
// Add maximum and minimum dimensions settings.
$max_resolution = explode('x', $settings['max_resolution']) + ['', ''];
$element['max_resolution'] = [
'#type' => 'item',
'#title' => $this->t('Maximum image dimensions'),
'#element_validate' => [[static::class, 'validateResolution']],
'#weight' => 4.1,
'#description' => $this->t('The maximum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a larger image is uploaded, it will be resized to reflect the given width and height. Resizing images on upload will cause the loss of <a href="http://wikipedia.org/wiki/Exchangeable_image_file_format">EXIF data</a> in the image.'),
];
$element['max_resolution']['x'] = [
'#type' => 'number',
'#title' => $this->t('Maximum width'),
'#title_display' => 'invisible',
'#default_value' => $max_resolution[0],
'#min' => 1,
'#field_suffix' => ' × ',
'#prefix' => '<div class="form--inline clearfix">',
];
$element['max_resolution']['y'] = [
'#type' => 'number',
'#title' => $this->t('Maximum height'),
'#title_display' => 'invisible',
'#default_value' => $max_resolution[1],
'#min' => 1,
'#field_suffix' => ' ' . $this->t('pixels'),
'#suffix' => '</div>',
];
$min_resolution = explode('x', $settings['min_resolution']) + ['', ''];
$element['min_resolution'] = [
'#type' => 'item',
'#title' => $this->t('Minimum image dimensions'),
'#element_validate' => [[static::class, 'validateResolution']],
'#weight' => 4.2,
'#description' => $this->t('The minimum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a smaller image is uploaded, it will be rejected.'),
];
$element['min_resolution']['x'] = [
'#type' => 'number',
'#title' => $this->t('Minimum width'),
'#title_display' => 'invisible',
'#default_value' => $min_resolution[0],
'#min' => 1,
'#field_suffix' => ' × ',
'#prefix' => '<div class="form--inline clearfix">',
];
$element['min_resolution']['y'] = [
'#type' => 'number',
'#title' => $this->t('Minimum height'),
'#title_display' => 'invisible',
'#default_value' => $min_resolution[1],
'#min' => 1,
'#field_suffix' => ' ' . $this->t('pixels'),
'#suffix' => '</div>',
];
// Remove the description option.
unset($element['description_field']);
// Add title and alt configuration options.
$element['alt_field'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable <em>Alt</em> field'),
'#default_value' => $settings['alt_field'],
'#description' => $this->t('Short description of the image used by screen readers and displayed when the image is not loaded. Enabling this field is recommended.'),
'#weight' => 9,
];
$element['alt_field_required'] = [
'#type' => 'checkbox',
'#title' => $this->t('<em>Alt</em> field required'),
'#default_value' => $settings['alt_field_required'],
'#description' => $this->t('Making this field required is recommended.'),
'#weight' => 10,
'#states' => [
'visible' => [
':input[name="settings[alt_field]"]' => ['checked' => TRUE],
],
],
];
$element['title_field'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable <em>Title</em> field'),
'#default_value' => $settings['title_field'],
'#description' => $this->t('The title attribute is used as a tooltip when the mouse hovers over the image. Enabling this field is not recommended as it can cause problems with screen readers.'),
'#weight' => 11,
];
$element['title_field_required'] = [
'#type' => 'checkbox',
'#title' => $this->t('<em>Title</em> field required'),
'#default_value' => $settings['title_field_required'],
'#weight' => 12,
'#states' => [
'visible' => [
':input[name="settings[title_field]"]' => ['checked' => TRUE],
],
],
];
// Add default_image element.
static::defaultImageForm($element, $settings);
$element['default_image']['#description'] = $this->t("If no image is uploaded, this image will be shown on display and will override the field's default image.");
return $element;
}
/**
* {@inheritdoc}
*/
public function preSave() {
parent::preSave();
$width = $this->get('width')->getValue();
$height = $this->get('height')->getValue();
// Determine the dimensions if necessary.
if ($this->entity && $this->entity instanceof EntityInterface) {
if ($width === NULL || $height === NULL) {
$image = \Drupal::service('image.factory')->get($this->entity->getFileUri());
if ($image->isValid()) {
$this->set('width', $image->getWidth());
$this->set('height', $image->getHeight());
}
}
}
else {
$this->getLogger('image')->warning("Missing file with ID %id.", ['%id' => $this->target_id]);
}
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();
$settings = $field_definition->getSettings();
static $images = [];
$min_resolution = empty($settings['min_resolution']) ? '100x100' : $settings['min_resolution'];
$max_resolution = empty($settings['max_resolution']) ? '600x600' : $settings['max_resolution'];
$extensions = array_intersect(explode(' ', $settings['file_extensions']), ['png', 'gif', 'jpg', 'jpeg']);
$extension = array_rand(array_combine($extensions, $extensions));
$min = explode('x', $min_resolution);
$max = explode('x', $max_resolution);
if (intval($min[0]) > intval($max[0])) {
$max[0] = $min[0];
}
if (intval($min[1]) > intval($max[1])) {
$max[1] = $min[1];
}
$max_resolution = "$max[0]x$max[1]";
// Generate a max of 5 different images.
if (!isset($images[$extension][$min_resolution][$max_resolution]) || count($images[$extension][$min_resolution][$max_resolution]) <= 5) {
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$tmp_file = $file_system->tempnam('temporary://', 'generateImage_');
$destination = $tmp_file . '.' . $extension;
try {
$file_system->move($tmp_file, $destination);
}
catch (FileException) {
// Ignore failed move.
}
if ($path = $random->image($file_system->realpath($destination), $min_resolution, $max_resolution)) {
$image = File::create();
$image->setFileUri($path);
$image->setOwnerId(\Drupal::currentUser()->id());
$guesser = \Drupal::service('file.mime_type.guesser');
$image->setMimeType($guesser->guessMimeType($path));
$image->setFileName($file_system->basename($path));
$destination_dir = static::doGetUploadLocation($settings);
$file_system->prepareDirectory($destination_dir, FileSystemInterface::CREATE_DIRECTORY);
// 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;
}
else {
return [];
}
}
else {
// Select one of the images we've already generated for this field.
$image_index = array_rand($images[$extension][$min_resolution][$max_resolution]);
$file = $images[$extension][$min_resolution][$max_resolution][$image_index];
}
[$width, $height] = getimagesize($file->getFileUri());
$values = [
'target_id' => $file->id(),
'alt' => $random->sentences(4),
'title' => $random->sentences(4),
'width' => $width,
'height' => $height,
];
return $values;
}
/**
* Element validate function for dimensions fields.
*/
public static function validateResolution($element, FormStateInterface $form_state) {
if (!empty($element['x']['#value']) || !empty($element['y']['#value'])) {
foreach (['x', 'y'] as $dimension) {
if (!$element[$dimension]['#value']) {
// We expect the field name placeholder value to be wrapped in
// $this->t() here, so it won't be escaped again as it's already
// marked safe.
$form_state->setError($element[$dimension], new TranslatableMarkup('Both a height and width value must be specified in the @name field.', ['@name' => $element['#title']]));
return;
}
}
$form_state->setValueForElement($element, $element['x']['#value'] . 'x' . $element['y']['#value']);
}
else {
$form_state->setValueForElement($element, '');
}
}
/**
* Builds the default_image details element.
*
* @param array $element
* The form associative array passed by reference.
* @param array $settings
* The field settings array.
*/
protected function defaultImageForm(array &$element, array $settings) {
$element['default_image'] = [
'#type' => 'details',
'#title' => $this->t('Default image'),
'#open' => TRUE,
];
// Convert the stored UUID to a FID.
$fids = [];
$uuid = $settings['default_image']['uuid'];
if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
$fids[0] = $file->id();
}
$element['default_image']['uuid'] = [
'#type' => 'managed_file',
'#title' => $this->t('Image'),
'#description' => $this->t('Image to be shown if no image is uploaded.'),
'#default_value' => $fids,
'#upload_location' => $settings['uri_scheme'] . '://default_images/',
'#element_validate' => [
'\Drupal\file\Element\ManagedFile::validateManagedFile',
[static::class, 'validateDefaultImageForm'],
],
'#upload_validators' => $this->getUploadValidators(),
];
$element['default_image']['alt'] = [
'#type' => 'textfield',
'#title' => $this->t('Alternative text'),
'#description' => $this->t('Short description of the image used by screen readers and displayed when the image is not loaded. This is important for accessibility.'),
'#default_value' => $settings['default_image']['alt'],
'#maxlength' => 512,
];
$element['default_image']['title'] = [
'#type' => 'textfield',
'#title' => $this->t('Title'),
'#description' => $this->t('The title attribute is used as a tooltip when the mouse hovers over the image.'),
'#default_value' => $settings['default_image']['title'],
'#maxlength' => 1024,
];
$element['default_image']['width'] = [
'#type' => 'value',
'#value' => $settings['default_image']['width'],
];
$element['default_image']['height'] = [
'#type' => 'value',
'#value' => $settings['default_image']['height'],
];
}
/**
* Validates the managed_file element for the default Image form.
*
* This function ensures the fid is a scalar value and not an array. It is
* assigned as an #element_validate callback in
* \Drupal\image\Plugin\Field\FieldType\ImageItem::defaultImageForm().
*
* @param array $element
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function validateDefaultImageForm(array &$element, FormStateInterface $form_state) {
// Consolidate the array value of this field to a single FID as #extended
// for default image is not TRUE and this is a single value.
if (isset($element['fids']['#value'][0])) {
$value = $element['fids']['#value'][0];
// Convert the file ID to a uuid.
if ($file = \Drupal::entityTypeManager()->getStorage('file')->load($value)) {
$value = $file->uuid();
}
}
else {
$value = '';
}
$form_state->setValueForElement($element, $value);
}
/**
* {@inheritdoc}
*/
public function isDisplayed() {
// Image items do not have per-item visibility settings.
return TRUE;
}
}

View File

@ -0,0 +1,356 @@
<?php
namespace Drupal\image\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\file\Entity\File;
use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
use Drupal\image\Entity\ImageStyle;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'image_image' widget.
*/
#[FieldWidget(
id: 'image_image',
label: new TranslatableMarkup('Image'),
field_types: ['image'],
)]
class ImageWidget extends FileWidget {
/**
* The image factory service.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* Constructs an ImageWidget object.
*
* @param string $plugin_id
* The plugin ID for the widget.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the widget is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager service.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info, ?ImageFactory $image_factory = NULL) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info);
$this->imageFactory = $image_factory ?: \Drupal::service('image.factory');
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'progress_indicator' => 'throbber',
'preview_image_style' => 'thumbnail',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$element['preview_image_style'] = [
'#title' => $this->t('Preview image style'),
'#type' => 'select',
'#options' => image_style_options(FALSE),
'#empty_option' => '<' . $this->t('no preview') . '>',
'#default_value' => $this->getSetting('preview_image_style'),
'#description' => $this->t('The preview image will be shown while editing the content.'),
'#weight' => 15,
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$image_styles = image_style_options(FALSE);
// Unset possible 'No defined styles' option.
unset($image_styles['']);
// Styles could be lost because of enabled/disabled modules that defines
// their styles in code.
$image_style_setting = $this->getSetting('preview_image_style');
if (isset($image_styles[$image_style_setting])) {
$preview_image_style = $this->t('Preview image style: @style', ['@style' => $image_styles[$image_style_setting]]);
}
else {
$preview_image_style = $this->t('No preview');
}
array_unshift($summary, $preview_image_style);
return $summary;
}
/**
* {@inheritdoc}
*/
protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
$elements = parent::formMultipleElements($items, $form, $form_state);
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
$file_upload_help = [
'#theme' => 'file_upload_help',
'#description' => '',
'#upload_validators' => $elements[0]['#upload_validators'],
'#cardinality' => $cardinality,
];
if ($cardinality == 1) {
// If there's only one field, return it as delta 0.
if (empty($elements[0]['#default_value']['fids'])) {
$file_upload_help['#description'] = $this->getFilteredDescription();
$elements[0]['#description'] = \Drupal::service('renderer')->renderInIsolation($file_upload_help);
}
}
else {
$elements['#file_upload_description'] = $file_upload_help;
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$field_settings = $this->getFieldSettings();
// Add image validation.
$element['#upload_validators']['FileIsImage'] = [];
// Add upload dimensions validation.
if ($field_settings['max_resolution'] || $field_settings['min_resolution']) {
$element['#upload_validators']['FileImageDimensions'] = [
'maxDimensions' => $field_settings['max_resolution'],
'minDimensions' => $field_settings['min_resolution'],
];
}
$extensions = $field_settings['file_extensions'];
$supported_extensions = $this->imageFactory->getSupportedExtensions();
// If using custom extension validation, ensure that the extensions are
// supported by the current image toolkit. Otherwise, validate against all
// toolkit supported extensions.
$extensions = !empty($extensions) ? array_intersect(explode(' ', $extensions), $supported_extensions) : $supported_extensions;
$element['#upload_validators']['FileExtension']['extensions'] = implode(' ', $extensions);
// Add mobile device image capture acceptance.
$element['#accept'] = 'image/*';
// Add properties needed by process() method.
$element['#preview_image_style'] = $this->getSetting('preview_image_style');
$element['#title_field'] = $field_settings['title_field'];
$element['#title_field_required'] = $field_settings['title_field_required'];
$element['#alt_field'] = $field_settings['alt_field'];
$element['#alt_field_required'] = $field_settings['alt_field_required'];
// Default image.
$default_image = $field_settings['default_image'];
if (empty($default_image['uuid'])) {
$default_image = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('default_image');
}
// Convert the stored UUID into a file ID.
if (!empty($default_image['uuid']) && $entity = \Drupal::service('entity.repository')->loadEntityByUuid('file', $default_image['uuid'])) {
$default_image['fid'] = $entity->id();
}
$element['#default_image'] = !empty($default_image['fid']) ? $default_image : [];
return $element;
}
/**
* Form API callback: Processes an image_image field element.
*
* Expands the image_image type to include the alt and title fields.
*
* This method is assigned as a #process callback in formElement() method.
*/
public static function process($element, FormStateInterface $form_state, $form) {
$item = $element['#value'];
$item['fids'] = $element['fids']['#value'];
$element['#theme'] = 'image_widget';
// Add the image preview.
if (!empty($element['#files']) && $element['#preview_image_style']) {
$file = reset($element['#files']);
$variables = [
'style_name' => $element['#preview_image_style'],
'uri' => $file->getFileUri(),
];
$dimension_key = $variables['uri'] . '.image_preview_dimensions';
// Determine image dimensions.
if (isset($element['#value']['width']) && isset($element['#value']['height'])) {
$variables['width'] = $element['#value']['width'];
$variables['height'] = $element['#value']['height'];
}
elseif ($form_state->has($dimension_key)) {
$variables += $form_state->get($dimension_key);
}
else {
$image = \Drupal::service('image.factory')->get($file->getFileUri());
if ($image->isValid()) {
$variables['width'] = $image->getWidth();
$variables['height'] = $image->getHeight();
}
else {
$variables['width'] = $variables['height'] = NULL;
}
}
$element['preview'] = [
'#weight' => -10,
'#theme' => 'image_style',
'#width' => $variables['width'],
'#height' => $variables['height'],
'#style_name' => $variables['style_name'],
'#uri' => $variables['uri'],
];
// Store the dimensions in the form so the file doesn't have to be
// accessed again. This is important for remote files.
$form_state->set($dimension_key, ['width' => $variables['width'], 'height' => $variables['height']]);
}
elseif (!empty($element['#default_image'])) {
$default_image = $element['#default_image'];
$file = File::load($default_image['fid']);
if (!empty($file)) {
$element['preview'] = [
'#weight' => -10,
'#theme' => 'image_style',
'#width' => $default_image['width'],
'#height' => $default_image['height'],
'#style_name' => $element['#preview_image_style'],
'#uri' => $file->getFileUri(),
];
}
}
// Add the additional alt and title fields.
$element['alt'] = [
'#title' => new TranslatableMarkup('Alternative text'),
'#type' => 'textfield',
'#default_value' => $item['alt'] ?? '',
'#description' => new TranslatableMarkup('Short description of the image used by screen readers and displayed when the image is not loaded. This is important for accessibility.'),
// @see https://www.drupal.org/node/465106#alt-text
'#maxlength' => 512,
'#weight' => -12,
'#access' => (bool) $item['fids'] && $element['#alt_field'],
'#required' => $element['#alt_field_required'],
'#element_validate' => $element['#alt_field_required'] == 1 ? [[static::class, 'validateRequiredFields']] : [],
];
$element['title'] = [
'#type' => 'textfield',
'#title' => new TranslatableMarkup('Title'),
'#default_value' => $item['title'] ?? '',
'#description' => new TranslatableMarkup('The title is used as a tool tip when the user hovers the mouse over the image.'),
'#maxlength' => 1024,
'#weight' => -11,
'#access' => (bool) $item['fids'] && $element['#title_field'],
'#required' => $element['#title_field_required'],
'#element_validate' => $element['#title_field_required'] == 1 ? [[static::class, 'validateRequiredFields']] : [],
];
return parent::process($element, $form_state, $form);
}
/**
* Validate callback for alt and title field, if the user wants them required.
*
* This is separated in a validate function instead of a #required flag to
* avoid being validated on the process callback.
*/
public static function validateRequiredFields($element, FormStateInterface $form_state) {
// Only do validation if the function is triggered from other places than
// the image process form.
$triggering_element = $form_state->getTriggeringElement();
if (!empty($triggering_element['#submit']) && in_array('file_managed_file_submit', $triggering_element['#submit'], TRUE)) {
$form_state->setLimitValidationErrors([]);
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$style_id = $this->getSetting('preview_image_style');
/** @var \Drupal\image\ImageStyleInterface $style */
if ($style_id && $style = ImageStyle::load($style_id)) {
// If this widget uses a valid image style to display the preview of the
// uploaded image, add that image style configuration entity as dependency
// of this widget.
$dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName();
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
$style_id = $this->getSetting('preview_image_style');
/** @var \Drupal\image\ImageStyleInterface $style */
if ($style_id && $style = ImageStyle::load($style_id)) {
if (!empty($dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) {
/** @var \Drupal\image\ImageStyleStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage($style->getEntityTypeId());
$replacement_id = $storage->getReplacementId($style_id);
// If a valid replacement has been provided in the storage, replace the
// preview image style with the replacement.
if ($replacement_id && ImageStyle::load($replacement_id)) {
$this->setSetting('preview_image_style', $replacement_id);
}
// If there's no replacement or the replacement is invalid, disable the
// image preview.
else {
$this->setSetting('preview_image_style', '');
}
// Signal that the formatter plugin settings were updated.
$changed = TRUE;
}
}
return $changed;
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
$element = parent::errorElement($element, $error, $form, $form_state);
$property_path_array = explode('.', $error->getPropertyPath());
return ($element === FALSE) ? FALSE : $element[$property_path_array[1]];
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Converts an image resource.
*/
#[ImageEffect(
id: "image_convert",
label: new TranslatableMarkup("Convert"),
description: new TranslatableMarkup("Converts an image to a format (such as JPEG)."),
)]
class ConvertImageEffect extends ConfigurableImageEffectBase {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
if (!$image->convert($this->configuration['extension'])) {
$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->configuration['extension'];
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$summary = [
'#markup' => mb_strtoupper($this->configuration['extension']),
];
$summary += parent::getSummary();
return $summary;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'extension' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$extensions = \Drupal::service('image.toolkit.manager')->getDefaultToolkit()->getSupportedExtensions();
$options = array_combine(
$extensions,
array_map('mb_strtoupper', $extensions)
);
$form['extension'] = [
'#type' => 'select',
'#title' => $this->t('Convert to'),
'#default_value' => $this->configuration['extension'],
'#required' => TRUE,
'#options' => $options,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['extension'] = $form_state->getValue('extension');
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Component\Utility\Image;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
/**
* Crops an image resource.
*/
#[ImageEffect(
id: "image_crop",
label: new TranslatableMarkup("Crop"),
description: new TranslatableMarkup("Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately."),
)]
class CropImageEffect extends ResizeImageEffect {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
[$x, $y] = explode('-', $this->configuration['anchor']);
$x = Image::getKeywordOffset($x, $image->getWidth(), (int) $this->configuration['width']);
$y = Image::getKeywordOffset($y, $image->getHeight(), (int) $this->configuration['height']);
if (!$image->crop($x, $y, $this->configuration['width'], $this->configuration['height'])) {
$this->logger->error('Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()]);
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$summary = [
'#theme' => 'image_crop_summary',
'#data' => $this->configuration,
];
$summary += parent::getSummary();
return $summary;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'anchor' => 'center-center',
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['anchor'] = [
'#type' => 'radios',
'#title' => $this->t('Anchor'),
'#options' => [
'left-top' => $this->t('Top left'),
'center-top' => $this->t('Top center'),
'right-top' => $this->t('Top right'),
'left-center' => $this->t('Center left'),
'center-center' => $this->t('Center'),
'right-center' => $this->t('Center right'),
'left-bottom' => $this->t('Bottom left'),
'center-bottom' => $this->t('Bottom center'),
'right-bottom' => $this->t('Bottom right'),
],
'#theme' => 'image_anchor',
'#default_value' => $this->configuration['anchor'],
'#description' => $this->t('The part of the image that will be retained during the crop.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['anchor'] = $form_state->getValue('anchor');
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
use Drupal\image\ImageEffectBase;
/**
* Desaturates (grayscale) an image resource.
*/
#[ImageEffect(
id: "image_desaturate",
label: new TranslatableMarkup("Desaturate"),
description: new TranslatableMarkup("Desaturate converts an image to grayscale."),
)]
class DesaturateImageEffect extends ImageEffectBase {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
if (!$image->desaturate()) {
$this->logger->error('Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()]);
return FALSE;
}
return TRUE;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Resizes an image resource.
*/
#[ImageEffect(
id: "image_resize",
label: new TranslatableMarkup("Resize"),
description: new TranslatableMarkup("Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately."),
)]
class ResizeImageEffect extends ConfigurableImageEffectBase {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
if (!$image->resize($this->configuration['width'], $this->configuration['height'])) {
$this->logger->error('Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()]);
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function transformDimensions(array &$dimensions, $uri) {
// The new image will have the exact dimensions defined for the effect.
$dimensions['width'] = $this->configuration['width'];
$dimensions['height'] = $this->configuration['height'];
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$summary = [
'#theme' => 'image_resize_summary',
'#data' => $this->configuration,
];
$summary += parent::getSummary();
return $summary;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'width' => NULL,
'height' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['width'] = [
'#type' => 'number',
'#title' => $this->t('Width'),
'#default_value' => $this->configuration['width'],
'#field_suffix' => ' ' . $this->t('pixels'),
'#required' => TRUE,
'#min' => 1,
];
$form['height'] = [
'#type' => 'number',
'#title' => $this->t('Height'),
'#default_value' => $this->configuration['height'],
'#field_suffix' => ' ' . $this->t('pixels'),
'#required' => TRUE,
'#min' => 1,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['height'] = $form_state->getValue('height');
$this->configuration['width'] = $form_state->getValue('width');
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Component\Utility\Color;
use Drupal\Component\Utility\Rectangle;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Rotates an image resource.
*/
#[ImageEffect(
id: "image_rotate",
label: new TranslatableMarkup("Rotate"),
description: new TranslatableMarkup("Rotating an image may cause the dimensions of an image to increase to fit the diagonal.")
)]
class RotateImageEffect extends ConfigurableImageEffectBase {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
if (!empty($this->configuration['random'])) {
$degrees = abs((float) $this->configuration['degrees']);
$this->configuration['degrees'] = rand(-$degrees, $degrees);
}
if (!$image->rotate($this->configuration['degrees'], $this->configuration['bgcolor'])) {
$this->logger->error('Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()]);
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function transformDimensions(array &$dimensions, $uri) {
// If the rotate is not random and current dimensions are set,
// then the new dimensions can be determined.
if (!$this->configuration['random'] && $dimensions['width'] && $dimensions['height']) {
$rect = new Rectangle($dimensions['width'], $dimensions['height']);
$rect = $rect->rotate($this->configuration['degrees']);
$dimensions['width'] = $rect->getBoundingWidth();
$dimensions['height'] = $rect->getBoundingHeight();
}
else {
$dimensions['width'] = $dimensions['height'] = NULL;
}
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$summary = [
'#theme' => 'image_rotate_summary',
'#data' => $this->configuration,
];
$summary += parent::getSummary();
return $summary;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'degrees' => 0,
'bgcolor' => NULL,
'random' => FALSE,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['degrees'] = [
'#type' => 'number',
'#default_value' => $this->configuration['degrees'],
'#title' => $this->t('Rotation angle'),
'#description' => $this->t('The number of degrees the image should be rotated. Positive numbers are clockwise, negative are counter-clockwise.'),
'#field_suffix' => '°',
'#required' => TRUE,
];
$form['bgcolor'] = [
'#type' => 'textfield',
'#default_value' => $this->configuration['bgcolor'],
'#title' => $this->t('Background color'),
'#description' => $this->t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave blank for transparency on image types that support it.'),
'#size' => 7,
'#maxlength' => 7,
];
$form['random'] = [
'#type' => 'checkbox',
'#default_value' => $this->configuration['random'],
'#title' => $this->t('Randomize'),
'#description' => $this->t('Randomize the rotation angle for each image. The angle specified above is used as a maximum.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
if (!$form_state->isValueEmpty('bgcolor') && !Color::validateHex($form_state->getValue('bgcolor'))) {
$form_state->setErrorByName('bgcolor', $this->t('Background color must be a hexadecimal color value.'));
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['degrees'] = $form_state->getValue('degrees');
$this->configuration['bgcolor'] = $form_state->getValue('bgcolor');
$this->configuration['random'] = $form_state->getValue('random');
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Component\Utility\Image;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
/**
* Scales and crops an image resource.
*/
#[ImageEffect(
id: "image_scale_and_crop",
label: new TranslatableMarkup("Scale and crop"),
description: new TranslatableMarkup("Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.")
)]
class ScaleAndCropImageEffect extends CropImageEffect {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
$width = (int) $this->configuration['width'];
$height = (int) $this->configuration['height'];
$scale = max($width / $image->getWidth(), $height / $image->getHeight());
[$x, $y] = explode('-', $this->configuration['anchor']);
$x = Image::getKeywordOffset($x, (int) round($image->getWidth() * $scale), $width);
$y = Image::getKeywordOffset($y, (int) round($image->getHeight() * $scale), $height);
if (!$image->apply('scale_and_crop', ['x' => $x, 'y' => $y, 'width' => $width, 'height' => $height])) {
$this->logger->error('Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()]);
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$summary = [
'#theme' => 'image_scale_and_crop_summary',
'#data' => $this->configuration,
];
$summary += parent::getSummary();
return $summary;
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Component\Utility\Image;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\image\Attribute\ImageEffect;
/**
* Scales an image resource.
*/
#[ImageEffect(
id: "image_scale",
label: new TranslatableMarkup("Scale"),
description: new TranslatableMarkup("Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.")
)]
class ScaleImageEffect extends ResizeImageEffect {
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
if (!$image->scale($this->configuration['width'], $this->configuration['height'], $this->configuration['upscale'])) {
$this->logger->error('Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', ['%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()]);
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function transformDimensions(array &$dimensions, $uri) {
if ($dimensions['width'] && $dimensions['height']) {
Image::scaleDimensions($dimensions, $this->configuration['width'], $this->configuration['height'], $this->configuration['upscale']);
}
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$summary = [
'#theme' => 'image_scale_summary',
'#data' => $this->configuration,
];
$summary += parent::getSummary();
return $summary;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'upscale' => FALSE,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['width']['#required'] = FALSE;
$form['height']['#required'] = FALSE;
$form['upscale'] = [
'#type' => 'checkbox',
'#default_value' => $this->configuration['upscale'],
'#title' => $this->t('Allow Upscaling'),
'#description' => $this->t('Let scale make images larger than their original size.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
if ($form_state->isValueEmpty('width') && $form_state->isValueEmpty('height')) {
$form_state->setErrorByName('data', $this->t('Width and height can not both be blank.'));
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['upscale'] = $form_state->getValue('upscale');
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Drupal\image\Plugin\migrate\destination;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* Entity image style destination.
*
* Every migration that uses this destination must have an optional
* dependency on the d6_file migration to ensure it runs first.
*/
#[MigrateDestination('entity:image_style')]
class EntityImageStyle extends EntityConfigBase {
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$effects = [];
// Need to set the effects property to null on the row before the ImageStyle
// is created, this prevents improper effect plugin initialization.
if ($row->getDestinationProperty('effects')) {
$effects = $row->getDestinationProperty('effects');
$row->setDestinationProperty('effects', []);
}
/** @var \Drupal\image\Entity\ImageStyle $style */
$style = $this->getEntity($row, $old_destination_id_values);
// Iterate the effects array so each effect plugin can be initialized.
// Catch any missing plugin exceptions.
foreach ($effects as $effect) {
try {
$style->addImageEffect($effect);
}
catch (PluginNotFoundException $e) {
throw new MigrateException($e->getMessage(), 0, $e);
}
}
$style->save();
return [$style->id()];
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Drupal\image\Plugin\migrate\field\d6;
use Drupal\file\Plugin\migrate\field\d6\FileField;
use Drupal\migrate_drupal\Attribute\MigrateField;
// cspell:ignore imagefield
/**
* MigrateField Plugin for Drupal 6 image fields.
*/
#[MigrateField(
id: 'imagefield',
core: [6],
source_module: 'imagefield',
destination_module: 'image',
)]
class ImageField extends FileField {}

View File

@ -0,0 +1,48 @@
<?php
namespace Drupal\image\Plugin\migrate\field\d7;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* Migrate field plugin for Drupal 7 image fields.
*/
#[MigrateField(
id: 'image',
core: [7],
source_module: 'image',
destination_module: 'image',
)]
class ImageField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'image' => 'image_default',
'image_miw' => 'image_image',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'fid',
'alt' => 'alt',
'title' => 'title',
'width' => 'width',
'height' => 'height',
],
];
$migration->mergeProcessOfProperty($field_name, $process);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Drupal\image\Plugin\migrate\process\d6;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
// cspell:ignore imagecache
/**
* Defines the image cache actions migrate process plugin.
*/
#[MigrateProcess('d6_imagecache_actions')]
class ImageCacheActions extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$effects = [];
foreach ($row->getSourceProperty('actions') as $action) {
if (empty($action['action'])) {
continue;
}
$id = preg_replace('/^imagecache/', 'image', $action['action']);
if ($id === 'image_crop') {
$action['data']['anchor'] = $action['data']['xoffset'] . '-' . $action['data']['yoffset'];
if (!preg_match('/^[a-z]*\-[a-z]*/', $action['data']['anchor'])) {
$migrate_executable->message->display(
'The Drupal 8 image crop effect does not support numeric values for x and y offsets. Use keywords to set crop effect offsets instead.',
'error'
);
}
unset($action['data']['xoffset']);
unset($action['data']['yoffset']);
}
$effects[] = [
'id' => $id,
'weight' => $action['weight'],
'data' => $action['data'],
];
}
return $effects;
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\image\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
// cspell:ignore imagecache presetid presetname
/**
* Drupal 6 imagecache presets source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_imagecache_presets",
* source_module = "imagecache"
* )
*/
class ImageCachePreset extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('imagecache_preset', 'icp')
->fields('icp');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'presetid' => $this->t('Preset ID'),
'presetname' => $this->t('Preset Name'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['presetid']['type'] = 'integer';
return $ids;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$actions = [];
$results = $this->select('imagecache_action', 'ica')
->fields('ica')
->condition('presetid', $row->getSourceProperty('presetid'))
->execute();
foreach ($results as $key => $result) {
$actions[$key] = $result;
$actions[$key]['data'] = unserialize($result['data']);
}
$row->setSourceProperty('actions', $actions);
return parent::prepareRow($row);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\image\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
// cspell:ignore isid
/**
* Drupal 7 image styles source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_image_styles",
* source_module = "image"
* )
*/
class ImageStyles extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('image_styles', 'ims')
->fields('ims');
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'isid' => $this->t('The primary identifier for an image style.'),
'name' => $this->t('The style machine name.'),
'label' => $this->t('The style administrative name.'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['isid']['type'] = 'integer';
return $ids;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$effects = [];
$results = $this->select('image_effects', 'ie')
->fields('ie')
->condition('isid', $row->getSourceProperty('isid'))
->execute();
foreach ($results as $key => $result) {
$result['data'] = unserialize($result['data']);
$effects[$key] = $result;
}
$row->setSourceProperty('effects', $effects);
return parent::prepareRow($row);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Drupal\image\Routing;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Defines a route subscriber to register a URL for serving image styles.
*/
class ImageStyleRoutes implements ContainerInjectionInterface {
/**
* The stream wrapper manager service.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* Constructs a new ImageStyleRoutes object.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
* The stream wrapper manager service.
*/
public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager) {
$this->streamWrapperManager = $stream_wrapper_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('stream_wrapper_manager')
);
}
/**
* Returns an array of route objects.
*
* @return \Symfony\Component\Routing\Route[]
* An array of route objects.
*/
public function routes() {
$routes = [];
// Generate image derivatives of publicly available files. If clean URLs are
// disabled image derivatives will always be served through the menu system.
// If clean URLs are enabled and the image derivative already exists, PHP
// will be bypassed.
$directory_path = $this->streamWrapperManager->getViaScheme('public')->getDirectoryPath();
$routes['image.style_public'] = new Route(
'/' . $directory_path . '/styles/{image_style}/{scheme}',
[
'_controller' => 'Drupal\image\Controller\ImageStyleDownloadController::deliver',
'required_derivative_scheme' => 'public',
],
[
'_access' => 'TRUE',
]
);
return $routes;
}
}