diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml
index 924d454f91..97e107ca2f 100644
--- a/core/modules/image/config/schema/image.schema.yml
+++ b/core/modules/image/config/schema/image.schema.yml
@@ -157,3 +157,14 @@ field.widget.settings.image_image:
preview_image_style:
type: string
label: 'Preview image style'
+
+filter_settings.filter_image_style:
+ type: filter
+ label: 'Display image styles'
+ mapping:
+ allowed_styles:
+ type: sequence
+ label: 'Allowed image styles'
+ sequence:
+ type: string
+ label: 'Image style ID'
diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 7dc1ff444e..0ddb5000f9 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -6,11 +6,14 @@
*/
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\file\Entity\File;
-use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
+use Drupal\image\ImageStyleInterface;
/**
* Image style constant for user presets in the database.
@@ -498,3 +501,119 @@ function image_field_config_delete(FieldConfigInterface $field) {
\Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
}
}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Alters the image dialog form for text editor, to allow the user to select an
+ * image style.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog::buildForm()
+ */
+function image_form_editor_image_dialog_alter(&$form, FormStateInterface $form_state) {
+ /** @var \Drupal\editor\EditorInterface $editor */
+ $editor = $form_state->getBuildInfo()['args'][0];
+
+ // Alter the editor image dialog when image style functionality is available.
+ /** @var \Drupal\image\Plugin\Filter\FilterImageStyle $filter */
+ $filter = $editor->getFilterFormat()->filters('filter_image_style');
+ if ($filter->status) {
+ // Get the image () that is being edited on the client.
+ $image_element = $form_state->get('image_element');
+
+ // Get an array of image styles to present as options for selection.
+ $image_style_options = array_map(function (ImageStyleInterface $image_style) {
+ return $image_style->label();
+ }, $filter->getAllowedImageStyles());
+
+ // If there is only a single image style the user doesn't have any choice.
+ // Just show a message in that case.
+ if (count($image_style_options) == 1) {
+ $label = reset($image_style_options);
+ $form['image_style']['selection'] = [
+ '#type' => 'value',
+ '#value' => key($image_style_options),
+ '#parents' => ['attributes', 'data-image-style'],
+ ];
+ $form['image_style']['help'] = [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => t('The image will be displayed using the %image_style image style.', ['%image_style' => $label]),
+ ];
+ }
+ else {
+ // Add a select element to choose an image style.
+ $form['image_style']['selection'] = [
+ '#title' => t('Use image style'),
+ '#type' => 'select',
+ '#default_value' => isset($image_element['data-image-style']) ? $image_element['data-image-style'] : NULL,
+ '#options' => $image_style_options,
+ '#required' => TRUE,
+ '#parents' => ['attributes', 'data-image-style'],
+ ];
+ }
+
+ // Add a link to the configuration of the image styles if the user has
+ // access to it.
+ $access_manager = \Drupal::service('access_manager');
+ $route_parameters = ['filter_format' => $editor->getFilterFormat()->id()];
+ if ($access_manager->checkNamedRoute('entity.filter_format.edit_form', $route_parameters, \Drupal::currentUser())) {
+ $url = Url::fromRoute('entity.filter_format.edit_form', $route_parameters);
+ $url->setOptions(['fragment' => 'edit-filters-filter-image-style-settings']);
+ $form['image_style']['link'] = [
+ '#title' => t('Configure allowed image styles'),
+ '#type' => 'link',
+ '#url' => $url,
+ ];
+ }
+
+ $form['actions']['save_modal']['#validate'][] = 'image_form_editor_image_dialog_validate';
+ }
+}
+
+/**
+ * Form validation handler for EditorImageDialog.
+ *
+ * Ensures the image shown in the text editor matches the chosen image style.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog::buildForm()
+ * @see \Drupal\editor\Form\EditorImageDialog::validateForm()
+ * @see image_form_editor_image_dialog_alter()
+ */
+function image_form_editor_image_dialog_validate(array &$form, FormStateInterface &$form_state) {
+ if (!empty($form_state->getValue('fid')[0])) {
+
+ $attributes = &$form_state->getValue('attributes');
+
+ /** @var \Drupal\image\ImageStyleInterface $image_style */
+ $image_style = ImageStyle::load($attributes['data-image-style']);
+
+ /** @var \Drupal\file\FileInterface $file */
+ $file = File::load($form_state->getValue('fid')[0]);
+
+ $uri = $file->getFileUri();
+
+ // Set the 'src' attribute to the image style URL. FilterImageStyle will
+ // look at the 'data-editor-file-uuid' attribute, not the 'src' attribute to
+ // render the appropriate output.
+ $image_url = $image_style->buildUrl($uri);
+ $attributes['src'] = file_url_transform_relative($image_url);
+
+ /** @var \Drupal\Core\Image\ImageInterface $image */
+ $image = \Drupal::service('image.factory')->get($uri);
+
+ if ($image->isValid()) {
+ // Get the original width and height of the image.
+ $dimensions = [
+ 'width' => $image->getWidth(),
+ 'height' => $image->getHeight(),
+ ];
+
+ // Transform the 'width' and 'height' dimensions of the image based on the
+ // image style.
+ $image_style->transformDimensions($dimensions, $attributes['src']);
+ $attributes['width'] = $dimensions['width'];
+ $attributes['height'] = $dimensions['height'];
+ }
+ }
+}
diff --git a/core/modules/image/image.post_update.php b/core/modules/image/image.post_update.php
index 04d8c4b7b9..79ce7e52b8 100644
--- a/core/modules/image/image.post_update.php
+++ b/core/modules/image/image.post_update.php
@@ -7,6 +7,8 @@
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\filter\Plugin\FilterInterface;
/**
* Saves the image style dependencies into form and view display entities.
@@ -20,3 +22,39 @@ function image_post_update_image_style_dependencies() {
$display->save();
}
}
+
+/**
+ * Update filter formats to allow the use of the image style filter.
+ */
+function image_post_update_enable_filter_image_style() {
+ /** @var \Drupal\filter\FilterFormatInterface[] $formats */
+ $formats = FilterFormat::loadMultiple();
+ foreach ($formats as $format) {
+ $changed = FALSE;
+ if (in_array($format->id(), ['basic_html', 'full_html'])) {
+ // Enable the image style filter, and set the weight to the highest
+ // current weight + 1 so that it appears last in the list.
+ $highest_weight = array_reduce($format->filters()->getAll(), function ($carry, FilterInterface $filter) {
+ return $filter->status !== FALSE && ($carry === NULL || $filter->weight > $carry) ? $filter->weight : $carry;
+ });
+ $format->setFilterConfig('filter_image_style', ['status' => TRUE, 'weight' => $highest_weight + 1]);
+ $changed = TRUE;
+ }
+ // Update the allowed html tags of filter_html filter if it's enabled.
+ if ($filter = $format->filters('filter_html')) {
+ $config = $filter->getConfiguration();
+ $allowed_html = !empty($config['settings']['allowed_html']) ? $config['settings']['allowed_html'] : NULL;
+ $matches = [];
+ if ($allowed_html && preg_match('/]*)>/', $allowed_html, $matches)) {
+ $attributes = array_filter(explode(' ', $matches[1]));
+ $attributes[] = 'data-image-style';
+ $config['settings']['allowed_html'] = preg_replace('/]*)>/', '', $allowed_html);
+ $format->setFilterConfig('filter_html', $config);
+ $changed = TRUE;
+ }
+ }
+ if ($changed) {
+ $format->save();
+ }
+ }
+}
diff --git a/core/modules/image/js/plugins/drupalimagestyle/plugin.js b/core/modules/image/js/plugins/drupalimagestyle/plugin.js
new file mode 100644
index 0000000000..ef8eae1d53
--- /dev/null
+++ b/core/modules/image/js/plugins/drupalimagestyle/plugin.js
@@ -0,0 +1,112 @@
+/**
+ * @file
+ * Drupal Image Style plugin.
+ *
+ * This alters the existing CKEditor image2 widget plugin, which is already
+ * altered by the Drupal Image plugin, to allow for the data-image-style
+ * attribute to be set.
+ *
+ * @ignore
+ */
+
+(function (CKEDITOR) {
+
+ 'use strict';
+
+ CKEDITOR.plugins.add('drupalimagestyle', {
+ requires: 'drupalimage',
+
+ beforeInit: function (editor) {
+ // Override the image2 widget definition to handle the additional
+ // data-image-style attributes.
+ editor.on('widgetDefinition', function (event) {
+ var widgetDefinition = event.data;
+ if (widgetDefinition.name !== 'image') {
+ return;
+ }
+ // Override default features definitions for drupalimagestyle.
+ CKEDITOR.tools.extend(widgetDefinition.features, {
+ drupalimagestyle: {
+ requiredContent: 'img[data-image-style]'
+ }
+ }, true);
+
+ // Override requiredContent & allowedContent.
+ var requiredContent = widgetDefinition.requiredContent.getDefinition();
+ requiredContent.attributes['data-image-style'] = '';
+ widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
+ widgetDefinition.allowedContent.img.attributes['!data-image-style'] = true;
+
+ // Decorate downcast().
+ var originalDowncast = widgetDefinition.downcast;
+ widgetDefinition.downcast = function (element) {
+ var img = originalDowncast.call(this, element);
+ if (!img) {
+ img = findElementByName(element, 'img');
+ }
+ img.attributes['data-image-style'] = this.data['data-image-style'];
+ return img;
+ };
+
+ // Decorate upcast().
+ var originalUpcast = widgetDefinition.upcast;
+ widgetDefinition.upcast = function (element, data) {
+ if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) {
+ return;
+ }
+ // Don't initialize on pasted fake objects.
+ else if (element.attributes['data-cke-realelement']) {
+ return;
+ }
+
+ // Parse the data-image-style attribute.
+ data['data-image-style'] = element.attributes['data-image-style'];
+
+ // Upcast after parsing so correct element attributes are parsed.
+ element = originalUpcast.call(this, element, data);
+
+ return element;
+ };
+
+ // Protected; keys of the widget data to be sent to the Drupal dialog.
+ // Append to the values defined by the drupalimage plugin.
+ // @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js
+ CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, {
+ 'data-image-style': 'data-image-style'
+ });
+ // Low priority to ensure drupalimage's event handler runs first.
+ }, null, null, 20);
+ }
+ });
+
+ /**
+ * Finds an element by its name.
+ *
+ * Function will check first the passed element itself and then all its
+ * children in DFS order.
+ *
+ * @param {CKEDITOR.htmlParser.element} element
+ * The element to search.
+ * @param {string} name
+ * The element name to search for.
+ *
+ * @return {?CKEDITOR.htmlParser.element}
+ * The found element, or null.
+ */
+ function findElementByName(element, name) {
+ if (element.name === name) {
+ return element;
+ }
+
+ var found = null;
+ element.forEach(function (el) {
+ if (el.name === name) {
+ found = el;
+ // Stop here.
+ return false;
+ }
+ }, CKEDITOR.NODE_ELEMENT);
+ return found;
+ }
+
+})(CKEDITOR);
diff --git a/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php
new file mode 100644
index 0000000000..0e6401b5b4
--- /dev/null
+++ b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php
@@ -0,0 +1,77 @@
+hasAssociatedFilterFormat()) {
+ return FALSE;
+ }
+
+ // Automatically enable this plugin if the text format associated with this
+ // text editor uses the filter_image_style filter and the DrupalImage button
+ // is enabled.
+ $format = $editor->getFilterFormat();
+ if ($format->filters('filter_image_style')->status) {
+ $toolbarButtons = CKEditorPluginManager::getEnabledButtons($editor);
+ return in_array('DrupalImage', $toolbarButtons);
+ }
+
+ return FALSE;
+ }
+
+}
diff --git a/core/modules/image/src/Plugin/Filter/FilterImageStyle.php b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php
new file mode 100644
index 0000000000..dec7c41d49
--- /dev/null
+++ b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php
@@ -0,0 +1,309 @@
+entityTypeManager = $entity_type_manager;
+ $this->entityRepository = $entity_repository;
+ $this->imageFactory = $image_factory;
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager'),
+ $container->get('entity.repository'),
+ $container->get('image.factory'),
+ $container->get('renderer')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function process($text, $langcode) {
+ if (stristr($text, 'data-image-style') !== FALSE) {
+ // Load all image styles so each image found in the text can be checked
+ // against a valid image style.
+ $image_styles = $this->getAllowedImageStyleIds();
+
+ $dom = Html::load($text);
+ $xpath = new \DOMXPath($dom);
+
+ // Process each image element found with the necessary attributes.
+ /** @var \DOMElement $dom_element */
+ foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid and @data-image-style]') as $dom_element) {
+ // Get the UUID and image style for the file.
+ $file_uuid = $dom_element->getAttribute('data-entity-uuid');
+ $image_style_id = $dom_element->getAttribute('data-image-style');
+
+ // If the image style is not a valid one, then don't transform the HTML.
+ if (empty($file_uuid) || !in_array($image_style_id, $image_styles)) {
+ continue;
+ }
+
+ // Transform the HTML for the img element by applying an image style.
+ $altered_img_markup = $this->getImageStyleHtml($file_uuid, $image_style_id, $dom_element);
+ $altered_img = $dom->createDocumentFragment();
+ $altered_img->appendXML($altered_img_markup);
+ $dom_element->parentNode->replaceChild($altered_img, $dom_element);
+ }
+
+ return new FilterProcessResult(Html::serialize($dom));
+ }
+
+ // Process the filter if no image style img elements are found.
+ return new FilterProcessResult($text);
+ }
+
+ /**
+ * Loads image styles.
+ *
+ * @param array $ids
+ * Optional array of image style IDs to load. If omitted, all image styles
+ * will be returned.
+ *
+ * @return \Drupal\image\ImageStyleInterface[]
+ * The image styles.
+ */
+ protected function loadImageStyles(array $ids = NULL) {
+ return $this->entityTypeManager->getStorage('image_style')->loadMultiple($ids);
+ }
+
+ /**
+ * Returns the machine names of the allowed image styles.
+ *
+ * @return string[]
+ * The machine names of the allowed image styles.
+ */
+ protected function getAllowedImageStyleIds() {
+ if (!empty($this->settings['allowed_styles'])) {
+ return $this->settings['allowed_styles'];
+ }
+ // If no image styles are selected, then all are allowed.
+ return array_keys($this->loadImageStyles());
+ }
+
+ /**
+ * Returns the allowed image styles.
+ *
+ * @return \Drupal\image\ImageStyleInterface[]
+ * The allowed image styles.
+ */
+ public function getAllowedImageStyles() {
+ $ids = !empty($this->settings['allowed_styles']) ? $this->settings['allowed_styles'] : NULL;
+ return $this->loadImageStyles($ids);
+ }
+
+ /**
+ * Gets the the width and height of an image based on the file UUID.
+ *
+ * @param string $file_uuid
+ * The UUID for the file.
+ *
+ * @return array
+ * The image information.
+ */
+ protected function getImageInfo($file_uuid) {
+ /** @var \Drupal\file\FileInterface $file */
+ $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid);
+
+ // Determine uri, width and height of the source image.
+ $image_uri = $image_width = $image_height = NULL;
+ $image = $this->imageFactory->get($file->getFileUri());
+ if ($image->isValid()) {
+ $image_uri = $file->getFileUri();
+ $image_width = $image->getWidth();
+ $image_height = $image->getHeight();
+ }
+
+ return [
+ 'uri' => $image_uri,
+ 'width' => $image_width,
+ 'height' => $image_height,
+ ];
+ }
+
+ /**
+ * Removes attributes that will be generated from image style theme function.
+ *
+ * @param \DOMElement $dom_element
+ * The DOM element for the img element.
+ *
+ * @return array
+ * The attributes array.
+ */
+ protected function prepareImageAttributes(\DOMElement $dom_element) {
+ // Remove attributes that are generated by the image style.
+ $dom_element->removeAttribute('width');
+ $dom_element->removeAttribute('height');
+ $dom_element->removeAttribute('src');
+
+ // Make sure all non-regenerated attributes are retained.
+ $attributes = [];
+ for ($i = 0; $i < $dom_element->attributes->length; $i++) {
+ $attr = $dom_element->attributes->item($i);
+ $attributes[$attr->name] = $attr->value;
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Gets the HTML for the image element after image style is applied.
+ *
+ * @param string $file_uuid
+ * The UUID for the file.
+ * @param string $image_style_id
+ * The ID for the image style.
+ * @param \DOMElement $dom_element
+ * The DOM element for the image element.
+ *
+ * @return string
+ * The img element with the image style applied.
+ */
+ protected function getImageStyleHtml($file_uuid, $image_style_id, \DOMElement $dom_element) {
+ $image_info = $this->getImageInfo($file_uuid);
+
+ // Remove attributes that will be generated by the image style.
+ $attributes = $this->prepareImageAttributes($dom_element);
+
+ // Re-render as an image style.
+ $image = [
+ '#theme' => 'image_style',
+ '#style_name' => $image_style_id,
+ '#uri' => $image_info['uri'],
+ '#width' => $image_info['width'],
+ '#height' => $image_info['height'],
+ '#attributes' => $attributes,
+ ];
+
+ $output = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$image) {
+ return $this->renderer->render($image);
+ });
+
+ return $output;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $image_styles = $this->loadImageStyles();
+ $options = array_map(function (ImageStyleInterface $image_style) {
+ return $image_style->label();
+ }, $image_styles);
+ $form['allowed_styles'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Allowed image styles'),
+ '#options' => $options,
+ '#default_value' => $this->settings['allowed_styles'],
+ '#description' => $this->t('The image styles that can be used. If none are selected then all image styles can be used.'),
+ '#multiple' => TRUE,
+ // Limit the select box in length if there are a large number of image
+ // styles.
+ '#size' => min(20, count($image_styles)),
+ ];
+ return $form;
+ }
+
+ public function setConfiguration(array $configuration) {
+ return parent::setConfiguration($configuration); // TODO: Change the autogenerated stub
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function tips($long = FALSE) {
+ if ($long) {
+ $image_styles = $this->getAllowedStyles();
+ $list = '' . implode('
, ', $image_styles) . '
';
+ return t('
You can display images using site-wide styles by adding a data-image-style
attribute, whose values is one of the image style machine names: !image-style-machine-name-list.
-
-
-
',
+ 'basic_html' => ' -
-
-
',
'restricted_html' => ' -
-
-
',
];
foreach ($filters_after as $name => $after) {
diff --git a/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml
index 92224c23cf..7a30fcd000 100644
--- a/core/profiles/standard/config/install/filter.format.basic_html.yml
+++ b/core/profiles/standard/config/install/filter.format.basic_html.yml
@@ -3,6 +3,7 @@ status: true
dependencies:
module:
- editor
+ - image
name: 'Basic HTML'
format: basic_html
weight: 0
@@ -15,7 +16,7 @@ filters:
status: true
weight: -10
settings:
- allowed_html: '