Add ImmutableBuffer.fromFilePath (#36623)

diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc
index 5417889..595988a 100644
--- a/lib/ui/dart_ui.cc
+++ b/lib/ui/dart_ui.cc
@@ -83,6 +83,7 @@
   V(ImageDescriptor::initEncoded, 3)                                  \
   V(ImmutableBuffer::init, 3)                                         \
   V(ImmutableBuffer::initFromAsset, 3)                                \
+  V(ImmutableBuffer::initFromFile, 3)                                 \
   V(ImageDescriptor::initRaw, 6)                                      \
   V(IsolateNameServerNatives::LookupPortByName, 1)                    \
   V(IsolateNameServerNatives::RegisterPortWithName, 2)                \
diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart
index 018060d..de27e80 100644
--- a/lib/ui/painting.dart
+++ b/lib/ui/painting.dart
@@ -6097,12 +6097,30 @@
     }).then((int length) => instance.._length = length);
   }
 
+  /// Create a buffer from the file with [path].
+  ///
+  /// Throws an [Exception] if the asset does not exist.
+  static Future<ImmutableBuffer> fromFilePath(String path) {
+    final ImmutableBuffer instance = ImmutableBuffer._(0);
+    return _futurize((_Callback<int> callback) {
+      return instance._initFromFile(path, callback);
+    }).then((int length) {
+      if (length == -1) {
+        throw Exception('Could not load file at $path.');
+      }
+      return instance.._length = length;
+    });
+  }
+
   @FfiNative<Handle Function(Handle, Handle, Handle)>('ImmutableBuffer::init')
   external String? _init(Uint8List list, _Callback<void> callback);
 
   @FfiNative<Handle Function(Handle, Handle, Handle)>('ImmutableBuffer::initFromAsset')
   external String? _initFromAsset(String assetKey, _Callback<int> callback);
 
+  @FfiNative<Handle Function(Handle, Handle, Handle)>('ImmutableBuffer::initFromFile')
+  external String? _initFromFile(String assetKey, _Callback<int> callback);
+
   /// The length, in bytes, of the underlying data.
   int get length => _length;
   int _length;
diff --git a/lib/ui/painting/immutable_buffer.cc b/lib/ui/painting/immutable_buffer.cc
index 7540135..ee36730 100644
--- a/lib/ui/painting/immutable_buffer.cc
+++ b/lib/ui/painting/immutable_buffer.cc
@@ -6,11 +6,14 @@
 
 #include <cstring>
 
+#include "flutter/fml/file.h"
+#include "flutter/fml/make_copyable.h"
 #include "flutter/lib/ui/ui_dart_state.h"
 #include "flutter/lib/ui/window/platform_configuration.h"
 #include "third_party/tonic/converter/dart_converter.h"
 #include "third_party/tonic/dart_args.h"
 #include "third_party/tonic/dart_binding_macros.h"
+#include "third_party/tonic/dart_persistent_value.h"
 
 #if FML_OS_ANDROID
 #include <sys/mman.h>
@@ -77,6 +80,71 @@
   return Dart_Null();
 }
 
