]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - bin/bdf.js
import terminus-font-4.48
[FreeBSD/FreeBSD.git] / bin / bdf.js
1 //
2 // Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
3 //
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License as
6 // published by the Free Software Foundation; either version 2 of
7 // the License, or (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14
15 'use strict';
16
17 const fnutil = require('./fnutil.js');
18
19
20 const WIDTH_MAX = 127;
21 const HEIGHT_MAX = 255;
22 const SWIDTH_MAX = 32000;
23
24 class Width {
25         constructor(x, y) {
26                 this.x = x;
27                 this.y = y;
28         }
29
30         static _parse(name, value,  limitX, limitY) {
31                 const words = fnutil.splitWords(name, value, 2);
32
33                 return new Width(fnutil.parseDec(name + ' X', words[0], -limitX, limitX),
34                         fnutil.parseDec(name + ' Y', words[1], -limitY, limitY));
35         }
36
37         static parseS(value) {
38                 return Width._parse('SWIDTH', value, SWIDTH_MAX, SWIDTH_MAX);
39         }
40
41         static parseD(value) {
42                 return Width._parse('DWIDTH', value, WIDTH_MAX, HEIGHT_MAX);
43         }
44 }
45
46
47 class BBX {
48         constructor(width, height, xoff, yoff) {
49                 this.width = width;
50                 this.height = height;
51                 this.xoff = xoff;
52                 this.yoff = yoff;
53         }
54
55         static parse(name, value) {
56                 const words = fnutil.splitWords(name, value, 4);
57
58                 return new BBX(fnutil.parseDec('width', words[0], 1, WIDTH_MAX),
59                         fnutil.parseDec('height', words[1], 1, HEIGHT_MAX),
60                         fnutil.parseDec('bbxoff', words[2], -WIDTH_MAX, WIDTH_MAX),
61                         fnutil.parseDec('bbyoff', words[3], -WIDTH_MAX, WIDTH_MAX));
62         }
63
64         rowSize() {
65                 return (this.width + 7) >> 3;
66         }
67
68         toString() {
69                 return `${this.width} ${this.height} ${this.xoff} ${this.yoff}`;
70         }
71 }
72
73
74 class Props {
75         constructor() {
76                 this.names = [];
77                 this.values = [];
78         }
79
80         add(name, value) {
81                 this.names.push(name);
82                 this.values.push(value);
83         }
84
85         clone() {
86                 let props = new Props();
87
88                 props.names = this.names.slice();
89                 props.values = this.values.slice();
90                 return props;
91         }
92
93         forEach(callback) {
94                 for (let index = 0; index < this.names.length; index++) {
95                         callback(this.names[index], this.values[index]);
96                 }
97         }
98
99         get(name) {
100                 return this.values[this.names.indexOf(name)];
101         }
102
103         parse(line, name, callback) {
104                 if (line === null || !line.startsWith(name)) {
105                         throw new Error(name + ' expected');
106                 }
107
108                 let value = line.substring(name.length).trimLeft();
109
110                 this.add(name, value);
111                 return callback == null ? value : callback(name, value);
112         }
113
114         push(line) {
115                 this.add('', line);
116         }
117
118         set(name, value) {
119                 let index = this.names.indexOf(name);
120
121                 if (index !== -1) {
122                         this.values[index] = value;
123                 } else {
124                         this.add(name, value);
125                 }
126         }
127 }
128
129
130 class Base {
131         constructor() {
132                 this.props = new Props();
133                 this.bbx = null;
134                 this.finis = [];
135         }
136
137         readFinish(input, endText) {
138                 if (this.readNext(input, this.finis) !== endText) {
139                         throw new Error(endText + ' expected');
140                 }
141                 this.finis.push(endText);
142         }
143
144         readNext(input, comout = this.props) {
145                 return input.readLines(line => {
146                         if (line.startsWith('COMMENT')) {
147                                 comout.push(line);
148                                 return null;
149                         }
150                         return line;
151                 });
152         }
153
154         readProp(input, name, callback) {
155                 return this.props.parse(this.readNext(input), name, callback);
156         }
157 }
158
159
160 class Char extends Base {
161         constructor() {
162                 super();
163                 this.code = -1;
164                 this.swidth = null;
165                 this.dwidth = null;
166                 this.data = null;
167         }
168
169         static bitmap(data, rowSize) {
170                 const bitmap = data.toString('hex').toUpperCase();
171                 const regex = new RegExp(`.{${rowSize << 1}}`, 'g');
172                 return bitmap.replace(regex, '$&\n');
173         }
174
175         _read(input) {
176                 // HEADER
177                 this.readProp(input, 'STARTCHAR');
178                 this.code = this.readProp(input, 'ENCODING', fnutil.parseDec);
179                 this.swidth = this.readProp(input, 'SWIDTH', (name, value) => Width.parseS(value));
180                 this.dwidth = this.readProp(input, 'DWIDTH', (name, value) => Width.parseD(value));
181                 this.bbx = this.readProp(input, 'BBX', BBX.parse);
182
183                 let line = this.readNext(input);
184
185                 if (line !== null && line.startsWith('ATTRIBUTES')) {
186                         this.props.parse(line, 'ATTRIBUTES');
187                         line = this.readNext(input);
188                 }
189
190                 // BITMAP
191                 if (this.props.parse(line, 'BITMAP') !== '') {
192                         throw new Error('BITMAP expected');
193                 }
194
195                 const rowLen = this.bbx.rowSize() * 2;
196                 let bitmap = '';
197
198                 for (let y = 0; y < this.bbx.height; y++) {
199                         line = this.readNext(input);
200
201                         if (line === null) {
202                                 throw new Error('bitmap data expected');
203                         }
204                         if (line.length === rowLen) {
205                                 bitmap += line;
206                         } else {
207                                 throw new Error('invalid bitmap line length');
208                         }
209                 }
210
211                 // FINAL
212                 this.readFinish(input, 'ENDCHAR');
213
214                 if (bitmap.match(/^[\dA-Fa-f]+$/) != null) {
215                         this.data = Buffer.from(bitmap, 'hex');
216                 } else {
217                         throw new Error('invalid BITMAP data characters');
218                 }
219                 return this;
220         }
221
222         static read(input) {
223                 return (new Char())._read(input);
224         }
225
226         write(output) {
227                 let header = '';
228
229                 this.props.forEach((name, value) => {
230                         header += (name + ' ' + value).trim() + '\n';
231                 });
232                 output.writeLine(header + Char.bitmap(this.data, this.bbx.rowSize()) + this.finis.join('\n'));
233         }
234 }
235
236
237 const XLFD = {
238         FOUNDRY:          1,
239         FAMILY_NAME:      2,
240         WEIGHT_NAME:      3,
241         SLANT:            4,
242         SETWIDTH_NAME:    5,
243         ADD_STYLE_NAME:   6,
244         PIXEL_SIZE:       7,
245         POINT_SIZE:       8,
246         RESOLUTION_X:     9,
247         RESOLUTION_Y:     10,
248         SPACING:          11,
249         AVERAGE_WIDTH:    12,
250         CHARSET_REGISTRY: 13,
251         CHARSET_ENCODING: 14
252 };
253
254 const CHARS_MAX = 65535;
255
256 class Font extends Base {
257         constructor() {
258                 super();
259                 this.chars = [];
260                 this.defaultCode = -1;
261         }
262
263         getAscent() {
264                 let ascent = this.props.get('FONT_ASCENT');
265
266                 if (ascent != null) {
267                         return fnutil.parseDec('FONT_ASCENT', ascent, -HEIGHT_MAX, HEIGHT_MAX);
268                 }
269                 return this.bbx.height + this.bbx.yoff;
270         }
271
272         getBold() {
273                 return Number(this.xlfd[XLFD.WEIGHT_NAME].toLowerCase().includes('bold'));
274         }
275
276         getItalic() {
277                 return Number(this.xlfd[XLFD.SLANT].match(/^[IO]/) != null);
278         }
279
280         _read(input) {
281                 // HEADER
282                 let line = input.readLines(Font.skipEmpty);
283
284                 if (this.props.parse(line, 'STARTFONT') !== '2.1') {
285                         throw new Error('STARTFONT 2.1 expected');
286                 }
287                 this.xlfd = this.readProp(input, 'FONT', (name, value) => value.split('-', 16));
288
289                 if (this.xlfd.length !== 15 || this.xlfd[0] !== '') {
290                         throw new Error('non-XLFD font names are not supported');
291                 }
292                 this.readProp(input, 'SIZE');
293                 this.bbx = this.readProp(input, 'FONTBOUNDINGBOX', BBX.parse);
294                 line = this.readNext(input);
295
296                 if (line !== null && line.startsWith('STARTPROPERTIES')) {
297                         const numProps = this.props.parse(line, 'STARTPROPERTIES', fnutil.parseDec);
298
299                         for (let i = 0; i < numProps; i++) {
300                                 line = this.readNext(input);
301
302                                 if (line === null) {
303                                         throw new Error('property expected');
304                                 }
305
306                                 let match = line.match(/^(\w+)\s+([-\d"].*)$/);
307
308                                 if (match == null) {
309                                         throw new Error('invalid property format');
310                                 }
311
312                                 let name = match[1];
313                                 let value = match[2];
314
315                                 if (name === 'DEFAULT_CHAR') {
316                                         this.defaultCode = fnutil.parseDec(name, value);
317                                 }
318
319                                 this.props.add(name, value);
320                         }
321
322                         if (this.readProp(input, 'ENDPROPERTIES') !== '') {
323                                 throw new Error('ENDPROPERTIES expected');
324                         }
325                         line = this.readNext(input);
326                 }
327
328                 // GLYPHS
329                 const numChars = this.props.parse(line, 'CHARS', (name, value) => fnutil.parseDec(name, value, 1, CHARS_MAX));
330
331                 for (let i = 0; i < numChars; i++) {
332                         this.chars.push(Char.read(input));
333                 }
334
335                 if (this.defaultCode !== -1 && this.chars.find(char => char.code === this.defaultCode) === -1) {
336                         throw new Error('invalid DEFAULT_CHAR');
337                 }
338
339                 // FINAL
340                 this.readFinish(input, 'ENDFONT');
341
342                 if (input.readLines(Font.skipEmpty) != null) {
343                         throw new Error('garbage after ENDFONT');
344                 }
345                 return this;
346         }
347
348         static read(input) {
349                 return (new Font())._read(input);
350         }
351
352         static skipEmpty(line) {
353                 return line.length > 0 ? line : null;
354         }
355
356         write(output) {
357                 this.props.forEach((name, value) => output.writeProp(name, value));
358                 this.chars.forEach(char => char.write(output));
359                 output.writeLine(this.finis.join('\n'));
360         }
361 }
362
363
364 module.exports = Object.freeze({
365         WIDTH_MAX,
366         HEIGHT_MAX,
367         SWIDTH_MAX,
368         Width,
369         BBX,
370         Char,
371         XLFD,
372         CHARS_MAX,
373         Font
374 });