diff --git a/core/core.services.yml b/core/core.services.yml
index 0ddf970ed4..d22951f9d8 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1519,6 +1519,18 @@ services:
     class: Drupal\Core\StreamWrapper\TemporaryStream
     tags:
       - { name: stream_wrapper, scheme: temporary }
+  stream_wrapper.module:
+    class: Drupal\Core\StreamWrapper\ModuleStream
+    tags:
+      - { name: stream_wrapper, scheme: module }
+  stream_wrapper.theme:
+    class: Drupal\Core\StreamWrapper\ThemeStream
+    tags:
+      - { name: stream_wrapper, scheme: theme }
+  stream_wrapper.profile:
+    class: Drupal\Core\StreamWrapper\ProfileStream
+    tags:
+      - { name: stream_wrapper, scheme: profile }
   image.toolkit.manager:
     class: Drupal\Core\ImageToolkit\ImageToolkitManager
     arguments: ['@config.factory']
diff --git a/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php b/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php
new file mode 100644
index 0000000000..499620a73c
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\Core\StreamWrapper;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines a base stream wrapper implementation.
+ *
+ * ExtensionStreamBase is a read-only Drupal stream wrapper base class for
+ * system files located in extensions: modules, themes and installed profile.
+ */
+abstract class ExtensionStreamBase extends LocalReadOnlyStream {
+
+  /**
+   * The request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getType() {
+    return StreamWrapperInterface::LOCAL | StreamWrapperInterface::READ;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUri($uri) {
+    if (strpos($uri, '://') === FALSE) {
+      // The delimiter ('://') was not found in $uri, malformed $uri passed.
+      throw new \InvalidArgumentException("Malformed URI: {$uri}");
+    }
+    $this->uri = $uri;
+  }
+
+  /**
+   * Gets the module, theme, or profile name from the URI.
+   *
+   * This will return the name of the module, theme or profile e.g.:
+   * @code
+   * ModuleStream::getExtensionName('module://foo')
+   * @endcode
+   * and
+   * @code
+   * ModuleStream::getExtensionName('module://foo/')
+   * @endcode
+   * will both return
+   * @code
+   * 'foo'
+   * @endcode
+   *
+   * @return string
+   *   The extension name.
+   */
+  protected function getExtensionName(): string {
+    $uri_parts = explode('://', $this->uri, 2);
+    $extension_name = strtok($uri_parts[1], '/');
+    $this->validateExtensionInstalled($extension_name);
+    return $extension_name;
+  }
+
+  /**
+   * Checks that a module, theme, or profile is installed.
+   *
+   * @param string $extension_name
+   *   The extension name.
+   *
+   * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
+   *   If the extension is missing.
+   */
+  abstract protected function validateExtensionInstalled(string $extension_name): void;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTarget($uri = NULL) {
+    if ($target = strstr(parent::getTarget($uri), '/')) {
+      return trim($target, '/');
+    }
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExternalUrl() {
+    $dir = $this->getDirectoryPath();
+    if (empty($dir)) {
+      throw new \RuntimeException("Extension directory for {$this->uri} does not exist.");
+    }
+    $path = rtrim(base_path() . $dir . '/' . $this->getTarget(), '/');
+    return $this->getRequest()->getUriForPath($path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dirname($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+    else {
+      $this->setUri($uri);
+    }
+    [$scheme] = explode('://', $uri, 2);
+    $dirname = dirname($this->getTarget($uri));
+    $dirname = $dirname !== '.' ? rtrim("/$dirname", '/') : '';
+    return "$scheme://{$this->getExtensionName()}{$dirname}";
+  }
+
+  /**
+   * Returns the current request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Request
+   *   The current request object.
+   */
+  protected function getRequest(): Request {
+    if (!isset($this->request)) {
+      $this->request = \Drupal::service('request_stack')->getCurrentRequest();
+    }
+    return $this->request;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php
new file mode 100644
index 0000000000..1af2fa7904
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Core\StreamWrapper;
+
+/**
+ * Defines the read-only module:// stream wrapper for module files.
+ *
+ * Usage:
+ * @code
+ * module://{name}
+ * @endcode
+ * Points to the module {name} root directory. Only enabled modules can be
+ * referred.
+ */
+class ModuleStream extends ExtensionStreamBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->t('Module files');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t("Local files stored under a module's directory.");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDirectoryPath() {
+    return \Drupal::moduleHandler()->getModule($this->getExtensionName())->getPath();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateExtensionInstalled(string $extension_name): void {
+    \Drupal::moduleHandler()->getModule($extension_name);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php
new file mode 100644
index 0000000000..6ffb0d0716
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\Core\StreamWrapper;
+
+/**
+ * Defines the read-only profile:// stream wrapper for installed profile files.
+ *
+ * Usage:
+ * @code
+ * profile://
+ * @endcode
+ * Points to the installed profile root directory.
+ */
+class ProfileStream extends ExtensionStreamBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->t('Installed profile files');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t("Local files stored under the installed profile's directory.");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dirname($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+    else {
+      $this->setUri($uri);
+    }
+    [$scheme] = explode('://', $uri, 2);
+    $dirname = dirname($this->getTarget($uri));
+    $dirname = $dirname !== '.' ? rtrim("$dirname", '/') : '';
+    return "$scheme://{$dirname}";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDirectoryPath() {
+    return \Drupal::service('extension.list.profile')->getPath($this->getExtensionName());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTarget($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+    [, $target] = explode('://', $uri, 2);
+    // Remove erroneous leading or trailing, forward-slashes and backslashes.
+    return trim($target, '\/');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExtensionName(): string {
+    return \Drupal::getContainer()->getParameter('install_profile');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateExtensionInstalled(string $extension_name): void {}
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php
new file mode 100644
index 0000000000..2c0b26011a
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Core\StreamWrapper;
+
+/**
+ * Defines the read-only theme:// stream wrapper for theme files.
+ *
+ * Usage:
+ * @code
+ * theme://{name}
+ * @endcode
+ * Points to the theme {name} root directory. Only installed themes can be
+ * referred.
+ */
+class ThemeStream extends ExtensionStreamBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->t('Theme files');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t("Local files stored under a theme's directory.");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDirectoryPath() {
+    return \Drupal::service('theme_handler')->getTheme($this->getExtensionName())->getPath();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateExtensionInstalled(string $extension_name): void {
+    \Drupal::service('theme_handler')->getTheme($extension_name);
+  }
+
+}
diff --git a/core/modules/image/config/install/image.settings.yml b/core/modules/image/config/install/image.settings.yml
index c92db4e155..7fa1d9b74f 100644
--- a/core/modules/image/config/install/image.settings.yml
+++ b/core/modules/image/config/install/image.settings.yml
@@ -1,3 +1,3 @@
-preview_image: core/modules/image/sample.png
+preview_image: module://image/sample.png
 allow_insecure_derivatives: false
 suppress_itok_output: false
diff --git a/core/tests/Drupal/KernelTests/Core/StreamWrapper/ExtensionStreamTest.php b/core/tests/Drupal/KernelTests/Core/StreamWrapper/ExtensionStreamTest.php
new file mode 100644
index 0000000000..15841793b7
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/StreamWrapper/ExtensionStreamTest.php
@@ -0,0 +1,325 @@
+<?php
+
+namespace Drupal\KernelTests\Core\StreamWrapper;
+
+use Drupal\Core\Extension\Exception\UnknownExtensionException;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests system stream wrapper functions.
+ *
+ * @group File
+ */
+class ExtensionStreamTest extends KernelTestBase {
+
+  /**
+   * A list of extension stream wrappers keyed by scheme.
+   *
+   * @var \Drupal\Core\StreamWrapper\StreamWrapperInterface[]
+   */
+  protected $streamWrappers = [];
+
+  /**
+   * The base url for the current request.
+   *
+   * @var string
+   */
+  protected $baseUrl;
+
+  /**
+   * The list of modules to enable.
+   *
+   * @var string[]
+   */
+  protected static $modules = ['file_module_test', 'system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(): void {
+    parent::setUp();
+
+    // Find the base url to be used later in tests.
+    $this->baseUrl = $this->container->get('request_stack')->getCurrentRequest()->getUriForPath(base_path());
+
+    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
+    $stream_wrapper_manager = $this->container->get('stream_wrapper_manager');
+
+    // Get stream wrapper instances.
+    foreach (['module', 'theme', 'profile'] as $scheme) {
+      $this->streamWrappers[$scheme] = $stream_wrapper_manager->getViaScheme($scheme);
+    }
+
+    /** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
+    $theme_installer = $this->container->get('theme_installer');
+    // Install Bartik and Seven themes.
+    $theme_installer->install(['bartik', 'seven']);
+
+    // Set 'minimal' as installed profile for the purposes of this test.
+    $this->setInstallProfile('minimal');
+    $this->enableModules(['minimal']);
+  }
+
+  /**
+   * Tests invalid stream uris.
+   *
+   * @param string $uri
+   *   The URI being tested.
+   *
+   * @dataProvider providerInvalidUris
+   */
+  public function testInvalidStreamUri(string $uri): void {
+    $this->expectException(\InvalidArgumentException::class);
+    $this->expectExceptionMessage("Malformed URI: {$uri}");
+    $this->streamWrappers['module']->dirname($uri);
+  }
+
+  /**
+   * Provides test cases for testInvalidStreamUri()
+   *
+   * @return array[]
+   *   A list of urls to test.
+   */
+  public function providerInvalidUris(): array {
+    return [
+      ['invalid/uri'],
+      ['invalid_uri'],
+      ['module/invalid/uri'],
+      ['module/invalid_uri'],
+      ['module:invalid_uri'],
+      ['module::/invalid/uri'],
+      ['module::/invalid_uri'],
+      ['module//:invalid/uri'],
+      ['module//invalid_uri'],
+      ['module//invalid/uri'],
+    ];
+  }
+
+  /**
+   * Tests call of ::dirname() without setting a URI first.
+   */
+  public function testDirnameAsParameter(): void {
+    $this->assertEquals('module://system', $this->streamWrappers['module']->dirname('module://system/system.admin.css'));
+  }
+
+  /**
+   * Tests the extension stream wrapper methods.
+   *
+   * @param string $uri
+   *   The uri to be tested.
+   * @param string $dirname
+   *   The expectation for dirname() method.
+   * @param string $realpath
+   *   The expectation for realpath() method.
+   * @param string $getExternalUrl
+   *   The expectation for getExternalUrl() method.
+   *
+   * @dataProvider providerStreamWrapperMethods
+   */
+  public function testStreamWrapperMethods(string $uri, string $dirname, string $realpath, string $getExternalUrl): void {
+    $this->enableModules(['image']);
+
+    // Prefix realpath() expected value with Drupal root directory.
+    $realpath = DRUPAL_ROOT . $realpath;
+    // Prefix getExternalUrl() expected value with base url.
+    $getExternalUrl = "{$this->baseUrl}$getExternalUrl";
+    $case = compact('dirname', 'realpath', 'getExternalUrl');
+
+    foreach ($case as $method => $expected) {
+      [$scheme] = explode('://', $uri);
+      $this->streamWrappers[$scheme]->setUri($uri);
+      $this->assertSame($expected, $this->streamWrappers[$scheme]->$method());
+    }
+  }
+
+  /**
+   * Provides test cases for testStreamWrapperMethods().
+   *
+   * @return array[]
+   *   A list of test cases. Each case consists of the following items:
+   *   - The uri to be tested.
+   *   - The result or the exception when running dirname() method.
+   *   - The result or the exception when running realpath() method. The value
+   *     is prefixed later, in the test method, with the Drupal root directory.
+   *   - The result or the exception when running getExternalUrl() method. The
+   *     value is prefixed later, in the test method, with the base url.
+   */
+  public function providerStreamWrapperMethods(): array {
+    return [
+      // Cases for module:// stream wrapper.
+      [
+        'module://system',
+        'module://system',
+        '/core/modules/system',
+        'core/modules/system',
+      ],
+      [
+        'module://system/css/system.admin.css',
+        'module://system/css',
+        '/core/modules/system/css/system.admin.css',
+        'core/modules/system/css/system.admin.css',
+      ],
+      [
+        'module://file_module_test/file_module_test.dummy.inc',
+        'module://file_module_test',
+        '/core/modules/file/tests/file_module_test/file_module_test.dummy.inc',
+        'core/modules/file/tests/file_module_test/file_module_test.dummy.inc',
+      ],
+      [
+        'module://image/sample.png',
+        'module://image',
+        '/core/modules/image/sample.png',
+        'core/modules/image/sample.png',
+      ],
+      // Cases for theme:// stream wrapper.
+      [
+        'theme://seven',
+        'theme://seven',
+        '/core/themes/seven',
+        'core/themes/seven',
+      ],
+      [
+        'theme://seven/style.css',
+        'theme://seven',
+        '/core/themes/seven/style.css',
+        'core/themes/seven/style.css',
+      ],
+      [
+        'theme://bartik/color/preview.js',
+        'theme://bartik/color',
+        '/core/themes/bartik/color/preview.js',
+        'core/themes/bartik/color/preview.js',
+      ],
+      // Cases for profile:// stream wrapper.
+      [
+        'profile://',
+        'profile://',
+        '/core/profiles/minimal',
+        'core/profiles/minimal',
+      ],
+      [
+        'profile://config/install/block.block.stark_login.yml',
+        'profile://config/install',
+        '/core/profiles/minimal/config/install/block.block.stark_login.yml',
+        'core/profiles/minimal/config/install/block.block.stark_login.yml',
+      ],
+      [
+        'profile://config/install/node.type.article.yml',
+        'profile://config/install',
+        '/core/profiles/minimal/config/install/node.type.article.yml',
+        'core/profiles/minimal/config/install/node.type.article.yml',
+      ],
+      [
+        'profile://minimal.info.yml',
+        'profile://',
+        '/core/profiles/minimal/minimal.info.yml',
+        'core/profiles/minimal/minimal.info.yml',
+      ],
+    ];
+  }
+
+  /**
+   * Test the dirname method on uninstalled extensions.
+   *
+   * @param string $uri
+   *   The uri to be tested.
+   * @param string $class_name
+   *   The class name of the expected exception.
+   * @param string $expected_message
+   *   The The expected exception message.
+   *
+   * @dataProvider providerStreamWrapperMethodsOnMissingExtensions
+   */
+  public function testStreamWrapperDirnameOnMissingExtensions(string $uri, string $class_name, string $expected_message): void {
+    [$scheme] = explode('://', $uri);
+    $this->streamWrappers[$scheme]->setUri($uri);
+
+    $this->expectException($class_name);
+    $this->expectExceptionMessage($expected_message);
+    $this->streamWrappers[$scheme]->dirname();
+  }
+
+  /**
+   * Test the realpath method on uninstalled extensions.
+   *
+   * @param string $uri
+   *   The uri to be tested.
+   * @param string $class_name
+   *   The class name of the expected exception.
+   * @param string $expected_message
+   *   The The expected exception message.
+   *
+   * @dataProvider providerStreamWrapperMethodsOnMissingExtensions
+   */
+  public function testStreamWrapperRealpathOnMissingExtensions(string $uri, string $class_name, string $expected_message): void {
+    [$scheme] = explode('://', $uri);
+    $this->streamWrappers[$scheme]->setUri($uri);
+
+    $this->expectException($class_name);
+    $this->expectExceptionMessage($expected_message);
+    $this->streamWrappers[$scheme]->realpath();
+  }
+
+  /**
+   * Test the getExternalUrl method on uninstalled extensions.
+   *
+   * @param string $uri
+   *   The uri to be tested.
+   * @param string $class_name
+   *   The class name of the expected exception.
+   * @param string $expected_message
+   *   The The expected exception message.
+   *
+   * @dataProvider providerStreamWrapperMethodsOnMissingExtensions
+   */
+  public function testStreamWrapperGetExternalUrlOnMissingExtensions(string $uri, string $class_name, string $expected_message): void {
+    [$scheme] = explode('://', $uri);
+    $this->streamWrappers[$scheme]->setUri($uri);
+
+    $this->expectException($class_name);
+    $this->expectExceptionMessage($expected_message);
+    $this->streamWrappers[$scheme]->getExternalUrl();
+  }
+
+  /**
+   * Test cases for testing stream wrapper methods on missing extensions.
+   *
+   * @return array[]
+   *   A list of test cases. Each case consists of the following items:
+   *   - The uri to be tested.
+   *   - The class name of the expected exception.
+   *   - The expected exception message.
+   */
+  public function providerStreamWrapperMethodsOnMissingExtensions(): array {
+    return [
+      // Cases for module:// stream wrapper.
+      [
+        'module://ckeditor/ckeditor.info.yml',
+        UnknownExtensionException::class,
+        'The module ckeditor does not exist.',
+      ],
+      [
+        'module://foo_bar/foo.bar.js',
+        UnknownExtensionException::class,
+        'The module foo_bar does not exist.',
+      ],
+      [
+        'module://image/sample.png',
+        UnknownExtensionException::class,
+        'The module image does not exist.',
+      ],
+      // Cases for theme:// stream wrapper.
+      [
+        'theme://fifteen/screenshot.png',
+        UnknownExtensionException::class,
+        'The theme fifteen does not exist.',
+      ],
+      [
+        'theme://stark/stark.info.yml',
+        UnknownExtensionException::class,
+        'The theme stark does not exist.',
+      ],
+    ];
+  }
+
+}