+Dart_Handle ImmutableBuffer::initFromFile(Dart_Handle raw_buffer_handle,
+                                          Dart_Handle file_path_handle,
+                                          Dart_Handle callback_handle) {
+  UIDartState::ThrowIfUIOperationsProhibited();
+  if (!Dart_IsClosure(callback_handle)) {
+    return tonic::ToDart("Callback must be a function");
+  }
+
+  uint8_t* chars = nullptr;
+  intptr_t file_path_length = 0;
+  Dart_Handle result =
+      Dart_StringToUTF8(file_path_handle, &chars, &file_path_length);
+  if (Dart_IsError(result)) {
+    return tonic::ToDart("File path must be valid UTF8");
+  }
+
+  std::string file_path = std::string{reinterpret_cast<const char*>(chars),
+                                      static_cast<size_t>(file_path_length)};
+
+  auto* dart_state = UIDartState::Current();
+  auto ui_task_runner = dart_state->GetTaskRunners().GetUITaskRunner();
+  auto buffer_callback =
+      std::make_unique<tonic::DartPersistentValue>(dart_state, callback_handle);
+  auto buffer_handle = std::make_unique<tonic::DartPersistentValue>(
+      dart_state, raw_buffer_handle);
+
+  auto ui_task = fml::MakeCopyable(
+      [buffer_callback = std::move(buffer_callback),
+       buffer_handle = std::move(buffer_handle)](const sk_sp<SkData>& sk_data,
+                                                 size_t buffer_size) mutable {
+        auto dart_state = buffer_callback->dart_state().lock();
+        if (!dart_state) {
+          return;
+        }
+        tonic::DartState::Scope scope(dart_state);
+        if (!sk_data) {
+          // -1 is used as a sentinel that the file could not be opened.
+          tonic::DartInvoke(buffer_callback->Get(), {tonic::ToDart(-1)});
+          return;
+        }
+        auto buffer = fml::MakeRefCounted<ImmutableBuffer>(sk_data);
+        buffer->AssociateWithDartWrapper(buffer_handle->Get());
+        tonic::DartInvoke(buffer_callback->Get(), {tonic::ToDart(buffer_size)});
+      });
+
+  dart_state->GetConcurrentTaskRunner()->PostTask(
+      [file_path = std::move(file_path),
+       ui_task_runner = std::move(ui_task_runner), ui_task] {
+        auto mapping = std::make_unique<fml::FileMapping>(fml::OpenFile(
+            file_path.c_str(), false, fml::FilePermission::kRead));
+
+        sk_sp<SkData> sk_data;
+        size_t buffer_size = 0;
+        if (mapping->IsValid()) {
+          buffer_size = mapping->GetSize();
+          const void* bytes = static_cast<const void*>(mapping->GetMapping());
+          sk_data = MakeSkDataWithCopy(bytes, buffer_size);
+        }
+        ui_task_runner->PostTask(
+            [sk_data = std::move(sk_data), ui_task = std::move(ui_task),
+             buffer_size]() { ui_task(sk_data, buffer_size); });
+      });
+  return Dart_Null();
+}
+
 #if FML_OS_ANDROID
 
 // Compressed image buffers are allocated on the UI thread but are deleted on a
diff --git a/lib/ui/painting/immutable_buffer.h b/lib/ui/painting/immutable_buffer.h
index 5c51f65..daf1d6c 100644
--- a/lib/ui/painting/immutable_buffer.h
+++ b/lib/ui/painting/immutable_buffer.h
@@ -56,6 +56,20 @@
                                    Dart_Handle asset_name_handle,
                                    Dart_Handle callback_handle);
 
+  /// Initializes a new ImmutableData from an File path.
+  ///
+  /// The zero indexed argument is the caller that will be registered as the
+  /// Dart peer of the native ImmutableBuffer object.
+  ///
+  /// The first indexed argumented is a String corresponding to the file path
+  /// to load.
+  ///
+  /// The second indexed argument is expected to be a void callback to signal
+  /// when the copy has completed.
+  static Dart_Handle initFromFile(Dart_Handle buffer_handle,
+                                  Dart_Handle file_path_handle,
+                                  Dart_Handle callback_handle);
+
   /// The length of the data in bytes.
   size_t length() const {
     FML_DCHECK(data_);
diff --git a/lib/ui/ui_dart_state.cc b/lib/ui/ui_dart_state.cc
index d67b540..c60fe10 100644
--- a/lib/ui/ui_dart_state.cc
+++ b/lib/ui/ui_dart_state.cc
@@ -38,6 +38,7 @@
     std::string advisory_script_uri,
     std::string advisory_script_entrypoint,
     std::shared_ptr<VolatilePathTracker> volatile_path_tracker,
+    std::shared_ptr<fml::ConcurrentTaskRunner> concurrent_task_runner,
     bool enable_impeller)
     : task_runners(task_runners),
       snapshot_delegate(std::move(snapshot_delegate)),
@@ -48,6 +49,7 @@
       advisory_script_uri(std::move(advisory_script_uri)),
       advisory_script_entrypoint(std::move(advisory_script_entrypoint)),
       volatile_path_tracker(std::move(volatile_path_tracker)),
+      concurrent_task_runner(concurrent_task_runner),
       enable_impeller(enable_impeller) {}
 
 UIDartState::UIDartState(
@@ -145,6 +147,11 @@
   return context_.volatile_path_tracker;
 }
 
+std::shared_ptr<fml::ConcurrentTaskRunner>
+UIDartState::GetConcurrentTaskRunner() const {
+  return context_.concurrent_task_runner;
+}
+
 void UIDartState::ScheduleMicrotask(Dart_Handle closure) {
   if (tonic::CheckAndHandleError(closure) || !Dart_IsClosure(closure)) {
     return;
diff --git a/lib/ui/ui_dart_state.h b/lib/ui/ui_dart_state.h
index 2103cda..a1dff34 100644
--- a/lib/ui/ui_dart_state.h
+++ b/lib/ui/ui_dart_state.h
@@ -54,6 +54,7 @@
             std::string advisory_script_uri,
             std::string advisory_script_entrypoint,
             std::shared_ptr<VolatilePathTracker> volatile_path_tracker,
+            std::shared_ptr<fml::ConcurrentTaskRunner> concurrent_task_runner,
             bool enable_impeller);
 
     /// The task runners used by the shell hosting this runtime controller. This
@@ -93,6 +94,10 @@
     /// Cache for tracking path volatility.
     std::shared_ptr<VolatilePathTracker> volatile_path_tracker;
 
+    /// The task runner whose tasks may be executed concurrently on a pool
+    /// of shared worker threads.
+    std::shared_ptr<fml::ConcurrentTaskRunner> concurrent_task_runner;
+
     /// Whether Impeller is enabled or not.
     bool enable_impeller = false;
   };
@@ -128,6 +133,8 @@
 
   std::shared_ptr<VolatilePathTracker> GetVolatilePathTracker() const;
 
+  std::shared_ptr<fml::ConcurrentTaskRunner> GetConcurrentTaskRunner() const;
+
   fml::WeakPtr<SnapshotDelegate> GetSnapshotDelegate() const;
 
   fml::WeakPtr<ImageDecoder> GetImageDecoder() const;
diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart
index c5ffd40..8b4dbed 100644
--- a/lib/web_ui/lib/painting.dart
+++ b/lib/web_ui/lib/painting.dart
@@ -728,6 +728,10 @@
     throw UnsupportedError('ImmutableBuffer.fromAsset is not supported on the web.');
   }
 
+  static Future<ImmutableBuffer> fromFilePath(String path) async {
+    throw UnsupportedError('ImmutableBuffer.fromFilePath is not supported on the web.');
+  }
+
   Uint8List? _list;
 
   int get length => _length;
diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc
index 9c95a58..24fd13e 100644
--- a/runtime/runtime_controller.cc
+++ b/runtime/runtime_controller.cc
@@ -61,7 +61,8 @@
       std::move(io_manager),          context_.unref_queue,
       std::move(image_decoder),       std::move(image_generator_registry),
       std::move(advisory_script_uri), std::move(advisory_script_entrypoint),
-      context_.volatile_path_tracker, context_.enable_impeller};
+      context_.volatile_path_tracker, context_.concurrent_task_runner,
+      context_.enable_impeller};
   auto result =
       std::make_unique<RuntimeController>(p_client,                      //
                                           vm_,                           //
diff --git a/shell/common/engine.cc b/shell/common/engine.cc
index b80d866..cbd2f55 100644
--- a/shell/common/engine.cc
+++ b/shell/common/engine.cc
@@ -100,6 +100,7 @@
           settings_.advisory_script_uri,           // advisory script uri
           settings_.advisory_script_entrypoint,    // advisory script entrypoint
           std::move(volatile_path_tracker),        // volatile path tracker
+          vm.GetConcurrentWorkerTaskRunner(),      // concurrent task runner
           settings_.enable_impeller,               // enable impeller
       });
 }
diff --git a/testing/dart/assets_test.dart b/testing/dart/assets_test.dart
index 64943c2..198f952 100644
--- a/testing/dart/assets_test.dart
+++ b/testing/dart/assets_test.dart
@@ -21,12 +21,29 @@
     expect(error is Exception, true);
   });
 
+  test('Loading a file that does not exist returns null', () async {
+    Object? error;
+    try {
+      await ImmutableBuffer.fromFilePath('ThisDoesNotExist');
+    } catch (err) {
+      error = err;
+    }
+    expect(error, isNotNull);
+    expect(error is Exception, true);
+  });
+
   test('returns the bytes of a bundled asset', () async {
     final ImmutableBuffer buffer = await ImmutableBuffer.fromAsset('DashInNooglerHat.jpg');
 
     expect(buffer.length == 354679, true);
   });
 
+  test('returns the bytes of a file', () async {
+    final ImmutableBuffer buffer = await ImmutableBuffer.fromFilePath('flutter/lib/ui/fixtures/DashInNooglerHat.jpg');
+
+    expect(buffer.length == 354679, true);
+  });
+
   test('Can load an asset with a space in the key', () async {
     // This assets actual path is "fixtures/DashInNooglerHat%20WithSpace.jpg"
     final ImmutableBuffer buffer = await ImmutableBuffer.fromAsset('DashInNooglerHat WithSpace.jpg');