From 3e4b17a7c9a926021202140022011bbefac29e99 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 9 Jul 2021 14:18:01 +0100
Subject: [PATCH] Issue #2669074 by kim.pepper, Berdir, gaydabura, andypost,
 ravi.shankar, KapilV, Wim Leers, bradjones1, nickolaj, alexpott, mondrake,
 20th, yogeshmpawar, gaurav.kapoor, voleger, Pavan B S, Neslee Canil Pinto,
 aerozeppelin, Cameron Tod, daffie, dhruveshdtripathi, abramm, Nono95230,
 s.messaris, Mile23, claudiu.cristea, larowlan, catch, jibran, SpadXIII:
 Convert file_create_url() & file_url_transform_relative() to service,
 deprecate it

---
 core/core.services.yml                        |  10 +-
 core/includes/file.inc                        |  98 ++-----
 core/includes/theme.inc                       |  20 +-
 .../Core/Asset/CssCollectionRenderer.php      |  19 +-
 core/lib/Drupal/Core/Asset/CssOptimizer.php   |  44 ++-
 .../Core/Asset/JsCollectionRenderer.php       |  19 +-
 .../InvalidStreamWrapperException.php         |   9 +
 .../lib/Drupal/Core/File/FileUrlGenerator.php | 229 ++++++++++++++++
 .../Core/File/FileUrlGeneratorInterface.php   |  97 +++++++
 core/lib/Drupal/Core/File/file.api.php        |   6 +-
 .../Core/Render/Element/ImageButton.php       |   2 +-
 .../Drupal/Core/Template/TwigExtension.php    |  21 +-
 .../tests/src/Functional/ImportOpmlTest.php   |   4 +-
 .../ckeditor/src/Plugin/Editor/CKEditor.php   |  28 +-
 .../tests/modules/src/Form/AjaxCssForm.php    |  29 +-
 .../Functional/CKEditorToolbarButtonTest.php  |   4 +-
 .../tests/src/Kernel/CKEditorTest.php         |  36 ++-
 core/modules/color/color.module               |   2 +-
 .../src/ColorSystemBrandingBlockAlter.php     |   2 +-
 .../color/tests/src/Functional/ColorTest.php  |   4 +-
 core/modules/file/file.module                 |  18 +-
 core/modules/file/src/ComputedFileUrl.php     |   4 +-
 core/modules/file/src/Entity/File.php         |   8 +-
 core/modules/file/src/FileInterface.php       |   3 +-
 .../BaseFieldFileFormatterBase.php            |  71 ++++-
 .../Field/FieldFormatter/FileUriFormatter.php |   5 +-
 .../file/src/Plugin/views/field/File.php      |  45 ++-
 .../tests/src/Functional/DownloadTest.php     |  29 +-
 .../src/Functional/FileFieldDisplayTest.php   |   4 +-
 .../Functional/FileFieldRSSContentTest.php    |   2 +-
 .../tests/src/Functional/FileListingTest.php  |   2 +-
 .../tests/src/Functional/FilePrivateTest.php  |  14 +-
 .../src/Functional/FileTokenReplaceTest.php   |   2 +-
 .../Formatter/FileVideoFormatterTest.php      |   2 +-
 .../Functional/Hal/FileHalJsonAnonTest.php    |   2 +-
 .../Hal/FileUploadHalJsonTestBase.php         |   2 +-
 .../PrivateFileOnTranslatedEntityTest.php     |   4 +-
 .../file/tests/src/Kernel/FileUrlTest.php     |   2 +-
 .../Formatter/FileEntityFormatterTest.php     |  18 +-
 core/modules/filter/filter.module             |   5 +-
 core/modules/image/image.admin.inc            |   8 +-
 core/modules/image/src/Entity/ImageStyle.php  |   4 +-
 .../modules/image/src/ImageStyleInterface.php |   2 +-
 .../Field/FieldFormatter/ImageFormatter.php   |  30 +-
 .../FieldFormatter/ImageUrlFormatter.php      |   5 +-
 .../src/Functional/ImageAdminStylesTest.php   |  15 +-
 .../src/Functional/ImageDimensionsTest.php    |   8 +-
 .../Functional/ImageEffect/ConvertTest.php    |   2 +-
 .../src/Functional/ImageFieldDisplayTest.php  |  16 +-
 .../Functional/ImageStylesPathAndUrlTest.php  |   2 +-
 .../src/Kernel/ImageThemeFunctionTest.php     |   4 +-
 .../Functional/JsonApiFunctionalTestBase.php  |   2 +-
 .../Functional/Hal/MediaHalJsonAnonTest.php   |   2 +-
 .../CKEditorIntegrationTest.php               |   4 +-
 .../FunctionalJavascript/MediaDisplayTest.php |   4 +-
 .../MediaSourceImageTest.php                  |   4 +-
 .../MediaStandardProfileTest.php              |  19 +-
 .../tests/src/Kernel/MediaEmbedFilterTest.php |   2 +-
 .../src/Functional/NodeRSSContentTest.php     |  10 +-
 .../Functional/FileFieldAttributesTest.php    |   2 +-
 .../responsive_image/responsive_image.module  |   8 +-
 .../ResponsiveImageFieldDisplayTest.php       |  29 +-
 .../EntityReferenceFieldItemNormalizer.php    |   5 +-
 .../src/Form/FormTestDisabledElementsForm.php |   1 +
 .../Functional/System/RetrieveFileTest.php    |   2 +-
 .../tests/src/Functional/System/ThemeTest.php |  10 +-
 .../src/Functional/Theme/EngineTwigTest.php   |   4 +-
 .../src/Functional/TaxonomyImageTest.php      |   4 +-
 .../tests/src/Functional/UserPictureTest.php  |   2 +-
 .../Core/Asset/AttachedAssetsTest.php         |  30 +-
 .../Core/File/FileSystemDeprecationTest.php   |  26 ++
 .../Core/File/FileUrlGeneratorTest.php        | 257 ++++++++++++++++++
 .../Core/File/StreamWrapperTest.php           |  11 +-
 .../Core/File/UrlRewritingTest.php            | 120 --------
 .../Core/File/UrlTransformRelativeTest.php    |   4 +-
 .../KernelTests/Core/Theme/ImageTest.php      |  19 +-
 .../Core/Theme/ThemeSettingsTest.php          |   4 +-
 .../Drupal/Tests/BrowserHtmlDebugTrait.php    |   4 +-
 .../Asset/CssCollectionRendererUnitTest.php   | 194 +++++--------
 .../Tests/Core/Asset/CssOptimizerUnitTest.php |  75 ++---
 .../Tests/Core/Template/TwigExtensionTest.php |  27 +-
 81 files changed, 1303 insertions(+), 603 deletions(-)
 create mode 100644 core/lib/Drupal/Core/File/Exception/InvalidStreamWrapperException.php
 create mode 100644 core/lib/Drupal/Core/File/FileUrlGenerator.php
 create mode 100644 core/lib/Drupal/Core/File/FileUrlGeneratorInterface.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/File/FileUrlGeneratorTest.php
 delete mode 100644 core/tests/Drupal/KernelTests/Core/File/UrlRewritingTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index f1b8355e7937..9b35d53c36e4 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -392,6 +392,9 @@ services:
   file_system:
     class: Drupal\Core\File\FileSystem
     arguments: ['@stream_wrapper_manager', '@settings', '@logger.channel.file']
+  file_url_generator:
+    class: Drupal\Core\File\FileUrlGenerator
+    arguments: ['@stream_wrapper_manager', '@request_stack', '@module_handler']
   form_builder:
     class: Drupal\Core\Form\FormBuilder
     arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@?csrf_token']
@@ -1580,12 +1583,13 @@ services:
     arguments: ['@settings']
   asset.css.collection_renderer:
     class: Drupal\Core\Asset\CssCollectionRenderer
-    arguments: [ '@state' ]
+    arguments: [ '@state', '@file_url_generator' ]
   asset.css.collection_optimizer:
     class: Drupal\Core\Asset\CssCollectionOptimizer
     arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@asset.css.dumper', '@state', '@file_system']
   asset.css.optimizer:
     class: Drupal\Core\Asset\CssOptimizer
+    arguments: ['@file_url_generator']
   asset.css.collection_grouper:
     class: Drupal\Core\Asset\CssCollectionGrouper
   asset.css.dumper:
@@ -1593,7 +1597,7 @@ services:
     arguments: ['@file_system']
   asset.js.collection_renderer:
     class: Drupal\Core\Asset\JsCollectionRenderer
-    arguments: [ '@state' ]
+    arguments: [ '@state', '@file_url_generator' ]
   asset.js.collection_optimizer:
     class: Drupal\Core\Asset\JsCollectionOptimizer
     arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state', '@file_system']
@@ -1634,7 +1638,7 @@ services:
       - { name: service_collector, tag: 'twig.extension', call: addExtension }
   twig.extension:
     class: Drupal\Core\Template\TwigExtension
-    arguments: ['@renderer', '@url_generator', '@theme.manager', '@date.formatter']
+    arguments: ['@renderer', '@url_generator', '@theme.manager', '@date.formatter', '@file_url_generator']
     tags:
       - { name: twig.extension, priority: 100 }
   # @todo Figure out what to do about debugging functions.
diff --git a/core/includes/file.inc b/core/includes/file.inc
index 2486e6689e93..83812983a323 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -5,9 +5,8 @@
  * API for handling file uploads and server file management.
  */
 
-use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\File\Exception\InvalidStreamWrapperException;
 use Drupal\Core\File\FileSystemInterface;
