2 // Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
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.
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.
17 const fnutil = require('./fnutil.js');
20 const WIDTH_MAX = 127;
21 const HEIGHT_MAX = 255;
22 const SWIDTH_MAX = 32000;
30 static _parse(name, value, limitX, limitY) {
31 const words = fnutil.splitWords(name, value, 2);
33 return new Width(fnutil.parseDec(name + ' X', words[0], -limitX, limitX),
34 fnutil.parseDec(name + ' Y', words[1], -limitY, limitY));
37 static parseS(value) {
38 return Width._parse('SWIDTH', value, SWIDTH_MAX, SWIDTH_MAX);
41 static parseD(value) {
42 return Width._parse('DWIDTH', value, WIDTH_MAX, HEIGHT_MAX);
48 constructor(width, height, xoff, yoff) {
55 static parse(name, value) {
56 const words = fnutil.splitWords(name, value, 4);
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));
65 return (this.width + 7) >> 3;
69 return `${this.width} ${this.height} ${this.xoff} ${this.yoff}`;
81 this.names.push(name);
82 this.values.push(value);
86 let props = new Props();
88 props.names = this.names.slice();
89 props.values = this.values.slice();
94 for (let index = 0; index < this.names.length; index++) {
95 callback(this.names[index], this.values[index]);
100 return this.values[this.names.indexOf(name)];
103 parse(line, name, callback) {
104 if (line === null || !line.startsWith(name)) {
105 throw new Error(name + ' expected');
108 let value = line.substring(name.length).trimLeft();
110 this.add(name, value);
111 return callback == null ? value : callback(name, value);
119 let index = this.names.indexOf(name);
122 this.values[index] = value;
124 this.add(name, value);
132 this.props = new Props();
137 readFinish(input, endText) {
138 if (this.readNext(input, this.finis) !== endText) {
139 throw new Error(endText + ' expected');
141 this.finis.push(endText);
144 readNext(input, comout = this.props) {
145 return input.readLines(line => {
146 if (line.startsWith('COMMENT')) {
154 readProp(input, name, callback) {
155 return this.props.parse(this.readNext(input), name, callback);
160 class Char extends Base {
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');
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);
183 let line = this.readNext(input);
185 if (line !== null && line.startsWith('ATTRIBUTES')) {
186 this.props.parse(line, 'ATTRIBUTES');
187 line = this.readNext(input);
191 if (this.props.parse(line, 'BITMAP') !== '') {
192 throw new Error('BITMAP expected');
195 const rowLen = this.bbx.rowSize() * 2;
198 for (let y = 0; y < this.bbx.height; y++) {
199 line = this.readNext(input);
202 throw new Error('bitmap data expected');
204 if (line.length === rowLen) {
207 throw new Error('invalid bitmap line length');
212 this.readFinish(input, 'ENDCHAR');
214 if (bitmap.match(/^[\dA-Fa-f]+$/) != null) {
215 this.data = Buffer.from(bitmap, 'hex');
217 throw new Error('invalid BITMAP data characters');
223 return (new Char())._read(input);
229 this.props.forEach((name, value) => {
230 header += (name + ' ' + value).trim() + '\n';
232 output.writeLine(header + Char.bitmap(this.data, this.bbx.rowSize()) + this.finis.join('\n'));
250 CHARSET_REGISTRY: 13,
254 const CHARS_MAX = 65535;
256 class Font extends Base {
260 this.defaultCode = -1;
264 let ascent = this.props.get('FONT_ASCENT');
266 if (ascent != null) {
267 return fnutil.parseDec('FONT_ASCENT', ascent, -HEIGHT_MAX, HEIGHT_MAX);
269 return this.bbx.height + this.bbx.yoff;
273 return Number(this.xlfd[XLFD.WEIGHT_NAME].toLowerCase().includes('bold'));
277 return Number(this.xlfd[XLFD.SLANT].match(/^[IO]/) != null);
282 let line = input.readLines(Font.skipEmpty);
284 if (this.props.parse(line, 'STARTFONT') !== '2.1') {
285 throw new Error('STARTFONT 2.1 expected');
287 this.xlfd = this.readProp(input, 'FONT', (name, value) => value.split('-', 16));
289 if (this.xlfd.length !== 15 || this.xlfd[0] !== '') {
290 throw new Error('non-XLFD font names are not supported');
292 this.readProp(input, 'SIZE');
293 this.bbx = this.readProp(input, 'FONTBOUNDINGBOX', BBX.parse);
294 line = this.readNext(input);
296 if (line !== null && line.startsWith('STARTPROPERTIES')) {
297 const numProps = this.props.parse(line, 'STARTPROPERTIES', fnutil.parseDec);
299 for (let i = 0; i < numProps; i++) {
300 line = this.readNext(input);
303 throw new Error('property expected');
306 let match = line.match(/^(\w+)\s+([-\d"].*)$/);
309 throw new Error('invalid property format');
313 let value = match[2];
315 if (name === 'DEFAULT_CHAR') {
316 this.defaultCode = fnutil.parseDec(name, value);
319 this.props.add(name, value);
322 if (this.readProp(input, 'ENDPROPERTIES') !== '') {
323 throw new Error('ENDPROPERTIES expected');
325 line = this.readNext(input);
329 const numChars = this.props.parse(line, 'CHARS', (name, value) => fnutil.parseDec(name, value, 1, CHARS_MAX));
331 for (let i = 0; i < numChars; i++) {
332 this.chars.push(Char.read(input));
335 if (this.defaultCode !== -1 && this.chars.find(char => char.code === this.defaultCode) === -1) {
336 throw new Error('invalid DEFAULT_CHAR');
340 this.readFinish(input, 'ENDFONT');
342 if (input.readLines(Font.skipEmpty) != null) {
343 throw new Error('garbage after ENDFONT');
349 return (new Font())._read(input);
352 static skipEmpty(line) {
353 return line.length > 0 ? line : null;
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'));
364 module.exports = Object.freeze({