Move all files to 2017/

This commit is contained in:
Oliver Davies 2025-09-29 22:25:17 +01:00
parent ac7370f67f
commit 2875863330
15717 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,3 @@
preview_image: core/modules/image/sample.png
allow_insecure_derivatives: false
suppress_itok_output: false

View file

@ -0,0 +1,14 @@
langcode: en
status: true
dependencies: { }
name: large
label: 'Large (480×480)'
effects:
ddd73aa7-4bd6-4c85-b600-bdf2b1628d1d:
uuid: ddd73aa7-4bd6-4c85-b600-bdf2b1628d1d
id: image_scale
weight: 0
data:
width: 480
height: 480
upscale: false

View file

@ -0,0 +1,14 @@
langcode: en
status: true
dependencies: { }
name: medium
label: 'Medium (220×220)'
effects:
bddf0d06-42f9-4c75-a700-a33cafa25ea0:
uuid: bddf0d06-42f9-4c75-a700-a33cafa25ea0
id: image_scale
weight: 0
data:
width: 220
height: 220
upscale: false

View file

@ -0,0 +1,14 @@
langcode: en
status: true
dependencies: { }
name: thumbnail
label: 'Thumbnail (100×100)'
effects:
1cfec298-8620-4749-b100-ccb6c4500779:
uuid: 1cfec298-8620-4749-b100-ccb6c4500779
id: image_scale
weight: 0
data:
width: 100
height: 100
upscale: false

View file

@ -0,0 +1,29 @@
# Basic data types for image.
image_size:
type: mapping
mapping:
width:
type: integer
label: 'Width'
height:
type: integer
label: 'Height'
field_default_image:
type: mapping
mapping:
uuid:
type: uuid
alt:
type: label
label: 'Alternative text'
title:
type: label
label: 'Title'
width:
type: integer
label: 'Width'
height:
type: integer
label: 'Height'

View file

@ -0,0 +1,163 @@
# Schema for configuration files of the Image module.
image.style.*:
type: config_entity
label: 'Image style'
mapping:
name:
type: string
label:
type: label
label: 'Label'
effects:
type: sequence
sequence:
type: mapping
mapping:
id:
type: string
data:
type: image.effect.[%parent.id]
weight:
type: integer
uuid:
type: uuid
image.effect.*:
type: mapping
label: 'Effect settings'
image.effect.image_crop:
type: image_size
label: 'Image crop'
mapping:
anchor:
label: 'Anchor'
type: string
image.effect.image_convert:
type: mapping
label: 'Convert'
mapping:
extension:
label: 'Extension'
type: string
image.effect.image_resize:
type: image_size
label: 'Image resize'
image.effect.image_rotate:
type: mapping
label: 'Image rotate'
mapping:
degrees:
type: integer
label: 'Rotation angle'
bgcolor:
label: 'Background color'
type: color_hex
random:
type: boolean
label: 'Randomize'
image.effect.image_scale:
type: image_size
label: 'Image scale'
mapping:
upscale:
type: boolean
label: 'Upscale'
# The image desaturate effect has no settings.
image.effect.image_desaturate:
type: sequence
image.effect.image_scale_and_crop:
type: image_size
label: 'Image scale and crop'
mapping:
anchor:
label: 'Anchor'
type: string
image.settings:
type: config_object
mapping:
preview_image:
type: string
label: 'Preview image'
allow_insecure_derivatives:
type: boolean
label: 'Allow insecure image derivatives'
suppress_itok_output:
type: boolean
label: 'Suppress the itok query string for image derivatives'
field.storage_settings.image:
type: field.storage_settings.file
label: 'Image settings'
mapping:
default_image:
type: field_default_image
label: 'Default value'
field.field_settings.image:
type: base_file_field_field_settings
label: 'Image settings'
mapping:
max_resolution:
type: string
label: 'Maximum image resolution'
min_resolution:
type: string
label: 'Minimum image resolution'
alt_field:
type: boolean
label: 'Enable Alt field'
alt_field_required:
type: boolean
label: 'Alt field required'
title_field:
type: boolean
label: 'Enable Title field'
title_field_required:
type: boolean
label: 'Title field required'
default_image:
type: field_default_image
label: 'Default value'
field.value.image:
type: field_default_image
label: 'Default value'
field.formatter.settings.image:
type: mapping
label: 'Image field display format settings'
mapping:
image_link:
type: string
label: 'Link image to'
image_style:
type: string
label: 'Image style'
field.formatter.settings.image_url:
type: mapping
label: 'Image URL formatter settings'
mapping:
image_style:
type: string
label: 'Image style'
field.widget.settings.image_image:
type: mapping
label: 'Image field display format settings'
mapping:
progress_indicator:
type: string
label: 'Progress indicator'
preview_image_style:
type: string
label: 'Preview image style'

View file

@ -0,0 +1,52 @@
/**
* @file
* Functional styles for the Image module's in-place editor.
*/
/**
* A minimum width/height is required so that users can drag and drop files
* onto small images.
*/
.quickedit-image-element {
min-width: 200px;
min-height: 200px;
}
.quickedit-image-dropzone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.quickedit-image-icon {
display: block;
width: 50px;
height: 50px;
background-repeat: no-repeat;
background-size: cover;
}
.quickedit-image-field-info {
display: flex;
align-items: center;
justify-content: flex-end;
}
.quickedit-image-text {
display: block;
}
/**
* If we do not prevent pointer-events for child elements, our drag+drop events
* will not fire properly. This can lead to unintentional redirects if a file
* is dropped on a child element when a user intended to upload it.
*/
.quickedit-image-dropzone * {
pointer-events: none;
}

View file

