diff options
Diffstat (limited to 'bin/bdfcheck.js')
-rw-r--r-- | bin/bdfcheck.js | 455 |
1 files changed, 455 insertions, 0 deletions
diff --git a/bin/bdfcheck.js b/bin/bdfcheck.js new file mode 100644 index 000000000000..c62d24db5e41 --- /dev/null +++ b/bin/bdfcheck.js @@ -0,0 +1,455 @@ +/* + 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); +} |