diff --git a/core/modules/image/src/Plugin/DataType/ComputedImageStyleDerivatives.php b/core/modules/image/src/Plugin/DataType/ComputedImageStyleDerivatives.php new file mode 100644 index 0000000000..724618e45b --- /dev/null +++ b/core/modules/image/src/Plugin/DataType/ComputedImageStyleDerivatives.php @@ -0,0 +1,190 @@ +valueComputed === FALSE) { + $this->computeValue(); + $this->valueComputed = TRUE; + } + } + + /** + * {@inheritdoc} + */ + protected function computeValue() { + /** @var \Drupal\image\Plugin\Field\FieldType\ImageItem $image_item */ + $image_item = $this->getParent(); + + /** @var \Drupal\file\FileInterface $file */ + $file = $image_item->entity; + $width = $image_item->width; + $height = $image_item->height; + if (!$file) { + return; + } + + $image_style_storage = \Drupal::entityTypeManager()->getStorage('image_style'); + $typed_data_manager = $this->getTypedDataManager(); + + if ($this->definition instanceof CacheableDependencyInterface) { + $this->addCacheableDependency($this->definition); + } + $image_style_ids = array_keys($this->definition->getPropertyDefinitions()); + /** @var \Drupal\image\ImageStyleInterface $style */ + foreach ($image_style_storage->loadMultiple($image_style_ids) as $style) { + $this->addCacheableDependency($style); + $value = $this->computeImageStyleMetadata($file, $width, $height, $style); + if ($value) { + $this->values[$style->getName()] = $typed_data_manager + ->getPropertyInstance($this, $style->getName(), $value); + } + } + } + + /** + * @param \Drupal\file\FileInterface $file + * @param $width + * @param $height + * @param \Drupal\image\ImageStyleInterface $style + * @return array|bool + * + * @todo rather than returning an array, return a value object that + * implements CacheableDependencyInterface + * + * @see \Drupal\image\Entity\ImageStyle::buildUrl + * -> tag: config:image.settings + * + * -> tag: $style->getCacheTags() + */ + protected function computeImageStyleMetadata(FileInterface $file, $width, $height, ImageStyleInterface $style) { + $file_uri = $file->getFileUri(); + + if (!$style->supportsUri($file_uri)) { + return NULL; + } + + $dimensions = [ + 'width' => $width, + 'height' => $height, + ]; + $style->transformDimensions($dimensions, $file_uri); + + return [ + 'url' => file_url_transform_relative($style->buildUrl($file_uri)), + // Cast this to strings as the data definition will cast them back to + // typed values. This makes this value behave as if they came from the + // database where all values are strings. + 'width' => (string) $dimensions['width'], + 'height' => (string) $dimensions['height'], + ]; + } + + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE) { + parent::setValue($values, $notify); + + if (isset($values)) { + $this->valueComputed = TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function get($index) { + $this->ensureComputedValue(); + return isset($this->values[$index]) ? $this->values[$index] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getString() { + $this->ensureComputedValue(); + return parent::getString(); + } + + /** + * {@inheritdoc} + */ + public function getProperties($include_computed = FALSE) { + return array_filter(parent::getProperties($include_computed)); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $this->ensureComputedValue(); + return parent::isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + $this->ensureComputedValue(); + return parent::getIterator(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + $this->ensureComputedValue(); + return $this->cacheTags; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $this->ensureComputedValue(); + return $this->cacheContexts; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + $this->ensureComputedValue(); + return $this->cacheMaxAge; + } + +} diff --git a/core/modules/image/src/Plugin/DataType/ImageStyleDerivativesDefinition.php b/core/modules/image/src/Plugin/DataType/ImageStyleDerivativesDefinition.php new file mode 100644 index 0000000000..8f606ec3b0 --- /dev/null +++ b/core/modules/image/src/Plugin/DataType/ImageStyleDerivativesDefinition.php @@ -0,0 +1,49 @@ +propertyDefinitions)) { + return $this->propertyDefinitions; + } + $image_style_definition = MapDataDefinition::create(); + $image_style_definition->setPropertyDefinition('url', DataDefinition::create('uri')); + $image_style_definition->setPropertyDefinition('width', DataDefinition::create('integer')); + $image_style_definition->setPropertyDefinition('height', DataDefinition::create('integer')); + // Map image style IDs to a definition. + $derivatives = NULL; + \Drupal::moduleHandler() + ->alter('image_derivatives_exposed_image_style_ids', $derivatives); + $this->propertyDefinitions = array_map(function () use ($image_style_definition) { + return $image_style_definition; + }, ImageStyle::loadMultiple($derivatives)); + return $this->propertyDefinitions; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return ['config:image_style_list']; + } + +} diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index ba4677d7ea..142d458950 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -13,6 +13,7 @@ use Drupal\Core\TypedData\DataDefinition; use Drupal\file\Entity\File; use Drupal\file\Plugin\Field\FieldType\FileItem; +use Drupal\image\Plugin\DataType\ImageStyleDerivativesDefinition; /** * Plugin implementation of the 'image' field type. @@ -163,6 +164,12 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setLabel(t('Height')) ->setDescription(t('The height of the image in pixels.')); + $properties['derivatives'] = ImageStyleDerivativesDefinition::create('image_style_derivatives') + ->setLabel(t('Derived images')) + ->setDescription(t('Images derived from the stored image, one per Image Style configured for this site. For each derived image, the URL, width and height are provided.')) + ->setComputed(TRUE) + ->setInternal(FALSE); + return $properties; } diff --git a/core/modules/image/tests/src/Kernel/ImageItemTest.php b/core/modules/image/tests/src/Kernel/ImageItemTest.php index bd2c53460d..57ba39ef96 100644 --- a/core/modules/image/tests/src/Kernel/ImageItemTest.php +++ b/core/modules/image/tests/src/Kernel/ImageItemTest.php @@ -128,7 +128,7 @@ public function testImageItem() { $entity->save(); // Test image item properties. - $expected = ['target_id', 'entity', 'alt', 'title', 'width', 'height']; + $expected = ['target_id', 'entity', 'alt', 'title', 'width', 'height', 'derivatives']; $properties = $entity->getFieldDefinition('image_test')->getFieldStorageDefinition()->getPropertyDefinitions(); $this->assertEqual(array_keys($properties), $expected); diff --git a/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php b/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php index 2b92c73be2..dbc6ee7b5a 100644 --- a/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php +++ b/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php @@ -222,7 +222,14 @@ protected function getNormalizedPostEntity() { * {@inheritdoc} */ protected function getExpectedCacheTags() { - return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:hal.settings']); + $derivatives_cache_tags = [ + 'config:image_style_list', + 'config:image.style.large', + 'config:image.style.medium', + 'config:image.style.thumbnail', + ]; + $tags = array_diff(parent::getExpectedCacheTags(), $derivatives_cache_tags); + return Cache::mergeTags($tags, ['config:hal.settings']); } } diff --git a/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php b/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php index 9dae018a02..92854e7577 100644 --- a/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php +++ b/core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\media\Functional\Rest; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\Cache; use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\media\Entity\Media; @@ -126,6 +127,8 @@ protected function createEntity() { ->setRevisionUserId(static::$auth ? $this->account->id() : 0) ->save(); + $this->config('image.settings')->set('suppress_itok_output', TRUE)->save(TRUE); + return $media; } @@ -189,6 +192,23 @@ protected function getExpectedNormalizedEntity() { 'target_uuid' => $thumbnail->uuid(), 'title' => NULL, 'url' => $thumbnail->createFileUrl(FALSE), + 'derivatives' => [ + 'large' => [ + 'url' => file_url_transform_relative(file_create_url('public://styles/large/public/media-icons/generic/generic.png')), + 'height' => 180, + 'width' => 180, + ], + 'medium' => [ + 'url' => file_url_transform_relative(file_create_url('public://styles/medium/public/media-icons/generic/generic.png')), + 'height' => 180, + 'width' => 180, + ], + 'thumbnail' => [ + 'url' => file_url_transform_relative(file_create_url('public://styles/thumbnail/public/media-icons/generic/generic.png')), + 'height' => 100, + 'width' => 100, + ], + ], ], ], 'status' => [ @@ -433,6 +453,21 @@ protected function getExpectedNormalizedFileEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags( + parent::getExpectedCacheTags(), + [ + 'config:image_style_list', + 'config:image.style.large', + 'config:image.style.medium', + 'config:image.style.thumbnail', + ] + ); + } + /** * {@inheritdoc} */ diff --git a/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php b/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php index a7e588ad35..8f42693344 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php @@ -99,6 +99,15 @@ protected function applyXmlFieldDecodingQuirks(array $normalization) { $width = (string) $width; $target_id = &$normalization[$field_name][$i]['target_id']; $target_id = (string) $target_id; + if ($normalization[$field_name][$i]['derivatives'] && is_array($normalization[$field_name][$i]['derivatives'])) { + $derivatives = &$normalization[$field_name][$i]['derivatives']; + if (is_array($derivatives)) { + foreach (array_keys($derivatives) as $image_style) { + $derivatives[$image_style]['width'] = (string) $derivatives[$image_style]['width']; + $derivatives[$image_style]['height'] = (string) $derivatives[$image_style]['height']; + } + } + } break; } } diff --git a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php index f8f4c308d0..dd7f5ad2a4 100644 --- a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php +++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php @@ -26,6 +26,7 @@ class ComplexDataNormalizer extends NormalizerBase { * {@inheritdoc} */ public function normalize($object, $format = NULL, array $context = []) { + $this->addCacheableDependency($context, $object); $attributes = []; // $object will not always match $supportedInterfaceOrClass. // @see \Drupal\serialization\Normalizer\EntityNormalizer