diff --git a/cli/Makefile b/cli/Makefile index e243f361..a5a8bea0 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,5 +1,5 @@ ENTRYPOINT=main.ts -DENO_PERMISSIONS=--allow-ffi --allow-net --allow-env --allow-read --allow-write +DENO_PERMISSIONS=--allow-net --allow-env --allow-read --allow-write run: fontgen cd ../scripts; \ @@ -17,14 +17,14 @@ build: fontgen check deno run --allow-env --allow-read --allow-net --allow-write bundle.ts install: build - deno install --unstable $(DENO_PERMISSIONS) -f -n fabric bundled.ts + deno install $(DENO_PERMISSIONS) -f -n fabric bundled.ts install-stable: build deno install $(DENO_PERMISSIONS) -f -n fabric bundled.ts init: rm -rf ./test - deno run --unstable $(DENO_PERMISSIONS) $(ENTRYPOINT) init ./test + deno run $(DENO_PERMISSIONS) $(ENTRYPOINT) init ./test init-stable: rm -rf ./test diff --git a/cli/commands/init.ts b/cli/commands/init.ts index 58d73305..8b52ac2a 100644 --- a/cli/commands/init.ts +++ b/cli/commands/init.ts @@ -13,9 +13,12 @@ import * as utils from "../utils.ts"; import { ensureDir } from "https://deno.land/std@0.177.1/fs/ensure_dir.ts"; import fontData from "../font.ts"; import { decodeBase64 } from "https://deno.land/std@0.203.0/encoding/base64.ts"; +import * as png from "https://deno.land/x/pngs@0.1.1/mod.ts"; +import * as pureimage from "npm:pureimage@0.4.13"; +// @deno-types="npm:@types/opentype.js" +import * as _opentype from "npm:opentype.js@1.3.4"; -const canGenerateIcon = Object.hasOwn(Deno, "dlopen") && - Deno.permissions.querySync({ name: "ffi" }).state == "granted"; +const canGenerateIcon = true; const error = colors.bold.red; const progress = colors.bold.yellow; @@ -47,10 +50,6 @@ const optionArg = { hidden: true, }; -interface CanvasFacade extends generator.CanvasFactory { - registerFont?(data: Uint8Array, alias?: string): void; -} - interface PromptedConfiguration extends generator.Configuration { generateIcon: boolean; } @@ -83,12 +82,19 @@ export function initCommand() { ) .arguments("[dir:file]") .action(async (options, dir: string | undefined) => { - await generate(options, dir); + const fontFile = await Deno.makeTempFile(); + + try { + await generate(options, fontFile, dir); + } finally { + await Deno.remove(fontFile); + } }); } async function generate( cli: CliOptions, + fontFile: string, outputDirName: string | undefined, ) { const outputDir = await getAndPrepareOutputDir(outputDirName); @@ -103,7 +109,9 @@ async function generate( ? defaultOptions(path.basename(outputDir)) : promptUser(path.basename(outputDir), cli)); - const canvas = await resolveCanvasFacade(config.generateIcon); + await Deno.writeFile(fontFile, decodeBase64(fontData)); + const fontLoader = pureimage.registerFont(fontFile, generator.ICON_FONT); + await fontLoader.load(); const options: generator.Options = { config, @@ -112,30 +120,48 @@ async function generate( await writeFile(outputDir, contentPath, content, options); }, }, - canvas, + canvas: { + create(width, height) { + const bitmap = pureimage.make(width, height); + + return { + getContext: (id) => bitmap.getContext(id), + getPng: () => { + const p = png.encode(bitmap.data, bitmap.width, bitmap.height); + return p; + }, + measureText(ctx: pureimage.Context, text) { + const font = fontLoader.font; + const fontSize = ctx._font.size!; + + let advance = 0; + let ascent = 0; + let descent = 0; + + const glyphs = font.stringToGlyphs(text); + + for (const glyph of glyphs) { + const metrics = glyph.getMetrics(); + advance += glyph.advanceWidth!; + ascent = Math.max(ascent, metrics.yMax); + descent = Math.min(descent, metrics.yMin); + } + + return { + width: (advance / font.unitsPerEm) * fontSize, + ascent: Math.abs((ascent / font.unitsPerEm) * fontSize), + descent: Math.abs((descent / font.unitsPerEm) * fontSize), + }; + }, + }; + }, + }, }; console.log(progress("Generating mod template...")); - const fontFile = await registerFont(canvas); - - try { - await generator.generateTemplate(options); - console.log(success("Done!")); - } finally { - if (fontFile) await Deno.remove(fontFile); - } -} - -async function registerFont(canvas: CanvasFacade): Promise { - if (!canvas.registerFont) return null; - - // Using the font data directly doesn't seem to work... - const fontFile = await Deno.makeTempFile(); - await Deno.writeFile(fontFile, decodeBase64(fontData)); - canvas.registerFont(await Deno.readFile(fontFile), generator.ICON_FONT); - - return fontFile; + await generator.generateTemplate(options); + console.log(success("Done!")); } async function getAndPrepareOutputDir( @@ -154,21 +180,6 @@ async function getAndPrepareOutputDir( return outputDir; } -async function resolveCanvasFacade(enabled: boolean): Promise { - if (!enabled) { - return { - create: () => null, - }; - } - - const canvas = await import("https://deno.land/x/skia_canvas@0.5.4/mod.ts"); - - return { - create: canvas.createCanvas, - registerFont: canvas.Fonts.register, - }; -} - async function promptUser( startingName: string, cli: CliOptions, @@ -343,7 +354,7 @@ async function writeFile( }; // is there a cleaner way to do this? - if (content instanceof ArrayBuffer) { + if (content instanceof ArrayBuffer || content instanceof Uint8Array) { const data = new Uint8Array(content); await Deno.writeFile(output, data, writeOptions); } else { @@ -388,7 +399,7 @@ async function requestPermissions(outputDir: string) { { name: "net", host: "maven.fabricmc.net", - } + }, ]; for (const permission of permissions) { diff --git a/cli/commands/upgrade.ts b/cli/commands/upgrade.ts index 3544d6aa..ddbce5d7 100644 --- a/cli/commands/upgrade.ts +++ b/cli/commands/upgrade.ts @@ -27,12 +27,8 @@ class UpdateProvider extends Provider { override async upgrade( {}: UpgradeOptions, ): Promise { - // Enable the unstable flag only if the current installation is unstable already - const unstable = Object.hasOwn(Deno, "dlopen") ? ["--unstable"] : []; - const args = [ "install", - ...unstable, "--force", "--reload", "--quiet", diff --git a/cli/fontgen.ts b/cli/fontgen.ts index b8c5a90b..afaa8eeb 100644 --- a/cli/fontgen.ts +++ b/cli/fontgen.ts @@ -1,6 +1,13 @@ import { encodeBase64 } from "https://deno.land/std@0.203.0/encoding/base64.ts"; -const font = await Deno.readFile("../assets/fonts/ComicRelief-Regular.woff2"); -const base64 = encodeBase64(font) +// @deno-types="npm:@types/wawoff2" +import * as wawoff2 from "npm:wawoff2@2.0.1"; -Deno.writeTextFileSync("./font.ts", `export default ${JSON.stringify(base64)};`); +const woff2 = await Deno.readFile("../assets/fonts/ComicRelief-Regular.woff2"); +const woff = await wawoff2.decompress(woff2); +const base64 = encodeBase64(woff); + +Deno.writeTextFileSync( + "./font.ts", + `export default ${JSON.stringify(base64)};`, +); diff --git a/scripts/src/lib/Template.svelte b/scripts/src/lib/Template.svelte index 9bf73d5a..8430c63c 100644 --- a/scripts/src/lib/Template.svelte +++ b/scripts/src/lib/Template.svelte @@ -5,6 +5,7 @@ import { ICON_FONT, getTemplateGameVersions } from "./template/template"; import { minecraftSupportsDataGen, minecraftSupportsSplitSources, computeCustomModIdErrors, sharedModIdChecks, formatPackageName, nameToModId} from "./template/minecraft"; import { computePackageNameErrors } from "./template/java" + import { decode64 } from "./template/utils"; let minecraftVersion: string; let projectName = "Template Mod"; @@ -75,7 +76,19 @@ const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; - return canvas; + + return { + getContext: (id) => canvas.getContext(id), + getPng: () => decode64(canvas.toDataURL().split(";base64,")[1]), + measureText(ctx: CanvasRenderingContext2D, text) { + const metrics = ctx.measureText(text); + return { + width: metrics.width, + ascent: metrics.actualBoundingBoxAscent, + descent: metrics.actualBoundingBoxDescent + } + } + }; }, } }); @@ -124,7 +137,7 @@

