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
+ );