456 lines
12 KiB
JavaScript
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);
|
|
}
|