Choose a name for your new mod. The mod ID will be {modid}. Use custom id

{/if} - + {#if modIdErrors != undefined} {#each modIdErrors as error} diff --git a/scripts/src/lib/template/icon.ts b/scripts/src/lib/template/icon.ts index 3cd5da03..998d4cd5 100644 --- a/scripts/src/lib/template/icon.ts +++ b/scripts/src/lib/template/icon.ts @@ -1,25 +1,24 @@ -import { ICON_FONT, type CanvasFactory, type CanvasLike } from "./template"; +import { ICON_FONT, type CanvasAdaptorFactory, type CanvasAdaptor } from "./template"; import { decode64 } from "./utils"; const DEFAULT_ICON = "iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAQMAAAD58POIAAAABlBMVEUAAAD///+l2Z/dAAABeklEQVRIx9XTsW1cMQwGYAoKIlfWbaAVUroKbxSPcBtIQRaTN9EILFkI+l3ovXuSLvYZCK4wK+IrWBD/T1iGHg41fBUcygwWKY7QLGiCuoLaNoOY+1BnKFfQDUhnyGeZIZ3lNEO+LFBef01gyutlAlvOEzRb6MIDVCskDAD01MEJCTeKjWgHI1wp1A3Ui9Wg5HSHoFaDkN1BgroOzwN4ORkJuQNXX738NsKFAULpEI2wdIhAdRLTCM1JzBIHsMJZokSAkK/ApQMAbMADhCycDzASsoQObwDwR31Sn154AFJHpwPqT6NENACZOgIambYBNrCYAOSQflAcIEDCmPXEDyrQ42GP9wBuBXuAMqBENxBneCGegVNYIPsFygrqFqh2gXYDZoG+j5BuIC6QVyh8D444aPgXBKi/B9XtUD+ANkH9CsAuNz4D/wH8/Rx4gPZIsP8DDlAeHrRBXUFXkCugQ/YLUJhBiCeg3o8JsICdAKm38OhtGio2zveBd37Jm8IEWUmfAAAAAElFTkSuQmCC"; -export function generateModIcon(name: string, factory: CanvasFactory): ArrayBufferLike { +export function generateModIcon(name: string, factory: CanvasAdaptorFactory): ArrayBufferLike { const canvas = factory.create(128, 128); if (canvas != null && drawModIcon(canvas, name)) { - return decode64(canvas.toDataURL().split(";base64,")[1]); + return canvas.getPng(); } else { return decode64(DEFAULT_ICON); } } -export function drawModIcon(canvas: CanvasLike, name: string): boolean { +export function drawModIcon(canvas: CanvasAdaptor, name: string): boolean { const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; if (ctx == null) return false; - ctx.rect(0, 0, 128, 128); ctx.fillStyle = "#ffffff"; - ctx.fill(); + ctx.fillRect(0, 0, 128, 128); const words = name.split(/\s+/); @@ -44,8 +43,8 @@ export function drawModIcon(canvas: CanvasLike, name: string): boolean { for (let i = 0; i < words.length; i++) { const word = words[i]; ctx.font = `${fontSize}px ${ICON_FONT}`; - const metrics = ctx.measureText(word); - lineHeight[i] = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; + const metrics = canvas.measureText(ctx, word); + lineHeight[i] = metrics.ascent + metrics.descent; totalHeight += lineHeight[i]; } @@ -65,10 +64,11 @@ export function drawModIcon(canvas: CanvasLike, name: string): boolean { ctx.font = `${fontSize}px ${ICON_FONT}`; ctx.fillStyle = "#000000"; ctx.textAlign = "center"; - const metrics = ctx.measureText(word); - ctx.fillText(word, 64, textY + offset + metrics.actualBoundingBoxAscent); + const metrics = canvas.measureText(ctx, word); + ctx.fillText(word, 64, textY + offset + metrics.ascent); } + console.log(fontSize) return true; } diff --git a/scripts/src/lib/template/modjson.ts b/scripts/src/lib/template/modjson.ts index c8de5bbf..38351f37 100644 --- a/scripts/src/lib/template/modjson.ts +++ b/scripts/src/lib/template/modjson.ts @@ -1,4 +1,4 @@ -import type { CanvasFactory, ComputedConfiguration, TemplateWriter } from "./template"; +import type { CanvasAdaptorFactory, ComputedConfiguration, TemplateWriter } from "./template"; import { generateClientMixin, generateMixin } from "./mixin"; import { generateEntrypoint } from "./modentrypoint"; import { getJavaVersion } from "./java" @@ -9,13 +9,13 @@ function usesNewModid(fabricVersion: string) : boolean { return Number(fabricVersion.split(".")[1]) >= 59; } -export async function addModJson(writer: TemplateWriter, canvas: CanvasFactory, config: ComputedConfiguration) { - var mixins = [ +export async function addModJson(writer: TemplateWriter, canvas: CanvasAdaptorFactory, config: ComputedConfiguration) { + const mixins = [ ...await generateMixin(writer, config), ...(config.splitSources ? await generateClientMixin(writer, config) : []) ]; - var fabricModJson : any = { + const fabricModJson : any = { "schemaVersion": 1, "id": config.modid, "version": "${version}", diff --git a/scripts/src/lib/template/template.ts b/scripts/src/lib/template/template.ts index 27b045b5..9cf7ba3c 100644 --- a/scripts/src/lib/template/template.ts +++ b/scripts/src/lib/template/template.ts @@ -16,7 +16,7 @@ export interface Options { * this might be directly to the filesystem or to a zip file. */ writer: TemplateWriter; - canvas: CanvasFactory; + canvas: CanvasAdaptorFactory; } export interface Configuration { @@ -55,13 +55,20 @@ export interface TemplateWriter { write(path: string, content: string | ArrayBufferLike, options?: FileOptions): Promise } -export interface CanvasFactory { - create(width: number, height: number): CanvasLike | null +export interface CanvasAdaptorFactory { + create(width: number, height: number): CanvasAdaptor | null } -export interface CanvasLike { - getContext(contextId: "2d"): unknown, - toDataURL(): string +export interface CanvasAdaptor { + getContext(contextId: "2d"): unknown + getPng(): ArrayBufferLike + measureText(ctx: unknown, text: string): TextMetricsAdaptor +} + +export interface TextMetricsAdaptor { + width: number + ascent: number + descent: number } export async function generateTemplate(options: Options) {