331 lines
7.5 KiB
JavaScript
331 lines
7.5 KiB
JavaScript
/*
|
|
Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
|
|
|
|
This program is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU General Public License as published by the Free
|
|
Software Foundation; either version 2 of the License, or (at your option)
|
|
any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along
|
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fnutil = require('./fnutil.js');
|
|
|
|
// -- Width --
|
|
const DPARSE_LIMIT = 512;
|
|
const SPARSE_LIMIT = 32000;
|
|
|
|
class Width {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
|
|
static parse(name, value, limit) {
|
|
const words = fnutil.splitWords(name, value, 2);
|
|
|
|
return new Width(fnutil.parseDec(name + '.x', words[0], -limit, limit),
|
|
fnutil.parseDec(name + '.y', words[1], -limit, limit));
|
|
}
|
|
|
|
static parseS(name, value) {
|
|
return Width.parse(name, value, SPARSE_LIMIT);
|
|
}
|
|
|
|
static parseD(name, value) {
|
|
return Width.parse(name, value, DPARSE_LIMIT);
|
|
}
|
|
|
|
toString() {
|
|
return `${this.x} ${this.y}`;
|
|
}
|
|
}
|
|
|
|
// -- BBX --
|
|
class BBX {
|
|
constructor(width, height, xoff, yoff) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.xoff = xoff;
|
|
this.yoff = yoff;
|
|
}
|
|
|
|
static parse(name, value) {
|
|
const words = fnutil.splitWords(name, value, 4);
|
|
|
|
return new BBX(fnutil.parseDec(name + '.width', words[0], 1, DPARSE_LIMIT),
|
|
fnutil.parseDec(name + '.height', words[1], 1, DPARSE_LIMIT),
|
|
fnutil.parseDec(name + '.xoff', words[2], -DPARSE_LIMIT, DPARSE_LIMIT),
|
|
fnutil.parseDec(name + '.yoff', words[3], -DPARSE_LIMIT, DPARSE_LIMIT));
|
|
}
|
|
|
|
rowSize() {
|
|
return (this.width + 7) >> 3;
|
|
}
|
|
|
|
toString() {
|
|
return `${this.width} ${this.height} ${this.xoff} ${this.yoff}`;
|
|
}
|
|
}
|
|
|
|
// -- Props --
|
|
function skipComments(line) {
|
|
return line.startsWith('COMMENT') ? null : line;
|
|
}
|
|
|
|
class Props extends Map {
|
|
forEach(callback) {
|
|
super.forEach((value, name) => callback(name, value));
|
|
}
|
|
|
|
read(input, name, callback) {
|
|
return this.parse(input.readLines(skipComments), name, callback);
|
|
}
|
|
|
|
parse(line, name, callback) {
|
|
if (line == null || !line.startsWith(name)) {
|
|
throw new Error(name + ' expected');
|
|
}
|
|
|
|
const value = line.substring(name.length).trimLeft();
|
|
|
|
this.set(name, value);
|
|
return callback == null ? value : callback(name, value);
|
|
}
|
|
|
|
set(name, value) {
|
|
super.set(name, value.toString());
|
|
}
|
|
}
|
|
|
|
// -- Base --
|
|
class Base {
|
|
constructor() {
|
|
this.props = new Props();
|
|
this.bbx = null;
|
|
}
|
|
}
|
|
|
|
// -- Char --
|
|
class Char extends Base {
|
|
constructor() {
|
|
super();
|
|
this.code = -1;
|
|
this.swidth = null;
|
|
this.dwidth = null;
|
|
this.data = null;
|
|
}
|
|
|
|
bitmap() {
|
|
const bitmap = this.data.toString('hex').toUpperCase();
|
|
const regex = new RegExp(`.{${this.bbx.rowSize() << 1}}`, 'g');
|
|
return bitmap.replace(regex, '$&\n');
|
|
}
|
|
|
|
_read(input) {
|
|
// HEADER
|
|
this.props.read(input, 'STARTCHAR');
|
|
this.code = this.props.read(input, 'ENCODING', fnutil.parseDec);
|
|
this.swidth = this.props.read(input, 'SWIDTH', Width.parseS);
|
|
this.dwidth = this.props.read(input, 'DWIDTH', Width.parseD);
|
|
this.bbx = this.props.read(input, 'BBX', BBX.parse);
|
|
|
|
let line = input.readLines(skipComments);
|
|
|
|
if (line != null && line.startsWith('ATTRIBUTES')) {
|
|
this.props.parse(line, 'ATTRIBUTES');
|
|
line = input.readLines(skipComments);
|
|
}
|
|
|
|
// BITMAP
|
|
if (this.props.parse(line, 'BITMAP') !== '') {
|
|
throw new Error('BITMAP expected');
|
|
}
|
|
|
|
const rowLen = this.bbx.rowSize() * 2;
|
|
let bitmap = '';
|
|
|
|
for (let y = 0; y < this.bbx.height; y++) {
|
|
line = input.readLines(skipComments);
|
|
|
|
if (line == null) {
|
|
throw new Error('bitmap data expected');
|
|
}
|
|
if (line.match(/^[\dA-Fa-f]+$/) == null) {
|
|
throw new Error('invalid bitmap character(s)');
|
|
}
|
|
if (line.length === rowLen) {
|
|
bitmap += line;
|
|
} else {
|
|
throw new Error('invalid bitmap line length');
|
|
}
|
|
}
|
|
|
|
this.data = Buffer.from(bitmap, 'hex');
|
|
|
|
// FINAL
|
|
if (input.readLines(skipComments) !== 'ENDCHAR') {
|
|
throw new Error('ENDCHAR expected');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
static read(input) {
|
|
return (new Char())._read(input);
|
|
}
|
|
|
|
write(output) {
|
|
let header = '';
|
|
|
|
this.props.forEach((name, value) => {
|
|
header += (name + ' ' + value).trim() + '\n';
|
|
});
|
|
output.writeLine(header + this.bitmap() + 'ENDCHAR');
|
|
}
|
|
}
|
|
|
|
// -- Font --
|
|
const XLFD = {
|
|
FOUNDRY: 1,
|
|
FAMILY_NAME: 2,
|
|
WEIGHT_NAME: 3,
|
|
SLANT: 4,
|
|
SETWIDTH_NAME: 5,
|
|
ADD_STYLE_NAME: 6,
|
|
PIXEL_SIZE: 7,
|
|
POINT_SIZE: 8,
|
|
RESOLUTION_X: 9,
|
|
RESOLUTION_Y: 10,
|
|
SPACING: 11,
|
|
AVERAGE_WIDTH: 12,
|
|
CHARSET_REGISTRY: 13,
|
|
CHARSET_ENCODING: 14
|
|
};
|
|
|
|
const CHARS_MAX = 65535;
|
|
|
|
class Font extends Base {
|
|
constructor() {
|
|
super();
|
|
this.chars = [];
|
|
this.defaultCode = -1;
|
|
}
|
|
|
|
get bold() {
|
|
return this.xlfd[XLFD.WEIGHT_NAME].toLowerCase().includes('bold');
|
|
}
|
|
|
|
get italic() {
|
|
return ['I', 'O'].indexOf(this.xlfd[XLFD.SLANT]) !== -1;
|
|
}
|
|
|
|
get proportional() {
|
|
return this.xlfd[XLFD.SPACING] === 'P';
|
|
}
|
|
|
|
_read(input) {
|
|
// HEADER
|
|
let line = input.readLine();
|
|
|
|
if (this.props.parse(line, 'STARTFONT') !== '2.1') {
|
|
throw new Error('STARTFONT 2.1 expected');
|
|
}
|
|
this.xlfd = this.props.read(input, 'FONT', (name, value) => value.split('-', 16));
|
|
|
|
if (this.xlfd.length !== 15 || this.xlfd[0] !== '') {
|
|
throw new Error('non-XLFD font names are not supported');
|
|
}
|
|
this.props.read(input, 'SIZE');
|
|
this.bbx = this.props.read(input, 'FONTBOUNDINGBOX', BBX.parse);
|
|
line = input.readLines(skipComments);
|
|
|
|
if (line != null && line.startsWith('STARTPROPERTIES')) {
|
|
const numProps = this.props.parse(line, 'STARTPROPERTIES', fnutil.parseDec);
|
|
|
|
for (let i = 0; i < numProps; i++) {
|
|
line = input.readLines(skipComments);
|
|
|
|
if (line == null) {
|
|
throw new Error('property expected');
|
|
}
|
|
|
|
const match = line.match(/^(\w+)\s+([-\d"].*)$/);
|
|
|
|
if (match == null) {
|
|
throw new Error('invalid property format');
|
|
}
|
|
|
|
const name = match[1];
|
|
const value = match[2];
|
|
|
|
if (this.props.get(name) != null) {
|
|
throw new Error('duplicate property');
|
|
}
|
|
if (name === 'DEFAULT_CHAR') {
|
|
this.defaultCode = fnutil.parseDec(name, value);
|
|
}
|
|
this.props.set(name, value);
|
|
}
|
|
|
|
if (this.props.read(input, 'ENDPROPERTIES') !== '') {
|
|
throw new Error('ENDPROPERTIES expected');
|
|
}
|
|
line = input.readLines(skipComments);
|
|
}
|
|
|
|
// GLYPHS
|
|
const numChars = fnutil.parseDec('CHARS', this.props.parse(line, 'CHARS'), 1, CHARS_MAX);
|
|
|
|
for (let i = 0; i < numChars; i++) {
|
|
this.chars.push(Char.read(input));
|
|
}
|
|
|
|
if (this.defaultCode !== -1 && this.chars.find(char => char.code === this.defaultCode) === -1) {
|
|
throw new Error('invalid DEFAULT_CHAR');
|
|
}
|
|
|
|
// FINAL
|
|
if (input.readLines(skipComments) !== 'ENDFONT') {
|
|
throw new Error('ENDFONT expected');
|
|
}
|
|
if (input.readLine() != null) {
|
|
throw new Error('garbage after ENDFONT');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
static read(input) {
|
|
return (new Font())._read(input, false);
|
|
}
|
|
|
|
write(output) {
|
|
this.props.forEach((name, value) => output.writeProp(name, value));
|
|
this.chars.forEach(char => char.write(output));
|
|
output.writeLine('ENDFONT');
|
|
}
|
|
}
|
|
|
|
// -- Export --
|
|
module.exports = Object.freeze({
|
|
DPARSE_LIMIT,
|
|
SPARSE_LIMIT,
|
|
Width,
|
|
BBX,
|
|
skipComments,
|
|
Props,
|
|
Base,
|
|
Char,
|
|
XLFD,
|
|
CHARS_MAX,
|
|
Font
|
|
});
|