Skip to content

Commit 8292630

Browse files
committed
wasm font rendering
(cherry picked from commit 5ff2118)
1 parent b041e88 commit 8292630

8 files changed

Lines changed: 173 additions & 0 deletions

File tree

‎ImageScript.js‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as png from './utils/png.js';
22
import * as gif from './utils/gif.js';
3+
import * as fontlib from './utils/wasm/font.js';
34

45
/**
56
* Represents an image; provides utility functions
@@ -21,7 +22,9 @@ export class Image {
2122
if (height < 1)
2223
throw new RangeError('Image has to be at least 1 pixel high');
2324

25+
/** @private */
2426
this.__width__ = width;
27+
/** @private */
2528
this.__height__ = height;
2629

2730
/**
@@ -934,6 +937,46 @@ export class Image {
934937

935938
return image;
936939
}
940+
941+
/**
942+
* Wrap at individual characters. For use with {@link Image.renderText}
943+
* @return {boolean}
944+
*/
945+
static get WRAP_STYLE_CHAR() {
946+
return true;
947+
}
948+
949+
/**
950+
* Wrap at word ends. For use with {@link Image.renderText}
951+
* @return {boolean}
952+
*/
953+
static get WRAP_STYLE_WORD() {
954+
return false;
955+
}
956+
957+
/**
958+
* Creates a new image containing the rendered text.
959+
* @param {Uint8Array} font .ttf font buffer to use
960+
* @param {number} scale Font size to use
961+
* @param {string} text Text to render
962+
* @param {number} color Text color to use
963+
* @param {number} wrapWidth Image width before wrapping
964+
* @param {boolean} wrap_style Whether to break at words (WRAP_STYLE_WORD) or at characters (WRAP_STYLE_CHAR)
965+
* @return {Promise<Image>} The rendered text
966+
*/
967+
static async renderText(font, scale, text, color = 0xffffffff, wrapWidth = Infinity, wrap_style = this.WRAP_STYLE_WORD) {
968+
const [r, g, b, a] = Image.colorToRGBA(color);
969+
await fontlib.load(0, font, scale);
970+
fontlib.render(0, 0, scale, r, g, b, text, wrapWidth === Infinity ? null : wrapWidth, wrap_style);
971+
const buffer = fontlib.buffer(0);
972+
const [width, height] = fontlib.meta(0);
973+
fontlib.free(0);
974+
const image = new Image(width, height);
975+
image.bitmap.set(buffer);
976+
image.opacity(a / 0xff);
977+
978+
return image;
979+
}
937980
}
938981

939982
/**

‎tests/font-1.png‎

21.8 KB
Loading

‎tests/font-2.png‎

20.6 KB
Loading

‎tests/font.js‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Fonts sourced from:
3+
https://www.1001fonts.com/carbon-font.html
4+
https://www.1001fonts.com/ethnocentric-font.html
5+
*/
6+
7+
import {Image} from '../ImageScript.js';
8+
import {equal} from "https://deno.land/std/bytes/mod.ts";
9+
10+
const panic = message => {
11+
console.error(message);
12+
process.exit(1);
13+
};
14+
15+
(async () => {
16+
{
17+
const font = await Image.renderText(await Deno.readFile('./tests/fonts/carbon phyber.ttf'), 128, 'ThE qUiCk');
18+
const encoded = await font.encode();
19+
20+
const desired = await Deno.readFile('./tests/font-1.png');
21+
if (!equal(desired, encoded)) panic('font 1 doesn\'t match');
22+
}
23+
24+
{
25+
const font = await Image.renderText(await Deno.readFile('./tests/fonts/ethnocentric rg.ttf'), 128, 'BrOwN fOx');
26+
const encoded = await font.encode();
27+
28+
const desired = await Deno.readFile('./tests/font-2.png');
29+
if (!equal(desired, encoded)) panic('font 2 doesn\'t match');
30+
}
31+
})();

‎tests/fonts/carbon phyber.ttf‎

147 KB
Binary file not shown.

