diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index d937411..4a2a52c 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -242,6 +242,9 @@ Path module
PHP module
- ?
+Picture module
+- Peter Droogmans 'attiks' http://drupal.org/user/105002
+
Poll module
- Andrei Mateescu 'amateescu' http://drupal.org/user/729614
diff --git a/core/modules/picture/lib/Drupal/picture/PictureMapping.php b/core/modules/picture/lib/Drupal/picture/PictureMapping.php
new file mode 100644
index 0000000..544e3e2
--- /dev/null
+++ b/core/modules/picture/lib/Drupal/picture/PictureMapping.php
@@ -0,0 +1,138 @@
+loadBreakpointGroup();
+ $this->loadAllMappings();
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity::save().
+ */
+ public function save() {
+ // Only save the keys, but return the full objects.
+ if (isset($this->breakpointGroup) && is_object($this->breakpointGroup)) {
+ $this->breakpointGroup = $this->breakpointGroup->id();
+ }
+ parent::save();
+ $this->loadBreakpointGroup();
+ $this->loadAllMappings();
+ }
+
+ /**
+ * Implements EntityInterface::createDuplicate().
+ */
+ public function createDuplicate() {
+ return entity_create('picture_mapping', array(
+ 'id' => '',
+ 'label' => t('Clone of !label', array('!label' => check_plain($this->label()))),
+ 'mappings' => $this->mappings,
+ ));
+ }
+
+ /**
+ * Load breakpoint group.
+ */
+ protected function loadBreakpointGroup() {
+ if ($this->breakpointGroup) {
+ $breakpoint_group = entity_load('breakpoint_group', $this->breakpointGroup);
+ $this->breakpointGroup = $breakpoint_group;
+ }
+ }
+
+ /**
+ * Load all mappings, remove non-existing ones.
+ */
+ protected function loadAllMappings() {
+ $loaded_mappings = $this->mappings;
+ $this->mappings = array();
+ if ($this->breakpointGroup) {
+ foreach ($this->breakpointGroup->breakpoints as $breakpoint_id => $breakpoint) {
+ // Get the mapping for the default multiplier.
+ $this->mappings[$breakpoint_id]['1x'] = '';
+ if (isset($loaded_mappings[$breakpoint_id]['1x'])) {
+ $this->mappings[$breakpoint_id]['1x'] = $loaded_mappings[$breakpoint_id]['1x'];
+ }
+
+ // Get the mapping for the other multipliers.
+ if (isset($breakpoint->multipliers) && !empty($breakpoint->multipliers)) {
+ foreach ($breakpoint->multipliers as $multiplier => $status) {
+ if ($status) {
+ $this->mappings[$breakpoint_id][$multiplier] = '';
+ if (isset($loaded_mappings[$breakpoint_id][$multiplier])) {
+ $this->mappings[$breakpoint_id][$multiplier] = $loaded_mappings[$breakpoint_id][$multiplier];
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if there's at least one mapping defined.
+ */
+ public function hasMappings() {
+ $mapping_found = FALSE;
+ foreach ($this->mappings as $breakpoint => $multipliers) {
+ $filtered_array = array_filter($multipliers);
+ if (!empty($filtered_array)) {
+ $mapping_found = TRUE;
+ break;
+ }
+ }
+ return $mapping_found;
+ }
+}
diff --git a/core/modules/picture/lib/Drupal/picture/PictureMappingFormController.php b/core/modules/picture/lib/Drupal/picture/PictureMappingFormController.php
new file mode 100644
index 0000000..0ad4ae2
--- /dev/null
+++ b/core/modules/picture/lib/Drupal/picture/PictureMappingFormController.php
@@ -0,0 +1,138 @@
+ 'textfield',
+ '#title' => t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => $picture_mapping->label(),
+ '#description' => t("Example: 'Main content' or 'Sidebar'."),
+ '#required' => TRUE,
+ );
+ $form['id'] = array(
+ '#type' => 'machine_name',
+ '#default_value' => $picture_mapping->id(),
+ '#machine_name' => array(
+ 'exists' => 'picture_mapping_load',
+ 'source' => array('label'),
+ ),
+ '#disabled' => (bool) $picture_mapping->id() && $this->operation != 'duplicate',
+ );
+ $form['breakpointGroup'] = array(
+ '#type' => 'select',
+ '#title' => t('Breakpoint Group'),
+ '#default_value' => !empty($picture_mapping->breakpointGroup) ? $picture_mapping->breakpointGroup->id() : '',
+ '#options' => breakpoint_group_select_options(),
+ '#required' => TRUE,
+ );
+
+ $image_styles = image_style_options(TRUE);
+ foreach ($picture_mapping->mappings as $breakpoint_id => $mapping) {
+ foreach ($mapping as $multiplier => $image_style) {
+ $label = $multiplier . ' ' . $picture_mapping->breakpointGroup->breakpoints[$breakpoint_id]->name . ' [' . $picture_mapping->breakpointGroup->breakpoints[$breakpoint_id]->mediaQuery . ']';
+ $form['mappings'][$breakpoint_id][$multiplier] = array(
+ '#type' => 'select',
+ '#title' => check_plain($label),
+ '#options' => $image_styles,
+ '#default_value' => $image_style,
+ );
+ }
+ }
+
+ $form['#tree'] = TRUE;
+
+ return parent::form($form, $form_state, $picture_mapping);
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::actions().
+ */
+ protected function actions(array $form, array &$form_state) {
+ // Only includes a Save action for the entity, no direct Delete button.
+ return array(
+ 'submit' => array(
+ '#value' => t('Save'),
+ '#validate' => array(
+ array($this, 'validate'),
+ ),
+ '#submit' => array(
+ array($this, 'submit'),
+ array($this, 'save'),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::validate().
+ */
+ public function validate(array $form, array &$form_state) {
+ $picture_mapping = $this->getEntity($form_state);
+
+ // Only validate on edit.
+ if (isset($form_state['values']['mappings'])) {
+ $picture_mapping->mappings = $form_state['values']['mappings'];
+
+ // Check if another breakpoint group is selected.
+ if ($form_state['values']['breakpointGroup'] != $form_state['complete_form']['breakpointGroup']['#default_value']) {
+ // Remove the mappings.
+ unset($form_state['values']['mappings']);
+ }
+ // Make sure at least one mapping is defined.
+ elseif (!$picture_mapping->isNew() && !$picture_mapping->hasMappings()) {
+ form_set_error('mappings', t('Please select at least one mapping.'));
+ }
+ }
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::save().
+ */
+ public function save(array $form, array &$form_state) {
+ $picture_mapping = $this->getEntity($form_state);
+ $picture_mapping->save();
+
+ watchdog('picture', 'Picture mapping @label saved.', array('@label' => $picture_mapping->label()), WATCHDOG_NOTICE);
+ drupal_set_message(t('Picture mapping %label saved.', array('%label' => $picture_mapping->label())));
+
+ // Redirect to edit form after creating a new mapping or after selecting
+ // another breakpoint group.
+ if (!$picture_mapping->hasMappings()) {
+ $uri = $picture_mapping->uri();
+ $form_state['redirect'] = $uri['path'] . '/edit';
+ }
+ else {
+ $form_state['redirect'] = 'admin/config/media/picturemapping';
+ }
+ }
+
+}
diff --git a/core/modules/picture/lib/Drupal/picture/PictureMappingListController.php b/core/modules/picture/lib/Drupal/picture/PictureMappingListController.php
new file mode 100644
index 0000000..f63127d
--- /dev/null
+++ b/core/modules/picture/lib/Drupal/picture/PictureMappingListController.php
@@ -0,0 +1,50 @@
+entityInfo['list path'];
+ $items = parent::hookMenu();
+
+ // Override the access callback.
+ $items[$path]['title'] = 'Picture Mappings';
+ $items[$path]['description'] = 'Manage list of picture mappings.';
+ $items[$path]['access callback'] = 'user_access';
+ $items[$path]['access arguments'] = array('administer pictures');
+
+ return $items;
+ }
+
+ /**
+ * Overrides Drupal\config\ConfigEntityListController::getOperations();
+ */
+ public function getOperations(EntityInterface $entity) {
+ $operations = parent::getOperations($entity);
+ $uri = $entity->uri();
+ $operations['duplicate'] = array(
+ 'title' => t('Duplicate'),
+ 'href' => $uri['path'] . '/duplicate',
+ 'options' => $uri['options'],
+ 'weight' => 15,
+ );
+ return $operations;
+ }
+
+}
diff --git a/core/modules/picture/lib/Drupal/picture/Plugin/field/formatter/PictureFormatter.php b/core/modules/picture/lib/Drupal/picture/Plugin/field/formatter/PictureFormatter.php
new file mode 100644
index 0000000..577d4f0
--- /dev/null
+++ b/core/modules/picture/lib/Drupal/picture/Plugin/field/formatter/PictureFormatter.php
@@ -0,0 +1,190 @@
+ $picture_mapping) {
+ if ($picture_mapping->hasMappings()) {
+ $picture_options[$machine_name] = $picture_mapping->label();
+ }
+ }
+ }
+
+ $elements['picture_mapping'] = array(
+ '#title' => t('Picture mapping'),
+ '#type' => 'select',
+ '#default_value' => $this->getSetting('picture_mapping'),
+ '#required' => TRUE,
+ '#options' => $picture_options,
+ );
+
+ $image_styles = image_style_options(FALSE);
+ $elements['fallback_image_style'] = array(
+ '#title' => t('Fallback image style'),
+ '#type' => 'select',
+ '#default_value' => $this->getSetting('fallback_image_style'),
+ '#empty_option' => t('Automatic'),
+ '#options' => $image_styles,
+ );
+
+ $link_types = array(
+ 'content' => t('Content'),
+ 'file' => t('File'),
+ );
+ $elements['image_link'] = array(
+ '#title' => t('Link image to'),
+ '#type' => 'select',
+ '#default_value' => $this->getSetting('image_link'),
+ '#empty_option' => t('Nothing'),
+ '#options' => $link_types,
+ );
+
+ return $elements;
+ }
+
+ /**
+ * Implements Drupal\field\Plugin\Type\Formatter\FormatterInterface::settingsForm().
+ */
+ public function settingsSummary() {
+ $summary = array();
+
+ $picture_mapping = entity_load('picture_mapping', $this->getSetting('picture_mapping'));
+ if ($picture_mapping) {
+ $summary[] = t('Picture mapping: @picture_mapping', array('@picture_mapping' => $picture_mapping->label()));
+
+ $image_styles = image_style_options(FALSE);
+ unset($image_styles['']);
+ if (isset($image_styles[$this->getSetting('fallback_image_style')])) {
+ $summary[] = t('Fallback Image style: @style', array('@style' => $image_styles[$this->getSetting('fallback_image_style')]));
+ }
+ else {
+ $summary[] = t('Automatic fallback');
+ }
+
+ $link_types = array(
+ 'content' => t('Linked to content'),
+ 'file' => t('Linked to file'),
+ );
+ // Display this setting only if image is linked.
+ if (isset($link_types[$this->getSetting('image_link')])) {
+ $summary[] = $link_types[$this->getSetting('image_link')];
+ }
+ }
+ else {
+ $summary[] = t('Please select a picture mapping');
+ }
+
+ return implode('
', $summary);
+ }
+
+ /**
+ * Implements Drupal\field\Plugin\Type\Formatter\FormatterInterface::viewElements().
+ */
+ public function viewElements(EntityInterface $entity, $langcode, array $items) {
+ $elements = array();
+ // Check if the formatter involves a link.
+ if ($this->getSetting('image_link') == 'content') {
+ $uri = $entity->uri();
+ }
+ elseif ($this->getSetting('image_link') == 'file') {
+ $link_file = TRUE;
+ }
+
+ $breakpoint_styles = array();
+ $fallback_image_style = '';
+
+ $picture_mapping = entity_load('picture_mapping', $this->getSetting('picture_mapping'));
+ if ($picture_mapping) {
+ foreach ($picture_mapping->mappings as $breakpoint_name => $multipliers) {
+ // Make sure there are multipliers.
+ if (!empty($multipliers)) {
+ // Make sure that the breakpoint exists and is enabled.
+ // @todo add the following is breakpoint->status is added again:
+ // $picture_mapping->breakpointGroup->breakpoints[$breakpoint_name]->status
+ if (isset($picture_mapping->breakpointGroup->breakpoints[$breakpoint_name])) {
+ $breakpoint = $picture_mapping->breakpointGroup->breakpoints[$breakpoint_name];
+
+ // Determine the enabled multipliers.
+ $multipliers = array_intersect_key($multipliers, $breakpoint->multipliers);
+ foreach ($multipliers as $multiplier => $image_style) {
+ // Make sure the multiplier still exists.
+ //if (!empty(array_intersect($multiplier, $breakpoint->multipliers))) {
+ if (!empty($image_style)) {
+ // First mapping found is used as fallback.
+ if (empty($fallback_image_style)) {
+ $fallback_image_style = $image_style;
+ }
+ if (!isset($breakpoint_styles[$breakpoint_name])) {
+ $breakpoint_styles[$breakpoint_name] = array();
+ }
+ $breakpoint_styles[$breakpoint_name][$multiplier] = $image_style;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Check if the user defined a custom fallback image style.
+ if ($this->getSetting('fallback_image_style')) {
+ $fallback_image_style = $this->getSetting('fallback_image_style');
+ }
+
+ foreach ($items as $delta => $item) {
+ if (isset($link_file)) {
+ $uri = array(
+ 'path' => file_create_url($item['uri']),
+ 'options' => array(),
+ );
+ }
+ $elements[$delta] = array(
+ '#theme' => 'picture_formatter',
+ '#attached' => array('library' => array(
+ array('picture', 'picturefill'),
+ )),
+ '#item' => $item,
+ '#image_style' => $fallback_image_style,
+ '#breakpoints' => $breakpoint_styles,
+ '#path' => isset($uri) ? $uri : '',
+ );
+ }
+
+ return $elements;
+ }
+}
+
diff --git a/core/modules/picture/lib/Drupal/picture/Tests/PictureAdminUITest.php b/core/modules/picture/lib/Drupal/picture/Tests/PictureAdminUITest.php
new file mode 100644
index 0000000..c519109
--- /dev/null
+++ b/core/modules/picture/lib/Drupal/picture/Tests/PictureAdminUITest.php
@@ -0,0 +1,143 @@
+ 'Picture administration functionality',
+ 'description' => 'Thoroughly test the administrative interface of the picture module.',
+ 'group' => 'Picture',
+ );
+ }
+
+ /**
+ * Drupal\simpletest\WebTestBase\setUp().
+ */
+ public function setUp() {
+ parent::setUp();
+
+ // Create user.
+ $this->admin_user = $this->drupalCreateUser(array(
+ 'administer pictures',
+ ));
+
+ $this->drupalLogin($this->admin_user);
+
+ // Add breakpoint_group and breakpoints.
+ $breakpoint_group = entity_create('breakpoint_group', array(
+ 'id' => 'atestset',
+ 'label' => 'A test set',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_USER_DEFINED,
+ ));
+
+ $breakpoints = array();
+ $breakpoint_names = array('small', 'medium', 'large');
+ for ($i = 0; $i < 3; $i++) {
+ $width = ($i + 1) * 200;
+ $breakpoint = entity_create('breakpoint', array(
+ 'name' => $breakpoint_names[$i],
+ 'mediaQuery' => "(min-width: {$width}px)",
+ 'source' => 'user',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_USER_DEFINED,
+ 'multipliers' => array(
+ '1.5x' => 0,
+ '2x' => '2x',
+ ),
+ ));
+ $breakpoint->save();
+ $breakpoint_group->breakpoints[$breakpoint->id()] = $breakpoint;
+ }
+ $breakpoint_group->save();
+
+ }
+
+ /**
+ * Test picture administration functionality.
+ */
+ public function testPictureAdmin() {
+ // We start without any default mappings.
+ $this->drupalGet('admin/config/media/picturemapping');
+ $this->assertText('There is no Picture mapping yet.');
+
+ // Add a new picture mapping, our breakpoint set should be selected.
+ $this->drupalGet('admin/config/media/picturemapping/add');
+ $this->assertFieldByName('breakpointGroup', 'atestset');
+
+ // Create a new group.
+ $edit = array(
+ 'label' => 'Mapping One',
+ 'id' => 'mapping_one',
+ 'breakpointGroup' => 'atestset',
+ );
+ $this->drupalPost('admin/config/media/picturemapping/add', $edit, t('Save'));
+
+ // Check if the new group is created.
+ $this->assertResponse(200);
+ $this->drupalGet('admin/config/media/picturemapping');
+ $this->assertNoText('There is no Picture mapping yet.');
+ $this->assertText('Mapping One');
+ $this->assertText('mapping_one');
+
+ // Edit the group.
+ $this->drupalGet('admin/config/media/picturemapping/mapping_one/edit');
+ $this->assertFieldByName('label', 'Mapping One');
+ $this->assertFieldByName('breakpointGroup', 'atestset');
+
+ // Check if the dropdows are present for the mappings.
+ $this->assertFieldByName('mappings[custom.user.small][1x]', '');
+ $this->assertFieldByName('mappings[custom.user.small][2x]', '');
+ $this->assertFieldByName('mappings[custom.user.medium][1x]', '');
+ $this->assertFieldByName('mappings[custom.user.medium][2x]', '');
+ $this->assertFieldByName('mappings[custom.user.large][1x]', '');
+ $this->assertFieldByName('mappings[custom.user.large][2x]', '');
+
+ // Save mappings for 1x variant only.
+ $edit = array(
+ 'label' => 'Mapping One',
+ 'breakpointGroup' => 'atestset',
+ 'mappings[custom.user.small][1x]' => 'thumbnail',
+ 'mappings[custom.user.medium][1x]' => 'medium',
+ 'mappings[custom.user.large][1x]' => 'large',
+ );
+ $this->drupalPost('admin/config/media/picturemapping/mapping_one/edit', $edit, t('Save'));
+ $this->drupalGet('admin/config/media/picturemapping/mapping_one/edit');
+ $this->assertFieldByName('mappings[custom.user.small][1x]', 'thumbnail');
+ $this->assertFieldByName('mappings[custom.user.small][2x]', '');
+ $this->assertFieldByName('mappings[custom.user.medium][1x]', 'medium');
+ $this->assertFieldByName('mappings[custom.user.medium][2x]', '');
+ $this->assertFieldByName('mappings[custom.user.large][1x]', 'large');
+ $this->assertFieldByName('mappings[custom.user.large][2x]', '');
+
+ // Delete the mapping.
+ $this->drupalGet('admin/config/media/picturemapping/mapping_one/delete');
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->drupalGet('admin/config/media/picturemapping');
+ $this->assertText('There is no Picture mapping yet.');
+ }
+
+}
diff --git a/core/modules/picture/lib/Drupal/picture/Tests/PictureFieldDisplayTest.php b/core/modules/picture/lib/Drupal/picture/Tests/PictureFieldDisplayTest.php
new file mode 100644
index 0000000..96324bb
--- /dev/null
+++ b/core/modules/picture/lib/Drupal/picture/Tests/PictureFieldDisplayTest.php
@@ -0,0 +1,190 @@
+ 'Picture field display tests',
+ 'description' => 'Test picture display formatter.',
+ 'group' => 'Picture',
+ );
+ }
+
+ /**
+ * Drupal\simpletest\WebTestBase\setUp().
+ */
+ public function setUp() {
+ parent::setUp();
+
+ // Create user.
+ $this->admin_user = $this->drupalCreateUser(array('administer pictures', 'access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles'));
+ $this->drupalLogin($this->admin_user);
+
+ // Add breakpoint_group and breakpoints.
+ $breakpoint_group = entity_create('breakpoint_group', array(
+ 'id' => 'atestset',
+ 'label' => 'A test set',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_USER_DEFINED,
+ ));
+
+ $breakpoints = array();
+ $breakpoint_names = array('small', 'medium', 'large');
+ for ($i = 0; $i < 3; $i++) {
+ $width = ($i + 1) * 200;
+ $breakpoint = entity_create('breakpoint', array(
+ 'name' => $breakpoint_names[$i],
+ 'mediaQuery' => "(min-width: {$width}px)",
+ 'source' => 'user',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_USER_DEFINED,
+ 'multipliers' => array(
+ '1.5x' => 0,
+ '2x' => '2x',
+ ),
+ ));
+ $breakpoint->save();
+ $breakpoint_group->breakpoints[$breakpoint->id()] = $breakpoint;
+ }
+ $breakpoint_group->save();
+
+ // Add picture mapping.
+ $picture_mapping = entity_create('picture_mapping', array(
+ 'id' => 'mapping_one',
+ 'label' => 'Mapping One',
+ 'breakpointGroup' => 'atestset',
+ ));
+ $picture_mapping->save();
+ $picture_mapping->mappings['custom.user.small']['1x'] = 'thumbnail';
+ $picture_mapping->mappings['custom.user.medium']['1x'] = 'medium';
+ $picture_mapping->mappings['custom.user.large']['1x'] = 'large';
+ $picture_mapping->save();
+ }
+
+ /**
+ * Test picture formatters on node display for public files.
+ */
+ public function testPictureFieldFormattersPublic() {
+ $this->_testPictureFieldFormatters('public');
+ }
+
+ /**
+ * Test picture formatters on node display for private files.
+ */
+ public function testPictureFieldFormattersPrivate() {
+ // Remove access content permission from anonymous users.
+ user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access content' => FALSE));
+ $this->_testPictureFieldFormatters('private');
+ }
+
+ /**
+ * Test picture formatters on node display.
+ */
+ public function _testPictureFieldFormatters($scheme) {
+ $field_name = drupal_strtolower($this->randomName());
+ $this->createImageField($field_name, 'article', array('uri_scheme' => $scheme));
+ // Create a new node with an image attached.
+ $test_image = current($this->drupalGetTestFiles('image'));
+ $nid = $this->uploadNodeImage($test_image, $field_name, 'article');
+ $node = node_load($nid, TRUE);
+
+ // Use the picture formatter.
+ $instance = field_info_instance('node', $field_name, 'article');
+ $instance['display']['default']['type'] = 'picture';
+ $instance['display']['default']['module'] = 'picture';
+
+ // Test that the default formatter is being used.
+ $image_uri = file_load($node->{$field_name}[LANGUAGE_NOT_SPECIFIED][0]['fid'])->uri;
+ $image_info = array(
+ 'uri' => $image_uri,
+ 'width' => 40,
+ 'height' => 20,
+ );
+ $default_output = theme('image', $image_info);
+ $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.');
+
+ // Use the picture formatter linked to file formatter.
+ $instance = field_info_instance('node', $field_name, 'article');
+ $instance['display']['default']['type'] = 'picture';
+ $instance['display']['default']['module'] = 'picture';
+ $instance['display']['default']['settings']['image_link'] = 'file';
+ field_update_instance($instance);
+ $default_output = l(theme('image', $image_info), file_create_url($image_uri), array('html' => TRUE));
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.');
+ // Verify that the image can be downloaded.
+ $this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.');
+ if ($scheme == 'private') {
+ // Only verify HTTP headers when using private scheme and the headers are
+ // sent by Drupal.
+ $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png', 'Content-Type header was sent.');
+ $this->assertEqual($this->drupalGetHeader('Content-Disposition'), 'inline; filename="' . $test_image->filename . '"', 'Content-Disposition header was sent.');
+ $this->assertTrue(strstr($this->drupalGetHeader('Cache-Control'), 'private') !== FALSE, 'Cache-Control header was sent.');
+
+ // Log out and try to access the file.
+ $this->drupalLogout();
+ $this->drupalGet(file_create_url($image_uri));
+ $this->assertResponse('403', 'Access denied to original image as anonymous user.');
+
+ // Log in again.
+ $this->drupalLogin($this->admin_user);
+ }
+
+ // Use the picture formatter with a picture mapping.
+ $instance['display']['default']['settings']['picture_mapping'] = 'mapping_one';
+ field_update_instance($instance);
+ // Output should contain all image styles and all breakpoints.
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw('/styles/thumbnail/');
+ $this->assertRaw('/styles/medium/');
+ $this->assertRaw('/styles/large/');
+ $this->assertRaw('media="(min-width: 200px)"');
+ $this->assertRaw('media="(min-width: 400px)"');
+ $this->assertRaw('media="(min-width: 600px)"');
+
+ // Test the fallback image style.
+ $instance['display']['default']['settings']['image_link'] = '';
+ $instance['display']['default']['settings']['fallback_image_style'] = 'large';
+ field_update_instance($instance);
+
+ $this->drupalGet(image_style_url('large', $image_uri));
+ $image_info['uri'] = $image_uri;
+ $image_info['width'] = 480;
+ $image_info['height'] = 240;
+ $image_info['style_name'] = 'large';
+ $default_output = '';
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.');
+
+ if ($scheme == 'private') {
+ // Log out and try to access the file.
+ $this->drupalLogout();
+ $this->drupalGet(image_style_url('large', $image_uri));
+ $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.');
+ }
+ }
+
+}
diff --git a/core/modules/picture/picture.info b/core/modules/picture/picture.info
new file mode 100644
index 0000000..3113a7f
--- /dev/null
+++ b/core/modules/picture/picture.info
@@ -0,0 +1,9 @@
+name = Picture
+description = Picture element
+package = Core
+version = VERSION
+core = 8.x
+dependencies[] = breakpoint
+dependencies[] = config
+dependencies[] = image
+configure = admin/config/media/picturemapping
diff --git a/core/modules/picture/picture.module b/core/modules/picture/picture.module
new file mode 100644
index 0000000..4816862
--- /dev/null
+++ b/core/modules/picture/picture.module
@@ -0,0 +1,355 @@
+' . t('About') . '';
+ $output .= '
' . t('The picture module allows you to output images using the new HTML5 picture tag.') . '
'; + $output .= '' . t('On this screen you can manage and create picture mappings.') . '
'; + break; + case 'admin/config/media/picturemapping/add': + $output .= '' . t('Create a new picture mapping by specifying a name and selecting a breakpoint group.') . '
'; + break; + case 'admin/config/media/picturemapping/%/edit': + $output .= '' . t('For each breakpoint you can select a corresponding image style, you have to select at least on image style.') . '
'; + $output .= '' . t("Warning: if you change the breakpoint group you lose all you're selected mappings.") . '
'; + break; + + } + return $output; +} + +/** + * Implements hook_permission(). + */ +function picture_permission() { + return array( + 'administer pictures' => array( + 'title' => t('Administer Pictures'), + 'description' => t('Administer Pictures'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function picture_menu() { + $items = array(); + + $items['admin/config/media/picturemapping'] = array( + 'title' => 'Picture Mappings', + 'description' => 'Manage picture mappings', + 'access arguments' => array('administer pictures'), + 'weight' => 10, + 'page callback' => 'picture_mapping_page', + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/add'] = array( + 'title' => 'Add picture mapping', + 'page callback' => 'picture_mapping_page_add', + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'type' => MENU_LOCAL_ACTION, + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/%picture_mapping/edit'] = array( + 'title' => 'Edit picture mapping', + 'page callback' => 'picture_mapping_page_edit', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/%picture_mapping/duplicate'] = array( + 'title' => 'Duplicate picture mapping', + 'page callback' => 'picture_mapping_page_duplicate', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/%picture_mapping/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('picture_mapping_action_confirm', 4, 5), + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'file' => 'picture_mapping.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_entity_info(). + */ +function picture_entity_info() { + $types['picture_mapping'] = array( + 'label' => 'Picture mapping', + 'entity class' => 'Drupal\picture\PictureMapping', + 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController', + 'config prefix' => 'picture.mappings', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + 'form controller class' => array( + 'default' => 'Drupal\picture\PictureMappingFormController', + 'add' => 'Drupal\picture\PictureMappingFormController', + 'duplicate' => 'Drupal\picture\PictureMappingFormController', + ), + 'list controller class' => 'Drupal\picture\PictureMappingListController', + 'list path' => 'admin/config/media/picturemapping', + 'uri callback' => 'picture_mapping_uri', + ); + + return $types; +} + +/** + * Implements hook_library_info(). + */ +function picture_library_info() { + $libraries['picturefill'] = array( + 'title' => t('Picturefill'), + 'website' => 'https://github.com/attiks/picturefill-proposal', + 'version' => '0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/picturefill/picturefill.js' => array('type' => 'file', 'weight' => -10, 'group' => JS_DEFAULT), + ), + 'dependencies' => array( + array('system', 'matchmedia'), + ), + ); + return $libraries; +} + +/** + * Load one picture by its identifier. + * + * @param int $id + * The id of the picture mapping to load. + * + * @return Drupal\picture\Picture + * The entity object, or FALSE if there is no entity with the given id. + * + * @todo Needed for menu_callback + * + * @see http://drupal.org/node/1798214 + * + */ +function picture_mapping_load($id) { + return entity_load('picture_mapping', $id); +} + +/** + * Picture uri callback. + */ +function picture_mapping_uri(PictureMapping $picture_mapping) { + return array( + 'path' => 'admin/config/media/picturemapping/' . $picture_mapping->id(), + ); +} + +/** + * Picture uri callback. + */ +function picture_mapping_set_uri(PictureMapping $picture_mapping) { + return array( + 'path' => 'admin/config/media/picturemapping/' . $picture_mapping->id(), + ); +} + +/** + * Implements hook_theme(). + */ +function picture_theme() { + return array( + 'picture' => array( + 'variables' => array( + 'style_name' => NULL, + 'path' => NULL, + 'width' => NULL, + 'height' => NULL, + 'alt' => '', + 'title' => NULL, + 'attributes' => array(), + 'breakpoints' => array(), + ), + ), + 'picture_formatter' => array( + 'variables' => array( + 'item' => NULL, + 'path' => NULL, + 'image_style' => NULL, + 'breakpoints' => array(), + ), + ), + 'picture_source' => array( + 'variables' => array( + 'src' => NULL, + 'srcset' => NULL, + 'dimension' => NULL, + 'media' => NULL, + ), + ), + ); +} + +function theme_picture_formatter($variables) { + if (!isset($variables['breakpoints']) || empty($variables['breakpoints'])) { + return theme('image_formatter', $variables); + } + + $item = $variables['item']; + + // Do not output an empty 'title' attribute. + if (isset($item['title']) && drupal_strlen($item['title']) == 0) { + unset($item['title']); + } + + $item['style_name'] = $variables['image_style']; + $item['breakpoints'] = $variables['breakpoints']; + + if (!isset($item['path']) && isset($variables['uri'])) { + $item['path'] = $variables['uri']; + } + $output = theme('picture', $item); + + if (isset($variables['path']['path'])) { + $path = $variables['path']['path']; + $options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); + $options['html'] = TRUE; + $output = l($output, $path, $options); + } + return $output; +} + +/** + * Theme a picture element. + */ +function theme_picture($variables) { + // Make sure that width and height are proper values + // If they exists we'll output them + // @see http://www.w3.org/community/respimg/2012/06/18/florians-compromise/ + if (isset($variables['width']) && empty($variables['width'])) { + unset($variables['width']); + unset($variables['height']); + } + elseif (isset($variables['height']) && empty($variables['height'])) { + unset($variables['width']); + unset($variables['height']); + } + + $sources = array(); + $output = array(); + + // Fallback image, output as source with media query. + $sources[] = array( + 'src' => image_style_url($variables['style_name'], $variables['uri']), + 'dimensions' => picture_get_image_dimensions($variables), + ); + + // All breakpoints and multipliers. + foreach ($variables['breakpoints'] as $breakpoint_name => $multipliers) { + $breakpoint = breakpoint_load($breakpoint_name); + if ($breakpoint) { + $new_sources = array(); + foreach ($multipliers as $multiplier => $image_style) { + $new_source = $variables; + $new_source['style_name'] = $image_style; + $new_source['#multiplier'] = $multiplier; + $new_sources[] = $new_source; + } + + // Only one image, use src. + if (count($new_sources) == 1) { + $sources[] = array( + 'src' => image_style_url($new_sources[0]['style_name'], $new_sources[0]['uri']), + 'dimensions' => picture_get_image_dimensions($new_sources[0]), + 'media' => $breakpoint->mediaQuery, + ); + } + else { + // Mutliple images, use srcset. + $srcset = array(); + foreach ($new_sources as $new_source) { + $srcset[] = image_style_url($new_source['style_name'], $new_source['uri']) . ' ' . $new_source['#multiplier']; + } + $sources[] = array( + 'srcset' => implode(', ', $srcset), + 'dimensions' => picture_get_image_dimensions($new_sources[0]), + 'media' => $breakpoint->mediaQuery, + ); + } + } + } + + if (!empty($sources)) { + $attributes = array(); + foreach (array('alt', 'title') as $key) { + if (isset($variables[$key])) { + $attributes[$key] = $variables[$key]; + } + } + $output[] = ''; + return implode("\n", $output); + } +} + +function theme_picture_source($variables) { + $output = array(); + if (isset($variables['media']) && !empty($variables['media'])) { + if (!isset($variables['srcset'])) { + $output[] = ''; + $output[] = ''; + } + elseif (!isset($variables['src'])) { + $output[] = ''; + $output[] = ''; + } + } + else { + $output[] = ''; + $output[] = ''; + } + return implode("\n", $output); +} + +function picture_get_image_dimensions($variables) { + // Determine the dimensions of the styled image. + $dimensions = array( + 'width' => $variables['width'], + 'height' => $variables['height'], + ); + + image_style_transform_dimensions($variables['style_name'], $dimensions); + + return $dimensions; +} diff --git a/core/modules/picture/picture_mapping.admin.inc b/core/modules/picture/picture_mapping.admin.inc new file mode 100644 index 0000000..42e85b1 --- /dev/null +++ b/core/modules/picture/picture_mapping.admin.inc @@ -0,0 +1,108 @@ +render(); +} + +/** + * Page callback: Presents the picture mapping editing form. + * + * @param Drupal\picture\PictureMapping $picture_mapping + * + * @return + * A render array for a page containing a list of content. + * + * @see picture_menu() + */ +function picture_mapping_page_edit($picture_mapping) { + drupal_set_title(t('Edit picture mapping @label', array('@label' => $picture_mapping->label())), PASS_THROUGH); + return entity_get_form($picture_mapping); +} + +/** + * Page callback: Presents the picture mapping duplicate form. + * + * @param Drupal\picture\PictureMapping $picture_mapping + * + * @return + * A render array for a page containing a list of content. + * + * @see picture_menu() + */ +function picture_mapping_page_duplicate($picture_mapping) { + drupal_set_title(t('Duplicate picture mapping @label', array('@label' => $picture_mapping->label())), PASS_THROUGH); + return entity_get_form($picture_mapping->createDuplicate()); +} + +/** + * Page callback: Provides the new picture mapping addition form. + * + * @return + * A render array for a page containing a list of content. + * + * @see picture_menu() + */ +function picture_mapping_page_add() { + $picture_mapping = entity_create('picture_mapping', array()); + $form = entity_get_form($picture_mapping); + return $form; +} + +/** + * Page callback: Form constructor for picture action confirmation form. + * + * @param Drupal\picture\PictureMapping $picture_mapping + * @param string $action + * + * @see picture_menu() + */ +function picture_mapping_action_confirm($form, &$form_state, $picture_mapping, $action) { + // Always provide entity id in the same form key as in the entity edit form. + if (in_array($action, array('delete'))) { + $form['id'] = array('#type' => 'value', '#value' => $picture_mapping->id()); + $form['action'] = array('#type' => 'value', '#value' => $action); + $form_state['picture_mapping'] = $picture_mapping; + $form = confirm_form($form, + t('Are you sure you want to @action the picture_mapping %title?', array('@action' => $action, '%title' => $picture_mapping->label())), + 'admin/config/media/picturemapping', + $action == 'delete' ? t('This action cannot be undone.') : '', + t(drupal_ucfirst($action)), + t('Cancel') + ); + } + return $form; +} + +/** + * Form submission handler for picture_action_confirm(). + */ +function picture_mapping_action_confirm_submit($form, &$form_state) { + $picture_mapping = $form_state['picture_mapping']; + $action = $form_state['values']['action']; + $picture_mapping->{$action}(); + $verb = ''; + switch ($action) { + case 'delete': + $verb = 'deleted'; + break; + } + drupal_set_message(t('Picture mapping %label has been @action.', array('%label' => $picture_mapping->label(), '@action' => $verb))); + watchdog('picture', 'Picture mapping %label has been @action.', array('%label' => $picture_mapping->label(), '@action' => $verb), WATCHDOG_NOTICE); + $form_state['redirect'] = 'admin/config/media/picturemapping'; +} diff --git a/core/modules/picture/picturefill/picturefill.js b/core/modules/picture/picturefill/picturefill.js new file mode 100644 index 0000000..8e4c231 --- /dev/null +++ b/core/modules/picture/picturefill/picturefill.js @@ -0,0 +1,126 @@ +/*jshint loopfunc: true, browser: true, curly: true, eqeqeq: true, expr: true, forin: true, latedef: true, newcap: true, noarg: true, trailing: true, undef: true, unused: true */ +/*! Picturefill - Author: Scott Jehl, 2012 | License: MIT/GPLv2 */ +(function( w ){ + + // Enable strict mode. + "use strict"; + + // Test if `