diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php
index 554ecd56738..0374e840ada 100644
--- a/core/modules/image/src/Controller/ImageStyleDownloadController.php
+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -167,19 +167,35 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
 
     $headers = [];
 
-    // Don't try to generate file if source is missing.
+    // Don't try to generate the image if the source is missing.
     if ($image_uri !== $sample_image_uri && !$this->sourceImageExists($image_uri, $token_is_valid)) {
-      // 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.
+      $source_found = FALSE;
+      // Legacy: handle old double-extension derivative filenames (e.g.
+      // image.png.webp) generated before addExtension() replaced extensions
+      // instead of appending them.
       $converted_image_uri = static::getUriWithoutConvertedExtension($image_uri);
       if ($converted_image_uri !== $image_uri &&
           $this->sourceImageExists($converted_image_uri, $token_is_valid)) {
-        // The converted file does exist, use it as the source.
         $image_uri = $converted_image_uri;
+        $source_found = TRUE;
       }
-      else {
+      if (!$source_found) {
+        // Current: derivative uses a single replaced extension (e.g.
+        // image.webp). Try swapping the extension with known source formats.
+        $derivative_extension = pathinfo(StreamWrapperManager::getTarget($image_uri), PATHINFO_EXTENSION);
+        if ($derivative_extension) {
+          $base_uri = substr($image_uri, 0, -(strlen($derivative_extension) + 1));
+          foreach (['jpeg', 'jpg', 'png', 'gif', 'webp', 'tiff', 'bmp'] as $ext) {
+            $candidate_uri = $base_uri . '.' . $ext;
+            if ($candidate_uri !== $image_uri && $this->sourceImageExists($candidate_uri, $token_is_valid)) {
+              $image_uri = $candidate_uri;
+              $source_found = TRUE;
+              break;
+            }
+          }
+        }
+      }
+      if (!$source_found) {
         $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,
@@ -283,16 +299,18 @@ private function sourceImageExists(string $image_uri, bool $token_is_valid): boo
   }
 
   /**
-   * Get the file URI without the extension from any conversion image style.
+   * Gets the file URI without a legacy double extension from style conversion.
    *
-   * If the image style converted the image, then an extension has been added
-   * to the original file, resulting in filenames like image.png.jpeg.
+   * Handles old-style derivative filenames (e.g. image.png.webp) that were
+   * generated before addExtension() was changed to replace extensions rather
+   * than append them.
    *
    * @param string $uri
    *   The file URI.
    *
    * @return string
-   *   The file URI without the extension from any conversion image style.
+   *   The URI with the appended conversion extension stripped, or the original
+   *   URI if it does not have a double extension.
    */
   public static function getUriWithoutConvertedExtension(string $uri): string {
     $original_uri = $uri;
diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php
index 3f622a8dcb0..3477c780e1a 100644
--- a/core/modules/image/src/Entity/ImageStyle.php
+++ b/core/modules/image/src/Entity/ImageStyle.php
@@ -513,24 +513,25 @@ protected function getHashSalt() {
   }
 
   /**
-   * Adds an extension to a path.
+   * Replaces the extension on a path when a derivative uses a different format.
    *
    * 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.
+   * replaces the original extension in the given path with the new one,
+   * producing clean single-extension filenames (e.g. image.webp, not
+   * image.png.webp).
    *
    * @param string $path
-   *   The path to add the extension to.
+   *   The path whose extension should be replaced.
    *
    * @return string
    *   The given path if this image style doesn't change its extension, or the
-   *   path with the added extension if it does.
+   *   path with the replaced 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;
+      $path = substr($path, 0, -(strlen($original_extension) + 1)) . '.' . $extension;
     }
     return $path;
   }
diff --git a/core/modules/image/src/Hook/ImageHooks.php b/core/modules/image/src/Hook/ImageHooks.php
index e26f41e2be9..17c44eed936 100644
--- a/core/modules/image/src/Hook/ImageHooks.php
+++ b/core/modules/image/src/Hook/ImageHooks.php
@@ -103,16 +103,28 @@ public function fileDownload($uri): array|int|null {
       // Check that the file exists and is an image.
       $image = \Drupal::service('image.factory')->get($uri);
       if ($image->isValid()) {
-        // 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.
+        // If the image style changed the extension, recover the source URI.
         if (!file_exists($original_uri)) {
+          // Legacy: handle old double-extension filenames (e.g. image.png.webp).
           $converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
           if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
-            // The converted file does exist, use it as the source.
             $original_uri = $converted_original_uri;
           }
+          else {
+            // Current: single replaced extension (e.g. image.webp). Try known
+            // source image extensions.
+            $derivative_extension = pathinfo(StreamWrapperManager::getTarget($original_uri), PATHINFO_EXTENSION);
+            if ($derivative_extension) {
+              $base_uri = substr($original_uri, 0, -(strlen($derivative_extension) + 1));
+              foreach (['jpeg', 'jpg', 'png', 'gif', 'webp', 'tiff', 'bmp'] as $ext) {
+                $candidate_uri = $base_uri . '.' . $ext;
+                if ($candidate_uri !== $original_uri && file_exists($candidate_uri)) {
+                  $original_uri = $candidate_uri;
+                  break;
+                }
+              }
+            }
+          }
         }
         // Check the permissions of the original to grant access to this image.
         $headers = \Drupal::moduleHandler()->invokeAll('file_download', [$original_uri]);
diff --git a/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php b/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php
index 86cf1a928cc..d87b21abe88 100644
--- a/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php
+++ b/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php
@@ -52,7 +52,7 @@ public function testConvertFileInRoot(): void {
     $this->assertFileExists($test_uri);
 
     // Execute the image style on the test image via a GET request.
-    $derivative_uri = 'public://styles/image_effect_test/public/image-test-do.png.jpeg';
+    $derivative_uri = 'public://styles/image_effect_test/public/image-test-do.jpeg';
     $this->assertFileDoesNotExist($derivative_uri);
     $url = \Drupal::service('file_url_generator')->transformRelative($image_style->buildUrl($test_uri));
     $this->drupalGet($this->getAbsoluteUrl($url));
diff --git a/core/modules/image/tests/src/Unit/ImageStyleTest.php b/core/modules/image/tests/src/Unit/ImageStyleTest.php
index 09d51a49bf3..33da7e0f8af 100644
--- a/core/modules/image/tests/src/Unit/ImageStyleTest.php
+++ b/core/modules/image/tests/src/Unit/ImageStyleTest.php
@@ -135,7 +135,7 @@ public function testBuildUri(): void {
       ->willReturn('png');
 
     $image_style = $this->getImageStyleMock($image_effect_id, $image_effect);
-    $this->assertEquals($image_style->buildUri('public://test.jpeg'), 'public://styles/' . $image_style->id() . '/public/test.jpeg.png');
+    $this->assertEquals($image_style->buildUri('public://test.jpeg'), 'public://styles/' . $image_style->id() . '/public/test.png');
 
     // Image style that doesn't change the extension.
     $image_effect_id = $this->randomMachineName();
@@ -175,9 +175,9 @@ public function testGetPathToken(): void {
       ->method('getHashSalt')
       ->willReturn($hash_salt);
 
-    // Assert the extension has been added to the URI before creating the token.
-    $this->assertEquals($image_style->getPathToken('public://test.jpeg.png'), $image_style->getPathToken('public://test.jpeg'));
-    $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.jpeg.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
+    // Assert the extension has been replaced in the URI before creating the token.
+    $this->assertEquals($image_style->getPathToken('public://test.png'), $image_style->getPathToken('public://test.jpeg'));
+    $this->assertEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.png', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
     $this->assertNotEquals(substr(Crypt::hmacBase64($image_style->id() . ':public://test.jpeg', $private_key . $hash_salt), 0, 8), $image_style->getPathToken('public://test.jpeg'));
 
     // Image style that doesn't change the extension.
