diff --git a/README.md b/README.md index 53f29d1..4f86b63 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Contrast | Supports changing contrast settings of an image. Also support Convolution | Allows to build custom image filters like blur, emboss, sharpen and others (see http://docs.gimp.org/en/plug-in-convmatrix.html). | X | IM only | ImageMagick arguments | Directly enter ImageMagick command line arguments. | | X | Interlace | Used to specify the type of interlacing scheme for raw image formats. | X | IM only | +Opacity | Change overall image transparency level. | X | IM only | Set canvas | Places the source image over a colored or a transparent background of a defined size. | X | IM only | Set transparent color | Defines the color to be used for transparency in GIF images. | X | IM only | Sharpen | Sharpens an image (using convolution). | X | IM only | diff --git a/config/schema/image_effects.schema.yml b/config/schema/image_effects.schema.yml index 8157985..2ad089e 100644 --- a/config/schema/image_effects.schema.yml +++ b/config/schema/image_effects.schema.yml @@ -166,6 +166,14 @@ image.effect.image_effects_imagemagick_arguments: type: string label: 'Height in px or %' +image.effect.image_effects_opacity: + type: mapping + label: 'Adjust image transparency level' + mapping: + opacity: + type: integer + label: 'Opacity % of the source image' + image.effect.image_effects_set_canvas: type: mapping label: 'Set canvas image effect' diff --git a/image_effects.module b/image_effects.module index 52ac175..271bfbd 100644 --- a/image_effects.module +++ b/image_effects.module @@ -43,6 +43,10 @@ function image_effects_theme() { 'image_effects_imagemagick_arguments_summary' => [ 'variables' => ['data' => NULL, 'effect' => []], ], + // Opacity image effect - summary + 'image_effects_opacity_summary' => [ + 'variables' => ['data' => NULL, 'effect' => []], + ], // Set canvas image effect - summary 'image_effects_set_canvas_summary' => [ 'variables' => ['data' => NULL, 'effect' => []], diff --git a/src/Plugin/ImageEffect/OpacityImageEffect.php b/src/Plugin/ImageEffect/OpacityImageEffect.php new file mode 100644 index 0000000..463549b --- /dev/null +++ b/src/Plugin/ImageEffect/OpacityImageEffect.php @@ -0,0 +1,72 @@ + 50, + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + return [ + '#theme' => 'image_effects_opacity_summary', + '#data' => $this->configuration, + ] + parent::getSummary(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['opacity'] = [ + '#type' => 'number', + '#title' => $this->t('Opacity'), + '#field_suffix' => '%', + '#description' => $this->t('Opacity: 0 - 100'), + '#default_value' => $this->configuration['opacity'], + '#min' => 0, + '#max' => 100, + '#maxlength' => 3, + '#size' => 3 + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['opacity'] = $form_state->getValue('opacity'); + } + + /** + * {@inheritdoc} + */ + public function applyEffect(ImageInterface $image) { + return $image->apply('opacity', ['opacity' => $this->configuration['opacity']]); + } + +} diff --git a/src/Plugin/ImageToolkit/Operation/OpacityTrait.php b/src/Plugin/ImageToolkit/Operation/OpacityTrait.php new file mode 100644 index 0000000..9dfab0c --- /dev/null +++ b/src/Plugin/ImageToolkit/Operation/OpacityTrait.php @@ -0,0 +1,34 @@ + [ + 'description' => 'Opacity.', + 'required' => FALSE, + 'default' => 100, + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function validateArguments(array $arguments) { + // Ensure opacity is in the range 0-100. + if (!is_numeric($arguments['opacity']) || $arguments['opacity'] > 100 || $arguments['opacity'] < 0) { + throw new \InvalidArgumentException("Invalid opacity ('{$arguments['opacity']}') specified for the image 'opacity' operation"); + } + return $arguments; + } + +} diff --git a/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php b/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php index 35f064a..fbb2f1a 100644 --- a/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php +++ b/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php @@ -221,4 +221,70 @@ protected function _imagettfbbox($size, $angle, $fontfile, $text) { } } + /** + * Change overall image transparency level. + * + * This method implements the algorithm described in + * http://php.net/manual/en/function.imagefilter.php#82162 + * + * @param resource $img + * Image resource id. + * @param int $pct + * Opacity of the source image in percentage. + * + * @return bool + * Returns TRUE on success or FALSE on failure. + * + * @see http://php.net/manual/en/function.imagefilter.php#82162 + */ + function filterOpacity($img, $pct) { + if (!isset($pct)) { + return false; + } + $pct /= 100; + + // Get image width and height. + $w = imagesx($img); + $h = imagesy($img); + + // Turn alpha blending off. + imagealphablending($img, FALSE); + + // Find the most opaque pixel in the image (the one with the smallest alpha + // value). + $min_alpha = 127; + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $alpha = (imagecolorat($img, $x, $y) >> 24) & 0xFF; + if ($alpha < $min_alpha) { + $min_alpha = $alpha; + } + } + } + + // Loop through image pixels and modify alpha for each. + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + // Get current alpha value (represents the TANSPARENCY!). + $color_xy = imagecolorat($img, $x, $y); + $alpha = ($color_xy >> 24) & 0xFF; + // Calculate new alpha. + if ($min_alpha !== 127) { + $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $min_alpha); + } + else { + $alpha += 127 * $pct; + } + // Get the color index with new alpha + $alpha_color_xy = imagecolorallocatealpha($img, ($color_xy >> 16) & 0xFF, ($color_xy >> 8) & 0xFF, $color_xy & 0xFF, $alpha); + // Set pixel with the new color + opacity. + if (!imagesetpixel($img, $x, $y, $alpha_color_xy)) { + return FALSE; + } + } + } + + return TRUE; + } + } diff --git a/src/Plugin/ImageToolkit/Operation/gd/Opacity.php b/src/Plugin/ImageToolkit/Operation/gd/Opacity.php new file mode 100644 index 0000000..9ccf605 --- /dev/null +++ b/src/Plugin/ImageToolkit/Operation/gd/Opacity.php @@ -0,0 +1,35 @@ +filterOpacity($this->getToolkit()->getResource(), $arguments['opacity']); + } + return TRUE; + } + +} diff --git a/src/Plugin/ImageToolkit/Operation/imagemagick/Opacity.php b/src/Plugin/ImageToolkit/Operation/imagemagick/Opacity.php new file mode 100644 index 0000000..df577ca --- /dev/null +++ b/src/Plugin/ImageToolkit/Operation/imagemagick/Opacity.php @@ -0,0 +1,55 @@ +getToolkit()->getPackage() === 'graphicsmagick') { + // GraphicsMagick does not support -alpha argument, return early. + // @todo implement a GraphicsMagick solution if possible. + return FALSE; + } + + switch ($arguments['opacity']) { + case 100: + // Fully opaque, leave image as-is. + break; + + case 0: + // Fully transparent, set full transparent for all pixels. + $this->getToolkit()->addArgument("-alpha set -channel Alpha -evaluate Set 0%"); + break; + + default: + // Divide existing alpha to the opacity needed. This preserves + // partially transparent images. + $divide = number_format((float) (100 / $arguments['opacity']), 4, '.', ','); + $this->getToolkit()->addArgument("-alpha set -channel Alpha -evaluate Divide {$divide}"); + break; + + } + + return TRUE; + } + +} diff --git a/src/Tests/ImageEffectsOpacityTest.php b/src/Tests/ImageEffectsOpacityTest.php new file mode 100644 index 0000000..846bd5a --- /dev/null +++ b/src/Tests/ImageEffectsOpacityTest.php @@ -0,0 +1,69 @@ +imagemagickPackages['graphicsmagick'] = FALSE; + } + + /** + * Opacity effect test. + */ + public function testOpacityEffect() { + // Test operations on toolkits. + $this->executeTestOnToolkits([$this, 'doTestOpacityOperations']); + } + + /** + * Opacity operations test. + */ + public function doTestOpacityOperations() { + // Test on the PNG test image. + $original_uri = $this->getTestImageCopyUri('/files/image-test.png', 'simpletest'); + + // Test data. + $test_data = [ + // No transparency change. + '100' => [$this->red, $this->green, $this->transparent, $this->blue], + // 50% transparency. + '50' => [[255, 0, 0, 63], [0, 255, 0, 63], $this->transparent, [0, 0, 255, 63]], + // 100% transparency. + '0' => [$this->transparent, $this->transparent, $this->transparent, $this->transparent], + ]; + + foreach ($test_data as $opacity => $colors) { + // Add Opacity effect to the test image style. + $effect = [ + 'id' => 'image_effects_opacity', + 'data' => [ + 'opacity' => $opacity, + ], + ]; + $uuid = $this->addEffectToTestStyle($effect); + + // Check that ::applyEffect generates image with expected opacity. + $derivative_uri = $this->testImageStyle->buildUri($original_uri); + $this->testImageStyle->createDerivative($original_uri, $derivative_uri); + $image = $this->imageFactory->get($derivative_uri, 'gd'); + $this->assertTrue($this->colorsAreEqual($colors[0], $this->getPixelColor($image, 0, 0))); + $this->assertTrue($this->colorsAreEqual($colors[1], $this->getPixelColor($image, 39, 0))); + $this->assertTrue($this->colorsAreEqual($colors[2], $this->getPixelColor($image, 0, 19))); + $this->assertTrue($this->colorsAreEqual($colors[3], $this->getPixelColor($image, 39, 19))); + + // Remove effect. + $uuid = $this->removeEffectFromTestStyle($uuid); + } + } +} diff --git a/templates/image-effects-opacity-summary.html.twig b/templates/image-effects-opacity-summary.html.twig new file mode 100644 index 0000000..0e05bcf --- /dev/null +++ b/templates/image-effects-opacity-summary.html.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Default theme implementation for a summary of an image opacity effect. + * + * Available variables: + * - data: The current configuration for this opacity effect, including: + * - opacity: The opacity of the image. + * - effect: The effect information, including: + * - id: The effect identifier. + * - label: The effect name. + * - description: The effect description. + * + * @ingroup themeable + */ +#} +{% spaceless %} + {{ data.opacity|e }}% +{% endspaceless %}