Skip to content

Commit 4f3202d

Browse files
authored
fix(🌍): fix pixel density bug in makeImageSnapshot on RN Web (#3507)
1 parent f68b8af commit 4f3202d

File tree

8 files changed

+680
-6
lines changed

8 files changed

+680
-6
lines changed

apps/example/index.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1, shrink-to-fit=no"
9+
/>
10+
<link rel="icon" href="/src/assets/react.png" type="image/png" />
11+
<script src="https://unpkg.com/canvaskit-wasm/bin/full/canvaskit.js"></script>
12+
13+
<title>Example</title>
14+
<!-- The `react-native-web` recommended style reset: https://necolas.github.io/react-native-web/docs/setup/#root-element -->
15+
<style id="react-native-web-reset">
16+
/* These styles make the body full-height */
17+
html,
18+
body {
19+
height: 100%;
20+
}
21+
/* These styles disable body scrolling if you are using <ScrollView> */
22+
body {
23+
overflow: hidden;
24+
}
25+
/* These styles make the root element full-height */
26+
#root {
27+
display: flex;
28+
height: 100%;
29+
flex: 1;
30+
}
31+
</style>
32+
</head>
33+
<body>
34+
<noscript>You need to enable JavaScript to run this app.</noscript>
35+
<div id="root"></div>
36+
<script
37+
src="index.bundle?platform=web&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.routerRoot=app&unstable_transformProfile=hermes-stable"
38+
defer
39+
></script>
40+
</body>
41+
</html>

apps/example/index.web.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { AppRegistry } from "react-native";
2+
3+
import App from "./src/App";
4+
import { name as appName } from "./app.json";
5+
6+
AppRegistry.registerComponent(appName, () => App);
7+
8+
const rootTag = document.getElementById("root");
9+
10+
if (process.env.NODE_ENV !== "production") {
11+
if (!rootTag) {
12+
throw new Error(
13+
'Required HTML element with id "root" was not found in the document HTML.'
14+
);
15+
}
16+
}
17+
18+
CanvasKitInit({
19+
locateFile: (file) => `https://unpkg.com/canvaskit-wasm/bin/full/${file}`,
20+
}).then((CanvasKit) => {
21+
window.CanvasKit = global.CanvasKit = CanvasKit;
22+
AppRegistry.runApplication(appName, { rootTag });
23+
});

