Skip to content

Commit 8629005

Browse files
committed
more font converter updates
1 parent 838ff47 commit 8629005

File tree

2 files changed

+125
-29
lines changed

2 files changed

+125
-29
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

fontconverter.js

+124-29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
/* Copyright 2023 Gordon Williams, [email protected]
2-
https://github.com/espruino/EspruinoWebTools/fontconverter.js
1+
/* https://github.com/espruino/EspruinoWebTools/fontconverter.js
32
3+
Copyright (C) 2024 Gordon Williams <[email protected]>
4+
5+
This Source Code Form is subject to the terms of the Mozilla Public
6+
License, v. 2.0. If a copy of the MPL was not distributed with this
7+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
8+
9+
----------------------------------------------------------------------------------------
410
Bitmap font creator for Espruino Graphics custom fonts
11+
12+
Takes input as a PNG font map, PBFF, or bitfontmaker2 JSON
13+
14+
Outputs in various formats to make a custom font
15+
----------------------------------------------------------------------------------------
16+
17+
Requires:
18+
19+
npm install btoa pngjs
520
*/
621
(function (root, factory) {
722
if (typeof define === 'function' && define.amd) {
@@ -18,7 +33,7 @@
1833
}
1934
}(typeof self !== 'undefined' ? self : this, function(heatshrink) {
2035

21-
// npm install btoa pngjs
36+
2237

2338
function bitsToBytes(bits, bpp) {
2439
var bytes = [];
@@ -45,7 +60,9 @@ function bitsToBytes(bits, bpp) {
4560
fn
4661
mapWidth,mapHeight
4762
mapOffsetX, mapOffsetY
48-
height
63+
bpp // optional (default 1)
64+
height // height of individual character
65+
range : [ {min, max}, {min,max} ] // for characters to use
4966
}
5067
*/
5168
function Font(info) {
@@ -54,9 +71,12 @@ function Font(info) {
5471
this.fn = info.fn;
5572
this.height = info.height;
5673
this.bpp = info.bpp||1;
57-
this.firstChar = (info.firstChar!==undefined) ? info.firstChar : 32;
58-
this.maxChars = info.maxChars || (256-this.firstChar);
59-
this.lastChar = this.firstChar + this.maxChars - 1;
74+
75+
this.range = info.range;
76+
if (!this.range)
77+
this.range = getRanges().ASCII.range;
78+
79+
6080
this.fixedWidth = !!info.fixedWidth;
6181
this.fullHeight = !!info.fullHeight; // output fonts at the full height available
6282
this.glyphPadX = 1; // padding at the end of glyphs (needed for old-style JS fonts)
@@ -70,6 +90,17 @@ function Font(info) {
7090
this.mapOffsetY = 0|info.mapOffsetY;
7191
}
7292

93+
/*
94+
font // owning font
95+
ch // character number this represents
96+
getPixel(x,y) // function to get a pixel within the font
97+
xStart
98+
yStart
99+
xEnd
100+
yEnd
101+
height
102+
advance
103+
*/
73104
function FontGlyph(font, ch, getPixel) {
74105
this.font = font;
75106
this.ch = ch;
@@ -80,8 +111,10 @@ function FontGlyph(font, ch, getPixel) {
80111

81112
// Populate the `glyphs` array with a range of glyphs
82113
Font.prototype.generateGlyphs = function(getCharPixel) {
83-
for (let ch=this.firstChar; ch<=this.lastChar; ch++)
84-
this.glyphs[ch] = this.getGlyph(ch, (x,y) => getCharPixel(ch,x,y));
114+
this.range.forEach(range => {
115+
for (let ch=range.min; ch<=range.max; ch++)
116+
this.glyphs[ch] = this.getGlyph(ch, (x,y) => getCharPixel(ch,x,y));
117+
});
85118
};
86119

87120
// Append the bits to define this glyph to the array 'bits'
@@ -188,7 +221,6 @@ FontGlyph.prototype.appendBits = function(bits, info) {
188221

189222
// Load a 16x16 charmap file (or mapWidth x mapHeight)
190223
function loadPNG(fontInfo) {
191-
fontInfo = new Font(fontInfo);
192224
var PNG = require("pngjs").PNG;
193225
var png = PNG.sync.read(require("fs").readFileSync(fontInfo.fn));
194226

@@ -223,7 +255,6 @@ function loadPNG(fontInfo) {
223255
}
224256

225257
function loadJSON(fontInfo) {
226-
fontInfo = new Font(fontInfo);
227258
// format used by https://www.pentacom.jp/pentacom/bitfontmaker2/editfont.php import/export
228259
var font = JSON.parse(require("fs").readFileSync(fontInfo.fn).toString());
229260
fontInfo.fmWidth = 16;
@@ -238,7 +269,6 @@ function loadJSON(fontInfo) {
238269

239270
function loadPBFF(fontInfo) {
240271
// format used by https://github.com/pebble-dev/renaissance/tree/master/files
241-
fontInfo = new Font(fontInfo);
242272
fontInfo.fmWidth = 0;
243273
fontInfo.fmHeight = fontInfo.height;
244274
fontInfo.glyphPadX = 0;
@@ -283,6 +313,7 @@ function loadPBFF(fontInfo) {
283313
}
284314

285315
function load(fontInfo) {
316+
fontInfo = new Font(fontInfo);
286317
if (fontInfo.fn && fontInfo.fn.endsWith("png")) return loadPNG(fontInfo);
287318
else if (fontInfo.fn && fontInfo.fn.endsWith("json")) return loadJSON(fontInfo);
288319
else if (fontInfo.fn && fontInfo.fn.endsWith("pbff")) return loadPBFF(fontInfo);
@@ -313,12 +344,49 @@ Font.prototype.debugChars = function() {
313344
});
314345
};
315346

347+
/* GNU unifont puts in placeholders for unimplemented chars -
348+
big filled blocks with the 4 digit char code. This detects these
349+
and removes them */
350+
Font.prototype.removeUnifontPlaceholders = function() {
351+
Object.keys(this.glyphs).forEach(ch => {
352+
let glyph = this.glyphs[ch];
353+
if (glyph.xStart==1 && glyph.yStart==1 && glyph.xEnd==15 && glyph.yEnd==14) {
354+
let borderEmpty = true;
355+
let edgesFilled = true;
356+
for (let x=1;x<15;x++) {
357+
if (glyph.getPixel(x,0)) borderEmpty = false;
358+
if (!glyph.getPixel(x,1)) edgesFilled = false;
359+
if (!glyph.getPixel(x,7)) edgesFilled = false;
360+
if (!glyph.getPixel(x,8)) edgesFilled = false;
361+
if (!glyph.getPixel(x,14)) edgesFilled = false;
362+
if (glyph.getPixel(x,15)) borderEmpty = false;
363+
// console.log(x, glyph.getPixel(x,0), glyph.getPixel(x,1))
364+
}
365+
for (let y=1;y<14;y++) {
366+
if (glyph.getPixel(0,y)) borderEmpty = false;
367+
if (!glyph.getPixel(1,y)) edgesFilled = false;
368+
if (!glyph.getPixel(2,y)) edgesFilled = false;
369+
if (!glyph.getPixel(7,y)) edgesFilled = false;
370+
if (!glyph.getPixel(8,y)) edgesFilled = false;
371+
if (!glyph.getPixel(13,y)) edgesFilled = false;
372+
if (!glyph.getPixel(14,y)) edgesFilled = false;
373+
}
374+
if (borderEmpty && edgesFilled) {
375+
// it's a placeholder!
376+
// glyph.debug();
377+
delete this.glyphs[ch]; // remove it
378+
}
379+
}
380+
});
381+
};
382+
316383
// Outputs as JavaScript for a custom font
317384
Font.prototype.getJS = function(options) {
318385
// options.compressed
319386
options = options||{};
320387
this.glyphPadX = 1;
321-
var charMin = Object.keys(this.glyphs)[0];
388+
var charCodes = Object.keys(this.glyphs).sort();
389+
var charMin = charCodes[0];
322390
// stats
323391
var minY = this.height;
324392
var maxY = 0;
@@ -361,12 +429,6 @@ Font.prototype.getJS = function(options) {
361429
encodedFont = "atob('" + btoa(String.fromCharCode.apply(null, fontData)) + "')";
362430
}
363431

364-
/* return `
365-
var font = atob("${require('btoa')(bytes)}");
366-
var widths = atob("${require('btoa')(widthBytes)}");
367-
g.setFontCustom(font, ${this.firstChar}, widths, ${this.height} | ${this.bpp<<16});
368-
`;*/
369-
370432
return `Graphics.prototype.setFont${this.id} = function() {
371433
// Actual height ${maxY+1-minY} (${maxY} - ${minY})
372434
// ${this.bpp} BPP
@@ -385,6 +447,10 @@ Font.prototype.getHeaderFile = function() {
385447
var packedChars = 5;
386448
var packedPixels = 6;
387449

450+
var charCodes = Object.keys(this.glyphs).sort();
451+
var charMin = charCodes[0];
452+
var charMax = charCodes[charCodes.length-1];
453+
388454
function genChar(font, glyph) {
389455
var r = [];
390456
for (var y=0;y<font.fmHeight;y++) {
@@ -398,8 +464,8 @@ Font.prototype.getHeaderFile = function() {
398464
}
399465

400466
var header = "";
401-
var ch = this.firstChar;
402-
while (ch <= this.lastChar) {
467+
var ch = charMin;
468+
while (ch <= charMax) {
403469
var chars = [];
404470
for (var i=0;i<packedChars;i++) {
405471
var glyph = this.glyphs[ch];
@@ -430,7 +496,7 @@ Font.prototype.getPBF = function() {
430496
this.fullHeight = false; // TODO: too late?
431497
// now go through all glyphs
432498
var glyphs = [];
433-
var hashtableSize = ((this.lastChar-this.firstChar)>1000) ? 255 : 64;
499+
var hashtableSize = ((this.glyphs.length)>1000) ? 255 : 64;
434500
var hashes = [];
435501
for (var i=0;i<hashtableSize;i++)
436502
hashes[i] = [];
@@ -618,43 +684,72 @@ JsVar *jswrap_graphics_setFont${options.name}(JsVar *parent) {
618684
`);
619685
};
620686

687+
function getRanges() {
688+
// https://arxiv.org/pdf/1801.07779.pdf#page=5 is handy to see what covers most writing
689+
return { // https://www.unicode.org/charts/
690+
"ASCII" : {range : [{ min : 32, max : 127 }] },
691+
"ASCII Capitals" : {range : [{ min : 32, max : 93 }] },
692+
"Numeric" : {range : [{ min : 46, max : 58 }] },
693+
"ISO8859-1": {range : [{ min : 32, max : 255 }] },
694+
"Extended": {range : [{ min : 32, max : 1103 }] }, // 150 languages + Cyrillic
695+
"All": {range : [{ min : 32, max : 0x9FAF }] },
696+
"Chinese": {range : [{ min : 32, max : 255 }, { min : 0x4E00, max : 0x9FAF }] },
697+
"Korean": {range : [{ min : 32, max : 255 }, { min : 0x1100, max : 0x11FF }, { min : 0x3130, max : 0x318F }, { min : 0xA960, max : 0xA97F }, { min : 0xD7B0, max : 0xD7FF }] },
698+
"Japanese": {range : [{ min : 32, max : 255 }, { min : 0x3000, max : 0x30FF }, { min : 0x4E00, max : 0x9FAF }, { min : 0xFF00, max : 0xFFEF }] },
699+
};
700+
}
701+
621702

622703
/* load() loads a font. fontInfo should be:
623704
{
624705
fn : "font6x8.png", // currently a built-in font
625706
height : 8, // actual used height of font map
626-
firstChar : 32,
627-
maxChars : 256-32
707+
range : [ min:32, max:255 ]
628708
}
629709
630710
or:
631711
632712
{
633713
fn : "renaissance_28.pbff",
634714
height : 28, // actual used height of font map
635-
firstChar : 32,
715+
range : [ min:32, max:255 ]
636716
yOffset : 4,
637-
maxChars : 256-32
638717
}
639718
640719
or for a font made using https://www.pentacom.jp/pentacom/bitfontmaker2/
641720
642721
{
643722
fn : "bitfontmaker2_14px.json",
644723
height : 14, // actual used height of font map
645-
firstChar : 32,
646-
maxChars : 256-32
724+
range : [ min:32, max:255 ]
647725
}
648726
649727
650728
Afterwards returns a Font object populated with the args given, and
651729
a `function getCharPixel(ch,x,y)` which can be used to get the font data
730+
731+
732+
load returns a `Font` class which contains:
733+
734+
735+
'generateGlyphs', // used internally to create the `glyphs` array
736+
'getGlyph', // used internally to create the `glyphs` array
737+
'debugPixelsUsed', // show how many pixels used on each row
738+
'debugChars', // dump all loaded chars
739+
'removeUnifontPlaceholders' // for GNU unitfont, remove placeholder characters
740+
'getJS', // return the font as JS - only works for <1000 chars
741+
'getHeaderFile', // return the font as a C header file (uses data for each char including blank ones)
742+
'getPBF', // return a binary PBF file
743+
// eg. require("fs").writeFileSync("font.pbf", Buffer.from(font.getPBF()))
744+
'getPBFAsC' // return a binary PBF file, but as a C file that can be included in Espruino
745+
652746
*/
653747

654748

655749
// =======================================================
656750
return {
657751
Font : Font,
658-
load : load,
752+
load : load, // load a font from a file (see above)
753+
getRanges : getRanges // get list of possible ranges of characters
659754
};
660755
}));

0 commit comments

Comments
 (0)