Skip to content

Commit f31c164

Browse files
authored
chore(rendering): support cross-platform skia-canvas (#835)
1 parent 3b21683 commit f31c164

11 files changed

Lines changed: 109 additions & 306 deletions

File tree

apps/api/src/skin/skin.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
* https://github.com/Statsify/statsify/blob/main/LICENSE
77
*/
88

9-
import { Canvas, type Image } from "skia-canvas";
109
import { HttpService } from "@nestjs/axios";
10+
import { type Image } from "skia-canvas";
1111
import { InjectModel } from "@m8a/nestjs-typegoose";
1212
import { Injectable, InternalServerErrorException } from "@nestjs/common";
1313
import { PlayerNotFoundException } from "@statsify/api-client";
1414
import { Skin } from "@statsify/schemas";
1515
import { catchError, lastValueFrom, map } from "rxjs";
16+
import { createCanvas, loadImage } from "@statsify/rendering";
1617
import { getMinecraftTexturePath } from "@statsify/assets";
17-
import { loadImage } from "@statsify/rendering";
1818
import { renderSkin } from "@statsify/skin-renderer";
1919
import type { ReturnModelType } from "@typegoose/typegoose";
2020

@@ -30,7 +30,7 @@ export class SkinService {
3030
.then((skin) => this.resolveSkin(skin?.skinUrl, skin?.slim ?? false))
3131
.catch(() => this.resolveSkin(undefined, false));
3232

33-
const canvas = new Canvas(size, size);
33+
const canvas = createCanvas(size, size);
3434
const ctx = canvas.getContext("2d");
3535
ctx.imageSmoothingEnabled = false;
3636

apps/discord-bot/src/commands/config/badge.command.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ import {
1717
LocalizeFunction,
1818
SubCommand,
1919
} from "@statsify/discord";
20-
import { Canvas, Image } from "skia-canvas";
20+
import { type Canvas, Image } from "skia-canvas";
2121
import { DemoProfile } from "./demo.profile.js";
2222
import { User, UserTier } from "@statsify/schemas";
23+
import { createCanvas, loadImage, render } from "@statsify/rendering";
2324
import { getBackground, getLogo } from "@statsify/assets";
2425
import { getTheme } from "#themes";
25-
import { loadImage, render } from "@statsify/rendering";
2626

2727
@Command({ description: (t) => t("commands.badge") })
2828
export class BadgeCommand {
@@ -88,7 +88,7 @@ export class BadgeCommand {
8888
(t) => t("errors.unknown.description")
8989
);
9090

91-
const canvas = new Canvas(32, 32);
91+
const canvas = createCanvas(32, 32);
9292
const ctx = canvas.getContext("2d");
9393
ctx.imageSmoothingEnabled = false;
9494

apps/discord-bot/src/commands/minecraft/cape.command.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
MojangPlayerArgument,
1515
PaginateService,
1616
} from "@statsify/discord";
17-
import { Canvas, Image, loadImage } from "skia-canvas";
17+
import { Image, loadImage } from "skia-canvas";
18+
import { createCanvas } from "@statsify/rendering";
1819
import type { Skin } from "@statsify/schemas";
1920

2021
@Command({ description: (t) => t("commands.cape"), args: [MojangPlayerArgument] })
@@ -71,7 +72,7 @@ export class CapeCommand {
7172
}
7273

7374
private renderCape(cape: Image) {
74-
const canvas = new Canvas(636, 1024);
75+
const canvas = createCanvas(636, 1024);
7576
const ctx = canvas.getContext("2d");
7677
ctx.imageSmoothingEnabled = false;
7778

apps/discord-bot/src/commands/minecraft/skin.command.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
IMessage,
1515
MojangPlayerArgument,
1616
} from "@statsify/discord";
17-
import { Canvas } from "skia-canvas";
1817
import { STATUS_COLORS } from "@statsify/logger";
18+
import { createCanvas } from "@statsify/rendering";
1919

2020
@Command({ description: (t) => t("commands.skin"), args: [MojangPlayerArgument] })
2121
export class SkinCommand {
@@ -30,7 +30,7 @@ export class SkinCommand {
3030
);
3131

3232
const skin = await this.apiService.getPlayerSkin(player.uuid, user);
33-
const canvas = new Canvas(skin.width, skin.height);
33+
const canvas = createCanvas(skin.width, skin.height);
3434
canvas.getContext("2d").drawImage(skin, 0, 0);
3535

3636
const embed = new EmbedBuilder()

apps/scripts/src/rank-emojis.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
* https://github.com/Statsify/statsify/blob/main/LICENSE
77
*/
88

9-
import { Canvas } from "skia-canvas";
10-
import { FontRenderer } from "@statsify/rendering";
9+
import { FontRenderer, createCanvas } from "@statsify/rendering";
1110
import { Logger } from "@statsify/logger";
1211
import { RestClient } from "tiny-discord";
1312
import { config, minecraftColors } from "@statsify/util";
@@ -75,15 +74,15 @@ const drawRank = async (formatted) => {
7574
const nodes = renderer.lex(formatted);
7675
const { width, height } = renderer.measureText(nodes);
7776

78-
const textCanvas = new Canvas(width, height);
77+
const textCanvas = createCanvas(width, height);
7978
const textCtx = textCanvas.getContext("2d");
8079
renderer.fillText(textCtx, nodes, 0, 0);
8180

8281
const imageCount = Math.ceil(width / SIZE);
8382
const images = [];
8483

8584
for (let x = 0; x < imageCount; x++) {
86-
const canvas = new Canvas(SIZE, SIZE);
85+
const canvas = createCanvas(SIZE, SIZE);
8786
const ctx = canvas.getContext("2d");
8887
ctx.drawImage(textCanvas, SIZE * x, 0, SIZE, SIZE, 0, 0, SIZE, SIZE);
8988
images.push(await canvas.toDataURL("png"));

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"verify-server": "pnpm --filter verify-server"
2727
},
2828
"packageManager": "pnpm@10.15.0",
29+
"pnpm": {
30+
"overrides": {
31+
"skia-canvas": "3.0.8"
32+
}
33+
},
2934
"devDependencies": {
3035
"@commitlint/cli": "^19.6.1",
3136
"@commitlint/config-conventional": "^19.6.0",

packages/rendering/src/canvas.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright (c) Statsify
3+
*
4+
* This source code is licensed under the GNU GPL v3 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* https://github.com/Statsify/statsify/blob/main/LICENSE
7+
*/
8+
9+
import { Canvas } from "skia-canvas";
10+
11+
type CanvasOptions = ConstructorParameters<typeof Canvas>[2] & { gpu?: boolean };
12+
13+
export function createCanvas(
14+
width?: number,
15+
height?: number,
16+
options: CanvasOptions = {}
17+
): Canvas {
18+
return new Canvas(width, height, { gpu: false, ...options });
19+
}

packages/rendering/src/font/font-renderer.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
import _positions from "../../positions.json" with { type: "json" };
1010
import _sizes from "../../sizes.json" with { type: "json" };
1111
import {
12-
Canvas,
12+
type Canvas,
1313
type CanvasRenderingContext2D,
1414
type ImageData,
1515
} from "skia-canvas";
1616
import { type TextNode, type Token, tokens } from "./tokens.js";
17+
import { createCanvas } from "../canvas.js";
1718
import { join } from "node:path";
1819
import { loadImage } from "#hooks";
1920
import { readdir } from "node:fs/promises";
@@ -34,9 +35,13 @@ interface Sizes {
3435

3536
export class FontRenderer {
3637
private images: Map<string, CanvasRenderingContext2D>;
38+
private canvases: WeakMap<CanvasRenderingContext2D, Canvas>;
39+
private scales: WeakMap<CanvasRenderingContext2D, number>;
3740

3841
public constructor(private gradient: boolean) {
3942
this.images = new Map();
43+
this.canvases = new WeakMap();
44+
this.scales = new WeakMap();
4045
}
4146

4247
public async loadImages(fontPath: string) {
@@ -47,13 +52,16 @@ export class FontRenderer {
4752
for (const file of pictures) {
4853
const image = await loadImage(join(fontPath, file));
4954

50-
const canvas = new Canvas(image.width, image.height);
55+
const canvas = createCanvas(image.width, image.height);
5156
const ctx = canvas.getContext("2d");
5257

5358
ctx.imageSmoothingEnabled = false;
5459

5560
ctx.drawImage(image, 0, 0);
5661

62+
this.canvases.set(ctx, canvas);
63+
this.scales.set(ctx, image.width / 256);
64+
5765
this.images.set(
5866
file.replace("unicode_page_", "").replace(".png", ""),
5967
ctx
@@ -62,6 +70,8 @@ export class FontRenderer {
6270
}
6371

6472
public measureText(nodes: TextNode[]): { width: number; height: number } {
73+
if (!nodes.length) return { width: 0, height: 0 };
74+
6575
let width = 0;
6676
let largestSize = nodes[0].size;
6777

@@ -189,7 +199,18 @@ export class FontRenderer {
189199
}
190200

191201
private getTextureScale(image: CanvasRenderingContext2D) {
192-
return image.canvas.width / 256;
202+
return this.scales.get(image) ?? image.canvas.width / 256;
203+
}
204+
205+
private getImageData(
206+
image: CanvasRenderingContext2D,
207+
x: number,
208+
y: number,
209+
width: number,
210+
height: number
211+
) {
212+
const ctx = this.canvases.get(image)?.getContext("2d") ?? image;
213+
return ctx.getImageData(x, y, width, height);
193214
}
194215

195216
private getCharacterIndexLocation(unicode: string, isAscii: boolean) {
@@ -306,7 +327,7 @@ export class FontRenderer {
306327
size,
307328
} = metadata;
308329

309-
const imageData = image.getImageData(charX, charY, width, height);
330+
const imageData = this.getImageData(image, charX, charY, width, height);
310331

311332
ctx.filter = this.gradient ? "brightness(15%)" : "brightness(25%)";
312333

packages/rendering/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* eslint-disable @typescript-eslint/no-namespace */
1010
export * from "./colors/index.js";
11+
export * from "./canvas.js";
1112
export * from "./font/index.js";
1213
export * from "./hooks/index.js";
1314
export * from "./jsx/index.js";

packages/rendering/src/jsx/render.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
*/
88

99
import * as Sentry from "@sentry/node";
10-
import { Canvas, type CanvasRenderingContext2D } from "skia-canvas";
10+
import { type Canvas, type CanvasRenderingContext2D } from "skia-canvas";
1111
import { Container } from "typedi";
1212
import { FontRenderer } from "#font";
1313
import { IntrinsicRenders, intrinsicRenders } from "./instrinsics.js";
14+
import { createCanvas } from "../canvas.js";
1415
import { createInstructions } from "./create-instructions.js";
1516
import { getPositionalDelta, getTotalSize } from "./util.js";
1617
import { noop } from "@statsify/util";
@@ -137,7 +138,7 @@ export function render(node: ElementNode, theme?: Theme): Canvas {
137138
description: "Render JSX",
138139
});
139140

140-
const canvas = new Canvas(width, height);
141+
const canvas = createCanvas(width, height);
141142
const ctx = canvas.getContext("2d");
142143
ctx.imageSmoothingEnabled = false;
143144

0 commit comments

Comments
 (0)