From e724bc8a8581bb871e4a8cd659c5b5207264a303 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 28 Jun 2024 08:47:50 +0200 Subject: [PATCH 1/2] Implement SVG support for Web (#2500) --- example/package.json | 1 + example/src/Examples/API/SVG.tsx | 5 +- example/webpack.config.js | 2 +- example/yarn.lock | 43 +++++---------- .../types/NativeBuffer/NativeBufferFactory.ts | 2 +- package/src/skia/web/JsiSkCanvas.ts | 8 ++- package/src/skia/web/JsiSkSVG.ts | 27 ++++++++++ package/src/skia/web/JsiSkSVGFactory.ts | 53 ++++++++++++++++--- 8 files changed, 98 insertions(+), 43 deletions(-) create mode 100644 package/src/skia/web/JsiSkSVG.ts diff --git a/example/package.json b/example/package.json index 0326961809..7e7bc1eed5 100644 --- a/example/package.json +++ b/example/package.json @@ -62,6 +62,7 @@ "jest-transform-stub": "2.0.0", "metro-react-native-babel-preset": "0.73.9", "prettier": "2.4.1", + "raw-loader": "^4.0.2", "react-test-renderer": "18.2.0", "typescript": "5.1.6", "url-loader": "4.1.1", diff --git a/example/src/Examples/API/SVG.tsx b/example/src/Examples/API/SVG.tsx index 69705d1e66..b0997e5b9e 100644 --- a/example/src/Examples/API/SVG.tsx +++ b/example/src/Examples/API/SVG.tsx @@ -1,10 +1,11 @@ import React from "react"; import { useWindowDimensions } from "react-native"; -import { Canvas, ImageSVG, useSVG } from "@shopify/react-native-skia"; +import { Canvas, ImageSVG, Skia } from "@shopify/react-native-skia"; export const SVG = () => { const { width, height } = useWindowDimensions(); - const svg = useSVG(require("./tiger.svg")); + const svg = Skia.SVG.MakeFromString(require("./tiger.svg").default); + // useSVG(require("./tiger.svg")); return ( diff --git a/example/webpack.config.js b/example/webpack.config.js index f37bab8f26..8781bd3d47 100644 --- a/example/webpack.config.js +++ b/example/webpack.config.js @@ -29,7 +29,7 @@ const svgLoaderConfiguration = { test: /\.(svg)$/, use: [ { - loader: "@svgr/webpack", + loader: "raw-loader", }, ], }; diff --git a/example/yarn.lock b/example/yarn.lock index 0fcef1e720..00b95942db 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -2465,9 +2465,7 @@ "@shopify/react-native-skia@link:../package": version "0.0.0" - dependencies: - canvaskit-wasm "0.39.1" - react-reconciler "0.27.0" + uid "" "@sideway/address@^4.1.5": version "4.1.5" @@ -9120,6 +9118,14 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rc@~1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -10134,16 +10140,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10221,7 +10218,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10235,13 +10232,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -11199,7 +11189,7 @@ wonka@^6.3.2: resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.4.tgz#76eb9316e3d67d7febf4945202b5bdb2db534594" integrity sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11217,15 +11207,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" diff --git a/package/src/skia/types/NativeBuffer/NativeBufferFactory.ts b/package/src/skia/types/NativeBuffer/NativeBufferFactory.ts index 51160cedd6..87ce186fca 100644 --- a/package/src/skia/types/NativeBuffer/NativeBufferFactory.ts +++ b/package/src/skia/types/NativeBuffer/NativeBufferFactory.ts @@ -25,7 +25,7 @@ export const isNativeBufferWeb = ( buffer instanceof HTMLCanvasElement || buffer instanceof ImageBitmap || buffer instanceof OffscreenCanvas || - buffer instanceof VideoFrame || + (typeof VideoFrame !== "undefined" && buffer instanceof VideoFrame) || buffer instanceof HTMLImageElement || buffer instanceof SVGImageElement || buffer instanceof CanvasKitWebGLBuffer; diff --git a/package/src/skia/web/JsiSkCanvas.ts b/package/src/skia/web/JsiSkCanvas.ts index ea2a8a31f2..2dcf1710af 100644 --- a/package/src/skia/web/JsiSkCanvas.ts +++ b/package/src/skia/web/JsiSkCanvas.ts @@ -42,6 +42,7 @@ import { JsiSkMatrix } from "./JsiSkMatrix"; import { JsiSkImageFilter } from "./JsiSkImageFilter"; import { JsiSkPoint } from "./JsiSkPoint"; import { JsiSkRSXform } from "./JsiSkRSXform"; +import type { JsiSkSVG } from "./JsiSkSVG"; export class JsiSkCanvas extends HostObject @@ -302,8 +303,11 @@ export class JsiSkCanvas ); } - drawSvg(_svgDom: SkSVG, _width?: number, _height?: number) { - throw new Error("drawSvg is not implemented on React Native Web"); + drawSvg(svg: SkSVG, _width?: number, _height?: number) { + const image = this.CanvasKit.MakeImageFromCanvasImageSource( + (svg as JsiSkSVG).ref + ); + this.ref.drawImage(image, 0, 0); } save() { diff --git a/package/src/skia/web/JsiSkSVG.ts b/package/src/skia/web/JsiSkSVG.ts new file mode 100644 index 0000000000..e61cdd50a2 --- /dev/null +++ b/package/src/skia/web/JsiSkSVG.ts @@ -0,0 +1,27 @@ +import type { CanvasKit } from "canvaskit-wasm"; + +import type { SkSVG } from "../types"; + +import { HostObject } from "./Host"; + +export class JsiSkSVG + extends HostObject + implements SkSVG +{ + constructor(CanvasKit: CanvasKit, ref: HTMLImageElement) { + super(CanvasKit, ref, "SVG"); + } + + width(): number { + return this.ref.width; + } + height(): number { + return this.ref.height; + } + + dispose = () => { + if (this.ref.parentNode) { + this.ref.parentNode.removeChild(this.ref); + } + }; +} diff --git a/package/src/skia/web/JsiSkSVGFactory.ts b/package/src/skia/web/JsiSkSVGFactory.ts index d6ede136f7..09593b40df 100644 --- a/package/src/skia/web/JsiSkSVGFactory.ts +++ b/package/src/skia/web/JsiSkSVGFactory.ts @@ -1,20 +1,61 @@ import type { CanvasKit } from "canvaskit-wasm"; -import type { SkData, SkSVG } from "../types"; +import type { SkSVG } from "../types"; import type { SVGFactory } from "../types/SVG/SVGFactory"; -import { Host, NotImplementedOnRNWeb } from "./Host"; +import { Host } from "./Host"; +import type { JsiSkData } from "./JsiSkData"; +import { JsiSkSVG } from "./JsiSkSVG"; export class JsiSkSVGFactory extends Host implements SVGFactory { constructor(CanvasKit: CanvasKit) { super(CanvasKit); } - MakeFromData(_data: SkData): SkSVG | null { - throw new NotImplementedOnRNWeb(); + MakeFromData(data: JsiSkData): SkSVG | null { + const decoder = new TextDecoder("utf-8"); + const str = decoder.decode(data.ref); + return this.MakeFromString(str); } - MakeFromString(_str: string): SkSVG | null { - throw new NotImplementedOnRNWeb(); + MakeFromString(str: string): SkSVG | null { + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(str, "image/svg+xml"); + const svgElement = svgDoc.documentElement; + + const attrWidth = svgElement.getAttribute("width"); + const attrHeight = svgElement.getAttribute("height"); + let width = attrWidth ? parseFloat(attrWidth) : null; + let height = attrHeight ? parseFloat(attrHeight) : null; + + const svgDataUrl = + "data:image/svg+xml;charset=utf-8," + encodeURIComponent(str); + // Create a new HTMLImageElement + const img = new Image(); + img.src = svgDataUrl; + + // Optionally set styles or attributes on the image + img.style.display = "none"; + img.alt = "SVG Image"; + if (!width || !height) { + const viewBox = svgElement.getAttribute("viewBox"); + if (viewBox) { + const viewBoxValues = viewBox.split(" "); + if (viewBoxValues.length === 4) { + width = width || parseFloat(viewBoxValues[2]); + height = height || parseFloat(viewBoxValues[3]); + } + } + } + if (width && height) { + img.width = width; + img.height = height; + } + + img.onerror = (e) => { + console.error("SVG failed to load", e); + }; + document.body.appendChild(img); + return new JsiSkSVG(this.CanvasKit, img); } } From 89d2a43ebedb5ea5bd9dcb016bd5d34a4bba5564 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 28 Jun 2024 09:09:07 +0200 Subject: [PATCH 2/2] :wrench: --- docs/docs/getting-started/web.mdx | 9 ++------- example/src/Examples/API/SVG.tsx | 5 ++--- package/src/skia/core/SVG.web.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 package/src/skia/core/SVG.web.ts diff --git a/docs/docs/getting-started/web.mdx b/docs/docs/getting-started/web.mdx index c8e04838e2..4793527679 100644 --- a/docs/docs/getting-started/web.mdx +++ b/docs/docs/getting-started/web.mdx @@ -105,8 +105,8 @@ export default function App() { ``` :::info -When using expo router in dev mode you CANNOT load components that are inside the app directory, as they will get evaluated by the router before CanvasKit has loaded. -Make syure the component to load lies outside the 'app' directory (or whatever has been configured as the router folder). +When using expo router in dev mode you cannot load components that are inside the app directory, as they will get evaluated by the router before CanvasKit is loaded. +Make sure the component to load lies outside the 'app' directory. ::: @@ -168,11 +168,6 @@ To request these features, please submit [a feature request on GitHub](https://g * `PathFactory.MakeFromText()` * `ShaderFilter` -**Unplanned** - -* `ImageSvg` - - ## Manual webpack Installation To enable React Native Skia on Web using webpack, three key actions are required: diff --git a/example/src/Examples/API/SVG.tsx b/example/src/Examples/API/SVG.tsx index b0997e5b9e..69705d1e66 100644 --- a/example/src/Examples/API/SVG.tsx +++ b/example/src/Examples/API/SVG.tsx @@ -1,11 +1,10 @@ import React from "react"; import { useWindowDimensions } from "react-native"; -import { Canvas, ImageSVG, Skia } from "@shopify/react-native-skia"; +import { Canvas, ImageSVG, useSVG } from "@shopify/react-native-skia"; export const SVG = () => { const { width, height } = useWindowDimensions(); - const svg = Skia.SVG.MakeFromString(require("./tiger.svg").default); - // useSVG(require("./tiger.svg")); + const svg = useSVG(require("./tiger.svg")); return ( diff --git a/package/src/skia/core/SVG.web.ts b/package/src/skia/core/SVG.web.ts new file mode 100644 index 0000000000..4c4c6c2b81 --- /dev/null +++ b/package/src/skia/core/SVG.web.ts @@ -0,0 +1,29 @@ +import { Skia } from "../Skia"; +import type { DataSourceParam } from "../types"; + +export const useSVG = ( + source: DataSourceParam, + onError?: (err: Error) => void +) => { + if (source === null || source === undefined) { + throw new Error(`Invalid svg data source. Got: ${source}`); + } + if ( + typeof source !== "object" || + source instanceof Uint8Array || + typeof source.default !== "string" + ) { + throw new Error( + `Invalid svg data source. Make sure that the source resolves to a string. Got: ${JSON.stringify( + source, + null, + 2 + )}` + ); + } + const svg = Skia.SVG.MakeFromString(source.default); + if (svg === null && onError !== undefined) { + onError(new Error("Failed to create SVG from source.")); + } + return svg; +};