Files
jameswitzeman.net/fonts/terminus-font/bin/bdfcheck.js
T
2026-05-29 11:30:10 -07:00

456 lines
12 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');
const fncli = require('./fncli.js');
const fnio = require('./fnio.js');
const bdf = require('./bdf.js');
// -- Params --
class Params extends fncli.Params {
constructor() {
super();
this.asciiChars = true;
this.bbxExceeds = true;
this.duplCodes = -1;
this.extraBits = true;
this.attributes = true;
this.duplNames = -1;
this.duplProps = true;
this.commonSlant = true;
this.commonWeight = true;
this.xlfdFontNm = true;
this.yWidthZero = true;
}
}
// -- Options --
const HELP = ('' +
'usage: bdfcheck [options] [INPUT...]\n' +
'Check BDF font(s) for various problems\n' +
'\n' +
' -A disable non-ascii characters check\n' +
' -B disable BBX exceeding FONTBOUNDINGBOX checks\n' +
' -c/-C enable/disable duplicate character codes check\n' +
' (default = enabled for registry ISO10646)\n' +
' -E disable extra bits check\n' +
' -I disable ATTRIBUTES check\n' +
' -n/-N enable duplicate character names check\n' +
' (default = enabled for registry ISO10646)\n' +
' -P disable duplicate properties check\n' +
' -S disable common slant check\n' +
' -W disable common weight check\n' +
' -X disable XLFD font name check\n' +
' -Y disable zero WIDTH Y check\n' +
' --help display this help and exit\n' +
' --version display the program version and license, and exit\n' +
' --excstk display the exception stack on error\n' +
'\n' +
'File directives: COMMENT bdfcheck --enable|disable-<check-name>\n' +
' (also available as long command line options)\n' +
'\n' +
'Check names: ascii-chars, bbx-exceeds, duplicate-codes, extra-bits,\n' +
' attributes, duplicate-names, duplicate-properties, common-slant,\n' +
' common-weight, xlfd-font, ywidth-zero\n' +
'\n' +
'The input BDF(s) must be v2.1 with unicode encoding.\n');
const VERSION = 'bdfcheck 1.61, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE;
class Options extends fncli.Options {
constructor() {
super([], HELP, VERSION);
}
parse(name, directive, params) {
const value = name.startsWith('--enable') || name[1].match('[a-z]');
switch (name) {
case '-A':
case '--enable-ascii-chars':
case '--disable-ascii-chars':
params.asciiChars = value;
break;
case '-B':
case '--enable-bbx-exceeds':
case '--disable-bbx-exceeds':
params.bbxExceeds = value;
break;
case '-c':
case '-C':
case '--enable-duplicate-codes':
case '--disable-duplicate-codes':
params.duplCodes = value;
break;
case '-E':
case '--enable-extra-bits':
case '--disable-extra-bits':
params.extraBits = value;
break;
case '-I':
case '--enable-attributes':
case '--disable-attributes':
params.attributes = value;
break;
case '-n':
case '-N':
case '--enable-duplicate-names':
case '--disable-duplicate-names':
params.duplNames = value;
break;
case '-P':
case '--enable-duplicate-properties':
case '--disable-duplicate-properties':
params.duplProps = value;
break;
case '-S':
case '--enable-common-slant':
case '--disable-common-slant':
params.commonSlant = value;
break;
case '-W':
case '--enable-common-weight':
case '--disable-common-weight':
params.commonWeight = value;
break;
case '-X':
case '--enable-xlfd-font':
case '--disable-xlfd-font':
params.xlfdFontNm = value;
break;
case '-Y':
case '--enable-ywidth-zero':
case '--disable-ywidth-zero':
params.yWidthZero = value;
break;
default:
return directive !== true && this.fallback(name, params);
}
return directive !== true || name.startsWith('--');
}
}
// -- DupMap --
class DupMap extends Map {
constructor(prefix, descript, severity) {
super();
this.prefix = prefix;
this.descript = descript;
this.severity = severity;
}
check() {
this.forEach((lines, value) => {
if (lines.length > 1) {
let text = `duplicate ${this.descript} ${value} at lines`;
for (let index = 0; index < lines.length; index++) {
text += (index === 0 ? ' ' : index === lines.length - 1 ? ' and ' : ', ');
text += lines[index];
}
fnutil.message(this.prefix, this.severity, text);
}
});
}
push(value, lineNo) {
let lines = this.get(value);
if (lines != null) {
lines.push(lineNo);
} else {
this.set(value, [lineNo]);
}
}
}
// -- InputFileStream --
const MODE = Object.freeze({
META: 0,
PROPS: 1,
BITMAP: 2
});
class InputFileStream extends fnio.InputFileStream {
constructor(fileName, parsed) {
super(fileName);
this.parsed = parsed;
this.mode = MODE.META;
this.proplocs = new DupMap(this.location(), 'property');
this.namelocs = new DupMap(this.location(), 'character name', 'warning');
this.codelocs = new DupMap(this.location(), 'encoding', 'warning');
this.HANDLERS = [
[ 'STARTCHAR', value => this.appendName(value) ],
[ 'ENCODING', value => this.appendCode(value) ],
[ 'SWIDTH', value => this.checkWidth('SWIDTH', value, bdf.Width.parseS) ],
[ 'DWIDTH', value => this.checkWidth('DWIDTH', value, bdf.Width.parseD) ],
[ 'BBX', value => this.setLastBox(value) ],
[ 'BITMAP', () => this.setMode(MODE.BITMAP) ],
[ 'SIZE', InputFileStream.checkSize ],
[ 'ATTRIBUTES', value => this.checkAttr(value) ],
[ 'STARTPROPERTIES', () => this.setMode(MODE.PROPS) ],
[ 'FONTBOUNDINGBOX', value => this.setFontBox(value) ]
];
this.xlfdName = false;
this.lastBox = null;
this.fontBox = null;
this.options = new Options();
}
append(option, valocs, value) {
if (option) {
valocs.push(value, this.lineNo);
}
}
appendCode(value) {
fnutil.parseDec('encoding', value);
this.append(this.parsed.duplCodes, this.codelocs, value);
}
appendName(value) {
this.append(this.parsed.duplNames, this.namelocs, `"${value}"`);
}
checkWidth(name, value, parse) {
if (this.parsed.yWidthZero && parse(name, value).y !== 0) {
fnutil.warning(this.location(), `non-zero ${name} Y`);
}
}
setFontBox(value) {
this.fontBox = bdf.BBX.parse('FONTBOUNDINGBOX', value);
}
setLastBox(value) {
const bbx = bdf.BBX.parse('BBX', value);
if (this.parsed.bbxExceeds) {
let exceeds = [];
if (bbx.xoff < this.fontBox.xoff) {
exceeds.push('xoff < FONTBOUNDINGBOX xoff');
}
if (bbx.yoff < this.fontBox.yoff) {
exceeds.push('yoff < FONTBOUNDINGBOX yoff');
}
if (bbx.width > this.fontBox.width) {
exceeds.push('width > FONTBOUNDINGBOX width');
}
if (bbx.height > this.fontBox.height) {
exceeds.push('height > FONTBOUNDINGBOX height');
}
exceeds.forEach(exceed => {
fnutil.message(this.location(), '', exceed);
});
}
this.lastBox = bbx;
}
setMode(newMode) {
this.mode = newMode;
}
static checkSize(value) {
const words = fnutil.splitWords('SIZE', value, 3);
fnutil.parseDec('point size', words[0], 1, null);
fnutil.parseDec('x resolution', words[1], 1, null);
fnutil.parseDec('y resolution', words[2], 1, null);
}
checkAttr(value) {
if (!value.match(/^[\dA-Fa-f]{4}$/)) {
throw new Error('ATTRIBUTES must be 4 hex-encoded characters');
}
if (this.parsed.attributes) {
fnutil.warning(this.location(), 'ATTRIBUTES may cause problems with freetype');
}
}
checkFont(value) {
const xlfd = value.substring(4).trimLeft().split('-', 16);
if (xlfd.length === 15 && xlfd[0] === '') {
let unicode = (xlfd[bdf.XLFD.CHARSET_REGISTRY].toUpperCase() === 'ISO10646');
if (this.parsed.duplCodes === -1) {
this.parsed.duplCodes = unicode;
}
if (this.parsed.duplNames === -1) {
this.parsed.duplNames = unicode;
}
if (this.parsed.commonWeight) {
let weight = xlfd[bdf.XLFD.WEIGHT_NAME];
let compare = weight.toLowerCase();
let consider = compare.includes('bold') ? 'Bold' : 'Normal';
if (compare === 'medium' || compare === 'regular') {
compare = 'normal';
}
if (compare !== consider.toLowerCase()) {
fnutil.warning(this.location(), `weight "${weight}" may be considered ${consider}`);
}
}
if (this.parsed.commonSlant) {
let slant = xlfd[bdf.XLFD.SLANT];
let consider = slant.match(/^[IO]/) ? 'Italic' : 'Regular';
if (slant.match(/^[IOR]$/) == null) {
fnutil.warning(this.location(), `slant "${slant}" may be considered ${consider}`);
}
}
} else {
if (this.parsed.xlfdFontNm) {
fnutil.warning(this.location(), 'non-XLFD font name');
}
value = 'FONT --------------';
}
return value;
}
checkProp(line) {
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 (value.startsWith('"')) {
if (value.length < 2 || !value.endsWith('"')) {
throw new Error('no closing double quote');
}
if (value.substring(1, value.length - 1).match(/[^"]"[^"]/)) {
throw new Error('unescaped double quote');
}
} else {
fnutil.parseDec('value', value, null, null);
}
this.append(this.parsed.duplProps, this.proplocs, name);
return `P${this.lineNo} 1`;
}
checkBitmap(line) {
if (line.length !== this.lastBox.rowSize() * 2) {
throw new Error('invalid bitmap length');
} else if (line.match(/^[\dA-Fa-f]+$/) == null) {
throw new Error('invalid bitmap data');
} else if (this.parsed.extraBits) {
const data = Buffer.from(line, 'hex');
const checkX = (this.lastBox.width - 1) | 7;
const lastByte = data[data.length - 1];
let bitNo = 7 - (this.lastBox.Width & 7);
for (let x = this.lastBox.Width; x <= checkX; x++) {
if (lastByte & (1 << bitNo)) {
fnutil.warning(this.location(), `extra bit(s) starting with x=${x}`);
break;
}
bitNo--;
}
}
}
checkLine(line) {
if (line.match(/[^\t\f\v\u0020-\u00ff]/)) {
throw new Error('control character(s)');
}
if (this.parsed.asciiChars && line.match(/[\u007f-\u00ff]/)) {
fnutil.warning(this.location(), 'non-ascii character(s)');
}
switch (this.mode) {
case MODE.META:
if (!this.xlfdName && line.startsWith('FONT')) {
line = this.checkFont(line);
this.xlfdName = true;
} else {
this.HANDLERS.findIndex(function(handler) {
if (line.startsWith(handler[0])) {
handler[1](line.substring(handler[0].length).trimLeft());
return true;
}
return false;
});
}
break;
case MODE.PROPS:
if (line.startsWith('ENDPROPERTIES')) {
this.mode = MODE.META;
} else {
line = this.checkProp(line);
}
break;
default: // MODE.BITMAP
if (line.startsWith('ENDCHAR')) {
this.mode = MODE.META;
} else {
this.checkBitmap(line);
}
}
return line;
}
readCheck(line, callback) {
const match = line.match(/^COMMENT\s*bdfcheck\s+(-.*)$/);
if (match && !this.options.parse(match[1], true, this.parsed)) {
throw new Error('invalid bdfcheck directive');
}
line = callback(line);
return line != null ? this.checkLine(line) : null;
}
readLines(callback) {
return super.readLines(line => this.readCheck(line, callback));
}
}
// -- Main --
function mainProgram(nonopt, parsed) {
(nonopt.length >= 1 ? nonopt : [null]).forEach(input => {
let ifs = new InputFileStream(input, parsed);
try {
bdf.Font.read(ifs);
ifs.close();
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
ifs.proplocs.check();
ifs.namelocs.check();
ifs.codelocs.check();
});
}
if (require.main === module) {
fncli.start('bdfcheck.js', new Options(), new Params(), mainProgram);
}