Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fabric] Support blob: images, and provide better image.onError callbacks #13285

Merged
merged 13 commits into from
Jun 4, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "[Fabric] Support blob: images, and provide better image.onError callbacks",
"packageName": "react-native-windows",
"email": "30809111+acoates-ms@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ const {

const IMAGE1 =
'https://www.facebook.com/assets/fb_lite_messaging/E2EE-settings@3x.png';

const IMAGE2 =
// [Windows #13284 - this image crashes e2etests]
// 'https://www.facebook.com/ar_effect/external_textures/648609739826677.png';
'https://www.facebook.com/ads/pics/successstories.png';
'https://www.facebook.com/ar_effect/external_textures/648609739826677.png';
acoates-ms marked this conversation as resolved.
Show resolved Hide resolved

const base64Icon =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg==';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ winrt::Microsoft::ReactNative::ReactNativeHost CreateReactNativeHost(

auto host = winrt::Microsoft::ReactNative::ReactNativeHost();

// Some of the images in RNTester require a user-agent header to properly fetch
winrt::Microsoft::ReactNative::Networking::SetDefaultUserAgent(
host.InstanceSettings(), L"React Native Windows Playground");

// Include any autolinked modules
RegisterAutolinkedNativeModulePackages(host.PackageProviders());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,43 +52,41 @@ void RegisterCustomComponent(winrt::Microsoft::ReactNative::IReactPackageBuilder
*
* This allows applications to provide custom image rendering pipelines.
*/
struct EllipseImageHandler : winrt::implements<
EllipseImageHandler,
winrt::Microsoft::ReactNative::Composition::Experimental::IUriBrushProvider,
winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
struct EllipseImageHandler
: winrt::implements<EllipseImageHandler, winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
bool CanLoadImageUri(winrt::Microsoft::ReactNative::IReactContext context, winrt::Windows::Foundation::Uri uri) {
return uri.SchemeName() == L"ellipse";
}

winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::Experimental::UriBrushFactory>
GetSourceAsync(
winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
GetImageResponseAsync(
const winrt::Microsoft::ReactNative::IReactContext &context,
const winrt::Microsoft::ReactNative::Composition::ImageSource &imageSource) {
co_return [uri = imageSource.Uri(), size = imageSource.Size(), scale = imageSource.Scale(), context](
const winrt::Microsoft::ReactNative::IReactContext &reactContext,
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext
&compositionContext) -> winrt::Microsoft::ReactNative::Composition::Experimental::IBrush {
auto compositor =
winrt::Microsoft::ReactNative::Composition::Experimental::MicrosoftCompositionContextHelper::InnerCompositor(
compositionContext);
auto drawingBrush = compositionContext.CreateDrawingSurfaceBrush(
size,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied);
POINT pt;
Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw(drawingBrush, scale, &pt);
auto renderTarget = autoDraw.GetRenderTarget();

winrt::com_ptr<ID2D1SolidColorBrush> brush;
renderTarget->CreateSolidColorBrush({1.0f, 0.0f, 0.0f, 1.0f}, brush.put());
renderTarget->DrawEllipse(
{{(pt.x + size.Width / 2) / scale, (pt.y + size.Height / 2) / scale},
(size.Width / 2) / scale,
(size.Height / 2) / scale},
brush.get());

return drawingBrush;
};
co_return winrt::Microsoft::ReactNative::Composition::Experimental::UriBrushFactoryImageResponse(
[uri = imageSource.Uri(), size = imageSource.Size(), scale = imageSource.Scale(), context](
const winrt::Microsoft::ReactNative::IReactContext &reactContext,
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compositionContext)
-> winrt::Microsoft::ReactNative::Composition::Experimental::IBrush {
auto compositor = winrt::Microsoft::ReactNative::Composition::Experimental::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uniform initialization?

MicrosoftCompositionContextHelper::InnerCompositor(compositionContext);
auto drawingBrush = compositionContext.CreateDrawingSurfaceBrush(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: {} - brace initialization to avoid any type conversion happening and ensuring uniform initialization? https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es23-prefer-the--initializer-syntax

size,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied);
POINT pt;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POINT pt{}?

Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw(drawingBrush, scale, &pt);
auto renderTarget = autoDraw.GetRenderTarget();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: uniform initialization?


winrt::com_ptr<ID2D1SolidColorBrush> brush;
renderTarget->CreateSolidColorBrush({1.0f, 0.0f, 0.0f, 1.0f}, brush.put());
renderTarget->DrawEllipse(
{{(pt.x + size.Width / 2) / scale, (pt.y + size.Height / 2) / scale},
(size.Width / 2) / scale,
(size.Height / 2) / scale},
brush.get());

return drawingBrush;
});
}
};

Expand Down Expand Up @@ -223,6 +221,10 @@ struct WindowData {
std::wstring(L"file://").append(appDirectory).append(L"\\Bundle\\").c_str());
host.InstanceSettings().UseDeveloperSupport(true);

// Some of the images in RNTester require a user-agent header to properly fetch
winrt::Microsoft::ReactNative::Networking::SetDefaultUserAgent(
host.InstanceSettings(), L"React Native Windows Playground");

// Currently there is only SystemVisualSiteBridge which supports hosing ContentIslands within System
// Composition So our custom components do not run when running on lifted composition. This can be enabled in
// lifted once we have a VisualSiteBridge that works in lifted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,13 +694,21 @@ void CompositionRootView::Arrange(
}

