diff --git a/package/src/__tests__/snapshots/font/green.png b/package/src/__tests__/snapshots/font/green.png new file mode 100644 index 0000000000..01e721a782 Binary files /dev/null and b/package/src/__tests__/snapshots/font/green.png differ diff --git a/package/src/__tests__/snapshots/font/red.png b/package/src/__tests__/snapshots/font/red.png new file mode 100644 index 0000000000..484940037f Binary files /dev/null and b/package/src/__tests__/snapshots/font/red.png differ diff --git a/package/src/renderer/__tests__/Data.spec.tsx b/package/src/renderer/__tests__/Data.spec.tsx new file mode 100644 index 0000000000..acacb016b1 --- /dev/null +++ b/package/src/renderer/__tests__/Data.spec.tsx @@ -0,0 +1,70 @@ +import path from "path"; + +import React from "react"; + +import { processResult } from "../../__tests__/setup"; +import { Fill } from "../components"; +import * as SkiaRenderer from "../index"; +import type { SkData } from "../../skia/types/Data/Data"; + +import { mountCanvas, nodeRequire, Skia } from "./setup"; + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface EmptyProps {} + +const CheckFont = ({}: EmptyProps) => { + const { useFont } = require("../../skia/core/Font"); + const font = useFont( + nodeRequire( + path.resolve(__dirname, "../../skia/__tests__/assets/Roboto-Medium.ttf") + ) + ); + if (!font) { + return ; + } + return ; +}; + +const CheckDataCollection = ({}: EmptyProps) => { + const { useDataCollection } = require("../../skia/core/Data"); + const font = useDataCollection( + [ + nodeRequire( + path.resolve(__dirname, "../../skia/__tests__/assets/Roboto-Medium.ttf") + ), + nodeRequire( + path.resolve(__dirname, "../../skia/__tests__/assets/Roboto-Medium.ttf") + ), + ], + (data: SkData) => Skia.Typeface.MakeFreeTypeFaceFromData(data) + ); + if (!font) { + return ; + } + return ; +}; + +describe("Data Loading", () => { + it("Loads renderer without Skia", async () => { + expect(SkiaRenderer).toBeDefined(); + }); + it("Should load a font file", async () => { + const { surface, draw } = mountCanvas(); + draw(); + processResult(surface, "snapshots/font/red.png"); + await wait(500); + draw(); + processResult(surface, "snapshots/font/green.png"); + }); + + it("Should load many font files", async () => { + const { surface, draw } = mountCanvas(); + draw(); + processResult(surface, "snapshots/font/red.png"); + await wait(500); + draw(); + processResult(surface, "snapshots/font/green.png"); + }); +}); diff --git a/package/src/renderer/__tests__/setup.tsx b/package/src/renderer/__tests__/setup.tsx index 20eb41f5b0..d0a9dd7eb2 100644 --- a/package/src/renderer/__tests__/setup.tsx +++ b/package/src/renderer/__tests__/setup.tsx @@ -1,3 +1,5 @@ +import fs from "fs"; + import React from "react"; import type { ReactNode } from "react"; import ReactReconciler from "react-reconciler"; @@ -13,6 +15,15 @@ import { LoadSkia } from "../../web"; export let Skia: ReturnType; +jest.mock("react-native", () => ({ + Platform: { OS: "web" }, + Image: { + resolveAssetSource: jest.fn, + }, +})); + +export const nodeRequire = (uri: string) => fs.readFileSync(uri); + beforeAll(async () => { await LoadSkia(); Skia = JsiSkApi(global.CanvasKit); @@ -39,6 +50,7 @@ export const drawOnNode = (element: ReactNode) => { }; export const mountCanvas = (element: ReactNode) => { + global.SkiaApi = Skia; expect(Skia).toBeDefined(); const surface = Skia.Surface.Make(width, height)!; expect(surface).toBeDefined(); diff --git a/package/src/skia/core/Data.ts b/package/src/skia/core/Data.ts index e755864e4a..8d641d73a9 100644 --- a/package/src/skia/core/Data.ts +++ b/package/src/skia/core/Data.ts @@ -11,71 +11,88 @@ const resolveAsset = (source: ReturnType) => { : Image.resolveAssetSource(source).uri; }; -export const useDataCollection = ( - sources: DataSource[], - factory: (data: SkData[]) => T, - deps: DependencyList = [] +const factoryWrapper = ( + data2: SkData, + factory: (data: SkData) => T, + onError?: (err: Error) => void ) => { - const [data, setData] = useState(null); - useEffect(() => { - const bytesOrURIs = sources.map((source) => { - if (source instanceof Uint8Array) { - return source; - } - return typeof source === "string" ? source : resolveAsset(source); - }); - Promise.all( - bytesOrURIs.map((bytesOrURI) => - bytesOrURI instanceof Uint8Array - ? Skia.Data.fromBytes(bytesOrURI) - : Skia.Data.fromURI(bytesOrURI) - ) - ).then((d) => setData(factory(d))); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, deps); - return data; + const factoryResult = factory(data2); + if (factoryResult === null) { + onError && onError(new Error("Could not load data")); + return null; + } else { + return factoryResult; + } }; -export const useRawData = ( - source: DataSource | null | undefined, +const loadDataCollection = ( + sources: DataSource[], + factory: (data: SkData) => T, + onError?: (err: Error) => void +): Promise<(T | null)[]> => + Promise.all(sources.map((source) => loadData(source, factory, onError))); + +const loadData = ( + source: DataSource, factory: (data: SkData) => T, onError?: (err: Error) => void +): Promise => { + if (source instanceof Uint8Array) { + return new Promise((resolve) => + resolve(factoryWrapper(Skia.Data.fromBytes(source), factory, onError)) + ); + } else { + const uri = typeof source === "string" ? source : resolveAsset(source); + return Skia.Data.fromURI(uri).then((d) => + factoryWrapper(d, factory, onError) + ); + } +}; + +type Source = DataSource | null | undefined; + +const useLoading = ( + source: Source, + loader: () => Promise, + deps: DependencyList = [] ) => { const [data, setData] = useState(null); - const prevSourceRef = useRef(); + const prevSourceRef = useRef(); useEffect(() => { - // Track to avoid re-fetching the same data if (prevSourceRef.current !== source) { prevSourceRef.current = source; - if (source !== null && source !== undefined) { - const factoryWrapper = (data2: SkData) => { - const factoryResult = factory(data2); - if (factoryResult === null) { - onError && onError(new Error("Could not load data")); - setData(null); - } else { - setData(factoryResult); - } - }; - if (source instanceof Uint8Array) { - factoryWrapper(Skia.Data.fromBytes(source)); - } else { - const uri = - typeof source === "string" ? source : resolveAsset(source); - Skia.Data.fromURI(uri).then((d) => factoryWrapper(d)); - } - } else { - // new source is null or undefined -> remove cached data - setData(null); - } + loader().then(setData); + } else { + setData(null); } - }, [factory, onError, source]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); return data; }; +export const useDataCollection = ( + sources: DataSource[], + factory: (data: SkData) => T, + onError?: (err: Error) => void, + deps?: DependencyList +) => + useLoading( + sources, + () => loadDataCollection(sources, factory, onError), + deps + ); + +export const useRawData = ( + source: DataSource | null | undefined, + factory: (data: SkData) => T, + onError?: (err: Error) => void, + deps?: DependencyList +) => useLoading(source, () => loadData(source, factory, onError), deps); + const identity = (data: SkData) => data; export const useData = ( source: DataSource | null | undefined, - onError?: (err: Error) => void -) => useRawData(source, identity, onError); + onError?: (err: Error) => void, + deps?: DependencyList +) => useRawData(source, identity, onError, deps); diff --git a/package/src/skia/core/Typeface.ts b/package/src/skia/core/Typeface.ts index 05610604a1..6ae36806f3 100644 --- a/package/src/skia/core/Typeface.ts +++ b/package/src/skia/core/Typeface.ts @@ -8,4 +8,9 @@ import { useRawData } from "./Data"; export const useTypeface = ( source: DataSource | null | undefined, onError?: (err: Error) => void -) => useRawData(source, Skia.Typeface.MakeFreeTypeFaceFromData, onError); +) => + useRawData( + source, + Skia.Typeface.MakeFreeTypeFaceFromData.bind(Skia.Typeface), + onError + );