1
- /* Copyright 2023 Gordon Williams, [email protected]
2
- https://github.com/espruino/EspruinoWebTools/fontconverter.js
1
+ /* https://github.com/espruino/EspruinoWebTools/fontconverter.js
3
2
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
+ ----------------------------------------------------------------------------------------
4
10
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
5
20
*/
6
21
( function ( root , factory ) {
7
22
if ( typeof define === 'function' && define . amd ) {
18
33
}
19
34
} ( typeof self !== 'undefined' ? self : this , function ( heatshrink ) {
20
35
21
- // npm install btoa pngjs
36
+
22
37
23
38
function bitsToBytes ( bits , bpp ) {
24
39
var bytes = [ ] ;
@@ -45,7 +60,9 @@ function bitsToBytes(bits, bpp) {
45
60
fn
46
61
mapWidth,mapHeight
47
62
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
49
66
}
50
67
*/
51
68
function Font ( info ) {
@@ -54,9 +71,12 @@ function Font(info) {
54
71
this . fn = info . fn ;
55
72
this . height = info . height ;
56
73
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
+
60
80
this . fixedWidth = ! ! info . fixedWidth ;
61
81
this . fullHeight = ! ! info . fullHeight ; // output fonts at the full height available
62
82
this . glyphPadX = 1 ; // padding at the end of glyphs (needed for old-style JS fonts)
@@ -70,6 +90,17 @@ function Font(info) {
70
90
this . mapOffsetY = 0 | info . mapOffsetY ;
71
91
}
72
92
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
+ */
73
104
function FontGlyph ( font , ch , getPixel ) {
74
105
this . font = font ;
75
106
this . ch = ch ;
@@ -80,8 +111,10 @@ function FontGlyph(font, ch, getPixel) {
80
111
81
112
// Populate the `glyphs` array with a range of glyphs
82
113
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
+ } ) ;
85
118
} ;
86
119
87
120
// Append the bits to define this glyph to the array 'bits'
@@ -188,7 +221,6 @@ FontGlyph.prototype.appendBits = function(bits, info) {
188
221
189
222
// Load a 16x16 charmap file (or mapWidth x mapHeight)
190
223
function loadPNG ( fontInfo ) {
191
- fontInfo = new Font ( fontInfo ) ;
192
224
var PNG = require ( "pngjs" ) . PNG ;
193
225
var png = PNG . sync . read ( require ( "fs" ) . readFileSync ( fontInfo . fn ) ) ;
194
226
@@ -223,7 +255,6 @@ function loadPNG(fontInfo) {
223
255
}
224
256
225
257
function loadJSON ( fontInfo ) {
226
- fontInfo = new Font ( fontInfo ) ;
227
258
// format used by https://www.pentacom.jp/pentacom/bitfontmaker2/editfont.php import/export
228
259
var font = JSON . parse ( require ( "fs" ) . readFileSync ( fontInfo . fn ) . toString ( ) ) ;
229
260
fontInfo . fmWidth = 16 ;
@@ -238,7 +269,6 @@ function loadJSON(fontInfo) {
238
269
239
270
function loadPBFF ( fontInfo ) {
240
271
// format used by https://github.com/pebble-dev/renaissance/tree/master/files
241
- fontInfo = new Font ( fontInfo ) ;
242
272
fontInfo . fmWidth = 0 ;
243
273
fontInfo . fmHeight = fontInfo . height ;
244
274
fontInfo . glyphPadX = 0 ;
@@ -283,6 +313,7 @@ function loadPBFF(fontInfo) {
283
313
}
284
314
285
315
function load ( fontInfo ) {
316
+ fontInfo = new Font ( fontInfo ) ;
286
317
if ( fontInfo . fn && fontInfo . fn . endsWith ( "png" ) ) return loadPNG ( fontInfo ) ;
287
318
else if ( fontInfo . fn && fontInfo . fn . endsWith ( "json" ) ) return loadJSON ( fontInfo ) ;
288
319
else if ( fontInfo . fn && fontInfo . fn . endsWith ( "pbff" ) ) return loadPBFF ( fontInfo ) ;
@@ -313,12 +344,49 @@ Font.prototype.debugChars = function() {
313
344
} ) ;
314
345
} ;
315
346
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
+
316
383
// Outputs as JavaScript for a custom font
317
384
Font . prototype . getJS = function ( options ) {
318
385
// options.compressed
319
386
options = options || { } ;
320
387
this . glyphPadX = 1 ;
321
- var charMin = Object . keys ( this . glyphs ) [ 0 ] ;
388
+ var charCodes = Object . keys ( this . glyphs ) . sort ( ) ;
389
+ var charMin = charCodes [ 0 ] ;
322
390
// stats
323
391
var minY = this . height ;
324
392
var maxY = 0 ;
@@ -361,12 +429,6 @@ Font.prototype.getJS = function(options) {
361
429
encodedFont = "atob('" + btoa ( String . fromCharCode . apply ( null , fontData ) ) + "')" ;
362
430
}
363
431
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
-
370
432
return `Graphics.prototype.setFont${ this . id } = function() {
371
433
// Actual height ${ maxY + 1 - minY } (${ maxY } - ${ minY } )
372
434
// ${ this . bpp } BPP
@@ -385,6 +447,10 @@ Font.prototype.getHeaderFile = function() {
385
447
var packedChars = 5 ;
386
448
var packedPixels = 6 ;
387
449
450
+ var charCodes = Object . keys ( this . glyphs ) . sort ( ) ;
451
+ var charMin = charCodes [ 0 ] ;
452
+ var charMax = charCodes [ charCodes . length - 1 ] ;
453
+
388
454
function genChar ( font , glyph ) {
389
455
var r = [ ] ;
390
456
for ( var y = 0 ; y < font . fmHeight ; y ++ ) {
@@ -398,8 +464,8 @@ Font.prototype.getHeaderFile = function() {
398
464
}
399
465
400
466
var header = "" ;
401
- var ch = this . firstChar ;
402
- while ( ch <= this . lastChar ) {
467
+ var ch = charMin ;
468
+ while ( ch <= charMax ) {
403
469
var chars = [ ] ;
404
470
for ( var i = 0 ; i < packedChars ; i ++ ) {
405
471
var glyph = this . glyphs [ ch ] ;
@@ -430,7 +496,7 @@ Font.prototype.getPBF = function() {
430
496
this . fullHeight = false ; // TODO: too late?
431
497
// now go through all glyphs
432
498
var glyphs = [ ] ;
433
- var hashtableSize = ( ( this . lastChar - this . firstChar ) > 1000 ) ? 255 : 64 ;
499
+ var hashtableSize = ( ( this . glyphs . length ) > 1000 ) ? 255 : 64 ;
434
500
var hashes = [ ] ;
435
501
for ( var i = 0 ; i < hashtableSize ; i ++ )
436
502
hashes [ i ] = [ ] ;
@@ -618,43 +684,72 @@ JsVar *jswrap_graphics_setFont${options.name}(JsVar *parent) {
618
684
` ) ;
619
685
} ;
620
686
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
+
621
702
622
703
/* load() loads a font. fontInfo should be:
623
704
{
624
705
fn : "font6x8.png", // currently a built-in font
625
706
height : 8, // actual used height of font map
626
- firstChar : 32,
627
- maxChars : 256-32
707
+ range : [ min:32, max:255 ]
628
708
}
629
709
630
710
or:
631
711
632
712
{
633
713
fn : "renaissance_28.pbff",
634
714
height : 28, // actual used height of font map
635
- firstChar : 32,
715
+ range : [ min: 32, max:255 ]
636
716
yOffset : 4,
637
- maxChars : 256-32
638
717
}
639
718
640
719
or for a font made using https://www.pentacom.jp/pentacom/bitfontmaker2/
641
720
642
721
{
643
722
fn : "bitfontmaker2_14px.json",
644
723
height : 14, // actual used height of font map
645
- firstChar : 32,
646
- maxChars : 256-32
724
+ range : [ min:32, max:255 ]
647
725
}
648
726
649
727
650
728
Afterwards returns a Font object populated with the args given, and
651
729
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
+
652
746
*/
653
747
654
748
655
749
// =======================================================
656
750
return {
657
751
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
659
754
} ;
660
755
} ) ) ;
0 commit comments