@ -0,0 +1,100 @@
/**
* @file
* Theme styles for the Image module's in-place editor.
*/
.quickedit-image-dropzone {
background: rgba(116, 183, 255, 0.8);
transition: background 0.2s;
}
.quickedit-image-icon {
margin: 0 0 10px 0;
transition: margin 0.5s;
}
.quickedit-image-dropzone.hover {
background: rgba(116, 183, 255, 0.9);
}
.quickedit-image-dropzone.error {
background: rgba(255, 52, 27, 0.81);
}
.quickedit-image-dropzone.upload .quickedit-image-icon {
background-image: url('../../images/upload.svg');
}
.quickedit-image-dropzone.error .quickedit-image-icon {
background-image: url('../../images/error.svg');
}
.quickedit-image-dropzone.loading .quickedit-image-icon {
margin: -10px 0 20px 0;
}
.quickedit-image-dropzone.loading .quickedit-image-icon::after {
display: block;
content: "";
margin-left: -10px;
margin-top: -5px;
animation-duration: 2s;
animation-name: quickedit-image-spin;
animation-iteration-count: infinite;
animation-timing-function: linear;
width: 60px;
height: 60px;
border-style: solid;
border-radius: 35px;
border-width: 5px;
border-color: white transparent transparent transparent;
}
@keyframes quickedit-image-spin {
0% { transform: rotate(0deg); }
50% { transform: rotate(180deg); }
100% { transform: rotate(360deg); }
}
.quickedit-image-text {
text-align: center;
color: white;
font-family: "Droid sans", "Lucida Grande", sans-serif;
font-size: 16px;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.quickedit-image-field-info {
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid #c5c5c5;
padding: 5px;
}
.quickedit-image-field-info div {
margin-right: 10px; /* LTR */
}
.quickedit-image-field-info div:last-child {
margin-right: 0; /* LTR */
}
[dir="rtl"] .quickedit-image-field-info div {
margin-left: 10px;
margin-right: 0;
}
[dir="rtl"] .quickedit-image-field-info div:last-child {
margin-left: 0;
}
.quickedit-image-errors .messages__wrapper {
margin: 0;
padding: 0;
}
.quickedit-image-errors .messages--error {
box-shadow: none;
}

View file

@ -0,0 +1,74 @@
/**
* Image style configuration pages.
*/
.image-style-new,
.image-style-new div {
display: inline;
}
.image-style-preview .preview-image-wrapper {
float: left;
padding-bottom: 2em;
text-align: center;
top: 50%;
width: 48%;
}
.image-style-preview .preview-image {
margin: auto;
position: relative;
}
.image-style-preview .preview-image .width {
border: 1px solid #666;
border-top: none;
bottom: -6px;
height: 2px;
left: -1px;
position: absolute;
box-sizing: content-box;
}
.image-style-preview .preview-image .width span {
position: relative;
top: 4px;
}
.image-style-preview .preview-image .height {
border: 1px solid #666;
border-left: none;
position: absolute;
right: -6px;
top: -1px;
width: 2px;
box-sizing: content-box;
}
.image-style-preview .preview-image .height span {
height: 2em;
left: 10px;
margin-top: -1em;
position: absolute;
top: 50%;
}
/**
* Improve image style preview on narrow viewports.
*/
@media screen and (max-width: 470px) {
.image-style-preview .preview-image-wrapper {
float: none;
margin-bottom: 1em;
}
.image-style-preview .preview-image-wrapper:last-child {
margin-bottom: 0;
}
}
/**
* Image anchor element.
*/
.image-anchor {
width: auto;
}
.image-anchor tr {
background: none;
}
.image-anchor td {
border: 1px solid #ccc;
}

View file

@ -0,0 +1,134 @@
<?php
/**
* @file
* Administration pages for image settings.
*/
use Drupal\Core\Render\Element;
/**
* Prepares variables for image style preview templates.
*
* Default template: image-style-preview.html.twig.
*
* @param array $variables
* An associative array containing:
* - style: \Drupal\image\ImageStyleInterface image style being previewed.
*/
function template_preprocess_image_style_preview(&$variables) {
// Style information.
$style = $variables['style'];
$variables['style_id'] = $style->id();
$variables['style_name'] = $style->label();
// Cache bypass token.
$variables['cache_bypass'] = REQUEST_TIME;
// Sample image info.
$sample_width = 160;
$sample_height = 160;
$image_factory = \Drupal::service('image.factory');
// Set up original file information.
$original_path = \Drupal::config('image.settings')->get('preview_image');
$original_image = $image_factory->get($original_path);
$variables['original'] = [
'url' => file_url_transform_relative(file_create_url($original_path)),
'width' => $original_image->getWidth(),
'height' => $original_image->getHeight(),
];
if ($variables['original']['width'] > $variables['original']['height']) {
$variables['preview']['original']['width'] = min($variables['original']['width'], $sample_width);
$variables['preview']['original']['height'] = round($variables['preview']['original']['width'] / $variables['original']['width'] * $variables['original']['height']);
}
else {
$variables['preview']['original']['height'] = min($variables['original']['height'], $sample_height);
$variables['preview']['original']['width'] = round($variables['preview']['original']['height'] / $variables['original']['height'] * $variables['original']['width']);
}
// Set up derivative file information.
$preview_file = $style->buildUri($original_path);
// Create derivative if necessary.
if (!file_exists($preview_file)) {
$style->createDerivative($original_path, $preview_file);
}
$preview_image = $image_factory->get($preview_file);
$variables['derivative'] = [
'url' => file_url_transform_relative(file_create_url($preview_file)),
'width' => $preview_image->getWidth(),
'height' => $preview_image->getHeight(),
];
if ($variables['derivative']['width'] > $variables['derivative']['height']) {
$variables['preview']['derivative']['width'] = min($variables['derivative']['width'], $sample_width);
$variables['preview']['derivative']['height'] = round($variables['preview']['derivative']['width'] / $variables['derivative']['width'] * $variables['derivative']['height']);
}
else {
$variables['preview']['derivative']['height'] = min($variables['derivative']['height'], $sample_height);
$variables['preview']['derivative']['width'] = round($variables['preview']['derivative']['height'] / $variables['derivative']['height'] * $variables['derivative']['width']);
}
// Build the preview of the original image.
$variables['original']['rendered'] = [
'#theme' => 'image',
'#uri' => $original_path,
'#alt' => t('Sample original image'),
'#title' => '',
'#attributes' => [
'width' => $variables['original']['width'],
'height' => $variables['original']['height'],
'style' => 'width: ' . $variables['preview']['original']['width'] . 'px; height: ' . $variables['preview']['original']['height'] . 'px;',
],
];
// Build the preview of the image style derivative. Timestamps are added
// to prevent caching of images on the client side.
$variables['derivative']['rendered'] = [
'#theme' => 'image',
'#uri' => $variables['derivative']['url'] . '?cache_bypass=' . $variables['cache_bypass'],
'#alt' => t('Sample modified image'),
'#title' => '',
'#attributes' => [
'width' => $variables['derivative']['width'],
'height' => $variables['derivative']['height'],
'style' => 'width: ' . $variables['preview']['derivative']['width'] . 'px; height: ' . $variables['preview']['derivative']['height'] . 'px;',
],
];
}
/**
* Prepares variables for image anchor templates.
*
* Default template: image-anchor.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the image.
*/
function template_preprocess_image_anchor(&$variables) {
$element = $variables['element'];
$rows = [];
$row = [];
foreach (Element::children($element) as $n => $key) {
$element[$key]['#attributes']['title'] = $element[$key]['#title'];
unset($element[$key]['#title']);
$row[] = [
'data' => $element[$key],
];
if ($n % 3 == 3 - 1) {
$rows[] = $row;
$row = [];
}
}
$variables['table'] = [
'#type' => 'table',
'#header' => [],
'#rows' => $rows,
'#attributes' => [
'class' => ['image-anchor'],
],
];
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Hooks related to image styles and effects.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the information provided in \Drupal\image\Annotation\ImageEffect.
*
* @param $effects
* The array of image effects, keyed on the machine-readable effect name.
*/
function hook_image_effect_info_alter(&$effects) {
// Override the Image module's 'Scale and Crop' effect label.
$effects['image_scale_and_crop']['label'] = t('Bangers and Mash');
}
/**
* Respond to image style flushing.
*
* This hook enables modules to take effect when a style is being flushed (all
* images are being deleted from the server and regenerated). Any
* module-specific caches that contain information related to the style should
* be cleared using this hook. This hook is called whenever a style is updated,
* deleted, or any effect associated with the style is update or deleted.
*
* @param \Drupal\image\ImageStyleInterface $style
* The image style object that is being flushed.
*/
function hook_image_style_flush($style) {
// Empty cached data that contains information about the style.
\Drupal::cache('mymodule')->deleteAll();
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Implement an image field, based on the file module's file field.
*/
use Drupal\Core\Render\Element;
/**
* Prepares variables for image widget templates.
*
* Default template: image-widget.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: A render element representing the image field widget.
*/
function template_preprocess_image_widget(&$variables) {
$element = $variables['element'];
$variables['attributes'] = ['class' => ['image-widget', 'js-form-managed-file', 'form-managed-file', 'clearfix']];
if (!empty($element['fids']['#value'])) {
$file = reset($element['#files']);
$element['file_' . $file->id()]['filename']['#suffix'] = ' <span class="file-size">(' . format_size($file->getSize()) . ')</span> ';
}
$variables['data'] = [];
foreach (Element::children($element) as $child) {
$variables['data'][$child] = $element[$child];
}
}
/**
* Prepares variables for image formatter templates.
*
* Default template: image-formatter.html.twig.
*
* @param array $variables
* An associative array containing:
* - item: An ImageItem object.
* - item_attributes: An optional associative array of html attributes to be
* placed in the img tag.
* - image_style: An optional image style.
* - url: An optional \Drupal\Core\Url object.
*/
function template_preprocess_image_formatter(&$variables) {
if ($variables['image_style']) {
$variables['image'] = [
'#theme' => 'image_style',
'#style_name' => $variables['image_style'],
];
}
else {
$variables['image'] = [
'#theme' => 'image',
];
}
$variables['image']['#attributes'] = $variables['item_attributes'];
$item = $variables['item'];
// Do not output an empty 'title' attribute.
if (mb_strlen($item->title) != 0) {
$variables['image']['#title'] = $item->title;
}
if (($entity = $item->entity) && empty($item->uri)) {
$variables['image']['#uri'] = $entity->getFileUri();
}
else {
$variables['image']['#uri'] = $item->uri;
}
foreach (['width', 'height', 'alt'] as $key) {
$variables['image']["#$key"] = $item->$key;
}
}

View file

@ -0,0 +1,9 @@
name: Image
type: module
description: 'Defines an image field type and provides image manipulation tools.'
package: Field types
version: VERSION
core: 8.x
dependencies:
- drupal:file
configure: entity.image_style.collection

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Install, update and uninstall functions for the image module.
*/
/**
* Implements hook_install().
*/
function image_install() {
// Create the styles directory and ensure it's writable.
$directory = file_default_scheme() . '://styles';
file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
}
/**
* Implements hook_uninstall().
*/
function image_uninstall() {
// Remove the styles directory and generated images.
file_unmanaged_delete_recursive(file_default_scheme() . '://styles');
}
/**
* Implements hook_requirements() to check the PHP GD Library.
*
* @param $phase
*/
function image_requirements($phase) {
if ($phase != 'runtime') {
return [];
}
$toolkit = \Drupal::service('image.toolkit.manager')->getDefaultToolkit();
if ($toolkit) {
$plugin_definition = $toolkit->getPluginDefinition();
$requirements = [
'image.toolkit' => [
'title' => 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' => t('Image toolkit'),
'value' => t('None'),
'description' => 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 enabled."),
'severity' => REQUIREMENT_ERROR,
],
];
}
return $requirements;
}
/**
* Flush caches as we changed field formatter metadata.
*/
function image_update_8201() {
// Empty update to trigger a cache flush.
// Use hook_post_update_NAME() instead to clear the cache. The use of
// hook_update_N() to clear the cache has been deprecated see
// https://www.drupal.org/node/2960601 for more details.
}

View file

@ -0,0 +1,21 @@
admin:
version: VERSION
css:
theme:
css/image.admin.css: {}
quickedit.inPlaceEditor.image:
version: VERSION
js:
js/editors/image.js: {}
js/theme.js: {}
css:
component:
css/editors/image.css: {}
theme:
css/editors/image.theme.css: {}
dependencies:
- core/jquery
- core/drupal
- core/underscore
- quickedit/quickedit

View file

@ -0,0 +1,5 @@
image_style_add_action:
route_name: image.style_add
title: 'Add image style'
appears_on:
- entity.image_style.collection

View file

@ -0,0 +1,5 @@
entity.image_style.collection:
title: 'Image styles'
description: 'Configure styles that can be used for resizing or adjusting images on display.'
parent: system.admin_config_media
route_name: entity.image_style.collection

View file

@ -0,0 +1,9 @@
entity.image_style.edit_form:
title: 'Edit'
route_name: entity.image_style.edit_form
base_route: entity.image_style.edit_form
entity.image_style.collection:
title: List
route_name: entity.image_style.collection
base_route: entity.image_style.collection

View file

@ -0,0 +1,513 @@
<?php
/**
* @file
* Exposes global functionality for creating image styles.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\file\FileInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\image\Entity\ImageStyle;
/**
* Image style constant for user presets in the database.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.0.
*
* @see https://www.drupal.org/node/1820974
*/
const IMAGE_STORAGE_NORMAL = 1;
/**
* Image style constant for user presets that override module-defined presets.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.0.
*
* @see https://www.drupal.org/node/1820974
*/
const IMAGE_STORAGE_OVERRIDE = 2;
/**
* Image style constant for module-defined presets in code.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.0.
*
* @see https://www.drupal.org/node/1820974
*/
const IMAGE_STORAGE_DEFAULT = 4;
/**
* Image style constant to represent an editable preset.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.0.
*
* @see https://www.drupal.org/node/1820974
*/
define('IMAGE_STORAGE_EDITABLE', IMAGE_STORAGE_NORMAL | IMAGE_STORAGE_OVERRIDE);
/**
* Image style constant to represent any module-based preset.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.0.
*
* @see https://www.drupal.org/node/1820974
*/
define('IMAGE_STORAGE_MODULE', IMAGE_STORAGE_OVERRIDE | IMAGE_STORAGE_DEFAULT);
/**
* The name of the query parameter for image derivative tokens.
*/
define('IMAGE_DERIVATIVE_TOKEN', 'itok');
/**
* Implements hook_help().
*/
function image_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.image':
$field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? \Drupal::url('help.page', ['name' => 'field_ui']) : '#';
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . 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' => \Drupal::url('entity.image_style.collection'), ':field' => \Drupal::url('help.page', ['name' => 'field']), ':field_ui' => $field_ui_url, ':image_documentation' => 'https://www.drupal.org/documentation/modules/image']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dt>' . t('Defining image styles') . '</dt>';
$output .= '<dd>' . 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' => \Drupal::url('entity.image_style.collection')]);
$output .= '<dt>' . t('Naming image styles') . '</dt>';
$output .= '<dd>' . 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>' . t('Configuring image fields') . '</dt>';
$output .= '<dd>' . 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 resolution 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>' . t('For accessibility and search engine optimization, all images that convey meaning on web sites 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>' . 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' => \Drupal::url('system.file_system_settings'), ':system-help' => \Drupal::url('help.page', ['name' => 'system'])]) . '</dd>';
$output .= '<dd>' . 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>' . t('You can also configure a minimum and/or maximum resolution 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>' . 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>' . t('Configuring displays and form displays') . '</dt>';
$output .= '<dd>' . 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>' . 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;
}
}
/**
* Implements hook_theme().
*/
function image_theme() {
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.
// - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
// - http://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.
*/
function image_file_download($uri) {
$path = file_uri_target($uri);
// Private file access for image style derivatives.
if (strpos($path, 'styles/') === 0) {
$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 = file_uri_scheme($uri) . '://' . implode('/', $args);
// Check that the file exists and is an image.
$image = \Drupal::service('image.factory')->get($uri);
if ($image->isValid()) {
// 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(),
// By not explicitly setting them here, this uses normal Drupal
// Expires, Cache-Control and ETag headers to prevent proxy or
// browser caching of private images.
];
}
}
return -1;
}
}
/**
* Implements hook_file_move().
*/
function image_file_move(FileInterface $file, FileInterface $source) {
// Delete any image derivatives at the original image path.
image_path_flush($source->getFileUri());
}
/**
* Implements hook_ENTITY_TYPE_predelete() for file entities.
*/
function image_file_predelete(FileInterface $file) {
// Delete any image derivatives of this image.
image_path_flush($file->getFileUri());
}
/**
* Clears cached versions of a specific file in all styles.
*
* @param $path
* The Drupal file path to the original image.
*/
function image_path_flush($path) {
$styles = ImageStyle::loadMultiple();
foreach ($styles as $style) {
$style->flush($path);
}
}
/**
* Gets an array of image styles suitable for using as select list options.
*
* @param $include_empty
* If TRUE a '- None -' option will be inserted in the options array.
* @return
* Array of image styles both key and value are set to style name.
*/
function image_style_options($include_empty = TRUE) {
$styles = ImageStyle::loadMultiple();
$options = [];
if ($include_empty && !empty($styles)) {
$options[''] = t('- None -');
}
foreach ($styles as $name => $style) {
$options[$name] = $style->label();
}
if (empty($options)) {
$options[''] = t('No defined styles');
}
return $options;
}
/**
* Prepares variables for image style templates.
*
* Default template: image-style.html.twig.
*
* @param array $variables
* An associative array containing:
* - width: The width of the image.
* - height: The height of the image.
* - style_name: The name of the image style to be applied.
* - attributes: Additional attributes to apply to the image.
* - uri: URI of the source image before styling.
* - alt: The alternative text for text-based browsers. 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, this variable defaults
* to an empty string, but can be set to NULL for the attribute 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 this variable.
* - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
* - http://www.w3.org/TR/xhtml1/dtds.html
* - http://dev.w3.org/html5/spec/Overview.html#alt
* - title: The title text is displayed when the image is hovered in some
* popular browsers.
* - attributes: Associative array of attributes to be placed in the img tag.
*/
function template_preprocess_image_style(&$variables) {
$style = ImageStyle::load($variables['style_name']);
// Determine the dimensions of the styled image.
$dimensions = [
'width' => $variables['width'],
'height' => $variables['height'],
];
$style->transformDimensions($dimensions, $variables['uri']);
$variables['image'] = [
'#theme' => 'image',
'#width' => $dimensions['width'],
'#height' => $dimensions['height'],
'#attributes' => $variables['attributes'],
'#style_name' => $variables['style_name'],
];
// If the current image toolkit supports this file type, prepare the URI for
// the derivative image. If not, just use the original image resized to the
// dimensions specified by the style.
if ($style->supportsUri($variables['uri'])) {
$variables['image']['#uri'] = $style->buildUrl($variables['uri']);
}
else {
$variables['image']['#uri'] = $variables['uri'];
// Don't render the image by default, but allow other preprocess functions
// to override that if they need to.
$variables['image']['#access'] = FALSE;
// Inform the site builders why their image didn't work.
\Drupal::logger('image')->warning('Could not apply @style image style to @uri because the style does not support it.', [
'@style' => $style->label(),
'@uri' => $variables['uri'],
]);
}
if (isset($variables['alt']) || array_key_exists('alt', $variables)) {
$variables['image']['#alt'] = $variables['alt'];
}
if (isset($variables['title']) || array_key_exists('title', $variables)) {
$variables['image']['#title'] = $variables['title'];
}
}
/**
* Accepts a keyword (center, top, left, etc) and returns it as a pixel offset.
*
* @param $value
* @param $current_pixels
* @param $new_pixels
*/
function image_filter_keyword($value, $current_pixels, $new_pixels) {
switch ($value) {
case 'top':
case 'left':
return 0;
case 'bottom':
case 'right':
return $current_pixels - $new_pixels;
case 'center':
return $current_pixels / 2 - $new_pixels / 2;
}
return $value;
}
/**
* Implements hook_entity_presave().
*
* Transforms default image of image field from array into single value at save.
*/
function image_entity_presave(EntityInterface $entity) {
// 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 = isset($entity->original) ? $entity->original->getSetting('default_image')['uuid'] : NULL;
if ($uuid != $original_uuid) {
$file = \Drupal::entityManager()->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'.
*/
function image_field_storage_config_update(FieldStorageConfigInterface $field_storage) {
if ($field_storage->getType() != 'image') {
// Only act on image fields.
return;
}
$prior_field_storage = $field_storage->original;
// 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::entityManager()->loadEntityByUuid('file', $uuid_new) : FALSE;
if ($uuid_new != $uuid_old) {
// Is there a new file?
if ($file_new) {
$file_new->status = FILE_STATUS_PERMANENT;
$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::entityManager()->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 && (file_uri_scheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme'))) {
$directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
file_move($file_new, $directory . $file_new->getFilename());
}
}
/**
* Implements hook_ENTITY_TYPE_update() for 'field_config'.
*/
function image_field_config_update(FieldConfigInterface $field) {
$field_storage = $field->getFieldStorageDefinition();
if ($field_storage->getType() != 'image') {
// Only act on image fields.
return;
}
$prior_instance = $field->original;
$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::entityManager()->loadEntityByUuid('file', $uuid_new) : FALSE;
if ($uuid_new != $uuid_old) {
// Save the new file, if present.
if ($file_new) {
$file_new->status = FILE_STATUS_PERMANENT;
$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::entityManager()->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 && (file_uri_scheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme'))) {
$directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
file_move($file_new, $directory . $file_new->getFilename());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
*/
function image_field_storage_config_delete(FieldStorageConfigInterface $field) {
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::entityManager()->loadEntityByUuid('file', $uuid))) {
\Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'field_config'.
*/
function image_field_config_delete(FieldConfigInterface $field) {
$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::entityManager()->loadEntityByUuid('file', $uuid))) {
\Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
}
}

View file

@ -0,0 +1,2 @@
administer image styles:
title: 'Administer image styles'

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Post-update functions for Image.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
/**
* Saves the image style dependencies into form and view display entities.
*/
function image_post_update_image_style_dependencies() {
// Merge view and form displays. Use array_values() to avoid key collisions.
$displays = array_merge(array_values(EntityViewDisplay::loadMultiple()), array_values(EntityFormDisplay::loadMultiple()));
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface[] $displays */
foreach ($displays as $display) {
// Re-save each config entity to add missed dependencies.
$display->save();
}
}
/**
* Add 'anchor' setting to 'Scale and crop' effects.
*/
function image_post_update_scale_and_crop_effect_add_anchor(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'image_style', function ($image_style) {
/** @var \Drupal\image\ImageStyleInterface $image_style */
$effects = $image_style->getEffects();
foreach ($effects as $effect) {
if ($effect->getPluginId() === 'image_scale_and_crop') {
return TRUE;
}
}
return FALSE;
});
}

View file

@ -0,0 +1,99 @@
image.style_add:
path: '/admin/config/media/image-styles/add'
defaults:
_entity_form: image_style.add
_title: 'Add image style'
requirements:
_permission: 'administer image styles'
entity.image_style.edit_form:
path: '/admin/config/media/image-styles/manage/{image_style}'
defaults:
_entity_form: image_style.edit
_title: 'Edit style'
requirements:
_permission: 'administer image styles'
entity.image_style.delete_form:
path: '/admin/config/media/image-styles/manage/{image_style}/delete'
defaults:
_entity_form: 'image_style.delete'
_title: 'Delete'
requirements:
_permission: 'administer image styles'
entity.image_style.flush_form:
path: '/admin/config/media/image-styles/manage/{image_style}/flush'
defaults:
_entity_form: 'image_style.flush'
_title: 'Flush'
requirements:
_permission: 'administer image styles'
image.effect_delete:
path: '/admin/config/media/image-styles/manage/{image_style}/effects/{image_effect}/delete'
defaults:
_form: '\Drupal\image\Form\ImageEffectDeleteForm'
_title: 'Delete image effect'
requirements:
_permission: 'administer image styles'
entity.image_style.collection:
path: '/admin/config/media/image-styles'
defaults:
_entity_list: 'image_style'
_title: 'Image styles'
requirements:
_permission: 'administer image styles'
image.style_private:
path: '/system/files/styles/{image_style}/{scheme}'
defaults:
_controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver'
requirements:
_access: 'TRUE'
image.effect_add_form:
path: '/admin/config/media/image-styles/manage/{image_style}/add/{image_effect}'
defaults:
_form: '\Drupal\image\Form\ImageEffectAddForm'
_title: 'Add image effect'
requirements:
_permission: 'administer image styles'
image.effect_edit_form:
path: '/admin/config/media/image-styles/manage/{image_style}/effects/{image_effect}'
defaults:
_form: '\Drupal\image\Form\ImageEffectEditForm'
_title: 'Edit image effect'
requirements:
_permission: 'administer image styles'
route_callbacks:
- '\Drupal\image\Routing\ImageStyleRoutes::routes'
image.upload:
path: '/quickedit/image/upload/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
_controller: '\Drupal\image\Controller\QuickEditImageController::upload'
options:
parameters:
entity:
type: entity:{entity_type}
requirements:
_permission: 'access in-place editing'
_access_quickedit_entity_field: 'TRUE'
_method: 'POST'
image.info:
path: '/quickedit/image/info/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
_controller: '\Drupal\image\Controller\QuickEditImageController::getInfo'
options:
parameters:
entity:
type: entity:{entity_type}
requirements:
_permission: 'access in-place editing'
_access_quickedit_entity_field: 'TRUE'
_method: 'GET'

View file

@ -0,0 +1,15 @@
services:
path_processor.image_styles:
class: Drupal\image\PathProcessor\PathProcessorImageStyles
arguments: ['@stream_wrapper_manager']
tags:
- { name: path_processor_inbound, priority: 300 }
plugin.manager.image.effect:
class: Drupal\image\ImageEffectManager
parent: default_plugin_manager
image.page_cache_response_policy.deny_private_image_style_download:
class: Drupal\image\PageCache\DenyPrivateImageStyleDownload
arguments: ['@current_route_match']
public: false
tags:
- { name: page_cache_response_policy }

View file

@ -0,0 +1,70 @@
<?php
/**
* @file
* Provide views data for image.module.
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_field_views_data().
*
* Views integration for image fields. Adds an image relationship to the default
* field data.
*
* @see views_field_default_views_data()
*/
function image_field_views_data(FieldStorageConfigInterface $field_storage) {
$data = views_field_default_views_data($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' => 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.
*/
function image_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
$entity_type_id = $field_storage->getTargetEntityTypeId();
$field_name = $field_storage->getName();
$entity_manager = \Drupal::entityManager();
$entity_type = $entity_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_manager->getStorage($entity_type_id)->getTableMapping();
list($label) = views_entity_field_label($entity_type_id, $field_name);
$data['file_managed'][$pseudo_field_name]['relationship'] = [
'title' => t('@entity using @field', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
'label' => t('@field_name', ['@field_name' => $field_name]),
'help' => 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,4 @@
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View file

@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/>
</svg>

After

Width:  |  Height:  |  Size: 202 B

View file

@ -0,0 +1,376 @@
/**
* @file
* Drag+drop based in-place editor for images.
*/
(function($, _, Drupal) {
Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(
/** @lends Drupal.quickedit.editors.image# */ {
/**
* @constructs
*
* @augments Drupal.quickedit.EditorView
*
* @param {object} options
* Options for the image editor.
*/
initialize(options) {
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
// Set our original value to our current HTML (for reverting).
this.model.set('originalValue', this.$el.html().trim());
// $.val() callback function for copying input from our custom form to
// the Quick Edit Field Form.
this.model.set('currentValue', function(index, value) {
const matches = $(this)
.attr('name')
.match(/(alt|title)]$/);
if (matches) {
const name = matches[1];
const $toolgroup = $(
`#${options.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
);
const $input = $toolgroup.find(
`.quickedit-image-field-info input[name="${name}"]`,
);
if ($input.length) {
return $input.val();
}
}
});
},
/**
* @inheritdoc
*
* @param {Drupal.quickedit.FieldModel} fieldModel
* The field model that holds the state.
* @param {string} state
* The state to change to.
* @param {object} options
* State options, if needed by the state change.
*/
stateChange(fieldModel, state, options) {
const from = fieldModel.previous('state');
switch (state) {
case 'inactive':
break;
case 'candidate':
if (from !== 'inactive') {
this.$el.find('.quickedit-image-dropzone').remove();
this.$el.removeClass('quickedit-image-element');
}
if (from === 'invalid') {
this.removeValidationErrors();
}
break;
case 'highlighted':
break;
case 'activating':
// Defer updating the field model until the current state change has
// propagated, to not trigger a nested state change event.
_.defer(() => {
fieldModel.set('state', 'active');
});
break;
case 'active': {
const self = this;
// Indicate that this element is being edited by Quick Edit Image.
this.$el.addClass('quickedit-image-element');
// Render our initial dropzone element. Once the user reverts changes
// or saves a new image, this element is removed.
const $dropzone = this.renderDropzone(
'upload',
Drupal.t('Drop file here or click to upload'),
);
$dropzone.on('dragenter', function(e) {
$(this).addClass('hover');
});
$dropzone.on('dragleave', function(e) {
$(this).removeClass('hover');
});
$dropzone.on('drop', function(e) {
// Only respond when a file is dropped (could be another element).
if (
e.originalEvent.dataTransfer &&
e.originalEvent.dataTransfer.files.length
) {
$(this).removeClass('hover');
self.uploadImage(e.originalEvent.dataTransfer.files[0]);
}
});
$dropzone.on('click', e => {
// Create an <input> element without appending it to the DOM, and
// trigger a click event. This is the easiest way to arbitrarily
// open the browser's upload dialog.
$('<input type="file">')
.trigger('click')
.on('change', function() {
if (this.files.length) {
self.uploadImage(this.files[0]);
}
});
});
// Prevent the browser's default behavior when dragging files onto
// the document (usually opens them in the same tab).
$dropzone.on('dragover dragenter dragleave drop click', e => {
e.preventDefault();
e.stopPropagation();
});
this.renderToolbar(fieldModel);
break;
}
case 'changed':
break;
case 'saving':
if (from === 'invalid') {
this.removeValidationErrors();
}
this.save(options);
break;
case 'saved':
break;
case 'invalid':
this.showValidationErrors();
break;
}
},
/**
* Validates/uploads a given file.
*
* @param {File} file
* The file to upload.
*/
uploadImage(file) {
// Indicate loading by adding a special class to our icon.
this.renderDropzone(
'upload loading',
Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }),
);
// Build a valid URL for our endpoint.
const fieldID = this.fieldModel.get('fieldID');
const url = Drupal.quickedit.util.buildUrl(
fieldID,
Drupal.url(
'quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode',
),
);
// Construct form data that our endpoint can consume.
const data = new FormData();
data.append('files[image]', file);
// Construct a POST request to our endpoint.
const self = this;
this.ajax({
type: 'POST',
url,
data,
success(response) {
const $el = $(self.fieldModel.get('el'));
// Indicate that the field has changed - this enables the
// "Save" button.
self.fieldModel.set('state', 'changed');
self.fieldModel.get('entity').set('inTempStore', true);
self.removeValidationErrors();
// Replace our html with the new image. If we replaced our entire
// element with data.html, we would have to implement complicated logic
// like what's in Drupal.quickedit.AppView.renderUpdatedField.
const $content = $(response.html)
.closest('[data-quickedit-field-id]')
.children();
$el.empty().append($content);
},
});
},
/**
* Utility function to make an AJAX request to the server.
*
* In addition to formatting the correct request, this also handles error
* codes and messages by displaying them visually inline with the image.
*
* Drupal.ajax is not called here as the Form API is unused by this
* in-place editor, and our JSON requests/responses try to be
* editor-agnostic. Ideally similar logic and routes could be used by
* modules like CKEditor for drag+drop file uploads as well.
*
* @param {object} options
* Ajax options.
* @param {string} options.type
* The type of request (i.e. GET, POST, PUT, DELETE, etc.)
* @param {string} options.url
* The URL for the request.
* @param {*} options.data
* The data to send to the server.
* @param {function} options.success
* A callback function used when a request is successful, without errors.
*/
ajax(options) {
const defaultOptions = {
context: this,
dataType: 'json',
cache: false,
contentType: false,
processData: false,
error() {
this.renderDropzone(
'error',
Drupal.t('A server error has occurred.'),
);
},
};
const ajaxOptions = $.extend(defaultOptions, options);
const successCallback = ajaxOptions.success;
// Handle the success callback.
ajaxOptions.success = function(response) {
if (response.main_error) {
this.renderDropzone('error', response.main_error);
if (response.errors.length) {
this.model.set('validationErrors', response.errors);
}
this.showValidationErrors();
} else {
successCallback(response);
}
};
$.ajax(ajaxOptions);
},
/**
* Renders our toolbar form for editing metadata.
*
* @param {Drupal.quickedit.FieldModel} fieldModel
* The current Field Model.
*/
renderToolbar(fieldModel) {
const $toolgroup = $(
`#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
);
let $toolbar = $toolgroup.find('.quickedit-image-field-info');
if ($toolbar.length === 0) {
// Perform an AJAX request for extra image info (alt/title).
const fieldID = fieldModel.get('fieldID');
const url = Drupal.quickedit.util.buildUrl(
fieldID,
Drupal.url(
'quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode',
),
);
const self = this;
self.ajax({
type: 'GET',
url,
success(response) {
$toolbar = $(Drupal.theme.quickeditImageToolbar(response));
$toolgroup.append($toolbar);
$toolbar.on('keyup paste', () => {
fieldModel.set('state', 'changed');
});
// Re-position the toolbar, which could have changed size.
fieldModel.get('entity').toolbarView.position();
},
});
}
},
/**
* Renders our dropzone element.
*
* @param {string} state
* The current state of our editor. Only used for visual styling.
* @param {string} text
* The text to display in the dropzone area.
*
* @return {jQuery}
* The rendered dropzone.
*/
renderDropzone(state, text) {
let $dropzone = this.$el.find('.quickedit-image-dropzone');
// If the element already exists, modify its contents.
if ($dropzone.length) {
$dropzone
.removeClass('upload error hover loading')
.addClass(`.quickedit-image-dropzone ${state}`)
.children('.quickedit-image-text')
.html(text);
} else {
$dropzone = $(
Drupal.theme('quickeditImageDropzone', {
state,
text,
}),
);
this.$el.append($dropzone);
}
return $dropzone;
},
/**
* @inheritdoc
*/
revert() {
this.$el.html(this.model.get('originalValue'));
},
/**
* @inheritdoc
*/
getQuickEditUISettings() {
return {
padding: false,
unifiedToolbar: true,
fullWidthToolbar: true,
popup: false,
};
},
/**
* @inheritdoc
*/
showValidationErrors() {
const errors = Drupal.theme('quickeditImageErrors', {
errors: this.model.get('validationErrors'),
});
$(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`).append(
errors,
);
this.getEditedElement().addClass('quickedit-validation-error');
// Re-position the toolbar, which could have changed size.
this.fieldModel.get('entity').toolbarView.position();
},
/**
* @inheritdoc
*/
removeValidationErrors() {
$(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
.find('.quickedit-image-errors')
.remove();
this.getEditedElement().removeClass('quickedit-validation-error');
},
},
);
})(jQuery, _, Drupal);

View file

@ -0,0 +1,227 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, _, Drupal) {
Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend({
initialize: function initialize(options) {
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
this.model.set('originalValue', this.$el.html().trim());
this.model.set('currentValue', function (index, value) {
var matches = $(this).attr('name').match(/(alt|title)]$/);
if (matches) {
var name = matches[1];
var $toolgroup = $('#' + options.fieldModel.toolbarView.getMainWysiwygToolgroupId());
var $input = $toolgroup.find('.quickedit-image-field-info input[name="' + name + '"]');
if ($input.length) {
return $input.val();
}
}
});
},
stateChange: function stateChange(fieldModel, state, options) {
var from = fieldModel.previous('state');
switch (state) {
case 'inactive':
break;
case 'candidate':
if (from !== 'inactive') {
this.$el.find('.quickedit-image-dropzone').remove();
this.$el.removeClass('quickedit-image-element');
}
if (from === 'invalid') {
this.removeValidationErrors();
}
break;
case 'highlighted':
break;
case 'activating':
_.defer(function () {
fieldModel.set('state', 'active');
});
break;
case 'active':
{
var self = this;
this.$el.addClass('quickedit-image-element');
var $dropzone = this.renderDropzone('upload', Drupal.t('Drop file here or click to upload'));
$dropzone.on('dragenter', function (e) {
$(this).addClass('hover');
});
$dropzone.on('dragleave', function (e) {
$(this).removeClass('hover');
});
$dropzone.on('drop', function (e) {
if (e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length) {
$(this).removeClass('hover');
self.uploadImage(e.originalEvent.dataTransfer.files[0]);
}
});
$dropzone.on('click', function (e) {
$('<input type="file">').trigger('click').on('change', function () {
if (this.files.length) {
self.uploadImage(this.files[0]);
}
});
});
$dropzone.on('dragover dragenter dragleave drop click', function (e) {
e.preventDefault();
e.stopPropagation();
});
this.renderToolbar(fieldModel);
break;
}
case 'changed':
break;
case 'saving':
if (from === 'invalid') {
this.removeValidationErrors();
}
this.save(options);
break;
case 'saved':
break;
case 'invalid':
this.showValidationErrors();
break;
}
},
uploadImage: function uploadImage(file) {
this.renderDropzone('upload loading', Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }));
var fieldID = this.fieldModel.get('fieldID');
var url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode'));
var data = new FormData();
data.append('files[image]', file);
var self = this;
this.ajax({
type: 'POST',
url: url,
data: data,
success: function success(response) {
var $el = $(self.fieldModel.get('el'));
self.fieldModel.set('state', 'changed');
self.fieldModel.get('entity').set('inTempStore', true);
self.removeValidationErrors();
var $content = $(response.html).closest('[data-quickedit-field-id]').children();
$el.empty().append($content);
}
});
},
ajax: function ajax(options) {
var defaultOptions = {
context: this,
dataType: 'json',
cache: false,
contentType: false,
processData: false,
error: function error() {
this.renderDropzone('error', Drupal.t('A server error has occurred.'));
}
};
var ajaxOptions = $.extend(defaultOptions, options);
var successCallback = ajaxOptions.success;
ajaxOptions.success = function (response) {
if (response.main_error) {
this.renderDropzone('error', response.main_error);
if (response.errors.length) {
this.model.set('validationErrors', response.errors);
}
this.showValidationErrors();
} else {
successCallback(response);
}
};
$.ajax(ajaxOptions);
},
renderToolbar: function renderToolbar(fieldModel) {
var $toolgroup = $('#' + fieldModel.toolbarView.getMainWysiwygToolgroupId());
var $toolbar = $toolgroup.find('.quickedit-image-field-info');
if ($toolbar.length === 0) {
var fieldID = fieldModel.get('fieldID');
var url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode'));
var self = this;
self.ajax({
type: 'GET',
url: url,
success: function success(response) {
$toolbar = $(Drupal.theme.quickeditImageToolbar(response));
$toolgroup.append($toolbar);
$toolbar.on('keyup paste', function () {
fieldModel.set('state', 'changed');
});
fieldModel.get('entity').toolbarView.position();
}
});
}
},
renderDropzone: function renderDropzone(state, text) {
var $dropzone = this.$el.find('.quickedit-image-dropzone');
if ($dropzone.length) {
$dropzone.removeClass('upload error hover loading').addClass('.quickedit-image-dropzone ' + state).children('.quickedit-image-text').html(text);
} else {
$dropzone = $(Drupal.theme('quickeditImageDropzone', {
state: state,
text: text
}));
this.$el.append($dropzone);
}
return $dropzone;
},
revert: function revert() {
this.$el.html(this.model.get('originalValue'));
},
getQuickEditUISettings: function getQuickEditUISettings() {
return {
padding: false,
unifiedToolbar: true,
fullWidthToolbar: true,
popup: false
};
},
showValidationErrors: function showValidationErrors() {
var errors = Drupal.theme('quickeditImageErrors', {
errors: this.model.get('validationErrors')
});
$('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId()).append(errors);
this.getEditedElement().addClass('quickedit-validation-error');
this.fieldModel.get('entity').toolbarView.position();
},
removeValidationErrors: function removeValidationErrors() {
$('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId()).find('.quickedit-image-errors').remove();
this.getEditedElement().removeClass('quickedit-validation-error');
}
});
})(jQuery, _, Drupal);

View file

@ -0,0 +1,92 @@
/**
* @file
* Provides theme functions for image Quick Edit's client-side HTML.
*/
(function(Drupal) {
/**
* Theme function for validation errors of the Image in-place editor.
*
* @param {object} settings
* Settings object used to construct the markup.
* @param {string} settings.errors
* Already escaped HTML representing error messages.
*
* @return {string}
* The corresponding HTML.
*/
Drupal.theme.quickeditImageErrors = function(settings) {
return `<div class="quickedit-image-errors">${settings.errors}</div>`;
};
/**
* Theme function for the dropzone element of the Image module's in-place
* editor.
*
* @param {object} settings
* Settings object used to construct the markup.
* @param {string} settings.state
* State of the upload.
* @param {string} settings.text
* Text to display inline with the dropzone element.
*
* @return {string}
* The corresponding HTML.
*/
Drupal.theme.quickeditImageDropzone = function(settings) {
return (
`<div class="quickedit-image-dropzone ${settings.state}">` +
' <i class="quickedit-image-icon"></i>' +
` <span class="quickedit-image-text">${settings.text}</span>` +
'</div>'
);
};
/**
* Theme function for the toolbar of the Image module's in-place editor.
*
* @param {object} settings
* Settings object used to construct the markup.
* @param {bool} settings.alt_field
* Whether or not the "Alt" field is enabled for this field.
* @param {bool} settings.alt_field_required
* Whether or not the "Alt" field is required for this field.
* @param {string} settings.alt
* The current value for the "Alt" field.
* @param {bool} settings.title_field
* Whether or not the "Title" field is enabled for this field.
* @param {bool} settings.title_field_required
* Whether or not the "Title" field is required for this field.
* @param {string} settings.title
* The current value for the "Title" field.
*
* @return {string}
* The corresponding HTML.
*/
Drupal.theme.quickeditImageToolbar = function(settings) {
let html = '<form class="quickedit-image-field-info">';
if (settings.alt_field) {
html +=
`<div><label for="alt" class="${
settings.alt_field_required ? 'required' : ''
}">${Drupal.t('Alternative text')}</label>` +
`<input type="text" placeholder="${settings.alt}" value="${
settings.alt
}" name="alt" ${settings.alt_field_required ? 'required' : ''}/>` +
' </div>';
}
if (settings.title_field) {
html +=
`<div><label for="title" class="${
settings.title_field_required ? 'form-required' : ''
}">${Drupal.t('Title')}</label>` +
`<input type="text" placeholder="${settings.title}" value="${
settings.title
}" name="title" ${settings.title_field_required ? 'required' : ''}/>` +
'</div>';
}
html += '</form>';
return html;
};
})(Drupal);

View file

@ -0,0 +1,29 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function (Drupal) {
Drupal.theme.quickeditImageErrors = function (settings) {
return '<div class="quickedit-image-errors">' + settings.errors + '</div>';
};
Drupal.theme.quickeditImageDropzone = function (settings) {
return '<div class="quickedit-image-dropzone ' + settings.state + '">' + ' <i class="quickedit-image-icon"></i>' + (' <span class="quickedit-image-text">' + settings.text + '</span>') + '</div>';
};
Drupal.theme.quickeditImageToolbar = function (settings) {
var html = '<form class="quickedit-image-field-info">';
if (settings.alt_field) {
html += '<div><label for="alt" class="' + (settings.alt_field_required ? 'required' : '') + '">' + Drupal.t('Alternative text') + '</label>' + ('<input type="text" placeholder="' + settings.alt + '" value="' + settings.alt + '" name="alt" ' + (settings.alt_field_required ? 'required' : '') + '/>') + ' </div>';
}
if (settings.title_field) {
html += '<div><label for="title" class="' + (settings.title_field_required ? 'form-required' : '') + '">' + Drupal.t('Title') + '</label>' + ('<input type="text" placeholder="' + settings.title + '" value="' + settings.title + '" name="title" ' + (settings.title_field_required ? 'required' : '') + '/>') + '</div>';
}
html += '</form>';
return html;
};
})(Drupal);

View file

@ -0,0 +1,25 @@
id: d6_imagecache_presets
label: ImageCache Presets
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_imagecache_presets
process:
name:
-
plugin: machine_name
source: presetname
-
plugin: make_unique_entity_field
entity_type: image_style
field: name
length: 32
label: presetname
effects:
plugin: d6_imagecache_actions
source:
- '@plugin'
- data
destination:
plugin: entity:image_style

View file

@ -0,0 +1,19 @@
id: d7_image_settings
label: Image configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- allow_insecure_derivatives
- suppress_itok_output
- image_style_preview_image
source_module: image
process:
suppress_itok_output: suppress_itok_output
allow_insecure_derivatives: allow_insecure_derivatives
preview_image: image_style_preview_image
destination:
plugin: config
config_name: image.settings

View file

@ -0,0 +1,19 @@
id: d7_image_styles
label: Image styles
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_image_styles
process:
name: name
label: label
effects:
plugin: sub_process
source: effects
process:
id: name
weight: weight
data: data
destination:
plugin: entity:image_style

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View file

@ -0,0 +1,55 @@
<?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.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* A brief description of the image effect.
*
* This will be shown when adding or configuring this image effect.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation (optional)
*/
public $description = '';
}

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,186 @@
<?php
namespace Drupal\image\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Lock\LockBackendInterface;
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;
/**
* Constructs a ImageStyleDownloadController object.
*
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
*/
public function __construct(LockBackendInterface $lock, ImageFactory $image_factory) {
$this->lock = $lock;
$this->imageFactory = $image_factory;
$this->logger = $this->getLogger('image');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('lock'),
$container->get('image.factory')
);
}
/**
* 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.
*
* @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) {
$target = $request->query->get('file');
$image_uri = $scheme . '://' . $target;
// Check that the style is defined, the scheme is valid, and the image
// derivative token is valid. Sites which require image derivatives to be
// generated without a token can set the
// 'image.settings:allow_insecure_derivatives' configuration to TRUE to
// bypass the latter 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/.
$valid = !empty($image_style) && file_stream_wrapper_valid_scheme($scheme);
if (!$this->config('image.settings')->get('allow_insecure_derivatives') || strpos(ltrim($target, '\/'), 'styles/') === 0) {
$valid &= $request->query->get(IMAGE_DERIVATIVE_TOKEN) === $image_style->getPathToken($image_uri);
}
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);
$headers = [];
// If using the private scheme, let other modules provide headers and
// control access to the file.
if ($scheme == 'private') {
$headers = $this->moduleHandler()->invokeAll('file_download', [$image_uri]);
if (in_array(-1, $headers) || empty($headers)) {
throw new AccessDeniedHttpException();
}
}
// Don't try to generate file if source is missing.
if (!file_exists($image_uri)) {
// 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.
$path_info = pathinfo($image_uri);
$converted_image_uri = $path_info['dirname'] . DIRECTORY_SEPARATOR . $path_info['filename'];
if (!file_exists($converted_image_uri)) {
$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);
}
else {
// The converted file does exist, use it as the source.
$image_uri = $converted_image_uri;
}
}
// 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, $this->t('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. We pass in FALSE for non-private schemes for the
// $public parameter to make sure we don't change the headers.
return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');
}
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);
}
}
}

View file

@ -0,0 +1,225 @@
<?php
namespace Drupal\image\Controller;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Render\Element\StatusMessages;
use Drupal\Core\Render\RendererInterface;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Returns responses for our image routes.
*/
class QuickEditImageController extends ControllerBase {
/**
* Stores The Quick Edit tempstore.
*
* @var \Drupal\Core\TempStore\PrivateTempStore
*/
protected $tempStore;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The image factory.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* Constructs a new QuickEditImageController.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
*/
public function __construct(RendererInterface $renderer, ImageFactory $image_factory, PrivateTempStoreFactory $temp_store_factory) {
$this->renderer = $renderer;
$this->imageFactory = $image_factory;
$this->tempStore = $temp_store_factory->get('quickedit');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('renderer'),
$container->get('image.factory'),
$container->get('tempstore.private')
);
}
/**
* Returns JSON representing the new file upload, or validation errors.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity of which an image field is being rendered.
* @param string $field_name
* The name of the (image) field that is being rendered
* @param string $langcode
* The language code of the field that is being rendered.
* @param string $view_mode_id
* The view mode of the field that is being rendered.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*/
public function upload(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
$field = $this->getField($entity, $field_name, $langcode);
$field_validators = $field->getUploadValidators();
$field_settings = $field->getFieldDefinition()->getSettings();
$destination = $field->getUploadLocation();
// Add upload resolution validation.
if ($field_settings['max_resolution'] || $field_settings['min_resolution']) {
$field_validators['file_validate_image_resolution'] = [$field_settings['max_resolution'], $field_settings['min_resolution']];
}
// Create the destination directory if it does not already exist.
if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
return new JsonResponse(['main_error' => $this->t('The destination directory could not be created.'), 'errors' => '']);
}
// Attempt to save the image given the field's constraints.
$result = file_save_upload('image', $field_validators, $destination);
if (is_array($result) && $result[0]) {
/** @var \Drupal\file\Entity\File $file */
$file = $result[0];
$image = $this->imageFactory->get($file->getFileUri());
// Set the value in the Entity to the new file.
/** @var \Drupal\file\Plugin\Field\FieldType\FileFieldItemList $field_list */
$value = $entity->$field_name->getValue();
$value[0]['target_id'] = $file->id();
$value[0]['width'] = $image->getWidth();
$value[0]['height'] = $image->getHeight();
$entity->$field_name->setValue($value);
// Render the new image using the correct formatter settings.
$entity_view_mode_ids = array_keys($this->entityManager()->getViewModes($entity->getEntityTypeId()));
if (in_array($view_mode_id, $entity_view_mode_ids, TRUE)) {
$output = $entity->$field_name->view($view_mode_id);
}
else {
// Each part of a custom (non-Entity Display) view mode ID is separated
// by a dash; the first part must be the module name.
$mode_id_parts = explode('-', $view_mode_id, 2);
$module = reset($mode_id_parts);
$args = [$entity, $field_name, $view_mode_id, $langcode];
$output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
}
// Save the Entity to tempstore.
$this->tempStore->set($entity->uuid(), $entity);
$data = [
'fid' => $file->id(),
'html' => $this->renderer->renderRoot($output),
];
return new JsonResponse($data);
}
else {
// Return a JSON object containing the errors from Drupal and our
// "main_error", which is displayed inside the dropzone area.
$messages = StatusMessages::renderMessages('error');
return new JsonResponse(['errors' => $this->renderer->render($messages), 'main_error' => $this->t('The image failed validation.')]);
}
}
/**
* Returns JSON representing an image field's metadata.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity of which an image field is being rendered.
* @param string $field_name
* The name of the (image) field that is being rendered
* @param string $langcode
* The language code of the field that is being rendered.
* @param string $view_mode_id
* The view mode of the field that is being rendered.
*
* @return \Drupal\Core\Cache\CacheableJsonResponse
* The JSON response.
*/
public function getInfo(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
$field = $this->getField($entity, $field_name, $langcode);
$settings = $field->getFieldDefinition()->getSettings();
$info = [
'alt' => $field->alt,
'title' => $field->title,
'alt_field' => $settings['alt_field'],
'title_field' => $settings['title_field'],
'alt_field_required' => $settings['alt_field_required'],
'title_field_required' => $settings['title_field_required'],
];
$response = new CacheableJsonResponse($info);
$response->addCacheableDependency($entity);
return $response;
}
/**
* Returns JSON representing the current state of the field.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity of which an image field is being rendered.
* @param string $field_name
* The name of the (image) field that is being rendered
* @param string $langcode
* The language code of the field that is being rendered.
*
* @return \Drupal\image\Plugin\Field\FieldType\ImageItem
* The field for this request.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Throws an exception if the request is invalid.
*/
protected function getField(EntityInterface $entity, $field_name, $langcode) {
// Ensure that this is a valid Entity.
if (!($entity instanceof ContentEntityInterface)) {
throw new BadRequestHttpException('Requested Entity is not a Content Entity.');
}
// Check that this field exists.
/** @var \Drupal\Core\Field\FieldItemListInterface $field_list */
$field_list = $entity->getTranslation($langcode)->get($field_name);
if (!$field_list) {
throw new BadRequestHttpException('Requested Field does not exist.');
}
// If the list is empty, append an empty item to use.
if ($field_list->isEmpty()) {
$field = $field_list->appendItem();
}
// Otherwise, use the first item.
else {
$field = $entity->getTranslation($langcode)->get($field_name)->first();
}
// Ensure that the field is the type we expect.
if (!($field instanceof ImageItem)) {
throw new BadRequestHttpException('Requested Field is not of type "image".');
}
return $field;
}
}

View file

@ -0,0 +1,552 @@
<?php
namespace Drupal\image\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Routing\RequestHelper;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
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 Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
/**
* Defines an image style configuration entity.
*
* @ConfigEntityType(
* id = "image_style",
* label = @Translation("Image style"),
* label_collection = @Translation("Image styles"),
* label_singular = @Translation("image style"),
* label_plural = @Translation("image styles"),
* label_count = @PluralTranslation(
* singular = "@count image style",
* plural = "@count image styles",
* ),
* handlers = {
* "form" = {
* "add" = "Drupal\image\Form\ImageStyleAddForm",
* "edit" = "Drupal\image\Form\ImageStyleEditForm",
* "delete" = "Drupal\image\Form\ImageStyleDeleteForm",
* "flush" = "Drupal\image\Form\ImageStyleFlushForm"
* },
* "list_builder" = "Drupal\image\ImageStyleListBuilder",
* "storage" = "Drupal\image\ImageStyleStorage",
* },
* admin_permission = "administer image styles",
* config_prefix = "style",
* entity_keys = {
* "id" = "name",
* "label" = "label"
* },
* 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",
* },
* 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 (!empty($this->original) && $this->id() !== $this->original->id()) {
// The old image style name needs flushing after a rename.
$this->original->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 (EntityViewDisplay::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 = $this->fileUriScheme($uri);
$default_scheme = $this->fileDefaultScheme();
if ($source_scheme) {
$path = $this->fileUriTarget($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 = $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) {
$uri = $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.
$original_uri = file_uri_scheme($path) ? file_stream_wrapper_uri_normalize($path) : file_build_uri($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 $e) {
}
}
// 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 && file_uri_scheme($uri) == 'public' && !file_exists($uri)) {
$directory_path = $this->getStreamWrapperManager()->getViaUri($uri)->getDirectoryPath();
return Url::fromUri('base:' . $directory_path . '/' . file_uri_target($uri), ['absolute' => TRUE, 'query' => $token_query])->toString();
}
$file_url = file_create_url($uri);
// Append the query string with the token, if necessary.
if ($token_query) {
$file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query);
}
return $file_url;
}
/**
* {@inheritdoc}
*/
public function flush($path = NULL) {
// A specific image path has been provided. Flush only that derivative.
if (isset($path)) {
$derivative_uri = $this->buildUri($path);
if (file_exists($derivative_uri)) {
file_unmanaged_delete($derivative_uri);
}
return $this;
}
// 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())) {
file_unmanaged_delete_recursive($directory);
}
}
// Let other modules update as necessary on flush.
$module_handler = \Drupal::moduleHandler();
$module_handler->invokeAll('image_style_flush', [$this]);
// Clear caches so that formatters may be added for this style.
drupal_theme_rebuild();
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_dirname($derivative_uri);
// Build the destination folder tree if it doesn't already exist.
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_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}
*/
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 for file_uri_scheme() to allow unit testing.
*
* Returns the scheme of a URI (e.g. a stream).
*
* @param string $uri
* A stream, referenced as "scheme://target" or "data:target".
*
* @see file_uri_target()
*
* @todo: Remove when https://www.drupal.org/node/2050759 is in.
*
* @return string
* A string containing the name of the scheme, or FALSE if none. For
* example, the URI "public://example.txt" would return "public".
*/
protected function fileUriScheme($uri) {
return file_uri_scheme($uri);
}
/**
* Provides a wrapper for file_uri_target() to allow unit testing.
*
* Returns the part of a URI after the schema.
*
* @param string $uri
* A stream, referenced as "scheme://target" or "data:target".
*
* @see file_uri_scheme()
*
* @todo: Convert file_uri_target() into a proper injectable service.
*
* @return string|bool
* A string containing the target (path), or FALSE if none.
* For example, the URI "public://sample/test.txt" would return
* "sample/test.txt".
*/
protected function fileUriTarget($uri) {
return file_uri_target($uri);
}
/**
* Provides a wrapper for file_default_scheme() to allow unit testing.
*
* Gets the default file stream implementation.
*
* @todo: Convert file_default_scheme() into a proper injectable service.
*
* @return string
* 'public', 'private' or any other file scheme defined as the default.
*/
protected function fileDefaultScheme() {
return file_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,65 @@
<?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', ['%label' => $this->imageEffect->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->urlInfo('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->urlInfo('edit-form'));
}
}

View file

@ -0,0 +1,34 @@
<?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', ['%label' => $this->imageEffect->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,141 @@
<?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 \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 $e) {
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->urlInfo('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 (!$this->imageEffect->getUuid()) {
$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->urlInfo('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,275 @@
<?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.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'),
'#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' => 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' => isset($user_input['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')]]
);
}
// 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->urlInfo('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.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->urlInfo('edit-form'));
}
}

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,105 @@
<?php
namespace Drupal\image;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
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, ConfigurablePluginInterface {
/**
* 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,40 @@
<?php
namespace Drupal\image;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* 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, 'Drupal\image\ImageEffectInterface', 'Drupal\image\Annotation\ImageEffect');
$this->alterInfo('image_effect_info');
$this->setCacheBackend($cache_backend, 'image_effect_plugins');
}
}

View file

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

View file

@ -0,0 +1,207 @@
<?php
namespace Drupal\image;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining an image style entity.
*/
interface ImageStyleInterface extends ConfigEntityInterface {
/**
* Returns the replacement ID.
*
* @return string|null
* The replacement image style ID or NULL if no replacement has been
* selected.
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.x. Use
* \Drupal\image\ImageStyleStorageInterface::getReplacementId() instead.
*
* @see \Drupal\image\ImageStyleStorageInterface::getReplacementId()
*/
public function getReplacementID();
/**
* 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 \Drupal\image\ImageStyleInterface
* 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 file_url_transform_relative()
*/
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,66 @@
<?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' => t('Flush'),
'weight' => 200,
'url' => $entity->urlInfo('flush-form'),
];
$operations = parent::getDefaultOperations($entity) + [
'flush' => $flush,
];
// Remove destination URL from the edit link to allow editing image
// effects.
if (isset($operations['edit'])) {
$operations['edit']['url'] = $entity->toUrl('edit-form');
}
return $operations;
}
/**
* {@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 isset($this->replacement[$name]) ? $this->replacement[$name] : NULL;
}
/**
* {@inheritdoc}
*/
public function clearReplacementId($name) {
unset($this->replacement[$name]);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\image;
/**
* Interface for storage controller for "image style" configuration entities.
*/
interface ImageStyleStorageInterface {
/**
* 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 a 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,44 @@
<?php
namespace Drupal\image\PageCache;
use Drupal\Core\PageCache\ResponsePolicyInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Cache policy for image preview page.
*
* This policy rule denies caching of responses generated by the
* entity.image.preview route.
*/
class DenyPrivateImageStyleDownload implements ResponsePolicyInterface {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a deny image preview page cache policy.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function check(Response $response, Request $request) {
if ($this->routeMatch->getRouteName() === 'image.style_private') {
return static::DENY;
}
}
}

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 (strpos($path, '/' . $directory_path . '/styles/') === 0) {
$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 (strpos($path, '/system/files/styles/') !== FALSE) {
$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) {
list($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,275 @@
<?php
namespace Drupal\image\Plugin\Field\FieldFormatter;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Link;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
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 = @Translation("Image"),
* field_types = {
* "image"
* },
* quickedit = {
* "editor" = "image"
* }
* )
*/
class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPluginInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The image style entity storage.
*
* @var \Drupal\image\ImageStyleStorageInterface
*/
protected $imageStyleStorage;
/**
* 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 settings.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
* The image style storage.
*/
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) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->currentUser = $current_user;
$this->imageStyleStorage = $image_style_storage;
}
/**
* {@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.manager')->getStorage('image_style')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'image_style' => '',
'image_link' => '',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $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' => t('Image style'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_style'),
'#empty_option' => t('None (original image)'),
'#options' => $image_styles,
'#description' => $description_link->toRenderable() + [
'#access' => $this->currentUser->hasPermission('administer image styles'),
],
];
$link_types = [
'content' => t('Content'),
'file' => t('File'),
];
$element['image_link'] = [
'#title' => t('Link image to'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_link'),
'#empty_option' => t('Nothing'),
'#options' => $link_types,
];
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[] = t('Image style: @style', ['@style' => $image_styles[$image_style_setting]]);
}
else {
$summary[] = t('Original image');
}
$link_types = [
'content' => t('Linked to content'),
'file' => 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];
}
return $summary;
}
/**
* {@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->urlInfo();
}
}
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) {
$cache_contexts = [];
if (isset($link_file)) {
$image_uri = $file->getFileUri();
// @todo Wrap in file_url_transform_relative(). This is currently
// impossible. As a work-around, we currently add the 'url.site' cache
// context to ensure different file URLs are generated for different
// sites in a multisite setup, including HTTP and HTTPS versions of the
// same site. Fix in https://www.drupal.org/node/2646744.
$url = Url::fromUri(file_create_url($image_uri));
$cache_contexts[] = 'url.site';
}
$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);
$elements[$delta] = [
'#theme' => 'image_formatter',
'#item' => $item,
'#item_attributes' => $item_attributes,
'#image_style' => $image_style_setting,
'#url' => $url,
'#cache' => [
'tags' => $cache_tags,
'contexts' => $cache_contexts,
],
];
}
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,48 @@
<?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::entityManager()->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,
]);
$file->_referringItem = $items[0];
}
}
return parent::getEntitiesToView($items, $langcode);
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Drupal\image\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'image_url' formatter.
*
* @FieldFormatter(
* id = "image_url",
* label = @Translation("URL to image"),
* field_types = {
* "image"
* }
* )
*/
class ImageUrlFormatter extends ImageFormatter {
/**
* {@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']);;
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
return [$summary[0]];
}
/**
* {@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\file\FileInterface[] $images */
foreach ($images as $delta => $image) {
$image_uri = $image->getFileUri();
$url = $image_style ? $image_style->buildUrl($image_uri) : file_create_url($image_uri);
$url = file_url_transform_relative($url);
// 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,505 @@
<?php
namespace Drupal\image\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\file\Entity\File;
use Drupal\file\Plugin\Field\FieldType\FileItem;
/**
* Plugin implementation of the 'image' field type.
*
* @FieldType(
* id = "image",
* label = @Translation("Image"),
* description = @Translation("This field stores the ID of an image file as an integer value."),
* category = @Translation("Reference"),
* default_widget = "image_image",
* default_formatter = "image",
* column_groups = {
* "file" = {
* "label" = @Translation("File"),
* "columns" = {
* "target_id", "width", "height"
* },
* "require_all_groups_for_translation" = TRUE
* },
* "alt" = {
* "label" = @Translation("Alt"),
* "translatable" = TRUE
* },
* "title" = {
* "label" = @Translation("Title"),
* "translatable" = TRUE
* },
* },
* list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
* constraints = {"ReferenceAccess" = {}, "FileValidation" = {}}
* )
*/
class ImageItem extends FileItem {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'default_image' => [
'uuid' => NULL,
'alt' => '',
'title' => '',
'width' => NULL,
'height' => NULL,
],
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
$settings = [
'file_extensions' => 'png gif jpg jpeg',
'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(t('Alternative text'))
->setDescription(t("Alternative image text, for the image's 'alt' attribute."));
$properties['title'] = DataDefinition::create('string')
->setLabel(t('Title'))
->setDescription(t("Image title text, for the image's 'title' attribute."));
$properties['width'] = DataDefinition::create('integer')
->setLabel(t('Width'))
->setDescription(t('The width of the image in pixels.'));
$properties['height'] = DataDefinition::create('integer')
->setLabel(t('Height'))
->setDescription(t('The height of the image in pixels.'));
return $properties;
}
/**
* {@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' => t('Upload destination'),
'#options' => $scheme_options,
'#default_value' => $settings['uri_scheme'],
'#description' => 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'] = 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 resolution settings.
$max_resolution = explode('x', $settings['max_resolution']) + ['', ''];
$element['max_resolution'] = [
'#type' => 'item',
'#title' => t('Maximum image resolution'),
'#element_validate' => [[get_class($this), 'validateResolution']],
'#weight' => 4.1,
'#field_prefix' => '<div class="container-inline">',
'#field_suffix' => '</div>',
'#description' => 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' => t('Maximum width'),
'#title_display' => 'invisible',
'#default_value' => $max_resolution[0],
'#min' => 1,
'#field_suffix' => ' × ',
];
$element['max_resolution']['y'] = [
'#type' => 'number',
'#title' => t('Maximum height'),
'#title_display' => 'invisible',
'#default_value' => $max_resolution[1],
'#min' => 1,
'#field_suffix' => ' ' . t('pixels'),
];
$min_resolution = explode('x', $settings['min_resolution']) + ['', ''];
$element['min_resolution'] = [
'#type' => 'item',
'#title' => t('Minimum image resolution'),
'#element_validate' => [[get_class($this), 'validateResolution']],
'#weight' => 4.2,
'#field_prefix' => '<div class="container-inline">',
'#field_suffix' => '</div>',
'#description' => 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' => t('Minimum width'),
'#title_display' => 'invisible',
'#default_value' => $min_resolution[0],
'#min' => 1,
'#field_suffix' => ' × ',
];
$element['min_resolution']['y'] = [
'#type' => 'number',
'#title' => t('Minimum height'),
'#title_display' => 'invisible',
'#default_value' => $min_resolution[1],
'#min' => 1,
'#field_suffix' => ' ' . t('pixels'),
];
// Remove the description option.
unset($element['description_field']);
// Add title and alt configuration options.
$element['alt_field'] = [
'#type' => 'checkbox',
'#title' => t('Enable <em>Alt</em> field'),
'#default_value' => $settings['alt_field'],
'#description' => 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' => t('<em>Alt</em> field required'),
'#default_value' => $settings['alt_field_required'],
'#description' => t('Making this field required is recommended.'),
'#weight' => 10,
'#states' => [
'visible' => [
':input[name="settings[alt_field]"]' => ['checked' => TRUE],
],
],
];
$element['title_field'] = [
'#type' => 'checkbox',
'#title' => t('Enable <em>Title</em> field'),
'#default_value' => $settings['title_field'],
'#description' => 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' => 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'] = 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->width;
$height = $this->height;
// Determine the dimensions if necessary.
if ($this->entity && $this->entity instanceof EntityInterface) {
if (empty($width) || empty($height)) {
$image = \Drupal::service('image.factory')->get($this->entity->getFileUri());
if ($image->isValid()) {
$this->width = $image->getWidth();
$this->height = $image->getHeight();
}
}
}
else {
trigger_error(sprintf("Missing file with ID %s.", $this->target_id), E_USER_WARNING);
}
}
/**
* {@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));
// Generate a max of 5 different images.
if (!isset($images[$extension][$min_resolution][$max_resolution]) || count($images[$extension][$min_resolution][$max_resolution]) <= 5) {
$tmp_file = drupal_tempnam('temporary://', 'generateImage_');
$destination = $tmp_file . '.' . $extension;
file_unmanaged_move($tmp_file, $destination);
if ($path = $random->image(\Drupal::service('file_system')->realpath($destination), $min_resolution, $max_resolution)) {
$image = File::create();
$image->setFileUri($path);
$image->setOwnerId(\Drupal::currentUser()->id());
$image->setMimeType(\Drupal::service('file.mime_type.guesser')->guess($path));
$image->setFileName(drupal_basename($path));
$destination_dir = static::doGetUploadLocation($settings);
file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY);
$destination = $destination_dir . '/' . basename($path);
$file = file_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];
}
list($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 resolution 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 t()
// here, so it won't be escaped again as it's already marked safe.
$form_state->setError($element[$dimension], t('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' => t('Default image'),
'#open' => TRUE,
];
// Convert the stored UUID to a FID.
$fids = [];
$uuid = $settings['default_image']['uuid'];
if ($uuid && ($file = $this->getEntityManager()->loadEntityByUuid('file', $uuid))) {
$fids[0] = $file->id();
}
$element['default_image']['uuid'] = [
'#type' => 'managed_file',
'#title' => t('Image'),
'#description' => 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',
[get_class($this), 'validateDefaultImageForm'],
],
'#upload_validators' => $this->getUploadValidators(),
];
$element['default_image']['alt'] = [
'#type' => 'textfield',
'#title' => t('Alternative text'),
'#description' => 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' => t('Title'),
'#description' => 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 a #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::entityManager()->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;
}
/**
* Gets the entity manager.
*
* @return \Drupal\Core\Entity\EntityManagerInterface
*/
protected function getEntityManager() {
if (!isset($this->entityManager)) {
$this->entityManager = \Drupal::entityManager();
}
return $this->entityManager;
}
}

View file

@ -0,0 +1,360 @@
<?php
namespace Drupal\image\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\file\Entity\File;
use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
use Drupal\image\Entity\ImageStyle;
/**
* Plugin implementation of the 'image_image' widget.
*
* @FieldWidget(
* id = "image_image",
* label = @Translation("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' => t('Preview image style'),
'#type' => 'select',
'#options' => image_style_options(FALSE),
'#empty_option' => '<' . t('no preview') . '>',
'#default_value' => $this->getSetting('preview_image_style'),
'#description' => 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 = t('Preview image style: @style', ['@style' => $image_styles[$image_style_setting]]);
}
else {
$preview_image_style = t('No preview');
}
array_unshift($summary, $preview_image_style);
return $summary;
}
/**
* Overrides \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formMultipleElements().
*
* Special handling for draggable multiple widgets and 'add more' button.
*/
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')->renderPlain($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']['file_validate_is_image'] = [];
// Add upload resolution validation.
if ($field_settings['max_resolution'] || $field_settings['min_resolution']) {
$element['#upload_validators']['file_validate_image_resolution'] = [$field_settings['max_resolution'], $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']['file_validate_extensions'][0] = 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::entityManager()->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 a 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(),
];
// Determine image dimensions.
if (isset($element['#value']['width']) && isset($element['#value']['height'])) {
$variables['width'] = $element['#value']['width'];
$variables['height'] = $element['#value']['height'];
}
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.
$element['width'] = [
'#type' => 'hidden',
'#value' => $variables['width'],
];
$element['height'] = [
'#type' => 'hidden',
'#value' => $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' => t('Alternative text'),
'#type' => 'textfield',
'#default_value' => isset($item['alt']) ? $item['alt'] : '',
'#description' => t('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 ? [[get_called_class(), 'validateRequiredFields']] : [],
];
$element['title'] = [
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => isset($item['title']) ? $item['title'] : '',
'#description' => t('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 ? [[get_called_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'])) {
// If the image is not there, we do not check for empty values.
$parents = $element['#parents'];
$field = array_pop($parents);
$image_field = NestedArray::getValue($form_state->getUserInput(), $parents);
// We check for the array key, so that it can be NULL (like if the user
// submits the form without using the "upload" button).
if (!array_key_exists($field, $image_field)) {
return;
}
}
else {
$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::entityManager()->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;
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Converts an image resource.
*
* @ImageEffect(
* id = "image_convert",
* label = @Translation("Convert"),
* description = @Translation("Converts an image between extensions (e.g. from PNG to 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' => t('Extension'),
'#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,90 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
/**
* Crops an image resource.
*
* @ImageEffect(
* id = "image_crop",
* label = @Translation("Crop"),
* description = @Translation("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) {
list($x, $y) = explode('-', $this->configuration['anchor']);
$x = image_filter_keyword($x, $image->getWidth(), $this->configuration['width']);
$y = image_filter_keyword($y, $image->getHeight(), $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' => t('Anchor'),
'#options' => [
'left-top' => t('Top left'),
'center-top' => t('Top center'),
'right-top' => t('Top right'),
'left-center' => t('Center left'),
'center-center' => t('Center'),
'right-center' => t('Center right'),
'left-bottom' => t('Bottom left'),
'center-bottom' => t('Bottom center'),
'right-bottom' => t('Bottom right'),
],
'#theme' => 'image_anchor',
'#default_value' => $this->configuration['anchor'],
'#description' => 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,30 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Image\ImageInterface;
use Drupal\image\ImageEffectBase;
/**
* Desaturates (grayscale) an image resource.
*
* @ImageEffect(
* id = "image_desaturate",
* label = @Translation("Desaturate"),
* description = @Translation("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,96 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Resizes an image resource.
*
* @ImageEffect(
* id = "image_resize",
* label = @Translation("Resize"),
* description = @Translation("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' => t('Width'),
'#default_value' => $this->configuration['width'],
'#field_suffix' => ' ' . t('pixels'),
'#required' => TRUE,
'#min' => 1,
];
$form['height'] = [
'#type' => 'number',
'#title' => t('Height'),
'#default_value' => $this->configuration['height'],
'#field_suffix' => ' ' . 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,128 @@
<?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\image\ConfigurableImageEffectBase;
/**
* Rotates an image resource.
*
* @ImageEffect(
* id = "image_rotate",
* label = @Translation("Rotate"),
* description = @Translation("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' => t('Rotation angle'),
'#description' => 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' => t('Background color'),
'#description' => 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' => t('Randomize'),
'#description' => 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,50 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Core\Image\ImageInterface;
/**
* Scales and crops an image resource.
*
* @ImageEffect(
* id = "image_scale_and_crop",
* label = @Translation("Scale and crop"),
* description = @Translation("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 = $this->configuration['width'];
$height = $this->configuration['height'];
$scale = max($width / $image->getWidth(), $height / $image->getHeight());
list($x, $y) = explode('-', $this->configuration['anchor']);
$x = image_filter_keyword($x, $image->getWidth() * $scale, $width);
$y = image_filter_keyword($y, $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,97 @@
<?php
namespace Drupal\image\Plugin\ImageEffect;
use Drupal\Component\Utility\Image;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
/**
* Scales an image resource.
*
* @ImageEffect(
* id = "image_scale",
* label = @Translation("Scale"),
* description = @Translation("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' => t('Allow Upscaling'),
'#description' => 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,39 @@
<?php
namespace Drupal\image\Plugin\InPlaceEditor;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\quickedit\Plugin\InPlaceEditorBase;
/**
* Defines the image text in-place editor.
*
* @InPlaceEditor(
* id = "image"
* )
*/
class Image extends InPlaceEditorBase {
/**
* {@inheritdoc}
*/
public function isCompatible(FieldItemListInterface $items) {
$field_definition = $items->getFieldDefinition();
// This editor is only compatible with single-value image fields.
return $field_definition->getFieldStorageDefinition()->getCardinality() === 1
&& $field_definition->getType() === 'image';
}
/**
* {@inheritdoc}
*/
public function getAttachments() {
return [
'library' => [
'image/quickedit.inPlaceEditor.image',
],
];
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\image\Plugin\migrate\cckfield\d7;
@trigger_error('ImageField is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d7\ImageField instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
/**
* @MigrateCckField(
* id = "image",
* core = {7},
* source_module = "image",
* destination_module = "file"
* )
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d7\ImageField instead.
*
* @see https://www.drupal.org/node/2751897
*/
class ImageField extends CckFieldPluginBase {
/**
* {@inheritdoc}
*/
public function processCckFieldValues(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,52 @@
<?php
namespace Drupal\image\Plugin\migrate\destination;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* Every migration that uses this destination must have an optional
* dependency on the d6_file migration to ensure it runs first.
*
* @MigrateDestination(
* id = "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,15 @@
<?php
namespace Drupal\image\Plugin\migrate\field\d6;
use Drupal\file\Plugin\migrate\field\d6\FileField;
/**
* @MigrateField(
* id = "imagefield",
* core = {6},
* source_module = "imagefield",
* destination_module = "image"
* )
*/
class ImageField extends FileField {}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\image\Plugin\migrate\field\d7;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* @MigrateField(
* id = "image",
* core = {7},
* source_module = "image",
* destination_module = "image"
* )
*/
class ImageField extends FieldPluginBase {
/**
* {@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,49 @@
<?php
namespace Drupal\image\Plugin\migrate\process\d6;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(
* id = "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) {
$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,66 @@
<?php
namespace Drupal\image\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
/**
* Drupal 6 imagecache presets source from database.
*
* @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,66 @@
<?php
namespace Drupal\image\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;
/**
* Drupal image styles source from database.
*
* @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,67 @@
<?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',
],
[
'_access' => 'TRUE',
]
);
return $routes;
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Drupal\image\Tests;
@trigger_error('The ' . __NAMESPACE__ . '\ImageFieldTestBase class is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Tests\image\Functional\ImageFieldTestBase instead. See https://www.drupal.org/node/2863626.', E_USER_DEPRECATED);
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
use Drupal\simpletest\WebTestBase;
/**
* TODO: Test the following functions.
*
* In file:
* - image.effects.inc:
* image_style_generate()
* \Drupal\image\ImageStyleInterface::createDerivative()
*
* - image.module:
* image_style_options()
* \Drupal\image\ImageStyleInterface::flush()
* image_filter_keyword()
*/
/**
* This class provides methods specifically for testing Image's field handling.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\image\Functional\ImageFieldTestBase instead.
*/
abstract class ImageFieldTestBase extends WebTestBase {
use ImageFieldCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'image', 'field_ui', 'image_module_test'];
/**
* An user with permissions to administer content types and image styles.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
$this->adminUser = $this->drupalCreateUser(['access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer node fields', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles', 'administer node display']);
$this->drupalLogin($this->adminUser);
}
/**
* Preview an image in a node.
*
* @param \Drupal\Core\Image\ImageInterface $image
* A file object representing the image to upload.
* @param string $field_name
* Name of the image field the image should be attached to.
* @param string $type
* The type of node to create.
*/
public function previewNodeImage($image, $field_name, $type) {
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
$edit['files[' . $field_name . '_0]'] = \Drupal::service('file_system')->realpath($image->uri);
$this->drupalPostForm('node/add/' . $type, $edit, t('Preview'));
}
/**
* Upload an image to a node.
*
* @param $image
* A file object representing the image to upload.
* @param $field_name
* Name of the image field the image should be attached to.
* @param $type
* The type of node to create.
* @param $alt
* The alt text for the image. Use if the field settings require alt text.
*/
public function uploadNodeImage($image, $field_name, $type, $alt = '') {
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
$edit['files[' . $field_name . '_0]'] = \Drupal::service('file_system')->realpath($image->uri);
$this->drupalPostForm('node/add/' . $type, $edit, t('Save'));
if ($alt) {
// Add alt text.
$this->drupalPostForm(NULL, [$field_name . '[0][alt]' => $alt], t('Save'));
}
// Retrieve ID of the newly created node from the current URL.
$matches = [];
preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
return isset($matches[1]) ? $matches[1] : FALSE;
}
/**
* Retrieves the fid of the last inserted file.
*/
protected function getLastFileId() {
return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField();
}
}

View file

@ -0,0 +1,14 @@
{#
/**
* @file
* Default theme implementation for a 3x3 grid of checkboxes for image anchors.
*
* Available variables:
* - table: HTML for the table of image anchors.
*
* @see template_preprocess_image_anchor()
*
* @ingroup themeable
*/
#}
{{ table }}

View file

@ -0,0 +1,32 @@
{#
/**
* @file
* Default theme implementation for a summary of an image crop effect.
*
* Available variables:
* - data: The current configuration for this resize effect, including:
* - width: The width of the resized image.
* - height: The height of the resized image.
* - anchor: The part of the image that will be retained after cropping.
* - anchor_label: The translated label of the crop anchor.
* - effect: The effect information, including:
* - id: The effect identifier.
* - label: The effect name.
* - description: The effect description.
*
* @ingroup themeable
*/
#}
{% if data.width and data.height -%}
{{ data.width }}×{{ data.height }}
{%- else -%}
{% if data.width %}
{% trans %}
width {{ data.width }}
{% endtrans %}
{% elseif data.height %}
{% trans %}
height {{ data.height }}
{% endtrans %}
{% endif %}
{%- endif %}

View file

@ -0,0 +1,20 @@
{#
/**
* @file
* Default theme implementation to display a formatted image field.
*
* Available variables:
* - image: A collection of image data.
* - image_style: An optional image style.
* - url: An optional URL the image can be linked to.
*
* @see template_preprocess_image_formatter()
*
* @ingroup themeable
*/
#}
{% if url %}
<a href="{{ url }}">{{ image }}</a>
{% else %}
{{ image }}
{% endif %}

View file

@ -0,0 +1,30 @@
{#
/**
* @file
* Default theme implementation for a summary of an image resize effect.
*
* Available variables:
* - data: The current configuration for this resize effect, including:
* - width: The width of the resized image.
* - height: The height of the resized image.
* - effect: The effect information, including:
* - id: The effect identifier.
* - label: The effect name.
* - description: The effect description.
*
* @ingroup themeable
*/
#}
{% if data.width and data.height -%}
{{ data.width }}×{{ data.height }}
{%- else -%}
{% if data.width %}
{% trans %}
width {{ data.width }}
{% endtrans %}
{% elseif data.height %}
{% trans %}
height {{ data.height }}
{% endtrans %}
{% endif %}
{%- endif %}

View file

@ -0,0 +1,28 @@
{#
/**
* @file
* Default theme implementation for a summary of an image rotate effect.
*
* Available variables:
* - data: The current configuration for this resize effect, including:
* - degrees: Degrees to rotate the image, positive values will rotate the
* image clockwise, negative values counter-clockwise.
* - bgcolor: The hex background color of the new areas created as consequence
* of rotation.
* - random: If the rotation angle is randomized.
* - effect: The effect information, including:
* - id: The effect identifier.
* - label: The effect name.
* - description: The effect description.
*
* @ingroup themeable
*/
#}
{% if data.random %}
{% set degrees = data.degrees|abs %}
{% trans %}
random between -{{ degrees }}° and {{ degrees }}°
{% endtrans %}
{% else %}
{{ data.degrees }}°
{% endif %}

View file

@ -0,0 +1,32 @@
{#
/**
* @file
* Default theme implementation for a summary of an image scale and crop effect.
*
* Available variables:
* - data: The current configuration for this resize effect, including:
* - width: The width of the resized image.
* - height: The height of the resized image.
* - anchor: The part of the image that will be retained after cropping.
* - anchor_label: The translated label of the crop anchor.
* - effect: The effect information, including:
* - id: The effect identifier.
* - label: The effect name.
* - description: The effect description.
*
* @ingroup themeable
*/
#}
{% if data.width and data.height -%}
{{ data.width }}×{{ data.height }}
{%- else -%}
{% if data.width %}
{% trans %}
width {{ data.width }}
{% endtrans %}
{% elseif data.height %}
{% trans %}
height {{ data.height }}
{% endtrans %}
{% endif %}
{%- endif %}

View file

@ -0,0 +1,37 @@
{#
/**
* @file
* Default theme implementation for a summary of an image scale effect.
*
* Available variables:
* - data: The current configuration for this resize effect, including:
* - width: The width of the resized image.
* - height: The height of the resized image.
* - upscale: If images larger than their original size can scale.
* - effect: The effect information, including:
* - id: The effect identifier.
* - label: The effect name.
* - description: The effect description.
*
* @ingroup themeable
*/
#}
{% if data.width and data.height -%}
{{ data.width }}×{{ data.height }}
{%- else -%}
{% if data.width %}
{% trans %}
width {{ data.width }}
{% endtrans %}
{% elseif data.height %}
{% trans %}
height {{ data.height }}
{% endtrans %}
{% endif %}
{%- endif %}
{% if data.upscale %}
{% trans %}
(upscaling allowed)
{% endtrans %}
{% endif %}

View file

@ -0,0 +1,57 @@
{#
/**
* @file
* Default theme implementation to display a preview of an image style.
*
* Available variables:
* - style_id: The ID of the image style.
* - style_name: The name of the image style.
* - cache_bypass: A timestamp token used to avoid browser caching of images.
* - original: An associative array containing:
* - url: The URL of the original image.
* - width: The width in pixels of the original image.
* - height: The height in pixels of the original image.
* - rendered: The render array for the original image.
* - derivative: An associative array containing:
* - url: The URL of the derivative image.
* - width: The width in pixels of the derivative image.
* - height: The height in pixels of the derivative image.
* - rendered: The rendered derivative image.
* - preview: An associative array containing:
* - original: An associative array containing:
* - width: The width in pixels of the original image in the preview.
* - height: The height in pixels of the original image in the preview.
* - derivative: An associative array containing:
* - width: The width in pixels of the derivative image in the preview.
* - height: The height in pixels of the derivative image in the preview.
*
* @see template_preprocess_image_style_preview()
*
* @ingroup themeable
*/
#}
<div class="image-style-preview preview clearfix">
{# Preview of the original image. #}
<div class="preview-image-wrapper">
{{ 'original'|t }} (<a href="{{ original.url }}">{{ 'view actual size'|t }}</a>)
<div class="preview-image original-image" style="width: {{ preview.original.width }}px; height: {{ preview.original.height }}px;">
<a href="{{ original.url }}">
{{ original.rendered }}
</a>
<div class="height" style="height: {{ preview.original.height }}px"><span>{{ original.height }}px</span></div>
<div class="width" style="width: {{ preview.original.width }}px"><span>{{ original.width }}px</span></div>
</div>
</div>
{# Derivative of the image style. #}
<div class="preview-image-wrapper">
{{ style_name }} (<a href="{{ derivative.url }}?{{ cache_bypass }}">{{ 'view actual size'|t }}</a>)
<div class="preview-image modified-image" style="width: {{ preview.derivative.width }}px; height: {{ preview.derivative.height }}px;">
<a href="{{ derivative.url }}?{{ cache_bypass }}">
{{ derivative.rendered }}
</a>
<div class="height" style="height: {{ preview.derivative.height }}px"><span>{{ derivative.height }}px</span></div>
<div class="width" style="width: {{ preview.derivative.width }}px"><span>{{ derivative.width }}px</span></div>
</div>
</div>
</div>

View file

@ -0,0 +1,20 @@
{#
/**
* @file
* Default theme implementation for an image using a specific image style.
*
* Available variables:
* - attributes: HTML attributes for the image, including the following:
* - src: Full URL or relative path to the image file.
* - class: One or more classes to be applied to the image.
* - width: The width of the image (if known).
* - height: The height of the image (if known).
* - title: The title of the image.
* - alt: The alternative text for the image.
*
* @see template_preprocess_image_style()
*
* @ingroup themeable
*/
#}
{{ image }}

View file

@ -0,0 +1,19 @@
{#
/**
* @file
* Default theme implementation for an image field widget.
*
* Available variables:
* - attributes: HTML attributes for the containing element.
* - data: Render elements of the image widget.
*
* @see template_preprocess_image_widget()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ data.preview }}
{# Render widget data without the image preview that was output already. #}
{{ data|without('preview') }}
</div>

View file

@ -0,0 +1,28 @@
langcode: en
status: true
name: test_scale_and_crop_add_anchor
label: test_scale_and_crop_add_anchor
effects:
8c7170c9-5bcc-40f9-8698-f88a8be6d434:
uuid: 8c7170c9-5bcc-40f9-8698-f88a8be6d434
id: image_scale_and_crop
weight: 1
data:
width: 100
height: 100
a8d83b12-abc6-40c8-9c2f-78a4e421cf97:
uuid: a8d83b12-abc6-40c8-9c2f-78a4e421cf97
id: image_scale_and_crop
weight: 2
data:
width: 100
height: 100
anchor: left-top
1bffd475-19d0-439a-b6a1-7e5850ce40f9:
uuid: 1bffd475-19d0-439a-b6a1-7e5850ce40f9
id: image_rotate
weight: 3
data:
degrees: 180
bgcolor: ''
random: false

View file

@ -0,0 +1,19 @@
<?php
/**
* @file
* Test fixture.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$connection->insert('config')
->fields([
'collection' => '',
'name' => 'image.style.test_scale_and_crop_add_anchor',
'data' => serialize(Yaml::decode(file_get_contents('core/modules/image/tests/fixtures/update/image.image_style.test_scale_and_crop_add_anchor.yml'))),
])
->execute();

View file

@ -0,0 +1,15 @@
image.effect.image_module_test_ajax:
type: mapping
label: 'Ajax test'
mapping:
test_parameter:
type: integer
label: 'Test Parameter'
image.style.*.third_party.image_module_test:
type: mapping
label: 'Schema for image_module_test module additions to image_style entity'
mapping:
foo:
type: string
label: 'Label for foo'

View file

@ -0,0 +1,6 @@
name: 'Image test'
type: module
description: 'Provides hook implementations for testing Image module functionality.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Provides Image module hook implementations for testing purposes.
*/
use Drupal\image\ImageStyleInterface;
function image_module_test_file_download($uri) {
$default_uri = \Drupal::state()->get('image.test_file_download') ?: FALSE;
if ($default_uri == $uri) {
return ['X-Image-Owned-By' => 'image_module_test'];
}
}
/**
* Implements hook_image_effect_info_alter().
*
* Used to keep a count of cache misses in \Drupal\image\ImageEffectManager.
*/
function image_module_test_image_effect_info_alter(&$effects) {
$image_effects_definition_called = &drupal_static(__FUNCTION__, 0);
$image_effects_definition_called++;
}
/**
* Implements hook_image_style_presave().
*
* Used to save test third party settings in the image style entity.
*/
function image_module_test_image_style_presave(ImageStyleInterface $style) {
$style->setThirdPartySetting('image_module_test', 'foo', 'bar');
}

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\image_module_test\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Empty renderer for a dummy field with an AJAX handler.
*
* @FieldFormatter(
* id = "image_module_test_dummy_ajax_formatter",
* module = "image_module_test",
* label = @Translation("Dummy AJAX"),
* field_types= {
* "image_module_test_dummy_ajax"
* }
* )
*/
class DummyAjaxFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = t('Renders nothing');
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$element = [];
return $element;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\image_module_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a dummy field containing an AJAX handler.
*
* @FieldType(
* id = "image_module_test_dummy_ajax",
* label = @Translation("Dummy AJAX"),
* description = @Translation("A field containing an AJAX handler."),
* category = @Translation("Field"),
* default_widget = "image_module_test_dummy_ajax_widget",
* default_formatter = "image_module_test_dummy_ajax_formatter"
* )
*/
class DummyAjaxItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'varchar',
'length' => 255,
],
],
];
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->get('value')->getValue());
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Dummy string value'));
return $properties;
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\image_module_test\Plugin\Field\FieldWidget;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Default widget for Dummy AJAX test.
*
* @FieldWidget(
* id = "image_module_test_dummy_ajax_widget",
* label = @Translation("Dummy AJAX widget"),
* field_types = {
* "image_module_test_dummy_ajax"
* },
* multiple_values = TRUE,
* )
*/
class DummyAjaxWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element['select_widget'] = [
'#type' => 'select',
'#title' => $this->t('Dummy select'),
'#options' => ['pow' => 'Pow!', 'bam' => 'Bam!'],
'#required' => TRUE,
'#ajax' => [
'callback' => get_called_class() . '::dummyAjaxCallback',
'effect' => 'fade',
],
];
return $element;
}
/**
* Ajax callback for Dummy AJAX test.
*
* @param array $form
* The build form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* Ajax response.
*/
public static function dummyAjaxCallback(array &$form, FormStateInterface $form_state) {
return new AjaxResponse();
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\image_module_test\Plugin\ImageEffect;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\image\ConfigurableImageEffectBase;
/**
* Provides a test effect using Ajax in the configuration form.
*
* @ImageEffect(
* id = "image_module_test_ajax",
* label = @Translation("Ajax test")
* )
*/
class AjaxTestImageEffect extends ConfigurableImageEffectBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'test_parameter' => 0,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['test_parameter'] = [
'#type' => 'number',
'#title' => t('Test parameter'),
'#default_value' => $this->configuration['test_parameter'],
'#min' => 0,
];
$form['ajax_refresh'] = [
'#type' => 'button',
'#value' => $this->t('Ajax refresh'),
'#ajax' => ['callback' => [$this, 'ajaxCallback']],
];
$form['ajax_value'] = [
'#id' => 'ajax-value',
'#type' => 'item',
'#title' => $this->t('Ajax value'),
'#markup' => 'bar',
];
return $form;
}
/**
* AJAX callback.
*/
public function ajaxCallback($form, FormStateInterface $form_state) {
$item = [
'#type' => 'item',
'#title' => $this->t('Ajax value'),
'#markup' => microtime(),
];
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#ajax-value', $item));
return $response;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['test_parameter'] = $form_state->getValue('test_parameter');
}
/**
* {@inheritdoc}
*/
public function applyEffect(ImageInterface $image) {
return TRUE;
}
}

Some files were not shown because too many files have changed in this diff Show more