apps/example/metro.config.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
const path = require("path");
2+
const { resolve: defaultResolve } = require("metro-resolver");
13
const { makeMetroConfig } = require("@rnx-kit/metro-config");
2-
module.exports = makeMetroConfig({
4+
5+
const root = path.resolve(__dirname, "../..");
6+
const rnwPath = path.resolve(root, "node_modules/react-native-web");
7+
const assetRegistryPath = path.resolve(
8+
root,
9+
"node_modules/react-native-web/dist/modules/AssetRegistry/index",
10+
);
11+
12+
const metroConfig = makeMetroConfig({
313
transformer: {
414
getTransformOptions: async () => ({
515
transform: {
@@ -9,3 +19,39 @@ module.exports = makeMetroConfig({
919
}),
1020
},
1121
});
22+
23+
function getWebMetroConfig(config) {
24+
config.resolver = config.resolver ?? {};
25+
config.resolver.platforms = ["ios", "android", "web"];
26+
27+
const origResolveRequest =
28+
config.resolver.resolveRequest ??
29+
((context, moduleName, platform) =>
30+
defaultResolve(context, moduleName, platform));
31+
32+
config.resolver.resolveRequest = (contextRaw, moduleName, platform) => {
33+
const context = {
34+
...contextRaw,
35+
preferNativePlatform: false,
36+
};
37+
38+
if (moduleName === "react-native") {
39+
return {
40+
filePath: path.resolve(rnwPath, "dist/index.js"),
41+
type: "sourceFile",
42+
};
43+
}
44+
45+
// Let default config handle other modules
46+
return origResolveRequest(context, moduleName, platform);
47+
};
48+
49+
config.transformer = config.transformer ?? {};
50+
config.transformer.assetRegistryPath = assetRegistryPath;
51+
52+
return config;
53+
}
54+
55+
module.exports = !!process.env.IS_WEB_BUILD
56+
? getWebMetroConfig(metroConfig)
57+
: metroConfig;

apps/example/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"pod:install": "cd ios && pod install",
1717
"test": "jest",
1818
"tsc": "tsc --noEmit",
19-
"start": "react-native start"
19+
"start": "react-native start",
20+
"web": "IS_WEB_BUILD=true react-native start"
2021
},
2122
"dependencies": {
2223
"@callstack/react-native-visionos": "^0.75.0",
@@ -30,13 +31,15 @@
3031
"cdt2d": "^1.0.0",
3132
"its-fine": "^2.0.0",
3233
"react": "19.0.0",
34+
"react-dom": "19.0.0",
3335
"react-native": "0.78.0",
3436
"react-native-gesture-handler": "^2.24.0",
3537
"react-native-macos": "^0.78.3",
3638
"react-native-reanimated": "3.19.1",
3739
"react-native-safe-area-context": "^5.2.0",
3840
"react-native-screens": "^4.9.1",
3941
"react-native-svg": "^15.11.2",
42+
"react-native-web": "^0.21.2",
4043
"react-native-windows": "^0.75.0"
4144
},
4245
"devDependencies": {
@@ -54,6 +57,8 @@
5457
"@rnx-kit/metro-config": "^2.0.0",
5558
"@types/jest": "^29.5.13",
5659
"@types/react": "^19.0.0",
60+
"@types/react-dom": "^19.0.0",
61+
"@types/react-native-web": "^0.19.2",
5762
"@types/react-test-renderer": "^19.0.0",
5863
"eslint": "9.36.0",
5964
"eslint-config-react-native-wcandillon": "4.0.1",

apps/example/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { HomeScreen } from "./Home";
3939
import type { StackParamList } from "./types";
4040
import { useAssets } from "./Tests/useAssets";
4141
import { Chess } from "./Examples/Chess";
42+
import "./resolveAssetSourcePolyfill";
4243

4344
const linking: LinkingOptions<StackParamList> = {
4445
config: {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Image, PixelRatio, Platform } from "react-native";
2+
import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
3+
4+
// react-native-web does not support resolveAssetSource out of the box
5+
// https://github.com/necolas/react-native-web/issues/1666
6+
if (Platform.OS === "web") {
7+
function resolveAssetUri(source) {
8+
let uri = null;
9+
if (typeof source === "number") {
10+
// get the URI from the packager
11+
const asset = getAssetByID(source);
12+
if (asset == null) {
13+
throw new Error(
14+
`Image: asset with ID "${source}" could not be found. Please check the image source or packager.`,
15+
);
16+
}
17+
// eslint-disable-next-line prefer-destructuring
18+
let scale = asset.scales[0];
19+
if (asset.scales.length > 1) {
20+
const preferredScale = PixelRatio.get();
21+
// Get the scale which is closest to the preferred scale
22+
scale = asset.scales.reduce((prev, curr) =>
23+
Math.abs(curr - preferredScale) < Math.abs(prev - preferredScale)
24+
? curr
25+
: prev,
26+
);
27+
}
28+
const scaleSuffix = scale !== 1 ? `@${scale}x` : "";
29+
uri = asset
30+
? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}`
31+
: "";
32+
} else if (typeof source === "string") {
33+
uri = source;
34+
} else if (source && typeof source.uri === "string") {
35+
// eslint-disable-next-line prefer-destructuring
36+
uri = source.uri;
37+
}
38+
39+
if (uri) {
40+
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
41+
const match = uri.match(svgDataUriPattern);
42+
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
43+
if (match) {
44+
const [, prefix, svg] = match;
45+
const encodedSvg = encodeURIComponent(svg);
46+
return `${prefix}${encodedSvg}`;
47+
}
48+
}
49+
50+
return uri;
51+
}
52+
53+
Image.resolveAssetSource = (source) => {
54+
const uri = resolveAssetUri(source) || "";
55+
return { uri };
56+
};
57+
}

packages/skia/src/__tests__/setup.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import type { SkSurface, SkImage } from "../skia/types";
99

1010
export const E2E = process.env.E2E === "true";
1111
export const CI = process.env.CI === "true";
12+
export const WEB = process.env.WEB === "true";
1213
export const itFailsE2e = E2E ? it.failing : it;
13-
export const itRunsE2eOnly = E2E ? it : it.skip;
14+
export const itRunsE2eOnly = E2E && !WEB ? it : it.skip;
1415
export const itRunsNodeOnly = E2E ? it.skip : it;
1516
export const itRunsCIAndNodeOnly = CI || !E2E ? it : it.skip;
1617

0 commit comments

Comments
 (0)