-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #254 from openeuropa/OPENEUROPA-2025
OPENEUROPA-2025: Use custom image effect to prevent retina image reducction.
- Loading branch information
Showing
7 changed files
with
300 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
modules/oe_theme_helper/config/schema/oe_theme_helper.schema.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
image.effect.retina_image_scale: | ||
type: image.effect.image_scale | ||
label: 'Retina Image scale' | ||
mapping: | ||
multiplier: | ||
type: integer | ||
label: 'Multiplier' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* OpenEuropa theme helper post updates. | ||
*/ | ||
|
||
declare(strict_types = 1); | ||
|
||
use Drupal\image\Entity\ImageStyle; | ||
|
||
/** | ||
* Use retina image styles on medium and small image styles. | ||
*/ | ||
function oe_theme_helper_post_update_use_retina_image_styles(array &$sandbox): void { | ||
\Drupal::service('plugin.manager.image.effect')->clearCachedDefinitions(); | ||
|
||
$image_styles = [ | ||
'oe_theme_medium_2x_no_crop', | ||
'oe_theme_small_2x_no_crop', | ||
]; | ||
|
||
foreach ($image_styles as $image_style_name) { | ||
$image_style = ImageStyle::load($image_style_name); | ||
$effects = $image_style->getEffects(); | ||
/** @var \Drupal\image\ImageEffectInterface $effect */ | ||
foreach ($effects as $effect) { | ||
if ($effect->getPluginId() == 'image_scale') { | ||
$image_style->deleteImageEffect($effect); | ||
$configuration = $effect->getConfiguration(); | ||
$new_configuration = [ | ||
'id' => 'retina_image_scale', | ||
'data' => $configuration['data'], | ||
'weight' => $configuration['weight'], | ||
]; | ||
$new_configuration['data']['multiplier'] = 2; | ||
$image_style->addImageEffect($new_configuration); | ||
$image_style->save(); | ||
break; | ||
} | ||
} | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
modules/oe_theme_helper/src/Plugin/ImageEffect/RetinaScaleImageEffect.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace Drupal\oe_theme_helper\Plugin\ImageEffect; | ||
|
||
use Drupal\Component\Utility\Image; | ||
use Drupal\Core\Form\FormStateInterface; | ||
use Drupal\Core\Image\ImageInterface; | ||
use Drupal\image\Plugin\ImageEffect\ScaleImageEffect; | ||
|
||
/** | ||
* Scales an image and upscales it for retina screens if needed. | ||
* | ||
* @ImageEffect( | ||
* id = "retina_image_scale", | ||
* label = @Translation("Retina Image 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. If the image is smaller than the specified dimensions, it will be upscaled according to the multiplier value.") | ||
* ) | ||
*/ | ||
class RetinaScaleImageEffect extends ScaleImageEffect { | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function applyEffect(ImageInterface $image) { | ||
// If we are not upscaling the image, check to see if it's smaller | ||
// than the defined dimensions. | ||
$upscale = $this->configuration['upscale']; | ||
$target_width = $this->configuration['width']; | ||
$target_height = $this->configuration['height']; | ||
if (!$upscale) { | ||
if ((!empty($target_width) && $target_width > $image->getWidth()) || (!empty($target_height) && $target_height > $image->getHeight())) { | ||
// If the image is smaller than the defined dimensions, | ||
// upscale it according to the defined multiplier. | ||
$target_width = $image->getWidth() * $this->configuration['multiplier']; | ||
$target_height = $image->getHeight() * $this->configuration['multiplier']; | ||
$upscale = TRUE; | ||
} | ||
} | ||
|
||
if (!$image->scale($target_width, $target_height, $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']) { | ||
return; | ||
} | ||
|
||
// If we are not upscaling the image, check to see if it's smaller | ||
// than the defined dimensions. | ||
$upscale = $this->configuration['upscale']; | ||
$target_width = $this->configuration['width']; | ||
$target_height = $this->configuration['height']; | ||
|
||
if (!$upscale) { | ||
if ((!empty($target_width) && $this->configuration['width'] > $dimensions['width']) || (!empty($target_height) && $this->configuration['height'] > $dimensions['height'])) { | ||
// If the image is smaller than the defined dimensions, | ||
// upscale it according to the defined multiplier. | ||
$target_width = $dimensions['width'] * $this->configuration['multiplier']; | ||
$target_height = $dimensions['height'] * $this->configuration['multiplier']; | ||
$upscale = TRUE; | ||
} | ||
} | ||
|
||
Image::scaleDimensions($dimensions, $target_width, $target_height, $upscale); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getSummary() { | ||
// Since we are extending the image scale effect and not altering it in any | ||
// major way, we use the same theme. | ||
$summary = [ | ||
'#theme' => 'image_scale_summary', | ||
'#data' => $this->configuration, | ||
]; | ||
$summary += parent::getSummary(); | ||
|
||
return $summary; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function defaultConfiguration() { | ||
return parent::defaultConfiguration() + [ | ||
'multiplier' => 2, | ||
]; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function buildConfigurationForm(array $form, FormStateInterface $form_state) { | ||
$form = parent::buildConfigurationForm($form, $form_state); | ||
$form['multiplier'] = [ | ||
'#type' => 'select', | ||
'#title' => $this->t('Multiplier'), | ||
'#options' => [ | ||
2 => '2x', | ||
3 => '3x', | ||
], | ||
'#default_value' => $this->configuration['multiplier'], | ||
'#required' => TRUE, | ||
'#description' => $this->t('The image will be upscaled (regardless of the value of the "Upscale" option) according to this multiplier if it is smaller than the dimensions defined in the "Width" and "Height" properties.'), | ||
]; | ||
|
||
return $form; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { | ||
parent::submitConfigurationForm($form, $form_state); | ||
$this->configuration['multiplier'] = $form_state->getValue('multiplier'); | ||
} | ||
|
||
} |
110 changes: 110 additions & 0 deletions
110
modules/oe_theme_helper/tests/src/Functional/RetinaScaleEffectTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
<?php | ||
|
||
declare(strict_types = 1); | ||
|
||
namespace Drupal\Tests\oe_theme_helper\Functional; | ||
|
||
use Drupal\FunctionalTests\Image\ToolkitTestBase; | ||
|
||
/** | ||
* Tests that the Retina Scale effect upscales images appropriately. | ||
* | ||
* @group image | ||
*/ | ||
class RetinaScaleEffectTest extends ToolkitTestBase { | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public static $modules = ['image', 'oe_theme_helper']; | ||
|
||
/** | ||
* The image effect manager. | ||
* | ||
* @var \Drupal\image\ImageEffectManager | ||
*/ | ||
protected $manager; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function setUp() { | ||
parent::setUp(); | ||
$this->manager = $this->container->get('plugin.manager.image.effect'); | ||
} | ||
|
||
/** | ||
* Test the retina scale effect using a big enough image. | ||
*/ | ||
public function testRetinaScaleEffect(): void { | ||
$this->assertImageEffect('retina_image_scale', [ | ||
// Set the desired width to be smaller than the image width. | ||
'width' => 10, | ||
'height' => 10, | ||
]); | ||
$this->assertToolkitOperationsCalled(['scale']); | ||
|
||
$calls = $this->imageTestGetAllCalls(); | ||
$this->assertEqual($calls['scale'][0][0], 10, 'Width was passed correctly'); | ||
$this->assertEqual($calls['scale'][0][1], 10, 'Height was based off aspect ratio and passed correctly'); | ||
} | ||
|
||
/** | ||
* Test the retina scale effect using upscaling. | ||
*/ | ||
public function testScaleEffectDefaultUpscaling(): void { | ||
$this->assertImageEffect('retina_image_scale', [ | ||
// Set the desired width to be higher than the image width. | ||
'width' => $this->image->getWidth() * 4, | ||
'upscale' => TRUE, | ||
]); | ||
$this->assertToolkitOperationsCalled(['scale']); | ||
|
||
$calls = $this->imageTestGetAllCalls(); | ||
$this->assertEqual($calls['scale'][0][0], $this->image->getWidth() * 4, 'Width was passed correctly'); | ||
} | ||
|
||
/** | ||
* Test the retina scale effect using an image smaller than desired. | ||
*/ | ||
public function testRetinaScaleEffectForcedUpscaling(): void { | ||
$this->assertImageEffect('retina_image_scale', [ | ||
// Set the desired width to be higher than the image width. | ||
'width' => $this->image->getWidth() * 10, | ||
]); | ||
$this->assertToolkitOperationsCalled(['scale']); | ||
|
||
$calls = $this->imageTestGetAllCalls(); | ||
$this->assertEqual($calls['scale'][0][0], $this->image->getWidth() * 2, 'Width is double the original size.'); | ||
} | ||
|
||
/** | ||
* Test the retina scale effect using a multiplier of 3. | ||
*/ | ||
public function testTripleMultiplierRetinaScaleEffect(): void { | ||
$this->assertImageEffect('retina_image_scale', [ | ||
// Set the desired width to be higher than the image width. | ||
'width' => $this->image->getWidth() * 10, | ||
'multiplier' => 3, | ||
]); | ||
$this->assertToolkitOperationsCalled(['scale']); | ||
|
||
$calls = $this->imageTestGetAllCalls(); | ||
$this->assertEqual($calls['scale'][0][0], $this->image->getWidth() * 3, 'Width is triple the original size.'); | ||
} | ||
|
||
/** | ||
* Asserts the effect processing of an image effect plugin. | ||
* | ||
* @param string $effect_name | ||
* The name of the image effect to test. | ||
* @param array $data | ||
* The data to pass to the image effect. | ||
*/ | ||
protected function assertImageEffect($effect_name, array $data): void { | ||
/** @var \Drupal\image\ImageEffectInterface $effect */ | ||
$effect = $this->manager->createInstance($effect_name, ['data' => $data]); | ||
$this->assertTrue($effect->applyEffect($this->image), 'Function returned the expected value.'); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters