From b98a997f100a8b5388a1cdfff640ad8d1b281eff Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Sat, 2 Nov 2024 22:46:14 +0000
Subject: [PATCH] Issue #2350849 by mondrake, fietserwin, ankithashetty,
 jhedstrom: Deprecate image_filter_keyword()

(cherry picked from commit fa4cdaf3f26a829d4a403946948b42b727c3f5f7)
---
 core/lib/Drupal/Component/Utility/Image.php   | 25 ++++++
 core/modules/image/image.module               |  6 ++
 .../Plugin/ImageEffect/CropImageEffect.php    |  5 +-
 .../ImageEffect/ScaleAndCropImageEffect.php   |  9 ++-
 .../src/Functional/ImageFieldTestBase.php     |  1 -
 .../tests/src/Kernel/ImageEffectsTest.php     | 10 +--
 .../tests/src/Unit/ImageDeprecationTest.php   | 24 ++++++
 .../Tests/Component/Utility/ImageTest.php     | 80 +++++++++++++++++++
 8 files changed, 148 insertions(+), 12 deletions(-)
 create mode 100644 core/modules/image/tests/src/Unit/ImageDeprecationTest.php

diff --git a/core/lib/Drupal/Component/Utility/Image.php b/core/lib/Drupal/Component/Utility/Image.php
index f1368c1b8390..d955644b5c99 100644
--- a/core/lib/Drupal/Component/Utility/Image.php
+++ b/core/lib/Drupal/Component/Utility/Image.php
@@ -56,4 +56,29 @@ public static function scaleDimensions(array &$dimensions, $width = NULL, $heigh
     return TRUE;
   }
 
+  /**
+   * Returns the offset in pixels from the anchor.
+   *
+   * @param string $anchor
+   *   The anchor ('top', 'left', 'bottom', 'right', 'center').
+   * @param int $current_size
+   *   The current size, in pixels.
+   * @param int $new_size
+   *   The new size, in pixels.
+   *
+   * @return int
+   *   The offset from the anchor, in pixels.
+   *
+   * @throws \InvalidArgumentException
+   *   When the $anchor argument is not valid.
+   */
+  public static function getKeywordOffset(string $anchor, int $current_size, int $new_size): int {
+    return match ($anchor) {
+      'bottom', 'right' => $current_size - $new_size,
+      'center' => (int) round($current_size / 2 - $new_size / 2),
+      'top', 'left' => 0,
+      default => throw new \InvalidArgumentException("Invalid anchor '{$anchor}' provided to getKeywordOffset()"),
+    };
+  }
+
 }
diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 6807f2c02b76..864be56cf41a 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -336,8 +336,14 @@ function template_preprocess_image_style(&$variables) {
  * @return int|string
  *   The offset from the anchor, in pixels, or the anchor itself, if its value
  *   isn't one of the accepted values.
+ *
+ * @deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Use
+ *   \Drupal\Component\Utility\Image::getKeywordOffset() instead.
+ *
+ * @see https://www.drupal.org/node/3268441
  */
 function image_filter_keyword($anchor, $current_size, $new_size) {
+  @trigger_error('image_filter_keyword() is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Use \Drupal\Component\Utility\Image::getKeywordOffset() instead. See https://www.drupal.org/node/3268441', E_USER_DEPRECATED);
   switch ($anchor) {
     case 'top':
     case 'left':
diff --git a/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php
index accdfe17b88f..7e31430e9c9e 100644
--- a/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php
+++ b/core/modules/image/src/Plugin/ImageEffect/CropImageEffect.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\image\Plugin\ImageEffect;
 
+use Drupal\Component\Utility\Image;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Image\ImageInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -22,8 +23,8 @@ class CropImageEffect extends ResizeImageEffect {
    */
   public function applyEffect(ImageInterface $image) {
     [$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']);
+    $x = Image::getKeywordOffset($x, $image->getWidth(), (int) $this->configuration['width']);
+    $y = Image::getKeywordOffset($y, $image->getHeight(), (int) $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;
diff --git a/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php b/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php
index 73489d7aceca..7ce4a2038859 100644
--- a/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php
+++ b/core/modules/image/src/Plugin/ImageEffect/ScaleAndCropImageEffect.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\image\Plugin\ImageEffect;
 
+use Drupal\Component\Utility\Image;
 use Drupal\Core\Image\ImageInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\image\Attribute\ImageEffect;
@@ -20,13 +21,13 @@ class ScaleAndCropImageEffect extends CropImageEffect {
    * {@inheritdoc}
    */
   public function applyEffect(ImageInterface $image) {
-    $width = $this->configuration['width'];
-    $height = $this->configuration['height'];
+    $width = (int) $this->configuration['width'];
+    $height = (int) $this->configuration['height'];
     $scale = max($width / $image->getWidth(), $height / $image->getHeight());
 
     [$x, $y] = explode('-', $this->configuration['anchor']);
-    $x = image_filter_keyword($x, $image->getWidth() * $scale, $width);
-    $y = image_filter_keyword($y, $image->getHeight() * $scale, $height);
+    $x = Image::getKeywordOffset($x, (int) round($image->getWidth() * $scale), $width);
+    $y = Image::getKeywordOffset($y, (int) round($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()]);
diff --git a/core/modules/image/tests/src/Functional/ImageFieldTestBase.php b/core/modules/image/tests/src/Functional/ImageFieldTestBase.php
index afbf29003fe5..da98d02866a2 100644
--- a/core/modules/image/tests/src/Functional/ImageFieldTestBase.php
+++ b/core/modules/image/tests/src/Functional/ImageFieldTestBase.php
@@ -18,7 +18,6 @@
  * - image.module:
  *   image_style_options()
  *   \Drupal\image\ImageStyleInterface::flush()
- *   image_filter_keyword()
  */
 
 /**
diff --git a/core/modules/image/tests/src/Kernel/ImageEffectsTest.php b/core/modules/image/tests/src/Kernel/ImageEffectsTest.php
index 6b7662e33e43..1e5c75339225 100644
--- a/core/modules/image/tests/src/Kernel/ImageEffectsTest.php
+++ b/core/modules/image/tests/src/Kernel/ImageEffectsTest.php
@@ -87,7 +87,7 @@ public function testCropEffect(): void {
     // @todo Test also keyword offsets in #3040887.
     // @see https://www.drupal.org/project/drupal/issues/3040887
     $this->assertImageEffect(['crop'], 'image_crop', [
-      'anchor' => 'top-1',
+      'anchor' => 'top-left',
       'width' => 3,
       'height' => 4,
     ]);
@@ -97,7 +97,7 @@ public function testCropEffect(): void {
     // X was passed correctly.
     $this->assertEquals(0, $calls['crop'][0][0]);
     // Y was passed correctly.
-    $this->assertEquals(1, $calls['crop'][0][1]);
+    $this->assertEquals(0, $calls['crop'][0][1]);
     // Width was passed correctly.
     $this->assertEquals(3, $calls['crop'][0][2]);
     // Height was passed correctly.
@@ -131,7 +131,7 @@ public function testScaleAndCropEffect(): void {
     // Check the parameters.
     $calls = $this->imageTestGetAllCalls();
     // X was computed and passed correctly.
-    $this->assertEquals(7.5, $calls['scale_and_crop'][0][0]);
+    $this->assertEquals(8, $calls['scale_and_crop'][0][0]);
     // Y was computed and passed correctly.
     $this->assertEquals(0, $calls['scale_and_crop'][0][1]);
     // Width was computed and passed correctly.
@@ -145,7 +145,7 @@ public function testScaleAndCropEffect(): void {
    */
   public function testScaleAndCropEffectWithAnchor(): void {
     $this->assertImageEffect(['scale_and_crop'], 'image_scale_and_crop', [
-      'anchor' => 'top-1',
+      'anchor' => 'top-left',
       'width' => 5,
       'height' => 10,
     ]);
@@ -155,7 +155,7 @@ public function testScaleAndCropEffectWithAnchor(): void {
     // X was computed and passed correctly.
     $this->assertEquals(0, $calls['scale_and_crop'][0][0]);
     // Y was computed and passed correctly.
-    $this->assertEquals(1, $calls['scale_and_crop'][0][1]);
+    $this->assertEquals(0, $calls['scale_and_crop'][0][1]);
     // Width was computed and passed correctly.
     $this->assertEquals(5, $calls['scale_and_crop'][0][2]);
     // Height was computed and passed correctly.
diff --git a/core/modules/image/tests/src/Unit/ImageDeprecationTest.php b/core/modules/image/tests/src/Unit/ImageDeprecationTest.php
new file mode 100644
index 000000000000..255f34284351
--- /dev/null
+++ b/core/modules/image/tests/src/Unit/ImageDeprecationTest.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\image\Unit;
+
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @group Image
+ * @group legacy
+ */
+class ImageDeprecationTest extends UnitTestCase {
+
+  /**
+   * Tests deprecation of image_filter_keyword.
+   */
+  public function testImageFilterKeywordDeprecation(): void {
+    include_once __DIR__ . '/../../../image.module';
+    $this->expectDeprecation('image_filter_keyword() is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Use \Drupal\Component\Utility\Image::getKeywordOffset() instead. See https://www.drupal.org/node/3268441');
+    $this->assertSame('miss', image_filter_keyword('miss', 0, 0));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Utility/ImageTest.php b/core/tests/Drupal/Tests/Component/Utility/ImageTest.php
index 7d179e3145b3..e67eb9a4d163 100644
--- a/core/tests/Drupal/Tests/Component/Utility/ImageTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/ImageTest.php
@@ -157,4 +157,84 @@ public static function providerTestScaleDimensions() {
     return $tests;
   }
 
+  /**
+   * @covers ::getKeywordOffset
+   */
+  public function testInvalidGetKeywordOffset(): void {
+    $this->expectException(\InvalidArgumentException::class);
+    $this->expectExceptionMessage('Invalid anchor \'foo\' provided to getKeywordOffset()');
+    Image::getKeywordOffset('foo', 0, 0);
+  }
+
+  /**
+   * @covers ::getKeywordOffset
+   *
+   * @dataProvider providerTestGetKeywordOffset
+   */
+  public function testGetKeywordOffset(array $input, int $expected): void {
+    $this->assertSame($expected, Image::getKeywordOffset($input['anchor'], $input['current'], $input['new']));
+  }
+
+  /**
+   * Provides data for testGetKeywordOffset().
+   *
+   * @return \Generator
+   *   Test scenarios.
+   *
+   * @see testGetKeywordOffset()
+   */
+  public static function providerTestGetKeywordOffset(): \Generator {
+    yield "'left' => return 0" => [
+      'input' => [
+        'anchor' => 'left',
+        'current' => 100,
+        'new' => 20,
+      ],
+      'expected' => 0,
+    ];
+    yield "'top' => return 0" => [
+      'input' => [
+        'anchor' => 'top',
+        'current' => 100,
+        'new' => 20,
+      ],
+      'expected' => 0,
+    ];
+
+    yield "'right' => return (current - new)" => [
+      'input' => [
+        'anchor' => 'right',
+        'current' => 100,
+        'new' => 20,
+      ],
+      'expected' => 80,
+    ];
+    yield "'bottom' => return (current - new)" => [
+      'input' => [
+        'anchor' => 'bottom',
+        'current' => 100,
+        'new' => 30,
+      ],
+      'expected' => 70,
+    ];
+
+    yield "a) 'center' => return (current - new)/2" => [
+      'input' => [
+        'anchor' => 'center',
+        'current' => 100,
+        'new' => 20,
+      ],
+      'expected' => 40,
+    ];
+    yield "b) 'center' => return (current - new)/2" => [
+      'input' => [
+        'anchor' => 'center',
+        'current' => 100,
+        'new' => 91,
+      ],
+      'expected' => 5,
+    ];
+
+  }
+
 }
-- 
GitLab