‎tests/fonts/ethnocentric rg.ttf‎

69.6 KB
Binary file not shown.

‎utils/wasm/font.js‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
let wasm;
2+
3+
{
4+
const module = new WebAssembly.Module(await fetch('https://github.com/matmen/ImageScript/raw/deno/utils/wasm/font.wasm').then(r => r.arrayBuffer()));
5+
const instance = new WebAssembly.Instance(module);
6+
7+
wasm = instance.exports;
8+
}
9+
10+
let u8array_ref = new Uint8Array(wasm.memory.buffer);
11+
let i32array_ref = new Int32Array(wasm.memory.buffer);
12+
let u32array_ref = new Uint32Array(wasm.memory.buffer);
13+
14+
const utf8encoder = new TextEncoder();
15+
16+
function u8array() {
17+
return u8array_ref.buffer === wasm.memory.buffer ? u8array_ref : (u8array_ref = new Uint8Array(wasm.memory.buffer));
18+
}
19+
20+
function i32array() {
21+
return i32array_ref.buffer === wasm.memory.buffer ? i32array_ref : (i32array_ref = new Int32Array(wasm.memory.buffer));
22+
}
23+
24+
function u32array() {
25+
return u32array_ref.buffer === wasm.memory.buffer ? u32array_ref : (u32array_ref = new Uint32Array(wasm.memory.buffer));
26+
}
27+
28+
function ptr_to_u8array(ptr, len) {
29+
return u8array().subarray(ptr, ptr + len);
30+
}
31+
32+
function ptr_to_u32array(ptr, len) {
33+
return u32array().subarray(ptr / 4, ptr / 4 + len);
34+
}
35+
36+
function u8array_to_ptr(buffer) {
37+
const ptr = wasm.__wbindgen_malloc(buffer.length);
38+
u8array().set(buffer, ptr);
39+
40+
return ptr;
41+
}
42+
43+
function string_to_ptr(string) {
44+
let offset = 0;
45+
let len = string.length;
46+
let ptr = wasm.__wbindgen_malloc(string.length);
47+
48+
const u8 = u8array();
49+
while (len > offset) {
50+
const code = string.charCodeAt(offset);
51+
52+
if (code > 0x7F) break;
53+
u8[ptr + offset++] = code;
54+
}
55+
56+
if (offset !== len) {
57+
if (offset !== 0) string = string.substring(offset);
58+
ptr = wasm.__wbindgen_realloc(ptr, len, len = offset + string.length * 3);
59+
const ret = utf8encoder.encodeInto(string, u8array().subarray(ptr + offset, ptr + len));
60+
61+
offset += ret.written;
62+
}
63+
64+
return [ptr, offset];
65+
}
66+
67+
const nullish = x => x == null;
68+
69+
export function render(ptr, id, scale, r, g, b, text, max_width, wrap_style = false) {
70+
const str = string_to_ptr(text);
71+
wasm.render(ptr, id, scale, r, g, b, str[0], str[1], !nullish(max_width), max_width || 0, wrap_style);
72+
}
73+
74+
export function buffer(id) {
75+
wasm.buffer(8, id);
76+
const i32 = i32array();
77+
const slice = ptr_to_u8array(i32[2], i32[3]).slice();
78+
wasm.__wbindgen_free(i32[2], i32[3]);
79+
80+
return slice;
81+
}
82+
83+
export function meta(id) {
84+
wasm.meta(8, id);
85+
const i32 = i32array();
86+
const slice = ptr_to_u32array(i32[2], i32[3]).slice();
87+
wasm.__wbindgen_free(i32[2], 4 * i32[3]);
88+
89+
return slice;
90+
}
91+
92+
export async function load(id, buffer, scale = 128) {
93+
wasm = await wasm;
94+
wasm.load(id, u8array_to_ptr(buffer), buffer.length, scale);
95+
}
96+
97+
export function free(id) {
98+
wasm.free(id);
99+
}

‎utils/wasm/font.wasm‎

150 KB
Binary file not shown.

0 commit comments

Comments
 (0)