#ifdef USE_WINUI3
winrt::Microsoft::UI::Content::ContentIsland CompositionRootView::Island() noexcept {
winrt::Microsoft::UI::Content::ContentIsland CompositionRootView::Island() {
if (!m_compositor) {
return nullptr;
}

if (!m_island) {
auto rootVisual = m_compositor.CreateSpriteVisual();
winrt::Microsoft::UI::Composition::SpriteVisual rootVisual{nullptr};
try {
rootVisual = m_compositor.CreateSpriteVisual();
} catch (const winrt::hresult_error &e) {
// If the compositor has been shutdown, then we shouldn't attempt to initialize the island
if (e.code() == RO_E_CLOSED)
return nullptr;
throw e;
}

InternalRootVisual(
winrt::Microsoft::ReactNative::Composition::Experimental::MicrosoftCompositionContextHelper::CreateVisual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct CompositionRootView

#ifdef USE_WINUI3
CompositionRootView(const winrt::Microsoft::UI::Composition::Compositor &compositor) noexcept;
winrt::Microsoft::UI::Content::ContentIsland Island() noexcept;
winrt::Microsoft::UI::Content::ContentIsland Island();
#endif

// property ReactViewHost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ void ImageComponentView::didReceiveImage(const std::shared_ptr<ImageResponseImag
void ImageComponentView::didReceiveFailureFromObserver(const facebook::react::ImageLoadError &error) noexcept {
auto imageEventEmitter = std::static_pointer_cast<facebook::react::ImageEventEmitter const>(m_eventEmitter);
if (imageEventEmitter) {
// TODO: #13262 - Pipe through ImageErrorInfo
imageEventEmitter->onError({});
std::shared_ptr<facebook::react::ImageErrorInfo> errorInfo =
std::static_pointer_cast<facebook::react::ImageErrorInfo>(error.getError());
imageEventEmitter->onError(*errorInfo);
imageEventEmitter->onLoadEnd();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,7 @@ winrt::com_ptr<::IDWriteTextLayout> WindowsTextInputComponentView::CreatePlaceho

void WindowsTextInputComponentView::DrawText() noexcept {
m_needsRedraw = true;
if (m_cDrawBlock || theme()->IsEmpty()) {
if (m_cDrawBlock || theme()->IsEmpty() || !m_textServices) {
return;
}

Expand Down
105 changes: 88 additions & 17 deletions vnext/Microsoft.ReactNative/Fabric/Composition/UriImageManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
#include "UriImageManager.h"

#include "Composition.ImageSource.g.h"
#include <Composition.Experimental.UriBrushFactoryImageResponse.g.cpp>
#include <Composition.ImageFailedResponse.g.cpp>
#include <Composition.ImageResponse.g.cpp>
#include <Composition.StreamImageResponse.g.cpp>
#include <Composition.UriBrushFactoryImageResponse.g.cpp>
#include <AutoDraw.h>
#include <IBlobPersistor.h>
#include <Networking/NetworkPropertyIds.h>
#include <ReactPropertyBag.h>
#include <d2d1_3.h>
#include <shcore.h>
#include <winrt/Microsoft.ReactNative.Composition.Experimental.h>
#include <winrt/Microsoft.ReactNative.Composition.h>
#include <winrt/Windows.Security.Cryptography.h>
#include <winrt/Windows.Storage.Streams.h>

Expand Down Expand Up @@ -54,16 +62,14 @@ winrt::Microsoft::ReactNative::Composition::ImageSource MakeImageSource(
* />
*
*/
struct SvgDataImageHandler : winrt::implements<
SvgDataImageHandler,
winrt::Microsoft::ReactNative::Composition::Experimental::IUriBrushProvider,
winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
struct SvgDataImageHandler
: winrt::implements<SvgDataImageHandler, winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
bool CanLoadImageUri(winrt::Microsoft::ReactNative::IReactContext context, winrt::Windows::Foundation::Uri uri) {
return uri.SchemeName() == L"data" && std::wstring_view(uri.Path()).starts_with(L"image/svg+xml;base64,");
}

winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::Experimental::UriBrushFactory>
GetSourceAsync(
winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
GetImageResponseAsync(
const winrt::Microsoft::ReactNative::IReactContext &context,
const winrt::Microsoft::ReactNative::Composition::ImageSource &imageSource) {
auto path = winrt::to_string(imageSource.Uri().Path());
Expand All @@ -85,7 +91,7 @@ struct SvgDataImageHandler : winrt::implements<
co_await memoryStream.WriteAsync(buffer);
memoryStream.Seek(0);

co_return
co_return winrt::Microsoft::ReactNative::Composition::Experimental::UriBrushFactoryImageResponse(
[memoryStream, size, scale](
const winrt::Microsoft::ReactNative::IReactContext &reactContext,
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compositionContext)
Expand Down Expand Up @@ -126,12 +132,13 @@ struct SvgDataImageHandler : winrt::implements<
renderTarget->SetTransform(originalTransform);

return drawingBrush;
};
});

} catch (winrt::hresult_error const &) {
} catch (winrt::hresult_error const &ex) {
co_return winrt::Microsoft::ReactNative::Composition::ImageFailedResponse(ex.message());
}

co_return nullptr;
winrt::throw_hresult(E_UNEXPECTED);
}
};

Expand All @@ -145,15 +152,14 @@ struct SvgDataImageHandler : winrt::implements<
* />
*
*/
struct DataImageHandler : winrt::implements<
DataImageHandler,
winrt::Microsoft::ReactNative::Composition::IUriImageStreamProvider,
winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
struct DataImageHandler
: winrt::implements<DataImageHandler, winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
bool CanLoadImageUri(winrt::Microsoft::ReactNative::IReactContext context, winrt::Windows::Foundation::Uri uri) {
return uri.SchemeName() == L"data";
}

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::Streams::IRandomAccessStream> GetSourceAsync(
winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
GetImageResponseAsync(
const winrt::Microsoft::ReactNative::IReactContext &context,
const winrt::Microsoft::ReactNative::Composition::ImageSource &imageSource) {
auto path = winrt::to_string(imageSource.Uri().Path());
Expand All @@ -173,12 +179,57 @@ struct DataImageHandler : winrt::implements<
co_await memoryStream.WriteAsync(buffer);
memoryStream.Seek(0);

co_return memoryStream;
co_return winrt::Microsoft::ReactNative::Composition::StreamImageResponse(memoryStream);
} catch (winrt::hresult_error const &) {
// Base64 decode failed
co_return winrt::Microsoft::ReactNative::Composition::ImageFailedResponse(
L"Invalid base64 encoding in inline image data");
}

winrt::throw_hresult(E_UNEXPECTED);
}
};

/**
* This ImageHandler will handle uri loading data from blobs
*
* <Image
* style={{width: 400, height: 200}}
* source={{uri:'blob:<guid>?offset=<offset>&size=<size>'}}
* />
*
*/
struct BlobImageHandler
: winrt::implements<BlobImageHandler, winrt::Microsoft::ReactNative::Composition::IUriImageProvider> {
bool CanLoadImageUri(winrt::Microsoft::ReactNative::IReactContext context, winrt::Windows::Foundation::Uri uri) {
return uri.SchemeName() == L"blob";
}

winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
GetImageResponseAsync(
const winrt::Microsoft::ReactNative::IReactContext &context,
const winrt::Microsoft::ReactNative::Composition::ImageSource &imageSource) {
if (auto prop = winrt::Microsoft::ReactNative::ReactPropertyBag(context.Properties())
.Get(::Microsoft::React::BlobModulePersistorPropertyId())) {
auto weakBlobPersistor = prop.Value();
if (auto persistor = weakBlobPersistor.lock()) {
auto queryParsed = imageSource.Uri().QueryParsed();
auto guid = winrt::to_string(imageSource.Uri().Path());
int64_t offset = _atoi64(winrt::to_string(queryParsed.GetFirstValueByName(L"offset")).c_str());
int64_t size = _atoi64(winrt::to_string(queryParsed.GetFirstValueByName(L"size")).c_str());

auto arr = persistor->ResolveMessage(std::move(guid), offset, size);
winrt::Windows::Storage::Streams::InMemoryRandomAccessStream memoryStream;
winrt::Windows::Storage::Streams::DataWriter dataWriter{memoryStream};
dataWriter.WriteBytes(arr);
co_await dataWriter.StoreAsync();
memoryStream.Seek(0);

co_return winrt::Microsoft::ReactNative::Composition::StreamImageResponse(memoryStream.CloneStream());
}
}

co_return nullptr;
co_return winrt::Microsoft::ReactNative::Composition::ImageFailedResponse(L"Failed to load image from blob");
}
};

Expand All @@ -191,6 +242,7 @@ static const ReactPropertyId<ReactNonAbiValue<std::shared_ptr<UriImageManager>>>
UriImageManager::UriImageManager() {
m_providers.push_back(winrt::make<SvgDataImageHandler>());
m_providers.push_back(winrt::make<DataImageHandler>());
m_providers.push_back(winrt::make<BlobImageHandler>());
}

void UriImageManager::Install(
Expand Down Expand Up @@ -227,4 +279,23 @@ IUriImageProvider UriImageManager::TryGetUriImageProvider(
return nullptr;
}

ImageResponseOrImageErrorInfo ImageResponse::ResolveImage() {
winrt::throw_hresult(E_NOTIMPL);
}

ImageResponseOrImageErrorInfo ImageFailedResponse::ResolveImage() {
ImageResponseOrImageErrorInfo imageOrError;
imageOrError.errorInfo = std::make_shared<facebook::react::ImageErrorInfo>();
imageOrError.errorInfo->responseCode = static_cast<int>(m_statusCode);
imageOrError.errorInfo->error = winrt::to_string(m_errorMessage);
if (imageOrError.errorInfo->error.empty()) {
imageOrError.errorInfo->error = "Failed to load image.";
}
for (auto &&[header, value] : m_responseHeaders) {
imageOrError.errorInfo->httpResponseHeaders.push_back(
std::make_pair<std::string, std::string>(winrt::to_string(header), winrt::to_string(value)));
}
return imageOrError;
}

} // namespace winrt::Microsoft::ReactNative::Composition::implementation