-use Drupal\Core\StreamWrapper\StreamWrapperManager;
 
 /**
  * @defgroup file File interface
@@ -47,58 +46,24 @@
  *   '/', nothing is done and the same string is returned. If a stream wrapper
  *   could not be found to generate an external URL, then FALSE is returned.
  *
+ * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0.
+ *   Use the appropriate method on \Drupal\Core\File\FileUrlGeneratorInterface
+ *   instead.
+ *
+ * @see https://www.drupal.org/node/2940031
  * @see https://www.drupal.org/node/515192
- * @see file_url_transform_relative()
+ * @see \Drupal\Core\File\FileUrlGeneratorInterface::generate()
+ * @see \Drupal\Core\File\FileUrlGeneratorInterface::generateString()
+ * @see \Drupal\Core\File\FileUrlGeneratorInterface::generateAbsoluteString()
+ * @see \Drupal\Core\File\FileUrlGeneratorInterface::transformRelative()
  */
 function file_create_url($uri) {
-  // Allow the URI to be altered, e.g. to serve a file from a CDN or static
-  // file server.
-  \Drupal::moduleHandler()->alter('file_url', $uri);
-
-  $scheme = StreamWrapperManager::getScheme($uri);
-
-  if (!$scheme) {
-    // Allow for:
-    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
-    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
-    //   http://example.com/bar.jpg by the browser when viewing a page over
-    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
-    // Both types of relative URIs are characterized by a leading slash, hence
-    // we can use a single check.
-    if (mb_substr($uri, 0, 1) == '/') {
-      return $uri;
-    }
-    else {
-      // If this is not a properly formatted stream, then it is a shipped file.
-      // Therefore, return the urlencoded URI with the base URL prepended.
-      $options = UrlHelper::parse($uri);
-      $path = $GLOBALS['base_url'] . '/' . UrlHelper::encodePath($options['path']);
-      // Append the query.
-      if ($options['query']) {
-        $path .= '?' . UrlHelper::buildQuery($options['query']);
-      }
-
-      // Append fragment.
-      if ($options['fragment']) {
-        $path .= '#' . $options['fragment'];
-      }
-
-      return $path;
-    }
+  @trigger_error('file_create_url() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use the appropriate method on \Drupal\Core\File\FileUrlGeneratorInterface instead. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+  try {
+    return \Drupal::service('file_url_generator')->generateAbsoluteString($uri);
   }
-  elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
-    // Check for HTTP and data URI-encoded URLs so that we don't have to
-    // implement getExternalUrl() for the HTTP and data schemes.
-    return $uri;
-  }
-  else {
-    // Attempt to return an external URL using the appropriate wrapper.
-    if ($wrapper = \Drupal::service('stream_wrapper_manager')->getViaUri($uri)) {
-      return $wrapper->getExternalUrl();
-    }
-    else {
-      return FALSE;
-    }
+  catch (InvalidStreamWrapperException $e) {
+    return FALSE;
   }
 }
 
@@ -109,38 +74,23 @@ function file_create_url($uri) {
  * content errors when using HTTPS + HTTP.
  *
  * @param string $file_url
- *   A file URL of a local file as generated by file_create_url().
+ *   A file URL of a local file as generated by
+ *   FileUrlGeneratorInterface::generateString().
  *
  * @return string
  *   If the file URL indeed pointed to a local file and was indeed absolute,
  *   then the transformed, relative URL to the local file. Otherwise: the
  *   original value of $file_url.
  *
- * @see file_create_url()
+ * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0.
+ *   Use \Drupal\Core\File\FileUrlGenerator::transformRelative() instead.
+ *
+ * @see https://www.drupal.org/node/2940031
+ * @see \Drupal\Core\File\FileUrlGeneratorInterface::transformRelative()
  */
 function file_url_transform_relative($file_url) {
-  // Unfortunately, we pretty much have to duplicate Symfony's
-  // Request::getHttpHost() method because Request::getPort() may return NULL
-  // instead of a port number.
-  $request = \Drupal::request();
-  $host = $request->getHost();
-  $scheme = $request->getScheme();
-  $port = $request->getPort() ?: 80;
-
-  // Files may be accessible on a different port than the web request.
-  $file_url_port = parse_url($file_url, PHP_URL_PORT) ?? $port;
-  if ($file_url_port != $port) {
-    return $file_url;
-  }
-
-  if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
-    $http_host = $host;
-  }
-  else {
-    $http_host = $host . ':' . $port;
-  }
-
-  return preg_replace('|^https?://' . preg_quote($http_host, '|') . '|', '', $file_url);
+  @trigger_error('file_url_transform_relative() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\File\FileUrlGenerator::transformRelative() instead. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+  return \Drupal::service('file_url_generator')->transformRelative($file_url);
 }
 
 /**
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 1b3e34f00a6e..d2f65b2bfcc6 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -347,13 +347,16 @@ function theme_get_setting($setting_name, $theme = NULL) {
         }
       }
 
+      /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+      $file_url_generator = \Drupal::service('file_url_generator');
+
       // Generate the path to the logo image.
       if ($cache[$theme]->get('logo.use_default')) {
         $logo = \Drupal::service('theme.initialization')->getActiveThemeByName($theme)->getLogo();
-        $cache[$theme]->set('logo.url', file_url_transform_relative(file_create_url($logo)));
+        $cache[$theme]->set('logo.url', $file_url_generator->generateString($logo));
       }
       elseif ($logo_path = $cache[$theme]->get('logo.path')) {
-        $cache[$theme]->set('logo.url', file_url_transform_relative(file_create_url($logo_path)));
+        $cache[$theme]->set('logo.url', $file_url_generator->generateString($logo_path));
       }
 
       // Generate the path to the favicon.
@@ -361,14 +364,14 @@ function theme_get_setting($setting_name, $theme = NULL) {
         $favicon_path = $cache[$theme]->get('favicon.path');
         if ($cache[$theme]->get('favicon.use_default')) {
           if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) {
-            $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url($favicon)));
+            $cache[$theme]->set('favicon.url', $file_url_generator->generateString($favicon));
           }
           else {
-            $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url('core/misc/favicon.ico')));
+            $cache[$theme]->set('favicon.url', $file_url_generator->generateString('core/misc/favicon.ico'));
           }
         }
         elseif ($favicon_path) {
-          $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url($favicon_path)));
+          $cache[$theme]->set('favicon.url', $file_url_generator->generateString($favicon_path));
         }
         else {
           $cache[$theme]->set('features.favicon', FALSE);
@@ -820,8 +823,11 @@ function template_preprocess_links(&$variables) {
  *     - http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#introduction-3:viewport-based-selection-2
  */
 function template_preprocess_image(&$variables) {
+  /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+  $file_url_generator = \Drupal::service('file_url_generator');
+
   if (!empty($variables['uri'])) {
-    $variables['attributes']['src'] = file_url_transform_relative(file_create_url($variables['uri']));
+    $variables['attributes']['src'] = $file_url_generator->generateString($variables['uri']);
   }
   // Generate a srcset attribute conforming to the spec at
   // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-srcset
@@ -829,7 +835,7 @@ function template_preprocess_image(&$variables) {
     $srcset = [];
     foreach ($variables['srcset'] as $src) {
       // URI is mandatory.
-      $source = file_url_transform_relative(file_create_url($src['uri']));
+      $source = $file_url_generator->generateString($src['uri']);
       if (isset($src['width']) && !empty($src['width'])) {
         $source .= ' ' . $src['width'];
       }
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
index 13c53e24b4cf..e81beab5df96 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Asset;
 
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\State\StateInterface;
 
 /**
@@ -16,14 +17,28 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
    */
   protected $state;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * Constructs a CssCollectionRenderer.
    *
    * @param \Drupal\Core\State\StateInterface $state
    *   The state key/value store.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
    */
-  public function __construct(StateInterface $state) {
+  public function __construct(StateInterface $state, FileUrlGeneratorInterface $file_url_generator = NULL) {
     $this->state = $state;
+    if (!$file_url_generator) {
+      @trigger_error('Calling CssCollectionRenderer::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0 and will be required before drupal:10.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
   }
 
   /**
@@ -55,7 +70,7 @@ public function render(array $css_assets) {
       switch ($css_asset['type']) {
         // For file items, output a LINK tag for file CSS assets.
         case 'file':
-          $element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data']));
+          $element['#attributes']['href'] = $this->fileUrlGenerator->generateString($css_asset['data']);
           // Only add the cache-busting query string if this isn't an aggregate
           // file.
           if (!isset($css_asset['preprocessed'])) {
diff --git a/core/lib/Drupal/Core/Asset/CssOptimizer.php b/core/lib/Drupal/Core/Asset/CssOptimizer.php
index e939b3d0542f..4ba7a24f4d0f 100644
--- a/core/lib/Drupal/Core/Asset/CssOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/CssOptimizer.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 
 /**
  * Optimizes a CSS asset.
@@ -17,6 +18,27 @@ class CssOptimizer implements AssetOptimizerInterface {
    */
   public $rewriteFileURIBasePath;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
+  /**
+   * Constructs a CssOptimizer.
+   *
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
+   */
+  public function __construct(FileUrlGeneratorInterface $file_url_generator = NULL) {
+    if (!$file_url_generator) {
+      @trigger_error('Calling CssOptimizer::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0. The $file_url_generator argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -258,7 +280,27 @@ public function rewriteFileURI($matches) {
       $last = $path;
       $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
     }
-    return 'url(' . file_url_transform_relative(file_create_url($path)) . ')';
+    return 'url(' . $this->getFileUrlGenerator()->generateString($path) . ')';
+  }
+
+  /**
+   * Returns the file URL generator.
+   *
+   * This is provided for BC as sub-classes may not call the parent constructor.
+   *
+   * @return \Drupal\Core\File\FileUrlGeneratorInterface
+   *   The file URL generator.
+   *
+   * @internal
+   *   This can be removed in Drupal 10.0.x when the constructor deprecation is
+   *   removed.
+   */
+  private function getFileUrlGenerator(): FileUrlGeneratorInterface {
+    if (!$this->fileUrlGenerator) {
+      @trigger_error('Calling CssOptimizer::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0. The $file_url_generator argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $this->fileUrlGenerator = \Drupal::service('file_url_generator');
+    }
+    return $this->fileUrlGenerator;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
index 99d0e14f545c..d4a618fd6b0d 100644
--- a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
+++ b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Asset;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\State\StateInterface;
 
 /**
@@ -17,14 +18,28 @@ class JsCollectionRenderer implements AssetCollectionRendererInterface {
    */
   protected $state;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * Constructs a JsCollectionRenderer.
    *
    * @param \Drupal\Core\State\StateInterface $state
    *   The state key/value store.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
    */
-  public function __construct(StateInterface $state) {
+  public function __construct(StateInterface $state, FileUrlGeneratorInterface $file_url_generator = NULL) {
     $this->state = $state;
+    if (!$file_url_generator) {
+      @trigger_error('Calling JsCollectionRenderer::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0. The $file_url_generator argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
   }
 
   /**
@@ -74,7 +89,7 @@ public function render(array $js_assets) {
         case 'file':
           $query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version'];
           $query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?';
-          $element['#attributes']['src'] = file_url_transform_relative(file_create_url($js_asset['data']));
+          $element['#attributes']['src'] = $this->fileUrlGenerator->generateString($js_asset['data']);
           // Only add the cache-busting query string if this isn't an aggregate
           // file.
           if (!isset($js_asset['preprocessed'])) {
diff --git a/core/lib/Drupal/Core/File/Exception/InvalidStreamWrapperException.php b/core/lib/Drupal/Core/File/Exception/InvalidStreamWrapperException.php
new file mode 100644
index 000000000000..aca019463742
--- /dev/null
+++ b/core/lib/Drupal/Core/File/Exception/InvalidStreamWrapperException.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Drupal\Core\File\Exception;
+
+/**
+ * Exception thrown when a file's stream wrapper is invalid.
+ */
+class InvalidStreamWrapperException extends FileException {
+}
diff --git a/core/lib/Drupal/Core/File/FileUrlGenerator.php b/core/lib/Drupal/Core/File/FileUrlGenerator.php
new file mode 100644
index 000000000000..dbfdb0d33bb6
--- /dev/null
+++ b/core/lib/Drupal/Core/File/FileUrlGenerator.php
@@ -0,0 +1,229 @@
+<?php
+
+namespace Drupal\Core\File;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\File\Exception\InvalidStreamWrapperException;
+use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Default implementation for the file URL generator service.
+ */
+class FileUrlGenerator implements FileUrlGeneratorInterface {
+
+  /**
+   * The stream wrapper manager.
+   *
+   * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
+   */
+  protected $streamWrapperManager;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a new file URL generator object.
+   *
+   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
+   *   The stream wrapper manager.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, RequestStack $request_stack, ModuleHandlerInterface $module_handler) {
+    $this->streamWrapperManager = $stream_wrapper_manager;
+    $this->requestStack = $request_stack;
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generateString(string $uri): string {
+    return $this->doGenerateString($uri, TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generateAbsoluteString(string $uri): string {
+    return $this->doGenerateString($uri, FALSE);
+  }
+
+  /**
+   * Creates an absolute web-accessible URL string.
+   *
+   * @param string $uri
+   *   The URI to a file for which we need an external URL, or the path to a
+   *   shipped file.
+   * @param bool $relative
+   *   Whether to return an relative or absolute URL.
+   *
+   * @return string
+   *   An absolute string containing a URL that may be used to access the
+   *   file.
+   *
+   * @throws \Drupal\Core\File\Exception\InvalidStreamWrapperException
+   *   If a stream wrapper could not be found to generate an external URL.
+   */
+  protected function doGenerateString(string $uri, bool $relative): string {
+    // Allow the URI to be altered, e.g. to serve a file from a CDN or static
+    // file server.
+    $this->moduleHandler->alter('file_url', $uri);
+
+    $scheme = StreamWrapperManager::getScheme($uri);
+
+    if (!$scheme) {
+      $baseUrl = $relative ? base_path() : $this->requestStack->getCurrentRequest()->getSchemeAndHttpHost() . base_path();
+      return $this->generatePath($baseUrl, $uri);
+    }
+    elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
+      // Check for HTTP and data URI-encoded URLs so that we don't have to
+      // implement getExternalUrl() for the HTTP and data schemes.
+      return $relative ? $this->transformRelative($uri) : $uri;
+    }
+    elseif ($wrapper = $this->streamWrapperManager->getViaUri($uri)) {
+      // Attempt to return an external URL using the appropriate wrapper.
+      $externalUrl = $wrapper->getExternalUrl();
+      return $relative ? $this->transformRelative($externalUrl) : $externalUrl;
+    }
+    throw new InvalidStreamWrapperException();
+  }
+
+  /**
+   * Generate a URL path.
+   *
+   * @param string $base_url
+   *   The base URL.
+   * @param string $uri
+   *   The URI.
+   *
+   * @return string
+   *   The URL path.
+   */
+  protected function generatePath(string $base_url, string $uri): string {
+    // Allow for:
+    // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
+    // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
+    //   http://example.com/bar.jpg by the browser when viewing a page over
+    //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
+    // Both types of relative URIs are characterized by a leading slash, hence
+    // we can use a single check.
+    if (mb_substr($uri, 0, 1) == '/') {
+      return $uri;
+    }
+    else {
+      // If this is not a properly formatted stream, then it is a shipped
+      // file. Therefore, return the urlencoded URI with the base URL
+      // prepended.
+      $options = UrlHelper::parse($uri);
+      $path = $base_url . UrlHelper::encodePath($options['path']);
+      // Append the query.
+      if ($options['query']) {
+        $path .= '?' . UrlHelper::buildQuery($options['query']);
+      }
+
+      // Append fragment.
+      if ($options['fragment']) {
+        $path .= '#' . $options['fragment'];
+      }
+
+      return $path;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generate(string $uri): Url {
+    // Allow the URI to be altered, e.g. to serve a file from a CDN or static
+    // file server.
+    $this->moduleHandler->alter('file_url', $uri);
+
+    $scheme = StreamWrapperManager::getScheme($uri);
+
+    if (!$scheme) {
+      // Allow for:
+      // - root-relative URIs (e.g. /foo.jpg in http://example.com/foo.jpg)
+      // - protocol-relative URIs (e.g. //bar.jpg, which is expanded to
+      //   http://example.com/bar.jpg by the browser when viewing a page over
+      //   HTTP and to https://example.com/bar.jpg when viewing a HTTPS page)
+      // Both types of relative URIs are characterized by a leading slash, hence
+      // we can use a single check.
+      if (mb_substr($uri, 0, 2) == '//') {
+        return Url::fromUri($uri);
+      }
+      elseif (mb_substr($uri, 0, 1) == '/') {
+        return Url::fromUri('base:' . str_replace($this->requestStack->getCurrentRequest()->getBasePath(), '', $uri));
+      }
+      else {
+        // If this is not a properly formatted stream, then it is a shipped
+        // file. Therefore, return the urlencoded URI.
+        $options = UrlHelper::parse($uri);
+        return Url::fromUri('base:' . UrlHelper::encodePath($options['path']), $options);
+      }
+    }
+    elseif ($scheme == 'http' || $scheme == 'https' || $scheme == 'data') {
+      // Check for HTTP and data URI-encoded URLs so that we don't have to
+      // implement getExternalUrl() for the HTTP and data schemes.
+      $options = UrlHelper::parse($uri);
+      return Url::fromUri(urldecode($options['path']), $options);
+    }
+    elseif ($wrapper = $this->streamWrapperManager->getViaUri($uri)) {
+      // Attempt to return an external URL using the appropriate wrapper.
+      return Url::fromUri('base:' . $this->transformRelative(urldecode($wrapper->getExternalUrl()), FALSE));
+    }
+    throw new InvalidStreamWrapperException();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformRelative(string $file_url, bool $root_relative = TRUE): string {
+    // Unfortunately, we pretty much have to duplicate Symfony's
+    // Request::getHttpHost() method because Request::getPort() may return NULL
+    // instead of a port number.
+    $request = $this->requestStack->getCurrentRequest();
+    $host = $request->getHost();
+    $scheme = $request->getScheme();
+    $port = $request->getPort() ?: 80;
+
+    // Files may be accessible on a different port than the web request.
+    $file_url_port = parse_url($file_url, PHP_URL_PORT) ?? $port;
+    if ($file_url_port != $port) {
+      return $file_url;
+    }
+
+    if (('http' == $scheme && $port == 80) || ('https' == $scheme && $port == 443)) {
+      $http_host = $host;
+    }
+    else {
+      $http_host = $host . ':' . $port;
+    }
+
+    // If this should not be a root-relative path but relative to the drupal
+    // base path, add it to the host to be removed from the URL as well.
+    if (!$root_relative) {
+      $http_host .= $request->getBasePath();
+    }
+
+    return preg_replace('|^https?://' . preg_quote($http_host, '|') . '|', '', $file_url);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/File/FileUrlGeneratorInterface.php b/core/lib/Drupal/Core/File/FileUrlGeneratorInterface.php
new file mode 100644
index 000000000000..1ab1bfcf4c92
--- /dev/null
+++ b/core/lib/Drupal/Core/File/FileUrlGeneratorInterface.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\Core\File;
+
+use Drupal\Core\Url;
+
+/**
+ * Generates file URLs for a stream to an external or local file.
+ *
+ * Compatibility: normal paths and stream wrappers.
+ *
+ * There are two kinds of local files:
+ * - "managed files", i.e. those stored by a Drupal-compatible stream wrapper.
+ *   These are files that have either been uploaded by users or were generated
+ *   automatically (for example through CSS aggregation).
+ * - "shipped files", i.e. those outside of the files directory, which ship as
+ *   part of Drupal core or contributed modules or themes.
+ *
+ * Separate methods are provided to provide absolute and relative URLs as well
+ * as plain strings or Url objects, depending on the requirements. In general,
+ * it is recommended to always use relative URLs unless absolute URL's are
+ * required.
+ */
+interface FileUrlGeneratorInterface {
+
+  /**
+   * Creates a root-relative web-accessible URL string.
+   *
+   * @param string $uri
+   *   The URI to a file for which we need an external URL, or the path to a
+   *   shipped file.
+   *
+   * @return string
+   *   For a local URL (matching domain), a root-relative string containing a
+   *   URL that may be used to access the file. An absolute URL may be returned
+   *   when using a CDN or a remote stream wrapper.
+   *
+   * @throws \Drupal\Core\File\Exception\InvalidStreamWrapperException
+   *   If a stream wrapper could not be found to generate an external URL.
+   */
+  public function generateString(string $uri): string;
+
+  /**
+   * Creates an absolute web-accessible URL string.
+   *
+   * @param string $uri
+   *   The URI to a file for which we need an external URL, or the path to a
+   *   shipped file.
+   *
+   * @return string
+   *   An absolute string containing a URL that may be used to access the
+   *   file.
+   *
+   * @throws \Drupal\Core\File\Exception\InvalidStreamWrapperException
+   *   If a stream wrapper could not be found to generate an external URL.
+   */
+  public function generateAbsoluteString(string $uri): string;
+
+  /**
+   * Creates a root-relative web-accessible URL object.
+   *
+   * @param string $uri
+   *   The URI to a file for which we need an external URL, or the path to a
+   *   shipped file.
+   *
+   * @return \Drupal\Core\Url
+   *   For a local URL (matching domain), a base-relative Url object containing
+   *   a URL that may be used to access the file. An Url object with absolute
+   *   URL may be returned when using a CDN or a remote stream wrapper. Use
+   *   setAbsolute() on the Url object to build an absolute URL.
+   *
+   * @throws \Drupal\Core\File\Exception\InvalidStreamWrapperException
+   *   If a stream wrapper could not be found to generate an external URL.
+   */
+  public function generate(string $uri): Url;
+
+  /**
+   * Transforms an absolute URL of a local file to a relative URL.
+   *
+   * May be useful to prevent problems on multisite set-ups and prevent mixed
+   * content errors when using HTTPS + HTTP.
+   *
+   * @param string $file_url
+   *   A file URL of a local file as generated by
+   *   \Drupal\Core\File\FileUrlGenerator::generate().
+   * @param bool $root_relative
+   *   (optional) TRUE if the URL should be relative to the root path or FALSE
+   *   if relative to the Drupal base path.
+   *
+   * @return string
+   *   If the file URL indeed pointed to a local file and was indeed absolute,
+   *   then the transformed, relative URL to the local file. Otherwise: the
+   *   original value of $file_url.
+   */
+  public function transformRelative(string $file_url, bool $root_relative = TRUE): string;
+
+}
diff --git a/core/lib/Drupal/Core/File/file.api.php b/core/lib/Drupal/Core/File/file.api.php
index 26501ff8a776..33a5be1c85a4 100644
--- a/core/lib/Drupal/Core/File/file.api.php
+++ b/core/lib/Drupal/Core/File/file.api.php
@@ -45,9 +45,9 @@ function hook_file_download($uri) {
 /**
  * Alter the URL to a file.
  *
- * This hook is called from file_create_url(), and  is called fairly
- * frequently (10+ times per page), depending on how many files there are in a
- * given page.
+ * This hook is called from \Drupal\Core\File\FileUrlGenerator::generate(),
+ * and is called fairly frequently (10+ times per page), depending on how many
+ * files there are in a given page.
  * If CSS and JS aggregation are disabled, this can become very frequently
  * (50+ times per page) so performance is critical.
  *
diff --git a/core/lib/Drupal/Core/Render/Element/ImageButton.php b/core/lib/Drupal/Core/Render/Element/ImageButton.php
index 2404ba8084cf..6f345ece40b7 100644
--- a/core/lib/Drupal/Core/Render/Element/ImageButton.php
+++ b/core/lib/Drupal/Core/Render/Element/ImageButton.php
@@ -70,7 +70,7 @@ public static function preRenderButton($element) {
     $element['#attributes']['type'] = 'image';
     Element::setAttributes($element, ['id', 'name', 'value']);
 
-    $element['#attributes']['src'] = file_url_transform_relative(file_create_url($element['#src']));
+    $element['#attributes']['src'] = \Drupal::service('file_url_generator')->generateString($element['#src']);
     if (!empty($element['#title'])) {
       $element['#attributes']['alt'] = $element['#title'];
       $element['#attributes']['title'] = $element['#title'];
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index 7deda60a356d..34257e349304 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -6,6 +6,7 @@
 use Drupal\Component\Render\MarkupInterface;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Render\AttachmentsInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Markup;
@@ -61,6 +62,13 @@ class TwigExtension extends AbstractExtension {
    */
   protected $dateFormatter;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * Constructs \Drupal\Core\Template\TwigExtension.
    *
@@ -72,12 +80,19 @@ class TwigExtension extends AbstractExtension {
    *   The theme manager.
    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
    *   The date formatter.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
    */
-  public function __construct(RendererInterface $renderer, UrlGeneratorInterface $url_generator, ThemeManagerInterface $theme_manager, DateFormatterInterface $date_formatter) {
+  public function __construct(RendererInterface $renderer, UrlGeneratorInterface $url_generator, ThemeManagerInterface $theme_manager, DateFormatterInterface $date_formatter, FileUrlGeneratorInterface $file_url_generator = NULL) {
     $this->renderer = $renderer;
     $this->urlGenerator = $url_generator;
     $this->themeManager = $theme_manager;
     $this->dateFormatter = $date_formatter;
+    if (!$file_url_generator) {
+      @trigger_error('Calling TwigExtension::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0 and will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031.', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
   }
 
   /**
@@ -92,9 +107,7 @@ public function getFunctions() {
       new TwigFunction('url', [$this, 'getUrl'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]),
       new TwigFunction('path', [$this, 'getPath'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]),
       new TwigFunction('link', [$this, 'getLink']),
-      new TwigFunction('file_url', function ($uri) {
-        return file_url_transform_relative(file_create_url($uri));
-      }),
+      new TwigFunction('file_url', [$this->fileUrlGenerator, 'generateString']),
       new TwigFunction('attach_library', [$this, 'attachLibrary']),
       new TwigFunction('active_theme_path', [$this, 'getActiveThemePath']),
       new TwigFunction('active_theme', [$this, 'getActiveTheme']),
diff --git a/core/modules/aggregator/tests/src/Functional/ImportOpmlTest.php b/core/modules/aggregator/tests/src/Functional/ImportOpmlTest.php
index d60b4062ed23..a690fbd72221 100644
--- a/core/modules/aggregator/tests/src/Functional/ImportOpmlTest.php
+++ b/core/modules/aggregator/tests/src/Functional/ImportOpmlTest.php
@@ -68,7 +68,7 @@ public function validateImportFormFields() {
     $path = $this->getEmptyOpml();
     $edit = [
       'files[upload]' => $path,
-      'remote' => file_create_url($path),
+      'remote' => \Drupal::service('file_url_generator')->generateAbsoluteString($path),
     ];
     $this->drupalGet('admin/config/services/aggregator/add/opml');
     $this->submitForm($edit, 'Import');
@@ -98,7 +98,7 @@ protected function submitImportForm() {
     $this->assertSession()->pageTextContains('No new feed has been added.');
 
     // Attempting to load empty OPML from remote URL
-    $edit = ['remote' => file_create_url($this->getEmptyOpml())];
+    $edit = ['remote' => \Drupal::service('file_url_generator')->generateAbsoluteString($this->getEmptyOpml())];
     $this->drupalGet('admin/config/services/aggregator/add/opml');
     $this->submitForm($edit, 'Import');
     $this->assertSession()->pageTextContains('No new feed has been added.');
diff --git a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
index 4e4ec799a9c6..fd5a338b7a2e 100644
--- a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\ckeditor\CKEditorPluginManager;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Render\Element;
@@ -58,6 +59,13 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
    */
   protected $renderer;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * The state key/value store.
    *
@@ -84,14 +92,21 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
    *   The renderer.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state key/value store.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, RendererInterface $renderer, StateInterface $state) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, RendererInterface $renderer, StateInterface $state, FileUrlGeneratorInterface $file_url_generator = NULL) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->ckeditorPluginManager = $ckeditor_plugin_manager;
     $this->moduleHandler = $module_handler;
     $this->languageManager = $language_manager;
     $this->renderer = $renderer;
     $this->state = $state;
+    if (!$file_url_generator) {
+      @trigger_error('Calling CKEditor::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0 and will be required before drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
   }
 
   /**
@@ -106,7 +121,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $container->get('module_handler'),
       $container->get('language_manager'),
       $container->get('renderer'),
-      $container->get('state')
+      $container->get('state'),
+      $container->get('file_url_generator')
     );
   }
 
@@ -307,11 +323,8 @@ public function getJSSettings(Editor $editor) {
     ];
 
     // Finally, set Drupal-specific CKEditor settings.
-    $root_relative_file_url = function ($uri) {
-      return file_url_transform_relative(file_create_url($uri));
-    };
     $settings += [
-      'drupalExternalPlugins' => array_map($root_relative_file_url, $external_plugin_files),
+      'drupalExternalPlugins' => array_map([$this->fileUrlGenerator, 'generateString'], $external_plugin_files),
     ];
 
     // Parse all CKEditor plugin JavaScript files for translations.
@@ -444,8 +457,7 @@ public function buildContentsCssJSSetting(Editor $editor) {
       $query_string_separator = (strpos($item, '?') !== FALSE) ? '&' : '?';
       return $item . $query_string_separator . $query_string;
     }, $css);
-    $css = array_map('file_create_url', $css);
-    $css = array_map('file_url_transform_relative', $css);
+    $css = array_map([$this->fileUrlGenerator, 'generateString'], $css);
 
     return array_values($css);
   }
diff --git a/core/modules/ckeditor/tests/modules/src/Form/AjaxCssForm.php b/core/modules/ckeditor/tests/modules/src/Form/AjaxCssForm.php
index e8c066825a71..504293341000 100644
--- a/core/modules/ckeditor/tests/modules/src/Form/AjaxCssForm.php
+++ b/core/modules/ckeditor/tests/modules/src/Form/AjaxCssForm.php
@@ -4,8 +4,10 @@
 
 use Drupal\ckeditor\Ajax\AddStyleSheetCommand;
 use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * A form for testing delivery of CSS to CKEditor via AJAX.
@@ -14,6 +16,30 @@
  */
 class AjaxCssForm extends FormBase {
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
+  /**
+   * Constructs an AjaxCssForm.
+   *
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
+   */
+  public function __construct(FileUrlGeneratorInterface $file_url_generator) {
+    $this->fileUrlGenerator = $file_url_generator;
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('file_url_generator'));
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -82,8 +108,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
   protected function generateResponse($editor_id) {
     // Build a URL to the style sheet that will be added.
     $url = drupal_get_path('module', 'ckeditor_test') . '/css/test.css';
-    $url = file_create_url($url);
-    $url = file_url_transform_relative($url);
+    $url = $this->fileUrlGenerator->generateString($url);
 
     $response = new AjaxResponse();
     return $response
diff --git a/core/modules/ckeditor/tests/src/Functional/CKEditorToolbarButtonTest.php b/core/modules/ckeditor/tests/src/Functional/CKEditorToolbarButtonTest.php
index ff75d6e4aa5f..40e43050fac2 100644
--- a/core/modules/ckeditor/tests/src/Functional/CKEditorToolbarButtonTest.php
+++ b/core/modules/ckeditor/tests/src/Functional/CKEditorToolbarButtonTest.php
@@ -76,7 +76,9 @@ public function testImageButtonDisplay() {
     $json_encode = function ($html) {
       return trim(Json::encode($html), '"');
     };
-    $markup = $json_encode(file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/icons/drupalimage.png')));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $markup = $json_encode($file_url_generator->generateString('core/modules/ckeditor/js/plugins/drupalimage/icons/drupalimage.png'));
     $this->assertRaw($markup);
   }
 
diff --git a/core/modules/ckeditor/tests/src/Kernel/CKEditorTest.php b/core/modules/ckeditor/tests/src/Kernel/CKEditorTest.php
index c0d21b2f5b85..ba994452dae9 100644
--- a/core/modules/ckeditor/tests/src/Kernel/CKEditorTest.php
+++ b/core/modules/ckeditor/tests/src/Kernel/CKEditorTest.php
@@ -35,6 +35,13 @@ class CKEditorTest extends KernelTestBase {
    */
   protected $ckeditor;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * The Editor Plugin Manager.
    *
@@ -44,6 +51,7 @@ class CKEditorTest extends KernelTestBase {
 
   protected function setUp(): void {
     parent::setUp();
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
 
     // Install the Filter module.
 
@@ -93,8 +101,8 @@ public function testGetJSSettings() {
       'language' => 'en',
       'stylesSet' => FALSE,
       'drupalExternalPlugins' => [
-        'drupalimage' => file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js')),
-        'drupallink' => file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js')),
+        'drupalimage' => $this->fileUrlGenerator->generateString('core/modules/ckeditor/js/plugins/drupalimage/plugin.js'),
+        'drupallink' => $this->fileUrlGenerator->generateString('core/modules/ckeditor/js/plugins/drupallink/plugin.js'),
       ],
     ];
     $this->assertEquals($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for default configuration.');
@@ -114,9 +122,9 @@ public function testGetJSSettings() {
     $expected_config['toolbar'][0]['items'][] = 'Format';
     $expected_config['format_tags'] = 'p;h2;h3;h4;h5;h6';
     $expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button';
-    $expected_config['drupalExternalPlugins']['llama_contextual'] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js'));
-    $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js'));
-    $expected_config['contentsCss'][] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css')) . $query_string;
+    $expected_config['drupalExternalPlugins']['llama_contextual'] = $this->fileUrlGenerator->generateString('core/modules/ckeditor/tests/modules/js/llama_contextual.js');
+    $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = $this->fileUrlGenerator->generateString('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js');
+    $expected_config['contentsCss'][] = $this->fileUrlGenerator->generateString('core/modules/ckeditor/tests/modules/ckeditor_test.css') . $query_string;
     $this->assertEquals($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
 
     // Change the allowed HTML tags; the "allowedContent" and "format_tags"
@@ -257,7 +265,7 @@ public function testBuildContentsCssJSSetting() {
 
     // Enable the editor_test module, which implements hook_ckeditor_css_alter().
     $this->enableModules(['ckeditor_test']);
-    $expected[] = file_url_transform_relative(file_create_url(drupal_get_path('module', 'ckeditor_test') . '/ckeditor_test.css')) . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString(drupal_get_path('module', 'ckeditor_test') . '/ckeditor_test.css') . $query_string;
     $this->assertSame($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a hook_ckeditor_css_alter() implementation exists.');
 
     // Enable LlamaCss plugin, which adds an additional CKEditor stylesheet.
@@ -269,17 +277,17 @@ public function testBuildContentsCssJSSetting() {
     $settings['toolbar']['rows'][0][0]['items'][] = 'LlamaCSS';
     $editor->setSettings($settings);
     $editor->save();
-    $expected[] = file_url_transform_relative(file_create_url(drupal_get_path('module', 'ckeditor_test') . '/css/llama.css')) . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString(drupal_get_path('module', 'ckeditor_test') . '/css/llama.css') . $query_string;
     $this->assertSame($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a CKEditorPluginInterface implementation exists.');
 
     // Enable the Bartik theme, which specifies a CKEditor stylesheet.
     \Drupal::service('theme_installer')->install(['bartik']);
     $this->config('system.theme')->set('default', 'bartik')->save();
-    $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/base/elements.css')) . $query_string;
-    $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/captions.css')) . $query_string;
-    $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/table.css')) . $query_string;
-    $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/text-formatted.css')) . $query_string;
-    $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/classy/components/media-embed-error.css')) . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString('core/themes/bartik/css/base/elements.css') . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString('core/themes/bartik/css/components/captions.css') . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString('core/themes/bartik/css/components/table.css') . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString('core/themes/bartik/css/components/text-formatted.css') . $query_string;
+    $expected[] = $this->fileUrlGenerator->generateString('core/themes/bartik/css/classy/components/media-embed-error.css') . $query_string;
     $this->assertSame($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a theme providing a CKEditor stylesheet exists.');
   }
 
@@ -543,8 +551,8 @@ protected function getDefaultToolbarConfig() {
   protected function getDefaultContentsCssConfig() {
     $query_string = '?0=';
     return [
-      file_url_transform_relative(file_create_url('core/modules/ckeditor/css/ckeditor-iframe.css')) . $query_string,
-      file_url_transform_relative(file_create_url('core/modules/system/css/components/align.module.css')) . $query_string,
+      $this->fileUrlGenerator->generateString('core/modules/ckeditor/css/ckeditor-iframe.css') . $query_string,
+      $this->fileUrlGenerator->generateString('core/modules/system/css/components/align.module.css') . $query_string,
     ];
   }
 
diff --git a/core/modules/color/color.module b/core/modules/color/color.module
index 712d65e7843b..a2c0cab28e97 100644
--- a/core/modules/color/color.module
+++ b/core/modules/color/color.module
@@ -474,7 +474,7 @@ function color_scheme_form_submit($form, FormStateInterface $form_state) {
     }
 
     foreach ($files as $file) {
-      $css_optimizer = new CssOptimizer();
+      $css_optimizer = new CssOptimizer(\Drupal::service('file_url_generator'));
       // Aggregate @imports recursively for each configured top level CSS file
       // without optimization. Aggregation and optimization will be
       // handled by drupal_build_css_cache() only.
diff --git a/core/modules/color/src/ColorSystemBrandingBlockAlter.php b/core/modules/color/src/ColorSystemBrandingBlockAlter.php
index 6408e1e71f7e..337c6bc9d7f6 100644
--- a/core/modules/color/src/ColorSystemBrandingBlockAlter.php
+++ b/core/modules/color/src/ColorSystemBrandingBlockAlter.php
@@ -25,7 +25,7 @@ public static function preRender($build) {
     // Override logo.
     $logo = $config->get('logo');
     if ($logo && $build['content']['site_logo'] && preg_match('!' . $theme_key . '/logo.svg$!', $build['content']['site_logo']['#uri'])) {
-      $build['content']['site_logo']['#uri'] = file_url_transform_relative(file_create_url($logo));
+      $build['content']['site_logo']['#uri'] = \Drupal::service('file_url_generator')->generateString($logo);
     }
 
     return $build;
diff --git a/core/modules/color/tests/src/Functional/ColorTest.php b/core/modules/color/tests/src/Functional/ColorTest.php
index a900cf2df252..e2072edb2838 100644
--- a/core/modules/color/tests/src/Functional/ColorTest.php
+++ b/core/modules/color/tests/src/Functional/ColorTest.php
@@ -121,9 +121,11 @@ public function _testColor($theme, $test_values) {
 
     $this->drupalGet('<front>');
     $stylesheets = $this->config('color.theme.' . $theme)->get('stylesheets');
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
     // Make sure the color stylesheet is included in the content.
     foreach ($stylesheets as $stylesheet) {
-      $this->assertSession()->responseMatches('|' . file_url_transform_relative(file_create_url($stylesheet)) . '|');
+      $this->assertSession()->responseMatches('|' . $file_url_generator->generateString($stylesheet) . '|');
       $stylesheet_content = implode("\n", file($stylesheet));
       $this->assertStringContainsString('color: #123456', $stylesheet_content, 'Make sure the color we changed is in the color stylesheet. (' . $theme . ')');
     }
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index ba2c6629601d..33a57d9ba935 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -1224,9 +1224,9 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
           break;
 
         case 'url':
-          // Ideally, this would use file_url_transform_relative(), but because
-          // tokens are also often used in e-mails, it's better to keep absolute
-          // file URLs. The 'url.site' cache context is associated to ensure the
+          // Ideally, this would use return a relative URL, but because tokens
+          // are also often used in e-mails, it's better to keep absolute file
+          // URLs. The 'url.site' cache context is associated to ensure the
           // correct absolute URL is used in case of a multisite setup.
           $replacements[$original] = $file->createFileUrl(FALSE);
           $bubbleable_metadata->addCacheContexts(['url.site']);
@@ -1482,13 +1482,9 @@ function template_preprocess_file_link(&$variables) {
   $file = $variables['file'];
   $options = [];
 
-  // @todo Wrap in file_url_transform_relative(). This is currently
-  // impossible. As a work-around, we currently add the 'url.site' cache context
-  // to ensure different file URLs are generated for different sites in a
-  // multisite setup, including HTTP and HTTPS versions of the same site.
-  // Fix in https://www.drupal.org/node/2646744.
-  $url = $file->createFileUrl(FALSE);
-  $variables['#cache']['contexts'][] = 'url.site';
+  /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+  $file_url_generator = \Drupal::service('file_url_generator');
+  $url = $file_url_generator->generate($file->getFileUri());
 
   $mime_type = $file->getMimeType();
   $options['attributes']['type'] = $mime_type;
@@ -1516,7 +1512,7 @@ function template_preprocess_file_link(&$variables) {
   $variables['attributes']->addClass($classes);
   $variables['file_size'] = format_size($file->getSize());
 
-  $variables['link'] = Link::fromTextAndUrl($link_text, Url::fromUri($url, $options))->toRenderable();
+  $variables['link'] = Link::fromTextAndUrl($link_text, $url->setOptions($options))->toRenderable();
 }
 
 /**
diff --git a/core/modules/file/src/ComputedFileUrl.php b/core/modules/file/src/ComputedFileUrl.php
index 2eb012edc659..276f2c7d8f74 100644
--- a/core/modules/file/src/ComputedFileUrl.php
+++ b/core/modules/file/src/ComputedFileUrl.php
@@ -27,7 +27,9 @@ public function getValue() {
     assert($this->getParent()->getEntity() instanceof FileInterface);
 
     $uri = $this->getParent()->getEntity()->getFileUri();
-    $this->url = file_url_transform_relative(file_create_url($uri));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $this->url = $file_url_generator->generateString($uri);
 
     return $this->url;
   }
diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php
index e707b9ae4f84..e6e19a15dd12 100644
--- a/core/modules/file/src/Entity/File.php
+++ b/core/modules/file/src/Entity/File.php
@@ -80,11 +80,9 @@ public function setFileUri($uri) {
    * {@inheritdoc}
    */
   public function createFileUrl($relative = TRUE) {
-    $url = file_create_url($this->getFileUri());
-    if ($relative && $url) {
-      $url = file_url_transform_relative($url);
-    }
-    return $url;
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    return $relative ? $file_url_generator->generateString($this->getFileUri()) : $file_url_generator->generateAbsoluteString($this->getFileUri());
   }
 
   /**
diff --git a/core/modules/file/src/FileInterface.php b/core/modules/file/src/FileInterface.php
index dd4ac32af754..0a565e23c956 100644
--- a/core/modules/file/src/FileInterface.php
+++ b/core/modules/file/src/FileInterface.php
@@ -59,8 +59,7 @@ public function setFileUri($uri);
    * @return string
    *   A string containing a URL that may be used to access the file.
    *
-   * @see file_create_url()
-   * @see file_url_transform_relative()
+   * @see \Drupal\Core\File\FileUrlGeneratorInterface
    */
   public function createFileUrl($relative = TRUE);
 
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php b/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php
index b54b17b5ecd0..f193d4590890 100644
--- a/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php
@@ -6,14 +6,67 @@
 use Drupal\Core\Field\FieldItemInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Base class for file formatters, which allow to link to the file download URL.
  */
 abstract class BaseFieldFileFormatterBase extends FormatterBase {
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
+  /**
+   * Constructs a BaseFieldFileFormatterBase object.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Any third party settings.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, FileUrlGeneratorInterface $file_url_generator = NULL) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+    if (!$file_url_generator) {
+      @trigger_error('Calling BaseFieldFileFormatterBase::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0 and the $file_url_generator argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('file_url_generator')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -47,9 +100,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     $url = NULL;
     // Add support to link to the entity itself.
     if ($this->getSetting('link_to_file')) {
-      // @todo Wrap in file_url_transform_relative(). This is currently
-      // impossible. See below.
-      $url = file_create_url($items->getEntity()->uri->value);
+      $url = $this->fileUrlGenerator->generate($items->getEntity()->getFileUri());
     }
 
     foreach ($items as $delta => $item) {
@@ -59,17 +110,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
         $elements[$delta] = [
           '#type' => 'link',
           '#title' => $view_value,
-          '#url' => Url::fromUri($url),
-          // @todo Remove the 'url.site' cache context by using a relative file
-          // URL (file_url_transform_relative()). This is currently impossible
-          // because #type => link requires a Url object, and Url objects do not
-          // support relative URLs: they require fully qualified URLs. Fix in
-          // https://www.drupal.org/node/2646744.
-          '#cache' => [
-            'contexts' => [
-              'url.site',
-            ],
-          ],
+          '#url' => $url,
         ];
       }
       else {
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php
index 0facb7e03388..7a0cb56f725b 100644
--- a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php
@@ -51,10 +51,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
   protected function viewValue(FieldItemInterface $item) {
     $value = $item->value;
     if ($this->getSetting('file_download_path')) {
-      // @todo Wrap in file_url_transform_relative(). This is currently
-      // impossible. See BaseFieldFileFormatterBase::viewElements(). Fix in
-      // https://www.drupal.org/node/2646744.
-      $value = file_create_url($value);
+      $value = $this->fileUrlGenerator->generateString($value);
     }
     return $value;
   }
diff --git a/core/modules/file/src/Plugin/views/field/File.php b/core/modules/file/src/Plugin/views/field/File.php
index fdf2d5966f41..6a7e1ed42191 100644
--- a/core/modules/file/src/Plugin/views/field/File.php
+++ b/core/modules/file/src/Plugin/views/field/File.php
@@ -2,11 +2,13 @@
 
 namespace Drupal\file\Plugin\views\field;
 
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\ResultRow;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Field handler to provide simple renderer that allows linking to a file.
@@ -17,6 +19,41 @@
  */
 class File extends FieldPluginBase {
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
+  /**
+   * Constructs a File object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, FileUrlGeneratorInterface $file_url_generator = NULL) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    if (!$file_url_generator) {
+      @trigger_error('Calling File::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0 and the $file_url_generator argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('file_url_generator'));
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -64,13 +101,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
   protected function renderLink($data, ResultRow $values) {
     if (!empty($this->options['link_to_file']) && $data !== NULL && $data !== '') {
       $this->options['alter']['make_link'] = TRUE;
-      // @todo Wrap in file_url_transform_relative(). This is currently
-      // impossible. As a work-around, we could add the 'url.site' cache context
-      // to ensure different file URLs are generated for different sites in a
-      // multisite setup, including HTTP and HTTPS versions of the same site.
-      // But unfortunately it's impossible to bubble a cache context here.
-      // Fix in https://www.drupal.org/node/2646744.
-      $this->options['alter']['path'] = file_create_url($this->getValue($values, 'uri'));
+      $this->options['alter']['url'] = $this->fileUrlGenerator->generate($this->getValue($values, 'uri'));
     }
 
     return $data;
diff --git a/core/modules/file/tests/src/Functional/DownloadTest.php b/core/modules/file/tests/src/Functional/DownloadTest.php
index 73d02a2116f7..1792e1d203eb 100644
--- a/core/modules/file/tests/src/Functional/DownloadTest.php
+++ b/core/modules/file/tests/src/Functional/DownloadTest.php
@@ -16,8 +16,16 @@ class DownloadTest extends FileManagedTestBase {
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   protected function setUp(): void {
     parent::setUp();
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
     // Clear out any hook calls.
     file_test_reset();
   }
@@ -28,7 +36,7 @@ protected function setUp(): void {
   public function testPublicFileTransfer() {
     // Test generating a URL to a created file.
     $file = $this->createFile();
-    $url = file_create_url($file->getFileUri());
+    $url = $this->fileUrlGenerator->generateAbsoluteString($file->getFileUri());
     // URLs can't contain characters outside the ASCII set so $filename has to be
     // encoded.
     $filename = $GLOBALS['base_url'] . '/' . \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath() . '/' . rawurlencode($file->getFilename());
@@ -40,7 +48,7 @@ public function testPublicFileTransfer() {
     // Test generating a URL to a shipped file (i.e. a file that is part of
     // Drupal core, a module or a theme, for example a JavaScript file).
     $filepath = 'core/assets/vendor/jquery/jquery.min.js';
-    $url = file_create_url($filepath);
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
     $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath, $url, 'Correctly generated a URL for a shipped file.');
     $response = $http_client->head($url);
     $this->assertEquals(200, $response->getStatusCode(), 'Confirmed that the generated URL is correct by downloading the shipped file.');
@@ -69,7 +77,7 @@ protected function doPrivateFileTransferTest() {
     $file->setPermanent();
     $file->save();
 
-    $url = file_create_url($file->getFileUri());
+    $url = $this->fileUrlGenerator->generateAbsoluteString($file->getFileUri());
 
     // Set file_test access header to allow the download.
     file_test_reset();
@@ -94,7 +102,7 @@ protected function doPrivateFileTransferTest() {
 
     // Try non-existent file.
     file_test_reset();
-    $url = file_create_url('private://' . $this->randomMachineName());
+    $url = $this->fileUrlGenerator->generateAbsoluteString('private://' . $this->randomMachineName());
     $response = $http_client->head($url, ['http_errors' => FALSE]);
     $this->assertSame(404, $response->getStatusCode(), 'Correctly returned 404 response for a non-existent file.');
     // Assert that hook_file_download is not called.
@@ -109,7 +117,7 @@ protected function doPrivateFileTransferTest() {
   }
 
   /**
-   * Tests file_create_url().
+   * Test FileUrlGeneratorInterface::generateString()
    */
   public function testFileCreateUrl() {
     // "Special" ASCII characters.
@@ -139,15 +147,16 @@ public function testFileCreateUrl() {
       $this->checkUrl('public', '', $basename, $base_path . '/' . $public_directory_path . '/' . $basename_encoded);
       $this->checkUrl('private', '', $basename, $base_path . '/' . $script_path . 'system/files/' . $basename_encoded);
     }
-    $this->assertEquals('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', file_create_url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='), t('Generated URL matches expected URL.'));
+    $this->assertEquals('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', $this->fileUrlGenerator->generateString('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', FALSE), t('Generated URL matches expected URL.'));
   }
 
   /**
-   * Download a file from the URL generated by file_create_url().
+   * Download a file from the URL generated by generateString().
    *
    * Create a file with the specified scheme, directory and filename; check that
-   * the URL generated by file_create_url() for the specified file equals the
-   * specified URL; fetch the URL and then compare the contents to the file.
+   * the URL generated by FileUrlGeneratorInterface::generateString() for the
+   * specified file equals the specified URL; fetch the URL and then compare the
+   * contents to the file.
    *
    * @param string $scheme
    *   A scheme, e.g. "public".
@@ -167,7 +176,7 @@ private function checkUrl($scheme, $directory, $filename, $expected_url) {
     \Drupal::service('file_system')->prepareDirectory($directory_uri, FileSystemInterface::CREATE_DIRECTORY);
     $file = $this->createFile($filepath, NULL, $scheme);
 
-    $url = file_create_url($file->getFileUri());
+    $url = $this->fileUrlGenerator->generateAbsoluteString($file->getFileUri());
     $this->assertEquals($expected_url, $url);
 
     if ($scheme == 'private') {
diff --git a/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php b/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php
index 48267e0e0cf7..8772cffd069a 100644
--- a/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php
+++ b/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php
@@ -228,7 +228,7 @@ public function testDescriptionDefaultFileFieldDisplay() {
 
     // Test default formatter.
     $this->drupalGet('node/' . $nid);
-    $this->assertSession()->elementTextContains('xpath', '//a[@href="' . $node->{$field_name}->entity->createFileUrl(FALSE) . '"]', $description);
+    $this->assertSession()->elementTextContains('xpath', '//a[@href="' . $node->{$field_name}->entity->createFileUrl() . '"]', $description);
 
     // Change formatter to "Table of files".
     $display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load('node.' . $type_name . '.default');
@@ -238,7 +238,7 @@ public function testDescriptionDefaultFileFieldDisplay() {
     ])->save();
 
     $this->drupalGet('node/' . $nid);
-    $this->assertSession()->elementTextContains('xpath', '//a[@href="' . $node->{$field_name}->entity->createFileUrl(FALSE) . '"]', $description);
+    $this->assertSession()->elementTextContains('xpath', '//a[@href="' . $node->{$field_name}->entity->createFileUrl() . '"]', $description);
   }
 
 }
diff --git a/core/modules/file/tests/src/Functional/FileFieldRSSContentTest.php b/core/modules/file/tests/src/Functional/FileFieldRSSContentTest.php
index 39729b1ff690..fced38d132a2 100644
--- a/core/modules/file/tests/src/Functional/FileFieldRSSContentTest.php
+++ b/core/modules/file/tests/src/Functional/FileFieldRSSContentTest.php
@@ -65,7 +65,7 @@ public function testFileFieldRSSContent() {
     $this->drupalGet('rss.xml');
     $selector = sprintf(
       '//enclosure[@url="%s" and @length="%s" and @type="%s"]',
-      file_create_url($node_file->getFileUri()),
+      $node_file->createFileUrl(FALSE),
       $node_file->getSize(),
       $node_file->getMimeType()
     );
diff --git a/core/modules/file/tests/src/Functional/FileListingTest.php b/core/modules/file/tests/src/Functional/FileListingTest.php
index 395ec3ec797d..f4081cd03ea8 100644
--- a/core/modules/file/tests/src/Functional/FileListingTest.php
+++ b/core/modules/file/tests/src/Functional/FileListingTest.php
@@ -116,7 +116,7 @@ public function testFileListingPages() {
     foreach ($nodes as $node) {
       $file = File::load($node->file->target_id);
       $this->assertSession()->pageTextContains($file->getFilename());
-      $this->assertSession()->linkByHrefExists(file_create_url($file->getFileUri()));
+      $this->assertSession()->linkByHrefExists($file->createFileUrl());
       $this->assertSession()->linkByHrefExists('admin/content/files/usage/' . $file->id());
     }
     $this->assertSession()->elementTextNotContains('css', 'table.views-table', 'Temporary');
diff --git a/core/modules/file/tests/src/Functional/FilePrivateTest.php b/core/modules/file/tests/src/Functional/FilePrivateTest.php
index 841fb7e86584..e7f8cdfc6550 100644
--- a/core/modules/file/tests/src/Functional/FilePrivateTest.php
+++ b/core/modules/file/tests/src/Functional/FilePrivateTest.php
@@ -57,11 +57,11 @@ public function testPrivateFile() {
     $this->drupalGet('node/' . $node->id());
     $this->assertRaw($node_file->getFilename());
     // Ensure the file can be downloaded.
-    $this->drupalGet(file_create_url($node_file->getFileUri()));
+    $this->drupalGet($node_file->createFileUrl(FALSE));
     $this->assertSession()->statusCodeEquals(200);
     $this->drupalLogOut();
     // Ensure the file cannot be downloaded after logging out.
-    $this->drupalGet(file_create_url($node_file->getFileUri()));
+    $this->drupalGet($node_file->createFileUrl(FALSE));
     $this->assertSession()->statusCodeEquals(403);
 
     // Create a field with no view access. See
@@ -76,7 +76,7 @@ public function testPrivateFile() {
     $node_file = File::load($node->{$no_access_field_name}->target_id);
 
     // Ensure the file cannot be downloaded.
-    $file_url = file_create_url($node_file->getFileUri());
+    $file_url = $node_file->createFileUrl(FALSE);
     $this->drupalGet($file_url);
     $this->assertSession()->statusCodeEquals(403);
 
@@ -151,7 +151,7 @@ public function testPrivateFile() {
     $this->assertTrue($file->isTemporary(), 'File is temporary.');
     $usage = $this->container->get('file.usage')->listUsage($file);
     $this->assertEmpty($usage, 'No file usage found.');
-    $file_url = file_create_url($file->getFileUri());
+    $file_url = $file->createFileUrl(FALSE);
     // Ensure the anonymous uploader has access to the temporary file.
     $this->drupalGet($file_url);
     $this->assertSession()->statusCodeEquals(200);
@@ -181,7 +181,7 @@ public function testPrivateFile() {
     $this->assertTrue($file->isTemporary(), 'File is temporary.');
     $usage = $this->container->get('file.usage')->listUsage($file);
     $this->assertEmpty($usage, 'No file usage found.');
-    $file_url = file_create_url($file->getFileUri());
+    $file_url = $file->createFileUrl(FALSE);
     // Ensure the anonymous uploader has access to the temporary file.
     $this->drupalGet($file_url);
     $this->assertSession()->statusCodeEquals(200);
@@ -204,7 +204,7 @@ public function testPrivateFile() {
     $this->assertTrue($file->isPermanent(), 'File is permanent.');
     $usage = $this->container->get('file.usage')->listUsage($file);
     $this->assertCount(1, $usage, 'File usage found.');
-    $file_url = file_create_url($file->getFileUri());
+    $file_url = $file->createFileUrl(FALSE);
     // Ensure the anonymous uploader has access to the file.
     $this->drupalGet($file_url);
     $this->assertSession()->statusCodeEquals(200);
@@ -231,7 +231,7 @@ public function testPrivateFile() {
     $this->assertTrue($file->isPermanent(), 'File is permanent.');
     $usage = $this->container->get('file.usage')->listUsage($file);
     $this->assertCount(1, $usage, 'File usage found.');
-    $file_url = file_create_url($file->getFileUri());
+    $file_url = $file->createFileUrl(FALSE);
     // Ensure the anonymous uploader cannot access to the file.
     $this->drupalGet($file_url);
     $this->assertSession()->statusCodeEquals(403);
diff --git a/core/modules/file/tests/src/Functional/FileTokenReplaceTest.php b/core/modules/file/tests/src/Functional/FileTokenReplaceTest.php
index 27c146ac81b6..71b9e71f8ea0 100644
--- a/core/modules/file/tests/src/Functional/FileTokenReplaceTest.php
+++ b/core/modules/file/tests/src/Functional/FileTokenReplaceTest.php
@@ -56,7 +56,7 @@ public function testFileTokenReplacement() {
     $tests['[file:path]'] = Html::escape($file->getFileUri());
     $tests['[file:mime]'] = Html::escape($file->getMimeType());
     $tests['[file:size]'] = format_size($file->getSize());
-    $tests['[file:url]'] = Html::escape(file_create_url($file->getFileUri()));
+    $tests['[file:url]'] = Html::escape($file->createFileUrl(FALSE));
     $tests['[file:created]'] = $date_formatter->format($file->getCreatedTime(), 'medium', '', NULL, $language_interface->getId());
     $tests['[file:created:short]'] = $date_formatter->format($file->getCreatedTime(), 'short', '', NULL, $language_interface->getId());
     $tests['[file:changed]'] = $date_formatter->format($file->getChangedTime(), 'medium', '', NULL, $language_interface->getId());
diff --git a/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php
index ec1ca79199fa..4f4e27e1891c 100644
--- a/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php
+++ b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php
@@ -92,7 +92,7 @@ public function testAttributes() {
 
     $this->drupalGet($entity->toUrl());
 
-    $file_url = file_url_transform_relative(file_create_url($file->getFileUri()));
+    $file_url = \Drupal::service('file_url_generator')->generateString($file->getFileUri());
 
     $assert_session = $this->assertSession();
     $assert_session->elementExists('css', "video[autoplay='autoplay'] > source[src='$file_url'][type='video/mp4']");
diff --git a/core/modules/file/tests/src/Functional/Hal/FileHalJsonAnonTest.php b/core/modules/file/tests/src/Functional/Hal/FileHalJsonAnonTest.php
index c40dd8a53a41..cd9dcac8f5c4 100644
--- a/core/modules/file/tests/src/Functional/Hal/FileHalJsonAnonTest.php
+++ b/core/modules/file/tests/src/Functional/Hal/FileHalJsonAnonTest.php
@@ -42,7 +42,7 @@ protected function getExpectedNormalizedEntity() {
 
     $normalization = $this->applyHalFieldNormalization($default_normalization);
 
-    $url = file_create_url($this->entity->getFileUri());
+    $url = $this->entity->createFileUrl(FALSE);
     if ($this->config('hal.settings')->get('bc_file_uri_as_url_normalizer')) {
       $normalization['uri'][0]['value'] = $url;
     }
diff --git a/core/modules/file/tests/src/Functional/Hal/FileUploadHalJsonTestBase.php b/core/modules/file/tests/src/Functional/Hal/FileUploadHalJsonTestBase.php
index 8039ffd05a17..f7491213891f 100644
--- a/core/modules/file/tests/src/Functional/Hal/FileUploadHalJsonTestBase.php
+++ b/core/modules/file/tests/src/Functional/Hal/FileUploadHalJsonTestBase.php
@@ -48,7 +48,7 @@ protected function getExpectedNormalizedEntity($fid = 1, $expected_filename = 'e
           // https://www.drupal.org/project/drupal/issues/2907402 is complete.
           // This link matches what is generated from File::url(), a resource
           // URL is currently not available.
-          'href' => file_create_url($normalization['uri'][0]['value']),
+          'href' => \Drupal::service('file_url_generator')->generateAbsoluteString($normalization['uri'][0]['value']),
         ],
         'type' => [
           'href' => $this->baseUrl . '/rest/type/file/file',
diff --git a/core/modules/file/tests/src/Functional/PrivateFileOnTranslatedEntityTest.php b/core/modules/file/tests/src/Functional/PrivateFileOnTranslatedEntityTest.php
index 70500cb5f2bb..9cde140d8724 100644
--- a/core/modules/file/tests/src/Functional/PrivateFileOnTranslatedEntityTest.php
+++ b/core/modules/file/tests/src/Functional/PrivateFileOnTranslatedEntityTest.php
@@ -98,7 +98,7 @@ public function testPrivateLanguageFile() {
     \Drupal::entityTypeManager()->getStorage('node')->resetCache([$default_language_node->id()]);
     $node = Node::load($default_language_node->id());
     $node_file = File::load($node->{$this->fieldName}->target_id);
-    $this->drupalGet(file_create_url($node_file->getFileUri()));
+    $this->drupalGet($node_file->createFileUrl(FALSE));
     $this->assertSession()->statusCodeEquals(200);
 
     // Translate the node into French.
@@ -126,7 +126,7 @@ public function testPrivateLanguageFile() {
     // Ensure the file attached to the translated node can be downloaded.
     $french_node = $default_language_node->getTranslation('fr');
     $node_file = File::load($french_node->{$this->fieldName}->target_id);
-    $this->drupalGet(file_create_url($node_file->getFileUri()));
+    $this->drupalGet($node_file->createFileUrl(FALSE));
     $this->assertSession()->statusCodeEquals(200);
   }
 
diff --git a/core/modules/file/tests/src/Kernel/FileUrlTest.php b/core/modules/file/tests/src/Kernel/FileUrlTest.php
index 8cc995c4185f..a7ca1ef36681 100644
--- a/core/modules/file/tests/src/Kernel/FileUrlTest.php
+++ b/core/modules/file/tests/src/Kernel/FileUrlTest.php
@@ -21,7 +21,7 @@ public function testFilesUrlWithDifferentHostName() {
     $directory_uri = 'public://' . dirname($filepath);
     \Drupal::service('file_system')->prepareDirectory($directory_uri, FileSystemInterface::CREATE_DIRECTORY);
     $file = $this->createFile($filepath, NULL, 'public');
-    $url = file_create_url($file->getFileUri());
+    $url = $file->createFileUrl(FALSE);
     $expected_url = $test_base_url . '/' . basename($filepath);
     $this->assertSame($url, $expected_url);
   }
diff --git a/core/modules/file/tests/src/Kernel/Formatter/FileEntityFormatterTest.php b/core/modules/file/tests/src/Kernel/Formatter/FileEntityFormatterTest.php
index dac6a3163186..d48a58089a56 100644
--- a/core/modules/file/tests/src/Kernel/Formatter/FileEntityFormatterTest.php
+++ b/core/modules/file/tests/src/Kernel/Formatter/FileEntityFormatterTest.php
@@ -3,7 +3,6 @@
 namespace Drupal\Tests\file\Kernel\Formatter;
 
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
-use Drupal\Core\Url;
 use Drupal\file\Entity\File;
 use Drupal\KernelTests\KernelTestBase;
 
@@ -26,12 +25,19 @@ class FileEntityFormatterTest extends KernelTestBase {
    */
   protected $files;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * {@inheritdoc}
    */
   protected function setUp(): void {
     parent::setUp();
-
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
     $this->installEntitySchema('file');
 
     $this->files = [];
@@ -80,7 +86,7 @@ public function testFormatterFileLink() {
 
     $build = $entity_display->buildMultiple($this->files)[0]['filename'][0];
     $this->assertEquals('file.png', $build['#title']);
-    $this->assertEquals(Url::fromUri(file_create_url('public://file.png')), $build['#url']);
+    $this->assertEquals($this->fileUrlGenerator->generate('public://file.png'), $build['#url']);
   }
 
   /**
@@ -98,12 +104,12 @@ public function testFormatterFileUri() {
 
     $entity_display->setComponent('uri', ['type' => 'file_uri', 'settings' => ['file_download_path' => TRUE]]);
     $build = $entity_display->buildMultiple($this->files)[0]['uri'][0];
-    $this->assertEquals(file_create_url('public://file.png'), $build['#markup']);
+    $this->assertEquals($this->fileUrlGenerator->generateString('public://file.png'), $build['#markup']);
 
     $entity_display->setComponent('uri', ['type' => 'file_uri', 'settings' => ['file_download_path' => TRUE, 'link_to_file' => TRUE]]);
     $build = $entity_display->buildMultiple($this->files)[0]['uri'][0];
-    $this->assertEquals(file_create_url('public://file.png'), $build['#title']);
-    $this->assertEquals(Url::fromUri(file_create_url('public://file.png')), $build['#url']);
+    $this->assertEquals($this->fileUrlGenerator->generateString('public://file.png'), $build['#title']);
+    $this->assertEquals($this->fileUrlGenerator->generate('public://file.png'), $build['#url']);
   }
 
   /**
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 70a603e8c5db..15aad887308d 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -768,11 +768,14 @@ function _filter_html_image_secure_process($text) {
 
   $html_dom = Html::load($text);
   $images = $html_dom->getElementsByTagName('img');
+
+  /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+  $file_url_generator = \Drupal::service('file_url_generator');
   foreach ($images as $image) {
     $src = $image->getAttribute('src');
     // Transform absolute image URLs to relative image URLs: prevent problems on
     // multisite set-ups and prevent mixed content errors.
-    $image->setAttribute('src', file_url_transform_relative($src));
+    $image->setAttribute('src', $file_url_generator->transformRelative($src));
 
     // Verify that $src starts with $base_path.
     // This also ensures that external images cannot be referenced.
diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc
index e43bd48765fb..178e5464b17d 100644
--- a/core/modules/image/image.admin.inc
+++ b/core/modules/image/image.admin.inc
@@ -17,6 +17,10 @@
  *   - style: \Drupal\image\ImageStyleInterface image style being previewed.
  */
 function template_preprocess_image_style_preview(&$variables) {
+
+  /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+  $file_url_generator = \Drupal::service('file_url_generator');
+
   // Style information.
   $style = $variables['style'];
   $variables['style_id'] = $style->id();
@@ -34,7 +38,7 @@ function template_preprocess_image_style_preview(&$variables) {
   $original_path = \Drupal::config('image.settings')->get('preview_image');
   $original_image = $image_factory->get($original_path);
   $variables['original'] = [
-    'url' => file_url_transform_relative(file_create_url($original_path)),
+    'url' => $file_url_generator->generateString($original_path),
     'width' => $original_image->getWidth(),
     'height' => $original_image->getHeight(),
   ];
@@ -55,7 +59,7 @@ function template_preprocess_image_style_preview(&$variables) {
   }
   $preview_image = $image_factory->get($preview_file);
   $variables['derivative'] = [
-    'url' => file_url_transform_relative(file_create_url($preview_file)),
+    'url' => $file_url_generator->generateString($preview_file),
     'width' => $preview_image->getWidth(),
     'height' => $preview_image->getHeight(),
   ];
diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php
index 39e7263fcadb..17d6f751a05b 100644
--- a/core/modules/image/src/Entity/ImageStyle.php
+++ b/core/modules/image/src/Entity/ImageStyle.php
@@ -247,7 +247,9 @@ public function buildUrl($path, $clean_urls = NULL) {
       return Url::fromUri('base:' . $directory_path . '/' . $stream_wrapper_manager::getTarget($uri), ['absolute' => TRUE, 'query' => $token_query])->toString();
     }
 
-    $file_url = file_create_url($uri);
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $file_url = $file_url_generator->generateAbsoluteString($uri);
     // Append the query string with the token, if necessary.
     if ($token_query) {
       $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query);
diff --git a/core/modules/image/src/ImageStyleInterface.php b/core/modules/image/src/ImageStyleInterface.php
index d3306e5d1596..2339858e7613 100644
--- a/core/modules/image/src/ImageStyleInterface.php
+++ b/core/modules/image/src/ImageStyleInterface.php
@@ -56,7 +56,7 @@ public function buildUri($uri);
    *   in an <img> tag. Requesting the URL will cause the image to be created.
    *
    * @see \Drupal\image\Controller\ImageStyleDownloadController::deliver()
-   * @see file_url_transform_relative()
+   * @see \Drupal\Core\File\FileUrlGeneratorInterface::transformRelative()
    */
   public function buildUrl($path, $clean_urls = NULL);
 
diff --git a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
index e350e53529ce..515dd92553c6 100644
--- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
+++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
@@ -43,6 +44,13 @@ class ImageFormatter extends ImageFormatterBase {
    */
   protected $imageStyleStorage;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * Constructs an ImageFormatter object.
    *
@@ -64,11 +72,18 @@ class ImageFormatter extends ImageFormatterBase {
    *   The current user.
    * @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
    *   The image style storage.
+   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
+   *   The file URL generator.
    */
-  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $image_style_storage) {
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $image_style_storage, FileUrlGeneratorInterface $file_url_generator = NULL) {
     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
     $this->currentUser = $current_user;
     $this->imageStyleStorage = $image_style_storage;
+    if (!$file_url_generator) {
+      @trigger_error('Calling ImageFormatter::__construct() without the $file_url_generator argument is deprecated in drupal:9.3.0 and the $file_url_generator argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2940031', E_USER_DEPRECATED);
+      $file_url_generator = \Drupal::service('file_url_generator');
+    }
+    $this->fileUrlGenerator = $file_url_generator;
   }
 
   /**
@@ -84,7 +99,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration['view_mode'],
       $configuration['third_party_settings'],
       $container->get('current_user'),
-      $container->get('entity_type.manager')->getStorage('image_style')
+      $container->get('entity_type.manager')->getStorage('image_style'),
+      $container->get('file_url_generator')
     );
   }
 
@@ -199,16 +215,9 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     }
 
     foreach ($files as $delta => $file) {
-      $cache_contexts = [];
       if (isset($link_file)) {
         $image_uri = $file->getFileUri();
-        // @todo Wrap in file_url_transform_relative(). This is currently
-        // impossible. As a work-around, we currently add the 'url.site' cache
-        // context to ensure different file URLs are generated for different
-        // sites in a multisite setup, including HTTP and HTTPS versions of the
-        // same site. Fix in https://www.drupal.org/node/2646744.
-        $url = Url::fromUri(file_create_url($image_uri));
-        $cache_contexts[] = 'url.site';
+        $url = $this->fileUrlGenerator->generate($image_uri);
       }
       $cache_tags = Cache::mergeTags($base_cache_tags, $file->getCacheTags());
 
@@ -226,7 +235,6 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
         '#url' => $url,
         '#cache' => [
           'tags' => $cache_tags,
-          'contexts' => $cache_contexts,
         ],
       ];
     }
diff --git a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php
index 7109627a43e9..9d70cc644359 100644
--- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php
+++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageUrlFormatter.php
@@ -61,11 +61,12 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
 
     /** @var \Drupal\image\ImageStyleInterface $image_style */
     $image_style = $this->imageStyleStorage->load($this->getSetting('image_style'));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
     /** @var \Drupal\file\FileInterface[] $images */
     foreach ($images as $delta => $image) {
       $image_uri = $image->getFileUri();
-      $url = $image_style ? $image_style->buildUrl($image_uri) : file_create_url($image_uri);
-      $url = file_url_transform_relative($url);
+      $url = $image_style ? $file_url_generator->transformRelative($image_style->buildUrl($image_uri)) : $file_url_generator->generateString($image_uri);
 
       // Add cacheability metadata from the image and image style.
       $cacheability = CacheableMetadata::createFromObject($image);
diff --git a/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php b/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php
index 27f005ed02d7..a8169aaedcd9 100644
--- a/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php
+++ b/core/modules/image/tests/src/Functional/ImageAdminStylesTest.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Tests\image\Functional;
 
-use Drupal\Core\Url;
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Url;
+use Drupal\file\Entity\File;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\image\ImageStyleInterface;
 use Drupal\node\Entity\Node;
-use Drupal\file\Entity\File;
 use Drupal\Tests\TestFileCreationTrait;
 
 /**
@@ -343,8 +343,11 @@ public function testStyleReplacement() {
     $original_uri = File::load($fid)->getFileUri();
 
     // Test that image is displayed using newly created style.
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+
     $this->drupalGet('node/' . $nid);
-    $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)));
+    $this->assertSession()->responseContains($file_url_generator->transformRelative($style->buildUrl($original_uri)));
 
     // Rename the style and make sure the image field is updated.
     $new_style_name = strtolower($this->randomMachineName(10));
@@ -360,7 +363,7 @@ public function testStyleReplacement() {
 
     // Reload the image style using the new name.
     $style = ImageStyle::load($new_style_name);
-    $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)));
+    $this->assertSession()->responseContains($file_url_generator->transformRelative($style->buildUrl($original_uri)));
 
     // Delete the style and choose a replacement style.
     $edit = [
@@ -373,7 +376,7 @@ public function testStyleReplacement() {
 
     $replacement_style = ImageStyle::load('thumbnail');
     $this->drupalGet('node/' . $nid);
-    $this->assertRaw(file_url_transform_relative($replacement_style->buildUrl($original_uri)));
+    $this->assertSession()->responseContains($file_url_generator->transformRelative($replacement_style->buildUrl($original_uri)));
   }
 
   /**
@@ -493,7 +496,7 @@ public function testConfigImport() {
 
     // Test that image is displayed using newly created style.
     $this->drupalGet('node/' . $nid);
-    $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)));
+    $this->assertRaw(\Drupal::service('file_url_generator')->transformRelative($style->buildUrl($original_uri)));
 
     // Copy config to sync, and delete the image style.
     $sync = $this->container->get('config.storage.sync');
diff --git a/core/modules/image/tests/src/Functional/ImageDimensionsTest.php b/core/modules/image/tests/src/Functional/ImageDimensionsTest.php
index 9fa6affb151e..4820e5d14d4d 100644
--- a/core/modules/image/tests/src/Functional/ImageDimensionsTest.php
+++ b/core/modules/image/tests/src/Functional/ImageDimensionsTest.php
@@ -50,7 +50,9 @@ public function testImageDimensions() {
     $style = ImageStyle::create(['name' => 'test', 'label' => 'Test']);
     $style->save();
     $generated_uri = 'public://styles/test/public/' . $file_system->basename($original_uri);
-    $url = file_url_transform_relative($style->buildUrl($original_uri));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $url = $file_url_generator->transformRelative($style->buildUrl($original_uri));
 
     $variables = [
       '#theme' => 'image_style',
@@ -266,7 +268,7 @@ public function testImageDimensions() {
     ];
     // PNG original image. Should be resized to 100x100.
     $generated_uri = 'public://styles/test_uri/public/' . $file_system->basename($original_uri);
-    $url = file_url_transform_relative($style->buildUrl($original_uri));
+    $url = \Drupal::service('file_url_generator')->transformRelative($style->buildUrl($original_uri));
     $this->assertEquals('<img src="' . $url . '" width="100" height="100" alt="" loading="lazy" class="image-style-test-uri" />', $this->getImageTag($variables));
     $this->assertFileDoesNotExist($generated_uri);
     $this->drupalGet($this->getAbsoluteUrl($url));
@@ -279,7 +281,7 @@ public function testImageDimensions() {
     $file = $files[1];
     $original_uri = $file_system->copy($file->uri, 'public://', FileSystemInterface::EXISTS_RENAME);
     $generated_uri = 'public://styles/test_uri/public/' . $file_system->basename($original_uri);
-    $url = file_url_transform_relative($style->buildUrl($original_uri));
+    $url = $file_url_generator->transformRelative($style->buildUrl($original_uri));
     $variables['#uri'] = $original_uri;
     $this->assertEquals('<img src="' . $url . '" width="50" height="50" alt="" loading="lazy" class="image-style-test-uri" />', $this->getImageTag($variables));
     $this->assertFileDoesNotExist($generated_uri);
diff --git a/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php b/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php
index e404e79550b3..168b5dda39aa 100644
--- a/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php
+++ b/core/modules/image/tests/src/Functional/ImageEffect/ConvertTest.php
@@ -49,7 +49,7 @@ public function testConvertFileInRoot() {
     // 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';
     $this->assertFileDoesNotExist($derivative_uri);
-    $url = file_url_transform_relative($image_style->buildUrl($test_uri));
+    $url = \Drupal::service('file_url_generator')->transformRelative($image_style->buildUrl($test_uri));
     $this->drupalGet($this->getAbsoluteUrl($url));
     $this->assertSession()->statusCodeEquals(200);
     $this->assertFileExists($derivative_uri);
diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
index 00b41d2c9f57..8e44c55afd9d 100644
--- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
+++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
@@ -106,6 +106,7 @@ public function _testImageFieldFormatters($scheme) {
     $node = $node_storage->load($nid);
 
     // Test that the default formatter is being used.
+    /** @var \Drupal\file\FileInterface $file */
     $file = $node->{$field_name}->entity;
     $image_uri = $file->getFileUri();
     $image = [
@@ -135,7 +136,7 @@ public function _testImageFieldFormatters($scheme) {
       '#height' => 20,
       '#alt' => $alt,
     ];
-    $default_output = '<a href="' . file_create_url($image_uri) . '">' . $renderer->renderRoot($image) . '</a>';
+    $default_output = '<a href="' . $file->createFileUrl() . '">' . $renderer->renderRoot($image) . '</a>';
     $this->drupalGet('node/' . $nid);
     $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $file->getCacheTags()[0]);
     // @todo Remove in https://www.drupal.org/node/2646744.
@@ -144,7 +145,7 @@ public function _testImageFieldFormatters($scheme) {
     $this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'image_style:');
     $this->assertRaw($default_output);
     // Verify that the image can be downloaded.
-    $this->assertEquals(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.');
+    $this->assertEquals(file_get_contents($test_image->uri), $this->drupalGet($file->createFileUrl(FALSE)), 'File was downloaded successfully.');
     if ($scheme == 'private') {
       // Only verify HTTP headers when using private scheme and the headers are
       // sent by Drupal.
@@ -153,7 +154,7 @@ public function _testImageFieldFormatters($scheme) {
 
       // Log out and ensure the file cannot be accessed.
       $this->drupalLogout();
-      $this->drupalGet(file_create_url($image_uri));
+      $this->drupalGet($file->createFileUrl(FALSE));
       $this->assertSession()->statusCodeEquals(403);
 
       // Log in again.
@@ -178,7 +179,7 @@ public function _testImageFieldFormatters($scheme) {
       '//a[@href=:path]/img[@src=:url and @alt=:alt and @width=:width and @height=:height]',
       [
         ':path' => $node->toUrl()->toString(),
-        ':url' => file_url_transform_relative(file_create_url($image['#uri'])),
+        ':url' => $file->createFileUrl(),
         ':width' => $image['#width'],
         ':height' => $image['#height'],
         ':alt' => $alt,
@@ -221,12 +222,12 @@ public function _testImageFieldFormatters($scheme) {
       'type' => 'image_url',
       'settings' => ['image_style' => ''],
     ];
-    $expected_url = file_url_transform_relative(file_create_url($image_uri));
+    $expected_url = $file->createFileUrl();
     $this->assertEquals($expected_url, $node->{$field_name}->view($display_options)[0]['#markup']);
 
     // Test the image URL formatter with an image style.
     $display_options['settings']['image_style'] = 'thumbnail';
-    $expected_url = file_url_transform_relative(ImageStyle::load('thumbnail')->buildUrl($image_uri));
+    $expected_url = \Drupal::service('file_url_generator')->transformRelative(ImageStyle::load('thumbnail')->buildUrl($image_uri));
     $this->assertEquals($expected_url, $node->{$field_name}->view($display_options)[0]['#markup']);
   }
 
@@ -285,7 +286,8 @@ public function testImageFieldSettings() {
     $node = $node_storage->load($nid);
     $file = $node->{$field_name}->entity;
 
-    $url = file_url_transform_relative(ImageStyle::load('medium')->buildUrl($file->getFileUri()));
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $url = $file_url_generator->transformRelative(ImageStyle::load('medium')->buildUrl($file->getFileUri()));
     $this->assertSession()->elementExists('css', 'img[width=40][height=20][class=image-style-medium][src="' . $url . '"]');
 
     // Add alt/title fields to the image and verify that they are displayed.
diff --git a/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php b/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php
index 9ce52c6ca648..6d89a920d85a 100644
--- a/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php
+++ b/core/modules/image/tests/src/Functional/ImageStylesPathAndUrlTest.php
@@ -322,7 +322,7 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s
     // Check that requesting a nonexistent image does not create any new
     // directories in the file system.
     $directory = $scheme . '://styles/' . $this->style->id() . '/' . $scheme . '/' . $this->randomMachineName();
-    $this->drupalGet(file_create_url($directory . '/' . $this->randomString()));
+    $this->drupalGet(\Drupal::service('file_url_generator')->generateAbsoluteString($directory . '/' . $this->randomString()));
     $this->assertDirectoryDoesNotExist($directory);
   }
 
diff --git a/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php b/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php
index 1ef1942be388..1f9654e71a5d 100644
--- a/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php
+++ b/core/modules/image/tests/src/Kernel/ImageThemeFunctionTest.php
@@ -93,7 +93,7 @@ public function testImageFormatterTheme() {
     // Create a style.
     $style = ImageStyle::create(['name' => 'test', 'label' => 'Test']);
     $style->save();
-    $url = file_url_transform_relative($style->buildUrl($original_uri));
+    $url = \Drupal::service('file_url_generator')->transformRelative($style->buildUrl($original_uri));
 
     // Create a test entity with the image field set.
     $entity = EntityTest::create();
@@ -155,7 +155,7 @@ public function testImageStyleTheme() {
     // Create a style.
     $style = ImageStyle::create(['name' => 'image_test', 'label' => 'Test']);
     $style->save();
-    $url = file_url_transform_relative($style->buildUrl($original_uri));
+    $url = \Drupal::service('file_url_generator')->transformRelative($style->buildUrl($original_uri));
 
     // Create the base element that we'll use in the tests below.
     $base_element = [
diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php
index a444a85185c6..7a123e728ca5 100644
--- a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php
+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php
@@ -286,7 +286,7 @@ protected function createDefaultContent($num_articles, $num_tags, $article_has_i
       }
       if ($article_has_image) {
         $file = File::create([
-          'uri' => 'vfs://' . $random->name() . '.png',
+          'uri' => 'public://' . $random->name() . '.png',
         ]);
         $file->setPermanent();
         $file->save();
diff --git a/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php b/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php
index c7102a33d8c5..f1c0ce3af26b 100644
--- a/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php
+++ b/core/modules/media/tests/src/Functional/Hal/MediaHalJsonAnonTest.php
@@ -178,7 +178,7 @@ protected function getExpectedNormalizedFileEntity() {
           // https://www.drupal.org/project/drupal/issues/2907402 is complete.
           // This link matches what is generated from File::url(), a resource
           // URL is currently not available.
-          'href' => file_create_url($normalization['uri'][0]['value']),
+          'href' => \Drupal::service('file_url_generator')->generateAbsoluteString($normalization['uri'][0]['value']),
         ],
         'type' => [
           'href' => $this->baseUrl . '/rest/type/file/file',
diff --git a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
index f60e345b8d71..6736fb057a4a 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
@@ -869,7 +869,9 @@ public function testLinkability($drupalimage_is_enabled) {
     if ($drupalimage_is_enabled) {
       // Add an image with a link wrapped around it.
       $uri = $this->media->field_media_image->entity->getFileUri();
-      $src = file_url_transform_relative(file_create_url($uri));
+      /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+      $file_url_generator = \Drupal::service('file_url_generator');
+      $src = $file_url_generator->generateString($uri);
       $this->host->body->value .= '<a href="http://www.drupal.org/association"><img alt="drupalimage test image" data-entity-type="" data-entity-uuid="" src="' . $src . '" /></a></p>';
     }
     $this->host->save();
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php
index 3ca198c13493..95b4135e9b60 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php
@@ -110,7 +110,9 @@ public function testMediaDisplay() {
     // Assert that the image src uses the large image style, the label is
     // visually hidden, and there is no link to the image file.
     $media_image = $assert_session->elementExists('css', '.media--type-image img');
-    $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg')));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg'));
     $this->assertStringContainsString($expected_image_src, $media_image->getAttribute('src'));
     $field = $assert_session->elementExists('css', '.field--name-field-media-image');
     $assert_session->elementExists('css', '.field__label.visually-hidden', $field);
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php
index 5b2a929d777f..e4a0f942c4e0 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceImageTest.php
@@ -72,7 +72,9 @@ public function testMediaImageSource() {
     // src attribute uses the large image style, the label is visually hidden,
     // and there is no link to the image file.
     $image_element = $assert_session->elementExists('css', '.field--name-field-media-image img');
-    $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg')));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/example_1.jpeg'));
     $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src'));
     $field = $assert_session->elementExists('css', '.field--name-field-media-image');
     $assert_session->elementExists('css', '.field__label.visually-hidden', $field);
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php
index f98e1b3e284f..728a4444a807 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaStandardProfileTest.php
@@ -153,7 +153,9 @@ protected function audioTest() {
     // Assert the audio file is present inside the media element and that its
     // src attribute matches the audio file.
     $audio_element = $assert_session->elementExists('css', 'article.media--type-audio .field--name-field-media-audio-file audio > source');
-    $expected_audio_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename)));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $expected_audio_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename));
     $this->assertSame($expected_audio_src, $audio_element->getAttribute('src'));
 
     // Assert the media name is updated through the field mapping when changing
@@ -180,7 +182,7 @@ protected function audioTest() {
     // Assert the audio file is present inside the media element and that its
     // src attribute matches the updated audio file.
     $audio_element = $assert_session->elementExists('css', 'article.media--type-audio .field--name-field-media-audio-file audio > source');
-    $expected_audio_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated)));
+    $expected_audio_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated));
     $this->assertSame($expected_audio_src, $audio_element->getAttribute('src'));
   }
 
@@ -240,7 +242,10 @@ protected function imageTest() {
     // src attribute uses the large image style, the label is visually hidden,
     // and there is no link to the image file.
     $image_element = $assert_session->elementExists('css', 'article.media--type-image img');
-    $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name)));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name));
+
     $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src'));
     $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden');
     $assert_session->elementNotExists('css', '.field--name-field-media-image a');
@@ -272,7 +277,7 @@ protected function imageTest() {
     // src attribute uses the large image style, the label is visually hidden,
     // and there is no link to the image file.
     $image_element = $assert_session->elementExists('css', 'article.media--type-image img');
-    $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated)));
+    $expected_image_src = $file_url_generator->generateString(\Drupal::token()->replace('public://styles/large/public/[date:custom:Y]-[date:custom:m]/' . $image_media_name_updated));
     $this->assertStringContainsString($expected_image_src, $image_element->getAttribute('src'));
     $assert_session->elementExists('css', '.field--name-field-media-image .field__label.visually-hidden');
     $assert_session->elementNotExists('css', '.field--name-field-media-image a');
@@ -524,7 +529,9 @@ protected function videoTest() {
     // Assert the video element is present inside the media element and that its
     // src attribute matches the video file.
     $video_element = $assert_session->elementExists('css', 'article.media--type-video .field--name-field-media-video-file video > source');
-    $expected_video_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename)));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $expected_video_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename));
     $this->assertSame($expected_video_src, $video_element->getAttribute('src'));
 
     // Assert the media name is updated through the field mapping when changing
@@ -551,7 +558,7 @@ protected function videoTest() {
     // Assert the video element is present inside the media element and that its
     // src attribute matches the updated video file.
     $video_element = $assert_session->elementExists('css', 'article.media--type-video .field--name-field-media-video-file video > source');
-    $expected_video_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated)));
+    $expected_video_src = $file_url_generator->generateString(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/' . $test_filename_updated));
     $this->assertSame($expected_video_src, $video_element->getAttribute('src'));
   }
 
diff --git a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php
index 3b84023d4f03..91d0cf9c0831 100644
--- a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php
+++ b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php
@@ -104,7 +104,7 @@ public function providerTestBasics() {
             'media:1',
             'media_view',
           ])
-          ->setCacheContexts(['url.site', 'user.permissions'])
+          ->setCacheContexts(['user.permissions'])
           ->setCacheMaxAge(Cache::PERMANENT),
       ],
       'custom attributes are retained' => [
diff --git a/core/modules/node/tests/src/Functional/NodeRSSContentTest.php b/core/modules/node/tests/src/Functional/NodeRSSContentTest.php
index c9a363f568b5..45bea1374b09 100644
--- a/core/modules/node/tests/src/Functional/NodeRSSContentTest.php
+++ b/core/modules/node/tests/src/Functional/NodeRSSContentTest.php
@@ -87,20 +87,22 @@ public function testUrlHandling() {
       'type' => 'article',
       'promote' => 1,
     ];
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
     $this->drupalCreateNode($defaults + [
       'body' => [
-        'value' => '<p><a href="' . file_url_transform_relative(file_create_url('public://root-relative')) . '">Root-relative URL</a></p>',
+        'value' => '<p><a href="' . $file_url_generator->generateString('public://root-relative') . '">Root-relative URL</a></p>',
         'format' => 'full_html',
       ],
     ]);
-    $protocol_relative_url = substr(file_create_url('public://protocol-relative'), strlen(\Drupal::request()->getScheme() . ':'));
+    $protocol_relative_url = substr($file_url_generator->generateAbsoluteString('public://protocol-relative'), strlen(\Drupal::request()->getScheme() . ':'));
     $this->drupalCreateNode($defaults + [
       'body' => [
         'value' => '<p><a href="' . $protocol_relative_url . '">Protocol-relative URL</a></p>',
         'format' => 'full_html',
       ],
     ]);
-    $absolute_url = file_create_url('public://absolute');
+    $absolute_url = $file_url_generator->generateAbsoluteString('public://absolute');
     $this->drupalCreateNode($defaults + [
       'body' => [
         'value' => '<p><a href="' . $absolute_url . '">Absolute URL</a></p>',
@@ -110,7 +112,7 @@ public function testUrlHandling() {
 
     $this->drupalGet('rss.xml');
     // Verify that root-relative URL is transformed to absolute.
-    $this->assertRaw(file_create_url('public://root-relative'));
+    $this->assertSession()->responseContains($file_url_generator->generateAbsoluteString('public://root-relative'));
     // Verify that protocol-relative URL is left untouched.
     $this->assertRaw($protocol_relative_url);
     // Verify that absolute URL is left untouched.
diff --git a/core/modules/rdf/tests/src/Functional/FileFieldAttributesTest.php b/core/modules/rdf/tests/src/Functional/FileFieldAttributesTest.php
index d8e1030d754a..28ffb6b26157 100644
--- a/core/modules/rdf/tests/src/Functional/FileFieldAttributesTest.php
+++ b/core/modules/rdf/tests/src/Functional/FileFieldAttributesTest.php
@@ -101,7 +101,7 @@ public function testNodeTeaser() {
     $html = \Drupal::service('renderer')->renderRoot($node_render_array);
 
     $node_uri = $this->node->toUrl('canonical', ['absolute' => TRUE])->toString();
-    $file_uri = file_create_url($this->file->getFileUri());
+    $file_uri = $this->file->createFileUrl(FALSE);
 
     // Node relation to attached file.
     $expected_value = [
diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module
index 566737104240..4e79255ac467 100644
--- a/core/modules/responsive_image/responsive_image.module
+++ b/core/modules/responsive_image/responsive_image.module
@@ -495,11 +495,15 @@ function _responsive_image_image_style_url($style_name, $path) {
     // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
     return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
   }
+
+  /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+  $file_url_generator = \Drupal::service('file_url_generator');
+
   $entity = ImageStyle::load($style_name);
   if ($entity instanceof ImageStyle) {
-    return file_url_transform_relative($entity->buildUrl($path));
+    return $file_url_generator->transformRelative($entity->buildUrl($path));
   }
-  return file_url_transform_relative(file_create_url($path));
+  return $file_url_generator->generateString($path);
 }
 
 /**
diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php
index 2aa5c15b0a2b..e6656c19b6c8 100644
--- a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php
+++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php
@@ -35,6 +35,13 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase {
    */
   protected $responsiveImgStyle;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * Modules to enable.
    *
@@ -52,6 +59,8 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase {
   protected function setUp(): void {
     parent::setUp();
 
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
+
     // Create user.
     $this->adminUser = $this->drupalCreateUser([
       'administer responsive images',
@@ -259,9 +268,9 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
     // No image style cache tag should be found.
     $this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'image_style:');
 
-    $this->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote(file_url_transform_relative(file_create_url($image_uri)), '/') . '"(.*?)>\s*<picture/');
+    $this->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote($this->fileUrlGenerator->generateString($image_uri), '/') . '"(.*?)>\s*<picture/');
     // Verify that the image can be downloaded.
-    $this->assertEquals(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.');
+    $this->assertEquals(file_get_contents($test_image->uri), $this->drupalGet($this->fileUrlGenerator->generateAbsoluteString($image_uri)), 'File was downloaded successfully.');
     if ($scheme == 'private') {
       // Only verify HTTP headers when using private scheme and the headers are
       // sent by Drupal.
@@ -270,7 +279,7 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
 
       // Log out and ensure the file cannot be accessed.
       $this->drupalLogout();
-      $this->drupalGet(file_create_url($image_uri));
+      $this->drupalGet($this->fileUrlGenerator->generateAbsoluteString($image_uri));
       $this->assertSession()->statusCodeEquals(403);
 
       // Log in again.
@@ -295,10 +304,10 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
       $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
       $thumbnail_style = ImageStyle::load('thumbnail');
       // Assert the output of the 'srcset' attribute (small multipliers first).
-      $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . file_url_transform_relative($thumbnail_style->buildUrl($image_uri)) . ' 1.5x');
+      $this->assertSession()->responseContains('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . $this->fileUrlGenerator->transformRelative($thumbnail_style->buildUrl($image_uri)) . ' 1.5x');
       $this->assertRaw('/styles/medium/');
       // Assert the output of the original image.
-      $this->assertRaw(file_url_transform_relative(file_create_url($image_uri)) . ' 3x');
+      $this->assertSession()->responseContains($this->fileUrlGenerator->generateString($image_uri) . ' 3x');
       // Assert the output of the breakpoints.
       $this->assertRaw('media="(min-width: 0px)"');
       $this->assertRaw('media="(min-width: 560px)"');
@@ -307,7 +316,7 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
       $this->assertSession()->responseMatches('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/');
       // Assert the output of the 'srcset' attribute (small images first).
       $medium_style = ImageStyle::load('medium');
-      $this->assertRaw(file_url_transform_relative($medium_style->buildUrl($image_uri)) . ' 220w, ' . file_url_transform_relative($large_style->buildUrl($image_uri)) . ' 360w');
+      $this->assertSession()->responseContains($this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)) . ' 220w, ' . $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)) . ' 360w');
       $this->assertRaw('media="(min-width: 851px)"');
     }
     $this->assertRaw('/styles/large/');
@@ -324,7 +333,7 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
     $fallback_image = [
       '#theme' => 'image',
       '#alt' => $alt,
-      '#uri' => file_url_transform_relative($large_style->buildUrl($image->getSource())),
+      '#uri' => $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image->getSource())),
     ];
     // The image.html.twig template has a newline after the <img> tag but
     // responsive-image.html.twig doesn't have one after the fallback image, so
@@ -404,7 +413,7 @@ public function testResponsiveImageFieldFormattersEmptyMediaQuery() {
     $thumbnail_style = ImageStyle::load('thumbnail');
     $node = $node_storage->load($nid);
     $image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
-    $this->assertSession()->responseMatches('/srcset="' . preg_quote(file_url_transform_relative($thumbnail_style->buildUrl($image_uri)), '/') . ' 1x".+?media="\(min-width: 0px\)"/');
+    $this->assertSession()->responseMatches('/srcset="' . preg_quote($this->fileUrlGenerator->transformRelative($thumbnail_style->buildUrl($image_uri)), '/') . ' 1x".+?media="\(min-width: 0px\)"/');
   }
 
   /**
@@ -451,7 +460,7 @@ public function testResponsiveImageFieldFormattersOneSource() {
     $medium_style = ImageStyle::load('medium');
     $node = $node_storage->load($nid);
     $image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
-    $this->assertRaw('<img srcset="' . file_url_transform_relative($medium_style->buildUrl($image_uri)) . ' 1x, ' . file_url_transform_relative($large_style->buildUrl($image_uri)) . ' 2x"');
+    $this->assertSession()->responseContains('<img srcset="' . $this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)) . ' 1x, ' . $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)) . ' 2x"');
   }
 
   /**
@@ -513,7 +522,7 @@ private function assertResponsiveImageFieldFormattersLink($link_type) {
     switch ($link_type) {
       case 'file':
         // Make sure the link to the file is present.
-        $this->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote(file_url_transform_relative(file_create_url($image_uri)), '/') . '"(.*?)>\s*<picture/');
+        $this->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote($this->fileUrlGenerator->generateString($image_uri), '/') . '"(.*?)>\s*<picture/');
         break;
 
       case 'content':
diff --git a/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php
index 7c3bd00d019e..8542b58d9113 100644
--- a/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\file\FileInterface;
 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
@@ -59,8 +60,8 @@ public function normalize($field_item, $format = NULL, array $context = []) {
       }
       // @todo Remove in https://www.drupal.org/project/drupal/issues/2925520
       // @see \Drupal\hal\Normalizer\FileEntityNormalizer
-      elseif ($entity->getEntityTypeId() === 'file') {
-        $values['url'] = file_create_url($entity->getFileUri());
+      elseif ($entity instanceof FileInterface) {
+        $values['url'] = $entity->createFileUrl(FALSE);
       }
     }
 
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestDisabledElementsForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestDisabledElementsForm.php
index a756dc50b66f..eef9efae5c85 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestDisabledElementsForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestDisabledElementsForm.php
@@ -209,6 +209,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['image_button'] = [
       '#type' => 'image_button',
       '#value' => 'Image button',
+      '#src' => '',
       '#disabled' => TRUE,
     ];
     $form['button'] = [
diff --git a/core/modules/system/tests/src/Functional/System/RetrieveFileTest.php b/core/modules/system/tests/src/Functional/System/RetrieveFileTest.php
index baf49a5b256f..95fab4c5c8ab 100644
--- a/core/modules/system/tests/src/Functional/System/RetrieveFileTest.php
+++ b/core/modules/system/tests/src/Functional/System/RetrieveFileTest.php
@@ -26,7 +26,7 @@ public function testFileRetrieving() {
     $file_system->mkdir($sourcedir = 'public://' . $this->randomMachineName());
     // cSpell:disable-next-line
     $filename = 'Файл для тестирования ' . $this->randomMachineName();
-    $url = file_create_url($sourcedir . '/' . $filename);
+    $url = \Drupal::service('file_url_generator')->generateAbsoluteString($sourcedir . '/' . $filename);
     $retrieved_file = system_retrieve_file($url);
     $this->assertFalse($retrieved_file, 'Non-existent file not fetched.');
 
diff --git a/core/modules/system/tests/src/Functional/System/ThemeTest.php b/core/modules/system/tests/src/Functional/System/ThemeTest.php
index b5173cbe26e4..e46d7133a3fe 100644
--- a/core/modules/system/tests/src/Functional/System/ThemeTest.php
+++ b/core/modules/system/tests/src/Functional/System/ThemeTest.php
@@ -75,21 +75,23 @@ public function testThemeSettings() {
     $file_relative = strtr($file->uri, ['public:/' => PublicStream::basePath()]);
     $default_theme_path = 'core/themes/classy';
 
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
     $supported_paths = [
       // Raw stream wrapper URI.
       $file->uri => [
         'form' => StreamWrapperManager::getTarget($file->uri),
-        'src' => file_url_transform_relative(file_create_url($file->uri)),
+        'src' => $file_url_generator->generateString($file->uri),
       ],
       // Relative path within the public filesystem.
       StreamWrapperManager::getTarget($file->uri) => [
         'form' => StreamWrapperManager::getTarget($file->uri),
-        'src' => file_url_transform_relative(file_create_url($file->uri)),
+        'src' => $file_url_generator->generateString($file->uri),
       ],
       // Relative path to a public file.
       $file_relative => [
         'form' => $file_relative,
-        'src' => file_url_transform_relative(file_create_url($file->uri)),
+        'src' => $file_url_generator->generateString($file->uri),
       ],
       // Relative path to an arbitrary file.
       'core/misc/druplicon.png' => [
@@ -198,7 +200,7 @@ public function testThemeSettings() {
         ':rel' => 'home',
       ]
     );
-    $this->assertEquals(file_url_transform_relative(file_create_url($uploaded_filename)), $elements[0]->getAttribute('src'));
+    $this->assertEquals($file_url_generator->generateString($uploaded_filename), $elements[0]->getAttribute('src'));
 
     $this->container->get('theme_installer')->install(['bartik']);
 
diff --git a/core/modules/system/tests/src/Functional/Theme/EngineTwigTest.php b/core/modules/system/tests/src/Functional/Theme/EngineTwigTest.php
index 470c28aab55b..fbe9819474b2 100644
--- a/core/modules/system/tests/src/Functional/Theme/EngineTwigTest.php
+++ b/core/modules/system/tests/src/Functional/Theme/EngineTwigTest.php
@@ -133,7 +133,9 @@ public function testTwigUrlToString() {
    */
   public function testTwigFileUrls() {
     $this->drupalGet('/twig-theme-test/file-url');
-    $filepath = file_url_transform_relative(file_create_url('core/modules/system/tests/modules/twig_theme_test/twig_theme_test.js'));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $filepath = $file_url_generator->generateString('core/modules/system/tests/modules/twig_theme_test/twig_theme_test.js');
     $this->assertRaw('<div>file_url: ' . $filepath . '</div>');
   }
 
diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyImageTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyImageTest.php
index adfa53364cc3..38f5be61cf67 100644
--- a/core/modules/taxonomy/tests/src/Functional/TaxonomyImageTest.php
+++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyImageTest.php
@@ -106,13 +106,13 @@ public function testTaxonomyImageAccess() {
 
     // Ensure a user that should be able to access the file can access it.
     $this->drupalLogin($access_user);
-    $this->drupalGet(file_create_url($image->getFileUri()));
+    $this->drupalGet($image->createFileUrl(FALSE));
     $this->assertSession()->statusCodeEquals(200);
 
     // Ensure a user that should not be able to access the file cannot access
     // it.
     $this->drupalLogin($no_access_user);
-    $this->drupalGet(file_create_url($image->getFileUri()));
+    $this->drupalGet($image->createFileUrl(FALSE));
     $this->assertSession()->statusCodeEquals(403);
   }
 
diff --git a/core/modules/user/tests/src/Functional/UserPictureTest.php b/core/modules/user/tests/src/Functional/UserPictureTest.php
index b20c9b4c7eb5..e83d2a214dfc 100644
--- a/core/modules/user/tests/src/Functional/UserPictureTest.php
+++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
@@ -116,7 +116,7 @@ public function testPictureOnNodeComment() {
 
     $image_style_id = $this->config('core.entity_view_display.user.user.compact')->get('content.user_picture.settings.image_style');
     $style = ImageStyle::load($image_style_id);
-    $image_url = file_url_transform_relative($style->buildUrl($file->getfileUri()));
+    $image_url = \Drupal::service('file_url_generator')->transformRelative($style->buildUrl($file->getfileUri()));
     $alt_text = 'Profile picture for user ' . $this->webUser->getAccountName();
 
     // Verify that the image is displayed on the node page.
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
index 1c5a92affdf5..221de328ca7e 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
@@ -35,6 +35,13 @@ class AttachedAssetsTest extends KernelTestBase {
    */
   protected $renderer;
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * {@inheritdoc}
    */
@@ -48,6 +55,7 @@ protected function setUp(): void {
 
     $this->assetResolver = $this->container->get('asset.resolver');
     $this->renderer = $this->container->get('renderer');
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
   }
 
   /**
@@ -88,8 +96,8 @@ public function testAddFiles() {
     $rendered_css = $this->renderer->renderPlain($css_render_array);
     $rendered_js = $this->renderer->renderPlain($js_render_array);
     $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
-    $this->assertStringContainsString('<link rel="stylesheet" media="all" href="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/bar.css')) . '?' . $query_string . '" />', $rendered_css, 'Rendering an external CSS file.');
-    $this->assertStringContainsString('<script src="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/foo.js')) . '?' . $query_string . '"></script>', $rendered_js, 'Rendering an external JavaScript file.');
+    $this->assertStringContainsString('<link rel="stylesheet" media="all" href="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/bar.css') . '?' . $query_string . '" />', $rendered_css, 'Rendering an external CSS file.');
+    $this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/foo.js') . '?' . $query_string . '"></script>', $rendered_js, 'Rendering an external JavaScript file.');
   }
 
   /**
@@ -142,7 +150,7 @@ public function testAttributes() {
     $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
     $rendered_js = $this->renderer->renderPlain($js_render_array);
     $expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
-    $expected_2 = '<script src="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/deferred-internal.js')) . '?v=1" defer bar="foo"></script>';
+    $expected_2 = '<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
     $this->assertStringContainsString($expected_1, $rendered_js, 'Rendered external JavaScript with correct defer and random attributes.');
     $this->assertStringContainsString($expected_2, $rendered_js, 'Rendered internal JavaScript with correct defer and random attributes.');
   }
@@ -158,7 +166,7 @@ public function testAggregatedAttributes() {
     $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
     $rendered_js = $this->renderer->renderPlain($js_render_array);
     $expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
-    $expected_2 = '<script src="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/deferred-internal.js')) . '?v=1" defer bar="foo"></script>';
+    $expected_2 = '<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
     $this->assertStringContainsString($expected_1, $rendered_js, 'Rendered external JavaScript with correct defer and random attributes.');
     $this->assertStringContainsString($expected_2, $rendered_js, 'Rendered internal JavaScript with correct defer and random attributes.');
   }
@@ -232,9 +240,9 @@ public function testHeaderHTML() {
     $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
     $rendered_js = $this->renderer->renderPlain($js_render_array);
     $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
-    $this->assertStringContainsString('<script src="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/header.js')) . '?' . $query_string . '"></script>', $rendered_js, 'The JS asset in common_test/js-header appears in the header.');
-    $this->assertStringContainsString('<script src="' . file_url_transform_relative(file_create_url('core/misc/drupal.js')), $rendered_js, 'The JS asset of the direct dependency (core/drupal) of common_test/js-header appears in the header.');
-    $this->assertStringContainsString('<script src="' . file_url_transform_relative(file_create_url('core/misc/drupalSettingsLoader.js')), $rendered_js, 'The JS asset of the indirect dependency (core/drupalSettings) of common_test/js-header appears in the header.');
+    $this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/header.js') . '?' . $query_string . '"></script>', $rendered_js, 'The JS asset in common_test/js-header appears in the header.');
+    $this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/misc/drupal.js'), $rendered_js, 'The JS asset of the direct dependency (core/drupal) of common_test/js-header appears in the header.');
+    $this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/misc/drupalSettingsLoader.js'), $rendered_js, 'The JS asset of the indirect dependency (core/drupalSettings) of common_test/js-header appears in the header.');
   }
 
   /**
@@ -264,8 +272,8 @@ public function testBrowserConditionalComments() {
     $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
     $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
     $rendered_js = $this->renderer->renderPlain($js_render_array);
-    $expected_1 = "<!--[if lte IE 8]>\n" . '<script src="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/old-ie.js')) . '?' . $default_query_string . '"></script>' . "\n<![endif]-->";
-    $expected_2 = "<!--[if !IE]><!-->\n" . '<script src="' . file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/no-ie.js')) . '?' . $default_query_string . '"></script>' . "\n<!--<![endif]-->";
+    $expected_1 = "<!--[if lte IE 8]>\n" . '<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/old-ie.js') . '?' . $default_query_string . '"></script>' . "\n<![endif]-->";
+    $expected_2 = "<!--[if !IE]><!-->\n" . '<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/no-ie.js') . '?' . $default_query_string . '"></script>' . "\n<!--<![endif]-->";
 
     $this->assertStringContainsString($expected_1, $rendered_js, 'Rendered JavaScript within downlevel-hidden conditional comments.');
     $this->assertStringContainsString($expected_2, $rendered_js, 'Rendered JavaScript within downlevel-revealed conditional comments.');
@@ -475,8 +483,8 @@ public function testAddJsFileWithQueryString() {
     $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
     $rendered_js = $this->renderer->renderPlain($js_render_array);
     $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
-    $this->assertStringContainsString('<link rel="stylesheet" media="all" href="' . str_replace('&', '&amp;', file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2'))) . '&amp;' . $query_string . '" />', $rendered_css, 'CSS file with query string gets version query string correctly appended..');
-    $this->assertStringContainsString('<script src="' . str_replace('&', '&amp;', file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2'))) . '&amp;' . $query_string . '"></script>', $rendered_js, 'JavaScript file with query string gets version query string correctly appended.');
+    $this->assertStringContainsString('<link rel="stylesheet" media="all" href="' . str_replace('&', '&amp;', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '" />', $rendered_css, 'CSS file with query string gets version query string correctly appended..');
+    $this->assertStringContainsString('<script src="' . str_replace('&', '&amp;', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '"></script>', $rendered_js, 'JavaScript file with query string gets version query string correctly appended.');
   }
 
 }
diff --git a/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php b/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php
new file mode 100644
index 000000000000..c383b0ab5924
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/File/FileSystemDeprecationTest.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\KernelTests\Core\File;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests deprecations in file.inc.
+ *
+ * @group File
+ * @group legacy
+ */
+class FileSystemDeprecationTest extends KernelTestBase {
+
+  /**
+   * Tests deprecated FileCreateUrl.
+   */
+  public function testDeprecatedFileCreateUrl() {
+    $this->expectDeprecation('file_create_url() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use the appropriate method on \Drupal\Core\File\FileUrlGeneratorInterface instead. See https://www.drupal.org/node/2940031');
+    $this->expectDeprecation('file_url_transform_relative() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\File\FileUrlGenerator::transformRelative() instead. See https://www.drupal.org/node/2940031');
+    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
+    $url = file_url_transform_relative(file_create_url($filepath));
+    $this->assertNotEmpty($url);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/File/FileUrlGeneratorTest.php b/core/tests/Drupal/KernelTests/Core/File/FileUrlGeneratorTest.php
new file mode 100644
index 000000000000..af8e25e9b525
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/File/FileUrlGeneratorTest.php
@@ -0,0 +1,257 @@
+<?php
+
+namespace Drupal\KernelTests\Core\File;
+
+use Drupal\Core\File\Exception\InvalidStreamWrapperException;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\File\FileUrlGenerator
+ *
+ * @group File
+ */
+class FileUrlGeneratorTest extends FileTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system', 'file_test'];
+
+  /**
+   * The file URL generator under test.
+   *
+   * @var \Drupal\Core\File\FileUrlGenerator
+   */
+  protected $fileUrlGenerator;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
+  }
+
+  /**
+   * Tests missing stream handler.
+   *
+   * @covers ::generate
+   */
+  public function testGenerateMissingStreamWrapper() {
+    $this->expectException(InvalidStreamWrapperException::class);
+    $result = $this->fileUrlGenerator->generate("foo://bar");
+  }
+
+  /**
+   * Tests missing stream handler.
+   *
+   * @covers ::generateString
+   */
+  public function testGenerateStringMissingStreamWrapper() {
+    $this->expectException(InvalidStreamWrapperException::class);
+    $result = $this->fileUrlGenerator->generateString("foo://bar");
+  }
+
+  /**
+   * Tests missing stream handler.
+   *
+   * @covers ::generateAbsoluteString
+   */
+  public function testGenerateAbsoluteStringMissingStreamWrapper() {
+    $this->expectException(InvalidStreamWrapperException::class);
+    $result = $this->fileUrlGenerator->generateAbsoluteString("foo://bar");
+  }
+
+  /**
+   * Tests the rewriting of shipped file URLs by hook_file_url_alter().
+   *
+   * @covers ::generateAbsoluteString
+   */
+  public function testShippedFileURL() {
+    // Test generating a URL to a shipped file (i.e. a file that is part of
+    // Drupal core, a module or a theme, for example a JavaScript file).
+
+    // Test alteration of file URLs to use a CDN.
+    \Drupal::state()->set('file_test.hook_file_url_alter', 'cdn');
+    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertEquals(FILE_URL_TEST_CDN_1 . '/' . $filepath, $url, 'Correctly generated a CDN URL for a shipped file.');
+    $filepath = 'core/misc/favicon.ico';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertEquals(FILE_URL_TEST_CDN_2 . '/' . $filepath, $url, 'Correctly generated a CDN URL for a shipped file.');
+
+    // Test alteration of file URLs to use root-relative URLs.
+    \Drupal::state()->set('file_test.hook_file_url_alter', 'root-relative');
+    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertEquals(base_path() . '/' . $filepath, $url, 'Correctly generated a root-relative URL for a shipped file.');
+    $filepath = 'core/misc/favicon.ico';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertEquals(base_path() . '/' . $filepath, $url, 'Correctly generated a root-relative URL for a shipped file.');
+
+    // Test alteration of file URLs to use protocol-relative URLs.
+    \Drupal::state()->set('file_test.hook_file_url_alter', 'protocol-relative');
+    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertEquals('/' . base_path() . '/' . $filepath, $url, 'Correctly generated a protocol-relative URL for a shipped file.');
+    $filepath = 'core/misc/favicon.ico';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertEquals('/' . base_path() . '/' . $filepath, $url, 'Correctly generated a protocol-relative URL for a shipped file.');
+
+    // Test alteration of file URLs with query strings and/or fragment.
+    \Drupal::state()->delete('file_test.hook_file_url_alter');
+    $filepath = 'core/misc/favicon.ico';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath . '?foo');
+    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '?foo=', $url, 'Correctly generated URL. The query string is present.');
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath . '?foo=bar');
+    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '?foo=bar', $url, 'Correctly generated URL. The query string is present.');
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath . '#v1.2');
+    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '#v1.2', $url, 'Correctly generated URL. The fragment is present.');
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath . '?foo=bar#v1.2');
+    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '?foo=bar#v1.2', $url, 'Correctly generated URL. The query string amd fragment is present.');
+  }
+
+  /**
+   * Tests the rewriting of public managed file URLs by hook_file_url_alter().
+   *
+   * @covers ::generateAbsoluteString
+   */
+  public function testPublicManagedFileURL() {
+    // Test generating a URL to a managed file.
+
+    // Test alteration of file URLs to use a CDN.
+    \Drupal::state()->set('file_test.hook_file_url_alter', 'cdn');
+    $uri = $this->createUri();
+    $url = $this->fileUrlGenerator->generateAbsoluteString($uri);
+    $public_directory_path = \Drupal::service('stream_wrapper_manager')
+      ->getViaScheme('public')
+      ->getDirectoryPath();
+    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+    $file_system = \Drupal::service('file_system');
+    $this->assertEquals(FILE_URL_TEST_CDN_2 . '/' . $public_directory_path . '/' . $file_system->basename($uri), $url, 'Correctly generated a CDN URL for a created file.');
+
+    // Test alteration of file URLs to use root-relative URLs.
+    \Drupal::state()->set('file_test.hook_file_url_alter', 'root-relative');
+    $uri = $this->createUri();
+    $url = $this->fileUrlGenerator->generateAbsoluteString($uri);
+    $this->assertEquals(base_path() . '/' . $public_directory_path . '/' . $file_system->basename($uri), $url, 'Correctly generated a root-relative URL for a created file.');
+
+    // Test alteration of file URLs to use a protocol-relative URLs.
+    \Drupal::state()->set('file_test.hook_file_url_alter', 'protocol-relative');
+    $uri = $this->createUri();
+    $url = $this->fileUrlGenerator->generateAbsoluteString($uri);
+    $this->assertEquals('/' . base_path() . '/' . $public_directory_path . '/' . $file_system->basename($uri), $url, 'Correctly generated a protocol-relative URL for a created file.');
+  }
+
+  /**
+   * Tests generate absolute string with relative URL.
+   *
+   * @covers ::generateAbsoluteString
+   */
+  public function testRelativeFileURL() {
+    // Disable file_test.module's hook_file_url_alter() implementation.
+    \Drupal::state()->set('file_test.hook_file_url_alter', NULL);
+
+    // Create a mock Request for transformRelative().
+    $request = Request::create($GLOBALS['base_url']);
+    $this->container->get('request_stack')->push($request);
+    \Drupal::setContainer($this->container);
+
+    // Shipped file.
+    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
+    $url = $this->fileUrlGenerator->generateAbsoluteString($filepath);
+    $this->assertSame(base_path() . $filepath, $this->fileUrlGenerator->transformRelative($url));
+
+    // Managed file.
+    $uri = $this->createUri();
+    $url = $this->fileUrlGenerator->generateAbsoluteString($uri);
+    $public_directory_path = \Drupal::service('stream_wrapper_manager')
+      ->getViaScheme('public')
+      ->getDirectoryPath();
+    $this->assertSame(base_path() . $public_directory_path . '/' . rawurlencode(\Drupal::service('file_system')
+      ->basename($uri)), $this->fileUrlGenerator->transformRelative($url));
+  }
+
+  /**
+   * @covers ::generate
+   *
+   * @dataProvider providerGenerateURI
+   */
+  public function testGenerateURI($filepath, $expected) {
+    // Disable file_test.module's hook_file_url_alter() implementation.
+    \Drupal::state()->set('file_test.hook_file_url_alter', NULL);
+
+    // Create a mock Request for transformRelative().
+    $request = Request::create($GLOBALS['base_url']);
+    $this->container->get('request_stack')->push($request);
+    \Drupal::setContainer($this->container);
+
+    // No schema file.
+    $url = $this->fileUrlGenerator->generate($filepath);
+    $this->assertEquals($expected, $url->getUri());
+  }
+
+  /**
+   * @covers ::generate
+   */
+  public function testGenerateURIWithSchema() {
+    // Disable file_test.module's hook_file_url_alter() implementation.
+    \Drupal::state()->set('file_test.hook_file_url_alter', NULL);
+
+    // Create a mock Request for transformRelative().
+    $request = Request::create($GLOBALS['base_url']);
+    $this->container->get('request_stack')->push($request);
+    \Drupal::setContainer($this->container);
+
+    $public_directory_path = \Drupal::service('stream_wrapper_manager')
+      ->getViaScheme('public')
+      ->getDirectoryPath();
+
+    $url = $this->fileUrlGenerator->generate('public://path/to/file.png');
+    $this->assertEquals('base:/' . $public_directory_path . '/path/to/file.png', $url->getUri());
+  }
+
+  /**
+   * Data provider.
+   */
+  public function providerGenerateURI() {
+    return [
+      'schemaless' =>
+        [
+          '//core/assets/vendor/jquery/jquery.min.js',
+          '//core/assets/vendor/jquery/jquery.min.js',
+        ],
+      'query string' =>
+        [
+          '//core/assets/vendor/jquery/jquery.min.js?foo',
+          '//core/assets/vendor/jquery/jquery.min.js?foo',
+        ],
+      'query string and hashes' =>
+        [
+          '//core/assets/vendor/jquery/jquery.min.js?foo=bar#whizz',
+          '//core/assets/vendor/jquery/jquery.min.js?foo=bar#whizz',
+        ],
+      'hashes' =>
+        [
+          '//core/assets/vendor/jquery/jquery.min.js#whizz',
+          '//core/assets/vendor/jquery/jquery.min.js#whizz',
+        ],
+      'root-relative' =>
+        [
+          '/core/assets/vendor/jquery/jquery.min.js',
+          'base:/core/assets/vendor/jquery/jquery.min.js',
+        ],
+      'relative' =>
+        [
+          'core/assets/vendor/jquery/jquery.min.js',
+          'base:core/assets/vendor/jquery/jquery.min.js',
+        ],
+      'external' =>
+        [
+          'https://www.example.com/core/assets/vendor/jquery/jquery.min.js',
+          'https://www.example.com/core/assets/vendor/jquery/jquery.min.js',
+        ],
+    ];
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/File/StreamWrapperTest.php b/core/tests/Drupal/KernelTests/Core/File/StreamWrapperTest.php
index 845cebef9f3b..6576c72c0607 100644
--- a/core/tests/Drupal/KernelTests/Core/File/StreamWrapperTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/StreamWrapperTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\StreamWrapper\PublicStream;
 use Symfony\Component\HttpFoundation\Request;
@@ -98,12 +99,14 @@ public function testUriFunctions() {
     $config->set('default_scheme', 'private')->save();
     $this->assertEquals('private://foo/bar.txt', file_build_uri('foo/bar.txt'), 'Got a valid URI from foo/bar.txt.');
 
-    // Test file_create_url()
+    // Test FileUrlGeneratorInterface::generateString()
     // TemporaryStream::getExternalUrl() uses Url::fromRoute(), which needs
     // route information to work.
-    $this->assertStringContainsString('system/temporary?file=test.txt', file_create_url('temporary://test.txt'), 'Temporary external URL correctly built.');
-    $this->assertStringContainsString(Settings::get('file_public_path') . '/test.txt', file_create_url('public://test.txt'), 'Public external URL correctly built.');
-    $this->assertStringContainsString('system/files/test.txt', file_create_url('private://test.txt'), 'Private external URL correctly built.');
+    $file_url_generator = $this->container->get('file_url_generator');
+    assert($file_url_generator instanceof FileUrlGeneratorInterface);
+    $this->assertStringContainsString('system/temporary?file=test.txt', $file_url_generator->generateString('temporary://test.txt'), 'Temporary external URL correctly built.');
+    $this->assertStringContainsString(Settings::get('file_public_path') . '/test.txt', $file_url_generator->generateString('public://test.txt'), 'Public external URL correctly built.');
+    $this->assertStringContainsString('system/files/test.txt', $file_url_generator->generateString('private://test.txt'), 'Private external URL correctly built.');
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/File/UrlRewritingTest.php b/core/tests/Drupal/KernelTests/Core/File/UrlRewritingTest.php
deleted file mode 100644
index fdabb1a3f83e..000000000000
--- a/core/tests/Drupal/KernelTests/Core/File/UrlRewritingTest.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-
-namespace Drupal\KernelTests\Core\File;
-
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Tests for file URL rewriting.
- *
- * @group File
- */
-class UrlRewritingTest extends FileTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  protected static $modules = ['file_test'];
-
-  /**
-   * Tests the rewriting of shipped file URLs by hook_file_url_alter().
-   */
-  public function testShippedFileURL() {
-    // Test generating a URL to a shipped file (i.e. a file that is part of
-    // Drupal core, a module or a theme, for example a JavaScript file).
-
-    // Test alteration of file URLs to use a CDN.
-    \Drupal::state()->set('file_test.hook_file_url_alter', 'cdn');
-    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
-    $url = file_create_url($filepath);
-    $this->assertEquals(FILE_URL_TEST_CDN_1 . '/' . $filepath, $url, 'Correctly generated a CDN URL for a shipped file.');
-    $filepath = 'core/misc/favicon.ico';
-    $url = file_create_url($filepath);
-    $this->assertEquals(FILE_URL_TEST_CDN_2 . '/' . $filepath, $url, 'Correctly generated a CDN URL for a shipped file.');
-
-    // Test alteration of file URLs to use root-relative URLs.
-    \Drupal::state()->set('file_test.hook_file_url_alter', 'root-relative');
-    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
-    $url = file_create_url($filepath);
-    $this->assertEquals(base_path() . '/' . $filepath, $url, 'Correctly generated a root-relative URL for a shipped file.');
-    $filepath = 'core/misc/favicon.ico';
-    $url = file_create_url($filepath);
-    $this->assertEquals(base_path() . '/' . $filepath, $url, 'Correctly generated a root-relative URL for a shipped file.');
-
-    // Test alteration of file URLs to use protocol-relative URLs.
-    \Drupal::state()->set('file_test.hook_file_url_alter', 'protocol-relative');
-    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
-    $url = file_create_url($filepath);
-    $this->assertEquals('/' . base_path() . '/' . $filepath, $url, 'Correctly generated a protocol-relative URL for a shipped file.');
-    $filepath = 'core/misc/favicon.ico';
-    $url = file_create_url($filepath);
-    $this->assertEquals('/' . base_path() . '/' . $filepath, $url, 'Correctly generated a protocol-relative URL for a shipped file.');
-
-    // Test alteration of file URLs with query strings and/or fragment.
-    \Drupal::state()->delete('file_test.hook_file_url_alter');
-    $filepath = 'core/misc/favicon.ico';
-    $url = file_create_url($filepath . '?foo');
-    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '?foo=', $url, 'Correctly generated URL. The query string is present.');
-    $url = file_create_url($filepath . '?foo=bar');
-    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '?foo=bar', $url, 'Correctly generated URL. The query string is present.');
-    $url = file_create_url($filepath . '#v1.2');
-    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '#v1.2', $url, 'Correctly generated URL. The fragment is present.');
-    $url = file_create_url($filepath . '?foo=bar#v1.2');
-    $this->assertEquals($GLOBALS['base_url'] . '/' . $filepath . '?foo=bar#v1.2', $url, 'Correctly generated URL. The query string amd fragment is present.');
-  }
-
-  /**
-   * Tests the rewriting of public managed file URLs by hook_file_url_alter().
-   */
-  public function testPublicManagedFileURL() {
-    // Test generating a URL to a managed file.
-
-    // Test alteration of file URLs to use a CDN.
-    \Drupal::state()->set('file_test.hook_file_url_alter', 'cdn');
-    $uri = $this->createUri();
-    $url = file_create_url($uri);
-    $public_directory_path = \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath();
-    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
-    $file_system = \Drupal::service('file_system');
-    $this->assertEquals(FILE_URL_TEST_CDN_2 . '/' . $public_directory_path . '/' . $file_system->basename($uri), $url, 'Correctly generated a CDN URL for a created file.');
-
-    // Test alteration of file URLs to use root-relative URLs.
-    \Drupal::state()->set('file_test.hook_file_url_alter', 'root-relative');
-    $uri = $this->createUri();
-    $url = file_create_url($uri);
-    $this->assertEquals(base_path() . '/' . $public_directory_path . '/' . $file_system->basename($uri), $url, 'Correctly generated a root-relative URL for a created file.');
-
-    // Test alteration of file URLs to use a protocol-relative URLs.
-    \Drupal::state()->set('file_test.hook_file_url_alter', 'protocol-relative');
-    $uri = $this->createUri();
-    $url = file_create_url($uri);
-    $this->assertEquals('/' . base_path() . '/' . $public_directory_path . '/' . $file_system->basename($uri), $url, 'Correctly generated a protocol-relative URL for a created file.');
-  }
-
-  /**
-   * Tests file_url_transform_relative().
-   */
-  public function testRelativeFileURL() {
-    // Disable file_test.module's hook_file_url_alter() implementation.
-    \Drupal::state()->set('file_test.hook_file_url_alter', NULL);
-
-    // Create a mock Request for file_url_transform_relative().
-    $request = Request::create($GLOBALS['base_url']);
-    $this->container->get('request_stack')->push($request);
-    \Drupal::setContainer($this->container);
-
-    // Shipped file.
-    $filepath = 'core/assets/vendor/jquery/jquery.min.js';
-    $url = file_create_url($filepath);
-    $this->assertSame(base_path() . $filepath, file_url_transform_relative($url));
-
-    // Managed file.
-    $uri = $this->createUri();
-    $url = file_create_url($uri);
-    $public_directory_path = \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath();
-    $this->assertSame(base_path() . $public_directory_path . '/' . rawurlencode(\Drupal::service('file_system')->basename($uri)), file_url_transform_relative($url));
-  }
-
-}
diff --git a/core/tests/Drupal/KernelTests/Core/File/UrlTransformRelativeTest.php b/core/tests/Drupal/KernelTests/Core/File/UrlTransformRelativeTest.php
index 1b2153b1da18..d6f2d00dc18a 100644
--- a/core/tests/Drupal/KernelTests/Core/File/UrlTransformRelativeTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/UrlTransformRelativeTest.php
@@ -15,7 +15,7 @@ class UrlTransformRelativeTest extends KernelTestBase {
   protected static $modules = ['file_test'];
 
   /**
-   * Tests file_url_transform_relative function.
+   * Tests transformRelative() function.
    *
    * @dataProvider providerFileUrlTransformRelative
    */
@@ -37,7 +37,7 @@ public function testFileUrlTransformRelative($host, $port, $https, $url, $expect
     $request = Request::createFromGlobals();
     \Drupal::requestStack()->push($request);
 
-    $this->assertSame($expected, file_url_transform_relative($url));
+    $this->assertSame($expected, \Drupal::service('file_url_generator')->transformRelative($url));
   }
 
   public function providerFileUrlTransformRelative() {
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ImageTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ImageTest.php
index 6e69db873318..ecbf860228ab 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/ImageTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/ImageTest.php
@@ -19,6 +19,13 @@ class ImageTest extends KernelTestBase {
    */
   protected static $modules = ['system'];
 
+  /**
+   * The file URL generator.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface
+   */
+  protected $fileUrlGenerator;
+
   /**
    * The images to test with.
    *
@@ -29,13 +36,15 @@ class ImageTest extends KernelTestBase {
   protected function setUp(): void {
     parent::setUp();
 
-    // The code under test uses file_url_transform_relative(), which relies on
+    // The code under test uses transformRelative(), which relies on
     // the Request containing the correct hostname. KernelTestBase doesn't set
     // it, so push another request onto the stack to ensure it's correct.
     $request = Request::create('/', 'GET', [], [], [], $_SERVER);
     $this->container = \Drupal::service('kernel')->getContainer();
     $this->container->get('request_stack')->push($request);
 
+    $this->fileUrlGenerator = $this->container->get('file_url_generator');
+
     $this->testImages = [
       'core/misc/druplicon.png',
       'core/misc/loading.gif',
@@ -79,7 +88,9 @@ public function testThemeImageWithSrc() {
     $this->render($image);
 
     // Make sure the src attribute has the correct value.
-    $this->assertRaw(file_url_transform_relative(file_create_url($image['#uri'])), 'Correct output for an image with the src attribute.');
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $this->fileUrlGenerator */
+    $this->fileUrlGenerator = $this->fileUrlGenerator;
+    $this->assertRaw($this->fileUrlGenerator->generateString($image['#uri']), 'Correct output for an image with the src attribute.');
   }
 
   /**
@@ -107,7 +118,7 @@ public function testThemeImageWithSrcsetMultiplier() {
     $this->render($image);
 
     // Make sure the srcset attribute has the correct value.
-    $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' 1x, ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' 2x', 'Correct output for image with srcset attribute and multipliers.');
+    $this->assertRaw($this->fileUrlGenerator->transformRelative($this->fileUrlGenerator->generateString($this->testImages[0])) . ' 1x, ' . $this->fileUrlGenerator->transformRelative($this->fileUrlGenerator->generateString($this->testImages[1])) . ' 2x', 'Correct output for image with srcset attribute and multipliers.');
   }
 
   /**
@@ -139,7 +150,7 @@ public function testThemeImageWithSrcsetWidth() {
     $this->render($image);
 
     // Make sure the srcset attribute has the correct value.
-    $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' ' . $widths[0] . ', ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' ' . $widths[1], 'Correct output for image with srcset attribute and width descriptors.');
+    $this->assertRaw($this->fileUrlGenerator->generateString($this->testImages[0]) . ' ' . $widths[0] . ', ' . $this->fileUrlGenerator->transformRelative($this->fileUrlGenerator->generateString($this->testImages[1])) . ' ' . $widths[1], 'Correct output for image with srcset attribute and width descriptors.');
   }
 
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php
index 924005304d34..f5ce5cfec567 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php
@@ -85,7 +85,9 @@ public function testLogoConfig() {
     theme_settings_convert_to_config($values, $config)->save();
 
     // Tests logo path with scheme.
-    $expected = file_url_transform_relative(file_create_url('public://logo_with_scheme.png'));
+    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
+    $file_url_generator = \Drupal::service('file_url_generator');
+    $expected = $file_url_generator->generateString('public://logo_with_scheme.png');
     $this->assertEquals($expected, theme_get_setting('logo.url', 'stark'));
 
     $values = [
diff --git a/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php b/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php
index b95164fc17a3..4288598a592a 100644
--- a/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php
+++ b/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php
@@ -123,8 +123,8 @@ protected function htmlOutput($message = NULL) {
     $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html';
     file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message);
     file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++);
-    // Do not use file_create_url() as the module_handler service might not be
-    // available.
+    // Do not use the file_url_generator service as the module_handler service
+    // might not be available.
     $uri = $this->htmlOutputBaseUrl . '/sites/simpletest/browser_output/' . $html_output_filename;
     file_put_contents($this->htmlOutputFile, $uri . "\n", FILE_APPEND);
   }
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
index 39d65bfa4e9f..cd1d6687962a 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\Core\Asset;
 
 use Drupal\Core\Asset\CssCollectionRenderer;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\State\StateInterface;
 
@@ -33,8 +34,15 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
   protected function setUp(): void {
     parent::setUp();
     $state = $this->prophesize(StateInterface::class);
+    $file_url_generator = $this->createMock(FileUrlGeneratorInterface::class);
+    $file_url_generator->expects($this->any())
+      ->method('generateString')
+      ->with($this->isType('string'))
+      ->willReturnCallback(function ($uri) {
+         return 'generated-relative-url:' . $uri;
+      });
     $state->get('system.css_js_query_string', '0')->shouldBeCalledOnce()->willReturn(NULL);
-    $this->renderer = new CssCollectionRenderer($state->reveal());
+    $this->renderer = new CssCollectionRenderer($state->reveal(), $file_url_generator);
     $this->fileCssGroup = [
       'group' => -100,
       'type' => 'file',
@@ -110,7 +118,7 @@ public function providerTestRender() {
           0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all', 'browsers' => []],
         ],
         [
-          0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/file-all')) . '?', 'all'),
+          0 => $create_link_element('generated-relative-url:public://css/file-all' . '?', 'all'),
         ],
       ],
       // Single file CSS asset with custom attributes.
@@ -119,7 +127,7 @@ public function providerTestRender() {
           0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all', 'browsers' => [], 'attributes' => $custom_attributes],
         ],
         [
-          0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/file-all')) . '?', 'all', [], $custom_attributes),
+          0 => $create_link_element('generated-relative-url:public://css/file-all' . '?', 'all', [], $custom_attributes),
         ],
       ],
       // 31 file CSS assets: expect 31 link elements.
@@ -158,37 +166,37 @@ public function providerTestRender() {
           30 => $create_file_css_asset('public://css/31.css'),
         ],
         [
-          0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/1.css')) . '?'),
-          1 => $create_link_element(file_url_transform_relative(file_create_url('public://css/2.css')) . '?'),
-          2 => $create_link_element(file_url_transform_relative(file_create_url('public://css/3.css')) . '?'),
-          3 => $create_link_element(file_url_transform_relative(file_create_url('public://css/4.css')) . '?'),
-          4 => $create_link_element(file_url_transform_relative(file_create_url('public://css/5.css')) . '?'),
-          5 => $create_link_element(file_url_transform_relative(file_create_url('public://css/6.css')) . '?'),
-          6 => $create_link_element(file_url_transform_relative(file_create_url('public://css/7.css')) . '?'),
-          7 => $create_link_element(file_url_transform_relative(file_create_url('public://css/8.css')) . '?'),
-          8 => $create_link_element(file_url_transform_relative(file_create_url('public://css/9.css')) . '?'),
-          9 => $create_link_element(file_url_transform_relative(file_create_url('public://css/10.css')) . '?'),
-          10 => $create_link_element(file_url_transform_relative(file_create_url('public://css/11.css')) . '?'),
-          11 => $create_link_element(file_url_transform_relative(file_create_url('public://css/12.css')) . '?'),
-          12 => $create_link_element(file_url_transform_relative(file_create_url('public://css/13.css')) . '?'),
-          13 => $create_link_element(file_url_transform_relative(file_create_url('public://css/14.css')) . '?'),
-          14 => $create_link_element(file_url_transform_relative(file_create_url('public://css/15.css')) . '?'),
-          15 => $create_link_element(file_url_transform_relative(file_create_url('public://css/16.css')) . '?'),
-          16 => $create_link_element(file_url_transform_relative(file_create_url('public://css/17.css')) . '?'),
-          17 => $create_link_element(file_url_transform_relative(file_create_url('public://css/18.css')) . '?'),
-          18 => $create_link_element(file_url_transform_relative(file_create_url('public://css/19.css')) . '?'),
-          19 => $create_link_element(file_url_transform_relative(file_create_url('public://css/20.css')) . '?'),
-          20 => $create_link_element(file_url_transform_relative(file_create_url('public://css/21.css')) . '?'),
-          21 => $create_link_element(file_url_transform_relative(file_create_url('public://css/22.css')) . '?'),
-          22 => $create_link_element(file_url_transform_relative(file_create_url('public://css/23.css')) . '?'),
-          23 => $create_link_element(file_url_transform_relative(file_create_url('public://css/24.css')) . '?'),
-          24 => $create_link_element(file_url_transform_relative(file_create_url('public://css/25.css')) . '?'),
-          25 => $create_link_element(file_url_transform_relative(file_create_url('public://css/26.css')) . '?'),
-          26 => $create_link_element(file_url_transform_relative(file_create_url('public://css/27.css')) . '?'),
-          27 => $create_link_element(file_url_transform_relative(file_create_url('public://css/28.css')) . '?'),
-          28 => $create_link_element(file_url_transform_relative(file_create_url('public://css/29.css')) . '?'),
-          29 => $create_link_element(file_url_transform_relative(file_create_url('public://css/30.css')) . '?'),
-          30 => $create_link_element(file_url_transform_relative(file_create_url('public://css/31.css')) . '?'),
+          0 => $create_link_element('generated-relative-url:public://css/1.css' . '?'),
+          1 => $create_link_element('generated-relative-url:public://css/2.css' . '?'),
+          2 => $create_link_element('generated-relative-url:public://css/3.css' . '?'),
+          3 => $create_link_element('generated-relative-url:public://css/4.css' . '?'),
+          4 => $create_link_element('generated-relative-url:public://css/5.css' . '?'),
+          5 => $create_link_element('generated-relative-url:public://css/6.css' . '?'),
+          6 => $create_link_element('generated-relative-url:public://css/7.css' . '?'),
+          7 => $create_link_element('generated-relative-url:public://css/8.css' . '?'),
+          8 => $create_link_element('generated-relative-url:public://css/9.css' . '?'),
+          9 => $create_link_element('generated-relative-url:public://css/10.css' . '?'),
+          10 => $create_link_element('generated-relative-url:public://css/11.css' . '?'),
+          11 => $create_link_element('generated-relative-url:public://css/12.css' . '?'),
+          12 => $create_link_element('generated-relative-url:public://css/13.css' . '?'),
+          13 => $create_link_element('generated-relative-url:public://css/14.css' . '?'),
+          14 => $create_link_element('generated-relative-url:public://css/15.css' . '?'),
+          15 => $create_link_element('generated-relative-url:public://css/16.css' . '?'),
+          16 => $create_link_element('generated-relative-url:public://css/17.css' . '?'),
+          17 => $create_link_element('generated-relative-url:public://css/18.css' . '?'),
+          18 => $create_link_element('generated-relative-url:public://css/19.css' . '?'),
+          19 => $create_link_element('generated-relative-url:public://css/20.css' . '?'),
+          20 => $create_link_element('generated-relative-url:public://css/21.css' . '?'),
+          21 => $create_link_element('generated-relative-url:public://css/22.css' . '?'),
+          22 => $create_link_element('generated-relative-url:public://css/23.css' . '?'),
+          23 => $create_link_element('generated-relative-url:public://css/24.css' . '?'),
+          24 => $create_link_element('generated-relative-url:public://css/25.css' . '?'),
+          25 => $create_link_element('generated-relative-url:public://css/26.css' . '?'),
+          26 => $create_link_element('generated-relative-url:public://css/27.css' . '?'),
+          27 => $create_link_element('generated-relative-url:public://css/28.css' . '?'),
+          28 => $create_link_element('generated-relative-url:public://css/29.css' . '?'),
+          29 => $create_link_element('generated-relative-url:public://css/30.css' . '?'),
+          30 => $create_link_element('generated-relative-url:public://css/31.css' . '?'),
         ],
       ],
       // 32 file CSS assets with the same properties, except for the 10th and
@@ -229,38 +237,38 @@ public function providerTestRender() {
           31 => $create_file_css_asset('public://css/32.css'),
         ],
         [
-          0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/1.css')) . '?'),
-          1 => $create_link_element(file_url_transform_relative(file_create_url('public://css/2.css')) . '?'),
-          2 => $create_link_element(file_url_transform_relative(file_create_url('public://css/3.css')) . '?'),
-          3 => $create_link_element(file_url_transform_relative(file_create_url('public://css/4.css')) . '?'),
-          4 => $create_link_element(file_url_transform_relative(file_create_url('public://css/5.css')) . '?'),
-          5 => $create_link_element(file_url_transform_relative(file_create_url('public://css/6.css')) . '?'),
-          6 => $create_link_element(file_url_transform_relative(file_create_url('public://css/7.css')) . '?'),
-          7 => $create_link_element(file_url_transform_relative(file_create_url('public://css/8.css')) . '?'),
-          8 => $create_link_element(file_url_transform_relative(file_create_url('public://css/9.css')) . '?'),
-          9 => $create_link_element(file_url_transform_relative(file_create_url('public://css/10.css')) . '?', 'screen'),
-          10 => $create_link_element(file_url_transform_relative(file_create_url('public://css/11.css')) . '?'),
-          11 => $create_link_element(file_url_transform_relative(file_create_url('public://css/12.css')) . '?'),
-          12 => $create_link_element(file_url_transform_relative(file_create_url('public://css/13.css')) . '?'),
-          13 => $create_link_element(file_url_transform_relative(file_create_url('public://css/14.css')) . '?'),
-          14 => $create_link_element(file_url_transform_relative(file_create_url('public://css/15.css')) . '?'),
-          15 => $create_link_element(file_url_transform_relative(file_create_url('public://css/16.css')) . '?'),
-          16 => $create_link_element(file_url_transform_relative(file_create_url('public://css/17.css')) . '?'),
-          17 => $create_link_element(file_url_transform_relative(file_create_url('public://css/18.css')) . '?'),
-          18 => $create_link_element(file_url_transform_relative(file_create_url('public://css/19.css')) . '?'),
-          19 => $create_link_element(file_url_transform_relative(file_create_url('public://css/20.css')) . '?', 'print'),
-          20 => $create_link_element(file_url_transform_relative(file_create_url('public://css/21.css')) . '?'),
-          21 => $create_link_element(file_url_transform_relative(file_create_url('public://css/22.css')) . '?'),
-          22 => $create_link_element(file_url_transform_relative(file_create_url('public://css/23.css')) . '?'),
-          23 => $create_link_element(file_url_transform_relative(file_create_url('public://css/24.css')) . '?'),
-          24 => $create_link_element(file_url_transform_relative(file_create_url('public://css/25.css')) . '?'),
-          25 => $create_link_element(file_url_transform_relative(file_create_url('public://css/26.css')) . '?'),
-          26 => $create_link_element(file_url_transform_relative(file_create_url('public://css/27.css')) . '?'),
-          27 => $create_link_element(file_url_transform_relative(file_create_url('public://css/28.css')) . '?'),
-          28 => $create_link_element(file_url_transform_relative(file_create_url('public://css/29.css')) . '?'),
-          29 => $create_link_element(file_url_transform_relative(file_create_url('public://css/30.css')) . '?'),
-          30 => $create_link_element(file_url_transform_relative(file_create_url('public://css/31.css')) . '?'),
-          31 => $create_link_element(file_url_transform_relative(file_create_url('public://css/32.css')) . '?'),
+          0 => $create_link_element('generated-relative-url:public://css/1.css' . '?'),
+          1 => $create_link_element('generated-relative-url:public://css/2.css' . '?'),
+          2 => $create_link_element('generated-relative-url:public://css/3.css' . '?'),
+          3 => $create_link_element('generated-relative-url:public://css/4.css' . '?'),
+          4 => $create_link_element('generated-relative-url:public://css/5.css' . '?'),
+          5 => $create_link_element('generated-relative-url:public://css/6.css' . '?'),
+          6 => $create_link_element('generated-relative-url:public://css/7.css' . '?'),
+          7 => $create_link_element('generated-relative-url:public://css/8.css' . '?'),
+          8 => $create_link_element('generated-relative-url:public://css/9.css' . '?'),
+          9 => $create_link_element('generated-relative-url:public://css/10.css' . '?', 'screen'),
+          10 => $create_link_element('generated-relative-url:public://css/11.css' . '?'),
+          11 => $create_link_element('generated-relative-url:public://css/12.css' . '?'),
+          12 => $create_link_element('generated-relative-url:public://css/13.css' . '?'),
+          13 => $create_link_element('generated-relative-url:public://css/14.css' . '?'),
+          14 => $create_link_element('generated-relative-url:public://css/15.css' . '?'),
+          15 => $create_link_element('generated-relative-url:public://css/16.css' . '?'),
+          16 => $create_link_element('generated-relative-url:public://css/17.css' . '?'),
+          17 => $create_link_element('generated-relative-url:public://css/18.css' . '?'),
+          18 => $create_link_element('generated-relative-url:public://css/19.css' . '?'),
+          19 => $create_link_element('generated-relative-url:public://css/20.css' . '?', 'print'),
+          20 => $create_link_element('generated-relative-url:public://css/21.css' . '?'),
+          21 => $create_link_element('generated-relative-url:public://css/22.css' . '?'),
+          22 => $create_link_element('generated-relative-url:public://css/23.css' . '?'),
+          23 => $create_link_element('generated-relative-url:public://css/24.css' . '?'),
+          24 => $create_link_element('generated-relative-url:public://css/25.css' . '?'),
+          25 => $create_link_element('generated-relative-url:public://css/26.css' . '?'),
+          26 => $create_link_element('generated-relative-url:public://css/27.css' . '?'),
+          27 => $create_link_element('generated-relative-url:public://css/28.css' . '?'),
+          28 => $create_link_element('generated-relative-url:public://css/29.css' . '?'),
+          29 => $create_link_element('generated-relative-url:public://css/30.css' . '?'),
+          30 => $create_link_element('generated-relative-url:public://css/31.css' . '?'),
+          31 => $create_link_element('generated-relative-url:public://css/32.css' . '?'),
         ],
       ],
     ];
@@ -294,57 +302,3 @@ public function testRenderInvalidType() {
   }
 
 }
-
-/**
- * Temporary mock for file_create_url(), until that is moved into
- * Component/Utility.
- */
-if (!function_exists('Drupal\Tests\Core\Asset\file_create_url')) {
-
-  function file_create_url($uri) {
-    return 'file_create_url:' . $uri;
-  }
-
-}
-
-/**
- * Temporary mock of file_url_transform_relative, until that is moved into
- * Component/Utility.
- */
-if (!function_exists('Drupal\Tests\Core\Asset\file_url_transform_relative')) {
-
-  function file_url_transform_relative($uri) {
-    return 'file_url_transform_relative:' . $uri;
-  }
-
-}
-
-/**
- * CssCollectionRenderer uses file_create_url() & file_url_transform_relative(),
- * which *are* available when using the Simpletest test runner, but not when
- * using the PHPUnit test runner; hence this hack.
- */
-namespace Drupal\Core\Asset;
-
-if (!function_exists('Drupal\Core\Asset\file_create_url')) {
-
-  /**
-   * Temporary mock for file_create_url(), until that is moved into
-   * Component/Utility.
-   */
-  function file_create_url($uri) {
-    return \Drupal\Tests\Core\Asset\file_create_url($uri);
-  }
-
-}
-if (!function_exists('Drupal\Core\Asset\file_url_transform_relative')) {
-
-  /**
-   * Temporary mock of file_url_transform_relative, until that is moved into
-   * Component/Utility.
-   */
-  function file_url_transform_relative($uri) {
-    return \Drupal\Tests\Core\Asset\file_url_transform_relative($uri);
-  }
-
-}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php
index dba9fc2ad2b9..782f3cd38391 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\Core\Asset;
 
 use Drupal\Core\Asset\CssOptimizer;
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -24,10 +25,23 @@ class CssOptimizerUnitTest extends UnitTestCase {
    */
   protected $optimizer;
 
+  /**
+   * The file URL generator mock.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $fileUrlGenerator;
+
   protected function setUp(): void {
     parent::setUp();
-
-    $this->optimizer = new CssOptimizer();
+    $this->fileUrlGenerator = $this->createMock(FileUrlGeneratorInterface::class);
+    $this->fileUrlGenerator->expects($this->any())
+      ->method('generateString')
+      ->with($this->isType('string'))
+      ->willReturnCallback(function ($uri) {
+        return 'generated-relative-url:' . $uri;
+      });
+    $this->optimizer = new CssOptimizer($this->fileUrlGenerator);
   }
 
   /**
@@ -58,7 +72,8 @@ public function providerTestOptimize() {
       // - Proper URLs in imported files. (https://www.drupal.org/node/265719)
       // - A background image with relative paths, which must be rewritten.
       // - The rewritten background image path must also be passed through
-      //   file_create_url(). (https://www.drupal.org/node/1961340)
+      //   FileUrlGeneratorInterface::generate().
+      //   (https://www.drupal.org/node/1961340)
       // - Imported files that are external (protocol-relative URL or not)
       //   should not be expanded. (https://www.drupal.org/node/2014851)
       [
@@ -72,7 +87,7 @@ public function providerTestOptimize() {
           'browsers' => ['IE' => TRUE, '!IE' => TRUE],
           'basename' => 'css_input_with_import.css',
         ],
-        str_replace('url(images/icon.png)', 'url(' . file_url_transform_relative(file_create_url($path . 'images/icon.png')) . ')', file_get_contents($absolute_path . 'css_input_with_import.css.optimized.css')),
+        str_replace('url(images/icon.png)', 'url(generated-relative-url:' . $path . 'images/icon.png)', file_get_contents($absolute_path . 'css_input_with_import.css.optimized.css')),
       ],
       // File. Tests:
       // - Retain comment hacks.
@@ -104,7 +119,7 @@ public function providerTestOptimize() {
           'browsers' => ['IE' => TRUE, '!IE' => TRUE],
           'basename' => 'css_input_with_import.css',
         ],
-        str_replace('url(../images/icon.png)', 'url(' . file_url_transform_relative(file_create_url($path . 'images/icon.png')) . ')', file_get_contents($absolute_path . 'css_subfolder/css_input_with_import.css.optimized.css')),
+        str_replace('url(../images/icon.png)', 'url(generated-relative-url:' . $path . 'images/icon.png)', file_get_contents($absolute_path . 'css_subfolder/css_input_with_import.css.optimized.css')),
       ],
       // File. Tests:
       // - Any @charset declaration at the beginning of a file should be
@@ -267,58 +282,10 @@ public function testTypeExternal() {
 }
 
 /**
- * Temporary mock for file_create_url(), until that is moved into
- * Component/Utility.
- */
-if (!function_exists('Drupal\Tests\Core\Asset\file_create_url')) {
-
-  function file_create_url($uri) {
-    return 'file_create_url:' . $uri;
-  }
-
-}
-
-/**
- * Temporary mock of file_url_transform_relative, until that is moved into
- * Component/Utility.
- */
-if (!function_exists('Drupal\Tests\Core\Asset\file_url_transform_relative')) {
-
-  function file_url_transform_relative($uri) {
-    return 'file_url_transform_relative:' . $uri;
-  }
-
-}
-
-/**
- * CssCollectionRenderer uses file_create_url() & file_url_transform_relative(),
- * which *are* available when using the Simpletest test runner, but not when
- * using the PHPUnit test runner; hence this hack.
+ * CssCollectionRenderer uses file_uri_scheme() which need to be mocked.
  */
 namespace Drupal\Core\Asset;
 
-if (!function_exists('Drupal\Core\Asset\file_create_url')) {
-
-  /**
-   * Temporary mock for file_create_url(), until that is moved into
-   * Component/Utility.
-   */
-  function file_create_url($uri) {
-    return \Drupal\Tests\Core\Asset\file_create_url($uri);
-  }
-
-}
-if (!function_exists('Drupal\Core\Asset\file_url_transform_relative')) {
-
-  /**
-   * Temporary mock of file_url_transform_relative, until that is moved into
-   * Component/Utility.
-   */
-  function file_url_transform_relative($uri) {
-    return \Drupal\Tests\Core\Asset\file_url_transform_relative($uri);
-  }
-
-}
 if (!function_exists('Drupal\Core\Asset\file_uri_scheme')) {
 
   function file_uri_scheme($uri) {
diff --git a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php
index cd367d128b71..a4c2c5b68b52 100644
--- a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php
+++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\Core\Template;
 
+use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\GeneratedLink;
 use Drupal\Core\Render\RenderableInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -60,6 +61,13 @@ class TwigExtensionTest extends UnitTestCase {
    */
   protected $systemUnderTest;
 
+  /**
+   * The file URL generator mock.
+   *
+   * @var \Drupal\Core\File\FileUrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $fileUrlGenerator;
+
   /**
    * {@inheritdoc}
    */
@@ -70,8 +78,9 @@ public function setUp(): void {
     $this->urlGenerator = $this->createMock('\Drupal\Core\Routing\UrlGeneratorInterface');
     $this->themeManager = $this->createMock('\Drupal\Core\Theme\ThemeManagerInterface');
     $this->dateFormatter = $this->createMock('\Drupal\Core\Datetime\DateFormatterInterface');
+    $this->fileUrlGenerator = $this->createMock(FileUrlGeneratorInterface::class);
 
-    $this->systemUnderTest = new TwigExtension($this->renderer, $this->urlGenerator, $this->themeManager, $this->dateFormatter);
+    $this->systemUnderTest = new TwigExtension($this->renderer, $this->urlGenerator, $this->themeManager, $this->dateFormatter, $this->fileUrlGenerator);
   }
 
   /**
@@ -167,6 +176,22 @@ public function testFormatDate() {
     $this->assertEquals('1978-11-19', $result);
   }
 
+  /**
+   * Tests the file_url filter.
+   */
+  public function testFileUrl() {
+    $this->fileUrlGenerator->expects($this->once())
+      ->method('generateString')
+      ->with('public://picture.jpg')
+      ->willReturn('sites/default/files/picture.jpg');
+
+    $loader = new StringLoader();
+    $twig = new Environment($loader);
+    $twig->addExtension($this->systemUnderTest);
+    $result = $twig->render('{{ file_url(file) }}', ['file' => 'public://picture.jpg']);
+    $this->assertEquals('sites/default/files/picture.jpg', $result);
+  }
+
   /**
    * Tests the active_theme_path function.
    */
-- 
GitLab